diff --git a/Cargo.lock b/Cargo.lock index 0ccb3d8ed7..2f2518bdc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,7 +453,6 @@ dependencies = [ "anyhow", "aprender-contracts", "aprender-contracts-macros", - "aprender-core", "aprender-cuda-edge", "aprender-gemm-codegen", "aprender-gpu", diff --git a/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd b/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd new file mode 100644 index 0000000000..32265c8925 --- /dev/null +++ b/ci/explicit-test-commands.d/298-aprender-compute-tuner-falsification-5803.cmd @@ -0,0 +1 @@ +cargo test -p aprender-compute --test tuner_falsification diff --git a/crates/aprender-compute/Cargo.toml b/crates/aprender-compute/Cargo.toml index 1aa9e9f4ab..ce1ee5b588 100644 --- a/crates/aprender-compute/Cargo.toml +++ b/crates/aprender-compute/Cargo.toml @@ -63,11 +63,9 @@ anyhow = "1.0.100" # Hardware capability detection (PMAT-447) chrono = "0.4" toml = "0.8" -# ML tuner integration (SHOWCASE-BRICK-001, Section 12) — aprender RandomForest is a -# DEV-dependency only: the SIMD foundation must not depend on the ML layer (layer -# inversion). The `ml-tuner` showcase lives in examples/ml_tuner_demo.rs + the tuner -# falsification tests, which consume aprender directly. Dev-dep path cycles are legal -# in cargo and dropped on publish, keeping the workspace a self-contained DAG (APR-MONO). +# ML tuner integration (SHOWCASE-BRICK-001, Section 12) — the SIMD foundation takes no +# dependency, not even a dev one, on the ML layer. The aprender RandomForest tests live in +# crates/aprender-core/tests/tuner_training_data_quality.rs (FLOW-010, #5803). # NOTE: trueno-graph (=aprender-graph) removed (APR-MONO self-containment): aprender-graph # depends on trueno, so compute→graph closed a cycle. The `execution-graph` feature's only # trueno-graph use was the thin `to_csr()`/`graph_to_csr()` CsrGraph exporters; CSR export @@ -106,9 +104,6 @@ trueno-cuda-edge = { workspace = true } # Sparse and solver crates for contract tests trueno-sparse = { workspace = true } trueno-solve = { workspace = true } -# ML-tuner showcase (SHOWCASE-BRICK-001): aprender RandomForest, DEV-only to avoid the -# compute→core layer inversion. Path dev-dep cycle is legal + dropped on publish. -aprender = { path = "../aprender-core", package = "aprender-core", default-features = false } ndarray = "0.17.2" faer = "0.24" # For GEMM benchmark comparison (fastest pure-Rust BLAS) matrixmultiply = "0.3" # Direct matrixmultiply crate benchmark (engine behind ndarray) @@ -139,9 +134,8 @@ hardware-detect = ["dep:dirs"] tui-monitor = ["presentar-core", "presentar-terminal", "crossterm", "trueno-gpu", "dep:tracing", "tracing-subscriber", "tracing-appender", "dep:dirs"] # CUDA support for TUI (real hardware monitoring) cuda = ["trueno-gpu/cuda"] -# ML-based kernel selection and throughput prediction (SHOWCASE-BRICK-001) -# Showcase: feed trueno tuner features into aprender RandomForest (example + tests only; -# aprender is a dev-dependency, so this feature activates no lib dependency). +# DEPRECATED no-op, kept so removing it is not a semver break for 0.71.x users (activates nothing; +# the aprender RandomForest tests moved to aprender-core, #5803). ml-tuner = [] # NOTE: `execution-graph` feature removed (APR-MONO self-containment) — its only purpose # was the trueno-graph CsrGraph exporters (see above). The exec-graph profiler itself diff --git a/crates/aprender-compute/examples/ml_tuner_demo.rs b/crates/aprender-compute/examples/ml_tuner_demo.rs index b176d6e348..6adb5f46f7 100644 --- a/crates/aprender-compute/examples/ml_tuner_demo.rs +++ b/crates/aprender-compute/examples/ml_tuner_demo.rs @@ -6,9 +6,6 @@ //! //! Run with default (heuristic models): //! cargo run --example ml_tuner_demo -//! -//! Run with RandomForest models (requires ml-tuner feature): -//! cargo run --example ml_tuner_demo --features ml-tuner use trueno::tuner::{BrickTuner, KernelClassifier, QuantType, ThroughputRegressor, TunerFeatures}; @@ -163,101 +160,15 @@ fn demo_full_tuner(features: &TunerFeatures) { println!(); } -/// Section 6: RandomForest models (ml-tuner showcase, SHOWCASE-BRICK-001). -/// -/// The SIMD `trueno` tuner produces `TunerFeatures` vectors; this section feeds them -/// directly into `aprender::RandomForestRegressor`/`RandomForestClassifier`. aprender is -/// a **dev-dependency** of `aprender-compute`, so the compute lib itself never depends on -/// the ML layer — the showcase lives entirely here at the example layer, above both. +/// Section 6: RandomForest models (SHOWCASE-BRICK-001). The aprender half of the showcase +/// is tested in `crates/aprender-core/tests/tuner_training_data_quality.rs`: compute takes +/// no dependency on the ML layer, not even a dev one (FLOW-010, #5803). fn demo_random_forest(features: &TunerFeatures) { - #[cfg(feature = "ml-tuner")] - { - use aprender::primitives::{Matrix, Vector}; - use aprender::tree::{RandomForestClassifier, RandomForestRegressor}; - - println!("6. RandomForest Models (ml-tuner showcase)"); - println!(" --------------------------------------"); - - let dim = TunerFeatures::DIM; - let n = 100usize; - - // Regressor: trueno features -> predicted throughput (tok/s). - let mut xr = Vec::with_capacity(n * dim); - let mut yr = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .gpu_mem_bw_gbs(1000.0) - .cuda_graphs(batch == 1) - .build(); - xr.extend(f.to_vector()); - yr.push(200.0 + (batch as f32) * 80.0 + (i as f32 * 0.5)); - } - println!(" Generated {n} regression samples"); - match Matrix::from_vec(n, dim, xr) { - Ok(x_mat) => { - let mut rf = RandomForestRegressor::new(100); - match rf.fit(&x_mat, &Vector::from_vec(yr)) { - Ok(()) => { - let fx = Matrix::from_vec(1, dim, features.to_vector().to_vec()) - .expect("single-row feature matrix"); - let pred = rf.predict(&fx); - println!( - " RF regressor: SUCCESS — M=4 prediction {:.1} tok/s", - pred.as_slice()[0] - ); - } - Err(e) => println!(" RF regressor: FAILED - {e}"), - } - } - Err(e) => println!(" RF regressor matrix: FAILED - {e}"), - } - - // Classifier: trueno features -> kernel-type label (3=Batched, 2=Vectorized). - let mut xc = Vec::with_capacity(n * dim); - let mut yc: Vec = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .quant_type(QuantType::Q4K) - .build(); - xc.extend(f.to_vector()); - yc.push(if batch >= 4 { 3 } else { 2 }); - } - match Matrix::from_vec(n, dim, xc) { - Ok(xc_mat) => { - let mut rf = RandomForestClassifier::new(50); - match rf.fit(&xc_mat, &yc) { - Ok(()) => { - let preds = rf.predict(&xc_mat); - let correct = - yc.iter().zip(preds.iter()).filter(|(a, b)| **a == **b).count(); - println!( - " RF classifier: SUCCESS — train accuracy {:.1}%", - correct as f32 / n as f32 * 100.0 - ); - } - Err(e) => println!(" RF classifier: FAILED - {e}"), - } - } - Err(e) => println!(" RF classifier matrix: FAILED - {e}"), - } - println!(); - } - - #[cfg(not(feature = "ml-tuner"))] - { - let _ = features; - println!("6. RandomForest Models"); - println!(" --------------------"); - println!(" [Disabled - enable with: --features ml-tuner]"); - println!(); - } + let _ = features; + println!("6. RandomForest Models"); + println!(" --------------------"); + println!(" [See aprender-core tests/tuner_training_data_quality.rs]"); + println!(); } fn main() { @@ -279,7 +190,7 @@ fn main() { println!(" - TunerFeatures: 42-dimension vector for ML models"); println!(" - Throughput prediction with roofline clamping (v1.1.0)"); println!(" - Kernel selection: BatchedQ4K for M>=4, VectorizedQ4K otherwise"); - println!(" - RandomForest available with --features ml-tuner"); + println!(" - RandomForest showcase: aprender-core tests/tuner_training_data_quality.rs"); println!(); println!("Next steps:"); println!(" cargo run --example quickstart # Basic trueno usage"); diff --git a/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs b/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs index 77e2f9ec82..759cab2cd5 100644 --- a/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs +++ b/crates/aprender-compute/tests/tuner_falsification/training_data_quality.rs @@ -1,101 +1,11 @@ //! F041-F060: Training Data Quality (20 points) //! -//! The `ml-tuner` showcase (SHOWCASE-BRICK-001) feeds trueno `TunerFeatures` into -//! `aprender::RandomForestRegressor`/`Classifier`. aprender is a dev-dependency of -//! aprender-compute (the SIMD foundation never depends on the ML layer), so these -//! falsification tests drive the aprender RandomForest pipeline directly, mirroring the -//! `<10 samples → InsufficientData` guard the old lib wrapper enforced. +//! The aprender RandomForest half (F041-F044, F046-F048) lives in +//! `crates/aprender-core/tests/tuner_training_data_quality.rs`: the SIMD foundation +//! takes no dependency, not even a dev one, on the ML layer (FLOW-010, #5803). use trueno::tuner::{ThroughputRegressor, TunerFeatures}; -#[allow(unused_imports)] -use trueno::tuner::{KernelClassifier, QuantType}; - -/// Train an aprender RandomForest regressor on trueno features, with the showcase's -/// `<10 samples` guard. Returns `Err` on insufficient data or matrix/fit failure. -#[cfg(feature = "ml-tuner")] -fn fit_throughput_rf( - rows: &[(TunerFeatures, f32)], - n_trees: usize, -) -> Result { - use aprender::primitives::{Matrix, Vector}; - use aprender::tree::RandomForestRegressor; - if rows.len() < 10 { - return Err(format!("insufficient data: {} < 10", rows.len())); - } - let dim = TunerFeatures::DIM; - let n = rows.len(); - let mut x = Vec::with_capacity(n * dim); - let mut y = Vec::with_capacity(n); - for (f, t) in rows { - x.extend(f.to_vector()); - y.push(*t); - } - let x_mat = Matrix::from_vec(n, dim, x).map_err(|e| e.to_string())?; - let mut rf = RandomForestRegressor::new(n_trees); - rf.fit(&x_mat, &Vector::from_vec(y)).map_err(|e| e.to_string())?; - Ok(rf) -} - -/// F041: Empty training data should error -#[cfg(feature = "ml-tuner")] -#[test] -fn f041_empty_training_errors() { - let empty_data: Vec<(TunerFeatures, f32)> = vec![]; - let result = fit_throughput_rf(&empty_data, 10); - assert!(result.is_err(), "F041 FALSIFIED: empty training data should error"); -} - -/// F042: Single sample training should error gracefully (below the 10-sample floor) -#[cfg(feature = "ml-tuner")] -#[test] -fn f042_single_sample_graceful() { - let features = TunerFeatures::builder().model_params_b(1.5).build(); - let data = vec![(features, 100.0)]; - // Should error (insufficient), not panic - let _ = fit_throughput_rf(&data, 10); -} - -/// F043: Training with NaN labels should not produce NaN predictions -#[cfg(feature = "ml-tuner")] -#[test] -fn f043_nan_labels_error() { - use aprender::primitives::Matrix; - let mut data: Vec<(TunerFeatures, f32)> = (0..10) - .map(|i| (TunerFeatures::builder().batch_size(1 + (i % 8) as u32).build(), 100.0)) - .collect(); - data[0].1 = f32::NAN; - if let Ok(rf) = fit_throughput_rf(&data, 10) { - let fx = Matrix::from_vec(1, TunerFeatures::DIM, data[1].0.to_vector().to_vec()).unwrap(); - let pred = rf.predict(&fx); - assert!( - pred.as_slice()[0].is_finite() || pred.as_slice()[0].is_nan(), - "F043: prediction must be a well-defined float" - ); - } -} - -/// F044: Training with negative labels should be handled without panic -#[cfg(feature = "ml-tuner")] -#[test] -fn f044_negative_labels_handled() { - let data: Vec<(TunerFeatures, f32)> = (0..12) - .map(|i| { - let f = TunerFeatures::builder().batch_size(1 + (i % 8) as u32).build(); - (f, if i % 2 == 0 { -100.0 } else { 100.0 }) - }) - .collect(); - // Must not panic; result may be Ok or Err. - let _ = fit_throughput_rf(&data, 10); -} - -// Stub tests for non-ml-tuner builds -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f041_f044_ml_tuner_disabled() { - // Pass - these tests require ml-tuner feature -} - /// F045: Heuristic model should work without training #[test] fn f045_heuristic_no_training() { @@ -106,89 +16,6 @@ fn f045_heuristic_no_training() { assert!(pred.predicted_tps > 0.0, "F045 FALSIFIED: heuristic prediction failed"); } -/// F046: Training improves over heuristic (or doesn't regress) -#[cfg(feature = "ml-tuner")] -#[test] -fn f046_training_improves() { - // Generate training data that matches heuristic pattern - let training_data: Vec<(TunerFeatures, f32)> = (0..50) - .map(|i| { - let batch = 1 + (i % 8) as u32; - let features = TunerFeatures::builder() - .model_params_b(1.5) - .batch_size(batch) - .gpu_mem_bw_gbs(1000.0) - .build(); - // Throughput scales with batch size - let throughput = 100.0 + (batch as f32) * 50.0; - (features, throughput) - }) - .collect(); - - let result = fit_throughput_rf(&training_data, 50); - assert!(result.is_ok(), "F046 FALSIFIED: training failed: {:?}", result.err()); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f046_ml_tuner_disabled() { - // Pass -} - -/// F047: Large training set should not OOM -#[cfg(feature = "ml-tuner")] -#[test] -fn f047_large_training_no_oom() { - let training_data: Vec<(TunerFeatures, f32)> = (0..1000) - .map(|i| { - let features = TunerFeatures::builder() - .model_params_b((i % 10) as f32 * 0.5 + 0.5) - .batch_size((i % 8 + 1) as u32) - .build(); - (features, 100.0 + (i as f32)) - }) - .collect(); - - let result = fit_throughput_rf(&training_data, 10); - assert!(result.is_ok(), "F047 FALSIFIED: large training failed"); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f047_ml_tuner_disabled() { - // Pass -} - -/// F048: Classifier training should work (aprender RandomForestClassifier on trueno features) -#[cfg(feature = "ml-tuner")] -#[test] -fn f048_classifier_training() { - use aprender::primitives::Matrix; - use aprender::tree::RandomForestClassifier; - - let dim = TunerFeatures::DIM; - let n = 50usize; - let mut x = Vec::with_capacity(n * dim); - let mut y: Vec = Vec::with_capacity(n); - for i in 0..n { - let batch = 1 + (i % 8) as u32; - let f = TunerFeatures::builder().model_params_b(1.5).batch_size(batch).build(); - x.extend(f.to_vector()); - // Label: BatchedQ4K (3) for M>=4, VectorizedQ4K (2) otherwise - y.push(if batch >= 4 { 3 } else { 2 }); - } - let x_mat = Matrix::from_vec(n, dim, x).expect("feature matrix"); - let mut classifier = RandomForestClassifier::new(10); - let result = classifier.fit(&x_mat, &y); - assert!(result.is_ok(), "F048 FALSIFIED: classifier training failed: {:?}", result.err()); -} - -#[cfg(not(feature = "ml-tuner"))] -#[test] -fn f048_ml_tuner_disabled() { - // Pass -} - /// F049: Training data variance check #[test] fn f049_training_data_variance() { diff --git a/docs/roadmaps/entries/PMAT-5446.yaml b/docs/roadmaps/entries/PMAT-5446.yaml new file mode 100644 index 0000000000..50606c00b7 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5446.yaml @@ -0,0 +1,22 @@ +- id: PMAT-5446 + github_issue: 5446 + item_type: task + title: 'ci: shrink the slow quick-tier tests (apr-cli test_llm_band, contracts-cli ont4 gate tests) at the root' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - 'contracts-cli ont4_relations_gate, ont4b_shapes_gate and ont5_consistency_gate lint a planted fixture or copied files, not the full contracts tree, and each finishes well under 60 s (landed by #5893 via the EMR-001 integration PR #6309)' + - the pv-sat corpus() lint tests use a planted .pv fixture (same change) + - apr-cli commands::test_llm_band tests no longer take 60-240 s each (separate PR; not covered by #6309) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Entry minted for the #5893 planted-.pv tests carried by #6309 (quorum handoff triage-quorum-tickets-6294-6309-0718Z). Issue #5446 names both slow families; #5893 is the contracts-cli half (gate tests lint a fixture, 11/11 per its body). The test_llm_band half has no PR yet, so the item stays in_progress. + epic: null diff --git a/docs/roadmaps/entries/PMAT-5803.yaml b/docs/roadmaps/entries/PMAT-5803.yaml new file mode 100644 index 0000000000..122a4e256e --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5803.yaml @@ -0,0 +1,23 @@ +- id: PMAT-5803 + github_issue: 5803 + item_type: task + title: 'build: FLOW-010 cut 1 — move the tuner tests that need aprender-core out of aprender-compute so compute drops its dev-dependency on core' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - the tuner training-data-quality tests move from crates/aprender-compute/tests/tuner_falsification to crates/aprender-core/tests/tuner_training_data_quality.rs and still run (ci/explicit-test-commands.d fragments 295 and 298 name them) + - aprender-compute has no dev-dependency on aprender-core + - scripts/check_tuner_test_move.sh proves the move lost no test (count and names before and after) + - one PR, at most 400 lines or a recorded PMAT-Size-Waiver (the move was 810 lines in #6294 commit ebf773587a, waiver sanctioned by quorum triage-quorum-door6261-conditions-2136Z) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Scope taken from #6294 (commit 2, PMAT-5803; issue #5803, FLOW-010 five-whys on crate fan-out, cut 1). Entry minted so w5032 can cut this piece as its own PR. + epic: null diff --git a/docs/roadmaps/entries/PMAT-5909.yaml b/docs/roadmaps/entries/PMAT-5909.yaml new file mode 100644 index 0000000000..2563051515 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5909.yaml @@ -0,0 +1,23 @@ +- id: PMAT-5909 + github_issue: 5909 + item_type: task + title: 'build: split env_key out of aprender-contracts into aprender-contracts-key so a contracts edit stops dirtying 47 of 85 crates' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - a new crate crates/aprender-contracts-key holds env_key (src/lib.rs, README, Cargo.toml) and aprender-contracts re-uses it through build_helper + - the build.rs and Cargo.toml of each dependent crate (aprender-core, -compute, -db, -orchestrate, -present-cli, -present-core, -rag, -serve, -simulate, -train, -viz-ttop) depend on aprender-contracts-key instead of aprender-contracts + - cargo metadata --locked is clean and the crate is registered in scripts/release/publish-order.txt and scripts/cascade-publish.sh; publishing it stays on the release path + - one PR, at most 400 lines, no Sensitive-path change beyond the workspace member and publish order lines (cut from #6294 commit 4b8f637715) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Scope taken from #6294 (commit 1, PMAT-5909; issue #5909: aprender-contracts is 24 commits in 30 days and dirties 47 of 85 crates). #6294 is split by quorum triage-quorum-tickets-6294-6309-0718Z into one PR per ticket; this entry lets w5032 cut this piece. + epic: null diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index b5bd1bc998..5f1bb142f2 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25319,6 +25319,28 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 4780 +- id: PMAT-5446 + github_issue: 5446 + item_type: task + title: 'ci: shrink the slow quick-tier tests (apr-cli test_llm_band, contracts-cli ont4 gate tests) at the root' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - 'contracts-cli ont4_relations_gate, ont4b_shapes_gate and ont5_consistency_gate lint a planted fixture or copied files, not the full contracts tree, and each finishes well under 60 s (landed by #5893 via the EMR-001 integration PR #6309)' + - the pv-sat corpus() lint tests use a planted .pv fixture (same change) + - apr-cli commands::test_llm_band tests no longer take 60-240 s each (separate PR; not covered by #6309) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Entry minted for the #5893 planted-.pv tests carried by #6309 (quorum handoff triage-quorum-tickets-6294-6309-0718Z). Issue #5446 names both slow families; #5893 is the contracts-cli half (gate tests lint a fixture, 11/11 per its body). The test_llm_band half has no PR yet, so the item stays in_progress. + epic: null - id: PMAT-5463 github_issue: 5463 item_type: task @@ -25375,6 +25397,29 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 3863 +- id: PMAT-5803 + github_issue: 5803 + item_type: task + title: 'build: FLOW-010 cut 1 — move the tuner tests that need aprender-core out of aprender-compute so compute drops its dev-dependency on core' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - the tuner training-data-quality tests move from crates/aprender-compute/tests/tuner_falsification to crates/aprender-core/tests/tuner_training_data_quality.rs and still run (ci/explicit-test-commands.d fragments 295 and 298 name them) + - aprender-compute has no dev-dependency on aprender-core + - scripts/check_tuner_test_move.sh proves the move lost no test (count and names before and after) + - one PR, at most 400 lines or a recorded PMAT-Size-Waiver (the move was 810 lines in #6294 commit ebf773587a, waiver sanctioned by quorum triage-quorum-door6261-conditions-2136Z) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Scope taken from #6294 (commit 2, PMAT-5803; issue #5803, FLOW-010 five-whys on crate fan-out, cut 1). Entry minted so w5032 can cut this piece as its own PR. + epic: null - id: PMAT-5817 github_issue: 5817 item_type: task @@ -25550,6 +25595,29 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:measurement notes: null epic: 5510 +- id: PMAT-5909 + github_issue: 5909 + item_type: task + title: 'build: split env_key out of aprender-contracts into aprender-contracts-key so a contracts edit stops dirtying 47 of 85 crates' + status: in_progress + priority: medium + assigned_to: null + created: 2026-10-25T07:50:00Z + updated: 2026-10-25T07:50:00Z + spec: null + acceptance_criteria: + - a new crate crates/aprender-contracts-key holds env_key (src/lib.rs, README, Cargo.toml) and aprender-contracts re-uses it through build_helper + - the build.rs and Cargo.toml of each dependent crate (aprender-core, -compute, -db, -orchestrate, -present-cli, -present-core, -rag, -serve, -simulate, -train, -viz-ttop) depend on aprender-contracts-key instead of aprender-contracts + - cargo metadata --locked is clean and the crate is registered in scripts/release/publish-order.txt and scripts/cascade-publish.sh; publishing it stays on the release path + - one PR, at most 400 lines, no Sensitive-path change beyond the workspace member and publish order lines (cut from #6294 commit 4b8f637715) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: |- + Scope taken from #6294 (commit 1, PMAT-5909; issue #5909: aprender-contracts is 24 commits in 30 days and dirties 47 of 85 crates). #6294 is split by quorum triage-quorum-tickets-6294-6309-0718Z into one PR per ticket; this entry lets w5032 cut this piece. + epic: null - id: PMAT-5914 github_issue: 5914 item_type: task