# 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:

```text
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:

```rust
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` is a record-set authority backed by a runtime-owned record store
  machine. It owns validated record storage, `StoreRecordId` wake state, and
  runtime lifecycle state.
- `LaceHandle` is a shareable participant capability. It is not a raw store,
  transaction, socket, WebSocket, or already-running protocol machine.
- `OpenInterlace` and `BoundedInterlace` are runtime-owned drivers around a core
  interlace machine. They own runtime IO, timers, waiting, event queues,
  cancellation, and transport adapters.
- Core remains sans-IO. Runtime packages own sockets, tasks, browser callbacks,
  wake routing, and lifecycle integration.
- Record store machines own record persistence, local `StoreRecordId` assignment,
  policy-neutral record-fact access, record loads, and typed-record stores.
  Public app APIs do not expose raw record insertion, hash-existence checks,
  record counts, or store iteration.

`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:

```rust
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.

- `list(target, selector_or_coordinate, limit/options)` is a bounded one-shot
  operation against any `LaceHandle`. It returns a finite result collection plus
  an operation status such as complete, more-may-exist, stale/incomplete,
  cancelled, or failed. It may use a trusted direct local list route when both
  the handle and policy allow it; otherwise it uses ordinary interlace.
- `get(target, selector_or_coordinate, options)` is the bounded one-result form
  over the same selection machinery and returns an optional record/hash plus an
  operation status.
- `store(target, records, policy/options)` creates or uses a source lace with
  typed validated records, runs a bounded provide/store interlace to the target,
  and returns a store operation outcome rather than an unbounded raw event
  buffer.
- `for_each(target, selector_or_coordinate, options)` is the open/subscription
  porcelain. It runs an open interlace and invokes or yields record-lifecycle
  observations for each newly matching record until cancelled or closed.

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:

- why a record was or was not selected;
- why a fact was or was not queryable by a peer;
- which rule generated `MaySend(P)` or `MayRequest(P)`;
- which local constraint blocked exposure or transfer;
- which route an interlace chose and why;
- which store operation was pending, blocked, incomplete, or failed at runtime.

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.
