# Plan 4 — Project Lifecycle State Machine + Inspections + Estimate Versions > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`). Follow the execution protocol (`2026-05-29-execution-protocol.md`) — sequential on shared files, parallel only where lanes are disjoint. **Goal:** Turn the static project-progression bar into an **interactive lifecycle state machine** (stage → progress %, with advance/regress + note + actor), add the **two-touchpoint inspection model** (damage verification + external final inspection, with an issues→rework→re-inspection loop and photo uploads), the **claimed-vs-verified scope-variance badge**, and an **estimate-versions** tab (v1 claim → v2 verified → signed; opens the full estimate document). This makes the Plan-3-tagged scenarios #2 (insurance up-scope), #5 (revisions), #8 (final inspection), and #10 (dispute) fully playable. **Architecture:** A new pure module `src/data/lifecycle.js` defines the stage set, the stage→% map, and next/prev helpers (reused by both the store mutator and the UI). `mockStore.jsx` gains `lifecycleStage` + `stageHistory` on every project, `inspections[]` / `estimateVersions[]` / `verifiedScope` on the scenario projects, and new mutators (advance stage, add inspection, add/resolve issue, add estimate version) following the existing `updateProject`/`addJobTeamMember` pattern. `OwnerProjectDetail.jsx` gets an interactive progression control, an Inspections tab, an Estimate Versions tab, and a scope-variance badge — reusing the existing `EstimateDetailModal` and the `URL.createObjectURL` photo pattern. **Tech stack:** React 18, Vite 7, mock React-context store, pnpm. No test harness → verify via `npm run build`, throwaway `node` checks for pure logic, and dev-run of each interaction. Nothing test-related is committed. **Spec/master:** `docs/superpowers/2026-05-29-MASTER-PLAN.md` §9 (lifecycle), §11 (estimate/inspection/scope-variance), Appendix A.5 (inspection), A.6 (estimate version), Appendix G Plan 4. Builds on Plan 3's `demoScenario` tags. --- ## Current state (verified) - `OwnerProjectDetail.jsx` has **10 tabs** (`overview, budget, team, changeOrders, rfis, milestones, invoices, risks, activity, docs`, array at lines 67-78). **No Inspections or Estimate Versions tab.** - The **"Project Progression"** block (lines 555-592, inside `overview`) is **static**: five hardcoded steps `["Lead","Inspection Scheduled","Damage Verified","Scope Approved","Project Completed"]` whose active state is `(i*25) <= project.completionPercentage`. No `onClick`, no advance control. **No `lifecycleStage` field exists** on any project. - Store mutator pattern (`mockStore.jsx`): `const [projects, setProjects] = useState(MOCK_PROJECTS)` (line ~7438); mutators are inline arrows on the context `value` object (~line 8036+): `updateProject(projectId, data)` (shallow-merge, line ~8181), `addJobTeamMember(projectId, member)` (nested-append with `id: \`jtm_${Date.now()}\``, ~8184). New mutators follow this exactly. - Estimate document renderer: **`EstimateDetailModal`** (`src/components/estimates/EstimateDetailModal.jsx`), props `{ isOpen, onClose, estimate }`. `estimate` = `{ id, clientName, address, date, status, total, templateId, templateName, estimatedBy, roofArea, notes }`. Line items are **computed** via `computeLineItems(template, roofArea)` (`src/utils/estimateExport.js`); a `!template` fallback (lines ~389-394) renders a stored-total-only view. Versions can therefore reuse this modal with either a real `templateId` or just a `total`. - Photo upload: copy **Pattern A** — `URL.createObjectURL(file)` into a local state array (`PropertyDetailDrawer.jsx` lines 235-246) — the only pattern that yields a displayable image URL. (The `CreateItemModal` `type:'file'` path captures a `File` but its submit is a stub.) - 29 projects (`PRJ-2026-001…029`); Plan 3 tagged 10 with `demoScenario:1..10`. ## Scope & parallelism note (read first) Plan 4 is **concentrated in two files** — `src/data/mockStore.jsx` (data + mutators) and `src/pages/owner/OwnerProjectDetail.jsx` (all UI). The only parallel-safe carve-out is the new pure `src/data/lifecycle.js`. Therefore **Phase A (data/store, mockStore.jsx) and Phase B (UI, OwnerProjectDetail.jsx) are each internally sequential**; Phase B depends on Phase A (it consumes the new fields + mutators). Don't fabricate parallelism here — run it sequentially and commit between tasks. --- ## Lane / ownership map | Task | Lane (exclusive files) | Phase | |------|------------------------|-------| | T1 `lifecycle.js` (new) | `src/data/lifecycle.js` | A (first; independent) | | T2 lifecycleStage + stageHistory on all 29 projects | `src/data/mockStore.jsx` | A (seq) | | T3 inspections / estimateVersions / verifiedScope on scenario projects | `src/data/mockStore.jsx` | A (seq, after T2) | | T4 store mutators | `src/data/mockStore.jsx` | A (seq, after T3) | | T5 interactive progression control | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) | | T6 Inspections tab | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) | | T7 Estimate Versions tab | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) | | T8 Scope-variance badge | `src/pages/owner/OwnerProjectDetail.jsx` | B (seq) | | T9 Verify | (read-only) | C | --- ## Task 1: Create `src/data/lifecycle.js` (stage model — pure) **Files:** Create `src/data/lifecycle.js`. Independent; do first. - [ ] **Step 1:** Create the file: ```js // src/data/lifecycle.js // The project lifecycle state machine (Master §9). Pure constants + helpers; no React. // Ordered linear stages. 'Rework In Progress' is an off-line regression state (entered on a failed // final inspection), so it is NOT in the linear order array — handle it explicitly. export const LIFECYCLE_STAGES = [ 'Lead', 'Damage Inspection Scheduled', 'Damage Verified', 'Scope Approved', 'Contract Signed', 'Work In Progress', 'Work Complete — Awaiting Inspection', 'Final Inspection Scheduled', 'Final Inspection In Progress', 'Project Completed', 'Final Payment Received — Closed', ]; export const REWORK_STAGE = 'Rework In Progress'; // Stage → canonical progress %. Advancing to a stage sets the project's completionPercentage to this. export const STAGE_PROGRESS = { 'Lead': 5, 'Damage Inspection Scheduled': 10, 'Damage Verified': 20, 'Scope Approved': 30, 'Contract Signed': 40, 'Work In Progress': 55, 'Work Complete — Awaiting Inspection': 80, 'Final Inspection Scheduled': 85, 'Final Inspection In Progress': 90, 'Rework In Progress': 75, 'Project Completed': 95, 'Final Payment Received — Closed': 100, }; export const stageToPct = (stage) => STAGE_PROGRESS[stage] ?? 0; export const nextStage = (stage) => { if (stage === REWORK_STAGE) return 'Final Inspection Scheduled'; // rework re-enters inspection const i = LIFECYCLE_STAGES.indexOf(stage); return i >= 0 && i < LIFECYCLE_STAGES.length - 1 ? LIFECYCLE_STAGES[i + 1] : stage; }; export const prevStage = (stage) => { const i = LIFECYCLE_STAGES.indexOf(stage); return i > 0 ? LIFECYCLE_STAGES[i - 1] : stage; }; // Is this stage at/after a reference stage? (for gating UI like "can schedule final inspection") export const isAtOrAfter = (stage, ref) => LIFECYCLE_STAGES.indexOf(stage) >= LIFECYCLE_STAGES.indexOf(ref); ``` - [ ] **Step 2 (verify):** throwaway `node` check (temp dir, deleted): assert `nextStage('Damage Verified')==='Scope Approved'`, `nextStage(REWORK_STAGE)==='Final Inspection Scheduled'`, `stageToPct('Contract Signed')===40`, `prevStage('Lead')==='Lead'`, `isAtOrAfter('Project Completed','Contract Signed')===true`. Then `node -e "import('./src/data/lifecycle.js').then(m=>console.log(Object.keys(m)))"` proves it loads. - [ ] **Step 3:** commit `git commit -m "feat(lifecycle): add pure project lifecycle stage model (stages, progress map, helpers)"` (after `git add src/data/lifecycle.js`). ## Task 2: Add `lifecycleStage` + `stageHistory` to all 29 projects **Files:** `src/data/mockStore.jsx` only. - [ ] **Step 1:** For EACH of the 29 projects, add a `lifecycleStage` (a value from `LIFECYCLE_STAGES`/`REWORK_STAGE`) derived from its current `status`/`phase`/`completionPercentage`: - `status:'completed'` & all paid → `'Final Payment Received — Closed'`; completed but not all paid → `'Project Completed'`. - active near-done (~80–90%) → `'Work Complete — Awaiting Inspection'` or `'Final Inspection Scheduled'`. - active mid (40–70%) → `'Work In Progress'`. - signed/pre-construction (the converted signed leads, ~0–30%) → `'Contract Signed'` or `'Scope Approved'`. - the disputed/on-hold scenario (#10) → keep its existing stage (e.g. `'Work In Progress'`) — On-Hold is represented by `status:'disputed'` + the dispute data; do NOT add an on-hold lifecycle stage (the §10 sub-task machine has On Hold; the project lifecycle does not). - Set `completionPercentage` to remain consistent with `stageToPct(lifecycleStage)` for discrete stages; for `'Work In Progress'` keep the project's existing milestone-driven % (40–70). - [ ] **Step 2:** Add a `stageHistory: [{ stage, date, by, note }]` to each project — at minimum the current stage as the last entry; for the scenario projects, author 2–4 prior transitions with real dates/actors (owner or rep names) telling the job's story. - [ ] **Step 3 (verify):** `npm run build` → `✓ built`. `git grep -c "lifecycleStage:" -- src/data/mockStore.jsx` → 29. Spot-check 3 projects that `completionPercentage` is consistent with the stage. - [ ] **Step 4:** commit `git commit -am "feat(data): add lifecycleStage + stageHistory to all projects"`. ## Task 3: Add inspections / estimateVersions / verifiedScope to scenario projects **Files:** `src/data/mockStore.jsx` only. After T2. Author rich data on the tagged scenarios (find them via `demoScenario`), per schemas A.5 / A.6 and §11. - [ ] **Step 1 — Damage inspection + scope variance (scenario #2, the hero up-scope).** On the `demoScenario:2` project add: - `reportedScope: { summary: 'Small roof leak near chimney', estTotal: 200, estDays: 1 }` (the customer's claim — also mirror onto its `sourceLeadId` lead as `reportedScope`). - `verifiedScope: { summary: 'Widespread hail bruising across north + west slopes; decking damage', estTotal: 15400, estDays: 5 }`. - `scopeVariance: true` (drives the badge). - `inspections: [{ id:'insp__1', kind:'damage', round:1, inspectorName:, inspectorRole:'Field Agent', inspectionDate:, result:'pass', feedbackNote:'Verified major hail damage far exceeding reported leak; photos attached.', issues:[] }]`. - `estimateVersions: [ { version:1, date:, total:200, note:'Initial — homeowner-reported leak', status:'sent' }, { version:2, date:, total:15400, note:'Revised after damage verification (insurance supplement)', status:'revised' }, { version:3, date:, total:15400, note:'Approved + signed after adjuster supplement', status:'signed', templateId: } ]`. - [ ] **Step 2 — Estimate revisions (scenario #5).** On `demoScenario:5` add `estimateVersions` of 3–4 versions showing a negotiation (v1 sent → v2 revised → v3 revised → v4 signed), each with a `note` and changing `total`; set `verifiedScope` if relevant. - [ ] **Step 3 — Final inspection + rework loop (scenario #8, near-complete).** On `demoScenario:8` add a final inspection round per A.5: `{ id, kind:'final', round:1, inspectorName:, inspectorContact:{ phone:, email: }, inspectionDate:, result:'fail', feedbackNote:'Flashing detail at the valley needs rework.', issues:[{ id, description:'Valley flashing not sealed to spec', note:'Re-seal and re-inspect', photos:[], status:'open' }] }`. Set its `lifecycleStage` to `'Final Inspection In Progress'` (or `REWORK_STAGE` to demo the regression). This gives the demo a live issues→rework loop. - [ ] **Step 4 — Closed, all-passed (scenario #9).** On `demoScenario:9` add a passing final inspection: `{ kind:'final', round:1, inspectorName, inspectorContact, inspectionDate, result:'pass', feedbackNote:'Passed — no punch items.', issues:[] }` and a signed `estimateVersions` entry, so a fully-closed job shows the complete paper trail. - [ ] **Step 5 — Dispute context (scenario #10).** Ensure the disputed project's `verifiedScope`/`estimateVersions` reflect the change-order dispute (a `status:'revised'` version the customer is contesting). (The paused payments + dispute timeline already exist from Plan 3.) - [ ] **Step 6 (verify):** `npm run build` → `✓ built`. `git grep -c "inspections:" -- src/data/mockStore.jsx` ≥ 4; `git grep -c "estimateVersions:" ` ≥ 4; `git grep -c "scopeVariance: true"` ≥ 1. Confirm any `templateId` used exists in the templates store (else omit it — the modal's no-template fallback handles it). - [ ] **Step 7:** commit `git commit -am "feat(data): add inspections, estimate versions, and scope variance to demo scenarios"`. ## Task 4: Store mutators **Files:** `src/data/mockStore.jsx` only. After T3. Add these inline arrows to the context `value` object, next to `updateProject` (follow that exact pattern): - [ ] **Step 1:** Add: ```js // Lifecycle: set stage, log the transition, sync completionPercentage from the stage map. advanceLifecycleStage: (projectId, stage, note, by) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, lifecycleStage: stage, completionPercentage: stage === 'Work In Progress' ? Math.max(p.completionPercentage || 0, 55) // WIP is milestone-driven; never regress within it : STAGE_PROGRESS[stage] ?? p.completionPercentage, stageHistory: [...(p.stageHistory || []), { stage, date: new Date().toISOString().slice(0, 10), by: by || 'Owner', note: note || '' }], } : p)); }, addInspection: (projectId, inspection) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, inspections: [...(p.inspections || []), { ...inspection, id: inspection.id || `insp_${Date.now()}` }] } : p)); }, addInspectionIssue: (projectId, inspectionId, issue) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, inspections: (p.inspections || []).map(ins => ins.id === inspectionId ? { ...ins, issues: [...(ins.issues || []), { ...issue, id: issue.id || `iss_${Date.now()}`, status: 'open' }] } : ins), } : p)); }, resolveInspectionIssue: (projectId, inspectionId, issueId) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, inspections: (p.inspections || []).map(ins => ins.id === inspectionId ? { ...ins, issues: (ins.issues || []).map(iss => iss.id === issueId ? { ...iss, status: 'fixed' } : iss) } : ins), } : p)); }, addEstimateVersion: (projectId, version) => { setProjects(prev => prev.map(p => p.id === projectId ? { ...p, estimateVersions: [...(p.estimateVersions || []), version] } : p)); }, ``` - [ ] **Step 2:** At the top of `mockStore.jsx`, import the progress map used by the mutator: `import { STAGE_PROGRESS } from './lifecycle';` (place with the other imports). - [ ] **Step 3 (verify):** `npm run build` → `✓ built`. `git grep -n "advanceLifecycleStage\|addInspection\|addEstimateVersion" -- src/data/mockStore.jsx` shows the new keys on the context value. - [ ] **Step 4:** commit `git commit -am "feat(store): add lifecycle/inspection/estimate-version mutators"`. ## Task 5: Interactive progression control **Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. Phase B. - [ ] **Step 1:** Import the lifecycle model + mutator: `import { LIFECYCLE_STAGES, REWORK_STAGE, nextStage, prevStage, stageToPct } from '../../data/lifecycle';` and pull `advanceLifecycleStage` from `useMockStore()`. - [ ] **Step 2:** Replace the static progression block (lines ~555-592) so it renders from `project.lifecycleStage` (fall back to deriving from `completionPercentage` if a project somehow lacks the field). Render the stage label + the progress bar driven by `project.completionPercentage` (which now tracks the stage). - [ ] **Step 3:** Add an owner/admin-only **Advance Stage** control: a button "Advance to: {nextStage(project.lifecycleStage)}" that opens the existing `CreateItemModal` (or a small inline prompt) capturing an optional note, then calls `advanceLifecycleStage(project.id, nextStage(project.lifecycleStage), note, user.name)`. Add a secondary "Regress / Send to Rework" affordance for the failed-inspection case (advance to `REWORK_STAGE` when at a Final Inspection stage). Gate the controls with `['OWNER','ADMIN'].includes(user?.role)`. - [ ] **Step 4:** Show the `stageHistory` as a compact timeline beneath the bar (stage · date · by · note). - [ ] **Step 5 (verify):** `npm run build` → `✓ built`. Dev-run: advancing a stage updates the bar + % live and appends to the history. - [ ] **Step 6:** commit `git commit -am "feat(projects): interactive lifecycle progression control with stage history"`. ## Task 6: Inspections tab **Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T5. - [ ] **Step 1:** Add `{ id: 'inspections', label: 'Inspections', icon: }` to the `tabs` array. - [ ] **Step 2:** Add the `activeTab === 'inspections'` render block (follow the existing `NeoCard`/table card patterns). Render `project.inspections` grouped by `kind` (Damage Verification vs Final Inspection). For each inspection show: kind, round, inspector name (+ role for damage, + contact phone/email for final), date, a pass/fail badge, and the `feedbackNote`. Below each, list its `issues[]` with an open/fixed badge. - [ ] **Step 3:** Interactions (owner/admin): **Add Issue** to an inspection (calls `addInspectionIssue`), **Mark Fixed** on an open issue (calls `resolveInspectionIssue`), and **Attach Photo** to an issue using Pattern A — a hidden `` whose `onChange` does `URL.createObjectURL(file)` and stores `{ id, url, caption }` into the issue's `photos[]` (local component state mirror is fine for session display; persist via a small `updateProject` if you want it sticky). Render thumbnails from `issue.photos[].url`. - [ ] **Step 4:** Add a **"Schedule Re-inspection"** action that appends a new final-inspection round (`round: prevRound+1`, `result` unset/pending) via `addInspection` — demonstrating the rework→re-inspection loop; older rounds stay visible. - [ ] **Step 5 (verify):** `npm run build` → `✓ built`. Dev-run on scenario #8: the failed final inspection shows its open issue; marking it fixed + scheduling re-inspection works; photo thumbnail appears. - [ ] **Step 6:** commit `git commit -am "feat(projects): Inspections tab (damage + final, issues, rework loop, photo upload)"`. ## Task 7: Estimate Versions tab **Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T6. - [ ] **Step 1:** Add `{ id: 'estimates', label: 'Estimates', icon: FileText }` to the `tabs` array. - [ ] **Step 2:** Import the estimate modal: `import EstimateDetailModal from '../../components/estimates/EstimateDetailModal';` and add a `const [selectedVersion, setSelectedVersion] = useState(null);`. - [ ] **Step 3:** Add the `activeTab === 'estimates'` block: a list of `project.estimateVersions` (version, date, `formatCurrency(total)`, note, and a status badge — green "Signed" for `status:'signed'`). Each row is clickable. - [ ] **Step 4:** On click, open `EstimateDetailModal` with an estimate object built from the version + project: `{ id: \`${project.id}-v${v.version}\`, clientName: project.customerName, address: project.address, date: v.date, status: v.status, total: v.total, templateId: v.templateId, templateName: v.templateName, estimatedBy: , roofArea: v.roofArea, notes: v.note }`. If the version has no `templateId`, the modal's no-template fallback renders the stored total — that's expected. - [ ] **Step 5 (verify):** `npm run build` → `✓ built`. Dev-run on scenario #2 and #5: the version history renders with a Signed badge on the final version; clicking opens the full estimate document. - [ ] **Step 6:** commit `git commit -am "feat(projects): Estimate Versions tab (history + full document view)"`. ## Task 8: Scope-variance badge **Files:** `src/pages/owner/OwnerProjectDetail.jsx` only. After T7. - [ ] **Step 1:** In the `overview` tab header area (near the project title/metrics), when `project.scopeVariance` is truthy and both `reportedScope` and `verifiedScope` exist, render a visible badge, e.g.: `Reported ~${formatCurrency(project.reportedScope.estTotal)} / ${project.reportedScope.estDays}d → Verified ${formatCurrency(project.verifiedScope.estTotal)} / ${project.verifiedScope.estDays}d` styled as an attention badge (amber), with a short tooltip/subtext summarizing the discrepancy story. - [ ] **Step 2 (verify):** `npm run build` → `✓ built`. Dev-run scenario #2: the badge shows the "~$200 → $15,400" jump. - [ ] **Step 3:** commit `git commit -am "feat(projects): claimed-vs-verified scope-variance badge"`. ## Task 9: Verify (read-only) - [ ] `npm run build` clean. - [ ] `git grep -c "lifecycleStage:" -- src/data/mockStore.jsx` → 29; every project's `completionPercentage` is consistent with `stageToPct(lifecycleStage)` for discrete stages (WIP exempt). - [ ] Scenario coverage: #2 has reportedScope+verifiedScope+scopeVariance+3 estimate versions+a damage inspection; #5 has multi-version estimate history; #8 has a failed final inspection with an open issue; #9 has a passing final inspection; #10's dispute reflected in a contested estimate version. - [ ] Dev-run the four interaction loops: **advance stage** (bar + % + history update); **log + resolve an inspection issue**; **schedule a re-inspection round**; **open an estimate version document**; **scope-variance badge** visible on #2. - [ ] No financial invariants broken (this plan adds lifecycle/inspection/estimate data; it must not change budgetBreakdown/committedCost/actualCost — confirm Plan 1/2 invariants still hold on any project touched). --- ## Self-review checklist - [ ] `lifecycle.js` pure, exports stages/progress/helpers; fixture-verified. - [ ] All 29 projects have `lifecycleStage` + `stageHistory`; % consistent with stage. - [ ] Scenario projects carry inspections / estimateVersions / scope-variance per §11. - [ ] 5 mutators added following the established `setProjects` pattern; `STAGE_PROGRESS` imported. - [ ] OwnerProjectDetail: progression control interactive (advance/regress + note + history); Inspections tab (issues + rework + photos); Estimates tab (versions + modal); scope-variance badge. - [ ] Owner/admin gate on mutating controls; build clean; no financial invariant regressions. ## Notes for Plan 5 Plan 5 (subcontractor task state machine, Master §10) is independent of these project-level changes and operates on `MOCK_SUBCONTRACTOR_TASKS` + `SubcontractorTasksPage.jsx` + `TaskViewModal.jsx`. The inspection/rework UI patterns built here (issues list, photo upload, status badges, re-review rounds) are the model to reuse for the sub-task Pre-Work/Post-Work review loop.