Lace / spec

Lace-100 · Developer Surfaces Overview

Tags: overview, dx, event-stream, porcelain, non-normative

Lace’s durable truth is record bytes, record facts, Datalog policy, exchange plans, and interlace convergence. Applications should not have to drive raw protocol phases or inspect store internals to use that truth. This document summarizes the normal developer-facing surface above specs 010–060.

The main surface is an event-driven interlace runtime. Porcelain helpers such as get, list, and store are convenience APIs that construct and drive interlaces; they are not direct store access.

Core truth remains record-based

The lower specs define the protocol machinery:

010 records
020 record facts and views
030 Datalog
040 exchange policy
050 interlace
060 ILTP

Those layers are explicit about hashes, validated facts, peer advertisement claims, query views, exchange plans, and transfer invariants. Developer surfaces compile to or operate through those layers. They do not replace them with remote commands, raw database access, WebSocket-specific objects, or application-level truth shortcuts.

Records are table-shaped for developers

A validated record can be presented as an immutable row. Its hash is the row identity, and fields are columns. A projection, or table view, chooses field names such as Group, App, Name, TAI, Signed-By, and Data-Length, then shows one row per record hash with those fields as columns.

This table shape is a developer surface over validated record facts. It does not make rows mutable, does not make Group/App/Name unique, and does not expose raw store iteration. Public APIs still use interlace-backed porcelain and store machine boundaries.

Rules and selector DSLs define dynamic subsets of rows. Those subsets may depend on other rows, such as membership evidence, link targets, manifests, or current name records. Interlace converges the rows selected by both sides.

The normal runtime shape

A runtime package exposes a small application model:

FilesystemLace::open(...)
IndexedDbLace::open(...)
Lace::connect_websocket(...)

start_open_interlace(left, right, policy_or_plan) -> OpenInterlace
bounded_interlace(left, right, policy_or_plan) -> BoundedInterlace

list(target, selector_or_coordinate, options) -> ListOutcome
get(target, selector_or_coordinate, options) -> GetOutcome
store(target, records, policy_or_options) -> StoreOutcome
for_each(target, selector_or_coordinate, options) -> Open observation stream

Exact method names may differ by language/runtime, but the ownership cuts are stable:

Lace::connect_websocket(...) and similar transport helpers create or wrap a typed peer stream capability over ILTP bytes. They are not store-like objects and not remote command clients.

Starting an open interlace constructs an OpenInterlace driver. It is not the async wait point by itself. Waiting or await belongs to driving work: polling next_event, running a bounded interlace to completion, or using interlace-backed porcelain such as list, get, store, and for_each. Manual runtimes drive the same objects with explicit try_next_event-style steps.

Interlace event streams are the primary surface

Applications observe interlace progress through record/lifecycle event streams. Raw protocol facts, advertisements, side-machine phases, store-machine vtables, and InterlaceStepOutcome values are advanced/internal driver surfaces, not normal application events.

Conceptual public events are:

pub enum InterlaceEvent {
    RecordSent { side: InterlaceSide, record_hash: RecordHash },
    RecordSaved { side: InterlaceSide, record_hash: RecordHash, store_id: Option<StoreRecordId> },
    RecordListed { side: InterlaceSide, record_hash: RecordHash },
    FixedPoint,
    Complete { outcome: BoundedInterlaceOutcome },
    PeerClosed,
    Closed,
    LogicError { error: LogicError },
    RuntimeIoError { error: RuntimeIoError },
    RuntimeStoreError { error: RuntimeStoreError },
}

A runtime may include record bytes in events when doing so is cheap and well-owned, or may expose record hashes plus a follow-up record result. The event must remain record/lifecycle-oriented. It must not expose protocol fact blocks, MaySend/MayRequest internals, peer advertisement records, store cursors, raw NotAvailable machinery, or low-level side-machine phases as normal app API.

InterlaceSide is an event-correlation label for this interlace. It does not mean authority, source/sink direction, durable receipt, initiator, locality, or transport.

try_next_event-style APIs perform at most one natural core unit plus minimal runtime interpretation, then return an event or no-event/would-block status. next_event-style APIs may drive natural units until an event is available or a runtime wait point is reached, then wait on runtime-owned IO, store readiness, timers, or cancellation. Runtime wrappers must not hide an unbounded pump behind a callback or invoke application observers while store locks or transport callbacks are active.

After a terminal event (Complete, Closed, PeerClosed, or fatal error), later polls report stream exhaustion rather than rerunning closed work.

Event queues and transport buffers are bounded. If an application stops consuming events, or a peer stops accepting bytes, the runtime must backpressure, stop reading, cancel/close, or drop only explicitly diagnostic data according to a documented policy. It must not grow unbounded buffers or collapse record accounting events.

Porcelain is interlace-backed

get, list, store, and for_each are developer conveniences over interlace. They are allowed record access paths because they construct operation laces and run the same convergence machinery as explicit interlaces.

Porcelain result types report operation-level outcomes: complete, more-may-exist, stale/incomplete, policy-refused or invalid input records, duplicate/no-op saves, cancellation, and failure. The public event stream used while driving the operation remains record/lifecycle-only.

Porcelain must not mutate a target store directly, inspect raw target store contents, or bypass store-machine boundaries. Trusted/direct local routes are optimized interlace routes over store-machine boundaries, not raw store bypasses.

Coordinates and DSLs are helper layers

Coordinates (110) and higher-level selector DSLs help applications name common record sets: coordinate prefixes, latest versions, linked closures, signed records, or application surfaces. They are developer input languages. They compile to explicit Lace policy artifacts: canonical lacegrams, exchange-plan operands, local executable policies, query-exposure constraints, and list/get, store, or for_each configuration.

A coordinate lookup may use a runtime/store index internally, but selected truth still comes from validated record facts and policy evaluation. A coordinate is not a replacement for record hashes, lacegrams, exchange plans, or store-machine boundaries.

Debuggability is diagnostic, not the normal event stream

Faceted exchange plans, operand scopes, origin-aware fact resolution, and query views are hard to inspect. Developer tooling should provide diagnostics such as source maps, rule ids, traces, and route reports explaining:

These diagnostics are separate from the normal record/lifecycle event stream. Canonical protocol text remains annotation-free. Authoring tools may keep source annotations and map them back to canonical policy, but those annotations are not protocol facts.

Direction

The developer-facing layer should be convenient without hiding Lace’s ownership model: records are validated bytes, facts are explicit, query views control peer visibility, interlace converges record sets, runtimes own IO and waiting, and record store machines own persistence plus local StoreRecordId assignment. Normal applications should use interlace event streams and interlace-backed porcelain instead of raw protocol or store APIs.