You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generated bindings for a &mut DTO parameter now return the updated value. A core function with a &mut T parameter on a non-opaque (serde DTO) type was emitted as an owned by-value parameter returning void: the binding converted the caller's object into an owned intermediate, mutated the intermediate, and dropped it. The call compiled, raised nothing, and silently did nothing observable, in Python, Node, PHP, Go, Java, Kotlin, Dart and Swift. The wrapper now returns the mutated value in all eight.
Migration. For a core signature fn tag_record(record: &mut Record), assign the result back over the value you passed in — Python tag_record(record) becomes record = tag_record(record); Node tagRecord(record) becomes record = tagRecord(record); PHP tagRecord($record) becomes $record = tagRecord($record); Go err := TagRecord(record) becomes record, err := TagRecord(record), where record is now a *Record; Java and Kotlin tagRecord(record) become record = tagRecord(record); Dart await tagRecord(record) becomes record = await tagRecord(record); Swift try tagRecord(record: record) becomes record = try tagRecord(record: record). A call site that ignored the previously-void result was already silently broken and needs the assignment added. No call shape keeps working unchanged.
Generation now fails, naming the function, for the two &mut DTO shapes a binding has no room to express: more than one &mut parameter, and a &mut parameter on a function that already returns a value. Both previously emitted a binding that accepted the argument and discarded the mutation. Change the core signature to return the updated value itself, or fold both results into one returned type.
Unchanged by design: a &mut parameter on an opaque handle type still mutates through the handle, which was already correct; and &mut on String, Vec<T> or a scalar still surfaces as a compile error in the generated Rust rather than a silent no-op. Neither shape was ever silently lossy.
Added
alef build --strict: fail the run when a language was skipped because its toolchain is not on PATH, naming each skipped language and the precondition that failed. Off by default (a missing local toolchain still leaves the rest of the build clean); pass it in CI so a skipped-and-never-built language surfaces as a non-zero exit instead of a log line nobody read.
Added unit tests pinning is_valid_for_result's intentional permissive/anchored asymmetry directly at the FieldResolver layer (src/e2e/field_access/resolver/classify.rs), so a future accidental over-anchoring of the permissive check is caught by cargo test --lib without needing the full presentation-layer suite.
alef e2e snippets-migrate and its coverage driver gained regression coverage for two curated_snippets path-resolution edge cases: an existing_root equal to the configured snippets.output now compares correctly, and a bare * glob that crosses a / into alef's own generated output is refused by name.
Kotlin e2e assertions now lower a tagged-union field path (<union>.<variant>.<field>) for ANY single-payload variant the IR resolves, not only the one hand-maintained fixture shape, narrowing via a real when (val v = …) { is <Union>.<Variant> -> { … } } block on both the kotlin and kotlin_android targets. The payload property name is computed from the IR through kotlin_field_name_with_type — the same helper the Kotlin binding backend itself uses — so it can never drift from the emitted binding. Detection reuses FieldResolver::tagged_union_split, the generic primitive Gleam/Dart/Swift already consult; FieldResolver::union_variant_payload is new.
FieldSkip::UnionTraversalNotImplementedForKotlin (GeneratorGap): a tagged-union boundary Kotlin detects but cannot yet lower (a multi-field variant, or a union type the IR never anchored) now emits a loud, named, counted skip instead of silently falling through to a flat accessor chain against a sealed class — code that does not compile.
TypeDef now records a struct's container-level serde conversion (serde_container_conversion, holding from/into/try_from/transparent), read from #[serde(...)] including through #[cfg_attr(...)]. These attributes were previously parsed for no purpose — extraction read only serde_rename_all — so a struct with a hand-written wire shape (commonly a tuple or array for a small value type) generated an object-shaped binding DTO that silently failed to round-trip at runtime.
New ValidationCode::SerdeContainerConversionUnsupported: a struct carrying any of those attributes now raises a named diagnostic instead of quietly generating a binding whose JSON shape disagrees with the core type's real one. Deliberately Warning severity — it never aborts a build, because the remedy today is to exclude the type — and scoped to the languages it actually affects (pyo3, napi, magnus, wasm, rustler, extendr, which re-derive their own local binding struct). The FFI-derived backends (Go, Java, C#, Dart, Swift, Kotlin, Zig) deserialize through the core type's own serde impl and are unaffected, so it does not fire for a consumer targeting only those.
A declared since that names a release newer than the crate's own version now raises a Warning (since_newer_than_crate_version) naming the item, the declared since, and the crate version it exceeds; an unparseable since raises a distinct since_version_unparseable rather than passing silently. Both #[alef(since = "...")] and #[deprecated(since = "...")] are checked, across all seven item kinds that carry version metadata. Comparison uses semver::Version::cmp_precedence, not the derived Ord — the latter orders build metadata (1.2.0+build > 1.2.0), which the SemVer spec forbids from affecting precedence.
Added codegen::mut_writeback, the single policy module every backend consults to decide whether a &mut parameter needs writing back, which type the binding must return in place of (), and which &mut shapes are unsupported. Backends no longer each answer that question their own way; the generated Rust reference asks it too, so the docs cannot describe a signature the binding does not emit.
Changed
validate_call_arg_signatures (unknown fixture arg / missing required parameter) is now Severity::Error and aborts e2e generation; a consumer-fleet survey found zero legitimate call sites it would have flagged.
validate_call_module_overrides's Go check (a bare-word overrides.go.module/module is never a resolvable Go import path) is now Severity::Error and aborts e2e generation. The equivalent Java check (a module override that looks like a class, not a package) stays Severity::Warning, since no consumer in the surveyed fleet currently sets that field.
kotlin/discriminated.rs::render_discriminated_union_assertion takes the sealed-class variant's payload property name as a parameter instead of assuming a literal name; existing callers pass the previous literal unchanged, so behavior for the hand-maintained fixture shape is identical.
The Python api.py facade and the <module>.pyi stub now derive parameter existence, order and optionality from one shared decision (backends::pyo3::py_signature) instead of re-deriving it independently, so the two artifacts cannot drift apart. New agreement tests render both from one fixture and assert identical parameters in identical order, in both the required and the defaulted direction.
Extracted the free-function delegation predicate that the WASM, NAPI and shared function generators each need into codegen::generators::can_auto_delegate_function_with_named_let_bindings, replacing two byte-identical private copies.
A String/Bytes parameter the source declared by value is no longer documented as &str/&[u8] on the Rust page; the borrow forms are emitted only when the IR records a borrow.
Added backends::php::layout::{php_class_output_dir, php_psr4_target} as the single authority for where the PHP userland classes live. The php backend, the scaffolded root composer.json and the e2e composer.json now all read it instead of each re-deriving the directory, so the root and e2e PSR-4 targets cannot name two different trees. The root manifest also honours [crates.php.stubs] output, which it previously ignored while the backend wrote the classes there.
FieldResolver::result_relative_path returns Cow<'_, str> instead of &str: the envelope projection it can now prepend is a computed path, not a slice of its input.
Fixed
.ai-rulez/skills/binding-audit/SKILL.md and the (now-removed, folded into that skill) binding-audit-pattern rule documented a grep for intentional binding-removal attributes that matched only #[alef::skip] and #[doc(hidden)]. The extractor (src/extract/extractor/helpers/attributes.rs:304-333) accepts three spellings — #[alef::skip], the list form #[alef(skip)], and either nested in #[cfg_attr(...)] (the form in common use, e.g. #[cfg_attr(alef, alef(skip))]) — so the documented grep missed the dominant real-world spelling and would misclassify a correctly-excluded item as a binding gap. The grep now matches all three spellings.
Ruby e2e snippet and spec generation no longer double-prefixes an options_type (or adapter request_type) that already names a module. Both generators share one constructor builder (ruby/args.rs::build_args_and_setup), which unconditionally prepended the call's module regardless of whether the configured name already carried one, turning e.g. "Sample::DocumentRequest" into Sample::Sample::DocumentRequest.new(...) in both outputs identically. values::qualify_ruby_type now prepends the module only when the name has no :: already, matching how csharp/go take options_type verbatim.
Fixed a false-positive field's #[serde(default)] value disagrees with its #[derive(Default)]/impl Default value warning that fired on genuinely agreeing enum-typed fields whenever the enum's declaring source file was extracted after the struct's manual impl Default; the disagreement check now runs once, after the whole crate is extracted, instead of inline per source file.
Fixed spurious impl Default body is neither a struct literal nor a constant-foldable delegation; field defaults are unresolved warnings for zero-field types whose impl Default delegates to an argument-free Self::new() returning a bare Self; the resolver now recognizes this shape directly.
Suppressed the same unresolved-default warning for types already excluded from every binding surface (#[alef(skip)] in any recognized spelling, or #[doc(hidden)]), since an excluded type's fields never reach codegen and the warning was pure regen-log noise. Non-excluded types with a genuinely unfoldable default still warn.
alef e2e snippets-migrate: fixed --config pointing at a config file outside the project making every project-root-relative curated_snippets glob resolve against the wrong directory; the project root is now the process working directory, matching how the same globs are already resolved at generation time.
pyo3: the api.py constructor-call converter now derives keyword-argument names by calling resolve_param_ident (the same function the .pyi__init__ stub and the real #[new] signature use) instead of re-deriving them separately; a field carrying #[serde(rename = "type")] (a keyword in both Rust and Python) previously emitted _rust.T(type_=...) in api.py while the native stub and constructor both used bare type, causing a pyrefly[unexpected-keyword] error.
pyo3: Vec<StructType> fields now convert element-wise in api.py's _to_rust_* converters instead of passing the raw list straight through to the native pyclass constructor, which pyrefly flagged as [bad-argument-type].
pyo3: an already-Option<Enum> field no longer routes through the kwarg-omission trick in api.py's converters, which made pyrefly cross-assign the two enum argument types between the two parameters when a constructor had two such fields; it now passes None directly via a ternary. The omission trick remains for fields that are non-optional in the native binding and rely on a real, non-None Rust-computed default. Test coverage for all three converter fixes above was added to the pyrefly_generated_package_tests fixture, closing the gap that let them ship with the pyrefly gate reporting clean.
validate_call_args's binding_excluded skip was language-blind: it skipped argument-signature validation for any function/method the IR marked binding_excluded, even when a resolved language (Rust) still emits a real, positionally-bound call against it, making a wrong arg name on such a call structurally undetectable. The validator now skips only when every resolved language in the current run agrees the call is excluded.
validate_call_module's Go precedence check missed a rung the generator actually consults: a named call with no override of its own falls back to the base [e2e.call]'s Go override before falling to [go].module, so a bad base override could be silently used by every named call's generated snippet while the check, run per named call, reported nothing. The validator's doc comment describing the Go resolution order had the same gap and was corrected alongside the fix.
Fixed the e2e snippet coverage driver treating every adapter-handled method ([[crates.adapters]]) as excluded from every non-Rust language's documentation snippets, even though every backend that consumes adapters still binds the method in every language except the ones the adapter's own skip_languages names. Coverage now re-derives the per-language answer from the adapter config instead of trusting the language-blind exclusion flag, so an adapter-handled method with an unaffected language stays in coverage.expected and still renders.
Generated Java pom.xml is now emitted in poly's canonical XML style (2-space indent; a multi-attribute root <project> tag wrapped one attribute per line), ending the alef generate → poly fmt oscillation that rewrote the file on every regen. The <developers> and capsule <dependency> blocks moved out of raw Rust format! string assembly into java_pom.xml.jinja loops over structured context values, per the jinja-templates rule.
Generated FFI *-ffi-config.cmake likewise matches poly's canonical CMake fixed point, and its body moved out of a raw multiline format! into a new ffi_config.cmake.jinja template. The canonical shape was verified empirically against already-canonical consumer files rather than assumed.
Pinned poly_would_reformat's probe subprocess to a stable cwd via spawn_from_stable_dir instead of a bare Command::new("poly"), fixing a flaky post_build_format_order_tests failure under full-suite parallel cargo test --lib. poly resolves its config/repo-root context from the process's ambient current directory, not from its argument paths, and this crate's tests share one process-wide cwd via CwdGuard around tempdirs they delete — so an unpinned spawn could inherit an already-deleted directory and report "would reformat" for content that is genuinely canonical.
Anchor the four chunks_have_content / chunks_have_embeddings / chunks_have_heading_context / first_chunk_starts_with_heading synthetic e2e assertion handlers at the call's configured result_fields envelope prefix (with an IR-confirmed index hop for a Vec<T> prefix) instead of hardcoding {result_var}.chunks. A result type that wraps its documents behind an envelope had every chunks-recipe assertion silently dropped across all eleven backends implementing the recipe, because the oracle backing the handlers only ever asked whether the bare chunks name resolved directly on the call's own root type. FieldResolver::anchor_leaf (src/e2e/field_access/leaf_anchor.rs) is new and strictly additive: it tries every result_fields prefix the IR confirms reaches the leaf before agreeing with a refusal, and the existing refusal for a genuinely unreachable field is unchanged and still fires.
Fixed the WASM backend emitting compile_error! stubs for free functions whose only non-delegatable parameters are non-opaque &Named or &[Named] references, producing a generated lib.rs that could not compile. Such functions now delegate to the real core call, matching the binding the NAPI backend already generated from the same IR — WASM was gated on a predicate stricter than its own delegation body requires. The deliberate compile_error! fallback for genuinely non-delegatable functions is untouched and still fires.
Fixed the generated Python api.py facade silently reordering parameters. A parameter whose type derives Default was promoted to = None and moved behind every required parameter, so the facade's positional order no longer matched the Rust source, the native #[pyo3(signature = ...)], the .pyi stub, or the generated docs — a positional call bound its arguments to the wrong parameters, with no type error. The facade now preserves declaration order and grants the extra default only when every later parameter is already defaulted.
Render the Rust API reference from canonical Rust instead of the binding-normalized IR. A &T or &mut T parameter is now documented as a borrow in the signature, the parameter table and the generated example, and a method's receiver comes from MethodDef::receiver instead of an unconditional &self — so a &mut self method is no longer documented as &self with a non-compiling example. The IR already carried is_ref/is_mut/receiver; the renderer was ignoring them. Binding pages are unchanged.
Preserve the declared Rust type of a struct or enum-variant field whose named type is not part of the binding surface. sanitize_unknown_types previously rewrote such a leaf to String with no record of what it was, so a Rust-only excluded field was documented as Option<String>; the pre-sanitization type is now recorded in FieldDef::original_type and rendered on the Rust page.
Fixed the generated e2e/php/composer.json (and the registry-mode test_apps/php manifest) autoloading a src/ subdirectory of the PHP class tree that no alef stage writes. The e2e generator appended a fixed /src/ to the resolved package root, so any layout whose [crates.output] php path did not already end in src sent Composer to an unmanaged directory — which only resolved while a duplicate copy of the class tree was kept beside the managed one.
Resolve a qualified Result path in a return type against the alias it names rather than against the module's use statements. A function returning crate::Result<T> from a module that also imports anyhow::Result for its internal helpers resolved its error_type to anyhow::Error, and the Zig backend then fell back to the first declared error set and emitted the wrong error union. crate::, self::, super:: and uniform paths into a locally declared module now select the alias they name; a qualified path naming another crate resolves to that crate's error instead of borrowing the local one. An unqualified Result<T> is unchanged. A parameterised alias with a default (type Result<T, E = CrateError>) now resolves to the default instead of recording the bare parameter name E.
Replaced consumer-specific fixture identifiers in the rustler backend's tests (sync_functions.rs, cfg_dedup/tests.rs) with neutral ones (score_pair, SampleVector, sample_crate::vector_ops, scoring/scoring-presets), matching the neutral convention the sibling wasm regression test already uses. Test-only rename; no behaviour change.
Fixed e2e accessors dropping a real nested struct hop on an envelope-shaped result root. With a result_fields-declared projection (results: Vec<Document>), FieldResolver::result_relative_path read a genuinely nested metadata.output_format as a virtual namespace label and emitted result.output_format — a member the root does not declare. The generic path now asks FieldResolver::anchor_leaf (the prefix search added for the synthetic chunks handler) where an envelope-rooted path lives instead of carrying its own copy, so both paths resolve one fixture field to one place.
alef publish's PHP manifest validation derived its expected PSR-4 targets from hardcoded src/ and packages/php/src/ literals — a fourth independent opinion on a directory backends::php::layout is the authority for — and so rejected any correctly-configured non-default PHP output layout. Both expectations now come from the authority (php_psr4_target for the root manifest, the new php_package_psr4_target for the package-local one, which relativizes against the manifest's own directory as Composer does). The pre-existing test for this validator was itself asserting a target that disagreed with what the scaffolder generates for its own config, and was corrected.
A struct field, method parameter, or return type declared as a fixed-size array of a named type the binding surface already carries ([Point; 4]) now lowers to the same typed list shape Vec<Point> produces, instead of being sanitized to a JSON String and failing the run on lossy_sanitized_surface. Serde gives a fixed array and a Vec the identical sequence wire form, so the rewrite is lossless and the declared length is preserved in original_type. The existing fallbacks are untouched: a fixed array of a type outside the binding surface still sanitizes to String, and the [(K, V); N] tuple-array shape the wasm backend reconstructs from still takes its JSON-string path.
The C# reference page described an API shape the emitted binding does not have: free-function examples called a bare, un-suffixed method name, when every C# free function is emitted as a public static member of the generated wrapper class and every async member carries an Async suffix. Examples now call the wrapper class through codegen::naming::csharp_wrapper_class_name, and docs::naming::csharp_async_member_name is the single rule both docs::signatures and docs::examples consult, replacing two independently duplicated copies.
The C/FFI reference page documented Vec<T> as a typed handle array and HashMap<K, V>/serde_json::Value as void*. The C ABI declares one JSON const char* for all three regardless of element type (FfiParamMapper/FfiReturnMapper in backends/ffi/type_map.rs), so a batch-of-handles parameter was documented as something the header never takes. Two existing tests had pinned the wrong void* spelling and were corrected.
The Rust reference page now shows a field's real declared type whenever the sanitizer recorded one in original_type, instead of only when the rewrite was also lossy. Without this, the lossless fixed-size-array lowering above ([Point; 4] to Vec<Point>) would have reintroduced the "binding shape, not canonical Rust" defect on the one page that exists to avoid it.
Enum variant conversions that JSON-round-trip a sanitized or Json-typed field — rustler and magnus data-carrying binding enums, pyo3 and extendr variant constructors — swallowed a parse failure and substituted Default::default() with no diagnostic at all. They now emit a tracing::warn! naming the field, the variant, and the offending value first. The conversion stays infallible, so every existing .into() call site is unaffected.
alef snippets check --strict failed on generated reference pages with unknown fenced code language: no_run. Bare no_run/ignore, multi-attribute combinations such as rust,no_run,should_panic, and rust,edition2021 are all valid rustdoc fence shorthand, but is_rust_code_block and audit_fences each matched the fence info string against a fixed set of literal combinations rather than parsing it, so anything outside that set fell through unrecognised and leaked the raw fence into generated pages. Language::from_fence_info is now the single parser both call sites use: split on commas, strip the documented rustdoc attribute vocabulary, and treat the remainder as Rust when it is empty or rust.
Removed
Removed the static enforce_build_dependency pre-flight gate from alef docs/alef all's snippet validation, which bailed under strict whenever a language had no configured docs.snippets.sessions.<target>.before step, even when that language's own validator builds the snippet from source without needing one. alef snippets check never called this gate, so the two commands could reach opposite pass/fail verdicts for the same corpus; both now rely solely on the same empirical validation results (enforce_snippet_summary). The gate's strict-bail message also pointed operators to "run alef build first", advice the gate's own doc admitted could never change its verdict since it read only static session config; that dead end is gone with the gate.