This note defines the cross-runtime composition below Lace-100: who owns a participant, operation, store, transport, and public projection. The numbered specifications define observable semantics. Mechanism-specific architecture notes own state transitions, policy compilation, store queries, durable layouts, wakes, and foreign store-result construction.
The implementation has four ownership layers:
lace-sans-io owns records, canonical Datalog
validation/evaluation, fixed exchange lowering, ILTP codecs including
065 stream encodings, and synchronous interlace machines.Lace, LaceHandle,
operation drivers, asynchronous store and transport execution, waits,
cancellation, deadlines, and public value projection.Lace owns
policy-neutral persistence, search, load, and store access. Each
physical root owns its record identities, admission order, and
authoritative committed state.lace-compiler compiles one complete self-contained
Lace-120/130 source into an ordinary facet-bearing policy and depends on
the core, never the reverse. Runtime source conveniences call it;
precompiled execution, including porcelain and policy replacement, does
not require it. Applications own file/code loading; the compiler owns no
IO, resolver, evaluator, or persistent compilation lifecycle. The core’s
n-ary canonical constraint composer also serves precompiled porcelain
and is available without the compiler feature. It rejects an empty
authoring collection and each helper-only input, isolates helpers,
intersects contributed selection and exposure facets, unions vetoes,
confines guest inputs to the authoring exposure view, and removes
records matching field exclusions.
Compiler availability is a build boundary, not a policy mode:
lace-rt and lace-tokio enable their
optional compiler feature by default. Without it,
operations accept prepared canonical InterlaceModule
values; source-string conveniences and compiler re-exports are
absent.lace-ffi has a default-off optional
compiler feature. Canonical policy, record, store, codec,
and interlace surfaces remain available without it.compiler. npm run build:wasm -- --no-compiler
builds the canonical-only artifact; compiler entry points are
unavailable, not replaced by JavaScript parsing.lace-compiler directly and
projects its compilation functions and prepared policies.Stream-encoding codecs are likewise a build boundary, with the
features Lace-100
defines; the JavaScript WASM build and PyO3 receive them through
lace-ffi. The compiled-in set fixes the default connected
offers and the tokens connected construction accepts.
Core interlace and Datalog machines own no store, socket, task,
timer, callback, worker, public event queue, or language runtime. The
lace-sans-io::runtime_support module may provide
synchronous policy-neutral stores, store-building tools, and codecs,
including native filesystem IO and the IltpCodecState
shared by lace-rt and Tokio byte-stream transports.
Concrete runtimes own asynchronous execution and communication between
those parts.
The optional lmdb-lace workspace crate implements the
existing synchronous store operation boundary and depends on
lace-sans-io. lace-rt, Tokio, FFI, and PyO3
enable it with their default-off lmdb feature; each retains
its direct sans-IO dependency. There is no reverse dependency, public
low-level LMDB API, or new store protocol. Its physical profile is owned
by native-lmdb-store.md.
A runtime drives typed machine effects and inputs. It does not mirror semantic phase, reinterpret ILTP item families, duplicate policy selection, or maintain a second state graph.
Encryption and transport establishment belong to applications. Lace
adopts established byte IO with Disabled,
ConfidentialChannel, or
ChannelBound(ChannelBinding) assurance; it implements no
encryption suite or handshake. Generic v3 proof validation signs and
compares the immutable opaque 32-byte binding. Its derivation and
protected-IO semantics belong to the channel implementation under
Lace-050/060; ordinary Seal construction is unchanged.
A Lace is one non-clonable runtime participant
owner. It exclusively adopts one concrete store object together with one
immutable position_may_advance_externally boolean and
owns:
StorePosition
publication;next_poll_at, poll_in_flight,
and external probe lifetime when the adopted boolean is true;false guarantees position advances only through
mutations executed by that Lace. true means an outside
advance is possible; it enables fixed internal polling and leaves
CurrentStorePosition as the sole position authority. The
boolean is adoption metadata, not a StoreOp, runtime fact,
protocol input, or operation option. Concrete constructor assignments
are owned by store-record-fact-access.md;
Lace-100 owns the public custom-store surface.
A native filesystem Lace exclusively owns its root; an independent
opener fails on lifetime-lock contention. LMDB permits cooperating
processes to open one root, with one environment per root per process.
Same-process operations share Lace handles. Lace construction publishes
its initial position only after authoritative
CurrentStorePosition succeeds; an unknown or malformed
initial result fails construction as Store rather than
inventing an empty position, and initial publication failure fails
construction as Integration. No participant is exposed
after either failure.
Optional Rust Lace::lmdb(path) and Python
Lace.lmdb(path) constructors establish the initial
authoritative position and adopt with external advancement enabled. LMDB
permits only one independent environment per root per process;
same-process callers share Lace handles. LMDB child processes exec
before reopening and do not use or destroy inherited C state.
Forking with live native filesystem store or Lace objects is unsupported. Applications fork before constructing these objects or exec before reopening; children do not use inherited owners.
A LaceHandle is a clonable opaque capability for that Lace. It duplicates no store, lifecycle, position source, socket, transaction, or protocol machine. Every operation retains one handle for each local participant position. Two handles may refer to the same Lace in direct composition.
A LocalPeer is a clonable local participant: one LaceHandle plus one prepared policy operand. It owns no IO or claim state and is never taken as a connected input. Route construction replaces its position’s operand with its own before claiming, and an open direct operation retains that operand for replacement.
A ConnectedPeer is a separate move-only one-use
capability that atomically adopts host-established IO and explicit
carrier role, optional By-secret, proof assurance, stream encodings,
optional port, and optional expected peer By. Bound
assurance requires a local secret and an owned channel binding. There is
no required public intermediate transport object. Connected input
acceptance transfers that resource into the invoking call or future
before prepared driver construction. A bounded operation settles it; an
open operation retains it until explicit close, clean peer closure,
failure, or drop. It is not a Lace and does not own a local store.
Rust’s by-value call/future ownership already precedes internal participant claiming. An unpolled Tokio future owns its moved inputs but has driven no protocol; drop releases them without async graceful shutdown. JavaScript and synchronous Python take genuine connected inputs at body entry, before fallible preparation or user-extensible input processing. Python argument binding happens first; JavaScript takes resources before its first await. Lace-100 defines the exact acceptance and invalid-input cases. None of these boundaries moves protocol activation or policy replacement staging ahead of preparation.
Dynamic peer transfer removes the owned payload from the public wrapper. The invocation retains it through private participant allocation, policy preparation, and driver construction, then either publishes the owner or disposes the payload. The driver owns the transport directly, not a source wrapper with privileged cleanup access. An emptied wrapper’s close is harmless and cannot close or wait for its recipient. Distinct available inputs are taken once even on invalid routes; rejected reuse never touches an incumbent operation. Private branding identifies ownership, not arbitrary getters or duck-typed close hooks. Existing payloads and setup locals suffice; no lifecycle registry or additional ownership layer is needed.
InterlaceSide, CarrierRole, and
ParticipantPosition remain distinct:
InterlaceSide::{Side0,Side1} identifies protocol
ownership;CarrierRole::{Listener,Connector} fixes connected
side0/side1 assignment;ParticipantPosition::{First,Second} identifies public
application argument position.Runtime drivers perform the mapping. Protocol side and carrier role are not public participant identity or authority.
Explicit Lace close is the final waitable participant boundary. It:
An external-position probe failure (Store) or
confirmed-position publication failure (Integration)
atomically poisons the Lace. Poison wakes every subscriber with the same
retained error and rejects every later store execution or participant
claim. Close still drains already-started non-cancellable work before
returning that first error. Ordinary operation-local store failures do
not poison the Lace.
Native close and drop perform no posting-list checkpoint. Filesystem close releases its lifetime kernel lock after active work settles. LMDB close releases only that process’s environment; other processes may continue using the root.
Rust consumes Lace (async in Tokio and
synchronous in lace-rt). JavaScript and Python expose
repeatable language-level close calls that wait for or observe the same
completed boundary. Python strongly retains each directly registered
open operation until the operation unregisters or Lace close attempts
every registered close; that explicit boundary clears the registrations
and breaks the ownership cycles before releasing the store.
Independently owned operation handles remain separately closeable and
cannot start new access after their Lace closes.
In runtimes with deterministic destruction, final owner drop is the
non-waitable resource-safety fallback. Handles, operations, and
already-started work retain the concrete participant resources they can
still access. The native filesystem lifetime lock remains held until
those owners release the store. JavaScript requires explicit Lace close
for a deterministic boundary; process or realm termination releases Web
Locks but may leave a Node sentinel. Low-level C/WASM free
functions destroy allocations only; they are not participant close.
Once transport establishment completes, one host operation owns
exactly one semantic InterlaceMachine plus its
runtime-specific execution state: participant handles, pending store
work, transport and codec ownership when connected, deadlines,
cancellation, waits, and bounded public queues. Tokio,
lace-rt, JavaScript, and Python implement those drivers
independently; there is no shared runtime context, scheduler, handle
table, or generic asynchronous executor.
A direct machine owns two endpoint machines and routes typed peer output between them. A connected machine owns one carrier/session composition and uses the runtime-owned byte transport. These are variants of one semantic operation owner, not two application APIs.
An open operation has one exclusive mutable owner for event driving and policy replacement. One-event reading is canonical; iteration and run-to-boundary helpers consume that reader rather than starting another driver. The handle retains terminal failure independently of one-shot error delivery.
Open and observer handles are explicitly closeable. Rust uses
consuming close(self) (async in Tokio);
JavaScript and Python use their ordinary explicit close forms. After a
connected peer is claimed, callers signal runtime-local operation
cancellation rather than closing its IO directly. Close cancels the
semantic machine, prevents further driving or replacement, waits for
already-started store and authoritative-refresh work through durable
position publication, and settles connected transport ownership.
Retirement of an operation that started an external probe does not
cancel publication or Lace-wide poisoning. Rust returns the final local
Closed(Cancelled) boundary appropriate to the handle when
no terminal failure wins. Drop performs the same retirement without a
waitable result. Dropping a borrowing Tokio event-read future does not
drop its operation; dropping a task handle does not cancel its task.
JavaScript promises and Python garbage collection supply no equivalent
waitable destruction boundary.
Setup protects resources as they are acquired, including foreign error/output allocations, codecs, private participants, and transferred transport. If a later construction step fails, cleanup releases already-acquired resources even when no driver was returned. Python covers BaseException interruption as well as ordinary exceptions. Its store workers receive owned StoreWork, never the machine or transport ownership. An interruption during mandatory draining is deferred through that boundary, not treated as proof that the worker stopped. Retired work retains its Lace through durable classification, refresh, and publication, without machine submission.
JavaScript open setup is async so construction failure can await cleanup before rejecting. Success returns a constructed handle, not proof establishment or fixed point. Observer setup uses that same settled constructor rather than collecting detached setup-cleanup promises. Synchronous Rust/Python and Tokio retain their natural synchronous and future-based opening forms respectively. No asyncio surface, eager wrapper, task or finalizer is added for API symmetry.
Explicit close attempts every owned cleanup action and waits for required work, including active observer event projection before capture release. It retains primary and distinct cleanup failures through the existing exception/error channel. JavaScript uses AggregateError for combined failures; typed projections retain its causes, not just its message. Replaying a retained failure does not add it again. Repeatable close retains one pending/settled result, not a second cleanup status. After driving starts, bounded collectors preserve partial results and expose cleanup failure as a structured failed terminal; setup, empty-store disposal, and open failures use their call error channel. Cancellation presence is separate from its reason value, including falsy JavaScript values. Canonical event consumption gates both existing waiters and new reads on required cancellation settlement, so queued success or exhaustion cannot overtake it. No cleanup result certifies remote acknowledgment or physical release by an arbitrary source/sink.
A bounded operation owns teardown internally and returns only its
outcome; Tokio’s owning BoundedInterlace also offers
consuming close, so a caller that stops driving can wait for settlement.
It reaches complete only after semantic fixed point and
required final output and transport settlement. Participant capabilities
are not returned with outcomes; callers retain reusable local access by
cloning a LaceHandle before starting the operation. Empty store still
releases its accepted connected input without creating an interlace or
producing observations. Public porcelain entry points pass accepted
resources to private runners, not another consuming public entry point.
Argument-shape conveniences are separate binding choices; JavaScript
array/promise shapes do not constrain Rust or Python collections.
Detailed carrier/session transitions, endpoint effects, deadlines,
backpressure, EOF, replacement cuts, and raw transport completion are
owned by interlace-transition-tables.md.
Direct composition receives two local Lace handles, two prepared
policy operands (the operation’s operand for each bare handle position,
a LocalPeer’s own operand for its position), explicit final
SessionAuthority values, and one limits value. It
constructs a DirectPair and starts both endpoint machines
without sockets or ILTP byte encoding. Typed peer output still crosses
the same semantic machine boundary; direct composition never copies
records between stores or bypasses store operations.
The two handles may refer to different Laces or to one Lace. Same-Lace composition uses one mutation/refresh sequence, one external-poll cadence when enabled, and one retained position-or-terminal-failure fanout. It adds no protocol-visible store identity or special interlace path.
Connected composition receives one claimed connected participant and
either one local Lace handle with one operand, which takes the
participant’s expected peer By, or, in the Rust runtimes, a
selector alone, which supplies each logical session’s operand and
expected peer By, requires a participant without one, and
with the first session’s operand supplies the local handle. The
connected participant fixes carrier role, proof assurance and any
binding, optional expected peer By, the local By-secret
input, stream encodings, port, and a fresh nonce seed. The driver
answers each SelectOperand poll from that selector; the
core owns no callback. Until the first answer a selecting driver owns
the transport, its codec reserved for a staging store, and the prefacing
deadline, but no handle, subscription, staging, or store work; the first
answer installs the handle, hands its operand cache to the carrier, sets
the codec’s staging, and subscribes to its retained state, and a later
answer naming another handle is refused. Foreign hosts open on a given
handle and select operands only. Endpoint configuration and diagnostics
remain outside policy facts, plan and proof inputs, and protocol
authority. Rust owns prefacing, proof and operand establishment,
plan/session state, semantic validation, and per-direction
stream-encoding selection and state; the runtime owns byte movement,
codec driving, IO, and transport settlement. The codec produces each
accepted batch in chunks of at most its configured chunk size and
decodes compressed peer bytes into a buffer of that size. It retains
control items but not records: a record item passes through the
incremental record validator, and the codec hands out its prefix, then
its data pieces, then its verified header. The codec’s owner writes each
piece to its record sink before decoding continues. Before each input
turn the runtime passes the carrier’s peer_record_ingress
to the codec state, which checks each record at its start, before it
reaches memory or a sink, and routes it as the transition
tables define: establishment records, proofs and requested operand
Blobs, go to memory whatever the store; an outstanding response goes to
the store’s record sink; a record during reset prefacing is discarded,
its length handed to the carrier as a PeerInput. A store
with staging (the native filesystem and portable file stores) supplies a
private staging directory through its StoreBackend; a
connected operation receiving into it writes each record to its own
staging file and syncs it before it becomes a file-range record, which
the store copies at admission in bounded pieces. Other stores receive
into memory: one allocation of the record’s canonical length, which the
record then owns. Only the verified header turns a sink into a record,
so nothing unverified reaches the carrier, facts, store or observations;
a failed or partial record is dropped with its sink, which removes its
staging file. A file-range record being sent is read in pieces of at
most one chunk. The codec state hands staging writes, syncs and these
reads to its owner as one blocking job at a time and neither decodes nor
produces output until the job returns: lace-rt runs it on
the driving thread and Tokio on its blocking pool, keeping the task
across interrupted waits. Ingress reads nothing more meanwhile. A
staging or read failure is a Store failure of the
operation. A direct pair has no codec and encodes nothing. A record
stream reader receives each record through the same validator and record
sink, without ILTP framing, running its staging jobs the same way; its
record-start check is the stream’s remaining byte bound.
The runtime owns these buffers’ size and budget. The codec state
allocates each chunk buffer on first need: one retained read buffer the
transport reads into directly, a compressed direction’s decoded input,
the output chunk, a file-range record’s read piece, and with staging the
piece a staging write holds (or a record’s metadata prefix).
IltpCodecState::chunk_buffer_bytes bounds them. Each Rust
runtime owns one process-wide io_budget::IoBudget and
creates the codec with its configured chunk size when a connected
operation is set up, reserving that bound from the budget. Releasing the
transport’s IO, after completion, closure, failure or cancellation,
drops the codec and the reservation. lace-rt runs blocking
jobs on the driving thread, so none outlives its transport; a Tokio
blocking job holds a share of the reservation, so a released carrier’s
buffers return once its job settles. A Lace passes the chunk size to its
store at adoption through StoreBackend::adopt and, when the
store has staging, reserves the store’s copy buffer until the store’s
close has run.
Direct composition accepts explicit final authority because both
endpoints are inside one trusted composition. Connected authority is
freshly derived from a live protocol proof together with a host
confidential-channel assertion or a binding supplied with locally
established protected IO; a host cannot inject final peer authority.
ChannelBinding::new([u8; 32]) owns the opaque binding bytes
for that channel. Lace trusts the channel implementation’s derivation
and IO, but verifies the bound proof itself rather than accepting a
provider-returned peer By. Policy operands and executable provenance
follow the ownership contract in interlace-policy-lowering.md.
A fixed-operand carrier retains its immutable expected peer
By and supplies it to every fresh session; a selecting
carrier supplies each session’s selected one. Rust establishment
compares the validated peer proof before advancing its proof phase;
foreign hosts do not inspect proofs or perform a parallel check. Lace-100
owns public inputs and outcomes, Lace-050
owns proof validation, and Lace-060
owns the wire grammar.
Both compositions expose the same public events, errors, bounded outcomes, open boundaries, and porcelain semantics.
The store is a policy-neutral
StoreOp -> StoreResult | StoreError boundary. It owns
admitted bytes and coherent search, load, and store execution. A native
physical root owns its root-wide
StoreRecordId/StorePosition; each operation
observes that authority through its backend’s coherent read boundary.
The store does not receive policies, exposure viewers, Datalog joins,
protocol sides, public collectors, open-operation registrations, or wake
callbacks.
An endpoint machine owns at most one active StoreCall.
The runtime executes that request against the Lace selected for the
endpoint. The Lace owns a mutation through durable classification,
mandatory current-position refresh, and position publication before
successful machine submission. The runtime submits every completed
response, including a StoreError, to the machine, which
owns the resulting terminal; only a failed mandatory refresh or
confirmed-position publication fails the operation in the runtime. Every
other same-Lace store call waits outside the
mutation-through-publication interval.
When the adoption boolean is true, fixed-point drivers consult the
Lace-owned poll state. The first eligible driver starts one Lace-owned
CurrentStorePosition refresh; other operations wait on
retained Lace state or the shared next time rather than queueing or
joining the probe. Polling occurs only while a relevant open operation
is fixed, never for bounded operations or active rounds. The refresh is
runtime work, not a machine StoreCall, and uses the same
publication gate and retained signal as mutation. Failure of the
external probe or any confirmed-position publication poisons the Lace
and wakes all subscribers. A terminal machine retires its call, but
already-started non-cancellable store or refresh work may retain the
Lace long enough to classify durable progress. It never submits to the
retired machine, and close returns the retained first terminal failure
after draining that work.
Compaction composes the sans-IO RetentionSweep machine
with one concrete store outside any Lace. The compaction call opens the
root’s store under the root’s rendezvous lock, executes each sweep
StoreCall through that store’s ordinary operation execution
(on the calling thread in lace-rt and Python, which
releases the GIL, on a blocking thread in Tokio, and asynchronously in
JavaScript), and then passes only the kept set to the store’s physical
compaction entry. No Lace, position publication, polling, or interlace
operation exists during the call, and the store receives no policy.
Sweep, files, locks, and recovery are owned by store-compaction.md.
This paragraph is the cross-runtime ownership rule, not the
response-building contract. Semantic operations, query/fact projection,
coherent reads, and store trust are owned by store-record-fact-access.md.
Move-only StoreWork, result construction, reserve/commit,
ticket validation, and late foreign completion are owned by store-request-response-ffi-boundary.md.
Open fixed-point registration and wake races are owned by open-store-wakes.md.
Filesystem commit, recovery, lock, and index mechanics are owned by filesystem-shaped-store.md.
A runtime atomically adopts host-established ILTP IO into a one-use
ConnectedPeer. Applications own endpoint configuration and
parsing, dialing, listening, and connection acceptance. There is no
shared endpoint enum, configuration, parser, dialer, or listener API.
Construction fixes CarrierRole::Listener or
CarrierRole::Connector before decoding. Paired endpoints
use complementary roles; IO shape and accepted or dialed provenance do
not infer role. The capability owns no Lace, policy, listener, process,
admission limit, hidden background task, or service lifecycle.
from_duplex / from_split adopt either one
runtime-native duplex IO owner or independent peer-to-local reader and
local-to-peer writer owners. They default to Disabled proof
assurance. Process stdin/stdout and child pipes use that same split
composition; applications explicitly transfer their IO owners.
ConfidentialChannel is the host’s assertion about the
complete byte channel, not transport protection supplied by Lace. The
full-route assurance requirement and the distinction between proof
freshness and channel binding are owned by Lace-050.
Endpoint configuration and diagnostics are not policy facts, plan or
proof inputs, or protocol authority.
The accepting call or future owns connected IO through prepared construction and operation settlement. Bounded completion settles final output, a compressed direction’s end mark, and shutdown as the transition tables define, then releases raw IO; an open operation retains the IO at fixed point. Runtime-local cancellation, failure, clean peer closure, and drop retire it through the connected lifecycle. Callers do not retain a post-claim IO backdoor. Applications continuously drive listeners, open or adopt process IO, accept and admit connections, spawn and supervise interlace operations, and stop and join them. A service may retain one Lace while replacing listener tasks. Raw transport constructors do not own a generic Lace server, process supervisor, admission policy, reconnect loop, or application shutdown graph.
Applications complete protected-channel establishment before connected adoption. They own handshake deadlines and cancellation; no pending encrypted-establishment state exists in the runtime driver. The prepared operand constructs an ordinary ILTP carrier using the supplied immutable binding and secret. Synchronous policy replacement changes only ILTP state, preserving the established channel.
An IO adapter’s flush completes when the channel implementation has accepted ownership of all preceding plaintext within bounded buffering; it then owns any encryption and delivery. The ordinary retained writer preserves the final accepted batch’s phase deadline and withholds fixed-point publication until that flush settles. The adapter supplies bounded authenticated input and independent read/write/flush progress. Transport protection errors become IO failures, not clean EOF. Shutdown settles owned output and reports its failure; failure or cancellation aborts IO. Lace-050 owns these IO obligations and the transition table owns the driver cut.
Empty-store disposal releases adopted IO without ILTP or an authentication claim; it does not claim the application performed no earlier transport handshake.
Core, browser, and Node entries share
ConnectedPeer.fromSplit over standard Web Streams and
ConnectedPeer.fromWebSocket over an already-open standard
WebSocket. Both use one byte-stream transport and the existing connected
carrier driver; there is no Node-specific connected driver or public
transport object. The Node entry adds filesystem adoption only.
The byte-stream transport locks one reader and writer, requests input only when the driver needs it, retains one bounded chunk without read-ahead while it is in use, and retains one write until its sink accepts ownership. The driver keeps requesting legal peer input while the writer owns a batch, so two endpoints writing at once never wait on each other’s reads. Source/sink implementations own physical IO cancellation, bounded buffering, delivery of accepted output, and immediate rejection of active work on errors. Generic stream conversions do not necessarily provide bounded or interruptible-write semantics.
The WebSocket adapter supplies those streams. It installs listeners
synchronously on adoption, accepts binary messages, and uses a bounded
byte queue. Since standard WebSockets expose no drain event, a fixed
internal timer checks bufferedAmount only while output is
pending, bounding the platform buffer to one write. The write promise
resolves at zero; it is not a peer acknowledgment. Close after
successful drain initiates the socket handshake and releases local
ownership; abort cancels local pending work and initiates closure
without draining. Already platform-owned bytes cannot be recalled.
Timers and listeners are retired with the adapter, and a socket cannot
be adopted twice.
The safe Rust binding layer projects typed machine values for foreign
hosts. The C/raw-WASM ABI is a thin ownership and value projection over
that layer; PyO3 wraps the same safe Rust types directly instead of
calling the C ABI. Native Rust hosts use lace-sans-io
directly.
The binding layer exposes opaque interlace, private one-item codec, record, policy, and move-only store-work parts needed by runtime implementors. It owns no Lace, store adoption, scheduler, worker, future, socket, route map, current-position watch, public event buffer, or application lifecycle. Every owning foreign pointer destroys only its Rust-owned allocation.
The C/WASM ABI version is defined in ABI.md; the
Python extension version gate is in python/lace/_native.py.
Each host checks the exact version for its surface before other calls or
type use. There is no alternate-version projection.
lace-ffi owns the C/WASM declarations and shared safe-Rust
inventory. python/crates/lace-py owns the PyO3 module
inventory and python/lace/_lace_py.pyi.
Policy composition borrows the complete authoring and guest collections of prepared-policy owners and the field exclusions, and calls the core n-ary composer once. The C/WASM boundary accepts one array of policy pointers and its length per class and one array of borrowed exclusion texts, not two policy operands to fold repeatedly. JavaScript and Python use the same collection semantics. Inputs remain caller-owned; success returns an independently owned policy. Source compilation takes complete UTF-8 text and a source-byte bound and returns a prepared policy directly.
Core canonical preparation and module construction preserve the
existing PreparationError through shared
LacePolicy and PyO3 APIs. Canonical C/WASM construction and
composition project malformed or invalid policies as
Policy, and fixed rule/arity/static TopK bound failures as
LimitExceeded, including without the authoring compiler.
Interlace and ILTP entry points likewise carry the core’s typed category
for construction, replacement, and machine failures (an oversized local
operand is LimitExceeded on both routes) and
PeerProtocol for decoder failures. Foreign hosts do not
recover categories from strings. This is local preparation: an invalid
received operand remains PeerProtocol at the carrier’s
peer-input boundary.
Foreign hosts own side-to-Lace routing, asynchronous store and external-position refresh execution, durable position-or-terminal-failure publication, transport IO, operation lifecycle, and public projection. They consume typed poll and error values; they do not infer state or error categories from diagnostic strings. One common WASM artifact contains no transport implementation.
JavaScript/WASM record-memory and OPFS ownership are defined in js-opfs-wasm-memory-layout.md.
The C/WASM and shared safe-Rust inventory is documented in ../../core/crates/lace-ffi/ABI.md.
The PyO3 module inventory is defined by
python/crates/lace-py/src/ and
python/lace/_lace_py.pyi.
Lace-100 owns the cross-runtime application contract. Runtime projection maps core observations to exactly these public families:
RecordSent { position, record_hash };RecordSaved { position, record_hash, record } on open
streams, without record in bounded outcomes;FixedPoint; andClosed { reason } where a lifecycle stream exposes
closure.Only record observations carry first or
second. Round numbers, protocol sides, phase generations,
fixed-point identities, store positions, wake tokens, and transport
settlement causes remain internal diagnostics. Replacement creates no
event.
Runtime errors preserve the shared typed categories
PeerProtocol, LimitExceeded,
Timeout, Policy, Store,
Transport, and Integration, with public
participant position, record hash, and timeout phase only when present.
Rust’s existing Error preserves its primary detail and
exposes additional failures through additional_errors();
std::error::Error::source() exposes the first additional
failure. Clones retain failure identity for replay suppression;
independently created equal diagnostics remain distinct. Combining
errors preserves occurrence order and typed details before any
application redaction. Errors are terminal values, not observations.
Bounded wrappers project core completion into complete,
stopped-by-limit, or failed and retain
observations produced before the terminal. Open wrappers project
fixed-point and close boundaries while retaining the stream for later
rounds. Direct and connected drivers use the same projection rules.
get, list, store,
store_stream, and for_each are runtime-local
operation compositions. They create ordinary private participants, drive
ordinary interlace, and collect public observations and outcomes. The
core RecordSaved observation carries the admitted
Record; collectors and open streams take it there at poll
time instead of reloading it from the private store, while bounded
outcome events keep only its hash. Their collectors stay in each
concrete host because they own runtime waits, IO, cancellation, and
lifecycle; lace-rt and Tokio share only the pure terminal
and outcome builders over already-collected observations in core
runtime_values. Stores and core machines do not own
porcelain collectors, and public applications receive no raw store
access through them.
See ../spec/100-DX-OVERVIEW.md
for exact event, outcome, close, and porcelain semantics.
lace-tokio runs each store call inline or through
spawn_blocking, as the adopted StoreBackend’s
execution marker requires; it owns Tokio tasks, readiness, transports, a
retained watch carrying position or terminal failure, Lace-scoped
single-flight polling for externally advancing stores, and async close
around the shared machines.lace-rt owns its Mio reactor, synchronous operation
loop, transports, a retained position-or-terminal-failure state that
wakes all existing Mio subscribers, Lace-scoped single-flight polling
for externally advancing stores, and close without depending on Tokio.
Adopted writers implement WriteShutdown (provided for Mio
TCP and Unix streams and pipe senders); bounded completion and clean
peer closure call it once after the final flush and drop a split writer,
so a pipe ends too. Bounded completion reads the peer direction to its
end while it writes the end mark and shuts down, and releases the IO
once both finish; a shutdown failure fails the operation unless that
direction ended cleanly.FixedPoint event.StoreWork or position refresh on its store
worker with the GIL released as appropriate; publication and machine
delivery remain with the driving thread.lace-ffi supplies runtime-implementor parts for C and
raw-WASM hosts. It is not another application runtime.All hosts preserve the same participant, operation, store, transport, and public projection boundaries while using runtime-appropriate scheduling and IO.