vendor-dashboard-reconciliationrun.md02_define/output/spec.mdEight stubs of this scope have landed. Each one put a defined metric — M1 through M10 — onto the claim surface, carrying its population, period and as-at, opening to records that add up to it. But the vendor dashboard those metrics sit on still reports several of the same concepts the old way, from a different engine, in a different currency, at a different scale. Active Service Revenue is on the screen twice: once as M8's stock on the headline strip, and once as an annualised run-rate tile also labelled "ASR". That is exactly the failure BR-4 names — one concept, two values, one screen — and it is the failure this whole scope exists to prevent.
This is the ninth and last stub of vendor-metrics, advancing Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners. The dashboard is the evidence a vendor takes into a renewal conversation. A vendor who catches one number disagreeing with another stops believing all of them, including the eight this scope just made defensible. Landing the correct numbers beside the differently-derived ones and stopping there would forfeit the scope's headline acceptance criterion.
Bring every figure on the vendor dashboard onto the dictionary, and delete what cannot come.
fmt(), which defaults to GBP and
abbreviates to thousands.Full claim chrome — population / period / as-at plus a drill-down whose records add up — is required
on /vendor/dashboard. The eight vendor sub-pages get the correctness half in this run: one euro
whole-unit formatter, no undefined concept, and no concept carrying a second value. Putting full
claim chrome on all nine routes is a larger job than this run; the sub-pages' remaining drill-downs
are named under Out of scope.
/vendor/delivery is deleted too (ruled by Jamie at Build)Removing the time-to-value section orphans /vendor/delivery: its only inbound link is
DeliveryVelocityCard, which nothing else renders — the same way removing the leakage card
orphaned /vendor/risk in the M10 run. Every figure on it also names a concept the dictionary does
not define: time to value, and a pctWithinSLA adherence percentage that is not M6 (M3 and M6
render only on /vendor/metrics). Both the "no unreachable route" and the "no undefined concept"
criteria therefore reach it, so it goes with /vendor/revenue.
Consequence, recorded rather than discovered later: SDM loses the delivery-oversight page.
M3 and M6 remain the defined delivery figures, on /vendor/metrics. Giving SDM a delivery surface
built on defined metrics is a separate scope.
The stub's criterion stands: both are deleted from the vendor dashboard, along with the two dictionary definitions seeded for them. Reinstating them is a separate scope.
This reverses the 2026-08-17 interrogation ruling ("define and keep"), which was recorded in
.icm/runs/vendor-metrics/02_design/output/stub-reconciliation.md and on the
web-correctness-fixes/vendor-headline-tiles stub, and was already executed by migration
1787270400000-seed-nrr-grr-definitions.ts. That ruling never reached this stub's own criterion, so
the two disagreed; Jamie ruled at Define that the stub wins. Recorded here so the deletion of shipped
work is a decision on the record and not a later surprise.
The engine behind them does not go. getRevenueRetention's perCustomerRevenue is the revenue
base for getRetentionCohorts (M7) and for the customer-health read — both defined, both shipped.
Only the headline figures and the fields no surviving screen reads are removed. GRR's formula
survives inside M7's per-cohort rows, where M7's definition governs it; GRR as an undefined headline
percentage does not.
The stub asks for this written down before any code changes. Read on main @ 3788833.
| Figure (where) | Claims to be | Governing definition | Verdict |
|---|---|---|---|
NRR % + change — headline-metrics-section.tsx, /vendor/retention |
Revenue retention | None (seeded def. removed with it) | Delete — ruled above |
GRR % + change — same two surfaces |
Gross revenue retention | None (seeded def. removed with it) | Delete — ruled above |
"Revenue in delivery (ASR)" £Xk — headline-metrics-section.tsx, /vendor/revenue |
Active Service Revenue | Collides with M8 — this is an annualised run-rate, M8 is a stock | Delete — BR-4. M8 on the headline strip is the one ASR |
| NRR movement breakdown — expansion / contraction / churn + top-account drivers | NRR's components | None once NRR goes | Delete with NRR |
Alert banner keyed on rr.nrr >= 100 |
Retention verdict | None once NRR goes | Delete with NRR |
ARR movement strip — revenue per lifecycle stage — revenue-flow-section.tsx, /vendor/revenue |
Revenue by delivery stage | None. Mixes stocks and a flow, unlabelled; last cell shows the ARR figure, not delivered value | Delete — BR-10, BR-11 |
| Time-to-value section — avg days, revenue-weighted avg, distribution | Time to value | None. Carried metricIds={["M6","M3"]} — two definitions that are not it |
Delete — Jamie's 2026-08-17 ruling on this stub |
"GRR at risk" line — customer-health-section.tsx |
Gross revenue retention | None once GRR goes | Delete the line; the section stays (M5/M4/M9) |
Retention by speed / region — retention-by-speed-section.tsx, /vendor/retention |
M7 Customer Retention Rate | M7 | Keep — already defined and on the claim surface |
M8 Active Service Revenue — headline-strip.tsx |
M8 | M8 | Keep |
M2 activation + funnel — ClaimFigure, funnel-section.tsx |
M1 / M2 | M1, M2 | Keep |
M10 Revenue at Risk — revenue-risk-section.tsx |
M10 | M10 | Keep |
Customer health / CSAT — customer-health-section.tsx, top-customers-table.tsx |
M5 / M4 / M9 | M5, M4, M9 | Keep |
| Headline strip "leads sourced" — tenant-wide count over a different window from M2's cohort | Leads sourced | M1 cohort, on a different window | Repoint to M1's window, or drop the count — two leads figures on one screen is BR-4 |
~36 money renders via fmt() — 8 sub-pages + 9 components |
Various | — | Repoint to one euro whole-unit formatter |
/vendor/revenue — three cards: ASR run-rate, ASR change, revenue-by-stage |
— | None survive | Delete the route — nothing left; already has no inbound link |
Four surviving sections do not read the engines their metrics are defined on. CustomerHealthSection
renders from vendorService.getCustomerHealth, while M5's definition is computed by
vendorHealthService.report — two engines for one concept, both live on this page today, since
the dashboard already loads the second one for M10's risk flags. FunnelSection and
TopCustomersTable (vendorService.getLeadFunnel / getTopCustomers) and RetentionBySpeedSection
(getRetentionCohorts) sit the same way against M1/M2, M9 and M7.
Putting each metric's ClaimFigure beside the section it duplicates would create the BR-4 defect
rather than remove it: the same concept, twice, from two engines. Repointing the sections is not the
"repoint" the stub anticipated either — the claim reports and the section view-models do not share a
shape, so it is a rebuild of three sections against different engines, with its own review and its
own risk of shipping a wrong number. It is carved out to its own run rather than half-done here.
Nothing in this run makes it worse: no figure was added, and the figures removed were the undefined ones.
/vendor/dashboard is rendered through the claim surface, carrying its
population, period and as-at (BR-1), and opens to the records behind it, which add up to it
(BR-2). Met for M2, M8 and M10. Not met for the customer-health, satisfaction, retention
and funnel sections — see "The second-engine problem" below./vendor/dashboard carries the definitions behind its numbers, in place,
linking into the dictionary at that metric — the popover and the dictionary page read the same
definition record./vendor/retention cards, the "GRR at
risk" line, and the two dictionary definitions seeded by
1787270400000-seed-nrr-grr-definitions.ts — removed by a new migration, not by editing the
shipped one. /vendor/metrics and /admin/metrics no longer list them.getRevenueRetention still returns perCustomerRevenue, and M7's getRetentionCohorts and
the customer-health read produce the same figures they did before this change. No field
survives on VendorRevenueRetention that no surviving screen or read consumes.fmt() or a hand-rolled £ template any more./vendor/revenue, left with no
defined figure, is deleted along with its route-policies.ts entry.apps/demo in step. The demo app is frozen (legacy, no new development), and the
prototype at /dashboards/vendor is deliberately left as the side-by-side comparison.03_build/output/notes.mdfeat: commit for the reconciliation, one for the run filesci-status.sh before hand-offDeletions dominate, as the stub predicted. Superseding deletes the superseded, in the same change.
headline-metrics-section.tsx — deleted. It was NRR, GRR, an annualised "ASR" run-rate, an
NRR-derived alert banner and the NRR movement breakdown. Nothing on it survived the ruling, so the
component went rather than being hollowed out.revenue-flow-section.tsx (ARR movement strip) — deleted. Stocks and a flow in one strip,
unlabelled, with the terminal cell showing the ARR figure rather than delivered value (BR-10, BR-11).time-to-value-section.tsx + delivery-velocity-card.tsx — deleted. Time to value has no
definition; Jamie's 2026-08-17 ruling was to remove it rather than define it here./vendor/revenue and /vendor/delivery — deleted, with their route-policies.ts entries.
/vendor/revenue held only the ASR run-rate and the ARR strip. /vendor/delivery was orphaned by
the time-to-value deletion (its only inbound link was DeliveryVelocityCard) and every figure on
it was undefined. Jamie ruled at Build; the SDM consequence is recorded in spec.md.headline-strip.tsx — reduced to the M8 tile. The "service leads" tile restated M2's cohort
over a different window, and "delivery rate" named an undefined concept and reached the screen with
no record set to open (BR-1, BR-2, BR-4). The header's N leads · N paid invoices · N active projects subtitle went for the same reason — it was a fourth leads figure.1787962000000-remove-nrr-grr-definitions.ts, a new migration;
the shipped seed is left exactly as it ran. down restores both records, so the pair is symmetric.apps/web/lib/vendor-money-format.ts → vendorMoney, a thin
delegation to the claim surface's own formatClaimMoney (euros, whole units, never abbreviated).
It replaced fmt() at 36 call sites across 12 vendor files, plus the hand-rolled £ template in
the dashboard page. fmt survives only for the customer and CSM surfaces, which this scope excludes.getRevenueRetention now returns perCustomerRevenue only — the shared revenue
base M7's getRetentionCohorts and the customer-health read were always the real consumers, and
both are untouched. getRevenueFlow, getHeadline (and vendor/headline.ts + its test) were
orphaned by the deletions and went, with their types and their barrel lines./vendor/dashboard through the claim surface (BR-1, BR-2) — partially met.
Met for M2, M8 and M10. Not met for the customer-health, satisfaction, retention-by-speed
and funnel sections: they render from vendorService.getCustomerHealth / getLeadFunnel /
getTopCustomers / getRetentionCohorts, not from the engines their metrics are defined on.
See Notes for Verify — this is the one criterion this run does not close, and it is carved
out in spec.md rather than dropped.["NRR","GRR","M6"] went with the section it annotated./vendor/retention cards,
the "GRR at risk" chain link, and both dictionary definitions.getRevenueRetention still returns perCustomerRevenue; M7's cohorts and the customer-health
read are unchanged. No field survives that nothing consumes.fmt() or a £ template any more.headline.ts module all deleted, not left on disk.emptyVendorData still backs every surviving section.CustomerHealthSection renders health from
vendorService.getCustomerHealth while M5's definition is computed by vendorHealthService.report
— and this page already loads the second one, for M10's risk flags. Two engines for one concept are
live on the dashboard today. This run does not make it worse (it added no figure and removed only
undefined ones), but it does not fix it either. Rendering each metric's ClaimFigure beside the
section that duplicates it would have created a visible BR-4 defect, so it was deliberately not
done. spec.md → "The second-engine problem" carries the reasoning; it wants its own run.apps/demo is deliberately untouched — frozen, and the prototype at /dashboards/vendor is the
side-by-side comparison. It still renders arrMovementStrip and NRR/GRR from its own mock; that is
expected, not a missed call site.CONVENTIONS.md → Testing): vendor-money-format.test.ts asserts
the formatter from the criterion (whole units, euros, never abbreviated, signed negatives), and
route-policies.test.ts gains the deny-by-default assertions for the two deleted routes. I deleted
vendor/headline.test.ts with the module it tested.resolveMetricLinks already demotes a link whose target has left the dictionary, and no surviving
definition links to NRR or GRR — so removing them leaves no dangling linksTo.04_verify/output/verify.mde187e90 — that is the last commit in this run that changes app
source, and ci-status.sh settled it GREEN. Every commit after it touches .icm/** only (this
file and its corrections); each was pushed and re-settled GREEN in turn, so the head Ship
inherits is green and carries no code beyond e187e90.e187e90 (the code): web and docs built and were smoked
(https://web-git-claude-vendor-dashboard-reconciliation-v8xsbq-sustentus.vercel.app ·
https://docs-git-claude-vendor-dashboard-reconciliatio-094249-sustentus.vercel.app).
marketing also built; demo, help-centre and storybook were skipped — no preview exists for
them on that commit and nothing is claimed on them..icm/**-only commits after it: web rebuilt each time; the other five were skipped,
correctly — those commits change no app source. Nothing is claimed on a skipped preview, and
no claim in this file rests on a preview built from anything but e187e90.complexity: complex). The CI Claude review
(Review diff against CONVENTIONS.md) also ran and passed with no posted comments; /code-review
was run anyway rather than treating a clean CONVENTIONS pass as a substantive review of a
36-file diff. 2 findings, both fixed.Agent-run (no preview credentials — nothing signed-in is claimed here):
/vendor/dashboard, /vendor/retention, /vendor/revenue,
/vendor/delivery all redirect to /sign-in?redirect_url=…. This is why the deletions
cannot be demonstrated signed-out: the auth redirect fires before routing, so a deleted
route is indistinguishable from a live one to a signed-out client. The deny-by-default
outcome is pinned by unit test instead (route-policies.test.ts), and the signed-in
behaviour is the operator's line below (agent)getRevenueRetention still derives
tenantOid and passes it to loadCustomerWindow, whose $match is { tenantId: tenantOid }.
Reads were removed, never rewired (agent, traced in the diff)fmt() or a £
template; vendorMoney delegates to formatClaimMoney, asserted by unit test (agent)Operator-demonstrated (asked, not yet returned):
/vendor/dashboard (operator)/vendor/metrics (operator)/vendor/revenue, /vendor/delivery and /vendor/retention redirect a signed-in vendor to
their role home rather than rendering (operator)/vendor/metrics and /admin/metrics no longer list NRR or GRR (operator)Blocker, fixed — the defect this run exists to remove, introduced by this run.
RetentionBySpeedSection and /vendor/retention computed (starting − churned − contraction) / starting over customer spend — the GRR formula — labelled "Gross retention of accounts" with
Total/Churned revenue columns, under a definition popover pointing at M7. M7's own seeded
whyItMatters says: "This counts customers, not revenue — a revenue retention figure (NRR/GRR) is
a different measure this dictionary does not yet define." Before this branch GRR at least had a
definition; this branch deleted it, leaving a revenue-retention percentage with no definition
anywhere, attributed to a metric that disclaims being it. Jamie ruled: repoint to M7's real claim.
Done — the dashboard now renders the same M7 claim /vendor/metrics renders, counting customers,
with its speed and region breakdowns. getRetentionCohorts, /vendor/retention, its policy and
docs row, the Time/Region toggle (which drove only that section) and getTimeToValue (whose last
caller was the cohort rollup) went with it, along with their types and constants.
Fixed — the spend sort was silently a no-op. top-customers-table.tsx's spendValue stripped
£ and commas, but this run changed spend to vendorMoney(...) → "€214,800", so parseFloat
returned NaN → 0 for every row: the default "By total spend" sort ordered by nothing while
claiming to sort by spend. spend now stays a number through the view-model and is formatted at
render, so there is no string to parse.
Fixed — three docblocks outlived their declarations and had attached themselves to whatever
followed, including the entire pre-change getRevenueRetention docblock still advertising
"NRR, GRR, an annualised ASR run-rate", stacked directly above its replacement. This mattered more
than ordinary comment rot because an acceptance criterion asserts nothing dead is left behind.
Fixed — the docs published all three deleted routes in the vendor feature-role matrix while asserting every row is a live vendor-only route.
Accepted, recorded — vendorMoney hardcodes the default reporting currency. The M8/M10 claim
tiles format with viewer.user.currency, so a vendor whose reporting currency is not EUR would see
one symbol in the headline tiles and another in the tables below. The legacy service reads these
tables use carry no currency at all, so any symbol on them is an assumption either way. Left as-is:
the scope fixes the reporting currency at euros (BR-19), and threading a per-vendor currency through
twelve components is its own change. Worth closing when the second-engine run repoints those tables.
Deferred, unchanged — the second-engine problem. The customer-health, satisfaction and funnel
sections still read vendorService.getCustomerHealth / getLeadFunnel / getTopCustomers rather
than the engines their metrics are defined on. Retention has now left that list. Recorded in
spec.md; its own run.
Not a Verify finding — the changelog. No apps/help entry exists for a user-visible removal of
three routes and several tiles. That is Ship's step, via the changelog-entry skill, not this
stage's.
Context budget: within the Inputs table. The production-readiness, code-review and security-review skills each read wider by design; their outputs are summarised above rather than carried in full.
05_ship/output/changelog.mdOver the last few weeks each figure on your dashboard was given a definition: what is counted, over what period, as at when, and the records behind it. Until now some of the older figures sat beside those definitions while still being worked out the old way — which is how the dashboard came to show active service revenue twice, in two currencies, at two different scales.
That is fixed. Every figure on the dashboard is now read from the dictionary, so a number and its definition can no longer disagree, and no concept appears twice with two values.
Three figures now lead the page, each opening to the same workings the KPI dictionary shows:
The retention section counted money before, under a definition that says in as many words that it counts customers. It counts customers now: of those who had work delivered last period, the share who started something new in this one, broken down by how fast their work was delivered and by where they are. Every bucket shows how many customers are in it, and opens to the customers themselves.
Every money figure on the dashboard and its pages is written in euros, in full, to the whole unit. Figures that used to be abbreviated to the nearest thousand — and rounded before you saw them — are now shown as they are. One of the customer tables was also sorting by nothing at all while claiming to sort by total spend; it sorts correctly now.
Some figures were removed rather than repointed, because nothing in the dictionary defines them and so nobody could say what they counted:
Three pages went with them — the revenue, delivery and retention pages — as each was left with no figure to show and nothing linking to it. The delivery figures that do have definitions, on-time delivery rate and delivery quality, are still in the KPI dictionary.
If you used the delivery page for oversight, nothing replaces it in this release. Building a delivery surface on defined figures is a separate piece of work.
05_ship/output/investor-update.mdWho it's for: Vendors, CSMs and SDMs What shipped: Every dashboard figure now reads from the KPI dictionary, in one currency, with its workings; figures nothing defines were removed. Why it matters: Refine the Bridge · Objective 1, Establish Product-Market Fit with Vendor Partners — one number contradicting another costs trust in all of them.
Dig deeper: https://github.com/sustentus/sustentus/pull/869 · https://help.sustentus.com/changelog/2026-08-21-vendor-dashboard-reconciliation
05_ship/output/release.mdci-status.sh on the head carrying this file. The last commit changing app
source is e187e90, settled GREEN at Verify; everything after it is .icm/**, docs and the
changelog, each re-settled in turn.apps/docs/app/technical/packages/services — the Metric dictionary section. It
said ten records are seeded and later changes land as further migrations; the dictionary held
twelve until this run removed NRR and GRR, so the count is only now true, and removal-by-migration
(never an edit to the migration that shipped) is what this run established.apps/docs/app/business/feature-role-matrix/vendors — the /vendor/revenue,
/vendor/delivery and /vendor/retention rows removed (done at Verify, when the routes went).
business/roles' vendor section needed no edit: its retention paragraph already described M7
counting customers — this run is what made the dashboard match it.apps/docs/archive/pipeline-runs/, and — this being the ninth and
last stub of vendor-metrics — the epic's intake folder too, provided every sibling run has
merged. Written as intent: the merge that carries this file is what makes it true./vendor/dashboard reads through the claim surface (BR-1, BR-2) — met for
M2, M7, M8 and M10; not met for customer-health, satisfaction and funnel, which still read
the legacy vendorService. Repointing them is a rebuild, carved out to its own run; rendering
their claims beside them would have created the BR-4 defect this run exists to remove.getRevenueRetention still returns perCustomerRevenue; no field survives that nothing
consumes. getRetentionCohorts was deleted at Verify — it fed GRR maths under M7's
definition, on a branch deleting GRR's definition./vendor/revenue, /vendor/delivery and /vendor/retention deleted with their policies.Two criteria ship unticked, both for the one reason recorded in spec.md as "the second-engine
problem". Nine of the eleven operator-demonstrated DoD lines in verify.md were not returned before
this merge; the agent-run half is recorded there, and the spend-sort fix — the one silently broken
behaviour — is covered by the claim that it now sorts on a number rather than a parsed string.