diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e3d10b..b9a8b1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,37 +30,11 @@ jobs: run: cargo check --workspace --all-targets - name: cargo test --workspace run: cargo test --workspace - # The OFF half of the plug-and-play activation contract. + # (A crate-scoped `--no-default-features` step lived here for the + # `blocks` activation feature. Both are gone: there is one codebook, no + # feature adds to it, and the guards that replaced the OFF-half gate are + # always compiled — `cargo test --workspace` above reaches them.) # - # `--workspace` CANNOT test it: `ogar-ro` dev-deps `ogar-blockly`, so - # feature unification turns ogar-vocab's `blocks` feature ON for the - # whole workspace run. Crate-scoped, `ogar-blockly` is not in the graph, - # the feature is off, and the `cfg(not(feature = "blocks"))` tests in - # `capability_registry` become reachable — the ones asserting a default - # build activates NOTHING and a frontend classid does not resolve. - # - # Without this step the design is only ever tested in the triggered - # direction, which is the vacuous shape of a guard nobody watched stay - # silent. - # `--no-default-features` FORCES the off build, so this job still - # reaches the gate even if `blocks` were added to `[features] default` - # (codex P2 on #259: with a plain `cargo test -p ogar-vocab` the module - # would compile out, cargo would exit 0, and the regression the gate - # exists to catch would be invisible). - # - # Exit code alone is NOT evidence here — a filtered-out test suite exits - # 0 too. So assert the named tests actually RAN. `the_off_gate_cannot_be - # _switched_off` is the always-compiled companion that fails if any - # activating feature reaches the default set. - - name: cargo test -p ogar-vocab (OFF half — forced, and proven to run) - run: | - set -euo pipefail - out=$(cargo test -p ogar-vocab --no-default-features 2>&1 | tee /dev/stderr) - for t in default_build_carries_no_activated_rows \ - the_off_gate_cannot_be_switched_off; do - echo "$out" | grep -q "$t" \ - || { echo "::error::OFF-half gate '$t' did not run — it compiled out"; exit 1; } - done # Exercise the feature-gated surrealql AST walk (lifts DDL -> # Class via the surrealdb-parser fork). Crate-scoped because the # parser dep is heavy and not needed by other workspace members; diff --git a/Cargo.toml b/Cargo.toml index c3771a1..f89f357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ members = [ "crates/ogar-a2ui-frame", "crates/ogar-from-docv1", "crates/ogar-render-typst", - "crates/ogar-blockly", "crates/ogar-loco", "crates/ogar-ro", "crates/ogar-elk", diff --git a/crates/ogar-blockly/Cargo.toml b/crates/ogar-blockly/Cargo.toml deleted file mode 100644 index 3d503a6..0000000 --- a/crates/ogar-blockly/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "ogar-blockly" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -authors.workspace = true -rust-version.workspace = true -description = "Visual block-programming vocabulary — the 256-slot command/concept palette shared by Blockly and Scratch frontends (the Blocks domain, 0x17XX), over the vocabulary-agnostic call ABI in ogar-loco. One content classid; a function body is 360 palette bytes in one 512-byte node. Plug-and-play: concept ids are authoritative here, never in the shared codebook." - -[features] -default = [] -serde = ["dep:serde", "ogar-vocab/serde", "ogar-loco/serde"] - -[dependencies] -ogar-loco = { path = "../ogar-loco" } -# `features = ["blocks"]` IS the activation: any build graph containing this -# crate activates the Blocks codebook in ogar-vocab, and no other build does. -# Cargo presence, not runtime detection. -ogar-vocab = { path = "../ogar-vocab", features = ["blocks"] } -serde = { workspace = true, optional = true } diff --git a/crates/ogar-blockly/examples/density.rs b/crates/ogar-blockly/examples/density.rs deleted file mode 100644 index 1795f1c..0000000 --- a/crates/ogar-blockly/examples/density.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Layout + density accounting for a block function node. -//! -//! Prints the byte budget and the amortized cost per call at several lane -//! shapes and occupancies, so the density claims in `docs/DISCOVERY-MAP.md` -//! `D-BLOCKS-PALETTE` can be re-measured rather than trusted. -//! -//! ```sh -//! cargo run -p ogar-blockly --example density -//! ``` - -use ogar_blockly::{ - CLASSID_BYTES, CONTENT_SLOTS, FunctionBody, LaneShape, PAYLOAD_BYTES_PER_SLOT, SLOT_STRIDE, - VALUE_SLAB_LEN, -}; - -/// Bytes of a whole node: key(16) + reserved(16) + value(480). -const NODE_BYTES: usize = 512; - -fn main() { - println!("── node layout ──"); - println!(" node {NODE_BYTES} B = 32 × {SLOT_STRIDE} B slots"); - println!(" key 16 B (slot 0)"); - println!(" reserved 16 B (slot 1 — zeroed; retired edge-block NOT revived)"); - println!(" value slab {VALUE_SLAB_LEN} B (slots 2..31 = {CONTENT_SLOTS} lanes)"); - println!( - " classid overhead {} B ({CONTENT_SLOTS} × {CLASSID_BYTES}, interleaved)", - CONTENT_SLOTS * CLASSID_BYTES - ); - println!( - " call bytes {} B ({CONTENT_SLOTS} × {PAYLOAD_BYTES_PER_SLOT})", - CONTENT_SLOTS * PAYLOAD_BYTES_PER_SLOT - ); - - println!("\n── lane shapes: same 360 bytes, three carvings ──"); - for shape in LaneShape::ALL { - println!( - " {shape:?}: {} B/call ({} immediate{}) → {} calls/lane, {} calls/node", - shape.bytes_per_call(), - shape.values_per_call(), - if shape.values_per_call() == 1 { - "" - } else { - "s" - }, - shape.calls_per_lane(), - shape.calls_per_function() - ); - } - - println!("\n── in-memory vs wire ──"); - println!( - " FunctionBody {} B ([u8; 360] + u16 len + LaneShape)", - size_of::() - ); - println!( - " wire payload 360 B (len & shape NOT written: padding is length, classid is shape)" - ); - - println!("\n── amortized cost per call (whole {NODE_BYTES} B node) ──"); - for shape in LaneShape::ALL { - let cap = shape.calls_per_function(); - for div in [1usize, 2, 4] { - let calls = cap / div; - let per_call = NODE_BYTES as f64 / calls as f64; - let occupancy = 100.0 / div as f64; - println!(" {shape:?} {calls:3} calls ({occupancy:5.1}% full) {per_call:6.3} B/call"); - } - } - - println!("\n── calls are NOT contiguous in the slab ──"); - for shape in [LaneShape::Pairs, LaneShape::Quads] { - let cpl = shape.calls_per_lane(); - for i in [0usize, cpl - 1, cpl, shape.calls_per_function() - 1] { - println!( - " {shape:?} call {i:3} → slab offset {:3} (lane {:2}, byte {:2} of its payload)", - FunctionBody::call_slab_offset(shape, i), - i / cpl, - (i % cpl) * shape.bytes_per_call() - ); - } - } -} diff --git a/crates/ogar-blockly/src/lib.rs b/crates/ogar-blockly/src/lib.rs deleted file mode 100644 index 935dd9f..0000000 --- a/crates/ogar-blockly/src/lib.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! `ogar-blockly` — the **visual block-programming vocabulary**: one 256-slot -//! palette of commands and concepts, shared by every block frontend. -//! -//! # What this is -//! -//! A block editor (Blockly, Scratch, or any successor) renders *tiles*. This -//! crate is the single public place the tile vocabulary lives: **one byte per -//! command**, 256 slots, deduplicated across frontends so that two editors -//! rendering the same operation land on the **same palette slot** rather than -//! on two ids that merely sit in the same domain. -//! -//! That convergence is the entire point. `logic_compare[LT]` (Blockly) and -//! `operator_lt` (Scratch) are ONE slot — [`FnIndex::LT`]. `operator_mathop` -//! (one Scratch block with a dropdown) fans out to the same slots that -//! `math_single` + `math_trig` (two Blockly blocks) fan out to. The palette is -//! where the two vocabularies actually meet. -//! -//! # The ABI lives one level down, in `ogar-loco` -//! -//! The call encoding this palette rides on — `Call = (function : value)` -//! rails, [`LaneShape`] carvings, [`FunctionBody`] budgets, the stored-node -//! round-trip, the constant pool, the [`Program`](ogar_loco::Program) -//! reference rules, and the shared computational core's arity tables — is the -//! **vocabulary-agnostic surface** every sibling codebook shares -//! (elixir-shaped templates and flow frontends are next in line). It was -//! hoisted from this crate into [`ogar_loco`]; this crate re-exports that -//! surface unchanged, so existing consumers keep compiling, and adds what is -//! genuinely Blockly/Scratch: -//! -//! - the palette constants' *meanings* (documented on [`FnIndex`]'s -//! associated constants, re-exported from the core where the shared -//! computational range is defined once for every vocabulary), -//! - the Blocks concept domain (`0x17XX`) and its ONE concept id (`0x1717`), -//! which names the PALETTE; the node shapes it stores into are -//! [`ogar_loco::LocoConcept`]'s (`0x1701` / `0x1702`), globally owned, -//! - the [`SoaSplit`] storage partitioning, -//! - [`BlocklyVocabulary`], this palette's [`Vocabulary`] implementation. -//! -//! # Classid routing — the reserved Blocks domain (`0x17XX`) -//! -//! `ogar_vocab` reserves `ConceptDomain::Blocks` (`0x17XX`) and ships ZERO -//! concept rows there. This crate is the authoritative home for the ids inside -//! that domain — the same plug-and-play posture as `ogar-obo` over -//! `ConceptDomain::Ontology`: only consumers that dep `ogar-blockly` compile -//! them, so ERP / clinical / project consumers never pull a block vocabulary -//! they have no use for. -//! -//! # Storage shape — inventory SoA + N content SoAs, split by function -//! -//! Functions are not pooled into one table. There is an **inventory** SoA (the -//! registry: which functions exist, addressed by identity) and **N content** -//! SoAs, **partitioned by function** — see [`SoaSplit`]. That partitioning is -//! the V3 mailbox doctrine, not a storage preference: one function = one owner -//! = its own SoA, so every write is owned and no singleton table accumulates -//! writers. -//! -//! The split belongs to the SUBSTRATE, not to this palette: both partitions -//! resolve to [`ogar_loco::LocoConcept`], because a registry row and a -//! function body are the same two shapes for every vocabulary. -//! -//! # Provenance fence (load-bearing, not decorative) -//! -//! Every palette entry here is derived from **permissively-licensed or -//! specification** sources — the Apache-2.0 Blockly block definitions and the -//! Apache-2.0 `scratch-blocks` block definitions — and **never** by -//! transcribing a GPL/AGPL implementation (`scratch-vm` is AGPL; it is not a -//! source for this table). That is what keeps this public codebook -//! unencumbered while a GPL consumer links it freely, and it is why the GPL -//! boundary can sit entirely inside the consumer repo instead of propagating -//! here. Cross-ref: `ogar_vocab::ConceptDomain::Blocks`, `docs/DISCOVERY-MAP.md` -//! `D-BLOCKS-DOMAIN`. - -#![warn(missing_docs)] -#![forbid(unsafe_code)] - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -pub use ogar_vocab::ConceptDomain; - -// ── The shared surface, re-exported unchanged ─────────────────────────────── -// The ABI hoist (ogar-loco) must be invisible to existing consumers: every -// name this crate exported before the hoist is re-exported here, same paths, -// same semantics. New surface (Vocabulary, node/pool/program modules) is NOT -// re-exported — a consumer that wants the vocabulary-agnostic machinery deps -// `ogar-loco` directly. -pub use ogar_loco::{ - BODY_BYTES, BodyError, CLASSID_BYTES, CONTENT_SLOTS, Call, FnIndex, FunctionBody, LaneShape, - MAX_VALUES_PER_CALL, PAYLOAD_BYTES_PER_SLOT, SLOT_STRIDE, VALUE_SLAB_LEN, call_in_slab, -}; - -use ogar_loco::{DOMAIN_FLOOR, RegistryError, Vocabulary, VocabularyRegistry}; - -/// The reserved Blocks [`ConceptDomain`] every block node routes on. Live in -/// `ogar_vocab` with zero shared codebook rows, so a consumer can branch on it -/// today. -pub const BLOCKS_DOMAIN: ConceptDomain = ConceptDomain::Blocks; - -/// The high byte of the Blocks domain (`0x17`) — the `id >> 8` a consumer -/// matches when routing a block node from a bare classid. -pub const BLOCKS_DOMAIN_HI: u8 = 0x17; - -/// First palette slot reserved for **device-specific** families — the -/// sprite/stage vocabulary (motion, looks, sound, events, sensing) that exists -/// in a Scratch-style frontend and has no counterpart in a general block -/// editor. -/// -/// This is this palette's reading of the core's -/// [`DOMAIN_FLOOR`] (re-exported under the -/// historical name): below the floor is the shared computational core, whose -/// tables live once in `ogar_loco::vocabulary::shared_core`; at/above it is -/// this vocabulary's own range. The range above the floor is **reserved, not -/// allocated** — 108 device opcodes were measured in the Apache-2.0 -/// `scratch-blocks` definitions, and they mint when a consumer needs them. -/// Reserve, don't reclaim. -pub const DEVICE_FAMILY_FLOOR: u8 = DOMAIN_FLOOR; - -// ── Concept ids (authoritative here, NOT in the shared codebook) ──────────── - -/// The **one** concept this crate owns: `0x1717`. -/// -/// # Why exactly one, and why not `0x1701`/`0x1702` -/// -/// The node shapes a block program stores into — the function body and the -/// inventory row — are NOT Blockly's. They are described entirely in -/// `ogar-loco`'s own vocabulary ([`FunctionBody`], [`LaneShape`], the value -/// slab), and an elixir-shaped thinking template or an RO relation body is -/// the same shape with a different palette. So they belong to the substrate -/// and live at [`ogar_loco::LocoConcept`] (`0x1701` / `0x1702`) — **global -/// interest**, because thinking orchestration rides the same call ABI. -/// -/// This crate previously owned those two ids, which read as though a -/// frontend owned the universal shape. It doesn't. What is genuinely -/// Blockly's is ONE thing: *which palette resolves the bytes* — and that -/// needs exactly one classid. -/// -/// # This crate is a CONSUMER inside loco's domain -/// -/// `0x17` belongs to the substrate, not to block programming (operator, -/// 2026-08-07). This palette is **seated at `0x1717`** — deliberately high, -/// so `0x1703`–`0x1716` stays contiguous headroom for `ogar-loco`'s own -/// growth (uplifting, Klickwege, whatever the ABI needs next). One slot is -/// the whole allocation: if a block frontend ever outgrows it, it gets its -/// **own domain** rather than expanding into the substrate's headroom. -/// -/// The operations themselves are palette **bytes**, never concepts: 256 -/// `FnIndex` slots resolved through this crate's [`BlocklyVocabulary`], not -/// 256 codebook rows. That is why one id suffices, and why the shared -/// codebook stays at zero `0x17XX` rows — this palette is **plug-and-play**, -/// activated only in a build that actually contains a block frontend. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[non_exhaustive] -pub enum BlockConcept { - /// `0x1717` — **the Blockly/Scratch palette**: the classid that says - /// "resolve this node's call bytes through [`BlocklyVocabulary`]". The - /// node's SHAPE comes from [`ogar_loco::LocoConcept`]; this names the - /// vocabulary, not the shape. - Palette, -} - -impl BlockConcept { - /// Every concept, in id order — the enumeration hook a consumer uses to - /// inherit the full set instead of hand-maintaining a parallel list. - pub const ALL: [BlockConcept; 1] = [BlockConcept::Palette]; - - /// This concept's canonical id inside the `0x17XX` Blocks domain. - /// - /// **Read from `ogar_vocab`, never re-declared here.** The id is declared - /// once in [`ogar_vocab::blocks_actions::BLOCK_PALETTE`] — the crate that - /// also declares the capability table keyed by it — because this crate - /// depends on `ogar-vocab` and the reverse is impossible. Two constants - /// for one id is exactly the drift the classid join exists to catch. - /// - /// That row is **activated, not canon**: it lives behind `ogar-vocab`'s - /// `blocks` feature, which this crate's dependency turns on, so it exists - /// only in a build graph that actually contains a block editor. The shared - /// `class_ids::ALL` keeps zero `0x17XX` rows — that surface is mirrored - /// into lance-graph under a compile-time fuse, and a frontend's palette is - /// not lance-graph's concern. - #[must_use] - pub const fn concept_id(self) -> u16 { - match self { - BlockConcept::Palette => ogar_vocab::blocks_actions::BLOCK_PALETTE, - } - } - - /// The full V3 render classid under a consumer's app prefix — canon-high - /// `(concept as u32) << 16 | app_prefix`. Identical idiom to - /// `ogar_obo::Namespace::render_classid` and `ogar_vocab::render_classid`. - #[must_use] - pub const fn render_classid(self, app_prefix: u16) -> u32 { - ((self.concept_id() as u32) << 16) | (app_prefix as u32) - } -} - -// ── Storage partitioning ──────────────────────────────────────────────────── - -/// How block content is partitioned across SoA tables. -/// -/// Functions are **not** pooled into one table: an [`Inventory`](SoaSplit::Inventory) -/// SoA registers which functions exist, and each function's body lives in its -/// own [`Content`](SoaSplit::Content) SoA. -/// -/// That split is the V3 mailbox doctrine rather than a storage preference — -/// one function = one owner = its own SoA, so every write is owned and no -/// shared table accumulates writers. A registry read (what exists, where) never -/// touches a body, and a body write never contends with another function's. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum SoaSplit { - /// The function registry — one row per function, addressed by identity. - Inventory, - /// One function's body — up to [`LaneShape::calls_per_function`] calls. - Content, -} - -impl SoaSplit { - /// The **`ogar-loco`** concept whose classid this partition's rows carry. - /// - /// The split is the substrate's, not this palette's: a registry row and a - /// function body are the same two shapes for every vocabulary, so they - /// resolve to [`ogar_loco::LocoConcept`], never to [`BlockConcept`]. This - /// crate's own id ([`BlockConcept::Palette`], `0x1717`) says which - /// vocabulary resolves the call bytes — a different question from which - /// shape the row is. - #[must_use] - pub const fn concept(self) -> ogar_loco::LocoConcept { - match self { - SoaSplit::Inventory => ogar_loco::LocoConcept::Inventory, - SoaSplit::Content => ogar_loco::LocoConcept::FunctionBody, - } - } -} - -// ── This palette's Vocabulary implementation ──────────────────────────────── - -/// The Blockly/Scratch palette as an `ogar-loco` [`Vocabulary`]. -/// -/// Every operation this palette has *allocated* sits below the floor — i.e. -/// in the shared computational core, whose tables live in the core crate — -/// so the domain hooks answer nothing yet. That is honest, not lazy: the -/// device families above [`DEVICE_FAMILY_FLOOR`] are **reserved, not -/// allocated**, and when they mint, their arity/body-reference tables land -/// here (and only here — the core never learns device vocabulary). -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct BlocklyVocabulary; - -impl Vocabulary for BlocklyVocabulary { - fn domain_stack_arity(&self, _f: FnIndex) -> Option { - // No device family is minted yet; an above-floor byte is refused, - // never guessed. - None - } - - fn domain_body_refs(&self, _f: FnIndex) -> u8 { - 0 - } -} - -// ── Plug-and-play ─────────────────────────────────────────────────────────── - -/// Validate this palette and plug it into a consumer's -/// [`VocabularyRegistry`] under the Blocks **content** concept -/// ([`BlockConcept::Palette`]) — the USB handshake for this device. -/// -/// A consumer (blockly-rs, lance-graph) builds ONE registry at boot and -/// calls each vocabulary crate's `plug_into`; every stored function node -/// then resolves through `registry.resolve_classid(node_classid)`, with no -/// consumer-side "this node must be Blockly" branch. Only the CONTENT -/// concept is plugged: it names WHICH vocabulary resolves a node's call -/// bytes. The node SHAPES ([`ogar_loco::LocoConcept`]) are the substrate's -/// and are not this palette's to register. -/// -/// # Errors -/// -/// [`RegistryError::ConceptTaken`] if something already claimed the Blocks -/// content concept — refused loudly rather than silently overwritten. -/// -/// # Panics -/// -/// Never in practice: [`BlocklyVocabulary`] conformance is pinned by this -/// crate's own tests, so `validate` cannot fail here. -pub fn plug_into(registry: &mut VocabularyRegistry) -> Result<(), RegistryError> { - let checked = ogar_loco::vocabulary::conformance::validate(BlocklyVocabulary) - .expect("BlocklyVocabulary conforms; pinned by this crate's tests"); - registry.plug(BlockConcept::Palette.concept_id(), &checked) -} - -#[cfg(test)] -mod tests { - use super::*; - use ogar_loco::vocabulary::{conformance, shared_core}; - use ogar_vocab::canonical_concept_domain; - - #[test] - fn every_concept_routes_to_the_blocks_domain() { - for c in BlockConcept::ALL { - assert_eq!(canonical_concept_domain(c.concept_id()), BLOCKS_DOMAIN); - assert_eq!((c.concept_id() >> 8) as u8, BLOCKS_DOMAIN_HI); - } - } - - #[test] - fn concept_ids_are_distinct_and_nonzero() { - // A zero id would collide with NodeGuid::CLASSID_DEFAULT; a duplicate - // would silently alias two schemas onto one classid. - let mut seen = Vec::new(); - for c in BlockConcept::ALL { - let id = c.concept_id(); - assert_ne!(id, 0, "{c:?} has a zero concept id"); - assert!(!seen.contains(&id), "{c:?} duplicates id {id:#06x}"); - seen.push(id); - } - } - - #[test] - fn render_classid_is_canon_high() { - // canon concept HIGH, app render prefix LOW (D-CLASSID-CANON-HIGH-FLIP). - let id = BlockConcept::Palette.render_classid(0x1000); - assert_eq!(id, 0x1717_1000); - assert_eq!(id >> 16, u32::from(BlockConcept::Palette.concept_id())); - assert_eq!(id & 0xFFFF, 0x1000); - } - - #[test] - fn soa_split_maps_each_partition_to_its_own_concept() { - // Inventory and Content must NOT share a classid — the whole point of - // the split is that a registry read never touches a body. - assert_eq!( - SoaSplit::Inventory.concept(), - ogar_loco::LocoConcept::Inventory - ); - assert_eq!( - SoaSplit::Content.concept(), - ogar_loco::LocoConcept::FunctionBody - ); - assert_ne!( - SoaSplit::Inventory.concept().concept_id(), - SoaSplit::Content.concept().concept_id() - ); - } - - #[test] - fn the_blockly_vocabulary_conforms_to_the_sharing_discipline() { - // The mechanical gate every vocabulary crate must run: shared-core - // bytes answer from the core, the domain range refuses what is not - // minted, and no reported shape can truncate a call's own body - // references. - assert_eq!(conformance::check(&BlocklyVocabulary), Ok(())); - // Spot-check the routing this palette relies on: control flow and - // expressions answer from the shared core THROUGH the vocabulary. - let v = BlocklyVocabulary; - assert_eq!(v.stack_arity(FnIndex::REPEAT), Some(1)); - assert_eq!(v.body_refs(FnIndex::IF_ELSE), 2); - assert_eq!(v.stack_arity(FnIndex::ADD), Some(2)); - // …and an unminted device byte is refused, not guessed. - assert_eq!(v.stack_arity(FnIndex(DEVICE_FAMILY_FLOOR)), None); - } - - #[test] - fn shared_core_and_device_family_partition_the_palette() { - // Can-fire AND can-stay-silent on the same predicate: a classifier that - // answers the same way for everything is worthless. (`is_domain_specific` - // is the core's name; this palette reads it as "device family".) - assert!(FnIndex::LT.is_shared_core()); - assert!(!FnIndex::LT.is_domain_specific()); - - let device = FnIndex(DEVICE_FAMILY_FLOOR); - assert!(device.is_domain_specific()); - assert!(!device.is_shared_core()); - - // NOP is not an operation at all — neither bucket claims it. - assert!(!FnIndex::NOP.is_shared_core()); - assert!(!FnIndex::NOP.is_domain_specific()); - } - - #[test] - fn every_named_op_is_a_distinct_slot_in_the_shared_core() { - // The whole value of the palette is that two frontends land on ONE - // slot. A duplicate here would silently merge two operations; a slot at - // or above the device floor would misclassify a shared op. This ALSO - // proves the re-export surface is complete for every named constant — - // the census compiles against `ogar_blockly::FnIndex`, exactly as the - // downstream consumers do. - let named: &[(&str, FnIndex)] = &[ - ("IF", FnIndex::IF), - ("IF_ELSE", FnIndex::IF_ELSE), - ("REPEAT", FnIndex::REPEAT), - ("REPEAT_UNTIL", FnIndex::REPEAT_UNTIL), - ("WHILE", FnIndex::WHILE), - ("FOREVER", FnIndex::FOREVER), - ("FOR_EACH", FnIndex::FOR_EACH), - ("FOR_RANGE", FnIndex::FOR_RANGE), - ("WAIT", FnIndex::WAIT), - ("WAIT_UNTIL", FnIndex::WAIT_UNTIL), - ("STOP", FnIndex::STOP), - ("BREAK", FnIndex::BREAK), - ("CONTINUE", FnIndex::CONTINUE), - ("RETURN", FnIndex::RETURN), - ("AND", FnIndex::AND), - ("OR", FnIndex::OR), - ("NOT", FnIndex::NOT), - ("TRUE", FnIndex::TRUE), - ("FALSE", FnIndex::FALSE), - ("NULL", FnIndex::NULL), - ("TERNARY", FnIndex::TERNARY), - ("EQ", FnIndex::EQ), - ("NEQ", FnIndex::NEQ), - ("LT", FnIndex::LT), - ("LTE", FnIndex::LTE), - ("GT", FnIndex::GT), - ("GTE", FnIndex::GTE), - ("ADD", FnIndex::ADD), - ("SUB", FnIndex::SUB), - ("MUL", FnIndex::MUL), - ("DIV", FnIndex::DIV), - ("POW", FnIndex::POW), - ("MOD", FnIndex::MOD), - ("NUMBER", FnIndex::NUMBER), - ("ABS", FnIndex::ABS), - ("NEG", FnIndex::NEG), - ("ROUND", FnIndex::ROUND), - ("FLOOR", FnIndex::FLOOR), - ("CEIL", FnIndex::CEIL), - ("SQRT", FnIndex::SQRT), - ("LN", FnIndex::LN), - ("LOG10", FnIndex::LOG10), - ("EXP_E", FnIndex::EXP_E), - ("EXP_10", FnIndex::EXP_10), - ("SIN", FnIndex::SIN), - ("COS", FnIndex::COS), - ("TAN", FnIndex::TAN), - ("ASIN", FnIndex::ASIN), - ("ACOS", FnIndex::ACOS), - ("ATAN", FnIndex::ATAN), - ("ATAN2", FnIndex::ATAN2), - ("RANDOM_INT", FnIndex::RANDOM_INT), - ("RANDOM_FLOAT", FnIndex::RANDOM_FLOAT), - ("CONSTRAIN", FnIndex::CONSTRAIN), - ("NUMBER_PROPERTY", FnIndex::NUMBER_PROPERTY), - ("CONSTANT", FnIndex::CONSTANT), - ("ON_LIST", FnIndex::ON_LIST), - ("TEXT", FnIndex::TEXT), - ("JOIN", FnIndex::JOIN), - ("LENGTH", FnIndex::LENGTH), - ("CHAR_AT", FnIndex::CHAR_AT), - ("INDEX_OF", FnIndex::INDEX_OF), - ("IS_EMPTY", FnIndex::IS_EMPTY), - ("SUBSTRING", FnIndex::SUBSTRING), - ("CHANGE_CASE", FnIndex::CHANGE_CASE), - ("TRIM", FnIndex::TRIM), - ("CONTAINS", FnIndex::CONTAINS), - ("APPEND", FnIndex::APPEND), - ("PRINT", FnIndex::PRINT), - ("PROMPT", FnIndex::PROMPT), - ("COUNT", FnIndex::COUNT), - ("REPLACE", FnIndex::REPLACE), - ("REVERSE", FnIndex::REVERSE), - ("LIST_EMPTY", FnIndex::LIST_EMPTY), - ("LIST_WITH", FnIndex::LIST_WITH), - ("LIST_REPEAT", FnIndex::LIST_REPEAT), - ("LIST_LENGTH", FnIndex::LIST_LENGTH), - ("LIST_IS_EMPTY", FnIndex::LIST_IS_EMPTY), - ("LIST_INDEX_OF", FnIndex::LIST_INDEX_OF), - ("LIST_GET", FnIndex::LIST_GET), - ("LIST_SET", FnIndex::LIST_SET), - ("LIST_INSERT", FnIndex::LIST_INSERT), - ("LIST_ADD", FnIndex::LIST_ADD), - ("LIST_DELETE", FnIndex::LIST_DELETE), - ("LIST_DELETE_ALL", FnIndex::LIST_DELETE_ALL), - ("LIST_SUBLIST", FnIndex::LIST_SUBLIST), - ("LIST_SPLIT", FnIndex::LIST_SPLIT), - ("LIST_SORT", FnIndex::LIST_SORT), - ("LIST_CONTAINS", FnIndex::LIST_CONTAINS), - ("VAR_GET", FnIndex::VAR_GET), - ("VAR_SET", FnIndex::VAR_SET), - ("VAR_CHANGE", FnIndex::VAR_CHANGE), - ("PROC_DEF", FnIndex::PROC_DEF), - ("PROC_CALL", FnIndex::PROC_CALL), - ("PROC_ARG", FnIndex::PROC_ARG), - ]; - - let mut seen: Vec<(u8, &str)> = Vec::new(); - for (name, op) in named { - assert!( - op.is_shared_core(), - "{name} at {:#04x} is not in the shared core", - op.0 - ); - if let Some((_, prior)) = seen.iter().find(|(slot, _)| *slot == op.0) { - panic!("{name} collides with {prior} at slot {:#04x}", op.0); - } - seen.push((op.0, name)); - } - - // Anti-vacuity: the table must actually be substantial, or "all - // distinct" is trivially true of a near-empty list. - assert!(seen.len() >= 90, "palette census shrank to {}", seen.len()); - - // And the shared core's tables must cover the control range this - // palette's frontends lower through — spot-anchored so a core-side - // regression is caught from the vocabulary side too. - assert_eq!(shared_core::stack_arity(FnIndex::REPEAT), Some(1)); - assert_eq!(shared_core::body_refs(FnIndex::REPEAT), 1); - } -} diff --git a/crates/ogar-loco/src/lib.rs b/crates/ogar-loco/src/lib.rs index f843389..69d66d2 100644 --- a/crates/ogar-loco/src/lib.rs +++ b/crates/ogar-loco/src/lib.rs @@ -3,7 +3,7 @@ //! //! # What this is, and why it exists //! -//! The block-editor arc (`ogar-blockly` + the `blockly-rs` consumers) proved a +//! The block-editor arc (the `blockly-rs` consumers) proved a //! storage shape for programs on the V3 substrate. The operator direction that //! created this crate generalizes it: *elixir-shaped templates are "just a //! rails-shaped semantic over classid index, 256:256 — not much different than @@ -17,7 +17,7 @@ //! budgets, the refuse-don't-truncate guards, the shared computational //! core's tables, the constant pool, the program/reference rules, and the //! [`Vocabulary`] seam a sibling codebook plugs into. -//! - **A vocabulary crate per domain** (`ogar-blockly` is the first) — the +//! - **A vocabulary per domain, declared by whoever owns it** — the //! palette *meanings* above the shared core, value-parameter codebooks, //! frontend membranes (Blockly JSON, a template DSL, flow JSON), and //! lowering from that frontend's records. @@ -107,7 +107,7 @@ //! //! - **No concept mints.** Content/Inventory concept ids per domain are //! operator decisions with ledger entries; vocabulary crates carry them -//! (`ogar-blockly`'s `0x1701`/`0x1702` are the precedent). +//! (this crate's own `0x1701`/`0x1702` are the precedent). //! - **No GUID minting.** [`node::FunctionNode`] round-trips an opaque key. //! - **No frontend lowering.** Casting a Blockly record / template DSL / flow //! JSON into calls is the vocabulary crate's job; this crate holds the @@ -150,7 +150,7 @@ pub use vocabulary::{FnSpec, ValueCodebook, Vocabulary, VocabularyTable}; /// /// This is the global half of the split: `ogar-loco` is global interest /// (thinking orchestration rides the same call ABI), whereas a particular -/// palette — Blockly/Scratch opcodes, `ogar-blockly`'s `0x1717` — is +/// palette — Blockly/Scratch opcodes, `blockly-rs`'s `0x1717` — is /// activated only in a build that actually contains that frontend. /// /// # `0x17` is LOCO's domain, and consumers are seated above @@ -162,7 +162,7 @@ pub use vocabulary::{FnSpec, ValueCodebook, Vocabulary, VocabularyTable}; /// |---|---|---| /// | `0x1701` / `0x1702` | **loco** | the node shapes — body + inventory | /// | `0x1703`–`0x1716` | **loco, reserved** | headroom for the substrate's own growth — uplifting, Klickwege, whatever the ABI needs next | -/// | `0x1717`+ | **consumers** | one slot per frontend palette (`ogar-blockly` = `0x1717`) | +/// | `0x1717`+ | **consumers** | one slot per frontend palette (`blockly-rs` = `0x1717`) | /// /// Consumers were deliberately seated *high* so the substrate keeps /// contiguous room beneath them. A frontend that outgrows a single slot gets @@ -341,7 +341,7 @@ const _: () = assert!(LaneShape::Quads.calls_per_function() == 90); /// mint; unallocated domain slots stay reserved for each vocabulary. Neither /// side ever annexes the other's range. /// -/// In the first vocabulary (`ogar-blockly`) the domain range hosts the +/// In the first vocabulary (the `blockly-rs` palette) the domain range hosts the /// Scratch-style *device families* (motion, looks, sound, …) and the constant /// is re-exported there under its historical name `DEVICE_FAMILY_FLOOR`. pub const DOMAIN_FLOOR: u8 = 0x90; @@ -360,7 +360,7 @@ const _: () = assert!( /// There is no opcode/function distinction: the named constants below are the /// primitive low range (the shared computational core) of the same `<256` /// codebook that user-defined functions mint into, resolved through the -/// vocabulary's inventory registry (see `ogar-blockly`'s `SoaSplit` for the +/// vocabulary's inventory registry (see the `blockly-rs` `SoaSplit` for the /// first concrete registry split). A [`Call`]'s first byte is a `FnIndex`; an /// editor's pick-from palette is a *rendering* of this codebook. /// @@ -601,7 +601,7 @@ impl FnIndex { /// Is this a **vocabulary-specific** operation — one whose meaning comes /// from the classid-selected vocabulary rather than the shared core? /// - /// (In the first vocabulary, `ogar-blockly`, this range hosts the + /// (In the first vocabulary, the `blockly-rs` palette, this range hosts the /// Scratch-style device families.) #[must_use] pub const fn is_domain_specific(self) -> bool { diff --git a/crates/ogar-loco/src/registry.rs b/crates/ogar-loco/src/registry.rs index 7d3be2a..d758498 100644 --- a/crates/ogar-loco/src/registry.rs +++ b/crates/ogar-loco/src/registry.rs @@ -36,7 +36,7 @@ //! # Who plugs in //! //! Each vocabulary crate ships a `plug_into(&mut registry)` helper — -//! `ogar-blockly` plugs the Blocks content concept, `ogar-ro` plugs the +//! `blockly-rs` plugs the Blockly palette (`0x1717`), `ogar-ro` plugs the //! relation-body concept — and a consumer (blockly-rs, lance-graph) builds //! ONE registry at boot from the crates it deps, then resolves every stored //! function node through it. Two frontends, one hub, no hardcoded "this diff --git a/crates/ogar-ro/Cargo.toml b/crates/ogar-ro/Cargo.toml index caedc02..3927aae 100644 --- a/crates/ogar-ro/Cargo.toml +++ b/crates/ogar-ro/Cargo.toml @@ -18,4 +18,3 @@ ogar-obo = { path = "../ogar-obo" } serde = { workspace = true, optional = true } [dev-dependencies] -ogar-blockly = { path = "../ogar-blockly" } diff --git a/crates/ogar-ro/src/lib.rs b/crates/ogar-ro/src/lib.rs index 86c4c0c..315a83b 100644 --- a/crates/ogar-ro/src/lib.rs +++ b/crates/ogar-ro/src/lib.rs @@ -41,8 +41,8 @@ //! [`CheckedVocabulary`](ogar_loco::CheckedVocabulary) proves the sharing //! discipline and the shape invariant — nothing more. It carries no //! "is this vocabulary actually invoked at runtime" marker, and none is -//! needed here: `ogar-ro` is validated and read exactly like `ogar-blockly` -//! is, whether or not any consumer ever executes a relation call. A +//! needed here: `ogar-ro` is validated and read exactly like any other +//! palette, whether or not any consumer ever executes a relation call. A //! callability flag would be a second, redundant proof for a property the //! type never claimed in the first place. //! @@ -72,8 +72,8 @@ //! in the shared `ogar_vocab` codebook: the relation-body content classid //! lives inside the already-reserved `ogar_vocab::ConceptDomain::Ontology` //! (`0x03XX`), one slot past `ogar_obo`'s own RO term-node concept -//! (`0x0305`), the same plug-and-play posture `ogar-blockly` uses for the -//! `Blocks` domain. +//! (`0x0305`), the same plug-and-play posture every palette uses — the +//! Blockly palette (`0x1717`) is declared in `blockly-rs`, its own consumer. #![warn(missing_docs)] #![forbid(unsafe_code)] @@ -92,13 +92,13 @@ use ogar_loco::{RegistryError, Vocabulary, VocabularyRegistry}; /// The relation-body content classid's concept id — one slot past /// `ogar_obo::Namespace::Ro`'s term-node concept (`0x0305`) inside the /// shared `Ontology` domain (`0x03XX`). Authoritative HERE, never minted in -/// `ogar_vocab`'s codebook (plug-and-play, mirroring `ogar_blockly::BlockConcept`). +/// `ogar_vocab`'s codebook — the codebook carries shared concepts, never a +/// palette's private reading of a body. pub const RELATION_BODY_CONCEPT_ID: u16 = 0x0306; /// The full V3 render classid under a consumer's app prefix — canon-high /// `(concept << 16) | app_prefix`, the same idiom every sibling vocabulary -/// uses (`ogar_blockly::BlockConcept::render_classid`, -/// `ogar_vocab::render_classid`). +/// uses (`ogar_vocab::render_classid`). #[must_use] pub const fn relation_body_render_classid(app_prefix: u16) -> u32 { ((RELATION_BODY_CONCEPT_ID as u32) << 16) | (app_prefix as u32) @@ -140,8 +140,7 @@ macro_rules! relation_palette { /// Every predicate this palette mints, in slot order — the /// enumeration hook a consumer uses to inherit the full set instead - /// of hand-maintaining a parallel list (mirrors - /// `ogar_blockly::BlockConcept::ALL`). + /// of hand-maintaining a parallel list. pub const RELATIONS: &[RelationPredicate] = &[ $( RelationPredicate { index: $ident, name: $name, curie: $curie }, @@ -174,8 +173,8 @@ relation_palette! { /// Every minted predicate is a **binary assertion**: it pops two operands /// (subject, object), branches to nothing (`body_refs = 0` — a relation is a /// leaf, never a nested body), and pushes nothing (W-RO-2). Bytes above the -/// mint (`0xA0..=0xFF`) are reserved, not allocated — the same posture -/// `ogar-blockly` takes for its unminted device families: refused rather +/// mint (`0xA0..=0xFF`) are reserved, not allocated — the same posture the +/// Blockly palette takes for its unminted device families: refused rather /// than guessed, until a consumer needs the next predicate. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RelationVocabulary; @@ -215,8 +214,8 @@ impl Vocabulary for RelationVocabulary { /// Validate this palette and plug it into a consumer's /// [`VocabularyRegistry`] under [`RELATION_BODY_CONCEPT_ID`] — the USB -/// handshake for this device, identical in shape to -/// `ogar_blockly::plug_into`. +/// handshake for this device, identical in shape to every other palette's +/// own `plug_into`. /// /// A consumer deps whichever vocabulary crates it needs and calls each /// one's `plug_into` at boot; a stored relation node then resolves its diff --git a/crates/ogar-ro/tests/plug_and_play.rs b/crates/ogar-ro/tests/plug_and_play.rs index dae0ba9..e74fbb5 100644 --- a/crates/ogar-ro/tests/plug_and_play.rs +++ b/crates/ogar-ro/tests/plug_and_play.rs @@ -1,18 +1,53 @@ //! Two vocabularies, one hub — the plug-and-play claim, tested across crates. //! //! This is the falsifier for "a consumer deps the vocabulary crates it wants -//! and routes purely by classid": it plugs `ogar-blockly` and `ogar-ro` into -//! one registry — exactly what blockly-rs / lance-graph do at boot — and +//! and routes purely by classid": it plugs `ogar-ro` and a second, LOCAL +//! vocabulary into one registry — exactly what a consumer does at boot — and //! proves a stored node's classid alone selects the right semantic table, //! with no consumer-side branch naming either vocabulary. +//! +//! The second vocabulary is defined HERE, in the test, rather than borrowed +//! from a frontend crate. That is the point being tested: a consumer declares +//! its own palette and plugs it, so the substrate never needs a crate — or a +//! `const` naming one — to know that frontend exists. Borrowing a real +//! consumer would have made this test depend on the very coupling it exists +//! to disprove. + +use ogar_loco::vocabulary::conformance; +use ogar_loco::{FnIndex, Vocabulary, VocabularyRegistry}; + +/// A stand-in consumer palette: a slot of its own, and no minted domain +/// bytes — the honest shape of a frontend whose operations are all in the +/// shared computational core. +#[derive(Debug, Clone, Copy)] +struct StubPalette; + +impl Vocabulary for StubPalette { + fn domain_stack_arity(&self, _f: FnIndex) -> Option { + None + } + fn domain_body_refs(&self, _f: FnIndex) -> u8 { + 0 + } +} + +/// The stub's own consumer slot, above the substrate's reserved range. +const STUB_CONCEPT: u16 = 0x1718; + +fn stub_render_classid(app_prefix: u16) -> u32 { + (u32::from(STUB_CONCEPT) << 16) | u32::from(app_prefix) +} -use ogar_loco::{FnIndex, VocabularyRegistry}; +fn plug_stub(hub: &mut VocabularyRegistry) -> Result<(), ogar_loco::registry::RegistryError> { + let checked = conformance::validate(StubPalette).expect("the stub conforms"); + hub.plug(STUB_CONCEPT, &checked) +} /// The boot sequence a consumer actually writes: one hub, N `plug_into` /// calls, nothing vocabulary-specific afterward. fn boot() -> VocabularyRegistry { let mut hub = VocabularyRegistry::new(); - ogar_blockly::plug_into(&mut hub).unwrap(); + plug_stub(&mut hub).unwrap(); ogar_ro::plug_into(&mut hub).unwrap(); hub } @@ -24,20 +59,20 @@ fn one_hub_routes_two_vocabularies_by_classid_alone() { // Two stored nodes under DIFFERENT app prefixes — routing must ignore // the lo u16 (render skin) and read only the hi u16 (concept). - let blockly_node = ogar_blockly::BlockConcept::Palette.render_classid(0x1000); + let stub_node = stub_render_classid(0x1000); let relation_node = ogar_ro::relation_body_render_classid(0xBEEF); - let blocks = hub.resolve_classid(blockly_node).expect("blocks plugged"); + let stub = hub.resolve_classid(stub_node).expect("stub plugged"); let relations = hub.resolve_classid(relation_node).expect("ro plugged"); - // The RO table covers its predicates; the Blockly table refuses that + // The RO table covers its predicates; the stub refuses that // same byte (no device family minted). Same FnIndex, two answers — // which is the whole point of routing by classid. let part_of = ogar_ro::PART_OF; assert_eq!(relations.stack_arity(part_of), Some(2)); assert_eq!(relations.name(part_of), Some("part_of")); - assert_eq!(blocks.stack_arity(part_of), None); - assert_eq!(blocks.name(part_of), None); + assert_eq!(stub.stack_arity(part_of), None); + assert_eq!(stub.name(part_of), None); } #[test] @@ -47,24 +82,22 @@ fn the_shared_core_is_identical_across_every_plugged_device() { // A drift here would mean `ADD` means two things depending on which node // you happened to load. let hub = boot(); - let blocks = hub - .resolve_classid(ogar_blockly::BlockConcept::Palette.render_classid(0x1000)) - .unwrap(); + let stub = hub.resolve_classid(stub_render_classid(0x1000)).unwrap(); let relations = hub .resolve_classid(ogar_ro::relation_body_render_classid(0x1000)) .unwrap(); for b in 0..ogar_loco::DOMAIN_FLOOR { let f = FnIndex(b); - assert_eq!(blocks.stack_arity(f), relations.stack_arity(f), "{f:?}"); - assert_eq!(blocks.body_refs(f), relations.body_refs(f), "{f:?}"); - assert_eq!(blocks.pushes_result(f), relations.pushes_result(f), "{f:?}"); - assert_eq!(blocks.name(f), relations.name(f), "{f:?}"); + assert_eq!(stub.stack_arity(f), relations.stack_arity(f), "{f:?}"); + assert_eq!(stub.body_refs(f), relations.body_refs(f), "{f:?}"); + assert_eq!(stub.pushes_result(f), relations.pushes_result(f), "{f:?}"); + assert_eq!(stub.name(f), relations.name(f), "{f:?}"); } // Anti-vacuity: the shared range must actually be covered somewhere, or // "identical" is trivially true of two empty tables. - assert_eq!(blocks.stack_arity(FnIndex::ADD), Some(2)); - assert_eq!(blocks.body_refs(FnIndex::IF_ELSE), 2); + assert_eq!(stub.stack_arity(FnIndex::ADD), Some(2)); + assert_eq!(stub.body_refs(FnIndex::IF_ELSE), 2); } #[test] @@ -89,7 +122,7 @@ fn plugging_the_same_device_twice_is_refused() { // they own one concept, which must surface at boot, not at read time. let mut hub = boot(); assert!(ogar_ro::plug_into(&mut hub).is_err()); - assert!(ogar_blockly::plug_into(&mut hub).is_err()); + assert!(plug_stub(&mut hub).is_err()); // …and the first device kept its port. assert_eq!(hub.len(), 2); let relations = hub diff --git a/crates/ogar-vocab/Cargo.toml b/crates/ogar-vocab/Cargo.toml index 4e6d3aa..16274f0 100644 --- a/crates/ogar-vocab/Cargo.toml +++ b/crates/ogar-vocab/Cargo.toml @@ -11,16 +11,6 @@ description = "Open Graph of Active Record — canonical IR types for the AR-sha [features] default = [] serde = ["dep:serde"] -# The Blocks (visual block-programming) codebook — ACTIVATED, not canon. -# Off by default, so a consumer that never touches a block editor carries -# zero 0x17XX rows. `ogar-blockly` turns it on for its own dependents, which -# makes activation Cargo presence rather than runtime detection (the same -# rule `lance-graph-ogar` documents). The rows it adds are consulted by -# `capability_registry::resolve_hotplug` ALONGSIDE `class_ids::ALL` and are -# deliberately never merged into it — that surface is mirrored into -# lance-graph under a compile-time count fuse, and a frontend's palette is -# not lance-graph's concern. -blocks = [] [dependencies] serde = { workspace = true, optional = true } diff --git a/crates/ogar-vocab/src/blocks_actions.rs b/crates/ogar-vocab/src/blocks_actions.rs deleted file mode 100644 index 1da0698..0000000 --- a/crates/ogar-vocab/src/blocks_actions.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Blocks capability surface — **feature-activated, never shared canon**. -//! -//! Compiled ONLY under `--features blocks`, which `ogar-blockly` turns on for -//! its own dependents. A consumer that never touches a block editor never -//! compiles this module, never carries its rows, and sees `0x17XX` exactly as -//! it did before: a reserved domain with zero concepts. -//! -//! # Why this is not in `class_ids::ALL` -//! -//! `class_ids::ALL` is mirrored into `lance_graph_contract::ogar_codebook` -//! under a **compile-time** count fuse (`lance_graph_ogar::parity::COUNT_FUSE`). -//! Anything minted there is, by construction, a lance-graph change — and a -//! block editor's palette is not lance-graph's concern. Minting these rows -//! there once already turned lance-graph red against `main` and dragged in -//! `ogar-class-view`, `all_promoted_classes` and both fuse halves. -//! -//! So the activated rows live HERE, in [`ACTIVATED_CONCEPTS`], which -//! [`resolve_hotplug`](crate::capability_registry::resolve_hotplug) consults -//! **in addition to** `class_ids::ALL`. The global codebook keeps its exact -//! contents; a particular frontend's codebook rides its own feature. -//! -//! # Why the table is here and not in `ogar-blockly` -//! -//! [`domain_tables`](crate::capability_registry::domain_tables) resolves its -//! entries at compile time from modules inside THIS crate, so a table declared -//! in the producer crate is a **parallel registry that verifies itself against -//! itself and passes** while the real port answers `NoCapabilitiesFor` — the -//! `ogar-osm` defect `geo_actions` was written to correct. `ogar-blockly` -//! cannot host it for a second reason: `ogar-blockly` depends on -//! `ogar-vocab`, so `ogar-vocab` could never read the ids back. -//! -//! That dependency direction is also why the id is declared here and *read* by -//! `ogar-blockly`, not the reverse — one source of truth for `0x1717`. -//! -//! # The subject -//! -//! Only the PALETTE concept binds capabilities. `ogar-loco`'s node shapes -//! (`0x1701` / `0x1702`) are deliberately absent: they are the substrate's, -//! shared by every vocabulary, and a consumer plugging them would claim -//! ownership of the shape every sibling rides. - -use crate::{ActionDef, ActionSubject, KausalSpec}; - -/// The Blocks **palette** concept — `0x1717`, canon-high. -/// -/// Seated at `0x1717` rather than low in the domain because `0x1701`–`0x1716` -/// is `ogar-loco`'s: `0x1701`/`0x1702` are the node shapes and `0x1703`– -/// `0x1716` is the substrate's reserved headroom. Consumers are seated high so -/// the substrate keeps contiguous room beneath them (OGAR #255). -/// -/// Read by `ogar_blockly::BlockConcept::Palette` — declared once, here, since -/// `ogar-blockly` deps this crate and the reverse is impossible. -pub const BLOCK_PALETTE: u16 = 0x1717; - -/// The concept rows this feature ACTIVATES — consulted by -/// [`resolve_hotplug`](crate::capability_registry::resolve_hotplug) alongside -/// `class_ids::ALL`, and deliberately never merged into it. -/// -/// This is the whole "codebook triggered by plug-and-play" mechanism: with the -/// feature off the slice does not exist, `canonical_concept_domain(0x1717)` -/// still routes to [`Blocks`](crate::ConceptDomain::Blocks) on the reserved -/// domain byte alone, and a plug of `0x1717` correctly reports -/// `UnknownClassid`. With the feature on — i.e. when a block editor is -/// actually in the build graph — the same plug resolves. -pub const ACTIVATED_CONCEPTS: &[(&str, u16)] = &[("block_palette", BLOCK_PALETTE)]; - -/// Every Blocks capability name, in table order — the `const`-evaluable -/// fingerprint of [`blocks_actions`]. -pub const BLOCKS_ACTION_NAMES: &[&str] = &[ - "lower_script", - "raise_calls", - "render_text", - "parse_text", - "klickweg_address", -]; - -/// One Blocks [`ActionDef`], keyed by the palette concept. -/// -/// `object_class` carries the concept name so `derive_action_rows` recovers it -/// from the last `/` segment — the same fuse shape as `geo_actions` and -/// `ocr_actions`. Resolution goes through the feature-activated rows, so this -/// resolves iff the feature that declares the concept is the one compiling it. -fn blocks_action_def(capability: &'static str) -> ActionDef { - let object_class = "ogit-blocks/block_palette".to_owned(); - let identity = format!("{object_class}::action_def::{capability}"); - ActionDef { - identity, - predicate: capability.to_owned(), - object_class, - // Lowering a workspace or raising a body is a pure transform the - // editor invokes on its own content — the caller is the substrate - // (an editor cast, a render pass), not an authenticated User. - default_subject: ActionSubject::System, - // Invoked directly by a same-process caller with no OGAR-side - // precondition to guard on — `KausalSpec::External`'s documented case. - kausal: Some(KausalSpec::External), - ..ActionDef::default() - } -} - -/// The Blocks capability surface — one [`ActionDef`] per capability, in -/// [`BLOCKS_ACTION_NAMES`] order. -/// -/// Every entry is a real `blockly-abi` public function. `resolve_hotplug` -/// checks coverage in BOTH directions, so an aspirational entry fails the -/// consumer's own activation rather than quietly describing work nobody did. -#[must_use] -pub fn blocks_actions() -> Vec { - BLOCKS_ACTION_NAMES - .iter() - .map(|&capability| blocks_action_def(capability)) - .collect() -} - -/// The executors the authority EXPECTS to register against this table. -pub const BLOCKS_EXPECTED_EXECUTORS: &[&str] = &["blockly-abi"]; - -/// The distinct subject classids this table binds. A registering consumer must -/// activate exactly this set — the substrate's `0x1701`/`0x1702` are -/// deliberately absent. -pub const BLOCKS_SUBJECT_CLASSIDS: &[u16] = &[BLOCK_PALETTE]; - -#[cfg(test)] -mod tests { - use super::*; - use crate::capability_registry::{HotplugDrift, resolve_hotplug}; - - #[test] - fn the_activated_concept_resolves_only_because_this_feature_is_on() { - // The whole point of the mechanism: 0x1717 is NOT in class_ids::ALL — - // it resolves through the feature-activated rows. Assert BOTH halves, - // or "it resolved" proves nothing about where it resolved from. - assert!( - !crate::class_ids::ALL - .iter() - .any(|&(_, id)| id == BLOCK_PALETTE), - "0x1717 must NEVER enter the globally-mirrored codebook" - ); - assert!( - ACTIVATED_CONCEPTS - .iter() - .any(|&(_, id)| id == BLOCK_PALETTE) - ); - - let (concepts, capabilities) = - resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, BLOCKS_ACTION_NAMES) - .expect("the blocks domain must activate under its own feature"); - assert_eq!(capabilities.len(), BLOCKS_ACTION_NAMES.len()); - assert_eq!( - concepts.iter().map(|&(n, _)| n).collect::>(), - vec!["block_palette"] - ); - } - - #[test] - fn the_palette_never_claims_the_substrates_node_shapes() { - // The ownership line, asserted rather than trusted. 0x1701/0x1702 are - // ogar-loco's; a plug of either must NOT resolve through this table. - assert!(!BLOCKS_SUBJECT_CLASSIDS.contains(&0x1701)); - assert!(!BLOCKS_SUBJECT_CLASSIDS.contains(&0x1702)); - assert!( - !ACTIVATED_CONCEPTS - .iter() - .any(|&(_, id)| id == 0x1701 || id == 0x1702) - ); - for shape in [0x1701u16, 0x1702] { - assert!( - matches!( - resolve_hotplug("blockly-abi", &[shape], BLOCKS_ACTION_NAMES), - Err(HotplugDrift::UnknownClassid(id)) if id == shape - ), - "plugging {shape:#06x} must not resolve — it is the substrate's" - ); - } - } - - #[test] - fn the_port_rejects_a_wrong_consumer_and_coverage_gaps_both_ways() { - // Can-fire halves, so the activation above is not "yes to everything". - assert!(matches!( - resolve_hotplug( - "some-other-crate", - BLOCKS_SUBJECT_CLASSIDS, - BLOCKS_ACTION_NAMES - ), - Err(HotplugDrift::UnexpectedConsumer(_)) - )); - assert!(matches!( - resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, &["lower_script"]), - Err(HotplugDrift::Uncovered(_)) - )); - let mut over = BLOCKS_ACTION_NAMES.to_vec(); - over.push("compile_to_wasm"); - assert!(matches!( - resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, &over), - Err(HotplugDrift::Undeclared(_)) - )); - } - - #[test] - fn the_fingerprint_matches_the_table_in_order() { - let defs = blocks_actions(); - assert_eq!(defs.len(), BLOCKS_ACTION_NAMES.len()); - for (def, name) in defs.iter().zip(BLOCKS_ACTION_NAMES) { - assert_eq!(&def.predicate, name, "fingerprint drifted from the table"); - } - } -} diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index bd15bb2..44df86b 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -169,6 +169,23 @@ pub struct DomainTable { /// Every registered authoritative domain table. Append-only: a new domain /// (thinking styles, …) adds one entry and is immediately resolvable. +/// +/// **Every table here is unconditional, and that is the design — not an +/// oversight.** Healthcare is the worked precedent: medcare-rs is a *private* +/// consumer, yet its concepts (`0x09XX`) are minted in the canon codebook and +/// `healthcare_actions` registers here in every build. Activation is Cargo +/// presence of the consumer (`lance-graph-ogar`'s rule), never a feature that +/// switches the codebook on — the consumer pulls addresses it does not own, +/// which is exactly what makes them shared: RBAC and ontology both key on the +/// canon half. +/// +/// The shape that does NOT belong here is a **palette** — which bytes above +/// `ogar-loco`'s shared floor mean what. That is a reading of a stored body, +/// private to one frontend, and it plugs through +/// `ogar_loco::registry::VocabularyRegistry` from the consumer's own crate. +/// A `blocks` table briefly lived here behind a Cargo feature; it was a second +/// activation mechanism for a case the registry already served, and it made +/// the substrate name a consumer crate in another repository. Removed. pub fn domain_tables() -> Vec { vec![ DomainTable { @@ -186,14 +203,6 @@ pub fn domain_tables() -> Vec { expected_executors: crate::healthcare_actions::HEALTHCARE_EXPECTED_EXECUTORS, entries: healthcare_entries, }, - // Feature-activated, not canon: present only when a block editor is - // actually in the build graph. Every table above ships in every build. - #[cfg(feature = "blocks")] - DomainTable { - domain: "blocks", - expected_executors: crate::blocks_actions::BLOCKS_EXPECTED_EXECUTORS, - entries: blocks_entries, - }, ] } @@ -237,14 +246,7 @@ fn derive_action_rows(actions: &[crate::ActionDef]) -> Vec { .iter() .map(|def| { let concept = def.object_class.rsplit('/').next().unwrap_or_default(); - // Canon first, then feature-activated rows — the SAME order as - // `resolve_concept_row`. Both sides of the join must consult the - // same set, or an activated concept resolves as a plugged classid - // (so the plug is accepted) while its capabilities silently land - // in the slag ledger, and the port answers `NoCapabilitiesFor` for - // a table that is right there. Measured, not hypothetical: that is - // exactly what this returned before the `or_else` arm. - match crate::canonical_concept_id(concept).or_else(|| activated_concept_id(concept)) { + match crate::canonical_concept_id(concept) { Some(id) => ActionRow::Resolved(def.predicate.clone(), id), None => ActionRow::Unminted(UnmintedRow { capability: def.predicate.clone(), @@ -341,164 +343,57 @@ fn geo_entries() -> Vec<(String, u16)> { entries_from_actions(&crate::geo_actions::geo_actions()) } -/// Blocks domain rows ([`crate::blocks_actions`], the blockly-abi table) — -/// compiled ONLY under the `blocks` feature. -#[cfg(feature = "blocks")] -fn blocks_entries() -> Vec<(String, u16)> { - entries_from_actions(&crate::blocks_actions::blocks_actions()) -} - -/// Resolve one plugged classid to its `(concept, id)` row. -/// -/// **The global codebook first, then whatever a feature ACTIVATED.** This is -/// the "codebook triggered by plug-and-play" seam: `class_ids::ALL` is the -/// canon every build carries (and the surface mirrored into lance-graph under -/// a compile-time count fuse), while a particular frontend's codebook rides -/// its own Cargo feature and is consulted only when that feature is compiled -/// in — i.e. only when the consumer that owns it is actually in the build -/// graph. +/// Resolve one plugged classid to its `(concept, id)` row — from +/// `class_ids::ALL`, the canon every build carries and the exact surface +/// mirrored into `lance-graph-contract` under a compile-time count fuse. /// -/// Auto-activation is **Cargo presence, not runtime detection** — the same -/// rule `lance-graph-ogar` already documents. With the feature off these rows -/// do not exist and a plug reports `UnknownClassid`, which is the honest -/// answer: that vocabulary is not in this binary. -/// -/// Order is deliberate: **canon wins.** A feature can ADD a concept the global -/// codebook does not carry; it can never SHADOW one it does. +/// **There is one codebook, and a feature never adds to it.** An earlier +/// arc grew a second, feature-activated concept set here so a frontend could +/// hot-plug a palette id; it was the wrong seam, and healthcare is the proof. +/// See [`domain_tables`] and the module docs for the two shapes that replace +/// it. fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { - if let Some(&(name, cid)) = crate::class_ids::ALL.iter().find(|&&(_, cid)| cid == id) { - return Some((name, cid)); - } - activated_concepts() + crate::class_ids::ALL .iter() .find(|&&(_, cid)| cid == id) .copied() } -/// Every concept row a compiled-in feature activates, beyond `class_ids::ALL`. +/// The canon carries no palette rows — always compiled, in every feature +/// configuration. /// -/// Empty in a default build — which is what keeps a consumer that never -/// touches a block editor free of a block editor's codebook. -/// The id of an activated concept BY NAME — the `derive_action_rows` half of -/// the same lookup [`resolve_concept_row`] does by id. Both halves must -/// consult the same set (see the call site). -fn activated_concept_id(concept: &str) -> Option { - activated_concepts() - .iter() - .find(|&&(name, _)| name == concept) - .map(|&(_, id)| id) -} - -/// Guard on the guard — **always compiled**, in every feature configuration. -/// -/// [`default_build_carries_no_activated_rows`] is `cfg(not(feature = -/// "blocks"))`, which makes it a gate that can DISAPPEAR: put `blocks` into -/// `[features] default` and the module compiles out, the job still exits 0, -/// and the regression the gate exists to catch becomes invisible (codex P2 on -/// #259 — correct, and the hole I had named in a check-in note without -/// actually closing). -/// -/// CI defends this with `--no-default-features`. That is necessary but not -/// sufficient: it only forces the build a human remembered to write that way. -/// This test is the part that cannot be forgotten or cfg'd away — it reads -/// the crate's OWN manifest at compile time and fails if any activating -/// feature has been added to the default set, in EVERY configuration, -/// including the one where the OFF module is absent. +/// `0x17` is `ogar-loco`'s domain: `0x1701`/`0x1702` are its node shapes, +/// `0x1717`+ are consumer palette slots that live in the CONSUMER (blockly-rs +/// declares its own and plugs it into `ogar_loco::registry::VocabularyRegistry` +/// at boot). A palette row reaching `class_ids::ALL` would put a frontend's +/// private reading into the globally-mirrored codebook and move the count the +/// lance-graph fuse pins — so catch it on THIS side first. #[cfg(test)] -mod the_off_gate_cannot_be_switched_off { - /// Every feature that activates codebook rows. A new activating feature - /// is added here in the same PR that introduces it. - const ACTIVATING: &[&str] = &["blocks"]; - +mod the_canon_carries_no_palette_rows { #[test] - fn no_activating_feature_is_in_the_default_set() { - // Compile-time read of this crate's own Cargo.toml — no runtime I/O, - // and no way for a feature flag to hide it. - let manifest = include_str!("../Cargo.toml"); - let default_line = manifest - .lines() - .map(str::trim) - .find(|l| l.starts_with("default")) - .expect("ogar-vocab must declare a `default` feature list"); - for feature in ACTIVATING { - assert!( - !default_line.contains(feature), - "`{feature}` is in the DEFAULT feature set ({default_line}). \ - Every build would then carry that codebook, and the OFF-half \ - gate would silently compile out. Activation must stay opt-in, \ - turned on by the consumer that owns it." - ); + fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { + assert_eq!(crate::class_ids::ALL.len(), 90); + for (_, id) in crate::class_ids::ALL { + assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } } -} - -/// The OFF half of the activation contract — compiled ONLY when no feature -/// activated anything. -/// -/// The positive half (`blocks_actions::tests`) can only run with the feature -/// ON, and a workspace build unifies it ON for every crate (`ogar-ro` -/// dev-deps `ogar-blockly`). So `cargo test --workspace` **cannot** exercise -/// the claim that a default build carries zero activated rows — the property -/// the whole design rests on. This module is that gate: it is -/// `cfg(not(...))`, so it vanishes the moment any activating feature is on, -/// and CI runs `cargo test -p ogar-vocab` (no `ogar-blockly` in the graph) to -/// reach it. -/// -/// Without it, "the codebook is triggered by plug-and-play" would be tested -/// only in the triggered direction — the vacuous shape of a guard nobody -/// watched stay silent. -/// -/// Verified to FAIL when it should: seeding one row into the `not(blocks)` -/// arm of [`activated_concepts`] turns the first test red. -#[cfg(all(test, not(feature = "blocks")))] -mod default_build_carries_no_activated_rows { - use super::{HotplugDrift, activated_concepts, resolve_hotplug}; #[test] - fn nothing_is_activated_and_a_frontend_classid_does_not_resolve() { - assert!( - activated_concepts().is_empty(), - "a default build must activate nothing" - ); - // 0x1717 is the Blocks palette. No block editor is in THIS build, so - // the honest answer is that the vocabulary is absent. + fn a_palette_classid_does_not_resolve_as_a_hot_plug() { + // The honest answer for a palette: this is not a capability-authority + // concept at all, in any build. Vocabulary routing is a different + // seam (`VocabularyRegistry`), keyed by the consumer's own slot. assert!(matches!( - resolve_hotplug("blockly-abi", &[0x1717], &[]), - Err(HotplugDrift::UnknownClassid(0x1717)) + super::resolve_hotplug("blockly-abi", &[0x1717], &[]), + Err(super::HotplugDrift::UnknownClassid(0x1717)) )); - // …and the domain still routes on the reserved byte alone, which is + // …while the domain still routes on the reserved byte alone, which is // what lets a consumer branch on 0x17XX with no concept minted. assert_eq!( crate::canonical_concept_domain(0x1717), crate::ConceptDomain::Blocks ); } - - #[test] - fn the_canon_is_untouched_by_the_activation_seam() { - // The count mirrored into lance-graph under the compile-time fuse. If - // an activated row ever leaks into `class_ids::ALL`, this moves and - // the lance-graph mirror breaks — catch it on THIS side first. - assert_eq!(crate::class_ids::ALL.len(), 90); - for (_, id) in crate::class_ids::ALL { - assert_ne!( - *id >> 8, - 0x17, - "a 0x17XX row reached the globally-mirrored codebook" - ); - } - } -} - -fn activated_concepts() -> &'static [(&'static str, u16)] { - #[cfg(feature = "blocks")] - { - crate::blocks_actions::ACTIVATED_CONCEPTS - } - #[cfg(not(feature = "blocks"))] - { - &[] - } } /// Healthcare domain rows ([`crate::healthcare_actions`], the medcare-rs diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 1ef4dbc..d83b2c9 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -37,10 +37,6 @@ pub mod recipe; /// Healthcare capability surface — the medcare-rs authoritative action /// table (parity-plan P3; hand-authored, harvest-informed — see the /// module doc for why the mechanical lift was falsified by the corpus). -/// The Blocks capability surface — **feature-activated**, never shared canon. -/// Compiled only under `--features blocks`, which `ogar-blockly` turns on. -#[cfg(feature = "blocks")] -pub mod blocks_actions; /// The tesseract-rs OCR capability surface — a hand-authored, non-`lift_*` /// [`ActionDef`] table (tesseract-rs has no source AST to extract from; see /// the module doc for why this is the authoritative action table rather