# sla — Planning

SLA duration settings, escalation rules, and conditional SLA overrides. Applied at either root (whole case) or stage level.

## When to Use

Pick this plugin whenever the sdd.md mentions deadlines, service-level agreements, time-to-complete expectations, or escalation notifications:

- "This case must resolve within 5 days"
- "Notify the manager when the SLA is at 80% risk"
- "If the case is flagged Urgent, use a 30-minute SLA"
- "Escalate to the group when the SLA breaches"

## Three Sub-Operations (one plugin, three workflows)

| Sub-op | Purpose |
|--------|---------|
| **Default SLA** | The time-based catch-all SLA. One per target (root or stage). Written into the SLA rules array with `expression: "=js:true"`. See [impl-json.md § Target resolution](impl-json.md) for the destination paths. |
| **Conditional SLA rules** | Expression-driven SLA overrides. Supported on root and stage targets. Prepended to the target's SLA rules array ahead of the default. |
| **Escalation rules** | Status markers and notifications triggered at-risk or on breach. Attached to a specific rule via `escalationRule[]`; their IDs may also drive an interrupting secondary-stage `sla-status-change` entry. |

## Applying SLA at Root vs Stage

- **Root** — the default SLA for the whole case. Target `"root"`; written to `metadata.slaRules[]`.
- **Stage** — stage-specific SLA. Target `"<stage-name>"`; written to the stage node's `data.slaRules[]`. Overrides the root default while the stage is active.

Set root SLA first, then stage SLAs. This mirrors the schema precedence: stage > root.

> **Conditional SLA rules use the same target scope as defaults.** Root rules live in `metadata.slaRules[]`; stage rules live in the stage node's `data.slaRules[]`. On either target, conditional entries precede the trailing `=js:true` default.

> **Secondary-stage SLA is supported.** Author it the same way as a regular Stage SLA — write `data.slaRules[]` on the `case-management:Stage` node (the secondary stage, i.e. `data.stageType: "secondary"`). See [`impl-json.md`](impl-json.md).

> **Per-conditional-rule escalations are supported.** Attach an escalation rule to any entry in `slaRules[]`, not only the default `"=js:true"` rule.

## Required Fields from sdd.md

### Default SLA

| Field | Source | Notes |
|-------|--------|-------|
| `count` | sdd.md duration number | Positive integer |
| `unit` | sdd.md duration unit | `min` \| `h` \| `d` \| `w` \| `m` |
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `display-name` | sdd.md `SLA Title` (§1 Case Metadata for root; `**SLA Title:**` under `#### Stage SLA`) or generated fallback | Required non-empty SLA title, unique within the target, and MUST NOT contain `:`. Carry the SDD title verbatim. If the SDD has no title, ask for one or use the deterministic fallback `SLA Rule {N}` and record it. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context for the target, duration, threshold, and escalation behavior. |

### Conditional SLA rule

| Field | Source | Notes |
|-------|--------|-------|
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `expression` | sdd.md condition | Natural-language in planning; the execution phase translates. **Do not fabricate syntax during planning.** |
| `count`, `unit` | sdd.md duration for this condition | Same units as default |
| `display-name` | sdd.md or generated fallback | Required non-empty unique title, no `:`; use `SLA Rule {N}` only when the author supplied no title. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context; not emitted into JSON. |

Rules are evaluated in insertion order — first truthy expression wins. The default SLA acts as the fallback.

### Escalation rule

| Field | Source | Notes |
|-------|--------|-------|
| `trigger-type` | sdd.md | `at-risk` \| `sla-breached` |
| `at-risk-percentage` | sdd.md | Required when `trigger-type: at-risk`; preserve the supplied value because FE validates presence (it does not enforce a numeric range here). |
| `recipient-scope` | sdd.md | `User` \| `UserGroup` |
| `recipient-target` | sdd.md → resolver | Recipient UUID. When sdd gives an email or group name, run [§ Identity Resolution](#identity-resolution) — resolved UUID written inline. On resolver failure or user decline, mark `<UNRESOLVED: user-uuid for <email>>` / `<UNRESOLVED: group-uuid for <name>>`. |
| `recipient-value` | sdd.md | Display value (typically the email for User, group name for UserGroup). |
| `display-name` | sdd.md escalation-table `Display Name` cell | Required non-empty escalation title, unique across the target, and MUST NOT contain `:`. Carry the SDD title verbatim. If omitted, use `Escalation Rule {N}` and record the fallback. |
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `attach-to` | sdd.md | `default` (attach to the `=js:true` rule) or `T<m>` pointing to the conditional-rule element the escalation fires under. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context. If this escalation enters a secondary stage, name that lane and why it is global/interrupting. |

## Identity Resolution

When sdd gives an escalation recipient as an email (`User: manager@corp.com`) or group name (`UserGroup: "Order Management Team"`), resolve to a directory UUID via `uip admin` while authoring the escalation rule. Resolved UUIDs land in `recipients-resolved.json`; [`impl-json.md`](impl-json.md) writes them straight into `escalationRule[].action.recipients[].target` — no sentinel needed. Resolution runs **Phase 1 only** — the design lane records email / group name as a string in sdd.md.

### Skip — UUID pass-through

Recipient value already matches `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` → skip CLI. Write `<uuid> / <uuid>` in `recipients-resolved.json`. Audit rationale: `uuid-pass-through`.

### Resolve `User`

1. `uip admin users list --search "<email>" --output json`
2. **Auto-accept** when response has exactly 1 entry AND `entry.email` equals sdd's email case-insensitively. Write `User: <id> / <email>`. Rationale: `auto-exact-email`.
3. **Fallback** on any other shape (0, >1, partial). If sdd carries a display name, retry `--search "<display-name>"`. Merge results, dedupe by `id`.
4. **Ask** via AskUserQuestion with up to 3 candidates. Each option: label `<displayName>`, description `<email> · id=<uuid-first-8>...`. Final option: `Keep as <UNRESOLVED>`. Rationale: `user-picked-from-N` or `user-declined-keep-unresolved`.

### Resolve `UserGroup`

`uip admin groups list` has **no `--search` flag**. Filter client-side.

1. **Pull once.** First group lookup in the planning session: `uip admin groups list --output json`. Cache the array in memory for the rest of Phase 1.
2. **Exact match** — entries where `name` OR `displayName` equals sdd's group name case-insensitively. Exactly 1 match → write `UserGroup: <id> / "<group-name>"`. Rationale: `auto-exact-name`.
3. **Substring fallback** — case-insensitive substring on `name` / `displayName`. Any hits → AskUserQuestion with top 3 (alphabetical by `name`) + `Keep as <UNRESOLVED>`.
4. **Empty** — both filters return 0 → AskUserQuestion: `No matching group found for "<group-name>". Keep as <UNRESOLVED>?` with a single `Keep as <UNRESOLVED>` option. Do NOT fabricate "fuzzy candidates"; the user patches the UUID externally per the standard decline path. Rationale: `user-declined-keep-unresolved`.

### Session cache

In-memory, scoped to the Phase 1 run. Key: lowercased sdd input. **Positive resolutions only** — auto-accept results and user-picked UUIDs. Do NOT cache `Keep as <UNRESOLVED>` decisions; same recipient appearing in a later element re-asks.

### CLI failure (auth / network / 403)

Non-zero exit from `uip admin ...` → AskUserQuestion:

```
Question: Identity resolution failed (<stderr first line>). How should we proceed?
Header:   Resolver failed
Options:
  - Retry (max 2 attempts)
      → re-run the same `uip admin ...`. Continue on success. After 2 failed retries the resolver auto-routes to "Skip resolution for this build" — do not loop further.
  - Skip resolution for this build
      → leave every recipient as <UNRESOLVED: ...>, log to tasks/build-issues.md, surface in completion report. Subsequent recipient lookups in this Phase 1 skip the CLI.
  - Abort planning
      → halt Phase 1.
```

### Audit — `tasks/recipients-resolved.json`

Append one object per resolution attempt (incremental Edit, mirroring `registry-resolved.json` discipline):

```json
{
  "sddInput": "manager@corp.com",
  "kind": "user",
  "searchTerm": "manager@corp.com",
  "allCandidates": [
    {"id": "a1b2c3d4-0000-0000-0000-000000000000", "email": "manager@corp.com", "displayName": "Anne Manager"}
  ],
  "selected": "a1b2c3d4-0000-0000-0000-000000000000",
  "rationale": "auto-exact-email"
}
```

Rationale values: `auto-exact-email`, `auto-exact-name`, `user-picked-from-N`, `user-declined-keep-unresolved`, `uuid-pass-through`, `cli-failed-skipped`.

## Ordering

SLA is the **last** element class written, after conditions. For each target, order within the target:

1. Default SLA element
2. Conditional SLA rule elements for that target
3. Escalation rule elements (one per rule)

## Fields to Resolve

SLA rules and escalations produce **no `tasks/registry-resolved.json` entry** — that ledger holds registry lookups. Resolved recipient identities go to `tasks/recipients-resolved.json` instead; everything below is a reasoning field Phase 2 reads from `sdd.md` ([planning.md § Step 4](../../planning.md)).

### Default SLA

```text
default SLA on "<target>" — <duration>
- target: "<root>" | "<stage-name>"
- display-name: "SLA Rule 1"              # required; use authored title or SLA Rule {N}
- rationale: "<why this SLA target and duration fit>"
- count: 5
- unit: d
- verify: Confirm Result: Success
```

### Conditional SLA rule

```text
conditional SLA rule on "<target>" — <condition summary>
- target: "root" | "<stage-name>"
- display-name: "Urgent SLA"              # required; target-unique, no ':'
- rationale: "<why this condition changes the SLA>"
- condition: "<natural-language condition from sdd.md>"
- count: 30
- unit: min
- verify: Confirm Result: Success
```

### Escalation rule

```text
escalation rule on "<target>" — <trigger summary>
- target: "<root>" | "<stage-name>"
- attach-to: default | T<m>
- rationale: "<why this threshold/recipient/action fits; name any interrupting secondary stage>"
- trigger-type: at-risk
- at-risk-percentage: 80
- recipients:
  - User: a1b2c3d4-0000-0000-0000-000000000000 / manager@corp.com
  - UserGroup: <UNRESOLVED: group-uuid for "Order Management Team"> / "Order Management Team"
- display-name: "Notify Manager"
- verify: Confirm Result: Success, capture EscalationRuleId
```

**Recipient format:** `<target> / <value>` where `<target>` is the resolved UUID (per [§ Identity Resolution](#identity-resolution)) — or `<UNRESOLVED: …>` sentinel when the resolver failed or the user declined — and `<value>` is the display string. Unresolved recipients survive into execution; the user patches the UUID externally after the build and the completion report lists every unresolved recipient.

**`attach-to: default`** is the default. Use `T<m>` when sdd.md attaches an escalation to a specific conditional SLA rule.

## Frontend validation parity

Before emitting SLA elements, reject or repair the same cases the Case App rejects:

- every SLA rule has a non-empty, target-unique `display-name`;
- every escalation has a non-empty, target-unique `display-name`;
- every SLA `count` is positive, and minute-based values are between 15 and 1000 inclusive;
- every non-default rule has a condition/expression;
- every escalation has at least one recipient, and every `at-risk` escalation carries `at-risk-percentage`.

## Anti-Patterns

- **Do not fabricate expression syntax.** Describe conditional SLA rules in natural language during planning; the execution phase handles the exact syntax.
- **Do not lose the conditional rule's target.** Root and stage rules have the same entry shape but different destinations (`metadata.slaRules[]` vs `node.data.slaRules[]`). Preserve the SDD row's target through to the caseplan write.
- **Do not invert rule order.** Conditional rules are evaluated in insertion order — insert them in the priority order the sdd.md specifies.
- **Do not skip the resolver to save a CLI call.** Email / group-name recipients MUST go through [§ Identity Resolution](#identity-resolution). Writing `<UNRESOLVED: ...>` directly without attempting `uip admin users/groups list` is a planning bug.
- **Do not fabricate UUIDs.** When the resolver returns 0 / multi / partial matches, AskUserQuestion or keep `<UNRESOLVED>` — never guess a UUID, never auto-pick the first candidate without the exact-email / exact-name gate.
- **Do not cache user declines.** Session cache holds positive resolutions only. Re-ask on each occurrence of the same unresolved recipient.

<!-- END: planning.md -->
