Skip to Content

← All archived runs

Run: sla-stage-targets

run.md

Run: sla-stage-targets

  • issue: #461 # canonical home of the spec + state (labels, comments)
  • branch: claude/loving-hamilton-w1a791
  • pr: #463

00_intake/stub.md

Stub: Per-stage SLA targets + timing engine

  • feature-slug: sla-stage-targets
  • epic: expert-dashboard-data
  • personas: Expert (view); CSM / SDM (own the targets); platform (computes)
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: expert-workbench-foundation
  • sequence: 2 of 8

Problem

The workbench shows per-item SLA targets and time-pressure signals everywhere — sla ("2 days"), dueIn, overdue, timeStuck, timeRemaining, and the on-track/at-risk/red status colours. None of this is modelled. Today the only SLA notion is an ad-hoc sla.breach notification (packages/services/src/notifications/sla) with no stored target; stage labels come from the status workflow and timing from milestone.dueDate

  • statushistory.changedAt. The work queue, work-in-progress, move-to-completion and the earnings "at risk" figure all need a single, consistent timing layer — so it is built once, here.

Proposed change

  • Define a canonical per-stage SLA-target model: for each delivery stage (mapped from the status workflow — e.g. BRD, Delivery, CSAT, Sign-off, Review), a target duration. Tenant-configurable; ship sensible defaults.
  • Build a timing derivation in @sustentus/services/server that, for a lead, computes: current stage, time-in-stage (from the latest statushistory entry for that status), the stage's SLA target, overdue / time-remaining, and a derived status band (on-track | at-risk | red).
  • Expose this as a reusable helper the work-table features consume; no UI of its own beyond what those features render.

Acceptance criteria (rough)

  • Each delivery stage has a configurable SLA target with a default; targets are queryable per tenant.
  • For a given lead, the engine returns current stage, time-in-stage, target, overdue/remaining and a status band — derived, not hard-coded.
  • The work-queue, active-projects and earnings features all read timing from this one helper.
  • A stage with no target configured degrades gracefully (no false "overdue").

Out of scope (this feature)

  • Re-plumbing the sla.breach notification onto these targets (later concern).
  • The blocker/escalation timing fields (timeStuck since a blocker, lastEscalation) — those come from work-blockers-escalations; this feature covers stage/SLA timing.
  • Rendering any specific table — features 4/5/7 consume the helper.

Notes for Define

  • Decide the stage taxonomy: derive purely from the status workflow displayNames, or introduce a small stage-grouping map. The mock uses BRD / Delivery / CSAT / Sign-off / Review / Design.
  • Decide model home: a tenant-setting entry vs a dedicated sla-target collection keyed by workflow+stage.
  • touches: packages/services/src/db/models (new SLA-target model or tenant-setting extension), packages/services/src/db/models/status-history.ts (read), packages/services/src/server (timing helper). </content> </invoke>

01_define/output/spec.md

Spec: Per-stage SLA timing engine

  • slug: sla-stage-targets
  • issue: #461
  • personas: Expert
  • touches: packages/services/src/db/services/sla/timing.ts (new), packages/services/src/db/services/sla/stage-map.ts (new), packages/services/src/db/services/sla/index.ts, packages/services/src/db/services/index.ts, packages/services/src/db/models/status-history.ts (read), packages/services/src/db/models/status.ts (read), packages/services/src/db/models/sla-definition.ts (read)
  • complexity: standard

Problem

The expert workbench renders per-item SLA pressure everywhere — sla ("2 days"), dueIn, overdue, timeRemaining, and the on-track / at-risk / red status colours (see apps/dashboards/lib/mock/expert.ts) — but none of it is derived from real data. The only live SLA notion is the ad-hoc notifySlaBreach helper (packages/services/src/notifications/sla/), which stores no target. This is feature 2 of 8 in the expert-dashboard-data epic, advancing Build the Bridge → Q2 2026 Objective 3 (Validate Technical Infrastructure & Payout Flow) by giving the expert work-table features one consistent, real timing layer instead of six independent mock fields.

Two things changed since the stub was cut. First, admin-sla-config (#455) already shipped the configurable per-stage target half of this stub: a per-tenant sla-definition model (targetDays / warningDays / breachDays in business days, plus owner/type/impact) over the six canonical service stages Request | BRD | Bidding | Delivery | Billing | CSAT, with slaConfigService.getSlaDefinitions(tenantId). So this feature does not introduce a new target model — it consumes that one. Second, the timing input — current stage and time-in-stage — already lives in statushistory (latest entry per lead, entityType: "lead"). What is still missing is the derivation engine that joins the two: map a lead's current workflow status to a canonical SLA stage, look up that stage's target, and compute time-in-stage, remaining/overdue, and a status band. Built once here, the work-queue, active-projects and earnings features (4 / 5 / 7) all read timing from it rather than each re-deriving it.

Proposed change

  • Static stage map (db/services/sla/stage-map.ts): a code-level table from lead workflow status.nameSlaStage (one of the six canonical stages), with a resolveSlaStage(statusName) helper that returns null for any status not mapped to a stage. No schema change, no migration, no per-tenant editing this round (a data-driven status.slaStage field is out of scope — noted below).
  • Timing engine (db/services/sla/timing.ts) in @sustentus/services/server that, for a lead, computes from its latest statushistory entry and the tenant's sla-definition rows:
    • currentStatus — the lead's current status (id, name, displayName) from the most recent statushistory entry (entityType: "lead"), or null if it has no history.
    • slaStage — the canonical stage that status maps to (via the static map), or null if unmapped.
    • timeInStageDaysbusiness days elapsed since that latest entry's changedAt (the stub's "time-in-stage from the latest statushistory entry for that status").
    • targetDays — the mapped stage's targetDays from the tenant's sla-definition, or null when the stage is unmapped or has no definition for that tenant.
    • remainingDaystargetDays − timeInStageDays (negative ⇒ past target), or null when no target.
    • overduetrue only when a target exists and timeInStageDays > targetDays; otherwise false (never a false "overdue" when there is no target).
    • band — the derived status band, computed from the three thresholds: on-track while timeInStageDays < warningDays, at-risk from warningDays up to breachDays, red at or past breachDays; null when the stage is unmapped or has no definition. (targetDays drives remaining/overdue; warningDays/breachDays drive the band — using all three thresholds the existing model already stores.)
  • A reusable, batched API the work-table features consume:
    • deriveLeadSlaTiming(tenantId, leadId) → one LeadSlaTiming.
    • deriveLeadSlaTimingForLeads(tenantId, leadIds)Map<leadId, LeadSlaTiming>, loading the tenant's sla-definition rows once and the latest status per lead in one pass (so a work-queue of N leads is not N×2 round-trips).
    • LeadSlaTiming, SlaBand and the functions are exported from db/services/sla/index.ts and re-exported through db/services/index.ts so they reach @sustentus/services/server.
  • Business-days computation is shared, not re-implemented: a businessDaysBetween(from, to) helper (Mon–Fri, ignoring public holidays this round) so elapsed time and the model's business-day thresholds are compared in the same unit.
  • No UI in this feature. The engine is server-only logic; the expert work-table features render it.

Acceptance criteria

  • deriveLeadSlaTiming(tenantId, leadId) returns currentStatus, slaStage, timeInStageDays, targetDays, remainingDays, overdue and band, all derived from statushistory + sla-definition — none hard-coded.
  • Time-in-stage is measured in business days from the lead's most recent statushistory entry, matching the unit of the sla-definition thresholds.
  • band is on-track below warningDays, at-risk from warningDays to below breachDays, and red at or past breachDays; overdue is true only when timeInStageDays > targetDays.
  • When the current status maps to no stage, or the mapped stage has no sla-definition for the tenant, the engine still returns currentStatus and timeInStageDays but sets slaStage/ targetDays/remainingDays/band to null and overdue to false (graceful degradation — no false "overdue").
  • deriveLeadSlaTimingForLeads(tenantId, leadIds) returns a per-lead map and loads the tenant's SLA definitions once (not once per lead).
  • The engine reuses the existing sla-definition model / slaConfigService for targets — no new target model is introduced.
  • LeadSlaTiming, SlaBand, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads and resolveSlaStage are exported and reachable from @sustentus/services/server.

Out of scope

  • Rendering any specific table — the work-queue, active-projects and earnings features (4 / 5 / 7) consume this helper; their UI wiring is their own scope.
  • Re-plumbing the sla.breach notification onto these targets (later concern).
  • Blocker / escalation timing (timeStuck since a blocker, lastEscalation) — those come from work-blockers-escalations.
  • A data-driven, per-tenant editable status→stage mapping (e.g. an slaStage field on the status model with seeding / a config surface) — the static map ships now; this is the future path.
  • Public-holiday calendars in the business-days calculation — Mon–Fri only this round.
  • Replacing the expert mock fields in apps/dashboards/lib/mock/expert.ts — the dashboards app is standalone mock data; consuming features swap to the engine when they are built.

Open questions

  • none

02_build/output/notes.md

Build notes: sla-stage-targets

  • branch: claude/loving-hamilton-w1a791
  • commits: feat: sla-stage-targets — per-stage SLA timing engine

What changed

  • packages/services/src/db/services/sla/stage-map.ts (new): resolveSlaStage(statusName) — a normalised static table mapping lead-workflow status.name values onto the six canonical SLA stages (Request | BRD | Bidding | Delivery | Billing | CSAT). Unmapped names return null. Normalisation lower-cases and collapses _/-/whitespace so granular statuses ("BRD ready", "brd_pending") group onto one stage.
  • packages/services/src/db/services/sla/business-days.ts (new): businessDaysBetween(from, to) — whole Mon–Fri days elapsed, computed in UTC (no public-holiday calendar this round), matching the business-day unit of the sla-definition thresholds.
  • packages/services/src/db/services/sla/timing.ts (new): the timing engine.
    • LeadSlaTiming / SlaBand types.
    • deriveLeadSlaTiming(tenantId, leadId) and deriveLeadSlaTimingForLeads(tenantId, leadIds).
    • Reads the lead's latest statushistory entry (entityType: "lead", populated status) for current status + time-in-stage, and the tenant's sla-definition rows (via slaConfigService) for the target. Derives targetDays, remainingDays, overdue, and the band from the stage's target/warning/breach thresholds.
  • packages/services/src/db/services/sla/index.ts: re-exports the new helpers/types.
  • packages/services/src/db/services/index.ts: re-exports resolveSlaStage, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads, businessDaysBetween, and the LeadSlaTiming/SlaBand types, so they reach @sustentus/services/server (via db/indexserver/index).

Acceptance criteria status

  • deriveLeadSlaTiming returns currentStatus, slaStage, timeInStageDays, targetDays, remainingDays, overdue, band — all derived from statushistory + sla-definition.
  • Time-in-stage is in business days from the most recent statushistory entry's changedAt.
  • band = on-track below warningDays, at-risk from warningDays to below breachDays, red at/past breachDays; overdue only when timeInStageDays > targetDays.
  • Unmapped status or no tenant definition → currentStatus/timeInStageDays returned with slaStage/targetDays/remainingDays/band null and overdue false (emptyTiming and the no-def branch in buildTiming).
  • deriveLeadSlaTimingForLeads returns a per-lead Map; definitions loaded once and latest statuses fetched in a single $in query (two queries for N leads, not 2N).
  • No new target model — reuses sla-definition / slaConfigService.getSlaDefinitions.
  • LeadSlaTiming, SlaBand, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads, resolveSlaStage exported and reachable from @sustentus/services/server.

Verify result

  • Format · lint · typecheck · build run in CI + the Vercel preview, not here. No check is expected to fail.

Notes for review

  • The stage map is keyed on status.name. Lead workflow status names are tenant data and not seeded in this repo, so the table is a best-effort grouping with common variants; it is the single place to extend when real workflow status names differ. Unmapped statuses degrade gracefully (no false "overdue"), so an incomplete table never produces wrong SLA signals — only absent ones.
  • band uses warningDays/breachDays; overdue/remainingDays use targetDays. All three thresholds the existing model stores are therefore exercised.

03_ship/output/changelog.md

Changelog: sla-stage-targets

No end-user changelog entry — internal infrastructure change.

This feature ships a server-only timing engine (@sustentus/services) that the expert work-table features will consume; it has no user-visible behaviour of its own this round (the consuming UI is wired by features 4/5/7 of the expert-dashboard-data epic). Nothing is appended to apps/help/app/changelog/page.mdx. The investor note frames it as delivery-timing infrastructure.

03_ship/output/investor-update.md

A single, trustworthy source for delivery timing

We've shipped the timing layer that powers SLA tracking across the expert workbench: one engine that measures how long each piece of work has sat in its current stage — in business days — against the stage's configured target, and derives whether it is on track, at risk, or breached. These signals used to be unmodelled placeholders; now they come from real delivery data and the targets our teams configure. It's foundational plumbing for keeping the lead-to-payout cycle inside target and giving experts, CSMs and SDMs the same reliable read on delivery health.

This advances Build the Bridge — specifically Q2 2026 Objective 3, Validate Technical Infrastructure & Payout Flow, where holding lead-to-payout cycle time under five business days depends on measuring stage timing reliably.

  • One engine, used everywhere — the work queue, active projects, and earnings views all read the same stage timing rather than each estimating it differently.
  • Real targets, not placeholders — timing is derived from each delivery stage's configured SLA target, with a clear on-track / at-risk / breached signal.
  • Degrades safely — work in a stage with no configured target shows no false "overdue", so the signal stays trustworthy as configuration is filled in.

03_ship/output/pr.md

Ship: sla-stage-targets

  • PR: #463 — https://github.com/sustentus/sustentus/pull/463
  • branch: claude/loving-hamilton-w1a791 (merged up to date with main before ship)
  • CI: format pass · lint pass · typecheck pass · issue-link pass · vercel preview pass
  • technical docs: no technical docs impact — apps/docs/technical/packages/services lists helper exports as examples ("e.g. notifyBidSubmitted…") and routes the full contract to AGENTS.md; a new server-only helper under an existing package doesn't change documented architecture (same determination as admin-sla-config).
  • business docs: no business docs impact — server-only timing engine, no new route/persona-capability/service-journey step; the expert work-table UI that surfaces it is features 4/5/7, shipped separately.
  • release notes: investor-only — investor draft in 03_ship/output/investor-update.md; no end-user changelog entry (internal change), recorded in 03_ship/output/changelog.md.

Review summary

/code-review (high) over the diff vs origin/main — correctness sweep found no bugs; 4 cleanup candidates, all accepted (consistent with repo patterns + dashboard scale):

  • Accepted — loadLatestStatusByLead fetches all status-history rows for the leads and keeps the latest per lead in JS rather than a $group/$first aggregation. Matches the established status/build-progress-items.ts pattern (.find().sort()), and runs at dashboard scale (tens of leads); an aggregation + manual status $lookup would add more risk than it saves here.
  • Accepted — idStr is a small local ObjectId→string helper also present in build-progress-items.ts; a shared util would be a repo-wide refactor (same call as admin-sla-config's isDuplicateKeyError).
  • Accepted — deriveLeadSlaTimingForLeads separates the invalid-id pass from the compute pass (two short loops); the separation is readable and correct, and N is small.
  • Accepted — deriveLeadSlaTiming (single) doesn't wrap the batch function; the explicit single-lead path keeps the one-id query obvious.

Acceptance check (vs spec)

  • deriveLeadSlaTiming(tenantId, leadId) returns currentStatus, slaStage, timeInStageDays, targetDays, remainingDays, overdue, band — derived from statushistory + sla-definition (timing.ts buildTiming).
  • Time-in-stage is business days from the latest statushistory.changedAt (businessDaysBetween, UTC, Mon–Fri).
  • band = on-track below warningDays, at-risk warningDays→below breachDays, red at/past breachDays; overdue only when timeInStageDays > targetDays (computeBand + the overdue expression).
  • Unmapped status or no tenant definition → currentStatus/timeInStageDays returned, slaStage/targetDays/remainingDays/band null, overdue false (emptyTiming + no-def branch).
  • deriveLeadSlaTimingForLeads returns a per-lead Map, definitions loaded once + latest statuses in one $in query (two queries for N leads).
  • No new target model — reuses slaConfigService.getSlaDefinitions / sla-definition.
  • LeadSlaTiming, SlaBand, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads, resolveSlaStage reachable from @sustentus/services/server (re-exported via sla/index.tsdb/services/index.ts).

Merge & deploy

  • merged: not yet — awaiting explicit human merge approval (gate:merge-approved).
  • deploy: pending merge.