sla-stage-map-engine-alignmentrun.md02_define/output/spec.mdThe SLA stage map (services/sla/stage-map.ts) predates the JSON workflow engine and was written
against the display names of the retired status collection. Of the nine canonical lead statuses
in db/workflows/workflows.json, only pending resolves to an SLA stage: work_in_progress
normalises to "work in progress" but the table holds "in progress", delivered ≠ "delivery", and
backlog, quotation_process, awaiting_confirmation, survey_sent, completed are absent
entirely. Every tenant's sla_definitions row — targets, warning/breach bands, escalation intent —
is therefore inert for the whole journey past intake, and the overdue / at-risk / breached states
are unreachable on every surface that renders them (expert work queue, expert active projects, CSM
blocker queue, SDM coverage and market signals). This is finding #1 (HIGH) of the demo-tenant data
audit and decisions D1 + D5 of _source/decisions.md; it advances Refine the bridge / Q2-2026
Objective 1 — Establish Product-Market Fit with Vendor Partners, because a demo tenant whose SLA
machinery never fires cannot show a vendor the operational control the product is sold on.
Rewrite the stage map so it is keyed on engine status names, not the retired display names, and
so every lead status in the engine resolves deliberately — to one of the six canonical SLA stages
(Request, BRD, Bidding, Delivery, Billing, CSAT) or explicitly to no stage for the two
terminal statuses. The agreed mapping:
| Engine lead status | SLA stage | Why |
|---|---|---|
pending |
Request |
Lead submitted, before requirements gathering starts |
backlog |
BRD |
Discovery Review / Scope & Plan — this is requirements gathering |
quotation_process |
Bidding |
Expert matching; carries the proposal sub-workflow |
awaiting_confirmation |
Bidding |
Quote review/negotiation; loops back to quotation_process |
work_in_progress |
Delivery |
The work being done; carries milestone + invoice |
delivered |
Billing |
Work complete — the outstanding clock is invoicing and payment |
survey_sent |
CSAT |
Post-delivery satisfaction |
completed |
none (null) |
Terminal — nothing is pending, so no clock may run |
qualified_out |
none (null) |
Terminal negative — same |
Three further changes travel with it:
1781913600000-status-to-string-keys rewrote every
statushistories.status from an ObjectId into the engine name string, so no alias key can be
reached by any live row — they are provably dead. The two terminal statuses are represented
explicitly (mapped to null), not by absence, so "unmapped" and "deliberately no stage" stop
being the same thing in the source.stage-map.ts:5-7 — status names are no longer tenant data; they are
in-repo engine data edited by PR only.getStatusesByWorkflow("lead") (never a hardcoded copy) and asserts every status has a
deliberate entry, so adding a status to workflows.json fails the suite loudly instead of
silently unmapping a stage.To assert a band without a database (the @sustentus/services vitest project is unit-tier only —
no mongodb-memory-server), timing.ts gains a named export of its pure timing computation
(today the module-private buildTiming): status row + now + a Map<SlaStage, ISlaDefinition> in,
LeadSlaTiming out, no I/O. deriveLeadSlaTiming / deriveLeadSlaTimingForLeads keep their
current signatures and behaviour and call the same function.
Expected downstream consequence, and it is the point: expert-workbench/active-projects.ts
buckets on CLOSE_OUT_STAGES = {Billing, CSAT}. Today every non-pending lead resolves to null,
so the "Move to completion" bucket is unreachable and everything lands in "Work in progress". After
this change, delivered and survey_sent leads route to "Move to completion" as that code always
intended.
workflows.json resolves through resolveSlaStage to a deliberate
result — one of the six SLA stages, or null for the two terminal statuses
(completed, qualified_out) — per the mapping table under Proposed change.packages/services/src/db/services/sla/ derives the lead status list from the
workflow engine (getStatusesByWorkflow("lead")) rather than a hardcoded list, and fails if
any engine status has no deliberate entry in the map.timing.ts exports a pure timing function, and a unit test proves that a lead in
work_in_progress, whose latest status-history row is backdated past that stage's
breachDays, computes band: "red", overdue: true and slaStage: "Delivery" from a
fabricated sla_definitions row — with no database.completed) still degrades gracefully:
current status and timeInStageDays returned, targetDays/remainingDays/band null,
overdue false.computeBand is unchanged — band semantics (>= breachDays → red, >= warningDays →
at-risk) are identical before and after.deriveLeadSlaTiming and deriveLeadSlaTimingForLeads keep their existing exported
signatures; no caller in packages/services or apps/web needs an edit to compile.assertTransition enforcement for milestone / expert-evidence transitions — a real doctrine gap
(audit finding #5) explicitly deferred for the whole scope (D1/D5).storyline-full-coverage-generator depends on this
stub for exactly that; nothing here writes demo data.computeBand, businessDaysBetween, or the sla-definition model — including the
six-stage SLA_STAGES vocabulary itself.proposal / quote / milestone / invoice sub-workflow statuses or the
expert-scoped verification workflow. resolveSlaStage is only ever called with a lead's
status-history row (entityType: "lead"); sub-workflow statuses are out of its domain.delivered → Billing; strip the legacy aliases to an
engine-exact map) were settled with Jamie during this stage and are recorded in the mapping table
and Proposed change above.03_build/output/notes.mdfeat: sla-stage-map-engine-alignment — resolve every engine lead status to an SLA stagepackages/services/src/db/services/sla/stage-map.ts: rewritten. The table is now a Map keyed
on the nine engine status names from workflows.json, exported as LEAD_STATUS_SLA_STAGE so the
test can walk it. The ~40 legacy display-name aliases and the normalise() helper are gone —
migration 1781913600000-status-to-string-keys rewrote every statushistories.status to the
engine name string, so no alias key was reachable. The stale "status names are tenant data"
comment is replaced with one that names workflows.json as the source of truth. A Map rather
than a plain object: object lookup would have resolved "toString" to a function typed as
SlaStage.packages/services/src/db/services/sla/timing.ts: the module-private buildTiming is now the
exported, pure buildLeadSlaTiming, and LatestStatusRow is exported alongside it. Both
deriveLeadSlaTiming and deriveLeadSlaTimingForLeads keep their signatures and call it
unchanged. Nothing else moved; computeBand is byte-identical.packages/services/src/db/services/sla/stage-map.test.ts (new): the agreed mapping asserted as
one literal, plus three engine-walking checks — every lead status has a deliberate entry, no key
is a non-engine name, and all six SLA stages are reachable. The status list comes from
getStatusesByWorkflow("lead"), never a copy.packages/services/src/db/services/sla/timing.test.ts (new): band behaviour with no database —
red past breach, at-risk past warning, on-track inside, plus the graceful-degradation paths
(terminal status, stage with no tenant definition, no status history).packages/services/src/db/services/expert-workbench/active-projects.ts: one comment sentence.
It justified the close-out boundary by saying the stage map "collapses review/sign-off into
Delivery" — statuses that no longer exist in the map after this change. Comment only; no logic
touched.completed and qualified_out present as explicit null.getStatusesByWorkflow("lead") and fails if any engine
status has no entry.timing.ts exports a pure function, and work_in_progress backdated ten business days past
an 8-day breach computes band: "red", overdue: true, slaStage: "Delivery" — no database.completed companion case asserts current status + timeInStageDays survive while
targetDays/remainingDays/band are null and overdue is false.computeBand unchanged — the at-risk/on-track/red cases pin the same semantics.deriveLeadSlaTiming / deriveLeadSlaTimingForLeads signatures unchanged; no caller edited.expert-workbench/active-projects.ts buckets
on CLOSE_OUT_STAGES = {Billing, CSAT}. Before this change every non-pending lead resolved to
null, so "Move to completion" was unreachable and everything landed in "Work in progress".
delivered → Billing and survey_sent → CSAT now route there. Worth an eye on the preview.delivered → Billing decision is the one that makes a tenant's Billing SLA fire at all;
settled with Jamie at Define and recorded in spec.md.block-local-checks.sh); the Quality workflow's Run tests step is the
signal. The date arithmetic is hand-checked: 2026-01-05 and 2026-01-19 are both Mondays, so
businessDaysBetween counts ten weekdays between them.storyline-full-coverage-generator,
which depends on this run.04_verify/output/verify.mdsrc/db/services/ but every database
interaction (StatusHistory.find, getSlaDefinitions) is untouched; the change is pure mapping
logic plus tests. See Findings for the one production-behaviour consequence, which is intended.Review diff against CONVENTIONS.md
job is skipped on this repo, so /code-review ran instead. One finding, below; nothing fixed
on branch.Quality Project success (format · lint · typecheck · Run tests) — the new unit tests pass.
Preview migration, DB audit and the four advisory pipeline checks are green. web preview deployed
and responds 200.
stage-map.test.ts walks
getStatusesByWorkflow("lead") and asserts a deliberate entry for all nine; green in the
Quality run (agent)byWorkflow.get("lead") holds
exactly the nine top-level statuses (agent)stage-map.test.ts, green (agent)work_in_progress computes a red band with no database — demonstrated:
timing.test.ts via the newly-exported pure buildLeadSlaTiming; 10 business days against an
8-day breach → red / overdue: true / slaStage: "Delivery", green (agent)completed case asserts status and
timeInStageDays survive while target/remaining/band are null and overdue false (agent)computeBand semantics unchanged — demonstrated: computeBand is untouched in the diff, and
the red/at-risk/on-track trio pins the same thresholds (agent)derive* signatures unchanged, no caller edited — demonstrated: diff touches no call site;
typecheck green across the monorepo (agent)stage-map.ts:1-21 (agent)web preview returns 200 unauthenticated (agent)storyline-full-coverage-generator), so expect the bands to be reachable
rather than populated.delivered / survey_sent leads appear under expert workbench "Move to completion"
rather than "Work in progress" — operator, signed in. This bucket was unreachable before
this change; it is the one visible behaviour shift.notifySLABreach is not
wired to these bands and is untouched)Needs an owner decision — "BRD" means two different statuses across surfaces. The vendor
funnel defines BRD_STAGE = "quotation_process" (services/vendor/index.ts:450, commented "the
BRD/scoping stage"), while this run maps backlog → BRD and quotation_process → Bidding.
The same lead is therefore counted as "reached BRD" on the vendor funnel while the CSM blocker
queue and expert workbench label its stage "Bidding", and the funnel's avg-lead-to-BRD figure is
anchored on a transition the tenant's BRD SLA row no longer governs. Nothing breaks — the two
surfaces read different code — but they will sit side by side in the demo.
This is not pre-existing: before this run neither status resolved to any SLA stage, so the funnel's vocabulary was unopposed. This change introduces the second, disagreeing voice.
Recommendation: keep this run's mapping and treat the funnel constant as the misnomer.
backlog's persona labels are unambiguously discovery ("Discovery Review", "Scope & Plan",
"Preparing Your Project"), and quotation_process carries the proposal sub-workflow with
labels "Expert Matching" / "Prepare Proposal" / "Finding the Right Experts" — that is bidding,
and the funnel's own comment says "a bid/quote process has begun". Renaming BRD_STAGE to
something like FIRST_REVENUE_STAGE is a pure rename with no behaviour change, but it is a
different surface and outside this spec's scope — recommended as a follow-up
/pipeline chore, not a drive-by here.
The alternative — re-mapping quotation_process → BRD to match the funnel — changes which SLA
timers govern the bidding phase and would reopen Define.
No other findings. The reviewer independently confirmed the alias removal is safe (every
entityType: "lead" status-history writer emits engine names, and migration
1781913600000-status-to-string-keys gates its conversions on isKnownStatus), that the
object → Map switch removes the prototype-key hazard, and that timing.test.ts is
timezone-independent.
Context budget: within the Inputs table; the one extra read was services/vendor/index.ts:444-780
to substantiate the code-review finding.
05_ship/output/changelog.mdThe stage timers you configure under Configure → SLA only ever applied to brand-new requests. Once a project moved past intake, the clock stopped: nothing was measured against your targets, and the on-track, at-risk and breached labels could not appear no matter how long a project sat.
Every stage of the journey is now measured.
Finished and qualified-out projects deliberately carry no timer — nothing is outstanding on them, so nothing counts against a target.
Your configured targets, warning and breach thresholds are unchanged, and so is what each band means. What changed is how much of the journey they are applied to.
05_ship/output/investor-update.mdWho it's for: CSMs, SDMs, experts and admins What shipped: Tenant SLA targets now measure every stage, not only new requests. Why it matters: Refine the bridge — vendors see stalled work surface itself, without chasing.
Nine project statuses now resolve to a stage; previously one did.
Dig deeper: https://github.com/sustentus/sustentus/pull/811 · https://help.sustentus.com/changelog/2026-08-14-sla-stage-map-engine-alignment
05_ship/output/release.mdQuality Project success (format · lint · typecheck · Run tests); preview
migration, DB audit and the four advisory pipeline checks all successtechnical/packages/services mentions SLA only as an onboarding default, unchanged herebusiness/service-journey/delivery's overdue rows are
milestone health, not SLA bands; business/initiatives/refine-the-bridge describes SLA
breach alerting as initiative intent, which this run advances rather than contradicts#product-update on merge (ship-note.yaml)work_in_progress computes a red band with no database — verified in Verify via
the pure buildLeadSlaTiming exportcomputeBand unchanged — verified in Verify; untouched in the diffderive* signatures unchanged, no caller edited — verified in Verify; typecheck greenverify.md left three operator
lines unticked — the bands rendering on the expert work queue / CSM blocker queue / SDM
adherence, the delivered/survey_sent move into the expert workbench's "Move to completion"
bucket, and persona sign-in. The agent has no preview credentials, so these were offered to the
operator and not reported back before the merge was authorised. Recorded here rather than
implied as done. All eight spec acceptance criteria were demonstrated by the unit suite and the
diff, which is what the ticked gate rests on.BRD_STAGE = "quotation_process" (services/vendor/index.ts:450) versus this run's
backlog → BRD / quotation_process → Bidding. Raised in verify.md; the gate was ticked on
the recommendation to keep this mapping and treat the funnel constant as the misnomer. The
rename is a follow-up /pipeline chore, not carried in this PR.storyline-full-coverage-generator, the stub
that depends on this run.