project-change-controlrun.md00_intake/stub.mdThe Change control section (change, timelineImpact, costImpact — e.g. "API integration scope
expanded … +2 days … +£500") is dummy. There is no change-control model. Scope changes that move
the timeline or cost are recorded nowhere, so the customer has no transparent log of how/why the plan
shifted — a trust-critical surface during delivery.
change (description), timelineImpact (e.g.
"+2 days" / "No change"), costImpact (e.g. "+£500" / "No change"), and an at date.timelineImpact/costImpact are free text (as the mock implies) or structured
(signed numbers) — structured would let finance/health react to them later.touches: packages/services/src/db/models (new change-control model),
packages/services/src/server,
apps/web/components/dashboard/customer/change-control-section.tsx.01_define/output/spec.mdDuring delivery, a project's scope sometimes shifts in ways that move the timeline or cost (e.g.
"API integration scope expanded … +2 days … +£500"). Today the customer dashboard's "Changes to your
project" section reads entirely from dummy data (apps/web/lib/mock/customer.ts → changeControl):
there is no change-control model, so those shifts are recorded nowhere and the customer has no
transparent log of how or why the plan changed. That transparency is trust-critical during delivery —
the customer-facing window onto the delivery step of the service journey — and is part of the
Build the Bridge initiative's Q2 2026 Objective 3 (validate the technical infrastructure that
backs the live delivery surfaces) by replacing the last mock section of the customer dashboard with
real, per-tenant, per-customer data.
lead with isActiveProject = true),
tenant-scoped and lead-scoped, mirroring the existing blocker model's shape and plugins
(tenantPlugin, softDeletePlugin, schemaPlugin). Fields:change — short description of what changed (required text).timelineImpactDays — signed integer; 0 means no timeline change (e.g. 2 → "+2 days").costImpactAmount — signed number; 0 means no cost change (e.g. 500 → "+£500").at — the date the change was recorded (defaults to now).createdBy — the team member who logged it (optional user ref, as on blocker).customerProjectService (FinanceCurrency:
EUR/USD/GBP, defaulting to EUR). No hard-coded £.ChangeControlService (change-control/index.ts + instance.ts) following the
BlockerService pattern, with record(tenantId, leadId, input) and
listByLead(tenantId, leadId) (newest first by at).apps/web/app/(app)/projects/[id]/actions.ts, alongside raiseBlockerAction) plus a small form on
that page, so the delivery team (the project's manager/CSM/SDM/expert) can log a change with a
description, a timeline-day impact, and a cost-amount impact. No standalone management console.CustomerProjectEnvelope returned by customerProjectService (the typed envelope every other
customer-dashboard section already extends), including the resolved currency, and replace
d.changeControl in apps/web/app/(app)/customer/dashboard/page.tsx with the real entries.ChangeControlSection to take structured entries { change, timelineImpactDays, costImpactAmount } plus the project currency, and format the badges ("+2 days" / "No change",
"+£500" / "No change") from those values — keeping the existing card layout and the
secondary-vs-warning badge styling (no change = secondary, any non-zero impact = warning).0 renders as a "No change" (secondary) badge; any non-zero impact
renders as a signed value in a warning badge.softDeletePlugin, but no UI path
is built this round).02_build/output/notes.mdpackages/services/src/db/models/change-control.ts: new tenant/lead-scoped change-control model
(change, timelineImpactDays, costImpactAmount, at, createdBy), mirroring blocker's shape
and plugins. Index { tenantId, lead, at: -1 } for the newest-first dashboard read.packages/services/src/db/services/change-control/{index,instance}.ts: ChangeControlService with
record() and listByLead() (newest first), following BlockerService.packages/services/src/db/{models,services}/index.ts: export the model, service, instance, and
RecordChangeControlInput.packages/services/src/db/services/customer-project/index.ts: added CustomerProjectChangeEntry
and a changeControl field on CustomerProjectEnvelope; getActiveProjectForCustomer now fetches
the project's change-control entries and toEnvelope maps them (date → ISO string).apps/web/app/(app)/projects/[id]/actions.ts: new recordChangeControlAction (delivery roles
only), composing the explicit zodToActionError/resolveActionContext/runActionBody pipeline.apps/web/components/service-leads/lead-change-control/*: minimal team-side capture (server
fetch wrapper + client form/list + loading), wired into the project detail page's delivery-only
column alongside blockers and action items.apps/web/components/dashboard/customer/change-control-section.tsx: now takes structured entries
{ change, timelineImpactDays, costImpactAmount } + project currency; formats the timeline/cost
badges ("+2 days" / "No change", "+£500" / "No change") from the structured values. Empty-list
behaviour (renders nothing) preserved.apps/web/lib/dashboard-utils.ts: exported currencySymbol(currency) (reusing the existing private
symbol map) so the section renders cost in the project currency, no hard-coded symbol.apps/web/app/(app)/customer/dashboard/page.tsx: ChangeControlSection now reads
project.changeControl + project.finance.currency instead of the mock.apps/web/lib/mock/customer.ts: removed the dummy changeControl array.recordChangeControlAction + LeadChangeControl capture (gated to admin/csm/sdm/expert).listByLead sorts
at: -1; page reads project.changeControl; mock array deleted.tenantId + lead; envelope is resolved from
the signed-in customer's own active project.costLabel uses
currencySymbol(project.finance.currency).0 impact → "No change" (secondary) badge; non-zero → signed warning badge — impactBadge.changes.length === 0 (preserved).Number(...) (with a Number.isFinite guard) before calling the action, so the call site
matches the action's inferred number params.03_release/output/changelog.mdPublished as apps/help/app/changelog/2026-06-22-project-change-control/page.mdx.
The "Changes to your project" section of your dashboard now shows the real scope changes on your project instead of placeholder text, so you always know how and why the plan has moved:
03_release/output/investor-update.mdWho it's for: Customers (delivery teams record the changes) What shipped: A live change-control log on the customer dashboard — every scope change with its timeline and cost impact, shown in the project's currency. Why it matters: Builds delivery trust, advancing Build the Bridge's Q2 objective to validate technical infrastructure.
Replaces the last placeholder section of the customer dashboard with real per-project data.
Dig deeper: https://github.com/sustentus/sustentus/pull/505 · https://help.sustentus.com/changelog/2026-06-22-project-change-control
03_release/output/release.mdd05eb948 on 2026-06-22apps/docs/technical)business/feature-role-matrix/projects (new "Record change control on project" row) and business/service-journey/delivery (new "Change control" section)apps/help/app/changelog/2026-06-22-project-change-control) + investor draft, both in this PRweb (platform.sustentus.com) and help-centre (help.sustentus.com)/code-review (correctness pass): no bugs found.recordChangeControlAction + LeadChangeControl capture (delivery roles only).listByLead sorts at: -1;
page reads project.changeControl; mock array removed.tenantId + lead; envelope resolved from the
customer's own active project.currencySymbol(finance.currency).0 impact → "No change" (secondary) badge; non-zero → signed warning badge.