diff --git a/Cargo.lock b/Cargo.lock index 5d0867e..f002ec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,8 @@ version = "0.1.0" dependencies = [ "aura-analysis", "aura-core", + "aura-engine", + "aura-registry", "serde", ] diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 6e590e2..fdd6b3c 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -84,11 +84,6 @@ use aura_runner::member::sim_optimal_manifest; // tests that exercised them — no longer imported here. #[cfg(test)] use aura_runner::member::CostLeg; -// `IcMetrics` is only reached from the test module's `ic_report`/`ic_family` carves -// (the `IcReport` DTO holds the scalar directly in production), mirroring `Bias` -// below. -#[cfg(test)] -use aura_measurement::IcMetrics; #[cfg(test)] use aura_backtest::{summarize, summarize_r}; #[cfg(test)] @@ -3860,100 +3855,3 @@ mod tests { } } -#[cfg(test)] -mod ic_tests { - use super::*; - - fn ic_report(sigs: Vec, frs: Vec) -> aura_engine::RunReport { - aura_engine::RunReport { - manifest: aura_engine::RunManifest { - commit: "c".to_string(), - params: vec![("p".to_string(), aura_core::Scalar::f64(1.0))], - defaults: vec![], - window: (aura_core::Timestamp(1), aura_core::Timestamp(2)), - seed: 0, - broker: "b".to_string(), - selection: None, - instrument: None, - topology_hash: None, - project: None, - }, - metrics: IcMetrics { - information_coefficient: aura_analysis::pearson_corr(&sigs, &frs), - sigs, - frs, - }, - } - } - - fn ic_family(members: Vec>) -> aura_engine::SweepFamily { - aura_engine::SweepFamily { - space: vec![], - points: members - .into_iter() - .enumerate() - .map(|(i, report)| aura_engine::SweepPoint { - params: vec![aura_core::Cell::from_f64(i as f64)], - report, - }) - .collect(), - } - } - - /// #147 acceptance: the registry's deflation dispatches to the IC - /// vocabulary's OWN permutation null — a genuinely predictive family - /// deflates to a low overfit probability, a noise family to a high one. - #[test] - fn ic_family_deflation_dispatches_the_permutation_null() { - let ramp: Vec = (0..24).map(|i| i as f64).collect(); - let engineered = ic_family(vec![ - ic_report(ramp.clone(), ramp.clone()), // corr = 1.0: a real edge - ic_report(ramp.clone(), ramp.iter().rev().cloned().collect()), // corr = -1.0 - ]); - let (winner, sel) = - aura_registry::optimize_deflated(&engineered, "information_coefficient", 500, 5, 0) - .expect("ic family deflates"); - assert!((winner.report.metrics.information_coefficient - 1.0).abs() < 1e-12); - let p = sel.overfit_probability.expect("resampling-null arm sets a probability"); - assert!(p < 0.05, "perfectly predictive family must not look like overfit: p={p}"); - - // orthogonal signal — IC exactly 0.0; permuted draws beat it ~half the time. - let noise = ic_family(vec![ic_report( - vec![0.03, 0.01, 0.04, 0.02, 0.05, 0.00, 0.06, -0.01], - vec![0.01, 0.02, 0.03, 0.04, -0.04, -0.03, -0.02, -0.01], - )]); - let (_, sel) = - aura_registry::optimize_deflated(&noise, "information_coefficient", 500, 5, 0) - .expect("noise family deflates"); - let p = sel.overfit_probability.expect("probability present"); - assert!(p > 0.1, "a noise family must carry a high overfit probability: p={p}"); - } - - /// Same seed → identical selection provenance (C1 through the generic path). - #[test] - fn ic_family_deflation_is_deterministic_given_seed() { - let ramp: Vec = (0..16).map(|i| i as f64).collect(); - let fam = ic_family(vec![ic_report(ramp.clone(), ramp)]); - let a = aura_registry::optimize_deflated(&fam, "information_coefficient", 300, 5, 7) - .expect("deflate a"); - let b = aura_registry::optimize_deflated(&fam, "information_coefficient", 300, 5, 7) - .expect("deflate b"); - assert_eq!(a.1, b.1, "same seed must yield identical FamilySelection"); - } - - /// C10 guard: cross-vocabulary names are refused, and the refusal names - /// the family's REAL vocabulary. - #[test] - fn cross_vocabulary_names_are_refused_both_ways() { - let ramp: Vec = (0..8).map(|i| i as f64).collect(); - let fam = ic_family(vec![ic_report(ramp.clone(), ramp)]); - let err = aura_registry::optimize_deflated(&fam, "sqn", 100, 5, 0).unwrap_err(); - let prose = err.to_string(); - assert!( - prose.contains("unknown metric 'sqn'") && prose.contains("information_coefficient"), - "refusal must name the IC vocabulary: {prose}" - ); - // and the R side still refuses the IC name with ITS roster: - assert!(aura_registry::check_r_metric("information_coefficient").is_err()); - } -} diff --git a/crates/aura-measurement/Cargo.toml b/crates/aura-measurement/Cargo.toml index 07723e3..4ee87b2 100644 --- a/crates/aura-measurement/Cargo.toml +++ b/crates/aura-measurement/Cargo.toml @@ -15,3 +15,11 @@ aura-analysis = { path = "../aura-analysis" } # R-vocabulary metric types (aura-backtest, aura-engine); the aligned pairs # ride #[serde(skip)] (in-memory null inputs only, never persisted). serde = { workspace = true } + +# The #147 registry-dispatch acceptance tests (tests/registry_dispatch.rs) +# exercise the generic deflation machinery over the IC vocabulary — pure +# library properties relocated from the shell with #295. Dev-only edges, +# C28-layering-exempt. +[dev-dependencies] +aura-engine = { path = "../aura-engine" } +aura-registry = { path = "../aura-registry" } diff --git a/crates/aura-measurement/tests/registry_dispatch.rs b/crates/aura-measurement/tests/registry_dispatch.rs new file mode 100644 index 0000000..1bb8b15 --- /dev/null +++ b/crates/aura-measurement/tests/registry_dispatch.rs @@ -0,0 +1,101 @@ +//! #147 acceptance at the library boundary: the registry's generic +//! deflation machinery dispatches to the IC vocabulary's OWN permutation +//! null, deterministically (C1), and the C10 wall refuses cross-vocabulary +//! metric names both ways. Relocated from the shell's test module with #295 +//! (these are pure library properties — a library-only consumer must be able +//! to run them; the registry/engine edges are dev-only, layering-exempt). + +use aura_measurement::IcMetrics; + +fn ic_report(sigs: Vec, frs: Vec) -> aura_engine::RunReport { + aura_engine::RunReport { + manifest: aura_engine::RunManifest { + commit: "c".to_string(), + params: vec![("p".to_string(), aura_core::Scalar::f64(1.0))], + defaults: vec![], + window: (aura_core::Timestamp(1), aura_core::Timestamp(2)), + seed: 0, + broker: "b".to_string(), + selection: None, + instrument: None, + topology_hash: None, + project: None, + }, + metrics: IcMetrics { + information_coefficient: aura_analysis::pearson_corr(&sigs, &frs), + sigs, + frs, + }, + } +} + +fn ic_family(members: Vec>) -> aura_engine::SweepFamily { + aura_engine::SweepFamily { + space: vec![], + points: members + .into_iter() + .enumerate() + .map(|(i, report)| aura_engine::SweepPoint { + params: vec![aura_core::Cell::from_f64(i as f64)], + report, + }) + .collect(), + } +} + +/// #147 acceptance: the registry's deflation dispatches to the IC +/// vocabulary's OWN permutation null — a genuinely predictive family +/// deflates to a low overfit probability, a noise family to a high one. +#[test] +fn ic_family_deflation_dispatches_the_permutation_null() { + let ramp: Vec = (0..24).map(|i| i as f64).collect(); + let engineered = ic_family(vec![ + ic_report(ramp.clone(), ramp.clone()), // corr = 1.0: a real edge + ic_report(ramp.clone(), ramp.iter().rev().cloned().collect()), // corr = -1.0 + ]); + let (winner, sel) = + aura_registry::optimize_deflated(&engineered, "information_coefficient", 500, 5, 0) + .expect("ic family deflates"); + assert!((winner.report.metrics.information_coefficient - 1.0).abs() < 1e-12); + let p = sel.overfit_probability.expect("resampling-null arm sets a probability"); + assert!(p < 0.05, "perfectly predictive family must not look like overfit: p={p}"); + + // orthogonal signal — IC exactly 0.0; permuted draws beat it ~half the time. + let noise = ic_family(vec![ic_report( + vec![0.03, 0.01, 0.04, 0.02, 0.05, 0.00, 0.06, -0.01], + vec![0.01, 0.02, 0.03, 0.04, -0.04, -0.03, -0.02, -0.01], + )]); + let (_, sel) = + aura_registry::optimize_deflated(&noise, "information_coefficient", 500, 5, 0) + .expect("noise family deflates"); + let p = sel.overfit_probability.expect("probability present"); + assert!(p > 0.1, "a noise family must carry a high overfit probability: p={p}"); +} + +/// Same seed → identical selection provenance (C1 through the generic path). +#[test] +fn ic_family_deflation_is_deterministic_given_seed() { + let ramp: Vec = (0..16).map(|i| i as f64).collect(); + let fam = ic_family(vec![ic_report(ramp.clone(), ramp)]); + let a = aura_registry::optimize_deflated(&fam, "information_coefficient", 300, 5, 7) + .expect("deflate a"); + let b = aura_registry::optimize_deflated(&fam, "information_coefficient", 300, 5, 7) + .expect("deflate b"); + assert_eq!(a.1, b.1, "same seed must yield identical FamilySelection"); +} + +/// C10 guard: cross-vocabulary names are refused, and the refusal names +/// the family's REAL vocabulary. +#[test] +fn cross_vocabulary_names_are_refused_both_ways() { + let ramp: Vec = (0..8).map(|i| i as f64).collect(); + let fam = ic_family(vec![ic_report(ramp.clone(), ramp)]); + let err = aura_registry::optimize_deflated(&fam, "sqn", 100, 5, 0).unwrap_err(); + let prose = err.to_string(); + assert!( + prose.contains("unknown metric 'sqn'") && prose.contains("information_coefficient"), + "refusal must name the IC vocabulary: {prose}" + ); + // and the R side still refuses the IC name with ITS roster: + assert!(aura_registry::check_r_metric("information_coefficient").is_err()); +} diff --git a/crates/aura-vocabulary/tests/c28_layering.rs b/crates/aura-vocabulary/tests/c28_layering.rs index 95667ca..8c12005 100644 --- a/crates/aura-vocabulary/tests/c28_layering.rs +++ b/crates/aura-vocabulary/tests/c28_layering.rs @@ -190,3 +190,29 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() { and presentation only — a new module needs a library home" ); } + +#[test] +fn the_external_data_server_tree_enters_at_pinned_points_only() { + // C28 (#295 audit): the external `data-server` tree is a production + // dependency of exactly the ingestion edge (External components), the + // assembly position, and the shell. Any further crate pulling it in is + // a new, undecided workspace entry point for the external tree — the + // aura-*-only direction table above cannot see it, so it is pinned here. + let permitted = ["aura-cli", "aura-ingest", "aura-runner"]; + for name in workspace_crate_names() { + let manifest = workspace_root().join("crates").join(&name).join("Cargo.toml"); + let text = std::fs::read_to_string(&manifest) + .unwrap_or_else(|e| panic!("read {}: {e}", manifest.display())); + let doc: toml::Value = toml::from_str(&text).expect("Cargo.toml parses as TOML"); + let has = doc + .get("dependencies") + .and_then(|d| d.as_table()) + .is_some_and(|deps| deps.contains_key("data-server")); + assert_eq!( + has, + permitted.contains(&name.as_str()), + "C28: `data-server` [dependencies] edge on `{name}` — the external \ + tree enters the workspace only via {permitted:?}" + ); + } +} diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 4a298ce..07eeb2a 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -2648,11 +2648,19 @@ composes the rungs into the canonical member-run recipe — harness assembly, input binding (C26), the C1-load-bearing param↔config translators, and the shipped default `MemberRunner` — importing the ladder rungs, `aura-composites`, `aura-ingest`, and the process column, and imported only by the shell and by -downstream World programs. The **shell** (the `aura` CLI) imports everything, +downstream World programs. It also carries a direct production dependency on +the external `data-server` tree (inherited from the shell with the recipe: the +runner constructs archive servers itself). The external tree therefore enters +the workspace at the ingestion edge (`aura-ingest`, External components), the +assembly position (`aura-runner`), and the shell (which imports everything by +definition) — and at no ladder or column crate beyond those; the +`c28_layering` guard pins the exact set. The **shell** (the `aura` CLI) imports everything, is imported by nothing, exports nothing, and holds no domain logic: -argv/dispatch, argv→document translation, and presentation only (rendering -stays in the shell until the C22 web face provides its second consumer — #295 -deferral). +argv/dispatch, argv→document translation (including the op-script construction +front-end, `graph_construct`), presentation, and the `aura new` project +scaffolder (authoring-tooling template emission — shell-resident, like +rendering, until a second consumer wants it). Rendering stays in the shell +until the C22 web face provides its second consumer (#295 deferral). **Import rule.** An outer ladder layer may import inner layers, never the reverse; *measurement* and *strategy* are siblings (no import either way); @@ -2738,10 +2746,11 @@ cleanly by layer, so the layering is realized only partially: interweave is resolved (phase 5, #291): the backtest reductions live in `aura-backtest::metrics` beside their `position_management` producer, and `aura-analysis` is reduced to domain-free statistics + selection provenance - (`[dependencies]` = serde only). No crate exists yet for *measurement* (its run - verb is the additive shape dispatch, #286) or *execution* (unbuilt — the C10/C13 - edge only). Under this model the `aura-registry → aura-research` edge is - column-internal and legal. + (`[dependencies]` = serde only). The *measurement* rung is seeded (#295): + `aura-measurement` carries the IC vocabulary + reduction (its run verb remains + the additive shape dispatch, #286). No crate exists yet for *execution* + (unbuilt — the C10/C13 edge only). Under this model the + `aura-registry → aura-research` edge is column-internal and legal. - Phased realization (each independently shippable; behaviour byte-identical except the purely additive shape dispatch): (1) this contract; (2) cut the engine's backtest-metrics edge via `RunReport` — **done** (#292: