commit 445c2b27f9613df72d0e13c79221caf29c242530
parent 52eca30465aa7daf5f8e60c50a3fedff636b1282
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 18:05:25 +0000
contract: govern semantic validator inventory
- classify every contract validator by parser and governed input
- replace lexical Rust witnesses with structured AST validation
- distinguish public functions and methods in source authority
- refresh generated outbox manifests after validator changes
Diffstat:
10 files changed, 849 insertions(+), 76 deletions(-)
diff --git a/contracts/semantic_validator_inventory.toml b/contracts/semantic_validator_inventory.toml
@@ -0,0 +1,137 @@
+schema_version = 1
+
+[[validators]]
+id = "artifact_transactions"
+semantic = true
+parsers = ["governed_bytes", "json"]
+implementation_paths = ["tools/xtask/src/contract/artifact_bundle.rs"]
+governed_inputs = ["contracts", "crates"]
+byte_hash_scopes = ["generated_contract_artifacts"]
+
+[[validators]]
+id = "event_store_successors"
+semantic = true
+parsers = ["executable_vector", "governed_bytes", "json", "json_schema", "rust_ast", "sqlite_catalog", "toml"]
+implementation_paths = [
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "tools/xtask/src/contract/nip09_reconciliation.rs",
+]
+governed_inputs = [
+ "contracts/conformance/vectors/event_store",
+ "crates/event_store/Cargo.toml",
+ "crates/event_store/contracts",
+ "crates/event_store/migrations",
+ "crates/event_store/src",
+]
+byte_hash_scopes = ["canonical_vectors_and_mirrors", "generated_contract_artifacts", "sql_migrations"]
+
+[[validators]]
+id = "feature_support"
+semantic = true
+parsers = ["rust_ast", "toml"]
+implementation_paths = ["tools/xtask/src/contract/feature_support.rs"]
+governed_inputs = ["Cargo.toml", "crates/nostr/Cargo.toml", "crates/nostr/src/lib.rs"]
+
+[[validators]]
+id = "immutable_predecessors"
+semantic = false
+parsers = ["governed_bytes", "json", "json_schema"]
+implementation_paths = [
+ "tools/xtask/src/contract/blossom_publication_readiness.rs",
+ "tools/xtask/src/contract/phase1_publication_allowlist.rs",
+ "tools/xtask/src/contract/phase1_publication_artifact.rs",
+ "tools/xtask/src/contract/raw_source_rebuild.rs",
+ "tools/xtask/src/contract/source_maintenance.rs",
+]
+governed_inputs = [
+ "crates/blossom/contracts",
+ "crates/event_codec/contracts",
+ "crates/event_store/contracts",
+]
+byte_hash_scopes = ["immutable_predecessor_artifacts"]
+
+[[validators]]
+id = "operations_and_boundaries"
+semantic = true
+parsers = ["executable_vector", "governed_bytes", "json", "markdown_table", "rust_ast", "toml"]
+implementation_paths = [
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/admission_authority.rs",
+ "tools/xtask/src/contract/comment_authority.rs",
+ "tools/xtask/src/contract/deletion_authority.rs",
+]
+governed_inputs = [
+ "contracts/conformance",
+ "contracts/event_boundary_matrix.md",
+ "contracts/manifest.toml",
+ "contracts/operations.toml",
+ "contracts/replica.toml",
+ "crates/event/src",
+ "crates/event_codec/src",
+ "crates/replica_sync/src",
+]
+byte_hash_scopes = ["canonical_vectors_and_mirrors"]
+
+[[validators]]
+id = "outbox_successors"
+semantic = true
+parsers = ["executable_vector", "governed_bytes", "json", "json_schema", "rust_ast", "sqlite_catalog", "toml"]
+implementation_paths = [
+ "tools/xtask/src/contract/outbox_migration.rs",
+ "tools/xtask/src/contract/outbox_phase1_publication.rs",
+]
+governed_inputs = [
+ "contracts/conformance/vectors/outbox",
+ "contracts/outbox_feature_matrix.toml",
+ "crates/outbox/Cargo.toml",
+ "crates/outbox/contracts",
+ "crates/outbox/migrations",
+ "crates/outbox/src",
+]
+byte_hash_scopes = ["canonical_vectors_and_mirrors", "generated_contract_artifacts", "sql_migrations"]
+
+[[validators]]
+id = "phase1_media_successors"
+semantic = true
+parsers = ["executable_vector", "governed_bytes", "json", "json_schema", "rust_ast", "toml"]
+implementation_paths = [
+ "tools/xtask/src/contract/blossom_raster_decoder_security.rs",
+ "tools/xtask/src/contract/phase1_publication_media_readiness.rs",
+]
+governed_inputs = [
+ "contracts/conformance/vectors/blossom",
+ "contracts/conformance/vectors/publication",
+ "crates/blossom/contracts",
+ "crates/blossom/src",
+ "crates/event_codec/contracts",
+ "crates/event_codec/src",
+ "fuzz/corpus",
+ "fuzz/fuzz_targets",
+]
+byte_hash_scopes = ["canonical_vectors_and_mirrors", "generated_contract_artifacts", "raw_fuzz_seeds"]
+
+[[validators]]
+id = "registry_v7"
+semantic = true
+parsers = ["governed_bytes", "json", "json_schema"]
+implementation_paths = ["tools/xtask/src/contract/registry_v7.rs"]
+governed_inputs = ["contracts/event_store", "crates/event/src/contract/registry_v7.rs"]
+byte_hash_scopes = ["generated_contract_artifacts"]
+
+[[validators]]
+id = "release_closure"
+semantic = true
+parsers = ["cargo_metadata", "git_object", "governed_bytes", "json", "json_schema", "tar_archive", "toml"]
+implementation_paths = [
+ "tools/xtask/src/contract/release_package.rs",
+ "tools/xtask/src/contract/release_provenance.rs",
+]
+governed_inputs = ["Cargo.lock", "Cargo.toml", "contracts/releases", "crates"]
+byte_hash_scopes = ["package_archives", "release_provenance_artifacts"]
+
+[[validators]]
+id = "validator_inventory"
+semantic = true
+parsers = ["rust_ast", "toml"]
+implementation_paths = ["tools/xtask/src/contract/validator_inventory.rs"]
+governed_inputs = ["contracts/semantic_validator_inventory.toml", "tools/xtask/src/contract"]
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json
@@ -296,19 +296,19 @@
},
{
"file": {
- "byte_length": 46807,
+ "byte_length": 47759,
"hash_algorithm": "sha256_bytes_v1",
"path": "tools/xtask/src/contract/outbox_migration.rs",
- "sha256": "feecba3ecb9ac05891877ef1de1b1e17f85fd349dbcc5b0f4cd7a09d2d6e0dda"
+ "sha256": "914361e2e65f544722a305c7e18076ebcf602f8b72bde39a8a16e7714efb237f"
},
"role": "contract_governance"
},
{
"file": {
- "byte_length": 492083,
+ "byte_length": 506380,
"hash_algorithm": "sha256_bytes_v1",
"path": "tools/xtask/src/contract.rs",
- "sha256": "a71ba50241e9569642060cc555f370e4ef8c0a525ee98235ec079372b2848469"
+ "sha256": "a0bfb6155613050b7e19fa6b2f8af72528b5dc12f446cd9fec05319d3ce924a1"
},
"role": "contract_dispatch"
},
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256
@@ -1 +1 @@
-67186bb3671dd4a8a36a2ae078b9ade0436bfb9e704f42de5b5fde28f5cf3728
+264233c2333f787390e9d394e63a6818a8067998bab95473fcf5ec132feaaad4
diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.json b/crates/outbox/contracts/phase1_publication_v1.manifest.json
@@ -112,9 +112,9 @@
},
{
"file": {
- "byte_length": 492083,
+ "byte_length": 506380,
"path": "tools/xtask/src/contract.rs",
- "sha256": "a71ba50241e9569642060cc555f370e4ef8c0a525ee98235ec079372b2848469"
+ "sha256": "a0bfb6155613050b7e19fa6b2f8af72528b5dc12f446cd9fec05319d3ce924a1"
},
"role": "contract_dispatch"
},
diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 b/crates/outbox/contracts/phase1_publication_v1.manifest.sha256
@@ -1 +1 @@
-79e03855f5cf0d8e18049a604de4f121fe0f6af688b0c9b4c4a2e547ba83dcfa
+a53a72f000e9b7982a418e09063a51c1f39c30ed4bccd42a49e32f47cc063943
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -28,6 +28,7 @@ mod registry_v7;
mod release_package;
mod release_provenance;
mod source_maintenance;
+mod validator_inventory;
pub(crate) use blossom_raster_decoder_security::{
validate_blossom_raster_decoder_security_manifest,
@@ -77,6 +78,7 @@ use deletion_authority::{
DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE, DELETION_SUPPRESSION_VALID_IDS,
REQUIRED_DELETION_PUBLIC_TYPES,
};
+use quote::ToTokens;
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -88,6 +90,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> {
+ validator_inventory::validate_semantic_validator_inventory(workspace_root)?;
feature_support::validate_feature_support(workspace_root)?;
validate_event_contract_registry_v7_inventory(workspace_root)?;
validate_nip09_reconciliation_manifest(workspace_root)?;
@@ -2093,7 +2096,7 @@ const COMMENT_WITNESSES: [EventBoundarySourceWitness; 8] = [
},
EventBoundarySourceWitness {
relative_path: "crates/nostr/src/client.rs",
- required_fragments: &["pub async fn send_nip22_comment_event_builder"],
+ required_fragments: &["pub async method fn send_nip22_comment_event_builder"],
},
EventBoundarySourceWitness {
relative_path: "crates/event/src/kinds.rs",
@@ -2120,7 +2123,7 @@ const DELETION_WITNESSES: [EventBoundarySourceWitness; 8] = [
EventBoundarySourceWitness {
relative_path: "crates/event_codec/src/deletion/reconciliation_v1.rs",
required_fragments: &[
- "pub struct RadrootsAdmittedNip09DeletionRequestEvent",
+ "pub type RadrootsAdmittedNip09DeletionRequestEvent",
"pub fn verify_and_admit_nip09_deletion_request_event",
],
},
@@ -2144,7 +2147,7 @@ const DELETION_WITNESSES: [EventBoundarySourceWitness; 8] = [
},
EventBoundarySourceWitness {
relative_path: "crates/nostr/src/client.rs",
- required_fragments: &["pub async fn send_nip09_deletion_request_event_builder"],
+ required_fragments: &["pub async method fn send_nip09_deletion_request_event_builder"],
},
EventBoundarySourceWitness {
relative_path: "crates/event/src/kinds.rs",
@@ -3521,19 +3524,305 @@ fn validate_event_boundary_source_witness(
Ok(source) => source,
Err(e) => return Err(format!("read {}: {e}", path.display())),
};
- for fragment in witness.required_fragments {
- if !source.contains(fragment) {
- return Err(format!(
- "canonical event row {} is missing required implementation fragment {} in {}",
- domain,
- fragment,
+ let syntax = syn::parse_file(&source)
+ .map_err(|error| format!("parse canonical event witness {}: {error}", path.display()))?;
+ for required in witness.required_fragments {
+ validate_rust_ast_witness(&syntax, required).map_err(|error| {
+ format!(
+ "canonical event row {domain} has invalid Rust AST witness `{required}` in {}: {error}",
path.display()
- ));
- }
+ )
+ })?;
}
Ok(())
}
+#[derive(Default)]
+struct RustAstWitnessInventory {
+ public_structs: BTreeSet<String>,
+ public_enums: BTreeSet<String>,
+ public_types: BTreeSet<String>,
+ public_functions: BTreeSet<String>,
+ public_methods: BTreeSet<String>,
+ public_consts: BTreeMap<String, (String, String)>,
+ public_exports: BTreeSet<String>,
+ match_arms: Vec<(String, String)>,
+ paths: BTreeSet<String>,
+ string_literals: BTreeSet<String>,
+}
+
+impl<'ast> syn::visit::Visit<'ast> for RustAstWitnessInventory {
+ fn visit_item_struct(&mut self, item: &'ast syn::ItemStruct) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_structs.insert(item.ident.to_string());
+ }
+ syn::visit::visit_item_struct(self, item);
+ }
+
+ fn visit_item_enum(&mut self, item: &'ast syn::ItemEnum) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_enums.insert(item.ident.to_string());
+ }
+ syn::visit::visit_item_enum(self, item);
+ }
+
+ fn visit_item_type(&mut self, item: &'ast syn::ItemType) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_types.insert(item.ident.to_string());
+ }
+ syn::visit::visit_item_type(self, item);
+ }
+
+ fn visit_item_fn(&mut self, item: &'ast syn::ItemFn) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_functions.insert(item.sig.ident.to_string());
+ }
+ syn::visit::visit_item_fn(self, item);
+ }
+
+ fn visit_impl_item_fn(&mut self, item: &'ast syn::ImplItemFn) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_methods.insert(item.sig.ident.to_string());
+ }
+ syn::visit::visit_impl_item_fn(self, item);
+ }
+
+ fn visit_item_const(&mut self, item: &'ast syn::ItemConst) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ self.public_consts.insert(
+ item.ident.to_string(),
+ (
+ compact_ast_tokens(item.ty.as_ref()),
+ compact_ast_tokens(item.expr.as_ref()),
+ ),
+ );
+ }
+ syn::visit::visit_item_const(self, item);
+ }
+
+ fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
+ if matches!(item.vis, syn::Visibility::Public(_)) {
+ collect_use_tree_exports(&item.tree, &mut self.public_exports);
+ }
+ syn::visit::visit_item_use(self, item);
+ }
+
+ fn visit_expr_match(&mut self, expression: &'ast syn::ExprMatch) {
+ self.match_arms.extend(expression.arms.iter().map(|arm| {
+ (
+ compact_ast_tokens(&arm.pat),
+ compact_ast_tokens(arm.body.as_ref()),
+ )
+ }));
+ syn::visit::visit_expr_match(self, expression);
+ }
+
+ fn visit_expr_path(&mut self, expression: &'ast syn::ExprPath) {
+ self.paths.insert(compact_ast_tokens(&expression.path));
+ syn::visit::visit_expr_path(self, expression);
+ }
+
+ fn visit_lit_str(&mut self, literal: &'ast syn::LitStr) {
+ self.string_literals.insert(literal.value());
+ syn::visit::visit_lit_str(self, literal);
+ }
+
+ fn visit_macro(&mut self, item: &'ast syn::Macro) {
+ collect_macro_string_literals(item.tokens.clone(), &mut self.string_literals);
+ syn::visit::visit_macro(self, item);
+ }
+}
+
+fn collect_macro_string_literals(
+ tokens: proc_macro2::TokenStream,
+ literals: &mut BTreeSet<String>,
+) {
+ for token in tokens {
+ match token {
+ proc_macro2::TokenTree::Group(group) => {
+ collect_macro_string_literals(group.stream(), literals);
+ }
+ proc_macro2::TokenTree::Literal(literal) => {
+ if let Ok(syn::Lit::Str(value)) = syn::parse_str::<syn::Lit>(&literal.to_string()) {
+ literals.insert(value.value());
+ }
+ }
+ proc_macro2::TokenTree::Ident(_) | proc_macro2::TokenTree::Punct(_) => {}
+ }
+ }
+}
+
+fn collect_use_tree_exports(tree: &syn::UseTree, exports: &mut BTreeSet<String>) {
+ match tree {
+ syn::UseTree::Name(name) => {
+ exports.insert(name.ident.to_string());
+ }
+ syn::UseTree::Rename(rename) => {
+ exports.insert(rename.rename.to_string());
+ }
+ syn::UseTree::Path(path) => collect_use_tree_exports(path.tree.as_ref(), exports),
+ syn::UseTree::Group(group) => {
+ for item in &group.items {
+ collect_use_tree_exports(item, exports);
+ }
+ }
+ syn::UseTree::Glob(_) => {}
+ }
+}
+
+fn compact_ast_tokens(tokens: &impl ToTokens) -> String {
+ tokens.to_token_stream().to_string().replace(' ', "")
+}
+
+fn validate_rust_ast_witness(file: &syn::File, required: &str) -> Result<(), String> {
+ use syn::visit::Visit;
+
+ let mut inventory = RustAstWitnessInventory::default();
+ inventory.visit_file(file);
+ let required = required.trim();
+
+ if let Some(rest) = required.strip_prefix("pub struct ") {
+ let name = rest
+ .split(|character: char| {
+ character.is_whitespace() || character == '{' || character == '<'
+ })
+ .next()
+ .unwrap_or_default();
+ return inventory
+ .public_structs
+ .contains(name)
+ .then_some(())
+ .ok_or_else(|| format!("missing public struct {name}"));
+ }
+ if let Some(rest) = required.strip_prefix("pub enum ") {
+ let name = rest
+ .split(|character: char| {
+ character.is_whitespace() || character == '{' || character == '<'
+ })
+ .next()
+ .unwrap_or_default();
+ return inventory
+ .public_enums
+ .contains(name)
+ .then_some(())
+ .ok_or_else(|| format!("missing public enum {name}"));
+ }
+ if let Some(rest) = required.strip_prefix("pub type ") {
+ let name = rest
+ .split(|character: char| {
+ character.is_whitespace() || character == '=' || character == '<'
+ })
+ .next()
+ .unwrap_or_default();
+ return inventory
+ .public_types
+ .contains(name)
+ .then_some(())
+ .ok_or_else(|| format!("missing public type alias {name}"));
+ }
+ if required.starts_with("pub method fn ") || required.starts_with("pub async method fn ") {
+ let rest = required
+ .strip_prefix("pub async method fn ")
+ .or_else(|| required.strip_prefix("pub method fn "))
+ .expect("public method prefix checked");
+ let name = rest
+ .split(|character: char| {
+ character == '(' || character == '<' || character.is_whitespace()
+ })
+ .next()
+ .unwrap_or_default();
+ return inventory
+ .public_methods
+ .contains(name)
+ .then_some(())
+ .ok_or_else(|| format!("missing public method {name}"));
+ }
+ if required.starts_with("pub fn ") || required.starts_with("pub async fn ") {
+ let rest = required
+ .strip_prefix("pub async fn ")
+ .or_else(|| required.strip_prefix("pub fn "))
+ .expect("public function prefix checked");
+ let name = rest
+ .split(|character: char| {
+ character == '(' || character == '<' || character.is_whitespace()
+ })
+ .next()
+ .unwrap_or_default();
+ return inventory
+ .public_functions
+ .contains(name)
+ .then_some(())
+ .ok_or_else(|| format!("missing public function {name}"));
+ }
+ if required.starts_with("pub const ") {
+ let declaration = required.trim_end_matches(';');
+ let rest = declaration
+ .strip_prefix("pub const ")
+ .expect("public const prefix checked");
+ let name = rest.split(':').next().unwrap_or_default().trim();
+ let actual = inventory
+ .public_consts
+ .get(name)
+ .ok_or_else(|| format!("missing public const {name}"))?;
+ let expected_type = rest
+ .split_once(':')
+ .map(|(_, suffix)| suffix.split('=').next().unwrap_or(suffix).trim())
+ .filter(|expected| !expected.is_empty())
+ .ok_or_else(|| format!("public const witness {name} has no type"))?;
+ let expected_type = syn::parse_str::<syn::Type>(expected_type)
+ .map_err(|error| format!("parse expected const {name} type: {error}"))?;
+ if actual.0 != compact_ast_tokens(&expected_type) {
+ return Err(format!("public const {name} type differs"));
+ }
+ if let Some((_, expected_expression)) = declaration.split_once('=') {
+ let expected = syn::parse_str::<syn::Expr>(expected_expression.trim())
+ .map_err(|error| format!("parse expected const {name} expression: {error}"))?;
+ if actual.1 != compact_ast_tokens(&expected) {
+ return Err(format!("public const {name} expression differs"));
+ }
+ }
+ return Ok(());
+ }
+ if let Some((pattern, expression)) = required.split_once("=>") {
+ let pattern = pattern.split_whitespace().collect::<String>();
+ let expression = expression.trim().trim_end_matches(',');
+ let matches = inventory
+ .match_arms
+ .iter()
+ .any(|(actual_pattern, actual_expression)| {
+ actual_pattern == &pattern
+ && (expression.is_empty()
+ || actual_expression == &expression.split_whitespace().collect::<String>())
+ });
+ return matches
+ .then_some(())
+ .ok_or_else(|| format!("missing match arm {required}"));
+ }
+ if required.starts_with('"') {
+ let literal = syn::parse_str::<syn::LitStr>(required)
+ .map_err(|error| format!("parse expected string literal: {error}"))?;
+ return inventory
+ .string_literals
+ .contains(&literal.value())
+ .then_some(())
+ .ok_or_else(|| format!("missing string literal {}", literal.value()));
+ }
+ if required.contains("::") {
+ let path = required.split_whitespace().collect::<String>();
+ return inventory
+ .paths
+ .contains(&path)
+ .then_some(())
+ .ok_or_else(|| format!("missing Rust path {path}"));
+ }
+
+ inventory
+ .public_exports
+ .contains(required)
+ .then_some(())
+ .ok_or_else(|| format!("missing public export {required}"))
+}
+
fn validate_canonical_event_boundary_with_override(
workspace_root: &Path,
event_boundary_override: Option<PathBuf>,
@@ -5655,20 +5944,65 @@ fn validate_replica_policy_source_witnesses(sync_root: &Path) -> Result<(), Stri
let types_path = sync_root.join("src/types.rs");
let types_source = fs::read_to_string(&types_path)
.map_err(|error| format!("read {}: {error}", types_path.display()))?;
+ let types = syn::parse_file(&types_source).map_err(|error| {
+ format!(
+ "parse replica request source {}: {error}",
+ types_path.display()
+ )
+ })?;
for type_name in [
"RadrootsReplicaFarmSelector",
"RadrootsReplicaSyncOptions",
"RadrootsReplicaSyncRequest",
] {
- let witness = format!("#[serde(deny_unknown_fields)]\npub struct {type_name}");
- if !types_source.contains(&witness) {
+ let structs = types
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ syn::Item::Struct(item) if item.ident == type_name => Some(item),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [request] = structs.as_slice() else {
+ return Err(format!(
+ "replica request source {} must define exactly one {type_name}",
+ types_path.display()
+ ));
+ };
+ let deny_unknown_fields = request.attrs.iter().any(|attribute| {
+ if !attribute.path().is_ident("serde") {
+ return false;
+ }
+ let mut found = false;
+ let _ = attribute.parse_nested_meta(|meta| {
+ if meta.path.is_ident("deny_unknown_fields") {
+ found = true;
+ }
+ Ok(())
+ });
+ found
+ });
+ if !matches!(request.vis, syn::Visibility::Public(_)) || !deny_unknown_fields {
return Err(format!(
"replica request type {type_name} must place #[serde(deny_unknown_fields)] immediately before its public struct declaration in {}",
types_path.display()
));
}
}
- if types_source.contains("include_profiles") {
+
+ #[derive(Default)]
+ struct IdentifierInventory {
+ names: BTreeSet<String>,
+ }
+ impl<'ast> syn::visit::Visit<'ast> for IdentifierInventory {
+ fn visit_ident(&mut self, identifier: &'ast syn::Ident) {
+ self.names.insert(identifier.to_string());
+ }
+ }
+ use syn::visit::Visit;
+ let mut identifiers = IdentifierInventory::default();
+ identifiers.visit_file(&types);
+ if identifiers.names.contains("include_profiles") {
return Err(format!(
"retired replica request identifier include_profiles is forbidden in {}",
types_path.display()
@@ -11177,6 +11511,53 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"]
}
#[test]
+ fn rust_ast_witnesses_ignore_formatting_and_reject_lexical_prefixes() {
+ let syntax = syn::parse_file(
+ r#"
+ pub struct ExactWitness<T> { value: T }
+ impl<T> ExactWitness<T> {
+ pub fn method_only() {}
+ }
+ pub type ExactAlias = ExactWitness<u8>;
+ pub const EXACT_KIND : u32 = 7;
+ pub use nested::{exported_symbol};
+ fn mapping(value: Mapping) -> u32 {
+ match value { Mapping::Exact => EXACT_KIND }
+ }
+ macro_rules! identity { () => { "radroots.exact.v1" }; }
+ "#,
+ )
+ .expect("structured Rust fixture");
+
+ for required in [
+ "pub struct ExactWitness",
+ "pub method fn method_only",
+ "pub type ExactAlias",
+ "pub const EXACT_KIND: u32 = 7;",
+ "Mapping::Exact => EXACT_KIND",
+ "exported_symbol",
+ "\"radroots.exact.v1\"",
+ ] {
+ validate_rust_ast_witness(&syntax, required).expect(required);
+ }
+ assert!(
+ validate_rust_ast_witness(&syntax, "pub struct Exact")
+ .expect_err("prefix-only struct name must fail")
+ .contains("missing public struct Exact")
+ );
+ assert!(
+ validate_rust_ast_witness(&syntax, "pub const EXACT_KIND: u32 = 8;")
+ .expect_err("different const expression must fail")
+ .contains("expression differs")
+ );
+ assert!(
+ validate_rust_ast_witness(&syntax, "pub fn method_only")
+ .expect_err("public method must not satisfy a free-function witness")
+ .contains("missing public function method_only")
+ );
+ }
+
+ #[test]
fn canonical_event_boundary_reports_row_drift() {
let root = workspace_root();
let matrix_path =
diff --git a/tools/xtask/src/contract/admission_authority.rs b/tools/xtask/src/contract/admission_authority.rs
@@ -1,6 +1,6 @@
use super::{
ConformanceVectorEntry, OperationsContractManifest, collect_non_empty_set,
- validate_conformance_vector_file, validate_operation_case_kinds,
+ validate_conformance_vector_file, validate_operation_case_kinds, validate_rust_ast_witness,
};
use serde_json::{Map, Value};
use std::{
@@ -264,12 +264,12 @@ fn validate_source_witnesses(workspace_root: &Path) -> Result<(), String> {
let source = fs::read_to_string(workspace_root.join(relative)).map_err(|error| {
format!("failed to read verified admission witness {relative}: {error}")
})?;
- for fragment in fragments {
- if !source.contains(fragment) {
- return Err(format!(
- "verified admission witness {relative} is missing `{fragment}`"
- ));
- }
+ let syntax = syn::parse_file(&source)
+ .map_err(|error| format!("parse verified admission witness {relative}: {error}"))?;
+ for required in fragments {
+ validate_rust_ast_witness(&syntax, required).map_err(|error| {
+ format!("verified admission Rust AST witness {relative} `{required}`: {error}")
+ })?;
}
}
Ok(())
diff --git a/tools/xtask/src/contract/blossom_raster_decoder_security.rs b/tools/xtask/src/contract/blossom_raster_decoder_security.rs
@@ -47,11 +47,6 @@ const FUZZ_LOCKFILE_RELATIVE: &str = "fuzz/Cargo.lock";
const FUZZ_TOOLCHAIN_RELATIVE: &str = "rust-toolchain-fuzz.toml";
const FUZZ_COMMON_RELATIVE: &str = "fuzz/fuzz_targets/common.rs";
const FUZZ_CORPUS_RELATIVE: &str = "fuzz/corpus";
-const NIX_APPS_RELATIVE: &str = "build/nix/apps.nix";
-const NIX_CHECKS_RELATIVE: &str = "build/nix/checks.nix";
-const NIX_COMMON_RELATIVE: &str = "build/nix/common.nix";
-const NIX_DEVSHELLS_RELATIVE: &str = "build/nix/devshells.nix";
-const NIX_TOOLCHAINS_RELATIVE: &str = "build/nix/toolchains.nix";
const SECURITY_DOCUMENT_RELATIVE: &str = "docs/blossom-raster-decoder-security.md";
const OPERATIONS_RELATIVE: &str = "contracts/operations.toml";
const RELEASE_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml";
@@ -1340,7 +1335,6 @@ fn validate_source_contract(workspace_root: &Path) -> Result<(), String> {
validate_error_authority(workspace_root)?;
validate_vector_executor(workspace_root)?;
validate_fuzz_authority(workspace_root, false)?;
- validate_nix_lanes(workspace_root)?;
validate_operations_authority(workspace_root)?;
validate_release_authority(workspace_root)?;
validate_vector(workspace_root)?;
@@ -1850,45 +1844,6 @@ fn validate_fuzz_authority(workspace_root: &Path, validate_corpus: bool) -> Resu
Ok(())
}
-fn validate_nix_lanes(workspace_root: &Path) -> Result<(), String> {
- for (relative, needles) in [
- (NIX_APPS_RELATIVE, vec!["decoder-security"]),
- (NIX_DEVSHELLS_RELATIVE, vec!["decoder-security"]),
- (
- NIX_CHECKS_RELATIVE,
- vec!["blossom-raster-decode-test", "blossom-decoder-fuzz-smoke"],
- ),
- (NIX_TOOLCHAINS_RELATIVE, vec!["rust-toolchain-fuzz.toml"]),
- (
- NIX_COMMON_RELATIVE,
- vec![
- "decoderSecurityCommand",
- "decoderSecurityStableCommand",
- "decoderSecurityFuzzCommand",
- "fuzz/Cargo.lock",
- "131072",
- "-runs=256",
- "-seed=424242",
- "-max_len=65536",
- "resource_cases='jpeg_grayscale jpeg_rgb jpeg_cmyk jpeg_sof1 png_rgb png_palette png_rgba png_adam7 webp_vp8_rgb webp_vp8_alpha webp_vp8l_rgb webp_vp8l_alpha'",
- "for repetition in 1 2 3",
- ],
- ),
- ] {
- let bytes = read_regular_file(workspace_root, relative)?;
- let source = std::str::from_utf8(&bytes)
- .map_err(|error| format!("{relative} must be UTF-8: {error}"))?;
- for needle in needles {
- if !source.contains(needle) {
- return Err(format!(
- "{relative} must declare the governed decoder-security lane fragment {needle}"
- ));
- }
- }
- }
- Ok(())
-}
-
fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> {
let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?;
let operations = manifest
diff --git a/tools/xtask/src/contract/outbox_migration.rs b/tools/xtask/src/contract/outbox_migration.rs
@@ -9,6 +9,7 @@ use std::fs;
use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
+use syn::{ItemFn, visit::Visit};
const CONTRACT_ID: &str = "radroots_outbox.migration_authority.v1";
const AUTHORITY_ID: &str = "versioned_outbox_migration_authority_v1";
@@ -691,9 +692,30 @@ fn validate_vector(workspace_root: &Path) -> Result<(), String> {
let source = read_regular_file(workspace_root, &authority.authority_path)?;
let source = std::str::from_utf8(&source)
.map_err(|error| format!("decode {}: {error}", authority.authority_path))?;
- if !source.contains(&authority.authority) {
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse {}: {error}", authority.authority_path))?;
+ #[derive(Default)]
+ struct TestFunctions(BTreeSet<String>);
+ impl<'ast> Visit<'ast> for TestFunctions {
+ fn visit_item_fn(&mut self, function: &'ast ItemFn) {
+ let is_test = function.attrs.iter().any(|attribute| {
+ attribute
+ .path()
+ .segments
+ .last()
+ .is_some_and(|segment| segment.ident == "test")
+ });
+ if is_test {
+ self.0.insert(function.sig.ident.to_string());
+ }
+ syn::visit::visit_item_fn(self, function);
+ }
+ }
+ let mut tests = TestFunctions::default();
+ tests.visit_file(&syntax);
+ if !tests.0.contains(&authority.authority) {
return Err(format!(
- "outbox migration delegated authority `{}` is not present in {}",
+ "outbox migration delegated test authority `{}` is not present in the Rust AST for {}",
authority.authority, authority.authority_path
));
}
diff --git a/tools/xtask/src/contract/validator_inventory.rs b/tools/xtask/src/contract/validator_inventory.rs
@@ -0,0 +1,278 @@
+use serde::Deserialize;
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ fs,
+ path::{Component, Path},
+};
+
+const INVENTORY_RELATIVE: &str = "contracts/semantic_validator_inventory.toml";
+const EXPECTED_VALIDATOR_IDS: [&str; 10] = [
+ "artifact_transactions",
+ "event_store_successors",
+ "feature_support",
+ "immutable_predecessors",
+ "operations_and_boundaries",
+ "outbox_successors",
+ "phase1_media_successors",
+ "registry_v7",
+ "release_closure",
+ "validator_inventory",
+];
+const EXPECTED_IMPLEMENTATIONS: [&str; 21] = [
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/admission_authority.rs",
+ "tools/xtask/src/contract/artifact_bundle.rs",
+ "tools/xtask/src/contract/blossom_publication_readiness.rs",
+ "tools/xtask/src/contract/blossom_raster_decoder_security.rs",
+ "tools/xtask/src/contract/comment_authority.rs",
+ "tools/xtask/src/contract/deletion_authority.rs",
+ "tools/xtask/src/contract/feature_support.rs",
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "tools/xtask/src/contract/nip09_reconciliation.rs",
+ "tools/xtask/src/contract/outbox_migration.rs",
+ "tools/xtask/src/contract/outbox_phase1_publication.rs",
+ "tools/xtask/src/contract/phase1_publication_allowlist.rs",
+ "tools/xtask/src/contract/phase1_publication_artifact.rs",
+ "tools/xtask/src/contract/phase1_publication_media_readiness.rs",
+ "tools/xtask/src/contract/raw_source_rebuild.rs",
+ "tools/xtask/src/contract/registry_v7.rs",
+ "tools/xtask/src/contract/release_package.rs",
+ "tools/xtask/src/contract/release_provenance.rs",
+ "tools/xtask/src/contract/source_maintenance.rs",
+ "tools/xtask/src/contract/validator_inventory.rs",
+];
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ValidatorInventory {
+ schema_version: u32,
+ validators: Vec<ValidatorEntry>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ValidatorEntry {
+ id: String,
+ semantic: bool,
+ parsers: Vec<ParserKind>,
+ implementation_paths: Vec<String>,
+ governed_inputs: Vec<String>,
+ #[serde(default)]
+ byte_hash_scopes: Vec<ByteHashScope>,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
+#[serde(rename_all = "snake_case")]
+enum ParserKind {
+ CargoMetadata,
+ ExecutableVector,
+ GitObject,
+ GovernedBytes,
+ Json,
+ JsonSchema,
+ MarkdownTable,
+ RustAst,
+ SqliteCatalog,
+ TarArchive,
+ Toml,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
+#[serde(rename_all = "snake_case")]
+enum ByteHashScope {
+ CanonicalVectorsAndMirrors,
+ GeneratedContractArtifacts,
+ ImmutablePredecessorArtifacts,
+ PackageArchives,
+ RawFuzzSeeds,
+ ReleaseProvenanceArtifacts,
+ SqlMigrations,
+}
+
+pub(super) fn validate_semantic_validator_inventory(workspace_root: &Path) -> Result<(), String> {
+ let path = workspace_root.join(INVENTORY_RELATIVE);
+ let source =
+ fs::read_to_string(&path).map_err(|error| format!("read {INVENTORY_RELATIVE}: {error}"))?;
+ let inventory = toml::from_str::<ValidatorInventory>(&source)
+ .map_err(|error| format!("parse {INVENTORY_RELATIVE}: {error}"))?;
+ if inventory.schema_version != 1 {
+ return Err(format!("{INVENTORY_RELATIVE} schema_version must be 1"));
+ }
+
+ let expected_ids = EXPECTED_VALIDATOR_IDS.into_iter().collect::<BTreeSet<_>>();
+ let mut entries = BTreeMap::new();
+ let mut previous_id = None::<&str>;
+ for entry in &inventory.validators {
+ if previous_id.is_some_and(|previous| previous >= entry.id.as_str()) {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator ids must be strictly sorted and unique"
+ ));
+ }
+ previous_id = Some(entry.id.as_str());
+ if entries.insert(entry.id.as_str(), entry).is_some() {
+ return Err(format!("{INVENTORY_RELATIVE} duplicates {}", entry.id));
+ }
+ }
+ if entries.keys().copied().collect::<BTreeSet<_>>() != expected_ids {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator id inventory is incomplete"
+ ));
+ }
+
+ let mut implementations = BTreeSet::new();
+ for entry in entries.values() {
+ validate_entry(workspace_root, entry)?;
+ for relative in &entry.implementation_paths {
+ if !implementations.insert(relative.as_str()) {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} implementation {relative} has multiple owners"
+ ));
+ }
+ }
+ }
+ let expected_implementations = EXPECTED_IMPLEMENTATIONS
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if implementations != expected_implementations {
+ let missing = expected_implementations
+ .difference(&implementations)
+ .copied()
+ .collect::<Vec<_>>();
+ let unexpected = implementations
+ .difference(&expected_implementations)
+ .copied()
+ .collect::<Vec<_>>();
+ return Err(format!(
+ "{INVENTORY_RELATIVE} implementation inventory drifted; missing {missing:?}, unexpected {unexpected:?}"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_entry(workspace_root: &Path, entry: &ValidatorEntry) -> Result<(), String> {
+ if entry.id.trim().is_empty()
+ || entry.parsers.is_empty()
+ || entry.implementation_paths.is_empty()
+ || entry.governed_inputs.is_empty()
+ {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator {} has an empty authority field",
+ entry.id
+ ));
+ }
+ validate_sorted_unique(&entry.id, "parser", &entry.parsers)?;
+ validate_sorted_unique(
+ &entry.id,
+ "implementation path",
+ &entry.implementation_paths,
+ )?;
+ validate_sorted_unique(&entry.id, "governed input", &entry.governed_inputs)?;
+ validate_sorted_unique(&entry.id, "byte hash scope", &entry.byte_hash_scopes)?;
+ if entry.semantic && entry.parsers == [ParserKind::GovernedBytes] {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} semantic validator {} cannot use byte identity as semantic authority",
+ entry.id
+ ));
+ }
+ if !entry.byte_hash_scopes.is_empty() && !entry.parsers.contains(&ParserKind::GovernedBytes) {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator {} declares byte hash scope without governed_bytes parsing",
+ entry.id
+ ));
+ }
+ for relative in entry
+ .implementation_paths
+ .iter()
+ .chain(entry.governed_inputs.iter())
+ {
+ validate_relative_path(relative)?;
+ if !workspace_root.join(relative).exists() {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator {} references missing path {relative}",
+ entry.id
+ ));
+ }
+ }
+ for relative in &entry.implementation_paths {
+ if !relative.starts_with("tools/xtask/src/contract") || !relative.ends_with(".rs") {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator {} has invalid implementation {relative}",
+ entry.id
+ ));
+ }
+ let source = fs::read_to_string(workspace_root.join(relative))
+ .map_err(|error| format!("read validator implementation {relative}: {error}"))?;
+ syn::parse_file(&source)
+ .map_err(|error| format!("parse validator implementation {relative}: {error}"))?;
+ }
+ Ok(())
+}
+
+fn validate_relative_path(relative: &str) -> Result<(), String> {
+ let path = Path::new(relative);
+ if relative.is_empty()
+ || path.is_absolute()
+ || path.components().any(|component| {
+ matches!(
+ component,
+ Component::ParentDir | Component::RootDir | Component::Prefix(_)
+ )
+ })
+ {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} contains invalid relative path {relative}"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_sorted_unique<T: Ord>(
+ validator: &str,
+ label: &str,
+ values: &[T],
+) -> Result<(), String> {
+ if values.windows(2).any(|pair| pair[0] >= pair[1]) {
+ return Err(format!(
+ "{INVENTORY_RELATIVE} validator {validator} {label}s must be strictly sorted and unique"
+ ));
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ fn workspace_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("xtask workspace root")
+ .to_path_buf()
+ }
+
+ #[test]
+ fn semantic_validator_inventory_is_complete_and_structured() {
+ validate_semantic_validator_inventory(&workspace_root()).expect("validator inventory");
+ }
+
+ #[test]
+ fn semantic_validator_cannot_delegate_meaning_to_byte_identity() {
+ let entry = ValidatorEntry {
+ id: "synthetic".to_owned(),
+ semantic: true,
+ parsers: vec![ParserKind::GovernedBytes],
+ implementation_paths: vec![
+ "tools/xtask/src/contract/validator_inventory.rs".to_owned(),
+ ],
+ governed_inputs: vec!["contracts/semantic_validator_inventory.toml".to_owned()],
+ byte_hash_scopes: vec![ByteHashScope::GeneratedContractArtifacts],
+ };
+ assert!(
+ validate_entry(&workspace_root(), &entry)
+ .expect_err("semantic byte-only authority must fail")
+ .contains("cannot use byte identity as semantic authority")
+ );
+ }
+}