# Support ticket triage workflow blueprint

```yaml
name: support-ticket-triage
version: 1.0.0
trigger:
  events: [ticket.created, customer.reply_material]
idempotency_key: channel + source_message_id + triage_policy_version
input:
  required: [ticket_id, source_message_id, original_reference, channel, policy_version]
  optional: [authenticated_account_id, locale, attachments, related_ticket_ids]
output:
  required: [triage_id, intent, product_area, language, priority, exception_state, provenance, versions]
side_effects:
  allowed: [write_triage_record, emit_triage_event, open_named_exception]
  forbidden: [send_customer_reply, merge_ticket, close_ticket, restrict_account, issue_refund]
```

## States

`registered -> normalized -> identity_resolved -> duplicate_checked -> classified -> rules_checked -> accepted|review_required -> committed`

Terminal exception states: `identity_resolution`, `possible_duplicate`, `multi_issue_split`, `protected_urgent_review`, `category_owner_review`, `triage_system_recovery`.

## Step contracts

| Step | Type | Reads | Writes | Retry rule |
|---|---|---|---|---|
| register | deterministic | source event | immutable intake | create-if-absent |
| identity | deterministic | trusted identity service | account IDs or exception | bounded retry, no broad search |
| duplicate | deterministic + retrieval | exact keys, bounded candidates | candidate references | read-only replay |
| classify | model | approved derived text, taxonomy | labels, confidence, spans | pin version; no side effect |
| priority | deterministic | verified impact/scope, matrix | priority, fired rules | pure function |
| gate | deterministic | proposed record, thresholds | accept or named exception | pure function |
| review | human | original, proposal, evidence, failed rule | correction event | never auto-repeat approval |
| commit | deterministic | accepted fields and versions | atomic triage record/event | compare-and-set triage ID |

## Invariants

1. Exactly one committed record per idempotency key and version.
2. Every model field has confidence and evidence spans.
3. Sentiment cannot independently increase priority.
4. Similarity cannot merge or close tickets.
5. Protected signals never enter ordinary auto-accept.
6. Every correction preserves proposed and final values.

## Events

- `triage.registered`
- `triage.classification_proposed`
- `triage.exception_opened`
- `triage.correction_recorded`
- `triage.completed`
- `triage.failed`

Each event includes `triage_id`, `ticket_id`, `occurred_at`, `policy_version`, `taxonomy_version`, `classifier_version`, `actor`, and `correlation_id`.

## Acceptance tests

1. Replayed source message creates no duplicate record.
2. Missing account enters identity resolution without retrieving account data.
3. Exact source/thread duplicate is linked deterministically.
4. Similar text produces candidates but no automatic merge.
5. Two intents in one message enter split review.
6. Low-confidence intent enters category review.
7. Security phrase enters protected review regardless of general confidence.
8. Negative sentiment alone does not set P1.
9. Validation conflict blocks commit.
10. Reviewer correction preserves the proposed label.
11. Classifier timeout can replay without duplicate side effects.
12. Atomic write failure emits no completion event.
13. Taxonomy change creates a new version rather than rewriting history.
14. Logs contain references and decisions but no credentials.
