Decision Log
This document centrally records design details that have already converged and questions still awaiting a ruling. When closing a decision, the corresponding authority document must be updated in step.
Authority document precedence:
- Final Architecture Overview
- Lua API Contract
- Runtime Config Semantics
- Runtime Semantics Decisions
- Official Optional Module Contracts
- Refactoring Roadmap
Closed Decisions
OD-001 CLI Contract
Status: closed. Authority document: Bootstrap Runtime Semantics.
Decisions:
--config <path>may be omitted; the default isconfig/app.yaml.--configmay be passed repeatedly; multiple files merge in the order given, with merge rules governed by Runtime Config Semantics.--node-idbelongs to theshield_clusteroptional module. Passing it without cluster enabled must return a CLI error.- Old subcommands exist only as legacy CLI and do not enter the new
shield::runpublic contract. - The
shieldexecutable and example executables should all callshield::run(argc, argv)directly, so they share one parameter set.
OD-002 Process Lifecycle
Status: closed. Authority document: Bootstrap Runtime Semantics.
Decisions:
shield::runhandles generic signal handling, the runtime loop, help/version output, and exit codes.- Windows uses
SetConsoleCtrlHandler; POSIX uses SIGINT/SIGTERM. shield::runinitializes the runtime then blocks waiting for a stop signal; the lower-level bootstrap API only handles initialize/shutdown.- A user-defined main may bypass
shield::runand call the lower-level bootstrap API directly. - Exit codes follow Bootstrap Runtime Semantics.
OD-003 Physical Directory Layout
Status: closed. Authority documents: Directory Structure Design and the Refactoring Roadmap.
Decisions:
- Short term, keep the current physical directories
src/core,src/net,src/lua, etc.; converge dependencies first with CMake targets and include boundaries. - Phase 1 does not simultaneously do large-scale file moves and semantic refactoring.
- Optional modules' target physical directories remain shapes like
src/optional/shield_cluster, created or migrated only when the corresponding module is implemented. - The public include path stays
include/shield/..., with no breaking renames following target names.
OD-004 Phase 1 Network Scope
Status: closed. Authority document: Network Runtime Semantics.
Decisions:
- Phase 1 freezes only TCP sessions and basic transport framing.
- UDP, KCP, and WebSocket remain target semantics and later extensions, not Phase 1 acceptance blockers.
- A business HTTP gateway does not enter core; HTTP management endpoints exist only when
shield_opsis explicitly enabled.
OD-005 Optional Module Failure Policy
Status: closed. Authority documents: Official Optional Module Contracts and Runtime Config Semantics.
Decisions:
- When configuration contains an optional module section but the module is not built or not enabled, startup fails.
- An explicitly enabled optional module failing to initialize fails fast by default.
shield_cluster's first version allows degrading to single node when remote connections fail, but must continuously expose the unhealthy state; invalid configuration, node identity conflicts, and local listener failures still fail fast.- After
shield_opsis enabled, a management port binding failure must fail fast. shield_global,shield_player, andshield_serverinitialization failures fail fast by default.
OD-006 Data API Phase Split
Status: closed. Authority documents: Data Access Runtime Semantics and the Lua API Contract.
Decisions:
- Data access does not enter
shield_coreand is no longer exposed through a separateshield_datamodule. - The current Phase 1 contract is Plugin System v1: manifest scan, explicit instances, explicit bindings, C ABI interfaces, and plugin self-registered Lua namespaces.
- SQL, document store, cache, queue, and leaderboard capabilities are each provided by provider plugins.
- Business Lua uses plugin namespaces + binding logical names, e.g.
shield.database.mysql("database.default"). shield.db.*/shield.redis.*are legacy APIs, deleted with no public compatibility shim.- Prepared statements, pipelines, eval, sentinel/cluster, and similar capabilities evolve independently in specific plugins or later plugin interfaces; they never expand the core API in reverse.
OD-007 Legacy Compatibility Window
Status: closed. Authority documents: the Refactoring Roadmap and the Lua API Tests.
Decisions:
- No public compatibility shim is provided.
- Legacy Lua API deletion is gated on the negative tests of
lua-api-tests.md. - Legacy C++ target deletion is gated on examples/tests no longer referencing them.
- Old CLI subcommands may exist short-term as legacy CLI code, but do not enter the new
shield::runpublic contract.
OD-008 Player Setup API Form
Status: closed. Authority documents: Player Lifecycle Runtime Semantics and the Official Optional Module Contracts.
Decisions:
- P0 freezes only
shield.player.setup(M, opts)as the sole recommended entry point for business Player Services. shield.player.Basestays as an advanced style for Phase 2+; not implemented in P0, with the P2 boundary in OD-014.- setup's opts fields drop the
on_prefix (auth/login/client_message/disconnect/reconnect/logout/save), keeping a separate namespace from module-levelon_init/on_exit. - Missing required hooks (
auth/login/client_message/disconnect/logout) returnsnil, Error{code="setup_invalid"}immediately; the service never enters the running state. - Optional hooks not provided in setup execute the framework's documented default implementations; implicit "not provided means noop" behavior is not allowed.
- When business overrides an optional hook, the default does not auto-run; keeping the default behavior requires explicitly calling the
shield.player.defaults.*helpers.
OD-009 Player Persistence Adapter Boundary
Status: closed. Authority documents: Player Lifecycle Runtime Semantics and the Official Optional Module Contracts.
Decisions:
- The persistence adapter is a lightweight contract owned by
shield_player; it does not belong toshield_data. - The adapter's底层 must access data capabilities through plugin namespaces + binding logical names; it does not re-define SQL/Redis/document store semantics.
- The adapter owns no connection pools; pools belong to the corresponding plugin instance.
- The adapter introduces no ORM, mappers, or schema toolchain; it accepts only whitelist fields encodable by LuaPack.
- Adapter failures reuse the corresponding data plugin error codes; the player domain only adds errors like
persistence_save_failedthat clearly belong to this module. - With no persistence configured,
on_save's default implementation is a no-op.
OD-010 Player Cross-Service Reference
Status: closed. Authority documents: Player Lifecycle Runtime Semantics and the Official Optional Module Contracts.
Decisions:
- The cross-service player reference is named
PlayerRef, notPlayerHandle, avoiding confusion withServiceHandle. PlayerRefis a player-module-internal reference, not a replacement forServiceHandle.- The
PlayerRefstructure is{ uid, node_id, service_id, epoch };epochcomes from shield_cluster'snode_epochand is 0 on a single node. - Cross-service payloads may only carry
PlayerRef(or the client RPC's read-onlyClientContext/ClientReftokens); bare connection handles or fullPlayerSessions are forbidden. shield.player.resolve(ref)at P0 freezes only local resolve; remote resolve is Phase 2+, defined jointly byshield_cluster+shield_player.- An invalid ref returns
nil, Error{code="player_not_found"}; it does not throw.
OD-011 Player Extended States
Status: closed. Authority document: Player Lifecycle Runtime Semantics.
Decisions:
anonymousandspectatorare both opt-in states; the default state machine remainsconnecting -> authenticating -> online.- The
authhook stays required; business declares anonymous or spectator connections via auth's return value. - When not explicitly enabled,
anonymous_disabledorspectator_disabledis returned. - Upgrading anonymous to a formal account must re-authenticate and generates a new
PlayerRef.
OD-012 Player Scale Model
Status: closed. Authority document: Player Lifecycle Runtime Semantics.
Decisions:
- The default model is
service_per_player. player_poolis an optional operating strategy for large-scale scenarios; it does not change theshield.player.*public API.- Under
player_pool,PlayerRef.service_idpoints at a pool shard; per-player routing is done byshield_playerwithin the shard. - Before implementation, benchmarks for memory, latency, GC, mailbox backlog, and reconnect-window caching must be added.
OD-013 Player Multi-Device Policy
Status: closed. Authority document: Player Lifecycle Runtime Semantics.
Decisions:
- The default policy is
single. kick_oldmakes the old session exit with thelogoutreason"replaced".multiuses a(uid, device_id)index;shield.player.get(uid)returns the primary session; device enumeration goes throughPlayerManager.get_devices(uid).- Without a stable
device_id, only temporary sessions are allowed, which do not participate in same-device reconnect.
OD-014 Player Base API Boundary
Status: closed. Authority document: Player Lifecycle Runtime Semantics.
Decisions:
shield.player.Baseis P2 syntactic sugar and must be based onshield.player.setup.- Base introduces no second lifecycle, supports no multiple inheritance, and does not restore the
on_*prefix for setup fields. - P0/P1 tests use setup as the norm; Base tests only verify there is no extra semantics.
OD-015 Runtime Primitives Scope
Status: closed. Authority documents: Base Components and Runtime Adaptation Boundaries, the Refactoring Roadmap, and Network Runtime Semantics.
Decisions:
- Runtime primitives like
shield.buffer/shield.crypto/shield.socket/shield.stream/shield.tlsremain a deferred direction only and do not enter the current main path. - Cosocket-style APIs are not put into the current default mental model for business users.
- The current recommended mental model keeps three things: client ingress goes through the gateway's single target + read-only
ClientContext/ClientRef; inter-service goes throughshield.send/call; external HTTP goes throughshield.http. - If primitives advance in the future, they should be an advanced or deferred-phase capability, not the current default recommended path.
- JWT/Auth is not a base-component-layer central capability and is not released as an official Shield plugin; business layers should implement it on the base primitives or external services.
Follow-up execution (2026-09-28, AD-08): the first shield.crypto slice has landed (codec / SHA-256 / HMAC-SHA256 / random_bytes / constant_time_compare), making the "business layer on base primitives" phrasing above workable; the reference implementation is the Lua-side scripts/lib/jwt.lua, and plugins/auth_jwt is marked deprecated (kept for one transition period). The remaining primitives (shield.buffer / socket / stream / tls) are still deferred directions.
OD-016 Server State Machine and API Surface
Status: closed. Authority documents: Server State Runtime Semantics and the Official Optional Module Contracts.
Decisions:
- ServerManager is a process-level C++ singleton (in the PlayerManager mold:
global()/set_global()), not a service: no mailbox, no CAF actor, no spawned services, no Starter orchestration. - The state machine is
starting → running ⇄ maintenance,running/maintenance/starting → shutdown;shutdownis terminal;→ startingis always illegal;starting → maintenanceis illegal; same-value set succeeds idempotently without notifying; illegal transitions return errors without crashing. starting → runningis auto-triggered by bootstrap init complete (mark_ready), not exposed as an API.- The config section is only
server_manager.{name, info.{name,version,region}}, all optional; the draft'senabled(enablement is decided only by the build switch),state(the initial state is always starting), andon_state_change(the notification switch is expressed by watch registration) fields are deleted. - State watchers register as
shield.server:watch(fn); delivery must go through the system message channel (send_system) — ordinarysend()silently rejectson_-prefixed methods, and state notification method names are in the reserved prefix domain. The callback signature carries the dispatch ctx (fn(ctx, new_state)), consistent with handler dispatch rules. - The "C++ registry holds Lua callback objects" option is rejected: it would make the pure-C++ ServerManager link the script binding layer, destroying unit-testability. The C++ registry stores only
{watch_id, service_id}; watcher callbacks are held by each VM's orchestration layer, which installs forwarding handlers; when a watcher service exits it is unregistered by liveness determination.
OD-017 Server Shutdown Handover and Maintenance Boundary
Status: closed. Authority document: Server State Runtime Semantics.
Decisions:
server:shutdown(ms)does exactly three things: migrate toshutdown, notify watchers, and aftermsmilliseconds issue the process stop request on the same path as SIGINT/SIGTERM. It does not take over the drain budget — timeouts and exit order remain on bootstrapshutdown.timeout.*.- Repeated
shutdownreturnsshutdown_already_scheduledwithout restarting the timer; the timer thread usesstd::jthread+ stop_token, and both bootstrap cleanup paths stop it explicitly. - Maintenance mode at P0 provides only the
state()truth query; the login admission gate is implemented by the business /shield_playerlayer; core and the gateway are unaware of maintenance state. - State notification callbacks enter the messaging layer via the bootstrap-injected
notify_fncallback; ServerManager does not depend onshield_luadirectly, avoiding a circular dependency. - ops exposure merges into
/ops/status'sserverblock and the consoleroot.servercommand; no standalone/ops/serverendpoint and no ops write entry.
Open Decisions
There are currently no base semantic questions still awaiting a design ruling. If new open questions appear later, record them in this section first, then update the corresponding authority documents in step.