diff --git a/PLAN.md b/PLAN.md index d3e13e6..3f22036 100644 --- a/PLAN.md +++ b/PLAN.md @@ -21,7 +21,7 @@ - [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages and invalid shards, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-build`, `hemx-core`, `hemx-js`, and the full `hemx-test` package cleanly; full package closure remains. - **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. - **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none; broad survivors currently remain in `hemx-derive` and `hemx-lsp` outside already-clean focused contracts. The xtask mutation runner now creates shard output parents before invoking mutest, fixing first-use failure for newly sharded packages. `hemx-derive` public shards `1/8` and `2/8` are clean. Shard `1/8` passed through deterministic partitions `1/16` (31 mutants: 24 caught, 7 unviable) and `9/16` (31 mutants: 28 caught, 3 unviable); shard `2/8` passed with 61 mutants (46 caught, 15 unviable) after exact client-handler mode/signature/codegen, generated-file diagnostics, form generic bounds, malformed symbol manifests, and unambiguous `Form` path matching; shards `3/8` through `8/8` remain. The mutation entry point accepts validated one-based `SHARD/TOTAL` operands, maps them to native zero-based shards, uses shard-specific output directories, grants repo-owned compiler probes a 120-second floor, and preserves the unsharded gate. All eight deterministic `hemx-build` shards now pass: 1,304 mutants total, 1,070 caught and 234 unviable, including final shard `8/8` with 159 mutants (139 caught, 20 unviable). The complete 470-mutant `hemx-axum` package gate passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. +- **Blocked by:** none; broad survivors currently remain in `hemx-derive` and `hemx-lsp` outside already-clean focused contracts. The xtask mutation runner now creates shard output parents before invoking mutest, fixing first-use failure for newly sharded packages. `hemx-derive` public shards `1/8` through `3/8` are clean. Shard `3/8` passed with 61 mutants (52 caught, 9 unviable) after exact missing-symbol fallback plus accumulated missing-field, singular/multiple, required/optional, and malformed form-contract diagnostics. Shards `4/8` through `8/8` remain. The mutation entry point accepts validated one-based `SHARD/TOTAL` operands, maps them to native zero-based shards, uses shard-specific output directories, grants repo-owned compiler probes a 120-second floor, and preserves the unsharded gate. All eight deterministic `hemx-build` shards now pass: 1,304 mutants total, 1,070 caught and 234 unviable, including final shard `8/8` with 159 mutants (139 caught, 20 unviable). The complete 470-mutant `hemx-axum` package gate passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. - **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 ## 3. Elect and enforce the release license policy diff --git a/hemx-derive/src/lib.rs b/hemx-derive/src/lib.rs index 2c06319..1f45345 100644 --- a/hemx-derive/src/lib.rs +++ b/hemx-derive/src/lib.rs @@ -1023,8 +1023,8 @@ fn compile_error(message: &str) -> TokenStream { mod tests { use super::{ add_app_registry_helper, add_component_register_helper, client_handler_has_inputs, - component_handler_names, expand_handler_function, form_fields, form_impl_generics, - form_model_type, form_resource_id, handle_params, handle_requires_form, + component_handler_names, expand_handler_function, form_contract_errors, form_fields, + form_impl_generics, form_model_type, form_resource_id, handle_params, handle_requires_form, handler_form_model_type, handler_placement, handler_syms_path, has_form_param, has_non_unit_return, is_type_named, join_contract_errors, missing_component_handlers, missing_form_generated_files_message, missing_handle_params, @@ -1249,6 +1249,58 @@ mod tests { // test req: derive_handler/001 req: derive_handler/005 } + #[test] + fn form_contract_diagnostics_accumulate_all_mismatches() { + let path = std::env::temp_dir().join(format!( + "hemx-derive-form-contract-errors-{}", + std::process::id() + )); + let missing: syn::ItemStruct = parse_quote!( + struct Profile { + first: String, + } + ); + assert_eq!( + form_contract_errors(&path, "profile", &missing), + vec!["#[hemx::form] could not find generated hemx symbols; add hemx_build::app().run()? to build.rs or check template generation"] + ); + + std::fs::write( + &path, + "hemx-syms-v1\nform\tprofile.heml::profile\tprofile\t42\nform_field\tprofile\tfirst\tfalse\tfalse\nform_field\tprofile\tsecond\ttrue\tfalse\nform_field\tprofile\ttags\tfalse\ttrue\nform_field\tprofile\trequired_name\ttrue\tfalse\n", + ) + .unwrap(); + assert_eq!( + form_contract_errors(&path, "unknown", &missing), + vec!["unknown hemx form `unknown`; add data-hemx-form=\"unknown\" to a template or rename this form binding"] + ); + let tuple: syn::ItemStruct = parse_quote!( + struct Profile(String); + ); + assert_eq!( + form_contract_errors(&path, "profile", &tuple), + vec!["hemx form `profile` must be a struct with named fields"] + ); + let mismatched: syn::ItemStruct = parse_quote!( + struct Profile { + first: Vec, + tags: String, + required_name: Option, + } + ); + assert_eq!( + form_contract_errors(&path, "profile", &mismatched), + vec![ + "hemx form `profile` field `first` accepts one value and must not be Vec<_>", + "hemx form `profile` is missing field `second` for form control `second`", + "hemx form `profile` field `tags` accepts multiple values and must be Vec<_>", + "hemx form `profile` field `required_name` is required in HTML and must not be Option<_>", + ] + ); + std::fs::remove_file(path).unwrap(); + // test req: form/004 req: diagnostics/003 + } + #[test] fn syms_lookup_matches_handle_ident() { let path = std::env::temp_dir().join("hemx-derive-syms-test.syms"); @@ -1265,7 +1317,12 @@ mod tests { assert_eq!(handle_params(&path, "create"), vec!["todo_id"]); assert!(handle_params(&path, "missing").is_empty()); - let _ = std::fs::remove_file(path); + let _ = std::fs::remove_file(&path); + assert!( + syms_contains_handle(&path, "create"), + "missing generated symbols defer to the dedicated generated-file diagnostic" + ); + assert!(!handle_requires_form(&path, "create")); } #[test]