How a read becomes a value in the little-sister storage engine — written as a ledger of the transforms that stack on the append-only log: what each consumes, what it produces, and whether it is built or proposed. This is the review gauge for Storage::Pointer (case task-06fe434c). The substrate is wanderland-core-storage-engine; the operator layer is wanderland-core-overlay-operators; the sibling that already treats labels as pointers is advaita-projection-data-model. Idiom borrowed from advaita-nextflow-router-data-model.
Every read is a composition over an append-only log of signed records at an address. Nothing is mutated and nothing is deleted; a write only ever appends one more signed record. What you get back depends on how many transforms you ask the engine to stack on the raw log:
raw log ─▶ antiparticle overlay ─▶ operator overlay ─▶ (IOU hydration) ─▶ (pointer deref) ─▶ value
│ what survives base + pokes markers→blobs addr→target
│ [✓ built] [✓ built] [✓ built] [◇ proposed]
└── each transform is a pure shape→shape function, each OPT-IN, each composable with the next
A boundary that just wants the record stops early — "fetch this record from storage" is the identity read, one composition and done. A boundary that wants the blob asks for hydration; a boundary that wants a pointer followed asks for a deref. The engine never forces a transform: an un-hydrated read returns lean markers, an un-deref'd pointer returns the plain pointer record. This is the whole design constraint on the pointer — it is the fourth optional transform, not a change to what a read is.
Picture the store as a checkerboard. Every cell is an address paired with a type — (to_addr, type_addr) — and each cell holds a stack of generations: every time you write a record of that type to that address you drop another checker on the pile, auto-sealed to the one beneath it via trace (§The record). The board has two axes of movement, and one primitive — the pointer — travels both:
@sha256:… instead of :latest.points_at). Resolving it hops to that cell and walks its stack; a pointer can name a cell that holds another pointer, so you skip across the board, and at every landing you again choose head-or-pinned.So a pointer is just a stored SELECT run through the overlay walk-up: which cell (a to_addr + type), which generation (head, or a pinned signature), and which field (a path — the projection — extracted once you land). Compose those three and you can address anything on the board — a specific cell, its live head, a frozen past revision, or a leaf deep inside whatever a chain of pointers finally resolves to — all with one primitive and the machinery the engine already has. "latest/promoted move by overlay" is just re-pointing a horizontal checker; "walk the promotion history" is reading down that cell's vertical stack. Cells, stacks, pointers — nothing else.
| Tag | Meaning |
|---|---|
| [✓] | Built in wanderland-core today — file cited at the stage |
| [◇] | Proposed by this case (task-06fe434c) — not yet in the codebase |
| [N] | Native — the mounted driver runs it in-backend (SQL window functions / json_extract) |
| [R] | Ruby — composed in-process by Activities::Overlay; the reference semantics [N] is property-tested equal to |
[N] and [R] must agree by construction (the parity spec). The pointer is a [R]-only value object — it orchestrates existing reads, it does not push down to SQL.
Every write produces exactly one of these. Physical columns of the records table (sqlite_driver.rb / postgres_driver.rb); the driver never UPDATEs or DELETEs.
to_addr: ":events:run:echo-20260717144738-d94b94" # where it lives; read-by-prefix returns all at/under
from_addr: ":sessions:claude" # who wrote it — a signed identity
type_addr: ":types:poke" # what KIND — itself an address; drives the transform
payload: { op: "$merge", path: "payload", value: { status: "succeeded" } } # the data (JSON); large leaves → IOU markers
trace: "b64-sig-of-predecessor" # [✓] the generation link — predecessor's signature (SealChain)
signature: "b64…" # signs the canonical record incl. `trace`
at: "2026-07-17T14:47:53Z" # ISO8601 — the overlay ordering key
Two fields carry the model and deserve a warning:
type_addr is the switch. :types:source / a domain type is a seed (a baseline). :types:poke / :snip / :splice / :annotate is an operation (an edit on top). :types:antiparticle is a cancellation. :types:pointer [◇] is the new one — a record whose payload names another address. Many types coexist at one address; the read composes them by kind. Re-writing the same (to_addr, type_addr) appends a new generation of that thing — the append is never an overwrite, so (to_addr, type_addr) is the identity of a revision stack (the cell, in §The board).trace is the generation link — one mechanism for signing over priors. In a signing pipeline, Activities::SealChain fills trace with the predecessor record's signature and signs the record over it, so an address-family is a tamper-evident hash chain (the causal record). In plain storage that field is otherwise unused — so the engine reuses the exact same mechanism as the generation spine: an append at the same (to_addr, type_addr) auto-seals to the current head at that cell (trace = head.signature, then sign), making every re-write a walkable, tamper-evident revision chain. There is no separate trace_addr column, and none is proposed — the causal/generation pointer that advaita-projection-data-model §Generation-lineage floated as a new address is subsumed here by reusing trace, the only generalization being that the predecessor is the head at this cell rather than a seq-templated sibling. See §The board, §The walk-up, and §Case D.Addresses are colon-paths; the first segments pick the driver. Registry.resolve does longest-prefix match, returns [driver, remainder] (registry.rb:24). This is what makes a pointer cross-mount for free — a points_at in mount A whose target lives in mount B is just two independent resolve calls.
mounts:
":events": { driver: postgres } # foxfire trace ledger
":projections": { driver: postgres } # memoized folds + labels
":nodes": { driver: sqlite } # loom markdown graph
":files:local": { driver: file } # blob backing for IOU
":types": { driver: sqlite } # the type records themselves
consumes: a boundary's intent — { to_addr, from_addr, type_addr, payload }
produces: one signed record in the log; large string leaves offloaded as IOU markers
transport: Registry.append → resolve(to_addr) → driver.append (registry.rb:36, sqlite_driver.rb:127)
On write, if the mount has a content driver configured, extract_with_driver walks the payload tree and replaces any string leaf over iou_threshold (default 4 KB) with a typed marker, writing the blob through the content driver (file_store). The record stored is lean; the blob lives out-of-row. This is the guarded content-driver mount landed in the sibling case (task-7be1e663, wanderland-core 2026.07.125). No content driver ⇒ everything inline.
# a task record carrying an 82KB command log — as APPENDED (post-extraction)
to_addr: ":events:task:echo-…:d94b94:SWALLOW"
type_addr: ":events:task"
payload:
process: SWALLOW
exit: 0
command_log: # the big leaf, extracted on write
_iou: "a1b2c3d4-…"
_driver: "file_store"
_args: { path: "events/…/SWALLOW/command_log" }
_size: 82311
consumes: all records at an address
produces: the records that survive cancellation
transport: driver.overlay → overlay_records (SQL window fn) → Activities::Overlay.partition
A :types:antiparticle cancels a prior record (LIFO pairing). The sqlite/postgres drivers resolve this natively in SQL (overlay_records); the Ruby reference does the same LIFO. The cancelled record and its antiparticle both remain in the log — the cancellation is itself a signed, timestamped fact. This is undo in loom, and delete everywhere.
consumes: the surviving records
produces: ONE composed record (the current entity)
transport: Activities::Overlay.partition (newest seed + sorted ops) → compose (overlay.rb:80)
partition picks the newest seed by (at, id) as the baseline and sorts the operations; compose applies each operator at its payload.path. The operator registry is open (overlay.rb:47): $set $merge $append $prepend $union $intersection $remove $inc $min $max + snip / splice. Most are commutative or last-writer-wins per key, so concurrent pokes need no operational transform — this is the CRDT-ish property loom leans on (§Case D). Entity.upsert is the write side: diff-before-write, appending a $merge poke for only the changed keys (entity.rb:27). The composed record is what "fetch this record" returns.
consumes: a composed record whose payload may hold IOU markers
produces: the same record with markers replaced by blob content
transport: driver.hydrate → hydrate_markers (Drivers.lookup(_driver).read(_args)) (sqlite_driver.rb:379)
Explicit and on-demand — overlay does not hydrate. A reader that needs the blob calls hydrate after overlay (see storage_ops.rb: overlay then hydrate). A projection deliberately skips hydration so a fold never drags an 82KB leaf into Ruby (advaita-projection-data-model §Invariants). This separation is load-bearing for the pointer: overlay alone is not IOU-applied, so a pointer that must "compose with IOU" has to hydrate each hop itself (§The pointer, deref algorithm).
consumes: an address whose record is type_addr :types:pointer
produces: the resolved value — walk pointer→pointer→…→source, apply path, hydrate on return
transport: Storage::Pointer.new(store:, addr:).deref — lib/wanderland/storage/pointer.rb (NEW)
The proposed fourth transform, and the default for a read that lands on a pointer: resolve to the target value. raw opts out (returns the undereferenced pointer record — the maintenance / label-move view). Hydration is deferred to the very end: the walk runs over composed-but-unhydrated records, and only the final, post-path value pulls its IOUs. Full spec in §The pointer.
flowchart LR
addr["address"] --> route["Registry.resolve<br/>longest-prefix mount"]
route --> log[("append log<br/>signed records at addr")]
log --> anti["antiparticle overlay<br/>what survives · [✓ N/R]"]
anti --> ops["operator overlay<br/>seed + pokes → entity · [✓ N/R]"]
ops --> rec{"composed record"}
rec -->|"boundary stops here"| plain["the record<br/>(identity read)"]
rec -->|"asks to hydrate"| iou["IOU hydration<br/>markers → blobs · [✓]"]
rec -->|"type = :types:pointer<br/>+ asks to deref"| ptr["Pointer.deref<br/>path + next hop · [◇ R]"]
ptr -.->|"points_at = another address<br/>(maybe another mount)"| route
iou --> value["value"]
plain --> value
ptr --> value
The dashed edge is the whole trick: a deref hop re-enters resolve, so chaining and cross-mount fall out of the routing that already exists. Nothing in the registry learns about pointers.
"Walk up" is the engine's answer to history and derived reads. It comes in built forms, plus the generation spine this case adds:
under(prefix) returns every record at or below an address in at order. foxfire's event_store appends immutable :events:event:<run>:<seq> leaves and folds them up into a composed :events:run:<run> / :events:task:<run>:<hash> via Entity.upsert. A projection (advaita-projection-data-model) is that fold, memoized and addressed.overlay_records, or from(addr)) are the full revision history — every poke and antiparticle a signed fact. The log is the audit trail.SealChain links each record in a family to its predecessor's signature through the trace column, making the family tamper-evident (seal_chain.rb:95).(to_addr, type_addr) is a new generation, and the append auto-seals to the current head at that cell (trace = head.signature, then sign; genesis = nothing under the cell yet). No <seq> in the address — the predecessor is the head, not a seq-templated sibling; the chain the trace links form is the sequence, so a pipeline a→b→c→b→c is five generations in one cell, ordered by the chain and told apart by from_addr. Two costs the DB owns so a deep stack stays cheap: a native head-read (ORDER BY at DESC LIMIT 1 on seeds, pushed to SQL like cap_list), and a snapshot/baseline checkpoint (:types:snapshot, loom's squash) that materialises the head while old generations remain as append-only audit.A cell holds a stack, and there are two conjugate ways to read it — chosen by intent, not by storage:
:events:run. latest/promoted resolve this way.trace chain (or at order) and return every generation in causal order. The read for a log — a pipeline trace, an event stream — where the generations are distinct steps, not versions of one value. Overlaying a trace would wrongly collapse a→b→c→b→c to "just the last step."Same cell, same chain; a pointer requests whichever it needs, and ShapeMatcher (§the selector) filters the walk.
unique(to_addr, trace)Appending a generation is read-head → seal-to-head. Two writers racing one cell would both seal to the same head and fork the chain. The fix lives in the DB, declaratively: a unique constraint on (to_addr, trace) — a predecessor signature can be claimed exactly once, so a fork is a constraint violation and the loser retries against the new head. That is compare-and-swap on the head, the mutex moved out of the app and into the schema — retiring foxfire's in-process write_mutex + generation cache and unblocking replicas: N (task-7d58f481 de-singularise). A single pipeline run never contends (its steps are serial; distinct runs are distinct cells); the constraint only bites where writers truly share a cell — a fold entity fed by parallel producers, or a concurrently-promoted label.
A single, deliberately dumb boundary — fetch: "get me what is at this address." Its whole surface is an address plus two opt-in flags. The same boundary produces four very different results depending only on what record it finds and which transforms the caller asked for — which is the point of §The one idea.
# the fetch boundary contract
input:
addr: <address> # required
deref: false # [◇] follow a :types:pointer to its target
hydrate: false # [✓] replace IOU markers with blob content
output: a record (or, with deref, a resolved value)
Nothing special: overlay composes the entity, the boundary hands it back.
# input
addr: ":events:run:echo-20260717144738-d94b94"
# → overlay(addr): seed :events:run + its $merge pokes composed
# output — the composed record, verbatim
to_addr: ":events:run:echo-20260717144738-d94b94"
type_addr: ":events:run"
payload: { status: succeeded, duration_ms: 15509, exit_status: 0, retries: 0,
session_id: "fee93b5e-3fd3-45e5-bb84-e09e5d436b6b" }
The found record is a :types:pointer. A read resolves it by default — the boundary walks the chain and returns the target value; pass raw to get the pointer record itself (the maintenance / label-move view), which round-trips exactly like Case A.
# input — default resolves
addr: ":projections:acq_manifest:drugbank:promoted"
# the record found there:
to_addr: ":projections:acq_manifest:drugbank:promoted"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:001419158" }
# resolve: one hop → overlay(:…:001419158) is a source record → its payload is the value
# output (default)
version: "001419158" status: promoted image_digest: "sha256:37159dba…"
metrics: { tables: { drugs: 28151, products: 323875 }, totals: { tables: 122 } }
# ── raw: true ── the undereferenced pointer, exactly like any plain read:
to_addr: ":projections:acq_manifest:drugbank:promoted"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:001419158" }
Chained + path — a pointer at a pointer, then one leaf; hydration waits until the very end:
# :deploy:img-website:current is a pointer WITH a path, at a pointer
to_addr: ":deploy:img-website:current"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:promoted", # itself a pointer (Case B)
path: "image_digest" }
# resolve(current): current → promoted(pointer) → 001419158(source),
# apply this pointer's path to the resolved value,
# THEN hydrate only that final leaf if it's an IOU (nothing else pulled)
# output: "sha256:37159dba…"
flowchart LR
c[":deploy:img-website:current<br/>pointer · path=image_digest"] -->|points_at| p[":…:drugbank:promoted<br/>pointer"]
p -->|points_at| s[":…:drugbank:001419158<br/>source record"]
s -->|"resolved payload"| v["{ version, image_digest, … }"]
v -->|"path: image_digest → hydrate on return"| out["sha256:37159dba…"]
The found record is an ordinary entity, but a payload leaf is an IOU marker (extracted at Stage 1). hydrate decides whether the blob comes back.
# input
addr: ":events:task:echo-…:d94b94:SWALLOW"
hydrate: true
# overlay(addr) → composed record with a lean marker (overlay does NOT hydrate):
payload:
process: SWALLOW
exit: 0
command_log: { _iou: "a1b2…", _driver: "file_store", _args: { path: "…" }, _size: 82311 }
# hydrate: true → Drivers.lookup("file_store").read(args) splices the blob back
# output (hydrate: true)
payload: { process: SWALLOW, exit: 0, command_log: "#!/usr/bin/env bash\n… 82KB …" }
# output (hydrate: false) — the marker stays; the OOM-safe / projection read
payload: { process: SWALLOW, exit: 0, command_log: { _iou: "a1b2…", _size: 82311 } }
Pointer × IOU compose: if deref lands on a record like this, the pointer must hydrate the resolved hop for the AC "composes with IOU" to hold — because overlay alone leaves the marker (Stage 4). See the deref algorithm.
D1 — overlay-as-CRDT (loom-pack style) [✓]. Many writes at one cell; the read composes them. Concurrent editors never need a merge because the operators are commutative / last-writer-wins per key (wanderland-loom-pack-design "commutative and need no operational transform").
addr: ":events:run:R"
# the append log at this cell — each line a signed generation:
- type_addr: ":events:run" payload: { status: running, retries: 0 } # seed @T0
- type_addr: ":types:poke" payload: { op: $inc, path: retries, value: 1 } # @T1
- type_addr: ":types:poke" payload: { op: $merge, path: payload,
value: { status: succeeded, duration_ms: 15509 } } # @T2
# overlay: newest seed as base, pokes in at-order, LWW per key
# output: { status: succeeded, retries: 1, duration_ms: 15509 }
D2 — a moving label leaves a walkable trace [◇]. Promotion moves a label, and a label is a pointer (advaita-projection-data-model). The immutable instances stay put; the moving promoted cell gets a new generation naming the new instance, auto-sealed to the prior one via trace — no seq, no new column.
# promote 001420777 over 001419158 — append a new generation at the label cell (:…:promoted, type :types:pointer):
# gen0 :…:drugbank:promoted { points_at: :…:001419158 } trace = <genesis>
# gen1 :…:drugbank:promoted { points_at: :…:001420777 } trace = <sig(gen0)>, signed over it
#
# read :…:promoted → head generation = gen1 → instance 001420777 (overlay newest-seed)
# walk promotion history → down the trace chain: gen1 →(trace)→ gen0
# cheap view = at-order over at(:…:promoted)
# verifiable view = recurse trace→signature within the cell (tamper-evident, gap-detecting)
The instances are pinned by revision (:…:drugbank:001419158 is a cell you cite by signature); the label is the moving head of a :types:pointer cell. Same board, one vertical stack — the history is the stack read downward.
A pointer's target is a selector resolved through the cell's reads (§The board, §Walking vs. overlay). Up to four parts — the first three find the generation, the last projects a field:
under: a to_addr (prefix or exact) bounds the run/family; from: picks the writer (the boundary); type: the kind. All three are indexed columns (idx_to_addr / idx_from_addr / idx_type_addr), so this narrows to a handful without a scan.expected:, boundary when:, capability filter:. Because it runs over the few records the address legs already selected, it's a cheap Ruby pass, not a store scan.at, default — the moving label), a pinned @signature (immutable — the Docker @digest), or walk them all (the list read).path, a dot-leaf out of the resolved value; composes across a chain.# pluck one image's digest out of a build that produced eight
points_at:
under: ":traces:sc-pipeline-images:<run-id>" # the run (idx_to_addr)
from: ":boundary:docker_build" # the 8 builds (idx_from_addr)
match:
result: { image: sc-pipeline-clownfish } # ShapeMatcher → the clownfish build
# revision: head (newest by at) — or @<sig> to pin a specific rebuild
path: result.image_digest # → "sha256:…"
sc-pipeline-images builds eight images, all through docker_build, so they are eight generations in one cell told apart only by their payload. from gets the eight; match plucks clownfish; at/head resolves a rebuilt one to the latest; path pulls the digest. Nothing in the pointer knows it was the fifth docker_build — position never enters it.
The vocabulary is addressable-first: the indexed address legs always run before the shape filter, so a board move is bounded before ShapeMatcher sees a record. A bare match: with no under:/from:/type: pre-scope (a store-wide shape scan) is the one thing rejected (§Open questions).
Reuses the label shape verbatim, so promotion tags and pointers are one mechanism.
to_addr: ":<anything>:<name>" # you name it whatever you want
from_addr: ":sessions:…" # signed like every record
type_addr: ":types:pointer" # the switch Stage 5 keys on
payload:
points_at: ":events:run:echo-…" # REQUIRED — any address, any mount
path: "metadata.image_digest" # OPTIONAL — dot-path into the resolved value
Storage::Pointer.new(store:, addr:) # store = Registry OR Adapters::Store (exposes resolve/overlay)
#deref(max_depth: N) → the resolved value
#record / #raw → the undereferenced pointer record (Case B, deref:false)
#points_at, #path → the pointer's own fields
#target → one hop only (optional)
Registry.pointer(addr) # optional convenience factory — NOT required
The registry stays neutral: "request a Pointer and .deref" vs "read a record" is the resolve-vs-manage distinction. A pointer you don't deref is the raw record, so a boundary can still move a label without ever resolving it.
resolve(addr, depth=0, seen={}): # the DEFAULT read; walks UNHYDRATED
raise PointerCycle if addr ∈ seen OR depth > max_depth # cycle-safe
rec = store.overlay(addr) # antiparticle + operator applied — NOT hydrated
raise PointerUnresolved if rec is nil # dangling hop — see policy
if rec.type_addr == ":types:pointer":
seen.add(addr)
resolved = resolve(rec.payload.points_at, depth+1, seen) # recurse → re-enters resolve → cross-mount
return extract(resolved, rec.payload.path) # this pointer's path applies to the resolved value
else:
return extract(rec.payload, path-from-the-pointer-that-reached-here or whole)
# hydration happens ONCE, at the boundary, on the value about to be returned:
return hydrate(resolve(addr)) # only the final post-path value pulls its IOUs
raw(addr) = store.overlay(addr) # the opt-out: the plain pointer record, no walk, no hydrate
Points that need nailing, each mapped to an AC:
| Semantic | Decision |
|---|---|
| Default = resolve | a read that lands on a :types:pointer walks the chain and returns the target value; raw opts out to the undereferenced record. |
| Chain | pointer→pointer→…→source, each hop store.overlay(next). Path composes: a pointer's path applies to the fully-resolved value of its points_at. |
| Cross-mount | points_at names any address; the recursion re-enters Registry.resolve, so a mount-A pointer resolves a mount-B record with zero special-casing. |
| Hydrate last | the walk reads composed-but-unhydrated records; hydration runs once, on the final post-path value being returned — intermediate hops and unselected fields never pull their IOUs. |
| Cycle-safe | visited-set and max_depth; a loop raises Storage::PointerCycle, never hangs. |
| Raw round-trips plain | raw (a.k.a. #record/#raw) returns the composed pointer record exactly like Case A — no walk, no hydrate. The resolve is default; the raw view is the opt-out and transparent. |
| Missing target | Recommend: resolve raises Storage::PointerUnresolved on any dangling hop (a written pointer names an address that should resolve; silence hides a bug). Existence probes use a non-raising raw/#target, which may return nil. Consistent rule: resolve is all-or-raise. |
A pointer is a storage device. The overlay read follows it, in the same layer that composes the rows of an address, so no reader above the store sees a pointer.
overlay(addr) :: StorageRegistry#overlay (runtime.rb) and Storage::Registry.overlay follow each pointer hop by hop, across mounts, at the reader's access level. Each hop's path applies to the payload, innermost first. A dangling pointer, a cycle, or a chain past 32 hops reads as absent.lstat(addr) :: the same read with no pointer followed. Storage.lstat(store, addr) falls back to overlay on a bare driver.lstat (Entity, Writer, the poke preview, content_migrate). A write to a pointer address therefore acts on the pointer. A :type:pointer write moves it.overlay_all returns each pointer as stored. A listing names the pointer, and a read of one address follows it.Storage::Pointer.follow :: the hop loop that the overlay calls (lib/wanderland/storage/pointer.rb). The Pointer value object and the Resolve ledger resolver read each hop with lstat. Each of them keeps its own cycle error.!Pluck wanderland-core-storage-data-model
How a read becomes a value in the little-sister storage engine — written as a ledger of the transforms that stack on the append-only log: what each consumes, what it produces, and whether it is built or proposed. This is the review gauge for Storage::Pointer (case task-06fe434c). The substrate is wanderland-core-storage-engine; the operator layer is wanderland-core-overlay-operators; the sibling that already treats labels as pointers is advaita-projection-data-model. Idiom borrowed from advaita-nextflow-router-data-model.
The one idea
Every read is a composition over an append-only log of signed records at an address. Nothing is mutated and nothing is deleted; a write only ever appends one more signed record. What you get back depends on how many transforms you ask the engine to stack on the raw log:
raw log ─▶ antiparticle overlay ─▶ operator overlay ─▶ (IOU hydration) ─▶ (pointer deref) ─▶ value
│ what survives base + pokes markers→blobs addr→target
│ [✓ built] [✓ built] [✓ built] [◇ proposed]
└── each transform is a pure shape→shape function, each OPT-IN, each composable with the next
A boundary that just wants the record stops early — "fetch this record from storage" is the identity read, one composition and done. A boundary that wants the blob asks for hydration; a boundary that wants a pointer followed asks for a deref. The engine never forces a transform: an un-hydrated read returns lean markers, an un-deref'd pointer returns the plain pointer record. This is the whole design constraint on the pointer — it is the fourth optional transform, not a change to what a read is.
The board — cells, stacks, and pointers
Picture the store as a checkerboard. Every cell is an address paired with a type — (to_addr, type_addr) — and each cell holds a stack of generations: every time you write a record of that type to that address you drop another checker on the pile, auto-sealed to the one beneath it via trace (§The record). The board has two axes of movement, and one primitive — the pointer — travels both:
- Vertical — up a cell's stack. Reading a cell resolves to the head (newest generation) automatically; that is the walk-up. You can instead pin an exact checker by its revision = the record's signature — the immutable digest of that generation — the way you cite a Docker image by
@sha256:… instead of :latest.
- Horizontal — across cells. A pointer is a checker whose payload names another cell (
points_at). Resolving it hops to that cell and walks its stack; a pointer can name a cell that holds another pointer, so you skip across the board, and at every landing you again choose head-or-pinned.
So a pointer is just a stored SELECT run through the overlay walk-up: which cell (a to_addr + type), which generation (head, or a pinned signature), and which field (a path — the projection — extracted once you land). Compose those three and you can address anything on the board — a specific cell, its live head, a frozen past revision, or a leaf deep inside whatever a chain of pointers finally resolves to — all with one primitive and the machinery the engine already has. "latest/promoted move by overlay" is just re-pointing a horizontal checker; "walk the promotion history" is reading down that cell's vertical stack. Cells, stacks, pointers — nothing else.
Realization legend
Tag
Meaning
[✓]
Built in wanderland-core today — file cited at the stage
[◇]
Proposed by this case (task-06fe434c) — not yet in the codebase
[N]
Native — the mounted driver runs it in-backend (SQL window functions / json_extract)
[R]
Ruby — composed in-process by Activities::Overlay; the reference semantics [N] is property-tested equal to
[N] and [R] must agree by construction (the parity spec). The pointer is a [R]-only value object — it orchestrates existing reads, it does not push down to SQL.
The record — one shape
Every write produces exactly one of these. Physical columns of the records table (sqlite_driver.rb / postgres_driver.rb); the driver never UPDATEs or DELETEs.
to_addr: ":events:run:echo-20260717144738-d94b94" # where it lives; read-by-prefix returns all at/under
from_addr: ":sessions:claude" # who wrote it — a signed identity
type_addr: ":types:poke" # what KIND — itself an address; drives the transform
payload: { op: "$merge", path: "payload", value: { status: "succeeded" } } # the data (JSON); large leaves → IOU markers
trace: "b64-sig-of-predecessor" # [✓] the generation link — predecessor's signature (SealChain)
signature: "b64…" # signs the canonical record incl. `trace`
at: "2026-07-17T14:47:53Z" # ISO8601 — the overlay ordering key
Two fields carry the model and deserve a warning:
type_addr is the switch. :types:source / a domain type is a seed (a baseline). :types:poke / :snip / :splice / :annotate is an operation (an edit on top). :types:antiparticle is a cancellation. :types:pointer [◇] is the new one — a record whose payload names another address. Many types coexist at one address; the read composes them by kind. Re-writing the same (to_addr, type_addr) appends a new generation of that thing — the append is never an overwrite, so (to_addr, type_addr) is the identity of a revision stack (the cell, in §The board).
trace is the generation link — one mechanism for signing over priors. In a signing pipeline, Activities::SealChain fills trace with the predecessor record's signature and signs the record over it, so an address-family is a tamper-evident hash chain (the causal record). In plain storage that field is otherwise unused — so the engine reuses the exact same mechanism as the generation spine: an append at the same (to_addr, type_addr) auto-seals to the current head at that cell (trace = head.signature, then sign), making every re-write a walkable, tamper-evident revision chain. There is no separate trace_addr column, and none is proposed — the causal/generation pointer that advaita-projection-data-model §Generation-lineage floated as a new address is subsumed here by reusing trace, the only generalization being that the predecessor is the head at this cell rather than a seq-templated sibling. See §The board, §The walk-up, and §Case D.
The address space routes the read
Addresses are colon-paths; the first segments pick the driver. Registry.resolve does longest-prefix match, returns [driver, remainder] (registry.rb:24). This is what makes a pointer cross-mount for free — a points_at in mount A whose target lives in mount B is just two independent resolve calls.
mounts:
":events": { driver: postgres } # foxfire trace ledger
":projections": { driver: postgres } # memoized folds + labels
":nodes": { driver: sqlite } # loom markdown graph
":files:local": { driver: file } # blob backing for IOU
":types": { driver: sqlite } # the type records themselves
The transforms (stages)
Stage 1 — Append (the write) [✓]
consumes: a boundary's intent — { to_addr, from_addr, type_addr, payload }
produces: one signed record in the log; large string leaves offloaded as IOU markers
transport: Registry.append → resolve(to_addr) → driver.append (registry.rb:36, sqlite_driver.rb:127)
On write, if the mount has a content driver configured, extract_with_driver walks the payload tree and replaces any string leaf over iou_threshold (default 4 KB) with a typed marker, writing the blob through the content driver (file_store). The record stored is lean; the blob lives out-of-row. This is the guarded content-driver mount landed in the sibling case (task-7be1e663, wanderland-core 2026.07.125). No content driver ⇒ everything inline.
# a task record carrying an 82KB command log — as APPENDED (post-extraction)
to_addr: ":events:task:echo-…:d94b94:SWALLOW"
type_addr: ":events:task"
payload:
process: SWALLOW
exit: 0
command_log: # the big leaf, extracted on write
_iou: "a1b2c3d4-…"
_driver: "file_store"
_args: { path: "events/…/SWALLOW/command_log" }
_size: 82311
Stage 2 — Antiparticle overlay: what survives [✓ · N/R]
consumes: all records at an address
produces: the records that survive cancellation
transport: driver.overlay → overlay_records (SQL window fn) → Activities::Overlay.partition
A :types:antiparticle cancels a prior record (LIFO pairing). The sqlite/postgres drivers resolve this natively in SQL (overlay_records); the Ruby reference does the same LIFO. The cancelled record and its antiparticle both remain in the log — the cancellation is itself a signed, timestamped fact. This is undo in loom, and delete everywhere.
Stage 3 — Operator overlay: base + pokes → entity [✓ · N/R]
consumes: the surviving records
produces: ONE composed record (the current entity)
transport: Activities::Overlay.partition (newest seed + sorted ops) → compose (overlay.rb:80)
partition picks the newest seed by (at, id) as the baseline and sorts the operations; compose applies each operator at its payload.path. The operator registry is open (overlay.rb:47): $set $merge $append $prepend $union $intersection $remove $inc $min $max + snip / splice. Most are commutative or last-writer-wins per key, so concurrent pokes need no operational transform — this is the CRDT-ish property loom leans on (§Case D). Entity.upsert is the write side: diff-before-write, appending a $merge poke for only the changed keys (entity.rb:27). The composed record is what "fetch this record" returns.
Stage 4 — IOU hydration: markers → blobs [✓]
consumes: a composed record whose payload may hold IOU markers
produces: the same record with markers replaced by blob content
transport: driver.hydrate → hydrate_markers (Drivers.lookup(_driver).read(_args)) (sqlite_driver.rb:379)
Explicit and on-demand — overlay does not hydrate. A reader that needs the blob calls hydrate after overlay (see storage_ops.rb: overlay then hydrate). A projection deliberately skips hydration so a fold never drags an 82KB leaf into Ruby (advaita-projection-data-model §Invariants). This separation is load-bearing for the pointer: overlay alone is not IOU-applied, so a pointer that must "compose with IOU" has to hydrate each hop itself (§The pointer, deref algorithm).
Stage 5 — Pointer deref: address → target [◇ · R]
consumes: an address whose record is type_addr :types:pointer
produces: the resolved value — walk pointer→pointer→…→source, apply path, hydrate on return
transport: Storage::Pointer.new(store:, addr:).deref — lib/wanderland/storage/pointer.rb (NEW)
The proposed fourth transform, and the default for a read that lands on a pointer: resolve to the target value. raw opts out (returns the undereferenced pointer record — the maintenance / label-move view). Hydration is deferred to the very end: the walk runs over composed-but-unhydrated records, and only the final, post-path value pulls its IOUs. Full spec in §The pointer.
The read pipeline
flowchart LR
addr["address"] --> route["Registry.resolve<br/>longest-prefix mount"]
route --> log[("append log<br/>signed records at addr")]
log --> anti["antiparticle overlay<br/>what survives · [✓ N/R]"]
anti --> ops["operator overlay<br/>seed + pokes → entity · [✓ N/R]"]
ops --> rec{"composed record"}
rec -->|"boundary stops here"| plain["the record<br/>(identity read)"]
rec -->|"asks to hydrate"| iou["IOU hydration<br/>markers → blobs · [✓]"]
rec -->|"type = :types:pointer<br/>+ asks to deref"| ptr["Pointer.deref<br/>path + next hop · [◇ R]"]
ptr -.->|"points_at = another address<br/>(maybe another mount)"| route
iou --> value["value"]
plain --> value
ptr --> value
The dashed edge is the whole trick: a deref hop re-enters resolve, so chaining and cross-mount fall out of the routing that already exists. Nothing in the registry learns about pointers.
The walk-up (trace, generations, lineage)
"Walk up" is the engine's answer to history and derived reads. It comes in built forms, plus the generation spine this case adds:
- Read-by-prefix fold [✓].
under(prefix) returns every record at or below an address in at order. foxfire's event_store appends immutable :events:event:<run>:<seq> leaves and folds them up into a composed :events:run:<run> / :events:task:<run>:<hash> via Entity.upsert. A projection (advaita-projection-data-model) is that fold, memoized and addressed.
- The append log IS the history [✓]. Stage 3 composes a baseline plus ordered pokes, so the pre-composition records (
overlay_records, or from(addr)) are the full revision history — every poke and antiparticle a signed fact. The log is the audit trail.
- The seal chain [✓].
SealChain links each record in a family to its predecessor's signature through the trace column, making the family tamper-evident (seal_chain.rb:95).
- The generation spine [◇] — one cell, a stack of revisions. This case's core move: a write at the same
(to_addr, type_addr) is a new generation, and the append auto-seals to the current head at that cell (trace = head.signature, then sign; genesis = nothing under the cell yet). No <seq> in the address — the predecessor is the head, not a seq-templated sibling; the chain the trace links form is the sequence, so a pipeline a→b→c→b→c is five generations in one cell, ordered by the chain and told apart by from_addr. Two costs the DB owns so a deep stack stays cheap: a native head-read (ORDER BY at DESC LIMIT 1 on seeds, pushed to SQL like cap_list), and a snapshot/baseline checkpoint (:types:snapshot, loom's squash) that materialises the head while old generations remain as append-only audit.
Walking vs. overlay — the two conjugate reads
A cell holds a stack, and there are two conjugate ways to read it — chosen by intent, not by storage:
- Overlay → head — "what is it now?" Compose the generations (newest seed + pokes, antiparticles applied) into one current value. The read for a thing whose generations are revisions of one entity — a label, an upserted
:events:run. latest/promoted resolve this way.
- Walk → ordered list — "how did it get here?" Follow the
trace chain (or at order) and return every generation in causal order. The read for a log — a pipeline trace, an event stream — where the generations are distinct steps, not versions of one value. Overlaying a trace would wrongly collapse a→b→c→b→c to "just the last step."
Same cell, same chain; a pointer requests whichever it needs, and ShapeMatcher (§the selector) filters the walk.
The write lock — unique(to_addr, trace)
Appending a generation is read-head → seal-to-head. Two writers racing one cell would both seal to the same head and fork the chain. The fix lives in the DB, declaratively: a unique constraint on (to_addr, trace) — a predecessor signature can be claimed exactly once, so a fork is a constraint violation and the loser retries against the new head. That is compare-and-swap on the head, the mutex moved out of the app and into the schema — retiring foxfire's in-process write_mutex + generation cache and unblocking replicas: N (task-7d58f481 de-singularise). A single pipeline run never contends (its steps are serial; distinct runs are distinct cells); the constraint only bites where writers truly share a cell — a fold entity fed by parallel producers, or a concurrently-promoted label.
Worked example — one boundary, four records
A single, deliberately dumb boundary — fetch: "get me what is at this address." Its whole surface is an address plus two opt-in flags. The same boundary produces four very different results depending only on what record it finds and which transforms the caller asked for — which is the point of §The one idea.
# the fetch boundary contract
input:
addr: <address> # required
deref: false # [◇] follow a :types:pointer to its target
hydrate: false # [✓] replace IOU markers with blob content
output: a record (or, with deref, a resolved value)
Case A — a plain record (the identity read) [✓]
Nothing special: overlay composes the entity, the boundary hands it back.
# input
addr: ":events:run:echo-20260717144738-d94b94"
# → overlay(addr): seed :events:run + its $merge pokes composed
# output — the composed record, verbatim
to_addr: ":events:run:echo-20260717144738-d94b94"
type_addr: ":events:run"
payload: { status: succeeded, duration_ms: 15509, exit_status: 0, retries: 0,
session_id: "fee93b5e-3fd3-45e5-bb84-e09e5d436b6b" }
Case B — a pointer [◇]
The found record is a :types:pointer. A read resolves it by default — the boundary walks the chain and returns the target value; pass raw to get the pointer record itself (the maintenance / label-move view), which round-trips exactly like Case A.
# input — default resolves
addr: ":projections:acq_manifest:drugbank:promoted"
# the record found there:
to_addr: ":projections:acq_manifest:drugbank:promoted"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:001419158" }
# resolve: one hop → overlay(:…:001419158) is a source record → its payload is the value
# output (default)
version: "001419158" status: promoted image_digest: "sha256:37159dba…"
metrics: { tables: { drugs: 28151, products: 323875 }, totals: { tables: 122 } }
# ── raw: true ── the undereferenced pointer, exactly like any plain read:
to_addr: ":projections:acq_manifest:drugbank:promoted"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:001419158" }
Chained + path — a pointer at a pointer, then one leaf; hydration waits until the very end:
# :deploy:img-website:current is a pointer WITH a path, at a pointer
to_addr: ":deploy:img-website:current"
type_addr: ":types:pointer"
payload: { points_at: ":projections:acq_manifest:drugbank:promoted", # itself a pointer (Case B)
path: "image_digest" }
# resolve(current): current → promoted(pointer) → 001419158(source),
# apply this pointer's path to the resolved value,
# THEN hydrate only that final leaf if it's an IOU (nothing else pulled)
# output: "sha256:37159dba…"
flowchart LR
c[":deploy:img-website:current<br/>pointer · path=image_digest"] -->|points_at| p[":…:drugbank:promoted<br/>pointer"]
p -->|points_at| s[":…:drugbank:001419158<br/>source record"]
s -->|"resolved payload"| v["{ version, image_digest, … }"]
v -->|"path: image_digest → hydrate on return"| out["sha256:37159dba…"]
Case C — a record with an IOU [✓]
The found record is an ordinary entity, but a payload leaf is an IOU marker (extracted at Stage 1). hydrate decides whether the blob comes back.
# input
addr: ":events:task:echo-…:d94b94:SWALLOW"
hydrate: true
# overlay(addr) → composed record with a lean marker (overlay does NOT hydrate):
payload:
process: SWALLOW
exit: 0
command_log: { _iou: "a1b2…", _driver: "file_store", _args: { path: "…" }, _size: 82311 }
# hydrate: true → Drivers.lookup("file_store").read(args) splices the blob back
# output (hydrate: true)
payload: { process: SWALLOW, exit: 0, command_log: "#!/usr/bin/env bash\n… 82KB …" }
# output (hydrate: false) — the marker stays; the OOM-safe / projection read
payload: { process: SWALLOW, exit: 0, command_log: { _iou: "a1b2…", _size: 82311 } }
Pointer × IOU compose: if deref lands on a record like this, the pointer must hydrate the resolved hop for the AC "composes with IOU" to hold — because overlay alone leaves the marker (Stage 4). See the deref algorithm.
Case D — a record with many revisions (two lineage models)
D1 — overlay-as-CRDT (loom-pack style) [✓]. Many writes at one cell; the read composes them. Concurrent editors never need a merge because the operators are commutative / last-writer-wins per key (wanderland-loom-pack-design "commutative and need no operational transform").
addr: ":events:run:R"
# the append log at this cell — each line a signed generation:
- type_addr: ":events:run" payload: { status: running, retries: 0 } # seed @T0
- type_addr: ":types:poke" payload: { op: $inc, path: retries, value: 1 } # @T1
- type_addr: ":types:poke" payload: { op: $merge, path: payload,
value: { status: succeeded, duration_ms: 15509 } } # @T2
# overlay: newest seed as base, pokes in at-order, LWW per key
# output: { status: succeeded, retries: 1, duration_ms: 15509 }
D2 — a moving label leaves a walkable trace [◇]. Promotion moves a label, and a label is a pointer (advaita-projection-data-model). The immutable instances stay put; the moving promoted cell gets a new generation naming the new instance, auto-sealed to the prior one via trace — no seq, no new column.
# promote 001420777 over 001419158 — append a new generation at the label cell (:…:promoted, type :types:pointer):
# gen0 :…:drugbank:promoted { points_at: :…:001419158 } trace = <genesis>
# gen1 :…:drugbank:promoted { points_at: :…:001420777 } trace = <sig(gen0)>, signed over it
#
# read :…:promoted → head generation = gen1 → instance 001420777 (overlay newest-seed)
# walk promotion history → down the trace chain: gen1 →(trace)→ gen0
# cheap view = at-order over at(:…:promoted)
# verifiable view = recurse trace→signature within the cell (tamper-evident, gap-detecting)
The instances are pinned by revision (:…:drugbank:001419158 is a cell you cite by signature); the label is the moving head of a :types:pointer cell. Same board, one vertical stack — the history is the stack read downward.
The pointer — the proposed data model [◇]
Cell, revision, projection — the selector
A pointer's target is a selector resolved through the cell's reads (§The board, §Walking vs. overlay). Up to four parts — the first three find the generation, the last projects a field:
- cell (address legs) — the indexed pre-scope.
under: a to_addr (prefix or exact) bounds the run/family; from: picks the writer (the boundary); type: the kind. All three are indexed columns (idx_to_addr / idx_from_addr / idx_type_addr), so this narrows to a handful without a scan.
- match (ShapeMatcher) — the occurrence filter, over the pre-scoped candidates. Same vocabulary as scenario
expected:, boundary when:, capability filter:. Because it runs over the few records the address legs already selected, it's a cheap Ruby pass, not a store scan.
- revision — of what survives: the head (newest by
at, default — the moving label), a pinned @signature (immutable — the Docker @digest), or walk them all (the list read).
- projection — the optional
path, a dot-leaf out of the resolved value; composes across a chain.
# pluck one image's digest out of a build that produced eight
points_at:
under: ":traces:sc-pipeline-images:<run-id>" # the run (idx_to_addr)
from: ":boundary:docker_build" # the 8 builds (idx_from_addr)
match:
result: { image: sc-pipeline-clownfish } # ShapeMatcher → the clownfish build
# revision: head (newest by at) — or @<sig> to pin a specific rebuild
path: result.image_digest # → "sha256:…"
sc-pipeline-images builds eight images, all through docker_build, so they are eight generations in one cell told apart only by their payload. from gets the eight; match plucks clownfish; at/head resolves a rebuilt one to the latest; path pulls the digest. Nothing in the pointer knows it was the fifth docker_build — position never enters it.
The vocabulary is addressable-first: the indexed address legs always run before the shape filter, so a board move is bounded before ShapeMatcher sees a record. A bare match: with no under:/from:/type: pre-scope (a store-wide shape scan) is the one thing rejected (§Open questions).
Record shape
Reuses the label shape verbatim, so promotion tags and pointers are one mechanism.
to_addr: ":<anything>:<name>" # you name it whatever you want
from_addr: ":sessions:…" # signed like every record
type_addr: ":types:pointer" # the switch Stage 5 keys on
payload:
points_at: ":events:run:echo-…" # REQUIRED — any address, any mount
path: "metadata.image_digest" # OPTIONAL — dot-path into the resolved value
The value object
Storage::Pointer.new(store:, addr:) # store = Registry OR Adapters::Store (exposes resolve/overlay)
#deref(max_depth: N) → the resolved value
#record / #raw → the undereferenced pointer record (Case B, deref:false)
#points_at, #path → the pointer's own fields
#target → one hop only (optional)
Registry.pointer(addr) # optional convenience factory — NOT required
The registry stays neutral: "request a Pointer and .deref" vs "read a record" is the resolve-vs-manage distinction. A pointer you don't deref is the raw record, so a boundary can still move a label without ever resolving it.
Deref algorithm (spec these)
resolve(addr, depth=0, seen={}): # the DEFAULT read; walks UNHYDRATED
raise PointerCycle if addr ∈ seen OR depth > max_depth # cycle-safe
rec = store.overlay(addr) # antiparticle + operator applied — NOT hydrated
raise PointerUnresolved if rec is nil # dangling hop — see policy
if rec.type_addr == ":types:pointer":
seen.add(addr)
resolved = resolve(rec.payload.points_at, depth+1, seen) # recurse → re-enters resolve → cross-mount
return extract(resolved, rec.payload.path) # this pointer's path applies to the resolved value
else:
return extract(rec.payload, path-from-the-pointer-that-reached-here or whole)
# hydration happens ONCE, at the boundary, on the value about to be returned:
return hydrate(resolve(addr)) # only the final post-path value pulls its IOUs
raw(addr) = store.overlay(addr) # the opt-out: the plain pointer record, no walk, no hydrate
Points that need nailing, each mapped to an AC:
Semantic
Decision
Default = resolve
a read that lands on a :types:pointer walks the chain and returns the target value; raw opts out to the undereferenced record.
Chain
pointer→pointer→…→source, each hop store.overlay(next). Path composes: a pointer's path applies to the fully-resolved value of its points_at.
Cross-mount
points_at names any address; the recursion re-enters Registry.resolve, so a mount-A pointer resolves a mount-B record with zero special-casing.
Hydrate last
the walk reads composed-but-unhydrated records; hydration runs once, on the final post-path value being returned — intermediate hops and unselected fields never pull their IOUs.
Cycle-safe
visited-set and max_depth; a loop raises Storage::PointerCycle, never hangs.
Raw round-trips plain
raw (a.k.a. #record/#raw) returns the composed pointer record exactly like Case A — no walk, no hydrate. The resolve is default; the raw view is the opt-out and transparent.
Missing target
Recommend: resolve raises Storage::PointerUnresolved on any dangling hop (a written pointer names an address that should resolve; silence hides a bug). Existence probes use a non-raising raw/#target, which may return nil. Consistent rule: resolve is all-or-raise.
Where it lives
A pointer is a storage device. The overlay read follows it, in the same layer that composes the rows of an address, so no reader above the store sees a pointer.
overlay(addr) :: StorageRegistry#overlay (runtime.rb) and Storage::Registry.overlay follow each pointer hop by hop, across mounts, at the reader's access level. Each hop's path applies to the payload, innermost first. A dangling pointer, a cycle, or a chain past 32 hops reads as absent.
lstat(addr) :: the same read with no pointer followed. Storage.lstat(store, addr) falls back to overlay on a bare driver.
- Writes :: the write path reads the address with
lstat (Entity, Writer, the poke preview, content_migrate). A write to a pointer address therefore acts on the pointer. A :type:pointer write moves it.
- Listings ::
overlay_all returns each pointer as stored. A listing names the pointer, and a read of one address follows it.
Storage::Pointer.follow :: the hop loop that the overlay calls (lib/wanderland/storage/pointer.rb). The Pointer value object and the Resolve ledger resolver read each hop with lstat. Each of them keeps its own cycle error.
- **
!Pluck steps** :: A `$ step reads the record of its source. The source is self or a : address. On a pointer address, path: $to_addr gives the resolved address.
Alignment — labels ARE pointers
advaita-projection-data-model already settled: no separate :pointers: namespace — a label lives in the instance family and is a thin pointer ({ points_at: ":…:<run_id>" }), exactly Docker's tag→digest. This case supplies the engine primitive that the projection-model's label mechanism becomes a special case of:
:…:promoted / :…:latest / :…:candidate are :types:pointer records; moving a tag is appending a new pointer (Case D2).
latestManifest(source) = Pointer.new(store, ":…:<source>:promoted").deref — one overlay, O(1).
- SSM-style "the image digest in production" = a pointer at a leaf of an already-published record (Case B chained + path). No separate SSM writes at deploy time.
- The trace-offload manifest (sibling task-7be1e663) reads its rollup through a pointer.
- GraphQL / operational reads become "read the address, get the value" — deref where a pointer is expected (later).
# BEFORE — one wrapper record carrying the subtree inline (abridged; the real one repeats cfn 3×)
to_addr: ":trace:foxfire-deploy:req-…:12" # component, at colon-depth 1
from_addr: ":boundary:component"
result:
action_taken: no_change
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd
_signals: # ← the children, embedded
- boundary: chain
result:
_signals:
- boundary: cfn_provisioner
result:
_signals:
- boundary: cfn_template_analyzer
- boundary: cfn_param_guard
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd # cfn result, copy 2
- boundary: publish_cfn_outputs
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd # cfn result, copy 3
- boundary: k8s_provisioner
result:
_signals:
- boundary: kustomize_set_image
- boundary: kubectl_apply
# AFTER — every actor its own generation in the run cell; ownership is `scope`, not embedding.
# All rows share to_addr = :trace:foxfire-deploy:01KYSSEG… (the run cell). `sig(x)` = x's signature.
- from_addr: ":boundary:start_run" trace: <genesis> scope: — # run root
- from_addr: ":boundary:artifacts" trace: sig(collect) scope: sig(start_run)
- from_addr: ":boundary:docker_build" trace: sig(artifacts) scope: sig(artifacts)
- from_addr: ":boundary:ecr_repository_provisioner" scope: sig(docker_build)
- from_addr: ":boundary:ecr_docker_login" scope: sig(docker_build)
- from_addr: ":boundary:component" trace: sig(docker_build) scope: sig(start_run)
- from_addr: ":boundary:cfn_provisioner" trace: sig(component) scope: sig(component)
- from_addr: ":boundary:cfn_template_analyzer" scope: sig(cfn_provisioner)
- from_addr: ":boundary:cfn_param_guard" scope: sig(cfn_provisioner)
- from_addr: ":boundary:publish_cfn_outputs" scope: sig(component)
- from_addr: ":boundary:k8s_provisioner" trace: sig(publish_cfn_outputs) scope: sig(component)
- from_addr: ":boundary:kustomize_set_image" scope: sig(k8s_provisioner)
- from_addr: ":boundary:kubectl_apply" scope: sig(k8s_provisioner)
- from_addr: ":boundary:complete_run" trace: sig(kubectl_apply) scope: sig(start_run)
execute_registration(boundary): # engine — the single seam per boundary
frame = stack.push(boundary) # entry: signature unknown (body hasn't run)
result = invoke(body) # children dispatch here → buffered in frame.inner
crossing = Crossing.new(result).sign(identity) # exit: result known → sig(boundary) exists
# flush THIS boundary's DIRECT children — their scope (= sig(boundary)) is now known:
frame.inner.each { |child| child.scope = crossing.signature; store.append(child) }
stack.pop
return crossing # NOT self-appended — buffered into the parent's
# inner; the parent flushes it at ITS exit
# top-level slots flush at run-close, scope = the run-root crossing's signature
The trace tier — a run is a cell, sub-boundaries scope to their parent [◇]
The context/crossing tier is the same board as everything above, read the same way: a run is a cell, every boundary crossing is a generation in it, and the composed run is a read-time fold — never a stored blob. This section specs how the engine's trace records land on the board, and retires the two mechanisms that predate it (:<seq> addressing and the _signals roll-up). Design-locked with Graeme 2026-07-30, grounded in a real trace (Jenkins foxfire-dev-deploy #9, wanderland --type cli vivarta.yml deploy --wanderland-trace).
What the old model produced (the two problems)
A depth-1 skeleton. Every crossing was addressed :trace:<svc>:req-<uuid>:<seq>, ordered by the <seq>, and the default trace view (Context#main_chain_predicate) filtered to colon-depth ≤ 1. The foxfire run emitted 16 crossings — adapter_cli, env_snapshot, start_run, collect, load_metadata, noop, artifacts, component, complete_run, and enforce_denials firing between every slot. The actual work — ecr_repository_provisioner, ecr_docker_login, docker_build, cfn_provisioner, k8s_provisioner, kubectl_apply — RAN but was invisible: it dispatched at colon-depth ≥ 2 and never surfaced in the trace. The <seq> was only ordering that skeleton.
_signals duplication. Ownership (which actor a wrapper dispatched) was captured by folding each child's result into the wrapper's own result under a reserved _signals array (Boundary#roll_up_inner_context). Because every roll-up layer re-embeds its children's full subtree, a two-level nest stores the leaf three times. In the real component crossing the CFN result (stack_name, ssm_writes, resource_writes) appears at component.result, component.result._signals[chain].result, AND …_signals[chain]._signals[cfn_provisioner].result — one deploy, the outputs stored 3×.
The model — one cell, an id, two edges
Every crossing carries a stable id minted the instant its boundary is entered — before the body runs, long before a result or signature exists. That id is the crossing's identity, and it is what the two lineage edges point at:
field
points at
role
when known
id
—
the crossing's own identity
entry (minted up front)
trace
predecessor's id
causal order — "what ran next"
entry (predecessor is the cell head)
scope
parent's id
ownership — "who dispatched me"
entry (parent is on the call stack)
signature
—
integrity — signs the record incl. its id/trace/scope
exit (commits to the result)
Every crossing in a run writes to the same to_addr — the run cell, keyed on the ULID start_run mints (:trace:foxfire-deploy:01KYSSEG1S255MTYZ2QFXFEW55). No <seq>, no nested :<name> depth. Crossings stack as generations; trace orders them (the generation spine, §The walk-up), scope nests them.
The id splits the two roles the signature used to conflate. A signature carries integrity and can only exist at exit (it commits to the result). But identity is needed at entry — so a child can name its parent, and a successor its predecessor. Binding both to the signature forced everything to wait for exit; that was the corner. Mint an id at entry and the wait disappears: trace and scope are entry-known id-pointers, the signature is an exit-time integrity artifact over them, and to verify a parent you resolve its id → its record → its signature (identity and integrity stay linked, just not welded).
_signals is deleted. Every sub-dispatched actor becomes a first-class crossing in the run cell with scope = <parent id>. The rolled-up value a consumer used to read out of _signals is reconstructed on read — a fold over WHERE scope = <parent id> (the same "every read is a composition" as overlay and generations), never a stored array. from_addr still names the boundary; payload shape tells same-boundary siblings apart (the eight images of an 8-way docker_build, §the selector).
Worked example — the artifacts and component subtrees
The real foxfire run, as it is stored today (nested _signals, the leaf duplicated) versus the model (flat crossings, each leaf once, joined by id):
# BEFORE — one wrapper record carrying the subtree inline (abridged; the real one repeats cfn 3×)
to_addr: ":trace:foxfire-deploy:req-…:12" # component, at colon-depth 1
from_addr: ":boundary:component"
result:
action_taken: no_change
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd
_signals: # ← the children, embedded
- boundary: chain
result:
_signals:
- boundary: cfn_provisioner
result:
_signals:
- boundary: cfn_template_analyzer
- boundary: cfn_param_guard
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd # cfn result, copy 2
- boundary: publish_cfn_outputs
stack_name: advaita-ecs-workloads-foxfire-useast13-dev-bd # cfn result, copy 3
- boundary: k8s_provisioner
result:
_signals:
- boundary: kustomize_set_image
- boundary: kubectl_apply
# AFTER — every actor its own generation in the run cell; ownership is `scope`, joined by `id`.
# All rows share to_addr = :trace:foxfire-deploy:01KYSSEG… (the run cell). id(x) = x's entry-minted id.
- from_addr: ":boundary:start_run" id: id(start_run) trace: — scope: — # run root
- from_addr: ":boundary:artifacts" id: id(artifacts) trace: id(collect) scope: id(start_run)
- from_addr: ":boundary:docker_build" id: id(docker_build) trace: id(artifacts) scope: id(artifacts)
- from_addr: ":boundary:ecr_repository_provisioner" scope: id(docker_build)
- from_addr: ":boundary:ecr_docker_login" scope: id(docker_build)
- from_addr: ":boundary:component" id: id(component) trace: id(docker_build) scope: id(start_run)
- from_addr: ":boundary:cfn_provisioner" id: id(cfn_prov) trace: id(component) scope: id(component)
- from_addr: ":boundary:cfn_template_analyzer" scope: id(cfn_prov)
- from_addr: ":boundary:cfn_param_guard" scope: id(cfn_prov)
- from_addr: ":boundary:publish_cfn_outputs" scope: id(component)
- from_addr: ":boundary:k8s_provisioner" id: id(k8s_prov) trace: id(publish_cfn_outputs) scope: id(component)
- from_addr: ":boundary:kustomize_set_image" scope: id(k8s_prov)
- from_addr: ":boundary:kubectl_apply" scope: id(k8s_prov)
- from_addr: ":boundary:complete_run" id: id(complete_run) trace: id(kubectl_apply) scope: id(start_run)
The docker_build and the ecr/cfn/k8s steps — invisible before — are now flat in one read. "The component's CFN outputs" is WHERE scope = id(cfn_prov), stored once. walk(trace) replays the whole run in order; group(scope) rebuilds the tree.
The call stack — an id per frame, eager writes, one read surface
The engine already threads the tree: Boundary#execute_registration is the single entry point for every boundary execution, and BoundaryInput#dispatch gives each wrapper an inner seam for its children. The model adds a stack of ids and makes the writes eager.
The corner this solves: a sibling read mid-loop (!Context for_boundary.git_hash.sha inside collect) must find "the children of the wrapper I'm currently inside." That set can't be keyed on the wrapper's signature — it's still executing, unsigned — and we don't want an in-memory @events array. The id resolves it:
- Entry: mint the boundary's
id, push it on the stack. Its record isn't written yet (no result) — and that's fine: nothing needs the parent's record, only its id, which is now on the stack.
- Children write eagerly. A child, on its own completion, reads
stack.top → stamps scope = <parent id> and appends itself to the run cell immediately — its id, trace, and scope are all known. So prior siblings are in the store during the parent's execution.
- Mid-loop reads are plain SELECTs against the SQL store:
WHERE scope = <current frame id> (optionally AND from_addr = …) returns the prior siblings. One read surface, live and durable alike — no @events, no waiting for unwind.
- Exit: the result now exists → build this boundary's record (
id, trace = predecessor id, scope = grandparent id, result), sign it (integrity, over the id-pointers), append it. Pop.
execute_registration(boundary):
id = mint_ulid() # entry: identity BEFORE the body runs
scope = stack.top # parent's id (nil at the run root)
stack.push(id)
result = invoke(body) # children read stack.top → scope=id; append EAGERLY
crossing = Crossing.new(id:, from_addr:, result:,
scope:, trace: cell.head_id).sign(identity) # exit: result known
store.append(crossing) # the boundary's own record, appended now
stack.pop
# a child already did, at ITS completion: store.append(child, scope: id) ← queryable mid-loop
Nothing is deferred; the parent's record simply lands after its children's, which is the true completion order the trace chain wants anyway.
Consequences (spec these into the reads)
- The wrapper's own result stops carrying merged child keys.
artifacts.result.image_tag (which today leaks up from docker_build via the LWW fold) is gone from the wrapper record — the wrapper computes its result by reading its own children (WHERE scope = <my id>, all present by its exit), and any consumer reconstructs the subtree the same way. !Context for_boundary.artifacts.image_tag, context[...], and the !Context { under:, boundary: } resolver all become folds over WHERE scope = <parent id> (recursively), replacing _signals-LWW and the retired colon-depth/segment matching.
- New columns:
id (ULID, minted at entry) and scope (parent id) on the record — both additive; trace now holds the predecessor's id rather than its signature. Index id and scope.
- Retired:
<seq> in the address, Context#allocate_addr's sequence, fork_at/sub_at colon-depth nesting, main_chain_predicate, in_scope?, the @events/@verified arrays, and roll_up_inner_context's _signals emission. (run_grid — the only fork/sub consumer — is dead code and goes with them.)
- Live visibility is unaffected and in fact improved: crossings hit the store as they complete (eager), and per-crossing span sinks (the foxfire emit path) still fire independently.
- Who signs the cell. The generation spine's head-seal (§The write lock) is one option for the DB-owned chain, but a trace crossing wants its identity (PKI) signature, not the mount's seal signer — reconcile before wiring the
:trace: mount to head-seal. With trace now an id-pointer, the write-lock unique(to_addr, trace) becomes unique(to_addr, trace-id) — same compare-and-swap on the head.
Invariants (must compose)
- Deref is the default;
raw is the opt-out. A read that lands on a :types:pointer resolves to the target value; raw returns the undereferenced record. Introducing :types:pointer changes no existing plain read — a non-pointer record reads exactly as before.
- Deref reads composed state. Every hop is
store.overlay(addr) (antiparticles + operators applied), never the raw log — a pointer resolves current truth, and moving a label is itself just an overlay write.
- Hydrate last. Nothing hydrates until the final, post-
path value is about to be returned to the client; the walk runs over lean records and unselected fields / intermediate hops never pull their IOUs. (A points_at string is far under threshold, so a pointer record is lean by construction anyway.)
- Append-only; a pointer move never mutates. Re-pointing a label appends a new pointer generation, auto-sealed to the head; the prior pointer and every instance remain signed facts. History is a walk, not a diff.
- Cycle raises; resolve is total.
resolve either returns a value or raises (PointerCycle / PointerUnresolved) — it never hangs and never returns a half-resolved marker.
Open questions for the implementation discussion
- The
store hydrate seam. Deref stays lean — it hydrates once, at the end, on the final post-path value. Whether that terminal hydrate is store.resolve(addr).first → driver.hydrate(value) (keeps the handle contract as-is — leaning) or widens the store handle with a hydrate is the only seam question. Decide before build.
- Predicate pushdown for
match: (later). Today match: runs in Ruby over the address-pre-scoped candidates (a handful) — fine, and the upper bound can grow. Later optimisation: extract the predicate-shaped parts of a ShapeMatcher match and push them into the driver as a WHERE on a by_json-indexed leaf when the shape is known — advertised per driver, native-or-fallback like the capability surface. Not building the pg-integrated ShapeMatcher compiler now; the seam is left open. Hard rule stays: no match: without an address pre-scope.
Registry.pointer(addr) factory — trivial convenience, deferred.
Resolved this session: deref is the default read (a read landing on a pointer resolves to the target); raw opts out to the undereferenced record (maintenance / label-move). Hydration is terminal-only — nothing hydrates until the final, post-path value is returned. Generation lineage reuses trace (predecessor = head at the cell) — no trace_addr, no <seq>; a step is addressed by from_addr, an occurrence picked by ShapeMatcher match: over an indexed pre-scope; revision id = the record signature; walk and overlay are conjugate reads; chain linearity via unique(to_addr, trace) (the DB write-lock); the storage track is consolidated under this case (task-7d58f481).