commit 4550601711d55a03a7d8df0092bfd0a49ed27258
parent f633a2604f675592e4af24acadefc2098a887dc9
Author: triesap <tyson@radroots.org>
Date: Fri, 20 Feb 2026 23:31:54 +0000
build: add contract manifest parser and validator
Diffstat:
3 files changed, 202 insertions(+), 1 deletion(-)
diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml
@@ -6,3 +6,5 @@ rust-version.workspace = true
license.workspace = true
[dependencies]
+serde = { workspace = true, features = ["derive"] }
+toml = { workspace = true }
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -0,0 +1,173 @@
+#![forbid(unsafe_code)]
+
+use serde::Deserialize;
+use std::collections::BTreeMap;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Deserialize)]
+pub struct ContractManifest {
+ pub contract: ManifestContract,
+ pub surface: Surface,
+ pub policy: Policy,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ManifestContract {
+ pub name: String,
+ pub version: String,
+ pub source: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Surface {
+ pub model_crates: Vec<String>,
+ pub algorithm_crates: Vec<String>,
+ pub wasm_crates: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Policy {
+ pub exclude_internal_workspace_crates: bool,
+ pub require_reproducible_exports: bool,
+ pub require_conformance_vectors: bool,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct VersionPolicy {
+ pub contract: VersionContract,
+ pub semver: SemverRules,
+ pub compatibility: CompatibilityRules,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct VersionContract {
+ pub version: String,
+ pub stability: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SemverRules {
+ pub major_on: Vec<String>,
+ pub minor_on: Vec<String>,
+ pub patch_on: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct CompatibilityRules {
+ pub requires_conformance_pass: bool,
+ pub requires_export_manifest_diff: bool,
+ pub requires_release_notes: bool,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ExportMapping {
+ pub language: ExportLanguage,
+ pub packages: BTreeMap<String, String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ExportLanguage {
+ pub id: String,
+ pub repository: String,
+}
+
+#[derive(Debug)]
+pub struct ContractBundle {
+ pub root: PathBuf,
+ pub manifest: ContractManifest,
+ pub version: VersionPolicy,
+ pub exports: Vec<ExportMapping>,
+}
+
+fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
+ let raw = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
+ toml::from_str::<T>(&raw).map_err(|e| format!("parse {}: {e}", path.display()))
+}
+
+fn contract_root(workspace_root: &Path) -> PathBuf {
+ workspace_root.join("contract")
+}
+
+pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> {
+ let root = contract_root(workspace_root);
+ let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?;
+ let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?;
+ let exports_dir = root.join("exports");
+ let mut exports = Vec::new();
+ let mut entries = fs::read_dir(&exports_dir)
+ .map_err(|e| format!("read dir {}: {e}", exports_dir.display()))?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|e| format!("read dir entries {}: {e}", exports_dir.display()))?;
+ entries.sort_by_key(|entry| entry.file_name());
+ for entry in entries {
+ let path = entry.path();
+ if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
+ continue;
+ }
+ exports.push(parse_toml::<ExportMapping>(&path)?);
+ }
+ Ok(ContractBundle {
+ root,
+ manifest,
+ version,
+ exports,
+ })
+}
+
+pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
+ if bundle.manifest.contract.name.trim().is_empty() {
+ return Err("contract name is required".to_string());
+ }
+ if bundle.manifest.contract.version.trim().is_empty() {
+ return Err("contract version is required".to_string());
+ }
+ if bundle.manifest.contract.source.trim().is_empty() {
+ return Err("contract source is required".to_string());
+ }
+ if bundle.manifest.surface.model_crates.is_empty() {
+ return Err("contract surface.model_crates must not be empty".to_string());
+ }
+ if bundle.exports.is_empty() {
+ return Err("at least one language export mapping is required".to_string());
+ }
+ for mapping in &bundle.exports {
+ if mapping.language.id.trim().is_empty() {
+ return Err("language.id is required".to_string());
+ }
+ if mapping.language.repository.trim().is_empty() {
+ return Err(format!("language.repository is required for {}", mapping.language.id));
+ }
+ if mapping.packages.is_empty() {
+ return Err(format!("packages map is required for {}", mapping.language.id));
+ }
+ }
+ if bundle.version.contract.version.trim().is_empty() {
+ return Err("version.contract.version is required".to_string());
+ }
+ if bundle.version.contract.stability.trim().is_empty() {
+ return Err("version.contract.stability is required".to_string());
+ }
+ if bundle.version.semver.major_on.is_empty()
+ || bundle.version.semver.minor_on.is_empty()
+ || bundle.version.semver.patch_on.is_empty()
+ {
+ return Err("version.semver rules must all be non-empty".to_string());
+ }
+ if !bundle.version.compatibility.requires_conformance_pass {
+ return Err("compatibility.requires_conformance_pass must be true".to_string());
+ }
+ if !bundle.version.compatibility.requires_export_manifest_diff {
+ return Err("compatibility.requires_export_manifest_diff must be true".to_string());
+ }
+ if !bundle.version.compatibility.requires_release_notes {
+ return Err("compatibility.requires_release_notes must be true".to_string());
+ }
+ if !bundle.manifest.policy.exclude_internal_workspace_crates
+ || !bundle.manifest.policy.require_reproducible_exports
+ || !bundle.manifest.policy.require_conformance_vectors
+ {
+ return Err("contract policy flags must all be true".to_string());
+ }
+ Ok(())
+}
diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs
@@ -1,6 +1,9 @@
#![forbid(unsafe_code)]
+mod contract;
+
use std::env;
+use std::path::{Path, PathBuf};
use std::process::ExitCode;
fn usage() {
@@ -9,10 +12,33 @@ fn usage() {
eprintln!(" cargo xtask sdk validate");
}
+fn workspace_root() -> Result<PathBuf, String> {
+ let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let Some(crates_dir) = manifest_dir.parent() else {
+ return Err("failed to resolve crates dir".to_string());
+ };
+ let Some(root) = crates_dir.parent() else {
+ return Err("failed to resolve workspace root".to_string());
+ };
+ Ok(root.to_path_buf())
+}
+
+fn validate_contract() -> Result<(), String> {
+ let root = workspace_root()?;
+ let bundle = contract::load_contract_bundle(&root)?;
+ contract::validate_contract_bundle(&bundle)?;
+ eprintln!(
+ "validated contract {} {}",
+ bundle.manifest.contract.name, bundle.manifest.contract.version
+ );
+ eprintln!("contract root: {}", bundle.root.display());
+ Ok(())
+}
+
fn run_sdk(args: &[String]) -> Result<(), String> {
match args.first().map(String::as_str) {
Some("export-ts") => Ok(()),
- Some("validate") => Ok(()),
+ Some("validate") => validate_contract(),
_ => Err("unknown sdk subcommand".to_string()),
}
}