diff --git a/Cargo.lock b/Cargo.lock index fef7ffd..2fdd0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,7 +137,6 @@ dependencies = [ name = "blockly-abi" version = "0.1.0" dependencies = [ - "ogar-blockly", "ogar-loco", ] @@ -146,7 +145,7 @@ name = "blockly-shim" version = "0.1.0" dependencies = [ "blockly-abi", - "ogar-blockly", + "ogar-loco", "serde_json", ] @@ -158,7 +157,7 @@ dependencies = [ "axum", "blockly-abi", "blockly-shim", - "ogar-blockly", + "ogar-loco", "serde", "serde_json", "tokio", @@ -389,22 +388,10 @@ dependencies = [ "autocfg", ] -[[package]] -name = "ogar-blockly" -version = "0.1.0" -dependencies = [ - "ogar-loco", - "ogar-vocab", -] - [[package]] name = "ogar-loco" version = "0.1.0" -[[package]] -name = "ogar-vocab" -version = "0.1.0" - [[package]] name = "once_cell" version = "1.21.4" diff --git a/Cargo.toml b/Cargo.toml index 81edd7a..1903bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ rust-version = "1.95" # #255). Deliberately the only frontend dependency: this half of the arc # stays permissive and never links the GPL `rash` JIT (that boundary # lives in scratch-rs). See OGAR docs/BLOCK-EDITOR-PLAN.md. -ogar-blockly = { path = "../OGAR/crates/ogar-blockly" } # The vocabulary-agnostic call ABI the palette rides on, and the owner of # the node shapes themselves (`LocoConcept::{FunctionBody, Inventory}`). # Post-flip, blockly-abi consumes these types directly; its own local diff --git a/Dockerfile b/Dockerfile index ca10c5a..d913cb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,20 +5,61 @@ # editor itself loads in the *browser* from a version-pinned CDN build, per # the arc's standing rule (JS drags puzzle pieces; Rust owns semantics). # -# The build fetches the `ogar-blockly` git dependency from the public OGAR -# repository; no token is required. +# ── Why the sibling clone ─────────────────────────────────────────────── +# +# This repo depends on exactly ONE external crate: `ogar-loco`, the +# substrate (call encoding, lane carvings, node layout, shared core). It is +# a PATH dependency to a sibling checkout, because a git dependency writes a +# rev pin into Cargo.lock and pins are forbidden here. +# +# Railway's build context is this repo alone, so `../OGAR` does not exist +# and `cargo` fails at manifest load — before compiling anything. The +# builder therefore clones the sibling into the layout the manifest already +# expects. OGAR is public: no token, no build secret. +# +# `--depth 1` on the default branch is deliberate — cloning a moving branch +# rather than a fixed rev is what keeps this pin-free. The trade is real and +# accepted: an image is reproducible only against OGAR's HEAD at build time. +# A deploy that needs a fixed substrate rebuilds from a fixed checkout; it +# does not get there by adding a rev here. +# +# ── Cache-busting the clone (learned the hard way) ───────────────────── +# +# "OGAR's HEAD at build time" only holds if the clone actually reruns. A +# `RUN git clone ` line is verbatim-identical on every build, so +# Docker's (and Railway's) layer cache treats it as immutable and reuses +# whatever checkout it first produced — silently, forever, with no signal +# that it happened. A build that ran before an OGAR crate was renamed or +# removed then keeps failing (or worse, keeps *succeeding* against a stale +# substrate) on every deploy after the fix has already landed upstream, and +# nothing in the Dockerfile text changed to explain why. +# +# The `ADD ` below is the standard bust: BuildKit re-fetches it on every +# build and conditionally invalidates the layer (and everything after it) the +# moment OGAR's `main` tip actually moves — a real signal instead of an +# accidental one. It costs one tiny JSON response, not a second clone. # # Railway: binds 0.0.0.0:$PORT — PORT is injected by the platform, and 8080 # below is only the local-run fallback, never a pin. FROM rust:1.95-bookworm AS builder -WORKDIR /src +WORKDIR /build + +# Cache-buster: re-fetched every build, invalidates the clone layer below +# exactly when OGAR's main tip has actually moved. See the note above. +ADD https://api.github.com/repos/AdaWorldAPI/OGAR/commits/main /tmp/ogar-head.json + +# The sibling first, so an OGAR-only change does not invalidate the layer +# holding this repo's sources. +RUN git clone --depth 1 https://github.com/AdaWorldAPI/OGAR.git /build/OGAR + +WORKDIR /build/blockly-rs COPY . . RUN cargo build --release -p blockly-web FROM debian:bookworm-slim RUN useradd --system --no-create-home blockly -COPY --from=builder /src/target/release/blockly-web /usr/local/bin/blockly-web +COPY --from=builder /build/blockly-rs/target/release/blockly-web /usr/local/bin/blockly-web USER blockly ENV PORT=8080 EXPOSE 8080 diff --git a/crates/blockly-abi/Cargo.toml b/crates/blockly-abi/Cargo.toml index 082b4f5..e99bb61 100644 --- a/crates/blockly-abi/Cargo.toml +++ b/crates/blockly-abi/Cargo.toml @@ -9,5 +9,4 @@ rust-version.workspace = true description = "The cast between a Blockly workspace block record and the V3 ABI-shaped SoA function body — semantics only, presentation held separately, zero serialization. Post-order lowering (a block's nested inputs are its operands) and the inverse raise." [dependencies] -ogar-blockly = { workspace = true } ogar-loco = { workspace = true } diff --git a/crates/blockly-abi/src/codebook.rs b/crates/blockly-abi/src/codebook.rs index c023615..b7ca45b 100644 --- a/crates/blockly-abi/src/codebook.rs +++ b/crates/blockly-abi/src/codebook.rs @@ -44,7 +44,7 @@ //! `PROC_ARG` has no dedicated block: argument reads go through an ordinary //! `variables_get` bound to the argument's variable model. -use ogar_blockly::FnIndex; +use ogar_loco::FnIndex; /// What a block's dropdown code does during resolution. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -679,7 +679,7 @@ mod tests { // no Blockly-core counterpart. for (ty, _, byte) in pinned { assert!( - *byte < ogar_blockly::DEVICE_FAMILY_FLOOR, + *byte < crate::palette::DEVICE_FAMILY_FLOOR, "{ty} reached the reserved device family" ); } @@ -687,8 +687,8 @@ mod tests { // ABOVE every pin (or the loop above would be impossible) and BELOW // the top of the byte range (or "below the floor" would exclude // nothing and the loop would be free). - const { assert!(ogar_blockly::DEVICE_FAMILY_FLOOR < u8::MAX) }; + const { assert!(crate::palette::DEVICE_FAMILY_FLOOR < u8::MAX) }; let highest_pin = pinned.iter().map(|(_, _, b)| *b).max().unwrap(); - assert!(highest_pin < ogar_blockly::DEVICE_FAMILY_FLOOR); + assert!(highest_pin < crate::palette::DEVICE_FAMILY_FLOOR); } } diff --git a/crates/blockly-abi/src/klickweg.rs b/crates/blockly-abi/src/klickweg.rs index 65e8c34..8ec3f21 100644 --- a/crates/blockly-abi/src/klickweg.rs +++ b/crates/blockly-abi/src/klickweg.rs @@ -22,7 +22,7 @@ //! //! The call index is the one number that is already the program: it is where //! [`lower_script`](crate::lower_script) put that block's -//! [`Call`](ogar_blockly::Call) in the body, so +//! [`Call`](ogar_loco::Call) in the body, so //! `raise_calls(body)[ordinal]` IS the clicked block's function, by //! construction rather than by convention. That makes the address checkable //! against the ABI instead of merely consistent with itself — and it is @@ -43,7 +43,8 @@ //! this crate depends on `a2ui-server`: the block editor produces addresses, //! the desktop consumes them, and neither imports the other. -use ogar_blockly::{BlockConcept, LaneShape}; +use crate::palette; +use ogar_loco::LaneShape; use crate::{BlockRecord, codebook}; @@ -91,7 +92,7 @@ pub fn addresses(top: &BlockRecord, app_prefix: u16) -> Vec<(String, BlockAddres // The PALETTE concept (0x1717) — which vocabulary reads the call bytes. // Not the node's shape: that is ogar-loco's `LocoConcept::FunctionBody` // (0x1701), and a block editor does not own it (OGAR #255). - let class_id = BlockConcept::Palette.render_classid(app_prefix); + let class_id = palette::render_classid(app_prefix); let mut out = Vec::new(); walk_chain(top, class_id, &mut out); out @@ -193,8 +194,8 @@ mod tests { for (id, addr) in addresses(&script, APP) { let call = &calls[addr.ordinal as usize]; let expected = match id.as_str() { - "a" | "b" => ogar_blockly::FnIndex::NUMBER, - "root" => ogar_blockly::FnIndex::ADD, + "a" | "b" => ogar_loco::FnIndex::NUMBER, + "root" => ogar_loco::FnIndex::ADD, other => panic!("unexpected block {other}"), }; assert_eq!( diff --git a/crates/blockly-abi/src/lib.rs b/crates/blockly-abi/src/lib.rs index a8d3015..777f7d9 100644 --- a/crates/blockly-abi/src/lib.rs +++ b/crates/blockly-abi/src/lib.rs @@ -4,7 +4,7 @@ //! # What this is //! //! Not a parser, not a serializer, not a renderer. One thing: a Blockly block -//! record goes in, [`ogar_blockly::FunctionBody`] comes out, and back again. +//! record goes in, [`ogar_loco::FunctionBody`] comes out, and back again. //! `to_le_bytes` / `from_le_bytes` on the far side ARE the wire format — there //! is no JSON, no intermediate DTO, no serde anywhere in this crate. //! @@ -79,16 +79,20 @@ #![warn(missing_docs)] #![forbid(unsafe_code)] -use ogar_blockly::{BodyError, Call, FnIndex, FunctionBody, LaneShape}; +use ogar_loco::{BodyError, Call, FnIndex, FunctionBody, LaneShape}; pub mod codebook; pub mod klickweg; +pub mod palette; pub mod program; pub mod projection; pub mod registry; pub use codebook::{OpcodeMapping, resolve_opcode}; pub use klickweg::{BlockAddress, address_of}; +pub use palette::{ + BlocklyPalette, DEVICE_FAMILY_FLOOR, PALETTE_CONCEPT, plug_into, render_classid, +}; pub use program::lower_program; pub use projection::{ProjectionError, parse_text, render_text}; pub use registry::registry; @@ -110,15 +114,15 @@ pub use ogar_loco::{ /// [`Program::references_are_resolvable`] and [`branches_of`] require. /// /// Validation is a 256-byte sweep done once (lazily) per process; -/// `BlocklyVocabulary`'s conformance is additionally pinned by its own -/// crate's tests, so the `expect` here is a compile-time-adjacent fact, +/// [`BlocklyPalette`]'s conformance is additionally pinned by +/// [`palette`]'s own tests, so the `expect` here is a near-static fact, /// not a runtime hope. #[must_use] -pub fn checked_vocabulary() -> &'static CheckedVocabulary { - static CHECKED: std::sync::LazyLock> = +pub fn checked_vocabulary() -> &'static CheckedVocabulary { + static CHECKED: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - ogar_loco::vocabulary::conformance::validate(ogar_blockly::BlocklyVocabulary) - .expect("BlocklyVocabulary conforms; pinned by ogar-blockly's own tests") + ogar_loco::vocabulary::conformance::validate(crate::palette::BlocklyPalette) + .expect("BlocklyPalette conforms; pinned by palette::tests") }); &CHECKED } @@ -608,12 +612,12 @@ pub(crate) fn call_for( // The immediates, in the block's own field order. A Selector dropdown // chose WHICH function and is already spent; a ValueParam dropdown is an // ARGUMENT and becomes a byte, or the cast refuses. - let mut values = [0u8; ogar_blockly::MAX_VALUES_PER_CALL]; + let mut values = [0u8; ogar_loco::MAX_VALUES_PER_CALL]; let mut n = 0usize; let mut push = |b: u8| -> Result<(), CastError> { // Silently dropping the overflow would emit a call that looks complete // and is not — the same failure the mutator guard above exists to stop. - if n >= ogar_blockly::MAX_VALUES_PER_CALL { + if n >= ogar_loco::MAX_VALUES_PER_CALL { return Err(CastError::TooManyValues { ty: block.ty.clone(), offered: block.fields.len(), @@ -1169,7 +1173,7 @@ mod tests { assert_eq!(pi.len(), 1); // Same function — a ValueParam must NOT fan the palette out… assert_eq!(pi[0].function, e[0].function); - assert_eq!(pi[0].function, ogar_blockly::FnIndex::CONSTANT); + assert_eq!(pi[0].function, ogar_loco::FnIndex::CONSTANT); // …and the two programs stay distinguishable anyway, which is the // whole point. Drop the code and these become equal. assert_ne!(pi[0].values[0], e[0].values[0]); @@ -1190,7 +1194,7 @@ mod tests { let lt = BlockRecord::leaf("logic_compare", "c").with_field("OP", FieldValue::Code("LT".into())); let calls = raise_calls(&lower_script(LaneShape::Pairs, <).unwrap()); - assert_eq!(calls[0].function, ogar_blockly::FnIndex::LT); + assert_eq!(calls[0].function, ogar_loco::FnIndex::LT); assert_eq!(calls[0].values[0], 0); } diff --git a/crates/blockly-abi/src/palette.rs b/crates/blockly-abi/src/palette.rs new file mode 100644 index 0000000..70e0480 --- /dev/null +++ b/crates/blockly-abi/src/palette.rs @@ -0,0 +1,163 @@ +//! This repo's palette — the vocabulary that reads a stored body's call bytes. +//! +//! Declared HERE because this repo *is* the Blockly consumer. The substrate +//! (`ogar-loco`) supplies the call encoding, the lane carvings, the node +//! layout and the shared computational core; what a *frontend* adds is a +//! palette — which bytes above the shared floor mean what — and that is the +//! consumer's own business, not the substrate's. +//! +//! # Why this is not an OGAR crate any more +//! +//! It was: OGAR hosted a Blockly crate supplying exactly a concept id, an +//! empty [`Vocabulary`] impl and a plug helper, and this repo depended back on +//! OGAR to obtain its own palette. Worse, `ogar-vocab`'s Blocks table named +//! this repo's executor crate in a `const` — the substrate knowing its +//! consumer by name, in another repository. +//! +//! [`VocabularyRegistry`] exists so that inversion is unnecessary. A consumer +//! declares its own slot in the reserved consumer range and plugs it at boot; +//! the substrate never learns that Blockly exists. The registry's +//! [`ConceptTaken`](RegistryError::ConceptTaken) is what keeps two frontends +//! from claiming one slot — enforcement at the plug, not a name in a table. +//! +//! Consequence for the build: this repo depends on `ogar-loco` alone. +//! +//! # The slot +//! +//! `0x17` is the substrate's domain. `0x1701`/`0x1702` are `ogar-loco`'s node +//! shapes (function body, inventory) and `0x1703`–`0x1716` is its reserved +//! headroom; consumers are seated from `0x1717` up, so the substrate keeps +//! contiguous room beneath them. This palette takes the first consumer slot. +//! +//! A classid selects **which vocabulary reads the call bytes**, never the +//! node's shape — a Blockly body and an elixir-shaped template body are the +//! same `0x1701` shape with different palettes. + +use ogar_loco::registry::{RegistryError, VocabularyRegistry}; +use ogar_loco::{FnIndex, Vocabulary}; + +/// This palette's content concept — the first consumer slot in the +/// substrate's `0x17` domain (see the module docs for the seating rule). +/// +/// Declared here and nowhere else. Uniqueness is enforced at plug time by +/// [`VocabularyRegistry`], not by a central table that would have to know +/// every frontend's name. +pub const PALETTE_CONCEPT: u16 = 0x1717; + +/// First byte reserved for **device-specific** families — the sprite/stage +/// vocabulary (motion, looks, sound, events, sensing) a Scratch-style +/// frontend has and a general block editor does not. +/// +/// This is this palette's reading of the substrate's domain floor: below it +/// is the shared computational core, whose tables live once in `ogar-loco`; +/// at or above it is this palette's own range. Currently **reserved, not +/// allocated** — 108 device opcodes were measured in the Apache-2.0 +/// `scratch-blocks` definitions and mint when a consumer needs them. +pub const DEVICE_FAMILY_FLOOR: u8 = ogar_loco::DOMAIN_FLOOR; + +/// The full V3 render classid under an app prefix — canon-high +/// `(concept << 16) | app_prefix`. The concept half is what +/// [`VocabularyRegistry::resolve_classid`] routes on; the app prefix chooses +/// a render skin and never participates in vocabulary routing. +#[must_use] +pub const fn render_classid(app_prefix: u16) -> u32 { + ((PALETTE_CONCEPT as u32) << 16) | (app_prefix as u32) +} + +/// The Blockly/Scratch palette as an `ogar-loco` [`Vocabulary`]. +/// +/// Every operation this palette has *allocated* sits below the floor — in the +/// shared computational core, whose tables live in the substrate — so the +/// domain hooks answer nothing yet. That is honest rather than lazy: the +/// device families above [`DEVICE_FAMILY_FLOOR`] are reserved, not allocated, +/// and when they mint their arity tables land here (and only here — the +/// substrate never learns device vocabulary). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct BlocklyPalette; + +impl Vocabulary for BlocklyPalette { + 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 this palette into a registry under [`PALETTE_CONCEPT`]. +/// +/// # Errors +/// +/// [`RegistryError::ConceptTaken`] if something already claimed the slot — +/// refused loudly rather than silently overwritten. +pub fn plug_into(registry: &mut VocabularyRegistry) -> Result<(), RegistryError> { + let checked = ogar_loco::vocabulary::conformance::validate(BlocklyPalette) + .expect("BlocklyPalette conforms; pinned by this module's tests"); + registry.plug(PALETTE_CONCEPT, &checked) +} + +#[cfg(test)] +mod tests { + use super::*; + use ogar_loco::vocabulary::{conformance, shared_core}; + + #[test] + fn the_palette_conforms_to_the_sharing_discipline() { + // The mechanical gate every vocabulary must pass: 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(&BlocklyPalette), Ok(())); + + // Spot-check the routing this palette relies on: control flow and + // expressions answer from the shared core THROUGH the vocabulary. + let v = BlocklyPalette; + 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 the_slot_sits_above_the_substrates_reserved_range() { + // The seating rule, asserted rather than trusted: consumers start at + // 0x1717 so the substrate keeps 0x1701-0x1716 contiguous beneath. + assert_eq!(PALETTE_CONCEPT, 0x1717); + assert!(PALETTE_CONCEPT > ogar_loco::LocoConcept::Inventory.concept_id()); + assert!(PALETTE_CONCEPT > ogar_loco::LocoConcept::FunctionBody.concept_id()); + // The domain byte is the substrate's, shared by both. + assert_eq!(PALETTE_CONCEPT >> 8, 0x17); + } + + #[test] + fn the_render_classid_keeps_the_concept_in_the_canon_half() { + let id = render_classid(0x1000); + assert_eq!(id, 0x1717_1000); + assert_eq!((id >> 16) as u16, PALETTE_CONCEPT); + assert_eq!((id & 0xFFFF) as u16, 0x1000); + } + + #[test] + fn the_shared_core_and_this_palette_partition_the_byte_space() { + // Can-fire AND can-stay-silent on the same predicate: a classifier + // answering the same way for everything carries no information. + 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()); + + // And the core's own tables cover the control range this palette + // lowers through — anchored so a substrate regression is caught here. + assert_eq!(shared_core::stack_arity(FnIndex::REPEAT), Some(1)); + assert_eq!(shared_core::body_refs(FnIndex::REPEAT), 1); + } +} diff --git a/crates/blockly-abi/src/program.rs b/crates/blockly-abi/src/program.rs index 67544b4..b1e73ad 100644 --- a/crates/blockly-abi/src/program.rs +++ b/crates/blockly-abi/src/program.rs @@ -35,7 +35,7 @@ //! an unminted operator decision. So a `Program` carries bodies and the caller //! supplies keys. -use ogar_blockly::FunctionBody; +use ogar_loco::FunctionBody; use ogar_loco::Program; use ogar_loco::vocabulary::shared_core; @@ -58,10 +58,7 @@ use crate::{BlockRecord, CastError, call_for}; /// As [`lower_script`](crate::lower_script), plus /// [`CastError::ShapeTooNarrow`] when a call's body references cannot fit the /// shape's immediate width, and [`CastError::TooManyFunctions`] past 255. -pub fn lower_program( - shape: ogar_blockly::LaneShape, - top: &BlockRecord, -) -> Result { +pub fn lower_program(shape: ogar_loco::LaneShape, top: &BlockRecord) -> Result { // The entry is reserved before the walk so that bodies discovered inside it // get indices 1.., and the entry keeps 0 no matter what order they appear. let mut functions: Vec> = vec![None]; @@ -77,7 +74,7 @@ pub fn lower_program( /// Walk a statement chain into one body, appending any referenced bodies. fn lower_chain_into( - shape: ogar_blockly::LaneShape, + shape: ogar_loco::LaneShape, block: &BlockRecord, functions: &mut Vec>, ) -> Result { @@ -94,7 +91,7 @@ fn lower_chain_into( /// with its statement inputs lowered into separate functions first, so their /// indices exist by the time the call that names them is built. fn lower_block_into( - shape: ogar_blockly::LaneShape, + shape: ogar_loco::LaneShape, block: &BlockRecord, body: &mut FunctionBody, functions: &mut Vec>, @@ -154,7 +151,7 @@ mod tests { FieldValue, FunctionNode, branches_of, checked_vocabulary, lower_script, raise_calls, render_text, }; - use ogar_blockly::{Call, FnIndex, LaneShape}; + use ogar_loco::{Call, FnIndex, LaneShape}; const S: LaneShape = LaneShape::Pairs; diff --git a/crates/blockly-abi/src/projection.rs b/crates/blockly-abi/src/projection.rs index 47bee65..7e03b3c 100644 --- a/crates/blockly-abi/src/projection.rs +++ b/crates/blockly-abi/src/projection.rs @@ -38,7 +38,7 @@ //! platitude: parsing is many-to-one, so the round-trip that matters is //! `body → text → body`, never `text → body → text`. -use ogar_blockly::{Call, FnIndex, FunctionBody, LaneShape}; +use ogar_loco::{Call, FnIndex, FunctionBody, LaneShape}; use crate::raise_calls; diff --git a/crates/blockly-abi/src/registry.rs b/crates/blockly-abi/src/registry.rs index 23ffc42..006f625 100644 --- a/crates/blockly-abi/src/registry.rs +++ b/crates/blockly-abi/src/registry.rs @@ -54,7 +54,7 @@ use ogar_loco::registry::{RegistryError, VocabularyRegistry}; /// content concept — refused loudly rather than silently overwritten. pub fn registry() -> Result { let mut registry = VocabularyRegistry::new(); - ogar_blockly::plug_into(&mut registry)?; + crate::palette::plug_into(&mut registry)?; Ok(registry) } @@ -69,7 +69,7 @@ mod tests { // The render classid a stored node actually carries — canon-high // concept over an app prefix — resolves to this palette's table. - let stored = ogar_blockly::BlockConcept::Palette.render_classid(0x1000); + let stored = crate::palette::render_classid(0x1000); let table = registry .resolve_classid(stored) .expect("a stored Blockly node resolves to the plugged palette"); @@ -96,7 +96,7 @@ mod tests { // registry must bang rather than let the second silently win. let mut registry = registry().unwrap(); assert!(matches!( - ogar_blockly::plug_into(&mut registry), + crate::palette::plug_into(&mut registry), Err(RegistryError::ConceptTaken { .. }) )); } diff --git a/crates/blockly-shim/Cargo.toml b/crates/blockly-shim/Cargo.toml index eb642ae..6e83d6d 100644 --- a/crates/blockly-shim/Cargo.toml +++ b/crates/blockly-shim/Cargo.toml @@ -10,7 +10,10 @@ description = "The membrane: Blockly's own save JSON in, BlockRecord out. serde [dependencies] blockly-abi = { path = "../blockly-abi" } -ogar-blockly = { workspace = true } +# The substrate, direct: the call encoding, lane carvings and node layout. +# Reached straight rather than through a re-export, so no crate has to be +# imported for the sole purpose of forwarding another crate's types. +ogar-loco = { workspace = true } # The ONLY serde in this workspace, and the reason this crate exists as a # separate one. Charter T3: "serde/JSON/proto exist ONLY at a membrane, behind # a feature, never on the hot path." A membrane is a crate boundary here, so diff --git a/crates/blockly-shim/src/lib.rs b/crates/blockly-shim/src/lib.rs index 564ea48..f3d4af6 100644 --- a/crates/blockly-shim/src/lib.rs +++ b/crates/blockly-shim/src/lib.rs @@ -301,8 +301,8 @@ fn byte_or_wide(s: String) -> FieldValue { mod tests { use super::*; use blockly_abi::{lower_program, lower_script, raise_calls, render_text}; - use ogar_blockly::FnIndex; - use ogar_blockly::LaneShape; + use ogar_loco::FnIndex; + use ogar_loco::LaneShape; /// `5 + 3`, exactly as `Blockly.serialization.workspaces.save` writes it. const ADD_JSON: &str = r#"{ diff --git a/crates/blockly-web/Cargo.toml b/crates/blockly-web/Cargo.toml index 8115a40..167f194 100644 --- a/crates/blockly-web/Cargo.toml +++ b/crates/blockly-web/Cargo.toml @@ -10,8 +10,11 @@ description = "The showcase: real Blockly drags blocks in the browser; every cha [dependencies] blockly-abi = { path = "../blockly-abi" } +# The substrate, direct: the call encoding, lane carvings and node layout. +# Reached straight rather than through a re-export, so no crate has to be +# imported for the sole purpose of forwarding another crate's types. +ogar-loco = { workspace = true } blockly-shim = { path = "../blockly-shim" } -ogar-blockly = { workspace = true } askama = "0.12" axum = "0.7" diff --git a/crates/blockly-web/src/cast.rs b/crates/blockly-web/src/cast.rs index b6e0c39..f07ea42 100644 --- a/crates/blockly-web/src/cast.rs +++ b/crates/blockly-web/src/cast.rs @@ -21,12 +21,12 @@ use blockly_abi::{FunctionNode, Program}; use blockly_abi::{lower_program, parse_text, raise_calls, render_text}; use blockly_shim::from_workspace_json; -use ogar_blockly::{FunctionBody, LaneShape}; +use ogar_loco::{FunctionBody, LaneShape}; use serde::Serialize; /// The demo key the entry node is stored under. /// -/// The classid half is real (`BlockConcept::Palette` = `0x1717`, canon-high +/// The classid half is real (`palette::PALETTE_CONCEPT` = `0x1717`, canon-high /// over an app prefix — the concept naming WHICH vocabulary reads the call /// bytes, not the node's shape, which is `ogar-loco`'s `0x1701`). The prefix /// `0xFF00` is a PLACEHOLDER — the real `blockly-rs` prefix is the unminted