feat(M01): add typed domain identities

Work-Package: M01-FND-DOMAIN-IDENTITIES-001

Agent: sindo-main-codex
This commit is contained in:
2026-07-13 16:11:00 +04:00
parent d68786355c
commit c1dc07c705
14 changed files with 621 additions and 1 deletions
+6 -1
View File
@@ -36,7 +36,12 @@ TrinityCore/AzerothCore ◄── Network Adapter ◄── Runtime Client
### Domain
Содержит идентификаторы, value objects, команды и события: `EntityId`, `WowGuid`, `MapPosition`, `SpellId`, `MoveIntent`, `EntityMoved`. Не содержит side effects.
Содержит идентификаторы, value objects, команды и события: stable authored
`ContentId`, session-local `EntityId`, opaque wire `WowGuid`, namespaced
`ServerEntryId`, typed coordinates, `SpellId`, `MoveIntent`, `EntityMoved`.
Эти identity namespaces не конвертируются неявно и не содержат side effects;
их lifecycle и mapping определены в
[`modules/domain-identities.md`](modules/domain-identities.md).
### Gameplay
+122
View File
@@ -0,0 +1,122 @@
# ADR 0002: Separate domain identity namespaces
- Status: Accepted for M01 contract implementation
- Date: 2026-07-13
- Work package: `M01-FND-DOMAIN-IDENTITIES-001`
- Decision owners: M01 foundation/integration owners
## Context
OpenWC must relate authored content, runtime objects, WoW protocol objects and
TrinityCore/AzerothCore rows. All four domains can expose an integer or string,
but their identity lifetimes and collision rules are different. A raw `int`, a
generic `id` field or one universal ID class would allow an entity sequence to
be persisted as content identity, a database entry to be sent as a wire GUID,
or equal entry numbers from different tables to collide.
The Content Project requires stable references before server entries are
allocated. Runtime entities require cheap session-local identity even when no
server object exists. WoW 3.3.5a GUIDs carry opaque 64 wire bits whose semantic
decomposition must be established by protocol fixtures. Server templates use
numeric entries scoped by an adapter/table namespace.
## Decision
### ContentId
`ContentId` stores canonical lowercase UUID text in the `8-4-4-4-12` shape.
It is the only identity in this decision intended for stable authored-content
references and serialization. Construction normalizes whitespace and letter
case; `is_valid()` reports malformed input without crashing an import.
The value validates text shape, not UUID version or variant bits. A future
Content Project allocator chooses the UUID generation policy and owns duplicate
detection. Every persisted schema that contains a `ContentId` still declares
its own schema version and field migration.
### EntityId
`EntityId` stores a positive integer allocated by one runtime session owner.
It is valid only inside that session and must not be persisted as content,
protocol or server identity. Sequence reuse policy belongs to the future entity
registry, not the value object.
### WowGuid
`WowGuid` stores opaque `high_32_bits` and `low_32_bits` integers. Each word is
validated against `0..0xffffffff`, preserving every unsigned wire bit without
combining it into GDScript's signed 64-bit integer. The all-zero value remains
an observable protocol sentinel and receives no gameplay interpretation here.
High-type masks, packed-GUID codecs and object-kind extraction are explicitly
deferred to the build-12340 protocol package and require packet fixtures.
### ServerEntryId
`ServerEntryId` stores a positive numeric entry together with a normalized,
non-empty adapter/table namespace. Equal numbers in different namespaces are
different identities. Mapping a server entry to a `ContentId` is explicit
adapter/snapshot data; neither value derives the other.
### Equality and keys
The types expose domain-named `equals` methods because `RefCounted` `==`
compares object references. Stable keys are available only where their lifetime
allows them: `ContentId.to_key()`, `WowGuid.to_hex_string()` and
`ServerEntryId.to_key()`. `EntityId` exposes only `to_debug_key()` to discourage
persistence.
## Alternatives considered
### One generic `Id<T>` abstraction
Rejected because GDScript has no compile-time generic type distinction and the
shared API would obscure different validation, lifetime and persistence rules.
### Use strings everywhere
Rejected because prefixes are a convention rather than a type boundary and
would still permit accidental cross-layer assignment.
### Store WowGuid in one integer
Rejected because unsigned 64-bit GUIDs can exceed GDScript's positive signed
integer range. Split words preserve the wire representation losslessly.
### Use server entries as ContentId
Rejected because entries are allocated by a particular backend/table, can
collide between namespaces and do not exist while authoring offline content.
### Generate UUIDs inside ContentId
Rejected because randomness/allocation is a side effect requiring duplicate,
fixture and deterministic-build policy. This value object only normalizes and
validates an allocator-provided value.
## Consequences
- Cross-layer contracts must name the identity type they accept.
- Content references remain stable before and after server deployment.
- Runtime entity registries need explicit mappings to wire GUIDs/content IDs.
- Server adapters must supply a namespace and maintain entry-to-content maps.
- Malformed external values remain representable long enough for adapters to
produce diagnostics, but consumers must call the relevant validation method.
- Additional typed IDs such as map, spell, item and quest remain separate
follow-up contracts rather than aliases of these four values.
## Verification
- UUID normalization, shape validation and value equality are fixture-tested.
- Zero/positive session sequences establish `EntityId` validity boundaries.
- Maximum unsigned GUID words round-trip to exactly sixteen hexadecimal digits;
overflow is rejected and zero remains detectable.
- Equal server entry numbers in creature/gameobject namespaces do not compare equal.
- Dependency search verifies that identity values do not inherit Node/Resource
or expose `Vector3`.
## References
- [RFC 9562: Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562.html)
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
- [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)
+1
View File
@@ -8,6 +8,7 @@
|---|---|---|
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
| Coordinate mapping | Implemented | [`coordinate-mapping.md`](coordinate-mapping.md) |
| Domain identities | Implemented contract | [`domain-identities.md`](domain-identities.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
+232
View File
@@ -0,0 +1,232 @@
# Domain Identities
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M01-FND-DOMAIN-IDENTITIES-001` |
| Owners | Foundation/domain |
| Last verified | Worktree `work/sindo-main-codex/m01-domain-identities`, 2026-07-13 |
| Profiles/capabilities | All profiles; identity contract version 1 |
## Purpose
Provide non-interchangeable, scene-free identity values for authored content,
runtime entities, WoW wire objects and namespaced server entries. Each value
states its lifetime and serialization rules instead of exposing a generic `id`.
## Non-goals
- Allocating, reserving or globally registering IDs.
- Parsing packet bytes or server database rows.
- Mapping GUID high types or server entries to gameplay object kinds.
- Defining Content Project, snapshot or replay schemas.
- Replacing future typed map/spell/item/quest/display/sound IDs.
## Context and boundaries
```mermaid
flowchart LR
Authoring[Content Project allocator] --> ContentId[ContentId]
Session[Entity registry] --> EntityId[EntityId]
Wire[Build 12340 packet adapter] --> WowGuid[WowGuid]
Server[Core snapshot adapter] --> ServerEntry[ServerEntryId]
ContentId --> Mapping[Explicit identity mapping records]
EntityId --> Mapping
WowGuid --> Mapping
ServerEntry --> Mapping
Mapping --> Consumers[Gameplay / network / authoring consumers]
Raw[Raw int or string] -. forbidden across boundary .-> Consumers
```
Allowed dependencies:
- GDScript scalar/string operations and `RefCounted` lifetime;
- callers in content, gameplay, network and server adapter layers.
Forbidden dependencies:
- Node, Resource, SceneTree, renderer and UI state;
- packet buffers, SQL connections or database table implementations;
- implicit conversion between identity namespaces;
- random/global allocation inside the value objects.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `ContentId` | Immutable value | Stable authored identity as canonical lowercase UUID text | Any thread; persisted content lifetime | `is_valid()` reports malformed shape |
| `ContentId.equals` | Pure method | Value equality across references | Call duration | Null compares false |
| `ContentId.to_key` | Pure method | Canonical serialization/dictionary key | Stable across schema lifetime | Caller must validate first |
| `EntityId` | Immutable value | Positive session-local runtime sequence | One runtime session | `is_valid()` rejects zero/negative |
| `EntityId.equals` | Pure method | Equality within an explicitly shared session | Call duration | Cross-session comparison is a caller error |
| `EntityId.to_debug_key` | Pure method | Non-persistent diagnostic label | Session/log lifetime | Must not be serialized as content |
| `WowGuid` | Immutable value | Opaque unsigned high/low wire words | Packet/replay/entity mapping lifetime | `has_valid_word_ranges()` reports overflow |
| `WowGuid.is_empty` | Pure method | Detect all-zero wire sentinel | Call duration | Does not assign semantic meaning |
| `WowGuid.to_hex_string` | Pure method | Sixteen-digit diagnostic/replay key | Wire/replay lifetime | Caller validates ranges first |
| `ServerEntryId` | Immutable value | Adapter/table namespace plus positive entry | Snapshot/deployment mapping lifetime | `is_valid()` rejects missing namespace/non-positive entry |
| `ServerEntryId.to_key` | Pure method | Namespaced mapping key | Snapshot/schema lifetime | Caller validates first |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | UUID text | Future Content Project allocator/parser | `ContentId` | Copied normalized string | Build/content lifetime |
| Input | Session sequence | Future entity registry | `EntityId` | Copied integer | One runtime session |
| Input | Two unsigned 32-bit words | Future build-12340 packet codec | `WowGuid` | Copied integers | Packet/replay/entity lifetime |
| Input | Namespace and numeric entry | Future core snapshot adapter | `ServerEntryId` | Copied normalized string/integer | Snapshot/deployment lifetime |
| Output | Validation/equality/key results | Identity values | Adapters/registries/schemas | Returned scalar/string | Call duration |
Side effects:
- none;
- no allocation registry, random generation, file/cache write, packet read,
database query, scene mutation or logging.
## Data flow
```mermaid
flowchart TD
ContentSource[Authored document] -->|UUID text| ContentId
ServerRow[Backend table row] -->|adapter + table + entry| ServerEntryId
Packet[World packet] -->|high/low wire words| WowGuid
Registry[Session entity allocator] -->|positive sequence| EntityId
ServerEntryId --> DeployMap[Explicit deployment mapping]
ContentId --> DeployMap
WowGuid --> RuntimeMap[Explicit runtime mapping]
EntityId --> RuntimeMap
DeployMap --> Snapshot[Versioned snapshot/change set]
RuntimeMap --> WorldState[Session world state]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
state ContentIdentity {
[*] --> Parsed
Parsed --> Persisted: valid ContentId
Persisted --> Referenced
}
state RuntimeIdentity {
[*] --> Allocated
Allocated --> Active
Active --> Released: session owner removes entity
Released --> [*]
}
state ExternalIdentity {
[*] --> Observed
Observed --> Mapped: WowGuid or ServerEntryId
Mapped --> Stale: disconnect or snapshot replacement
Stale --> [*]
}
```
The values themselves are stateless and immutable. Allocation, release,
mapping and stale-state ownership belong to future registries/adapters.
## Main sequence
Identity construction and validation are synchronous. An external adapter reads
its format, constructs the matching value, validates it, then publishes an
explicit mapping or typed domain event. No value object performs I/O or retries.
## Ownership, threading and resources
- Values own only copied strings/integers and are caller-owned `RefCounted` references.
- Public methods do not mutate values or shared state and are worker-safe.
- Content/session/wire/server mapping records are owned by their future layer,
not by these values.
- No Node, RID, file, socket, database, lock or cache resource is acquired.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Malformed ContentId | `is_valid()` | Invalid value remains inspectable | Source field/path supplied by parser | Regenerate or repair authored ID |
| Invalid EntityId sequence | `is_valid()` | Registry must reject publication | Session/correlation context | Fix allocator/session mapping |
| GUID word overflow | `has_valid_word_ranges()` | Codec must reject packet/replay value | Packet offset/opcode context | Reject malformed input; do not truncate |
| Empty GUID | `is_empty()` | Preserved as protocol sentinel | Adapter decides whether field permits empty | Apply opcode/field-specific rule |
| Missing server namespace/entry | `is_valid()` | Snapshot adapter rejects mapping | Backend/table/row context | Correct adapter schema mapping |
| Unknown/stale mapping | Registry/snapshot lookup | No implicit numeric fallback | Typed IDs in diagnostic | Refresh snapshot/session or reject event |
There is no cancellation inside constant-time value operations.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Identity contract version | 1 | All | No | Four separate namespaces and validation rules |
| ContentId text form | Lowercase `8-4-4-4-12` UUID | Authoring/runtime/tools | No | Stable persisted key |
| GUID decomposition | Opaque two-word storage | `wotlk_335a_build_12340` | No | Prevents unverified high-type assumptions |
## Persistence, cache and migration
- `ContentId.uuid_text` is the only stable persisted identity introduced here.
- Containing schemas must store their own schema version and duplicate/reference policy.
- `EntityId` must never be persisted across sessions.
- `WowGuid` may appear in versioned packet replays as explicit high/low words or
the exact sixteen-digit hex form; a replay schema owns that choice.
- `ServerEntryId` may appear in snapshots only with both namespace and entry.
- This package is additive and migrates no existing data or cache.
## Diagnostics and observability
- Stable diagnostics use `ContentId.to_key`, `WowGuid.to_hex_string` and
`ServerEntryId.to_key`.
- Runtime diagnostics may use `EntityId.to_debug_key` with a session correlation ID.
- Production metrics/logging belong to registries and adapters; values emit none.
## Verification
- Contract test: `src/tools/verify_domain_identities.gd`.
- Fixtures cover UUID normalization/malformed shape, positive/zero session IDs,
maximum/overflow GUID words, zero GUID and cross-table server entry collisions.
- Dependency gate confirms no Node/Resource/Vector3 exposure.
- Fidelity boundary: GUID bit meaning remains unclaimed until build-12340 packet fixtures.
- Performance: constant-time integer checks and a bounded 36-character UUID scan.
## Extension points
- Content Project allocator and duplicate/reference validation.
- Session-owned entity registry and explicit `WowGuid ↔ EntityId` mappings.
- Backend capability adapters that define server entry namespaces.
- Separate typed map/spell/item/quest/display/sound values.
- Fixture-backed build-12340 GUID high-type decoding in the protocol layer.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Stable authored ContentId | Implemented contract | UUID normalization/validation fixtures | Allocator and schema arrive with Content Project |
| Session-local EntityId | Implemented contract | Positive/zero/equality fixtures | Registry lifecycle arrives with gameplay world state |
| Lossless opaque WowGuid | Implemented contract | Full unsigned words and hex fixture | Packet codec/high-type semantics need build-12340 fixtures |
| Namespaced ServerEntryId | Implemented contract | Cross-table collision fixture | Backend namespace capability mapping arrives with server adapters |
| Cross-namespace mappings | Planned | Architecture/ADR boundary | Implement only in owning registries/adapters |
## Known gaps and risks
- No allocator or duplicate registry exists yet.
- GDScript `==` compares references; consumers must use domain `equals` methods
or canonical keys until a registry owns keying.
- UUID shape validation deliberately does not enforce generation version/variant.
- Server namespace vocabulary is adapter-owned and not yet centrally catalogued.
- GUID high-type/object-kind semantics remain unverified rather than guessed.
## Source map
| Path | Responsibility |
|---|---|
| `src/domain/identity/content_id.gd` | Stable canonical UUID content identity |
| `src/domain/identity/entity_id.gd` | Session-local runtime identity |
| `src/domain/identity/wow_guid.gd` | Opaque unsigned WoW wire identity |
| `src/domain/identity/server_entry_id.gd` | Namespaced backend entry identity |
| `src/tools/verify_domain_identities.gd` | Headless value/boundary regression |
| `docs/adr/0002-domain-identity-namespaces.md` | Normative namespace/lifetime decision |
## Related decisions and references
- ADR: [`../adr/0002-domain-identity-namespaces.md`](../adr/0002-domain-identity-namespaces.md)
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
- Foundation plan: [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)