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
3 changes: 3 additions & 0 deletions crates/openspine-kernel/src/action_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use openspine_schemas::selection::SelectionTokenType;
mod action_catalog_contracts;
#[path = "action_catalog_data.rs"]
mod action_catalog_data;
#[path = "action_catalog_tool_descriptors.rs"]
mod action_catalog_tool_descriptors;
/// The canonical declaration of what a standing rule for an action must bind,
/// re-exported so activation-time scope-binding enforcement reads the same
/// descriptor table the catalog itself is assembled from.
Expand Down Expand Up @@ -141,6 +143,7 @@ pub fn canonical_catalog() -> ActionCatalog {
SelectionTokenType::email_thread_selection(),
)])
.with_egress_declarations(decls)
.with_tool_descriptors(action_catalog_tool_descriptors::tool_descriptors())
.with_delegation_descriptors(action_catalog_data::delegation_descriptors())
.with_implementation_descriptors(action_catalog_data::implementation_descriptors())
.with_effect_paths([
Expand Down
33 changes: 33 additions & 0 deletions crates/openspine-kernel/src/action_catalog_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,39 @@ fn handler_registry_requires_explicit_classification() {
}
}

#[test]
fn every_dispatchable_action_has_a_tool_descriptor() {
// Fail-closed completeness (spec #209 D3): a granted, dispatchable action
// that lacks a tool descriptor is a capability gap a human must
// consciously accept, so it fails here rather than silently omitting.
// Mirrors `handler_registry_requires_explicit_classification`.
let catalog = canonical_catalog();
let registry = crate::api::handler_registry::ActionHandlerRegistry::default_registrations();
for action in registry.registered_action_ids() {
let descriptor = catalog
.tool_descriptor_for(&action)
.unwrap_or_else(|| panic!("dispatchable action {action} lacks a tool descriptor"));
// The presentation flag must stay honest against the catalog's own
// selection-token axis: a descriptor may not claim a different
// token requirement than the action actually carries.
assert_eq!(
descriptor.selection_token_required,
catalog.requires_selection_token(&action).is_some(),
"tool descriptor for {action} has a selection_token_required flag \
that disagrees with the catalog"
);
}
// Cardinality, not just membership: pins the dispatchable descriptor count
// so a deliberately-removed descriptor (or a stray extra one) fails, not
// just a lookup miss. 15 is the current dispatchable set; changing it is a
// conscious edit here.
assert_eq!(
catalog.tool_descriptor_count(),
15,
"expected exactly 15 dispatchable tool descriptors"
);
}

#[test]
fn worker_actions_declare_no_egress_and_no_output_channel() {
// `worker.commission` / `worker.report_result` / `worker.failed` must
Expand Down
Loading