Schema docs (docs/schema) cover tool mapping, input/output contracts and endpoint coverage for all 31 MCP tools; megaplan M1 phases 01-03 applied (namespace rename to read_*/write_*, input schema rectification). Go module provides config loading, Ombi client, and tool registry skeleton for implementation.
166 lines
8.0 KiB
Markdown
166 lines
8.0 KiB
Markdown
# Phase 04 — Semantic Abstraction & Enum Translation Layer
|
||
|
||
## Objective
|
||
|
||
Define the bidirectional string↔integer contract between agent-facing enums and Ombi wire constants, replacing the "numeric codes retained pending verification" stance for the four mapped enums.
|
||
|
||
## Prerequisites
|
||
|
||
- Phase 01 complete (tool names are `read_*`/`write_*`).
|
||
- Consumers of this contract: Phase 03 (`write_issue_create`, `write_request_reprocess`, `read_issues`, `write_issue_manage`, `write_settings_patch` string enums), Phase 05 (`issue`/`retry` output defs), Phase 06 (routing rule #4 rewrite and verification-gap rows).
|
||
- Documents mutated: `docs/schema/02-tool-mapping.md` (routing rule #4), `docs/schema/06-verification.md` (enum-labels gap row). The Go package below is the baseline for `internal/translate/enums.go`, published via `docs/schema/08-go-baseline.md` in Phase 06.
|
||
|
||
## Specification
|
||
|
||
### T1 — `RequestType` (upstream `Ombi.Store.Entities.RequestType`, `enum:[0,1,2]`)
|
||
|
||
**Critical**: upstream order is `TvShow=0, Movie=1, Album=2` — NOT positional. Verified against upstream source; still label **Verify** per-instance for version drift.
|
||
|
||
| MCP string | Upstream int | Used by |
|
||
|---|---|---|
|
||
| `movie` | 1 | `write_issue_create.request_type`, `write_request_reprocess.request_type`, issue/retry outputs |
|
||
| `tv` | 0 | same |
|
||
| `album` | 2 | same |
|
||
|
||
### T2 — `IssueStatus` (upstream `enum:[0,1,2,3]`; frontend confirms Pending=0, InProgress=1, Resolved=2; `closed`→3)
|
||
|
||
| MCP string | Upstream int | Used by |
|
||
|---|---|---|
|
||
| `pending` | 0 | `read_issues` list/summary `status`, `write_issue_manage.set_status.status`, issue output `status` |
|
||
| `in_progress` | 1 | same |
|
||
| `resolved` | 2 | same |
|
||
| `closed` | 3 | same |
|
||
|
||
### T3 — `NotificationAgent` (upstream `enum:[0..11]`, labels from upstream source)
|
||
|
||
| MCP string | Int | | MCP string | Int |
|
||
|---|---|---|---|---|
|
||
| `email` | 0 | | `mobile` | 7 |
|
||
| `discord` | 1 | | `gotify` | 8 |
|
||
| `pushbullet` | 2 | | `webhook` | 9 |
|
||
| `pushover` | 3 | | `whatsapp` | 10 |
|
||
| `telegram` | 4 | | `ntfy` | 11 |
|
||
| `slack` | 5 | | | |
|
||
| `mattermost` | 6 | | | |
|
||
|
||
### T4 — `NotificationType` (upstream `enum:[0..16]`, labels from upstream source)
|
||
|
||
| MCP string | Int | | MCP string | Int |
|
||
|---|---|---|---|---|
|
||
| `new_request` | 0 | | `issue_resolved` | 9 |
|
||
| `issue` | 1 | | `issue_comment` | 10 |
|
||
| `request_available` | 2 | | `newsletter` | 11 |
|
||
| `request_approved` | 3 | | `partially_available` | 12 |
|
||
| `admin_note` | 4 | | `plex_watchlist_token_expired` | 13 |
|
||
| `test` | 5 | | `request_deleted` | 14 |
|
||
| `request_declined` | 6 | | `issue_in_progress` | 15 |
|
||
| `item_added_to_fault_queue` | 7 | | `issue_deleted` | 16 |
|
||
| `welcome_email` | 8 | | | |
|
||
|
||
### Enums deliberately NOT abstracted (stay numeric, **Verify**)
|
||
|
||
`VoteType [0,1]` (labels unverified), `RequestSource`, `RequestLimitType [0,1,2]`, legacy `orderType`/`statusType`/`availabilityType` filter ints. Documented in 06-verification.md as remaining gaps.
|
||
|
||
### Read-side fallback contract (graceful degradation)
|
||
|
||
For every mapped enum in an upstream response:
|
||
- mapped int → emit string label in the `*_label` field name (`status`, `request_type`, `agent`, `notification_type`) **and** the raw int in its `*_code` twin (`status_code`, `request_type_code`, `agent_code`, `notification_type_code`) — preserves provenance;
|
||
- unmapped int → omit the label field, emit only `*_code` int, append warning `unmapped upstream enum value <n> for <field>` to `warnings[]`.
|
||
|
||
Input side: label fields are the only accepted form (`*_code` rejected via `additionalProperties:false`); unknown label → `INVALID_ARGUMENT`.
|
||
|
||
### Go translation package (baseline for `internal/translate/enums.go` — printed in full)
|
||
|
||
```go
|
||
// Package translate maps agent-facing enum labels to upstream Ombi
|
||
// integer constants. All maps are bidirectional.
|
||
package translate
|
||
|
||
import "fmt"
|
||
|
||
var requestTypeToWire = map[string]int{"movie": 1, "tv": 0, "album": 2}
|
||
var requestTypeFromWire = map[int]string{0: "tv", 1: "movie", 2: "album"}
|
||
|
||
var issueStatusToWire = map[string]int{
|
||
"pending": 0, "in_progress": 1, "resolved": 2, "closed": 3,
|
||
}
|
||
var issueStatusFromWire = map[int]string{
|
||
0: "pending", 1: "in_progress", 2: "resolved", 3: "closed",
|
||
}
|
||
|
||
var notificationAgentToWire = map[string]int{
|
||
"email": 0, "discord": 1, "pushbullet": 2, "pushover": 3,
|
||
"telegram": 4, "slack": 5, "mattermost": 6, "mobile": 7,
|
||
"gotify": 8, "webhook": 9, "whatsapp": 10, "ntfy": 11,
|
||
}
|
||
var notificationAgentFromWire = map[int]string{
|
||
0: "email", 1: "discord", 2: "pushbullet", 3: "pushover",
|
||
4: "telegram", 5: "slack", 6: "mattermost", 7: "mobile",
|
||
8: "gotify", 9: "webhook", 10: "whatsapp", 11: "ntfy",
|
||
}
|
||
|
||
var notificationTypeToWire = map[string]int{
|
||
"new_request": 0, "issue": 1, "request_available": 2,
|
||
"request_approved": 3, "admin_note": 4, "test": 5,
|
||
"request_declined": 6, "item_added_to_fault_queue": 7,
|
||
"welcome_email": 8, "issue_resolved": 9, "issue_comment": 10,
|
||
"newsletter": 11, "partially_available": 12,
|
||
"plex_watchlist_token_expired": 13, "request_deleted": 14,
|
||
"issue_in_progress": 15, "issue_deleted": 16,
|
||
}
|
||
var notificationTypeFromWire = map[int]string{
|
||
0: "new_request", 1: "issue", 2: "request_available",
|
||
3: "request_approved", 4: "admin_note", 5: "test",
|
||
6: "request_declined", 7: "item_added_to_fault_queue",
|
||
8: "welcome_email", 9: "issue_resolved", 10: "issue_comment",
|
||
11: "newsletter", 12: "partially_available",
|
||
13: "plex_watchlist_token_expired", 14: "request_deleted",
|
||
15: "issue_in_progress", 16: "issue_deleted",
|
||
}
|
||
|
||
// ErrUnknownLabel is returned for an out-of-enum agent label.
|
||
var ErrUnknownLabel = fmt.Errorf("unknown enum label")
|
||
|
||
func RequestTypeToWire(s string) (int, error) { return lookup(requestTypeToWire, s) }
|
||
func IssueStatusToWire(s string) (int, error) { return lookup(issueStatusToWire, s) }
|
||
func AgentToWire(s string) (int, error) { return lookup(notificationAgentToWire, s) }
|
||
func NotifTypeToWire(s string) (int, error) { return lookup(notificationTypeToWire, s) }
|
||
|
||
func RequestTypeFromWire(i int) (string, bool) { return lookupRev(requestTypeFromWire, i) }
|
||
func IssueStatusFromWire(i int) (string, bool) { return lookupRev(issueStatusFromWire, i) }
|
||
func AgentFromWire(i int) (string, bool) { return lookupRev(notificationAgentFromWire, i) }
|
||
func NotifTypeFromWire(i int) (string, bool) { return lookupRev(notificationTypeFromWire, i) }
|
||
|
||
func lookup(m map[string]int, s string) (int, error) {
|
||
v, ok := m[s]
|
||
if !ok {
|
||
return 0, fmt.Errorf("%w: %q", ErrUnknownLabel, s)
|
||
}
|
||
return v, nil
|
||
}
|
||
|
||
func lookupRev(m map[int]string, i int) (string, bool) {
|
||
s, ok := m[i]
|
||
return s, ok
|
||
}
|
||
```
|
||
|
||
## Target artefacts
|
||
|
||
- `docs/schema/02-tool-mapping.md` — shared routing rule #4: replace "Numeric-code inputs are deliberately retained… Publish a verified label map later" with the four label maps above (published); remaining numeric enums listed as gaps.
|
||
- `docs/schema/06-verification.md` — "Enum labels" gap row updated: T1–T4 marked **published**; `VoteType`/`RequestSource`/`RequestLimitType`/legacy filters remain gaps.
|
||
- `internal/translate/enums.go` — baseline specified in full above (consumed by Phase 06's `08-go-baseline.md`).
|
||
|
||
## Verification gates
|
||
|
||
- [ ] All four tables printed in both directions (label→int and int→label), 3/4/12/17 entries.
|
||
- [ ] `movie`→1, `tv`→0 documented explicitly with the non-positional warning.
|
||
- [ ] Input rejection of `*_code` fields and read-side fallback wording present.
|
||
- [ ] No tool schema still accepts integer enum codes for the four mapped enums.
|
||
|
||
## Evidence labels
|
||
|
||
- **Documented** — upstream enum value ranges (`RequestType [0,1,2]`, `IssueStatus [0,1,2,3]`, `NotificationAgent [0..11]`, `NotificationType [0..16]`) per RAML; frontend confirms Pending/InProgress/Resolved ordering.
|
||
- **Design** — the string-label contract, `*_code` twin fields, read-side fallback with `warnings[]`, and input-side `INVALID_ARGUMENT` on unknown labels are intentional MCP contracts backed by upstream-source evidence.
|
||
- **Verify** — `closed`→3 (not frontend-confirmed); per-instance version drift on all four maps; `VoteType`/`RequestSource`/`RequestLimitType` label maps remain unverified.
|