diff --git a/dag/test/retirement/map_lookup_dual_dispatch_retired.dag b/dag/test/retirement/map_lookup_dual_dispatch_retired.dag new file mode 100644 index 00000000000..0b6684a71e8 --- /dev/null +++ b/dag/test/retirement/map_lookup_dual_dispatch_retired.dag @@ -0,0 +1,10 @@ +module test.retirement.map_lookup_dual_dispatch_retired + +import test.retirement.model { TestModuleRetirement, DeleteLowValue } + +data map_lookup_dual_dispatch_retirement: TestModuleRetirement = TestModuleRetirement { + module: "map_lookup_dual_dispatch_test.rs", + disposition: DeleteLowValue { + reason: "Residual of the 5-test-migration lane. Four runtime tests already migrated to dag/test/claim/map_lookup_dual_dispatch_witness_test.dag (method get, index sugar, lookup builtin, std.graph adjacency; routing proven by chokepoint perturbation). The two remaining are NOT redundant-by-coverage, so this is low-value-at-v1-sunset rather than DeleteRedundant: (1) map_lookup_operations_do_not_probe_value_map_outside_chokepoint is a v1_interpreter.rs source-text bypass scan the module itself documents as a pinned-harness fact 'dissolving with the v1 interpreter' (a transient v1-Rust scaffold, no v2-relevant concept; the routing it statically complements is perturbation-proven for the live surfaces); (2) map_get_function_returns_optional_for_empty_list_hit is #[ignore]d on a pre-existing empty-list inference bug (never run in CI), inert." + } +} diff --git a/dag/test/retirement/model.dag b/dag/test/retirement/model.dag new file mode 100644 index 00000000000..ba1c7460edc --- /dev/null +++ b/dag/test/retirement/model.dag @@ -0,0 +1,10 @@ +module test.retirement.model + +type RetirementDisposition + = DeleteRedundant { covered_by: String } + | DeleteLowValue { reason: String } + +type TestModuleRetirement { + module: String + disposition: RetirementDisposition +} diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index d7705f45931..1a0ef765766 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9668,9 +9668,48 @@ fn test_migration_debt_stem_covered(v1_stem: &str, floor_stems: &[String]) -> bo floor_stems.iter().any(|floor_stem| floor_stem == v1_stem) } +// Second stem source (typed retirement path): a `_retired.dag` declaration under the +// corpus records a reviewed, typed retirement (delete-redundant / delete-low-value) for a v1 +// test module whose behavior does NOT migrate to an exact-stem floor `*_test.dag` witness. A +// retired stem covers the module identically to a floor-witness stem — it excludes the module +// from the debt roster and authorizes its delete through the delete-guard. The typed disposition +// and its justification live in the `.dag` decl (`test.retirement.model`, single authority, +// type-checked by the compile-clean gate); this guard reads only the filename stem, exactly as +// it reads floor witnesses. A file counts only if it actually *constructs* a `TestModuleRetirement` +// (the `TestModuleRetirement {` constructor form) — an empty stub, or one that merely imports the +// type without declaring a retirement (`{ TestModuleRetirement }`), cannot silence the guard. The +// compile-clean gate independently type-checks the constructed value against `test.retirement.model`. +fn test_migration_retired_stems() -> Vec { + let mut stems: Vec = corpus_dag_files() + .into_iter() + .filter(|(_, content)| content.contains("TestModuleRetirement {")) + .filter_map(|(path, _)| { + let file_name = std::path::Path::new(&path) + .file_name() + .and_then(|n| n.to_str())?; + file_name + .strip_suffix("_retired.dag") + .map(|s| s.to_string()) + }) + .collect(); + stems.sort(); + stems.dedup(); + stems +} + +// The covered set consumed by both the debt roster and the delete-guard: floor-witness stems +// (migrate path) unioned with retired stems (delete path). One union, two consumers. +fn test_migration_covered_stems() -> Vec { + let mut stems = test_migration_debt_floor_stems(); + stems.extend(test_migration_retired_stems()); + stems.sort(); + stems.dedup(); + stems +} + fn build_test_migration_debt_report() -> TestMigrationDebtReport { let dir = test_migration_debt_v1_test_dir(); - let floor_stems = test_migration_debt_floor_stems(); + let floor_stems = test_migration_covered_stems(); let mut entries = Vec::new(); let read_dir = match std::fs::read_dir(&dir) { Ok(rd) => rd, @@ -9847,7 +9886,7 @@ fn test_migration_delete_guard_uncovered_deletes_inner() -> Result, if base_rev == head_rev { return Ok(Vec::new()); } - let floor_stems = test_migration_debt_floor_stems(); + let floor_stems = test_migration_covered_stems(); let deleted = test_migration_delete_guard_deleted_v1_test_paths(&base, &head)?; let mut violations = Vec::new(); for path in deleted { @@ -9916,6 +9955,38 @@ mod test_migration_debt_tests { let stem = test_migration_debt_stem("cron_tag_test.rs"); assert!(!test_migration_debt_stem_covered(&stem, &floor_stems)); } + + // Green-by-execution for the typed retirement path: the demonstrator + // `dag/test/retirement/map_lookup_dual_dispatch_retired.dag` declares a `TestModuleRetirement` + // whose stem is `map_lookup_dual_dispatch`. That stem is NOT a floor-witness stem (its covering + // witness is `map_lookup_dual_dispatch_witness_test.dag`, stem `map_lookup_dual_dispatch_witness`), + // so the retirement is the *only* thing that covers it — the union must pick it up. + #[test] + fn retired_stem_is_covered_but_not_a_floor_stem() { + let stem = "map_lookup_dual_dispatch"; + assert!( + test_migration_retired_stems().iter().any(|s| s == stem), + "retirement declaration must contribute its stem" + ); + // Discriminating control: the same stem is NOT a floor-witness stem — so the coverage + // comes strictly from the retirement path, not an accidental floor match. + assert!( + !test_migration_debt_floor_stems().iter().any(|s| s == stem), + "stem must be covered only via retirement, not a floor witness" + ); + assert!(test_migration_covered_stems().iter().any(|s| s == stem)); + } + + // The retired module no longer appears in the debt roster (the retirement excluded it). + #[test] + fn retired_module_is_not_debt() { + assert!( + !test_migration_debt_module_names() + .iter() + .any(|m| m == "map_lookup_dual_dispatch_test.rs"), + "a retired module must drop out of the debt roster" + ); + } } // Host-fed fact extraction for `v2.lens.host_language_transport_script` — the lens `.dag` table diff --git a/src/v1/tests/src/lib.rs b/src/v1/tests/src/lib.rs index fa2c14d6cc5..ae4c92916d4 100644 --- a/src/v1/tests/src/lib.rs +++ b/src/v1/tests/src/lib.rs @@ -81,8 +81,6 @@ mod list_free_monoid_chokepoint_test; #[cfg(test)] mod map_literal_string_key_test; #[cfg(test)] -mod map_lookup_dual_dispatch_test; -#[cfg(test)] mod measure_alias_ctor_test; #[cfg(test)] mod measure_field_access_test; diff --git a/src/v1/tests/src/map_lookup_dual_dispatch_test.rs b/src/v1/tests/src/map_lookup_dual_dispatch_test.rs deleted file mode 100644 index 16d174d8816..00000000000 --- a/src/v1/tests/src/map_lookup_dual_dispatch_test.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! RESIDUAL after 5-test-migration (2026-07-02): 4 of the original 6 tests are -//! migrated to marker-discovered floor witnesses in -//! dag/test/claim/map_lookup_dual_dispatch_witness_test.dag (method get, index -//! sugar, lookup builtin, std.graph adjacency smoke — a chokepoint perturbation -//! reds all four, proving the routing claim by execution). -//! The 2 tests below stay: -//! - map_lookup_operations_do_not_probe_value_map_outside_chokepoint: scans -//! v1_interpreter.rs source text for forbidden bypass patterns — a pinned-harness -//! fact, dissolving with the v1 interpreter. -//! - map_get_function_returns_optional_for_empty_list_hit: #[ignore]d on a -//! pre-existing empty-list-literal inference bug (bucket=inference); migrate when -//! the bug is fixed and the witness can be authored green. -use std::rc::Rc; - -use v1_compiler::v1_compiler_compile::{compile_to_resolved, ResolvedPipelineResult}; -use v1_compiler::v1_interpreter::{self, Value}; - -use crate::helpers::resolve_imports_transitively; - -fn assert_resolved_no_hard_errors(result: &ResolvedPipelineResult) { - let msgs: Vec = result - .diagnostics - .iter() - .map(|d| v1_compiler::v1_std_core::diagnostic_to_message(d.diagnostic.clone())) - .filter(|m| !m.starts_with("complexity: ")) - .collect(); - assert!( - msgs.is_empty() && result.graph.is_some(), - "expected resolved graph, got diagnostics {:?} (graph present: {})", - msgs, - result.graph.is_some() - ); -} - -#[test] -fn map_lookup_operations_do_not_probe_value_map_outside_chokepoint() { - let source = include_str!("../../stage0/src/v1_interpreter.rs"); - let forbidden = [ - "(Value::Map(m), Value::Str(k)) => Ok(m.get(k.as_str())", - "(Value::Map(m), [Value::Str(k)]) =>", - "Value::Map(m) => {\n let key = expect_str(args.first(), \"get\")?;\n Ok(m.get(&key)", - "[Value::Map(m), Value::Str(k)] => {\n Ok(Some(m.get(k.as_str())", - "Value::Map(m) => Ok(m.get(field).cloned().unwrap_or(Value::Null))", - ]; - for needle in forbidden { - assert!( - !source.contains(needle), - "Map lookup bypass: native Value::Map key-probe outside raw_map_lookup.\n\ - forbidden pattern:\n{needle}\n\ - Route map key probes through raw_map_lookup (ctrl#1476 B6 Option-C)." - ); - } -} - -#[test] -#[ignore = "failing: empty list literal inference error 'expected type is not a collection'. Pre-existing (never run in CI under the 3-test allowlist), surfaced by the run-all widening #5427; fix as follow-up. bucket=inference"] -fn map_get_function_returns_optional_for_empty_list_hit() { - let src = r#"module test.map_get_empty_list -fn hit() -> Bool { - let m = empty_map() |> map_insert("a", []) - match map_get(m, "a") { - Present { value: ns } => ns == [] - Absent => false - } -} - -fn miss() -> Bool { - let m = empty_map() |> map_insert("a", []) - match map_get(m, "b") { - Present { value: _ } => false - Absent => true - } -} -"#; - let sources = resolve_imports_transitively("test.dag", src); - let resolved = compile_to_resolved(Rc::new(sources)); - assert_resolved_no_hard_errors(&resolved); - let graph = resolved - .graph - .as_ref() - .expect("graph after successful resolve"); - let si = resolved.source_indices.clone(); - - match v1_interpreter::run(graph, si.clone(), "hit") { - Ok(Value::Bool(true)) => {} - other => panic!("expected empty-list map_get hit, got {other:?}"), - } - match v1_interpreter::run(graph, si, "miss") { - Ok(Value::Bool(true)) => {} - other => panic!("expected map_get miss -> Absent, got {other:?}"), - } -}