Phase 8 of the roadmap: SLA policy engine (most-specific-match, calendar- aware due dates), durable pause/resume and breach detection (never an in-memory timer), and a rule-driven escalation engine that re-assigns via 007's engine scoped to a specific target node. Bounded to the two SLA-derived trigger types this feature can compute a real signal for; the other eight doc 05 §6 trigger types remain valid rule configuration without a wired event source yet. First feature to give 006's HierarchyNode.slaPolicyId/escalationPolicyId fields a real target. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
309 lines
18 KiB
Markdown
309 lines
18 KiB
Markdown
# Feature Specification: SLA and Escalation
|
|
|
|
**Feature Branch**: `008-sla-escalation`
|
|
|
|
**Created**: 2026-09-03
|
|
|
|
**Status**: Draft
|
|
|
|
**Input**: User description: "Phase 8 of docs/10-implementation-roadmap.md: SLA policy engine,
|
|
business calendar/holiday support, durable pause/resume via BullMQ, rule-driven escalation
|
|
engine, escalation event audit. Per docs/05-orchestration-sla-escalation.md §5-6 and
|
|
docs/06-database-schema.md 'Domain: SLA' / 'Domain: Escalation'."
|
|
|
|
## User Scenarios & Testing *(mandatory)*
|
|
|
|
### User Story 1 - An admin defines SLA policies as configuration (Priority: P1)
|
|
|
|
An administrator defines an SLA policy — first-response/investigation/resolution/customer-
|
|
response time limits — optionally scoped to a product, category, problem type, and/or priority,
|
|
and tied to a business calendar. Nothing about SLA thresholds is hardcoded anywhere in the
|
|
system.
|
|
|
|
**Why this priority**: Every later capability in this feature reads a policy that has to exist
|
|
first.
|
|
|
|
**Independent Test**: Create an SLA policy with a resolution limit; confirm it's retrievable and
|
|
its fields are stored exactly as given.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** an admin creates an SLA policy with a resolution time limit, **When** it's saved,
|
|
**Then** it's retrievable with every field exactly as given, `active: true` by default.
|
|
2. **Given** a policy scoped to a product/category/problem type/priority, **When** two policies
|
|
could both apply to the same ticket, **Then** the more specific one is preferred — same
|
|
most-specific-match convention this system already uses for confidence policy (005) and
|
|
capability scope (006).
|
|
3. **Given** an admin creates a business calendar with working hours and holidays, **When** it's
|
|
referenced by a policy, **Then** due-date calculations for tickets under that policy use it.
|
|
|
|
---
|
|
|
|
### User Story 2 - An SLA run starts automatically when a ticket is assigned, with calendar-aware due dates (Priority: P1)
|
|
|
|
When orchestration (007) assigns a ticket, an SLA run starts for it automatically, with due dates
|
|
computed against the applicable policy's business calendar — never a naive `createdAt + N hours`
|
|
that ignores weekends, holidays, or working hours.
|
|
|
|
**Why this priority**: Nothing else in this feature — breach detection, pause/resume, escalation
|
|
— has anything to act on until a real, calendar-correct due date exists.
|
|
|
|
**Independent Test**: Assign a ticket under a policy with a resolution limit and a calendar whose
|
|
working hours exclude a weekend; confirm the computed due date skips the excluded time rather
|
|
than counting straight through it.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a ticket is assigned (007), **When** an applicable SLA policy resolves for it (User
|
|
Story 1's most-specific-match rule), **Then** an SLA run starts with a resolution due date
|
|
computed against that policy's calendar.
|
|
2. **Given** no policy matches, **When** a ticket is assigned, **Then** no SLA run is created
|
|
rather than applying an arbitrary default — this feature does not invent a policy that was
|
|
never configured.
|
|
3. **Given** a calendar with defined working hours and a holiday, **When** a due date is
|
|
computed, **Then** time outside working hours and on holidays is excluded from the countdown.
|
|
|
|
---
|
|
|
|
### User Story 3 - SLA pause and resume are durable, not in-memory (Priority: P1)
|
|
|
|
When a ticket's status indicates it's waiting on the customer, its SLA run pauses — the clock
|
|
stops counting against the agent. When it resumes, the clock continues from where it left off,
|
|
never from zero and never having kept counting while paused. This state survives a process
|
|
restart.
|
|
|
|
**Why this priority**: Constitution Principle VII names this exact scenario — an SLA that keeps
|
|
counting during a customer-caused delay, or that resets on a restart, actively misrepresents
|
|
whether a real commitment was honored.
|
|
|
|
**Independent Test**: Start an SLA run, pause it, wait, resume it; confirm the resulting due date
|
|
reflects the paused duration being excluded, not counted twice.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a running SLA, **When** the ticket transitions to a waiting-for-customer state,
|
|
**Then** the run's status becomes `paused` and its due date stops approaching.
|
|
2. **Given** a paused SLA, **When** the ticket transitions back to in-progress, **Then** the run's
|
|
status becomes `running` again and the remaining time is preserved from the pause point.
|
|
3. **Given** a paused or running SLA run, **When** the process restarts, **Then** its state is
|
|
unchanged on restart — pause/resume state is never held only in memory.
|
|
|
|
---
|
|
|
|
### User Story 4 - A breach is detected durably, never silently missed (Priority: P2)
|
|
|
|
When a running SLA's due date passes without the corresponding milestone happening, the run is
|
|
marked breached — detected by a durable background job, not a timer that only fires if the
|
|
process happens to still be running at the right moment.
|
|
|
|
**Why this priority**: Depends on User Stories 2-3 (a real due date, correctly paused/resumed)
|
|
existing first. A breach that's never detected is worse than no SLA at all — it's a false sense
|
|
of a commitment being tracked.
|
|
|
|
**Independent Test**: Start an SLA run with a very short resolution limit; confirm it's marked
|
|
`breached` once the due date passes, even simulating the checking job running in a separate
|
|
process invocation from the one that started the run.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a running SLA run whose due date has passed, **When** the breach-detection job next
|
|
runs, **Then** the run's status becomes `breached` and its `breachedAt` timestamp is recorded.
|
|
2. **Given** an SLA run that completes (its milestone actually happens) before its due date,
|
|
**When** breach detection later runs, **Then** it is not marked breached — completion is
|
|
checked against the actual event, not assumed from elapsed time alone.
|
|
3. **Given** a paused SLA run, **When** breach detection runs while it's paused, **Then** it is
|
|
never marked breached — a paused clock cannot breach.
|
|
|
|
---
|
|
|
|
### User Story 5 - A breach automatically triggers rule-driven escalation (Priority: P2)
|
|
|
|
An administrator defines escalation rules — trigger condition, target support-hierarchy node, who
|
|
to notify. When an SLA breach (or another configured trigger) occurs, the matching rule fires
|
|
automatically: the ticket moves toward the rule's target node and orchestration (007)
|
|
re-assigns it there, and an escalation event is durably recorded. This is never a hardcoded
|
|
`if L1 then L2` — it's evaluated against configured rules.
|
|
|
|
**Why this priority**: Depends on User Story 4 (a real breach signal to trigger on). This is the
|
|
other half of "durable SLA enforcement actually matters" — detecting a breach that nobody acts on
|
|
isn't meaningfully different from not detecting it.
|
|
|
|
**Independent Test**: Configure an escalation rule for `resolution_breach` targeting a specific
|
|
hierarchy node; breach an SLA run; confirm an escalation event is recorded, the ticket is
|
|
re-assigned via 007 scoped to that specific node, and the rule's configured notification target
|
|
is recorded (not necessarily delivered — see Assumptions).
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** an active escalation rule for `resolution_breach` scoped to a target node, **When**
|
|
a matching SLA run breaches, **Then** an `EscalationEvent` is recorded with the rule, the
|
|
reason, and `triggeredBy: system`.
|
|
2. **Given** an escalation event fires, **When** it completes, **Then** 007's assignment engine
|
|
re-runs scoped specifically to the rule's target node — not a fresh, unscoped resolution —
|
|
and the ticket's assignment history (007) reflects the new assignment with `strategy` reused
|
|
from whatever the target node itself configures.
|
|
3. **Given** no escalation rule matches a breach, **When** the breach is detected, **Then** the
|
|
SLA run is still marked breached (User Story 4) — the absence of a matching rule doesn't
|
|
suppress breach detection, it only means no automatic escalation follows.
|
|
4. **Given** multiple active rules could match the same trigger, **When** more than one does,
|
|
**Then** every matching rule fires its own escalation event — this feature does not pick just
|
|
one.
|
|
|
|
---
|
|
|
|
### User Story 6 - A human can manually trigger escalation, audited the same way (Priority: P3)
|
|
|
|
An agent or admin can explicitly escalate a ticket to a specific target node, for a stated
|
|
reason, without waiting for an automatic trigger — recorded through the same `EscalationEvent`
|
|
audit trail as an automatic one.
|
|
|
|
**Why this priority**: Depends on User Story 5's event/re-assignment mechanism already existing.
|
|
Automatic triggers won't cover every real reason to escalate (doc 05 §10 names several this
|
|
feature doesn't compute automatically — see Assumptions); a human needs an explicit path that
|
|
still produces the same durable record.
|
|
|
|
**Independent Test**: Manually escalate a ticket to a named target node with a reason; confirm an
|
|
`EscalationEvent` with `triggeredBy` set to the calling actor is recorded and the ticket is
|
|
re-assigned via 007 to that node.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** an admin manually escalates a ticket to a target node, **When** it completes,
|
|
**Then** an `EscalationEvent` is recorded with `triggeredBy` set to the actor (never
|
|
`system`), and 007 re-assigns the ticket scoped to that node.
|
|
2. **Given** a manual escalation targets a node that doesn't exist, **When** it's attempted,
|
|
**Then** it's rejected — never a dangling escalation event pointing nowhere.
|
|
|
|
---
|
|
|
|
### Edge Cases
|
|
|
|
- What happens if a ticket has no assigned agent yet when its SLA would otherwise start? An SLA
|
|
run only starts on a successful assignment (User Story 2) — an unassigned ticket (007's "no
|
|
eligible agent" outcome) has no SLA run to track, consistent with there being no one yet to
|
|
hold to a commitment.
|
|
- What happens if a business calendar has no working hours configured at all for a given day?
|
|
That day contributes zero time toward any due-date countdown — an unconfigured day is never
|
|
silently treated as 24 available hours.
|
|
- What happens if an SLA policy's calendar is deleted or unreferenced after runs already exist
|
|
against it? Out of scope — this feature does not implement calendar deletion, only creation and
|
|
the `active` state every other configuration entity in this system already uses.
|
|
- What happens when a ticket is reopened after being resolved, with a completed SLA run already
|
|
on record? Out of scope for this feature to define a new run automatically — reopening (a
|
|
future problem-resolution-phase concept) may need its own SLA-run-restart decision; this
|
|
feature's SLA runs are 1:1 with a ticket's first, straightforward assignment→resolution
|
|
lifecycle.
|
|
- What happens if two escalation rules would move a ticket to the same target node at once (a
|
|
race between an automatic breach and a simultaneous manual escalation)? Both `EscalationEvent`
|
|
rows are recorded (never lost — durable audit is unconditional); 007's own re-assignment
|
|
path already handles a ticket being assigned twice in quick succession correctly (it's the same
|
|
version-row-per-period `Assignment` mechanism 007 already built and tested for concurrent
|
|
writes), so no new concurrency mechanism is needed here.
|
|
|
|
## Requirements *(mandatory)*
|
|
|
|
### Functional Requirements
|
|
|
|
- **FR-001**: The system MUST let an admin create an SLA policy with first-response/
|
|
investigation/resolution/customer-response time limits, optionally scoped to product/category/
|
|
problem type/priority, referencing a business calendar.
|
|
- **FR-002**: When multiple SLA policies could apply to the same ticket context, the system MUST
|
|
prefer the most specific match — never an arbitrary or first-found selection.
|
|
- **FR-003**: The system MUST let an admin create a business calendar with working hours and
|
|
holidays.
|
|
- **FR-004**: Due-date computation MUST exclude time outside a calendar's working hours and on
|
|
its holidays — MUST NOT compute a due date as a naive elapsed-time addition that ignores the
|
|
calendar.
|
|
- **FR-005**: An SLA run MUST start automatically when a ticket is successfully assigned (007),
|
|
using the most-specific-matching policy (FR-002); when no policy matches, no run is created.
|
|
- **FR-006**: An SLA run's status MUST be one of `running`, `paused`, `warning`, `breached`, or
|
|
`completed`, matching doc 06's defined set.
|
|
- **FR-007**: When a ticket transitions to a waiting-for-customer state, its SLA run MUST pause;
|
|
when it transitions back, the run MUST resume with its remaining time preserved — never reset
|
|
to the full original duration and never having continued counting while paused.
|
|
- **FR-008**: SLA pause/resume state and due dates MUST be durable — recomputable and correct
|
|
after a process restart, never dependent on an in-memory timer (Constitution Principle VII).
|
|
- **FR-009**: A running SLA run whose due date has passed MUST be detected and marked `breached`
|
|
by a durable background job — never missed because the triggering process wasn't running at
|
|
the exact due moment.
|
|
- **FR-010**: An SLA run that completes its milestone before its due date MUST NOT be marked
|
|
breached, regardless of what a naive elapsed-time check alone would suggest.
|
|
- **FR-011**: A paused SLA run MUST NOT be marked breached while paused.
|
|
- **FR-012**: The system MUST let an admin create an escalation rule — trigger type, condition,
|
|
target hierarchy node, and who to notify — scoped to a policy, matching doc 06's
|
|
`EscalationRule` shape.
|
|
- **FR-013**: An SLA breach (FR-009) MUST be evaluated against every active escalation rule
|
|
configured for `resolution_breach` (and, where applicable, the other SLA-derived trigger
|
|
types this feature computes — see Assumptions); every matching rule MUST fire its own
|
|
escalation event — never just the first match.
|
|
- **FR-014**: Firing an escalation rule MUST record a durable `EscalationEvent` (rule, reason,
|
|
`triggeredBy`, timestamp) and MUST re-run 007's assignment engine scoped specifically to the
|
|
rule's `targetNodeId` — never a fresh, unscoped resolution that could land elsewhere.
|
|
- **FR-015**: A breach with no matching escalation rule MUST still be recorded as breached
|
|
(FR-009) — the absence of a rule never suppresses breach detection itself.
|
|
- **FR-016**: The system MUST let an admin or agent manually escalate a ticket to a specific,
|
|
existing target node with a reason, recorded through the same `EscalationEvent` mechanism as an
|
|
automatic escalation, with `triggeredBy` set to the calling actor.
|
|
- **FR-017**: A manual escalation targeting a nonexistent hierarchy node MUST be rejected.
|
|
|
|
### Key Entities
|
|
|
|
- **SLA Policy**: A configured set of time limits (first response/investigation/resolution/
|
|
customer response) scoped to product/category/problem type/priority, referencing a business
|
|
calendar — never a hardcoded threshold.
|
|
- **SLA Run**: The durable, per-ticket tracking of one policy's due dates and status against a
|
|
real ticket, survivable across a process restart.
|
|
- **Business Calendar / Holiday**: Working hours and excluded dates a due-date calculation
|
|
respects — the mechanism that keeps SLA math honest.
|
|
- **Escalation Policy / Rule**: Configured trigger conditions and target hierarchy nodes — never
|
|
a hardcoded escalation ladder.
|
|
- **Escalation Event**: The durable, audited record of every escalation, automatic or manual,
|
|
including which rule (if any) fired it and who/what triggered it.
|
|
|
|
## Success Criteria *(mandatory)*
|
|
|
|
### Measurable Outcomes
|
|
|
|
- **SC-001**: 100% of due-date calculations under a policy with a configured calendar exclude
|
|
non-working time — verified against a calendar with at least one full excluded day.
|
|
- **SC-002**: 100% of SLA runs correctly reflect pause/resume across a simulated process restart
|
|
— the paused duration is never double-counted and never dropped.
|
|
- **SC-003**: 100% of SLA runs whose due date has passed are marked `breached` within one
|
|
breach-detection job cycle, even when the detecting process is a different invocation than the
|
|
one that started the run.
|
|
- **SC-004**: 100% of SLA breaches with a matching active escalation rule produce both an
|
|
`EscalationEvent` and a re-assignment scoped to the rule's target node.
|
|
- **SC-005**: 100% of manual escalations targeting a nonexistent node are rejected, never
|
|
producing a dangling event.
|
|
|
|
## Assumptions
|
|
|
|
- **This feature does not build a notification-delivery mechanism** — an escalation rule's
|
|
`notify` field (doc 06) is stored and returned as configured, but this feature does not send an
|
|
email/Slack/webhook notification; `platform/notifications` (doc 07) remains an untouched module
|
|
group, same convention as every prior feature leaving an adjacent, not-yet-built module alone.
|
|
- **Only `resolution_breach` and `first_response_breach` are wired to a real trigger signal in
|
|
this feature** — doc 05 §6 lists ten trigger types; the other eight (inactivity, priority
|
|
increase, customer escalation request, repeated reopen, manual [built as its own user story,
|
|
User Story 6, not a rule trigger], product defect, dependency timeout, critical incident)
|
|
require signals this codebase doesn't compute yet (idle-time tracking, reopen counting,
|
|
explicit defect/incident flagging) — `EscalationRule.triggerType` accepts any of doc 06's
|
|
values as configuration data, but only the two SLA-breach types are ever actually evaluated by
|
|
this feature. This mirrors 006's `assignmentStrategy`/`slaPolicyId` fields being stored as real
|
|
data before this feature gave them a real consumer.
|
|
- **Investigation SLA and customer-response SLA are stored as policy fields (FR-001) but this
|
|
feature only computes/tracks the resolution and first-response due dates on `SLARun`** — doc 06's
|
|
`SLARun` itself only models `firstResponseDueAt`/`resolutionDueAt` explicitly; investigation and
|
|
customer-response timers would need their own due-date fields doc 06 doesn't define, which is a
|
|
refinement left for whichever future phase actually needs to enforce them (matching this
|
|
system's "refine the conceptual schema when a feature needs the refinement, not speculatively"
|
|
convention).
|
|
- **SLA run creation happens once, on a ticket's first successful assignment** — reopening,
|
|
multiple resolution cycles, and re-running an SLA clock for a reassigned-after-resolution
|
|
ticket are out of scope (Edge Cases) — that's future problem-resolution-phase territory.
|
|
- **Breach detection runs on a durable, periodically-scheduled BullMQ job** (a repeatable job,
|
|
not a per-run delayed job scheduled at creation time) — checking every active `running` run's
|
|
due date against the current time on each tick, rather than scheduling one delayed job per SLA
|
|
run. This is a deliberate simplicity/robustness tradeoff, not an aspiration to replace later —
|
|
see research.md for the full reasoning.
|