시간 도메인과 정지 중 메시지·베팅 경계 정리
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# Time-domain inventory
|
||||
|
||||
This document is the authoritative classification of persistent timestamps,
|
||||
deadlines, cooldowns, and process-local elapsed-time rules. Classification is
|
||||
per rule, not per table. A feature may record both a wall occurrence and a game
|
||||
effect; those are two facts, never one fallback clock.
|
||||
|
||||
## Domain contract
|
||||
|
||||
| Domain | Authority | Advances while suspended/reconciling | Reconciliation |
|
||||
| ------------------------ | -------------------------------------------------------------- | ------------------------------------ | ----------------------------- |
|
||||
| `GAME_TIME` | `world_state.clock_tick` under phase/revision/generation fence | no | `SHIFT`, `KEEP`, or `REBUILD` |
|
||||
| `WALL_TIME` | PostgreSQL UTC `CURRENT_TIMESTAMP` for persistent decisions | yes | never |
|
||||
| `MONOTONIC_ELAPSED_TIME` | `performance.now()` / monotonic process clock | process-local only | never persisted |
|
||||
|
||||
`GameTick`, `ClockRevision`, `DeadlineGeneration`, `WallInstant`, and
|
||||
`MonotonicDuration` name these meanings in new/refactored APIs. Existing
|
||||
`createdAt`/`updatedAt` fields remain wall audit timestamps unless this inventory
|
||||
explicitly calls them game projections.
|
||||
|
||||
## Game database inventory
|
||||
|
||||
`Pause` means whether the rule continues to age during `SUSPENDED` or
|
||||
`RECONCILING`. `Projection` means a non-authoritative compatibility/display
|
||||
representation.
|
||||
|
||||
| Table / rule / field(s) | Current meaning | Domain and authority | Pause | Reconcile / projection | Decision and reason |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `world_state.clock_tick` | observed world coordinate | GAME, self-authoritative | stop | REBUILD | root of all game-time decisions |
|
||||
| `world_state.last_turn_tick` | executed turn cursor | GAME | stop | SHIFT | execution order must preserve skipped-turn policy |
|
||||
| `world_state.clock_revision`, `deadline_generation` | clock/future-deadline generations | GAME metadata | stop | REBUILD | stale commands/workers must fail their fence |
|
||||
| `world_state.clock_base_time`, `clock_wall_anchor` | tick-to-date mapping and wall observation anchor | GAME projection metadata | n/a | REBUILD | not business wall deadlines |
|
||||
| `world_state.updated_at` | row audit | WALL, DB UTC | advance | excluded | operational history is not shifted |
|
||||
| `clock_suspension.source_revision`, `target_revision`, `cut_tick`, `catch_up_ticks`, `gap_ticks`, `shift_ticks`, `aligned_tick` | reconciliation plan/audit | GAME metadata | stop | KEEP | immutable clock operation facts |
|
||||
| `clock_suspension.cut_wall_at`, `resume_wall_at`, `created_at`, `updated_at` | operator/runtime occurrence audit | WALL, DB UTC | advance | excluded | records when the real operation occurred |
|
||||
| `clock_projection_outbox.target_revision` | target GAME generation | GAME metadata | stop | KEEP | projection fence |
|
||||
| `clock_projection_outbox.available_at`, `locked_at`, `applied_at`, `created_at`, `updated_at` | retry/lease/audit | WALL, DB UTC | advance | excluded | worker control cannot pause with game time |
|
||||
| `clock_reconciliation_participant` checksum/count/policy | immutable operation evidence | GAME operation metadata | n/a | KEEP | evidence, not a deadline |
|
||||
| `input_event.accepted_game_tick`, `accepted_clock_revision`, `accepted_deadline_generation` | daemon-claim boundary | GAME metadata | stop | KEEP across matching revision; rebase pending legacy rows only | API does not pre-stamp these; daemon owns acceptance |
|
||||
| `input_event.processing_game_tick`, `processing_clock_revision`, `processing_deadline_generation` | actual mutation boundary | GAME metadata | stop | KEEP | effect validation/RNG uses this coordinate |
|
||||
| `input_event.created_at`, `processing_at`, `completed_at`, `lease_until` | request receipt, processing audit, lease | WALL, DB UTC | advance | excluded | external occurrence and worker lease |
|
||||
| `read_model_outbox.*_at`, `web_push_outbox.*_at` | availability, claim, delivery, audit | WALL, DB UTC | advance | excluded | retry and notification delivery are operational |
|
||||
| `turn_daemon_lease.lease_until`, `heartbeat_at` | daemon liveness | WALL, DB UTC | advance | excluded | a paused game must still lose a dead daemon lease |
|
||||
| `general.turn_tick` | next general turn | GAME | stop | SHIFT; `turn_time` projection | determines engine order |
|
||||
| `general.recent_war_tick` | past battle occurrence | GAME | stop | KEEP; `recent_war_time` projection | historical event does not move |
|
||||
| `general.meta.next_change_tick` | N-turn reselection cooldown | GAME | stop | SHIFT; `next_change`/`nextChangeAt` projections | expressed in turns; missing tick fails closed |
|
||||
| `general.created_at`, `updated_at` | entity audit | WALL, DB UTC | advance | excluded | no gameplay deadline meaning |
|
||||
| `select_pool.reserved_until_tick` | selection reservation deadline | GAME | stop | SHIFT; `reserved_until` projection | reservation is measured in game turns |
|
||||
| `select_npc_token.valid_until_tick`, `pick_more_from_tick` | NPC selection windows | GAME | stop | SHIFT; DateTime projections | token is a game selection schedule; missing ticks fail closed |
|
||||
| `general_access_log.last_refresh`, `last_action_at`; `general_access_batch.created_at`; `traffic_period.started_at`, `last_refresh`; `traffic_period_general.last_refresh` | traffic/access accounting | WALL, DB UTC | advance | excluded | community/operations usage, not world progression |
|
||||
| `message.created_at_wall`, `delete_until_wall`, `tombstoned_at_wall` | envelope send/delete lifecycle | WALL, DB UTC | advance | excluded | normal messages work while the game is paused; deletion is real five minutes |
|
||||
| `message.occurred_game_tick` | optional game context | GAME occurrence | stop | KEEP | context only, never deletion authority |
|
||||
| `message.time`, `time_tick`, `valid_until`, `valid_until_tick` | rolling compatibility projections | projection only | n/a | recompute only for `message_action`; general-envelope values never decide lifecycle | old columns are not fallback authority |
|
||||
| `message_action.created_game_tick`, `resolved_game_tick` | action occurrence/resolution | GAME occurrence | stop | KEEP | actionable message lifecycle is separate from envelope |
|
||||
| `message_action.expires_game_tick` | proposal response deadline | GAME | stop | SHIFT | remaining game duration survives pause |
|
||||
| `message_action.clock_revision`, `deadline_generation` | response fence | GAME metadata | stop | REBUILD | stale responses are rejected |
|
||||
| `message_action.created_at_wall`, `updated_at_wall`; `message_read_state.updated_at` | audit/read occurrence | WALL, DB UTC | advance | excluded | community UX state |
|
||||
| `diplomacy_letter.date` | document authored/sent time | WALL, DB UTC | advance | excluded | game effect dates live in diplomacy/action state, not the document timestamp |
|
||||
| diplomacy war/nonaggression start/end month data | diplomatic effect schedule | GAME calendar | stop | handled by engine schedule | affects world turns and war validity |
|
||||
| `inheritance_point.updated_at`, `inheritance_log.created_at`, `inheritance_result.created_at`, baseline/user-state audit fields | account ledger/result audit | WALL, DB UTC | advance | excluded | account/external-currency history |
|
||||
| `inheritance_ledger.requested_at_wall`, `consumed_at_wall`, `created_at_wall` | direct purchase receipt | WALL, DB UTC | advance | excluded | real request/debit receipt |
|
||||
| `inheritance_ledger.applied_clock_revision`, `applied_deadline_generation` | game-effect fence metadata | GAME metadata | stop | KEEP | no `applied_game_tick`: current direct effects are timeless immediate state changes |
|
||||
| inheritance command `input_event` | durable effect state/idempotency | WALL receipt + GAME processing fence | mixed, separated | only GAME coordinate participates | one transaction commits debit, receipt, effect, and command success; failure leaves durable input event and no debit |
|
||||
| `auction.open_tick`, `auction_bid.occurred_game_tick` | open/bid game occurrence | GAME | stop | KEEP; bid `event_at` is projection | event order/RNG/replay context |
|
||||
| `auction.close_tick` | in-world close deadline | GAME | stop | SHIFT; `close_at` projection | authoritative worker/finalizer deadline; missing tick fails closed |
|
||||
| `auction_bid.requested_at_wall`, `created_at`; `auction.finalizing_at`, `finished_at`, `created_at`, `updated_at` | request/processing/audit | WALL, DB UTC | advance | excluded | real action and recovery history |
|
||||
| `auction.latest_event_at` | optimistic compatibility projection of latest game event | GAME projection | stop | follows authoritative event tick | never used as wall deadline |
|
||||
| `vote_poll.start_tick`, `end_tick` | poll occurrence/deadline | GAME | stop | KEEP/SHIFT; `start_at`/`end_at` projections | poll is an in-world survey; missing deadline tick fails closed |
|
||||
| `vote_poll.closed_at`, `created_at`, `updated_at`; `vote.created_at`; `vote_comment.created_at` | closure/user/audit occurrence | WALL, DB UTC | advance | excluded | closure receipt and community content history |
|
||||
| tournament `nextTick`, `bettingCloseTick` in Redis | stage/betting deadlines | GAME | stop | REBUILD; `nextAt`/`bettingCloseAt` projections | stages advance with the world; legacy date-only state fails closed |
|
||||
| nation betting open/close year-month and tournament phase | in-world availability | GAME calendar/tick | stop | engine/Redis participant | tied to tournament turns |
|
||||
| tournament/nation bet submission | user WALL request + effect at the current frozen GAME coordinate | WALL + GAME, separated | submission is allowed during `SUSPENDED`; GAME deadline does not age | receipt excluded; GAME availability/fence retained | pausing stage progress must not close an already-open betting window |
|
||||
| `nation_betting.*_at`, `nation_bet.*_at` | user/audit occurrence | WALL, DB UTC | advance | excluded | receipts, not close authority |
|
||||
| `game_history.date`, old-general `turntime`, archived projected dates | archived game-calendar projection | GAME historical display | stop | KEEP, never shifted | immutable archive/replay record |
|
||||
| archive/entity `created_at`, cancellation `opened_at`/`cancelled_at`, unification `completed_at` | operation/archive audit | WALL, DB UTC | advance | excluded | real creation/completion facts |
|
||||
| `general_turn_revision.lease_expires_at`, `nation_turn_revision.lease_expires_at` and audit timestamps | edit lease/revision audit | WALL, DB UTC | advance | excluded | editor concurrency timeout |
|
||||
| board post/comment, log/error/event, legacy migration timestamps | content/audit/migration history | WALL, DB UTC | advance | excluded | community and operational evidence |
|
||||
|
||||
The year-9999 message sentinel remains a legacy projection only.
|
||||
`MAX_SAFE_GAME_TICK` is the separate GAME-domain infinite sentinel. Neither is
|
||||
converted into or used as the other domain's ordinary deadline.
|
||||
|
||||
## Gateway database inventory
|
||||
|
||||
Gateway has no gameplay clock authority. Every Gateway `DateTime` is WALL_TIME:
|
||||
|
||||
- `app_user`: identity/session/icon/terms/privacy/Kakao/grace/deletion/login and
|
||||
`created_at`/`updated_at` fields.
|
||||
- access grants, retired identities, admin audits, user icons, legacy member
|
||||
logs, and migration timestamps.
|
||||
- profile lifecycle `preopen_at`, `open_at`, `scheduled_start_at`, build request,
|
||||
start/completion/last-used, and row audit timestamps. These are real control
|
||||
plane schedules; they do not replace a profile's `world_state.clock_tick`.
|
||||
- subscriptions/preferences/receipts/notifications and web-push delivery
|
||||
`available_at`, `locked_at`, `delivered_at`, expiration and audit fields.
|
||||
- runtime actions, operations, releases, and bulk releases: schedule, start,
|
||||
completion, retry, lease, heartbeat, successful and audit timestamps.
|
||||
|
||||
Competitive Gateway operation/release claim and lease renewal decisions read
|
||||
PostgreSQL `CURRENT_TIMESTAMP` inside the persistence transaction; the caller's
|
||||
poll timestamp is not authoritative. A Gateway PREOPEN wall schedule is an operational request; once
|
||||
the game exists, gameplay schedules use the game database tick.
|
||||
|
||||
## Process-local monotonic inventory
|
||||
|
||||
The following are `MONOTONIC_ELAPSED_TIME` and are never persisted: daemon/RPC
|
||||
wait budgets, turn processing budgets, worker poll/resync intervals, lock
|
||||
acquisition waits, readiness loops, short-lived cache TTLs, latency metrics, and
|
||||
test wait loops. Production implementations use `performance.now()` where an
|
||||
elapsed duration is measured. `Date.now()`/`new Date()` remains valid only when
|
||||
creating or formatting a WALL occurrence, calculating a non-competitive auth
|
||||
TTL for an external protocol, or providing an explicit test clock.
|
||||
|
||||
## API phase policy
|
||||
|
||||
| Operation | During `SUSPENDED` / `RECONCILING` | Fence |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| normal public/private/nation message send/read/delete, including receiving/reading an existing recruitment letter envelope | allowed | WALL DB transaction only; actionable deadline remains frozen GAME state |
|
||||
| notification/account/inheritance history/audit reads | allowed | WALL |
|
||||
| actionable message response | rejected/queued except the explicitly authorized unification response | daemon clock phase/revision/generation |
|
||||
| tournament/nation bet submission while its GAME window is open | allowed in `SUSPENDED`; rejected in `RECONCILING` | frozen GAME deadline + phase/revision/generation fence |
|
||||
| tournament stage transition/close/settlement and nation-bet close/settlement | not applied | daemon GAME fence |
|
||||
| turn, reservation, war/diplomacy effect, auction, vote, other tournament mutation | not applied | daemon GAME fence |
|
||||
| direct inheritance state mutation | rejected if the daemon GAME fence cannot commit | atomic input-event + revision/generation fence |
|
||||
|
||||
There is currently no account-only inheritance purchase endpoint. Direct
|
||||
inheritance commands use policy 1 (no debit if game mutation cannot commit), not
|
||||
an ambiguous partially-applied state. Their `InputEvent.requestId` is the durable
|
||||
idempotency/effect state; `InheritanceLedger.requestId` proves the one successful
|
||||
receipt.
|
||||
|
||||
## `loadCurrentGameTime` call-site audit
|
||||
|
||||
All production call sites were reviewed. The remaining uses are GAME-only:
|
||||
|
||||
- `messages/store`: create/read/invalidate the separate `message_action`; normal
|
||||
envelope creation, display, read state, and deletion do not load game time.
|
||||
- `messages/diplomaticResponse`: game-effect/log calendar context after an
|
||||
actionable response fence; the letter's authored date is DB WALL_TIME.
|
||||
- auction `open`, `scheduler`, `worker`, and router: open/close tick projection,
|
||||
due evaluation, and fence context; bid receipt time is separate WALL_TIME.
|
||||
- vote router, tournament router/worker: GAME poll/stage deadlines and Redis
|
||||
projection fences.
|
||||
- troop/general/join selection routers: current world turn/schedule context;
|
||||
the NPC reservation mutation holds the clock advisory lock and `world_state`
|
||||
row fence before reading it.
|
||||
- lobby: display-only projected server game time and phase.
|
||||
|
||||
No inheritance receipt, ordinary message timestamp/delete rule, audit log,
|
||||
lease, outbox retry, API timeout, or worker budget calls this helper.
|
||||
|
||||
Ordinary message send/read-state/delete mutations retain their durable API
|
||||
`InputEvent` transaction and read-model journal, but use the WALL-only input
|
||||
boundary. That boundary deliberately does not acquire the game clock advisory
|
||||
lock, so a reconciliation transaction cannot unnecessarily serialize community
|
||||
messaging. Actionable responses continue to use the GAME-fenced boundary.
|
||||
|
||||
## Migration boundary
|
||||
|
||||
Migration `20260903140000_split_message_wall_and_game_time` adds and backfills
|
||||
the explicit message, action, auction-bid, inheritance receipt, and selection
|
||||
cooldown authorities. It never extends an already-expired message delete window.
|
||||
Old projection columns remain during rolling deployment, but new code never
|
||||
chooses a clock by NULL fallback: GAME rules require their tick; WALL rules use
|
||||
their wall column. The disposable migration verifier covers populated upgrade,
|
||||
indexes/constraints, replay safety, and a second no-op deploy.
|
||||
Reference in New Issue
Block a user