World Data Loaders — Implementation Plan¶
Design Document: docs/designs/v3.1/04-world-data-loaders.md (v3.4)
Estimated Timeline: 12 weeks (Sprint 0 + 4 phases)
Summary¶
This plan implements a unified World Data Loader framework for MAID that provides
shared infrastructure for declarative content authoring via YAML files. Data-loading
infrastructure does not yet exist in the codebase; existing content is loaded via bespoke
Python code in content pack on_load() methods. This framework provides a composable
pipeline for non-programmer content creation, validated against existing Pydantic component
schemas, with templates, cross-entity references, and atomic loading with rollback.
Key deliverables:
- Composable 6-phase pipeline (discover → parse+lint → prepare → resolve refs → instantiate → post-load)
- YAML 1.2 parsing via ruamel.yaml (coexists with PyYAML for legacy code)
- Single-inheritance templates with variable substitution
- Pre-assigned deterministic UUIDs for reference resolution
- Definition vs. instance state machine for persistence reconciliation
- CLI tooling: maid data validate, maid data lint, maid data preview, maid data load, maid data resolve
- Security hardening (resource limits, path jail, no aliases)
Cross-document dependencies:
- Doc 01 (Durable Persistence): DocumentStore integration for instance state, quarantine collection, startup authority boundary (Doc 04 loads defined entities first, Doc 01 restores dynamic entities after)
- Doc 02 (Database Migrations): Schema evolution for DataProvenanceComponent / InstanceStateComponent persistence
Sprint 0: Validation Spike (Week 0)¶
Validate YAML format decisions against real content before building infrastructure.
0.1 Dependency Setup¶
Package: maid-engine | Priority: P0 | Dependencies: None
- [ ] Add
ruamel.yaml>=0.18tomaid-engine/pyproject.tomldependencies - [ ] Verify
watchfilesis already present inmaid-engine/pyproject.toml(line 59:"watchfiles>=0.21.0") - [ ] Run
uv syncto verify dependency resolution - [ ] Verify
ruamel.yamlC extension (ruamel.yaml.clib) installs when C compiler is available
0.2 Spike: Load Tutorial World Data¶
Package: maid-engine, maid-tutorial-world | Priority: P0 | Dependencies: 0.1
- [ ] Write minimal spike script that parses existing
maid-tutorial-worlddata files withruamel.yamlin YAML 1.2 safe mode - [ ] Verify line/column tracking is preserved on parsed nodes
- [ ] Validate parsed data against existing Pydantic component models (
HealthComponent,DescriptionComponent,NPCComponent) - [ ] Identify any format incompatibilities between existing YAML files and the design spec
- [ ] Produce a
MIGRATION_GAPS.mddocumenting required changes tomaid-tutorial-worldYAML files andmaid-classic-rpgdata files, including any list-based vs. keyed-entity format differences - [ ] Document findings and any required YAML format adjustments
- [ ] Delete spike script after findings are documented
Phase 1: Core Pipeline Infrastructure (Weeks 1–4)¶
1.1 Data Models and Error Types¶
Package: maid-engine | Priority: P0 | Dependencies: 0.1
- [ ] Create
packages/maid-engine/src/maid_engine/loader/__init__.py - [ ] Create
packages/maid-engine/src/maid_engine/loader/models.pywith: - [ ]
ErrorSeverityenum (WARNING,ERROR,CRITICAL) - [ ]
LoadErrorfrozen dataclass with fields:code,file_path,line,field_path,message,severity,suggestion - [ ]
DiscoveredFiledataclass (path, relative_path, file_size, mtime) - [ ]
ParsedDocumentdataclass (path, data dict, source_map for line tracking) - [ ]
EntityDefinitiondataclass (id, entity_type, components dict, tags, source_file, line, template_ref, raw_data) - [ ]
DataFileMetaPydantic model for_metablock parsing (schema, pack, description, author, load_order) - [ ]
SchemaRefPydantic model withparse()classmethod fornamespace:type:versionformat - [ ]
LoaderConfigdataclass with all configurable limits:strict_mode: bool = Truemax_file_size: int = 10_485_760(10 MB)max_entities_per_file: int = 1000max_components_per_entity: int = 50max_template_depth: int = 5max_ref_chain_depth: int = 20max_errors_per_file: int = 50max_quarantine_entries: int = 500pipeline_timeout: float = 60.0fuzzy_match_max_candidates: int = 5000
- [ ]
LoaderContextdataclass with fields:world,config,reference_registry,template_registry,discovered_files,parsed_docs,entity_definitions,errors - [ ]
EntityTypeConfigfrozen dataclass with fields:type_name,required_components,default_components,default_tags,allowed_top_level_fields - [ ] Write unit tests for
SchemaRef.parse()andDataFileMeta.model_validate() - [ ] Write unit tests for
LoaderConfigdefaults and validation
1.1a ComponentRegistry¶
Package: maid-engine | Priority: P0 | Dependencies: None
- [ ] Create
ComponentRegistryinpackages/maid-engine/src/maid_engine/core/ecs/registry.py: - [ ] Central registry mapping component type names to Python classes
- [ ] Populate via
Component.__init_subclass__hook or runtime subclass scan - [ ]
get(type_name) -> type[Component] | Nonemethod - [ ]
list() -> dict[str, type[Component]]method - [ ] Used by the loader's PreparePhase to resolve component names from YAML to classes
- [ ] Write unit tests for ComponentRegistry discovery and lookup
1.2 Pipeline Coordinator and Phase Protocol¶
Package: maid-engine | Priority: P0 | Dependencies: 1.1
- [ ] Create
packages/maid-engine/src/maid_engine/loader/pipeline.pywith: - [ ]
PhaseProtocol withnameproperty andasync execute(context) -> PhaseResult - [ ]
PhaseResultdataclass (errors, warnings, artifacts dict, phase_name, duration_ms) - [ ]
PipelineResultdataclass (success, errors, warnings, phase_results, entity_count, duration_ms) - [ ]
CancellationTokenclass withis_cancelledproperty andcancel()method - [ ]
Pipelineclass:- Constructor accepting
phases: list[Phase],timeout: float = 60.0 async run(context, cancellation_token=None) -> PipelineResult— executes phases sequentially, checks cancellation between phases, enforces timeoutrun_for_pack(pack, data_path=None, data_paths=None) -> PipelineResultconvenience method
- Constructor accepting
- [ ]
DEFAULT_PHASESlist constant defining the standard 6-phase ordering - [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/__init__.py - [ ] Write unit tests for
Pipelinecoordinator: - [ ] Test sequential phase execution
- [ ] Test pipeline abort on CRITICAL error (per-file, not per-pipeline)
- [ ] Test cancellation token halts pipeline between phases
- [ ] Test pipeline timeout enforcement
- [ ] Test error collection across phases
1.3 DiscoverPhase (Phase 1)¶
Package: maid-engine | Priority: P0 | Dependencies: 1.2
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/discover.pywithDiscoverPhase: - [ ] Recursively scan content pack
data/directory for.yamland.jsonfiles - [ ] Parse
_meta.yamlat directory root forload_orderif present - [ ] Respect
load_order: templates load before entities that reference them - [ ] Handle
_meta.yamlfailure modes per design:- Missing
_meta.yaml→ use convention defaults - Unparseable
_meta.yaml→ CRITICAL error, entire pack fails - References missing file → ERROR, skip that file, continue
- Files not in
load_order→ load after ordered files, emit INFO
- Missing
- [ ] Skip hidden files (starting with
.) except_meta.yaml - [ ] Ignore symlinks (security: symlink escape prevention)
- [ ] Reject paths that traverse outside pack root via
is_safe_path() - [ ] Enforce
max_file_sizelimit - [ ] Populate
context.discovered_files - [ ] Create
packages/maid-engine/src/maid_engine/loader/security.pywith: - [ ]
is_safe_path(pack_root, candidate) -> boolfunction - [ ] Resource limit validation helpers
- [ ] Write unit tests:
- [ ] Test recursive directory scanning
- [ ] Test
load_orderrespect from_meta.yaml - [ ] Test all four
_meta.yamlfailure modes - [ ] Test symlink rejection
- [ ] Test path traversal rejection (
../../etc/passwd) - [ ] Test hidden file skipping
- [ ] Test max file size enforcement
1.4 ParsePhase (Phase 2: Parse + Lint)¶
Package: maid-engine | Priority: P0 | Dependencies: 1.3
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/parse.pywithParsePhase: - [ ] Use
ruamel.yamlwithtyp="safe"andversion=(1, 2) - [ ] Disable YAML aliases (safe mode does this by default)
- [ ] Set
allow_duplicate_keys = False - [ ] Preserve source line/column positions on parsed nodes for error reporting
- [ ] Re-verify
is_safe_path()immediately beforeopen()(TOCTOU mitigation) - [ ] Populate
context.parsed_docswithParsedDocumentinstances - [ ] Handle parse errors with file path and line information
- [ ] Integrated pre-parse lint rules (operate on raw YAML text before parsing):
- [ ]
MAID-Y001(Warn): Unquoted YAML 1.1 boolean literal (NO,YES,On,Off, etc.) - [ ]
MAID-Y002(Warn): Unquoted sexagesimal value (1:30) - [ ]
MAID-Y003(Warn): Integer with leading zero (octal in 1.1) (0755) - [ ]
MAID-Y005(Warn): Special float (.inf,.nan) - [ ]
MAID-Y006(Warn): Inconsistent block scalar indentation - [ ]
MAID-Y007(Error): YAML merge key (<<:) - [ ]
MAID-Y008(Error): Duplicate mapping key
- [ ]
- [ ] Enforce
max_errors_per_filelimit per file - [ ] Write unit tests:
- [ ] Test valid YAML parsing with line tracking
- [ ] Test YAML 1.2 semantics (NO → string, not bool)
- [ ] Test duplicate key rejection
- [ ] Test parse error reporting with file/line
- [ ] Test TOCTOU re-verification
- [ ] Test each lint rule (MAID-Y001 through MAID-Y008)
- [ ] Test error cap enforcement
1.5 PreparePhase (Phase 3: Route + Register IDs + Validate + Semantic)¶
Package: maid-engine | Priority: P0 | Dependencies: 1.4
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/prepare.pywithPreparePhase: - Schema routing:
- [ ] Extract
_meta.schemafrom parsed documents - [ ] Implement implicit schema inference when
_meta.schemais absent: - Directory name →
maid:{dir}:v1 - Top-level key →
maid:{key}:v1 - Conflict between directory and top-level key → ERROR with suggestion
- [ ] Route to
EntityAssemblerwith appropriateEntityTypeConfig - [ ] Unknown types produce "Did you mean…" error with fuzzy matching
- [ ] Resolve template directives (
_extends,_use,_vars,_append) in this phase - [ ] Perform variable substitution (
${var},${var:default}) - [ ] Output fully-expanded entity definitions (no unresolved templates/variables)
- [ ] Extract
- ID registration:
- [ ] Collect all
_idvalues from entity definitions - [ ] Scope IDs to directory path within the pack
- [ ] Pre-assign deterministic UUIDs via
uuid5(NAMESPACE_DATA, f"{pack}:{scoped_id}") - [ ] Define
NAMESPACE_DATAconstant UUID - [ ] Implement
pre_assign_uuid(pack_name, scoped_id) -> UUIDfunction - [ ] Support explicit
_uuidoverrides - [ ] Check UUID collisions at three levels:
- Current load batch
- Dependency packs (via
ReferenceRegistry) - Existing world entities
- [ ] Register IDs in
context.reference_registry
- [ ] Collect all
- Validation:
- [ ] Validate component blocks against Pydantic models via
model_validate() - [ ] Auto-discover component types from
ComponentRegistryusingcomponent_typeClassVar - [ ] Support validation modes:
- Strict:
extrafields → Error, unknown refs → Error, missing optional → Warning - Lenient:
extrafields → Warning, unknown refs → Warning, missing optional → Silent - [ ] Enforce resource limits:
max_entities_per_file,max_components_per_entity - [ ] Rewrite Pydantic validation errors for content author readability
- [ ] Handle union types (
str | TranslatableText), complex nested types (UUID, datetime) - [ ] Post-parse lint rules (require parsed structure):
- [ ]
MAID-Y004(Error): Empty/null in required string field - [ ]
MAID-Y009(Error): Unresolvable@ref:reference (basic check) - [ ]
MAID-Y010(Warn): Cross-file@ref:withoutdepends_on - [ ]
MAID-Y011(Warn): Runtime-only field set in data file (in_combat: true) - [ ]
MAID-Y012(Error): Unknown component type name with "Did you mean…" suggestion
- [ ] Validate component blocks against Pydantic models via
- Semantic checks (v1: minimal rule set):
- [ ]
SemanticRuleProtocol with properties:id,description,severity,applies_to; and method:check(entity, context) -> list[LoadError](synchronous — pure functions over in-memory data, no timeout needed) - [ ] Filter rules by
applies_toentity type - [ ] Support
--skip-ruleIDs (viaLoaderConfig.skip_rules: set[str]) - [ ] V1 rules:
- [ ]
MAID-S001:current <= maximumforHealthComponent(Error) - [ ]
MAID-S005: Rooms should have at least one exit (Warning)
- [ ]
- [ ] Create
packages/maid-engine/src/maid_engine/loader/assembler.pywithEntityAssembler: - [ ] Constructor accepting
configs: dict[str, EntityTypeConfig] - [ ]
assemble(definition, config, world) -> Entitymethod - [ ] Apply
required_componentsvalidation - [ ] Apply
default_componentsfor absent components - [ ] Apply
default_tags - [ ] Validate against
allowed_top_level_fields - [ ] Create
packages/maid-engine/src/maid_engine/loader/entity_types.pywith standard configs: - [ ]
ROOM_CONFIG— required:DescriptionComponent; defaults:ExtendedRoomComponent; tags:room; top-level:exits,zone - [ ]
NPC_CONFIG— required:DescriptionComponent,NPCComponent; tags:npc; top-level:location - [ ]
ITEM_CONFIG— required:DescriptionComponent,ItemComponent; tags:item; top-level:location - [ ]
TEMPLATE_CONFIG— for template definitions - [ ] Create
packages/maid-engine/src/maid_engine/loader/rules/__init__.py - [ ] Create
packages/maid-engine/src/maid_engine/loader/rules/builtin.pywith v1 semantic rules - [ ] Write unit tests:
- [ ] Test explicit schema routing
- [ ] Test implicit schema inference (directory, top-level key, conflict)
- [ ] Test "Did you mean…" suggestions for unknown types
- [ ] Test
EntityAssemblerwith eachEntityTypeConfig - [ ] Test required component validation
- [ ] Test default component injection
- [ ] Test default tag application
- [ ] Test deterministic UUID generation
- [ ] Test directory-scoped ID uniqueness
- [ ] Test UUID collision detection at all three levels
- [ ] Test
_uuidexplicit override - [ ] Test cross-reload UUID stability (same input → same UUID)
- [ ] Test valid component validation
- [ ] Test unknown component type detection with suggestions
- [ ] Test strict vs. lenient mode behavior
- [ ] Test resource limit enforcement
- [ ] Test error message rewriting for readability
- [ ] Test each semantic rule
- [ ] Test
--skip-ruleintegration - [ ] Test post-parse lint rules (MAID-Y004, Y009–Y012)
Phase 2: Templates and References (Weeks 5–7)¶
2.1 Template Resolver¶
Package: maid-engine | Priority: P0 | Dependencies: 1.6
- [ ] Create
packages/maid-engine/src/maid_engine/loader/resolver.pywithTemplateResolver: - [ ] Single inheritance via
_extendsdirective - [ ] Template instantiation via
_usedirective - [ ] Variable substitution via
_vars/${var}/${var:default}syntax - [ ] Generic
_appendblock for additive list merging on any list field - [ ] Deep merge logic:
- Scalars: child overrides parent
- Component dicts: deep-merged at field level
- Lists: child replaces parent entirely (unless
_append) _appendblock: unions with parent for specified list fields (duplicates removed)
- [ ] Circular inheritance detection with visited-set
- [ ] Max template depth enforcement (
max_template_depth = 5) - [ ] Variable type inference for defaults (int, float, bool, str)
- [ ] Single-pass variable substitution (no recursive expansion — security)
- [ ] Template entities marked with
_template: trueare NOT instantiated as world entities - [ ] Write unit tests:
- [ ] Test single-level
_extendsinheritance - [ ] Test multi-level inheritance chain (3+ levels)
- [ ] Test
_usetemplate instantiation - [ ] Test variable substitution with typed defaults
- [ ] Test
_appendfor tags and other list fields - [ ] Test deep merge at component field level
- [ ] Test circular inheritance detection
- [ ] Test max depth enforcement (6 levels → error)
- [ ] Test
_template: trueentities are skipped during instantiation
2.2 TemplateRegistry Integration¶
Package: maid-engine, maid-stdlib | Priority: P0 | Dependencies: 2.1
- [ ] The existing
TemplateRegistryinpackages/maid-stdlib/src/maid_stdlib/commands/building/create.pyalready providesregister(),get(),unregister(),list(),clear(). Use it directly — no Protocol abstraction needed until a second implementation exists. - [ ] The data loader accepts a
TemplateRegistryinstance via constructor injection - [ ] Update
GameEngine.__init__()to accept optionaltemplate_registryparameter and store asself.template_registry - [ ] Default to
Nonewhen not provided (loader skips template features if absent) - [ ] Write unit tests for loader integration with existing TemplateRegistry
2.3 ReferenceRegistry (Engine-Owned)¶
Package: maid-engine | Priority: P0 | Dependencies: 1.7
- [ ] Add
ReferenceRegistryclass topackages/maid-engine/src/maid_engine/loader/resolver.py: - [ ]
_entries: dict[str, UUID]mapping symbolic names to UUIDs - [ ]
_lock: asyncio.Lockfor write synchronization - [ ]
_snapshot: MappingProxyType[str, UUID]copy-on-write snapshot for lock-free reads - [ ]
async register(name, entity_id)— acquires lock, stores entry, eagerly rebuilds snapshot under lock (DD-28/DD-37) - [ ]
snapshot() -> Mapping[str, UUID]— returns immutable snapshot (no lock needed) - [ ]
async unregister(name) -> bool - [ ]
async clear() - [ ] Add
self.reference_registry = ReferenceRegistry()toGameEngine.__init__() - [ ] Write unit tests:
- [ ] Test register and snapshot retrieval
- [ ] Test concurrent register calls (asyncio.Lock correctness)
- [ ] Test eager snapshot rebuild (no stale reads)
- [ ] Test unregister and clear
2.4 ResolveRefsPhase (Phase 4)¶
Package: maid-engine | Priority: P0 | Dependencies: 2.3
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/resolve_refs.pywithResolveRefsPhase: - [ ] Parse
@ref:type/namesyntax for same-pack references - [ ] Parse
@ref:pack:type/namesyntax for cross-pack references - [ ] Parse
@ref:uuid:<uuid>syntax for direct UUID references - [ ] Replace
@ref:strings with pre-assigned UUIDs fromReferenceRegistry - [ ] Unresolved references produce errors with fuzzy-match suggestions
- [ ] Fuzzy match rate limiting: cap candidate pool at
fuzzy_match_max_candidates - [ ] Resolve references in exit destination and key fields (validated against the engine's existing exit structure, no separate ExitDefinition model)
- [ ] Reference resolution scope: current pack → dependency packs → engine registry
- [ ] In lenient mode, unresolved references produce flat error messages listing the failed entity and all entities that reference it (no dependency graph needed)
- [ ] Write unit tests:
- [ ] Test same-pack reference resolution
- [ ] Test cross-pack namespaced reference resolution
- [ ] Test unresolved reference error with "Did you mean…" suggestion
- [ ] Test fuzzy match rate limiting
- [ ] Test
@ref:uuid:<uuid>direct references - [ ] Test lenient mode cascade error reporting
2.5 DataLoaderPack Protocol Extension¶
Package: maid-engine | Priority: P0 | Dependencies: 1.5, 2.1
- [ ] Add to
packages/maid-engine/src/maid_engine/loader/protocols.py: - [ ]
@runtime_checkableDataLoaderPack(Protocol)with methods:get_entity_type_configs() -> list[EntityTypeConfig]get_semantic_rules() -> list[SemanticRule]get_data_paths() -> list[Path]
- [ ]
@runtime_checkableReloadableSystem(Protocol)with method:async on_entity_force_destroyed(entity_id: UUID, reason: str) -> None
- [ ] Write unit tests:
- [ ] Test
isinstance()detection for packs implementingDataLoaderPack - [ ] Test
isinstance()detection for systems implementingReloadableSystem - [ ] Test that base
ContentPackdoes NOT satisfyDataLoaderPack
Phase 3: Integration (Weeks 8–10)¶
3.1 Provenance and Instance State Components¶
Package: maid-engine (or maid-stdlib) | Priority: P0 | Dependencies: 1.1
- [ ] Create
DataProvenanceComponent(Component): - [ ] Immutable fields:
source_file: str,pack_name: str,definition_id: str,definition_hash: str,definition_version: str = "1.0",loaded_at: datetime - [ ]
definition_hashcomputed from canonicalized dict (sorted keys, JSON withsort_keys=True, ensure_ascii=True, SHA-256) — DD-24 - [ ] Create
InstanceStateComponent(Component): - [ ] Mutable fields:
modified: bool = False,modified_components: set[str],last_modified_at: datetime | None,modified_by: str | None - [ ]
mark_modified(component_type, actor=None)method — manual dirty tracking only for v1 (automaticTrackedComponentmixin deferred to v2) - [ ] Implement
compute_definition_hash(definition_dict) -> strutility function - [ ] Write unit tests:
- [ ] Test
DataProvenanceComponentimmutability semantics - [ ] Test
InstanceStateComponentmodification tracking viamark_modified() - [ ] Test definition hash canonicalization (reordered keys → same hash, changed values → different hash)
3.2 Staging EntityManager and LoadTransaction¶
Package: maid-engine | Priority: P0 | Dependencies: 1.5, 3.1
- [ ] Create
packages/maid-engine/src/maid_engine/loader/staging.pywith: - [ ] Verify existing
EntityManagerinpackages/maid-engine/src/maid_engine/core/ecs/entity.pycan be used as detached staging area (no EventBus dependency) — it already can,EntityManageris standalone - [ ] Add
EntityManager.adopt(entity: Entity) -> Nonepublic method for transferring entities between managers - [ ] Add
World.adopt_entity(entity: Entity) -> Nonemethod to transfer entity from externalEntityManagerinto live world:- Register entity in world's
EntityManagerviaadopt() - Update all component/tag indexes
- Update
RoomIndexif entity hasPositionComponent - Duplicate UUID behavior: If entity UUID already exists, replace-with-warning (log warning, destroy old entity, adopt new). This supports hot reload use case.
- Missing room: If entity references a room that doesn't exist, raise
ValueErrorwith descriptive message
- Register entity in world's
- [ ]
LoadTransactionclass:- Constructor:
__init__(self, world: World, reference_registry: ReferenceRegistry) _staging: EntityManager— detached staging area_staged_ids: list[UUID]_registered_refs: list[str]— track refs registered during this transactionstage_entity(entity_id, **kwargs) -> Entity— create in staging areaverify_references() -> list[LoadError]— post-staging integrity checkasync commit() -> None— transfer staged entities to live World viaadopt_entity(), then emitEntityCreatedEventfor eachasync rollback() -> None— discard staging area, clear staged IDs, clean upReferenceRegistryentries added during this transaction (remove_registered_refsfrom registry)
- Constructor:
- [ ] Write unit tests:
- [ ] Test
World.adopt_entity()transfers entity correctly - [ ] Test
World.adopt_entity()replaces existing entity with same UUID (with warning) - [ ] Test
World.adopt_entity()raises on missing room reference - [ ] Test staging entity creation does NOT emit events
- [ ] Test commit transfers entities and emits events
- [ ] Test rollback discards all staged entities
- [ ] Test rollback cleans up ReferenceRegistry entries
- [ ] Test reference verification catches broken refs
- [ ] Test partial failure → full rollback
3.3 InstantiatePhase (Phase 5)¶
Package: maid-engine | Priority: P0 | Dependencies: 3.2
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/instantiate.pywithInstantiatePhase: - [ ] Create
LoadTransactionfor each file or pack - [ ] For each non-template entity definition:
- Stage entity with pre-assigned UUID
- Add all validated components
- Apply tags from
EntityTypeConfig - Attach
DataProvenanceComponentwith source file, pack name, definition hash - Attach
InstanceStateComponent
- [ ] Run
verify_references()after staging all entities - [ ] On success:
await transaction.commit() - [ ] On failure:
await transaction.rollback()and report errors - [ ] Skip entities marked with
_template: true - [ ] Write unit tests:
- [ ] Test successful instantiation creates entities in World
- [ ] Test
DataProvenanceComponentis attached correctly - [ ] Test template entities are skipped
- [ ] Test reference verification failure triggers rollback
- [ ] Test atomic behavior (all-or-nothing per file)
3.4 PostLoadPhase (Phase 6)¶
Package: maid-engine | Priority: P1 | Dependencies: 3.3
- [ ] Create
packages/maid-engine/src/maid_engine/loader/phases/post_load.pywithPostLoadPhase: - [ ] Call content pack post-load callbacks for cross-entity relationship setup
- [ ] Build indexes (room connections, NPC location mapping)
- [ ] Emit summary statistics (files processed, entities created, templates, references resolved)
- [ ] Log pipeline results via
logging.getLogger("maid.loader")with structured context (for productionon_load()calls where CLI output isn't visible) - [ ] Write unit tests
3.5 GameEngine Pipeline Factory¶
Package: maid-engine | Priority: P0 | Dependencies: 3.3, 2.3, 2.2
- [ ] Add
create_data_pipeline(config: LoaderConfig | None = None) -> Pipelinefactory method toGameEngine: - [ ] Create
LoaderContextwith injected shared resources (world,reference_registry,template_registry,config) - [ ] Collect
EntityTypeConfigfrom all loaded packs implementingDataLoaderPack - [ ] Collect
SemanticRuleinstances from all loaded packs implementingDataLoaderPack - [ ] Return
PipelinewithDEFAULT_PHASESand collected configs/rules - [ ] Write integration test:
- [ ] Test full pipeline execution from
GameEngine.create_data_pipeline() - [ ] Test cross-pack reference resolution (stdlib entities referenced by classic-rpg)
3.6 Entity Load State Machine¶
Package: maid-engine | Priority: P0 | Dependencies: 3.3
- [ ] Add to
packages/maid-engine/src/maid_engine/loader/models.py: - [ ]
EntityLoadStateenum:NEW,INSTANCE_CLEAN,INSTANCE_STALE,INSTANCE_CORRUPT,RESET_REQUESTED - [ ]
EntityLoadActionenum:CREATE_FROM_DEFINITION,LOAD_FROM_INSTANCE,LOAD_INSTANCE_WARN_STALE,QUARANTINE_LOAD_DEFINITION,DELETE_INSTANCE_RELOAD - [ ]
resolve_load_action(state, reset) -> EntityLoadActionpure function implementing the state → action mapping table - [ ] Integrate state machine into
InstantiatePhase: - [ ] Check DocumentStore for existing instance
- [ ] Compare definition hashes to determine state (
INSTANCE_CLEANvsINSTANCE_STALE) - [ ] Handle corrupt instances: move to
_quarantinecollection, log WARNING, create from definition - [ ] Enforce quarantine cap (
max_quarantine_entries, FIFO eviction) - [ ] Support
--resetflag forDELETE_INSTANCE_RELOADaction - [ ] Write unit tests for ALL state × reset combinations:
- [ ]
NEW→CREATE_FROM_DEFINITION - [ ]
INSTANCE_CLEAN+ no reset →LOAD_FROM_INSTANCE - [ ]
INSTANCE_CLEAN+ reset →DELETE_INSTANCE_RELOAD - [ ]
INSTANCE_STALE+ no reset →LOAD_INSTANCE_WARN_STALE - [ ]
INSTANCE_STALE+ reset →DELETE_INSTANCE_RELOAD - [ ]
INSTANCE_CORRUPT→QUARANTINE_LOAD_DEFINITION - [ ]
RESET_REQUESTED→DELETE_INSTANCE_RELOAD - [ ] Write unit tests for quarantine mechanics
3.7 Hot Reload Integration¶
Package: maid-engine | Priority: P1 | Dependencies: 3.3, 3.6
- [ ] Add
DATA_FILES = auto()to existingReloadScopeenum inpackages/maid-engine/src/maid_engine/reload/manager.py - [ ] Implement refuse-if-active reload strategy:
- [ ] Query entities from target file(s) via
DataProvenanceComponent.source_file - [ ] Check for active interactions by querying systems implementing
ReloadableSystem - [ ] If active: refuse with error listing active entities and their interactions
- [ ] Support
--forceflag:- Call
on_entity_force_destroyed(entity_id, reason)on eachReloadableSystem - Notify connected players
- Destroy and recreate from updated definition
- Post-reload reference re-validation
- Call
- [ ] Support
--skip-activeflag to reload only inactive entities - [ ] Implement
unload_pack(engine, pack_name) -> UnloadResult: - [ ] Identify entities via
DataProvenanceComponent.pack_name - [ ] Check cross-pack references (refuse if other packs reference this pack's entities)
- [ ] Check active interactions (refuse unless
--force) - [ ] Destroy entities from World
- [ ] Clean
ReferenceRegistryandTemplateRegistry - [ ] Write unit tests:
- [ ] Test refuse-if-active behavior
- [ ] Test
--forceteardown flow - [ ] Test
--skip-activepartial reload - [ ] Test
unload_packwith cross-pack reference checking - [ ] Test
unload_packregistry cleanup
Phase 4: CLI Tooling and Polish (Weeks 11–12)¶
4.1 maid data CLI Command Group¶
Package: maid-engine | Priority: P0 | Dependencies: 3.5
- [ ] Create
maid datacommand group in CLI (likely add topackages/maid-engine/src/maid_engine/cli/app.pyor newdata.pysubmodule): - [ ]
maid data validate <path>— run Phases 1–4 (no instantiation):--strict/--lenientmode flag--skip-rule <RULE_ID>(repeatable)--list-rules— show all registered semantic rules- Output: file-by-file error/warning report with summary
- [ ]
maid data lint <path>— run Phase 1 + lint portion of Phase 2 (Discover + Parse with lint):- Output: lint issues per file with line numbers
- [ ]
maid data preview <path>— run Phases 1–4 + dry-run summary:- Output: statistics (files, entities, templates, references) without creating entities
- [ ]
maid data load <path>— run all 6 phases:--resetflag for instance state reset--strict/--lenientmode flag
- [ ]
maid data resolve <template>— display template inheritance chain and effective definition (moved frommaid content resolve) - [ ]
maid data reload <path>— hot reload specific data files:--forceflag for active entity teardown--skip-activeflag
- [ ]
maid data unload <pack_name>— unload all entities from a content pack:--forceflag
- [ ] Write CLI integration tests for each command
4.2 maid data schema CLI Command¶
Package: maid-engine | Priority: P2 | Dependencies: 4.1
- [ ] Add
maid data schemacommand: - [ ]
maid data schema list— list all known component types with descriptions - [ ]
maid data schema show <component_type>— show component fields, types, defaults, and constraints - [ ] Output should be content-author friendly (not raw Pydantic schema)
- [ ] Write CLI integration tests
4.3 @export Builder Command¶
Package: maid-stdlib | Priority: P1 | Dependencies: 3.1
- [ ] Implement
@exportin-game builder command: - [ ]
@export rooms <filter> --output <path>— export rooms to YAML data file - [ ]
@export npcs <filter> --output <path>— export NPCs - [ ]
@export <entity_type> --zone <zone> --output <path>— filter by zone - [ ] Export captures definition-state only (original authored values from
DataProvenanceComponent, not runtime state) - [ ] Generate valid YAML with
_metablock,_idfields, proper component structure - [ ] Preserve
@ref:syntax for entity references - [ ] Write unit tests for export correctness
- [ ] Write round-trip test: load entities from YAML → export to YAML → re-load exported YAML → verify entity equality
Phase 5: Content Pack Migration and Integration Tests (Ongoing)¶
5.1 Tutorial World Migration¶
Package: maid-tutorial-world | Priority: P1 | Dependencies: 3.5
- [ ] Add
_metablocks to existing tutorial world YAML data files - [ ] Convert tutorial world
pack.pyto use data pipeline inon_load(): - [ ] Implement
DataLoaderPackprotocol - [ ] Return
EntityTypeConfigdeclarations for rooms, NPCs, items - [ ] Remove bespoke loading code
- [ ] Run
maid data validateagainst tutorial world data - [ ] Write integration test: load tutorial world via pipeline, verify entity creation
5.2 Classic RPG Migration¶
Package: maid-classic-rpg | Priority: P1 | Dependencies: 5.1
- [ ] Create YAML data files for spells, skills, and abilities (these currently exist as empty directories or Python-only definitions — no bespoke loader classes exist to replace)
- [ ] Add
_metablocks to new YAML data files - [ ] Register
SpellTypeConfig,SkillTypeConfig,AbilityTypeConfigviaget_entity_type_configs() - [ ] Register classic-rpg-specific semantic rules via
get_semantic_rules() - [ ] Update
EcosystemSystem._load_definitions()andWorldEventSystem._load_definitions()(both fully implemented, loading JSON fromdata/directories) to use pipeline - [ ] Run
maid data validateagainst classic-rpg data - [ ] Write integration test: load classic-rpg data via pipeline
5.3 Full Integration Test Suite¶
Package: maid-engine | Priority: P0 | Dependencies: 3.5
- [ ] Create test fixtures in
packages/maid-engine/tests/fixtures/data/: - [ ]
valid/rooms.yaml— valid room definitions - [ ]
valid/npcs.yaml— valid NPC definitions with references - [ ]
valid/items.yaml— valid item definitions - [ ]
valid/templates.yaml— template definitions with inheritance - [ ]
invalid/missing_meta.yaml— missing_metablock (implicit inference test) - [ ]
invalid/circular_extends.yaml— circular template inheritance - [ ]
invalid/unknown_component.yaml— unknown component types - [ ]
invalid/bad_references.yaml— unresolvable@ref:strings - [ ]
cross_ref/rooms.yaml+cross_ref/npcs.yaml— cross-file reference testing - [ ]
large/stress_1000.yaml— 1000 entities for performance testing - [ ]
security/path_traversal.yaml— path traversal attempt - [ ]
security/oversized.yaml— file exceeding size limit - [ ] Write end-to-end integration tests:
- [ ] Full pipeline: discover → validate → instantiate valid data
- [ ] Error collection: multiple errors across multiple files
- [ ] Template inheritance: 3-level chain with variable substitution
- [ ] Cross-file references: NPCs referencing rooms in different files
- [ ] Atomic rollback: partial failure rolls back entire file
- [ ] State machine: definition vs. instance reconciliation
- [ ] Security: resource limit enforcement, path jail
5.4 Performance Benchmarks¶
Package: maid-engine | Priority: P2 | Dependencies: 5.3
- [ ] Write benchmark tests (can use
pytest-benchmarkor manual timing): - [ ] 100 entities: target < 200ms
- [ ] 1,000 entities: target < 2s
- [ ] 10,000 entities: target < 20s
- [ ] Incremental reload (1 file changed): target < 500ms
- [ ] Document benchmark results
Files Created (Summary)¶
packages/maid-engine/src/maid_engine/core/ecs/
registry.py # ComponentRegistry (component type name → class mapping)
packages/maid-engine/src/maid_engine/loader/
__init__.py
models.py # Data models, EntityTypeConfig, LoaderContext,
# LoaderConfig, ErrorSeverity, LoadError, EntityLoadState,
# EntityLoadAction, resolve_load_action()
pipeline.py # Pipeline coordinator, Phase protocol, CancellationToken,
# PipelineResult, PhaseResult
protocols.py # DataLoaderPack, ReloadableSystem protocols
staging.py # LoadTransaction (staging EntityManager, commit/rollback)
assembler.py # EntityAssembler (generic, uses EntityTypeConfig)
entity_types.py # Standard EntityTypeConfig declarations (ROOM, NPC, ITEM, TEMPLATE)
resolver.py # TemplateResolver + ReferenceRegistry (eager snapshot)
security.py # is_safe_path(), resource limit validation
phases/
__init__.py
discover.py # DiscoverPhase (Phase 1)
parse.py # ParsePhase (Phase 2, integrated lint + TOCTOU re-verification)
prepare.py # PreparePhase (Phase 3, schema route + register IDs + validate + semantic)
resolve_refs.py # ResolveRefsPhase (Phase 4)
instantiate.py # InstantiatePhase (Phase 5, LoadTransaction)
post_load.py # PostLoadPhase (Phase 6)
rules/
__init__.py
builtin.py # MAID-S001, MAID-S005 (v1 minimal rule set)
Files Modified (Summary)¶
packages/maid-engine/pyproject.toml # Add ruamel.yaml dep
packages/maid-engine/src/maid_engine/core/ecs/entity.py # Add adopt() method to EntityManager
packages/maid-engine/src/maid_engine/core/world.py # Add adopt_entity() method
packages/maid-engine/src/maid_engine/core/engine.py # Add reference_registry, template_registry,
# create_data_pipeline() factory
packages/maid-engine/src/maid_engine/reload/manager.py # Add DATA_FILES to ReloadScope
packages/maid-engine/src/maid_engine/cli/app.py # Add 'maid data' command group
Deferred to v2+¶
The following are explicitly out of scope for v1 (per design doc §1 and review feedback):
- VS Code extension
- Migration tool (AST/runtime)
- Multiple inheritance / mixins (
_mixinssyntax) - Three-way definition/instance merge (field-level dirty tracking)
- TrackedComponent mixin (automatic Pydantic model_validator dirty tracking)
- Lazy zone loading
- Parallel file parsing (ProcessPoolExecutor)
- Conditional content loading (
_meta.conditions) - Localization framework
- Compiled binary cache (msgpack+zstd)
- JSON parsed-definition cache (SHA-256 manifest invalidation)
- DataFileWatcher (automatic file watching with stability checks, debouncing); use
maid data reloadCLI for v1 - DeclarativeContentPack / No-Code mode (no community content ecosystem exists yet)
- SkipGraph dependency cascade (flat error messages sufficient for v1 lenient mode)
- AI prompt injection lint patterns (runtime AI safety layer is the correct defense)
- TemplateRegistry Protocol abstraction (use concrete class until second implementation exists)
- ExitDefinition Pydantic model (validate exits against engine's existing structure)
- Property-based tests with
hypothesis(verify with parameterized unit tests instead)