PreFab Registry — Design¶
Status: Ruled. Written after stages 1–7 landed; Everett's rulings recorded 2026-07-20 (§5, §7). Implementation may proceed per §6; nothing is implemented yet.
1. Motivation¶
Stages 1–7 made prefabs world content: a template is an entity with a
Prefab marker, component values, and a ChildOf subtree; instantiate
copies it and records IsA lineage; editing a template never mutates
instances, so both generations sit on the ledger. What is still missing is
the library layer: how agents discover prefabs, how versions are named, how a
prefab binds to the eval suite that grades it, and how templates move between
worlds. That layer is the registry, and it closes the hill-climb loop: agents
author prefabs, the ledger grades them, validated prefabs become the floor
for the next generation.
2. What the ledger already provides¶
Most classic registry machinery is unnecessary here, for the same reason most of Sander's relationship roadmap was: the substrate keeps everything.
- Versioning is ticks. A prefab's identity is
(world_id, entity_id); its version is the tick at which its values were read. Edits are new rows, so every version that ever existed is queryable, and "pin to a version" means instantiating from a historicalGraphViewslice. - Lineage is edges.
IsArows already record which instance came from which template, and the edge's own tick records when. Population-level provenance queries are joins. - Fitness is queries. Grading a prefab is instantiating it into a lab
world, running a scenario, and grading from history — the autoresearch
pattern. Fitness rows key naturally by
(prefab, version tick).
The registry therefore adds only four genuinely new things: names, cross-world provenance, eval binding, and an explicit declaration of the behavior modules required to interpret an asset's schemas.
3. Proposed decisions¶
R1 — A prefab library is a world¶
No package format, no store. A library is an ordinary world whose population
is templates. It forks, grades, resumes, and is discovered like any world.
Cross-world instantiation already has its seam: instantiate(world, view,
prefab) takes the target handle and the source view separately, so
"import" is instantiating from a library world's view into a consumer world.
R2 — IsA gains cross-world provenance payload¶
Today IsA.target is an entity id, which is world-local: an instance
imported from a library world holds a dangling reference. IsA grows payload
fields — world: str = "" (source world id; empty means same-world),
run_id: str = "" (the source run that persisted the copied row — one
world's history spans runs, so a tick alone under-identifies a version), and
at_tick: int = -1 (the captured tick) — so lineage is complete and durable
across worlds: the full version coordinate is (world, run, tick). This is a schema change to a shipped component and
must land before adoption spreads (#543's lesson: schema evolution against
persisted tables bites).
R3 — The manifest is an artifact plus a typed registry row¶
A registry entry stores its immutable manifest through the registered
artifacts-family handler and lets the owning registry workflow publish its
typed fields through StorageService (per docs/guide/artifacts.md): name,
source (world_id, run_id, entity_id, tick), required behavior-module
identities, the component set with prefixed schema hashes, the subtree
inventory, the eval-suite reference, and evaluation-result identities.
Publishing a prefab version creates one file occurrence plus one typed
prefab-manifest row keyed to its artifact_id. No new storage system, generic
ingestion facade, or artifact publication protocol is required.
R4 — Schema identity is the compatibility contract¶
Imports match components by name plus prefixed schema — the same
_same_component rule stages 4–7 converged on. A consumer whose component
class has drifted from the manifest's schema hash fails loudly at
instantiation (the #543 failure becomes a named, expected error instead of a
FieldNotFound deep in a query). Migration is explicit: re-author the
template or adapt the consumer; the registry never coerces silently.
R5 — Eval binding is part of the name¶
A registry name without a grading story is advertising. The manifest's
eval-suite reference is mandatory; FLAG-style validations (cycle checks,
schema drift, orphaned lineage) run as evals over the library world, and a
version without evidence receipts is visibly ungraded in the index.
R6 — Code registration precedes asset loading¶
Biome imports C modules before evaluating asset scripts. The same ordering is required here: a host installs approved component classes, processors, resources, and hooks before it loads, imports, or evaluates prefab content. Those registrations are executable process state; prefab entities and rules are durable world state. They are related, but they are not one registry.
A prefab manifest declares its required behavior modules and their schema identities. It never embeds a callable, imports a module by an untrusted string, or treats code as automatically executable asset data. The host resolves each declared requirement through an allowlisted composition root, installs a fresh world-local registration, and then checks the manifest's component schemas under R4. A cold resume repeats code registration before the first step; it does not reconstruct processors or resources from ledger rows.
This preserves the useful half of Flecs' two-layer model without turning the
artifact index into a plugin loader. Family examples may provide convenience
factories such as the example-local register_biome_rts(), while durable asset
authoring remains a separate operation such as
author_prefab_library(world). That example bundle is not the reusable
registration contract; the PrefabLibrary model in §7 is.
R7 — The relation-copy boundary¶
instantiate copies component values, recursively copies the ChildOf
subtree, rebuilds only ChildOf edges, and records IsA provenance. It
copies no other relation — catalog structure, assignments, sockets, supply
lines — and exposes no source-to-instance id map. Domain wiring belongs in
rule entities interpreted by a driver or service after instantiation. If
enough families need arbitrary graph cloning, the sanctioned broadening is a
separate InstantiationResult(root_id, id_map) API with explicit
relation-copy policies; instantiate is never silently widened. The Biome
example's AssetChildOf remains example-local until a catalog relation is
deliberately generalized under archetype.prefabs.
4. What stays out¶
- No central naming authority beyond the namespace directory (§7); cross-org trust and signing are future work.
- No automatic migration of drifted schemas (R4 forbids silent coercion).
- Processor-native instantiation is IN scope once the mutation-outbox seam
lands: ruled on #604 (2026-07-20), design in
docs/design/mutation-outbox.md— designed, not implemented. - No declarative prefab file format yet (Biome's
.flecslayer); Python authoring through the library object comes first. - No automatic execution of code named by a manifest. Module resolution is an explicit, allowlisted host-composition decision (R6).
5. Rulings (Everett, 2026-07-20)¶
- Naming ownership — hybrid, split by rate of change. The control
catalog holds only the namespace directory (library name → library world
id): small, authoritative, RBAC'd, rarely changing. Prefab names below a
namespace are world content (
Prefab.namerows), unique-per-library via a FLAG eval, versioned by ticks, forking with the library. Resolution is two hops: catalog for the namespace, ledger for the name at a version. IsApayload — land now. Necessary for cross-world lineage; the installed base is a week old, so the schema change is nearly free today and expensive later (#543's lesson). First implementation step.- Family placement —
archetype.prefabsis its own family, separate fromarchetype.graph(mechanics stay in graph, governance in prefabs); manifest models + frame-pure index readers in the family, the binding service underapp. Registered via its ownquality/architecture.d/prefabs.tomlfragment.
6. Implementation sketch (rulings applied)¶
IsAprovenance payload + migration note (R2) — small, ships first.PrefabLibraryauthoring object inarchetype.prefabs(§7): emit and register components/relations/processors/hooks under a namespace; publish prefab entities into a library world; install into consumer worlds.- Manifest models + schema-hash and required-library capture in the family (R3, R4, R6).
- Namespace directory in the control catalog + binding service under
app(§5.1); publish/lookup composes the registered artifacts handler andStorageService(R3). -
604 MutationOutbox seam in core (rulings on the issue), then¶
processor-native instantiate. - Eval-binding conventions + library-world validations (R5).
- Cross-world import example: a library world feeding the Biome RTS reference example (#603).
7. Registration model (ruled 2026-07-20)¶
Biome's registration is two layers: C modules (ECS_IMPORT) register
component types, systems, observers, and hooks under a module namespace;
.flecs scripts then declaratively compose prefabs from those registered
parts, namespaced by blocks (buildings.Solar). Archetype adopts the same
split with what already exists:
- Layer 1 — the library object (code).
PrefabLibrary(name)is the authoring surface: it emits and registers the primitives you build with — component and relation classes, processors, hooks — under its namespace, and carries the prefab-authoring API. Installing a library into a world is theECS_IMPORTanalog and rides the declarative world config (R6: processors, resources, hooks in declared order). Registration is explicit, not import-magic. - Layer 2 — the library world (data). Prefab entities composed from layer-1 parts, exactly what stage 7 built. Publishing writes them and the manifest artifact; the catalog's namespace directory points at the world.
Sketch of the authoring surface:
units = PrefabLibrary("vangelis.units")
@units.component
class Chassis(Component):
armor: int = 10
@units.processor
class Locomotion(AsyncProcessor): ...
harvester = units.prefab("harvester", Chassis(armor=42))
world = runtime.world("sim", **units.install()) # layer 1 into a world
await units.publish(library_world) # layer 2 + manifest
Collision rule: component names are registered under the library's namespace in the directory at publish/install time; installing two libraries whose component names collide fails loudly at install, before any table exists. No column-prefix mangling — the prefix convention stands.
Biome's auto-wiring (EcsWith pairs, on-add hooks assigning building bits)
uses existing registration shapes: a library can bundle OnSpawn/
OnComponentAdded hooks and counter resources. That does not make lifecycle
hooks transactional. Their failures are logged rather than aborting a
mutation, so correctness-critical auto-wiring must use explicitly authored
components, pure processors, or the ruled mutation-outbox/application seam.
Biome's declarative script layer is deferred (§4).
Security rule: a manifest may identify the PrefabLibrary implementation
required to interpret its component schemas, but it never embeds a callable or
causes a module named by an untrusted string to execute. The host composition
root resolves declared library identities through an allowlist and installs
them explicitly. Neither the common artifact index nor the typed prefab index
is a plugin loader.
Lifecycle rule: registrations are process-local and world-local. The same approved library bundle must install atomically for a new world and again before the first step of a cold-resumed world. New-world configuration already has the required processor/resource/hook shape; cold-resume parity remains runtime-contract work and must not be bypassed through internal services.