diff options
| author | Valentin Popov <valentin@popov.link> | 2026-07-04 00:50:59 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-07-04 00:50:59 +0300 |
| commit | 08550849c7f293f3c49abb0ba5c47c65b68bee0d (patch) | |
| tree | 54af8ee37f25879923de9af536e4ecc833f46a32 | |
| parent | 0c54142ad5f7dea0f4371de16a277b002110e008 (diff) | |
| download | fparkan-08550849c7f293f3c49abb0ba5c47c65b68bee0d.tar.xz fparkan-08550849c7f293f3c49abb0ba5c47c65b68bee0d.zip | |
chore: clarify planning acceptance semantics
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 8 | ||||
| -rw-r--r-- | apps/fparkan-viewer/src/main.rs | 18 | ||||
| -rw-r--r-- | crates/fparkan-render/src/lib.rs | 5 | ||||
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 18 | ||||
| -rw-r--r-- | xtask/src/main.rs | 155 |
5 files changed, 175 insertions, 29 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 252cd16..8d9d89b 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -19,7 +19,7 @@ ) )] #![allow(clippy::print_stderr, clippy::print_stdout)] -//! `FParkan` rendered game composition root. +//! `FParkan` render-planning composition root. use fparkan_assets::PreparedVisual; use fparkan_platform::WindowPort; @@ -98,7 +98,7 @@ fn run(args: &[String]) -> Result<String, String> { let capture_report = backend.report(); Ok(format!( - "{{\"mission\":{},\"objects\":{},\"frames\":{},\"tick\":{},\"draws\":{},\"captures\":{},\"last_capture_bytes\":{},\"hash\":{}}}", + "{{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":{},\"objects\":{},\"frames\":{},\"tick\":{},\"draws\":{},\"submission_plans\":{},\"last_command_capture_bytes\":{},\"hash\":{}}}", json_string(&args.mission), loaded.object_count, args.frames, @@ -325,12 +325,12 @@ mod tests { RenderCase { root: "IS", mission: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma", - expected: "{\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"captures\":1,\"last_capture_bytes\":810,\"hash\":\"ca17cc76e55c45e83c1c9c1c088e84bf1a698be91a7730943210fe27596af841\"}", + expected: "{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"submission_plans\":1,\"last_command_capture_bytes\":810,\"hash\":\"ca17cc76e55c45e83c1c9c1c088e84bf1a698be91a7730943210fe27596af841\"}", }, RenderCase { root: "IS2", mission: "MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma", - expected: "{\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"captures\":1,\"last_capture_bytes\":235,\"hash\":\"5d720b3ab690076a398a79a404850bbeaee2e33811b5bb570ec8a96d4a7a2fc4\"}", + expected: "{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"submission_plans\":1,\"last_command_capture_bytes\":235,\"hash\":\"5d720b3ab690076a398a79a404850bbeaee2e33811b5bb570ec8a96d4a7a2fc4\"}", }, ] { assert_eq!( diff --git a/apps/fparkan-viewer/src/main.rs b/apps/fparkan-viewer/src/main.rs index 783ef92..6befe2a 100644 --- a/apps/fparkan-viewer/src/main.rs +++ b/apps/fparkan-viewer/src/main.rs @@ -19,7 +19,7 @@ ) )] #![allow(clippy::print_stderr, clippy::print_stdout)] -//! `FParkan` asset viewer composition root. +//! `FParkan` asset inspection composition root. use fparkan_inspection::{ inspect_land_file, inspect_model_from_root, inspect_texture_from_root, ArchiveInspection, @@ -68,14 +68,14 @@ fn inspect_archive(args: &[String]) -> Result<String, String> { lookup_order_valid, sample, } => Ok(format!( - "{{\"kind\":\"NRes\",\"path\":{},\"entries\":{},\"lookup_order_valid\":{},\"sample\":[{}]}}", + "{{\"report_kind\":\"archive-inspection\",\"kind\":\"NRes\",\"path\":{},\"entries\":{},\"lookup_order_valid\":{},\"sample\":[{}]}}", json_string(&file.display().to_string()), entries, lookup_order_valid, render_nres_entries(&sample) )), ArchiveInspection::Rsli { entries } => Ok(format!( - "{{\"kind\":\"RsLi\",\"path\":{},\"entries\":{}}}", + "{{\"report_kind\":\"archive-inspection\",\"kind\":\"RsLi\",\"path\":{},\"entries\":{}}}", json_string(&file.display().to_string()), entries )), @@ -92,7 +92,7 @@ fn inspect_model(args: &[String]) -> Result<String, String> { let inspection = inspect_model_from_root(&query.root, &query.archive, &query.name)?; Ok(format!( - "{{\"kind\":\"model\",\"archive\":{},\"name\":{},\"streams\":{},\"nodes\":{},\"slots\":{},\"positions\":{},\"indices\":{},\"batches\":{}}}", + "{{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"archive\":{},\"name\":{},\"streams\":{},\"nodes\":{},\"slots\":{},\"positions\":{},\"indices\":{},\"batches\":{}}}", json_string(&query.archive), json_string(&query.name), inspection.streams, @@ -136,7 +136,7 @@ impl ViewerModelService { .count(); Ok(format!( - "{{\"kind\":\"model\",\"fixture\":{},\"service\":\"synthetic-model\",\"draw_commands\":{draw_commands}}}", + "{{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"fixture\":{},\"service\":\"synthetic-model-inspection\",\"draw_commands\":{draw_commands}}}", json_string(fixture) )) } @@ -147,7 +147,7 @@ fn inspect_texture(args: &[String]) -> Result<String, String> { let inspection = inspect_texture_from_root(&query.root, &query.archive, &query.name)?; Ok(format!( - "{{\"kind\":\"texture\",\"archive\":{},\"name\":{},\"width\":{},\"height\":{},\"format\":{},\"mips\":{},\"pages\":{}}}", + "{{\"report_kind\":\"texture-inspection\",\"kind\":\"texture\",\"archive\":{},\"name\":{},\"width\":{},\"height\":{},\"format\":{},\"mips\":{},\"pages\":{}}}", json_string(&query.archive), json_string(&query.name), inspection.width, @@ -180,7 +180,7 @@ fn inspect_map(args: &[String]) -> Result<String, String> { fn render_map_inspection_json(path: &str, kind: &str, inspection: &MapInspection) -> String { match kind { "land-msh" => format!( - "{{\"kind\":\"land-msh\",\"path\":{},\"streams\":{},\"positions\":{},\"faces\":{},\"slots\":{}}}", + "{{\"report_kind\":\"map-inspection\",\"kind\":\"land-msh\",\"path\":{},\"streams\":{},\"positions\":{},\"faces\":{},\"slots\":{}}}", json_string(path), inspection.streams, inspection.positions, @@ -188,7 +188,7 @@ fn render_map_inspection_json(path: &str, kind: &str, inspection: &MapInspection inspection.slots ), "land-map" => format!( - "{{\"kind\":\"land-map\",\"path\":{},\"areals\":{},\"declared_areals\":{},\"grid_width\":{},\"grid_height\":{}}}", + "{{\"report_kind\":\"map-inspection\",\"kind\":\"land-map\",\"path\":{},\"areals\":{},\"declared_areals\":{},\"grid_width\":{},\"grid_height\":{}}}", json_string(path), inspection.areals, inspection.declared_areals, @@ -342,7 +342,7 @@ mod tests { fn model_fixture_uses_viewer_service_and_render_commands() -> Result<(), String> { assert_eq!( run(&strings(&["model", "--fixture", "synthetic/model-basic"]))?, - "{\"kind\":\"model\",\"fixture\":\"synthetic/model-basic\",\"service\":\"synthetic-model\",\"draw_commands\":1}" + "{\"report_kind\":\"model-inspection\",\"kind\":\"model\",\"fixture\":\"synthetic/model-basic\",\"service\":\"synthetic-model-inspection\",\"draw_commands\":1}" ); Ok(()) } diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs index fa8d3c3..79e5ae5 100644 --- a/crates/fparkan-render/src/lib.rs +++ b/crates/fparkan-render/src/lib.rs @@ -389,6 +389,11 @@ pub trait RenderBackend { fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError>; } +/// Marker trait for backends that execute draws against a live GPU. +/// +/// Planning and capture-only backends must not implement this trait. +pub trait GpuRenderBackend: RenderBackend {} + /// Backend that validates commands and intentionally produces no pixels. #[derive(Clone, Debug, Default)] pub struct NullBackend; diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 4a70de1..597c234 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -1,5 +1,5 @@ # Acceptance coverage manifest. -# Format: <acceptance-id>\t<covered|partial|blocked|omitted>\t<evidence> +# Format: <acceptance-id>\t<covered|covered-planning|covered-stub|covered-gpu|partial|blocked|omitted>\t<evidence> # Scope note: Stage 0 is closed for the macOS/codebase review scope. # Scope note: cross-platform native runtime and hosted CI evidence remain deferred. L0-COPYRIGHT-001 covered cargo test -p fparkan-corpus --offline report_json_contains_metrics_and_hashes_not_paths_or_payloads @@ -32,7 +32,7 @@ S0-PLAT-001 covered cargo test -p fparkan-platform-winit --offline window_port_r S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent S0-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles -S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present +S0-VK-001 covered-planning cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format @@ -52,8 +52,8 @@ S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_j S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable checked_in_shader_manifest_matches_generated_report S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers -S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable -S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present +S0-VK-021 covered-planning cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable +S0-VK-022 covered-planning cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe @@ -233,8 +233,8 @@ L3-P1-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_ L3-P2-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse L3-P1-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations L3-P2-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations -S2-PLANNING-CAPTURE-P1-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures -S2-PLANNING-CAPTURE-P2-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures +S2-PLANNING-CAPTURE-P1-001 covered-planning cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures +S2-PLANNING-CAPTURE-P2-001 covered-planning cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures S3-WEAR-001 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index S3-WEAR-002 covered cargo test -p fparkan-material --offline wear_requires_declared_rows S3-WEAR-003 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index @@ -292,7 +292,7 @@ S2-PLANNING-RENDER-004 covered cargo test -p fparkan-render --offline command_or S2-PLANNING-RENDER-005 covered cargo test -p fparkan-render --offline command_capture_independent_of_snapshot_construction_order S2-PLANNING-RENDER-006 covered cargo test -p fparkan-render --offline invalid_range_returns_contextual_error S2-PLANNING-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable -S2-PLANNING-RENDER-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures +S2-PLANNING-RENDER-008 covered-planning cargo test -p fparkan-render --offline recording_backend_stores_captures S2-PLANNING-RENDER-009 covered cargo xtask policy S3-VK-MESH-UPLOAD-001 blocked awaits Stage 3 Vulkan asset renderer and GPU upload path S3-VK-TEXM-UPLOAD-001 blocked awaits Stage 3 Vulkan texture upload path @@ -441,7 +441,7 @@ L5-P1-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpo L5-P2-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations L5-P1-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks L5-P2-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks -L5-P1-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures -L5-P2-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures +L5-P1-RENDER-001 covered-planning cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures +L5-P2-RENDER-001 covered-planning cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures L3-DEVICE-001 omitted outside the current macOS-focused goal scope; RG40XX-capable device/profile evidence remains documented for the portable target scope L5-RG40-001 omitted outside the current macOS-focused goal scope; RG40XX 640x480 on-device mission smoke/performance/memory evidence remains documented for the portable target scope diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 249a7bb..647e43f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2340,6 +2340,9 @@ struct CoverageEntry { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CoverageStatus { Covered, + CoveredPlanning, + CoveredStub, + CoveredGpu, Partial, Blocked, Omitted, @@ -2349,12 +2352,27 @@ 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)] @@ -2366,22 +2384,34 @@ struct AcceptanceAudit { 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() } @@ -2392,6 +2422,7 @@ impl AcceptanceAudit { .chain(&self.blocked) .chain(&self.omitted) .chain(&self.missing) + .chain(&self.invalid_coverage) .cloned() .collect() } @@ -2469,6 +2500,9 @@ fn build_acceptance_audit( 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(); @@ -2487,6 +2521,9 @@ fn build_acceptance_audit( *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()), @@ -2497,6 +2534,11 @@ fn build_acceptance_audit( } } + let invalid_coverage = coverage + .iter() + .filter_map(|(id, entry)| coverage_semantic_failure(id, entry.status)) + .collect(); + let unknown_coverage = coverage .keys() .filter(|id| { @@ -2516,16 +2558,63 @@ fn build_acceptance_audit( 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, @@ -2536,18 +2625,26 @@ struct AcceptanceAuditJson<'a> { 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>, } @@ -2562,19 +2659,27 @@ fn render_audit_json(audit: &AcceptanceAudit) -> Result<String, String> { rust_toolchain: &audit.rust_toolchain, msrv: &audit.msrv, required_total: audit.required_total, - covered_total: audit.covered.len(), + 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, }; @@ -2936,7 +3041,7 @@ mod tests { #[test] fn builds_acceptance_audit_counts() { - let required = ["S0-ARCH-001", "S0-ARCH-002", "L3-DEVICE-001", "L5-RG40-001"] + let required = ["S0-ARCH-001", "S0-ARCH-002", "L3-GPU-001", "L5-RG40-001"] .into_iter() .map(str::to_string) .collect::<BTreeSet<_>>(); @@ -2949,10 +3054,10 @@ mod tests { }, ), ( - "L3-DEVICE-001".to_string(), + "L3-GPU-001".to_string(), CoverageEntry { - status: CoverageStatus::Omitted, - evidence: "outside macos scope".to_string(), + status: CoverageStatus::CoveredPlanning, + evidence: "planning-only evidence".to_string(), }, ), ( @@ -2976,14 +3081,46 @@ mod tests { 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.omitted, ["L3-DEVICE-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(), - strings(&["L5-RG40-001", "L3-DEVICE-001", "S0-ARCH-002"]) + 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() + ] ); } @@ -2997,10 +3134,14 @@ mod tests { 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(), |
