Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Mod contract: packages, versions and linking

The mod contract is the set of WIT interfaces a mod’s component imports from the engine and exports to it. It is split into versioned WIT packages, one per owning program, that share one version (lockstep). This page is the current contract; the contract foundation design (pull request #332) has the full design and the steps still to land.

Packages

The WIT source lives in crates/manifold-wasm-abi/wit/, one directory per package:

DirectoryPackageInterfaces today
core/manifold:coretypes, text, ops (types only), logging; the plugin-lifecycle export
ecs/manifold:ecsresource-bundle; the plugin-systems export
world/manifold:%world (manifold:world everywhere outside WIT source)block-world
internal/manifold:internaltest-only probe interfaces (shapes, probe, the plugin-probe export); see Tracks
worlds/manifold:worldsthe worlds binding generators select: imports, host
legacy/game/manifold:game@0.1.0the contract 0.1 interfaces not yet moved: asset-source, clock, persistent-storage, ui, and the plugin export reduced to its three UI functions
legacy/client/manifold:client@0.1.0the client-plugin world, bound on its own
  • One version. Every contract package carries the version in wit/CONTRACT_VERSION (0.2.0); the legacy packages stay at 0.1.0 until they are retired. A track is a semver-compatible range: 0.2 holds 0.2.0, 0.2.1, …
  • No deps/. Every binding call pushes the package directories in the order wit/ORDER gives, dependencies first. manifold-contract (crates/manifold-contract/) reads ORDER once and provides it as WIT_PACKAGE_DIRS and the with_wit_paths! macro, which the host bindgen! uses; manifold-mod-macros (crates/manifold-mod-macros/build.rs) reads it for the guest bindings (see Exports).
  • %world. world is a WIT keyword, so the package is spelled manifold:%world in WIT source. The component binary, wasmtime linker names, Rust paths (manifold::world::block_world) and browser keys use manifold:world.
  • Shared types. core/types holds the identity and spatial records every package uses (block-pos, world-space-id, player-ref, entity-ref, …); core/text holds localized-text. A function-bearing interface shares only resource types with other interfaces; records live in types-only interfaces.
  • Owners. wit/ATTRIBUTES.toml records, per interface, the owning program, its consent class and the sides its functions work on, and per function its kernel profiles and the other("<code>") codes it reports through a shared error (other-codes). The build fails if a contract interface has no entry, or an entry names a function the interface lacks.

localized-text carries nested messages in a side table, because WIT has no recursive types: text(i) names entry i of nested, and an entry may only name later entries. At most 16 nested messages, depth 4. The conversion between the WIT record and LocalizedText is LocalizedText::to_flat and LocalizedText::from_flat in manifold-l10n-types (crates/manifold-l10n-types/src/flat.rs), shared by both hosts and the guest SDK (manifold_wasm_abi::text). A host receiving text that breaks these rules shows engine.text.malformed and logs the mod; the call never fails.

What a component imports

The component’s import section is the authority. manifold-contract’s component (crates/manifold-contract/src/component.rs) module reads it into one entry per imported manifold:* instance, with the exact version, the functions imported (resource methods included) and the resources. From them:

SetHoldsUsed for
Link setevery instance with a function or a resourcewhat a host must define for the component to instantiate
Consent setevery instance with a functionwhat an operator will approve (the consent step is not built yet)

The two differ for an instance imported only for a resource type: a mod that exports plugin-systems imports manifold:ecs/resource-bundle because run-system takes a resource-bundle-handle, even if it never calls a bundle method (block-world-smoke is such a mod). Such an instance must be linked, but the guest can call nothing in it. An instance imported only for records (core/types, core/text) is in neither set; hosts define nothing for it.

Linking

Native. ModInstance runs the import check, then links exactly the component’s link set through add_interface_to_linker (crates/manifold-wasm-host/src/link.rs), which is generated from the contract table: an interface in the table with no host implementation fails to compile. An interface with @unstable items takes wasmtime LinkOptions; the dispatch sets every flag, so preview items are always linked. The legacy manifold:game interfaces are still linked by the mod’s [caps] (ui always), and block_world, logging and resource_bundle in [caps] link nothing: remove them from manifests.

Import check. Before instantiating, the native host refuses a component that imports a contract interface the engine does not have on the import’s track, a track the engine does not link (the current one and each closed track in wit/frozen/, see Tracks), contract packages at two tracks, or manifold:internal (reserved for host tests). The legacy packages are outside the check. The check is manifold_modloader::contract::check_imports, written to run on both hosts; the browser receives a jco bundle rather than the component, so it runs the check once bundles carry a contract summary. Until then a browser import the host lacks fails at jco instantiate.

Browser. Every import key names its package’s track: manifold:world@0.2/block-world, manifold:game@0.1/ui. jco transpile emits these keys only when given the --map arguments of manifold_contract::component::jco_map_args (cargo mod jco-args <component> prints them). Every transpile in the repository passes them (cargo mod publish, game/flagship-web/build.rs, the parity harness’s build.sh), and a test fails if a file runs jco transpile without them. The web host inserts a shim for every contract interface the table links and records which keys jco reads at instantiate, which is the component’s link set; resource constructors come from the shim builders, not from the imports object. One shim object serves every linked track that has its interface unchanged: it is inserted under each of their keys. Exports are found by full versioned name with a track prefix (manifold:core/plugin-lifecycle@0.2.), newest linked track first, never by bare name.

Routing by import and role. A mod reaches per-interface host state because it imports the interface. Players’ block-edit requests go to the player-edit handler (Enki B §3.1), which a mod declares:

[block_world]
player_edits = "handler"

A mod whose component imports manifold:world/block-world and has no [block_world] table is the handler too (the legacy form); a table without player_edits takes no role. The orchestrator decides the handler at load (block_world_roles::player_edit_handler), never by load order: two handlers, or a handler without the import, fail the load naming the mods; player_edits = "filter" is refused until filters run.

Exports

A mod exports exactly what it implements, each export resolved on its own:

ExportPackageFunctionsRequired
plugin-lifecyclemanifold:coredeclarations() -> list<u8>, start(context: list<u8>) -> result<_, string>yes
plugin-systemsmanifold:ecsrun-system(system-id, borrow<resource-bundle-handle>) -> result<_, string>no
plugin (UI)manifold:game@0.1.0ui-manifest, build-ui-state, handle-ui-eventno; moves to manifold:ui/plugin-ui in M2

Lifecycle payloads. declarations returns one postcard ModDeclarations (crates/manifold-mod-types/src/lifecycle.rs): the shared, server and client declaration sections (each action carrying its kind, contexts and default bindings), command leaves, synced resources and engine-feature requests, at format 13 (DECLARATIONS_FORMAT_VERSION). It is pure, so the host reads it once per instance. start takes a postcard StartContext (the instance’s side). Both are envelope payloads (envelope.rs (crates/manifold-mod-types/src/envelope.rs)): a leading format_version, read before the body. The host migrates a supported older format forward and refuses a newer or retired one with a diagnostic that names the mod (“rebuild the mod against the current SDK”, or “update the game”). Format 13 is also the oldest read: a format-12 mod is refused at load (WasmHostError::DeclarationsFormat, with both versions and the player message engine.mod.declarations_too_old). The host encodes StartContext at the version the mod’s declarations name (start_context_version), so an older guest never receives fields it cannot decode. Growth happens inside these payloads, by a new format version; the export itself never changes within a track.

Loading. Native: the host builds the linker, calls instantiate_pre, resolves plugin-lifecycle (a component without it is refused), then each optional export, on the newest linked track the component exports it on. Component::get_export_index tells an absent export (“not implemented”) from one present without a function (refused, naming the export), before the generated GuestIndices::new is called. A component’s exports and contract imports must be on one track. The host calls every contract export through its facade (see Tracks): ModInstance holds Box<dyn PluginLifecycle> and Box<dyn PluginSystems> and never names a track. Browser: the same lookups by track prefix on jco’s root object. Calling an export the mod does not implement is WasmHostError::NotExported. Both hosts bring an instance up through declare_and_start: declarations, the declarations/exports check, the side’s manifest, start.

Declarations and exports agree. A mod’s declarations must not need an export it does not implement. The check is table-driven, exports_check::EXPORT_REQUIREMENTS (crates/manifold-wasm-host-core/src/exports_check.rs), one row per optional export, and refuses a mismatch naming both sides:

DeclarationRequires export
any system in ModDeclarationsmanifold:ecs/plugin-systems (systems)
ui = true in [caps]the UI export (ui)

A mod is a UI backend if and only if it exports the UI functions; ui = true without them is refused. The loader runs the check on both hosts, and cargo mod build runs it on every game module it builds (below), warning about an export nothing declared uses. A program adding an optional export adds its row in the same change ([[extends]] needs plugin-extensions, [jobs] kinds need plugin-jobs, a reviewer role needs plugin-review); a test fails while an optional export of the contract table has no row.

cargo mod build extracts a game module’s declarations before packing: it instantiates the component with every import but the pure kernel profile (logging) trapping (kernel::extract_declarations (crates/manifold-wasm-host/src/kernel.rs)), calls declarations, decodes them and runs the check. An SDK that called another import while declaring would trap there, naming it.

Guest SDK. #[mod_entry(exports(systems, ui))] on the Plugin struct names the optional exports; plugin-lifecycle is always exported and wired to the SDK (guest_glue::declarations, guest_glue::start), systems to the registered system table, and ui to the struct’s ModUi impl. An unknown name is a compile error. The macro emits wit_bindgen::generate! with an inline world that includes manifold:worlds/imports and exports those interfaces, and a with: map sending every import to the shared bindings in manifold-wasm-abi, so the component imports only what its code calls: a mod without ui imports nothing from manifold:game/ui. The paths, the with: map and the full @unstable feature list are computed from the WIT tree by manifold-mod-macros‘s build script, which also emits the shared bindings’ own generate! (abi_bindings!), so both read one list. A guest that implements its exports by hand uses manifold_wasm_abi::guest_bindings!(exports(...)), which emits the same bindings module (manifold_guest, with its export! macro).

Module sets

A session runs one module set (Enki B §10, issue 40; ModuleSet (crates/manifold-modloader/src/bundled.rs)), resolved dependencies first with ties broken by mod id, so the server and every client get the same list. A module comes from one of three sources:

  • Baked into the build (BundledModSource: manifest, component and asset archive). The flagship builds bake the flagship; flagship-game-bundle’s enki-stub feature adds the Enki stand-in for the multi-module tests and the browser gate.
  • Installed in the host’s mods directory: a .mod archive, or an unpacked directory whose assets/, locales/ and data/ are packed into one archive when the set is resolved. An installed copy with a baked module’s id replaces the baked copy. Two installed copies of one id are refused naming the mod.
  • Delivered by the server a client joined, over CH_MODS, cached by SHA-256.

The set holds one source per id, and every module in it is required: its declarations are in the engine, so a module that fails to load or traps stops the session naming it. Each module’s archive is its content for the session (digest, data, catalogs, models, particles, sounds, textures).

  • Who runs which set. A host that runs a world resolves its local set (manifold_wasm_host::resolve_local_modules): the baked modules and the game modules installed in its mods directory (client plugins and translation packs never join). When the full set does not resolve or fold because of an installed module, it boots the baked modules and installed overrides of them instead and logs the disabled mods (LoadOutcome::RequiredOnly). The server’s orchestrator loads exactly the set (load_set), the handshake advertises it, and its .mod modules are what CH_MODS serves. Native singleplayer and hosting share that set with their client, so nothing is missing at the handshake and nothing is delivered.

  • Joining a server. A joining client’s local set is its baked modules. For each module the server runs that it lacks, it uses a cached artifact or an installed .mod whose SHA-256 is the server’s artifact (no transfer), or has it delivered; an installed copy with other bytes is not used. The session set (ModuleSet::with_delivered) is keyed by the server’s ids, so an installed and a delivered copy never both load.

  • Session content (joining clients, Enki B issue 40 phase 3). When the session set differs from the client’s own, the connect builds the client’s content from it before any world data applies, in its own phase (ConnectPhase::BuildingContent, bounded by CONTENT_BUILD_TIMEOUT, behind the SessionContentBuilder (crates/manifold-host-core/src/session_content.rs) seam). The connect order is: handshake; mod fetch (CH_MODS, or the cache); session content build; the module set check, then every registry hash against the session content; the client world built on it; only then the join snapshot and the gameplay frames buffered meanwhile. The build runs the boot’s steps in the boot’s order: fold, block registry (fluids, light), data stage, engine (actions, components, archetypes, resources), asset overlay, atlas, models, particles, sounds, catalogs. Natively, flagship::client_content (game/flagship/src/client_content.rs) is that one build for the boot and the session (a worker thread at connect); the host then swaps engine, registry, texture map, the renderer’s block texture array and icons, the mesh, light and far-terrain workers, input config, models, particles, sounds and catalogs. In the browser (session_content.rs (crates/manifold-host-web/src/session_content.rs)) the build is an async step: delivered jco backends instantiate from the OPFS-hydrated cache, every set module’s declarations fold, the session engine builds, and at accept the driver swaps the client orchestrator, engine, texture addresses (the renderer is built after the accept), sounds and catalogs. A build that fails or times out ends the connect naming the mods and the step (ConnectError::SessionContent, handshake-module_content_failed); a build whose hashes still differ from the server’s is refused naming the delivered mods (“mod X differs”). A client without a builder (tests) keeps the #521 data-only path, where a delivered module with registry content fails the hash checks, named. Browser singleplayer runs the baked set (no installed modules yet), so it never delivers.

  • Fold. Every host folds the list through one function, fold_modules (crates/manifold-wasm-host-core/src/module_content.rs) (natively behind manifold_wasm_host::fold_module_set, in the browser over its jco backends): blocks, input actions, novel components, archetypes, host resources, role bindings, the player archetype (two modules declaring one is an error) and terrain recipes. The data stage, catalogs (locales/), model files and asset overlays take every module, each archive under its own mod id.

  • Digests. Each module’s content digest (module_digest (crates/manifold-wasm-host-core/src/module_digest.rs)) covers its declarations payload and the member index (name, size, CRC-32) of its archive’s assets/, locales/ and data/ entries, so separately compiled native and browser builds of one module, and its baked zip, .mod and browser delivery artifact, agree. The server advertises them (WorldInfo.game_module_digests, protocol 54); a content-hash mismatch names the modules whose digests differ (“mod X differs”, handshake-module_mismatch).

  • Write attribution. The tick driver tags every guest entity write, insert, remove and resource write with the mod whose system made it last; each mod’s write-back applies only its own share, under its own declared access.

  • Default bindings. Two actions sharing a default chord both stay bound; the engine reports the pair with each action’s module (Engine::default_binding_conflicts, input spec §11.5).

The browser’s WebGame.extra_modules lists the modules a game bakes beside its own; its client and server worker build one orchestrator over all of them (ModOrchestrator::from_loaded_backends), and the worker advertises the real set.

Tracks

The host links the current track and every closed track it still supports (during 0.x, the previous one). A closed track is a frozen copy of the tree at its last released patch, wit/frozen/<track>/ with its own ORDER, written by the release that closes it (none yet). manifold-wasm-host’s build script (crates/manifold-wasm-host/build.rs) runs contract-adapters (tools/contract-adapters/src/lib.rs) over the trees and generates, so nothing is maintained by hand and nothing goes stale:

  • Bindings: one bindgen! per closed track, bindings::v0_N, with every host resource and every unchanged types-only interface mapped to the next track’s Rust types with with: (wasmtime type-checks records and variants structurally), and imports trappable.
  • Adapters: each closed track’s import Host traits, implemented for HostState by converting the arguments to the next track by name (record fields, variant cases, flags), calling that track’s implementation and converting the result back. A value the older track cannot represent traps, naming it. Adapters chain 0.N → 0.N+1 → … → current.
  • Facades: one trait per contract export (crate::exports (crates/manifold-wasm-host/src/exports/mod.rs): PluginLifecycle, PluginSystems) in current-track types, with one implementation per linked track and a …Indices::resolve that picks the track. An older track’s implementation converts the arguments down and the result up; an argument the guest’s track cannot represent (a variant case added later) is FacadeError::CannotReceive, and the call is skipped as if the export had returned an error (“<mod> (contract 0.N) cannot receive …”).
  • Dispatch: each closed track’s linker arms, beside the current track’s.

A change the generator cannot bridge (a record gaining a field, a function gaining a parameter) fails the build naming the type; that interface is adapted by hand under capability/frozen/v0_N/ and listed in wit/frozen/<track>/HANDWRITTEN. In the browser, jco objects are untyped: an unchanged interface’s shim serves every track and an export’s facade is the track-prefixed lookup; jco’s own lowering refuses a variant case the guest’s type lacks before entering it.

Closed exports. Within a track a patch adds, never changes: new interfaces, new functions in imports, new types. An export interface is closed: a patch adds a new export interface, never a function to a released one, because a host resolving an export looks up every function it knows and an older guest lacks the new one. contract_compat.rs (crates/manifold-wasm-abi/tests/contract_compat.rs) compares the open tree with wit/frozen/<current track>/ once a version of the current track is released (wit/RELEASED.toml) and fails on any removal or change, and on any export that gained or lost a function. The breaking-change gate applies the same comparison to contract.json, which adds the schema payloads.

manifold:internal proves the machinery before any track is released. wit/internal/ is the current track and wit-synthetic/0.1/internal/ a synthetic closed one (its plugin-probe.command lacks the halt case 0.2 added). Probe guests built against each are checked in as bytes, tools/test-mods/contract-fixtures/internal-probe-<track>.component.wasm (rebuilt by that directory’s build.sh). Hosts link the package only with the contract-internal feature, which host tests and the parity harness enable. contract_tracks.rs (crates/manifold-wasm-host/tests/contract_tracks.rs) runs both guests in one linker and one store, the 0.1 guest through the generated adapter and facade; the parity harness runs them through jco.

Kernel profiles

A kernel or hook instance of a mod’s component links only a profile of the contract and traps every other import. Membership is per function in wit/ATTRIBUTES.toml (kernel = { log = ["pure", "read-windows"] }), exposed as Function::profiles and manifold_contract::profile_functions. KernelPlan (crates/manifold-wasm-host-core/src/kernel.rs) (wasm-safe) splits a component’s link set into interfaces linked whole and interfaces linked in part. Natively, kernel::link_kernel (crates/manifold-wasm-host/src/kernel.rs) applies it: allow_shadowing(true), define_unknown_imports_as_traps, each whole interface by its add_to_linker, then each partial instance opened once with its resources defined, the profile’s functions registered one by one (a per-function registration generated from the current track) and its other functions trapping with a message naming them. Talos G’s kernel loader (Enki DECISIONS E4) builds its linkers with it. The browser has no trapping linker: Talos G’s browser loader is to build, from the same plan, an imports object holding the profile’s shims and, for everything else, a Proxy whose every property throws; that half is not built yet.

Payloads

Every list<u8> in every package, current and legacy, is classified in wit/PAYLOADS.toml (crates/manifold-wasm-abi/wit/PAYLOADS.toml), which manifold-contract generates into manifold_contract::payloads (crates/manifold-contract/src/payloads.rs):

KindHoldsDefined by
Schema payloada postcard value of one Rust type[schemas.<id>]: the type’s path, its owner, and its versioning: envelope (a leading format_version: u16 the host migrates forward, with the version and the constant holding it) or track (append-only, versioned by the package’s track)
Packed layouta little-endian buffer of fixed-stride records[layouts.<id>]: the heading of the document holding its layout table, its owner, and optionally schema, the path of a const manifold_schema::LayoutSchema (crates/manifold-schema/src/layout.rs) holding the same table
Opaque bytesa shape the platform cannot know: asset files, a mod’s config, component values keyed by TypeId64[opaque.<id>]: who owns the description and where it lives

Each site names the list<u8> it classifies: <package>/<interface>.<item>#<root><steps>, for example manifold:core/plugin-lifecycle.start#context. The item is a function (resource.method for a method) and the root a parameter or result; [] enters a list element, .0/.1 a tuple field and .err a result’s error, while options and a result’s ok add nothing. A list<u8> field of a record or variant is classified once, where the type is defined: manifold:core/types.<record>#<field>.

Registering a payload type. A schema payload type lives in the crate that owns it (manifold-mod-types, manifold-ui-protocol, a program’s api module); neither manifold-contract nor manifold-mod-types depends on those crates. To publish a function that carries one:

  1. Add [schemas.<id>] with the type’s path from the extern prelude (rust = "manifold_physics::api::PhysicsOp"), its owner and its versioning, and a [sites] entry for every list<u8> that carries it.
  2. Derive manifold_schema::Schema on the type and every type it contains (see Describing a payload).
  3. Add the owning crate to manifold-contract’s [dev-dependencies] (with default-features = false if its defaults pull a runtime). tests/payload_types.rs (crates/manifold-contract/tests/payload_types.rs) is the one place that names every payload type: it expands for_each_schema_payload! into each type’s path, so an entry naming a missing type or constant, or a type without Schema, does not compile, and it checks that each type is a serde value and each envelope constant equals the table’s current.

The schema id, not the Rust path, is the payload’s name in the contract, so a type can move between crates without a contract change. The description tools bind every schema the same way: schema_payload_nodes!() expands to one SchemaPayloadNode { id, node } per schema payload, and packed_layout_schemas!() to one PackedLayoutSchema { id, layout } per layout that names its table; the contract.json generator (M5) reads both.

Describing a payload

#[derive(Schema)] (manifold-schema) describes a serde type as serde sees it, which is also what its postcard bytes are: field names after #[serde(rename)], which fields may be omitted, each enum variant with its serde variant index (its position in the declaration), fixed arrays, tuple structs and byte strings. Two attributes add what serde does not say:

  • #[schema(index = n)] on a variant pins its index. The derive refuses a pin that is not the variant’s position, so inserting a variant before a pinned one fails to compile instead of renumbering the wire. Every enum reachable from a track payload carries pins.
  • #[schema(since = "0.2.0")] on a field or variant records the contract version that added it.

A field whose type cannot implement Schema (a foreign type) takes #[schema(with = path)], a function returning its description; a byte buffer behind an alias takes #[schema(bytes)]. A value whose shape is not decided yet is SchemaNode::Pending with the work that will describe it (manifold_schema::pending("C P0 (enki/c-p0): …")): today the two ron::Value fields of the UI tree, until Enki C P0’s typed UiValue. Leaf::Custom names a value with a hand-written form and says nothing about its bytes, so it is a last resort; payloads use none. tests/payload_shapes.rs (crates/manifold-contract/tests/payload_shapes.rs) lists every pending node and custom leaf payloads may hold, reports each pending node as unchecked, and refuses others.

That test checks each description against serde: it writes postcard bytes from the description alone, rotating through every variant, decodes them as the Rust type, re-encodes them to the same bytes, and compares every field and variant name, and every leaf value, with what serde_json writes for the decoded value.

contract.json and the reference

contract.json (crates/manifold-wasm-abi/wit/contract.json) describes the open contract for tools: every package, interface, function and type in ORDER with its WIT docs, @since and @unstable gates, and from the contract table its kind (import, export, types-only), owner, consent class (per interface, and per function as its interface’s class), sides, kernel profiles and documented other codes. The legacy 0.1 and internal packages have sections of their own. The payloads section has every schema payload with its full postcard shape (the named types it reaches, their fields, variant indices and since), the packed layouts and the opaque bytes, each with its sites. A part not described yet is {"pending": "<the work that will describe it>"}, never left out. The file is byte-deterministic: sorted keys, no timestamps or paths, \n line endings.

tools/contract-describe writes it, and from it the generated reference (one page per package, the payloads, a changelog from @since and the released snapshots) and docs/reference/llms.txt:

cargo run -p contract-describe            # regenerate after a contract change
cargo run -p contract-describe -- gate    # the breaking-change gate

cargo test -p contract-describe fails when a checked-in file is stale (MANIFOLD_REGEN_REFERENCE=1 rewrites them instead) and runs the breaking-change gate: the open contract.json against wit/frozen/<track>/contract.json, the snapshot of its track the release froze, through contract_adapters::compare, the same differ as the compatibility check. Beyond the closed-export rule it reports a removed or changed function or type, a case removed, added or moved to another index, a field removed, added, moved or retyped, and, for schema payloads, the same at the level of each type: a track payload may append a variant at a new index (never a field: postcard reads exactly the declared fields), and an envelope payload may change once its format version is bumped. It lists every pending part as unchecked. Until a version of the open track is released it reports “no released version on track 0.2: nothing to compare”.

Adding to the contract

A new interface goes in its owning program’s package at the open version, with an ATTRIBUTES.toml entry, a native host implementation (the linker dispatch will not compile without one) and a browser shim in manifold-wasm-host-web’s registry (crates/manifold-wasm-host-web/src/shims/mod.rs) (the web crate fails to compile without one). Every list<u8> it adds needs a PAYLOADS.toml entry (Payloads). A new export’s function list is complete at its first release (Tracks); it gets a facade generated with no further work, and a row in the declarations/exports table (Exports). A function a kernel profile may call gets its kernel membership in ATTRIBUTES.toml.

Regression coverage

CheckWhere
Lockstep versions, one package per directory, topological ORDER, unique interface names, type sharingcontract_layout.rs (crates/manifold-wasm-abi/tests/contract_layout.rs)
Every linked interface has a native dispatch armlink.rs (crates/manifold-wasm-host/src/link.rs) tests
Every linked interface has a browser shimcompile-time check in shims/mod.rs (crates/manifold-wasm-host-web/src/shims/mod.rs)
Import check, link and consent setsmanifold-modloader contract tests (crates/manifold-modloader/src/contract.rs), block_world_roundtrip.rs (crates/manifold-server/tests/block_world_roundtrip.rs)
Nested localized-text round tripmanifold-l10n-types tests (crates/manifold-l10n-types/tests/localized_text.rs), manifold-wasm-abi::text (crates/manifold-wasm-abi/src/text.rs)
Every list<u8> classified, every classification names a site and a defined payloadmanifold_contract::payloads (crates/manifold-contract/src/payloads.rs) tests
Every schema payload names an existing serde type that derives Schema, and its envelope constant; every named layout table is valid; only the listed layouts lack a tablepayload_types.rs (crates/manifold-contract/tests/payload_types.rs)
Every schema payload’s description matches its serde shape, variant by variantpayload_shapes.rs (crates/manifold-contract/tests/payload_shapes.rs)
contract.json and the generated reference are current; the breaking-change gate, each break class on a fixturecontract-describe tests (tools/contract-describe/tests/)
Every jco transpile passes the map argumentsjco_invocations.rs (tools/cargo-mod/tests/jco_invocations.rs)
Block edits and reject-edit identical on wasmtime and jcoweb_parity.rs (crates/manifold-wasm-host/tests/web_parity.rs) (browser CI job)
Per-export resolution: lifecycle required, optional exports absent or refused when malformed, declared systems need the systems export, guests export and import only what they implementexport_split.rs (crates/manifold-wasm-host/tests/export_split.rs)
ModDeclarations and StartContext round trips, the envelope’s version gate and migrationlifecycle.rs (crates/manifold-mod-types/src/lifecycle.rs), envelope.rs (crates/manifold-mod-types/src/envelope.rs) and manifold-wasm-host-core’s orchestrator (crates/manifold-wasm-host-core/src/orchestrator.rs) tests
The export split in the contract tablemanifold-contract (crates/manifold-contract/src/lib.rs) tests
Generated adapters, facades and registration over the synthetic tracks; the closed-export rulecontract-adapters tests (tools/contract-adapters/tests/generate.rs)
An older guest’s imports through the adapter and exports through the facade, one resource type under two instance names in one linker, a case the older track lacks never sent, a kernel profile linking part of an instancecontract_tracks.rs (crates/manifold-wasm-host/tests/contract_tracks.rs)
The same probe guests through jco, compared with nativeweb_parity.rs (crates/manifold-wasm-host/tests/web_parity.rs) (browser CI job)
A released track only grows; exports are closedcontract_compat.rs (crates/manifold-wasm-abi/tests/contract_compat.rs)
Every optional export has a declarations/exports row; mismatches name both sides; ui = true needs the UI exportexports_check.rs (crates/manifold-wasm-host-core/src/exports_check.rs) tests, export_split.rs (crates/manifold-wasm-host/tests/export_split.rs)
Declarations extract with every import but logging trappingexport_split.rs (crates/manifold-wasm-host/tests/export_split.rs), and cargo mod build of every game-module test mod
Module sets: load order, one fold per host, digests and “mod X differs”, the declared edit handler, per-mod write attributionenki_s1_multi_module.rs (game/flagship/tests/enki_s1_multi_module.rs), assert-enki-modules.mjs (crates/manifold-host-web/e2e/assert-enki-modules.mjs) (browser CI job), block_world_roles.rs (crates/manifold-wasm-host-core/src/block_world_roles.rs), module_content.rs (crates/manifold-wasm-host-core/src/module_content.rs) and tick_driver.rs (crates/manifold-wasm-host-core/src/tick_driver.rs) tests