ADR-0197: vision compiler over Delta-CRDT (docs only) (#513)

This commit is contained in:
Shay 2026-05-31 20:32:08 -07:00 committed by GitHub
parent 1c56ac710e
commit 81062bb0fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 643 additions and 0 deletions

View file

@ -0,0 +1,188 @@
# ADR-0197: CORE-native Vision Compiler over the Delta-CRDT Substrate
**Status:** Proposed
**Date:** 2026-05-31
**Authors:** Joshua M. Shay, Core R&D Engine
**Domains:** `sensorium/vision/`, `sensorium/adapters/vision.py`, `packs/vision/`, `core-rs/src/vault.rs` (read-only contract), `evals/vision_sensorium/`
**Depends on:** ADR-0013 (Sensorium Multimodal Protocol), ADR-0180 (Delta-CRDT Sharded Substrate), ADR-0181 (Audio Compiler — structural precedent)
**Companion docs:** [vision-compiler-spec.md](../plans/vision-compiler-spec.md) *(to be written)*, [vision-compiler-eval-plan.md](../plans/vision-compiler-eval-plan.md) *(to be written)*
---
## 1. Context & Problem Statement
`sensorium/protocol.py` already fixes the contract vision must satisfy:
- `ProjectionHead.project(S) -> (32,) float32` is the **Logos-recovery boundary** (`CL41_DIM = 32`). A visual scene is recovered as words in the one manifold; once it crosses the boundary the field has no concept of "vision."
- `Modality.VISION` already exists in the enum but has **no adapter and no compiler**`sensorium/adapters/` ships `text.py` and `audio.py` only.
- `ModalityPack` enforces gate/checksum invariants at construction; `ModalityRegistry.mount()` runs the unitarity check and `project()` refuses a closed gate.
ADR-0013 (Accepted) requires every new modality to cross this boundary *before* it reaches `ingest/gate.py`, and forbids touching `ingest/`, `field/`, `generate/`, `vault/`, `vocab/` to add one. Vision must therefore **arrive already compiled** into a `(32,)` Cl(4,1) versor.
ADR-0180 (Proposed) introduces the Delta-CRDT sharded substrate **explicitly to absorb the continuous, high-density streams that audio and vision produce** — vision is named, not incidental — and imposes the same hard constraint audio answered (§1.5.2): any operation upstream of `vault/store` that the substrate parallelizes must either be proven order-invariant on its inputs, or carry an explicit serialization barrier.
ADR-0181 (Proposed) is the structural precedent. This ADR deliberately inherits its skeleton — pipeline shape, checksum chain, merge-key triple, serialization-barrier discipline, PR ladder, gate-closed default, and the **"compiler, not embedding bridge"** doctrine. What it does **not** inherit is audio's canonical ordering or blade semantics: those are temporal and acoustic. Vision's manifest is **spatial and scale-structured**, and that single difference is the load-bearing redesign of §2.1 and §2.6.
The problem this ADR solves: **how does vision enter CORE as a lawful, deterministic, replayable modality that is also a well-behaved Delta-CRDT delta producer** — without violating the no-core-mutation rule, the exact-recall rule, or ADR-0180's order-invariance obligation.
The wrong answer is an embedding bridge (ViT / CLIP / DINOv2 / SAM as substrate). The temptation is far stronger here than for audio because pretrained vision encoders are excellent — and that is exactly why it must be resisted. An opaque latent cannot be checksummed, cannot be replayed bit-for-bit, and cannot supply the content-addressed merge key ADR-0180 §1.5.3 demands. The right answer is a **deterministic visual compiler**.
## 2. Decision
We will build `vision_core_v1` as a **deterministic visual compiler** under `sensorium/vision/`, lowering a canonical image (or single video frame) through a typed `VisionIR` into a `(32,)` float32 Cl(4,1) versor, and we will make its **spatial chunk boundary the Delta-CRDT delta boundary**. Learned models (ViT, CLIP, DINOv2, SAM, depth/flow estimators) are admitted only as subordinate teacher/shadow lanes, never as the substrate.
The compilation pipeline (detailed in the companion spec) mirrors audio's shape with spatial substitutions:
```text
image / single video frame
→ canonicalizer (fixed colorspace + gamma, fixed resampling grid, source+canonical sha256)
→ spatial grid (tile lattice × scale pyramid — the analog of audio's frame grid)
→ visual lexer (orientation energy, spatial-frequency bands, luminance/chroma stats,
corner/blob onsets, region boundaries)
→ typed VisionIR (regions/segments, contour arcs, salient-object events, texture atoms,
content anchors)
→ operator registry (pack-local blade aliases + quantized theta rules)
→ rotor lowering (elliptic bivector rotors only in v1)
→ versor composition (geometric_product → unitize_versor → versor_condition < 1e-6)
→ (32,) float32 == one VisionCompilationUnit
```
Normalization sites stay inside CLAUDE.md's allowlist: quantization and resampling live in the vision pack/compiler construction boundary, `unitize_versor` is algebra-owned, and **no hot-path drift repair is added**.
### 2.1 The optimal mapping to Delta-CRDT (the load-bearing decision)
ADR-0180 §1.5.2 gives vision the same binary choice it gave audio: prove order-invariance, or carry a serialization barrier. We answer both, at two granularities — but the *within-chunk ordering is the part that differs from audio and must be designed, not copied*.
| Granularity | Operation | CRDT treatment | Why |
|---|---|---|---|
| **Within a chunk** (one tile, or one image at one scale) | `compile_events` (rotor chaining via `geometric_product`) | **Serialization barrier** | The sandwich product is non-commutative. In-chunk composition runs serially, single-threaded, in **canonical spatial order**, inside one thread-local arena. |
| **Across tiles / scales / frames** | merge of `VisionCompilationUnit`s into the Vault | **Order-invariant delta** | Each unit is `(versor, provenance)` written at the `vault/store` layer — the only semilattice-eligible layer. Merge is commutative, associative, idempotent. |
**The redesign vs ADR-0181.** Audio's canonical within-chunk order is the temporal hop index — total and obvious. Vision has no time axis inside a frame, so the canonical order must be an **explicit, deterministic, resolution-independent spatial total order over the IR events**. The v1 proposal: order by `(scale_level, morton_code(tile_row, tile_col), event_category_precedence, stable_event_id)`, where `morton_code` is a Z-order curve over the fixed tile lattice. This makes the in-chunk fold reproducible and gives proof obligation V-4 something concrete to assert against. **This ordering rule is the single most important thing to red-line in this ADR** (see §2.6).
### 2.2 Content-addressed merge key from the checksum chain
Identical to ADR-0181 §2.2 in structure; only the layer names change:
```text
source_sha256 → canonical_sha256 → tile_stream_sha256 → ir_sha256
→ pack_manifest_sha256 → projection_sha256
```
The **merge key for a vision delta is `(canonical_sha256, ir_sha256, projection_sha256)`** — the same triple `AudioCompilationUnit.merge_key` exposes, so the existing merge kernel and `core-rs` content-addressed sort apply unchanged. Consequences carry over verbatim: idempotence is structural (identical canonical pixels under an identical pack → identical key → CRDT join deduplicates), the content-addressed sort is free, and `projection_sha256` is computed behind the serialization barrier on the serialized in-chunk composition (ADR-0180 §1.5.3 point 3).
### 2.3 Physical sharding mirrors the visual domain's natural concurrency
ADR-0180 §2.1 assigns each active adapter a thread-local arena and forbids `sensorium/adapters/*` from writing global `epistemic_state` directly. For vision this is semantically aligned, not merely mechanical: **independent spatial regions, scale levels, and successive video frames are genuinely concurrent visual streams.** Each gets its own arena; each `VisualEvent` retains its tile/scale coordinates so the merge reconstructs spatial layout without a global lock during ingestion. The substrate's physical sharding is a faithful image of the visual source's structure — a tiled image is *already* a set of independent deltas.
### 2.4 Eventual-consistency window is safe for vision
Same argument as ADR-0181 §2.4. `ProjectionHead.project` is pure on the signal (ADR-0180 T-4): the vision compiler reads no cross-modal or global state during compilation, so a delayed merge cannot change what it produces. Each unit retains its own spatial coordinates, so cross-modal resonance re-anchors on merged state after the sub-50ms window closes; recall remains exact byte-for-byte once merged, never approximate.
### 2.5 Gate-closed by default
`vision_core_v1` mounts with `gate_engaged = false` until the eval gates in the companion plan pass. A closed gate makes `ModalityRegistry.project("vision_core_v1", …)` raise — vision contributes no deltas to any arena until determinism, checksum, unitarity, and mount-validation gates are green. This reuses existing registry enforcement; no new gating machinery is added.
### 2.6 OPEN DESIGN QUESTION — blade semantics for vision (red-line target)
The `(32,)` output shape and the unitarity check are **rigid contract** and not open. What is open — and what must be settled in the companion spec before PR-2 — is the **geometric meaning of the blades for a visual signal**. Audio used "elliptic bivector rotors only in v1" over an acoustic event vocabulary; vision cannot inherit that assignment because the underlying structure is spatial.
Cl(4,1) gives us (per `algebra/cl41.py`): grade-0 (scalar, idx 0), grade-1 (5 vectors, idx 15), grade-2 (10 bivectors, idx 615), grade-3 (10 trivectors, idx 1625), grade-4 (5, idx 2630), grade-5 (pseudoscalar, idx 31).
Candidate v1 assignment **(to be red-lined, not yet decided):**
- **grade-1 vectors** → canonical spatial/scale frame (2 image axes + scale axis + 2 CGA null directions for position encoding, reusing the CGA machinery in `algebra/cga.py`).
- **grade-2 bivectors** → oriented local structure: orientation energy and spatial-frequency bands lowered as elliptic rotors (the closest direct analog to audio's prosody arcs).
- **grade-3 / grade-4** → compositional/region relationships (contour closure, containment, figureground).
- **scalar / pseudoscalar** → global luminance/saliency magnitude and chirality.
Open sub-questions for review:
1. Is `S` a whole canonical frame, a single tile, or a region crop? (Affects whether one image is one unit or many.) Proposed: **one tile at one scale = one chunk = one unit**; whole-image versor is their merged contribution.
2. Should position be carried in CGA null vectors (conformal embedding) or quantized into the operator registry's theta rules, as audio quantizes pitch?
3. Are elliptic rotors sufficient for v1, or does figureground require a hyperbolic/parabolic generator? (Default: elliptic-only in v1, matching ADR-0181.)
4. What is the resolution-independence guarantee for `morton_code` ordering across canonical grid sizes within one pack version?
## 3. Consequences
### 3.1 Positive
- **Second concrete exerciser of ADR-0180**, and the first *spatial* one — it stresses the substrate's order-invariance proof against a non-temporal canonical order, which audio never exercised.
- **Order-invariance is proven, not hoped.** §2.1/§2.2 supply a concrete serialization barrier and the same content-addressed key, closing ADR-0180 §1.5.2 for the vision path.
- **No core mutation.** Everything new lives under `sensorium/vision/`, `packs/vision/`, `tests/`, `evals/`. `ingest/`, `field/`, `generate/`, `vault/`, `vocab/` are untouched (ADR-0013).
- **Substrate reuse is total.** The merge-key triple is byte-identical in shape to audio's, so `core-rs` merge/dedup, trace hygiene, and the Python arena mirror all apply without substrate changes.
- **Trace hygiene composes.** Turn traces record `(canonical_sha256, ir_sha256, projection_sha256)` and pack IDs — never raw pixels.
### 3.2 Negative / Risks
- **Embedding-bridge temptation (primary risk).** Pretrained vision encoders are so strong that reviewers will be tempted to admit one as substrate. The doctrine line must hold: substrate is the deterministic compiler; learned models are teacher/shadow lanes only.
- **Semantic underreach (v1).** The compiler captures layout, structure, orientation, and salient regions better than fine-grained object identity. Acceptable: caption/detector teachers backfill identity while the substrate stays native (mirrors audio's transcript-teacher posture).
- **Canonicalization brittleness.** Colorspace, gamma, and resampling kernel must be pinned; an unpinned resize is the vision analog of audio's unpinned FIR and will break A-1/V-1 determinism. Frozen in the pack manifest.
- **Spatial quantization regime.** Tile size, scale-pyramid depth, and orientation bins frozen in the manifest; `basis_version` is part of the merge key's pack-manifest leg, so projections stay comparable across versions or are explicitly incomparable.
- **Streaming seam artifacts (video).** Cross-frame continuity, optical-flow state, and temporal dedup are deferred to a streaming phase; **v1 is single-frame / whole-image, offline.**
- **Licensing contamination.** Any GPL/non-commercial reference detector is an oracle only, never a runtime dependency.
## 4. Execution Plan & Proof Obligations
### 4.1 PR stack (additive, doctrine-first)
| PR | Scope | Gate |
|---|---|---|
| **PR-1 (this)** | ADR-0197 + vision-compiler-spec + eval plan (docs only) | review + blade-semantics red-line (§2.6) resolved |
| **PR-2** | Deterministic substrate: `sensorium/vision/{types,canonical,checksum,resample,grid,lexer,parser,operators,compiler,trace}.py` | determinism + versor unit tests |
| **PR-3** | Pack artifacts `packs/vision/vision_core_v1/*` + `VisionProjectionHead` adapter + mount tests | mount/gate/checksum gates |
| **PR-4** | `evals/vision_sensorium/` fixtures, expected IR, expected projection hashes | full eval-gate table |
| **PR-5** | Delta-CRDT wiring: `VisionCompilationUnit` → thread-local arena → merge key, behind ADR-0180's substrate | sequential==concurrent trace-hash proof |
| **PR-6** | Teacher/shadow lanes (ViT/CLIP/DINOv2/SAM/depth) behind optional extras | teachers admitted only as typed hints |
PR-5 must not start until ADR-0180's §1.5.4 obligations (T-1…T-4) are green on `main`. PR-2 must not start until §2.6 is resolved.
### 4.2 Vision-specific proof obligations (extend ADR-0180 §1.5.4; mirror ADR-0181 §4.2)
Each must **fail loudly** under the violation it names:
- **V-1 (determinism).** Same canonical pixels + same pack ⇒ byte-identical `(32,)`, across repeated calls, threads, and processes. Fails on any non-determinism (dict ordering, unpinned resize/colorspace, float reduction order).
- **V-2 (set-equality of merges).** A set of `VisionCompilationUnit`s folds to the same Vault state regardless of arena flush order (permutation invariance). Fails if a delta's contribution is order-sensitive at the merge layer.
- **V-3 (content-addressed key).** Trace-hash over vision deltas is invariant under set-equal Vault states when keyed by `(canonical_sha256, ir_sha256, projection_sha256)`. Fails if the reduction consumes deltas in arrival order.
- **V-4 (serialization barrier + canonical spatial order).** In-chunk `compile_events` is asserted order-sensitive (negative test): swapping two events in canonical spatial order changes the versor. **Plus the vision-specific clause:** the canonical spatial order (§2.1) is itself deterministic and stable under the pack's fixed grid — re-tiling the same canonical image yields the same event order.
- **V-5 (versor condition).** Every emitted unit satisfies `versor_condition(v) < 1e-6`; threshold never weakened to pass.
- **V-6 (trace hygiene).** No raw pixel bytes appear in any `TurnEvent`/Vault record; only the three hashes + pack IDs + optional teacher provenance.
### 4.3 The strict compilation invariant
```text
same canonical image bytes
+ same compiler version
+ same pack manifest (incl. basis_version, tile/scale/orientation regime)
+ same operator registry
+ same canonical spatial ordering rule
= same VisionIR
= same versor
= same projection hash
= same CRDT merge key
= identical post-merge Vault contribution (idempotent under re-ingest)
```
## 5. Alternatives Considered
- **Embedding-first projector (ViT / CLIP / DINOv2 as substrate).** Fast and semantically rich, but opaque; cannot be replayed bit-for-bit and cannot supply ADR-0180 §1.5.3's content-addressed key. Rejected as substrate; retained as teacher/shadow.
- **Segmentation-first (SAM as substrate).** Produces good region proposals but is non-deterministic across versions and gives no checksummable key. Rejected as substrate; useful as a teacher for region anchors.
- **Patch-token codec (VQ-GAN / image tokenizer).** Strategically interesting for a future *generation/output* lane (and a likely touch-point with the eventual MOTOR decoder work); poor first substrate for an epistemically explicit engine. Deferred to a shadow/output lane, mirroring ADR-0181's EnCodec deferral.
- **Vision as a downstream cognition mutation.** Violates ADR-0013's no-core-mutation rule and ADR-0180's "adapters never write global state directly" rule. Rejected.
## 6. Cross-References
- ADR-0013 — projection boundary; no-core-mutation constraint; Logos-recovery framing.
- ADR-0180 — Delta-CRDT substrate; §1.5.2 order-invariance (closed here for vision via §2.1), §1.5.3 content-addressed merge key, §1.5.4 T-1…T-4 (vision analogs in §4.2), §1.5.5 trace hygiene.
- ADR-0181 — Audio compiler; the structural precedent this ADR mirrors. Divergences are confined to canonical ordering (§2.1) and blade semantics (§2.6).
- ADR-0054 — Vault recall indexing/batching; the read-side contract merged vision deltas must preserve (exact CGA recall).
- CLAUDE.md §Normalization Rules — quantization/resampling confined to pack/compiler construction; `unitize_versor` algebra-owned; no hot-path repair.
- `sensorium/protocol.py`, `sensorium/registry.py` — the `ProjectionHead` / `ModalityPack` / `ModalityRegistry` contracts this ADR implements a vision instance of.
---
### Open questions for red-line (consolidated)
1. **§2.6 blade assignment** — confirm or revise the grade→meaning mapping. *Blocks PR-2.*
2. **§2.1 canonical spatial order** — accept `(scale_level, morton_code, precedence, stable_id)` or propose an alternative deterministic total order. *Blocks V-4.*
3. **Unit granularity** — is one tile-at-one-scale the chunk (proposed), or the whole frame?
4. **Position encoding** — CGA null vectors vs. quantized operator theta rules.
5. **Companion specs** — confirm `vision-compiler-spec.md` + `vision-compiler-eval-plan.md` as the PR-1 deliverables alongside this ADR.

View file

@ -0,0 +1,137 @@
# Vision Compiler Eval Plan — `vision_core_v1`
**Companion to:** [ADR-0197](../decisions/ADR-0197-vision-compiler-delta-crdt.md),
[vision-compiler-spec.md](./vision-compiler-spec.md)
**Status:** Proposed (PR-1 docs)
This plan defines the seeding corpus, the acceptance gates that lift `vision_core_v1` from gate-closed to gate-engaged, the Delta-CRDT proof obligations, and the teacher-migration policy. It is the last PR-1 companion ADR-0197 names; it is what PR-2's "determinism + versor tests" gate is measured against.
---
## 1. Seeding corpus — visual atoms, not captions
Small, curated, checksum-locked. Four tiers:
- **Tier A — structural atoms:** flat field, oriented edge (each orientation bin), corner/junction, blob/region onset, open vs. closed contour, low- vs. high-spatial-frequency band.
- **Tier B — region & figureground:** salient figure on ground, background texture, occlusion boundary, symmetry, periodic repetition, scale-change across pyramid levels.
- **Tier C — color/material/lighting:** hue/saturation regimes, high vs. low luminance contrast, shadow/highlight, specular vs. matte, gradient ramp vs. hard step.
- **Tier D — alignment anchors:** caption hypothesis, detector box, segment mask, OCR text span, linked text surface (only when alignment is trustworthy).
Tier D is auxiliary; object/lexical semantics is a later enrichment step. Synthetic fixtures (oriented sinusoidal gratings, checkerboards, luminance gradients, single-dot impulses, step edges, solid color fields, controlled occlusions) drive first-pass determinism; checksum-locked real fixtures cover what synthesis does poorly (natural texture, soft occlusion, cluttered figureground).
## 2. Acceptance gates (gate-engaged criteria)
| Gate | Pass criterion | Obligation |
|---|---|---|
| Projection shape | exactly `(32,)` | V-1 |
| Projection dtype | exactly `float32` | V-1 |
| Compiler replay | bit-identical on same platform/build | V-1 |
| Cross-platform stability | equal after quantization, within declared numeric tolerance | V-1 |
| Canonical-ordering stability | re-tiling the same canonical image yields the same event order | V-4 |
| `versor_condition` | `< 1e-6` (never weakened) | V-5 |
| Canonical checksum stability | 100% on fixture corpus (colorspace + resize pinned) | V-1 |
| Gate closure | projection blocked when `gate_engaged = false` | — |
| Mount validation | bad checksum or bad unitarity blocks pack mount | — |
| Trace hygiene | no raw pixel bytes in any turn trace | V-6 |
| IR replay | `VisionIR -> versor` replays identically from stored IR | V-1 |
These mirror the existing modality-test posture (`sensorium/registry.py` mount/gate enforcement, `ModalityPack.__post_init__` invariants) — no new gating machinery.
## 3. Delta-CRDT proof obligations (ADR-0197 §4.2)
Each must be able to **fail loudly** under the violation it names (CLAUDE.md §Schema-Defined Proof Obligations — no decorative tests):
| ID | Obligation | Fails if… | ADR-0180 analog |
|---|---|---|---|
| **V-1** | Determinism: same canonical pixels + pack ⇒ byte-identical `(32,)` across calls/threads/processes | dict ordering, unpinned resize/colorspace, or float reduction order leaks in | T-4 |
| **V-2** | Set-equality of merges: a set of tile units folds to the same Vault state for any arena flush permutation | a delta's contribution is order-sensitive at the merge layer | T-1 |
| **V-3** | Content-addressed trace-hash: invariant under set-equal Vault states when keyed by `(canonical, ir, projection)` sha | the reduction consumes deltas in arrival order | T-2 |
| **V-4** | Serialization barrier **+ canonical spatial order**: in-chunk `compile_events` is order-sensitive (negative test), and the §6 spatial order is itself deterministic and re-tiling-stable | swapping two spatial-order events fails to change the versor, **or** re-tiling the same image reorders events | T-3 |
| **V-5** | `versor_condition < 1e-6` on every emitted unit | the threshold is weakened to pass | — |
| **V-6** | Trace hygiene: no pixels in any `TurnEvent`/Vault record | raw pixel data leaks into a delta's provenance | §1.5.5 |
V-4 carries one clause audio's A-4 did not: vision has no temporal axis, so the canonical *spatial* order (vision-compiler-spec §6) is the thing that makes the fold reproducible, and it must be tested as such.
### 3.1 The sequential==concurrent proof (PR-5 acceptance)
The load-bearing test for the CRDT mapping:
```text
ingest tiles [t1, t2, t3] sequentially → vault_seq, trace_hash_seq
ingest the same tiles across N arenas merged → vault_conc, trace_hash_conc
assert set(vault_seq) == set(vault_conc) # V-2
assert trace_hash_seq == trace_hash_conc # V-3
assert re-ingesting any tile is a no-op on the vault # idempotence (ADR-0197 §4.3)
```
This is the vision instance of ADR-0180 §4.3's `hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)` and must pass before PR-5 merges. Because tiles are spatially independent, this proof is the first *non-temporal* exerciser of the substrate's order-invariance (ADR-0197 §3.1).
### 3.2 Pytest skeleton
```python
def test_vision_projection_is_deterministic(vision_fixture, vision_pack): # V-1
v1 = vision_pack.projection.project(vision_fixture)
v2 = vision_pack.projection.project(vision_fixture)
assert v1.shape == (32,)
assert v1.dtype.name == "float32"
assert np.array_equal(v1, v2)
def test_vision_pack_gate_blocks_projection(vision_pack_closed, vision_fixture): # gate closure
with pytest.raises(Exception):
_ = vision_pack_closed.projection.project(vision_fixture)
def test_vision_ir_replay_matches_original(vision_fixture, compiler): # IR replay
unit = compiler.compile_image(vision_fixture)
replay = compiler.compile_ir(unit.vision_ir)
assert np.array_equal(unit.versor, replay.versor)
assert unit.ir_sha256 == replay.ir_sha256
def test_chunk_composition_is_order_sensitive(two_events, compiler): # V-4 barrier
a = compiler.compile_events([two_events[0], two_events[1]])
b = compiler.compile_events([two_events[1], two_events[0]])
assert not np.array_equal(a, b)
def test_canonical_spatial_order_is_retiling_stable(vision_fixture, compiler): # V-4 spatial clause
order_a = compiler.canonical_event_order(vision_fixture)
order_b = compiler.canonical_event_order(compiler.recanonicalize(vision_fixture))
assert [e.stable_event_id for e in order_a] == [e.stable_event_id for e in order_b]
def test_merge_is_permutation_invariant(units): # V-2
import itertools
states = {fold_into_vault(p) for p in itertools.islice(_perms(units), 8)}
assert len(states) == 1
```
## 4. Teacher / shadow lane policy (PR-6)
Teachers label or align; they **never** define the substrate and **never** fold embeddings into the main versor path. They are admitted only through typed, versioned, checksummed hints stored as `content_anchors` / `evidence_ids` in the IR.
| Source | Best role in CORE | Why not the substrate |
|---|---|---|
| **CLIP** | coarse scene/object labels, image-text alignment prototypes | embedding model; opaque latent violates the design goal |
| **DINOv2** | self-supervised region/correspondence hints | dense embedding; not a checksummable typed visual IR |
| **ViT (supervised)** | classification evidence, weak object labels | latent features, no content-addressed key |
| **SAM** | region/segment proposals as `region` anchors | non-deterministic across versions; gives no checksummable key |
| **Depth Anything / RAFT** | depth/flow evidence lanes (and the seam for future video) | estimators, not a CORE-native deterministic compiler |
| **Tesseract / OCR** | text-span anchors when glyphs are present | text extraction, wrong primary ontology for vision |
Migration policy, verbatim (identical to audio):
```text
Use teachers to label or align.
Never let teachers define the substrate.
Never fold teacher embeddings directly into the main versor path.
Only admit teacher outputs through typed, versioned, checksumed hints.
```
This is the line ADR-0197 §3.2 names as the primary risk: pretrained vision encoders are strong enough to tempt substrate use. They stay in this table, never on the versor path.
## 5. Phased sequence (priority order, no calendar)
1. **Doctrine** — ADR + spec + eval plan locked (PR-1, this).
2. **Deterministic substrate** — canonicalizer, checksums, resample, grid, lexer, parser, operators, compiler (PR-2).
3. **Governance** — pack artifacts, adapter, mount/gate/checksum tests (PR-3).
4. **Evaluation** — fixtures, expected IR, expected projection hashes, gate table (PR-4).
5. **Delta-CRDT wiring** — arena + merge key + sequential==concurrent proof (PR-5), gated on ADR-0180 §1.5.4 (T-1…T-4) green on `main`.
6. **Auxiliary lanes** — CLIP/DINOv2/SAM/depth/OCR teachers behind optional extras (PR-6).
7. **Streaming** — stateful incremental compiler preserving continuity across frames/seams (later; v1 is single-frame/whole-image, offline).

View file

@ -0,0 +1,318 @@
# Vision Compiler Spec — `vision_core_v1`
**Companion to:** [ADR-0197](../decisions/ADR-0197-vision-compiler-delta-crdt.md)
**Status:** Proposed (PR-1 docs)
**Scope:** the deterministic substrate (PR-2/PR-3) and its Delta-CRDT delta interface (PR-5).
This spec fixes the typed IR, the operator/manifest format, the numeric determinism rules, and the `VisionCompilationUnit` → Delta-CRDT delta contract. It is implementation-facing; the *why* lives in ADR-0197, the *acceptance* lives in the eval plan. It also **resolves the two ADR-0197 red-line blockers** — §2.1 canonical spatial ordering and §2.6 blade semantics — see §6 and §7 below.
---
## 0. Resolutions of the ADR-0197 open questions
| ADR-0197 open Q | Resolution in this spec |
|---|---|
| **#1 unit granularity** | **One tile at one scale level = one chunk = one `VisionCompilationUnit`.** The whole-image versor is the *merged* contribution of its tile units, never a separate object. |
| **#2 position encoding** | **v1: position is carried in IR tile/scale coordinates and modulates rotor `theta`; layout is preserved by the canonical spatial order (§6).** CGA conformal *translators* (parabolic, `n_inf` generator) are deferred to v2. Rationale: keeps v1 elliptic-only and audio-parallel, and avoids unproven parabolic numerics on the hot path. |
| **#3 elliptic sufficiency** | **Elliptic bivector rotors only in v1** (square = 1), matching ADR-0181. Figureground is a grade-2 saliency rotor (`B_SALIENCE`), not a boost. |
| **#4 morton resolution-independence** | **Resolved by construction:** tiling happens *after* canonicalization to the pack's fixed grid (§3), so `morton_code` runs over fixed normalized tile indices and is identical regardless of source resolution. |
The CGA machinery in `algebra/cga.py` (`embed_point`, `cga_inner`) remains the **recall-side** distance metric for merged vision deltas in the Vault (ADR-0054) — exactly as for audio. This spec governs only how the **compile-side** versor is *built* (elliptic-rotor composition); it does not change how versors are *compared* at recall.
## 1. Two-clock architecture
A low-level **spatial clock** measures pixel facts; a higher-level **visual-grammar clock** emits typed events. The primary path is fully deterministic; learned systems are confined to auxiliary evidence lanes (PR-6).
```mermaid
flowchart LR
A[Image bytes / single video frame] --> B[Canonicalizer<br/>fixed colorspace + gamma + grid + checksums]
B --> C[Spatial grid<br/>tile lattice × scale pyramid]
C --> D[Visual lexer<br/>orientation energy, spatial-freq bands,<br/>luma/chroma stats, corner/blob onset, region boundaries]
D --> E[Typed VisionIR parser<br/>regions/segments, contour arcs,<br/>salient-object events, texture atoms, anchors]
E --> F[Canonical spatial ordering<br/>scale → morton → precedence → stable_id]
F --> G[Operator registry<br/>pack manifest + blade aliases + theta rules]
G --> H[Rotor lowering]
H --> I[Versor composition<br/>unitize_versor + versor_condition]
I --> J["(32,) float32 — one VisionCompilationUnit"]
E --> K[Vision evidence trace<br/>hashes, teacher provenance, pack IDs]
J --> L[Thread-local arena<br/>ADR-0180 §2.1]
L --> M[Semilattice merge<br/>keyed by content-addressed sha]
```
## 2. Typed VisionIR
The IR is built from **typed regions and events**, never from raw pixels or feature maps. Detector/caption hypotheses may exist only as auxiliary content anchors, never as the sole meaning of the image.
```python
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import numpy as np
@dataclass(frozen=True, slots=True)
class VisionImage:
pixels: np.ndarray # canonical linear-light float32, shape (H, W, C)
grid_h: int # canonical tile rows
grid_w: int # canonical tile cols
scale_levels: int
source_sha256: str
canonical_sha256: str
@dataclass(frozen=True, slots=True)
class TileCoord:
scale_level: int # 0 = finest
tile_row: int
tile_col: int
@property
def morton(self) -> int: # Z-order interleave of (tile_row, tile_col)
r, c, code, bit = self.tile_row, self.tile_col, 0, 0
while (r >> bit) or (c >> bit):
code |= ((r >> bit) & 1) << (2 * bit)
code |= ((c >> bit) & 1) << (2 * bit + 1)
bit += 1
return code
@dataclass(frozen=True, slots=True)
class VisualToken:
kind: Literal[
"flat", "edge", "corner", "blob", "texture",
"orient_bin", "freq_bin", "chroma_bin",
]
coord: TileCoord
value_q: tuple[int, ...] # canonical quantized payload
@dataclass(frozen=True, slots=True)
class VisualEvent:
event_type: str
coord: TileCoord
attrs: tuple[tuple[str, int | str], ...] # quantized ints / short strings
evidence_ids: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class VisionIR:
regions: tuple[VisualEvent, ...]
contour_arcs: tuple[VisualEvent, ...]
orient_events: tuple[VisualEvent, ...]
texture_atoms: tuple[VisualEvent, ...]
salient_events: tuple[VisualEvent, ...]
content_anchors: tuple[VisualEvent, ...]
ir_sha256: str
```
### 2.1 The compilation unit (the CRDT delta)
```python
@dataclass(frozen=True, slots=True)
class VisionCompilationUnit:
canonical_sha256: str
ir_sha256: str
pack_id: str
pack_manifest_sha256: str
projection_sha256: str
coord: TileCoord # tile/scale this unit covers
versor: np.ndarray # (32,) float32
versor_condition: float
@property
def merge_key(self) -> tuple[str, str, str]:
# ADR-0197 §2.2 — same triple as AudioCompilationUnit.
return (self.canonical_sha256, self.ir_sha256, self.projection_sha256)
```
`VisionCompilationUnit` is the single object the vision adapter writes into its thread-local arena (ADR-0180 §2.1). It carries no pixels (ADR-0197 §3.1 / ADR-0180 §1.5.5). `coord` is retained so the merge can reconstruct spatial layout without a global lock (ADR-0197 §2.3).
## 3. Canonical signal formation
- Internal representation: **linear-light float**, fixed colorspace (sRGB primaries, gamma linearized to a pinned LUT), alpha dropped, fixed canonical resolution grid. Original-source bytes preserved separately for provenance.
- Resampling: **pinned separable kernel** (fixed Lanczos-3 coefficients), generated **once**, stored as a pack artifact (`resample_kernel_v1.npy`) and checksummed in the manifest. The runtime never relies on library defaults — this is the vision analog of audio's pinned FIR (ADR-0197 §3.2).
- Tiling happens **after** canonical resize, so the tile lattice and `morton` order are resolution-independent (resolves ADR-0197 #4).
## 4. Visual lexer
Operates on **measured facts**, not semantic guesses. Default tile 16×16 px at the finest scale; a fixed 3-level Gaussian/Laplacian pyramid. Each tile yields quantized descriptors: dominant gradient-orientation histogram bin, oriented-energy bin per spatial-frequency band (low/high), local luminance-contrast bin, quantized hue/saturation regime, corner/blob response bins, texture periodicity/entropy bin, flat-region flag.
## 5. Parser → typed events
Promotes lexer output into typed regions/events. Preserves the distinctions a downstream reader needs: a hard oriented edge vs. a soft luminance ramp, a closed contour vs. a dangling one, a salient figure vs. background texture. "Unstructured texture" is the fallback only when a more specific parse is impossible — the visual analog of audio's "chaotic noise" fallback.
## 6. Canonical spatial ordering (resolves ADR-0197 §2.1)
The in-chunk fold is a **serialization barrier** (ADR-0197 §2.1); it requires a deterministic total order over the chunk's `VisualEvent`s. The order is:
```text
key(event) = (coord.scale_level,
coord.morton,
event_precedence[category(event.event_type)],
stable_event_id)
```
- `coord.morton` is the Z-order interleave in `TileCoord.morton` — a space-filling curve that keeps spatially-near events adjacent in the fold.
- `event_precedence` is a fixed list in the manifest (§6.1 `[ordering]`).
- `stable_event_id` is the content hash of the event's quantized attrs (final tiebreak; never wall-clock).
**This order is the thing V-4 asserts against** (ADR-0197 §4.2): re-tiling the same canonical image yields the same order, and swapping two events changes the versor.
## 7. Operator registry (pack-local blade aliases)
Because the `(32,)` boundary is fixed but no canonical *semantic* blade map is exposed, v1 uses **pack-local, versioned, checksummed blade aliases**, identical in discipline to ADR-0181 §6. v1 uses **elliptic bivector operators only** (square = 1), so every rotor is `R = cos(θ/2) + B·sin(θ/2)`. Parabolic (CGA translator) and hyperbolic operators are deferred to v2.
Position does **not** get its own geometric generator in v1 (resolves ADR-0197 #2): tile/scale coordinates modulate `theta` and order the fold; they are not embedded as a CGA point on the compile path.
| Visual atom family | Measured source | Alias | Default blade index | Theta rule |
|---|---|---|---|---|
| Oriented edge energy | gradient-orientation histogram | `B_ORIENT` | 6 | `q(base + g1·orient_q + g2·scale_level)` |
| Low spatial-frequency band | low bandpass energy | `B_FREQ_LOW` | 7 | `q(base + g3·energy_q)` |
| High spatial-frequency band | high bandpass energy | `B_FREQ_HIGH` | 8 | `q(base + g4·energy_q)` |
| Corner / junction | corner response bin | `B_CORNER` | 9 | `q(base + g5·corner_q)` |
| Blob / region onset | blob detector bin | `B_BLOB` | 10 | `q(base + g6·blob_q)` |
| Contour closure | boundary continuity | `B_CONTOUR` | 11 | `q(base + g7·closure_q)` |
| Luminance contrast | local contrast bin | `B_CONTRAST` | 12 | `q(base + g8·contrast_q)` |
| Chroma / color regime | quantized hue/sat bin | `B_CHROMA` | 13 | `q(base + g9·hue_q + g10·sat_q)` |
| Texture regularity | periodicity / entropy bin | `B_TEXTURE` | 14 | `q(base + g11·texture_q)` |
| Saliency / figureground | center-surround salience | `B_SALIENCE` | 15 | `q(base + g12·salience_q)` |
Indices are **reasonable defaults, not metaphysical claims** about Cl(4,1) (verbatim the ADR-0181 §6 stance). The contract is that the mapping is explicit, versioned, checksummed, and frozen in the manifest. `B_SALIENCE` is the figureground atom that ADR-0197 §2.6 declined to model with a boost.
### 7.1 Minimal manifest (`packs/vision/vision_core_v1/manifest.toml`)
```toml
pack_id = "vision_core_v1"
modality = "vision"
cl41_dim = 32
compiler_version = "0.1.0"
basis_version = "vision-basis-v1"
[canonical]
colorspace = "srgb_linear"
gamma_lut = "gamma_lut_v1.npy"
tile_px = 16
scale_levels = 3
output_dtype = "float32"
internal_dtype = "float64"
[resampling]
algorithm = "separable_lanczos3"
kernel_path = "resample_kernel_v1.npy"
kernel_sha256 = "sha256:REPLACE_ME"
[gating]
gate_engaged = false
checksum_verified = false
versor_condition_max = 1.0e-6
[ordering]
event_precedence = ["region", "contour", "orient", "texture", "salient", "content_anchor"]
```
### 7.2 Operator row (`operators.jsonl`)
```json
{
"operator_id": "vision.orient.edge_energy.v1",
"event_type": "orient.edge_energy",
"blade_alias": "B_ORIENT",
"blade_index": 6,
"rotor_kind": "elliptic",
"base_theta_q": 48,
"gain_rules": {"orient_q": 3, "scale_level": 2, "confidence_q": 1},
"theta_clip_q": 384,
"version": "1"
}
```
## 8. Numeric determinism
Rule (verbatim from ADR-0181 §7): **quantize before semantics, normalize after composition.** Quantization regime (frozen in manifest): orientation in 16 ordinal bins, oriented energy in log bins, contrast in dB-like bins, hue/sat in fixed ordinal bins, all confidences in uint8. After quantization, compute in float64, compose sparse rotors in canonical spatial order, call algebra-owned `unitize_versor`, cast to float32 **only** at the output boundary.
```python
import math
import numpy as np
def quantize_theta(theta: float, step: float = 1.0 / 1024.0) -> float:
return round(theta / step) * step
def build_elliptic_rotor(blade_index: int, theta: float) -> np.ndarray:
out = np.zeros(32, dtype=np.float64)
half = quantize_theta(theta) / 2.0
out[0] = math.cos(half)
out[blade_index] = math.sin(half)
return out
def compile_events(events, registry, geometric_product, unitize_versor, versor_condition):
# SERIALIZATION BARRIER (ADR-0197 §2.1): in-chunk composition is order-sensitive,
# single-threaded, in CANONICAL SPATIAL ORDER (§6). The substrate never parallelizes this.
v = np.zeros(32, dtype=np.float64)
v[0] = 1.0
for ev in events: # MUST already be in §6 canonical spatial order
spec = registry[ev.event_type]
theta = spec.theta_from_event(ev) # deterministic, quantized inputs only
r = build_elliptic_rotor(spec.blade_index, theta)
v = geometric_product(v, r)
v = unitize_versor(v)
if versor_condition(v) >= 1e-6:
raise ValueError("vision compilation failed versor check")
return v.astype(np.float32)
```
`geometric_product`, `unitize_versor`, `versor_condition` are imported from `algebra/`; the vision compiler adds **no** new normalization function. `embed_point`/`cga_inner` are used only on the Vault recall side, never here.
## 9. Repo-facing adapter (`sensorium/adapters/vision.py`)
```python
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True, slots=True)
class VisionProjectionHead:
compiler: "VisionCompiler"
modality = ... # Modality.VISION
@property
def embedding_dim(self) -> int:
return 32
def project(self, image: "VisionImage") -> np.ndarray:
# One image projects to its merged tile-unit versor (granularity resolution #1):
out = self.compiler.compile_image(image).versor
if out.shape != (32,):
raise ValueError(f"expected (32,), got {out.shape}")
if out.dtype != np.float32:
raise TypeError(f"expected float32, got {out.dtype}")
return out
def project_batch(self, images: list["VisionImage"]) -> np.ndarray:
return np.stack([self.project(im) for im in images], axis=0)
def verify_unitarity(self, image: "VisionImage") -> bool:
return self.compiler.compile_image(image).versor_condition < 1e-6
```
The adapter is thin and pack-governed; it satisfies the `ProjectionHead` protocol in `sensorium/protocol.py` and is mounted as `ModalityPack(modality_type=Modality.VISION, gate_engaged=False)` until the eval gates pass.
## 10. Delta-CRDT delta interface (PR-5)
The vision adapter **never** writes the global `epistemic_state` (ADR-0180 §2.1). Instead:
1. `compile_tile()` produces one `VisionCompilationUnit` per tile/scale (the §8 serialization barrier runs here).
2. Each unit is written lock-free into the adapter's **thread-local arena**. Independent regions/scales/frames each have their own arena (ADR-0197 §2.3).
3. The **Merge Kernel** (ADR-0180 §2.2, explicitly mounted, not a daemon) folds pending units into the Vault ordered by `unit.merge_key`. Duplicate keys deduplicate (idempotence).
4. The kernel surfaces its pending-delta count in `TurnEvent` for replay evidence (ADR-0180 §1.5.5).
The per-tile Vault contribution is `(versor, provenance)` where provenance = `{merge_key, pack_id, pack_manifest_sha256, coord}` — content-addressed, no pixels.
## 11. File plan (PR-2 … PR-6)
```text
sensorium/vision/{__init__,types,canonical,checksum,resample,grid,lexer,parser,operators,compiler,trace,fixtures,teachers}.py
sensorium/adapters/vision.py
packs/vision/vision_core_v1/{manifest.toml,basis_map.json,operators.jsonl,atoms.jsonl,prototypes.jsonl,resample_kernel_v1.npy,gamma_lut_v1.npy,checksums.json}
tests/test_vision_{image,resample,grid,lexer,parser,ordering,pack_manifest,sensorium_mount,trace,crdt_delta}.py
evals/vision_sensorium/{fixtures/*.png,manifest.json,expected_ir.jsonl,expected_projection_hashes.json}
```