diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 9476f3e05..50e8e32b9 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -14,10 +14,25 @@ ignore = [ "RUSTSEC-2025-0141", # bincode: transitive, widely used "RUSTSEC-2026-0002", # lru 0.12.5: transitive via ratatui - # wasmtime 27.0.0 — test-only dep (aprender-test-lib), not in production path + # wasmtime 27.0.0 — test-only dep (aprender-test-lib), not in production path. + # Upgrade to wasmtime 43 tracked in PR #731. All advisories are test-only. "RUSTSEC-2025-0046", "RUSTSEC-2025-0118", "RUSTSEC-2026-0020", "RUSTSEC-2026-0021", "RUSTSEC-2026-0087", + # wasmtime 27 batch 2026-04-09 — 10 new advisories (test-only, not production) + "RUSTSEC-2026-0085", + "RUSTSEC-2026-0086", + "RUSTSEC-2026-0088", + "RUSTSEC-2026-0089", + "RUSTSEC-2026-0091", + "RUSTSEC-2026-0092", + "RUSTSEC-2026-0093", + "RUSTSEC-2026-0094", + "RUSTSEC-2026-0095", + "RUSTSEC-2026-0096", + + # rand 0.10.0 — unsound with custom logger using rand::rng(), transitive via quickcheck + "RUSTSEC-2026-0097", ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed7e4c6ab..dc3a670c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,33 @@ jobs: cargo test -p aprender-core --test monorepo_invariants cargo test -p aprender-core --test readme_contract cargo test -p apr-cli --test cli_commands + - name: Fix file ownership (container runs as root, runner as noah:1000) + if: always() + run: | + # Five-whys: Docker container creates files as root on bind-mounted + # workspace. Runner (noah:1000) can't git-clean them on next run + # → checkout fails → CI breaks. This runs inside the container + # (as root) restoring host ownership for subsequent bare-metal jobs. + chown -R 1000:1000 "$GITHUB_WORKSPACE" || true + + # Top-level gate: satisfies org ruleset "Green Main" which requires check named "gate". + # The reusable workflow produces "ci / gate" but rulesets need exact match on "gate". + gate: + runs-on: self-hosted + needs: [ci, workspace-test] + if: always() + steps: + - name: Check required jobs + run: | + if [ "${{ needs.ci.result }}" != "success" ]; then + echo "ci failed: ${{ needs.ci.result }}" + exit 1 + fi + if [ "${{ needs.workspace-test.result }}" != "success" ]; then + echo "workspace-test failed: ${{ needs.workspace-test.result }}" + exit 1 + fi + echo "All required jobs passed" mutants: runs-on: self-hosted @@ -55,7 +82,7 @@ jobs: container: image: localhost:5000/sovereign-ci:stable timeout-minutes: 120 - needs: [ci, workspace-test] + needs: [gate] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 diff --git a/contracts/model-families/gptneox.yaml b/contracts/model-families/gptneox.yaml new file mode 100644 index 000000000..e8651454e --- /dev/null +++ b/contracts/model-families/gptneox.yaml @@ -0,0 +1,180 @@ +metadata: + version: "1.0" + created: "2026-04-12" + description: "Model family descriptor: gptneox" + kind: model-family + references: + - "https://huggingface.co/" + +family: gptneox +display_name: "GPT-NeoX / Pythia" +vendor: EleutherAI +architectures: + - GPTNeoXForCausalLM +hf_pattern: "EleutherAI/pythia-*" + +size_variants: + 70m: + parameters: "70M" + hidden_dim: 512 + num_layers: 6 + num_heads: 8 + num_kv_heads: 8 + intermediate_dim: 2048 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 64 + rope_theta: 10000.0 + norm_eps: 0.00001 + 160m: + parameters: "160M" + hidden_dim: 768 + num_layers: 12 + num_heads: 12 + num_kv_heads: 12 + intermediate_dim: 3072 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 64 + rope_theta: 10000.0 + norm_eps: 0.00001 + 410m: + parameters: "410M" + hidden_dim: 1024 + num_layers: 24 + num_heads: 16 + num_kv_heads: 16 + intermediate_dim: 4096 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 64 + rope_theta: 10000.0 + norm_eps: 0.00001 + 1b: + parameters: "1B" + hidden_dim: 2048 + num_layers: 16 + num_heads: 8 + num_kv_heads: 8 + intermediate_dim: 8192 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 256 + rope_theta: 10000.0 + norm_eps: 0.00001 + 1.4b: + parameters: "1.4B" + hidden_dim: 2048 + num_layers: 24 + num_heads: 16 + num_kv_heads: 16 + intermediate_dim: 8192 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 128 + rope_theta: 10000.0 + norm_eps: 0.00001 + 2.8b: + parameters: "2.8B" + hidden_dim: 2560 + num_layers: 32 + num_heads: 32 + num_kv_heads: 32 + intermediate_dim: 10240 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 80 + rope_theta: 10000.0 + norm_eps: 0.00001 + 6.9b: + parameters: "6.9B" + hidden_dim: 4096 + num_layers: 32 + num_heads: 32 + num_kv_heads: 32 + intermediate_dim: 16384 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 128 + rope_theta: 10000.0 + norm_eps: 0.00001 + 12b: + parameters: "12B" + hidden_dim: 5120 + num_layers: 36 + num_heads: 40 + num_kv_heads: 40 + intermediate_dim: 20480 + vocab_size: 50304 + max_position_embeddings: 2048 + head_dim: 128 + rope_theta: 10000.0 + norm_eps: 0.00001 + +constraints: + attention_type: mha + activation: gelu + norm_type: layernorm + has_bias: true + tied_embeddings: false + positional_encoding: rope + mlp_type: gelu_mlp + +# GPT-NeoX tensor naming: gpt_neox.layers.{n}.* with fused query_key_value. +# Mapped to APR canonical names by Architecture::gpt_neox_map_name(). +# +# HuggingFace raw name mapping (for reference): +# gpt_neox.embed_in.weight -> model.embed_tokens.weight +# gpt_neox.layers.{n}.input_layernorm.weight -> model.layers.{n}.input_layernorm.weight +# gpt_neox.layers.{n}.input_layernorm.bias -> model.layers.{n}.input_layernorm.bias +# gpt_neox.layers.{n}.post_attention_layernorm.* -> model.layers.{n}.post_attention_layernorm.* +# gpt_neox.layers.{n}.attention.query_key_value.* -> split into q_proj/k_proj/v_proj +# gpt_neox.layers.{n}.attention.dense.* -> model.layers.{n}.self_attn.o_proj.* +# gpt_neox.layers.{n}.mlp.dense_h_to_4h.* -> model.layers.{n}.mlp.up_proj.* +# gpt_neox.layers.{n}.mlp.dense_4h_to_h.* -> model.layers.{n}.mlp.down_proj.* +# gpt_neox.final_layer_norm.* -> model.norm.* +# embed_out.weight -> lm_head.weight +tensor_template: + embedding: "model.embed_tokens.weight" + lm_head: "lm_head.weight" + final_norm: "model.norm.weight" + per_layer: + # QKV is fused in source; split by Architecture::split_neox_fused_qkv() + q_proj_weight: "model.layers.{n}.self_attn.q_proj.weight" + q_proj_bias: "model.layers.{n}.self_attn.q_proj.bias" + k_proj_weight: "model.layers.{n}.self_attn.k_proj.weight" + k_proj_bias: "model.layers.{n}.self_attn.k_proj.bias" + v_proj_weight: "model.layers.{n}.self_attn.v_proj.weight" + v_proj_bias: "model.layers.{n}.self_attn.v_proj.bias" + o_proj_weight: "model.layers.{n}.self_attn.o_proj.weight" + o_proj_bias: "model.layers.{n}.self_attn.o_proj.bias" + up_proj_weight: "model.layers.{n}.mlp.up_proj.weight" + up_proj_bias: "model.layers.{n}.mlp.up_proj.bias" + down_proj_weight: "model.layers.{n}.mlp.down_proj.weight" + down_proj_bias: "model.layers.{n}.mlp.down_proj.bias" + input_layernorm_weight: "model.layers.{n}.input_layernorm.weight" + input_layernorm_bias: "model.layers.{n}.input_layernorm.bias" + post_attention_layernorm_weight: "model.layers.{n}.post_attention_layernorm.weight" + post_attention_layernorm_bias: "model.layers.{n}.post_attention_layernorm.bias" + gate_proj_weight: null + gate_proj_bias: null + +shape_template: + embedding: "[vocab_size, hidden_dim]" + lm_head: "[vocab_size, hidden_dim]" + final_norm: "[hidden_dim]" + q_proj: "[hidden_dim, hidden_dim]" + k_proj: "[hidden_dim, hidden_dim]" + v_proj: "[hidden_dim, hidden_dim]" + o_proj: "[hidden_dim, hidden_dim]" + up_proj: "[hidden_dim, intermediate_dim]" + down_proj: "[intermediate_dim, hidden_dim]" + input_layernorm: "[hidden_dim]" + post_attention_layernorm: "[hidden_dim]" + bias: "[hidden_dim]" + +quantizations: + - q4_k_m + - q8_0 + - f16 + - f32 diff --git a/contracts/model-families/opt.yaml b/contracts/model-families/opt.yaml new file mode 100644 index 000000000..e8810c6b0 --- /dev/null +++ b/contracts/model-families/opt.yaml @@ -0,0 +1,154 @@ +metadata: + version: "1.0" + created: "2026-04-12" + description: "Model family descriptor: opt" + kind: model-family + references: + - "https://huggingface.co/" + +family: opt +display_name: "Meta OPT" +vendor: Meta +architectures: + - OPTForCausalLM + - OPTModel +hf_pattern: "facebook/opt-*" + +size_variants: + 125m: + parameters: "125M" + hidden_dim: 768 + num_layers: 12 + num_heads: 12 + num_kv_heads: 12 + intermediate_dim: 3072 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 64 + norm_eps: 0.00001 + 350m: + parameters: "350M" + hidden_dim: 1024 + num_layers: 24 + num_heads: 16 + num_kv_heads: 16 + intermediate_dim: 4096 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 64 + norm_eps: 0.00001 + 1.3b: + parameters: "1.3B" + hidden_dim: 2048 + num_layers: 24 + num_heads: 32 + num_kv_heads: 32 + intermediate_dim: 8192 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 64 + norm_eps: 0.00001 + 2.7b: + parameters: "2.7B" + hidden_dim: 2560 + num_layers: 32 + num_heads: 32 + num_kv_heads: 32 + intermediate_dim: 10240 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 80 + norm_eps: 0.00001 + 6.7b: + parameters: "6.7B" + hidden_dim: 4096 + num_layers: 32 + num_heads: 32 + num_kv_heads: 32 + intermediate_dim: 16384 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 128 + norm_eps: 0.00001 + 13b: + parameters: "13B" + hidden_dim: 5120 + num_layers: 40 + num_heads: 40 + num_kv_heads: 40 + intermediate_dim: 20480 + vocab_size: 50272 + max_position_embeddings: 2048 + head_dim: 128 + norm_eps: 0.00001 + +constraints: + attention_type: mha + activation: relu + norm_type: layernorm + has_bias: true + tied_embeddings: false + positional_encoding: absolute + mlp_type: gelu_mlp # Standard 2-layer MLP (up→activation→down, no gate). Uses ReLU activation. + +# OPT tensor naming: model.decoder.layers.{n}.* with separate Q/K/V. +# Mapped to APR canonical names by Architecture::opt_map_name(). +# +# HuggingFace raw name mapping (for reference): +# model.decoder.embed_tokens.weight -> model.embed_tokens.weight +# model.decoder.embed_positions.weight -> model.position_embedding.weight +# model.decoder.layers.{n}.self_attn_layer_norm.* -> model.layers.{n}.input_layernorm.* +# model.decoder.layers.{n}.final_layer_norm.* -> model.layers.{n}.post_attention_layernorm.* +# model.decoder.layers.{n}.self_attn.q_proj.* -> model.layers.{n}.self_attn.q_proj.* +# model.decoder.layers.{n}.self_attn.k_proj.* -> model.layers.{n}.self_attn.k_proj.* +# model.decoder.layers.{n}.self_attn.v_proj.* -> model.layers.{n}.self_attn.v_proj.* +# model.decoder.layers.{n}.self_attn.out_proj.* -> model.layers.{n}.self_attn.o_proj.* +# model.decoder.layers.{n}.fc1.* -> model.layers.{n}.mlp.up_proj.* +# model.decoder.layers.{n}.fc2.* -> model.layers.{n}.mlp.down_proj.* +# model.decoder.final_layer_norm.* -> model.norm.* +# lm_head.weight -> lm_head.weight +tensor_template: + embedding: "model.embed_tokens.weight" + position_embedding: "model.position_embedding.weight" + lm_head: "lm_head.weight" + final_norm: "model.norm.weight" + per_layer: + q_proj_weight: "model.layers.{n}.self_attn.q_proj.weight" + q_proj_bias: "model.layers.{n}.self_attn.q_proj.bias" + k_proj_weight: "model.layers.{n}.self_attn.k_proj.weight" + k_proj_bias: "model.layers.{n}.self_attn.k_proj.bias" + v_proj_weight: "model.layers.{n}.self_attn.v_proj.weight" + v_proj_bias: "model.layers.{n}.self_attn.v_proj.bias" + o_proj_weight: "model.layers.{n}.self_attn.o_proj.weight" + o_proj_bias: "model.layers.{n}.self_attn.o_proj.bias" + up_proj_weight: "model.layers.{n}.mlp.up_proj.weight" + up_proj_bias: "model.layers.{n}.mlp.up_proj.bias" + down_proj_weight: "model.layers.{n}.mlp.down_proj.weight" + down_proj_bias: "model.layers.{n}.mlp.down_proj.bias" + input_layernorm_weight: "model.layers.{n}.input_layernorm.weight" + input_layernorm_bias: "model.layers.{n}.input_layernorm.bias" + post_attention_layernorm_weight: "model.layers.{n}.post_attention_layernorm.weight" + post_attention_layernorm_bias: "model.layers.{n}.post_attention_layernorm.bias" + gate_proj_weight: null + gate_proj_bias: null + +shape_template: + embedding: "[vocab_size, hidden_dim]" + position_embedding: "[max_position_embeddings + 2, hidden_dim]" + lm_head: "[vocab_size, hidden_dim]" + final_norm: "[hidden_dim]" + q_proj: "[hidden_dim, hidden_dim]" + k_proj: "[hidden_dim, hidden_dim]" + v_proj: "[hidden_dim, hidden_dim]" + o_proj: "[hidden_dim, hidden_dim]" + up_proj: "[hidden_dim, intermediate_dim]" + down_proj: "[intermediate_dim, hidden_dim]" + input_layernorm: "[hidden_dim]" + post_attention_layernorm: "[hidden_dim]" + bias: "[hidden_dim]" + +quantizations: + - q4_k_m + - q8_0 + - f16 + - f32 diff --git a/contracts/model-family-parity-v1.yaml b/contracts/model-family-parity-v1.yaml new file mode 100644 index 000000000..a9408d460 --- /dev/null +++ b/contracts/model-family-parity-v1.yaml @@ -0,0 +1,78 @@ +metadata: + version: "1.0.0" + created: "2026-04-12" + author: PAIML Engineering + registry: true + references: + - "docs/specifications/aprender-monorepo-consolidation.md" + - "docs/specifications/archive/compiler-enforced-model-types-model-oracle.md" + description: | + PMAT-546: Architecture enum ↔ model-family YAML 1:1 parity contract. + Every non-Auto Architecture variant MUST have a matching model-family YAML, + and every model-family YAML MUST have a matching Architecture variant. + +kind: ParityContract +name: model-family-parity +version: "1.0.0" +scope: "Architecture enum (converter_types.rs) ↔ contracts/model-families/*.yaml" + +description: | + Five-Whys: Why this contract? + + 1. Why did PMAT-526 miss 5 model families? Architecture enum and YAML contracts + were authored at different times with no cross-validation. + 2. Why no cross-validation? Each was treated as an independent deliverable. + 3. Why independent? No single contract enforced the invariant that they must match. + 4. Why no enforcement? The parity invariant was implicit, not codified. + 5. Why not codified? Because nobody had yet written the contract. This contract + closes the gap with a falsifiable CI test. + + Root cause: implicit assumption that YAML contracts and Rust enum variants + would stay in sync without enforcement. + +equations: + enum_has_yaml: + description: "Every non-Auto Architecture variant has a matching model-family YAML" + formula: "∀ variant ∈ Architecture \\ {Auto} : ∃ file ∈ contracts/model-families/{variant_key}.yaml" + note: "variant_key is the lowercase/snake_case form of the variant name" + + yaml_has_enum: + description: "Every model-family YAML has a matching Architecture variant" + formula: "∀ file ∈ contracts/model-families/*.yaml \\ {_schema.yaml} : ∃ variant ∈ Architecture where from_model_type(family) = Some(variant)" + note: "family is the 'family' field in the YAML file" + + display_name_exhaustive: + description: "display_name() covers all variants (no wildcard fallback)" + formula: "∀ variant ∈ Architecture : display_name(variant) ≠ variant.debug_name()" + note: "Ensures every variant has a human-readable name, not just Debug formatting" + + is_llm_classified: + description: "Every non-Auto variant is explicitly classified as LLM or non-LLM" + formula: "∀ variant ∈ Architecture \\ {Auto} : is_llm(variant) ∨ ¬is_llm(variant) is intentional" + note: "Audio models (Whisper, Moonshine) and BERT return false; all others return true" + +falsification: + - id: FALSIFY-PARITY-001 + description: "Enum→YAML parity: every non-Auto variant has a YAML file" + test: "tests in converter_types_tests_parity.rs: test_every_architecture_has_model_family_yaml" + evidence: "cargo test -p aprender-core --lib test_every_architecture_has_model_family_yaml" + + - id: FALSIFY-PARITY-002 + description: "YAML→Enum parity: every YAML family is recognized by from_model_type()" + test: "tests in converter_types_tests_parity.rs: test_every_model_family_yaml_has_architecture" + evidence: "cargo test -p aprender-core --lib test_every_model_family_yaml_has_architecture" + + - id: FALSIFY-PARITY-003 + description: "from_model_type() returns correct variant for all new architectures" + test: "tests in converter_types_tests_parity.rs: test_from_model_type_new_variants" + evidence: "cargo test -p aprender-core --lib test_from_model_type_new_variants" + + - id: FALSIFY-PARITY-004 + description: "display_name() is non-empty for all variants" + test: "tests in converter_types_tests_parity.rs: test_display_name_all_variants" + evidence: "cargo test -p aprender-core --lib test_display_name_all_variants" + + - id: FALSIFY-PARITY-005 + description: "is_llm() classification matches model-family contract constraints" + test: "tests in converter_types_tests_parity.rs: test_is_llm_matches_contract" + evidence: "cargo test -p aprender-core --lib test_is_llm_matches_contract" diff --git a/crates/aprender-core/src/format/converter/import_tests_infer_arch.rs b/crates/aprender-core/src/format/converter/import_tests_infer_arch.rs index 6eec15d20..0731bd1b6 100644 --- a/crates/aprender-core/src/format/converter/import_tests_infer_arch.rs +++ b/crates/aprender-core/src/format/converter/import_tests_infer_arch.rs @@ -457,3 +457,36 @@ let result = infer_architecture_from_names(&tensors); assert_eq!(result, Some("qwen3".to_string()), "QK norm must infer qwen3"); } + + // PMAT-546: Mamba and RWKV detection in import pipeline + #[test] + fn test_infer_arch_mamba_from_mixer() { + let mut tensors: BTreeMap, Vec)> = BTreeMap::new(); + tensors.insert( + "backbone.layers.0.mixer.in_proj.weight".to_string(), + (vec![1.0], vec![1]), + ); + tensors.insert( + "backbone.layers.0.mixer.out_proj.weight".to_string(), + (vec![1.0], vec![1]), + ); + + let result = infer_architecture_from_names(&tensors); + assert_eq!(result, Some("mamba".to_string())); + } + + #[test] + fn test_infer_arch_rwkv_from_blocks() { + let mut tensors: BTreeMap, Vec)> = BTreeMap::new(); + tensors.insert( + "rwkv.blocks.0.att.key.weight".to_string(), + (vec![1.0], vec![1]), + ); + tensors.insert( + "rwkv.blocks.0.ffn.key.weight".to_string(), + (vec![1.0], vec![1]), + ); + + let result = infer_architecture_from_names(&tensors); + assert_eq!(result, Some("rwkv".to_string())); + } diff --git a/crates/aprender-core/src/format/converter/tests/core.rs b/crates/aprender-core/src/format/converter/tests/core.rs index 19b7feb35..59a558da4 100644 --- a/crates/aprender-core/src/format/converter/tests/core.rs +++ b/crates/aprender-core/src/format/converter/tests/core.rs @@ -210,7 +210,7 @@ mod tests_name_mapping { /// FALSIFY-TNAME-APRENDER-002: Unknown architectures return None (not panic). #[test] fn test_falsify_from_model_type_unknown_returns_none() { - let unknowns = ["mamba", "rwkv", "jamba", "future_model_2027", ""]; + let unknowns = ["jamba", "future_model_2027", ""]; for name in &unknowns { assert_eq!( Architecture::from_model_type(name), diff --git a/crates/aprender-core/src/format/converter/tokenizer_loader.rs b/crates/aprender-core/src/format/converter/tokenizer_loader.rs index 92227c94f..83db2026b 100644 --- a/crates/aprender-core/src/format/converter/tokenizer_loader.rs +++ b/crates/aprender-core/src/format/converter/tokenizer_loader.rs @@ -410,6 +410,22 @@ fn infer_architecture_from_names( .any(|k| k.starts_with("h.") && k.contains(".attn.")); let has_blk = tensors.keys().any(|k| k.contains("blk.")); + // PMAT-546: Detect Mamba (backbone.layers.N.mixer.*) — SSM, not transformer + let has_mamba = tensors + .keys() + .any(|k| k.contains("mixer.in_proj") || k.contains("mixer.out_proj")); + if has_mamba { + return Some("mamba".to_string()); + } + + // PMAT-546: Detect RWKV (rwkv.blocks.N.*) + let has_rwkv = tensors + .keys() + .any(|k| k.starts_with("rwkv.blocks.") || k.contains("blocks.0.att.")); + if has_rwkv { + return Some("rwkv".to_string()); + } + // GH-311: Detect GPT-NeoX (gpt_neox.layers.N.*) — must check before model.layers let has_gpt_neox = tensors.keys().any(|k| k.starts_with("gpt_neox.")); if has_gpt_neox { diff --git a/crates/aprender-core/src/format/converter_types.rs b/crates/aprender-core/src/format/converter_types.rs index a182454ae..5c32dc507 100644 --- a/crates/aprender-core/src/format/converter_types.rs +++ b/crates/aprender-core/src/format/converter_types.rs @@ -127,6 +127,16 @@ pub enum Architecture { Gemma, /// Mistral AI (Mistral, Mixtral) Mistral, + /// TII Falcon-H1 (Hybrid Transformer+SSM) + FalconH1, + /// state-spaces Mamba (State Space Model) + Mamba, + /// `UsefulSensors` Moonshine (audio, encoder-decoder) + Moonshine, + /// Apple `OpenELM` (variable-width attention) + OpenElm, + /// RWKV-7 (linear attention / recurrence) + Rwkv7, } include!("tensor_expectation.rs"); diff --git a/crates/aprender-core/src/format/converter_types_tests.rs b/crates/aprender-core/src/format/converter_types_tests.rs index 49b58307a..3b538a95c 100644 --- a/crates/aprender-core/src/format/converter_types_tests.rs +++ b/crates/aprender-core/src/format/converter_types_tests.rs @@ -4,4 +4,5 @@ mod tests { use super::*; include!("converter_types_tests_source_parse.rs"); include!("converter_types_tests_gpt2_split.rs"); +include!("converter_types_tests_parity.rs"); } diff --git a/crates/aprender-core/src/format/converter_types_tests_parity.rs b/crates/aprender-core/src/format/converter_types_tests_parity.rs new file mode 100644 index 000000000..4ec0cb5d9 --- /dev/null +++ b/crates/aprender-core/src/format/converter_types_tests_parity.rs @@ -0,0 +1,227 @@ + + // ======================================================================== + // PMAT-546: Architecture ↔ model-family YAML parity tests + // Contract: contracts/model-family-parity-v1.yaml + // ======================================================================== + + /// All non-Auto Architecture variants and their expected YAML family key. + /// This table is the single source of truth for the parity mapping. + const ARCH_YAML_MAP: &[(Architecture, &str)] = &[ + (Architecture::Whisper, "whisper"), + (Architecture::Llama, "llama"), + (Architecture::Bert, "bert"), + (Architecture::Qwen2, "qwen2"), + (Architecture::Qwen3, "qwen3"), + (Architecture::Qwen3_5, "qwen3_5"), + (Architecture::Gpt2, "gpt2"), + (Architecture::Phi, "phi"), + (Architecture::GptNeoX, "gptneox"), + (Architecture::Opt, "opt"), + (Architecture::DeepSeek, "deepseek"), + (Architecture::Gemma, "gemma"), + (Architecture::Mistral, "mistral"), + (Architecture::FalconH1, "falcon_h1"), + (Architecture::Mamba, "mamba"), + (Architecture::Moonshine, "moonshine"), + (Architecture::OpenElm, "openelm"), + (Architecture::Rwkv7, "rwkv7"), + ]; + + /// FALSIFY-PARITY-001: Every non-Auto Architecture variant has a model-family YAML. + #[test] + fn test_every_architecture_has_model_family_yaml() { + let model_families_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crates dir") + .parent() + .expect("workspace root") + .join("contracts/model-families"); + + let mut missing = Vec::new(); + for (arch, yaml_key) in ARCH_YAML_MAP { + let yaml_path = model_families_dir.join(format!("{yaml_key}.yaml")); + if !yaml_path.exists() { + missing.push(format!( + "Architecture::{arch:?} → contracts/model-families/{yaml_key}.yaml (NOT FOUND)" + )); + } + } + assert!( + missing.is_empty(), + "FALSIFY-PARITY-001 FAIL: Architecture variants missing YAML contracts:\n {}", + missing.join("\n ") + ); + } + + /// FALSIFY-PARITY-002: Every model-family YAML is recognized by from_model_type(). + #[test] + fn test_every_model_family_yaml_has_architecture() { + let model_families_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crates dir") + .parent() + .expect("workspace root") + .join("contracts/model-families"); + + let mut unrecognized = Vec::new(); + for entry in std::fs::read_dir(&model_families_dir).expect("read model-families dir") { + let entry = entry.expect("dir entry"); + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + // Skip schema files + if filename_str.starts_with('_') || !filename_str.ends_with(".yaml") { + continue; + } + + let family_key = filename_str.trim_end_matches(".yaml"); + + // Read the YAML to get the family field + let contents = + std::fs::read_to_string(entry.path()).expect("read YAML"); + let family_field = contents + .lines() + .find(|l| l.starts_with("family:")) + .map(|l| l.trim_start_matches("family:").trim().to_string()) + .unwrap_or_else(|| family_key.to_string()); + + if Architecture::from_model_type(&family_field).is_none() { + unrecognized.push(format!( + "{filename_str} (family: {family_field}) → from_model_type() returns None" + )); + } + } + assert!( + unrecognized.is_empty(), + "FALSIFY-PARITY-002 FAIL: Model-family YAMLs not recognized by from_model_type():\n {}", + unrecognized.join("\n ") + ); + } + + /// FALSIFY-PARITY-003: from_model_type() returns correct variant for all new architectures. + #[test] + fn test_from_model_type_new_variants() { + // FalconH1 + assert_eq!(Architecture::from_model_type("falcon_h1"), Some(Architecture::FalconH1)); + assert_eq!(Architecture::from_model_type("falcon-h1"), Some(Architecture::FalconH1)); + assert_eq!(Architecture::from_model_type("falcon3"), Some(Architecture::FalconH1)); + + // Mamba + assert_eq!(Architecture::from_model_type("mamba"), Some(Architecture::Mamba)); + assert_eq!(Architecture::from_model_type("mamba2"), Some(Architecture::Mamba)); + + // Moonshine + assert_eq!(Architecture::from_model_type("moonshine"), Some(Architecture::Moonshine)); + + // OpenELM + assert_eq!(Architecture::from_model_type("openelm"), Some(Architecture::OpenElm)); + + // RWKV-7 + assert_eq!(Architecture::from_model_type("rwkv"), Some(Architecture::Rwkv7)); + assert_eq!(Architecture::from_model_type("rwkv7"), Some(Architecture::Rwkv7)); + assert_eq!(Architecture::from_model_type("rwkv-7"), Some(Architecture::Rwkv7)); + } + + /// FALSIFY-PARITY-004: display_name() returns a non-empty human-readable string for all variants. + #[test] + fn test_display_name_all_variants() { + let all_variants = [ + Architecture::Auto, + Architecture::Whisper, + Architecture::Llama, + Architecture::Bert, + Architecture::Qwen2, + Architecture::Qwen3, + Architecture::Qwen3_5, + Architecture::Gpt2, + Architecture::Phi, + Architecture::GptNeoX, + Architecture::Opt, + Architecture::DeepSeek, + Architecture::Gemma, + Architecture::Mistral, + Architecture::FalconH1, + Architecture::Mamba, + Architecture::Moonshine, + Architecture::OpenElm, + Architecture::Rwkv7, + ]; + + for variant in &all_variants { + let name = variant.display_name(); + assert!( + !name.is_empty(), + "FALSIFY-PARITY-004 FAIL: Architecture::{variant:?} has empty display_name()" + ); + } + + // Verify specific display names for new variants + assert_eq!(Architecture::FalconH1.display_name(), "Falcon-H1"); + assert_eq!(Architecture::Mamba.display_name(), "Mamba"); + assert_eq!(Architecture::Moonshine.display_name(), "Moonshine"); + assert_eq!(Architecture::OpenElm.display_name(), "OpenELM"); + assert_eq!(Architecture::Rwkv7.display_name(), "RWKV-7"); + } + + /// FALSIFY-PARITY-005: is_llm() classification matches expected categories. + /// Audio models and encoder-only models return false; decoder-only LLMs return true. + #[test] + fn test_is_llm_matches_contract() { + // Non-LLM architectures (audio, encoder-only) + assert!(!Architecture::Auto.is_llm(), "Auto should not be classified as LLM"); + assert!(!Architecture::Whisper.is_llm(), "Whisper (audio) should not be LLM"); + assert!(!Architecture::Bert.is_llm(), "BERT (encoder-only) should not be LLM"); + assert!(!Architecture::Moonshine.is_llm(), "Moonshine (audio) should not be LLM"); + + // LLM architectures (decoder-only text generation) + assert!(Architecture::Llama.is_llm(), "LLaMA should be LLM"); + assert!(Architecture::Qwen2.is_llm(), "Qwen2 should be LLM"); + assert!(Architecture::Qwen3.is_llm(), "Qwen3 should be LLM"); + assert!(Architecture::Qwen3_5.is_llm(), "Qwen3.5 should be LLM"); + assert!(Architecture::Gpt2.is_llm(), "GPT-2 should be LLM"); + assert!(Architecture::Phi.is_llm(), "Phi should be LLM"); + assert!(Architecture::GptNeoX.is_llm(), "GPT-NeoX should be LLM"); + assert!(Architecture::Opt.is_llm(), "OPT should be LLM"); + assert!(Architecture::DeepSeek.is_llm(), "DeepSeek should be LLM"); + assert!(Architecture::Gemma.is_llm(), "Gemma should be LLM"); + assert!(Architecture::Mistral.is_llm(), "Mistral should be LLM"); + assert!(Architecture::FalconH1.is_llm(), "Falcon-H1 should be LLM"); + assert!(Architecture::Mamba.is_llm(), "Mamba (causal LM) should be LLM"); + assert!(Architecture::OpenElm.is_llm(), "OpenELM should be LLM"); + assert!(Architecture::Rwkv7.is_llm(), "RWKV-7 (causal LM) should be LLM"); + } + + /// FALSIFY-PARITY-006: map_name() handles all variants without panic. + #[test] + fn test_map_name_all_variants_no_panic() { + let test_name = "model.layers.0.self_attn.q_proj.weight"; + let all_variants = [ + Architecture::Auto, + Architecture::Whisper, + Architecture::Llama, + Architecture::Bert, + Architecture::Qwen2, + Architecture::Qwen3, + Architecture::Qwen3_5, + Architecture::Gpt2, + Architecture::Phi, + Architecture::GptNeoX, + Architecture::Opt, + Architecture::DeepSeek, + Architecture::Gemma, + Architecture::Mistral, + Architecture::FalconH1, + Architecture::Mamba, + Architecture::Moonshine, + Architecture::OpenElm, + Architecture::Rwkv7, + ]; + + for variant in &all_variants { + let mapped = variant.map_name(test_name); + assert!( + !mapped.is_empty(), + "Architecture::{variant:?}.map_name() returned empty string" + ); + } + } diff --git a/crates/aprender-core/src/format/rosetta/arch_inference.rs b/crates/aprender-core/src/format/rosetta/arch_inference.rs index 5d6b479de..466a568db 100644 --- a/crates/aprender-core/src/format/rosetta/arch_inference.rs +++ b/crates/aprender-core/src/format/rosetta/arch_inference.rs @@ -14,10 +14,33 @@ impl RosettaStone { } /// GH-249: Infer architecture from tensor naming patterns when metadata is absent. + /// + /// PMAT-546: Extended with GPT-NeoX, OPT, BERT, Mamba, RWKV detection. + /// Order matters: specific patterns before generic ones. fn infer_architecture_from_tensors(tensors: &[TensorInfo]) -> Option { let names: Vec<&str> = tensors.iter().map(|t| t.name.as_str()).collect(); let has = |pat: &str| names.iter().any(|n| n.contains(pat)); + // PMAT-546: Mamba SSM — mixer.in_proj is unique to Mamba + if has("mixer.in_proj") || has("mixer.out_proj") { + return Some("mamba".to_string()); + } + // PMAT-546: RWKV — rwkv.blocks.* is unique to RWKV + if has("rwkv.blocks.") || has("blocks.0.att.") { + return Some("rwkv".to_string()); + } + // PMAT-546: GPT-NeoX — gpt_neox.* prefix with fused query_key_value + if has("gpt_neox.") || has("query_key_value") { + return Some("gpt_neox".to_string()); + } + // PMAT-546: OPT — model.decoder.layers.* (distinct from model.layers.*) + if has("model.decoder.layers.") { + return Some("opt".to_string()); + } + // PMAT-546: BERT — bert.encoder.layer.* + if has("bert.") { + return Some("bert".to_string()); + } // GPT-2: uses c_attn, c_proj, c_fc (Conv1D-style naming) if has("c_attn") || has("attn.c_proj") { return Some("gpt2".to_string()); diff --git a/crates/aprender-core/src/format/rosetta/tests.rs b/crates/aprender-core/src/format/rosetta/tests.rs index 5400d77ed..7fa5fa153 100644 --- a/crates/aprender-core/src/format/rosetta/tests.rs +++ b/crates/aprender-core/src/format/rosetta/tests.rs @@ -426,3 +426,5 @@ mod tests_conversion_inspection; mod minimal; #[path = "tests_tokenizer_stress.rs"] mod tests_tokenizer_stress; +#[path = "tests_arch_inference.rs"] +mod tests_arch_inference; diff --git a/crates/aprender-core/src/format/rosetta/tests_arch_inference.rs b/crates/aprender-core/src/format/rosetta/tests_arch_inference.rs new file mode 100644 index 000000000..5debb3228 --- /dev/null +++ b/crates/aprender-core/src/format/rosetta/tests_arch_inference.rs @@ -0,0 +1,142 @@ +// PMAT-546: Architecture inference from tensor naming patterns +// Tests for RosettaStone::infer_architecture_from_tensors() + +use super::*; + +fn make_tensors(names: &[&str]) -> Vec { + names + .iter() + .map(|n| TensorInfo { + name: n.to_string(), + dtype: "F32".to_string(), + shape: vec![1], + size_bytes: 4, + stats: None, + }) + .collect() +} + +#[test] +fn infer_mamba_from_mixer_tensors() { + let tensors = make_tensors(&[ + "backbone.embeddings.weight", + "backbone.layers.0.mixer.in_proj.weight", + "backbone.layers.0.mixer.out_proj.weight", + "backbone.layers.0.norm.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("mamba".to_string()) + ); +} + +#[test] +fn infer_rwkv_from_blocks_tensors() { + let tensors = make_tensors(&[ + "rwkv.blocks.0.att.key.weight", + "rwkv.blocks.0.att.value.weight", + "rwkv.blocks.0.ffn.key.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("rwkv".to_string()) + ); +} + +#[test] +fn infer_gpt_neox_from_gpt_neox_prefix() { + let tensors = make_tensors(&[ + "gpt_neox.embed_in.weight", + "gpt_neox.layers.0.attention.query_key_value.weight", + "gpt_neox.layers.0.mlp.dense_h_to_4h.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("gpt_neox".to_string()) + ); +} + +#[test] +fn infer_opt_from_model_decoder_prefix() { + let tensors = make_tensors(&[ + "model.decoder.embed_tokens.weight", + "model.decoder.layers.0.self_attn.q_proj.weight", + "model.decoder.layers.0.fc1.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("opt".to_string()) + ); +} + +#[test] +fn infer_bert_from_bert_prefix() { + let tensors = make_tensors(&[ + "bert.embeddings.word_embeddings.weight", + "bert.encoder.layer.0.attention.self.query.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("bert".to_string()) + ); +} + +#[test] +fn infer_gpt2_from_c_attn() { + let tensors = make_tensors(&[ + "transformer.wte.weight", + "transformer.h.0.attn.c_attn.weight", + "transformer.h.0.mlp.c_fc.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("gpt2".to_string()) + ); +} + +#[test] +fn infer_qwen2_from_q_proj_with_bias() { + let tensors = make_tensors(&[ + "model.layers.0.self_attn.q_proj.weight", + "model.layers.0.self_attn.q_proj.bias", + "model.layers.0.mlp.gate_proj.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("qwen2".to_string()) + ); +} + +#[test] +fn infer_llama_from_q_proj_gate_no_bias() { + let tensors = make_tensors(&[ + "model.layers.0.self_attn.q_proj.weight", + "model.layers.0.mlp.gate_proj.weight", + "model.layers.0.mlp.up_proj.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("llama".to_string()) + ); +} + +#[test] +fn infer_none_from_empty() { + let tensors: Vec = vec![]; + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + None + ); +} + +#[test] +fn infer_transformer_fallback() { + let tensors = make_tensors(&[ + "encoder.layers.0.self_attn.in_proj_weight", + "encoder.layers.0.linear1.weight", + ]); + assert_eq!( + RosettaStone::infer_architecture_from_tensors(&tensors), + Some("transformer".to_string()) + ); +} diff --git a/crates/aprender-core/src/format/tensor_expectation.rs b/crates/aprender-core/src/format/tensor_expectation.rs index 41a1de11c..6e6529182 100644 --- a/crates/aprender-core/src/format/tensor_expectation.rs +++ b/crates/aprender-core/src/format/tensor_expectation.rs @@ -19,6 +19,12 @@ impl Architecture { Self::DeepSeek => Self::llama_map_name(source_name), Self::Gemma => Self::llama_map_name(source_name), Self::Mistral => Self::llama_map_name(source_name), + // PMAT-546: Model-family parity — new architectures + Self::FalconH1 => Self::llama_map_name(source_name), // HuggingFace model.layers naming + Self::OpenElm => Self::llama_map_name(source_name), // HuggingFace model.layers naming + Self::Moonshine => Self::whisper_map_name(source_name), // Audio model, strip model. prefix + Self::Mamba => Self::auto_map_name(source_name), // SSM: mixer.* naming, passthrough + Self::Rwkv7 => Self::auto_map_name(source_name), // Recurrence: rwkv.blocks.* naming, passthrough } } @@ -32,8 +38,8 @@ impl Architecture { } /// PMAT-526: Returns true for decoder-only LLM architectures that use BPE tokenizers - /// and support chat templates. Returns false for audio models (Whisper), encoder-only - /// models (BERT), and Auto (indeterminate). + /// and support chat templates. Returns false for audio models (Whisper, Moonshine), + /// encoder-only models (BERT), and Auto (indeterminate). #[must_use] pub fn is_llm(&self) -> bool { matches!( @@ -49,6 +55,10 @@ impl Architecture { | Self::DeepSeek | Self::Gemma | Self::Mistral + | Self::FalconH1 + | Self::Mamba + | Self::OpenElm + | Self::Rwkv7 ) } @@ -89,6 +99,11 @@ impl Architecture { Self::DeepSeek => "DeepSeek", Self::Gemma => "Gemma", Self::Mistral => "Mistral", + Self::FalconH1 => "Falcon-H1", + Self::Mamba => "Mamba", + Self::Moonshine => "Moonshine", + Self::OpenElm => "OpenELM", + Self::Rwkv7 => "RWKV-7", } } @@ -117,6 +132,12 @@ impl Architecture { "deepseek" | "deepseek_v2" | "deepseek-v2" => Some(Self::DeepSeek), "gemma" | "gemma2" | "gemma3" => Some(Self::Gemma), "mistral" | "mixtral" => Some(Self::Mistral), + // PMAT-546: Model-family parity — new architecture variants + "falcon_h1" | "falcon-h1" | "falconh1" | "falcon3" => Some(Self::FalconH1), + "mamba" | "mamba2" => Some(Self::Mamba), + "moonshine" => Some(Self::Moonshine), + "openelm" => Some(Self::OpenElm), + "rwkv" | "rwkv7" | "rwkv-7" => Some(Self::Rwkv7), // LLaMA derivatives (use LLaMA tensor naming) "smollm" | "smollm2" | "granite" | "granite3" | "nemotron" => Some(Self::Llama), _ => None, diff --git a/crates/aprender-present-terminal/benches/terminal.rs b/crates/aprender-present-terminal/benches/terminal.rs index de4de3109..b730a5df0 100644 --- a/crates/aprender-present-terminal/benches/terminal.rs +++ b/crates/aprender-present-terminal/benches/terminal.rs @@ -17,11 +17,11 @@ //! - 95% CI width < 5% of mean for typical variance //! - Sufficient for bootstrap CI estimation (10,000 resamples) -use std::hint::black_box; use criterion::{criterion_group, criterion_main, Criterion, SamplingMode, Throughput}; use presentar_core::{Color, Point, Rect, TextStyle}; use presentar_terminal::direct::{CellBuffer, DiffRenderer, DirectTerminalCanvas, Modifiers}; use presentar_terminal::{Canvas, ColorMode}; +use std::hint::black_box; // ============================================================================= // CELL BUFFER BENCHMARKS diff --git a/crates/aprender-present-terminal/src/widgets/collapsible_panel.rs b/crates/aprender-present-terminal/src/widgets/collapsible_panel.rs index 93c423e45..79d8f325e 100644 --- a/crates/aprender-present-terminal/src/widgets/collapsible_panel.rs +++ b/crates/aprender-present-terminal/src/widgets/collapsible_panel.rs @@ -370,8 +370,7 @@ impl Widget for CollapsiblePanel { let title_len = displayed_title.chars().count() + 2; // +2 for spaces let rest_start = title_start + title_len; if rest_start < width - 1 { - let rest: String = std::iter::repeat_n(top, width - rest_start - 1) - .collect(); + let rest: String = std::iter::repeat_n(top, width - rest_start - 1).collect(); canvas.draw_text( &rest, Point::new(self.bounds.x + rest_start as f32, self.bounds.y), diff --git a/crates/aprender-train-lora/src/merge.rs b/crates/aprender-train-lora/src/merge.rs index 3e3fe3d20..5c0d384cb 100644 --- a/crates/aprender-train-lora/src/merge.rs +++ b/crates/aprender-train-lora/src/merge.rs @@ -274,7 +274,7 @@ mod tests { fn test_merge_adds_adapter_contribution() { let engine = MergeEngine::new(); let base = vec![1.0, 2.0, 3.0, 4.0]; // [2, 2] weight matrix - // rank=1: A=[2,1], B=[1,2] + // rank=1: A=[2,1], B=[1,2] let lora_a = vec![0.1, 0.2]; let lora_b = vec![0.5, 0.5]; diff --git a/crates/aprender-train/examples/ssc_preflight.rs b/crates/aprender-train/examples/ssc_preflight.rs index 348b693cb..784805356 100644 --- a/crates/aprender-train/examples/ssc_preflight.rs +++ b/crates/aprender-train/examples/ssc_preflight.rs @@ -18,8 +18,7 @@ use std::path::PathBuf; fn main() { let args: Vec = std::env::args().collect(); - let model_dir = - get_arg(&args, "--model-dir").map(PathBuf::from).expect("--model-dir required"); + let model_dir = get_arg(&args, "--model-dir").map(PathBuf::from).expect("--model-dir required"); println!("=== SSC Run 8 Preflight Gate (Step 8.4) ==="); println!("Model: {}", model_dir.display()); diff --git a/crates/aprender-train/src/finetune/training_plan.rs b/crates/aprender-train/src/finetune/training_plan.rs index 64c9d7ee3..6e649ff0d 100644 --- a/crates/aprender-train/src/finetune/training_plan.rs +++ b/crates/aprender-train/src/finetune/training_plan.rs @@ -925,8 +925,8 @@ pub fn execute_plan( let mut tracker = ExperimentTracker::open(&apply.output_dir, plan); // GH-377: Resolve model config — error on unknown instead of silent tiny() - let model_config = TransformerConfig::from_size_str(&plan.model.size) - .map_err(crate::Error::ConfigError)?; + let model_config = + TransformerConfig::from_size_str(&plan.model.size).map_err(crate::Error::ConfigError)?; let total_start = std::time::Instant::now(); diff --git a/crates/aprender-train/src/lora/pissa.rs b/crates/aprender-train/src/lora/pissa.rs index 6889f6e17..cc486def3 100644 --- a/crates/aprender-train/src/lora/pissa.rs +++ b/crates/aprender-train/src/lora/pissa.rs @@ -102,8 +102,7 @@ fn truncated_svd( for r in 0..rank { // Initialize random vector v - let mut v: Vec = - (0..d_in).map(|i| (i as f32 * 0.7 + r as f32 * 1.3).sin()).collect(); + let mut v: Vec = (0..d_in).map(|i| (i as f32 * 0.7 + r as f32 * 1.3).sin()).collect(); normalize(&mut v); let mut u = vec![0.0f32; d_out]; diff --git a/crates/aprender-zram-core/src/benchmark.rs b/crates/aprender-zram-core/src/benchmark.rs index f929e92f0..3dff23288 100644 --- a/crates/aprender-zram-core/src/benchmark.rs +++ b/crates/aprender-zram-core/src/benchmark.rs @@ -361,7 +361,9 @@ mod tests { // Correctness: 1000 compressions all succeed for _ in 0..1000 { - compressor.compress(&page).expect("compression must succeed"); + compressor + .compress(&page) + .expect("compression must succeed"); } } diff --git a/deny.toml b/deny.toml index 5b3ea90b5..251da0d40 100644 --- a/deny.toml +++ b/deny.toml @@ -25,6 +25,19 @@ ignore = [ { id = "RUSTSEC-2025-0046", reason = "wasmtime: test-only dep (aprender-test-lib), not production" }, { id = "RUSTSEC-2026-0020", reason = "wasmtime: test-only dep (aprender-test-lib), not production" }, { id = "RUSTSEC-2026-0087", reason = "wasmtime: test-only dep (aprender-test-lib), not production" }, + # wasmtime 27 batch 2026-04-09 — 10 new advisories (test-only, not production) + { id = "RUSTSEC-2026-0085", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0086", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0088", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0089", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0091", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0092", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0093", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0094", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0095", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + { id = "RUSTSEC-2026-0096", reason = "wasmtime 27: test-only dep, not production. Upgrade to 43 in PR #731" }, + # rand 0.10.0 — unsound with custom logger using rand::rng(), transitive via quickcheck + { id = "RUSTSEC-2026-0097", reason = "rand 0.10: unsound with custom logger, transitive via quickcheck (test-only)" }, ] [licenses] diff --git a/docs/specifications/aprender-monorepo-consolidation.md b/docs/specifications/aprender-monorepo-consolidation.md index 9d6cd5ef8..6a7834fb2 100644 --- a/docs/specifications/aprender-monorepo-consolidation.md +++ b/docs/specifications/aprender-monorepo-consolidation.md @@ -14,6 +14,7 @@ ### Changes since v2.0 (2026-04-10 Falsification Audit) +- **PMAT-546: Architecture↔model-family parity** (2026-04-12): Added 5 missing Architecture enum variants (FalconH1, Mamba, Moonshine, OpenElm, Rwkv7). Created 2 missing model-family YAML contracts (gptneox.yaml, opt.yaml). Provable contract `model-family-parity-v1.yaml` with 5 falsification conditions. 6 parity tests + updated 1 pre-existing test. 19 Architecture variants ↔ 18 model-family YAMLs (Auto excluded). aprender-core now 13,011 tests. - **Test count corrected**: apr-cli is 4,577 (was 4,070); workspace total is 25,806 (was 18,416) - **Contract YAML count corrected**: 797 (was 522). Growth from model-family contracts + new CLI contracts - **`#[contract]` annotation count corrected**: 52 total (was "44 on CLI commands"). NONE are on CLI commands — they live in serve, compute, train, and contracts crates. CLI `#[contract]` coverage is a new P0 gap (PMAT-543) @@ -52,7 +53,7 @@ | Coverage (aprender-compute) | **48.56%** lines (212K/437K) | **≥95%** | **FAIL** — 3,497 tests. Per-crate measurement. | | Coverage (workspace aggregate) | **~55%** weighted average | **≥95%** | **FAIL** — prior "46%" was instrumentation artifact. True baseline ~55%. PMAT-541. | | Tests (apr-cli) | **4,633** (lib) + **108** (integration) | — | PASS — +37 Phase 4 inline + 10 Phase 4 integration | -| Tests (aprender-core) | **13,005** | — | PASS — +24 tokenizer_loader + 6 arch + 1 fix | +| Tests (aprender-core) | **13,023** | — | PASS — +18 PMAT-546 (6 parity + 10 arch inference + 2 import inference), +24 tokenizer_loader + 6 arch + 1 fix | | Tests (contracts) | 1,371 | — | PASS | | Tests (workspace total) | **28,700+** | — | PASS | | Integration (monorepo) | 8/8 | 8/8 | PASS | @@ -60,7 +61,7 @@ | Clippy errors | 0 | 0 | PASS | | `#[contract]` annotations | **172** (70 cli + 52 serve/compute/train + 50 other) | ≥50 | **PASS** | | `#[contract]` on CLI commands | **70** (59 cmd files + 11 dispatch) | ≥57 | **PASS** — PMAT-543 | -| Contract YAML files | 797 | — | INFO | +| Contract YAML files | 800 | — | INFO — +3: gptneox.yaml, opt.yaml, model-family-parity-v1.yaml | | unwrap() in production code | **0** (test-only: 584 in test files) | 0 | **PASS** — clippy ban effective | | pmat TDG | 92.5/100 (A) | A+ | **PASS** | | pmat comply | PASS (4 warnings) | PASS | **PASS** — 52 work contracts valid, 85 bindings verified, 0 ghosts | @@ -201,7 +202,8 @@ workspace-wide number. | Epic | Status | |------|--------| -| ~~PMAT-526 (Model Type)~~ | `is_llm()` + 3 new Architecture variants + import guards + contract. 6 falsification tests. | +| ~~PMAT-526 (Model Type)~~ | `is_llm()` + 3 new Architecture variants + import guards + contract. 6 falsification tests. Extended by PMAT-546. | +| ~~PMAT-546 (Model-Family Parity)~~ | 5 new Architecture variants (FalconH1, Mamba, Moonshine, OpenElm, Rwkv7) + 2 new YAML contracts (gptneox, opt). 19 variants ↔ 18 YAMLs: 1:1 parity enforced. 6 falsification tests. Contract: `model-family-parity-v1.yaml`. | | ~~PMAT-532 (QA Migration)~~ | 5 crates ported, 2,792 tests, 256 playbooks. Source repo archived. | | ~~PMAT-543 (CLI Contracts)~~ | 172 annotations workspace-wide. 0 unannotated CLI handlers. | | ~~PMAT-544 (unwrap)~~ | 0 production unwrap(). False positive from test files. Clippy ban effective. |