diff options
Diffstat (limited to 'xtask')
| -rw-r--r-- | xtask/Cargo.toml | 17 | ||||
| -rw-r--r-- | xtask/src/main.rs | 3861 |
2 files changed, 0 insertions, 3878 deletions
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml deleted file mode 100644 index 9808023..0000000 --- a/xtask/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "xtask" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -fparkan-binary = { path = "../crates/fparkan-binary", version = "0.1.0" } -fparkan-corpus = { path = "../crates/fparkan-corpus", version = "0.1.0" } -cargo_metadata = "0.23.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.9" - -[lints] -workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs deleted file mode 100644 index e2f4a53..0000000 --- a/xtask/src/main.rs +++ /dev/null @@ -1,3861 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr( - test, - allow( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - clippy::cast_precision_loss, - clippy::expect_used, - clippy::float_cmp, - clippy::identity_op, - clippy::too_many_lines, - clippy::uninlined_format_args, - clippy::map_unwrap_or, - clippy::needless_raw_string_hashes, - clippy::semicolon_if_nothing_returned, - clippy::type_complexity, - clippy::panic, - clippy::unwrap_used - ) -)] -#![allow(clippy::print_stderr, clippy::print_stdout)] -//! Repository automation for `FParkan`. - -use cargo_metadata::MetadataCommand; -use fparkan_binary::{sha256, sha256_hex}; -use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; - -const CORPORA_MANIFEST_ENV: &str = "FPARKAN_CORPORA_MANIFEST"; -const PART1_ROOT_ENV: &str = "FPARKAN_CORPUS_PART1_ROOT"; -const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT"; -const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_roadmap.md"; -const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv"; -const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-audit.json"; -const SHADER_MANIFEST_REPORT: &str = "adapters/fparkan-render-vulkan/shaders/manifest.json"; -const STAGE_PACKAGE_MANIFEST: &str = "fixtures/acceptance/stage_packages.toml"; -const SUPPLY_CHAIN_POLICY_CONFIG: &str = "deny.toml"; -/// Windows is the only supported native runtime platform. -const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["windows"]; -const APPROVED_REGISTRY_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index"; -const SUPPLY_CHAIN_BANNED_PACKAGES: &[&str] = &["native-tls", "openssl", "openssl-sys"]; -const PINNED_RUST_TOOLCHAIN: &str = "1.97.1"; -const WORKSPACE_MSRV: &str = "1.97"; -const ALLOW_SUPPLY_CHAIN_FALLBACK_ENV: &str = "FPARKAN_ALLOW_SUPPLY_CHAIN_FALLBACK"; - -fn workspace_root_path() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR"))) - .to_path_buf() -} - -fn workspace_relative_path(path: &str) -> PathBuf { - workspace_root_path().join(path) -} - -fn main() { - let args = std::env::args().skip(1).collect::<Vec<_>>(); - let code = match run(&args) { - Ok(()) => 0, - Err(err) => { - eprintln!("{err}"); - 2 - } - }; - std::process::exit(code); -} - -fn run(args: &[String]) -> Result<(), String> { - match args { - [cmd] if cmd == "ci" => { - run_cargo_fmt_check()?; - run_policy(Path::new("."))?; - run_shader_provenance_verification()?; - cargo(&["test", "--workspace", "--all-targets", "--all-features", "--locked"])?; - cargo(&[ - "clippy", - "--workspace", - "--all-targets", - "--all-features", - "--locked", - "--", - "-D", - "warnings", - ])?; - run_cargo_doc()?; - run_cargo_deny()?; - run_acceptance_audit(&AuditOptions { - roadmap: PathBuf::from(CI_ACCEPTANCE_ROADMAP), - coverage: PathBuf::from(CI_ACCEPTANCE_COVERAGE), - out: PathBuf::from(CI_ACCEPTANCE_REPORT), - strict: true, - })?; - Ok(()) - } - [cmd] if cmd == "policy" => run_policy(Path::new(".")), - [cmd] if cmd == "shader-provenance" => run_shader_provenance_verification(), - [cmd, subcmd, rest @ ..] if cmd == "acceptance" && subcmd == "report" => { - let options = parse_acceptance_options(rest)?; - run_acceptance_report(&options) - } - [cmd, subcmd, rest @ ..] if cmd == "acceptance" && subcmd == "audit" => { - let options = parse_audit_options(rest)?; - run_acceptance_audit(&options) - } - [cmd, subcmd, rest @ ..] if cmd == "native-smoke" && subcmd == "audit" => { - let options = parse_native_smoke_audit_options(rest)?; - run_native_smoke_audit(&options) - } - [cmd, rest @ ..] if cmd == "package" => { - let options = parse_package_options(rest)?; - run_package(&options) - } - [cmd, suite, rest @ ..] if cmd == "test" && suite == "synthetic" => { - let options = parse_test_options(rest, PathBuf::from("testdata"))?; - run_stage_tests(options.stage, TestSuite::Synthetic, None) - } - [cmd, suite, rest @ ..] if cmd == "test" && suite == "licensed" => { - let options = parse_test_options(rest, PathBuf::from("testdata"))?; - let roots = load_licensed_roots(options.manifest.as_deref())?; - run_stage_tests(options.stage, TestSuite::Licensed, Some(&roots)) - } - [cmd, subcmd, rest @ ..] if cmd == "corpus" && subcmd == "baseline" => { - run_corpus_baseline(&parse_corpus_baseline_options(rest)?) - } - _ => Err( - "usage: cargo xtask ci | policy | shader-provenance | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | native-smoke audit --dir <path> [--expected-commit <sha>] [--expected-shader-manifest-hash <sha256>] | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path> [--out <path>]" - .to_string(), - ), - } -} - -fn cargo(args: &[&str]) -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let status = Command::new(cargo) - .args(args) - .status() - .map_err(|err| format!("failed to run cargo: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo exited with {status}")) - } -} - -fn cargo_owned(args: &[String]) -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let status = Command::new(cargo) - .args(args) - .status() - .map_err(|err| format!("failed to run cargo: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo exited with {status}")) - } -} - -fn cargo_with_env(args: &[&str], envs: &[(&str, &Path)]) -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let mut command = Command::new(cargo); - command.args(args); - for (key, value) in envs { - command.env(key, value); - } - let status = command - .status() - .map_err(|err| format!("failed to run cargo: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo exited with {status}")) - } -} - -fn run_cargo_fmt_check() -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let status = Command::new(cargo) - .args(["fmt", "--all", "--", "--check"]) - .status() - .map_err(|err| format!("failed to run rustfmt: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo fmt exited with {status}")) - } -} - -fn run_shader_provenance_verification() -> Result<(), String> { - let manifest_path = workspace_relative_path(SHADER_MANIFEST_REPORT); - let manifest = load_shader_manifest(&manifest_path)?; - let compiler_path = resolve_required_tool("FPARKAN_GLSLANG_VALIDATOR", &manifest.compiler)?; - let validator_path = resolve_required_tool("FPARKAN_SPIRV_VAL", &manifest.validator)?; - - verify_tool_metadata(&compiler_path, &manifest.compiler)?; - verify_tool_metadata(&validator_path, &manifest.validator)?; - - let out_dir = shader_provenance_output_dir(); - if out_dir.exists() { - fs::remove_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?; - } - fs::create_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?; - - for module in &manifest.modules { - verify_shader_module(&manifest, module, &compiler_path, &validator_path, &out_dir)?; - } - Ok(()) -} - -fn load_shader_manifest(path: &Path) -> Result<ShaderManifestJson, String> { - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - let manifest = serde_json::from_str::<ShaderManifestJson>(&text) - .map_err(|err| format!("{}: invalid shader manifest JSON: {err}", path.display()))?; - if manifest.modules.is_empty() { - return Err(format!( - "{}: shader manifest must describe at least one module", - path.display() - )); - } - if manifest.manifest_hash.trim().is_empty() { - return Err(format!( - "{}: shader manifest must include a non-empty manifest_hash", - path.display() - )); - } - Ok(manifest) -} - -fn resolve_required_tool( - env_var: &str, - manifest: &ShaderToolManifestJson, -) -> Result<PathBuf, String> { - let requested = std::env::var(env_var).unwrap_or_else(|_| manifest.name.clone()); - resolve_tool_path(&requested).ok_or_else(|| { - format!( - "required shader tool {} is unavailable (set {env_var} to override path)", - manifest.name - ) - }) -} - -fn resolve_tool_path(tool: &str) -> Option<PathBuf> { - let candidate = Path::new(tool); - if candidate.components().count() > 1 { - return candidate.is_file().then(|| candidate.to_path_buf()); - } - - if let Some(path) = find_tool_on_path(tool) { - return Some(path); - } - - windows_vulkan_sdk_tool(tool) -} - -fn find_tool_on_path(tool: &str) -> Option<PathBuf> { - let path = std::env::var_os("PATH")?; - let extensions: &[&str] = if cfg!(windows) { &[".exe", ""] } else { &[""] }; - for directory in std::env::split_paths(&path) { - for extension in extensions { - let candidate = directory.join(format!("{tool}{extension}")); - if candidate.is_file() { - return Some(candidate); - } - } - } - None -} - -#[cfg(windows)] -fn windows_vulkan_sdk_tool(tool: &str) -> Option<PathBuf> { - let mut roots = Vec::new(); - if let Some(root) = std::env::var_os("VULKAN_SDK") { - roots.push(PathBuf::from(root)); - } - let sdk_root = PathBuf::from(r"C:\VulkanSDK"); - let mut versions = fs::read_dir(&sdk_root) - .ok()? - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - .collect::<Vec<_>>(); - versions.sort(); - versions.reverse(); - roots.extend(versions); - - roots - .into_iter() - .map(|root| root.join("Bin").join(format!("{tool}.exe"))) - .find(|candidate| candidate.is_file()) -} - -#[cfg(not(windows))] -fn windows_vulkan_sdk_tool(_tool: &str) -> Option<PathBuf> { - None -} - -fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Result<(), String> { - let actual_name = path - .file_name() - .and_then(|value| value.to_str()) - .ok_or_else(|| format!("{}: invalid tool filename", path.display()))?; - let actual_name = actual_name.strip_suffix(".exe").unwrap_or(actual_name); - if actual_name != manifest.name { - return Err(format!( - "{}: tool name mismatch, expected {}, found {}", - path.display(), - manifest.name, - actual_name - )); - } - let actual_version = tool_version(path)?; - let actual_sha256 = sha256_file(path)?; - if actual_version != manifest.version || actual_sha256 != manifest.binary_sha256 { - eprintln!( - "warning: {} is non-canonical shader tooling (expected version {:?}, SHA-256 {}; found version {:?}, SHA-256 {}). Checked-in SPIR-V hashes remain mandatory.", - path.display(), - manifest.version, - manifest.binary_sha256, - actual_version, - actual_sha256 - ); - } - Ok(()) -} - -fn tool_version(path: &Path) -> Result<String, String> { - let output = Command::new(path) - .arg("--version") - .output() - .map_err(|err| format!("{} --version: {err}", path.display()))?; - if !output.status.success() { - return Err(format!( - "{} --version exited with {}", - path.display(), - output.status - )); - } - let stdout = String::from_utf8(output.stdout) - .map_err(|err| format!("{} --version: invalid UTF-8: {err}", path.display()))?; - stdout - .lines() - .find(|line| !line.trim().is_empty()) - .map(str::trim) - .map(|line| { - line.strip_prefix("Glslang Version: ") - .unwrap_or(line) - .to_string() - }) - .ok_or_else(|| format!("{} --version returned no version line", path.display())) -} - -fn verify_shader_module( - manifest: &ShaderManifestJson, - module: &ShaderModuleManifestJson, - compiler_path: &Path, - validator_path: &Path, - out_dir: &Path, -) -> Result<(), String> { - let source_path = workspace_relative_path(&module.source_path); - let checked_in_spirv_path = workspace_relative_path(&module.spirv_path); - let generated_spirv_path = out_dir.join(format!("{}.spv", module.name)); - - let source_sha256 = sha256_canonical_text_file(&source_path)?; - if source_sha256 != module.source_sha256 { - return Err(format!( - "{}: source SHA-256 mismatch, expected {}, found {}", - source_path.display(), - module.source_sha256, - source_sha256 - )); - } - - let checked_in_spirv_sha256 = sha256_file(&checked_in_spirv_path)?; - if checked_in_spirv_sha256 != module.sha256 { - return Err(format!( - "{}: checked-in SPIR-V SHA-256 mismatch, expected {}, found {}", - checked_in_spirv_path.display(), - module.sha256, - checked_in_spirv_sha256 - )); - } - - compile_shader_module( - compiler_path, - &manifest.target_env, - module, - &source_path, - &generated_spirv_path, - )?; - validate_shader_module(validator_path, &manifest.target_env, &generated_spirv_path)?; - - let generated_spirv_sha256 = sha256_file(&generated_spirv_path)?; - if generated_spirv_sha256 != module.sha256 { - return Err(format!( - "{}: generated SPIR-V SHA-256 mismatch, expected {}, found {}", - generated_spirv_path.display(), - module.sha256, - generated_spirv_sha256 - )); - } - Ok(()) -} - -fn compile_shader_module( - compiler_path: &Path, - target_env: &str, - module: &ShaderModuleManifestJson, - source_path: &Path, - output_path: &Path, -) -> Result<(), String> { - let stage = glslang_stage(&module.stage).ok_or_else(|| { - format!( - "{}: unsupported shader stage {:?}", - source_path.display(), - module.stage - ) - })?; - let output = Command::new(compiler_path) - .args(["-V", "--target-env", target_env, "-S", stage, "-e"]) - .arg(&module.entry_point) - .arg(source_path) - .arg("-o") - .arg(output_path) - .output() - .map_err(|err| { - format!( - "{}: shader compile failed to start: {err}", - source_path.display() - ) - })?; - if !output.status.success() { - return Err(format!( - "{}: shader compile failed:\n{}{}", - source_path.display(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(()) -} - -fn validate_shader_module( - validator_path: &Path, - target_env: &str, - module_path: &Path, -) -> Result<(), String> { - let output = Command::new(validator_path) - .args(["--target-env", target_env]) - .arg(module_path) - .output() - .map_err(|err| { - format!( - "{}: shader validation failed to start: {err}", - module_path.display() - ) - })?; - if !output.status.success() { - return Err(format!( - "{}: shader validation failed:\n{}{}", - module_path.display(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(()) -} - -fn glslang_stage(stage: &str) -> Option<&'static str> { - match stage { - "vertex" => Some("vert"), - "fragment" => Some("frag"), - _ => None, - } -} - -fn sha256_file(path: &Path) -> Result<String, String> { - let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?; - Ok(sha256_hex(&sha256(&bytes))) -} - -fn sha256_canonical_text_file(path: &Path) -> Result<String, String> { - let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?; - let mut canonical = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') { - canonical.push(b'\n'); - index += 2; - } else { - canonical.push(bytes[index]); - index += 1; - } - } - Ok(sha256_hex(&sha256(&canonical))) -} - -fn shader_provenance_output_dir() -> PathBuf { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - workspace_root_path() - .join("target") - .join("fparkan") - .join("shader-provenance") - .join(format!("{}-{}", std::process::id(), nonce)) -} - -fn run_cargo_deny() -> Result<(), String> { - validate_supply_chain_policy_config(&workspace_relative_path(SUPPLY_CHAIN_POLICY_CONFIG))?; - let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into()); - let version_output = match Command::new(&cargo_deny).arg("--version").output() { - Ok(output) => output, - Err(err) => { - return handle_cargo_deny_fallback(&format!( - "failed to run cargo-deny --version: {err}" - )); - } - }; - if !version_output.status.success() { - return handle_cargo_deny_fallback(&format!( - "cargo-deny --version exited with {}", - version_output.status - )); - } - let version_text = String::from_utf8(version_output.stdout) - .map_err(|err| format!("cargo-deny --version produced invalid UTF-8: {err}"))?; - if !version_text.contains(PINNED_CARGO_DENY_VERSION) { - return handle_cargo_deny_fallback(&format!( - "cargo-deny version mismatch: expected {PINNED_CARGO_DENY_VERSION}, found {}", - version_text.trim() - )); - } - - let status = Command::new(cargo_deny) - .args(["check", "advisories", "bans", "licenses", "sources"]) - .status() - .map_err(|err| format!("failed to run cargo-deny: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo-deny exited with {status}")) - } -} - -const PINNED_CARGO_DENY_VERSION: &str = "0.19.9"; - -fn handle_cargo_deny_fallback(reason: &str) -> Result<(), String> { - if allow_supply_chain_fallback() { - eprintln!( - "{reason}; running built-in supply-chain policy fallback because {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is set" - ); - run_builtin_supply_chain_policy(Path::new(".")) - } else if std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && ci_env_active() { - Err(format!( - "{reason}; {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is for local developer convenience only and is forbidden when CI is set" - )) - } else { - Err(format!( - "{reason}; install cargo-deny {PINNED_CARGO_DENY_VERSION} or explicitly opt into the fallback with {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV}=1" - )) - } -} - -fn validate_supply_chain_policy_config(path: &Path) -> Result<(), String> { - if path.is_file() { - Ok(()) - } else { - Err(format!( - "reviewed supply-chain policy config is missing: {}", - path.display() - )) - } -} - -fn allow_supply_chain_fallback() -> bool { - std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && !ci_env_active() -} - -fn ci_env_active() -> bool { - ci_env_value_is_active(std::env::var("CI").ok().as_deref()) -} - -fn ci_env_value_is_active(value: Option<&str>) -> bool { - value.is_some_and(|value| { - let trimmed = value.trim(); - !trimmed.is_empty() && trimmed != "0" && !trimmed.eq_ignore_ascii_case("false") - }) -} - -fn run_builtin_supply_chain_policy(root: &Path) -> Result<(), String> { - let mut failures = Vec::new(); - validate_workspace_license(root, &mut failures)?; - validate_lockfile_supply_chain(root, &mut failures)?; - if failures.is_empty() { - Ok(()) - } else { - Err(format!( - "built-in supply-chain policy failed:\n{}", - failures.join("\n") - )) - } -} - -fn run_cargo_doc() -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let status = Command::new(cargo) - .args([ - "doc", - "--workspace", - "--all-features", - "--locked", - "--no-deps", - ]) - .env( - "RUSTDOCFLAGS", - "-D warnings -D rustdoc::broken_intra_doc_links", - ) - .status() - .map_err(|err| format!("failed to run cargo doc: {err}"))?; - if status.success() { - Ok(()) - } else { - Err(format!("cargo doc exited with {status}")) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct LicensedCorpusRoots { - part1: PathBuf, - part2: PathBuf, -} - -impl LicensedCorpusRoots { - fn envs(&self) -> [(&str, &Path); 2] { - [ - (PART1_ROOT_ENV, self.part1.as_path()), - (PART2_ROOT_ENV, self.part2.as_path()), - ] - } -} - -fn load_licensed_roots(manifest: Option<&Path>) -> Result<LicensedCorpusRoots, String> { - let manifest = manifest - .map(Path::to_path_buf) - .or_else(|| std::env::var_os(CORPORA_MANIFEST_ENV).map(PathBuf::from)) - .ok_or_else(|| { - format!( - "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>" - ) - })?; - parse_licensed_manifest(&manifest) -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct LicensedManifest { - schema: Option<u8>, - #[serde(rename = "corpus")] - corpora: Vec<CorpusEntry>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CorpusEntry { - id: String, - kind: CorpusKind, - root: String, - expected_profile: Option<String>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -enum CorpusKind { - Part1, - Part2, -} - -fn parse_licensed_manifest(path: &Path) -> Result<LicensedCorpusRoots, String> { - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - let manifest: LicensedManifest = toml::from_str(&text) - .map_err(|err| format!("failed to parse {}: {err}", path.display()))?; - if manifest.schema.is_some_and(|version| version != 1) { - return Err(format!( - "unsupported corpora manifest schema {} (expected 1)", - manifest.schema.unwrap_or(1) - )); - } - - let mut part1 = None; - let mut part2 = None; - - for entry in manifest.corpora { - match entry.kind { - CorpusKind::Part1 => { - let root = PathBuf::from(entry.root); - assign_manifest_root(&mut part1, root, "part1")?; - } - CorpusKind::Part2 => { - let root = PathBuf::from(entry.root); - assign_manifest_root(&mut part2, root, "part2")?; - } - } - if entry.expected_profile.is_none() { - return Err(format!( - "{}: corpus entry '{}' must define expected_profile", - path.display(), - entry.id - )); - } - } - - let roots = LicensedCorpusRoots { - part1: part1 - .ok_or_else(|| "licensed manifest is missing part1 corpus entry".to_string())?, - part2: part2 - .ok_or_else(|| "licensed manifest is missing part2 corpus entry".to_string())?, - }; - validate_licensed_part("part1", &roots.part1)?; - validate_licensed_part("part2", &roots.part2)?; - Ok(roots) -} - -fn assign_manifest_root( - target: &mut Option<PathBuf>, - root: PathBuf, - kind: &str, -) -> Result<(), String> { - if target.replace(root).is_some() { - return Err(format!("licensed manifest contains duplicate {kind} root")); - } - Ok(()) -} - -fn validate_licensed_part(kind: &str, root: &Path) -> Result<(), String> { - if root.is_dir() { - Ok(()) - } else { - Err(format!( - "licensed corpus {kind} root is missing: {}", - root.display() - )) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct CorpusBaselineOptions { - root: PathBuf, - out: Option<PathBuf>, -} - -fn parse_corpus_baseline_options(args: &[String]) -> Result<CorpusBaselineOptions, String> { - let mut root = None; - let mut out = None; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--root" => { - root = Some( - iter.next() - .map(PathBuf::from) - .ok_or_else(|| "--root requires a path".to_string())?, - ); - } - "--out" => { - out = Some( - iter.next() - .map(PathBuf::from) - .ok_or_else(|| "--out requires a path".to_string())?, - ); - } - other => return Err(format!("unknown corpus baseline option: {other}")), - } - } - Ok(CorpusBaselineOptions { - root: root.ok_or_else(|| "missing --root".to_string())?, - out, - }) -} - -fn run_corpus_baseline(options: &CorpusBaselineOptions) -> Result<(), String> { - let manifest = - discover(&options.root, DiscoverOptions::default()).map_err(|error| error.to_string())?; - let baseline = report(&options.root, &manifest).map_err(|error| error.to_string())?; - let json = render_report_json(&baseline); - if let Some(out) = &options.out { - if let Some(parent) = out.parent() { - fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; - } - fs::write(out, json).map_err(|error| format!("{}: {error}", out.display()))?; - println!("{}", out.display()); - } else { - println!("{json}"); - } - Ok(()) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct PackageOptions { - target: String, - app: AppPackage, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum AppPackage { - Cli, - Game, - Headless, - Viewer, -} - -impl AppPackage { - fn parse(value: &str) -> Result<Self, String> { - match value { - "cli" => Ok(Self::Cli), - "game" => Ok(Self::Game), - "headless" => Ok(Self::Headless), - "viewer" => Ok(Self::Viewer), - _ => Err(format!("unknown app: {value}")), - } - } - - fn package(self) -> &'static str { - match self { - Self::Cli => "fparkan-cli", - Self::Game => "fparkan-game", - Self::Headless => "fparkan-headless", - Self::Viewer => "fparkan-viewer", - } - } -} - -fn parse_package_options(args: &[String]) -> Result<PackageOptions, String> { - let mut target = None; - let mut app = None; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--target" => { - target = Some( - iter.next() - .cloned() - .ok_or_else(|| "--target requires a value".to_string())?, - ); - } - "--app" => { - let value = iter - .next() - .ok_or_else(|| "--app requires a value".to_string())?; - app = Some(AppPackage::parse(value)?); - } - _ => return Err(format!("unknown package option: {arg}")), - } - } - Ok(PackageOptions { - target: target.ok_or_else(|| "missing --target".to_string())?, - app: app.ok_or_else(|| "missing --app".to_string())?, - }) -} - -fn run_package(options: &PackageOptions) -> Result<(), String> { - cargo_owned(&[ - "build".to_string(), - "-p".to_string(), - options.app.package().to_string(), - "--release".to_string(), - "--locked".to_string(), - "--offline".to_string(), - "--target".to_string(), - options.target.clone(), - ]) -} - -fn run_policy(root: &Path) -> Result<(), String> { - let mut failures = Vec::new(); - validate_toolchain_policy(root, &mut failures)?; - scan_policy_dir(root, &mut failures)?; - validate_cargo_metadata(root, &mut failures)?; - validate_cargo_metadata_dependency_closures(root, &mut failures)?; - validate_lockfile(root, &mut failures); - validate_workspace_license(root, &mut failures)?; - validate_dependency_boundaries(root, &mut failures)?; - if failures.is_empty() { - Ok(()) - } else { - Err(format!("workspace policy failed:\n{}", failures.join("\n"))) - } -} - -fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let manifest = root.join("Cargo.toml"); - if !manifest.exists() { - return Ok(()); - } - let metadata = workspace_metadata(root)?; - if metadata.workspace_members.is_empty() { - failures.push(format!( - "{}: cargo metadata produced no workspace members", - manifest.display() - )); - return Ok(()); - } - let stage_manifest_path = root.join(STAGE_PACKAGE_MANIFEST); - let stage_manifest = load_stage_package_manifest(&stage_manifest_path)?; - let workspace_packages = metadata - .workspace_packages() - .iter() - .map(|package| package.name.to_string()) - .collect::<BTreeSet<_>>(); - if let Err(err) = - validate_stage_package_entries(&stage_manifest, &workspace_packages, &stage_manifest_path) - { - failures.push(err); - } - Ok(()) -} - -fn validate_cargo_metadata_dependency_closures( - root: &Path, - failures: &mut Vec<String>, -) -> Result<(), String> { - let metadata = workspace_metadata(root)?; - let deps_by_package = workspace_dependency_names(&metadata); - validate_package_closure_excludes("fparkan-headless", &deps_by_package, failures); - Ok(()) -} - -/// Returns declared direct dependencies for each workspace package from Cargo's -/// own metadata model. Cargo, rather than a source-text heuristic, resolves -/// package identities and workspace membership before this policy sees them. -fn workspace_dependency_names( - metadata: &cargo_metadata::Metadata, -) -> BTreeMap<String, BTreeSet<String>> { - metadata - .workspace_packages() - .iter() - .map(|package| { - ( - package.name.to_string(), - package - .dependencies - .iter() - .map(|dependency| dependency.name.clone()) - .collect(), - ) - }) - .collect() -} - -fn validate_package_closure_excludes( - package: &str, - deps_by_package: &BTreeMap<String, BTreeSet<String>>, - failures: &mut Vec<String>, -) { - if !deps_by_package.contains_key(package) { - failures.push(format!( - "workspace manifest graph missing package {package}" - )); - return; - } - let closure = dependency_closure_names(package, deps_by_package); - if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&closure) { - failures.push(format!( - "workspace manifest closure: package {package} depends on forbidden platform/render dependency {forbidden}" - )); - } -} - -fn dependency_closure_names( - root: &str, - deps_by_package: &BTreeMap<String, BTreeSet<String>>, -) -> BTreeSet<String> { - let mut seen = BTreeSet::new(); - let mut names = BTreeSet::new(); - let mut stack = deps_by_package - .get(root) - .cloned() - .unwrap_or_default() - .into_iter() - .collect::<Vec<_>>(); - while let Some(name) = stack.pop() { - if !seen.insert(name.clone()) { - continue; - } - names.insert(name.clone()); - if let Some(deps) = deps_by_package.get(&name) { - stack.extend(deps.iter().cloned()); - } - } - names -} - -fn validate_toolchain_policy(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let toolchain_path = root.join("rust-toolchain.toml"); - let toolchain_text = fs::read_to_string(&toolchain_path) - .map_err(|err| format!("{}: {err}", toolchain_path.display()))?; - let toolchain = toml::from_str::<RustToolchainToml>(&toolchain_text) - .map_err(|err| format!("{}: invalid TOML: {err}", toolchain_path.display()))?; - if toolchain.toolchain.channel != PINNED_RUST_TOOLCHAIN { - failures.push(format!( - "{}: toolchain channel must be exact {PINNED_RUST_TOOLCHAIN}", - toolchain_path.display() - )); - } - if !is_exact_rust_patch_version(&toolchain.toolchain.channel) { - failures.push(format!( - "{}: toolchain channel must include major.minor.patch, not a moving channel", - toolchain_path.display() - )); - } - - let manifest_path = root.join("Cargo.toml"); - let manifest_text = fs::read_to_string(&manifest_path) - .map_err(|err| format!("{}: {err}", manifest_path.display()))?; - let manifest = toml::from_str::<WorkspaceManifestToml>(&manifest_text) - .map_err(|err| format!("{}: invalid TOML: {err}", manifest_path.display()))?; - if manifest.workspace.package.rust_version != WORKSPACE_MSRV { - failures.push(format!( - "{}: workspace.package.rust-version must be {WORKSPACE_MSRV}", - manifest_path.display() - )); - } - if !PINNED_RUST_TOOLCHAIN.starts_with(&format!("{}.", manifest.workspace.package.rust_version)) - { - failures.push(format!( - "{}: workspace.package.rust-version must match pinned toolchain major.minor", - manifest_path.display() - )); - } - Ok(()) -} - -fn is_exact_rust_patch_version(value: &str) -> bool { - let parts = value.split('.').collect::<Vec<_>>(); - parts.len() == 3 - && parts - .iter() - .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit())) -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RustToolchainToml { - toolchain: RustToolchainTable, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RustToolchainTable { - channel: String, - #[allow(dead_code)] - components: Option<Vec<String>>, - #[allow(dead_code)] - targets: Option<Vec<String>>, -} - -#[derive(Debug, Deserialize)] -struct WorkspaceManifestToml { - workspace: WorkspaceTable, -} - -#[derive(Debug, Deserialize)] -struct WorkspaceTable { - package: WorkspacePackageTable, -} - -#[derive(Debug, Deserialize)] -struct WorkspacePackageTable { - #[serde(rename = "rust-version")] - rust_version: String, -} - -fn validate_lockfile(root: &Path, failures: &mut Vec<String>) { - let lockfile = root.join("Cargo.lock"); - if !lockfile.is_file() { - failures.push(format!( - "{}: workspace lockfile is required for locked/offline builds", - lockfile.display() - )); - } -} - -fn validate_lockfile_supply_chain(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let lockfile = root.join("Cargo.lock"); - let packages = read_lockfile_packages(&lockfile)?; - for package in packages { - if let Some(source) = package.source.as_deref() { - if source != APPROVED_REGISTRY_SOURCE { - failures.push(format!( - "{}: package {} {} uses unapproved source {source}", - lockfile.display(), - package.name, - package.version - )); - } - } - if SUPPLY_CHAIN_BANNED_PACKAGES.contains(&package.name.as_str()) { - failures.push(format!( - "{}: package {} {} is banned by built-in supply-chain policy", - lockfile.display(), - package.name, - package.version - )); - } - } - Ok(()) -} - -fn read_lockfile_packages(lockfile: &Path) -> Result<Vec<CargoLockPackage>, String> { - let text = - fs::read_to_string(lockfile).map_err(|err| format!("{}: {err}", lockfile.display()))?; - let parsed = toml::from_str::<CargoLock>(&text) - .map_err(|err| format!("{}: invalid Cargo.lock TOML: {err}", lockfile.display()))?; - Ok(parsed.package) -} - -#[derive(Debug, Deserialize)] -struct CargoLock { - package: Vec<CargoLockPackage>, -} - -#[derive(Debug, Deserialize)] -struct CargoLockPackage { - name: String, - version: String, - source: Option<String>, -} - -fn validate_workspace_license(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let manifest = root.join("Cargo.toml"); - let license = fs::read_to_string(root.join("LICENSE.txt")) - .map_err(|err| format!("{}: {err}", root.join("LICENSE.txt").display()))?; - let expected = if license.contains("GNU GENERAL PUBLIC LICENSE") - && license.contains("Version 2, June 1991") - { - "GPL-2.0-only" - } else { - failures.push(format!( - "{}: unsupported repository license text", - root.join("LICENSE.txt").display() - )); - return Ok(()); - }; - - let mut manifests = Vec::new(); - collect_cargo_manifests(root, &mut manifests)?; - manifests.push(manifest); - manifests.sort(); - manifests.dedup(); - - for manifest in manifests { - let policy = load_cargo_manifest_policy(&manifest)?; - let explicit_license = policy.license(); - let is_root = manifest == root.join("Cargo.toml"); - if is_root { - if explicit_license.as_deref() != Some(expected) { - failures.push(format!( - "{}: workspace.package license must be {expected}", - manifest.display() - )); - } - } else if let Some(license) = explicit_license { - if license != expected { - failures.push(format!( - "{}: package license {license} does not match repository license {expected}", - manifest.display() - )); - } - } - } - Ok(()) -} - -fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let mut manifests = Vec::new(); - collect_cargo_manifests(root, &mut manifests)?; - for manifest in manifests { - let policy = load_cargo_manifest_policy(&manifest)?; - let Some(package) = policy.package_name() else { - continue; - }; - if is_removed_legacy_adapter_manifest(root, &manifest) { - failures.push(format!( - "{}: legacy SDL/OpenGL adapter crate must be removed: {package}", - manifest.display() - )); - continue; - } - let dependencies = policy.dependency_names(); - if !is_adapter_like_package(package) && !is_app_package(package) { - for dependency in &dependencies { - if is_forbidden_gui_dependency(dependency) { - failures.push(format!( - "{}: package {package} depends on forbidden GUI/adapter package {dependency}", - manifest.display() - )); - } - } - } - if is_app_package(package) { - if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) { - failures.push(format!( - "{}: app package {package} depends on parser crate {forbidden}", - manifest.display() - )); - } - } - if package == "fparkan-headless" { - if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&dependencies) { - failures.push(format!( - "{}: headless package {package} depends on platform/render bridge dependency {forbidden}", - manifest.display() - )); - } - } - - if package == "fparkan-runtime" { - if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) { - failures.push(format!( - "{}: runtime package {package} depends on parser crate {forbidden}", - manifest.display() - )); - } - if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&dependencies) { - failures.push(format!( - "{}: runtime package {package} depends on forbidden platform/driver crate {forbidden}", - manifest.display() - )); - } - } - - if package == "fparkan-prototype" { - if let Some(forbidden) = first_forbidden_visual_dependency(&dependencies) { - failures.push(format!( - "{}: prototype package {package} depends on forbidden visual parser {forbidden}", - manifest.display() - )); - } - } - } - Ok(()) -} - -fn is_app_package(package: &str) -> bool { - matches!( - package, - "fparkan-cli" - | "fparkan-game" - | "fparkan-headless" - | "fparkan-vulkan-smoke" - | "fparkan-viewer" - ) -} - -fn is_adapter_like_package(package: &str) -> bool { - matches!(package, "fparkan-platform-winit" | "fparkan-render-vulkan") -} - -fn first_forbidden_parser_dependency(dependencies: &BTreeSet<String>) -> Option<&str> { - [ - "fparkan-msh", - "fparkan-nres", - "fparkan-rsli", - "fparkan-terrain-format", - "fparkan-texm", - "fparkan-mission-format", - "fparkan-material", - "fparkan-fx", - ] - .iter() - .find_map(|forbidden| { - if dependencies.contains(*forbidden) { - Some(*forbidden) - } else { - None - } - }) -} - -fn first_forbidden_visual_dependency(dependencies: &BTreeSet<String>) -> Option<&str> { - [ - "fparkan-msh", - "fparkan-material", - "fparkan-texm", - "fparkan-fx", - "fparkan-terrain-format", - ] - .iter() - .find_map(|forbidden| { - if dependencies.contains(*forbidden) { - Some(*forbidden) - } else { - None - } - }) -} - -fn first_forbidden_platform_bridge_dependency(dependencies: &BTreeSet<String>) -> Option<&str> { - [ - "fparkan-platform-winit", - "fparkan-render-vulkan", - "winit", - "ash", - "ash-window", - ] - .iter() - .find_map(|forbidden| { - if dependencies.contains(*forbidden) { - Some(*forbidden) - } else { - None - } - }) -} - -fn is_forbidden_domain_dependency(dependency: &str) -> bool { - matches!( - dependency, - "fparkan-cli" - | "fparkan-game" - | "fparkan-headless" - | "fparkan-viewer" - | "fparkan-platform-sdl" - | "fparkan-render-gl" - | "sdl2" - | "gl" - | "glow" - | "glium" - | "glutin" - ) -} - -fn is_forbidden_gui_dependency(dependency: &str) -> bool { - is_forbidden_domain_dependency(dependency) || is_forbidden_platform_dependency(dependency) -} - -fn is_forbidden_platform_dependency(dependency: &str) -> bool { - matches!( - dependency, - "fparkan-platform-winit" | "fparkan-render-vulkan" | "winit" | "ash" | "ash-window" - ) -} - -fn collect_cargo_manifests(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> { - let entries = fs::read_dir(dir).map_err(|err| format!("{}: {err}", dir.display()))?; - for entry in entries { - let entry = entry.map_err(|err| format!("{}: {err}", dir.display()))?; - let path = entry.path(); - if should_skip_policy_path(&path) { - continue; - } - let file_type = entry - .file_type() - .map_err(|err| format!("{}: {err}", path.display()))?; - if file_type.is_dir() { - collect_cargo_manifests(&path, out)?; - } else if file_type.is_file() - && path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name == "Cargo.toml") - { - out.push(path); - } - } - Ok(()) -} - -/// Typed Cargo manifest subset used by the architecture policy. -/// -/// Dependency values deliberately remain `toml::Value`: the policy only needs -/// keys and Cargo accepts several value forms (version string, inline table, -/// or workspace inheritance). -#[derive(Debug, Default, Deserialize)] -struct CargoManifestPolicy { - package: Option<CargoPackagePolicy>, - workspace: Option<CargoWorkspacePolicy>, - dependencies: Option<BTreeMap<String, toml::Value>>, - #[serde(rename = "dev-dependencies")] - dev_dependencies: Option<BTreeMap<String, toml::Value>>, - #[serde(rename = "build-dependencies")] - build_dependencies: Option<BTreeMap<String, toml::Value>>, -} - -#[derive(Debug, Deserialize)] -struct CargoPackagePolicy { - name: String, - license: Option<toml::Value>, -} - -#[derive(Debug, Deserialize)] -struct CargoWorkspacePolicy { - package: Option<CargoWorkspacePackagePolicy>, -} - -#[derive(Debug, Deserialize)] -struct CargoWorkspacePackagePolicy { - license: Option<toml::Value>, -} - -impl CargoManifestPolicy { - fn package_name(&self) -> Option<&str> { - self.package.as_ref().map(|package| package.name.as_str()) - } - - fn license(&self) -> Option<String> { - self.package - .as_ref() - .and_then(|package| cargo_license_string(package.license.as_ref())) - .or_else(|| { - self.workspace - .as_ref() - .and_then(|workspace| workspace.package.as_ref()) - .and_then(|package| cargo_license_string(package.license.as_ref())) - }) - } - - fn dependency_names(&self) -> BTreeSet<String> { - [ - self.dependencies.as_ref(), - self.dev_dependencies.as_ref(), - self.build_dependencies.as_ref(), - ] - .into_iter() - .flatten() - .flat_map(|dependencies| dependencies.keys().cloned()) - .collect() - } -} - -fn cargo_license_string(value: Option<&toml::Value>) -> Option<String> { - value.and_then(toml::Value::as_str).map(ToString::to_string) -} - -fn load_cargo_manifest_policy(path: &Path) -> Result<CargoManifestPolicy, String> { - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - toml::from_str(&text).map_err(|err| format!("{}: invalid Cargo TOML: {err}", path.display())) -} - -fn is_removed_legacy_adapter_manifest(root: &Path, manifest: &Path) -> bool { - let normalized = manifest.strip_prefix(root).unwrap_or(manifest); - normalized.starts_with("adapters/fparkan-platform-sdl") - || normalized.starts_with("adapters/fparkan-render-gl") -} - -fn scan_policy_dir(dir: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let entries = match fs::read_dir(dir) { - Ok(entries) => entries, - Err(err) => return Err(format!("{}: {err}", dir.display())), - }; - for entry in entries { - let entry = entry.map_err(|err| format!("{}: {err}", dir.display()))?; - let path = entry.path(); - if should_skip_policy_path(&path) { - continue; - } - let file_type = entry - .file_type() - .map_err(|err| format!("{}: {err}", path.display()))?; - if file_type.is_dir() { - if is_forbidden_generic_crate_dir(&path) { - failures.push(format!( - "{}: package under crates/ must use the fparkan-* prefix", - path.display() - )); - } - scan_policy_dir(&path, failures)?; - } else if file_type.is_file() { - scan_repository_file_policy(&path, failures)?; - if is_policy_source(&path) { - scan_policy_file(&path, failures)?; - } - } - } - Ok(()) -} - -fn should_skip_policy_path(path: &Path) -> bool { - path.file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| { - matches!( - name, - ".git" | "target" | "testdata" | ".idea" | ".vscode" | ".DS_Store" - ) - }) -} - -fn is_policy_source(path: &Path) -> bool { - path.extension() - .and_then(|ext| ext.to_str()) - .is_some_and(|ext| matches!(ext, "rs" | "toml")) -} - -fn is_forbidden_generic_crate_dir(path: &Path) -> bool { - path.parent() - .and_then(Path::file_name) - .and_then(|name| name.to_str()) - .is_some_and(|name| name == "crates") - && path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| !name.starts_with("fparkan-")) -} - -fn scan_repository_file_policy(path: &Path, failures: &mut Vec<String>) -> Result<(), String> { - if path - .extension() - .and_then(|ext| ext.to_str()) - .is_some_and(|ext| ext == "py") - { - failures.push(format!( - "{}: Python source file is forbidden", - path.display() - )); - } - - let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?; - if bytes.starts_with(b"#!") { - let first_line = bytes - .split(|byte| *byte == b'\n') - .next() - .unwrap_or_default() - .to_ascii_lowercase(); - if first_line - .windows("python".len()) - .any(|window| window == b"python") - { - failures.push(format!("{}: Python shebang is forbidden", path.display())); - } - } - if is_workflow_file(path) { - let text = String::from_utf8_lossy(&bytes).to_ascii_lowercase(); - if text.contains("python") { - failures.push(format!("{}: Python CI step is forbidden", path.display())); - } - } - Ok(()) -} - -fn is_workflow_file(path: &Path) -> bool { - let mut previous = None; - for component in path.components() { - let name = component.as_os_str().to_string_lossy(); - if previous.as_deref() == Some(".github") && name == "workflows" { - return true; - } - previous = Some(name.into_owned()); - } - false -} - -fn scan_policy_file(path: &Path, failures: &mut Vec<String>) -> Result<(), String> { - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - let lower = text.to_ascii_lowercase(); - if lower.contains(concat!("app.", "notion.com")) || lower.contains(concat!("385e", "79f2")) { - failures.push(format!( - "{}: external knowledge-base reference in source", - path.display() - )); - } - validate_unsafe_module_attribute_policy(path, &text, failures); - let mut previous_line_has_safety_comment = false; - for (index, line) in text.lines().enumerate() { - let trimmed = line.trim_start(); - if is_comment_line(trimmed) { - previous_line_has_safety_comment = has_safety_comment(trimmed); - continue; - } - if trimmed.is_empty() { - previous_line_has_safety_comment = false; - continue; - } - if contains_unsafe_construct(trimmed) - && !is_authorized_unsafe_construct(path, trimmed, previous_line_has_safety_comment) - { - failures.push(format!( - "{}:{}: unsafe construct in workspace source", - path.display(), - index + 1 - )); - } - previous_line_has_safety_comment = false; - } - Ok(()) -} - -fn validate_unsafe_module_attribute_policy(path: &Path, text: &str, failures: &mut Vec<String>) { - let declares_unsafe_module_boundary = text - .lines() - .map(str::trim_start) - .any(|line| line.starts_with("#![allow(unsafe_code)]")); - match ( - is_audited_unsafe_source(path), - declares_unsafe_module_boundary, - ) { - (true, false) => failures.push(format!( - "{}: audited Vulkan FFI module must declare #![allow(unsafe_code)]", - path.display() - )), - (false, true) => failures.push(format!( - "{}: #![allow(unsafe_code)] is only permitted in audited Vulkan FFI modules", - path.display() - )), - (true, true) | (false, false) => {} - } -} - -fn contains_unsafe_construct(line: &str) -> bool { - line.contains(concat!("un", "safe {")) - || line.contains(concat!("un", "safe fn")) - || line.contains(concat!("un", "safe impl")) - || line.contains(concat!("extern ", "\"C\"")) -} - -fn is_comment_line(line: &str) -> bool { - line.starts_with("//") || line.starts_with("//!") || line.starts_with("///") -} - -fn has_safety_comment(line: &str) -> bool { - line.contains("SAFETY:") -} - -const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[ - "adapters/fparkan-render-vulkan/src/ffi/instance.rs", - "adapters/fparkan-render-vulkan/src/ffi/capabilities.rs", - "adapters/fparkan-render-vulkan/src/ffi/resources.rs", - "adapters/fparkan-render-vulkan/src/ffi/runtime.rs", - "adapters/fparkan-render-vulkan/src/ffi/smoke.rs", - "adapters/fparkan-render-vulkan/src/ffi/swapchain.rs", - "adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs", - "adapters/fparkan-render-vulkan/src/ffi/surface.rs", - "adapters/fparkan-render-vulkan/src/ffi/validation.rs", -]; - -fn is_audited_unsafe_source(path: &Path) -> bool { - let as_path = path.as_os_str().to_string_lossy().replace('\\', "/"); - AUDITED_UNSAFE_SOURCE_FILES - .iter() - .any(|candidate| as_path.ends_with(candidate)) -} - -fn is_authorized_unsafe_construct( - path: &Path, - line: &str, - previous_line_has_safety_comment: bool, -) -> bool { - if !is_audited_unsafe_source(path) { - return false; - } - previous_line_has_safety_comment || has_safety_comment(line) -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum Stage { - All, - Number(u8), -} - -impl Stage { - fn parse(value: &str) -> Result<Self, String> { - if value == "all" { - return Ok(Self::All); - } - let stage = value - .parse::<u8>() - .map_err(|_| format!("invalid stage: {value}"))?; - if stage <= 5 { - Ok(Self::Number(stage)) - } else { - Err(format!("stage out of range: {stage}")) - } - } -} - -impl fmt::Display for Stage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::All => f.write_str("all"), - Self::Number(stage) => write!(f, "{stage}"), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct TestOptions { - stage: Stage, - root: PathBuf, - manifest: Option<PathBuf>, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum TestSuite { - Licensed, - Synthetic, -} - -impl TestSuite { - fn parse(value: &str) -> Result<Self, String> { - match value { - "licensed" => Ok(Self::Licensed), - "synthetic" => Ok(Self::Synthetic), - _ => Err(format!("unknown suite: {value}")), - } - } - - fn as_str(self) -> &'static str { - match self { - Self::Licensed => "licensed", - Self::Synthetic => "synthetic", - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct AcceptanceOptions { - suite: TestSuite, - stage: Stage, - root: PathBuf, - manifest: Option<PathBuf>, - out: PathBuf, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct AuditOptions { - roadmap: PathBuf, - coverage: PathBuf, - out: PathBuf, - strict: bool, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct NativeSmokeAuditOptions { - dir: PathBuf, - expected_commit: String, - expected_shader_manifest_hash: String, -} - -#[derive(Debug, Deserialize)] -struct ShaderManifestJson { - target_env: String, - compiler: ShaderToolManifestJson, - validator: ShaderToolManifestJson, - modules: Vec<ShaderModuleManifestJson>, - manifest_hash: String, -} - -#[derive(Debug, Deserialize)] -struct ShaderToolManifestJson { - name: String, - version: String, - binary_sha256: String, -} - -#[derive(Debug, Deserialize)] -struct ShaderModuleManifestJson { - name: String, - stage: String, - entry_point: String, - source_path: String, - source_sha256: String, - spirv_path: String, - sha256: String, -} - -fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOptions, String> { - let mut options = TestOptions { - stage: Stage::All, - root: default_root, - manifest: None, - }; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--stage" => { - let value = iter - .next() - .ok_or_else(|| "--stage requires a value".to_string())?; - options.stage = Stage::parse(value)?; - } - "--root" => { - let value = iter - .next() - .ok_or_else(|| "--root requires a path".to_string())?; - options.root = PathBuf::from(value); - } - "--manifest" => { - let value = iter - .next() - .ok_or_else(|| "--manifest requires a path".to_string())?; - options.manifest = Some(PathBuf::from(value)); - } - _ => return Err(format!("unknown test option: {arg}")), - } - } - Ok(options) -} - -fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String> { - let mut suite = None; - let mut stage = Stage::All; - let mut root = PathBuf::from("testdata"); - let mut manifest = None; - let mut out = None; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--suite" => { - let value = iter - .next() - .ok_or_else(|| "--suite requires a value".to_string())?; - suite = Some(TestSuite::parse(value)?); - } - "--stage" => { - let value = iter - .next() - .ok_or_else(|| "--stage requires a value".to_string())?; - stage = Stage::parse(value)?; - } - "--root" => { - let value = iter - .next() - .ok_or_else(|| "--root requires a path".to_string())?; - root = PathBuf::from(value); - } - "--manifest" => { - let value = iter - .next() - .ok_or_else(|| "--manifest requires a path".to_string())?; - manifest = Some(PathBuf::from(value)); - } - "--out" => { - let value = iter - .next() - .ok_or_else(|| "--out requires a path".to_string())?; - out = Some(PathBuf::from(value)); - } - _ => return Err(format!("unknown acceptance option: {arg}")), - } - } - - let suite = suite.ok_or_else(|| "missing --suite".to_string())?; - let out = out.unwrap_or_else(|| { - PathBuf::from("target") - .join("fparkan") - .join("reports") - .join("acceptance") - .join(format!("{}-stage-{}.json", suite.as_str(), stage)) - }); - Ok(AcceptanceOptions { - suite, - stage, - root, - manifest, - out, - }) -} - -fn parse_audit_options(args: &[String]) -> Result<AuditOptions, String> { - let mut roadmap = workspace_relative_path(CI_ACCEPTANCE_ROADMAP); - let mut coverage = workspace_relative_path(CI_ACCEPTANCE_COVERAGE); - let mut out = workspace_root_path() - .join("target") - .join("fparkan") - .join("reports") - .join("acceptance") - .join("coverage-audit.json"); - let mut strict = false; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--roadmap" => { - let value = iter - .next() - .ok_or_else(|| "--roadmap requires a path".to_string())?; - roadmap = PathBuf::from(value); - } - "--coverage" => { - let value = iter - .next() - .ok_or_else(|| "--coverage requires a path".to_string())?; - coverage = PathBuf::from(value); - } - "--out" => { - let value = iter - .next() - .ok_or_else(|| "--out requires a path".to_string())?; - out = PathBuf::from(value); - } - "--strict" => strict = true, - _ => return Err(format!("unknown audit option: {arg}")), - } - } - Ok(AuditOptions { - roadmap, - coverage, - out, - strict, - }) -} - -fn parse_native_smoke_audit_options(args: &[String]) -> Result<NativeSmokeAuditOptions, String> { - let mut dir = None; - let mut expected_commit = expected_native_smoke_commit()?; - let mut expected_shader_manifest_hash = current_shader_manifest_hash()?; - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - match arg.as_str() { - "--dir" => { - let value = iter - .next() - .ok_or_else(|| "--dir requires a path".to_string())?; - dir = Some(PathBuf::from(value)); - } - "--expected-commit" => { - let value = iter - .next() - .ok_or_else(|| "--expected-commit requires a value".to_string())?; - if !is_commit_sha(value) { - return Err(format!( - "--expected-commit must be a 40-character lowercase or uppercase hex string, found {value:?}" - )); - } - expected_commit.clone_from(value); - } - "--expected-shader-manifest-hash" => { - let value = iter.next().ok_or_else(|| { - "--expected-shader-manifest-hash requires a value".to_string() - })?; - if value.trim().is_empty() { - return Err("--expected-shader-manifest-hash must be non-empty".to_string()); - } - expected_shader_manifest_hash.clone_from(value); - } - _ => return Err(format!("unknown native-smoke audit option: {arg}")), - } - } - Ok(NativeSmokeAuditOptions { - dir: dir.ok_or_else(|| "native-smoke audit requires --dir".to_string())?, - expected_commit, - expected_shader_manifest_hash, - }) -} - -fn run_native_smoke_audit(options: &NativeSmokeAuditOptions) -> Result<(), String> { - let reports = read_native_smoke_reports(&options.dir)?; - let failures = audit_native_smoke_reports( - &reports, - &options.expected_commit, - &options.expected_shader_manifest_hash, - ); - if failures.is_empty() { - println!("native smoke artifacts passed: {}", options.dir.display()); - Ok(()) - } else { - Err(format!( - "native smoke artifacts incomplete:\n{}", - failures.join("\n") - )) - } -} - -fn read_native_smoke_reports(dir: &Path) -> Result<BTreeMap<String, serde_json::Value>, String> { - let mut reports = BTreeMap::new(); - let entries = fs::read_dir(dir).map_err(|err| format!("{}: {err}", dir.display()))?; - for entry in entries { - let entry = entry.map_err(|err| format!("{}: {err}", dir.display()))?; - let path = entry.path(); - if path.extension().and_then(|value| value.to_str()) != Some("json") { - continue; - } - let text = fs::read_to_string(&path).map_err(|err| format!("{}: {err}", path.display()))?; - let json = serde_json::from_str::<serde_json::Value>(&text) - .map_err(|err| format!("{}: {err}", path.display()))?; - let platform = json_string_field(&json, "platform") - .map_err(|err| format!("{}: {err}", path.display()))?; - let platform = platform.to_string(); - if reports.insert(platform.clone(), json).is_some() { - return Err(format!( - "{}: duplicate native smoke report for platform {platform}", - path.display() - )); - } - } - Ok(reports) -} - -fn audit_native_smoke_reports( - reports: &BTreeMap<String, serde_json::Value>, - expected_commit: &str, - expected_shader_manifest_hash: &str, -) -> Vec<String> { - let mut failures = Vec::new(); - let mut commit_shas = BTreeSet::new(); - let mut rust_toolchains = BTreeSet::new(); - for platform in REQUIRED_NATIVE_SMOKE_PLATFORMS { - let Some(report) = reports.get(*platform) else { - failures.push(format!("{platform}: missing native smoke report")); - continue; - }; - validate_native_smoke_report(platform, report, &mut failures); - expect_string_field( - platform, - report, - "commit_sha", - expected_commit, - &mut failures, - ); - expect_string_field( - platform, - report, - "shader_manifest_hash", - expected_shader_manifest_hash, - &mut failures, - ); - if let Ok(commit_sha) = json_string_field(report, "commit_sha") { - if commit_sha == "unknown" { - failures.push(format!("{platform}: commit_sha must not be \"unknown\"")); - } else { - commit_shas.insert(commit_sha.to_string()); - } - } - if let Ok(toolchain) = json_string_field(report, "rust_toolchain") { - rust_toolchains.insert(toolchain.to_string()); - } - } - for platform in reports.keys() { - if !REQUIRED_NATIVE_SMOKE_PLATFORMS.contains(&platform.as_str()) { - failures.push(format!("{platform}: unexpected native smoke platform")); - } - } - if commit_shas.len() > 1 { - failures.push(format!( - "native smoke reports disagree on commit_sha: {}", - commit_shas.into_iter().collect::<Vec<_>>().join(", ") - )); - } - if rust_toolchains.len() > 1 { - failures.push(format!( - "native smoke reports disagree on rust_toolchain: {}", - rust_toolchains.into_iter().collect::<Vec<_>>().join(", ") - )); - } - failures -} - -fn expected_native_smoke_commit() -> Result<String, String> { - if let Ok(commit_sha) = std::env::var("GITHUB_SHA") { - if is_commit_sha(&commit_sha) { - return Ok(commit_sha); - } - return Err(format!( - "GITHUB_SHA must be a 40-character lowercase or uppercase hex string when set, found {commit_sha:?}" - )); - } - let commit_sha = current_git_commit_sha(); - if is_commit_sha(&commit_sha) { - Ok(commit_sha) - } else { - Err( - "native-smoke audit could not resolve expected commit from GITHUB_SHA or git rev-parse HEAD" - .to_string(), - ) - } -} - -fn current_shader_manifest_hash() -> Result<String, String> { - let manifest_path = workspace_relative_path(SHADER_MANIFEST_REPORT); - let manifest = load_shader_manifest(&manifest_path)?; - Ok(manifest.manifest_hash) -} - -fn validate_native_smoke_report( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - validate_native_smoke_status_fields(platform, report, failures); - validate_native_smoke_provenance_fields(platform, report, failures); - validate_native_smoke_runtime_fields(platform, report, failures); -} - -fn validate_native_smoke_status_fields( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - expect_string_field( - platform, - report, - "schema_version", - "fparkan-native-smoke-v1", - failures, - ); - expect_string_field(platform, report, "status", "passed", failures); - expect_string_field( - platform, - report, - "vulkan_loader_status", - "available", - failures, - ); - expect_string_field( - platform, - report, - "vulkan_instance_status", - "created", - failures, - ); - expect_string_field(platform, report, "window_status", "created", failures); - expect_string_field( - platform, - report, - "vulkan_surface_status", - "created", - failures, - ); - expect_string_field( - platform, - report, - "vulkan_device_status", - "selected", - failures, - ); - expect_string_field( - platform, - report, - "vulkan_logical_device_status", - "created", - failures, - ); - expect_string_field( - platform, - report, - "vulkan_swapchain_status", - "created", - failures, - ); -} - -fn validate_native_smoke_provenance_fields( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - expect_commit_sha(platform, report, failures); - expect_bool_field(platform, report, "git_dirty", failures); - expect_bool_field_value(platform, report, "git_dirty", false, failures); - expect_nonempty_string(platform, report, "runner_identity", failures); - expect_nonempty_string(platform, report, "runner_architecture", failures); - expect_runner_architecture_matches_platform(platform, report, failures); - expect_string_field( - platform, - report, - "rust_toolchain", - &measured_rust_toolchain_version(), - failures, - ); - expect_string_field(platform, report, "platform", platform, failures); - expect_nonempty_string(platform, report, "target_triple", failures); - expect_target_triple_matches_platform(platform, report, failures); -} - -fn validate_native_smoke_runtime_fields( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - expect_u64_at_least(platform, report, "frames", 300, failures); - expect_u64_at_least(platform, report, "resize_count", 1, failures); - expect_u64_at_least(platform, report, "swapchain_recreate_count", 1, failures); - expect_u64_field(platform, report, "validation_warning_count", 0, failures); - expect_u64_field(platform, report, "validation_error_count", 0, failures); - expect_nonempty_string(platform, report, "shader_manifest_hash", failures); - expect_nonempty_string(platform, report, "vulkan_device_name", failures); - expect_u64_at_least( - platform, - report, - "vulkan_logical_device_enabled_extension_count", - 1, - failures, - ); - expect_u64_at_least( - platform, - report, - "vulkan_logical_device_graphics_queue_family", - 0, - failures, - ); - expect_u64_at_least( - platform, - report, - "vulkan_logical_device_present_queue_family", - 0, - failures, - ); - expect_u64_at_least(platform, report, "vulkan_swapchain_width", 1, failures); - expect_u64_at_least(platform, report, "vulkan_swapchain_height", 1, failures); - expect_u64_at_least( - platform, - report, - "vulkan_swapchain_image_count", - 2, - failures, - ); -} - -fn expect_string_field( - platform: &str, - report: &serde_json::Value, - field: &str, - expected: &str, - failures: &mut Vec<String>, -) { - match json_string_field(report, field) { - Ok(actual) if actual == expected => {} - Ok(actual) => failures.push(format!( - "{platform}: {field} expected {expected:?}, found {actual:?}" - )), - Err(err) => failures.push(format!("{platform}: {err}")), - } -} - -fn expect_nonempty_string( - platform: &str, - report: &serde_json::Value, - field: &str, - failures: &mut Vec<String>, -) { - match json_string_field(report, field) { - Ok(value) if !value.trim().is_empty() => {} - Ok(_) => failures.push(format!("{platform}: {field} must be non-empty")), - Err(err) => failures.push(format!("{platform}: {err}")), - } -} - -fn expect_bool_field( - platform: &str, - report: &serde_json::Value, - field: &str, - failures: &mut Vec<String>, -) { - match report.get(field) { - Some(serde_json::Value::Bool(_)) => {} - Some(_) => failures.push(format!("{platform}: {field} must be a boolean")), - None => failures.push(format!("{platform}: missing {field}")), - } -} - -fn expect_bool_field_value( - platform: &str, - report: &serde_json::Value, - field: &str, - expected: bool, - failures: &mut Vec<String>, -) { - match report.get(field) { - Some(serde_json::Value::Bool(actual)) if *actual == expected => {} - Some(serde_json::Value::Bool(actual)) => { - failures.push(format!( - "{platform}: {field} expected {expected}, found {actual}" - )); - } - Some(_) => failures.push(format!("{platform}: {field} must be a boolean")), - None => failures.push(format!("{platform}: missing {field}")), - } -} - -fn expect_commit_sha(platform: &str, report: &serde_json::Value, failures: &mut Vec<String>) { - match report.get("commit_sha") { - Some(serde_json::Value::String(commit_sha)) - if commit_sha.len() == 40 && commit_sha.chars().all(|ch| ch.is_ascii_hexdigit()) => {} - Some(serde_json::Value::String(_)) => failures.push(format!( - "{platform}: commit_sha must be a 40-character lowercase or uppercase hex string" - )), - Some(_) => failures.push(format!("{platform}: commit_sha must be a string")), - None => failures.push(format!("{platform}: missing commit_sha")), - } -} - -fn expect_target_triple_matches_platform( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - let Ok(target_triple) = json_string_field(report, "target_triple") else { - return; - }; - let matches_platform = match platform { - "windows" => target_triple.contains("windows"), - _ => true, - }; - if !matches_platform { - failures.push(format!( - "{platform}: target_triple {target_triple:?} does not match platform" - )); - } -} - -fn expect_runner_architecture_matches_platform( - platform: &str, - report: &serde_json::Value, - failures: &mut Vec<String>, -) { - let Ok(runner_architecture) = json_string_field(report, "runner_architecture") else { - return; - }; - let matches_platform = !runner_architecture.trim().is_empty(); - if !matches_platform { - failures.push(format!( - "{platform}: runner_architecture {runner_architecture:?} does not match platform policy" - )); - } -} - -fn expect_u64_at_least( - platform: &str, - report: &serde_json::Value, - field: &str, - minimum: u64, - failures: &mut Vec<String>, -) { - match json_u64_field(report, field) { - Ok(value) if value >= minimum => {} - Ok(value) => failures.push(format!( - "{platform}: {field} expected >= {minimum}, found {value}" - )), - Err(err) => failures.push(format!("{platform}: {err}")), - } -} - -fn expect_u64_field( - platform: &str, - report: &serde_json::Value, - field: &str, - expected: u64, - failures: &mut Vec<String>, -) { - match json_u64_field(report, field) { - Ok(value) if value == expected => {} - Ok(value) => failures.push(format!( - "{platform}: {field} expected {expected}, found {value}" - )), - Err(err) => failures.push(format!("{platform}: {err}")), - } -} - -fn json_string_field<'a>(json: &'a serde_json::Value, field: &str) -> Result<&'a str, String> { - json.get(field) - .and_then(serde_json::Value::as_str) - .ok_or_else(|| format!("{field} must be a string")) -} - -fn json_u64_field(json: &serde_json::Value, field: &str) -> Result<u64, String> { - json.get(field) - .and_then(serde_json::Value::as_u64) - .ok_or_else(|| format!("{field} must be an unsigned integer")) -} - -fn run_acceptance_audit(options: &AuditOptions) -> Result<(), String> { - let roadmap_text = fs::read_to_string(&options.roadmap) - .map_err(|err| format!("{}: {err}", options.roadmap.display()))?; - let required = extract_acceptance_ids(&roadmap_text); - let coverage = read_coverage_manifest(&options.coverage)?; - let audit = build_acceptance_audit(&required, &coverage); - if let Some(parent) = options.out.parent() { - fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; - } - let rendered = render_audit_json(&audit)?; - fs::write(&options.out, rendered).map_err(|err| format!("{}: {err}", options.out.display()))?; - println!("{}", options.out.display()); - let strict_failures = audit.strict_failures(); - if options.strict && (!strict_failures.is_empty() || !audit.unknown_coverage.is_empty()) { - Err(format!( - "acceptance coverage incomplete: {} strict failures, {} unknown", - strict_failures.len(), - audit.unknown_coverage.len() - )) - } else { - Ok(()) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct CoverageEntry { - status: CoverageStatus, - evidence: String, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum CoverageStatus { - Covered, - CoveredPlanning, - CoveredStub, - CoveredGpu, - Partial, - Blocked, - Omitted, -} - -impl CoverageStatus { - fn parse(value: &str) -> Result<Self, String> { - match value { - "covered" => Ok(Self::Covered), - "covered-planning" => Ok(Self::CoveredPlanning), - "covered-stub" => Ok(Self::CoveredStub), - "covered-gpu" => Ok(Self::CoveredGpu), - "partial" => Ok(Self::Partial), - "blocked" => Ok(Self::Blocked), - "omitted" => Ok(Self::Omitted), - _ => Err(format!("unknown coverage status: {value}")), - } - } - - const fn as_str(self) -> &'static str { - match self { - Self::Covered => "covered", - Self::CoveredPlanning => "covered-planning", - Self::CoveredStub => "covered-stub", - Self::CoveredGpu => "covered-gpu", - Self::Partial => "partial", - Self::Blocked => "blocked", - Self::Omitted => "omitted", - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct AcceptanceAudit { - commit_sha: String, - git_dirty: bool, - runner_identity: String, - rust_toolchain: String, - msrv: String, - required_total: usize, - covered: Vec<String>, - covered_planning: Vec<String>, - covered_stub: Vec<String>, - covered_gpu: Vec<String>, - partial: Vec<String>, - blocked: Vec<String>, - omitted: Vec<String>, - missing: Vec<String>, - invalid_coverage: Vec<String>, - unknown_coverage: Vec<String>, - coverage_evidence: BTreeMap<String, String>, - by_stage: BTreeMap<String, usize>, -} - -impl AcceptanceAudit { - fn covered_total(&self) -> usize { - self.covered.len() - + self.covered_planning.len() - + self.covered_stub.len() - + self.covered_gpu.len() - } - - fn unverified(&self) -> Vec<String> { - self.partial - .iter() - .chain(&self.blocked) - .chain(&self.omitted) - .chain(&self.missing) - .chain(&self.invalid_coverage) - .cloned() - .collect() - } - - fn strict_failures(&self) -> Vec<String> { - self.partial - .iter() - .chain(&self.blocked) - .chain(&self.omitted) - .chain(&self.missing) - .chain(&self.invalid_coverage) - .cloned() - .collect() - } -} - -fn extract_acceptance_ids(text: &str) -> BTreeSet<String> { - let mut ids = BTreeSet::new(); - for segment in text.split('`') { - if is_acceptance_id(segment) { - ids.insert(segment.to_string()); - } - } - ids -} - -fn is_acceptance_id(value: &str) -> bool { - let bytes = value.as_bytes(); - bytes.len() >= 9 - && matches!(bytes[0], b'S' | b'L') - && matches!(bytes[1], b'0'..=b'5') - && bytes[2] == b'-' - && bytes.iter().all(|byte| { - byte.is_ascii_uppercase() || byte.is_ascii_digit() || *byte == b'-' || *byte == b'_' - }) -} - -fn read_coverage_manifest(path: &Path) -> Result<BTreeMap<String, CoverageEntry>, String> { - if !path.exists() { - return Ok(BTreeMap::new()); - } - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - let mut entries = BTreeMap::new(); - for (line_number, line) in text.lines().enumerate() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } - let mut fields = trimmed.splitn(3, '\t'); - let id = fields - .next() - .ok_or_else(|| format!("{}:{}: missing id", path.display(), line_number + 1))?; - let status = fields - .next() - .ok_or_else(|| format!("{}:{}: missing status", path.display(), line_number + 1))?; - let evidence = fields - .next() - .ok_or_else(|| format!("{}:{}: missing evidence", path.display(), line_number + 1))?; - if evidence.trim().is_empty() { - return Err(format!( - "{}:{}: empty evidence", - path.display(), - line_number + 1 - )); - } - if !is_acceptance_id(id) { - return Err(format!( - "{}:{}: invalid acceptance id: {id}", - path.display(), - line_number + 1 - )); - } - entries.insert( - id.to_string(), - CoverageEntry { - status: CoverageStatus::parse(status)?, - evidence: evidence.to_string(), - }, - ); - } - Ok(entries) -} - -fn build_acceptance_audit( - required: &BTreeSet<String>, - coverage: &BTreeMap<String, CoverageEntry>, -) -> AcceptanceAudit { - let mut covered = Vec::new(); - let mut covered_planning = Vec::new(); - let mut covered_stub = Vec::new(); - let mut covered_gpu = Vec::new(); - let mut partial = Vec::new(); - let mut blocked = Vec::new(); - let mut omitted = Vec::new(); - let mut missing = Vec::new(); - let mut by_stage = BTreeMap::new(); - let mut coverage_evidence = BTreeMap::new(); - let required_scopes = required - .iter() - .filter_map(|id| id.get(0..2).map(ToString::to_string)) - .collect::<BTreeSet<_>>(); - - for id in required { - let stage = id - .get(0..2) - .map_or_else(|| "??".to_string(), ToString::to_string); - *by_stage.entry(stage).or_insert(0) += 1; - match coverage.get(id).map(|entry| entry.status) { - Some(CoverageStatus::Covered) => covered.push(id.clone()), - Some(CoverageStatus::CoveredPlanning) => covered_planning.push(id.clone()), - Some(CoverageStatus::CoveredStub) => covered_stub.push(id.clone()), - Some(CoverageStatus::CoveredGpu) => covered_gpu.push(id.clone()), - Some(CoverageStatus::Partial) => partial.push(id.clone()), - Some(CoverageStatus::Blocked) => blocked.push(id.clone()), - Some(CoverageStatus::Omitted) => omitted.push(id.clone()), - None => missing.push(id.clone()), - } - if let Some(entry) = coverage.get(id) { - coverage_evidence.insert(id.clone(), entry.evidence.clone()); - } - } - - let invalid_coverage = coverage - .iter() - .filter_map(|(id, entry)| coverage_semantic_failure(id, entry.status)) - .collect(); - - let unknown_coverage = coverage - .keys() - .filter(|id| { - !required.contains(*id) - && id - .get(0..2) - .is_some_and(|scope| required_scopes.contains(scope)) - }) - .cloned() - .collect(); - - AcceptanceAudit { - commit_sha: current_git_commit_sha(), - git_dirty: current_git_dirty(), - runner_identity: measured_runner_identity(), - rust_toolchain: measured_rust_toolchain_version(), - msrv: WORKSPACE_MSRV.to_string(), - required_total: required.len(), - covered, - covered_planning, - covered_stub, - covered_gpu, - partial, - blocked, - omitted, - missing, - invalid_coverage, - unknown_coverage, - coverage_evidence, - by_stage, - } -} - -fn coverage_semantic_failure(id: &str, status: CoverageStatus) -> Option<String> { - if is_stage3_or_later(id) - && matches!( - status, - CoverageStatus::CoveredPlanning | CoverageStatus::CoveredStub - ) - && has_gpu_delivery_token(id) - { - return Some(format!( - "{id}: {} cannot satisfy Stage 3+ GPU/pixel acceptance; use covered-gpu or leave the row non-green", - status.as_str() - )); - } - - if matches!(status, CoverageStatus::CoveredStub) - && is_fx_lifecycle_compatibility_id(id) - && !id.contains("STUB") - { - return Some(format!( - "{id}: covered-stub is reserved for explicit STUB-only FX lifecycle acceptance rows" - )); - } - - None -} - -fn is_stage3_or_later(id: &str) -> bool { - id.as_bytes().get(1).is_some_and(|stage| *stage >= b'3') -} - -fn has_gpu_delivery_token(id: &str) -> bool { - id.split('-').any(|token| { - matches!( - token, - "VK" | "GPU" | "DRAW" | "PIXEL" | "VALIDATION" | "RENDERED" - ) - }) -} - -fn is_fx_lifecycle_compatibility_id(id: &str) -> bool { - id.starts_with("S4-FX-") || id.starts_with("L4-P1-EFFECT-") || id.starts_with("L4-P2-EFFECT-") -} - -#[derive(Serialize)] -struct AcceptanceAuditJson<'a> { - schema_version: &'static str, - commit_sha: &'a str, - git_dirty: bool, - runner_identity: &'a str, - rust_toolchain: &'a str, - msrv: &'a str, - required_total: usize, - covered_total: usize, - covered_planning_total: usize, - covered_stub_total: usize, - covered_gpu_total: usize, - partial_total: usize, - blocked_total: usize, - omitted_total: usize, - missing_total: usize, - unverified_total: usize, - invalid_coverage_total: usize, - unknown_coverage_total: usize, - by_stage: &'a BTreeMap<String, usize>, - covered: &'a [String], - covered_planning: &'a [String], - covered_stub: &'a [String], - covered_gpu: &'a [String], - partial: &'a [String], - blocked: &'a [String], - omitted: &'a [String], - missing: &'a [String], - invalid_coverage: &'a [String], - unknown_coverage: &'a [String], - coverage_evidence: &'a BTreeMap<String, String>, -} - -fn render_audit_json(audit: &AcceptanceAudit) -> Result<String, String> { - let unverified = audit.unverified(); - let report = AcceptanceAuditJson { - schema_version: "fparkan-acceptance-coverage-v1", - commit_sha: &audit.commit_sha, - git_dirty: audit.git_dirty, - runner_identity: &audit.runner_identity, - rust_toolchain: &audit.rust_toolchain, - msrv: &audit.msrv, - required_total: audit.required_total, - covered_total: audit.covered_total(), - covered_planning_total: audit.covered_planning.len(), - covered_stub_total: audit.covered_stub.len(), - covered_gpu_total: audit.covered_gpu.len(), - partial_total: audit.partial.len(), - blocked_total: audit.blocked.len(), - omitted_total: audit.omitted.len(), - missing_total: audit.missing.len(), - unverified_total: unverified.len(), - invalid_coverage_total: audit.invalid_coverage.len(), - unknown_coverage_total: audit.unknown_coverage.len(), - by_stage: &audit.by_stage, - covered: &audit.covered, - covered_planning: &audit.covered_planning, - covered_stub: &audit.covered_stub, - covered_gpu: &audit.covered_gpu, - partial: &audit.partial, - blocked: &audit.blocked, - omitted: &audit.omitted, - missing: &audit.missing, - invalid_coverage: &audit.invalid_coverage, - unknown_coverage: &audit.unknown_coverage, - coverage_evidence: &audit.coverage_evidence, - }; - serde_json::to_string_pretty(&report) - .map(|json| format!("{json}\n")) - .map_err(|err| format!("acceptance audit serialization failed: {err}")) -} - -fn current_git_commit_sha() -> String { - Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .ok() - .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout).ok()) - .map(|value| value.trim().to_string()) - .filter(|value| value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())) - .unwrap_or_else(|| "unknown".to_string()) -} - -fn is_commit_sha(value: &str) -> bool { - value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit()) -} - -fn current_git_dirty() -> bool { - Command::new("git") - .args(["status", "--short"]) - .output() - .ok() - .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout).ok()) - .is_some_and(|output| !output.trim().is_empty()) -} - -fn measured_rust_toolchain_version() -> String { - Command::new("rustc") - .args(["-Vv"]) - .output() - .ok() - .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout).ok()) - .and_then(|stdout| { - stdout.lines().find_map(|line| { - line.strip_prefix("release:") - .map(str::trim) - .map(ToString::to_string) - }) - }) - .unwrap_or_else(|| PINNED_RUST_TOOLCHAIN.to_string()) -} - -fn measured_runner_identity() -> String { - if std::env::var_os("GITHUB_ACTIONS").is_some() { - let run_id = std::env::var("GITHUB_RUN_ID").unwrap_or_else(|_| "unknown-run".to_string()); - let job = std::env::var("GITHUB_JOB").unwrap_or_else(|_| "unknown-job".to_string()); - format!("github-actions/{run_id}/{job}") - } else if std::env::var_os("CI").is_some() { - let job = std::env::var("CI_JOB_NAME") - .or_else(|_| std::env::var("BUILD_ID")) - .unwrap_or_else(|_| "generic-ci".to_string()); - format!("ci/{job}") - } else { - format!("local/{}", std::env::consts::OS) - } -} - -fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> { - let roots = if options.suite == TestSuite::Licensed { - Some(load_licensed_roots(options.manifest.as_deref())?) - } else { - None - }; - run_stage_tests(options.stage, options.suite, roots.as_ref())?; - - if let Some(parent) = options.out.parent() { - fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; - } - let report = render_acceptance_report(options)?; - fs::write(&options.out, report).map_err(|err| format!("{}: {err}", options.out.display()))?; - println!("{}", options.out.display()); - Ok(()) -} - -#[derive(Serialize)] -struct AcceptanceLicensedCorpusReport<'a> { - root: &'a str, - parts: [&'a str; 2], -} - -#[derive(Serialize)] -struct AcceptanceReportJson { - schema_version: &'static str, - suite: String, - stage: String, - status: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - licensed_corpus: Option<AcceptanceLicensedCorpusReport<'static>>, - packages: Vec<String>, -} - -fn render_acceptance_report(options: &AcceptanceOptions) -> Result<String, String> { - let report = AcceptanceReportJson { - schema_version: "fparkan-acceptance-report-v1", - suite: options.suite.as_str().to_string(), - stage: options.stage.to_string(), - status: "passed", - licensed_corpus: if options.suite == TestSuite::Licensed { - Some(AcceptanceLicensedCorpusReport { - root: "redacted", - parts: ["IS", "IS2"], - }) - } else { - None - }, - packages: stage_report_packages(options.stage).unwrap_or_default(), - }; - serde_json::to_string_pretty(&report) - .map(|json| format!("{json}\n")) - .map_err(|err| format!("acceptance report serialization failed: {err}")) -} - -fn stage_report_packages(stage: Stage) -> Result<Vec<String>, String> { - let workspace_root = workspace_root_path(); - match stage { - Stage::All => workspace_package_names(&workspace_root) - .map(|packages| packages.into_iter().collect::<Vec<_>>()), - Stage::Number(number) => stage_packages(number), - } -} - -fn run_stage_tests( - stage: Stage, - suite: TestSuite, - roots: Option<&LicensedCorpusRoots>, -) -> Result<(), String> { - let mut suffix = Vec::new(); - if suite == TestSuite::Licensed { - suffix.extend(["--", "--ignored"]); - } - let envs = roots.map(LicensedCorpusRoots::envs); - match stage { - Stage::All => { - let mut args = vec!["test", "--workspace", "--locked", "--offline"]; - args.extend(suffix); - if let Some(envs) = envs { - cargo_with_env(&args, &envs) - } else { - cargo(&args) - } - } - Stage::Number(number) => { - for package in stage_packages(number)? { - let mut args = vec![ - "test".to_string(), - "-p".to_string(), - package, - "--locked".to_string(), - "--offline".to_string(), - ]; - args.extend(suffix.iter().map(|value| (*value).to_string())); - if let Some(envs) = envs { - let borrowed = args.iter().map(String::as_str).collect::<Vec<_>>(); - cargo_with_env(&borrowed, &envs)?; - } else { - cargo_owned(&args)?; - } - } - Ok(()) - } - } -} - -fn stage_packages(stage: u8) -> Result<Vec<String>, String> { - let manifest_path = workspace_relative_path(STAGE_PACKAGE_MANIFEST); - let manifest = load_stage_package_manifest(&manifest_path)?; - let packages = manifest - .stages - .get(&stage.to_string()) - .cloned() - .ok_or_else(|| format!("stage out of range: {stage}"))?; - validate_stage_package_entries( - &manifest, - &workspace_package_names(&workspace_root_path())?, - &manifest_path, - )?; - Ok(packages) -} - -fn workspace_package_names(root: &Path) -> Result<BTreeSet<String>, String> { - let metadata = workspace_metadata(root)?; - Ok(metadata - .workspace_packages() - .iter() - .map(|package| package.name.to_string()) - .collect()) -} - -fn workspace_metadata(root: &Path) -> Result<cargo_metadata::Metadata, String> { - let manifest = root.join("Cargo.toml"); - MetadataCommand::new() - .manifest_path(&manifest) - .no_deps() - .other_options(["--offline".to_string(), "--locked".to_string()]) - .exec() - .map_err(|error| format!("{}: cargo metadata failed: {}", manifest.display(), error)) -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct StagePackageManifest { - schema: Option<u8>, - stages: BTreeMap<String, Vec<String>>, -} - -fn load_stage_package_manifest(path: &Path) -> Result<StagePackageManifest, String> { - let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; - let manifest = toml::from_str::<StagePackageManifest>(&text) - .map_err(|err| format!("failed to parse {}: {err}", path.display()))?; - if manifest.schema != Some(1) { - return Err(format!( - "{}: unsupported stage package manifest schema {:?} (expected 1)", - path.display(), - manifest.schema - )); - } - Ok(manifest) -} - -fn validate_stage_package_entries( - manifest: &StagePackageManifest, - workspace_packages: &BTreeSet<String>, - path: &Path, -) -> Result<(), String> { - let required_stages = (0_u8..=5_u8) - .map(|stage| stage.to_string()) - .collect::<BTreeSet<_>>(); - let declared_stages = manifest.stages.keys().cloned().collect::<BTreeSet<_>>(); - if declared_stages != required_stages { - return Err(format!( - "{}: stage package manifest must declare stages 0 through 5 exactly once", - path.display() - )); - } - - let mut assigned = BTreeSet::new(); - for (stage, packages) in &manifest.stages { - for package in packages { - if !workspace_packages.contains(package) { - return Err(format!( - "{}: stage {stage} references unknown package {package}", - path.display() - )); - } - if !assigned.insert(package.clone()) { - return Err(format!( - "{}: package {package} is assigned to multiple stages", - path.display() - )); - } - } - } - - let missing = workspace_packages - .difference(&assigned) - .cloned() - .collect::<Vec<_>>(); - if !missing.is_empty() { - return Err(format!( - "{}: stage package manifest is missing workspace packages: {}", - path.display(), - missing.join(", ") - )); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn strings(values: &[&str]) -> Vec<String> { - values.iter().map(|value| (*value).to_string()).collect() - } - - fn temp_dir(name: &str) -> PathBuf { - let suffix = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |duration| duration.as_nanos()); - std::env::temp_dir().join(format!("fparkan-xtask-{name}-{suffix}")) - } - - #[test] - fn parses_stage_and_root_options() { - let args = strings(&["--stage", "3", "--root", "fixtures"]); - let parsed = parse_test_options(&args, PathBuf::from("testdata")); - - assert_eq!( - parsed, - Ok(TestOptions { - stage: Stage::Number(3), - root: PathBuf::from("fixtures"), - manifest: None, - }) - ); - } - - #[test] - fn parses_corpus_baseline_output_option() { - let options = parse_corpus_baseline_options(&strings(&[ - "--root", - "C:/GOG Games/Parkan - Iron Strategy", - "--out", - "target/fparkan/corpus/part1.json", - ])); - - assert_eq!( - options, - Ok(CorpusBaselineOptions { - root: PathBuf::from("C:/GOG Games/Parkan - Iron Strategy"), - out: Some(PathBuf::from("target/fparkan/corpus/part1.json")), - }) - ); - } - - #[test] - fn parses_acceptance_report_options() { - let parsed = parse_acceptance_options(&strings(&[ - "--suite", - "licensed", - "--stage", - "5", - "--manifest", - "corpora.toml", - "--out", - "target/report.json", - ])); - - assert_eq!( - parsed, - Ok(AcceptanceOptions { - suite: TestSuite::Licensed, - stage: Stage::Number(5), - root: PathBuf::from("testdata"), - manifest: Some(PathBuf::from("corpora.toml")), - out: PathBuf::from("target/report.json"), - }) - ); - } - - #[test] - fn acceptance_report_redacts_licensed_root() { - let options = AcceptanceOptions { - suite: TestSuite::Licensed, - stage: Stage::Number(0), - root: PathBuf::from("/private/game"), - manifest: Some(PathBuf::from("/private/corpora.toml")), - out: PathBuf::from("target/report.json"), - }; - let report = - render_acceptance_report(&options).expect("acceptance report should serialize"); - - assert!(report.contains("\"root\": \"redacted\"")); - assert!(!report.contains("/private/game")); - assert!(report.contains("\"fparkan-corpus\"")); - } - - #[test] - fn extracts_acceptance_ids_from_backticks_only() { - let ids = - extract_acceptance_ids("`S0-ARCH-001` text S0-ARCH-002 `L5-P1-MISSION-001` `bad`"); - - assert!(ids.contains("S0-ARCH-001")); - assert!(ids.contains("L5-P1-MISSION-001")); - assert!(!ids.contains("S0-ARCH-002")); - assert_eq!(ids.len(), 2); - } - - #[test] - fn builds_acceptance_audit_counts() { - let required = ["S0-ARCH-001", "S0-ARCH-002", "L3-GPU-001", "L5-RG40-001"] - .into_iter() - .map(str::to_string) - .collect::<BTreeSet<_>>(); - let coverage = [ - ( - "S0-ARCH-001".to_string(), - CoverageEntry { - status: CoverageStatus::Covered, - evidence: "cargo xtask policy".to_string(), - }, - ), - ( - "L3-GPU-001".to_string(), - CoverageEntry { - status: CoverageStatus::CoveredPlanning, - evidence: "planning-only evidence".to_string(), - }, - ), - ( - "L5-RG40-001".to_string(), - CoverageEntry { - status: CoverageStatus::Blocked, - evidence: "device not attached".to_string(), - }, - ), - ( - "S0-ARCH-099".to_string(), - CoverageEntry { - status: CoverageStatus::Partial, - evidence: "bad id".to_string(), - }, - ), - ] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let audit = build_acceptance_audit(&required, &coverage); - - assert_eq!(audit.covered, ["S0-ARCH-001"]); - assert_eq!(audit.covered_planning, ["L3-GPU-001"]); - assert_eq!(audit.blocked, ["L5-RG40-001"]); - assert_eq!(audit.missing, ["S0-ARCH-002"]); - assert_eq!(audit.unknown_coverage, ["S0-ARCH-099"]); - assert_eq!(audit.by_stage.get("S0"), Some(&2)); - assert_eq!( - audit.strict_failures(), - vec![ - "L5-RG40-001".to_string(), - "S0-ARCH-002".to_string(), - "L3-GPU-001: covered-planning cannot satisfy Stage 3+ GPU/pixel acceptance; use covered-gpu or leave the row non-green".to_string(), - ] - ); - } - - #[test] - fn covered_stub_requires_explicit_stub_fx_acceptance_row() { - let required = ["S4-FX-018"] - .into_iter() - .map(str::to_string) - .collect::<BTreeSet<_>>(); - let coverage = [( - "S4-FX-018".to_string(), - CoverageEntry { - status: CoverageStatus::CoveredStub, - evidence: "reference stub output".to_string(), - }, - )] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let audit = build_acceptance_audit(&required, &coverage); - - assert_eq!(audit.covered_stub, ["S4-FX-018"]); - assert_eq!( - audit.invalid_coverage, - vec![ - "S4-FX-018: covered-stub is reserved for explicit STUB-only FX lifecycle acceptance rows" - .to_string() - ] - ); - } - - #[test] - fn audit_json_escapes_evidence() { - let mut audit = AcceptanceAudit { - commit_sha: "0123456789abcdef0123456789abcdef01234567".to_string(), - git_dirty: false, - runner_identity: "windows-local/stage0".to_string(), - rust_toolchain: PINNED_RUST_TOOLCHAIN.to_string(), - msrv: WORKSPACE_MSRV.to_string(), - required_total: 1, - covered: vec!["S0-ARCH-001".to_string()], - covered_planning: Vec::new(), - covered_stub: Vec::new(), - covered_gpu: Vec::new(), - partial: Vec::new(), - blocked: Vec::new(), - omitted: Vec::new(), - missing: Vec::new(), - invalid_coverage: Vec::new(), - unknown_coverage: Vec::new(), - coverage_evidence: BTreeMap::new(), - by_stage: BTreeMap::new(), - }; - audit - .coverage_evidence - .insert("S0-ARCH-001".to_string(), "quoted \"value\"".to_string()); - - let json = render_audit_json(&audit).expect("acceptance audit should serialize"); - - assert!(json.contains("quoted \\\"value\\\"")); - assert!(json.contains("\"commit_sha\": \"0123456789abcdef0123456789abcdef01234567\"")); - assert!(json.contains("\"git_dirty\": false")); - assert!(json.contains("\"runner_identity\": \"windows-local/stage0\"")); - assert!(json.contains("\"rust_toolchain\": \"1.97.1\"")); - assert!(json.contains("\"msrv\": \"1.97\"")); - } - - #[test] - fn native_smoke_audit_accepts_complete_required_platform_pass() { - let expected_commit = "0123456789abcdef0123456789abcdef01234567"; - let expected_shader_manifest_hash = - "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; - let reports = ["windows"] - .into_iter() - .map(|platform| { - let target_triple = match platform { - "windows" => "x86_64-pc-windows-msvc", - _ => "unknown-target", - }; - ( - platform.to_string(), - serde_json::json!({ - "schema_version": "fparkan-native-smoke-v1", - "commit_sha": "0123456789abcdef0123456789abcdef01234567", - "git_dirty": false, - "runner_identity": "windows-local/stage0", - "runner_architecture": "x86_64", - "rust_toolchain": measured_rust_toolchain_version(), - "target_triple": target_triple, - "platform": platform, - "status": "passed", - "frames": 300, - "resize_count": 1, - "swapchain_recreate_count": 1, - "validation_warning_count": 0, - "validation_error_count": 0, - "shader_manifest_hash": expected_shader_manifest_hash, - "vulkan_loader_status": "available", - "vulkan_instance_status": "created", - "window_status": "created", - "vulkan_surface_status": "created", - "vulkan_device_status": "selected", - "vulkan_device_name": format!("{platform} GPU"), - "vulkan_logical_device_status": "created", - "vulkan_logical_device_graphics_queue_family": 0, - "vulkan_logical_device_present_queue_family": 0, - "vulkan_logical_device_enabled_extension_count": 1, - "vulkan_swapchain_status": "created", - "vulkan_swapchain_width": 1280, - "vulkan_swapchain_height": 720, - "vulkan_swapchain_image_count": 3, - "vulkan_portability_enumeration": false, - "vulkan_portability_subset_enabled": false - }), - ) - }) - .collect::<BTreeMap<_, _>>(); - - assert_eq!( - audit_native_smoke_reports(&reports, expected_commit, expected_shader_manifest_hash,), - Vec::<String>::new() - ); - } - - #[test] - fn native_smoke_audit_rejects_blocked_or_incomplete_reports() { - let expected_commit = "0123456789abcdef0123456789abcdef01234567"; - let expected_shader_manifest_hash = - "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; - let reports = [( - "windows".to_string(), - serde_json::json!({ - "schema_version": "fparkan-native-smoke-v1", - "commit_sha": "unknown", - "git_dirty": true, - "runner_identity": "", - "runner_architecture": "x86_64", - "rust_toolchain": measured_rust_toolchain_version(), - "target_triple": "x86_64-unknown-linux-gnu", - "platform": "windows", - "status": "blocked", - "frames": 0, - "resize_count": 0, - "swapchain_recreate_count": 0, - "validation_warning_count": null, - "validation_error_count": null, - "shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c", - "vulkan_loader_status": "unavailable", - "vulkan_instance_status": "skipped", - "window_status": "planned", - "vulkan_surface_status": "skipped", - "vulkan_device_status": "skipped", - "vulkan_swapchain_status": "skipped" - }), - )] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let failures = - audit_native_smoke_reports(&reports, expected_commit, expected_shader_manifest_hash); - - assert!(failures - .contains(&"windows: status expected \"passed\", found \"blocked\"".to_string())); - assert!(failures.contains( - &"windows: commit_sha must be a 40-character lowercase or uppercase hex string" - .to_string() - )); - assert!(failures.contains(&"windows: git_dirty expected false, found true".to_string())); - assert!(failures.contains(&"windows: runner_identity must be non-empty".to_string())); - assert!(failures.contains( - &"windows: target_triple \"x86_64-unknown-linux-gnu\" does not match platform" - .to_string() - )); - assert!(failures.contains(&"windows: frames expected >= 300, found 0".to_string())); - assert!(failures - .contains(&"windows: validation_error_count must be an unsigned integer".to_string())); - } - - #[test] - fn native_smoke_audit_rejects_stale_commit_sha() { - let expected_shader_manifest_hash = - "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c"; - let reports = [( - "windows".to_string(), - serde_json::json!({ - "schema_version": "fparkan-native-smoke-v1", - "commit_sha": "fedcba98765432100123456789abcdef01234567", - "git_dirty": false, - "runner_identity": "windows-local/stage0", - "runner_architecture": "x86_64", - "rust_toolchain": measured_rust_toolchain_version(), - "target_triple": "x86_64-pc-windows-msvc", - "platform": "windows", - "status": "passed", - "frames": 300, - "resize_count": 1, - "swapchain_recreate_count": 1, - "validation_warning_count": 0, - "validation_error_count": 0, - "shader_manifest_hash": expected_shader_manifest_hash, - "vulkan_loader_status": "available", - "vulkan_instance_status": "created", - "window_status": "created", - "vulkan_surface_status": "created", - "vulkan_device_status": "selected", - "vulkan_device_name": "Windows test GPU", - "vulkan_logical_device_status": "created", - "vulkan_logical_device_graphics_queue_family": 0, - "vulkan_logical_device_present_queue_family": 0, - "vulkan_logical_device_enabled_extension_count": 1, - "vulkan_swapchain_status": "created", - "vulkan_swapchain_width": 1280, - "vulkan_swapchain_height": 720, - "vulkan_swapchain_image_count": 3, - "vulkan_portability_enumeration": false, - "vulkan_portability_subset_enabled": false - }), - )] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let failures = audit_native_smoke_reports( - &reports, - "0123456789abcdef0123456789abcdef01234567", - expected_shader_manifest_hash, - ); - - assert!(failures.contains( - &"windows: commit_sha expected \"0123456789abcdef0123456789abcdef01234567\", found \"fedcba98765432100123456789abcdef01234567\"".to_string() - )); - } - - #[test] - fn native_smoke_audit_rejects_stale_shader_manifest_hash() { - let expected_commit = "0123456789abcdef0123456789abcdef01234567"; - let reports = [( - "windows".to_string(), - serde_json::json!({ - "schema_version": "fparkan-native-smoke-v1", - "commit_sha": expected_commit, - "git_dirty": false, - "runner_identity": "windows-local/stage0", - "runner_architecture": "x86_64", - "rust_toolchain": measured_rust_toolchain_version(), - "target_triple": "x86_64-pc-windows-msvc", - "platform": "windows", - "status": "passed", - "frames": 300, - "resize_count": 1, - "swapchain_recreate_count": 1, - "validation_warning_count": 0, - "validation_error_count": 0, - "shader_manifest_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "vulkan_loader_status": "available", - "vulkan_instance_status": "created", - "window_status": "created", - "vulkan_surface_status": "created", - "vulkan_device_status": "selected", - "vulkan_device_name": "Windows test GPU", - "vulkan_logical_device_status": "created", - "vulkan_logical_device_graphics_queue_family": 0, - "vulkan_logical_device_present_queue_family": 0, - "vulkan_logical_device_enabled_extension_count": 1, - "vulkan_swapchain_status": "created", - "vulkan_swapchain_width": 1280, - "vulkan_swapchain_height": 720, - "vulkan_swapchain_image_count": 3, - "vulkan_portability_enumeration": false, - "vulkan_portability_subset_enabled": false - }), - )] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let failures = audit_native_smoke_reports( - &reports, - expected_commit, - "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c", - ); - - assert!(failures.contains( - &"windows: shader_manifest_hash expected \"dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c\", found \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"".to_string() - )); - } - - #[test] - fn defaults_to_all_stage_and_testdata_root() { - let args = Vec::new(); - let parsed = parse_test_options(&args, PathBuf::from("testdata")); - - assert_eq!( - parsed, - Ok(TestOptions { - stage: Stage::All, - root: PathBuf::from("testdata"), - manifest: None, - }) - ); - } - - #[test] - fn parses_licensed_corpora_manifest() -> Result<(), String> { - let root = temp_dir("manifest"); - let part1 = root.join("IS"); - let part2 = root.join("IS2"); - fs::create_dir_all(&part1).map_err(|err| err.to_string())?; - fs::create_dir_all(&part2).map_err(|err| err.to_string())?; - let manifest = root.join("corpora.toml"); - fs::write( - &manifest, - format!( - "schema = 1\n\n[[corpus]]\nid = \"part1-local\"\nkind = \"part1\"\nroot = '{}'\nexpected_profile = \"parkan-is-part1\"\n\n[[corpus]]\nid = \"part2-local\"\nkind = \"part2\"\nroot = '{}'\nexpected_profile = \"parkan-is-part2\"\n", - part1.display(), - part2.display() - ), - ) - .map_err(|err| err.to_string())?; - - assert_eq!( - parse_licensed_manifest(&manifest)?, - LicensedCorpusRoots { part1, part2 } - ); - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn licensed_roots_require_manifest_configuration() { - let previous = std::env::var_os(CORPORA_MANIFEST_ENV); - std::env::remove_var(CORPORA_MANIFEST_ENV); - - assert_eq!( - load_licensed_roots(None), - Err(format!( - "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>" - )) - ); - - if let Some(value) = previous { - std::env::set_var(CORPORA_MANIFEST_ENV, value); - } - } - - #[test] - fn rejects_unknown_stage() { - assert_eq!(Stage::parse("6"), Err("stage out of range: 6".to_string())); - assert_eq!( - Stage::parse("assets"), - Err("invalid stage: assets".to_string()) - ); - } - - #[test] - fn maps_stage_packages() { - assert!(stage_packages(0) - .is_ok_and(|packages| packages.contains(&"fparkan-platform".to_string()))); - assert!(stage_packages(0) - .is_ok_and(|packages| packages.contains(&"fparkan-vulkan-smoke".to_string()))); - assert!(stage_packages(1) - .is_ok_and(|packages| packages.contains(&"fparkan-inspection".to_string()))); - assert!(stage_packages(5) - .is_ok_and(|packages| packages.contains(&"fparkan-runtime".to_string()))); - assert!( - stage_packages(5).is_ok_and(|packages| packages.contains(&"fparkan-game".to_string())) - ); - assert_eq!(stage_packages(9), Err("stage out of range: 9".to_string())); - } - - #[test] - fn stage_package_manifest_covers_workspace_once() -> Result<(), String> { - let manifest_path = workspace_relative_path(STAGE_PACKAGE_MANIFEST); - let manifest = load_stage_package_manifest(&manifest_path)?; - let workspace_packages = workspace_package_names(&workspace_root_path())?; - - validate_stage_package_entries(&manifest, &workspace_packages, &manifest_path)?; - - Ok(()) - } - - #[test] - fn workspace_metadata_keeps_headless_closure_platform_free() -> Result<(), String> { - let metadata = workspace_metadata(&workspace_root_path())?; - let dependencies = workspace_dependency_names(&metadata); - let mut failures = Vec::new(); - - validate_package_closure_excludes("fparkan-headless", &dependencies, &mut failures); - - assert!(failures.is_empty(), "{failures:?}"); - Ok(()) - } - - #[test] - fn parses_manifest_dependencies_for_arch_policy() { - let manifest = r#" -[package] -name = "fparkan-example" - -[dependencies] -fparkan-render = { path = "../fparkan-render" } -"quoted-dep" = "1" - -[dev-dependencies] -fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" } -"#; - - let policy = toml::from_str::<CargoManifestPolicy>(manifest) - .expect("typed policy manifest should parse"); - assert_eq!(policy.package_name(), Some("fparkan-example")); - let deps = policy.dependency_names(); - assert!(deps.contains("fparkan-render")); - assert!(deps.contains("quoted-dep")); - assert!(deps.contains("fparkan-render-vulkan")); - } - - #[test] - fn typed_manifest_policy_accepts_workspace_inherited_license() { - let manifest = r#" -[package] -name = "fparkan-example" -license.workspace = true -"#; - - let policy = toml::from_str::<CargoManifestPolicy>(manifest) - .expect("workspace inheritance should be valid Cargo TOML"); - - assert_eq!(policy.package_name(), Some("fparkan-example")); - assert_eq!(policy.license(), None); - } - - #[test] - fn workspace_manifest_closure_detects_transitive_platform_bridge() { - let deps_by_package = [ - ( - "fparkan-headless".to_string(), - ["fparkan-runtime".to_string()].into_iter().collect(), - ), - ( - "fparkan-runtime".to_string(), - ["fparkan-render-vulkan".to_string()].into_iter().collect(), - ), - ("fparkan-render-vulkan".to_string(), BTreeSet::new()), - ] - .into_iter() - .collect::<BTreeMap<_, _>>(); - - let closure = dependency_closure_names("fparkan-headless", &deps_by_package); - - assert!(closure.contains("fparkan-runtime")); - assert_eq!( - first_forbidden_platform_bridge_dependency(&closure), - Some("fparkan-render-vulkan") - ); - } - - #[test] - fn toolchain_policy_rejects_moving_toolchain() -> Result<(), String> { - let root = temp_dir("toolchain-moving"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - fs::write( - root.join("rust-toolchain.toml"), - "[toolchain]\nchannel = \"stable\"\n", - ) - .map_err(|err| err.to_string())?; - fs::write( - root.join("Cargo.toml"), - "[workspace]\n[workspace.package]\nrust-version = \"1.97\"\n", - ) - .map_err(|err| err.to_string())?; - - let mut failures = Vec::new(); - validate_toolchain_policy(&root, &mut failures)?; - - assert_eq!(failures.len(), 2); - assert!(failures[0].contains("must be exact")); - assert!(failures[1].contains("major.minor.patch")); - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn toolchain_policy_rejects_msrv_mismatch() -> Result<(), String> { - let root = temp_dir("toolchain-msrv"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - fs::write( - root.join("rust-toolchain.toml"), - format!("[toolchain]\nchannel = \"{PINNED_RUST_TOOLCHAIN}\"\n"), - ) - .map_err(|err| err.to_string())?; - fs::write( - root.join("Cargo.toml"), - "[workspace]\n[workspace.package]\nrust-version = \"1.86\"\n", - ) - .map_err(|err| err.to_string())?; - - let mut failures = Vec::new(); - validate_toolchain_policy(&root, &mut failures)?; - - assert_eq!(failures.len(), 2); - assert!(failures[0].contains("rust-version must be")); - assert!(failures[1].contains("must match pinned toolchain")); - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn lockfile_supply_chain_rejects_unapproved_sources() -> Result<(), String> { - let root = temp_dir("lockfile-source"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - fs::write( - root.join("Cargo.lock"), - r#" -[[package]] -name = "external" -version = "1.0.0" -source = "git+https://example.invalid/repo" -"#, - ) - .map_err(|err| err.to_string())?; - - let mut failures = Vec::new(); - validate_lockfile_supply_chain(&root, &mut failures)?; - - assert_eq!(failures.len(), 1); - assert!(failures[0].contains("uses unapproved source")); - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn lockfile_supply_chain_rejects_banned_packages() -> Result<(), String> { - let root = temp_dir("lockfile-ban"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - fs::write( - root.join("Cargo.lock"), - format!( - "[[package]]\nname = \"openssl\"\nversion = \"0.10.0\"\nsource = \"{APPROVED_REGISTRY_SOURCE}\"\n" - ), - ) - .map_err(|err| err.to_string())?; - - let mut failures = Vec::new(); - validate_lockfile_supply_chain(&root, &mut failures)?; - - assert_eq!(failures.len(), 1); - assert!(failures[0].contains("is banned")); - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn supply_chain_policy_config_must_exist() -> Result<(), String> { - let root = temp_dir("supply-chain-config"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - - let missing = root.join("deny.toml"); - assert!(validate_supply_chain_policy_config(&missing).is_err()); - - fs::write(&missing, "[graph]\nall-features = true\n").map_err(|err| err.to_string())?; - assert_eq!(validate_supply_chain_policy_config(&missing), Ok(())); - - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } - - #[test] - fn ci_env_truthy_values_are_detected() { - assert!(ci_env_value_is_active(Some("true"))); - assert!(ci_env_value_is_active(Some("1"))); - assert!(ci_env_value_is_active(Some("yes"))); - assert!(!ci_env_value_is_active(None)); - assert!(!ci_env_value_is_active(Some(""))); - assert!(!ci_env_value_is_active(Some("0"))); - assert!(!ci_env_value_is_active(Some("false"))); - assert!(!ci_env_value_is_active(Some(" FALSE "))); - } - - #[test] - fn detects_forbidden_domain_dependencies() { - assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan")); - assert!(is_forbidden_domain_dependency("sdl2")); - assert!(is_forbidden_domain_dependency("fparkan-platform-sdl")); - assert!(!is_forbidden_domain_dependency("fparkan-render")); - assert!(!is_forbidden_domain_dependency("fparkan-platform")); - } - - #[test] - fn parses_package_options() { - assert_eq!( - parse_package_options(&strings(&[ - "--target", - "aarch64-apple-darwin", - "--app", - "viewer" - ])), - Ok(PackageOptions { - target: "aarch64-apple-darwin".to_string(), - app: AppPackage::Viewer, - }) - ); - assert_eq!( - parse_package_options(&strings(&["--target", "x", "--app", "bad"])), - Err("unknown app: bad".to_string()) - ); - } - - #[test] - fn app_packages_map_to_cargo_packages() { - assert_eq!(AppPackage::Cli.package(), "fparkan-cli"); - assert_eq!(AppPackage::Game.package(), "fparkan-game"); - assert_eq!(AppPackage::Headless.package(), "fparkan-headless"); - assert_eq!(AppPackage::Viewer.package(), "fparkan-viewer"); - } - - #[test] - fn policy_source_detection_is_scoped_to_code_files() { - assert!(is_policy_source(Path::new("src/main.rs"))); - assert!(is_policy_source(Path::new("Cargo.toml"))); - assert!(!is_policy_source(Path::new("README.md"))); - assert!(should_skip_policy_path(Path::new("target"))); - assert!(should_skip_policy_path(Path::new("testdata"))); - assert!(!should_skip_policy_path(Path::new("crates/experimental"))); - assert!(!should_skip_policy_path(Path::new("crates/fparkan-render"))); - } - - #[test] - fn unsafe_construct_detector_ignores_lints_and_comments() { - assert!(contains_unsafe_construct(concat!( - "un", - "safe fn call() {}" - ))); - assert!(contains_unsafe_construct(concat!( - "let value = un", - "safe { call() };" - ))); - assert!(!contains_unsafe_construct("#![forbid(unsafe_code)]")); - } - - #[test] - fn unsafe_module_attributes_must_match_audited_ffi_allowlist() { - let audited = Path::new("adapters/fparkan-render-vulkan/src/ffi/runtime.rs"); - let non_audited = Path::new("adapters/fparkan-render-vulkan/src/ffi.rs"); - - let mut failures = Vec::new(); - validate_unsafe_module_attribute_policy(audited, "mod x;\n", &mut failures); - assert_eq!( - failures, - vec![format!( - "{}: audited Vulkan FFI module must declare #![allow(unsafe_code)]", - audited.display() - )] - ); - - failures.clear(); - validate_unsafe_module_attribute_policy( - non_audited, - "#![allow(unsafe_code)]\nmod runtime;\n", - &mut failures, - ); - assert_eq!( - failures, - vec![format!( - "{}: #![allow(unsafe_code)] is only permitted in audited Vulkan FFI modules", - non_audited.display() - )] - ); - } - - #[test] - fn audited_unsafe_allowlist_accepts_windows_separators() { - let path = Path::new(r".\adapters\fparkan-render-vulkan\src\ffi\runtime.rs"); - - assert!(is_audited_unsafe_source(path)); - } - - #[test] - fn shader_source_hash_normalizes_windows_line_endings() -> Result<(), String> { - let root = temp_dir("shader-source-hash"); - fs::create_dir_all(&root).map_err(|err| err.to_string())?; - let lf = root.join("lf.glsl"); - let crlf = root.join("crlf.glsl"); - fs::write(&lf, b"#version 450\nvoid main() {}\n").map_err(|err| err.to_string())?; - fs::write(&crlf, b"#version 450\r\nvoid main() {}\r\n").map_err(|err| err.to_string())?; - - assert_eq!( - sha256_canonical_text_file(&lf)?, - sha256_canonical_text_file(&crlf)? - ); - - fs::remove_dir_all(root).map_err(|err| err.to_string())?; - Ok(()) - } -} |
