Tags: dx, event-stream, porcelain, developer-contract
Lace’s durable truth is record bytes, record facts, Datalog policy, exchange plans, and interlace convergence. Applications should not need protocol phases or store internals to use that truth. This document defines the shared semantic shape of normal application surfaces above specs 010–060 so Rust, JavaScript, Python, and future runtimes do not drift into different developer models.
Lace-100 is authoritative for cross-runtime developer semantics: record construction intent, participant boundaries, interlace and porcelain behavior, event lifecycle, and operation outcomes. A language binding may use language-idiomatic spelling where this document shows multiple names, but a name change does not change the underlying operation or semantics. Specs 010–060 remain authoritative for protocol behavior.
Developer APIs compile to or operate through the records, policy, exposure, and interlace semantics defined by 010–060. They do not replace those semantics with remote commands, raw database access, WebSocket-specific objects, or mutable application truth shortcuts.
A validated record can be presented as an immutable row: the hash is
the row identity and fields are columns. A projection chooses fields
such as Group, App, Name,
TAI, By, and Data-Length;
repeated fields appear as an ordered sequence rather than one
scalar.
This table shape does not make rows mutable, make
Group/App/Name unique, or expose
raw store iteration. Rules and selector DSLs define dynamic row subsets,
including subsets that depend on evidence, links, manifests, or current
name records. Interlace converges the records selected by both
sides.
App-facing constructors accept one record specification and infer the stored record form from caller intent:
By-secret value present -> Seal record
app, name, group, fields, tai, By-secret value -> coordinate-bearing intent
coordinate-bearing intent -> Plex/Seal record with missing coordinate parts defaulted
only data present -> Blob record
omitted group -> "u"
omitted app -> "none"
omitted name -> "record"
omitted tai -> tai_now()
omitted data -> empty bytes
The default coordinate for coordinate-bearing creation is
//u//none//record. A data-only specification remains a Blob
record; coordinate defaults apply only after the caller provides a
coordinate part, fields, tai, or a By-secret value.
String data is UTF-8; binary APIs accept their
language’s ordinary byte value. App-defined fields use an ordered
sequence so repeated values retain caller order. Constructors reject 010
reserved extra-field names. A By value alone cannot create
a Seal because it provides no By-secret value.
Rust record surfaces use ByValue for canonical public
V.<b64a>.H3 text and one zeroizing
BySecret owner for canonical private
&.<b64a>.H3 text; BySecret parses
and emits only that canonical form. ByValue::parse(...)
accepts canonical text, ByValue::from_verification_key(...)
performs the low-level 009 translation,
BySecret::by_value() derives the corresponding
ByValue, and Record::by() returns an optional
ByValue. JavaScript, Python, C, facts, and wire surfaces
carry the same canonical strings rather than raw key bytes or a second
public wrapper.
Foreign record-construction surfaces use the same direct names
without aliases: JavaScript record specifications use
bySecret, Python uses by_secret, and C uses
lace_generate_by_secret and
lace_record_builder_set_by_secret. CLI key generation
prints canonical By-Secret: &.<payload>.H3 and
By: V.<payload>.H3 lines, and record-building options
use --by-secret.
An exact coordinate is sugar for group,
app, and name. In one object or builder state,
coordinate is mutually exclusive with explicit coordinate
parts. During template merging, a later coordinate replaces earlier
parts and later explicit parts replace coordinate-derived components.
The final merged specification is validated once.
Creation returns a validated Record, not raw bytes.
Exact Blob, Plex, and Seal byte constructors are advanced or
protocol-test surfaces.
A runtime package exposes store-backed Lace participants, connected peer participants, interlace, and porcelain:
Rust/native: Lace::filesystem(...), Lace::memory(...)
JS/core: Lace.memory(...)
JS/browser: Lace.opfs(...), Lace.browserLocalStorage(...), Lace.openBestEffort(...)
JS/Node: Lace.filesystem(...)
open_interlace / openInterlace(first, second, policy) -> open handle
bounded_interlace / boundedInterlace(first, second, policy) -> bounded outcome
list(target, selector_or_coordinate, options) -> ListOutcome
get(target, selector_or_coordinate, options) -> GetOutcome
store(target, records, options) -> StoreOutcome
for_each / forEach(target, selector_or_coordinate, options) -> observation stream
Language bindings may adjust names that collide with host-language built-ins. Operation options share the five 050 limits: received control bytes, transferred record bytes, evaluation work, mandatory rounds, and phase timeout. They are local and are not negotiated with a peer. Fixed parser/wire caps and transport buffer sizes are not operation options.
Ordinary bounded and open interlace are duplex. Their policy argument is an operand template, not a merged executable or prepared exchange plan. When one runtime represents both store-backed participants, it instantiates the template in both operand slots. A direct interlace supports same-Lace composition, where both participant arguments refer to one Lace, without changing interlace behavior or outcomes. For a connected peer, the local participant contributes one operand and the peer contributes the other as the role-assigned operand Blob. Advanced per-slot builders may exist, but the ordinary shorthand retains these semantics.
An operand may derive TxDisabled/0 or
RxDisabled/0; there is no separate transfer-direction
option. One runtime-owned Lace is the non-clonable
participant owner of one exclusively adopted store, mutation
publication, current-StorePosition watch, and lifecycle.
LaceHandle is its clonable opaque participant capability
and duplicates none of that state. Operations retain a handle for each
local participant, including both participant positions in same-Lace
direct composition. Bounded interlace, list,
get, and store return only their outcomes, not
participant handles. Callers that need reusable local capabilities
retain or clone handles before the call; a claimed connected participant
is consumed and settled by a bounded operation. Neither Lace nor
LaceHandle exposes a raw store, socket, transaction, or protocol
machine.
The owning Lace has one final close boundary. Rust consumes
Lace (close().await in Tokio and
close() in the single-threaded runtime), JavaScript awaits
Lace.close(), and Python calls Lace.close().
Close first prevents new work through every retained handle, requests
closure of operations directly owned by bindings that provide that
relationship, and waits for every already-started store read or mutation
to finish its durable classification and confirmed position publication.
It then checkpoints and releases the adopted store and its profile lock.
Independently owned operation handles remain separately closeable and
can no longer begin store access. A retained handle after close is
invalid; it does not retain access to released resources. Repeated
language-level close waits for or observes the same completed boundary.
C ABI destructors and low-level wrappers around raw WASM allocations
retain free() solely for allocation destruction; that
operation is not Lace participant lifecycle. This includes advanced
IltpItem, IltpEncoder, and
IltpDecoder wrappers.
Without explicit close, final owner drop keeps store resources and physical exclusion alive while any handle, operation, or already-started work can still access them. Releasing the final such owner releases the store, so a durable store path can be reopened immediately in the same process. Close and final drop do not probe PIDs, delete lock files, sleep, or retry.
A connected runtime accepts an optional local By-secret value and one optional peer-proof override:
Rust: ConnectedPeerOptions::new().with_by_secret(by_secret).with_peer_proof(PeerProofOverride::TrustConfidentialChannel)
JS: connectWebSocket(address, { bySecret, peerProof: 'disabled' | 'trust-confidential-channel' })
Python: Lace.connect_websocket(address, by_secret=secret,
peer_proof=PeerProofOverride.TRUST_CONFIDENTIAL_CHANNEL)
CLI: --by-secret &.<b64a>.H3 --peer-proof disabled|trust-confidential-channel
Omission uses the transport default: QUIB is channel-bound, WSS is a
trusted confidential channel, and WS/TCP/Unix/stdio are disabled. Each
connected logical session starts with an ILTP 1.0 nonce preface pair,
then runs role-ordered optional Seal proofs and complete operands. Rust
freshly freezes Here(local_key), proof-derived
PeerConfidential(peer_key), and
Transport(address) before policy evaluation.
PeerConfidential(peer_key) discloses that the live peer
proved control of the matching By-secret value and that the complete
session channel is confidential, integrity-protected, and
endpoint-confined. Connected callers cannot inject final peer authority.
A direct pair remains a separate trusted composition with explicit final
SessionAuthority values.
One connected carrier may carry successive logical sessions, each with one immutable interlace policy and plan. An ordinary open interlace exposes hard replacement through the same exclusive mutable handle:
Rust/Tokio and Rust/single: open.replace_policy(next_policy)
JavaScript: open.replacePolicy(nextPolicy)
Python: open.replace_policy(next_policy)
The argument uses the same authoring/frame surface as opening the interlace. Preparation failure leaves the interlace unchanged. Success is synchronous and means only that an equal operand was a no-op, the current prefacing operand was replaced, or one latest replacement was staged for a running owner. The call does not poll or step, retire a session, execute store or transport IO, touch an accepted writer batch, consume or produce events, or wait for activation. A later successful call may supersede the staged operand before the next drive step.
At the next poll or step, before endpoint work or fixed-point watch delivery, a connected owner consumes the staged operand, retires the complete logical session, and begins ILTP 1.0 re-prefacing on the same carrier. Fresh proof, complete role-ordered operands, authority, immutable plan construction, and rounds follow. A peer re-preface performs the same hard cut. A direct open interlace instead stages both prepared operands and, on the next step after both endpoint store effects have settled, atomically recreates both endpoints with the same explicit authorities and limits. There is no per-slot direct replacement.
The open handle cannot be used for replacement concurrently with
event driving, store execution, or another operation. JavaScript and
Python reject replacement while asynchronous drive/store work owns the
handle. Calling replacement after clean closure or cancellation is a
Policy error stating that the interlace is not open; an
already failed handle returns its owned typed error. Porcelain, capture,
provider, and observer wrappers keep operation-owned fixed policy and do
not expose replacement.
Bounded and open APIs wrap the same core fixed point. A bounded wrapper commits its semantic result there and returns after required final-output and route settlement. An open wrapper waits there on broad store position watches and may drive another ordinary round after local store progress. This host lifecycle choice does not change protocol states, limits, or wire grammar.
A connected carrier has one stable role:
CarrierRole::Listener derives protocol side0 and
CarrierRole::Connector derives protocol side1. Carrier role
is not participant position, application identity, or policy
authority.
A runtime may construct a connected participant from one host-established ILTP transport. The host passes that participant to the same ordinary interlace and porcelain APIs as a runtime-connected peer. Listener binding, acceptance, admission policy, transport handshake, task supervision, service reload, and shutdown/join behavior belong to the host application, not to a shared Lace listener surface. Host admission counts and transport buffer limits are not any of the five interlace operation limits.
Applications observe records and lifecycle rather than raw control facts or store operations. The shared public observations are:
RecordSent, when a provider emits a record
response;RecordSaved, when the observing participant settles
local admission of a received record;FixedPoint on fixed-point entry; andClosed for clean peer or local closure where an API
exposes lifecycle events.Bindings expose these as one tagged observation union:
RecordSent { kind, position, record_hash, optional owned Record }
RecordSaved { kind, position, record_hash, optional owned Record }
FixedPoint { kind }
Closed { kind, reason }
Only record observations carry a participant position,
first or second, matching the application
argument position. Rust spells these
ParticipantPosition::First and
ParticipantPosition::Second; bindings expose the same
values idiomatically. FixedPoint and Closed
are operation-wide. Public close reasons are PeerClosed and
Cancelled. Round numbers, fixed-point identities, store
positions, raw binding dictionaries, and protocol phases are diagnostics
rather than required observation fields. Language-idiomatic spelling
does not change the event meaning.
A RecordSaved observation stays with the participant
that admitted the record; it is not a remote acknowledgement. An
observation accepted by the host event path before replacement remains
deliverable historical output; only observations still owned by the
retired machine are dropped. Result-collecting wrappers derive their own
list, get, store, and at-most-once observation results from these
produced observations and fixed-point state rather than adding protocol
event families.
A participant position correlates an observation with one application argument in that interlace. It does not establish authority, protocol side, locality, initiator status, or durable receipt. Runtimes may attach record bytes when ownership is clear, or report a hash plus a follow-up record result.
On QUIB, an open operation that has committed fixed point and
authorized post-fixed-point transport settlement still emits exactly one
Closed(PeerClosed) when the peer retires by zero-code
application close or authenticated stateless reset. Those transport
causes remain distinct diagnostics below this public event; they do not
add close reasons or success events.
Fixed-point entry is observed once per entry. Each entry has fresh
opaque local identity so replacement and later sessions cannot accept
stale store-watch readiness. Policy replacement has no
PolicyReplaced event, activation waiter, or
policy-identifier event.
Every host exposes one-event reading as the canonical open-operation
primitive. Iteration delegates to it, and a run-to-fixed-point helper
consumes it and returns every observation consumed through
FixedPoint or Closed; helpers do not silently
discard observations or maintain a parallel driver. Rust open and
observer handles additionally expose consuming close(self)
operations (async in Tokio) that settle already-started
work and connected transport ownership, then return the final
Closed(Cancelled) boundary or observer event. Drop remains
a non-waitable resource-safety fallback. Fixed-point entry yields
FixedPoint once and leaves the stream open. Clean closure
yields Closed once and then exhaustion. Terminal failure
returns one typed error with no synthetic event and then exhausts, while
the failed handle retains that error for methods such as policy
replacement. Event queues and transport buffers remain bounded; a
consumer that stops reading causes backpressure, cancellation, or
closure rather than unbounded growth.
get, list, store, and
for_each construct operation-owned participants and run
ordinary interlace. They do not inspect or mutate the target store
directly.
list creates a private capture Lace and returns only
records validated and admitted there. Its collector consumes ordinary
record-saved observations; there is no separate dedicated result
observation. Records captured before a limit or failure remain in the
outcome, whose terminal states that the result is not exhaustive. A
result bound that stops before selected output exhaustion is not
success. ILTP defines no result order; an API promising order defines it
separately.get is the one-result capture form. The first admitted
record remains the result even if a later limit or failure stops the
operation, but the operation still attempts to settle outstanding
obligations and mandatory follow-up rounds. Empty selection completes
normally. get adds no hidden latest-record rule.store creates a provider Lace containing exactly the
supplied validated records. The target’s policy decides what to request
and persist. Empty input is an explicit complete no-op with no record
results or observations; it does not construct an interlace or
synthesize FixedPoint. Nonempty input reports one
route-neutral disposition for each unique input record:
RecordSent
for that record and the bounded operation subsequently reached fixed
point;RecordSent followed by fixed point confirms that the
target’s admission obligation settled, including a duplicate admission
after a frozen request. It does not claim a new physical insertion,
filesystem flush, independently attested remote persistence, or honesty
by a nonconforming peer. Skipped intentionally leaves
target-already-held and policy-declined indistinguishable on direct and
connected routes. A complete outcome never reports both
Skipped and a sent record. If a binding exposes
provenance fields, provider_sent means that the local
provider emitted RecordSent, while
target_saved means protocol-confirmed settled admission and
is true exactly for Saved.for_each creates a persistent observer Lace and exposes
one observer stream: at-most-once RecordSaved observations
carrying the observer-owned validated Record, plus
operation-wide FixedPoint and Closed. It does
not expose RecordSent, error events, a callback queue, or a
record-only subscription. Observer records do not flow back to the
target.Bounded operation terminals are exactly:
A bounded interlace outcome owns its terminal and ordered observations. List, get, and store outcomes additionally own their records or per-input results. The structured error exists only inside failed; stopped-by-limit has no error payload. Caller cancellation is an open/runtime closure reason, not a bounded terminal.
Successful argument validation, policy preparation, participant
claiming, route setup, and driver construction occur before the
operation-start linearization point. Failures there are ordinary typed
call errors. Once driving starts, failures become the outcome terminal
and preserve prior observations. A complete operation appends record
observations in production order, settles obligations and required final
output, settles its route, appends FixedPoint, then
returns. For QUIB, peer settlement is authorized only after fixed point,
an empty ILTP codec writer, and transfer of every final ILTP byte into
the QUIC send stream; codec acceptance is insufficient. Normal graceful
finish waits for connection-owned final-data/FIN acknowledgement, close,
RouteDrainEvent, endpoint RouteRemoval, exact
host-route removal, and confirmation. After authorization, peer
application close with code zero or authenticated stateless reset may
settle that transport instead without revoking
complete. The two internal causes remain distinct, but
neither creates another public success terminal, observation, or close
reason.
The same close before authorization is premature peer EOF, and the
same reset before authorization is a Transport failure.
Nonzero application close remains a Transport failure. QUIB
post-fixed-point transport settlement does not reuse the interlace phase
timeout. A limit or failure returns prior observations without
FixedPoint or an error observation. Bounded teardown does
not append Closed.
The core reports LimitExceeded independently of host
lifecycle; only a bounded wrapper maps it to
stopped-by-limit. Phase expiry is a distinct failure. A
boundary EOF is complete only at the fixed point defined by 050;
premature semantic EOF is failure. Internal scheduling yields and
would-block states are not operation outcomes. Open observations end by
cancellation, closure, or failure rather than a bounded completion
status.
Rust, C, WebAssembly, JavaScript, and Python expose the same error
categories: PeerProtocol, LimitExceeded,
Timeout, Policy, Store,
Transport, and Integration. Structured errors
carry category, optional participant position (first or
second), optional record hash, human-readable message,
and—only for Timeout—one stable phase:
Prefacing, Side0Proof,
Side1Proof, Side0Operand,
Side1Operand, Advertising,
Requesting, Answering, or
Fencing. Bindings do not require callers to parse debug
text. Errors are not observations.
Malformed, truncated, oversized, or noncanonical incoming framing is
PeerProtocol. Failure of a local encoder or construction
invariant is Integration. Connected local failure closes
the transport, so the peer may observe transport failure, framing
truncation, or premature EOF rather than the originating side’s local
error details.
Public runtime APIs use one cross-runtime authoring representation:
PolicyAuthoringSource = Interlang(text) | Datalog(text)
PolicyFrame = zero or more authoring-source contributions
There is no separate policy-source versus policy-module-source language or API. Each contribution is one private-helper and linked-provider ownership boundary; a label or path may travel separately for diagnostics but does not enter policy identity. A direct convenience policy is a one-source frame. Operation-owned capture/provider vetoes and endpoint-owned configuration are ordinary additional frame contributions, not special policy modes.
The ordinary string shorthand is Interlang. A bare coordinate pattern such as:
//todo-demo//todo/task//task/{*}
exposes and converges that checked record shape.
converge selects the active shared set and implies
exposure; expose selects whole local records that
peer-origin rules may inspect. Exposure alone does not transfer a
record. Interlang supports helpers, advertised-field candidate
specialization, checked-support hash closure, quoted coordinates, and
restricted nested converge inside expose. Raw
030/040 Datalog is the advanced surface.
Authoring operations own finality rather than switching parser types:
validate_standard_interlang(text)
validate_policy_frame(profile, frame) -> Complete | Incomplete(requirements)
finalize_policy_frame(profile, frame) -> canonical policy | no facet result
prepare_runtime_policy(profile, frame) -> prepared canonical module
Strict standard validation accepts only the portable Lace-120 subset
and requires an effective policy. Local frame authoring additionally
accepts the 030 extended policy-module surface, including linked
relations. Frame validation never returns an operand: an unresolved
name-and-arity requirement is a valid Incomplete result,
while syntax, safety, profile, ownership, and other checks available at
that stage are errors. Finalization requires every link, composes
facet-bearing contributions, and returns no facet result for an empty,
helper-only, or provider-only frame. Runtime preparation additionally
requires a facet-bearing result and returns exactly one self-contained
canonical module. It does not derive a side-local executable: that
requires the two ordered modules, their compiled plan, and a typed local
slot. Every finalized operand is ordinary self-contained canonical 030
Datalog.
Policy frames are semantic sets of normalized contributions. Caller order, duplicate identical entries, diagnostic paths, and equivalent provider tuple ordering do not change canonical output. Finalized linked values do change the canonical module and therefore operand and exchange-plan identity.
Language bindings may use idiomatic constructors and result spelling, but they must expose these same source kinds and operation outcomes rather than duplicate standard/extended compiler APIs, boolean dangling-link switches, or one-source plus separate-module containers.
Indexes may accelerate coordinate constraints, but selected truth still comes from validated record facts and policy. Coordinates do not create a separate lookup authority.
Developer tools may explain selection, exposure, route choice, and runtime failure through source maps, rule identifiers, traces, and route reports. Diagnostics are not normal application events or protocol facts and do not alter canonical policy text.
The developer layer should be convenient without hiding Lace’s model: records are validated bytes, facts are explicit, exposure controls peer-origin inspection, and interlace converges selected record sets. Applications use participant handles, record/lifecycle events, and interlace-backed porcelain—not raw protocol phases or store APIs.