Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ The bitmask **is** the selected / unselected partition:
Versions, roles, and projections are simply **different masks over the same
template**. There is no template per version — one compiled artifact, any subset.

## Wide classes — class-conditioned shape, not a locked width

The `u64` mask above is **one bucket** of 64 field positions. A wide class — an
Odoo `account.move` carries ~100+ fields — overflows one bucket, but **not the
pattern**: the mask widens with the class, and the `selected()` loop is
bucket-agnostic (it filters `FieldDesc[]` by `idx`; the bucket is `idx / 64`, the
bit `idx % 64`). So a ~109-field model is clean.

**Crucially, the width is not a locked constant — it is class-conditioned**
(operator veto 2026-06-29). The mask shape is **mapped from the class's inherited
format and selected by `classid`** (the filter): the cascade is one of the
per-class [`CascadeShape`](../../lance-graph/crates/lance-graph-contract/src/facet.rs)s
— **Rails → `6×2`, other frameworks → `4×3`, the canonical GUID → `3×4`** (all
`G·D = 12`, 8-bit tiers; the depth `D ∈ {2,3,4}` is the per-class knob, via
`CascadeShape::from_levels(d)`). Do **not** restate or lock a `[u64; 4]`
"quadruplet" — that was a misread of the `3×4` GUID shape; the real knob is the
inherited, classid-selected `D`.

The only fixed bound is the **god-object cardinality**: `< 256` (the byte
cardinality / the per-tier sibling rank) is maskable by one ClassView; `≥ 256`
is the SoC split signal — split into sub-ClassViews, never widen/lock a mask.
Pinned + tested in `ruff_spo_address::soc`: `FIELD_MASK_CAP = MAX_SIBLINGS_PER_TIER`
(one cap, not a second lock), the `Duplication` verdict collapses to
`≤ FIELD_MASK_CAP` distinct `field_type`s (a 109-field class is `Duplication`/
maskable, not a `Counterexample`). The matching
`lance_graph_contract::class_view::FieldMask` (today `u64` / `MAX_FIELDS = 64`)
is the *eventual* expansion — to the class-conditioned shape, not a locked width,
validated by the ruff test.

## Simple rules (operator 2026-06-29)

- **If it's a template, it's probably a ClassView.** A Redmine ERB partial, an
Odoo view, an Askama field partial — each is a render over a class's field set,
i.e. a `ClassView` + a mask. Don't reach for a per-template type; reach for a
mask over the generated `FieldDesc[]`.
- **Deduplicate routes.** N routes that are "the same record, different visible
fields" (a card, a full view, an RBAC view, a tenant projection) are **one**
templated ClassView render with N masks — not N handlers. Route proliferation
is usually an un-applied mask.
- **`< 256` is clean; `≥ 256` is the god-object signal.** A field/sibling set
under the byte cardinality is maskable by one ClassView (in whatever
class-conditioned shape its `classid` selects). At/over 256 the design (not the
storage) is the problem — split concerns into sub-ClassViews, the same SoC the
`ruff_spo_address::soc` lint flags. Never widen/lock a mask to dodge the split;
and never restate the shape — it is inherited and `classid`-selected.

## Why this is right (not just convenient)

1. **§1.5 alignment — render-mask = read-mask.** "One compiled reader subsumes
Expand Down Expand Up @@ -119,10 +165,13 @@ user-authored, per-tenant templates — never as the default.

## Summary

One generic Askama field partial + a generated `FieldDesc[]` table + a `u64`
mask = the whole dynamic ClassView field view: Redmine-shaped, JSON-free, no
conditionals in the template, type-checked structure, dynamic across
versions/roles/projections. **The mask carves; the loop renders.** Askama's
One generic Askama field partial + a generated `FieldDesc[]` table + a mask
whose width follows the class's **class-conditioned shape** (`6×2`/`4×3`/`3×4`,
selected by `classid` from the inherited format — never a locked width) = the
whole dynamic ClassView field view: Redmine-shaped, JSON-free, no conditionals
in the template, type-checked structure, dynamic across versions/roles/
projections, and wide-class-clean (a god object at `≥ 256` is a split signal, not
a wider/locked mask). **The mask carves; the loop renders.** Askama's
compile-time nature is not a cage — the mask is the runtime knob, and it is the
same selector the data layer already uses to prune columns.

Expand All @@ -135,3 +184,13 @@ same selector the data layer already uses to prune columns.
pattern slots into.
- `I-LEGACY-API-FEATURE-GATED` — why the mask bits must be generated, never
hand-numbered alongside a layout.
- `ruff_spo_address::soc` — `FIELD_MASK_CAP = MAX_SIBLINGS_PER_TIER` (the
byte-cardinality cap, one bound not a second lock) and the `≥ 256` god-object
SoC lint (where the "wide classes / split, don't widen, shape is inherited"
rule is tested).
- `lance_graph_contract::facet::CascadeShape` — the class-conditioned shape
(`6×2`/`4×3`/`3×4`, `from_levels(d)`) the mask width follows; selected by
`classid`, never locked.
- `lance_graph_contract::canonical_node::GUIDS_PER_NODE` (= 32) — the node-level
twin: clean/SoC over packed, Tetris concerns across the 32 GUID slots; the
field-level mask here is the same SoC doctrine one level down.
97 changes: 63 additions & 34 deletions docs/OGAR-TRANSPILE-SUBSTRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,38 +87,49 @@ facet, baked into the binary. Three properties:

### a. One ClassView *rotates* the facet layout — no "versions"

The 16-byte facet's tier payload is not locked to a single carving. A ClassView
can **always rotate** — read the SAME 12 cascade bytes under a different
grouping — to fit the class. The carvings (pinned as
`lance_graph_contract::facet::CascadeShape`, `CascadeShape::ROTATIONS`):
The 16-byte facet's tier payload is not locked to a single carving. The shape is
**class-conditioned** — *mapped from the class's inherited format and selected by
`classid`* (the filter), never restated or locked (operator veto 2026-06-29). A
ClassView can also **rotate** — read the SAME 12 cascade bytes under a different
grouping. The shapes (pinned as `lance_graph_contract::facet::CascadeShape`,
`CascadeShape::from_levels(d)`):

```
6× (1:2) ALIGNED default — 6 tiers, each a 1:2 hierarchy (group_of = i >> 1, a shift)
3× (1:2:3:4) ALIGNED default — 3 tier-pairs, each 1:2:3:4 (group_of = i >> 2, a shift)
4× (1:2:3) WORST CASE — straddles tier boundaries (group_of = i / 3, a DIVIDE)
6× (1:2) Rails — 6 tiers, each a 1:2 hierarchy (group_of = i >> 1, a shift)
4× (1:2:3) other frameworks — 4 tier-groups, each 1:2:3 (group_of = i / 3, a divide)
3× (1:2:3:4) canonical GUID — 3 tier-pairs, each 1:2:3:4 (group_of = i >> 2, a shift)
```

**Only the byte-aligned carvings are defaults.** `6×(1:2)` and `3×(1:2:3:4)`
keep `group_of` a pure shift (the canon's "tier-of-level is a shift, never a
branch"). **`4×(1:2:3)` is the worst case, not a co-equal carving** — it
straddles tier boundaries so `group_of` must DIVIDE (`CascadeShape::is_byte_aligned()`
is `false`, `shift()` is `None`). It is *prevented on the common path* and kept
only as the **rare rotation / escape hatch**: a ClassView may rotate to it
deliberately when a rare class (some Odoo models) needs to relieve
**classid-stacking entropy** — rotate the reading rather than mint another
classid. So there is **no need for hardcoded facet "versions" (V1/V2/V3)** — one
compiled ClassView subsumes the rotation set; the straddle stays legal only as a
deliberate, rare rotation. Hardcoding a format per version is the thing to
*delete*.

> **Carvings address the VIEW, never the functions.** A rotation re-reads the
> data layout; it does NOT reach behaviour. Functions are encoded by the
**The shape follows the class, not a global lock** (operator: "Rails might need
6x2x8bit, others 4x3x8bit"). The depth `D ∈ {2,3,4}` is a per-class constant the
`classid` resolves (`CascadeShape::from_levels`). `6×2`/`3×4` carve on tier
boundaries so `group_of` is a pure shift (the canon's "tier-of-level is a shift,
never a branch"); **`4×3` is legitimate for the frameworks that need it** — its
`group_of` divides (`is_byte_aligned()` is `false`), the per-class *cost* a class
opts into, **not a prohibition**. So there is **no need for hardcoded facet
"versions" (V1/V2/V3)** — one compiled ClassView reads whichever shape the
classid selects. Hardcoding a format per version (or locking a single shape) is
the thing to *delete*.

> **Carvings address the VIEW, never the functions.** A shape/rotation re-reads
> the data layout; it does NOT reach behaviour. Functions are encoded by the
> **classid acting as an additional switch** — `lance_graph_contract::facet::ClassArm`
> `{ View, Functions }`, the OGAR THINK/DO split (`OGAR-AST-CONTRACT.md`).
> Reaching a function = switch the classid to the `Functions` arm (the
> `ActionDef`/`KausalSpec` on the resolved Core node), *never* slice the
> tier-bytes. A straddling carve to "get to" a function is exactly the worst
> case the `4×(1:2:3)` example warns against.
> tier-bytes — that is the genuine mistake (a straddle used to "get to" a
> function), distinct from a class whose *data layout* legitimately needs `4×3`.

**Out of transpile scope — the `G2×48bit` encoding lane.** The byte-shaped
shapes above (`6×2`/`4×3`/`3×4`, 8-bit tiers) are the *transpile / ClassView
field-grouping* lane. A **separate** lane reads the same facet bytes at
`G2×48bit` granularity — the two 48-bit chains (`FacetCascade::hi_chain` /
`lo_chain`, cf. the CAM-PQ `6×256` path code) — for **helix** (location encoding;
q2 / helix) and **CAM-PQ** (centroid encoding; lance-graph / DeepNSM). These are
**not required by transpile** and must not be dragged into ClassView shape
selection (operator 2026-06-29). The DeepNSM **COCA** 4096-word English-vocabulary
CAM index codebook is a likely future `G2×48bit` consumer — it may *earn its
keep* there, but it is not a transpile dependency.

### b. Sub-range mapping + nested ClassViews stacked into constructors

Expand Down Expand Up @@ -172,16 +183,19 @@ case.)
> `lance_graph_contract::facet::CascadeShape` (`G6D2` / `G4D3` / `G3D4`,
> `G·D = CASCADE_UNITS = 12`) over `FacetCascade::tier_bytes()` — `index(g,l) =
> g·D + l`, `group_of`/`level_of` inverses, `cascade_byte`, per-group LCP
> `cascade_group_shared`. `CascadeShape::ALIGNED = [G3D4, G6D2]` are the
> shift-`group_of` **defaults** (`shift()` is `Some`); `CascadeShape::ROTATIONS`
> is the full rotation set a ClassView may rotate through. **`G4D3` is the worst
> case** — `is_byte_aligned()` is `false`, `shift()` is `None`, `group_of`
> divides — excluded from `ALIGNED`, kept in `ROTATIONS` only as the rare
> escape-hatch rotation (classid-stacking-entropy relief). **Functions are NOT a
> carving** — `facet::ClassArm { View, Functions }` is the classid's additional
> THINK/DO switch; carvings address `View` only. Zero-dep, `const fn`,
> probe-verified (lance-graph #621). One algebra for both the facet bytes and a
> 12-field class — the shared substrate the three language SDKs (§1.6) all read.
> `cascade_group_shared`. The shape is **class-conditioned**, selected by the
> `classid` from the inherited format via `CascadeShape::from_levels(d)` —
> `2 → G6D2` (Rails), `3 → G4D3` (other frameworks), `4 → G3D4` (the GUID
> default); `CascadeShape::ALIGNED = [G3D4, G6D2]` are the shift-`group_of` shapes
> and `ROTATIONS` the full set. **`G4D3` (`4×3`) is legitimate per-class, not a
> "worst case to prevent"** (operator veto 2026-06-29): its `group_of` divides
> (`is_byte_aligned()` is `false`) — a per-class *cost*, not a prohibition;
> `is_byte_aligned`/`shift`/`ALIGNED` distinguish the shift fast-path from the
> divide shape, never a reject gate. **Functions are NOT a carving** —
> `facet::ClassArm { View, Functions }` is the classid's additional THINK/DO
> switch; carvings address `View` only. Zero-dep, `const fn`, probe-verified
> (lance-graph #621). One algebra for both the facet bytes and a 12-field class —
> the shared substrate the three language SDKs (§1.6) all read.

---

Expand Down Expand Up @@ -310,6 +324,21 @@ A node is `key(128/GUID) + value`. Lance may compress the value arbitrarily
(columnar, dictionary, PQ); the key is never compressed and never needs the
value decoded to route. (Canon: "THE GUID IS THE KEY OF KEY-VALUE.")

**Two doctrines for the consumer model (operator 2026-06-29), neither a blocker:**

- **Clean ⇒ expansion is `classid`-inherited.** A clean class can grow its field
set / capacity; the `classid` selects the expanded shape — no global change.
Expansion is never a transpile blocker (cf. the class-conditioned `CascadeShape`
/ "don't lock the shape").
- **Bulk raw data → a *separate* table, not the SoA value.** The 480-byte value
is for structured/compressible columns; a raw payload that won't fit even
compressed (a ~3.2 Gbp genome; the FMA / BodyParts3D anatomy mesh at **4M
vertices / 6M triangles**) lives in its own Lance table, referenced by
`key`/`classid` — out-of-line, addressed not inlined, and still not a blocker
(the anatomy mesh **baked cleanly as a SoA release**). Transpile mints the
*structured* class; the bulk payload is a table reference, not a transpile
dependency.

---

## 3. The 85 / 15 split (the consumer model)
Expand Down
Loading