diff --git a/docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md b/docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md index 500d56a..0f1b08a 100644 --- a/docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md +++ b/docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md @@ -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 @@ -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. @@ -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. diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index 9d76673..e58e2fb 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -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 @@ -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. --- @@ -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)