Skip to Content

← All archived runs

Run: project-health-engine

run.md

Run: project-health-engine

  • branch: claude/affectionate-cerf-zluz12
  • pr: #506

00_intake/stub.md

Stub: Customer dashboard — computed project health engine

  • feature-slug: project-health-engine
  • epic: customer-dashboard-data
  • personas: Customer
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: customer-project-foundation, milestone-delivery-tracking, project-blockers
  • sequence: 4 of 9

Problem

The status-summary boxes (overallStatus: on-track | minor-issues | at-risk), the go-live strip (status, daysToGoLive, goLiveDate, confidence, delta, atRiskReasons) and the riskStatus badge are all dummy. There is no health concept in the data model. Per the scope decision these values are computed by the platform, not entered by a human — so they need a deterministic heuristic over the now-real milestone, blocker, due-date and invoice data.

Proposed change

  • Build a health computation service that derives, for the customer's active project:
    • overallStatus — on-track / minor-issues / at-risk
    • goLivestatus, goLiveDate (from lead.endDate), daysToGoLive, confidence (High | Medium | Low), delta (change vs. plan), atRiskReasons[]
    • riskStatus — Low | Medium | High
    • inputs: milestone delivery status + blocksGoLive + dueDate slippage, open blockers, and overdue/unpaid invoices.
  • Wire the status-summary boxes, the go-live strip and the risk badge to this computed output.

Acceptance criteria (rough)

  • Health/go-live/risk are computed from real milestones + blockers + dates + invoices — no manual field and no mock.
  • A blocking, overdue milestone or an open go-live-impacting blocker pushes the project to at-risk with a populated atRiskReasons.
  • daysToGoLive and goLiveDate reflect the real project end date.
  • The heuristic is documented (thresholds for each RAG band) so it's reproducible.

Out of scope (this feature)

  • Manual CSM/SDM override of health (the decision was computed-only; an override is a later epic).
  • The underlying milestone/blocker data (owned by their features) — this only reads them.

Notes for Define

  • Pin the heuristic thresholds explicitly with the user during Define — this is the feature's real substance. Keep it a pure function of the inputs so it's testable by inspection.
  • Consider whether customer-action-items overdue items should also feed risk (sequenced after this; can be a follow-up input).
  • touches: packages/services/src/server (health service), apps/web/app/(app)/customer/dashboard/page.tsx (status boxes), apps/web/components/dashboard/customer/project-status-strip.tsx, apps/web/components/dashboard/customer/risk-blockers-feedback.tsx.

01_define/output/spec.md

Spec: Customer dashboard — computed project health engine

  • slug: project-health-engine
  • personas: Customer
  • touches: packages/services/src/db/services/customer-project, apps/web/app/(app)/customer/dashboard/page.tsx, apps/web/components/dashboard/customer/project-status-strip.tsx, apps/web/components/dashboard/customer/risk-blockers-feedback.tsx
  • complexity: standard

Problem

The customer dashboard's status-summary boxes (overallStatus: on-track | minor-issues | at-risk), the go-live strip (status, daysToGoLive, goLiveDate, confidence, delta, atRiskReasons) and the riskStatus badge are still read from apps/web/lib/mock/customer.ts — there is no health concept anywhere in the data. The dependency features that this one builds on are now real: the customerProjectService envelope serves the active project, milestones carry status / blocksGoLive / dueDate, the blocker model exists (its index comment literally notes "the hot read project-health-engine consumes"), and invoices carry paid / dueDate. What is missing is the computed layer that turns those facts into the three health badges.

This is the customer's only honest signal of whether their project will land on time, so it must be computed by the platform (not hand-entered) and derive deterministically from the real milestone, blocker, due-date and invoice data. It advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by making the delivery step of the service journey trustworthy end to end, and it is feature 4 of 9 in the customer-dashboard-data epic.

Proposed change

Add a deterministic, pure health-computation function and wire its output into the dashboard.

The heuristic (single severity score, worst-signal-wins). Compute one severity for the active project — 0 (clear), 1 (minor), 2 (critical) — taking the highest severity any signal raises:

Signal (evaluated over the active project) Raises severity to
A go-live-blocking milestone is overdue (blocksGoLive && dueDate < today && status ≠ done) 2 (critical)
An unpaid invoice is overdue (!paid && dueDate < today) 2 (critical)
A non-blocking milestone is overdue (!blocksGoLive && dueDate < today && status ≠ done) 1 (minor)
At least one open blocker exists (status = open) 1 (minor)
none of the above 0 (clear)

The three badges are then a direct mapping of that one score (so they can never disagree):

severity overallStatus riskStatus go-live confidence go-live status
0 on-track Low High on-track
1 minor-issues Medium Medium on-track
2 at-risk High Low at-risk

Other go-live-strip fields are derived from real project data:

  • goLiveDate — formatted from lead.endDate (the project's planned end).
  • daysToGoLive — whole days from today to lead.endDate (0 if past / unset).
  • atRiskReasons[] — one human-readable string per contributing signal (e.g. "Milestone 'Core API build' is overdue and blocks go live", "Open blocker: waiting on API key", "Invoice overdue"). Empty at severity 0.
  • delta — a derived label, not date arithmetic (there is no stored baseline end date to diff against): "On track, no change" at severity 0, "Minor issues — being monitored" at 1, and at 2 the top atRiskReasons entry.

Wiring. Surface a health object on the CustomerProjectEnvelope (the service already fetches milestones, the quote's invoices and the lead; it additionally reads open blockers for the project via the existing { tenantId, lead, status } index). The dashboard page then reads project.health.overallStatus for the status-summary boxes, project.health.goLive for the go-live strip, and project.health.riskStatus for the risk badge — replacing the corresponding d.* mock reads. When there is no active project, health follows the existing empty-state path.

Documentation. The thresholds above are committed as a doc-comment on the pure function so the RAG bands are reproducible by inspection.

Acceptance criteria

  • overallStatus, the full go-live strip, and riskStatus are computed from real milestones + open blockers + due dates + invoices for the customer's active project — no manual field and no read from lib/mock/customer.ts for these values.
  • An overdue go-live-blocking milestone or an overdue unpaid invoice puts the project at at-risk / High / confidence Low, with a populated atRiskReasons.
  • An open blocker, or a non-blocking overdue milestone, with no critical signal puts the project at minor-issues / Medium / confidence Medium, with the cause named in atRiskReasons.
  • With no overdue milestones, no open blockers and no overdue invoices, the project is on-track / Low / confidence High and atRiskReasons is empty.
  • goLiveDate and daysToGoLive reflect the real lead.endDate.
  • The health computation is a pure function whose RAG thresholds are documented in a doc-comment, so the result is reproducible from its inputs by inspection.

Out of scope

  • Manual CSM/SDM/admin override of the computed health (the decision was computed-only; an override is a later epic).
  • The underlying milestone, blocker and invoice data and their write/capture flows — owned by their own features; this feature only reads them.
  • Wiring the blockers list UI in the Risk/Blockers section (owned by project-blockers); this feature wires only the riskStatus badge there.
  • Wiring the latestMessage and changeControl sections (owned by project-messaging and project-change-control); they keep reading mock this round.
  • Feeding customer-action-items overdue items into the score — sequenced after this; a possible follow-up input, not built now.
  • Numeric go-live delta against a stored baseline end date — there is no baseline to diff; delta is a derived label.

Open questions

  • none.

02_build/output/notes.md

Build notes: project-health-engine

  • commits: feat: project-health-engine — computed health engine + dashboard wiring

What changed

  • packages/services/src/db/services/customer-project/health.ts (new): the pure computeProjectHealth function. Worst-signal-wins severity (0/1/2) over overdue milestones (blocking → 2, non-blocking → 1), overdue unpaid invoices (→ 2) and open blockers (→ 1), mapped to overallStatus / riskStatus / go-live confidence + status, with daysToGoLive / goLiveDate from the lead end date and a populated atRiskReasons. The RAG thresholds are documented in the function's doc-comment.
  • packages/services/src/db/services/milestone/index.ts: added dueDate to MilestoneDeliveryRow (selected in findDeliveryByProposal) so the health engine reads due dates off the milestones the envelope already fetches — no extra query.
  • packages/services/src/db/services/customer-project/index.ts: fetch the project's open blockers (blockerService.listOpenByLead), compute health in toEnvelope, and expose it on CustomerProjectEnvelope.
  • packages/services/src/db/services/index.ts: re-export the health types.
  • apps/web/app/(app)/customer/dashboard/page.tsx: wire the status-summary boxes (health.overallStatus), the go-live strip (health.goLive, ISO date formatted for the strip) and the risk badge (health.riskStatus) to the computed output, replacing the mock reads. The blockers list, latest message and change control still read mock (owned by other features).

Acceptance criteria status

  • overallStatus, the full go-live strip, and riskStatus are computed from real milestones + open blockers + due dates + invoices — the page no longer reads these from lib/mock/customer.ts.
  • An overdue go-live-blocking milestone or an overdue unpaid invoice → at-risk / High / confidence Low with a populated atRiskReasons (severity 2).
  • An open blocker, or a non-blocking overdue milestone, with no critical signal → minor-issues / Medium / confidence Medium, cause named in atRiskReasons (severity 1).
  • No overdue milestones, no open blockers, no overdue invoices → on-track / Low / confidence High, empty atRiskReasons (severity 0).
  • goLiveDate and daysToGoLive derive from lead.endDate.
  • The health computation is a pure function (computeProjectHealth, now injected) whose RAG thresholds are documented in a doc-comment.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here.

Notes for review

  • The blocker model has no blocksGoLive flag, so (per the Define decision) any open blocker is a minor (severity 1) signal — only overdue blocking milestones and overdue invoices reach at-risk. At severity 1 the go-live strip stays "on track" (the summary boxes carry the minor-issues state); this matches the spec's severity→badge table.
  • delta is a derived label (no stored baseline end date to diff against), per spec.

03_release/output/changelog.md

Know at a glance whether your project will go live on time

Customer · 2026-06-23 · PR #506 · live entry: apps/help/app/changelog/2026-06-23-project-health-engine

Your dashboard's status, go-live and risk signals are now worked out automatically from what's really happening on your project — your milestones, blockers, due dates and invoices — instead of placeholder text:

  • See at a glance whether your project is on track, has minor issues, or is at risk of delay.
  • The go-live panel shows your expected go-live date, the days remaining, and how confident we are it will land on time.
  • When something could push your date out — an overdue milestone that blocks go-live, an overdue invoice, or an open blocker — it's named for you in plain language so you know exactly what's holding things up.
  • Everything is computed for you and kept up to date, so the picture you see always reflects the real state of your project.

03_release/output/investor-update.md

Customers can now see at a glance whether their project will go live on time

Who it's for: Customers What shipped: The customer dashboard now computes each project's status, go-live confidence and risk from its real milestones, blockers and invoices. Why it matters: A trustworthy delivery view advances Build the Bridge — Objective 3, Validate Technical Infrastructure & Payout Flow.

Health is derived from live project data by a single reproducible rule — no manual entry, no mock.

Dig deeper: <merged-PR URL> · <changelog entry URL>