// Package operation defines the runtime-operation audit-log domain // types owned by Game Master. // // One OperationEntry maps to one row of the `operation_log` PostgreSQL // table (see // `galaxy/gamemaster/internal/adapters/postgres/migrations/00001_init.sql`). // The OpKind / OpSource / Outcome enums match the SQL CHECK constraints // verbatim and feed the telemetry counters declared in // `galaxy/gamemaster/README.md §Observability`. package operation import ( "fmt" "strings" "time" ) // OpKind identifies the kind of operation Game Master performed. type OpKind string const ( // OpKindRegisterRuntime records a register-runtime operation // (engine init plus first transition to running). OpKindRegisterRuntime OpKind = "register_runtime" // OpKindTurnGeneration records a turn-generation operation // (scheduler ticker or admin force). OpKindTurnGeneration OpKind = "turn_generation" // OpKindForceNextTurn records the admin force-next-turn driver // (separate from the turn-generation entry it produces, so audit // callers can tell scheduler ticks from manual ones). OpKindForceNextTurn OpKind = "force_next_turn" // OpKindBanish records a /admin/race/banish call against the // engine container. OpKindBanish OpKind = "banish" // OpKindStop records the admin stop driver (the underlying RTM // stop call is recorded in Runtime Manager's own operation log). OpKindStop OpKind = "stop" // OpKindPatch records the admin patch driver. OpKindPatch OpKind = "patch" // OpKindEngineVersionCreate records a registry CREATE. OpKindEngineVersionCreate OpKind = "engine_version_create" // OpKindEngineVersionUpdate records a registry PATCH. OpKindEngineVersionUpdate OpKind = "engine_version_update" // OpKindEngineVersionDeprecate records a registry DELETE / soft // deprecate. OpKindEngineVersionDeprecate OpKind = "engine_version_deprecate" // OpKindEngineVersionDelete records a registry hard delete: the // row is removed from `engine_versions` after the service layer // confirms no non-finished runtime still references it. OpKindEngineVersionDelete OpKind = "engine_version_delete" ) // IsKnown reports whether kind belongs to the frozen op-kind vocabulary. func (kind OpKind) IsKnown() bool { switch kind { case OpKindRegisterRuntime, OpKindTurnGeneration, OpKindForceNextTurn, OpKindBanish, OpKindStop, OpKindPatch, OpKindEngineVersionCreate, OpKindEngineVersionUpdate, OpKindEngineVersionDeprecate, OpKindEngineVersionDelete: return true default: return false } } // AllOpKinds returns the frozen list of every op-kind value. The slice // order is stable across calls. func AllOpKinds() []OpKind { return []OpKind{ OpKindRegisterRuntime, OpKindTurnGeneration, OpKindForceNextTurn, OpKindBanish, OpKindStop, OpKindPatch, OpKindEngineVersionCreate, OpKindEngineVersionUpdate, OpKindEngineVersionDeprecate, OpKindEngineVersionDelete, } } // OpSource identifies where one operation entered Game Master. type OpSource string const ( // OpSourceGatewayPlayer identifies entries triggered by a verified // player command, order, or report read forwarded through Edge // Gateway. OpSourceGatewayPlayer OpSource = "gateway_player" // OpSourceLobbyInternal identifies entries triggered by Game Lobby // over the trusted internal REST surface (register-runtime, // memberships invalidate, banish, liveness). OpSourceLobbyInternal OpSource = "lobby_internal" // OpSourceAdminRest identifies entries triggered by Admin Service // (or system administrators today). The default when the // `X-Galaxy-Caller` header is missing or unrecognised. OpSourceAdminRest OpSource = "admin_rest" ) // IsKnown reports whether source belongs to the frozen op-source // vocabulary. func (source OpSource) IsKnown() bool { switch source { case OpSourceGatewayPlayer, OpSourceLobbyInternal, OpSourceAdminRest: return true default: return false } } // AllOpSources returns the frozen list of every op-source value. The // slice order is stable across calls. func AllOpSources() []OpSource { return []OpSource{ OpSourceGatewayPlayer, OpSourceLobbyInternal, OpSourceAdminRest, } } // Outcome reports the high-level outcome of one operation. type Outcome string const ( // OutcomeSuccess reports that the operation completed without // surfacing an error. OutcomeSuccess Outcome = "success" // OutcomeFailure reports that the operation surfaced a stable // error code recorded in OperationEntry.ErrorCode. OutcomeFailure Outcome = "failure" ) // IsKnown reports whether outcome belongs to the frozen outcome // vocabulary. func (outcome Outcome) IsKnown() bool { switch outcome { case OutcomeSuccess, OutcomeFailure: return true default: return false } } // AllOutcomes returns the frozen list of every outcome value. The slice // order is stable across calls. func AllOutcomes() []Outcome { return []Outcome{OutcomeSuccess, OutcomeFailure} } // OperationEntry stores one append-only audit row of the `operation_log` // table. ID is zero on records that have not been persisted yet; the // store assigns it from the table's bigserial column. FinishedAt is a // pointer because the column is nullable for in-flight rows even though // the service layer finalises the row in the same transaction. type OperationEntry struct { // ID identifies the persisted row. Zero before persistence. ID int64 // GameID identifies the platform game this operation acted on. GameID string // OpKind classifies what the operation did. OpKind OpKind // OpSource classifies how the operation entered Game Master. OpSource OpSource // SourceRef stores an opaque per-source reference such as a request // id, a Redis Stream entry id, or an admin user id. Empty when the // source does not provide one. SourceRef string // Outcome reports whether the operation succeeded or failed. Outcome Outcome // ErrorCode stores the stable error code on failure. Empty on // success. ErrorCode string // ErrorMessage stores the operator-readable detail on failure. // Empty on success. ErrorMessage string // StartedAt stores the wall-clock at which the operation began. StartedAt time.Time // FinishedAt stores the wall-clock at which the operation // finalised. Nil for in-flight rows. FinishedAt *time.Time } // Validate reports whether entry satisfies the operation-log invariants // implied by the SQL CHECK constraints and the README §Persistence // Layout listing. func (entry OperationEntry) Validate() error { if strings.TrimSpace(entry.GameID) == "" { return fmt.Errorf("game id must not be empty") } if !entry.OpKind.IsKnown() { return fmt.Errorf("op kind %q is unsupported", entry.OpKind) } if !entry.OpSource.IsKnown() { return fmt.Errorf("op source %q is unsupported", entry.OpSource) } if !entry.Outcome.IsKnown() { return fmt.Errorf("outcome %q is unsupported", entry.Outcome) } if entry.StartedAt.IsZero() { return fmt.Errorf("started at must not be zero") } if entry.FinishedAt != nil { if entry.FinishedAt.IsZero() { return fmt.Errorf("finished at must not be zero when present") } if entry.FinishedAt.Before(entry.StartedAt) { return fmt.Errorf("finished at must not be before started at") } } if entry.Outcome == OutcomeFailure && strings.TrimSpace(entry.ErrorCode) == "" { return fmt.Errorf("error code must not be empty for failure entries") } return nil }