Files
ombi-mcp/docs/megaplans/m1/04-enum-translation-layer.md
gronod 722108ebd0 Initial commit: Ombi MCP server design docs and Go skeleton
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.
2026-09-18 15:55:44 +01:00

8.0 KiB
Raw Permalink Blame History

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)

// 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.