Wanderland

advaita-projection-engine

Make the datastore the owner of truth. The move is to lift the fold down into the store — as materialized projections computed once and read cheaply — so foxfire trends toward a stateless reader. Two layers of that ship today: a named-capability read surface in wanderland-core, and a set of seal-time rollup projections foxfire memoizes when a run closes. The generic, data-declared projection engine that would unify them is the target this node also describes.

The shapes this engine consumes are the contract: advaita-projection-data-model (the correctness gauge — read it alongside). This node is the why and the how; that node is the what.

Built, and target

Lineage — big sister to little sister to here

crossings (big sister)                       four-address store · three-way FORKING WALK ·
[crossings-architecture](crossings-architecture)                    capability → compile → plan → execute → bridges ·
                                              Pause/Fetch/Splice · the store IS the queue
        │  keep the record shape + capability ADDRESSING
        │  DROP the walk + planner + bridges
        ▼
wanderland-core storage engine (little sister)   append-and-read-by-prefix · overlay via
[wanderland-core-storage-engine](wanderland-core-storage-engine)                antiparticles · signed-from · IOU blobs
        │  bring capability addressing BACK, scoped to storage reductions
        │  swap compile-to-bridge-plan → dispatch-to-driver-native-or-fallback
        ▼
storage capability surface (task-7d58f481)       BUILT: list / aggregate / pending AT an address ·
                                                  resolved longest-match · [N] native / [R] fallback ·
                                                  fallback IS reference semantics
        │  memoize the fold · address the result · defer recompute
        ▼
seal-cached projections (foxfire, built)         config triggers → rollup_* → :projections:<run>:<kind> ·
                                                  read as sealed ∪ live
        │  [target] generalize: data-declared spec · watermark · /reload
        ▼
the declarative projection engine (target)       closed/open + watermark · seal-cached materialization ·
                                                  /reload · full de-singularize

The big sister (crossings-architecture) is the full capability engine: a request (address, capability, request_context) triggers a reductive three-way walk that compiles a plan, previewed then executed through bridges — its four phases are the fundamental algorithm (Pause → Fetch → Splice → Continue). The little sister kept the record shape and dropped the walk and planner. This engine reintroduces exactly one idea from the big sister — capabilities addressed at a type_addr — scoped to storage reductions, and realizes them by dispatch to the mounted driver, not by compiling to bridges. It does not rebuild the planner.

A projection is the fundamental algorithm

A projection is a memoized fold over the append-only ledger, and reading it is Pause → Fetch → Splice → Continue:

Burn the CPU iff you need it is deferring the Splice until a query forces it — delayed binding, the same move as lazy evaluation everywhere.

The three layers

Layer Owns Built form
Storage engine (wanderland-core-storage-engine) the append log + overlaid current state append-only, signed, prefix-mounted
Capability surface (task-7d58f481) compute a reduction once, portably list / aggregate / pending, [N] native or [R] fallback (reference)
Projection / cache when NOT to recompute — materialize + memoize foxfire's config-triggered rollup_* seals writing :projections:<run>:<kind>

The capability answers "get me the count and cost without OOMing" ([N] GROUP BY); the projection answers "don't run even that again for a sealed run." They compose: capability = compute the fold; projection = memoize the fold. [N]-vs-[R] and cache-vs-source are both the same fold relocated to a cheaper coordinate, never new truth. refold_run proves rederivation. [target] a single declarative engine that both computes and memoizes from one spec is the layer these two grow into.

The capability surface — resolution × realization

Crossings' capability addressing, minus the walk — built:

resolution:  (address | type_addr, capability_name)  --longest match-->  mounted driver
realization: driver.capability?(name)  →  yes: driver.cap_<name>(addr, **args)   no: Capabilities.<name>(driver, addr, **args)

What it is not: no compiler, no plan/preview/execute pipeline, no bridges, no forking walk, no query language. Named ops only.

The projection layer — seal triggers, built

foxfire memoizes a completed run's folds through data-declared triggers. config.yml triggers: match a record type_addr and fire a named rollup_* boundary; rollup_run runs first on run:completed, then cost, reconcile, OTLP, and envelope read the run projection it wrote. Each rollup_* is a [B] reducer ending in one Entity.upsert under :projections:<run>:<kind>. This realizes freshness: seal without a declarative field: the trigger table is the lifecycle, and a completed run's frozen inputs make the row permanent.

The reduce split holds at this layer. A metric read uses the list / aggregate capability (data-driven, native-or-fallback); a judgment is a named Ruby boundary (verify_run, the rollup_* seals). Data-driven wiring with code-defined reduction for the hard cases — the same split as actor type: dispatch across the framework. Forcing a judgment into PL/pgSQL would re-grow the crossings planner the little sister dropped.

[target] The declarative engine generalizes the wiring: projections declared as data (over / partition / reduce / into / freshness / seal_when — advaita-projection-data-model §spec), a metric reduce: verb compiling to an SQL template plus a Ruby reference reducer, and one materialization scheduler deciding when (on write / on read / at seal / on cron) and where to persist. foxfire's per-run seals are the seal case of this, hand-wired.

Composite judgments — the exemplar

The record Graeme surfaced — verdict: clean, summary, findings, per-process planned-vs-placed-vs-used drift, "1 death absorbed by retry: SWALLOW" — is the proof the model generalizes past metrics. Today it is two records, not one:

[target] One run_verdict [B] boundary, freshness: seal, folding verify_run and reconcile into a single document at :projections:run_verdict:<run_id>, served forever. Not built: reconcile is sealed, the seal-chain verify is still live. (Worked values: advaita-projection-data-model §3.)

Hot reload — /reload/projections

[target] Push a ConfigMap, hit an endpoint, projections update in-process. Two halves of different weight, neither built:

Today a projection change is a code change to a rollup_* boundary and a config.yml trigger, applied by redeploy.

De-singularize — the north star

foxfire is replicas: 1 because part of the read model still lives in one Ruby process. The seq/seal-chain half of the ceiling is gone; the read-cache half remains.

Target: foxfire as a stateless facade over store-owned projections — replicas: N. The GraphQL schema (schema.rb) already resolves :projections: rows as fields — the run list, run detail, and reconcile read the seals — so widening replicas turns on retiring the generation cache, not on rewriting the read surface.

Decisions (Graeme, 2026-07-29)

Ecosystem reference: Marten (.NET/Postgres) is the projection-lifecycle vocabulary borrowed here (inline/async/live + rebuild-from-events = our refold). pg_ivm = incremental matviews, aggregate-limited. TimescaleDB continuous aggregates + uddsketch/tdigest = closest match to closed/open+watermark+cheap-p95, declined. Nobody ships "YAML spec → PL/pgSQL-or-Ruby, hot-reload, sqlite fallback" — that compiler is the bespoke work over standard parts.

Build spine

Generalizing the hand-rolled seals — traps

The general engine must subsume foxfire's config-triggered rollup_* seals without duplicating them and without deepening the single-writer ceiling. These constraints hold for the build.

Done test

The engine is general when all six rollup_* seals, trace_otlp, and the promoted manifest label are declarations, with nothing hand-rolled left in foxfire. The hard cases are rollup_envelope (a cross-run merge — incremental plus a watermark) and the ordered rollup_run-then-dependents chain.

Open questions

Refold as traceable generations

refold_run is built as a re-derivation: it groups a braided run's events by run_name, replays the fold per group under the true identity, and tombstones the stale entity (advaita-projection-data-model §Generation lineage).

[target] Refold as a new generation that traces to the one it supersedes, reintroducing crossings' fourth address, trace_addr (the causal pointer). A refold would write a fresh full projection whose trace_addr points at the prior generation; reads take the head, and walk(trace_addr) recovers gen N → … → gen 0. The name is trace_addr, not trace — foxfire already uses trace for the Nextflow resource-trace payload. Not built: the current refold tombstones and rebuilds in place.

Pointers are labels — the Docker convention

The run-free "current" reads (latestManifest, the promotion pointer) are the built Storage::Pointer: a pointer is an instance record whose leaf ref is a label — :projections:acq_manifest:<source>:promoted / :latest, exactly Docker's repo:tag (run_id ≈ immutable digest, label ≈ moving tag). Pointer#deref walks the chain unhydrated, applies each hop's path, and hydrates the final value once. latestManifest = resolve the promoted label; manifests(last: N) = walk it back N. Status becomes a tag (candidate → promoted are labels), so "compare against the last promoted, not the last emitted" is structural, not a filter. The label is written on promotion, so latest_by scan-reduce is only the rederivation oracle. Full role/instance/label convention: advaita-projection-data-model §Stage-record addressing.