sla-stage-targetsrun.md00_intake/stub.mdThe 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.status
workflow — e.g. BRD, Delivery, CSAT, Sign-off, Review), a target duration. Tenant-configurable;
ship sensible defaults.@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).sla.breach notification onto these targets (later concern).timeStuck since a blocker, lastEscalation) — those come
from work-blockers-escalations; this feature covers stage/SLA timing.status workflow displayNames, or introduce a
small stage-grouping map. The mock uses BRD / Delivery / CSAT / Sign-off / Review / Design.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.mdThe 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.
db/services/sla/stage-map.ts): a code-level table from lead workflow
status.name → SlaStage (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).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.timeInStageDays — business 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.remainingDays — targetDays − timeInStageDays (negative ⇒ past target), or null when no
target.overdue — true 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.)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.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.deriveLeadSlaTiming(tenantId, leadId) returns currentStatus, slaStage, timeInStageDays,
targetDays, remainingDays, overdue and band, all derived from statushistory +
sla-definition — none hard-coded.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.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).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.sla.breach notification onto these targets (later concern).timeStuck since a blocker, lastEscalation) — those come from
work-blockers-escalations.slaStage field on the status
model with seeding / a config surface) — the static map ships now; this is the future path.apps/dashboards/lib/mock/expert.ts — the dashboards app is
standalone mock data; consuming features swap to the engine when they are built.02_build/output/notes.mdfeat: sla-stage-targets — per-stage SLA timing enginepackages/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).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/index → server/index).deriveLeadSlaTiming returns currentStatus, slaStage, timeInStageDays, targetDays,
remainingDays, overdue, band — all derived from statushistory + sla-definition.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.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).sla-definition / slaConfigService.getSlaDefinitions.LeadSlaTiming, SlaBand, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads, resolveSlaStage
exported and reachable from @sustentus/services/server.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.mdNo 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.mdWe'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.
03_ship/output/pr.mdmain before ship)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).03_ship/output/investor-update.md; no end-user changelog entry (internal change), recorded in 03_ship/output/changelog.md./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):
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.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).deriveLeadSlaTimingForLeads separates the invalid-id pass from the compute pass (two short loops); the separation is readable and correct, and N is small.deriveLeadSlaTiming (single) doesn't wrap the batch function; the explicit single-lead path keeps the one-id query obvious.deriveLeadSlaTiming(tenantId, leadId) returns currentStatus, slaStage, timeInStageDays, targetDays, remainingDays, overdue, band — derived from statushistory + sla-definition (timing.ts buildTiming).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).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).slaConfigService.getSlaDefinitions / sla-definition.LeadSlaTiming, SlaBand, deriveLeadSlaTiming, deriveLeadSlaTimingForLeads, resolveSlaStage reachable from @sustentus/services/server (re-exported via sla/index.ts → db/services/index.ts).gate:merge-approved).