Skip to Content

← All archived runs

Run: sla-stage-map-engine-alignment

run.md

Run: sla-stage-map-engine-alignment

  • branch: claude/sla-stage-map-engine-alignment-fi76vv
  • pr: #811

02_define/output/spec.md

Spec: Align the SLA stage map with the workflow engine

  • slug: sla-stage-map-engine-alignment
  • personas: CSM, Expert, Admin
  • touches: packages/services/src/db/services/sla
  • complexity: standard

Problem

The 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.

Proposed change

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:

  • Strip the ~40 legacy display-name keys ("brd ready", "under negotiation", "in progress", "invoice sent", …). Migration 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.
  • Fix the stale comment at stage-map.ts:5-7 — status names are no longer tenant data; they are in-repo engine data edited by PR only.
  • Cover the mapping with a test that walks the engine. The test derives the lead status list from 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.

Acceptance criteria

  • Every lead status in 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.
  • A test in 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.
  • The stage map contains no key that is not an engine status name; the six SLA stages are each reachable from at least one status.
  • 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.
  • A companion case proves an unmapped/terminal status (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.
  • The stale "status names are tenant data" comment is replaced by one that describes the engine as the source of truth.

Out of scope

  • assertTransition enforcement for milestone / expert-evidence transitions — a real doctrine gap (audit finding #5) explicitly deferred for the whole scope (D1/D5).
  • Seeding the data that exercises the new bands. storyline-full-coverage-generator depends on this stub for exactly that; nothing here writes demo data.
  • Any change to computeBand, businessDaysBetween, or the sla-definition model — including the six-stage SLA_STAGES vocabulary itself.
  • A per-tenant, data-driven editable stage mapping. Still a static in-repo table this round.
  • Mapping the 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.
  • Adjusting the expert workbench's close-out bucketing. The bucket shift described above is the existing code finally receiving correct input, not a change to it.
  • Any migration or index work — this is pure code plus tests.

Open questions

  • none. The two Define decisions (deliveredBilling; 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.md

Build notes: sla-stage-map-engine-alignment

  • commits: feat: sla-stage-map-engine-alignment — resolve every engine lead status to an SLA stage

What changed

  • packages/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.

Acceptance criteria status

  • Every lead status resolves deliberately — the nine engine statuses map per the spec's table, with completed and qualified_out present as explicit null.
  • The test derives the status list from getStatusesByWorkflow("lead") and fails if any engine status has no entry.
  • No key that is not an engine status name; all six stages reachable — both asserted.
  • 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.
  • The 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.
  • Stale comment replaced.

Notes for Verify

  • Expected behaviour change, and it is the point. 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.
  • The deliveredBilling decision is the one that makes a tenant's Billing SLA fire at all; settled with Jamie at Define and recorded in spec.md.
  • Tests written, not run (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.
  • Nothing seeds data that exercises the new bands — that is storyline-full-coverage-generator, which depends on this run.

04_verify/output/verify.md

Verify: sla-stage-map-engine-alignment

  • production-readiness: not required — the diff adds no migration, no schema or index change, no new env var, and no auth/payments code. It sits under src/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.
  • code-review: medium (spec complexity: standard) — the CI Review diff against CONVENTIONS.md job is skipped on this repo, so /code-review ran instead. One finding, below; nothing fixed on branch.
  • security-review: not required — no auth, payments, PII or route policies in the diff.
  • playwright: TODO — manual DoD smoke performed instead.

CI

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.

DoD smoke (on the preview — each line says who verified it)

  • Every lead status resolves deliberately — demonstrated: stage-map.test.ts walks getStatusesByWorkflow("lead") and asserts a deliberate entry for all nine; green in the Quality run (agent)
  • Mapping test derives the status list from the engine, not a copy — demonstrated: the test imports the engine accessor; reviewer independently confirmed byWorkflow.get("lead") holds exactly the nine top-level statuses (agent)
  • No non-engine key; all six SLA stages reachable — demonstrated: both asserted in stage-map.test.ts, green (agent)
  • Backdated 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)
  • Terminal status degrades gracefully — demonstrated: the 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)
  • Stale "statuses are tenant data" comment replaced — demonstrated: stage-map.ts:1-21 (agent)
  • Preview loads — demonstrated: web preview returns 200 unauthenticated (agent)
  • Overdue / at-risk / breached actually render on the expert work queue, CSM blocker queue and SDM adherence — operator, signed in. Note the demo tenant has no seeded data in these states yet (that is 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.
  • auth: expert / CSM / SDM sign-in + dashboard OK — operator
  • payments: not touched — no test transaction needed
  • notifications: none expected — this run fires no notification (notifySLABreach is not wired to these bands and is untouched)

Findings & cleanup

  • 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 backlogBRD and quotation_processBidding. 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_processBRD 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.md


title: SLA timing now follows a project the whole way, not just at intake date: 2026-08-14T13:10:00Z personas: [csm, sdm, expert, admin] slug: sla-stage-map-engine-alignment pr: https://github.com/sustentus/sustentus/pull/811

SLA timing now follows a project the whole way, not just at intake

The 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.

  • Experts see time against target on the work queue, with overdue projects called out instead of reading as on track.
  • CSMs get a real stage and a real band on the blocker queue, so a project that has stalled in scoping, bidding or delivery surfaces on its own.
  • SDMs see adherence figures that count the whole journey rather than the first step.
  • Projects that are delivered or out for a satisfaction survey now sit under Move to completion in the expert workbench, where the closing stages belong, rather than staying mixed in with live delivery work.

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.md

SLA timing now covers the whole project journey

Who 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.md

Ship: sla-stage-map-engine-alignment

  • pr: #811 · https://github.com/sustentus/sustentus/pull/811 · merge: authorised — Ready to merge ticked by Jamie; this commit rides the squash
  • CI: green — Quality Project success (format · lint · typecheck · Run tests); preview migration, DB audit and the four advisory pipeline checks all success
  • technical docs: no technical docs impact — no page documents the status→SLA-stage mapping; technical/packages/services mentions SLA only as an onboarding default, unchanged here
  • business docs: no business docs impact — business/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
  • release notes: both
  • sent: ship note queued for #product-update on merge (ship-note.yaml)

Acceptance check (vs spec)

  • Every lead status resolves deliberately — verified in Verify: engine-walking test green
  • Mapping test derives the status list from the engine, not a copy — verified in Verify
  • No non-engine key; all six SLA stages reachable — verified in Verify
  • Backdated work_in_progress computes a red band with no database — verified in Verify via the pure buildLeadSlaTiming export
  • Terminal status degrades gracefully — verified in Verify
  • computeBand unchanged — verified in Verify; untouched in the diff
  • derive* signatures unchanged, no caller edited — verified in Verify; typecheck green
  • Stale "statuses are tenant data" comment replaced — verified in Verify

Carried forward

  • The signed-in half of the DoD smoke was never demonstrated. verify.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" names two different statuses across surfaces — the vendor funnel's 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.
  • No data yet exercises the new bands — that is storyline-full-coverage-generator, the stub that depends on this run.