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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/ogar-blockly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ serde = ["dep:serde", "ogar-vocab/serde", "ogar-loco/serde"]

[dependencies]
ogar-loco = { path = "../ogar-loco" }
ogar-vocab = { path = "../ogar-vocab" }
# `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 }
16 changes: 13 additions & 3 deletions crates/ogar-blockly/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,22 @@ impl BlockConcept {

/// This concept's canonical id inside the `0x17XX` Blocks domain.
///
/// Authoritative HERE; `ogar_vocab`'s shared CODEBOOK deliberately carries
/// zero `0x17XX` rows (plug-and-play, mirroring `ogar_obo::Namespace`).
/// **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 => 0x1717,
BlockConcept::Palette => ogar_vocab::blocks_actions::BLOCK_PALETTE,
}
}

Expand Down
10 changes: 10 additions & 0 deletions crates/ogar-vocab/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ 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 }
Expand Down
208 changes: 208 additions & 0 deletions crates/ogar-vocab/src/blocks_actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! 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<ActionDef> {
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<_>>(),
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");
}
}
}
80 changes: 77 additions & 3 deletions crates/ogar-vocab/src/capability_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ pub fn domain_tables() -> Vec<DomainTable> {
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,
},
]
}

Expand Down Expand Up @@ -229,7 +237,14 @@ fn derive_action_rows(actions: &[crate::ActionDef]) -> Vec<ActionRow> {
.iter()
.map(|def| {
let concept = def.object_class.rsplit('/').next().unwrap_or_default();
match crate::canonical_concept_id(concept) {
// 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)) {
Some(id) => ActionRow::Resolved(def.predicate.clone(), id),
None => ActionRow::Unminted(UnmintedRow {
capability: def.predicate.clone(),
Expand Down Expand Up @@ -326,6 +341,65 @@ 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.
///
/// 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.
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()
.iter()
.find(|&&(_, cid)| cid == id)
.copied()
Comment on lines +372 to +375

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize activated IDs in verify_registration

When the blocks feature is enabled, resolve_hotplug now accepts 0x1717 through this activated-concept lookup, but the other public registration path, verify_registration, still checks every subject only against class_ids::ALL. Consequently, a CapabilityRegistration using BLOCKS_SUBJECT_CLASSIDS, the matching action names, and the expected blockly-abi consumer deterministically returns RegistrationDrift::Unminted(0x1717) even though this feature declares the ID as minted; the activated lookup needs to be shared with that verifier as well.

Useful? React with 👍 / 👎.

}

/// Every concept row a compiled-in feature activates, beyond `class_ids::ALL`.
///
/// 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<u16> {
activated_concepts()
.iter()
.find(|&&(name, _)| name == concept)
.map(|&(_, id)| id)
}

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
/// table — parity-plan P3), derived through the same generic
/// [`entries_from_actions`] path as OCR.
Expand Down Expand Up @@ -353,8 +427,8 @@ pub fn resolve_hotplug(
) -> Result<HotplugActivation, HotplugDrift> {
let mut concepts = Vec::new();
for &id in classids {
match crate::class_ids::ALL.iter().find(|&&(_, cid)| cid == id) {
Some(&(name, cid)) => concepts.push((name, cid)),
match resolve_concept_row(id) {
Some((name, cid)) => concepts.push((name, cid)),
None => return Err(HotplugDrift::UnknownClassid(id)),
}
}
Expand Down
10 changes: 7 additions & 3 deletions crates/ogar-vocab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ use serde::{Deserialize, Serialize};
/// `<port>::<path>(<shape>)` grammar (`E-GRAMMAR-IS-THE-RECIPE-SHAPE`).
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
Expand All @@ -42,9 +49,6 @@ pub mod recipe;
/// `render_tsv` / `render_hocr` / `render_searchable_pdf`, each targeting a
/// minted `0x08XX` [`class_ids`] concept.
pub mod capability_registry;
/// 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).
pub mod geo_actions;
pub mod healthcare_actions;
pub mod ocr_actions;
Expand Down
Loading