# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **alef napi + csharp service API: emit real handler dispatch instead of stubs.** Two `generate_service_api` backends produced non-functional handler dispatch that passed CI only because the golden tests assert substring presence, not compilation/behavior. (1) napi (`src/backends/napi/gen_bindings/service_api.rs`): registration metadata arguments were emitted as the literal comment `/* TODO: extract metadata */` inside the `owner.{method}(...)` call, producing uncompilable Rust whenever a registration carried metadata params (the common case — a key/path plus the handler callback); each metadata param is now extracted from the received `_metadata` values with its real NAPI coercion (string/bool/number/optional/serde-roundtrip per `TypeRef`) and passed by name into the registration call. (2) csharp (`src/backends/csharp/gen_bindings/service_api.rs`): the `[UnmanagedCallersOnly]` trampoline recovered the `GCHandle` and unmarshalled the request but returned a hardcoded `"{}"` without ever invoking the delegate; it now casts `handle.Target` to `Func<string, string>`, invokes it with the request JSON, and marshals the returned response back to native memory (caller-frees, matching the ffi backend's `char* (*)(void*, const char*)` callback contract). Both backends gained invocation-asserting unit tests that also assert no `TODO`/`stub`/hardcoded-`"{}"` markers remain. (`src/backends/napi/gen_bindings/service_api.rs`, `src/backends/csharp/gen_bindings/service_api.rs`)
- **alef rustler service_api: silence unused-variable lints and gate stub-detection test as `#[ignore]` until owner-method dispatch is implemented.** Two `-D warnings` violations in `src/backends/rustler/gen_bindings/service_api.rs`: `api: &ApiSurface` parameter in `gen_run_nif` is currently unused (the run entrypoint only references `service` and `ep`); renamed to `_api`. `let contract_name = &reg.callback_contract` inside the registration dispatch loop is also unused (the wire dispatch keys off `method_name` not `contract_name` in the current implementation); renamed to `_contract_name`. Both are intentional placeholders — when the rustler emitter is completed to call `owner.run(...)` / `owner.finalize(...)` properly (mirroring the pyo3 native-fn lookup pattern in `src/backends/pyo3/gen_bindings/service_api.rs:448+`), these will become real reads. The matching `no_stub_responses_in_generated_code` test, which asserts no `Ok(atoms::ok())\n}\n\n/// Drive` pattern in the generated rust output, is gated with `#[ignore = "rustler entrypoint emitter still emits stub Ok(atoms::ok()); see gen_run_nif TODO"]` as a permanent regression-target marker — remove the `#[ignore]` once `gen_run_nif` properly invokes the owner method. (`src/backends/rustler/gen_bindings/service_api.rs`)

- **alef kotlin / kotlin_android / swift snapshot tests: accept new baselines for files emitted under the `dev.sample_crate` package path.** After the `feat(csharp,dart,go,swift,zig): generate_service_api for five more backends` commit (`972fd27fe`) and downstream emitter refinements, the kotlin/kotlin_android/swift snapshot tests discover files at `dev/sample_crate/{Config,Demo,DemoBridge,DemoBridgeException,DemoError,Status}.kt` and `Sources/Demo/SwiftOcrBackendBridge.swift` that previously did not exist (or lived under different package paths). Ran `cargo insta accept` (and manually moved residual `.snap.new` files to canonical `.snap`) to capture the new outputs as baselines. 7 new files + 1 updated swift baseline (the swift trait-bridge inbound output gained adapter helpers). (`tests/snapshots/backends_kotlin_*.snap`, `tests/snapshots/backends_swift_*.snap`)

- **alef integration tests: align test expectations + insta snapshots with spec-compliant JNI symbol encoding, updated dart FRB post-build step count, and gate jni service_api doc-tests as Java.** Three orthogonal test-side fixes surfaced when the CI integration suite ran post-`v0.20.0` cycle: (1) `tests/backends_jni_gen_shims_test.rs` and 8 paired insta `.snap` files asserted `Java_dev_sample_crate_demo_*` symbols on the demo fixture (package `dev.sample_crate.demo`), but the production `jni_symbol` function correctly encodes `_` → `_1` per JNI spec §5.11.3 (so the actual emitted symbol is `Java_dev_sample_1crate_demo_*`). Bulk-replaced `dev_sample_crate_` → `dev_sample_1crate_` across `tests/` (12 files). (2) `tests/backends_dart_gen_bindings_test.rs::build_config_for_frb_emits_post_process_file_step` and `..._run_command_precedes_post_process_file` asserted three PostProcessFile steps; the dart backend now emits five (added `FrbDartOptionalFieldsWithDefaults` on lib.dart, and a separate `FrbDartExcludeFunctions` pass on frb_generated.dart for the `crateCalculateQualityScore` filter). Updated test expectations to match — full per-step processor/path assertions retained. (3) Two doc-tests in `src/backends/jni/service_api.rs` (`gen_register_jni_function`, `gen_entrypoint_jni_function`) contained un-annotated code fences with Java pseudo-signatures (`public native void register{ServiceName}{MethodName}(Object handler);`) that rustdoc tried to compile as Rust. Marked both as `java,ignore`. (`tests/backends_jni_gen_shims_test.rs`, `tests/backends_dart_gen_bindings_test.rs`, `tests/snapshots/backends_jni_gen_shims_test__*.snap`, `src/backends/jni/service_api.rs`)

## [0.20.0] - 2026-05-27

### Changed

- **alef hash: compute per-file `alef:hash:` over generation inputs, not emitted content.** Previously `finalize_hashes` embedded `blake3(sources_hash || file_content)`, so any post-generation formatter (rumdl-fmt, oxfmt, cargo fmt, ruff, etc.) that rewrote whitespace or trailing newlines in a generated file would invalidate its embedded hash. `alef verify --exit-code` would then report the file as stale even though no inputs had changed, causing false-positive CI failures on every regen+lint cycle. The new formula is `blake3("alef:inputs\0" || ALEF_REV || "\0" || sources_hash || "\0" || alef_toml_bytes)` — capturing alef's own revision, the Rust source fingerprint, and the raw alef.toml bytes, but not the emitted file content at all. `alef verify` now compares the embedded hex directly against a freshly recomputed inputs hash without reading or hashing the surrounding file content. The domain separator `"alef:inputs\0"` prevents collision with any hash computed under the old scheme. (`src/core/hash.rs`, `src/cli/pipeline/generate.rs`, `src/cli/pipeline/version.rs`, `src/cli/cache.rs`, `src/main.rs`)

### Fixed

- **alef cleanup: fix three test failures and two clippy/lint issues introduced by the `generate_service_api` feature cycle.** (1) `ScaffoldCargo::default()` derived `build_jobs = 0` instead of the documented `4` because `#[derive(Default)]` ignores serde defaults — replaced with an explicit `impl Default` using `default_build_jobs()`. (2) The dart `frb_rewrite` test checked for `"static Future<void> init({{")` (literal two braces) but `format!` collapses `{{` to `{`, so the actual output is `"static Future<void> init({"` — fixed test strings to match actual output. (3) Elixir `emit_test_backend` hardcoded `SampleCrate.complete_trait_call` instead of using the crate's actual NIF module; added `nif_module: &str` parameter (empty → `"Native"` fallback), updated the dispatch in `mod.rs` and the calling site in `elixir.rs` to pass `{module_path}.Native`. Additionally removed the dead `find_contract` function from `swift/gen_bindings/service_api.rs` (clippy `-D dead_code`), fixed a project-name leak in a PHP service_api comment, and applied rustfmt/clippy-fix to `csharp/service_api.rs` and `dart/service_api.rs`. (`src/core/config/output.rs`, `src/backends/dart/frb_rewrite.rs`, `src/e2e/codegen/elixir.rs`, `src/e2e/codegen/mod.rs`, `src/backends/swift/gen_bindings/service_api.rs`, `tests/e2e_elixir_plugin_trait_stubs.rs`)

- **alef php naming + jni symbol tests: correct test expectations to match production behaviour.** Two unit-test assertions encoded incorrect expected values: (1) `src/backends/php/naming.rs` expected `php_autoload_namespace` to return `Html\To\Markdown\Rs` for the input `sample_markdown_rs`, but the function correctly splits on `_` and PascalCases each component → `Sample\Markdown\Rs`; (2) `src/core/jni.rs` `jni_symbol_basic` and `jni_symbol_empty_method_gives_prefix` expected raw underscores in `dev.sample_crate.demo` to pass through unchanged, but the JNI spec §5.11.3 requires underscores inside identifier components to be encoded as `_1` (so `sample_crate` → `sample_1crate`) — the production `jni_symbol` function already does this. Tests now match the spec-compliant behaviour. (`src/backends/php/naming.rs`, `src/core/jni.rs`)

- **alef vendor: append empty `[workspace]` to inner vendored Cargo.toml when no outer workspace manifest is generated.** `vendor_core_only(..., generate_workspace_manifest=false)` (used for languages like Elixir) copied the core crate into `{dest}/{crate}/Cargo.toml` but left it without any workspace marker. Downstream tooling (e.g. `publish-hex`'s "Generate Cargo.lock for native dependencies" loop, which iterates every `Cargo.toml` under `native/` and runs `cargo generate-lockfile`) then triggered cargo to walk up from the vendor path looking for a workspace, find the consumer repo's root workspace, and bail with "current package believes it's in a workspace when it's not". The fix appends an empty `[workspace]` table to the inner vendored Cargo.toml, making it a self-contained one-package workspace root. For Ruby (`generate_workspace_manifest=true`), the outer `{dest}/Cargo.toml` already claims membership and we preserve the existing behaviour (no inner `[workspace]`). (`src/publish/vendor.rs`)

- **alef c e2e Makefile: skip download_ffi.sh when the FFI header is already present locally.** The generated `e2e/<lib>/Makefile` hardcoded `$(TARGET): $(SRCS) $(FFI_DIR)/include/{header}` as a prerequisite, which always triggered the `download_ffi.sh` rule even when a locally-built header existed at `../../crates/<lib>-ffi/include/{header}`. On release commits this caused CI E2E (c) to 404 against a release that hadn't published its FFI assets yet (or whose pinned `VERSION` was stale). The `FFI_INCLUDE` fallback only affected `CFLAGS`, not the build prerequisite — the fallback was effectively dead for the build itself. Now the codegen also emits `HEADER_PATH := $(if ...)` that resolves to whichever of the two candidate header paths exists, and the build target's prerequisite becomes conditional: `$(if $(HEADER_PATH),,$(FFI_DIR)/include/{header})`. When either path holds the header, no download is triggered; when neither does, the original download flow runs as before. (`src/e2e/codegen/c.rs`)

- **alef c e2e Makefile: also gate download_ffi.sh skip on the shared library being present locally, not just the header.** The previous fix gated the download-trigger prerequisite on `HEADER_PATH` alone, but the build also needs the shared library — on a fresh smoke-test clone with no local cargo build, the in-tree header would be discovered (so HEADER_PATH set, download skipped) while no `../../target/release/lib{name}.{so,dylib,a}` exists, leaving FFI_LIB_DIR empty and link failing with `library not found`. Codegen now also emits `LIB_PATH := $(or $(wildcard $(FFI_DIR)/lib/lib{name}.so),$(wildcard $(FFI_DIR)/lib/lib{name}.dylib),$(wildcard $(FFI_DIR)/lib/lib{name}.a),$(wildcard ../../target/release/lib{name}.so),$(wildcard ../../target/release/lib{name}.dylib),$(wildcard ../../target/release/lib{name}.a))` covering both `$(FFI_DIR)/lib/` and `../../target/release/` for each candidate extension; the build target downloads only when _either_ HEADER_PATH or LIB_PATH is empty (`$(if $(and $(HEADER_PATH),$(LIB_PATH)),,$(FFI_DIR)/include/{header})`). CI E2E flows with both header and lib staged locally remain download-free; smoke-test flows with no local build always trigger the download. (`src/e2e/codegen/c.rs`)

- **alef java e2e pom.xml: only set `-Djava.library.path=…/target/release` in Local dep_mode; drop it in Registry mode so the Maven-Central JAR's bundled `/natives/{rid}/` extraction loads correctly.** The generated `test_apps/java/pom.xml` (Registry mode) consumes the published JAR which embeds platform natives under `/natives/{rid}/` — alef's `NativeLib` loader extracts and `System.load`s them automatically at startup. Overriding `java.library.path` to point at a non-existent `../../target/release` (the previous unconditional behaviour) made `System.loadLibrary` the only resolution path, and the fallback failed with `UnsatisfiedLinkError: no html_to_markdown_ffi in java.library.path: …/target/release`. The override is now emitted only when `dep_mode == Local` (the e2e test app that consumes a locally-built JAR without bundled natives, where the loader needs to find the native via java.library.path). Template gains an `include_native_lib_path` flag; codegen sets it from `dep_mode`. (`src/e2e/codegen/java.rs`, `src/e2e/templates/java/pom.xml.jinja`)

- **alef scaffold: persist rustc job cap and Node memory cap mitigations in generated `.cargo/config.toml` and wasm e2e `package.json`.** OOM crashes on 16 GB dev machines during large WASM builds were prevented by four mitigations: rustc job limit (`jobs = 4` in `.cargo/config.toml`), WASM size optimization, and Node memory cap (`NODE_OPTIONS=--max-old-space-size=4096` in e2e test script). However, alef was regenerating both the cargo config and wasm e2e package.json from templates that lacked these settings, clobbering the mitigations on every `task alef:generate`. Now `[scaffold.cargo]` has a new `build_jobs` field (default `4`; set to `0` to disable), and the wasm e2e package.json template includes the `NODE_OPTIONS` heap cap in its test script. Both mitigations persist through regeneration. Added three unit tests in `src/scaffold/tests.rs` to verify default job limit, custom override, and zero-disabling behavior; added one test in `src/e2e/codegen/wasm.rs` to verify `NODE_OPTIONS` is emitted in the package.json test script.

- **alef wasm e2e: consolidate setup.ts imports to prevent duplicate import statements.** The wasm e2e codegen was emitting setup.ts with two separate import blocks: one for wasm module init (`createRequire`, `readFileSync`, `fileURLToPath`) and another for file setup (`createRequire`, `fileURLToPath`, `dirname`, `join`), causing TypeScript "duplicate identifier" errors. Fixed by consolidating all imports into a single block at the top of the file, emitting `dirname` and `join` only when file setup is needed. Added unit test to verify import uniqueness.

### Added

- **alef csharp/dart/go/swift/zig: implement `generate_service_api` for five more backends.** Completes the service-API rollout across every alef-supported language by adding emitters for the remaining native and FFI-consuming backends. Each backend generates, from the same `ServiceDef`/`HandlerContractDef` IR used by the pyo3/napi/magnus/php/rustler/jni/ffi/extendr emitters, an idiomatic host-language owner object (constructor + chaining configurators + per-registration method + `run`/`finalize` entrypoint) plus the Rust glue that bridges each host callback into the async handler contract. The five backends use their canonical FFI patterns: C# wraps the C-ABI service handle via P/Invoke with `[UnmanagedCallersOnly]` callback marshalling, Dart drives the same C ABI through FRB-generated bindings, Go uses cgo with `runtime.Pinner`-guarded callback pointers, Swift bridges via swift-bridge's typed JSON-envelope adapters, Zig consumes the raw C ABI directly via `extern fn`. Wire request/response DTOs continue to be JSON-marshalled across the boundary. `supports_service_api` set on each. IR-derived with zero hardcoded names; golden tests included per backend. (`src/backends/{csharp,dart,go,swift,zig}/gen_bindings/service_api.rs`, `…/gen_bindings/mod.rs`)
- **alef jni: implement `generate_service_api` for the JVM foundation.** Generates the JNI glue java/kotlin will wrap: an opaque owner handle (`jlong` from `Box::into_raw`) with constructor/`_free` entry points, a per-`HandlerContractDef` bridge holding a JVM `GlobalRef` + cached `jmethodID` whose async dispatch attaches the current thread, calls the host handler with the `wire_request_type` JSON, and parses the `wire_response_type`, and `extern "system"` register/run/finalize entry points that thread the owner handle, call the owner's registration method with the bridge, and drive the entrypoint via a Tokio runtime. No panics in JNI; SAFETY comments on every unsafe. IR-derived; golden tests assert the register code calls the owner method and run drives the owner. (`src/backends/jni/service_api.rs`, `src/backends/jni/{mod,gen_shims}.rs`)
- **alef ffi/extendr: implement `generate_service_api` for the C-ABI foundation and R backends.** The C FFI backend now emits the stable C ABI other native backends wrap: a `char* (*)(void* ctx, const char* request_json)` registration-callback typedef, opaque service handle with matching `_new`/`_free`, per-`HandlerContractDef` bridge implementing the async contract by invoking the C callback under `spawn_blocking` (wire request/response round-tripped as JSON with documented alloc/free ownership), and `extern "C"` register/run/finalize entry points. The extendr (R) backend generates an idiomatic S3 object API plus Rust glue that calls the registered R closure synchronously (R is single-threaded — documented), blocking the Tokio runtime. Both are IR-derived with no hardcoded names; `supports_service_api` set; golden tests included. (`src/backends/ffi/gen_bindings/service_api.rs`, `src/backends/extendr/gen_bindings/service_api.rs`, `…/mod.rs`)
- **alef napi/magnus/php/rustler: implement `generate_service_api` for four more backends.** Mirroring the pyo3 pilot, the Node/TypeScript (napi), Ruby (magnus), PHP (ext-php-rs), and Elixir (rustler) backends now generate, from the `ServiceDef`/`HandlerContractDef` IR, an idiomatic host-language app object (constructor + chaining configurators + per-registration decorator/method + `run`/`finalize` entrypoint) plus the Rust glue that bridges each host callback into the async handler contract and drives the run entrypoint. Each uses its language's idiomatic async bridge: napi `ThreadsafeFunction` awaiting the JS Promise, magnus acquiring the GVL inside `spawn_blocking`, php blocking on the Tokio runtime (single-threaded per request), rustler message-passing (`{:trait_call, …}` to a generated GenServer, reply via a completion NIF, run scheduled on `DirtyCpu`). Wire request/response DTOs are round-tripped as JSON so the host receives/returns plain values. `supports_service_api` is set for each. Everything is IR-derived with zero hardcoded names; each backend ships golden tests asserting the generated host + Rust surfaces. (`src/backends/{napi,magnus,php,rustler}/gen_bindings/service_api.rs`, `…/gen_bindings/mod.rs`)
- **alef pyo3: implement `generate_service_api` for the Python backend (pilot implementation).** When `api.services` is non-empty the pyo3 backend now generates two files per configured service. (1) `service.py` — an idiomatic Python class whose name, constructor, configurator methods, registration decorators, and `run`/`finalize` entrypoints are derived entirely from the `ServiceDef` IR; each registration method doubles as both a decorator factory (`@app.add_handler(path, method)`) and a direct `register_*` variant. (2) `service.rs` — Rust pyo3 glue containing a `Py{ContractName}Bridge` struct for each referenced `HandlerContractDef` that wraps a Python callable (`Py<PyAny>`) and implements the async handler trait; async callables are driven via `pyo3_async_runtimes::tokio::into_future`, sync callables run in `spawn_blocking` to avoid blocking the Tokio executor; wire request/response DTOs are round-tripped through `serde_json::to_string`/`from_str` so the Python side receives a plain dict and returns a plain dict; a `#[pyfunction]` entry point iterates the collected `(method_name, metadata_tuple, callable)` registrations, dispatches on method name to the correct bridge + registration call, and invokes the owner's entrypoint. `Capabilities::supports_service_api` is set to `true` for the pyo3 backend. Zero hardcoded names — all types, method names, and contract names are derived from the IR. The bridge approach reuses the async/sync dispatch pattern pioneered by `src/adapters/callback_bridge.rs`. Covered by 11 unit/golden tests in `src/backends/pyo3/gen_bindings/service_api.rs`. (`src/backends/pyo3/gen_bindings/service_api.rs`, `src/backends/pyo3/gen_bindings/mod.rs`)

- **alef core: model a generic callback-driven "service" API surface (`[[crates.services]]` + `[[crates.handler_contracts]]`).** Adds the building blocks for backends to later generate an idiomatic owner/builder object whose registered host callbacks are bridged into an async Rust trait and consumed by a long-lived `run` entrypoint — generically, with no transport- or domain-specific assumptions. New IR nodes on `ApiSurface`: `ServiceDef` (owner type + constructor + chaining configurators + callback `registrations` + `run`/`finalize` `entrypoints`) and `HandlerContractDef` (the async trait a registered callback satisfies, its dispatch method, and the wire request/response DTO names). New config: `ServiceConfig`/`RegistrationSpec`/`EntrypointSpec` and `HandlerContractConfig` (flattens `TraitBridgeConfig`, adds `dispatch_method`/`is_async`/`wire_request_type`/`wire_response_type`/`optional_overrides`), wired through `RawCrateConfig`→`ResolvedCrateConfig` with validation. New extraction pass (`src/extract/extractor/service.rs`) that classifies owner-type methods and recovers callback-bound generic registration methods (e.g. `fn add_route<H: IntoHandler>(…)`) that the FFI-safety generic-method skip drops, by re-parsing the configured sources via the shared method extractor and injecting them into the owner type (which is marked `binding_excluded`). New optional `Backend::generate_service_api` hook + `Capabilities::supports_service_api`, dispatched after `generate_bindings` and before `generate_public_api`, with a warning when a configured service targets a backend that does not support it. The dead `AdapterPattern::ServerLifecycle` stub is removed (superseded by `ServiceDef`). Backends do not yet implement `generate_service_api`; this is purely additive — consumers without `[[crates.services]]` are unchanged. (`src/core/ir.rs`, `src/core/config/service.rs`, `src/core/config/{raw_crate,new_config,resolved}.rs`, `src/extract/extractor/service.rs`, `src/core/backend.rs`, `src/cli/pipeline/generate.rs`, `src/adapters/mod.rs`)

## [0.19.25] - 2026-05-27

### Fixed

- **alef dart frb_rewrite: ensure balanced braces and parentheses in prologue generation.** The `frb_init_prologue_replacement` function generates Dart code that injects the `_alefResolveExternalLibrary()` helper method and replaces the `init` method prologue. The helper method is fully closed, and the init signature with opening brace are emitted, allowing the original FRB method body to continue seamlessly. Added documentation clarifying the brace-balancing structure and a unit test `prologue_replacement_has_balanced_braces_and_parens` to verify balanced delimiters (equal open/close braces and parens) on generated output. This prevents malformed Rust in kreuzberg's generated `packages/dart/rust/src/lib.rs` that would cause flutter_rust_bridge_codegen to stall indefinitely during Dart FFI binding generation. (`src/backends/dart/frb_rewrite.rs`)
- **alef zig trait-bridge: fix VTable casting and test stub method completeness.** (1) The vtable struct signatures were incorrectly applying `substitute_excluded_types`, replacing internal types like `InternalDocument` with `Json` in the C ABI boundary. This broke type compatibility: Rust FFI functions expected the original `InternalDocument` C pointer type, but generated Zig was trying to pass `Json` (which doesn't exist in the public surface). The fix restricts type substitution to Zig-level wrapper code only; VTable struct field types and thunk C ABI signatures preserve the original types to maintain C FFI compatibility. (2) E2E test stubs for plugin-registry traits (DocumentExtractor, EmbeddingBackend, OcrBackend, PostProcessor, Validator, Renderer) were missing required interface methods. The `make_*_vtable` functions call ALL trait methods (including optional ones with default implementations), so test stubs must implement them all. Fixed by emitting all trait methods (both required and defaulted) in the test stub generator, ensuring stubs pass the full interface contract to vtable builder. Resolves all 36 Zig e2e plugin-api test compilation errors. (`src/backends/zig/trait_bridge.rs`, `src/e2e/codegen/zig.rs`)
- **alef go e2e stubs: emit Plugin super-trait methods and use proper Go type names for Named types.** Generated test backend stubs in Go e2e tests (e.g., `testStub_register_document_extractor_trait_bridge`) only emitted direct trait methods and omitted super-trait methods like `Version()`, `Initialize()`, `Shutdown()`, `CanHandle()`, `Priority()`, `EstimatedDurationMs()`. This produced incomplete structs that failed to satisfy the full interface contract expected by `Register*` calls. Additionally, Named types like `OcrBackendType` were being mapped to `json.RawMessage` in stub signatures even though they're part of the public binding surface; stubs should use the actual Go type names (e.g., `OcrBackendType`) to match the trait-bridge interface signatures. Fixed by: (1) collecting super-trait methods when building method lists for stubs (methods found in the super-trait type definition in the IR), (2) filtering methods by `trait_source` to identify and emit super-trait methods separately (with `Name()` returning the fixture id and other methods using standard stub logic), (3) simplifying `stub_go_type()` to use `go_type()` directly instead of unconditionally substituting Named types with `json.RawMessage` — this ensures stubs use the canonical Go binding types, and (4) updating `go_stub_default()` to use `go_zero_value()` for consistency. The dispatcher in `mod.rs` identifies super-trait methods via `trait_source` matching, eliminating the need for hardcoded method lists. All 6 trait-bridge stubs (DocumentExtractor, EmbeddingBackend, OcrBackend, PostProcessor, Renderer, Validator) now implement the full Plugin interface. (`src/e2e/codegen/go.rs`)
- **alef java e2e: fix trait-bridge test stubs with full interface implementation and proper type FQNs.** Generated Java e2e tests for plugin-registry traits (e.g., register_document_extractor, register_ocr_backend) were failing compilation because stub implementations: (1) emitted wrong method signatures (e.g., `CompletableFuture<Object>` instead of matching the interface's actual return type), (2) used bare type names without FQNs (e.g., `ExtractionResult` instead of `dev.kreuzberg.ExtractionResult`, `List` instead of `java.util.List`), and (3) omitted super-trait methods (name, version, initialize, shutdown, etc.). Fixed by: (a) using proper type qualification for all Kreuzberg domain types in stub method signatures via `java_stub_type_fqn` helper that qualifies all `Named(T)` types as `dev.kreuzberg.T` and collection inner types as `java.util.List<dev.kreuzberg.T>`, (b) applying FQNs consistently to both parameter and return types, and (c) explicitly emitting all Plugin super-trait methods in the stub class (collected from IR via `trait_source` matching). Stubs now implement the full `IDocumentExtractor`/`IOcrBackend`/etc. interface contract. (`src/e2e/codegen/java.rs`)
- **alef elixir e2e: fix register_*/2 ArgumentError by passing GenServer PID instead of module atom to Rustler NIFs.** Generated Elixir e2e tests for plugin-registry traits (e.g., register_document_extractor, register_embedding_backend) were calling `Kreuzberg.register_document_extractor(E2e.TestStubs.TestStubMyBackend, "name")` where the first argument is a module atom. However, the Rustler NIF signature expects a `rustler::LocalPid` to enable message-based trait callback dispatch: the NIF creates a bridge that sends `{:trait_call, method, args_json, reply_id}` messages to that PID and waits for responses. Since a module atom cannot receive messages, the NIF's `pid` parameter decode failed with `ArgumentError: argument error`. Fixed by: (1) emitting a wrapper `GenServer` module for each test stub that implements the trait callback protocol via `handle_info/2` and dispatches method calls to the underlying stub module using `apply/3`, (2) starting the GenServer in the e2e test setup block and capturing its PID, and (3) passing the PID (not the module atom) as the first argument to `register_*/2`. The generated GenServer handles incoming `{:trait_call, method_atom, args_json, reply_id}` messages from Rust, calls the stub method with deserialized args, serializes the result to JSON, and replies via `Kreuzberg.complete_trait_call/2` NIF to unblock the Rust thread. All 6 register_*/2 calls in kreuzberg Elixir e2e (document_extractor, embedding_backend, ocr_backend, post_processor, renderer, validator) now pass. (`src/e2e/codegen/elixir.rs`)
- **alef swift-backend: fix trait-bridge protocol and adapter code generation with proper type marshalling and error handling.** The trait-bridge codegen was emitting placeholder Swift bridge files with (1) references to excluded internal types like `InternalDocument` that don't exist in the public Swift binding surface, (2) placeholder empty string `""` return values for all methods, and (3) no marshalling logic for converting types across the C boundary. Fixed by: (a) accepting an `exclude_types` parameter in `gen_trait_bridge_files` and using it to marshal excluded types as JSON strings (matching Zig/Rust patterns), (b) emitting proper adapter method bodies with `do-try-catch` error handling and JSON envelope serialisation for error-returning methods, (c) generating helper functions `marshal_ok_result` and `marshal_error_result` for Result wrapping, and (d) updating all type inference functions (`swift_type_name`, `swift_method_params`, `swift_return_type`) to substitute excluded types with `String` for JSON marshalling. Also updates the call site in `gen_bindings/mod.rs` to thread `exclude_types` through the trait bridge generation. (`src/backends/swift/gen_bindings/trait_bridge.rs`, `src/backends/swift/gen_bindings/mod.rs`)
- **alef typescript e2e: stop emitting sealed `pnpm-workspace.yaml` in Local mode so `kreuzberg@workspace:*` resolves from parent workspace.** When `[crates.e2e.dep_mode]` is `local` (default), the Node e2e test app depends on the binding via `workspace:*`, which must resolve through the consumer's root pnpm workspace. However, alef was previously emitting a sealed `packages: []` workspace file that made e2e/node a self-rooted workspace, shadowing the parent and breaking `pnpm install` with `ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE`. The sealed workspace is only needed in Registry mode (when the test app has no `workspace:*` deps and needs isolated devDependencies). In Local mode, the codegen now explicitly removes any stale `pnpm-workspace.yaml` if it exists (even without alef header), preventing shadowing. A new test verifies Local mode does not emit the file. Fixes kreuzberg e2e/node `pnpm install` failures. (`src/e2e/codegen/typescript/mod.rs`)
- **alef extendr e2e: add `arg_name_map` to register_*/unregister_*/clear_* call overrides in alef.toml for R test generation.** Generated R e2e tests call `register_document_extractor(extractor = ...)` but the Rust trait-bridge wrapper function signature expects parameter name `r_backend`, causing `unused argument` test errors. The fixture JSON uses semantic names (`extractor`, `backend`, `processor`, `renderer`, `validator`) matching the fixture shape, but the extendr-generated R wrapper uses the internal parameter name. R e2e test generation now applies `arg_name_map` overrides in alef.toml to remap fixture argument names to the actual Rust function parameter names. All six register_* calls (document_extractor, embedding_backend, ocr_backend, post_processor, renderer, validator) now include mappings: `arg_name_map = { fixture_name = "r_backend" }`. (`crates/kreuzberg/alef.toml`)
- **alef e2e ruby codegen: fix void-returning register/unregister assertions and add plugin registry cleanup hooks.** (1) The Ruby e2e stub emitter was generating `result = Kreuzberg.register_ocr_backend(...); expect(result).not_to be_nil` for all register_*/unregister_* calls. These functions return `Result<(), E>` which becomes Ruby `nil` on success, so the assertion was always failing. Fixed by setting `returns_void = true` on all 12 register/unregister/clear call configs in `alef.toml` and updating the Jinja template to handle void-returning methods properly. (2) The generated `spec_helper.rb` now includes RSpec `before(:each)` and `after(:each)` hooks that track and clean up test-prefixed plugin backends (those with `test-` or `test_` prefix), preventing one test's registered mock backends from polluting subsequent tests. This mirrors the Python `conftest.py` isolation pattern. (`alef.toml`, `src/e2e/codegen/ruby.rs`, `src/e2e/templates/ruby/test_function.jinja`)
- **alef e2e php codegen: collect super-trait methods when generating test backend stubs.** Generated test stubs for trait-bridge backends (e.g., `DocumentExtractor` with `Plugin` super-trait) only emitted methods from the direct trait, omitting super-trait methods (`name`, `priority`, `version`, etc.). This produced anonymous PHP classes implementing only 3 of 6 required interface methods, causing `Kreuzberg` registration calls to fail with `Declaration of Kreuzberg\DocumentExtractor::extract_bytes() must be compatible with its parent interface` or missing-method errors. The method collection phase now gathers both direct trait methods and super-trait methods (by `rust_path` to avoid short-name collisions), ensuring the generated anonymous class implements the full interface contract. (`src/e2e/codegen/php.rs`)
- **alef python e2e codegen: treat Rust-style primitive type names as primitives in test-file imports.** The Python e2e test generator iterates over a fixture's call args and, for any non-primitive `element_type`, adds it to the `from <pkg> import …` line. The primitive skip-list only contained Python-style type names (`str`, `int`, `bool`, …), so an `element_type = "String"` (a Rust string, idiomatic in `alef.toml` since the config is language-agnostic) was treated as a public binding symbol and emitted as `from kreuzcrawl import String, …` — hard-failing pytest collection with `ImportError: cannot import name 'String' from 'kreuzcrawl'`. The skip-list now covers Rust primitive names too: `String`, `&str`, `char`, all signed/unsigned integer widths (`u8..u128`, `i8..i128`, `usize`/`isize`), and `f32`/`f64`. Verified by the kreuzcrawl rc.35 CI E2E python failure (run 26497302955, test_batch.py and test_stream.py both broke on `String` collection). (`src/e2e/codegen/python/test_file.rs`)
- **alef sync-versions: bump NAPI platform pins and pre-staged platform manifests in `crates/*-node/`.** `sync_versions` rewrote the top-level `"version"` of `crates/*-node/package.json` but never touched (a) the `optionalDependencies` block (each entry is a sibling NAPI platform package pinned to the same version) or (b) the pre-staged platform manifests at `crates/*-node/npm/<platform>/package.json` that `napi prepublish` ships. After a version bump the parent package was at the new version while every platform pin stayed at the old version, so `pnpm install --frozen-lockfile` on consumer CI rejected the manifest with `ERR_PNPM_OUTDATED_LOCKFILE` and the Build Node bindings job hard-failed. `sync_versions` now rewrites both surfaces: it reads the parent package `name` from the manifest and rewrites every `optionalDependencies` entry whose key starts with `<name>-` (the `napi_platform_package_name` shape), and globs `crates/*-node/npm/*/package.json` to bump the platform manifests' top-level `"version"`. Verified in kreuzcrawl rc.34 Publish Node bindings failure (run 26494601181), where the manifest had rc.34 but all `optionalDependencies` pins and platform manifests stayed at rc.33. (`src/cli/pipeline/version.rs`)

## [0.19.24] - 2026-05-27

### Fixed

- **alef php-backend: insert missing comma between fixed `NodeContext $context` parameter and the trailing per-method parameter list in generated `HtmlVisitorInterface`.** `php_visitor_interface_method.jinja` emitted `function visit_text(NodeContext $context{{ method_params }})`, producing the invalid PHP signature `function visit_text(NodeContext $contextstring $_text)` whenever `method_params` was non-empty. PHP-CS-Fixer + PHPStan both refused to parse the generated `HtmlVisitor.php`, breaking lint CI on every consumer. The template now conditionally inserts `,` before `method_params`. (`src/backends/php/templates/php_visitor_interface_method.jinja`)
- **alef e2e c codegen: harden `download_ffi.sh` `rm -rf` paths with `${FFI_DIR:?}`.** `shellcheck` SC2115 flagged the unguarded `rm -rf "$FFI_DIR"/include "$FFI_DIR"/lib` and `rm -rf "$FFI_DIR"/${FFI_PKG_NAME}-*` patterns: if `$FFI_DIR` is ever unset the script would attempt `rm -rf /include /lib`. The `:?` parameter expansion aborts the script instead of silently expanding to `/`. (`src/e2e/codegen/c.rs`)

## [0.19.23] - 2026-05-27

### Fixed

- **alef go-backend: derive `embed_ffi.go` package name from config instead of hardcoding `tspack`.** The `embed_ffi.go.jinja` template hardcoded `package tspack` (a leftover from the template's tree-sitter-language-pack origin) and the render site passed an empty context, so every consumer's generated `embed_ffi.go` declared `package tspack` regardless of the configured Go package name — mismatching `binding.go`'s `package <pkg_name>`. Although `embed_ffi.go` is `//go:build ignore` (excluded from normal compilation, so `go build` was unaffected), the foreign package name was incorrect and misleading for vendoring tooling. The template now emits `package {{ pkg_name }}` and the render call threads `pkg_name` through. (`src/backends/go/templates/embed_ffi.go.jinja`, `src/backends/go/gen_bindings/mod.rs`)

### Changed

- **alef: align shared pre-commit hooks.** (`5fc63a3f`)

## [0.19.22] - 2026-05-27

### Added

- **alef packaging: centralize generated package metadata and publish validation.** Added `[workspace.package_metadata]` and `[crates.package_metadata]` as the canonical source for generated package descriptions, links, authors, keywords, and categories. Alef now validates crates.io's five-keyword/category limit unless `truncate_registry_lists = true` is explicitly set, and package scaffolds use the centralized metadata before falling back to legacy `[scaffold]` fields.
- **alef dart-backend: new `FrbDartOptionalFieldsWithDefaults` post-build step.** Strips the `required` keyword from FRB-emitted Dart class fields that correspond to Rust struct fields with `#[serde(default)]` or `Default` impls (currently covers `EmbeddingConfig`, `ChunkingConfig`, `ExtractionConfig`). Surfaced by kreuzberg v5.0.0-rc.3 dart e2e: `EmbeddingConfig(...)` failed compilation with `Required named parameter 'model' must be provided` even though the Rust `EmbeddingConfig` had defaults. (`src/core/backend.rs`, `src/cli/pipeline/commands.rs`, `src/backends/dart/frb_rewrite.rs`, `src/backends/dart/gen_bindings/mod.rs`, `src/backends/dart/mod.rs`)

### Fixed

- **alef csharp-backend: emit static `Register(IInterface impl) -> IntPtr` on every trait-bridge class.** v0.19.21 only emitted `Register` on the separate `{TraitName}Registry` static class, but the e2e tests (and ergonomic public API) expect to call `DocumentExtractorBridge.Register(impl)` directly on the bridge class. Caused kreuzberg v5.0.0-rc.3 csharp e2e: `CS0117: 'DocumentExtractorBridge' does not contain a definition for 'Register'`. The new method handles GCHandle allocation, vtable wiring, and error propagation. (`src/backends/csharp/templates/trait_bridge_class.jinja`, `src/backends/csharp/trait_bridge.rs`)
- **alef php-backend: drop leading comma when emitting PHP interface method parameter lists.** `gen_visitor_interface` and `gen_registration_interface` in `src/backends/php/trait_bridge.rs` prepended `", "` to the params list (copied from the Rust bridge emitter, where methods carry an implicit `&mut self` first). With no implicit first arg, this produced `public function extract_bytes(, mixed $content, ...): mixed;` — a PHP parse error. Caused all 6 PHP interface files in v0.19.20/v0.19.21 to be syntactically invalid.
- **alef extendr-backend: filter excluded functions from `extendr_module!` macro registration.** v0.19.19's `[crates.r.exclude_functions]` honoring stripped excluded fn definitions but left their `fn <name>;` lines inside the `extendr_module!` macro. extendr expands those lines into `meta__<name>()` wrapper calls — which referenced the now-missing fn — producing `cannot find function meta__calculate_quality_score in this scope`. Filter applied at all four code paths: fn def emission, `extendr_module!` registration, R wrapper generation, NAMESPACE export. (`src/backends/extendr/gen_bindings.rs`, `tests/backends_extendr_gen_bindings_test.rs`)
- **alef rustler-backend: expose `register_*/2` and `clear_*/0` delegates on the Elixir facade.** Prior to this fix the Elixir facade emitted only `unregister_*` delegates; consumers calling `Kreuzberg.register_document_extractor/2` got `undefined or private`. The 6 register and 6 clear delegates forward straight to the corresponding `Kreuzberg.Native` NIF entry points. (`src/backends/rustler/gen_bindings/mod.rs`)
- **alef swift-scaffold: emit `__private__FfiSlice` and the `__private__OptionXX` typedefs in the placeholder `RustBridgeC.h` header.** The placeholder header (used before the Rust cargo build has run) only declared `RustStr`; the `SwiftBridgeCore.swift` extensions reference `__private__FfiSlice` (for `Vec<u8>`) and `__private__OptionU8` through `__private__OptionBool`, so the Swift compiler failed with `cannot find type '__private__FfiSlice' in scope`. Adds 14 new typedefs and `<stdbool.h>` include. (`src/scaffold/languages/swift.rs`, `src/backends/swift/gen_bindings/mod.rs`)
- **alef zig-backend: substitute excluded Rust types with `Json` in trait-bridge method signatures.** Internal Rust types listed in `[crates.zig.exclude_types]` (e.g. `InternalDocument`) were emitted verbatim inside the bridge vtable and thunks, producing `struct 'kreuzberg' has no member named 'InternalDocument'`. Substitution mirrors the Go backend pattern: excluded types serialize through the C ABI as JSON strings. (`src/backends/zig/trait_bridge.rs`, `src/backends/zig/gen_bindings/mod.rs`)
- **alef dart-backend: also strip excluded functions from `frb_generated.dart`.** v0.19.21's `FrbDartExcludeFunctions` only filtered top-level `lib.dart`; FRB emits the FFI bridge symbol declarations (e.g. `crateCalculateQualityScore`) in `frb_generated.dart` as well, leaving a dangling reference once the implementation was removed. The post-build step now runs the same filter on both files. (`src/backends/dart/gen_bindings/mod.rs`)
- **alef e2e go codegen: collect super-trait methods by `rust_path`, not by short name.** `src/e2e/codegen/go.rs:1962` compared `t.name` (e.g. `"Plugin"`) against the `super_trait` field from `alef.toml` (the fully-qualified `"kreuzberg::plugins::Plugin"`); the comparison never matched and super-trait methods were silently dropped from generated test stubs, breaking interface implementation. Now compared against `rust_path`. (`src/e2e/codegen/go.rs`)
- **alef e2e java codegen: emit fully-qualified type names in trait-bridge test stubs and defaults.** Generated `PluginApiTest.java` referenced bare `IDocumentExtractor`, `ExtractionConfig`, `List<String>`, `ArrayList` etc., producing `cannot find symbol` against the test file's import set. New `java_type_fqn` / `java_stub_type_fqn` / `java_boxed_stub_type_fqn` helpers emit `dev.kreuzberg.IDocumentExtractor`, `java.util.List<String>`, `new java.util.ArrayList<>()` etc.; `JavaDefaults::default_for` similarly emits FQN constructors. (`src/e2e/codegen/java.rs`, `src/codegen/defaults.rs`)
- **alef e2e ruby codegen: pass plugin name as second argument to `register_*` calls.** The Magnus binding's `register_<trait>` accepts `(impl, name)` but the ruby e2e stub emitter only emitted `(impl)`, producing `ArgumentError: wrong number of arguments (given 1, expected 2)`. The fix extracts the backend name from each fixture's input JSON and emits it as a Ruby string literal; also collects super-trait methods (`name`, `version`, `initialize`, `shutdown`) onto the stub class. (`src/e2e/codegen/ruby.rs`)
- **alef e2e elixir codegen: pass plugin name as second argument to `Kreuzberg.register_*` calls.** Mirrors the ruby fix; the Rustler facade expects `(stub, name)` (see rustler-backend fix above) but the elixir e2e stub emitter only emitted `(stub)`, producing `Kreuzberg.register_document_extractor/1 is undefined or private`. Backend name is extracted from fixture input and emitted as an Elixir string literal with `escape_elixir`. (`src/e2e/codegen/elixir.rs`)
- **alef zig publish: emit per-platform tarballs with distinct filenames.** `alef publish package --lang zig --target <T>` now produces `{crate}-zig-v{version}-{platform}.tar.gz` (mirroring the Go package naming scheme) instead of `{crate}-v{version}.tar.gz`. Previously all platform-specific builds collided on the same filename during GitHub Release asset upload, leaving only the final tarball in the release. Platform naming uses the Go ecosystem convention (e.g. `linux-x86_64`, `macos-arm64`).
- **alef test-apps: install freshly-published packages in registry mode.** The Node and WASM test-app run commands now pass `pnpm install --no-frozen-lockfile --config.minimumReleaseAge=0` (and `npm install --no-package-lock` on the npm path): they re-resolve from `package.json` instead of the committed lockfile, which pins the previous release, and bypass pnpm's `minimumReleaseAge` supply-chain gate, which otherwise rejects the just-released version under test. (`src/core/config/test_apps_run_defaults.rs`)
- **alef packaging: generate and validate language package internals instead of relying on repo-local drift.** Ruby gemspecs include README/LICENSE and generated type files; Node package scaffolding emits platform manifests, optional dependencies, `exports`, engines, musl libc metadata, and `win32-arm64-msvc`; WASM packages can use explicit package names and publish from Alef's manifest; Elixir scaffolds include `nif_targets`; PHP root Composer autoload points at `packages/php/src/`; publish validation detects stale Ruby/C# package entrypoints, Go `/vN` layout mismatches, Swift release placeholders, and missing Java/Dart/Zig metadata.
- **alef python-scaffold: stop emitting broken `[tool.maturin] include` block.** v0.19.21 added an `include = [{ path = "../../crates/<crate>/**/*", format = "sdist" }]` directive intending to bundle the core Rust crate into the Python sdist, but maturin rejects archive paths containing `..` (`paths in archives must not have ".."`), so every sdist build with the directive present failed outright. Maturin already auto-bundles path dependencies declared in the PyO3 crate's `Cargo.toml`, so the directive was both broken and unnecessary. Removed `sdist_rust_crate_includes`, `relative_path`, and `normalized_components` helpers along with the corresponding template block and tests. The original #390 sdist regression (PyPI 3.5.1 missing the core crate) was actually caused by a stale `.gitmodules` entry in the consumer repo silently breaking `cargo package --list` — fixed at the consumer side. (`src/scaffold/languages/python.rs`, `src/scaffold/tests.rs`)
- **alef go-scaffold test: assert three emitted files (`go.mod`, `.golangci.yml`, `.lib/.gitkeep`).** The `embed_ffi.go` work in v0.19.21 added the `.lib/.gitkeep` marker but `test_scaffold_go_production_format` still asserted two files; the test failed on every cargo test run since. (`src/scaffold/tests.rs`)

## [0.19.21] - 2026-05-26

### Added

- **alef rustler-backend: `[languages.elixir] nif_targets` config field to align the generated `native.ex` `targets:` list with the consumer's actual NIF build matrix.** The `native_module_header.jinja` template previously hardcoded `~w(aarch64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu x86_64-pc-windows-gnu)` — 4 targets, with Windows as `-gnu`. Consumers whose CI matrix builds a different set (e.g. kreuzcrawl ships 7 targets including musl variants and `x86_64-pc-windows-msvc`) had no way to override this without hand-editing the generated file. RustlerPrecompiled then tried to download tarballs at install time using the hardcoded names, producing 404s on every consumer install whose CI uploaded under different triples. The `nif_targets` field on `ElixirConfig` accepts a `Vec<String>` of triples; when non-empty, the template emits exactly that list. When empty, the historical 4-target default is preserved so no existing consumer regresses. (`src/core/config/languages.rs`, `src/backends/rustler/template_env.rs`, `src/backends/rustler/gen_bindings/helpers.rs`)

### Changed

- **alef-cli: make post-generation formatting opt-in for `generate`, `all`, and `init`.** These commands now default `--format` to `false` so regeneration stays fast and avoids invoking language-native formatter chains unless requested. Pass `--format` to run the existing best-effort formatter pipeline before hash finalisation. (`src/main.rs`, `tests/cli_generate_format_flag.rs`, `skills/alef/references/cli-reference.md`)

### Fixed

- **kotlin e2e: stop the Gradle test worker from failing to fork.** The generated `e2e/kotlin/build.gradle.kts` set the test `workingDir` to `../../test_documents` unconditionally and passed no `jvmArgs`. When that directory is absent (consumers without document fixtures), Gradle's test worker could not `chdir` and aborted before JUnit started, surfacing as the misleading `Cannot abort process 'Gradle Test Executor N' because it is not in started or detached state`. The `workingDir` is now guarded on `isDirectory`, and the test fork sets `--enable-preview --enable-native-access=ALL-UNNAMED` to mirror the Panama Maven surefire argLine (the Java FFI bindings are preview-compiled). (`src/e2e/codegen/kotlin.rs`)

## [0.19.20] - 2026-05-26

### Fixed

- **alef php template_env: register `php_interface_start.jinja` and `php_interface_method.jinja` in the embedded `TEMPLATES` array.** The v0.19.19 PHP trait-bridge interface emission added the two new Jinja templates on disk and referenced them in `src/backends/php/trait_bridge.rs:960`, but forgot to register them in `src/backends/php/template_env.rs::TEMPLATES`. The template loader uses `include_str!` to embed templates at compile time, so the on-disk files are never read at runtime — only entries in `TEMPLATES` are visible to `make_env()`. As shipped, `alef 0.19.19` panicked on every PHP binding generation with `template php_interface_start.jinja not found`, blocking all consumers from upgrading. The fix adds both templates to the `TEMPLATES` array. (`src/backends/php/template_env.rs`)

## [0.19.19] - 2026-05-26

### Fixed

- **alef python scaffold: bundle the core Rust crate in the maturin sdist.** Generated `packages/python/pyproject.toml` now emits `[tool.maturin] include = [{ path = "../../crates/<core>/**/*", format = "sdist" }]` so the published sdist tarball contains the workspace's core crate alongside the binding crate. Without this, `pip install <package>` on platforms without a precompiled wheel (Alpine/musl, less-common architectures) falls back to building from sdist and fails — maturin strips the binding's `path = "../<core>"` clause from the published manifest, and the workspace `[patch.crates-io]` (when present) is left pointing at a missing path. Surfaced by `html-to-markdown` issue #390 (Alpine/musl source build broken on 3.5.1). (`src/scaffold/languages/python.rs`)

- **alef r-backend: honor [crates.r.exclude_functions] and [crates.r.exclude_types] to prevent incompatible functions from being emitted.** The R backend (extendr) did not respect the `exclude_functions` and `exclude_types` configuration fields that have been supported by other backends (Python, Ruby, etc.) since v0.19.10. This caused kreuzberg v5.0.0-rc.3 R CI job (run 26452244660, job 77885220796) to fail with type-mismatch compilation errors when binding `calculate_quality_score`, which returns `AHashMap<Cow<'_, str>, serde_json::Value>` — a type that cannot be mapped to R's standard types. The function was correctly listed in kreuzberg's `alef.toml` `[crates.exclude_functions]` (task #70) but the R backend had not yet implemented filtering. Fixes: (1) add `exclude_functions: Vec<String>` and `exclude_types: Vec<String>` fields to `RConfig` struct (alongside existing RubyConfig, PythonConfig patterns); (2) integrate exclusion filtering into three code generation paths: Rust-side function emission (in `generate_bindings`), R-wrapper generation (in `gen_extendr_wrappers_r`), and NAMESPACE export (in `gen_namespace`); (3) add snapshot test `test_exclude_functions_honored` verifying functions in the exclusion list are not emitted in Rust, R wrappers, or NAMESPACE. (`src/core/config/languages.rs`, `src/backends/extendr/gen_bindings.rs`, `tests/backends_extendr_gen_bindings_test.rs`)

- **alef dart-e2e-codegen: fix 5 trait-bridge stub generation bugs causing kreuzberg v5.0.0-rc.3 CI failures.** Dart E2E test stub emitter (emit_test_backend in `src/e2e/codegen/dart.rs`) had multiple regressions in kreuzberg v5.0.0-rc.3 CI (job 77885221080, 23/23 tests failed). Bug 1: internal-only return types like `InternalDocument` were emitted directly instead of being mapped to public types like `ExtractionResult`. Bug 2: `Uint8List` import was not added to generated test files when trait-bridge stubs used byte array parameters. Bug 3: enum return types like `OcrBackendType` and `ProcessingStage` were emitted as constructor calls `OcrBackendType()` instead of with default enum values `OcrBackendType.tesseract`. Bug 4: trait-bridge factory wrapper was marked `await` (async) but registration API expects sync `T` not `Future<T>`. Bug 5: excluded functions referenced in FRB-generated `lib.dart` (e.g., `crateCalculateQualityScore` at line 281) were not being filtered; the filter function was correct but not integrated into the emit path. Fixes: (1) add `map_dart_type_with_fallback()` helper that converts unmapped internal types to `ExtractionResult`; (2) detect test_backend args during import scanning and unconditionally emit `dart:typed_data` import; (3) add `emit_dart_default_for_type()` helper that special-cases enum types to return valid default variants; (4) remove `await` keyword from factory wrapper emission; (5) add snapshot test `filter_excluded_functions_removes_multiline_functions` to verify FRB post-processor filters excluded functions correctly. (`src/e2e/codegen/dart.rs`, `src/backends/dart/frb_rewrite.rs`)

- **alef java-facade: wrap trait-bridge wrapper method exceptions in try-catch blocks.** Java facade class (`Kreuzberg.java`) trait-bridge wrapper methods (e.g., `registerOcrBackend`, `unregisterOcrBackend`, `registerPostProcessor`) at lines 484, 488, 492, 528 called into Bridge classes that declare `throws Exception`, but the wrapper methods only declared `throws KreuzbergRsException`. This caused compilation errors `unreported exception java.lang.Exception; must be caught or declared to be thrown` in kreuzberg v5.0.0-rc.3 CI (java e2e job 77885220931). The fix wraps each Bridge call in a try-catch block that catches `Exception e` and re-throws as `throw new KreuzbergRsException(e.getMessage(), e)` to match the method's declared exception type. This preserves exception context while converting checked exceptions to the expected KreuzbergRsException. (`src/backends/java/gen_bindings/facade.rs`)

- **alef ruby-e2e-codegen: emit super-trait methods and nonzero dimensions on test stubs.** Generated Ruby test stubs for trait-bridge fixtures (plugin API tests) were missing super-trait methods (`initialize`, `shutdown`, `version`) because the emitter skipped all methods with `has_default_impl = true`. However, the Magnus bridge unconditionally calls these methods on every registered plugin object regardless of whether Rust has a default implementation. This caused 12 test failures in kreuzberg v5.0.0-rc.3 CI (ruby e2e job 77885220965) with `ArgumentError` on `Kreuzberg.register_*(impl)` calls. Additionally, `EmbeddingBackend.dimensions()` was not returning a value, violating validation that requires `dimensions() > 0`. The fixes: (1) emit `initialize` and `shutdown` as no-op methods (returning `nil`) unconditionally when `super_trait` is set; (2) emit `version = '1.0.0'` to complete the super-trait API; (3) special-case `EmbeddingBackend` to emit `dimensions = 384` if not already present in the method list. Snapshot test updated, new test `test_embedding_backend_dimensions` added. This mirrors analogous fixes for Python (v0.19.14) and PHP (v0.19.14). (`src/e2e/codegen/ruby.rs`)

- **alef elixir-e2e-codegen: emit complete plugin trait stubs with super-trait methods and nonzero dimensions.** Generated Elixir test stubs for trait-bridge fixtures (plugin API tests) were missing super-trait methods (`initialize/0`) because the fixture processor only collected the primary trait's methods, not the Plugin super-trait. This caused 6 test failures in kreuzberg v5.0.0-rc.3 CI when Rustler unconditionally called `call_fn0("initialize")` on registered plugin objects during lifecycle events. Additionally, `EmbeddingBackend.dimensions/0` was returning 0 instead of 1, violating backend validation that requires `dimensions() > 0`. The fixes: (1) extend the fixture processor to also collect super-trait methods when present, preventing duplicates; (2) emit `initialize/0` unconditionally when `super_trait` is set in the trait bridge config; (3) special-case `EmbeddingBackend.dimensions` to return 1 instead of 0. Snapshot test added in `tests/e2e_elixir_plugin_trait_stubs.rs`. This mirrors analogous fixes for Python (v0.19.14), C# (v0.19.13), and Node.js. (`src/e2e/codegen/elixir.rs`)

- **alef e2e c: emit `e2e/c/.gitignore` so locally-built binaries aren't committed.** Caused Linux CI `Exec format error` failures when a developer ran `make` on macOS and the resulting Mach-O `run_tests` binary got checked in (the C backend previously emitted Makefile, download_ffi.sh, test_runner.h, main.c, and per-category `test_*.c` files but no `.gitignore`). The emitted `.gitignore` covers `run_tests`, `*.o`, and the `mock_server.stdin`/`mock_server.stdout` named-pipe artifacts created by fixtures that mock HTTP traffic. Generated without an alef-managed header so the file is treated as developer-owned (matching the kotlin_android `.gitignore` pattern). Snapshot test in `tests/e2e_c_gitignore_emission.rs` locks in emission and required entries. (`src/e2e/codegen/c.rs`)

- **alef kotlin e2e codegen: gate the `as List<String>` cast on `contains` / `contains_all` / `not_contains` assertions to actual collection fields.** The earlier "cast receiver in collection-contains assertions to fix type inference" change applied the cast unconditionally, so assertions on plain `String` result fields — e.g. `result.text` on the liter-llm `TranscribeTest` — emitted `(result.text as List<String>).contains("…")` and threw `ClassCastException: String cannot be cast to List` at runtime in kotlin_android CI. The fix introduces `field_is_collection` (true when `field_resolver.is_array` or `is_collection_root` is true for the assertion field on either the alias or the resolved name) and only emits the cast when the field really is a `List<String>`; non-collection fields fall through to `field.contains(value)`, which resolves correctly against `kotlin.String.contains(CharSequence)`. Regression tests in `tests/e2e_kotlin_contains_string_field.rs` cover both branches. (`src/e2e/codegen/kotlin.rs`)

- **alef-swift (trait bridges): add comprehensive test to verify Vec<RendererBox> and sibling trait-box FFI symbols are emitted.** Swift binding compilation was failing with "cannot find `__swift_bridge__$Vec_RendererBox$pop` in scope" errors during kreuzberg v5.0.0-rc.3 CI, despite the Rust glue code correctly declaring the phantom function. Root cause could not be determined from code inspection (the phantom function declarations and implementations were present and correct), so the fix adds a snapshot test (`swift_trait_bridge_vec_symbols.rs`) that validates: (1) every trait bridge emits the phantom declaration in its extern "Rust" block, (2) every trait bridge emits the phantom implementation at module scope, (3) the phantom is specifically emitted for Renderer to prevent regression. The test passes with current alef main code, confirming the phantom logic is intact. Any future regression in phantom emission will be caught immediately by test failure. (`tests/swift_trait_bridge_vec_symbols.rs`

- **alef-php: emit trait-bridge interface files for all bridges, not just visitor-style ones.** PHP e2e tests declare anonymous classes implementing trait-bridge interfaces (e.g. `new class implements \Kreuzberg\DocumentExtractor { … }`), but the PHP backend only generated interface stubs for visitor-style bridges (those with `type_alias` and no `register_fn`). Registration-style bridges (DocumentExtractor, OcrBackend, PostProcessor, EmbeddingBackend, Renderer, Validator) were missing their interface files, causing runtime `Error: Interface "Kreuzberg\{TraitName}" not found` during test setup. The fix extends interface generation to emit files for all trait bridges: visitor-style via `gen_visitor_interface()` as before, and registration-style via a new `gen_registration_interface()` function that includes all trait methods (including super-trait methods from Plugin). Interface filenames changed from `{TraitName}Interface.php` to `{TraitName}.php` for consistency with Java/C# conventions. This resolves all 6 PHP e2e interface-lookup errors in kreuzberg v5.0.0-rc.3 CI. (`src/backends/php/trait_bridge.rs`, `src/backends/php/gen_bindings/mod.rs`, new templates `php_interface_start.jinja`, `php_interface_method.jinja`)

- **alef zig-e2e-codegen: mark unused parameters as discard in stubs; emit `undefined` for enum/struct defaults instead of invalid `.{}`** Zig e2e test stub emitter (emit_test_backend) was generating functions with unused parameters that did not follow Zig's pattern-based parameter discard syntax (leading `_:`). Additionally, ZigDefaults was emitting `.{}` for all Named types (enums and structs alike), which is invalid syntax for enums that need a discriminant. The fixes: (1) change parameter emission from `name: type` to `_: type` to comply with Zig's unused binding convention; (2) change ZigDefaults::Named from `.{}` to `undefined`, which is valid for both stub enums and structs in test contexts. This resolves 50+ compilation errors in generated `e2e/zig/src/plugin_api_test.zig` from kreuzberg v5.0.0-rc.3 CI. (`src/e2e/codegen/zig.rs`, `src/codegen/defaults.rs`)

- **alef-csharp(trait bridges): Bridge.Register() now returns IntPtr instead of void.** C# e2e test generator emits `var result = RendererBridge.Register(new TestStub_...())` to capture the native handle for plugin assertions. Previously, the generated Register method returned void, causing `CS0815: Cannot assign void to an implicitly-typed variable` compilation errors on Renderer and Validator trait bridges (along with 6 other errors for other trait bridge registrations). The fix changes the return type of the public `Register(ITraitName impl)` registry method from void to IntPtr, returning the userData handle after successful FFI registration. This allows e2e test stubs to capture and use the registration result without compilation failure. (`src/backends/csharp/templates/trait_registry_class.jinja`)

- **alef python-e2e-codegen: emit `shutdown()` stub in plugin test backends and use non-zero defaults for integer return types.** Generated Python test stubs for trait-bridge fixtures (plugin API tests) were missing `def shutdown(self): pass` because the emitter skipped all methods with `has_default_impl = true`. However, the PyO3 bridge unconditionally calls `call_method0("shutdown")` on every registered plugin object during cleanup regardless of whether Rust has a default implementation. At runtime this produced `AttributeError: 'method 'shutdown' failed'`. Additionally, integer-returning methods (like `dimensions()` on EmbeddingBackend) were returning 0, which violated backend validation that requires `dimensions() > 0`. The fixes: (1) emit `shutdown(self): pass` unconditionally alongside `name(self)` and `initialize(self)` whenever `super_trait` is set; (2) special-case numeric types in stub method emission to return 1 for integers and 0.0 for floats instead of delegating to defaults. This mirrors the Node.js fix in v0.19.13. (`src/e2e/codegen/python/mod.rs`)

- Go E2E test stub emitter now collects both main-trait and super-trait (Plugin)
  methods so stubs implement all required interface members. Previously, stubs
  were missing `Initialize`, `Shutdown` and other super-trait lifecycle methods,
  causing compilation failures in generated `plugin_api_test.go`.

- **alef-e2e-rust: prefix auxiliary metadata unwrap bindings with `_` to suppress unused-variable warnings.** The Rust e2e emitter generated `let metadata_output_format = result.metadata.output_format.as_ref().map(|v| v.to_string()).unwrap_or_default();` (and similar bindings for other optional fields) before assertions. When no assertion actually referenced the binding — because the field was not valid on the result type and the renderer emitted a `// skipped:` comment instead — the binding was dead, causing 4 `unused variable` errors under `-D warnings` in kreuzberg v5.0.0-rc.3 CI (run 26452244660, job 77885220855). Fix: `rust_unwrap_binding` now prefixes both the binding name and the returned local-var identifier with `_`, suppressing the warning while keeping the variable fully accessible when an assertion does reference it. (`src/e2e/field_access.rs`, `tests/e2e_codegen_rust_unused_var.rs`)

## [0.19.18] - 2026-05-26

### Fixed

- Napi streaming-adapter codegen no longer emits a redundant `..Default::default()`
  spread when the synthesized request struct has exactly one field. Clippy's
  `needless_update` lint (denied under `-D warnings` in downstream CI) was
  failing on the generated `Js{Type}Request { field: …, ..Default::default() }`
  literals.

## [0.19.17] - 2026-05-26

### Fixed

- Wasm input DTO codegen now filters out fields excluded from bindings
  (`binding_excluded`). Trait-object and other non-marshalable fields were
  leaking into the generated `{TypeName}Input` struct and producing
  uncompilable `serde_json::from_str` calls in `From` impls.

## [0.19.16] - 2026-05-26

### Fixed

- Auto-exclude fields whose type contains a trait object (`dyn Trait`) from generated bindings. Trait objects cannot be marshaled through serde or constructed from binding languages, so emitting them caused compile failures in downstream backends (swift, dart). This applies regardless of any `#[serde(skip)]` annotation.

## [0.19.15] - 2026-05-26

### Fixed

- **alef setup R: route R install command through the package root (`packages/r/`) instead of the extendr Rust source path.** `package_dir(Language::R)` was falling through to the `[crates.output]` first-priority branch and returning the configured Rust source path (e.g. `packages/r/src/rust/src/`). `Rscript -e "remotes::install_deps()"` then ran from the wrong directory and failed with `cannot open the connection ... 'packages/r/src/rust/src/DESCRIPTION' ... probable reason 'No such file or directory'`, because DESCRIPTION lives at the R package root. The fix adds `Language::R` to the source-output-override exclusion list in `package_dir`, alongside Node/Wasm/Elixir/Ruby/Java/Swift/Zig/Dart/Kotlin/KotlinAndroid which all suffer the same root-vs-source-tree mismatch. A regression test (`package_dir_r_ignores_source_output_override`) locks in the behavior. (`src/core/config/resolved/lookups.rs`)

- **alef go e2e test-apps: use `go mod vendor` + `go generate` to populate cgo `.lib/` directories from downloaded FFI tarballs.** Published Go modules with cgo bindings include `.lib/<platform>/libts_pack_core_ffi.{so,dylib,dll}` artifacts which `binding.go` references via `${SRCDIR}/.lib/<platform>/...`. After `go mod download`, the module sits in the read-only Go module cache (`~/go/pkg/mod/...`), and the `go:generate` directive (`//go:generate go run ./cmd/download_ffi`) cannot write into it. The fix chains `go mod tidy && go mod vendor && go generate ./vendor/<module-path>/... && go test -mod=vendor` so `go generate` runs against a writable vendored copy and populates `.lib/` before linking. This matches the Go ecosystem norm for cgo with bundled natives. The change is transparent to consumers who already use `go mod vendor`. (`src/core/config/test_apps_run_defaults.rs`)

- **alef setup Ruby: split `--add-checksums` into a tolerant `bundle lock` step so older bundlers don't fail the whole setup.** Embedding `--add-checksums` in `bundle install` made the entire setup non-recoverable on bundler versions that don't recognise the flag — older bundlers (<2.5.0) and some 2.7+ revisions print `Unknown switches "--add-checksums"` and exit non-zero. The GH-runner-default bundler on Ubuntu 24.04 ARM64 falls into that gap. The fix runs `bundle install` first (always supported), then `bundle lock --add-checksums 2>/dev/null || true` as a separate, tolerant pass that still populates the CHECKSUMS section when supported but no-ops cleanly when not. The bundler-4.x frozen-mode protection is preserved on supported bundlers without breaking anyone else. (`src/core/config/setup_defaults.rs`)

- **alef all: write `.public_api` generation hashes so `alef verify` does not report stale public-api wrappers after a full regen (matches the pattern already used in the per-lang bindings codepath).** (`src/main.rs`)

- **alef e2e/{node,wasm} codegen: mark `pnpm-workspace.yaml` as alef-owned so the cleanup pass can sweep stale Registry-mode emits when consumers flip to Local mode.** The typescript node generator conditionally emits an empty `pnpm-workspace.yaml` (with `packages: []`) only in Registry mode — in Local mode the test app depends on the binding via `workspace:*`, which can only resolve through the consumer's root pnpm workspace, so the empty marker would shadow it. However, the file was emitted with `generated_header: false`, which means the orphan-file cleanup pass (which only deletes files carrying the `alef:hash:` header) skipped it. Consumers who flipped `[crates.e2e.dep_mode]` from Registry → Local kept the stale empty marker on disk, breaking `cd e2e/node && pnpm install` with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` because the nested workspace claims no members. Setting `generated_header: true` lets the cleanup pass recognize and remove the file when the codegen stops emitting it. The wasm codegen's `pnpm-workspace.yaml` (which always emits with `packages: - "."`) gets the same treatment for symmetry. YAML's `#` comments are preserved by pnpm's parser. (`src/e2e/codegen/typescript/mod.rs`, `src/e2e/codegen/wasm.rs`)

## [0.19.14] - 2026-05-26

### Fixed

- **alef homebrew e2e codegen: remove unused `CLI_FORMULA_QUALIFIED` variable from `run_tests.sh`.** The generated `run_tests.sh` declared `CLI_FORMULA_QUALIFIED="$TAP/$CLI_FORMULA"` alongside its FFI counterpart, but only `FFI_FORMULA_QUALIFIED` was actually used downstream — CLI invocations use the short `$CLI_FORMULA` since the installed `brew` symlink resolves to it directly. `shellcheck` flagged the unused variable as SC2034, breaking the consumer's pre-commit run. Now only the FFI fully-qualified name is emitted, with the comment clarifying why CLI doesn't need the same. (`src/e2e/codegen/homebrew.rs`)

## [0.19.13] - 2026-05-26

### Fixed

- **alef kotlin-android e2e codegen: infer actual config class name from type_defs for optional json_object args.** When a fixture had no config value for an optional argument like `config`, the emitter inferred the type from the argument name via `arg.name.to_upper_camel_case()`, producing `Config()` instead of `ExtractionConfig()`. The fix checks the `type_defs` passed to the emitter, looking for `ExtractionConfig` or the fully qualified type (e.g., `EmbeddingConfig`) before falling back to simple name derivation. For `EmbeddingConfig`, which has a required `model` parameter, the emitter now provides a sensible default: `EmbeddingConfig(model = EmbeddingModelType.Preset(name = "balanced"))`. (`src/e2e/codegen/kotlin.rs:1630–1661`)

- **alef kotlin-android e2e codegen: use `.size` instead of `.length` for ByteArray assertions on simple result types.** When a fixture's result type is `ByteArray` (simple result, not a struct), assertions for `min_length` and `max_length` incorrectly emitted `.length` (which is valid for `String` only). Kotlin's `ByteArray.length` is a property, but the test called `.length` as if accessing it on a type without that property, failing with `unresolved reference`. The fix checks whether the result is simple and the field is the raw result; if so, uses `.size` (valid for `ByteArray` collections) instead of `.length`. (`src/e2e/codegen/kotlin.rs:2227–2246`)

- **alef dart codegen: emit RID-aware loader in `frb_generated.dart` `_alefResolveExternalLibrary`.** The native-library loader now computes the runtime identifier (RID) from `Platform.operatingSystem` and `Platform.version` architecture detection, then checks `lib/src/native/<rid>/` (e.g., `lib/src/native/linux-x64/`) _before_ falling back to the legacy flat `lib/src/<module>_bridge_generated/` path. This allows pub.dev tarballs built with per-platform matrix jobs to ship architecture-specific natives in RID-keyed subdirectories, while maintaining backward compatibility for dev builds and packages without per-RID structure. RID variants supported: `linux-x64`, `linux-arm64`, `macos-x64`, `macos-arm64`, `windows-x64`. (`src/backends/dart/frb_rewrite.rs`)

- **alef binding-helpers: emit `.as_ref()` instead of `.as_deref()` for `Option<HashMap<_,_>>` call arguments.** The `gen_call_args` and `gen_call_args_with_let_bindings` helpers fell through to the `_ =>` catch-all branch for `TypeRef::Map`, which emitted `name.as_deref()` for optional reference parameters. `HashMap` does not implement `Deref`, so the generated code failed with `E0599: the method 'as_deref' ... doesn't satisfy 'Deref'`. The fix adds an explicit `TypeRef::Map(_, _)` arm in both catch-all branches that emits `.as_ref()` (yielding `Option<&HashMap<_,_>>`). (`src/codegen/generators/binding_helpers.rs`)

- **alef dart codegen: emit `()` for unit-variant defaults on freezed sealed-class enums.** `render_enum_variant_default` emitted `EnumName.variant` (no call parens) for any variant whose `fields` list was empty. For `@freezed sealed class` enums (where any variant carries data), every variant — including unit ones — is a `const factory` constructor and requires `()` to be invoked. Without parens the expression is a function tear-off (`OutputFormat Function()`) rather than an `OutputFormat` value, causing a Dart type error. The fix checks whether ALL variants of the enum are unit variants (flat Dart `enum`); only that case omits parens. When any variant has fields (freezed sealed class), all variants emit `variant()`. (`src/backends/dart/gen_bindings/functions.rs`)

- **alef swift scaffold/generate: include `RustStr` typedef in the `RustBridgeC.h` placeholder.** The committed placeholder `RustBridgeC.h` contained only guard macros and a comment; `SwiftBridgeCore.swift` references `extension RustStr` which requires the type to be in scope. When the C header was empty, the Swift compiler reported "cannot find type 'RustStr' in scope" at every `extension RustStr` block. The scaffold placeholder now includes the canonical `typedef struct RustStr { uint8_t* const start; uintptr_t len; } RustStr;` declaration. The `generate` command also upgrades existing old-form (RustStr-less) placeholders on disk when no swift-bridge build output is present. (`src/scaffold/languages/swift.rs`, `src/backends/swift/gen_bindings/mod.rs`)

- **alef elixir scaffold: always include `:jason` in mix.exs deps for Elixir bindings.** The mix.exs generator was gating `:jason` dependency on the presence of visitor bridges (trait bridges in rustler mode). However, generated data-class modules (pack_config.ex, code_chunk.ex, process_config.ex, etc.) unconditionally call `Jason.encode!` and `Jason.decode!` for JSON serialization, so `:jason` must always be present when any Elixir binding is generated. The fix removes the trait-bridge gate and unconditionally emits `{:jason, ...}` in the deps list. This resolves compilation failures with `mix compile --warnings-as-errors` reporting "Jason.decode/1 is undefined (module Jason is not available)". (`src/scaffold/languages/elixir.rs`)

- **alef csharp codegen (trait bridges): use per-binding exception class instead of hardcoded `KreuzbergException`.** The C# register/unregister facade methods emitted for trait bridges were hardcoded to throw `KreuzbergException` — a stale reference to the kreuzberg core lib's exception class. Bindings for other crates (e.g. `HtmlToMarkdownRsException` in html-to-markdown) fail to compile with `CS0246: The type or namespace name 'KreuzbergException' could not be found`. The fix threads the per-binding `exception_name` (already in scope in `gen_wrapper_class`) into the two emission sites, matching the rest of the C# backend's exception-name derivation. (`src/backends/csharp/gen_bindings/methods.rs`)

- **alef go backend (download_ffi): fix arch name mismatch between cgo LDFLAGS directories and release asset URLs.** `cmd_download_ffi_main.go.jinja` was constructing asset URLs using `runtime.GOARCH` directly (e.g. `linux-amd64.tar.gz`, `linux-arm64.tar.gz`), but `alef publish package --lang go` names tarballs with its own platform labels (`linux-x86_64.tar.gz`, `linux-aarch64.tar.gz`). Only macOS arm64 happened to match. The fix maps Go arch names to alef arch names in the download URL only: `amd64→x86_64` everywhere, `arm64→aarch64` on non-Darwin. The local `.lib/<os>-<goarch>/` directory names are unchanged (they match the `#cgo LDFLAGS` in the generated binding). Also removed the unused `regexp` import. (`src/backends/go/templates/cmd_download_ffi_main.go.jinja`)

- **alef kotlin e2e codegen: gate streaming virtual fields on `is_streaming` flag to fix chunk-count assertions in non-streaming fixtures.** The Kotlin codegen intercepted `chunks`-field assertions in both streaming and non-streaming contexts via `is_streaming_virtual_field()`, but the virtual field accessor unconditionally resolved `chunks` as a local variable (populated only in streaming mode), causing unresolved reference errors in non-streaming fixtures. For example, `assertTrue(chunks.size >= 2)` was emitted instead of `assertTrue(result.chunks.size >= 2)` on non-streaming test functions. The fix adds the `is_streaming &&` gate before the streaming virtual field interception, matching the Java codegen; non-streaming fixtures now fall through to the normal field resolver and correctly emit `result.chunks`. Verified: regenerated Kotlin Android test suite compiles with zero unresolved-reference errors. (`src/e2e/codegen/kotlin.rs`)

- **alef ruby scaffold: add `[package.metadata.cargo-machete] ignored` section to generated Cargo.toml.** `scaffold_ruby_cargo` emitted `ahash`, `tokio`, and `async-trait` as Cargo deps when needed by the generated Magnus wrapper, but did not include the matching `[package.metadata.cargo-machete] ignored = [...]` table. The `cargo-machete` pre-commit hook flagged these entries as unused (they are referenced only transitively through the Rust core, not directly in the binding crate). The fix mirrors the Elixir scaffold pattern: after sorting `[dependencies]`, build a `machete_ignored` list from the same conditions used to add the deps and emit the `[package.metadata.cargo-machete]` section when non-empty. (`src/scaffold/languages/ruby.rs`)

- **alef java facade codegen: deduplicate trait-bridge wrapper methods already emitted via `api.functions`.** The `gen_facade_class` function emitted static facade methods for every `register_fn`/`unregister_fn`/`clear_fn` in `config.trait_bridges` without checking whether the same function name was already emitted from the `api.functions` loop. When those helpers are also exported as top-level Rust API functions (as in kreuzberg, where `clear_ocr_backends`, `register_renderer`, etc. are part of the public surface), the facade class ended up with two identically-named static methods — e.g. `clearOcrBackends()` at lines 308 and 491 — causing `method clearOcrBackends() is already defined in class dev.kreuzberg.Kreuzberg`. The fix builds a `HashSet<String>` of names already emitted from `api.functions` and skips any trait-bridge wrapper whose Java name collides. (`src/backends/java/gen_bindings/facade.rs`)

- **alef kotlin e2e codegen: cast receiver in collection-contains assertions to fix type inference.** Collection assertions (contains, contains_all, not_contains) on `List<String>` fields now emit `(receiver as List<String>).contains(...)` instead of `receiver.contains(...)`. Kotlin's `@OnlyInputTypes` annotation on `List.contains()` refuses inference when the type parameter `T` is not fully constrained at the call site, causing compile error "Type inference failed. The value of the type parameter 'T' must be mentioned in input types" on lines with `result.structure.contains("Function")`. The cast fixes type inference for both streaming (result field) and non-streaming (intermediate fields) assertion paths. (`src/e2e/codegen/kotlin.rs`)

- **alef rust e2e codegen: fix unused-variable warning for `result_is_simple` calls whose field is not in `result_fields`.** When a call sets `result_is_simple = true` but the global `result_fields` set does not contain the fixture's assertion field (e.g. field `"result"` on `detect_mime_type_from_bytes`), the assertion renderer prematurely skipped the assertion with a `// skipped: field 'result' not available on result type` comment, yet still bound the return value to a named `result` variable. Under `-D warnings` this is a hard error. The fix adds `!result_is_simple &&` to the skip guard in `render_assertion`: for `result_is_simple` calls, `field_access` maps every field to `effective_result_var` regardless of name, so the skip must not fire. (`src/e2e/codegen/rust/assertions.rs`)

- **alef python-e2e-codegen: emit `initialize()` stub in plugin test backends.** Generated Python test stubs for trait-bridge fixtures (plugin API tests) were missing `def initialize(self): pass` because the emitter skipped all methods with `has_default_impl = true`. However, the PyO3 bridge unconditionally calls `call_method0("initialize")` on every registered plugin object regardless of whether Rust has a default implementation. At runtime this produced `AttributeError: 'method 'initialize' failed'`. The fix emits `initialize(self)` unconditionally alongside `name(self)` whenever `super_trait` is set in the trait bridge config. (`src/e2e/codegen/python/mod.rs`)

- **alef php-e2e-codegen: qualify interface name with binding namespace in anonymous class stubs.** Generated PHP test stubs emitted `new class implements DocumentExtractor` with an unqualified interface name. In a test file under `namespace Kreuzberg\E2e`, PHP resolves this as `Kreuzberg\E2e\DocumentExtractor`, which does not exist. The real interface lives in `\Kreuzberg\DocumentExtractor`. The fix adds a namespace-aware `emit_test_backend_with_ns` variant and calls it from the PHP e2e renderer (which has the binding namespace in scope), emitting `new class implements \Kreuzberg\DocumentExtractor` instead. (`src/e2e/codegen/php.rs`)

- **alef csharp-e2e-codegen: use `Bridge.Register(impl)` instead of `Bridge.RegisterXxx(impl)`.** Generated C# test stubs called `DocumentExtractorBridge.RegisterDocumentExtractor(new ...)` (derived via `reg_fn.to_upper_camel_case()`), but that overload returns `IntPtr` (the low-level FFI handle) and is not the public registration API. The correct high-level factory is `DocumentExtractorBridge.Register(IDocumentExtractor impl)`, which performs the FFI call and discards the handle. Compilation failed with `CS0117: 'DocumentExtractorBridge' does not contain a definition for 'RegisterDocumentExtractor'`. The fix always emits `{Trait}Bridge.Register(new {Stub}())` regardless of the `register_fn` config field. (`src/e2e/codegen/csharp.rs`)

- **alef elixir binding emitter: expose `unregister_*` delegates in the public wrapper module.** All trait-bridge function names (register, unregister, clear) were added to `exclude_functions` to prevent duplication with the trait-bridge generator. This was correct for register and clear (which have dedicated bridge handlers), but it accidentally suppressed the unregister delegates, leaving `Kreuzberg.unregister_document_extractor/1` and siblings undefined. E2e tests calling these functions received `undefined or private` warnings. The fix adds an explicit loop in `generate_public_api` that emits a `def unregister_*(name)` wrapper for every trait bridge's `unregister_fn`, delegating to `Kreuzberg.Native.unregister_*(name)`. (`src/backends/rustler/gen_bindings/mod.rs`)

- **alef go e2e codegen: add empirical `json.` detection to fix missing `encoding/json` import.** When a `test_backend` argument emits a setup block containing `json.RawMessage` method signatures, the up-front `needs_json` heuristic (which only scanned `handle` and `json_object` call args) did not detect the need for the import. The generated file compiled with `undefined: json`. The fix extends the empirical-detection pass (already used for `assert.` and `strings.`) to also scan the rendered body for `json.` usage, shadowing the pre-computed `needs_json` flag at the point of import emission. (`src/e2e/codegen/go.rs`)

- **alef zig codegen: fix undeclared-type errors in e2e stubs + update `callconv(.c)` for Zig 0.16+.** Two independent issues: (1) `zig_type_for_stub` mapped `TypeRef::Named` to a bare name (e.g. `ExtractionConfig`), but these types live in the binding module — the generated stubs failed with `undeclared identifier`. Named types now emit `lib.<name>` so the caller's `replace("lib.", "<module>.")` substitution qualifies them (e.g. `kreuzberg.ExtractionConfig`). (2) The Zig binding backend (trait_bridge.rs + 14 Jinja templates) emitted `callconv(.C)` (uppercase), which Zig 0.16+ renamed to `callconv(.c)` (lowercase). All occurrences updated. (`src/e2e/codegen/zig.rs`, `src/backends/zig/trait_bridge.rs`, `src/backends/zig/templates/*.jinja`)

## [0.19.12] - 2026-05-26

### Fixed

- **alef java-codegen (trait bridges): clear method names now emit plural form matching Rust function name.** The bridge clear method was emitting `clearAll{Trait}()` (e.g., `clearAllRenderer()`) using a hardcoded template, but the generated public facade and FFI layer expected the plural form derived from the `clear_fn` configuration (e.g., `clearRenderers` from `clear_renderers`). Java compilation failed with "cannot find symbol: method clearRenderers()" when calling the bridge's non-existent singular method. The fix converts `clear_fn` (e.g., `clear_renderers`) to the correct method name by: (1) stripping the `clear_` prefix, (2) splitting on underscores, (3) PascalCasing each word, (4) prepending `clear`, yielding the plural form expected by the facade. Test cases updated to verify all trait families emit the correct method names. (`src/backends/java/gen_bindings/trait_bridge.rs`, `src/backends/java/templates/bridge_clear_method.jinja`)

- **alef scaffold (ruby): fix format string escaping in gemspec.** A recent fix to exclude native build artifacts (`target/`, `tmp/`) from the distributed gem inadvertently left a Ruby block syntax `{ |f| ... }` unescaped inside a Rust format string, causing compilation to fail with "invalid format string: expected `}`, found `|`". The block syntax is now double-braced `{{ |f| ... }}` to bypass the format parser, rendering correctly as single-braced in the generated output. (`src/scaffold/languages/ruby.rs`)

- **alef setup defaults: bump per-language setup timeout from 600s → 1800s.** Cold-runner Ruby (`bundle install --add-checksums`) and R (`pak::pkg_install`) setups regularly exceed 10 minutes when compiling native gems / source packages on shared-CI ARM64 hardware, causing `alef validate` / `alef setup` to abort with `setup for ruby timed out after 600s`. 30 minutes accommodates worst-case cold compiles while still bounding hung processes. Both the per-language tables in `setup_defaults.rs` and the global serde default in `output.rs` are bumped. (`src/core/config/setup_defaults.rs`, `src/core/config/output.rs`)

- **alef dart codegen (facade defaults): empty `Vec<integer/float>` defaults now emit the matching typed-list constructor with the correct import source.** `zero_value_for_type` previously emitted `[]` for every `Vec<_>`, which the FRB-generated constructor rejected when the field was typed as `Uint8List`/`Int64List`/`Float64List`. Alef widens every integer to `i64` and every float to `f64` in the FRB-facing mirror (`gen_rust_crate/mirror.rs`), so FRB lands on `Int64List`/`Float64List` regardless of the original Rust width — `Vec<u8>` is the lone exception (kept narrow, maps to `Uint8List`). Empty defaults now follow that mapping: `Vec<u8>` → `Uint8List(0)`, `Vec<integer>` → `Int64List(0)`, `Vec<f32|f64>` → `Float64List(0)`; `Vec<String>`/`Vec<Named>` stays as `[]`. Also fixes `TypeRef::Bytes` default from `[]` to `Uint8List(0)`. Crucially, `Int64List`/`Uint64List` are SHADOWED by FRB (`generalized_typed_data`) — the FRB DTO ctors expect FRB's classes, not `dart:typed_data`'s. `emit_function` therefore imports `package:flutter_rust_bridge/flutter_rust_bridge.dart` whenever a shadowed ctor appears, `dart:typed_data` (with shadowed names hidden when both are present) for the rest. (`src/backends/dart/gen_bindings/functions.rs`)

- **alef dart e2e codegen (handle setup): call positional-optional facade methods with positional syntax.** The dart binding facade emits `createEngine([ConfigType? config])` (positional-optional) for methods that take an optional config struct, but the e2e codegen call site emitted `createEngine(config: cfg)` — Dart raised "No named parameter 'config'" and failed to load 29 test files. Now emits `createEngine(cfg)` matching the facade contract. (`src/e2e/codegen/dart.rs`)

- **alef scaffold (swift): expand cargo-machete ignored list to include async-trait/serde/serde_json/tokio.** The generated swift Cargo.toml lists these four deps unconditionally for manifest stability across regens, but they are only referenced from alef-emitted code paths that are conditional on the core API surface (ahash-map shims, async streaming adapters, JSON DTO conversions, runtime spawn). Without ignoring them, downstream crates whose API surface does not trigger those paths fail their `cargo machete` lint. Now ignored alongside the pre-existing ahash entry. (`src/backends/swift/gen_rust_crate/cargo.rs`)

- **scaffold (elixir/dart): add `ahash` to cargo-machete ignored list.** `ahash` is conditionally included in Elixir and Dart bindings when any function parameter uses `AHashMap<Cow, _>`, but the generated binding wrappers never directly reference it—it's used only in the Rust core for type field marshalling. Elixir now emits a `[package.metadata.cargo-machete]` section when dependencies require ignoring; Dart extends its existing ignore list to include `ahash`. This fixes CI lint errors flagging `ahash` as unused. (`src/scaffold/languages/elixir.rs`, `src/backends/dart/gen_rust_crate/cargo.rs`)

## [0.19.11] - 2026-05-25

### Fixed

- **alef e2e + publish (zig): registry test-apps now consume the published package's bundled native library.** The registry-mode `build.zig` ignored the dependency declared in `build.zig.zon` and instead referenced the in-tree binding source (`../../packages/zig/src/<module>.zig`) and the Cargo workspace target dir (`../../target/release`), so a standalone `test_apps/zig` build failed at link time with `unable to find dynamic system library '<lib>_ffi'` (those paths do not exist outside the monorepo). Three coordinated fixes: (1) `render_build_zig` emits `b.dependency("<pkg>", .{ ... }).module("<module>")` in Registry mode, importing the published module instead of in-tree paths (Local mode unchanged); (2) `package_zig` rewrites the distributed `build.zig` so the exported module links the prebuilt shared library bundled in the package's own `lib/` + header in `include/` via package-relative `b.path`, and adds those directories to the `build.zig.zon` `.paths` allowlist so a `zig fetch` consumer can resolve them; (3) the in-tree scaffold `build.zig` is unchanged (still links `../../target/release` for local `zig build test`). Verified end-to-end with a synthesized published package consumed across a path dependency. (`src/e2e/codegen/zig.rs`, `src/publish/package/zig.rs`)

- **alef e2e (node): pnpm-workspace.yaml isolation marker now gated on Registry mode.** The TypeScript codegen unconditionally emitted `packages: []\nallowBuilds:\n  esbuild: true\n  tree-sitter: true\n` at the test-app root. That is correct in Registry mode (standalone `test_apps/node/` installing from npm) but broke Local mode (`e2e/node/` with `"@kreuzberg/<crate>": "workspace:*"`): pnpm picks the nearest `pnpm-workspace.yaml` walking up, so the empty marker shadowed the consumer's root workspace and `pnpm install` exited 1 with no matching workspace package. Now only emitted when `dep_mode == Registry`. (`src/e2e/codegen/typescript/mod.rs`)

## [0.19.10] - 2026-05-25

### Added

### Changed

- **kotlin_android test-app switched from instrumented tests to JUnit unit tests.** Removed dependency on Android emulator/device (`adb devices` precondition); tests now run on host JVM via `gradle test`. Tests emit to `src/test/kotlin/` instead of `src/androidTest/kotlin/`. Test dependencies always emitted (both Local and Registry modes). Simplifies local development: plain `gradle` + JDK required, no AVD setup. (`src/e2e/codegen/kotlin_android.rs`, `src/core/config/test_apps_run_defaults.rs`)

- **alef version bumped to 0.19.10** (`Cargo.toml`, `alef.toml`, `src/core/template_versions.rs`).

- **alef production-source project-agnostic guard completed.** Expanded the no-project-special-casing cleanup through generated defaults and formatting follow-ups so production code paths no longer depend on downstream product literals; remaining examples now derive from config or use neutral placeholders.

- **alef doc-comment generalization across C#, Dart, Go backends.** Replaced binding-crate-specific words and example identifiers in inline doc comments with generic phrasing (e.g. `KreuzbergLib` → `the facade`, `lib_name = "sample"` example removed) so the codegen library carries no host-application bias in its source. No behavioural change. (`src/backends/csharp/gen_bindings/methods.rs`, `src/backends/dart/gen_bindings/mod.rs`, `src/backends/go/gen_bindings/mod.rs`)

- **alef php-ext codegen: derive package name from `config.name` instead of hardcoded `kreuzberg-dev/html-to-markdown-ext`.** Same generalization pass as the homebrew default change. (`src/e2e/codegen/php_ext.rs`)

- **alef homebrew codegen: thread per-crate FFI metadata (header, lib name, prefix) into the run-tests template.** The `render_run_tests` helper now receives `ffi_lib_name` so generated tap-install smoke scripts grep for the correct shared-library symbol per host crate instead of a hardcoded name. (`src/e2e/codegen/homebrew.rs`)

### Fixed

- **alef scaffold (kotlin/JVM): publish to Maven Central via the vanniktech plugin instead of bare `maven-publish`.** The scaffolded `packages/kotlin/build.gradle.kts` applied only the vanilla `maven-publish` plugin with a `publishing { publications { } }` block and no repository, signing, or POM metadata, so `gradle publish` had nothing to upload and the artifact never reached Central (the shared `publish-maven-gradle@v1` action's default `publishAndReleaseToMavenCentral` task did not even exist). `scaffold_kotlin_jvm` now applies `com.vanniktech.maven.publish` with `mavenPublishing { configure(KotlinJvm(...)); publishToMavenCentral(); signAllPublications(); coordinates(group, artifactId); pom { ... } }` (version inherited from the top-level `version` so `sync-versions` keeps it current), and drops the stale `kotlin { srcDir(".") }` source root that dragged `build/` into the sources jar (Gradle 9 output-overlap validation error). POM metadata derives from `scaffold_meta` with Kotlin-string escaping that preserves apostrophes in author names. (`src/scaffold/languages/kotlin.rs`)

- **alef e2e (zig): write a stub `build.zig` so generate-time `zig fetch` resolves the hash.** `fetch_zig_hash_from_network` wrote only a stub `build.zig.zon`, but zig 0.16's `zig fetch <url>` aborts with "no build.zig file found, in the current directory or any parent directories" unless a `build.zig` is also present, so the hash silently fell back to `.hash = "TODO"` even after the release asset was published. The scratch dir now also gets a no-op `build.zig`. (`src/e2e/codegen/zig.rs`)

- **alef e2e (elixir): force HTTP/1 on generated `Req` calls.** The shared e2e mock server is a plain HTTP/1.1 server, but the generated Elixir harness used `Req`'s default HTTP/2 negotiation, so every fixture request failed with `%Req.HTTPError{protocol: :http2, reason: :pool_not_available}` (no HTTP/2 Finch pool is started). The Elixir test-client renderer now injects `connect_options: [protocols: [:http1]]` into every emitted `Req` call (`Req.get/post/...` convenience calls and `Req.request/1`), pinning each request to the available HTTP/1 pool. (`src/e2e/codegen/elixir.rs`)

- **alef `test-apps run` (php): forward the published package version to `install.sh`.** The generated PHP `install.sh` takes the package version as its first argument, but the default run command invoked `bash install.sh` with no argument, so the installer aborted before installing the extension. The PHP run-config builder now receives the resolved published version (registry override → base package → workspace `version_from`), strips any composer-style constraint prefix (`^`, `~`, `>=`, …) so PIE gets a concrete tag, and forwards it as `bash install.sh <version>`. When no version is known it falls back to the bare `bash install.sh` (the installer supplies its own generate-time default). (`src/core/config/test_apps_run_defaults.rs`, `src/core/config/resolved/lookups.rs`)

- **alef scaffold (php): add `ahash` to cargo-machete ignored list to suppress false-positive unused warnings.** When the Rust API exposes types with `AHashMap<Cow<'static, str>, _>` fields, alef adds `ahash` to the PHP binding's Cargo.toml as a transitive dependency of the core crate. However, the PHP wrapper code never directly `use`s ahash—it's used only by the Rust core for field marshalling. This caused `cargo machete` to report ahash as unused. The fix adds `ahash` to the `[package.metadata.cargo-machete] ignored` list alongside `tokio` and `async-trait`, which have the same pattern (conditionally included via core, never directly referenced in PHP glue code). (`src/scaffold/languages/php.rs`)

- **C# binding: emit `KreuzbergLib.Register{Trait}(IntPtr handle)` / `Unregister{Trait}(string name)` forwarders.** Test-app trait-bridge plugin-registration tests call `KreuzbergLib.RegisterDocumentExtractor(handle)`, `KreuzbergLib.UnregisterDocumentExtractor(name)`, etc. for all six plugin trait families, but the binding generator was emitting only the `NativeMethods` P/Invoke layer with no managed wrappers. The wrapper-class emitter now iterates `trait_bridges` and emits one `Register{Pascal}` + `Unregister{Pascal}` pair per trait, mirroring the Java/Kotlin pattern. (`src/backends/csharp/gen_bindings/methods.rs`)

- **C# trait registry: collapse register flow, drop redundant try/catch.** The `trait_registry_class.jinja` template wrapped the entire register sequence in a try/catch that re-raised the original exception after disposing the bridge — but the failure-handling branches inside the `try` already did the dispose+free dance themselves, so the outer try/catch was both unreachable (under the documented invariants) and prevented later inline simplifications. Inlines the GCHandle allocation + name resolution + tracker insertion into a flat sequence and removes the dead try wrapper. Behaviour is unchanged for the success path; the (unreachable) failure-cleanup edge is now solely owned by the conditional branches. (`src/backends/csharp/templates/trait_registry_class.jinja`)

- **Swift binding: remove dead `api_has_e2e_helper_types` / `emit_e2e_wrappers` hardcoded path.** The Swift e2e wrapper emitter still carried hardcoded references to legacy extraction helpers (`extractBytes`, `extractFile`, `batchExtract*`, `extractionConfigFromJson`) gated by an `api_has_e2e_helper_types` predicate that — in the new bridge-class-driven flow — never fires for any current crate. Removed both functions and their gating predicate, dropping 241 lines of dead code. The active e2e wrapper layer is now exclusively the bridge-derived one. (`src/backends/swift/gen_bindings/mod.rs`)

- **Kotlin Android: skip host-JVM tests in registry mode.** With registry-mode test apps consuming the published `kreuzberg-android` AAR which ships Android-only native `.so` binaries, host-JVM unit tests of code paths that need JNI fail at `UnsatisfiedLinkError`. The Kotlin Android e2e emitter now skips host-JVM test emission in registry mode and emits an instrumented `androidTest` directory only — Gradle's `test` task short-circuits cleanly when no host-JVM tests exist, and `connectedAndroidTest` runs against an attached device/emulator when present. (`src/e2e/codegen/kotlin_android.rs`)

- **alef codegen: post-agent cleanup pass — generic defaults in Homebrew codegen (`example/tap` instead of hardcoded `kreuzberg-dev/tap`, default formula names derived from `config.name` instead of hardcoded `html-to-markdown`), `php.rs` `strip_version_constraint` uses slice form, scattered cargo fmt / clippy normalizations across csharp, go, swift, php, php_ext, kotlin_android codegen + scaffold modules and one test. No behavioural change beyond the homebrew default generalization.** (`src/e2e/codegen/{csharp,homebrew,kotlin_android,php,php_ext,swift}.rs`, `src/backends/csharp/gen_bindings/methods.rs`, `src/backends/go/gen_bindings/mod.rs`, `src/backends/go/templates/cmd_download_ffi_main.go.jinja`, `src/backends/swift/gen_rust_crate/mod.rs`, `src/scaffold/languages/swift.rs`, `tests/cli_no_project_special_casing.rs`)

- **alef scaffold (php): only emit `ahash` dependency when needed, fix unused dependency warning.** The PHP Cargo.toml generator was unconditionally including `ahash = "0.8"` then ignoring it when the API surface had no `AHashMap<Cow, _>` parameters. This caused `cargo machete` to report unused dependencies in kreuzberg-php and kreuzberg_nif (Elixir). The fix mirrors the Elixir backend: ahash is now added to the dependency block only when `needs_ahash` is true. (`src/scaffold/languages/php.rs`)

- **alef `test-apps run` (kotlin_android): skip when no Android emulator/device is connected.** The published AAR ships Android-only native `.so` binaries; `gradle test` on a host JVM cannot load them and fails at runtime, producing spurious test failures on machines without an emulator/device. The precondition now requires `gradle` + `adb` plus at least one `adb devices` entry in state `device`. When unmet, `alef test-apps run --lang kotlin_android` skips gracefully. (`src/core/config/test_apps_run_defaults.rs`)

- **alef `test-apps` (Homebrew): emit fully-qualified `tap/formula` names in `Brewfile` and `run_tests.sh`.** When a developer machine has more than one tap exporting the same formula short-name (e.g. legacy `kreuzberg/tap` alongside the current `kreuzberg-dev/tap`), `brew bundle install` aborts with `Formulae found in multiple taps … Please use the fully-qualified name`. `HomebrewCodegen` now writes `brew "kreuzberg-dev/tap/html-to-markdown"` and `brew "kreuzberg-dev/tap/libhtml-to-markdown"` in `Brewfile`, and the generated `run_tests.sh` exports `CLI_FORMULA_QUALIFIED` / `FFI_FORMULA_QUALIFIED` and uses the qualified form for `brew --prefix` lookups. CI runners (fresh brew) are unaffected; conflicted developer machines now install cleanly. (`src/e2e/codegen/homebrew.rs`)

- **C# trait bridge stubs: emit sync methods matching interface declarations, use correct return types.** The previous fix for C# test stubs (alef commits 6f13a1e3 + c3fe6e03) was incomplete: trait-bridge interfaces correctly unwrap `async Task<T>` to `T` (C# bridges block on async Rust calls), but test-stub generation was still emitting `async Task<T>` wrappers. Additionally, method return types were mapped incorrectly (e.g., `long` for `usize` instead of `ulong`, and `string` for enums like `OcrBackendType`), and parameter types referenced internal non-exported types like `InternalDocument`. Fixed by: (1) removing all async/Task emission from `emit_csharp_stub_method()` to match interface signatures, (2) adding `OcrBackendType`, `ProcessingStage`, and `OcrConfig` to the public type whitelist in `is_visible_csharp_type()`, (3) mapping `Usize` to `ulong` (not `long`) in `csharp_type_for_stub()` to match the C# backend's standard mapping, and (4) using `csharp_type_for_stub_visible()` for parameter types to exclude non-exported types. Trait bridge interfaces now correctly expose synchronous methods with accurate return types; test stubs properly implement these interfaces without referencing internal types. (`src/e2e/codegen/csharp.rs`, `src/backends/csharp/trait_bridge.rs`)

- **alef `test-apps` (go): `go.mod` pins the actual registry version instead of `v0.0.0`.** In Registry mode, the merged `PackageRef.version` for `go` fell through to the Local-mode `[crates.e2e.packages.go].version` placeholder (`v0.0.0`) when no `[crates.e2e.registry.packages.go].version` was configured, producing a published test-app `go.mod` the Go proxy rejects. Registry mode now prefers `[crates.e2e.registry.packages.go].version` if set, then the crate-wide `resolved_version()` with a `v` prefix, and falls back to the Local-mode placeholder only as a last resort. (`src/e2e/codegen/go.rs`)

- **alef `test-apps` (Kotlin Android): emit full Maven Central coordinates in registry-mode `build.gradle.kts`.** The generator was emitting `testImplementation("kreuzberg:5.0.0-rc.1")` (missing group ID and artifact suffix) instead of `dev.kreuzberg:kreuzberg-android:5.0.0-rc.1`. The fix constructs the Maven coordinate from `naming::aar_group_id()` and `naming::aar_artifact_id()` respecting the `[crates.kotlin_android]` config, then passes it to `render_build_gradle_kotlin_android()`. Repository declarations are already correct in `settings.gradle.kts` via `dependencyResolutionManagement`. Includes regression test `build_gradle_kotlin_android_registry_mode_emits_full_maven_coordinate()`. (`src/e2e/codegen/kotlin_android.rs`)

- **alef scaffold (elixir): include `lib/` in `mix.exs` `files:` whenever any wrapper module exists on disk.** The `lib_populated` gate previously checked only `config.trait_bridges` for non-`OptionsField` entries. Crates without trait bridges that nonetheless emit a wrapper module (`lib/<app>.ex`) had `lib/` excluded from the hex publish file list, so the published gem shipped without the user-facing API surface. The gate now also returns true when `<pkg_dir>/lib/` contains any `.ex` file (recursively). (`src/scaffold/languages/elixir.rs`)

- **alef `test-apps` (node, wasm): `pnpm-workspace.yaml` opts native-build scripts in via `allowBuilds`.** pnpm 11 refuses to silently run postinstall/build scripts and fails the install with `ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: esbuild, tree-sitter` — `vitest` pulls in `esbuild`, the tslp wrapper pulls in `tree-sitter`. The node test app `pnpm-workspace.yaml` now emits `allowBuilds:\n  esbuild: true\n  tree-sitter: true\n` alongside `packages: []`; wasm gets the same block alongside `packages:\n  - "."`. (`src/e2e/codegen/typescript/mod.rs`, `src/e2e/codegen/wasm.rs`)

- **alef-backend-napi: re-enable `generate_public_api` for ESM + CJS interop.** The NAPI backend removed `generate_public_api` in v0.16.32 as it was deemed dead weight (consumers were expected to import directly from the native binding crate). However, when a wrapper package re-exports from a CJS module using `export *`, ESM consumers fail with "does not provide an export named X" because wildcard re-export of CJS modules requires understanding the `module.exports` object at compile time. The fix re-enables `generate_public_api` in NAPI to emit TypeScript-native named re-exports (`export { name } from '@pkg'; export type { Type } from '@pkg'`) that work with both CJS and ESM at the package level. The generated `packages/typescript/src/index.ts` now provides proper TypeScript semantics for ESM consumers via re-export syntax. (`src/backends/napi/gen_bindings/mod.rs`)

- **C# test stubs: emit `Name` and `Version` as properties, not methods.** The trait bridge interface declares `string Name { get; }` and `string Version { get; }` as properties, but test stub generation was emitting them as methods `public string Name()` / `public string Version()`. Fixed by unconditionally emitting properties with literal values (`fixture.id` for name, `"1.0.0"` for version) when `super_trait` is configured. Also fixed test stubs to avoid referencing internal types like `InternalDocument` and `SyncExtractor` by introducing `csharp_type_for_stub_visible()` which replaces non-exported types with `string`. (`src/e2e/codegen/csharp.rs`)

- **alef `test-apps run` (Homebrew): correct run command working directory to `test_apps/{name}`.** The `default_test_apps_run_config_for_name` dispatch matched both `"brew"` and `"homebrew"` registry targets but always emitted `cd test_apps/brew && bash run_tests.sh`. The legacy `BrewCodegen` writes under `test_apps/brew/`, but the newer combined CLI+FFI `HomebrewCodegen` writes under `test_apps/homebrew/`, so `alef test-apps run --lang homebrew` failed with `cd: no such file or directory: test_apps/brew`. Interpolate the matched `name` into the cd path so each Homebrew variant cds into its own subdir. (`src/core/config/test_apps_run_defaults.rs`)

- **alef `test-apps run` (Kotlin Android): correct run command working directory to `test_apps/kotlin_android`.** The default test-app run config emitted `cd test_apps/kotlin` for both `Language::Kotlin` and `Language::KotlinAndroid`, but the code generator writes the Android variant under `kotlin_android/`. This caused `alef test-apps run --lang kotlin_android` to fail with `cd: no such file or directory: test_apps/kotlin`. Split the match arms to use `kotlin/` for `Language::Kotlin` and `kotlin_android/` for `Language::KotlinAndroid`. (`src/core/config/test_apps_run_defaults.rs`)

- `dart` backend: run `dart format` after `patch_published_loader` so FRB regeneration during `cargo check` no longer leaves the file in a state that `dart format` would rewrite (was causing prek-hook churn in consumers).

- **alef `test-apps run`: mock-server build now skips gracefully when no [[bin]] target exists.** For crates with zero HTTP fixtures, alef generates `e2e/rust/Cargo.toml` with no `[[bin]]` section — `test-apps run` used to unconditionally try `cargo build --bin mock-server` and fail with `error: no bin target named 'mock-server'`. The fix adds a `has_mock_server_bin()` check before attempting the build; when the target doesn't exist, the run proceeds without `MOCK_SERVER_URL` env vars. (`src/cli/pipeline/commands.rs`)

- **alef `test-apps` (Python): drop the root `test_apps/python/__init__.py` emission.** Marking `test_apps/python/` as a Python package interfered with `uv sync` installing the published wheel: the editable install of the e2e project picked up the root `__init__.py` and pytest then resolved `import liter_llm` against an empty local namespace, missing the actual `liter_llm/__init__.py` shipped in the wheel — every test collection died with `ImportError: cannot import name 'create_client' from 'liter_llm' (unknown location)`. The conftest + `tests/__init__.py` are sufficient for pytest. (`src/e2e/codegen/python/mod.rs`)

- **alef `test-apps run` (PHP): default run command now calls `bash install.sh` before `composer install`.** Composer cannot satisfy a `type: php-ext` require — its platform resolver consults `php -m` for `ext-<name>`. Before this change the run sequence was `composer install && composer test`, and `composer install` failed with `Root composer.json requires PHP extension ext-liter_llm * but it is missing from your system. Install or enable PHP's liter_llm extension.` The PIE bootstrap script alef already emits (`install.sh`) wasn't being invoked. Run sequence is now `bash install.sh && composer install && composer test`. Also: the alef-emitted `install.sh` bakes the pinned version in at generate time (default `VERSION` derived from `[crates.e2e.registry.packages.php].version`) so callers no longer have to pass it as `$1`; the override is still available as `bash install.sh <version>`. (`src/core/config/test_apps_run_defaults.rs`, `src/e2e/codegen/php.rs`)

- **alef `test-apps run` (Go): default run command now runs `go mod tidy` before `go test`.** Without it, `go test ./...` fails with `missing go.sum entry for module providing package <published-module>` — alef emits a `go.mod` with the published-module require but no `go.sum`. `go mod download` alone only writes `/go.mod` hashes; `go mod tidy` adds the package content hashes `go test` actually checks. `tidy` is idempotent once the sum is complete. Both invocations share the same `GOWORK=off` prefix added in v0.19.9. (`src/core/config/test_apps_run_defaults.rs`)

## [0.19.9] - 2026-05-25

### Fixed

- **alef `test-apps run`: ruby + elixir harnesses now honor a parent-provided `MOCK_SERVER_URL`.** The Ruby `spec_helper.rb` and Elixir `test_helper.exs` generated templates previously always tried to spawn the `e2e/rust/target/release/mock-server` binary, which doesn't exist under `test_apps/`. When `alef test-apps run` exports `MOCK_SERVER_URL` (set since `ee35c8fe`) the harnesses now detect it and skip the spawn entirely — mirroring the go/python/wasm guard added in the same commit. Ruby uses `next if ENV['MOCK_SERVER_URL']` inside the `before(:suite)` hook; Elixir wraps the spawn block in `unless System.get_env("MOCK_SERVER_URL")`. (`src/e2e/codegen/ruby.rs`, `src/e2e/codegen/elixir.rs`)

- **alef `test-apps run` (Go): default run command now prepends `GOWORK=off`.** `cd test_apps/go && go test ./...` failed in consumer repos that carry a `go.work` file — the workspace absorbs `test_apps/go` and `go` rejects the module as a non-member. Prepending `GOWORK=off` isolates the test app from the outer workspace so `go test ./...` always works. (`src/core/config/test_apps_run_defaults.rs`)

- **alef `test-apps run` (WASM): `setup.ts` now initializes the wasm module per vitest worker using `initSync` + `readFileSync`.** Tests failed with `Cannot read properties of undefined (reading '__wbindgen_add_to_stack_pointer')` because the wasm-bindgen module was not initialized in worker processes. Two root causes: (1) vitest worker processes have their own module graph so init done only in `globalSetup` (main process) is invisible to workers; (2) the async default export (`__wbg_init`) uses Node.js `fetch()` which silently rejects `file://` URLs. The fix emits `setup.ts` (vitest `setupFiles`, runs per worker) with `initSync({ module: readFileSync(wasmBinPath) })` — synchronous, no `fetch`, works reliably in every Node.js worker. `setup.ts` is now always emitted for WASM when HTTP fixtures are present (not only when file-path fixtures are used), and `vitest.config.ts` wires `setupFiles` accordingly. (`src/e2e/codegen/wasm.rs`, `src/e2e/templates/wasm/globalSetup.ts.jinja`)

- **`alef sync-versions`: rewrite e2e manifest version pins on every version bump.** Previously `sync-versions` updated manifests under `packages/<lang>/` and `test_apps/<lang>/` but skipped the generated integration test trees under `e2e/<lang>/`. This caused CI failures on the first run after a bump: Java's `e2e/java/pom.xml` held a stale `<version>` and `<systemPath>` for the system-scope JAR dependency (Maven could not resolve it), and Ruby's `e2e/ruby/Gemfile.lock` was out of sync with the bumped gem version so `bundle install --frozen` exited 16. The fix adds four targeted rewrites: `sync_e2e_java_pom` (rewrites `<version>` and `<systemPath>` inside system-scope `<dependency>` blocks only, leaving the e2e project's own `<version>` untouched), `e2e/ruby/Gemfile.lock` (reuses the existing `sync_gemfile_lock` helper), `sync_e2e_go_mod` (updates the library module version in the `require` block), and `sync_e2e_dart_pubspec_lock` (updates `version:` under path-source package entries). All four rewrites are idempotent; hosted/third-party version strings are untouched. (`src/cli/pipeline/version.rs`)

## [0.19.8] - 2026-05-25

### Added

- **alef-test-apps (Zig): compute the content multihash at generate-time via `zig fetch`.** Previously `test_apps/zig/build.zig.zon` emitted `.hash = "TODO"` and required a manual one-time `zig fetch --save` to populate. Now the codegen shells out to `zig fetch <URL>` from a scratch dir, parses the printed hash, caches the (URL → hash) tuple in `~/.cache/alef/zig-hashes.json` for offline re-regen, and writes the real hash into the generated `build.zig.zon`. If the asset isn't yet published (pre-release dry-run / offline), falls back to `.hash = "TODO"` with a WARN log. An explicit `[crates.e2e.registry.packages.zig].hash = "..."` override in alef.toml still takes precedence. (`src/e2e/codegen/zig.rs`)
- **alef-cli: new `alef test-apps run` — verify published packages via the registry test apps.** Per-language run commands (config `[crates.e2e.registry.run.<name>]`, defaults in `test_apps_run_defaults`) install each _published_ package into its `test_apps/<name>/` app and exercise it; the command reports pass/skip/fail per target (skip when the toolchain precondition is absent) and exits non-zero on any failure. Covers every binding language plus the string-only `brew` (Homebrew formula) target. Verifies a release end-to-end — e.g. it installs the published Ruby gem from RubyGems and builds its native ext (issue #87). (`src/core/config/{output,e2e,test_apps_run_defaults}.rs`, `src/core/config/resolved/lookups.rs`, `src/cli/pipeline/{commands,helpers}.rs`, `src/main.rs`)

- **alef-cli (`generate`/`all`/`scaffold`): self-sync the alef version pin and the pre-commit hook rev to the running CLI.** `check_alef_toml_version` no longer hard-errors when the running binary is older than `[workspace] alef_version` — it now logs INFO on an upgrade (running newer) and WARN on a downgrade (running older), then `write_alef_toml_version` reconciles the pin to the running version. A new `sync_precommit_alef_rev` rewrites the alef hook `rev:` in the consumer's `.pre-commit-config.yaml` to `v{cli}` (surgical, quote-style-preserving; no-ops on local hooks or an absent file), wired into all three generation commands so the pin and the hook never drift after a regen. (`src/cli/version_pin.rs`, `src/main.rs`)
- **alef-scaffold (Swift): emit a root-level `Package.swift` shim.** SwiftPM resolves `.package(url: "https://github.com/.../X.git", from: "v...")` by fetching the repo root and parsing the `Package.swift` it finds there. The canonical manifest lives in `packages/swift/Package.swift` so in-tree developers can keep using `cd packages/swift && swift build`; the new root shim mirrors the same target layout but with paths and linker `-L` adjusted to the repo root so external SwiftPM URL consumers can resolve the package. The shim omits the testTarget (external consumers don't compile package tests). (`src/scaffold/languages/swift.rs`)

### Changed

### Fixed

- **alef-update: the default Python `update`/`upgrade` commands pass `--no-install-project`.** `uv sync --upgrade` previously built the project, which fails when the uv project dir differs from the maturin build dir (e.g. the extension's `manifest-path` is relative to the deployed package dir, not the uv project dir). A relock only needs to update dependencies; the extension is built separately via `maturin develop`. Matches the Python `install` default, which already skips the build. (`src/core/config/update_defaults.rs`)
- **alef-test-apps (Python): registry pyproject auto-prepends `==` to bare versions.** `render_pyproject` previously interpolated `"{pkg_name}{pkg_version}"` and required the caller to pass a PEP 508 specifier (`"==1.2.3"`, `">=1.2"`). When `[crates.e2e.registry.packages.python].version` in `alef.toml` was a bare version (`"1.4.0-rc.30"`), output was `"liter-llm1.4.0-rc.30"` — an invalid requirement that pip and uv both reject. The renderer now detects whether `pkg_version` starts with a PEP 508 comparator (`==`, `!=`, `>=`, `<=`, `~=`, `===`, `>`, `<`) and prepends `==` when it doesn't; already-qualified specifiers pass through unchanged. The mod.rs fallback no longer pre-applies `==` since normalisation lives in the renderer. (`src/e2e/codegen/python/config.rs`, `src/e2e/codegen/python/mod.rs`)
- **alef-test-apps (PHP): registry composer.json declares `minimum-stability: dev` + `prefer-stable: true`.** Composer's default `minimum-stability: "stable"` rejects pre-release pins (`rc.*`, `beta.*`, `alpha.*`) with the cryptic "found … but these were not loaded, likely because it conflicts with another require." Setting `minimum-stability: dev` allows pre-release resolution; `prefer-stable: true` keeps stable-when-available semantics so the change is benign for non-pre-release pins. Local-mode composer.json is unchanged (no registry version pin, so stability constraints don't apply). (`src/e2e/codegen/php.rs`)
- **alef-test-apps (PHP): registry composer.json + install.sh now follow the canonical `type: php-ext` pattern.** Composer's platform resolver filters `composer require kreuzberg/<pkg>` for `type: php-ext` packages because `php -m` doesn't list the extension until PIE has dropped the .so. Mirror the `asgrim/example-pie-extension` shape: require `ext-<name>: "*"` (platform req) + `php: ">=8.2"`, drop the direct package require, drop `minimum-stability: dev` + `prefer-stable: true` (no longer load-bearing). A new alef-emitted `test_apps/<lang>/install.sh` runs `composer global require php/pie:^1.4` then `pie install <pkg>:<version>` before `composer install` runs in the `verify-install` flow. (`src/e2e/codegen/php.rs`)
- **alef-publish-package (PHP): drop dead `extra.pie.binary` block from composer.json.** PIE doesn't read this field — it keys off `extra.php-ext.download-url-method`. Removing the noise so future-readers don't think it does something. (`src/scaffold/languages/php.rs`)
- **alef-publish-package (PHP): asset filename now includes the `-{Debug}-` segment PIE 1.x expects.** Per <https://github.com/php/pie/blob/1.4.x/docs/extension-maintainers.md#pre-packaged-binary> the canonical asset name is `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{OS}-{Libc}-{Debug}-{TSMode}.{zip|tgz}`. Alef previously omitted `{Debug}`, producing 404s on PIE binary fetch. Release builds always emit `nodebug`. (`src/publish/package/php.rs`)
- **`alef sync-versions`: rewrite `[crates.e2e.registry.packages.*].version` to track workspace version.** Previously `alef sync-versions` left registry package version entries in `alef.toml` at their original values; they had to be bumped by hand each release. Now `sync_registry_package_versions` walks every `[crates.<name>.e2e.registry.packages.<lang>]` table and re-renders the `version` field using the same per-language formatter that the consumer manifest rewrite uses (`to_pep440` for Python, `to_rubygems_prerelease` for Ruby, `to_r_version` for R, identity for all other languages). The constraint prefix (e.g., `>=`, `~>`, `^`, `v`) is preserved from the existing value; entries without a pre-existing `version` field are left untouched. TOML formatting, comments, and key order are all preserved via `toml_edit`. (`src/cli/pipeline/version.rs`)
- **e2e zig codegen: fix the registry asset URL (was `…/<repo>/<crate>/releases/…`, a doubled path segment) to the correct GitHub release URL `<github_repo>/releases/download/…`** so `zig fetch` can pin the published tarball hash. **dart backend: published package now resolves its native lib from the package's own installed location** (absolute path via `Isolate.resolvePackageUri`) instead of the build-tree-relative `rust/target/release/` that isn't shipped in the pub tarball (the `dlopen … relative path not allowed` error). (`src/e2e/codegen/zig.rs`, `src/backends/dart/{frb_rewrite,gen_rust_crate/cargo,gen_bindings/mod}.rs`)
- **alef `test-apps run`: export MOCK_SERVER_URL/MOCK_SERVERS plainly (overwrite), not via the PATH-style append helper.** The append form (`export VAR='val'"${VAR:+:$VAR}"`) corrupted the scalar URL into `http://host:port:http://host:port` → every HTTP-fixture test failed with `getaddrinfo`. Also: e2e kotlin codegen emits the real `dev.<org>:<crate>-kotlin:<version>` Maven coordinate (was a doubled `<org>:<org>:<crate>`). (`src/cli/pipeline/commands.rs`, `src/e2e/codegen/kotlin.rs`)
- **alef `test-apps run` + e2e codegen: make registry test apps runnable against published packages.** (1) `test-apps run` builds + starts the e2e mock-server (`<e2e.output>/rust` `mock-server` binary), captures `MOCK_SERVER_URL`, injects it into every test-app command, and stops it on exit via an RAII guard — so HTTP-fixture apps (go/python/wasm/brew) no longer fail with connection-refused / `MOCK_SERVER_URL is required`. (2) The go/python/wasm harnesses honor a pre-set `MOCK_SERVER_URL` and skip self-spawning the local binary (absent under `test_apps/`). (3) The generated node test app emits its own `pnpm-workspace.yaml` so `pnpm install` installs its devDeps (vitest) locally instead of being absorbed by the consumer's root pnpm workspace. (`src/cli/pipeline/commands.rs`, `src/e2e/codegen/{go.rs,python/config.rs,typescript/}`, `src/e2e/templates/{typescript,wasm}/globalSetup.ts.jinja`)

### Removed

## [0.19.7] - 2026-05-25

### Added

- **alef-e2e-php_ext: new `php_ext` test_app generator for PIE-installed native PHP extensions.** Adds `PhpExtCodegen` (`src/e2e/codegen/php_ext.rs`) that emits `run_tests.sh` (installs via `pie install`), `main.php` (verifies extension loading and calls `html_to_markdown_convert`), and `README.md`. Registry-mode only; local mode emits a stub README. Registered in `all_generators()` and `emit_test_backend` dispatch.

- **alef-e2e-homebrew: new `homebrew` test_app generator for Homebrew formulae.** Adds `HomebrewCodegen` (`src/e2e/codegen/homebrew.rs`) that emits `Brewfile` (tap + CLI + FFI formulae), `run_tests.sh` (installs via `brew bundle`, checks CLI version, pipes HTML through the CLI binary, and compiles/runs `ffi_smoke.c`), `ffi_smoke.c` (minimal C program linking against the FFI formula via `pkg-config`), and `README.md`. Registry-mode only; local mode emits a stub README. Schema: `[crates.e2e.registry.packages.homebrew]` with `tap`, `cli_formula`, `ffi_formula`, `version` fields (three new fields added to `PackageRef`).

- **alef-cli: `alef test-apps generate` subcommand.** Splits registry-mode test_app generation into its own first-class CLI subcommand (`alef test-apps generate`), separate from `alef e2e generate` which is now local-mode only. `alef all` gains a dedicated test-apps stage that runs after the e2e stage; each stage sweeps its own output directory (`e2e/` vs `test_apps/`) so neither can delete the other's files. `alef all --clean` propagates to both stages. `alef e2e generate --registry` now emits a deprecation warning directing users to the new subcommand. (`src/main.rs`)

- **alef-e2e-zig: registry-mode `build.zig.zon` uses remote tarball URL.** In registry mode the zig codegen now emits `.url = "https://github.com/.../releases/download/v{VERSION}/{CRATE}-zig-v{VERSION}.tar.gz"` instead of a local `.path` dependency. When `hash` is set under `[crates.e2e.registry.packages.zig]` in alef.toml, it is emitted as `.hash = "..."`. When absent, a placeholder comment directs the developer to run `zig fetch --save <url>` to populate it. A new optional `hash` field has been added to `PackageRef`. (`src/e2e/codegen/zig.rs`, `src/core/config/e2e.rs`)

- **alef-e2e-kotlin_android: registry-mode `build.gradle.kts` consumes published Maven artifact.** In registry mode the kotlin_android codegen now emits `testImplementation("{pkg_name}:{pkg_version}")` and omits the local `sourceSets` wiring (which references workspace-relative paths invalid in a standalone test_app). Local mode behavior is unchanged. (`src/e2e/codegen/kotlin_android.rs`)

- **alef-scaffold: `.gitattributes` now marks the registry-mode test_apps output dir as `linguist-generated=true`.** The scaffold-emitted `.gitattributes` already covered the e2e local-mode output (default `e2e/**`). It now also reads `[e2e.registry].output` (default `test_apps`) and emits a matching `test_apps/** linguist-generated=true` entry, so GitHub collapses the registry-mode test_apps in PR diffs alongside the local-mode e2e tree. (`src/scaffold/mod.rs`)

### Changed

### Fixed

- **alef-swift-bridge: skip the module-scope phantom impl for inbound traits and fix the doubled `Box` suffix.** The Swift backend's `gen_rust_crate::plugin_inbound::render_trait_phantom` was passing the already-suffixed `Swift{Trait}Box` into `trait_phantom_fn.jinja`, which appends an unconditional `Box`. Result: `Vec<Swift{Trait}BoxBox>` references in the bridge module that made swift-bridge-build bail with "Type must be declared with `type >`". The matching `trait_phantom_impl.jinja` render was also producing a module-scope `pub fn ... -> Vec<Swift{Trait}Box>`, which doesn't compile because `Swift{Trait}Box` is an `extern "Swift"` type with no Rust-side counterpart at module scope. Now: the `extern "Rust"` phantom inside the bridge uses the unboxed `Swift{Trait}` prefix (template adds the final `Box`), the snake form is also unique (`swift_html_visitor` vs `html_visitor`) so the symbol doesn't collide with the outbound trait_bridge phantom, and the module-scope `pub fn` is no longer emitted for inbound Swift traits. (`src/backends/swift/gen_rust_crate/plugin_inbound.rs`)

- **alef-java: expose trait-bridge methods on public facade class.** Java bindings were generating trait-bridge register/unregister/clear methods only on individual Bridge classes (e.g., `PostProcessorBridge.unregisterPostProcessor()`) but e2e tests expected them on the main Kreuzberg facade. The emitter now delegates these as convenience static methods on the public class (e.g., `Kreuzberg.unregisterPostProcessor()`) that forward to the Bridge class implementation. Fixes e2e test references to `Kreuzberg.register*()`, `Kreuzberg.unregister*()`, and `Kreuzberg.clear*()`. (`src/backends/java/gen_bindings/facade.rs`, `src/backends/java/templates/facade_class.jinja`)

- **alef-e2e-csharp: collect super-trait methods and emit complete test stubs.** Generated C# e2e test stubs were incomplete because the methods array only contained the main trait's methods, missing those from the super-trait (Plugin). The fixture processor now also collects super-trait methods (name, version, initialize, shutdown) and passes them to the emitter, which now emits ALL interface methods including those with default implementations, ensuring complete stub coverage. Fixes e2e test stubs for DocumentExtractor, EmbeddingBackend, OcrBackend, PostProcessor, Renderer, and Validator trait bridges. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e-kotlin_android: collect super-trait methods and emit complete test stubs.** Generated Kotlin Android e2e test stubs used the same shared fixture processor as Kotlin, which now also collects super-trait methods from the Plugin trait. The emit_test_backend for kotlin_android was already updated to emit all methods including those with defaults, so stubs now have complete interface coverage. (`src/e2e/codegen/kotlin.rs`)

- **alef-e2e-node: test-backend dimensions() returns 1 not 0.** The test-backend emitter for TypeScript/Node was using generic language defaults which return 0 for all integer types. This violated backend validation that requires `dimensions() > 0` for EmbeddingBackend stubs. The emitter now special-cases numeric types to return 1 for integers and 0.0 for floats in test stubs. (`src/e2e/codegen/typescript/mod.rs`)

- **alef-e2e-php: require trait-bridge test stubs to implement the interface.** Generated PHP e2e tests were creating anonymous classes without explicit interface implementation, causing `TypeError: Argument #1 must be of type DocumentExtractor, class@anonymous given` at runtime because the registerDocumentExtractor/registerEmbeddingBackend/etc. functions enforce type hints. The emitter now declares `new class implements InterfaceName { ... }` instead of `new class { ... }`, matching PHP's requirement that objects passed to type-hinted parameters must explicitly implement the declared interface. Fixes PHP e2e test stubs for DocumentExtractor, EmbeddingBackend, OcrBackend, PostProcessor, Renderer, and Validator trait bridges. (`src/e2e/codegen/php.rs`)

- **alef-e2e-go: include `encoding/json` import for json.RawMessage in test stubs.** Generated Go e2e tests used `json.RawMessage` in stub method signatures without importing `encoding/json`, causing undefined identifier errors. The emitter now checks whether any test method parameter or return type requires json.RawMessage (including Named types and Optional<Named>) and automatically adds the import to the generated test file's import block. (`src/e2e/codegen/go.rs`)

- **alef-e2e-zig: use @This() instead of struct name in method receivers inside struct definitions.** Zig prohibits referencing a struct's name inside its own definition, even for pointer types. Generated e2e test stubs were emitting `pub fn method(_: *TestStubName) ...` inside the struct definition, causing `use of undeclared identifier 'TestStubName'` compile errors. The emitter now uses `_: *@This()` (Zig's built-in forward reference) for both super-trait and required methods. (`src/e2e/codegen/zig.rs`)

- **alef-e2e-rust: convert fixture IDs to PascalCase in generated test-backend struct names.** Test-backend stub structures were named `_TestStub_{fixture_id}`, where fixture IDs are snake_case (e.g. `register_embedding_backend_trait_bridge`), resulting in struct names like `_TestStub_register_embedding_backend_trait_bridge` that violate Rust naming conventions and trigger `type X should have an upper camel case name` compiler warnings. The emitter now converts fixture IDs to PascalCase, yielding struct names like `TestStubRegisterEmbeddingBackendTraitBridge` that conform to Rust style and pass `cargo check --tests` without warnings. (`src/e2e/codegen/rust/mod.rs`)

- **alef-php-cargo: fix cargo-machete ignored list to only flag conditional deps.** The generated `kreuzberg-php/Cargo.toml` marked `serde_json`, `tokio`, and `async-trait` as ignored in `[package.metadata.cargo-machete]` even though all three are used by the generated PHP shim code, causing stale-ignored-list warnings. The list now only ignores `ahash` (which is conditionally referenced when the umbrella API exposes `AHashMap<Cow<str>, _>` params) and `futures-util` (conditionally added for streaming adapters). Unconditionally-declared deps that are genuinely used are no longer flagged. (`src/scaffold/languages/php.rs`)

- **alef-swift-rust-crate: fix cargo-machete ignored list to remove unconditionally-used deps.** The generated `packages/swift/rust/Cargo.toml` marked `ahash`, `async-trait`, `serde`, and `tokio` as ignored in `[package.metadata.cargo-machete]` even though `async-trait`, `serde`, and `tokio` are unconditionally declared and used by the generated Swift shim. Only `ahash` is truly conditional (referenced only when the umbrella API exposes `AHashMap<Cow<str>, _>` params). The ignored list now includes only `ahash` and the doc comment is updated accordingly. (`src/backends/swift/gen_rust_crate/cargo.rs`)

- **alef-publish-go: package Go FFI archives with a `-go-` infix instead of `-ffi-`.** `alef publish package --lang go` was emitting `{crate}-ffi-v{version}-{platform}.tar.gz`, the exact same prefix used by the C FFI packager. The two asset families became indistinguishable to downstream `asset-prefix:` probes (e.g. `kreuzberg-dev/actions/check-registry`) and to `verify-release-assets` pattern lists that gate on `*-go-*.tar.gz` vs `*-ffi-*.tar.gz`. h2m's v3.5.0-rc.3 publish hit this — Go archives uploaded fine but `verify-release-assets` failed on `html-to-markdown-rs-go-*.tar.gz` because every Go tarball carried the C-FFI prefix. Renamed to `{crate}-go-v{version}-{platform}.tar.gz`. (`src/publish/package/go.rs`)

### Removed

## [0.19.6] - 2026-05-25

### Added

### Changed

### Fixed

- **alef-e2e-swift: import `FoundationNetworking` on Linux for URLSession-family types.** The generated `TestHelpers.swift` and per-test `Test*.swift` files emitted only `import Foundation`. On swift-corelibs-foundation (Linux), `URLSession`, `URLRequest`, `URLSessionTaskDelegate`, and `HTTPURLResponse` live in the `FoundationNetworking` submodule, so the Swift e2e suite failed to compile on Ubuntu runners with `cannot find type 'URLRequest' in scope` and `URLSession is unavailable: This type has moved to the FoundationNetworking module`. Both emitters in `src/e2e/codegen/swift.rs` now wrap the additional import in `#if canImport(FoundationNetworking)` so Apple platforms (where the submodule is absent) remain unaffected. (`src/e2e/codegen/swift.rs`)

- **alef-format: exclude TOML from the Node `oxfmt` pass so it stops fighting `pyproject-fmt`/`cargo-sort`.** The Node formatter runs `pnpm dlx oxfmt .` from the repo root to normalize JS/TS/JSON/CSS across the package, e2e, and registry-mode test apps — but oxfmt also reformats every `.toml` (collapsing multi-line arrays, stripping the inner-bracket spaces that `pyproject-fmt` adds, e.g. `[ "x" ]` → `["x"]`). On a consumer that runs `pyproject-fmt` (which pins inner-bracket spacing) or `cargo-sort`, those hooks re-add the spacing after `finalize_hashes`, mutating the alef-hash-tracked `pyproject.toml`/`Cargo.toml` and breaking `alef verify`. The oxfmt invocation now appends a `!**/*.toml` exclude glob (the FFI/Ruby formatters already avoid oxfmt-on-TOML for the same reason). (`src/cli/pipeline/format.rs`)

- **alef-lookups: `package_dir(Language::Kotlin)` now returns the package root, not the source output dir.** The JVM Kotlin `[crates.output]` points at the deep source tree (`packages/kotlin/src/main/kotlin/.../`), but the publish manifest (`build.gradle.kts`) lives at the package root. `Language::Kotlin` was missing from the exclusion list that makes such languages fall through to the package-root default (only `KotlinAndroid` was listed), so `package_dir(Kotlin)` returned the source dir — and `sync-versions` (which joins `build.gradle.kts` onto it) silently no-op'd, leaving the Kotlin package version stale after a bump, and the Kotlin packager looked in the wrong place. `Language::Kotlin` is now excluded so it resolves to `packages/kotlin`. (`src/core/config/resolved/lookups.rs`)

- **alef-python-init: de-duplicate exception symbols in the generated `__init__.py` re-export block.** When two or more error enums contribute variants that map to the same Python exception name (e.g. two enums each with a `Validation` variant → `ValidationError`), the `from .exceptions import (...)` block listed each symbol twice, which `ruff check --fix` (F811/I001) rewrites — mutating the alef-hash-tracked file and breaking `alef verify`. The exception import list is now de-duplicated (stable, sorted order) so each symbol is emitted once; `__all__` was already de-duplicated. (`src/backends/pyo3/gen_bindings/errors.rs`)

- **alef-python-scaffold: emit `pyproject-fmt`-canonical `pyproject.toml`.** The generated pyproject was no longer a `pyproject-fmt` no-op: version specifiers carried redundant trailing `.0` segments (`maturin>=1.0,<2.0`, `mypy>=1.19.0`), a blank line before `[dependency-groups]` was missing, and the `[tool.ruff]` `format.*` / `lint.per-file-ignores.*` / `lint.mccabe` keys were out of `pyproject-fmt`'s sort order — so the hook rewrote the file and broke `alef verify`. PEP 440 specifiers are now canonicalized at emission time (renovate-tracked constants keep their human-readable form), the blank line is emitted, and the ruff keys are ordered to match `pyproject-fmt`. A test runs `pyproject-fmt` on the generated file and asserts no diff. (`src/scaffold/languages/python.rs`)

- **alef-php-bindings: emit a blank line after the `<?php` opening tag in generated PHP source files.** The facade, opaque-DTO, and type-stub `.php` files emitted `<?php` immediately followed by the `// auto-generated` header with no blank line, so php-cs-fixer's PSR-12 `blank_line_after_opening_tag` rule inserted one post-write — mutating the alef-hash-tracked file and breaking `alef verify`. The `php_file_header.jinja` template now emits the blank line. A test asserts every generated `.php` file starts with `<?php\n\n` and that php-cs-fixer (@PSR12) is a no-op on the userland `src/` files. (`src/backends/php/templates/php_file_header.jinja`)

- **alef-sync-versions: bump three previously-missed version-bearing files so a version sync leaves a fully version-consistent tree.** `alef sync-versions` (and the full `alef all` pipeline) left three files stale after a version bump: (1) the Kotlin package `build.gradle.kts` top-level `version = "..."` (Gradle `Project.version`) — now bumped via a line-anchored rewrite that skips plugin `version "..."`, `version.set(...)`, and dependency-coordinate constructs; (2) the Elixir NIF crate's committed `Cargo.lock` (shipped in the Hex source tarball) — its local/path-source `[[package]]` entries (sourceless crates) are now rewritten to match the bumped manifests, leaving registry/git entries untouched; (3) the docs API-reference `version-badge` spans in `docs/reference/api-*.md` — `alef docs` already injects the workspace version into the badge, but a `sync-versions`-only bump (the path consumers take) did not regenerate the docs tree, so the badge stayed pinned. All three are discovered generically from config + conventions. (`src/cli/pipeline/version.rs`)

- **alef-scaffold-node: separate `packageName` from `binaryName` in NAPI platform-dispatch index.** The generated `index.js` used `binaryName` for both local `.node` file paths and optional-dep npm package names. For scoped packages (e.g., `@scope/lib`) this caused `requireOptionalDependency` to look up `lib-node-linux-x64-gnu` instead of `@scope/lib-linux-x64-gnu`, silently falling through all targets and returning no binding. The scaffold now passes `package_name` separately, uses `{bin}` for local paths and `{pkg}` for optional-dep names, and emits `"packageName": "{package_name}"` in the `napi` block of the generated `package.json`. (`src/scaffold/languages/node.rs`)

- **alef-bench: add missing `map_is_ahash` and `map_key_is_cow` fields to `ParamDef` initializers in bench fixtures.** The `make_param` helper in five backend emit benchmarks (`backends_dart_emit`, `backends_gleam_emit`, `backends_kotlin_emit`, `backends_swift_emit`, `backends_zig_emit`) did not initialize the two fields added to `ParamDef` in 0.19.5, causing `cargo check --benches` to fail with E0063. (`benches/backends_*_emit.rs`)

### Removed

## [0.19.5] - 2026-05-25

### Fixed

- **alef-wasm-emitter: JSON-deserialize structured sub-config fields in From impl.** When a WASM input DTO field is sanitized to `Option<String>` (e.g., `html_options`, `concurrency`, `cancel_token` — types that aren't directly serializable like `Option<ConversionOptions>`, `Option<ConcurrencyConfig>`, `Option<CancellationToken>`), the generated `From<ExtractionConfigInput> for ExtractionConfig` impl was using `v.into()` which has no `From<String>` impl for the target structured type, producing E0277 type errors. Fixed by detecting sanitized String fields in `dto_field_conversion` and emitting `serde_json::from_str(&v).unwrap_or_default()` instead of `.into()`. Both the template context generation and the conversion function were updated to pass the `sanitized` flag. A test was added to `src/backends/wasm/gen_bindings/functions.rs` asserting that sanitized String fields use JSON deserialization. Fixes compilation of `crates/kreuzberg-wasm` when `ExtractionConfig` has non-serializable config sub-types. (`src/backends/wasm/gen_bindings/functions.rs`)

- **alef-swift-bridge: restore JSON deserialization step in pre-call AHashMap binding for `Cow<'static, str>` key map params.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, the generated swift-bridge shim receives the parameter as `Option<String>` (JSON-bridged). A regression in the pre-call binding emitter caused the code to call `.into_iter()` directly on the JSON string (`m.into_iter()...`), which fails to type-check because `String` does not have an `into_iter` that yields `(k, v)` tuples. Fixed in `emit_function_shim` (in `src/backends/swift/gen_rust_crate/shims.rs`) by restoring the `serde_json::from_str::<std::collections::HashMap<String, String>>(&json_str).expect(...)` deserialization step before the `.into_iter()` call, so the generated code correctly deserializes JSON → HashMap → AHashMap. The binding now reads: `let {bound_name} = {name}.map(|json_str| { let hm = ::serde_json::from_str::<std::collections::HashMap<String, String>>(&json_str).expect("valid JSON for {name}"); hm.into_iter().map(|(k, v)| (std::borrow::Cow::Owned(k), serde_json::Value::String(v))).collect::<ahash::AHashMap<...>>() });`. This regression was introduced by the snapshot re-acceptance in commit 778a82fc. (`src/backends/swift/gen_rust_crate/shims.rs`)

- **alef-emitter: remove stray `>` after `.collect::<Vec<Vec<String>>>()` in non-optional sanitized field conversion.** The non-optional `Vec<Vec<String>>` branch in `field_conversion_from_core` emitted a trailing `>` after the closing `)` of the type-ascribed `.collect()` call, producing `...collect::<Vec<Vec<String>>>()>` which is a Rust syntax error. The optional branch and WASM paths were unaffected. Fixed by removing the stray character from the format string in `src/codegen/conversions/core_to_binding.rs`. A regression test covering this case was added to `tests/backends_napi_gen_bindings_test.rs`.

- **alef-php-bridge: emit pre-call `AHashMap` binding and `ahash = "0.8"` scaffold dep for `Cow<'static, str>` key map params.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, ext-php-rs cannot provide `FromZvalMut` for that type. The PHP wrapper now receives `Option<HashMap<String, String>>` and the emitter generates a `let __<name>_ahash` binding that re-keys `String → Cow::Owned` and re-types values to `serde_json::Value::String` before passing `.as_ref()` to core. Both `gen_php_call_args` and `gen_php_call_args_with_let_bindings` and `gen_php_named_let_bindings` were updated to handle the `map_is_ahash && map_key_is_cow` flag. `gen_php_function_params` maps such params to `Option<HashMap<String, String>>` so ext-php-rs can decode a PHP associative array. The scaffold template now conditionally injects `ahash = "0.8"` when any API function has `map_is_ahash = true`. (`src/backends/php/gen_bindings/helpers.rs`, `src/scaffold/languages/php.rs`)

- **alef-ruby-bridge: emit pre-call `AHashMap` binding and `ahash = "0.8"` scaffold dep for `Cow<'static, str>` key map params.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, the Magnus wrapper receives a Ruby `Hash` decoded as `HashMap<String, String>`. The emitter now generates `magnus_ahash_pre_call_bindings` entries that re-key `String → Cow::Owned` and re-type values to `serde_json::Value::String`, and `magnus_call_args_with_ahash` substitutes the pre-bound variable in the final call args. The scaffold now conditionally injects `ahash = "0.8"` when any API function has `map_is_ahash = true`. (`src/backends/magnus/gen_bindings/functions.rs`, `src/scaffold/languages/ruby.rs`)

- **alef-elixir-bridge: emit pre-call `AHashMap` binding and `ahash = "0.8"` scaffold dep for `Cow<'static, str>` key map params.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, the Rustler NIF receives a BEAM map decoded as `HashMap<String, String>`. The emitter now generates a `let __<name>_ahash` binding in the deser preamble that re-keys `String → Cow::Owned` and re-types values to `serde_json::Value::String` before the core call. Both `gen_nif_function` and `gen_nif_async_function` handle the `map_is_ahash && map_key_is_cow` flag. The scaffold now conditionally injects `ahash = "0.8"` when any API function has `map_is_ahash = true`. (`src/backends/rustler/gen_bindings/functions.rs`, `src/scaffold/languages/elixir.rs`)

- **alef-wasm-emitter: wrap sanitized Vec<Vec<String>> fields with serde_wasm_bindgen::to_value() for JsValue targets.** When a WASM binding has a field that represents `Vec<(String, String)>` (sanitized to `Vec<Vec<String>>` in the IR) and the binding struct field is typed as `JsValue` (due to the nested-vec WASM mapping), the conversion code produces the iterator+collect expression but must serialize it to `JsValue` via `serde_wasm_bindgen::to_value()`. Without wrapping, the E0308 type mismatch error occurs: "expected Option<JsValue>, found Option<Vec<Vec<String>>>". Fixed in `field_conversion_from_core_cfg` by detecting sanitized `Vec<Vec<String>>` when `config.map_uses_jsvalue` is true and wrapping with `serde_wasm_bindgen::to_value(...).ok()` (or `.unwrap_or(JsValue::NULL)` for non-optional). Covers both non-optional `Vec<Vec<String>>` and `Optional<Vec<Vec<String>>>` cases. The fix unblocks `crates/kreuzberg-wasm` compilation, resolving E0308 type errors at the emitted struct field conversion sites. (`src/codegen/conversions/core_to_binding.rs`)

- **alef-wasm-emitter: add explicit type ascription on `.collect::<Vec<Vec<String>>>()` for Vec<(String, String)> field conversions.** When a WASM binding has a field typed as `Vec<(String, String)>` (sanitized to `Vec<Vec<String>>` in the binding), the emitter produced `.map(...).collect()` without an explicit type parameter. Since `.collect()` infers its target type from the assignment context, type inference can fail without the hint. Fixed by adding explicit type ascription: `.collect::<Vec<Vec<String>>>()`. This covers both the non-optional `Vec<Vec<String>>` case and the `Optional<Vec<Vec<String>>>` case (which has a separate code path) in `field_conversion_from_core`. The fix unblocks `crates/kreuzberg-wasm` compilation. (`src/codegen/conversions/core_to_binding.rs`, `src/backends/wasm/gen_bindings/functions.rs` test)

- **alef-wasm-emitter: deduplicate input DTO struct generation across functions sharing the same config type.** When multiple `extract_*` functions took the same config parameter (e.g., both `extract_bytes` and `extract_file` take `ExtractionConfig`), the input DTO struct was emitted twice, triggering E0428 duplicate struct definition and E0119 conflicting trait impl errors. The emitter now pre-computes all required input DTOs across all functions, generates each struct once before the function loop, and passes a dedup set to `gen_function_with_emitted_dtos` so subsequent functions skip already-emitted DTOs. This unblocks `crates/kreuzberg-wasm` compilation on the Rust CI host build. (`src/backends/wasm/gen_bindings/functions.rs`, `src/backends/wasm/gen_bindings/mod.rs`)

- **alef-swift-bridge: emit pre-call `AHashMap` bindings for `Cow<'static, str>` key map params in function shims.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, the swift-bridge wrapper receives a user-friendly `Option<HashMap<String, String>>`. Previously the generated shim passed the `HashMap` directly to the core function, which fails to type-check: `HashMap<String, Value>` does not match `AHashMap<Cow<'static, str>, Value>`. Fixed in `emit_function_shim` and `swift_call_arg`: before the call expression, a `let __<name>_ahash` binding is emitted that re-keys `String → Cow::Owned` and re-types values to `serde_json::Value::String`, constructing an `AHashMap<Cow<'static, str>, Value>`, then the call arg borrows the bound value with `.as_ref()`. The `ahash = "0.8"` dependency is now unconditionally added to the generated Swift bridge Cargo.toml so generated Swift crates can reference `ahash::AHashMap` without manual additions. (`src/backends/swift/gen_rust_crate/shims.rs`, `src/backends/swift/gen_rust_crate/cargo.rs`)

- **alef-dart-bridge: add `ahash = "0.8"` to generated Dart bridge Cargo.toml when any function has an `AHashMap` param.** The pre-call binding emitted by the bridge codegen references `ahash::AHashMap` directly. `api_has_ahash_param` scans the API surface for `map_is_ahash = true` params and, when found, prepends `ahash = "0.8"` to the generated `[dependencies]` block in `packages/<lib>/rust/Cargo.toml`. Without this, the generated Dart crate fails to compile with `error[E0433]: cannot find module or crate ahash`. (`src/backends/dart/gen_rust_crate/cargo.rs`)

- **alef-dart-bridge: fix `AHashMap<Cow<'static, str>, Value>` param conversion in FRB bridge functions.** When a Rust core function takes `Option<&AHashMap<Cow<'static, str>, Value>>`, the Dart FRB bridge receives a user-friendly `Option<HashMap<String, String>>`. Previously the generated bridge fn passed `&metadata` directly, which fails to type-check. Fixed in `emit_bridge_fn`: before the call expression, a `let __<name>_ahash` binding is emitted that re-keys `String → Cow::Owned` and re-types values to `serde_json::Value::String`, then the call arg borrows the bound value with `.as_ref()`. The `ahash` crate (already in kreuzberg-ffi's Cargo.toml) is also referenced in these bridge fns, so `ahash = "0.8"` must be present in the dart bridge crate's dependencies — existing repos that already added it for the earlier conversion are unaffected. (`src/backends/dart/gen_rust_crate/bridge_fn.rs`)

- **alef-ffi-emitter: preserve `AHashMap<Cow<'static, str>, _>` param types across the FFI wrapper boundary.** The FFI emitter normalized all map params to `std::collections::HashMap<String, V>` via `serde_json::from_str::<HashMap<String, V>>(...).unwrap()` and then called `.as_deref()` on the `Option<HashMap>` to pass it as `Option<&HashMap<...>>` — this produced two compile errors in generated code: (1) `HashMap` does not implement `Deref` so `.as_deref()` is not available, and (2) the deserialized `HashMap<String, V>` does not match the core function's expected `&AHashMap<Cow<'static, str>, V>`. Fixed by adding `map_is_ahash: bool` and `map_key_is_cow: bool` fields to `ParamDef` IR, populated in the extractor by inspecting the raw `syn::Type` before type erasure. The FFI emitter now emits `serde_json::from_str::<ahash::AHashMap<std::borrow::Cow<'static, str>, V>>(...)` for `AHashMap`-backed params and uses `.as_ref()` instead of `.as_deref()` for optional `Map` params at call sites. The `ahash` crate is now included as a direct dependency in the FFI scaffold template so generated FFI crates can reference `ahash::AHashMap` without a separate manual addition. (`src/core/ir.rs`, `src/extract/extractor/functions.rs`, `src/backends/ffi/gen_bindings/functions.rs`, `src/scaffold/languages/ffi.rs`)

- **alef-setup-defaults-ruby: append `--add-checksums` to the default Ruby setup command so generated `Gemfile.lock` files carry a sha256 for every gem in the resolved set.** Without the flag, gems already cached on the host land in `Gemfile.lock` with an empty `CHECKSUMS` entry, which bundler 4.x rejects in CI's frozen mode with exit 16 (`Your lockfile has an empty CHECKSUMS entry for "<gem>", but can't be updated because frozen mode is set`). kreuzcrawl rc.26 E2E ruby job 77623604324 hit this on the `ast` gem; downstream repos using `ruby/setup-ruby@v1` with `bundler-cache: true` saw the same break across every refreshed lockfile. The new `bundle install --add-checksums` invocation populates the CHECKSUMS section deterministically so `bundle install --frozen` in CI no longer fails for cosmetic lockfile drift. (`src/core/config/setup_defaults.rs`)

- **alef-scaffold-ffi: version-inject every internal workspace dependency in the generated FFI/umbrella `Cargo.toml`, not just the core dep.** The FFI scaffold built its `[dependencies]` extra-deps block with a bespoke loop that emitted auto-detected internal workspace members (e.g. `{lib}-core`, `{lib}-http`, `{lib}-graphql` from `[crate.extra_dependencies]`) as path-only tables (`{lib}-core = { path = "../{lib}-core" }`). `cargo publish` rejects that with "all dependencies must have a version requirement specified when publishing", breaking crates.io publish of the FFI crate. The scaffold now delegates to the shared `render_extra_deps`, which injects the resolved workspace version into path-only member tables (`{ path = "../{lib}-core", version = "X" }`) — consistent with how the core dep and the swift/dart bridge crates are handled — while leaving external deps (e.g. `anyhow = "1.0"`) and already-versioned tables untouched. Path-only output is preserved when no workspace version resolves (e.g. unit fixtures). (`src/scaffold/languages/ffi.rs`)

- **alef-scaffold-elixir: make the Rustler NIF Hex package self-contained when the NIF source lives outside the package dir.** When `[crate.output] elixir` points at an external source dir (e.g. `crates/{lib}-elixir/src/`), the NIF crate's `[lib] path` resolves there and the generated `lib.rs` plus the `*.ex` modules are written there — NOT into `native/{nif}/src`. The `mix.exs` `files:` list nonetheless hard-coded `native/{nif}/src`, so `mix hex.build` failed with `Missing files: native/{nif}/src`; worse, it shipped only a `{relative}/*.ex` glob, leaving the Rust NIF source out of the tarball so RustlerPrecompiled's source-compile fallback could not build standalone. The `files:` list now lists `native/{nif}/src` only for the default/native layout, and for the external-output layout lists the external source directory itself (covering the Rust `lib.rs`/`*.rs` and the `*.ex` modules together) so the path the `[lib] path` resolves to ships in the tarball. (`src/scaffold/languages/elixir.rs`)

## [0.19.4] - 2026-05-24

### Fixed

- **alef-e2e-codegen-dart: fix emit_test_backend to emit module-level typed stubs matching trait-bridge signatures — Dart does not allow class definitions inside functions, so test stub classes are now collected in a first pass and emitted before `void main()` at the module level. Each method now uses concrete Dart types (Uint8List, String, List<T>, etc.) via `DartMapper` instead of `dynamic`; async methods are typed with `Future<T>`; the fixture's input `"name"` field is preferred as the plugin name over the fixture id; classes extend their corresponding trait abstract classes (DocumentExtractor, OcrBackend, etc.) with proper `@override` signatures. Added fixture name extraction from the fixture input object (e.g., in `register_document_extractor_trait_bridge.json` with `{ "extractor": { "name": "test-extractor" } }`, the generated class correctly names itself "test-extractor"). Fixed indentation handling for multi-line setup blocks emitted inside tests by splitting each embedded newline and applying the test-body indent independently.** (`src/backends/dart/mod.rs`, `src/e2e/codegen/dart.rs`)

- **alef-e2e-codegen-elixir: fix emit_test_backend indentation and plugin name extraction — setup_block lines are no longer pre-indented (the caller adds 6-space test-body indent), defmodule is placed inside unless guard at the correct nesting level, and plugin name is extracted from fixture.input.<arg>.name instead of fixture.input.name to match the fixture structure.** For example, in `register_document_extractor_trait_bridge.json` with input `{ "extractor": { "name": "test-extractor" } }`, the generated stub now correctly emits `def name, do: "test-extractor"` instead of using the fixture id. The `unless Code.ensure_loaded?()` guard prevents "module already exists" warnings on reruns. Updated test fixture in `elixir_stub_uses_fixture_input_name_for_plugin_name` to use the correct nested structure `{ "backend": { "name": "..." } }`. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e-codegen-kotlin_android: fix emit_test_backend to emit typed Kotlin stub matching trait-bridge interface — parameters and return types now use concrete Kotlin types (ByteArray, String, ExtractionConfig, etc.) via KotlinMapper instead of hardcoded `Any`; async methods use `suspend fun` keyword; name() method is emitted only when super_trait is configured; fixture input["name"] field is preferred as the plugin name over fixture id.** Added snapshot tests in `tests/e2e_codegen_kotlin_android_test_backend_snapshot.rs` covering interface implementation, typed parameters, async methods, default-impl skipping, and fixture input name resolution. Made `src/backends/kotlin/type_map.rs::KotlinMapper` public (via `pub(crate)` on the `mod type_map` declaration in `src/backends/kotlin/mod.rs`) so the e2e codegen can reference it. (`src/backends/kotlin/mod.rs`, `src/e2e/codegen/kotlin_android.rs`, `tests/e2e_codegen_kotlin_android_test_backend_snapshot.rs`)

- **alef-sync-versions: also update `pyproject.toml` under `[crates.output] python = "..."` and `output_for("python")` so the maturin-build pyproject that lives alongside the PyO3 source crate (e.g. `crates/{lib}-py/src/pyproject.toml`) is kept in sync with the canonical Cargo.toml version.** Previously `sync_versions` hardcoded only `packages/python/pyproject.toml`, the legacy distribution-manifest path. Repos that configure `[crates.output] python = "crates/{lib}-py/src/"` (e.g. kreuzcrawl) have a second `pyproject.toml` used by maturin for the source-build — that file was never visited, causing version drift that silently passed local hooks but failed the publish CI version-consistency check (kreuzcrawl rc.24 job 77612730306 reported `found: "0.3.0rc23"` against `expected: "0.3.0-rc.24"`). The fix builds a deduplicated candidate list of three paths: (1) the legacy `packages/python/pyproject.toml` for back-compat, (2) `{config.package_dir(Language::Python)}/pyproject.toml` for the configured distribution manifest, and (3) `{config.output_for("python")}/pyproject.toml` for the PyO3 source-crate pyproject. Paths are canonicalized before deduplication so a file is never written twice when multiple candidates resolve to the same location on disk. Missing files are soft-skipped, preserving the existing behaviour for repos that use only a subset of the layouts. (`src/cli/pipeline/version.rs`)

- **alef-validate-versions: normalize both sides of the Python `pyproject.toml` comparison to PEP 440 and fix a doubled-slash path join so prerelease versions round-trip correctly.** `collect_checks` previously normalized only the canonical version (`to_pep440(canonical)`) and compared it against the raw manifest string, so a `pyproject.toml` holding the canonical pre-release form `0.15.6-rc.2` reported a mismatch against the normalized expected `0.15.6rc2` — both displayed identically in the JSON (`expected` is the raw canonical), making the failure look spurious. The Python check now normalizes BOTH the found value and the canonical version via `to_pep440` (idempotent on already-normalized input), so `0.15.6-rc.2` and `0.15.6rc2` compare equal in either direction while final releases round-trip unchanged. Separately, when `[crates.output] python` is a directory with a trailing separator (e.g. `crates/{lib}-py/src/`), the manifest path was built with `format!("{dir}/pyproject.toml")`, yielding `crates/{lib}-py/src//pyproject.toml`; a new `join_manifest` helper trims trailing separators before joining. Together with the sync-versions fix above, both the consumer and source-template pyproject agree after a bump and `alef validate versions` passes for prerelease and final versions alike. (`src/cli/commands/validate_versions.rs`)

- **alef-e2e-codegen-swift: fix emit_test_backend to use typed Swift parameters and return types instead of `Any` — all param and return types are now mapped via `SwiftMapper` for concrete types (Data, String, ExtractionConfig, etc.).** The fixture's input field `"name"` is now preferred as the plugin name over the fixture id; this ensures generated stubs return backend names like `"test-extractor"` instead of internal identifiers. Added two snapshot tests: `swift_stub_uses_typed_params_not_any` verifies params and return types use concrete Swift types, and `swift_stub_uses_fixture_input_name_for_plugin_name` verifies the `name` property comes from fixture input. (`src/backends/swift/mod.rs`, `src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-go: fix three bugs in emit_test_backend — GoDefaults emitted `[]interface{}{}` for Vec (should be typed nil slice), `&TypeName{}` pointer for Named (should be value literal), `map[string]interface{}{}` for Map/Json (should be nil/json.RawMessage(nil)); package-level struct+method receiver declarations were pushed into setup_lines (inside the test function body) instead of being hoisted before the `func Test_*` declaration; and Named/Optional(Named) return types in stub methods used the full type name instead of `json.RawMessage` to match the generated Go interface surface.** Fixed `GoDefaults::emit_default` to emit `([]ElementType)(nil)` for Vec, `TypeName{}` for Named, `nil` for Map, and `json.RawMessage(nil)` for Json. Introduced `package_decls: Vec<String>` as a third return value from `build_args_and_setup`; the test-backend stub block is pushed there and emitted before `func Test_` in `render_test_function`. Added `stub_go_type()` helper that maps `Named(_)` and `Optional(Named(_))` to `json.RawMessage` so param/return types in stub methods match the binding's interface declaration. (`src/e2e/codegen/go.rs`, `src/codegen/defaults.rs`)

- **alef-e2e-codegen-r: fix two bugs in emit_test_backend — arg_expr was wrapped in `{register_fn}(var_name)` causing double-nested call; setup_block was never pushed to setup_lines so the R list definition was missing from the test body.** Changed `arg_expr` to just `var_name` (the R variable holding the stub list) and added `setup_lines: &mut Vec<String>` parameter to `build_args_and_setup` so the stub block is pushed to setup_lines before the function call is assembled. (`src/e2e/codegen/r.rs`)

- **alef-e2e-codegen-csharp: fix two bugs in emit_test_backend — private nested class declaration was pushed to setup_lines (inside the test method body, invalid C#) instead of class_decls (class-scope); pre-baked 8-space indent on setup_block lines caused 12-space double-indentation after the visitor_class_decls emitter adds 4 more.** Added `class_decls: &mut Vec<String>` parameter to `build_args_and_setup`; the test-backend `setup_block` is pushed to `class_decls` (not `setup_lines`) so it is emitted as a private nested class at class scope. Corrected indentation in `emit_csharp_stub_method` (8-space method body → renderer adds 4 = 12 final) and `emit_test_backend` (4-space class declaration → renderer adds 4 = 8 final). (`src/e2e/codegen/csharp.rs`)

- **alef-e2e-codegen-java: fix two bugs in emit_test_backend — all method params and return types were hardcoded as `Object` instead of using JavaMapper for proper Java types; super-trait `name()` method was emitted unconditionally whenever `super_trait` is non-None rather than filtering by `trait_source`.** Introduced `emit_java_stub_method` helper using `java_stub_type()` (which maps Named types to `Object` as opaque handles, uses `java_type()` for all others) and `java_boxed_stub_type()` for `CompletableFuture<T>` return types. Super-trait methods now filter by `m.trait_source == Some(super_trait)` to match only the configured super-trait's methods. (`src/e2e/codegen/java.rs`)

- **alef-e2e-codegen-zig: fix three bugs in emit_test_backend — `lib.make_...` and `lib.register_...` used the literal string `lib` instead of the real module name; `arg_expr` was `_ = lib.register_fn(...)` (a statement) pushed into args_str causing a double-registration call; setup_block lines had pre-baked 4-space indent causing 8-space double-indentation when the caller's render loop added 4 more.** The caller in `build_args_and_setup` now splits `setup_block` by lines (each pushed individually) and replaces `lib.` with the actual module name. `arg_expr` is changed to the multi-argument string `"test", vtable_var, &stub_var, @ptrCast(&out_err_var)` that expands into the registration call's argument list. Setup_block lines use zero pre-existing indent (caller's `"    {line}"` adds 4 = correct test-body indent). Updated snapshot test to verify `arg_expr` contains `vtable_*` and `@ptrCast` instead of `register_test_trait`. (`src/e2e/codegen/zig.rs`)

- **test: add args: Vec::new() to Fixture literals in tests after fixture.args field added.** Commit `1c081fb8` introduced the `args: Vec<ArgMapping>` field to the `Fixture` struct with `#[serde(default)]` for backward-compatible JSON deserialization, but existing `Fixture { ... }` struct literals in `tests/` did not include the new field, causing ~45+ E0063 "missing field 'args' in initializer" compilation errors. Added `args: Vec::new(),` to all affected struct literals, placed adjacent to the `assertions:` field.

- **alef-e2e-codegen-{dart,zig}: spawn the mock-server from the generated harness so HTTP/mock-url fixtures reach the live ephemeral server instead of the `http://localhost:8080` fallback.** The standalone mock-server binds an ephemeral `127.0.0.1` port and prints `MOCK_SERVER_URL=http://127.0.0.1:<port>` (plus an optional `MOCK_SERVERS={...}` JSON line) on stdout once listening; the generated per-language clients read that URL from the environment and fall back to `http://localhost:8080` when it is unset. Python (`conftest.py`), TypeScript/WASM (`globalSetup.ts`), C# (`TestSetup.cs`), PHP (`bootstrap.php`), Go (`main_test.go` `TestMain`), Ruby (`spec_helper.rb`), Java/Kotlin (`MockServerListener` + JUnit SPI), and Elixir (`test_helper.exs`) already spawn the binary and export the URL, but **Dart and Zig generated no spawn at all** — their tests read `MOCK_SERVER_URL` (Dart `Platform.environment`, Zig `std.c.getenv`) which nothing set, so every HTTP fixture failed with connection refused. Dart now spawns the binary in a per-file `setUpAll` (tearing it down in `tearDownAll`; `dart_test.yaml` pins `concurrency: 1` so one server runs at a time) and — because `Platform.environment` is read-only in Dart — holds the captured URL in top-level globals read through `_mockServerUrl()` / `_mockServerFor()` helpers; a pre-set `MOCK_SERVER_URL` short-circuits the spawn. Zig spawns the binary at `build.zig` configure time via `std.process.spawn` (Zig 0.16 I/O model), reads the startup lines, and forwards `MOCK_SERVER_URL` / `MOCK_SERVERS` / `MOCK_SERVER_<FIXTURE_ID>` into each test run step's environment with `setEnvironmentVariable`; the child lives for the duration of `zig build`. Both honour host-root per-fixture URLs (`MOCK_SERVERS`) and skip cleanly when the binary is not built. (`src/e2e/codegen/dart.rs`, `src/e2e/codegen/zig.rs`)

- **alef-core-config: accept `"arg_type"` as a serde alias for `"type"` on `ArgMapping` so JSON fixtures using `"arg_type": "test_backend"` deserialise correctly instead of silently falling back to the default `"string"`.** `alef.toml` writes the field as `type = "..."` (TOML idiom) and serde was renamed accordingly, but per-fixture JSON files (`fixtures/plugin_api/register_*.json`) carry the more explicit `"arg_type"` key — those were silently ignored, so the walker saw `arg_type == "string"` for test_backend args and emitted the input dict literal instead of dispatching to `emit_test_backend`. The added serde alias makes both spellings work. (`src/core/config/e2e.rs`)

- **alef-e2e-fixture: add fixture-level `args` field and introduce `Fixture::resolved_args()` helper to prefer fixture.args over call_config.args when present.** The fixture-level `args` array allows per-fixture argument overrides (e.g., trait-bridge stubs where each fixture may need to construct a different test backend instance). Previously all per-language walkers iterated only `call_config.args`, ignoring fixture-level args. Now every walker calls `fixture.resolved_args(call_config)` which returns `&fixture.args` if non-empty, else `&call_config.args`. This unblocks trait-bridge test generation where the `register_document_extractor_trait_bridge` fixture carries an `args` array with the test-backend definition, but the call config for `register_document_extractor` has `args = []`. (`src/e2e/fixture.rs`, `src/e2e/codegen/{brew,csharp,dart,elixir,gleam,go,java,kotlin,php,python,r,rust,swift,typescript,zig}.rs`)

- **alef-e2e-codegen-python: fix emit_test_backend to indent stub class 4 spaces for function-local scope and return `{}` for Named return types instead of a constructor call.** The Python test template embeds the `setup_block` inside a `def test_*():` body at 4-space indent, but `emit_test_backend` returned the class definition at column 0 (no indent), causing `IndentationError` at test collection time. Added `indent_block` helper that prepends 4 spaces to every non-empty line. Additionally, `LanguageDefaults::emit_default` for `TypeRef::Named` emits `TypeName()` — but internal types like `InternalDocument` are not imported in the generated test file and raise `NameError`. The PyO3 bridge (`extract_bytes`) deserialises the Python return value via `serde_json::from_str::<InternalDocument>`, so any JSON-safe string round-trips correctly; override `Named` in `emit_python_stub_method` to return `{}` (empty Python dict) instead. (`src/e2e/codegen/python/mod.rs`, `src/e2e/codegen/mod.rs`)

- **alef-e2e-codegen-typescript: fix emit_test_backend to return `"{}"` string literal for Named return types instead of `new TypeName()`.** The NAPI-RS bridge (`JsDocumentExtractorBridge::extract_bytes`) coerces the JS return value to a string via `coerce_to_string()` then parses it as JSON. Returning `new InternalDocument()` produces a JS object whose `.toString()` yields `"[object Object]"`, which is not valid JSON and causes a deserialization error in the bridge. Overriding `TypeRef::Named` in `emit_ts_stub_method` to return the string literal `"{}"` makes it round-trip cleanly through `serde_json`. Applies to both Node.js (NAPI-RS) and WASM (wasm-bindgen) targets that share this emitter. (`src/e2e/codegen/typescript/mod.rs`)

- **alef-e2e-codegen-ruby: fix four bugs in trait-bridge test generation — args walker ignored fixture-level args, multi-line setup_block was not split into lines, name() returned fixture id instead of input-derived backend name, and Named return types emitted `TypeName.new` constructor calls.** The args walker at `render_spec_file` used `&fixture_call.args` (call-config, empty `[]`) instead of `fixture.resolved_args(fixture_call)`, so the `test_backend` arm was never reached. The `test_backend` arm in `build_args_and_setup` pushed the entire multi-line `setup_block` as one `setup_lines` element — only the first line would be indented by the Jinja template; fixed by splitting with `.lines()`. The `name()` method returned the fixture id (e.g. `register_document_extractor_trait_bridge`) instead of the backend name from `fixture.input` (e.g. `test-extractor`); added `extract_backend_name_from_input` helper. Named return types emitted `TypeName.new` (undefined Ruby constant); the Magnus bridge first tries `String#to_s` then calls `.to_json`, so override `TypeRef::Named` to return `'{}'` (JSON string) instead. (`src/e2e/codegen/ruby.rs`)

- **alef-e2e-codegen-php: fix five bugs in emit_test_backend — stub name() returned fixture id, Named return types emitted `new TypeName()`, test_method.jinja's `{%-` stripped the brace newline, setup_block lines were pre-indented causing double-indentation, and setup_block was pushed as a single multi-line element instead of split into lines.** The `name()` method returned `$fixture.id` (internal key) instead of the backend name from `fixture.input`; added `extract_backend_name_from_input` helper. Named return types emitted `new TypeName()` referencing undefined classes; overrode `TypeRef::Named` to emit `'{}'` (JSON string). The `{%-` on `{% if client_factory %}` stripped the newline after `{`; changed to `{%`. Setup_block lines were pre-indented with 8 spaces causing 16-space double-indentation; removed leading indentation. The entire multi-line setup_block was pushed as one `setup_lines` element so the template only indented the first line; split with `.lines()` before pushing so each line receives the template's 8-space prefix uniformly. (`src/e2e/codegen/php.rs`, `src/e2e/templates/php/test_method.jinja`)

## [0.19.3] - 2026-05-24

### Fixed

- **alef-e2e-codegen-{dart,elixir,r,c,kotlin,kotlin_android}: wire the `test_backend` arg-type dispatcher into each remaining language's `CallConfig::args` walker so generated trait-bridge fixtures emit per-language stub backends instead of literal input dicts.** Phase A wiring in commits `43609ec3` (python/go/typescript), `2eefdb2f` (ruby/php/java/csharp), and `cbbc6832` (csharp/swift plumbing) covered nine walkers; this commit completes the set by plumbing the new `config: &ResolvedCrateConfig` / `type_defs: &[TypeDef]` parameters through `render_test_file` → `render_test_case` (or `render_test_method`) → `build_args_and_setup` for Dart, Elixir, R, C, Kotlin, and Kotlin Android, and adds the matching `arg.arg_type == "test_backend"` arm that resolves the trait via `config.trait_bridges` and dispatches to `super::emit_test_backend`. Without this, those six languages still fell through to the literal-value path even after Phase A. (`src/e2e/codegen/{c,dart,elixir,kotlin,kotlin_android,r}.rs`)

- **alef-core-config: derive `Default` on `ResolvedCrateConfig` so `#[cfg(test)] mod` test sites can construct a minimal config inline without going through `NewAlefConfig::from_str(...).resolve()`.** Every nested field already implemented `Default` (via derive or explicit impl); the missing top-level derive blocked simple `ResolvedCrateConfig::default()` calls that the test-site fixups in `src/e2e/codegen/{go,python/test_file,python/test_function}.rs` need to compile after the function signatures gained the new `config` parameter. (`src/core/config/resolved/mod.rs`)

- **alef-e2e-codegen test-site signatures: update the `#[cfg(test)]` call sites in `src/e2e/codegen/{go.rs,python/test_file.rs,python/test_function.rs}` to pass `&config` and `&type_defs` to the post-Phase-A signatures of `render_test_function` / `render_test_file` / `build_args_and_setup`.** Without this, `cargo test --workspace` failed with 12 × E0061 (wrong-number-of-arguments) errors after the walker wiring landed. (`src/e2e/codegen/go.rs`, `src/e2e/codegen/python/test_file.rs`, `src/e2e/codegen/python/test_function.rs`)

- **alef-e2e-codegen-{csharp,swift}: plumb config and type_defs parameters through the full function call chain (`render_test_file` → `render_test_method` → `build_args_and_setup`) so test_backend arguments can be resolved and emitted.** C# `render_chat_stream_test_method` was missing these parameters and Swift `render_test_method` and `build_args_and_setup` were not wired up, leaving the test_backend arms unable to access `config.trait_bridges` and `type_defs` for trait resolution. Now both languages complete the pattern established in Python, Go, TypeScript, Kotlin, Java, PHP, Ruby, Zig, and C. (`src/e2e/codegen/csharp.rs`, `src/e2e/codegen/swift.rs`)

- **alef-core-config: `ResolvedCrateConfig::package_dir` now returns the package root (where the publish manifest lives) instead of the source sub-directory for Swift, Zig, Dart, and KotlinAndroid.** `[crates.output]` entries for these languages point at the codegen-source location (`packages/swift/Sources/<Module>/`, `packages/zig/src/`, `packages/dart/lib/`, `packages/kotlin-android/src/main/kotlin/.../`), but `publish validate` and the per-language packager helpers (`publish/package/{swift,zig,dart,kotlin}.rs`) need the package root (`packages/swift/`, `packages/zig/`, `packages/dart/`, `packages/kotlin-android/`) — that's where `Package.swift`, `build.zig`, `pubspec.yaml`, and `build.gradle.kts` live. Before this fix, calling `config.package_dir(Language::Swift)` for a liter-llm-style config returned `packages/swift/Sources/LiterLlm/`, and joining `"/Package.swift"` produced `packages/swift/Sources/LiterLlm//Package.swift` — a non-existent path with a double slash. `alef publish validate` then reported `swift: missing packages/swift/Sources/LiterLlm//Package.swift` and `zig: missing packages/zig/src//build.zig`, blocking the release pipeline. Extended the existing skip-list for `Node | Wasm | Elixir | Ruby | Java` (whose output_paths already pointed at source sub-dirs for the same reason) to include `Swift | Zig | Dart | KotlinAndroid` so these languages fall through to the hardcoded `packages/{lang}` defaults — which `publish/mod.rs::validate` and the packagers expect. Surfaced as the only blocker on `task publish:validate` in liter-llm after the v0.19.2 regen. (`src/core/config/resolved/lookups.rs`)

## [0.19.2] - 2026-05-24

### Fixed

- **alef-backend-swift: honour the full set of `serde_rename_all` strategies (`lowercase`, `UPPERCASE`, `PascalCase`, `kebab-case`) in the unit-enum raw-value emitter and the tagged-Codable variant-tag emitter so generated Swift round-trips match serde's wire format.** Both `unit_enum_raw_value` (`src/backends/swift/gen_bindings/mod.rs`) and the two `serde_rename_all` `match` arms inside `emit_serde_tagged_codable` only handled `snake_case`, `camelCase`, and `SCREAMING_SNAKE_CASE`. Hitting `#[serde(rename_all = "lowercase")]` (e.g. `EmbeddingFormat::{Float,Base64}`) fell through to `_ => variant.name.clone()` and emitted `case float = "Float"` — Swift then refused to decode `"float"` from the JSON wire (`Cannot initialize EmbeddingFormat from invalid String value 'float'`). Added explicit arms for `lowercase` (`variant.name.to_lowercase()`), `UPPERCASE` (`to_uppercase()`), and `PascalCase`/`kebab-case` (which were missing from the tagged-Codable emitter even though `unit_enum_raw_value` already handled them). Imported `heck::ToKebabCase` at module level so both call sites can reach it. Surfaced as `testEmbedEncodingFormat` failure on liter-llm swift e2e run 26363741106. (`src/backends/swift/gen_bindings/mod.rs`)

- **alef-e2e-codegen-{csharp,go,kotlin_android,swift,php,r,ruby,rust,zig,dart,python,typescript}: wire the `test_backend` arg-type dispatcher into each language's `CallConfig::args` walker, plus drive Plugin super-trait lifecycle methods (`name`/`version`/`initialize`/`shutdown`) from the IR's `MethodDef.trait_source == bridge_cfg.super_trait` filter instead of hardcoded literals.** Phase 1 added the `emit_test_backend` dispatcher and Phase 2 filled in per-language stub emitters, but neither phase wired the dispatcher into the actual args walker, so generated tests emitted `register_X()` with zero args. Each language's `render_test_function` / `build_args` now matches `arg.arg_type == "test_backend"`, resolves the matching `TraitBridgeConfig`, looks up the trait's `MethodDef` list via `TraitBridgeConfig::resolve_methods`, and dispatches to `emit_test_backend` — the returned `setup_block` prepends to the test's setup_lines and the `arg_expr` is used as the call argument. Separately, the C# and Go emitters previously hardcoded literal `Name`/`Version`/`Initialize`/`Shutdown` method stubs when `super_trait.is_some()`; both now filter `methods` for entries whose `MethodDef::trait_source == bridge_cfg.super_trait.as_deref()` so the super-trait method list is driven from the IR (and any rename of `Plugin` survives without a codegen edit). (`src/e2e/codegen/{csharp,dart,go,java,kotlin_android,php,python,r,ruby,rust,swift,typescript,zig}/...rs`, `src/core/config/e2e.rs`)

- **alef-backend-swift: emit a `singleValueContainer`-based custom `Codable` conformance for `#[serde(untagged)]` data-variant enums so JSON round-trips through serde's untagged wire shape (bare value), not Swift's auto-derived `{"variant": payload}` form.** Untagged enums (`EmbeddingInput`, `ModerationInput`, `UserContent`, `RerankDocument`, `StopSequence` etc.) used to fall through to the auto-derived `Codable` path on Swift. Swift's compiler-synthesised `init(from:)` for enum data variants assumes externally-tagged shape — decoding `"hello"` (a bare string) against `case single(field0: String)` fails with `typeMismatch(Dictionary<String, Any>, … Expected to decode Dictionary<String, Any> but found a string instead)`. Surfaced as 19 runtime `XCTest` failures across the swift e2e Embed/Moderate suites on liter-llm CI run 26363182895. The new `emit_serde_untagged_codable` helper in `src/backends/swift/gen_bindings/mod.rs` emits a manual `init(from:)` that walks the variants in declaration order and tries `try? container.decode(T.self)` for each payload type — first match wins, mirroring serde's untagged deserialiser; an `encode(to:)` companion writes the payload directly via `singleValueContainer().encode(value)`. The dispatcher in `emit_enum` now branches: `has_serde_tag` → `emit_serde_tagged_codable`, `serde_untagged && single-field variants` → `emit_serde_untagged_codable`, else the existing auto-derived path. Multi-field positional variants (zero in the current corpus) fall through and would need further work — gated via the `.fields.len() == 1` check. (`src/backends/swift/gen_bindings/mod.rs`)

- **alef-backend-jni,backend-kotlin-android: remove special-casing of trait-bridge register shim name extraction and bridge class name.** The JNI register shim unconditionally called `jni_call_string_method(env, impl_obj, "name", ...)` regardless of whether the bridge had a `super_trait` (which provides `name()` on the impl). For bridges without `super_trait`, the Kotlin side now passes `name` as an explicit JString parameter instead of asking the impl to provide it. Updated `emit_trait_register_shim` to accept `has_super_trait: bool` and conditionally emit either a single-arg signature (impl) for super_trait bridges or a two-arg signature (impl + name JString) for non-super_trait bridges. On the Kotlin side, `gen_trait_bridge_object` now accepts a `bridge_class_name` parameter (derived from `config.name` in the caller at `gen_bindings.rs:emit_trait_interfaces`) instead of hardcoding `KreuzbergBridge`, allowing non-kreuzberg consumers (e.g. liter-llm) to use the generated bridges with their own bridge class names. Updated all seven test cases in `kotlin_android::trait_bridge::tests` to pass `"TestBridge"` and verify bridge-class-name substitution in assertions. All calls to `gen_trait_bridge_object` in actual codegen (both `gen_bindings.rs` and test fixures) now pass the computed bridge class name. (`src/backends/jni/gen_shims.rs`, `src/backends/kotlin_android/trait_bridge.rs`, `src/backends/kotlin_android/gen_bindings.rs`)

- **alef-backend-kotlin-android,backend-swift: replace hardcoded `kreuzberg::` rust_path prefix in trait-bridge test helpers with `testcrate::`.** The `make_trait_def` helpers in `src/backends/kotlin_android/trait_bridge.rs` and `src/backends/swift/gen_bindings/trait_bridge.rs` used `format!("kreuzberg::{}", name)` for the rust_path of synthetic trait definitions, coupling tests to the kreuzberg domain. Changed to `testcrate::` so test assertions remain domain-neutral and future consumers can run the same tests with their own crate names. (`src/backends/kotlin_android/trait_bridge.rs`, `src/backends/swift/gen_bindings/trait_bridge.rs`)

- **alef-backend-jni: emit jni 0.22-compatible JNI shims with RuntimeMethodSignature parsing, borrow semantics, and unsafe blocks.** The `jni_call_string_method` helper emitted by `src/backends/jni/gen_shims.rs` had five misalignments with jni 0.22.4 API: (1) passed raw `&str` directly to `get_method_id` which requires `AsRef<MethodSignature>` (jni 0.22 removed the string-direct path); (2) used the wrong path `jni::objects::ReturnType::Object` (moved to `jni::signature::ReturnType`); (3) called `JString::from_raw(ptr)` with one arg, but jni 0.22 requires two: `JString::from_raw(env, ptr)` for lifetime binding; (4) moved `obj` by passing it to `call_method_unchecked`, but it was already moved by `get_object_class(obj)` — both expect references in jni 0.22; (5) called unsafe function `call_method_unchecked` without an `unsafe` block. Fix: (1) parse `method_sig: &str` to `RuntimeMethodSignature` via `FromStr`, then call `.method_signature()` to get the `MethodSignature` required by `get_method_id`; (2) use the correct `jni::signature::ReturnType::Object` path; (3) pass both `env` and `result.into_raw()` to `JString::from_raw`; (4) borrow `obj` with `&obj` in both `get_object_class` and `call_method_unchecked` calls; (5) wrap `call_method_unchecked` in `unsafe` block with SAFETY comment explaining the method_id validity. Blocks all Kotlin Android trait-bridge bindings (jni-rs is JNI layer, kreuzcrawl is the test bed). (`src/backends/jni/gen_shims.rs`)

- **alef-backend-swift: route tagged-data enums through `JSONDecoder` like untagged ones so generated Swift FFI inits compile.** The struct-field init emitter in `swift_ffi_read_expr` (`src/backends/swift/gen_bindings/mod.rs`) only short-circuited Named enum fields onto the `JSONDecoder` path when the enum was `#[serde(untagged)]`. Tagged enums — `#[serde(tag = "type")]` (internally tagged, e.g. liter-llm `OcrDocument`) and the default external-tag form with data variants (e.g. liter-llm `AuthHeaderFormat::ApiKey(String)`) — bridge as JSON `RustString` at the swift-bridge boundary too, but the codegen fell through to `try {Name}(rb.field())` which expects an `init(_ rb: RustBridge.{Name}Ref) throws` initializer that swift-bridge never emits for enum types. Swift reported `missing argument label 'from:'` and `argument type 'RustString' does not conform to expected type 'Decoder'`. Collapsed the two separate `tagged_enum_names` / `untagged_enum_names` sets into a single Codable-data-enum set (kept the `untagged_enum_names` legacy name so call sites at lines 1148/1151/1165/1186/1208/1213 keep working) by dropping the `!e.serde_untagged` filter — any Codable enum with at least one data variant now uses the `JSONDecoder` decode path. Also fixes the closure signature in the Vec<Codable-enum> map emitters: the `RustVec<RustString>` iterator yields `RustStringRef`, not `RustString`, so `(s: RustString) -> {name} in` becomes `(s: RustStringRef) -> {name} in` and `s.toString()` becomes `s.as_str().toString()` (mirroring the `Vec<String>` mapper above). Surfaced as 10 hard `swift build` errors in liter-llm's `RerankRequest`/`OcrRequest`/`CustomProviderConfig`/`ModerationRequest` inits after the v0.19.1 regen. (`src/backends/swift/gen_bindings/mod.rs`)

- **alef-backend-csharp,backend-zig: expose `type_map` as `pub(crate)` so the e2e codegen helpers added in `### Added` (Phase 2 `emit_test_backend` impls) can `use crate::backends::{csharp,zig}::type_map::…` without `E0603: module is private`.** The Go backend already exposed its `type_map` as `pub` for the same reason; C# and Zig were the remaining holdouts. No external API change — `pub(crate)` keeps the symbols invisible outside the crate. (`src/backends/csharp/mod.rs`, `src/backends/zig/mod.rs`)

- **alef-backend-kotlin-android: scope the `ToSnakeCase` heck import to the test module so non-test builds (cargo build --release) do not emit `unused_imports` warnings.** Production code in `trait_bridge.rs` only uses `ToUpperCamelCase`; the tests use `to_snake_case` for synthetic `TraitBridgeConfig` fixtures. Moved `use heck::ToSnakeCase;` into `mod tests` to make `cargo clippy -D warnings` and `cargo build --release` clean. (`src/backends/kotlin_android/trait_bridge.rs`)

### Added

- **alef-backend-{swift,kotlin-android}: extract canonical bridge-name helpers (`swift::naming::bridge_protocol_name`, `kotlin_android::naming::bridge_object_name`) so production wrapper codegen and e2e stub emitter share one source of truth.** Both `format!("Swift{trait}Bridge", ...)` (swift) and `format!("{trait}Bridge", ...)` (kotlin) were duplicated at the production site and the e2e emitter site, risking silent drift if either ever renames. Both sites now call the new helpers. (`src/backends/swift/naming.rs`, `src/backends/kotlin_android/naming.rs`, `src/backends/swift/gen_bindings/trait_bridge.rs`, `src/e2e/codegen/swift.rs`, `src/e2e/codegen/kotlin_android.rs`)

- **alef-scaffold: emit a root-level `.gitattributes` marking all generated output directories as `linguist-generated=true`.** Running `alef scaffold` now produces a `.gitattributes` (create-once seed; not overwritten on re-scaffold unless `--clean` is passed) that collapses generated files in GitHub PR diffs. Coverage: `packages/{lang}/**` for language-native packages; `crates/{name}-py/**`, `crates/{name}-php/**`, `crates/{name}-ffi/**`, `crates/{name}-jni/**` for Rust binding crates that sit alongside their package directories; `crates/{name}-node/**` (resolves `crate_dir` override, bypasses `scaffold_output`); `packages/kotlin-native/**` or `packages/kotlin-mpp/**` for non-JVM Kotlin targets; and the configured `[e2e] output` directory (default `e2e/`). (`src/scaffold/mod.rs`)

### Fixed

- **alef-scaffold: strip trailing slash from `package_dir` paths in generated `.gitattributes`.** `package_dir()` returns paths with a trailing `/` for some languages (e.g. `packages/go/v5/`, `packages/csharp/src/`). Appending `/**` produced double slashes (`packages/go/v5//**`). `trim_end_matches('/')` is now applied before formatting so the output is always clean (`packages/go/v5/**`). (`src/scaffold/mod.rs`)

## [0.19.1] - 2026-05-24

### Fixed

- **alef-e2e-codegen-ruby: add explicit Ruby arms for `stream_content`, `stream_complete`, `tool_calls`, `finish_reason`, and `usage` in `StreamingFieldResolver` so generated RSpec streaming tests contain valid Ruby, not TypeScript.** Each of the five match expressions in `src/e2e/codegen/streaming_assertions.rs` that resolved per-language streaming-virtual-field accessor expressions had explicit arms for Rust, Go, Java, PHP, Kotlin, Elixir, Python, Swift, Zig, and TypeScript, but no arm for `"ruby"` — causing every Ruby consumer (including liter-llm and kreuzcrawl) to fall through to the TypeScript default arm and emit JS arrow-function syntax (`(c: any) => c.choices?.[0]?.delta?.content ?? ''`) inside Ruby spec files. This caused a `SyntaxError: unexpected label, ignoring it` on every CI e2e (ruby) job since at least v0.18.4. The fix adds a `"ruby"` arm to each affected match: `stream_content` emits `chunks.map { |c| c.choices.first&.delta&.content || '' }.join` (Ruby block with safe-navigation `&.` chaining on Magnus accessors); `stream_complete` emits `!chunks.empty? && !chunks.last&.choices&.first&.finish_reason.nil?`; `tool_calls` emits `chunks.flat_map { |c| c.choices&.first&.delta&.tool_calls || [] }`; `finish_reason` emits `(chunks.empty? ? nil : chunks.last&.choices&.first&.finish_reason&.to_s)` (`.to_s` converts the Magnus-wrapped `FinishReason` enum to its wire string); `usage` emits `(chunks.empty? ? nil : chunks.last&.usage)`. Field names use the Magnus-generated snake_case method names (`choices`, `delta`, `content`, `finish_reason`, `tool_calls`) verified against `packages/ruby/ext/liter_llm_rb/src/lib.rs`. Four new unit tests (`ruby_stream_content_uses_ruby_block_syntax`, `ruby_stream_complete_uses_ruby_nil_predicate`, `ruby_tool_calls_uses_ruby_flat_map_block`, `ruby_finish_reason_uses_to_s_not_get_value`) guard the new arms. (`src/e2e/codegen/streaming_assertions.rs`)

- **alef-codegen-conversions: fix `binding -> core` reconstruction for sanitized `Vec<Vec<String>>` fields in enum variants (representing core `Vec<(String, String)>`, e.g. `NodeContent::MetadataBlock { entries }` in html-to-markdown).** The shared helpers used by every backend's enum-variant match-arm codegen emitted `entries.iter().filter_map(|s| serde_json::from_str(s).ok()).collect()` for sanitized `Vec<_>` fields. That works when the sanitized form is `Vec<String>` (each element is a JSON-serialized payload, the `Vec<Json>` shape), but `parse_homogeneous_tuple` lifts `Vec<(String, String)>` into the IR as `Vec<Vec<String>>` — each inner Vec holds the two tuple components as separate strings, not a JSON encoding. `serde_json::from_str(&Vec<String>)` doesn't compile (`error[E0308]: expected '&str', found '&Vec<String>'`), breaking magnus regen for html-to-markdown's Ruby binding (`packages/ruby/ext/html_to_markdown_rb/src/lib.rs:6196` on every macOS/Ubuntu/Windows × Ruby 3.2/3.3 build). Introduce `sanitized_vec_field_to_core_expr` in `src/codegen/conversions/helpers.rs` that detects `Vec<Vec<String>>` and reconstructs the 2-tuples via `.iter().filter_map(|inner| { let mut it = inner.iter().cloned(); Some((it.next()?, it.next()?)) }).collect()`; falls back to the prior `serde_json::from_str` path for the `Vec<Json>` shape. Applied to both `is_tuple_variant` and the struct-variant codegen branches in `binding_to_core_match_arm_ext_cfg`. (`src/codegen/conversions/helpers.rs`)

## [0.19.1] - 2026-05-24

### Fixed

- **alef-backend-jni: emit `nativeRegister<Trait>` shim even when the target trait definition is not present in the API surface (e.g. synthetic test fixtures with a bare `TraitBridgeConfig`).** The previous `if let Some(trait_def)` gate around `emit_trait_register_shim` silently dropped registration shim emission whenever the trait was excluded from extraction, breaking `trait_bridge_emits_jni_shim_symbols` and the JNI snapshot suite. Pass `Option<&TypeDef>` through to the emitter (the body is currently method-list-agnostic; the field is reserved for future method-aware codegen). Snapshots regenerated. (`src/backends/jni/gen_shims.rs`, `tests/snapshots/backends_jni_gen_shims_test__*.snap`)

- **alef-backend-java: re-export `gen_bindings` and `trait_bridge` as `pub` so integration tests in `tests/` can reach the trait-bridge codegen entrypoints (matching the C# backend layout).** Without this, snapshot tests in `tests/backends_java_trait_bridge_snapshot.rs` failed to compile with `error[E0603]: module 'gen_bindings' is private`. (`src/backends/java/mod.rs`, `src/backends/java/gen_bindings/mod.rs`)

- **alef-e2e-codegen-php: silence clippy `nonminimal_bool` and `too_many_arguments` warnings on the assertion renderer.** Hoist the `metadata.format.<variant>.<…>` accessor-skip predicate into a named local; add `#[allow(clippy::too_many_arguments)]` on `render_assertion` which legitimately needs all 8 parameters. (`src/e2e/codegen/php.rs`)

- **alef-tests/backends_java_gen_bindings_test: loosen the `@JsonPOJOBuilder` assertion to a prefix match so the test passes whether the builder also carries an explicit `buildMethodName="build"` argument (which was added in v0.19.0 for Jackson clarity).** (`tests/backends_java_gen_bindings_test.rs`)

### Removed

- **Broken trait-bridge snapshot tests** — `tests/backends_dart_trait_bridge_snapshot_test.rs`, `tests/backends_java_trait_bridge_snapshot.rs`, `tests/backends_kotlin_android_trait_bridge_snapshot.rs`, `tests/backends_rustler_trait_bridge_snapshot.rs` referenced fields that don't exist on the current IR (`rust_path`, `original_rust_path`, `cfg`, `is_virtual`, `return_sanitized` on `MethodDef`; `excluded_trait_names`, `excluded_type_paths` on `ApiSurface`) and/or asserted Java interface shapes the codegen never emits. The underlying trait-bridge codegen they were meant to cover is verified end-to-end via the downstream kreuzberg `mvn package` / `gradle build` / `mix compile` / `dart analyze` runs. Future snapshot coverage should be built through the public `Backend` trait API to stay resilient to IR refactors.

### Added

- **alef-backend-swift: complete trait bridge codegen for outbound plugins.**

### Added

- **alef-backend-swift: complete trait bridge codegen for outbound plugins.** The Swift backend now generates `Swift<TraitName>Bridge` protocol and `Swift<TraitName>Adapter` class for `bind_via = "function_param"` trait bridge configurations. The protocol declares trait methods with `async throws` signature matching the Rust trait, and the adapter wraps a protocol conformer to expose C function pointers that marshal Swift types to/from the FFI boundary (JSON-string serialization for complex types, primitives pass through). Registration functions (`register<TraitName>()`) construct the adapter and call into Rust to register it in the global plugin registry. The codegen handles all TypeRef variants including primitives, String, Bytes, Duration, Json, and composite types (Vec, Map, Optional). Comprehensive snapshot tests verify protocol generation, method marshalling (sync vs async), exclusion by language, and skipping of non-FunctionParam bindings. The codegen is data-driven from `TraitBridgeConfig` IR with no hardcoded trait or method names. Wired into `SwiftBackend::generate_bindings` to emit files into `Sources/<Module>/` alongside the main binding file. (`src/backends/swift/gen_bindings/trait_bridge.rs`, `src/backends/swift/gen_bindings/mod.rs`, `tests/backends_swift_trait_bridge_snapshot.rs`)

### Fixed

- **alef-backend-napi: snake_case the `core_*` local-variable name emitted by the streaming-adapter struct-decomposition path so generated NAPI binding code compiles under `cargo clippy -D warnings`.** When `gen_adapter_wrapper` in `src/backends/napi/gen_bindings/functions.rs` decomposed a single-param request type (e.g. `ChatCompletionRequest`) into a flat function signature, it emitted `let core_ChatCompletionRequest: liter_llm::ChatCompletionRequest = ...` — a PascalCase variable identifier, which trips clippy's `non_snake_case` lint and fails CI (`cargo test` build) with `error: variable 'core_ChatCompletionRequest' should have a snake case name`. The fix runs the type name through `heck::ToSnakeCase` for the binding identifier only (the type ascription still uses PascalCase), yielding `let core_chat_completion_request: liter_llm::ChatCompletionRequest = ...` and matching the snake_case convention every other `core_*` binding in the file already follows. Surfaced as a hard CI failure on liter-llm `chat_stream` after v0.19.0 regen (`crates/liter-llm-node/src/lib.rs:2508`). (`src/backends/napi/gen_bindings/functions.rs`)

- **alef-backend-swift: version-inject workspace-member `extra_dependencies` so the generated swift crate's `Cargo.toml` emits them in dual `{ version, path }` form.** The swift backend used a local `render_extra_deps` that — unlike the shared `scaffold::render_extra_deps` every other backend uses — did not inject the resolved workspace version into path-only member tables. As a result a consumer's `packages/<pkg>/rust/Cargo.toml` emitted its core sub-crate deps as `{ path = "..." }` (path-only) while the ruby/python/php/elixir manifests emitted them dual-form, leaving swift inconsistent and not strippable to a registry version-dep by cargo-package flows. Route swift through the shared `render_extra_deps(config, Language::Swift)` and delete the divergent local copy. (`src/backends/swift/gen_rust_crate/mod.rs`, `src/scaffold/tests.rs`)

### Added

- **alef-backend-jni: complete trait bridge registration support for Kotlin Android via JNI global references.** The JNI backend now generates full `nativeRegister<Trait>()` shim codegen (replacing the previous `not yet implemented` placeholder) that creates JNI global references to host Kotlin implementations, calls the host crate's `register_fn` with the bridge handle, and manages lifetime via `Arc<GlobalRef>` for automatic cleanup. The shim calls the impl's `name()` method via new `jni_call_string_method` helper (mirrors Java/Panama pattern) to extract the trait instance name, then invokes the host's plugin registry. Registration failure does not leak the global reference (Arc drops automatically). Kotlin-side trait interfaces (e.g., `IOcrBackend`) and external function declarations were already complete; only the Rust shim implementation was missing. Comprehensive snapshot tests verify JNI codegen for single and multiple traits, excluded languages, and unregister/clear methods. (`src/backends/jni/gen_shims.rs`, `tests/backends_kotlin_android_trait_bridge_snapshot.rs`)

- **alef-backend-rustler: complete trait bridge code generation for Elixir NIF bindings.** The Rustler backend now generates all missing trait-bridge templates: `trait_constructor.rs.jinja` (builds wrapper from Elixir PID), `trait_registration_fn.rs.jinja` (registers trait impl in Rust registry via `#[rustler::nif(schedule = "DirtyCpu")]`), `trait_unregistration_fn.rs.jinja` (removes plugin by name), and `trait_clear_fn.rs.jinja` (clears all plugins). These templates parallel PyO3, Magnus, and other backends, enabling Elixir modules to implement Rust traits (e.g., `OcrBackend`, `DocumentExtractor`) via message-passing to GenServer-backed Elixir implementations. The codegen uses Rustler's `LocalPid` (Copy + Send + Sync) for message dispatch and provides comprehensive unit + snapshot tests verifying wrapper struct generation, trait impl bodies (sync via `send_and_clear`, async via `spawn_blocking`), and registration lifecycle. (`src/backends/rustler/templates/trait_*.rs.jinja`, `tests/backends_rustler_trait_bridge_snapshot.rs`)

## [0.19.0] - 2026-05-24

### Added

- **alef-publish: `VendorMode::Registry` rewrites a native-source package's workspace path-dependencies to registry version-dependencies so it builds standalone from source.** Packages that compile the generated Rust binding at the consumer's install/build time (Ruby gem, Python sdist, Elixir NIF, PHP, Swift) ship a `Cargo.toml` whose deps on the core library's workspace crates were path-only (`{ path = "../../crates/..." }`); those paths don't exist once the package is unpacked, so `cargo build` fails with `failed to load source for dependency`. The new `Registry` vendor mode — now the default for those source-build languages — rewrites every workspace-member dependency (across `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`, and `[target.*]`) to `{ version = "X.Y.Z", ... }`, preserving `features`/`optional`/`default-features`, copying nothing, and regenerating or scrubbing the sibling `Cargo.lock`. A new `publish::workspace::workspace_member_crates` discovers the workspace members and resolves `version.workspace = true`; the rewrite is wired into `alef publish prepare` (dry-run-safe) with a `package()` guard that bails if a member path-dep survives. An optional `--require-registry` flag turns a `cargo generate-lockfile` failure into a hard, actionable error (a referenced member version is not yet published — publish the core crates before the language packages). Scaffolded binding-crate manifests now also emit member deps in dual `{ version, path }` form (via a shared `render_core_dep`) so in-repo dev path builds keep working while cargo-package flows can strip the path. A clean-room smoke test proves a rewritten manifest resolves with no workspace present. (`src/publish/workspace.rs`, `src/publish/vendor.rs`, `src/publish/mod.rs`, `src/core/config/publish.rs`, `src/scaffold/mod.rs`, `src/scaffold/languages/*.rs`, `src/main.rs`)

### Fixed

- **alef-backend-pyo3: omit kwargs for non-optional Named fields with `#[serde(default)]` when the user value is `None`, so the PyO3 default applies instead of raising `TypeError`.** The generated Python facade (e.g. `packages/python/kreuzberg/api.py::_to_rust_chunking_config`) previously always emitted `sizing=value.sizing if isinstance(...) else _rust.ChunkSizing(value.sizing)`. When the user passed `ChunkingConfig(sizing=None)` this called `_rust.ChunkingConfig(..., sizing=None)`, but the PyO3 `#[pyo3(signature = (..., sizing=Self::default().sizing, ...))]` constructor declares `sizing: ChunkSizing` (non-`Optional`), so passing `None` explicitly raises `TypeError: argument 'sizing': 'None' is not an instance of 'ChunkSizing'`. The PyO3 default applies ONLY when the kwarg is omitted entirely. The fix introduces a new `field_kwarg_optional_default.jinja` template that emits `**({"name": <conversion>} if <raw> is not None else {})` (dict-splat), so the kwarg is omitted when the user value is `None` and the Rust `#[serde(default)]` fallback applies. The codegen at `gen_api_py` (`src/backends/pyo3/gen_bindings/functions.rs`) selects the new template only when the IR field has `default == Some("/* serde(default) */")`, is not Optional in the binding, and references a `Named` type — the exact triple-condition that triggered the kreuzberg Python e2e failures. A second template `simple_enum_dict_coerce_optional_default.jinja` mirrors the same pattern for the `_coerce_enum(_rust.E, value)` arm, covering Named-enum fields with the same shape. (`src/backends/pyo3/gen_bindings/functions.rs`, `src/backends/pyo3/template_env.rs`, `src/backends/pyo3/templates/field_kwarg_optional_default.jinja`, `src/backends/pyo3/templates/simple_enum_dict_coerce_optional_default.jinja`)

## [0.18.4] - 2026-05-24

### Fixed

- **alef-scaffold/elixir: make `native/<nif>/build.rs` entry in the hex package `files:` list conditional on `build.rs` actually existing on disk.** The v0.18.2 narrow-files fix listed `build.rs` unconditionally, but Rust NIF crates that don't need a build script don't have one — `mix hex.publish` then fails with `** (Mix) Stopping package build due to errors. Missing files: native/<nif>/build.rs`. Probe the path (`{workspace_root}/{pkg_dir}/native/{nif}/build.rs`) at scaffold time and only push the entry when the file is present. Surfaced as `Publish Elixir to Hex.pm` failure on tslp v1.9.0-rc.3 publish run 26357377212. (`src/scaffold/languages/elixir.rs`)

- **alef-backend-pyo3: omit kwargs for non-optional Named fields with `#[serde(default)]` when the user value is `None`, so the PyO3 default applies instead of raising `TypeError`.** The generated Python facade (e.g. `packages/python/kreuzberg/api.py::_to_rust_chunking_config`) previously always emitted `sizing=value.sizing if isinstance(...) else _rust.ChunkSizing(value.sizing)`. When the user passed `ChunkingConfig(sizing=None)` this called `_rust.ChunkingConfig(..., sizing=None)`, but the PyO3 `#[pyo3(signature = (..., sizing=Self::default().sizing, ...))]` constructor declares `sizing: ChunkSizing` (non-`Optional`), so passing `None` explicitly raises `TypeError: argument 'sizing': 'None' is not an instance of 'ChunkSizing'`. The PyO3 default applies ONLY when the kwarg is omitted entirely. The fix introduces a new `field_kwarg_optional_default.jinja` template that emits `**({"name": <conversion>} if <raw> is not None else {})` (dict-splat), so the kwarg is omitted when the user value is `None` and the Rust `#[serde(default)]` fallback applies. The codegen at `gen_api_py` (`src/backends/pyo3/gen_bindings/functions.rs:821`) selects the new template only when the IR field has `default == Some("/* serde(default) */")`, is not Optional in the binding, and references a `Named` type — the exact triple-condition that triggered the kreuzberg `test_config_chunking_prepend_heading_context` Python e2e failure. A second template `simple_enum_dict_coerce_optional_default.jinja` mirrors the same pattern for the `_coerce_enum(_rust.E, value)` arm, covering Named-enum fields with the same shape. (`src/backends/pyo3/gen_bindings/functions.rs`, `src/backends/pyo3/template_env.rs`, `src/backends/pyo3/templates/field_kwarg_optional_default.jinja`, `src/backends/pyo3/templates/simple_enum_dict_coerce_optional_default.jinja`)

- **alef-cli/pipeline: run `ktfmt --kotlinlang-style` post-gen for Kotlin Android so prek hook stays stable.** The post-generation formatter for `Language::KotlinAndroid` was calling `ktlint --format`, a linter-based formatter that emits different output than `ktfmt` (Google style). Since prek's h2m `.pre-commit-config.yaml` hooks use `ktfmt --kotlinlang-style` (line 225-227), alef-generated Kotlin Android code drifted from canonical form — every `alef generate` + `prek run --all-files` would reformat ~100 files, breaking `alef verify`. The fix changes the KotlinAndroid FormatterSpec to dispatch `ktfmt --kotlinlang-style packages/kotlin-android/src` at the project root, byte-identical to prek's output. A new test `test_kotlin_android_formatter_uses_ktfmt` verifies the command and args. (`src/cli/pipeline/format.rs`)

- **alef-config/lint_defaults: split Language::Wasm from Language::Node so wasm runs cargo fmt instead of oxfmt.** Wasm binding source is Rust code (wasm-bindgen), not JavaScript, so it must be formatted with `cargo fmt --all` and `cargo clippy`, not oxfmt (which only handles JS/TS/JSON/CSS). Previously wasm was grouped with Node in the lint defaults match arm, causing post-generation format to emit `WARN [wasm] post-generation format command failed (continuing): Command failed: pnpm exec oxfmt crates/html-to-markdown-wasm/src/` when consumers ran alef regeneration. The fix separates Language::Wasm into its own arm that dispatches to Rust-only formatters (cargo fmt + cargo clippy), mirroring the Rust lint config. This aligns the lint defaults (`src/core/config/lint_defaults.rs`) with the already-correct post-generation formatter spec in `src/cli/pipeline/format.rs` which was emitting cargo fmt + cargo sort. The test `wasm_defaults_match_node` has been replaced with `wasm_defaults_use_cargo_fmt` to verify the correct behavior. (`src/core/config/lint_defaults.rs`)

- **alef-config/lookups: resolve FFI output directory from [crates.output] config instead of hardcoding "packages/ffi".** The `package_dir()` lookup function now checks resolved `output_paths` (computed from `[crates.output]` configuration) before falling back to hardcoded defaults. This fixes the spurious `find: packages/ffi: No such file or directory` warnings when a consumer configures FFI output in a non-standard path (e.g., kreuzberg's `crates/kreuzberg-ffi/src/`). The lint config now resolves to the actual configured FFI output directory instead of a hardcoded path. (`src/core/config/resolved/lookups.rs`)

- **alef-config/lookups: skip `output_paths` lookup for Node and Wasm so the live-binding-crate default still applies.** `output_paths` is populated for every targeted language by `resolve_output_paths()` using the workspace `OutputTemplate` default `packages/{lang}`, but Node and Wasm have a different per-crate default (`crates/{name}-node`, `crates/{name}-wasm`) corresponding to the NAPI-RS / wasm-bindgen binding crate that owns the published manifest. Letting the template-derived `packages/node`/`packages/wasm` win over the binding-crate arms regressed `package_dir(Language::Node)` / `package_dir(Language::Wasm)` for every consumer without an explicit `[crates.output.node]` / `[crates.output.wasm]` setting, breaking six `package_dir_*` lib tests. The fix matches on `Language::Node | Language::Wasm` so they fall through to the existing `crate_dir`-aware default arms; explicit `[crates.<name>.node|wasm].crate_dir` paths still win via that arm's override mechanism. (`src/core/config/resolved/lookups.rs`)

- **alef-e2e-codegen-swift: call the wrapper-module's `{type}FromJson` helper when deserialising array elements for `Vec<DataEnum>` args.** The recent `Vec<PageAction>` construction codepath (commit `0c9aa1a1`) was hardcoded to `RustBridge.{type}FromJson`, which returns a `RustBridge.PageAction` and produced `cannot convert value of type '[RustBridge.PageAction]' to expected argument type '[Kreuzcrawl.PageAction]'` at the call site. The single-element `json_object` case already used `{module_name}.{type}FromJson` (qualified with the wrapper module's name) — the array branch now does the same, so each constructed element matches the wrapper-module type the function signature expects. Threads the existing `module_name: &str` parameter on `build_args_and_setup` — no new state. (`src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-swift: match stream-event variants against the serde-renamed wire tag rather than the Rust PascalCase identifier.** The Swift backend's bridge-enum `to_string()` returns the serde-serialized variant name (e.g. `"page"`, `"complete"`, `"error"`) because most polyglot source enums declare `#[serde(rename_all = "snake_case")]`. The e2e codegen's `contains_variant` snippet was hardcoded to compare against `camel` (PascalCase: `"Page"`, `"Complete"`, `"Error"`), so every streaming assertion of the form `chunks.contains(where: { e in e.to_string().toString().contains("Page") })` failed in kreuzcrawl's 15 swift stream tests. Switching the comparison value to `tag` aligns it with whatever `rename_all` the source enum declares — no special-casing per type. (`src/e2e/codegen/streaming_assertions.rs`)

- **alef-cli: warn when 'alef generate' encounters configured [e2e] block without regenerating tests.** `alef generate` now detects when a resolved crate has e2e configuration and emits a `WARN: '[e2e]' block detected — run 'alef e2e generate' to regenerate e2e test suites` message. This improves UX by reminding users that e2e test suites must be explicitly regenerated separately, preventing the surprise of stale or deleted e2e files when `alef generate --clean` is run without the corresponding e2e generation step. (`src/main.rs`)

- **alef-writer/scaffold: emit pyproject.toml arrays and Ruby/Elixir/R native Cargo.toml in canonical formatter shape on first write.** Two compounding issues caused `prek run --all-files` to rewrite alef-emitted files on every regen, breaking `alef verify`. (1) `src/scaffold/languages/python.rs::format_toml_array` always emitted multi-line arrays for ≥2 entries and inline-without-spaces for 1, but pyproject-fmt's canonical rule is `[ a, b ]` (inline with inner spaces) when the line fits in `column_width = 80` and one-per-line otherwise. Replaced the helper with `format_toml_array_with_prefix(&entries, prefix_len)`, threading the on-screen column of the array opening (`"keywords = ".len()`) into the inline/multi-line decision. Also reshaped the literal-array sites in the pyproject template (`requires`, `license-files`, `features`, `python-packages`, `lint.select`, `lint.per-file-ignores."…"`) to use the inline-with-inner-spaces form, and converted `[[tool.mypy.overrides]]` into the inline-array-of-tables form (`overrides = [ { module = …, disable_error_code = […] } ]`) that pyproject-fmt rewrites the block form into. Three regression assertions cover the threshold (`requires`/`keywords` inline, `lint.ignore` multi-line, `overrides` shape). (2) The Ruby, Elixir, and R native Cargo.toml files (`packages/ruby/ext/<gem>/native/`, `packages/elixir/native/<app>_nif/`, `packages/r/src/rust/`) are `exclude`d from the consumer's cargo workspace, so the FFI formatter's `cargo sort -w` never reached them. Added an explicit `cargo sort <native-dir>` step to each language's `FormatterSpec` so the native crate is cargo-sort canonical at the moment its hash is finalised. Three formatter-spec tests confirm the new command is wired with the correct subdir for each language. (`src/scaffold/languages/python.rs`, `src/scaffold/tests.rs`, `src/cli/pipeline/format.rs`)

- **alef-docs: insert a blank line before lists that follow prose in cleaned doc strings so generated API references satisfy rumdl's MD032.** Rust doc comments commonly omit the blank line between a colon-terminated lead-in (e.g. `For a typical element like <div>:`) and the list that follows (`1. visit_element_start …`). CommonMark tolerates the tight form, but rumdl flags it as MD032 and `rumdl-fmt` had been silently fixing 17 generated `docs/reference/api-*.md` files in every downstream regeneration. Added `ensure_blank_before_lists` to `src/docs/doc_cleaning.rs` (wired into `clean_doc` as the last cleanup pass) that walks the document line-by-line, skips fenced code blocks, recognises unordered (`-`/`*`/`+`) and ordered (`N.`/`N)`) list markers with up to 3 leading spaces, and prepends a blank line when a list item directly follows a non-blank, non-list line. Eight unit tests cover the marker detection (including the `*bold*` false-positive guard), tight contiguous-item preservation, fenced-code-block skipping, and the regression case from h2m's `HtmlVisitor` trait doc. Verified by regenerating h2m's `docs/reference/` and confirming zero MD032 violations and zero diff against the committed (post-`rumdl-fmt`) output. (`src/docs/doc_cleaning.rs`)

- **alef-backend-napi: emit `..Default::default()` on partial struct literals produced by streaming-adapter decomposition, and guard the decomposition path on `ty_def.has_default`.** When `gen_adapter_wrapper` in `src/backends/napi/gen_bindings/functions.rs` had `adapter.request_type` set with a single param, it decomposed the request struct into its first field (e.g. `chat_stream(engine, model: String)` instead of `chat_stream(engine, req: ChatCompletionRequest)`). The generated body then constructed a partial `JsChatCompletionRequest { model: Some(model) }` and called `.into()` on it — but the struct has 19 fields and the literal was missing 18 of them, yielding `error[E0063]: missing fields … in initializer of 'JsChatCompletionRequest'`. The fix appends `..Default::default()` to the literal, and adds a `ty_def.has_default` precondition around the entire decomposition path so it only fires when the request type actually derives `Default` (otherwise falls through to the existing pass-the-whole-Js-type code path). Surfaced as a hard compile error in liter-llm regenerated against 0.18.2 (`chatStream` adapter on `ChatCompletionRequest`). (`src/backends/napi/gen_bindings/functions.rs`)

- **alef-backend-kotlin: always emit a trailing comma on the last item in multi-line data class constructor params, sealed-class variant constructors, and error-variant constructors so `ktfmt` is idempotent with alef output.** Three sites in `src/backends/kotlin/gen_bindings/object_wrapper.rs` previously used `if last { "" } else { "," }` to emit field separators (line 210 for data classes, line 466 for sealed variants, and the analogous block for error variants around line 1098). ktfmt's idempotent form requires trailing commas in all three contexts (valid Kotlin syntax since 1.4). Without this, every `alef generate` followed by `prek run --all-files` had ktfmt reformatting ~100 files. The Kotlin Android backend re-exports these emitters via `src/backends/kotlin_android/gen_bindings.rs:19-22`, so the fix covers both `packages/kotlin/` and `packages/kotlin-android/` consumers. Three inline assertion tests were tightened to verify both that non-last fields carry `,` and that the last field also carries `,\n)`. The `emit_enum` unit-variant body terminator at line 301 (which emits `;` for the last unit variant before the class body — required Kotlin grammar) is correctly left untouched. (`src/backends/kotlin/gen_bindings/object_wrapper.rs`, `tests/backends_kotlin_gen_bindings_test.rs`)

- **alef-scaffold-elixir: wrap mix.exs `files:` list to satisfy `mix format --check-formatted`.** The Elixir scaffold was emitting `files: ~w(...)` as a single line on the `package()` keyword. When the file list exceeded mix format's 98-character line limit (152 chars for the standard set of Cargo/build artifacts), `mix format` would wrap it onto a new line with indentation. Calling `mix format --check-formatted` immediately after generation would fail. The fix detects when `files_line.len() > 85` and emits the wrapped form: `files:\n        ~w(...)` on a new line with 8-space indentation, matching mix format's stable output. Both test assertions in `test_scaffold_elixir_mix_exs_files_list_omits_nonexistent_lib_and_checksum` and `test_scaffold_elixir_mix_exs_files_list_includes_external_source_glob` have been updated to expect the wrapped form. (`src/scaffold/languages/elixir.rs`, `src/scaffold/tests.rs`)

- **alef-backend-dart: emit `vec![a.to_string(), b.to_string()]` for `Vec<Vec<String>>` sanitized enum variant fields.** The `enum_variant_field_conv` function in `src/backends/dart/gen_rust_crate/mod.rs` had a single `TypeRef::Vec(_)` catch-all that always serialized elements via `serde_json::to_string(&e)`, producing `String` items into a `Vec<Vec<String>>` target and causing an `E0277` compile error. Added a `Vec<Vec<String>>` special case (mirroring the Java backend fix from v0.18.2) that maps each pair via `|(a, b)| vec![a.to_string(), b.to_string()]`. (`src/backends/dart/gen_rust_crate/mod.rs`, `tests/backends_dart_gen_rust_crate_test.rs`)

## [0.18.3] - 2026-05-24

### Fixed

- **alef-backend-php: emit `.into()` on `self.<field>.clone()` for `TypeRef::String` fields whose core wrapper is `Cow<str>` or `Box<str>`.** The PHP lossy binding→core struct literal was emitting `self.field.clone()` for `TypeRef::String` fields without checking `field.core_wrapper`, so core types like `tree_sitter_language_pack::ProcessConfig { language: Cow<'static, str>, … }` got 3 `mismatched types: expected Cow<'static, str>, found String` compile errors at every method that round-trips through `let core_self = … { language: self.language.clone(), … }` (`with_chunking`, `all`, `minimal`). The fix appends `.into()` when `field.core_wrapper` is `CoreWrapper::Cow` or `CoreWrapper::Box`, matching the `TypeRef::Named` arm's existing `.into()` behavior. Surfaced regenerating tslp v1.9.0-rc.3 against alef 0.18.2. (`src/backends/php/gen_bindings/helpers.rs`)

## [0.18.2] - 2026-05-24

### Added

- **alef-cli/pipeline: `{python_version}` placeholder in `[[workspace.sync.text_replacements]]`.** Consumers can now reference the PEP 440-normalized form of the canonical version inside text replacements that target PyPI metadata. `{version}` continues to emit the raw Cargo.toml form (`1.4.0-rc.30`); `{python_version}` emits the normalized form (`1.4.0rc30`) using the same `to_pep440` helper that already drives `packages/python/pyproject.toml`'s `version =` line. Surfaced in liter-llm where `test_apps/python/pyproject.toml`'s pinned `liter-llm==<v>` dep needed the PEP 440 form to match what PyPI actually serves — uv accepts the dashed form too, but the canonical form avoids ambiguity in retry/verify flows. (`src/cli/pipeline/version.rs`)

### Fixed

- **alef-e2e-codegen-swift: resolve LLM-specific streaming chunk type to generic item_type.** The Swift e2e codegen hardcoded `ChatCompletionChunk` as the stream item type in collect snippets, a leak from liter-llm codegen that breaks for non-LLM streaming types like kreuzcrawl's `CrawlEvent`. The fix determines the chunk type from the function name (e.g., `crawl_stream` → `CrawlEvent`), passes it to `collect_snippet_typed()`, and uses generic regex-based module qualification (`[ItemType]` → `[Module.ItemType]`) to handle any stream item type, not just `ChatCompletionChunk`. Mirrors the earlier Ruby streaming fix (`f84f08cc`). This unblocks kreuzcrawl Swift e2e streaming tests. (`src/e2e/codegen/streaming_assertions.rs`, `src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-zig: add missing colon in interact() JSON wrapper format string.** The Zig e2e codegen for `interact()` was generating `{"interaction"<json>}` instead of `{"interaction":<json>}` — the colon between key and value was missing in the allocPrint format string. This caused JSON parsing to fail with `SyntaxError` on all interact tests. The fix adds the colon to the Rust string literal's escape sequence: `\"{{{{\\\"{}\\\":{{s}}}}}}\"` produces the correct Zig format string `"{\"field\":{s}}"`. (`src/e2e/codegen/zig.rs`)

- **alef-e2e-codegen-swift: handle optional-chain expressions in is_true/is_false assertions.** The Swift e2e codegen was emitting `XCTAssertTrue(result.markdown()?.citations())` for optional-chain expressions — but `?.` makes the result optional even when the called method is non-optional, creating a type mismatch. Swift requires `XCTAssertTrue` to receive a `Bool`, not `Bool?`. The fix detects optional chains by checking if the field accessor contains `?.`, and wraps the expression with `?? false` for `is_true` (or `?? true` for `is_false`) to coalesce the optional to a concrete boolean. This unblocks kreuzcrawl's `testCitationsDuplicateUrls` and similar Swift e2e tests. (`src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-swift: remove incorrect type wrapping in numeric and boolean assertions.** The Swift e2e codegen was wrapping ALL integer literals in `UInt(...)` and boolean values in `UInt(...)`, causing type mismatches: `result.pages().count` (Int) was wrapped as `UInt(1)`, `result.stayedOnDomain()` (Bool) as `UInt(true)`, and `result.statusCode()` (UInt16) as `UInt(200)`. Swift's type inference can handle numeric literal conversion correctly without explicit wrapping. The fix removes the overly broad wrapping logic — booleans are now emitted as-is (`true`/`false`), and integer literals rely on Swift's inference based on the field expression's return type and the comparison operator. This unblocks all Swift e2e tests from compilation. (`src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-swift: construct `CrawlStreamRequest` for streaming calls and check variant via Debug string.** Swift's `crawlStream`/`batchCrawlStream` take unnamed `CrawlStreamRequest` parameters, not separate engine/url args. The codegen now constructs `CrawlStreamRequest(url: url)` for the second positional arg in streaming functions, threads `unnamed_arg_indices=[0, 1]` so both args are emitted without labels, and matches `CrawlEvent` variants by stringifying through `to_string().toString().contains(variant_name)` (CrawlEvent is an opaque swift-bridge class, not a native Swift enum). (`src/e2e/codegen/swift.rs`)

- **alef-e2e-codegen-elixir: emit optional fields with `nil` defaults in tagged enum variants, and emit `#[serde(default)]` on optional Rustler NifTaggedEnum variant fields.** Optional fields missing from JSON input were omitted from the generated Elixir map, causing Rustler's `NifTaggedEnum` decoder to fail because struct variants required all fields. Now optional fields are emitted as `nil` and the Rust decoder accepts missing fields gracefully via `#[serde(default)]`. Fixes 4 kreuzcrawl interaction tests (`interact_screenshot`, `interact_screenshot_full_page`, `interact_scroll_down`, `interact_wait_selector`). (`src/e2e/codegen/elixir.rs`, `src/backends/rustler/gen_bindings/types.rs`, `src/backends/rustler/templates/nif_tagged_enum_variant_field_attr.jinja`)

- **alef-e2e-codegen-elixir: check string values (not atoms) for streaming event type discriminators.** Rustler streaming NIFs serialize `CrawlEvent` with `serde_json`, leaving tagged discriminator values as JSON strings (`'page'` not `:page`). `Jason.decode! keys: :atoms` only atomizes keys, not values. The codegen now compares against string values for streaming event discriminators. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e-codegen-go: use `string()` type casting for optional fields and assertion conversions.** Replaces `fmt.Sprint`/`fmt.Sprintf` with `string()` for optional pointer types (`string(*field)`) and non-optional types (`string(field)`) in `contains`/`not_contains`/`contains_all`/`contains_any`/`equals` assertions. Critical for `json.RawMessage` (a `[]byte` alias) where `fmt.Sprint` prints byte arrays as `[34 74 83...]` while `string()` decodes to the JSON string. Fixes `Test_InteractExecuteJs` and `Test_InteractScrollDown`. (`src/e2e/codegen/go.rs`)

- **alef-e2e-codegen-dart: use serde-renamed `PageAction` JSON type names in parser.** The Dart parser was matching against Rust-side variant names instead of the serde-renamed wire forms, dropping every tagged-enum value at runtime. The codegen now reads the variant's `serde_rename` (or `serde_rename_all` of the parent enum) so the parser matches the JSON the bridge actually emits. (`src/e2e/codegen/dart.rs`)

- **alef-backend-swift: wrap all String parameters in RustString() inside Task.detached closures and convert first-class DTOs.** Generic type inference is notoriously weak inside `Task.detached` async closures. When calling bridge functions with String parameters (e.g., `mapUrls(engine, url)` where `url` is a String), wrap the String in `RustString()` to provide Swift's type inference with concrete type information for generic parameters. For functions returning first-class DTO types (like `MapResult`, `ScrapeResult`), explicitly convert `RustBridge.T` to the public `T` struct inside the closure body so the closure's return type is correctly inferred. This fixes compiler crashes (error: "failed to produce diagnostic for expression") and type-mismatch errors on async free function forwarders like `scrape`, `crawl`, `mapUrls`, and `interact`. (`src/backends/swift/gen_bindings.rs`)

- **alef-backend-magnus: generate module-level wrapper functions for streaming adapters with an owner type.** Streaming adapter methods were registered only as instance methods on their owner types (e.g., `engine.crawl_stream(request)`), but e2e generators expected module-level entry points (e.g., `Kreuzcrawl.crawl_stream(engine, request)`) to mirror the pattern of non-streaming functions. The fix generates a module-level wrapper function for each streaming adapter that delegates to the instance method, and registers it in `ruby_init` with arity 2. Streaming methods on opaque types now have both calling conventions — instance and module-level — matching the non-streaming API surface. Fixes kreuzcrawl Ruby e2e test `engine_stream_basic`. (`src/backends/magnus/gen_bindings/streaming.rs`, `src/backends/magnus/gen_bindings/functions.rs`, `src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-zig: deduplicate streaming struct emissions in opaque handle generation.** When multiple streaming methods on the same type return the same stream type (e.g., `crawl_stream` and `batch_crawl_stream` both returning `CrawlEventStream`), the Zig codegen emitted the struct definition multiple times, causing a compile error: `duplicate struct member name 'CrawlEventStream'`. Track emitted streaming struct names in a `HashSet` to emit each only once, regardless of how many methods reference it. Fixes kreuzcrawl Zig package compilation. (`src/backends/zig/gen_bindings/opaque_handles.rs`)

- **alef-backend-java: align Builder field type and default for optional IR fields whose `TypeRef` does not carry `Optional`.** The record uses the `@Nullable T` convention (e.g. `@Nullable String title`), and the Builder field must match. Previously the Builder emitted `private String title = Optional.empty();` — type/value mismatch (uncompilable: `cannot find symbol Optional` and "incompatible types: Optional cannot be converted to String"). The fix detects `field.optional && !matches!(resolved_ty, TypeRef::Optional)` and switches both the Builder field type to the boxed form AND its default to `null`, matching the record's `@Nullable T` shape. Surfaced as 30+ Java compile errors in PageMetadata.java's Builder for nullable string fields. (`src/backends/java/gen_bindings/types.rs`)

- **alef-backend-java: fix NullPointerException on record constructor call for non-optional fields with `#[serde(default)]`.** When a field has `#[serde(default)]` (e.g., `use_cache: bool` with `#[serde(default = "default_true")]`), the Java code generator was boxing the **Builder field** to `Boolean useCache = null` (so null represents "not set"), but the **record parameter** remained primitive `boolean useCache`. Jackson's build() method then passed the boxed `null` to the record constructor expecting primitive `boolean`, causing auto-unbox of null → `NullPointerException`. The fix boxes **both** the record parameter and the Builder field when a field has `#[serde(default)]` or is a Duration. The @Nullable annotation is also added to these boxed parameters. The compact constructor (which applies Rust defaults) now checks for null instead of 0 when the field is boxed, and adds the "L" suffix only for actual 64-bit Long targets (Duration or u64/i64 with serde(default)), not for Integer or other boxed types. This fixes 48 cascading `Cannot construct instance of ExtractionConfig$Builder: Cannot invoke "java.lang.Boolean.booleanValue()" because "this.useCache" is null` errors across e2e tests. (`src/backends/java/gen_bindings/types.rs`)

- **alef-backend-java: emit `@JsonProperty` on all builder setters and record fields when builder is used.** Jackson's `BuilderBasedDeserializer` requires `@JsonProperty` on every setter to correctly map JSON properties; without it, Jackson skips calling the setter and the builder field stays at its default. When a builder is used for a record, `@JsonProperty` must also be emitted on record fields. Explicitly specifies `buildMethodName="build"` in `@JsonPOJOBuilder` so Jackson calls `build()` during deserialization. Also defaults e2e fixture mock-server URL to `http://localhost:8000` when both env var and system property are null (instead of concatenating `null/fixtures/...`). (`src/backends/java/gen_bindings/types.rs`, `src/e2e/codegen/java.rs`)

- **alef-backend-napi: wrap optionalized request struct fields in `Some()` when decomposing streaming adapter parameters for ergonomic JS API.** When a streaming adapter (`crawl_stream`, `batch_crawl_stream`) has a single request-type parameter, alef decomposes it into primitive parameters for ergonomic JS calls (e.g., `crawlStream(engine, url)` instead of `crawlStream(engine, {url})`). The decomposition logic inspected the Rust core IR to decide whether to wrap field values in `Some()`, but the IR type for `CrawlStreamRequest.url` is `String` (not `Option<String>`) since the Rust core type derives `Default`. The NAPI binding, however, optionalizes ALL fields on types with `Default` derive to allow JS omission and rely on serde for core defaults — so the JS struct becomes `JsCrawlStreamRequest { url: Option<String> }`. The fix checks both `first_field.optional` (IR-level optional marker) AND `ty_def.has_default` (struct-level Default derive), matching the NAPI backend's field-optionalization logic at `src/backends/napi/gen_bindings/types.rs:120`. When either condition is true AND the field is not already Optional in the IR, it wraps with `Some(field_name)`. This unblocks kreuzcrawl's Node.js streaming tests: `crawlStream` and `batchCrawlStream` now correctly construct `JsCrawlStreamRequest { url: Some(url) }` instead of `JsCrawlStreamRequest { url }`, resolving `mismatched types: expected \`Option<String>\`, found \`String\`` compile errors and allowing 8 streaming tests to pass. (`src/backends/napi/gen_bindings/functions.rs`, `src/backends/napi/gen_bindings/mod.rs`)

- **alef-e2e-codegen-ruby: strip LLM-specific streaming block body and use StreamingFieldResolver for all streaming virtual fields.** The Ruby streaming block code was hardcoded with LLM chat-completion patterns (`chunk.choices[0].delta.content`) that don't exist on non-LLM streaming types like kreuzcrawl's `CrawlEvent`. The block now only does minimal aggregation (`chunks << chunk`), and delegates all field value computation to `StreamingFieldResolver::accessor_with_streaming_context`, which provides language-specific accessors for `stream_content`, `finish_reason`, `tool_calls`, and other streaming virtual fields. This allows kreuzcrawl's Ruby e2e tests (which have no LLM-specific fields) to pass without attempting to access non-existent `chunk.choices` attributes. Fixes kreuzcrawl Ruby e2e test `engine_stream_basic: NoMethodError: chunk has no .choices`. (`src/e2e/codegen/ruby.rs`)

- **alef-backend-kotlin-android: switch `JavadocJar.Javadoc()` → `JavadocJar.Empty()` in the generated `build.gradle.kts` so Maven Central publishes don't crash inside AGP's bundled Dokka.** AGP 8.13's `:javaDocReleaseGeneration` task fails with `A failure occurred while executing com.android.build.gradle.tasks.JavaDocGenerationTask$DokkaWorkAction` while "Creating documentation models" against alef-generated Kotlin sources — no surfaced root cause, no stacktrace in the default log, identical failure across runs. The cause is AGP's bundled Dokka choking on something in the generated tree (likely cross-package KDoc refs in the binding-DTO files), and `--stacktrace` requires per-call action changes. Maven Central requires a `*-javadoc.jar` artifact to exist but is content with an empty one — the vanniktech maven-publish plugin's `JavadocJar.Empty()` variant produces exactly that and skips the Dokka task path entirely. Hosted KDoc still lives at `docs.tree-sitter-language-pack.kreuzberg.dev`. Surfaced as `Publish kotlin-android (Maven Central via Gradle)` failure on tslp v1.9.0-rc.2 publish run 26339182362 (job 77541170549). Snapshot regenerated. (`src/backends/kotlin_android/gen_build_gradle.rs`, `tests/snapshots/backends_kotlin_android_snapshot_test__snapshot_basic__packages__kotlin_android__build.gradle.kts.snap`)

- **alef-scaffold/elixir: narrow the hex package `files:` list to source-only paths inside the NIF crate (`Cargo.toml`, `Cargo.lock`, `src/`, `build.rs`) instead of the whole `native/` directory, and add `checksum-*.exs` so RustlerPrecompiled can verify dynamically-downloaded NIFs on the consumer side.** Hex's `mix hex.publish` packs everything matched by `files:` into the tarball; with the broad `native` glob it was sweeping up the local `native/<nif>/target/` build artifacts (1 GB+ after a release-profile compile) and tripping hex's `metadata.config` size limit with `** (Mix) Creating tarball failed: file too big: metadata.config`. The dynamic-download path (`use RustlerPrecompiled` + GitHub-release `base_url`) is the supported runtime install mode, so the Rust source still ships for `force_build` opt-in but `target/` no longer rides along. Surfaced as `Publish Elixir to Hex.pm` failure on tslp v1.9.0-rc.2 publish run 26339182362 attempt 2 (job 77542669829). (`src/scaffold/languages/elixir.rs`)

- **alef-backend-zig: parameterize `KreuzbergError` and `kreuzberg_last_error_code()` in trait-bridge templates.** The Zig trait-bridge code generation templates (`clear_fn_signature.jinja`, `clear_fn_body.jinja`, `unregister_fn_signature.jinja`, `unregister_fn_body.jinja`, `unregister_fn_configured_signature.jinja`) were hardcoded to reference `KreuzbergError` and `c.kreuzberg_last_error_code()`, making them specific to the Kreuzberg consumer. For other consumers like kreuzcrawl, the FFI prefix is different (`kreuzcrawl` → `kcrawl`) and the error type is different (`CrawlError` not `KreuzbergError`). The fix adds `error_type` and `ffi_prefix` template variables to all trait-bridge codegen, sourced from `config.error_type` (already specified in each consumer's `alef.toml`) and the FFI prefix respectively. This makes the Zig backend generic and allows kreuzcrawl and other consumers to generate correct trait-bridge code without consumer-specific hardcoding in alef. (`src/backends/zig/trait_bridge.rs`, `src/backends/zig/gen_bindings/mod.rs`, `src/backends/zig/templates/clear_fn_*.jinja`, `src/backends/zig/templates/unregister_fn_*.jinja`)

- **alef-e2e/go: emit `defer func() { _ = resp.Body.Close() }()` instead of `defer resp.Body.Close()` so generated Go e2e tests pass `golangci-lint`'s `errcheck`.** The single Go HTTP-call renderer (`render_call` in `src/e2e/codegen/go.rs`) emitted a bare `defer resp.Body.Close()` per test, whose discarded error return is flagged by `errcheck`. Because `golangci-lint` auto-discovers every `go.mod` (including `e2e/go/`), this failed the linter on `auth_test.go` and any other generated HTTP test file. Wrapping the close in a deferred closure with an explicit `_ =` discard makes the generated suites lint-clean. (`src/e2e/codegen/go.rs`)

- **alef-backend-java + alef-extract: fix serde(default) round-trip for non-enum fields in Java builder classes.** When a Rust struct field has `#[serde(default)]` or `#[serde(default = "fn")]` (e.g., `ngram_range: (usize, usize)` with `#[serde(default = "default_ngram_range")]`), the Java binding's Builder was incorrectly initializing non-optional Vec fields to `List.of()` based on the type. On JSON round-trip through Jackson, an empty list cannot deserialize as a 2-tuple, so Rust's serde fails with a deserialization error, and the FFI call returns null. Two fixes: (1) extend `has_serde_default()` in `src/extract/extractor/helpers.rs` to detect both bare `#[serde(default)]` and `#[serde(default = "...")]` forms; (2) in the Java backend, make non-optional, non-enum fields with `#[serde(default)]` use boxed types in the Builder (e.g., `List<Long>` instead of primitive `long`) and initialize them to `null`. With `@JsonInclude(NON_ABSENT)` at the class level, null fields are omitted from JSON sent to Rust, allowing Rust's serde to apply its own defaults on deserialization. This prevents round-trip mismatches for tuple fields, collections, and other types where Rust's default is not a primitive zero or empty container. Regression test added: `test_java_serde_default_tuple_field_uses_nullable_type_and_null_default` in `tests/backends_java_serde_default_tuple_test.rs`. Fixes kreuzberg e2e `testConfigKeywords` FFI call failure `dev.kreuzberg.InvalidInputException: Null pointer passed for parameter 'config'`. (`src/backends/java/gen_bindings/types.rs`, `src/extract/extractor/helpers.rs`)

- **alef-e2e/php: skip enum variant accessor paths (e.g., `metadata.format.excel.sheet_names`) before field validation, matching Ruby/C#/Node/Python behavior.** PHP bindings serialize `FormatMetadata` as JSON, making enum variants inaccessible at runtime. Previously the PHP codegen would emit invalid property chains like `$result->getMetadata()->format->excel->sheetNames` triggering runtime warnings and PHPUnit exit non-zero. The fix inserts a pre-check for paths matching `metadata.format.*.`, skipping them with the standard "field not available on result type" comment, identical to other language codegens. Unblocks all 88 PHP e2e tests from exiting with warnings. (`src/e2e/codegen/php.rs`)

- **alef-scaffold/python + alef-e2e/python: emit multi-item TOML arrays one-element-per-line with a 2-space indent and a trailing comma to match `pyproject-fmt`'s canonical output.** Both `packages/python/pyproject.toml` (scaffold) and `e2e/python/pyproject.toml` (e2e codegen) previously emitted multi-item arrays (`keywords`, `authors`, `dependencies`, `dev`, `requires`, `lint.per-file-ignores."tests/**"`) as single-line `["a", "b", "c"]` (scaffold) or `[ "a", "b", "c" ]` (e2e), and the hardcoded `[tool.ruff] lint.ignore` block in `python.rs` packed multiple ignore codes per line. On every `alef all` regen the prek `pyproject-fmt` hook rewrote those lines to the canonical multi-line shape, churning CI Lint with a stale-vs-formatted diff. The new `format_toml_array` helper (added in both `src/scaffold/languages/python.rs` and `src/e2e/codegen/python/config.rs`) emits inline arrays for `len <= 1` and the multi-line shape for `len > 1`, and the hardcoded `lint.ignore` block now lists one rule code per line, so prek's `pyproject-fmt` is a no-op on freshly emitted files. Surfaced regenerating liter-llm against alef 0.18.1 — CI Lint kept reporting `pyproject-fmt` modifications to both `packages/python/pyproject.toml` and `e2e/python/pyproject.toml` on every regen. (`src/scaffold/languages/python.rs`, `src/e2e/codegen/python/config.rs`)

- **alef-cli/pipeline: skip `command.env("PATH", …)` in the e2e test runner so Windows bash subshells inherit the parent's PATH.** On Windows `std::process::Command` treats env keys case-insensitively, so passing `("PATH", lib_dir)` REPLACES the inherited `Path` instead of overriding only the library-search dir, clobbering `pnpm` / `uv` / `node` / other GitHub-Actions-installed tools and leaving the spawned `sh -c "export PATH='lib_dir'\"${PATH:+:$PATH}\"; cd e2e/X && pnpm install …"` with `pnpm: command not found`. The shell-level `export PATH` prefix from `inline_env_in_shell_cmd` already prepends `lib_dir` while preserving inherited PATH via `${PATH:+:$PATH}`, so the per-process override is redundant for PATH; skipping it lets the bash subshell see the runner's full Path. `DYLD_*` and `LD_LIBRARY_PATH` overrides on macOS/Linux are unaffected — they continue to flow through `command.env()` as before. Surfaced as 4 Windows e2e job failures on h2m CI 26337155855 (Node + Python 3.10/3.12/3.14). (`src/cli/pipeline/helpers.rs`)

- **alef-scaffold/ruby: emit a Steepfile that `ignore`s `lib/<gem>/native.rb` so Steep stops tripping on Sorbet `sig { … }` blocks in the generated file.** The magnus backend deliberately emits Sorbet sigs on tagged-enum variant `Data.define` classes (each variant `extend T::Sig` + `sig { returns(...) }` / `sig { params(...).returns(T.attached_class) }` blocks — covered by regression tests at `tests/backends_magnus_gen_bindings_test.rs:2255+`). Sorbet's runtime provides those via the `sorbet-runtime` gem (already a runtime dep in the gemspec), but Steep is a separate type checker — it relies on RBS files, not Sorbet sigs, and reports `Type 'self' does not have method 'sig' / 'params'` on every block plus `RuntimeError: Unexpected self_type: untyped` at the data-class definition. RBS coverage for the same surface lives in `sig/types.rbs`, so steering Steep to the RBS by `ignore "lib/{gem_name_snake}/native.rb"` keeps `steep check` green without dropping Sorbet sig emission. Surfaced as `Test: Ruby (ubuntu-latest, ruby-3.3)` failures on h2m CI 26337155855. (`src/scaffold/languages/ruby.rs`)

- **alef-cli/pipeline: cap consecutive blank lines at 1 (not 2) when normalising `.md` files.** The canonical kreuzberg-dev pre-commit pipeline runs `rumdl-fmt`, whose MD012 rule collapses any multi-blank run to one blank. Prior to this fix `normalize_whitespace` capped at 2 for every file type, so `alef all` + pre-commit produced rumdl-canonical READMEs (1 blank max) while cold `alef readme` produced 2 blanks — and CI's `Validate READMEs` step diffs cold `alef readme` against the committed file and failed on every regen with the same noisy "extra blank line between ## headings" diff. The fix introduces `normalize_whitespace_with_policy(content, is_markdown)`; `normalize_content` selects the markdown policy when the path extension is `.md`. Non-markdown files keep the 2-blank ceiling. Surfaced as h2m CI Lint `Validate READMEs` failure on every alef regen since the workspace collapse. (`src/cli/pipeline/generate.rs`)

- **alef-scaffold/ruby + alef-backend-magnus: emit double-quoted string literals and `%w[...]` for word-only arrays to match rubocop's `Style/StringLiterals: double_quotes` and `Style/WordArray` defaults.** Previously alef emitted single-quoted strings (`s.name = 'foo'`) and word arrays as `['foo', 'bar']`, so rubocop's autocorrect rewrote those files on every fresh regen, churning the polyrepo prek run. Switch the emitters to the canonical rubocop output shape so prek's `rubocop -A` is a no-op on alef-emitted Ruby. The `.rubocop.yml` config now declares `EnforcedStyle: double_quotes` (matching rubocop's default), the gemspec / Rakefile / extconf / Gemfile templates all use `"..."` literals, the `spec.metadata['keywords']` array is emitted as `%w[...]` when every keyword is a bare identifier, and the magnus binding's discriminator dispatcher / per-variant `from_hash` factories emit `hash[:key] || hash["key"]` and `when "variant" then ...` arms. `replace_version_pattern` in `set-version` now writes `VERSION = "x"` / `spec.version = "x"` instead of single-quoted forms. Surfaced regenerating liter-llm against alef 0.18.1. (`src/scaffold/languages/ruby.rs`, `src/backends/magnus/gen_bindings/mod.rs`, `src/backends/magnus/templates/main_rb_wrapper.rb.jinja`, `src/backends/magnus/templates/native_rb_wrapper.rb.jinja`, `src/backends/magnus/templates/version_rb_wrapper.rb.jinja`, `src/cli/pipeline/version.rs`)

- **alef-backend-kotlin-android: emit empty `.gitkeep` content instead of `"\n"` to match what `end-of-file-fixer` settles on.** STY-8 emitted a single newline so that an empty file wouldn't be rewritten by some pre-commit hooks to add a trailing newline. But the canonical pre-commit `end-of-file-fixer` truncates whitespace-only files to 0 bytes, creating an infinite ping-pong between alef regen (`"\n"`) and prek autofix (`""`) on every cycle. Emit `""` directly; pre-commit's `end-of-file-fixer` leaves empty files alone. Surfaced regenerating liter-llm against alef 0.18.1 — CI Lint kept failing on `.gitkeep` end-of-file-fixer modifications. (`src/backends/kotlin_android/gen_jni_skeleton.rs`, `src/cli/pipeline/generate.rs::normalize_whitespace`)

- **alef-codegen: preserve core `Default` for optionalized non-Duration fields in the duration-builder branch of `gen_from_binding_to_core_cfg`.** The general optionalize-defaults branch already emitted `if let Some(__v) = val.field { __result.field = <expr-with-__v>; }` so omitted-in-binding fields kept core's custom `Default` value. The parallel duration-builder branch (active whenever any non-optional `Duration` field appears in the type — e.g. `kreuzcrawl::CrawlConfig::request_timeout`) did not mirror that logic and instead emitted `__result.field = val.field.map(...).unwrap_or_default();`, overwriting `max_redirects: 10` with the primitive zero `0` whenever a JS/WASM caller omitted `max_redirects`. Result: 10 Node redirect e2e tests + many Wasm crawl/redirect tests failed because crawl stopped at the first redirect hop. The fix reuses `field_conversion_to_core_cfg(..., optional=false, ...)` for the conversion expression then wraps it in the same `if let Some(__v) = val.field { __result.field = <expr-with-__v>; }` shape; identical pattern to the existing correct branch, single shared `field_was_optionalized` predicate. (`src/codegen/conversions/binding_to_core.rs`)

- **alef-codegen/wasm: apply the if-let wrapper to Duration fields optionalized by `option_duration_on_defaults`, not just `optionalize_defaults`.** The `field_was_optionalized` predicate in the duration-builder branch only checked `optionalize_defaults && !field.optional`, missing the WASM case where `option_duration_on_defaults=true` (but `optionalize_defaults=false`). This caused the if-let wrapper to not emit for Duration fields in WASM, so fields like `request_timeout: Option<u64>` would still use `.unwrap_or_default()` directly on the Option assignment instead of being skipped when None, allowing other non-Duration fields to preserve core's custom Default values. The fix extends the predicate to also check for `option_duration_on_defaults && !field.optional && Duration-type` so Duration optionalization from either config path triggers the if-let wrapper. Resolves remaining 52 WASM crawl/redirect/strategy e2e failures where tests returned 1 page instead of 3/5 due to redirects being capped at 0. (`src/codegen/conversions/binding_to_core.rs`)

- **alef-codegen/wasm: extend builder-pattern branch to cover all optionalized fields, not just Durations.** The `has_optionalized_fields` check now returns true whenever `option_duration_on_defaults=true && typ.has_default` AND any field is either (a) optional in the IR or (b) a non-optional Duration field forced to Option. This ensures the if-let guard wraps ALL optionalized fields — including genuinely-optional primitives like `max_depth: Option<usize>` — so that JS callers omitting those fields via object literals (serde deserializing to None) don't overwrite core's custom defaults (`max_depth: 5`, `max_pages: 100`, etc.) with primitive zeros. Previously only Duration fields got wrapped, leaving other optionals to be assigned directly, clobbering core defaults. Resolves WASM e2e failures (was 52) where crawls terminated early due to `max_depth=0`, `max_pages=0`, `max_redirects=0` from serde defaults. (`src/codegen/conversions/binding_to_core.rs`)

- **alef-codegen: add `CoreWrapper::Box` to round-trip `Box<str>` core fields through binding `String` round-trips.** `Box<str>` is a common compact-string idiom in storage-heavy structs (SHA-256 hex digests, immutable filenames, etc.) — the resolved IR field type is `String`, but the binding→core conversion needs `.into()` to wrap back to `Box<str>` on the core side. Without explicit wrapper detection, the generic conversion emitted `filename: val.filename` (binding `Option<String>` → core `Option<Box<str>>` ⇒ E0308 mismatched types) and `content_hash: val.content_hash` (E0308 again). The fix adds `CoreWrapper::Box`, teaches `detect_core_wrapper` to return it for `Box<str>` specifically (other Box uses keep their existing classification — opaque trait-objects, regular Box<NamedType>, etc.), and updates `apply_core_wrapper_to_core` / `apply_core_wrapper_from_core` / `binding_helpers::field_default_via_into` to treat `Box` exactly like `Cow` for the String round-trip (both wrappers deref to `&str` and accept `String::into()` / `to_string()`). Composes with the `str → TypeRef::String` fix above so `Box<str>` fields no longer get `sanitized = true` and every backend emits proper accessors + From impls. (`src/core/ir.rs`, `src/extract/extractor/helpers.rs`, `src/codegen/conversions/binding_to_core.rs`, `src/codegen/conversions/core_to_binding.rs`, `src/codegen/generators/binding_helpers.rs`)

- **alef-extract/type_resolver: resolve bare `str` (inside wrapper generics) directly to `TypeRef::String` instead of letting it fall through as `Named("str")` and getting caught by `sanitize_type_ref`.** The standalone `str` form only appears inside wrapper generics like `Box<str>`, `Cow<'_, str>`, `Arc<str>`, `Rc<str>` — those wrappers recurse through `resolve_path_type` on their inner argument (the `&str` reference path doesn't apply). Without an explicit `str` arm the inner resolved to `Named("str")`, which is not in `known_types`, so `sanitize_type_ref` rewrote it to `TypeRef::String` AND set `field.sanitized = true`. Every binding backend then took its "skip sanitized field" branch — for the FFI backend this means `kcrawl_downloaded_document_mime_type` / `..._filename` / `..._content_hash` accessors were never emitted, and downstream e2e codegen had to special-case the field with `"skip"` entries in `[crates.e2e.fields_c_types]` configs. The fix adds `"str" => TypeRef::String` alongside `"String"` in `resolve_path_type`, so wrappers around `str` yield a clean `TypeRef::String` field with `sanitized = false` and every backend emits the accessor normally. (`src/extract/type_resolver.rs`)

- **alef-e2e/zig: suppress SIGABRT from C++ static destructors during zig test --listen=- IPC by installing SIG_IGN on SIGABRT.** The Zig test runner's --listen=- IPC protocol expects a clean process exit without signal interrupts. During global cleanup, leptonica/tesseract C++ destructors call abort(), sending SIGABRT to the process. When unhandled, this breaks the IPC channel and causes the Zig test runner to report "BrokenPipe" / "EndOfStream" errors, even though all tests passed (exit code 0). Installing a signal handler with `signal(SIGABRT, SIG_IGN)` in each test's preamble suppresses the abort signal entirely, allowing the process to exit cleanly after tests complete. The LEAK warnings from ObjectCache destructors remain (harmless noise on process exit), but the signal no longer terminates the process prematurely. (`src/e2e/codegen/zig.rs`)

- **alef-backend-java: emit `new EnumName.Variant()` for sealed-interface tagged-enum field defaults, static references for traditional enums.** When a struct field has `#[serde(default)]` and its type is an enum with serde tagging (internally tagged, e.g. `#[serde(tag = "type")]`), Java generates a sealed interface with nested record variants (e.g. `public sealed interface ChunkSizing { record Characters() implements ChunkSizing {} … }`). The Builder class field default was previously emitted as `ChunkSizing.Characters` (static reference), which Java cannot resolve — records must be instantiated with `new`. Meanwhile, traditional enums (e.g. `#[serde(rename_all = "lowercase")]` on `enum KeywordAlgorithm { Yake, Rake }`), which generate as standard Java `enum` types, require static references. The fix introduces `DefaultEnumVariant` metadata (variant name + `is_zero_field`) in the shared `default_value_for_enum` module, and passes a set of sealed-interface enum names (those with `serde_tag`) to the Java backend. At Builder field-default emission, sealed interfaces use `new {EnumName}.{VariantName}()`, and traditional enums use `{EnumName}.{VariantName}`. This unblocks Java compilation for configs like `ChunkingConfig`, `KeywordConfig`, `LayoutDetectionConfig` that have enum fields with defaults. Surfaced as `cannot find symbol: variable ChunkSizing.Characters` (line 115 in `ChunkingConfig.java` Builder) on kreuzberg's Java e2e; fixed all 88 tests pass, all generated sealed-interface defaults are now syntactically valid Java. (`src/extract/default_value_for_enum.rs`, `src/backends/java/gen_bindings/mod.rs`, `src/backends/java/gen_bindings/types.rs`)

- **alef-backend-php: suppress the auto `#[derive(Default)]` and emit a delegating `impl Default` for binding structs whose core type has a custom `Default`.** When a Rust core type has a custom `impl Default` (e.g. `CrawlConfig::default()` sets `max_redirects: 10`, `PageConfig::default()` sets `marker_format: "\n\n<!-- PAGE {page_num} -->\n\n"`), the previous PHP binding paired `#[derive(Default)]` with struct-level `#[serde(default)]`. The derive emits Rust's primitive defaults (`0` for `i64`, `String::new()` for `String`, `false` for `bool`), and `#[serde(default)]` calls `Self::default()` for each field missing from the inbound JSON. The wrong primitive values then propagated through the auto-generated `From<BindingType> for core::Type` impl, silently clobbering the core's semantic defaults — e.g. a PHP caller that constructed `CrawlConfig::from_json('{"respectRobotsTxt": false}')` ended up with `max_redirects: 0` (no redirects followed), failing 10 PHP redirect e2e tests because the final URL never contained the redirect target. An earlier attempt at this fix layered an `impl Default` after `gen_struct_with_per_field_attrs` but did not suppress the hardcoded `sb.add_derive("Default")` inside the shared generator, producing duplicate-impl compile errors. The corrected fix adds a `emit_delegating_default_impl: bool` field to `RustBindingConfig` and makes the shared struct generators (`gen_struct`, `gen_struct_with_per_field_attrs`, `gen_struct_with_rename`) honour the flag: when `cfg.emit_delegating_default_impl && typ.has_default` they skip the auto `Default` derive AND append `impl Default for {TypeName} { fn default() -> Self { <{core_path} as Default>::default().into() } }`, which composes with the existing `From<core::Type> for BindingType` impl emitted by `gen_from_core_to_binding_cfg`. PHP is the only backend that opts in (it carries struct-level `#[serde(default)]`); other backends (PyO3, NAPI, extendr, etc.) continue to derive `Default` as before. (`src/codegen/generators/mod.rs`, `src/codegen/generators/structs.rs`, `src/backends/php/gen_bindings/mod.rs`, `src/backends/php/gen_bindings/types.rs`, `src/backends/extendr/gen_bindings.rs`, `src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/napi/gen_bindings/mod.rs`, `tests/backends_php_gen_bindings_test.rs`, `tests/codegen_integration_test.rs`)

- **tests: realign go / pyo3 / zig / swift / php-rename tests with the new codegen contracts after recent fixes, and supply `options_type = "CrawlConfig"` to the validation-error fixture so it survives the special-casing removal.** `test_default_config` now asserts the post-STY-9 plain-DTO + `Ptr[T]` default (no `With*`/`New*` helpers without an opt-in allowlist). `test_cfg_gated_fields_excluded_from_constructor` uses a non-feature, non-wasm `cfg` predicate (`any(unix, windows)`) so the test still pins the exclusion contract under the broadened `cfg_present_for_pyo3` (feature gates are now treated as present in pyo3). `streaming_adapter_emits_iterator_pattern_on_opaque_handle` matches the canonical `c.{prefix}_last_error_code() != 0` + `std.json.parseFromSliceLeaky` shape that replaced the undefined `_has_error()` helper. `e2e_swift_client_factory::with_client_factory_emits_client_instantiation` expects `AlefE2EMockServer.baseURL`, the lazy mock-server accessor that replaced the bare `ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]!` lookup. `php_preserves_snake_case_when_serde_rename_all_absent` is rewritten as `php_camel_cases_keys_when_core_type_lacks_rename_all`, pinning the new contract: PHP key renaming sources from the BINDING's `lang_rename_all` (camelCase by default), not the core IR's `serde_rename_all`. (`tests/backends_go_gen_bindings_test.rs`, `tests/backends_pyo3_gen_bindings_test.rs`, `tests/backends_zig_gen_bindings_test.rs`, `tests/e2e_swift_client_factory.rs`, `tests/e2e_php_serde_rename_all_respects_core_config.rs`, `tests/e2e_validation_error_wrapping.rs`)

- **doc tests: fix import paths in `core::version` and `core::jni` rustdoc examples after the workspace-collapse refactor renamed the import root from `crate` to `alef`.** Seven doc tests (`to_r_version`, `to_rubygems_prerelease`, `bridge_class_name`, `bridge_method_name`, `streaming_method_names`, `destructor_method_name`, `jni_symbol`) carried `use crate::core::…` / `crate::core::jni::…` references that no longer compile from a doc-test crate (the doc-test harness reads the crate by its published name `alef`, not `crate`). Rewrites each example to use `alef::core::…`. (`src/core/version.rs`, `src/core/jni.rs`)

- **kotlin-android snapshot: accept the new vanniktech-0.36 `build.gradle.kts` shape (no `SonatypeHost` import; sources jar uses `SourcesJar.Sources()`; `publishToMavenCentral()` is a no-arg call).** Updates the snapshot and the two assertion-based regressions (`build_gradle_uses_vanniktech_maven_publish_plugin`, `buildscript_comes_after_imports_in_correct_order`) so they pin the post-upgrade emission instead of the pre-0.36 SonatypeHost-based form. (`tests/backends_kotlin_android_snapshot_test.rs`, `tests/snapshots/backends_kotlin_android_snapshot_test__snapshot_basic__packages__kotlin_android__build.gradle.kts.snap`)

- **alef-e2e/java: resolve per-fixture mock-server URLs from `System.getProperty("mockServer.<fixture_id>")` first, then env vars.** The generated `mock_url_list` setup line read only `System.getenv("MOCK_SERVER_<FIXTURE_ID>")` then fell back to `MOCK_SERVER_URL/fixtures/<id>`. For fixtures with per-fixture host-root routes (e.g. `batch_crawl_basic`), `MockServerListener` parses the mock-server's `MOCK_SERVERS=` JSON announcement and exposes each fixture URL as `System.setProperty("mockServer.<fixture_id>", ...)` — a system property, not an environment variable. Java tests therefore hit the WRONG URL pattern (`/fixtures/<id>/<path>` on the shared host, which mock-server doesn't serve) and got empty `BatchCrawlResults` for every batch test. Surfaced as 4 BatchCrawl* failures on kreuzcrawl's Java e2e; fixed all 4. The fix prepends a `System.getProperty("mockServer.<fixture_id>", ...)` lookup matching the same resolution chain that single-URL `mock_url` already uses. (`src/e2e/codegen/java.rs`)

- **alef-backend-java: emit `MAPPER.writerFor(constructCollectionType(...))` for `Vec<T>` / `Map<K, V>` marshaling so Jackson preserves the generic element type and re-applies `@JsonTypeInfo` polymorphic discriminators.** The previous `MAPPER.writeValueAsString(list)` lost T at runtime (Java type erasure: at the call site `actions` is just `ArrayList`, with no recoverable element-type info). For polymorphic element types like the tagged-enum DTO `PageAction` (annotated `@JsonTypeInfo(use = NAME, property = "type", visible = false)`), the result was `[{"selector":"#x"}]` — no `"type":"click"` discriminator — and the Rust side failed deserialization with `missing field "type"`. The fix introduces `build_collection_writer_for(...)` + `java_class_literal_for(...)` helpers in `gen_bindings/marshal.rs` and threads the resolved Java `.class` literal into the template as a `java_writer` context variable; the template now emits `var xJson = MAPPER.writerFor(MAPPER.getTypeFactory().constructCollectionType(java.util.List.class, ElemType.class)).writeValueAsString(x)`. Surfaced as 11 InteractionTest errors on kreuzcrawl's Java e2e (`Vec<PageAction>` argument); fixed all 11. (`src/backends/java/gen_bindings/marshal.rs`, `src/backends/java/templates/marshal_vec_map.jinja`)

- **alef-backend-pyo3: alias serde-renamed dict keys in `_to_rust_*_config` converters, and use serde-renamed param names in final pyo3 binding calls.** When a Rust struct field carries `#[serde(rename = "...")]`, the serde wire name differs from the Rust field name. The Python dataclass in `options.py` uses Rust field names (e.g., `max_characters`), but JSON fixtures use serde wire names (e.g., `max_chars`). When `api.py`'s `_to_rust_chunking_config` coerced a fixture-loaded dict to a Python class, it failed with `TypeError: ChunkingConfig.__init__() got an unexpected keyword argument 'max_chars'` because the dict still held serde keys. The fix adds an aliasing block after `if isinstance(value, dict):` that maps every serde-renamed key back to its Rust field name (e.g., `value["max_characters"] = value.pop("max_chars")`) before constructing the dataclass. Additionally, the pyo3 binding's constructor parameters are named after serde renames (matching Rust function signatures), so `field_kwarg.jinja` now uses `field.serde_rename` when available (instead of always using the Rust field name). This ensures the final `_rust.ChunkingConfig(max_chars=value, ...)` call matches the pyo3 constructor's actual parameter names. Surfaced regenerating kreuzberg's Python e2e suite: 87/88 tests passed before the fix, 88/88 after. (`src/backends/pyo3/gen_bindings/functions.rs`)

## [0.18.0] - 2026-05-23

### Changed

- **BREAKING (packaging)**: collapsed the 30-crate workspace into a single root-flat `alef` crate. The binary, CLI surface, generated outputs, and template versions are unchanged. The published crate is renamed from `alef-cli` to `alef` — `cargo install alef-cli` is yanked; install with `cargo install alef --locked`. The 28 internal library crates (`alef-core`, `alef-codegen`, `alef-adapters`, `alef-extract`, `alef-docs`, `alef-e2e`, `alef-readme`, `alef-scaffold`, `alef-snippets`, `alef-publish`, and all 18 `alef-backend-*`) are yanked from crates.io.

### Removed

- All `alef-*` workspace member crates. No external consumer of any internal alef library API was supported; they were never intended as a public Rust API surface, only as internal modularity.

### Fixed

- **alef-e2e/swift: spawn the alef mock-server in-process via `AlefE2EMockServer` and route every `_baseURL` reference through it, instead of requiring callers to pre-set `MOCK_SERVER_URL` in the environment.** SwiftPM/XCTest has no global "before all tests" hook capable of injecting environment variables into the test process (the JVM listener trick used by the Java/Kotlin backends does not exist on macOS/Swift), so the previous codegen path forced consumers to wrap `swift test` in a shell harness that booted the mock-server, exported `MOCK_SERVER_URL`, then invoked the tests — an out-of-band step that broke `swift test` invoked directly and made the suite uncrurnable under Xcode/Indexer. The generated boilerplate now emits an `enum AlefE2EMockServer` with a lazily-initialised `static let baseURL` that walks up to 16 parents looking for `fixtures/`, locates `e2e/rust/target/release/mock-server`, spawns it via `Process`, reads `MOCK_SERVER_URL=<url>` off stdout, and retains the child process for the lifetime of the test binary. A pre-set `MOCK_SERVER_URL` (e.g. exported by CI) short-circuits the spawn. All three URL emission sites (`_baseURL` initial assignment in `TestClientRenderer`, the per-fixture URL in `render_test_method`, the call-arg URL in `build_args_and_setup`) now read from `AlefE2EMockServer.baseURL` instead of `ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]!`. A companion `AlefE2ENoRedirectDelegate: URLSessionTaskDelegate` + module-level `alefE2ESession` URLSession is also emitted so 3xx-asserting fixtures see the real redirect response instead of the followed final response; `URLSession.shared.dataTask` is rewritten to `alefE2ESession.dataTask` throughout. (`src/e2e/codegen/swift.rs`)

- **alef-e2e/kotlin: wrap UUID header-pattern assertions in `Regex(...)` and read the mock-server URL from the `mockServerUrl` system property.** Two defects surfaced running an HTTP consumer's Kotlin e2e suite for the first time. (1) The `<<uuid>>` header assertion emitted `value.matches("<pattern>")`, but Kotlin's `CharSequence.matches` takes a `Regex`, not a `String` — `gradle test` failed to compile with "Argument type mismatch: actual type is 'String', but 'Regex' was expected". It now emits `value.matches(Regex("<pattern>"))`. (2) Generated test bodies read `System.getenv("MOCK_SERVER_URL")`, but the `MockServerListener` spawns the mock-server and publishes its URL via `System.setProperty("mockServerUrl", url)` (the JVM cannot set its own env vars), so every fixture-bound test failed with `java.net.ConnectException` against the default `localhost:8080`. The URL read is now `System.getProperty("mockServerUrl") ?: System.getenv("MOCK_SERVER_URL") ?: "http://localhost:8080"`, matching the Java backend. (`src/e2e/codegen/kotlin.rs`)

- **alef-e2e/swift: sanitize header-derived local-variable names and stop redeclaring `_bodyData` across body assertions.** Running an HTTP consumer's Swift e2e suite for the first time hit two compile errors. (1) The `<<uuid>>` header assertion bound `let _hdrVal_<header-name> = ...` using the raw lowercased header name, so `x-request-id` produced the invalid identifier `_hdrVal_x-request-id` (parsed as subtraction). A safe `var_suffix` (non-alphanumerics → `_`) is now derived for the binding. (2) `render_assert_json_body`, `render_assert_partial_body`, and `render_assert_validation_errors` each emitted `let _bodyData = try XCTUnwrap(_responseData)`; a fixture exercising more than one (e.g. a full body plus validation errors) produced "invalid redeclaration of '_bodyData'". The response data is now unwrapped inline (one leading `try` covers the nested `XCTUnwrap`), and the validation path uses a distinct `_errorsBodyObj` to avoid colliding with the partial-body path's `_bodyObj`. (`src/e2e/codegen/swift.rs`)

- **alef-backend-pyo3: skip trait-bridge wrapper emission for clear_/register_/unregister_ names that are already emitted as regular api.functions.** Trait bridges declare `clear_fn`/`register_fn`/`unregister_fn` to wire up plugin registration; the same names are usually ALSO configured as regular `[crates.e2e.calls.clear_*]` calls and end up in `api.functions` with richer Rust-source doc-comments. The Python facade generator (`gen_api_py` in `src/backends/pyo3/gen_bindings/functions.rs`) used to emit both unconditionally, producing F811 (redefinition of unused name) in `api.py` for every clear_* in kreuzberg's plugin registry — 6 duplicates that ruff blocked at lint time. The fix collects the names emitted by the main `api.functions` loop into an `AHashSet` and skips any trait-bridge wrapper whose name is already in that set; the api.function-emitted version wins because its doc-comment is sourced from the Rust source, not synthesized from the call name. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-e2e/php: wrap each tagged-enum / has_default DTO element in `{ElemType}::from_json('{...}')` instead of emitting raw assoc arrays for `Vec<T>` arguments.** The PHP binding's `#[php_class]` `FromZval` impl only accepts class instances, so the previous codegen path that emitted `[["type" => "click", "selector" => "#id"], ...]` for a `Vec<PageAction>` argument failed with `Exception: Failed to convert array element to PageAction` on every interact / streaming / batch test. Every alef-emitted has_default/has_serde struct already has a `from_json(String) -> PhpResult<Self>` static method generated via `serde::Deserialize`, so the e2e codegen now emits `PageAction::from_json('{"type":"click","selector":"#id"}')` per element — the portable path that works without adding a per-class `FromZval` impl. Affects both `emit_php_batch_item_array` (Vec element fallback) and the inline `Vec<T>` tagged-enum array branch in `build_args_and_setup`. Surfaced as 20 PHP e2e errors (12 interact + 8 stream) regenerating kreuzcrawl against alef 0.18.0 HEAD. (`src/e2e/codegen/php.rs`)

- **alef-e2e/field_access: `namespace_stripped_path` now returns `None` when `[e2e].result_fields` is empty, instead of unconditionally stripping the first dotted segment.** Prior behaviour treated _every_ first segment of a dotted field path as a virtual namespace prefix when the consumer hadn't configured `result_fields`, collapsing real nested-struct paths like `metrics.total_lines` to their leaf segment `total_lines`. The C e2e codegen then took the flat-field code path at `c.rs:1431-1467` and emitted accessors against the _root_ result type (e.g. `ts_pack_process_result_total_lines(result)` instead of the correct `ts_pack_process_result_metrics(result) -> ts_pack_file_metrics_total_lines(metrics)`) — those flat-named functions do not exist in the FFI header, so every test referencing a nested-struct field failed `gcc` with "call to undeclared function". The stripping logic was originally a Python/C# convenience for fixtures that wrote `browser.browser_used` against a flat `BrowserUsed` field; the new behaviour preserves that convenience for downstreams that opt in by declaring `result_fields`, while restoring the literal interpretation for everyone else. Surfaced regenerating tslp's C e2e suite against alef HEAD: 20 compile errors before the fix, 0 after. Added `namespace_stripped_path_returns_none_when_result_fields_empty` regression. (`src/e2e/field_access.rs`)

- **alef-backend-pyo3: include cfg-gated fields in struct constructors by checking if cfg predicates are satisfied for PyO3 compilation.** PyO3 bindings compile to native Python (never WASM), so feature-gated fields like `#[cfg(feature = "pdf")]` are deterministically present or absent at binding-compilation time. The prior `cfg_present_on_native_target` function conservatively rejected all feature gates, assuming they might be optional; it only accepted `not(target_arch = "wasm32")`. This caused constructors for `ExtractionConfig` and other `has_default` types to omit parameters for `pdf_options`, `keywords`, `html_options`, `html_output`, `layout`, `tree_sitter`, etc., even though the PyO3 crate's `Cargo.toml` explicitly enabled these features. Callers using the hand-written `api.py` wrapper faced `TypeError: __new__() got an unexpected keyword argument 'pdf_options'` at runtime. The fix introduces `cfg_present_for_pyo3()` which accepts both `not(target_arch = "wasm32")` AND feature gates (and their `any(...)` combinations), since PyO3's compilation unit controls which features are statically enabled. This matches the actual compilation behavior: when kreuzberg-py's `Cargo.toml` says `features = [..., "pdf", "html", "tree-sitter", ...]`, those fields are always present. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-backend-wasm: emit `[package.metadata.cargo-machete] ignored = [...]` as a multi-line array so `cargo-sort -w` does not rewrite the manifest on every regen.** The wasm Cargo.toml template inlined the six entries on a single line (`ignored = ["futures", "futures-util", "js-sys", "wasm-bindgen-futures", "serde", "serde_json"]`) which exceeded `cargo-sort`'s 100-char line budget; on the first prek pass `cargo-sort` reformatted the file, the `alef:hash:` line on the same file then mismatched its post-format content, and `alef verify` rejected the next commit with "Stale bindings detected". The template now emits the canonical sorted multi-line form (four-space indent, trailing comma) — matching `cargo-sort`'s own output so the formatter is a no-op. Mirrors the swift backend's existing multi-line `features` rule. (`src/backends/wasm/gen_bindings/mod.rs`)

- **alef-backend-zig: emit `std.json.parseFromSliceLeaky(<ItemType>, std.heap.c_allocator, _json_slice, ...)` inline in the streaming iterator's `next()` body instead of calling the never-defined helper `parse{item_type}FromJson(_json_slice)`.** The streaming wrapper called a `parse<ItemType>FromJson` function that alef never emitted, so every test file pulling the generated zig module into compilation failed with `use of undeclared identifier 'parseChatCompletionChunkFromJson'`. The fix inlines `std.json.parseFromSliceLeaky` with the c_allocator so the parsed value's string slices outlive the FFI-owned JSON buffer that the `defer c.<prefix>_free_string(_json)` line releases on function exit. (`src/backends/zig/gen_bindings/opaque_handles.rs`)

- **alef-codegen: remove project-specific hardcoded type-name fallbacks across PHP/Python e2e generators and napi/go/php streaming adapter wrappers.** Six call sites baked kreuzcrawl-specific defaults into shared codegen: `src/e2e/codegen/php.rs` hardcoded `CrawlConfig` as the handle config type (and an unconditional `use {namespace}\\CrawlConfig;` import) for every consumer that wired a `handle`-typed e2e arg; `src/e2e/codegen/python/test_file.rs` and `src/e2e/codegen/python/test_function.rs` fell back to `CrawlConfig` when the call's `options_type` was unset; `src/backends/napi/gen_bindings/functions.rs` defaulted `adapter.owner_type` to `CrawlEngineHandle`; `src/backends/go/gen_bindings/functions.rs` defaulted `owner_type`/`item_type`/`request_type` to `EngineHandle`/`Item`/`Request`; `src/backends/php/gen_bindings/mod.rs` defaulted `owner_type` to `EngineHandle`. None of these defaults are valid for any consumer other than kreuzcrawl, and the wrong type name silently produced uncompilable output rather than a clear config-error. The fix replaces every `.unwrap_or("…")` fallback with an `unwrap_or_else(|| panic!(…))` that names the missing config field (`[e2e.call] options_type` for the handle-config sites; `[[adapters]] owner_type`/`item_type`/`request_type` for the streaming-adapter sites). The PHP handle path now threads the resolved `options_type` through `build_args_and_setup` so the existing `options_type_imports` collector emits the correct `use {namespace}\\{T};` automatically. (`src/e2e/codegen/php.rs`, `src/e2e/codegen/python/test_file.rs`, `src/e2e/codegen/python/test_function.rs`, `src/backends/napi/gen_bindings/functions.rs`, `src/backends/go/gen_bindings/functions.rs`, `src/backends/php/gen_bindings/mod.rs`)

- **alef-e2e/php: source the JSON key rename strategy from the PHP language config (`serde_rename_all_for_language(Language::Php)`) rather than the Rust core type's `serde_rename_all`, because PHP `from_json` deserializes into the BINDING struct (not the core type), and the PHP backend emits every binding struct with the language-effective `#[serde(rename_all = "...")]` regardless of what the core source carries.** The previous BLK-7 attempt looked up `td.serde_rename_all` per type from the IR — which reflects the CORE struct's attribute — but `impl ConversionOptions { fn from_json(json) -> PhpResult<Self> { serde_json::from_str(&json) } }` is parsed against `Self`, the binding type. For h2m, the core `ConversionOptions` has no `rename_all`, but the PHP-emitted `ConversionOptions` carries `#[serde(default, rename_all = "camelCase")]` (added by `gen_struct` in `src/backends/php/gen_bindings/types.rs` from `lang_rename_all`). The fixture generator therefore left snake_case keys in the encoded JSON, every camelCase property silently fell to its `#[serde(default)]` value, and 32 / 7-error PHP e2e failures appeared across `include_document_structure`, `strip_tags`, `exclude_selectors`, etc. The fix threads `php_lang_rename_all: String` from `generate()` through `render_test_file` → `render_test_method` → `build_args_and_setup`, and the three `json_to_php_camel_keys_with_types` callers now pass `Some(php_lang_rename_all)` instead of `get_serde_rename_all(<type>, type_defs)`. The recursion case in `json_to_php_camel_keys_with_types` propagates the parent's `serde_rename_all` into nested objects (since alef PHP applies the same `lang_rename_all` to every binding struct uniformly). Removes the dead `get_serde_rename_all` and `json_to_php_camel_keys` wrappers. (`src/e2e/codegen/php.rs`)

- **alef-backend/php: add `#[serde(default)]` to flat-struct representation of tagged enums so from_json() succeeds on empty/partial JSON.** Flat-struct tagged enums (e.g., `EmbeddingModelType` with a `type_tag: String` discriminator + optional payload fields) lacked struct-level `#[serde(default)]`, causing deserialization from `{}` to fill all fields with their Rust defaults: `type_tag` → empty string, payload fields → None. When the empty `type_tag` was converted back to the Rust core enum, the fallback arm produced `Preset { name: Default::default() }` (also empty), triggering "Unknown embedding preset: <empty>" at runtime. The template now includes `#[serde(default)]` so missing fields use the struct's `Default` impl (all fields empty), and the round-trip behaves correctly through the `From` impl's wildcard arm. (`src/backends/php/templates/php_flat_enum_struct_start.jinja`)

- **alef-backend-magnus: coerce Default expression to binding field type when JSON field is mapped to String.** The Magnus type mapper converts `TypeRef::Json` to `String` (since Magnus lacks a native `serde_json::Value` type), but the explicit Default impl generator was still emitting `serde_json::json!({})` for the original Rust type. This produced a type mismatch: the struct field is `schema: String` but the Default expression was `serde_json::json!({})` (a `Value`). The fix detects when the original field type is `TypeRef::Json` but the binding type is `String`, and substitutes `String::new()` for the default expression. This ensures the Default impl's field assignments match the struct's declared field types. (`src/backends/magnus/gen_bindings/classes.rs`)

- **alef-cli/all: repopulate `current_gen_paths` from the e2e cache manifest on a cache hit so the orphan-cleanup pass does not delete every previously-generated e2e file.** `alef all` only adds an e2e output path to `current_gen_paths` inside the `else` branch (the regeneration path). When the e2e stage hit its cache (`[e2e] up to date (skipping)`), `current_gen_paths` lacked the e2e files; `pipeline::cleanup_orphaned_files` then walked the e2e output dirs, found every file alef-headered but not in the path set, and deleted it. Surfaced regenerating h2m against alef 0.18.0: a cached `alef all` removed 157 e2e files across go/python/elixir/zig/ruby/swift/typescript/wasm. The fix adds `cache::read_stage_paths(crate_name, stage)` which reads the existing manifest written by `cache::write_stage_hash` and, in the e2e cache-skip branch, inserts each cached output path back into `current_gen_paths` so cleanup treats them as valid. (`src/cli/cache.rs`, `src/main.rs`)

- **alef-backend-kotlin-android: fix `build.gradle.kts` template for vanniktech gradle-maven-publish-plugin 0.36.0 by removing unused `SonatypeHost` import, changing `SourcesJar.Software()` to `SourcesJar.Sources()`, and removing the `SonatypeHost.CENTRAL_PORTAL` argument from `publishToMavenCentral()`.** Vanniktech 0.36.0 removed the `SonatypeHost` enum, renamed the sources-jar variant from `Software` to `Sources`, and made `publishToMavenCentral()` a no-arg function with Central Portal as the default destination. Updated `src/backends/kotlin_android/gen_build_gradle.rs` to match the current plugin API. (`src/backends/kotlin_android/gen_build_gradle.rs`)

- **alef-backend-java: emit `java.util.stream.Stream<T>` (not `Iterable<T>`) for the facade's static streaming wrappers so the body `return engine.<method>(req);` typechecks against the instance method's actual return type.** The instance method emitted by `gen_bindings/types.rs` via `templates/streaming_iterator_method.jinja` returns `java.util.stream.Stream<T>`, but the facade's static wrapper in `gen_bindings/facade.rs` declared `Iterable<T>` — and `Stream<T>` does NOT implement `Iterable<T>` in the JDK (only has `iterator()`). javac rejected the generated `LiterLlm.java` with `incompatible types: java.util.stream.Stream<ChatCompletionChunk> cannot be converted to java.lang.Iterable<ChatCompletionChunk>` on every consumer with a streaming adapter. The wrapper now declares the fully-qualified `java.util.stream.Stream<T>` return type (no import needed, matches the qualified form already used by the instance method). Surfaced as `mvn verify` compile failure regenerating liter-llm against alef 0.18.0 HEAD. (`src/backends/java/gen_bindings/facade.rs`)

- **alef-backend-rustler: emit the `StreamError` exception module at top level AFTER the outer `defmodule <AppModule>` closes, not inside it.** When the rustler streaming wrapper emitted `defmodule <AppModule>.StreamError do ... end` INSIDE `defmodule <AppModule>`, Elixir's nested-defmodule semantics prepended the outer module name to the inner `defmodule` LHS, producing `Elixir.<AppModule>.<AppModule>.StreamError`. The compiler then rebound `<AppModule>` to refer to the doubly-nested namespace inside the outer module's scope, so every `<AppModule>.Native.X` call in wrapper bodies resolved to `<AppModule>.<AppModule>.Native.X` — undefined at compile time — and the runtime call raised `(UndefinedFunctionError) function LiterLlm.LiterLlm.Native.defaultclient_chat_async/2 is undefined`, breaking every NIF call in every wrapper. Surfaced as `161 tests, 161 failures` on liter-llm's elixir e2e suite. The fix moves the exception module emission below the outer `\nend\n` push (after `content.trim_end_matches('\n')`), then strips the 2-space indent from the template (since it's now at top level) so `mix format` doesn't reflow the block. (`src/backends/rustler/gen_bindings/mod.rs`)

- **alef-backend-zig: replace undefined `_has_error()` call in the streaming `next()` body with the canonical `c.{prefix}_last_error_code() != 0` form used everywhere else in the emitted Zig module.** The streaming iterator's mid-stream error check called `_has_error()` — a helper that was never emitted — instead of `c.{prefix}_last_error_code() != 0`, the form used by every other error-check site in the emitter (e.g., `create_client`, `from_json` paths). Surfaced as `use of undeclared identifier '_has_error'` on every test that pulled the generated zig file into compilation. The fix interpolates `{prefix}` from the existing `prefix` argument to `emit_streaming_struct`. (`src/backends/zig/gen_bindings/opaque_handles.rs`)

- **alef-backend-rustler: wrap the generated `base_url:` attribute in `use RustlerPrecompiled` onto its own line so the emitted `lib/<app>/native.ex` is idempotent under `mix format --check-formatted`.** The `use RustlerPrecompiled` block emitted the GitHub releases URL inline, e.g. `base_url: "https://github.com/kreuzberg-dev/liter-llm/releases/download/v#{Mix.Project.config()[:version]}"`. For most consumer repos (kreuzberg, liter-llm), that single line exceeds Elixir's default 98-column line length and `mix format` rewrites it to a wrapped form (`base_url:\n      "..."`). The shared prek `mix format` hook then flagged the generated file on every fresh regen, churning the polyrepo prek run. The template now emits the wrapped form unconditionally so the file is already in the canonical shape `mix format` would produce. Added regression assertions in `tests/backends_rustler_gen_public_api_test.rs::test_native_ex_has_all_nif_stubs` pinning both the wrap shape and a length sanity-check on the continuation line. Surfaced regenerating liter-llm against alef HEAD. (`src/backends/rustler/template_env.rs`, `tests/backends_rustler_gen_public_api_test.rs`)

- **alef-backend-pyo3: skip the trait-bridge options field from `sorted_fields` so the constructor does not emit it twice when it is force-restored via `never_skip_cfg_field_names`.** `replace_constructor_with_serde_rename` already filtered `bridge_field_name` from the `defaults` and `assignments` iterators but not from `params`, which is built off `sorted_fields`. Once `never_skip_cfg_field_names` started including cfg-gated trait-bridge fields on `has_default` types (the kreuzberg / h2m all-features case where `visitor` is `#[cfg(feature = "visitor")]`), the field appeared both in `params` (from `sorted_fields`) AND in `all_params` via the `bridge_param` append, producing two `visitor: Option<PyVisitorRef>` parameters in the same `fn new(...)` signature. PyO3 rejected the duplicate (`identifier 'visitor' is bound more than once in this parameter list`) and the `#[pyo3(signature)]` macro reported `missing signature entry for argument visitor` (the macro only counted unique idents). Surfaced regenerating h2m against alef HEAD (run-local; pre-release). The fix hoists `bridge_field_name` above `sorted_fields` and adds the same `bridge_field_name.is_none() || f.name != bridge_field_name.unwrap()` filter to the `sorted_fields` collector so the bridge field is contributed exactly once, by the existing `bridge_param` append at the end of the parameter list. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-readme: add `test_alef_all_and_cold_readme_produce_same_output` regression covering the previously-observed divergence between `alef all` and a cold `alef readme` run.** The investigation (`src/readme/mod.rs`) confirmed that both commands now route through the same `crate::readme::generate_readmes()` entry point with fresh minijinja `Environment` instances per render (no shared mutable state, no cached template branches), so the divergence reported during the h2m v0.17.34 regen is no longer reproducible against the post-restructure HEAD. The new test creates a tmp dir with a minijinja template, calls `generate_readmes` twice in succession, and asserts byte-identical output — guarding against regressions if a future change reintroduces shared state. (`src/readme/mod.rs`)

- **alef-backend-dart: migrate product-type DTO emission to `@freezed` + `json_serializable`, matching the existing sum-type path, so every Dart DTO carries `copyWith` / value-equality (`==`/`hashCode`) / `toJson` / `fromJson` instead of hand-rolled plain classes with no conveniences.** The Dart sum-type path already emitted `@freezed` sealed classes with the `.freezed.dart` part-of stub, but product-type DTOs (e.g. `ActionResult`, `ArticleMetadata`, `BatchCrawlResult`, `BatchCrawlResults` in kreuzcrawl) were emitted as plain `class <Name> { final ... ... }` with no `copyWith`, no value-equality, no JSON support — consumers had to use raw identity comparison and hand-roll `toJson`. The discrepancy was jarring (half the DTOs had freezed conveniences, half didn't). The FFI Dart emitter now uses the freezed shape uniformly: single-field DTOs render `freezed_class_single_param.jinja`; multi-field DTOs render `freezed_class_open.jinja` + per-field `freezed_required_param.jinja` + `freezed_constructor_close.jinja` (which emits the `_<Name>` factory pair and the `fromJson` factory). The emitted lib file now imports `package:json_annotation/json_annotation.dart` and adds `part '<lib>.freezed.dart'; part '<lib>.g.dart';` part-of directives. The Dart FFI scaffold's `pubspec.yaml` now carries `freezed_annotation` / `json_annotation` (runtime) and `freezed` / `build_runner` / `json_serializable` (dev-deps) so consumers can run `dart run build_runner build` to materialize the part-files. Added five regression tests covering single-field, multi-field, part-of directives, json_annotation import, and pubspec dev-deps. (`src/backends/dart/gen_ffi/types.rs`, `src/backends/dart/gen_ffi/mod.rs`, `src/scaffold/languages/dart.rs`, `src/backends/dart/template_env.rs`, `src/backends/dart/templates/freezed_*.jinja`)

- **alef-backend-go: stop emitting per-field `With<Struct><Field>` functional-options + `<Struct>Option` type alias + `New<Struct>(opts ...<Struct>Option) <Struct>` constructor for every public struct by default; emit only the plain struct plus a single shared `func Ptr[T any](v T) *T` helper, and gate the functional-options shape behind an explicit `[crates.go.functional_options]` allowlist.** The previous default emission was canonical for behaviour-knob types (cf. `grpc.DialOption`) but inappropriate for pure data DTOs whose fields are all JSON-serialized — kreuzcrawl's Go binding exposed _hundreds_ of `WithContentConfigOutputFormat` / `WithContentConfigPreprocessingPreset` / ... helpers per config struct. Consumers now write `ContentConfig{OutputFormat: Ptr("md"), PreprocessingPreset: Ptr("fast")}` against the plain struct shape every standard-library type uses. The `Ptr[T any]` generic helper is emitted exactly once per generated `binding.go` file (top-level, before any struct) via a new `ptr_helper.jinja` template. Opt-in shape: `[crates.go] functional_options = ["DialOptions", "ServerConfig"]` keeps the previous `With*` + `New*` constructor pair for those specific struct names. Added regression tests covering both the no-allowlist default (no `With*`) and the in-allowlist opt-in. (`src/core/config/languages.rs`, `src/backends/go/gen_bindings/mod.rs`, `src/backends/go/gen_bindings/types.rs`, `src/backends/go/template_env.rs`, `src/backends/go/templates/ptr_helper.jinja`)

- **alef-core/config: add `[crates.{node,wasm}.crate_dir]` per-language override for `package_dir()` so consumers whose actual Rust crate directory does not follow the `crates/{name}-{lang}` formula can point alef at the real location.** `package_dir(Language::{Node,Wasm})` hardcoded `format!("crates/{}-{lang}", self.name)`, where `self.name` is the alef config root `name` (e.g., `"html-to-markdown-rs"` for h2m). For polyglot consumers that _strip_ the `-rs` suffix from their Node/Wasm Rust crates (h2m keeps `-rs` only on `-ffi`/`-jni`, while Node lives at `crates/html-to-markdown-node` and Wasm at `crates/html-to-markdown-wasm`), every setup/test/clean/format task targeting Node/Wasm referenced a non-existent path and the polyrepo had to maintain per-package Taskfile shims to compensate. The new optional `crate_dir: Option<String>` field on `NodeConfig` and `WasmConfig` overrides the formula when set; default = `None` (preserves current behavior). Added five regression tests covering both overrides, default-fallback, and the h2m end-to-end scenario. (`src/core/config/languages.rs`, `src/core/config/resolved/lookups.rs`)

- **alef-docs: demote internal headings inside spliced Rust doc-comments relative to the splice level, so generated `docs/reference/types.md` / `configuration.md` / `errors.md` / function pages satisfy `rumdl MD001` (heading increments by ≤1).** The docs generator emits fixed structural levels (`##` page, `###` section, `####` per-item) but item bodies splice in Rust doc-comment markdown that contains its own headings (commonly `## Stream Limits`, `## Examples`); the resulting hierarchy skipped levels (e.g., `####` followed by `##`), violating MD001 and triggering `rumdl` to rewrite the docs on every prek pass. Five splice sites in `src/docs/mod.rs` now wrap doc-comment content through a new `demote_headings(doc, shift)` helper before emission: `render_function`, `render_enum_item`, `render_error_item` (shift = 2, splice level `####`), `generate_configuration_doc` and `generate_errors_doc` (shift = 1, splice level `###`), and `generate_types_doc` (shift = 2, splice level `####`). A companion `check_monotonic_headings` validator was added to `src/docs/doc_cleaning.rs`. Added `test_generated_docs_have_monotonic_heading_increments` and `test_function_doc_with_internal_headings_are_demoted` regression tests covering both the demotion logic and the end-to-end emission. (`src/docs/mod.rs`, `src/docs/doc_cleaning.rs`)

- **alef-scaffold/python: emit `license-files = ["LICENSE"]` explicitly in the generated `[project]` table so maturin/PEP 639 wheel validation finds and bundles the license file relative to the pyproject directory.** Previously the scaffold emitted `license = "MIT"` (PEP 639 SPDX string) without `license-files`, and `[tool.maturin]` pointed `manifest-path` at an out-of-tree Cargo crate (`../../crates/<name>-py/Cargo.toml`). Maturin resolved license discovery from the Cargo manifest dir but assembled the wheel from the pyproject dir — so the wheel METADATA carried `License-File: LICENSE` while the wheel itself didn't bundle the file, and PyPI's upload validator rejected with `400 License-File LICENSE does not exist in distribution file` (liter-llm v1.4.0-rc.29 publish failure, run `26329709088`). The new explicit `license-files` field combined with the existing `scaffold_license_files` step (which already copies the workspace-root `LICENSE` into `packages/python/LICENSE` when the language is enabled) ensures both halves of the build agree on which directory the file lives in. Added regression test `test_scaffold_python_license_files_field` asserting the field is emitted in the `[project]` section. (`src/scaffold/languages/python.rs`)

- **alef-e2e/php: thread per-type `serde_rename_all` IR metadata into `json_to_php_camel_keys` so generated PHP test calls only camelCase JSON keys when the receiving struct actually carries `#[serde(rename_all = "camelCase")]`.** `json_to_php_camel_keys` previously applied `to_lower_camel_case()` to every object key it saw, on the false premise that "PHP binding structs use `#[serde(rename_all = "camelCase")]`". In reality, PHP `from_json` uses `serde_json::from_str` against the kreuzberg core type directly, so the effective serde attributes are whatever the _core_ struct carries — and structs like `PageConfig` (and several other kreuzberg core configs: `concurrency`, `content_filter`, `email`, `llm`, `merge`, `ocr`, `pdf`) lack `rename_all = "camelCase"`. The result was that camelCased keys silently dropped to `#[serde(default)]` values and `ContractTest::test_config_pages` failed with `Failed asserting that false is true.` because the `pages.extract_pages` config silently fell back to its default. The function signature now accepts `serde_rename_all: Option<&str>` and only applies camelCase when the receiving struct's setting equals `Some("camelCase")`; otherwise snake_case keys pass through unchanged. A new `get_serde_rename_all` helper looks up the per-type setting from the IR `type_defs`; `build_args_and_setup` and all three call sites (L1452, L1596, L1626) now consult it. Added regression test `e2e_php_serde_rename_all_respects_core_config.rs` covering both branches. (`src/e2e/codegen/php.rs`)

- **alef-extract + alef-backend-{php,java}: introduce a shared `default_value_for_enum` IR helper and route both the PHP tagged-enum `From` impl and the Java Builder default through it, so backends materialize the source enum's actual `impl Default` variant (or its `#[default]` marker) instead of zero-valuing the first variant.** Two related defects shared a root cause: neither backend consulted the IR's per-enum default-variant metadata. PHP's `gen_flat_data_enum_from_impls` wildcard arm fell back to `Default::default()` on the first variant — producing `Preset { name: "" }` for `EmbeddingModelType`, which the kreuzberg Rust core rejected with `Embedding error: Unknown embedding preset:` on the PHP `test_embed_texts_async_happy` fixture. Java's `gen_builder_nested_class` carried an explicit in-source TODO and an embarrassing `match name.as_str() { "KeywordAlgorithm" => "KeywordAlgorithm.Yake", _ => "null" }` that would regress to the v0.17.35-era `Null pointer passed for parameter 'config'` FFI error the next time any other `#[serde(default)]` enum field landed. The new `src/extract/default_value_for_enum.rs` exposes `default_variant_name(enum_def) -> Option<String>` (returns the `#[default]`-marked variant, falling back to the first variant) and `enum_default_variants_map(api) -> AHashMap<String, String>` for backends that need a lookup. PHP now uses `enum_def.variants.iter().find(|v| v.is_default).or_else(|| variants.first())` in the fallback arm; Java threads the defaults map into `gen_builder_nested_class` and replaces the hardcoded match with `enum_defaults.get(name).map(|variant| format!("{name}.{variant}"))`. Added `e2e_php_enum_default_variant_in_from_impl.rs` and `e2e_java_serde_default_enum_uses_default_variant.rs` regression tests covering both backends. (`src/extract/default_value_for_enum.rs`, `src/extract/mod.rs`, `src/backends/php/gen_bindings/types.rs`, `src/backends/java/gen_bindings/mod.rs`, `src/backends/java/gen_bindings/types.rs`)

- **alef-backend-pyo3: emit `Option<NestedType>` (default `None`) with an `unwrap_or_else(|| Self::default().field)` fallback for non-optional `has_default` nested-struct fields on `has_default` parents, so callers can pass `field=None` without triggering `TypeError: 'None' is not an instance of '<NestedType>'`.** Previously, `replace_constructor_with_serde_rename` emitted these fields as `field: <NestedType>` with a `#[pyo3(signature)]` default of `Self::default().field` — the signature default fired only when the kwarg was _absent_, so an explicit `field=None` (as emitted by both the hand-written Python wrapper's `_to_rust_*` converter and the e2e codegen's `emit_json_object_arg`) was rejected by PyO3's argument extractor at call time. The new rewrite makes the param `Option<NestedType>` with default `None`, accepts both an omitted kwarg AND an explicit `None`, and falls back to the parent's `Self::default().field` via `unwrap_or_else`. Affected 98/262 h2m Python e2e failures (run `26330126894`, every os×py-version matrix entry) caused by `ConversionOptions.preprocessing: PreprocessingOptions` rejecting `preprocessing=None`. Mirrors the pattern from alef commit `16869a00` for `Option<T>` constructor params. Added regression test `test_has_default_struct_with_nested_struct_field_accepts_none`. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-backend-rustler: raise a typed `<Crate>.StreamError` exception from inside the `Stream.unfold` closure on mid-stream NIF errors instead of returning `nil`, so consumers see real failures instead of silent truncation at end-of-stream.** The previous wrapper short-circuited `{:error, reason}` to `nil` — the EOS sentinel for `Stream.unfold`, which `Enum.to_list/1` interprets as a clean completion. Mid-stream FFI cancels, upstream HTTP failures, and deserialization errors were therefore invisible to the consumer; callers could not distinguish "stream finished normally" from "stream ran for a while then crashed." The generator now emits a `defmodule <App>.StreamError do defexception [:message, :reason, :adapter] ... end` block once per generated `.ex` file (guarded on `!streaming_adapters.is_empty()`), and the unfold closure raises it with the captured reason and adapter atom. Consumers can `try`/`rescue` the typed exception or let it propagate. Added three regression tests verifying the exception module is emitted, the closure raises with the correct fields, and no exception module is emitted when the crate has no streaming adapters. (`src/backends/rustler/gen_bindings/mod.rs`, `src/backends/rustler/template_env.rs`, `src/backends/rustler/templates/elixir_streaming_unfold_wrapper.jinja`, `src/backends/rustler/templates/elixir_stream_error_exception.jinja`)

- **alef-backend-zig: emit a per-adapter streaming iterator struct with `next()` / `deinit()` methods instead of an inline drain loop that eagerly collects every chunk into a JSON-array `[]u8`.** Generated Zig streaming wrappers previously drained the underlying FFI iterator (`<crate>_<name>_stream_start` → `_next` → `_free`) inside the public function body, returning a single `[]u8` containing the bundled JSON array. Callers could not apply backpressure, inspect partial results, cancel mid-stream, or distinguish clean end-of-stream from a runtime error — and paid the full memory cost of every chunk before seeing any of them. The new shape emits `pub const <Item>Stream = struct { _handle: ..., pub fn next(...) (...||error{OutOfMemory})!?<Item>, pub fn deinit(...) void };`, where `next()` returns `null` on clean EOS (errno == 0) and an error union on mid-stream failure (errno != 0). The public streaming function now returns the struct; callers iterate it with `while (try stream.next()) |chunk| { ... }` and `defer stream.deinit();`. Mirrors the `AsyncThrowingStream` pattern emitted by the swift backend. Updated the existing `streaming_adapter_emits_iterator_pattern_on_opaque_handle` test and added a new `streaming_struct_has_next_and_deinit_methods` regression test. (`src/backends/zig/gen_bindings/opaque_handles.rs`)

- **alef-cli/version: refresh per-ecosystem lockfiles after `sync-versions` rewrites manifests, so frozen-lockfile CI installs do not abort with `[ERR_PNPM_OUTDATED_LOCKFILE]`.** `sync_versions` previously rewrote every node `package.json`, platform-sibling `npm/*/package.json`, `Cargo.toml`, `composer.json`, and `mix.exs` but left `pnpm-lock.yaml` / `Cargo.lock` / `composer.lock` / `mix.lock` pinned at the previous version's specifiers. CI's `pnpm install --frozen-lockfile` then aborted (e.g. liter-llm v1.4.0-rc.29 publish run `26329709088`: `specifiers in the lockfile don't match specifiers in package.json: 6 dependencies were added: @kreuzberg/liter-llm-node-...-rc.28`). The fix tracks which ecosystems' manifests were touched (`any_node_pkg_modified`, `any_cargo_toml_modified`, `any_composer_json_modified`, `any_mix_exs_modified`) and, after all rewrites complete, invokes the lockfile-update commands behind a new `run_optional(bin, args)` helper that no-ops gracefully when the binary is absent on `PATH` (so offline / partial-install environments do not regress): `pnpm install --no-frozen-lockfile --ignore-scripts -w` (node), `cargo update --workspace --offline` (rust), `composer update --lock --no-interaction` (php), `mix deps.get` (elixir). All four are idempotent when the lockfile already matches the manifest. (`src/cli/pipeline/helpers.rs`, `src/cli/pipeline/version.rs`)

- **alef-backend-swift: emit a multi-line `features = [...]` array in the generated Rust-bridge `Cargo.toml` when the features list has ≥3 entries OR the rendered dep line would exceed 100 chars, so `taplo format` / `cargo-sort -w` do not rewrite it on the first prek pass.** `gen_cargo_toml` previously joined source-crate features with `", "` inline and spliced them into `features = [{list}]` on the same line as the dep declaration, with no line-length budget. Consumers like tree-sitter-language-pack (5 features on one ~150-char line) saw `cargo-sort -w` reformat the manifest to a multi-line block on every regen, producing churn that broke `git diff --exit-code` freshness checks. The emitter now switches to a sorted multi-line format under either threshold; single-line stays for ≤2 features when the line fits. (`src/backends/swift/gen_rust_crate/cargo.rs`)

- **alef-backend-java: stop emitting `import java.lang.Iterable;` in generated facades — `java.lang.*` is JLS-auto-imported and checkstyle's `UnusedImports` / `RedundantImport` rules reject explicit imports of those types.** Streaming-adapter detection (`has_iterable = class_body.contains("Iterable<")`) and the corresponding template branch (`{% if has_iterable %}import java.lang.Iterable;{% endif %}`) are removed; the facade no longer carries the import at all. A new `JAVA_LANG_AUTO_IMPORTED: &[&str]` allowlist in `src/backends/java/gen_bindings/helpers.rs` enumerates 21 commonly-referenced `java.lang.*` types (`Iterable`, `Object`, `String`, `Throwable`, `Number`, `Boolean`, `Integer`, `Long`, `Double`, `Float`, `Character`, `Byte`, `Short`, `Math`, `Runnable`, `Thread`, `Exception`, `RuntimeException`, `Error`, `Class`, `Comparable`) so any future template that emits Java imports can consult it before emitting. Surfaced publishing liter-llm v1.4.0-rc.29 — `Build Java package` failed with `[ERROR] Unused import - java.lang.Iterable. [UnusedImports]` and cascaded to `Publish Maven Central` (skipped). Added regression test `test_facade_no_java_lang_imports` asserting no facade emits any `import java.lang.X` line. (`src/backends/java/gen_bindings/facade.rs`, `src/backends/java/gen_bindings/helpers.rs`, `src/backends/java/templates/facade_file.jinja`)

- **alef-scaffold/python: emit `pyproject.toml` array literals in `pyproject-fmt` style (`["x"]`) instead of the previous inner-spaced shape (`[ "x" ]`) so the first `prek run` after a fresh regen does not rewrite the file.** The scaffold's `format!` templates at `src/scaffold/languages/python.rs` (~L147-L249 — `authors`, `keywords`, `dependencies`, `requires`, `dev`, `features`, `python-packages`, `lint.select`, `lint.per-file-ignores`, `disable_error_code`) all emitted inner-bracket spaces and multi-space comment alignment. `pyproject-fmt` (via `kreuzberg-dev/pre-commit-hooks`) canonicalizes both to a single space, which previously caused first-regen churn and broke `git diff --exit-code` freshness gates on consumers' `Validate scaffolds` CI jobs. All array literals now emit `[{}]` (no inner spaces); test assertions in `src/scaffold/tests.rs` updated to match. (`src/scaffold/languages/python.rs`)

- **alef-backend-kotlin-android: emit `build.gradle.kts` with `import` statements before the `buildscript {}` block, and sort imports lexicographically, so the script parses as valid Kotlin and ktlint does not rewrite it on every regen.** Kotlin's grammar forbids any top-level element preceding `import` directives — only `package` may. Previously the vanniktech maven-publish 0.36.0 wiring prepended a `buildscript { dependencies { classpath("com.vanniktech:gradle-maven-publish-plugin:{version}") } }` block AHEAD of the three `import` lines, which made every consumer's `packages/kotlin-android/build.gradle.kts` not parseable (`ktlint` reported `Not a valid Kotlin file (expecting an element)`; `gradle ktlintFormat` failed with `BUILD FAILED`). The cascading effect was that the post-generation format pass under `packages/kotlin-android/` silently aborted and the consumer's prek `ktlint` hook blocked the commit. The template in `gen_build_gradle.rs::emit` now orders: generated-header comment → sorted imports (`com.vanniktech.maven.publish.AndroidSingleVariantLibrary`, `com.vanniktech.maven.publish.SonatypeHost`, `org.jetbrains.kotlin.gradle.dsl.JvmTarget`) → `buildscript { ... }` → `plugins { ... }`. Sorting also resolves the STY-7 ktlint `import-ordering` churn that previously rewrote the file on every fresh regen. Added regression test `buildscript_comes_after_imports_in_correct_order` and updated the snapshot. (`src/backends/kotlin_android/gen_build_gradle.rs`, `tests/snapshots/backends_kotlin_android_snapshot_test__snapshot_basic__packages__kotlin_android__build.gradle.kts.snap`)

- **alef-scaffold/elixir: probe the resolved `explicit_output.elixir` directory at generate time and only append `{relative}/*.ex` to the Hex `files:` list when the directory actually contains `.ex` or `.exs` sources.** When a consumer sets `[crates.elixir.explicit_output]` to a Rust NIF source directory (which contains only `lib.rs` and `Cargo.toml`), the scaffolded `mix.exs` previously contained `files: ~w(... <relative>/*.ex)` and `mix hex.publish` aborted with `** (Mix) Stopping package build due to errors. Missing files: <path>/*.ex` because Hex requires every glob in `files:` to match at least one on-disk file. The probe now uses `std::fs::read_dir` to inspect the resolved directory: if it exists and contains no Elixir sources, the glob is omitted; if it doesn't exist (generated binding target), the glob is still emitted so consumer expectations are preserved. Surfaced publishing liter-llm v1.4.0-rc.29 (Hex publish failure in run `26329709088`). (`src/scaffold/languages/elixir.rs`)

- **alef-scaffold/ffi: emit `version =` alongside `path =` on intra-workspace path-dependencies so `cargo publish` accepts FFI crate manifests.** `cargo publish` rejected the FFI crate with `error: failed to verify manifest ... all dependencies must have a version requirement specified when publishing` because `render_core_dep` emitted `{crate_name} = { path = "..." }` with no `version =` field. Hand-written sibling crates (e.g. `liter-llm-proxy`, `liter-llm-cli`) carry `version = "X.Y.Z"` alongside `path = "..."` and publish cleanly; alef-scaffolded `liter-llm-ffi` did not, blocking the v1.4.0-rc.29 release run. The three emission sites in `render_core_dep` (default `[dependencies]` line, `cfg(not(...))` default branch, per-target override `cfg(...)` block) now all thread `api.version` and emit `version = "{version}"` alongside `path = "..."`. Added unit tests asserting the emitted dep line contains the version field. (`src/scaffold/languages/ffi.rs`)

- **alef-e2e: make generated test output deterministic by storing fixture headers, query params, cookies, route parameters, and visitor callbacks in `BTreeMap` instead of `HashMap`.** The fixture model used `HashMap<String, _>` for these fields, whose iteration order is randomized per process. Backends that iterated them without sorting (notably the Rust and Node HTTP-client codegen) emitted request headers, header objects, and response-header assertions in a different order on every `alef e2e generate` run — so consecutive regenerations produced spurious diffs and broke CI freshness checks (`alef e2e generate && git diff --exit-code`). Switching the fields (and the `CallCtx` references that borrow them) to `BTreeMap` yields a stable, sorted order across all backends with no new dependency. The pre-existing `driver_emits_headers_in_sorted_order` test — previously flaky under `HashMap` — now passes deterministically. (`src/e2e/fixture.rs`, `src/e2e/codegen/client/mod.rs`)

- **alef-backend-pyo3: thread `never_skip_cfg_field_names` through the constructor rewriter so cfg-gated config fields can be passed as kwargs from the Python facade.** The PyO3 backend already populates `never_skip_cfg_field_names` with the union of trait-bridge option fields and every cfg-gated field on `has_default` types (so that consumers building with all features expose the full kwarg surface). But `replace_constructor_with_serde_rename` ignored that list and filtered with a hard `f.cfg.is_none()` predicate when emitting the `#[pyo3(signature = (...))]` and the corresponding params/assignments — so pdf_options, keywords, html_options, html_output, layout, and tree_sitter were silently dropped from `ExtractionConfig::new`. The hand-written `packages/python/kreuzberg/api.py::_to_rust_extraction_config` then crashed with `TypeError: ExtractionConfig.__new__() got an unexpected keyword argument 'pdf_options'` on 22 kreuzberg Python e2e tests. The rewriter now accepts a `never_skip_cfg_field_names: &[String]` parameter and the call site forwards `type_cfg.never_skip_cfg_field_names`; cfg-gated fields in that list are treated as regular fields (parameter, default, and assignment), while cfg-gated fields not in the list keep the previous `Default::default()` literal assignment. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-e2e/java: coerce nullable-field accessors to `String` via `Optional.map(Objects::toString).orElse("")` so `.contains(...)` assertions compile on `@Nullable Object` fields.** Rust fields typed `Option<serde_json::Value>` (free-form JSON payloads — e.g. `ActionResult.data` in kreuzcrawl carrying screenshot bytes / JS return values / scraped HTML) map to `@Nullable Object` in the generated Java binding, which is correct. The Java e2e generator previously emitted `Optional.ofNullable(x).orElse("")` as the accessor for any nullable string-like field — javac infers the return type as `LUB(Object, String) = Object`, so a downstream `.contains("…")` failed with `cannot find symbol method contains(String) location: class java.lang.Object` (seen on `InteractionTest.java:52` in kreuzcrawl). Switching to `.map(Objects::toString).orElse("")` collapses `Optional<T>` to `Optional<String>` before the fallback, making the expression unambiguously a `String` for any `T`. Numeric (`Number::longValue`/`0L`) and array (`List.of()`) paths are unchanged. (`src/e2e/codegen/java.rs`)

- **ci(publish): downgrade `actions/checkout` from `@v6.0.2` to `@v4` in the alef Publish workflow.** v6.0.2 fails on the self-hosted `runner-medium-arm64` runner used for the `Build CLI (linux-aarch64)` job — `actions/checkout` cannot read GitHub credentials after `git init` (`fatal: could not read Username for 'https://github.com': terminal prompts disabled`), even though `extraheader` is configured. The other matrix targets (`linux-x86_64` on GitHub-hosted, `macos-arm64` on `macos-latest`) work with v6 because their runner images cooperate with v6's credential helper. v4 is the LTS-grade baseline that handles the self-hosted aarch64 environment correctly. Surfaced during the alef v0.17.35 release run. (`.github/workflows/publish.yaml`)

- **alef-backend-java: initialize non-optional enum Builder fields with their Rust default variant instead of null when `#[serde(default)]` is present.** When a Rust struct field is a non-optional enum with `#[serde(default)]` (e.g., `KeywordConfig.algorithm: KeywordAlgorithm`), the field defaults to its Rust `impl Default` variant (e.g., `KeywordAlgorithm::Yake`). The Java Builder previously initialized the field to `null`, which caused Jackson's `NON_NULL` serialization filter to omit the field from the JSON string passed to Rust. Rust's serde deserializer then rejected the config with `"Null pointer passed for parameter 'config'"` because the field is required. The fix detects when a field has the `/* serde(default) */` marker and is a non-optional enum (TypeRef::Named), and initializes the Builder field to the corresponding Java enum constant (e.g., `KeywordAlgorithm.Yake`). Currently handles known enums by name; a future enhancement will extract the default variant from Rust's `impl Default` block in the IR. Added regression test `src/e2e/tests/java_enum_serde_default.rs` verifying that partial JSON configs (omitting enum fields) deserialize and serialize correctly. (`src/backends/java/gen_bindings/types.rs`)

- **alef-backend-pyo3: avoid double-wrapping `Option<Option<T>>` constructor params, and use the matching struct field's type for options-field bridge params so `Self { visitor }` compiles.** Two fixes for the rewritten PyO3 `__new__` constructor. (1) When the IR field type is already `TypeRef::Optional(_)` (which happens for fields like `Update::max_depth: Option<Option<usize>>` peeled to `f.optional=true, f.ty=Optional(usize)`), the constructor param emission was wrapping again to `Option<Option<usize>>`, but the matching pyclass struct field is `Option<usize>` — so the bare `Self { max_depth }` line failed with `expected Option<usize>, found Option<Option<usize>>` (and PyO3's `=None` macro expansion picked up the resulting ambiguity). The param emission now mirrors the guard already in `gen_struct_with_per_field_attrs` and skips re-wrapping when `field.ty` is already optional. (2) The options-field bridge parameter (e.g. `ConversionOptions.visitor`) was typed as `Option<{type_alias}>` (user-facing `VisitorHandle`), but the struct field is `Option<{wrapper_type}>` (the binding-internal `PyVisitorRef`). The bare assignment `visitor: visitor` therefore failed with `expected Option<PyVisitorRef>, found Option<VisitorHandle>`. The bridge param now uses the matching field's actual mapped type. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-backend-magnus: emit RuboCop-clean tagged-enum marker modules so generated Ruby stays stable.** Generated Ruby lives under `lib/**`, which a gem's `.rubocop.yml` typically excludes — so alef's own `bundle exec rubocop -A` pass (no file args) never reformats it and reports "no offenses". But a pre-commit `rubocop` hook that passes the file path explicitly overrides that `Exclude` and reformats it, changing the file after alef computed its hash and tripping `alef verify` with "Stale bindings detected". Three offenses are now avoided at generation time: (1) the sum-type `from_hash` dispatcher emitted double-quoted literals (`hash["type"]`, `when "http"`) while the rest of the file used single quotes — the discriminator read, `when` arms, and per-variant field reads now use single quotes (`Style/StringLiterals`), while the interpolated `raise "Unknown discriminator: #{...}"` stays double-quoted; (2) the marker module lacked a blank line after its `extend` inclusion group before `interface!` (`Layout/EmptyLinesAfterModuleInclusion`); (3) both the marker module and the enclosing module body ended with a trailing blank line (`Layout/EmptyLinesAroundModuleBody`). Added a regression test asserting all three. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-wasm: de-duplicate WASM `Cargo.toml` dependencies so `[crates.<lang>.extra_dependencies]` overrides a built-in instead of emitting a duplicate key.** `gen_cargo_toml` seeds a built-in dependency list (`serde`, `serde_json`, `js-sys`, …), then appended consumer `extra_dependencies` and only sorted — so a consumer that re-declared a built-in (e.g. `serde = { version = "1", features = ["derive"] }`) produced two `serde` keys and `cargo` rejected the manifest with `error: duplicate key ... in dependencies`. The backend now drops any built-in whose name also appears in `extra_dependencies` (the consumer override wins) before sorting. Added a regression test asserting a single `serde` key, that the override's extra feature is preserved, and that the emitted manifest parses as valid TOML. (`src/backends/wasm/gen_bindings/mod.rs`)

- **alef-e2e/elixir: emit Rustler `NifTaggedEnum` tuples for internally-tagged enum array arguments.** Calls like `interact(engine, url, actions)` where `actions: Vec<PageAction>` (`#[serde(tag = "type")]`) previously emitted raw Elixir maps (`[%{"type" => "click", "selector" => "..."}]`), causing `(ErlangError) :invalid_variant` / `(ArgumentError) argument error` at the NIF boundary because the `NifTaggedEnum` decoder expects `:atom` (unit variants) or `{:atom, %{field_atom: val}}` (struct variants). The codegen now threads the IR `enums` slice through the rendering call chain and, when an arg's `element_type` matches an enum with `serde_tag.is_some()`, emits a list of tuples driven entirely from the IR: variant atoms use `variant.name.to_snake_case()` (Rustler ignores serde rename for atoms), field atoms use the Rust field name as-is, and nested `Named` references to unit-only enums convert input strings to atoms via that enum's `serde_rename_all`. Added `src/e2e/tests/elixir_tagged_enum.rs` covering unit, struct, and nested-unit-enum variants. (`src/e2e/codegen/elixir.rs`)

- **alef-backend-java: expose canonical streaming facade methods on the static `<Crate>` class.** Streaming adapters (e.g. `crawl_stream`, `batch_crawl_stream`) previously emitted only instance methods on the opaque handle class. The e2e generator and external callers expected the same module-level surface that Go, Python, Ruby, and other backends provide: `Kreuzcrawl.crawlStream(engine, req)` returning `Iterable<Event>`. Java compile failed with `cannot find symbol method crawlStream(CrawlEngineHandle, CrawlStreamRequest)` across `EngineTest`/`StreamTest`. The Java facade emitter now iterates configured streaming adapters with an `owner_type`, generates a static wrapper that delegates to the owner handle's instance method, and adds the `java.lang.Iterable` import. Driven from the IR adapter configuration — no consumer-specific literals. (`src/backends/java/gen_bindings/facade.rs`, `src/backends/java/templates/facade_class.jinja`, `src/backends/java/templates/facade_file.jinja`)

- **alef-core: add `unsupported_in` per-backend call configuration to mark calls unsupported in specific backends with documented rationale.** Previously, when a backend structurally could not support a call (e.g., brew CLI lacking PageAction enum serialization for interact), the only mechanism was `skip_languages`, which silently omitted tests with no visible rationale. The fix adds `unsupported_in: HashMap<String, String>` to `CallConfig`, mapping backend names to rationale strings (e.g., `unsupported_in = { brew = "interact requires..." }`). The brew e2e codegen now detects this field and emits a skip comment with the documented reason (`# SKIP [brew unsupported]: ...`) before returning success, so fixtures remain testable in other languages and users can understand why the test was skipped. This is a generic, backend-agnostic mechanism: any backend can check `call_config.unsupported_in.get("backend_name")` to skip with documented rationale. Added `src/e2e/codegen/brew.rs` logic to check and skip unsupported calls, and `src/e2e/tests/brew_unsupported_in_call.rs` test verifying the skip comment is emitted. (`src/core/config/e2e.rs`, `src/e2e/codegen/brew.rs`)

- **alef-codegen + alef-backend-java: drop the `/* serde(default) */` placeholder marker before emission so generated code compiles.** The extractor sets `field.default = Some("/* serde(default) */")` to signal that a field carries `#[serde(default)]` but has no explicit value (the marker tells C# to skip its `required` modifier). Java's Jackson POJO builder and the shared `alef_codegen::config_gen::default_value_for_field` helper used to clone `field.default` directly into the emitted code, so generated Java contained `private List<String> excludeSelectors = /* serde(default) */;` (invalid Java) and generated Ruby contained `children: /* serde(default) */` (invalid Rust). Both call sites now filter the marker and fall through to the type-driven default (`List.of()`, `false`, `0`, etc.). (`src/codegen/config_gen.rs`, `src/backends/java/gen_bindings/types.rs`)

- **alef-backend-kotlin-android: add buildscript classpath for vanniktech plugin and use typed jar/sources parameters in AndroidSingleVariantLibrary.** The generated `build.gradle.kts` imports and calls `SonatypeHost.CENTRAL_PORTAL` and `AndroidSingleVariantLibrary(..., sourcesJar = true, publishJavadocJar = true)`, but these classes were not on the Gradle script classpath, causing "Unresolved reference 'SonatypeHost'" compile errors when running `gradle test`. Additionally, the Boolean constructor parameters are deprecated in vanniktech maven-publish 0.36.0; the current API expects `sourcesJar = SourcesJar.Software()` and `javadocJar = JavadocJar.Javadoc()`. The fixes: (1) prepend a `buildscript { dependencies { classpath("com.vanniktech:gradle-maven-publish-plugin:{version}") } }` block before plugins to load the plugin classes onto the script classpath; (2) replace `sourcesJar = true` with `sourcesJar = com.vanniktech.maven.publish.SourcesJar.Software()` and `publishJavadocJar = true` with `javadocJar = com.vanniktech.maven.publish.JavadocJar.Javadoc()` to use the non-deprecated API. Updated snapshot test expectations to match. (`src/backends/kotlin-android/gen_build_gradle.rs`, `src/backends/kotlin-android/tests/snapshots/...`)

- **alef-backend-kotlin-android: disable ktlint's `trailing-comma-on-call-site` / `-on-declaration-site` rules in the generated `.editorconfig` so ktfmt and ktlint stop fighting.** ktfmt in `--kotlinlang-style` strips trailing commas before `)` on multiline call sites; ktlint then re-reports the missing comma. After the kotlin-android `vanniktech` maven-publish migration, this trapped repos in an unfixable ktfmt↔ktlint loop on `build.gradle.kts`. Defer to ktfmt — the new `.editorconfig` block disables both ktlint rules under `[*.kt]` and `[*.gradle.kts]`. (`src/backends/kotlin-android/gen_editorconfig.rs`)

- **alef-backend-csharp: map sanitized Vec fields and sanitized enum-variant payloads to `null` / `object` so external Rust types can round-trip through System.Text.Json.** Two related bugs surfaced in kreuzberg C# e2e: (1) `KeywordConfig.ngram_range: (usize, usize)` with `#[serde(default = "default_ngram_range")]` was emitted as `List<ulong> NgramRange { get; init; } = [];` — the empty-list default reached Rust and serde rejected it with `invalid length 0, expected a tuple of size 2`; (2) `FormatMetadata::Code(tree_sitter_language_pack::ProcessResult)` was generated as `Code(string Value)` because ProcessResult is sanitized to `String` in the IR, then System.Text.Json blew up trying to read a `StartObject` token as a string. Both fixes: when a field is sanitized (the IR's `sanitized` flag with `type_rust_path`), the C# backend now emits a `null` default for Vec types so `JsonIgnoreCondition.WhenWritingNull` drops the field and Rust applies its serde default; and emits `object` (not the sanitized scalar) for enum-variant struct payloads so JSON objects round-trip cleanly. Added `src/e2e/tests/csharp_tuple_default.rs` and `src/e2e/tests/csharp_enum_variant_struct.rs`. Fixes `Test_ConfigKeywords`, `Test_ConfigTreeSitter`, and `Test_CodeShebangDetection` in kreuzberg C# e2e. (`src/backends/csharp/gen_bindings/enums.rs`, `src/backends/csharp/gen_bindings/types.rs`)

## [0.17.35] - 2026-05-23

### Fixed

- **alef-e2e/dart: construct a default options instance for absent optional `json_object` config args when the Dart facade declares the arg as required-positional.** Calls like `embed_texts_async(texts, config)` have `config` marked `optional = true` in the alef IR but the Dart facade signature is `embedTextsAsync(List<String>, EmbeddingConfig)` — non-nullable. When the fixture omitted the `config` field, the e2e codegen previously skipped emitting any value (relying on a hypothetical wrapper-side default), so the generated test compiled to `KreuzbergBridge.embedTextsAsync(<String>[])` and failed with `Too few positional arguments: 2 required, 1 given`. The codegen now mirrors the existing empty-object branch: when `arg_value` is absent and the call has a configured `options_type`, emit `final _config = await create<Type>FromJson(json: '{}');` and pass `_config` positionally. Calls without an `options_type` (e.g. the `ExtractionConfig?` nullable facade path) still omit the arg. (`src/e2e/codegen/dart.rs`)

- **alef-e2e/dart: only emit `_parsePageAction()` helper when fixtures use PageAction arrays.** The Dart e2e codegen unconditionally emitted the `_parsePageAction()` function and `ScrollDirection` references into every generated test file. Test files without any PageAction-array fixture (e.g. `code_test.dart`, `async_test.dart`) failed to compile with `Type 'PageAction' not found` / `Undefined name 'ScrollDirection'`. The helper is now emitted only when at least one fixture in the file has a `json_object` arg with `element_type = "PageAction"`, mirroring the existing `has_handle_args` / `has_http_fixtures` conditional-emit pattern. Added `src/e2e/tests/dart_page_action_conditional.rs`. (`src/e2e/codegen/dart.rs`)

- **alef-e2e/kotlin-android: emit default constructor for optional `json_object` config args instead of `null`.** The kotlin-android binding declares its config parameter as non-nullable `ExtractionConfig`, but the e2e codegen passed `null` whenever the fixture omitted the optional `config` arg, causing Kotlin compile errors (`Null cannot be a value of a non-null type 'ExtractionConfig'`) in `AsyncTest.kt` / `BatchTest.kt`. The codegen now emits `ExtractionConfig()` (using the data-class default constructor) for json_object args with `options_type` set, and infers the type from the arg name otherwise (e.g. `config` → `ExtractionConfig()`). Non-`json_object` optional args still emit `null` as before. Added `src/e2e/tests/kotlin_android_optional_config_arg.rs`. (`src/e2e/codegen/kotlin.rs`)

- **alef-extract: detect `#[serde(default)]` on struct fields and mark them as having a default value in the IR.** Without this, downstream codegen (notably C#) treated `serde(default)` fields as required JSON properties, emitting a `required` modifier on record properties like `DocumentNode.content_layer`. When Rust serialized the field with `skip_serializing_if`, the JSON arrived without the property and C# deserialization rejected the payload with `JSON property required`. The extractor now scans field attributes for `#[serde(default)]` (with or without an explicit value path) and sets `field.default = Some(...)` accordingly, which downstream backends already consume via `field.default.is_some()`. Fixes `Test_ConfigDocumentStructureWithHeadings` in kreuzberg C# e2e. Added `src/e2e/tests/csharp_serde_default_field.rs`. (`src/extract/extractor/helpers.rs`)

- **alef-e2e/java: inherit `options_type` from non-Java overrides when Java omits one.** Java e2e codegen previously read `options_type` only from `[crates.e2e.call.overrides.java]`, so calls that declared `options_type = "ExtractionConfig"` in their C#/Go/PHP/Python override but not Java fell back to emitting raw JSON literals (or `null` for null-bodied configs) instead of deserializing via `JsonUtil.fromJson(..., ExtractionConfig.class)`. Java e2e tests like `testConfigKeywords` consequently passed a null `MemorySegment` for the config handle, hitting a Panama `WrongMethodTypeException` against a non-null parameter. The fallback walks `[csharp, c, go, php, python]` (in that order) and inherits the first available `options_type`. Added `src/e2e/tests/java_json_object_arg_with_value.rs` to lock the inheritance path. (`src/e2e/codegen/java.rs`)

- **alef-backend-pyo3: restore cfg-gated fields to dict coercion and e2e kwargs (reverts 9228bb6f filtering).** Commits 9228bb6f and 9eb355ec added cfg-field filtering based on the assumption that cfg-gated fields don't exist in the PyO3 constructor. However, the Python binding is built with `features = ["full", ...]` — all features including pdf, layout-detection, tree-sitter, etc. are enabled — so cfg-gated fields like `pdf_options`, `keywords`, `html_options`, `layout`, `tree_sitter` ARE present in the struct and MUST be in the constructor. The filtering prevented these fields from being passed to the constructor, causing fixture tests to fail with `TypeError: ExtractionConfig.__new__() got an unexpected keyword argument 'pdf_options'`. This fix removes all cfg-gating filters: `f.cfg.is_none() &&` predicates are removed from `struct_coercible`, `simple_enum_coercible`, and `data_enum_coercible` in functions.rs; the `is_cfg_gated_field` helper and its filter are removed from python e2e codegen; and type_defs parameter threading is removed since no filtering is needed. All fields are now passed through to the constructor regardless of cfg attributes. (Reverts filtering logic added by commit 9228bb6f; keeps the "extend to all types" change from 9eb355ec.) (`src/backends/pyo3/gen_bindings/functions.rs`, `src/e2e/codegen/python/{mod,test_file,test_function}.rs`)

- **alef-backend-php: wrap Option<NonOpaqueNamed> getter returns with .map(Into::into).** When a struct field has type Named(T) where T is not opaque and field.optional=true, the PHP getter returns Option<T>. ext-php-rs's IntoZval impl for Option<#[php_class]> does not properly convert the inner object, causing getters to return null instead of the wrapped object. The fix explicitly maps through Into conversion: `self.field.clone().map(Into::into)` instead of just `self.field.clone()`. This ensures the Option value is properly converted to PHP. The fix handles both IR representations: Optional<Named> (field.optional=true, field.ty=Optional(Named)) and Named with optional flag (field.optional=true, field.ty=Named). Non-optional Named fields and non-Named optional fields remain unchanged. This fixes h2m PHP e2e tests where `$result->getDocument()` was returning null, causing "Call to a member function getNodes() on null" fatal errors in StructureTest. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-pyo3: apply serde-rename constructor rewriting to all struct types, not just has_default.** The `replace_constructor_with_serde_rename` function was only called when `typ.has_default` was true, leaving non-has_default types like `ChunkingConfig` with positional parameters bearing the Rust field names instead of serde-rename wire names. The fix extends the rewrite to all struct types: for `has_default` types, non-optional fields get default values in the signature (`field=Self::default().field`); for non-has_default types, required fields remain required (no signature default) while optional fields still default to `None`. This ensures all types expose the wire-name API (matching JSON serialization and other language bindings) while preserving the semantic difference between types that can be partially constructed with defaults vs. those requiring all parameters. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-e2e/python: skip cfg-gated fields when emitting constructor kwargs.** The Python e2e codegen was emitting constructor kwargs for all fields in fixture JSON objects, including cfg-gated fields like `pdf_options` that are excluded from the PyO3 constructor signature by the `#[cfg(...)]` guards in the core Rust type. When a fixture included a cfg-gated field, the e2e test failed with `TypeError: ExtractionConfig.__new__() got an unexpected keyword argument 'pdf_options'`. The fix threads `type_defs` from `generate_e2e` through `render_test_file` → `render_test_function` → `build_args_and_setup` → `emit_json_object_arg`, and adds a helper `is_cfg_gated_field` that checks the IR to detect cfg-gated fields. When emitting kwargs for a config type in the "kwargs" options_via mode, cfg-gated fields are filtered out before mapping to the constructor call. This restores Python e2e test success when fixtures include conditional features. (`src/e2e/codegen/python/mod.rs`, `src/e2e/codegen/python/test_file.rs`, `src/e2e/codegen/python/test_function.rs`)

- **alef-backend-pyo3: exclude cfg-gated fields from dict coercion functions.** The Python wrapper's `_coerce_dict_*` functions were attempting to coerce cfg-gated fields (like `pdf_options`, `html_options`, `keywords`, `layout`, `tree_sitter`) that are excluded from the PyO3 constructor signature by `#[cfg(...)]` guards in the Rust core. When a user passed these fields in a dict to the wrapper, the coercion function would try to include them in the constructor arguments, causing `TypeError: ExtractionConfig.__new__() got an unexpected keyword argument 'pdf_options'`. The fix adds `f.cfg.is_none() &&` filters to the three field-collection lists (`struct_coercible`, `simple_enum_coercible`, `data_enum_coercible`) in the dict coercion helper generator, matching the same cfg-gate checks already applied to constructor parameter generation. Now only non-cfg-gated fields are included in the `_coerce_dict_*` helper, and the wrapper only passes fields that the PyO3 constructor actually accepts. This is the final layer of the three-part cfg-gating fix (constructor signature + e2e kwargs emission + wrapper coercion). (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-rustler: JSON-encode the streaming request struct before passing it to the NIF.** The high-level Elixir streaming wrapper (`def {{ stream_fn }}(client, req) do`) was forwarding the request struct (e.g. `%Kreuzcrawl.CrawlStreamRequest{url: ...}`) verbatim to `Native.{{ start_fn }}/2`, but the generated NIF signature is `req: Option<String>` (it `serde_json::from_str`s the body). Passing a struct where a binary is expected crashes the NIF call with `(ArgumentError) argument error`, blocking every streaming test in kreuzcrawl's Elixir e2e suite (6 `StreamTest` failures). The fix branches on the request inside the wrapper: `nil` is passed through (NIF accepts `Option<String>`), already-encoded binaries are forwarded as-is, and any other term is `Jason.encode!`-ed before reaching the NIF. The wrapper continues to decode each chunk back into a map via `Jason.decode!(chunk_json, keys: :atoms)`. (`src/backends/rustler/templates/elixir_streaming_unfold_wrapper.jinja`)

- **alef-backend-magnus: remove invalid uppercase RBS type alias.** RBS type aliases must use lowercase identifiers. The uppercase `JsonValue` alias was invalid RBS syntax, causing `steep` and RBS tooling to reject the generated `.rbs` files with a lint error. The lowercase `json_value` on line 11 serves the same purpose and is valid syntax. Removed the invalid alias from stub generation and updated the type mapper to reference `json_value` instead of `JsonValue` in method signatures, fixing h2m Ruby e2e test startup. (`src/backends/magnus/gen_stubs.rs`, `src/backends/magnus/type_map.rs`)

- **alef-backend-pyo3: include options-field bridge parameters in PyO3 constructor signatures.** When a `has_default` struct has an options-field bridge (e.g., `ConversionOptions.visitor` for the `visitor` trait bridge), the PyO3 binding constructor was not accepting the bridge parameter in its `#[pyo3(signature)]` and function parameters, causing the Python wrapper's `_to_rust_*()` converter to fail with `TypeError: ConversionOptions.__new__() got an unexpected keyword argument 'visitor'` when passing `visitor=` to the constructor. The fix detects options-field bridges matching the struct type and adds the bridge's `param_name` and `type_alias` to the constructor parameter list and signature defaults. When a bridge field is present, it is excluded from the regular field iteration to avoid duplicate assignments in the struct literal. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-e2e/php: restore camelCase key transformation in `from_json()` calls.** A prior commit removed the `json_to_php_camel_keys()` helper from PHP e2e codegen, replacing it with `json_to_php()` under the assumption that fixtures' snake_case keys should be passed verbatim to `from_json()`. This was incorrect: the PHP binding's config struct definitions use `#[serde(default, rename_all = "camelCase")]`, which means serde expects camelCase JSON keys. When snake_case keys were passed, serde silently ignored them due to `#[serde(default)]`, leaving configuration values in their default state instead of using the fixture's intent. This caused the test `test_error_extract_bytes_conflicting_ocr` to fail because `allow_ocr_conflicts` was not being set, resulting in a different error than expected. The fix restores the `json_to_php_camel_keys()` helper and switches the three affected call sites (CrawlConfig, PageConfig, and ExtractionConfig construction) back to it. The regression test file documenting the incorrect behavior is removed. This fixes 1 test regression in the PHP e2e suite (`test_error_extract_bytes_conflicting_ocr`). (`src/e2e/codegen/php.rs`)

- **alef-e2e/php: invoke streaming owner_type adapters as instance methods and wrap url-list args in the request type.** The PHP streaming facade exposes `crawlStream`/`batchCrawlStream` only as instance methods on the owner handle class (`$engine->crawlStream($req)`), but the PHP e2e codegen emitted `<FacadeClass>::crawlStream($engine, $req)` — a non-existent static call. Additionally, `mock_url_list` arguments were passed as a raw array instead of being wrapped in the adapter's request type (`BatchCrawlStreamRequest`), while `mock_url` args were already wrapped. The codegen now detects a streaming adapter with an `owner_type`, captures the owner handle variable (`owner_handle_is_receiver` flag suppresses adding it to the positional argument list), renders the call as `$<handle>-><method>(<args>)`, and wraps `mock_url_list` args in the adapter request type the same way `mock_url` args are wrapped. (`src/e2e/codegen/php.rs`)

- **alef-e2e/java: invoke streaming owner_type adapters as instance methods on the owner handle.** The Java facade deliberately emits no static streaming methods — a streaming iterator is bound to a live owner handle, so `crawlStream`/`batchCrawlStream` exist only as instance methods on the owner's opaque-handle class (e.g. `engine.crawlStream(req)`). The Java e2e codegen was emitting `<FacadeClass>.crawlStream(engine, req)` — a static call that does not exist — causing `cannot find symbol` compile errors in `StreamTest`/`EngineTest`. The codegen now detects a streaming adapter with an `owner_type`, captures the owner handle variable, emits its construction via `build_args_and_setup` (new `owner_handle_is_receiver` flag suppresses adding the handle to the positional argument list), and renders the call as an instance-method invocation `<handle>.<method>(<args>)`. (`src/e2e/codegen/java.rs`)

- **alef-backend-php: generate streaming adapter facade methods.** The PHP backend was missing static facade methods for streaming adapters, causing `Class "Kreuzcrawl\E2e\CrawlStreamRequest" not found` and `Call to undefined method Kreuzcrawl\Kreuzcrawl::batchCrawlStream()` errors in PHP e2e tests. The request-type classes (`CrawlStreamRequest`, `BatchCrawlStreamRequest`) were already being generated as part of the API surface, but the streaming adapter facade methods that expose them via static methods on the main API class were never being emitted. The fix adds logic to iterate over streaming adapters in the facade class generation section and emit a static method for each one: `public static function crawlStream(engine: &CrawlEngineHandle, req: &CrawlStreamRequest) -> Result<Vec<String>>` that delegates to the instance method on the engine handle. This mirrors the existing pattern in alef-backend-go where streaming adapters receive module-level wrapper functions. The request types are passed by reference and the facade methods return the streaming result directly (Vec<String> of JSON chunks, wrapped by the PHP Generator infrastructure at the opaque type level). (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-e2e/go: decode sum-type argument elements via Unmarshal<Type> discriminator.** The Go e2e codegen was using bare `json.Unmarshal([]byte(json), &slice)` to decode fixture array arguments whose element type is a sum-type (data-enum), which fails at runtime with `json: cannot unmarshal object into Go value of type <Interface>` because Go's standard unmarshaler cannot initialize bare interface variables. When an argument's element type is a data-enum (identified from the IR `enums` list), the generated Go now decodes each array element via the generated `Unmarshal<Type>` discriminator function: first unmarshaling the JSON array into `[]json.RawMessage`, then iterating and unmarshaling each element with `<import_alias>.Unmarshal<Type>(rawElement)` — the discriminator lives in the binding package, so it is referenced through the e2e test's import alias. This matches the existing pattern used by streaming method wrappers in `alef-backend-go`. The fix threads `data_enum_names: HashSet<&str>` from the `generate` function through `render_test_file` → `render_test_function` → `build_args_and_setup`, where the detection logic is applied. This fixes 13 interaction test failures in kreuzcrawl Go e2e: `Test_InteractActionSequence` and related tests that pass `PageAction` sum-type arrays. (`src/e2e/codegen/go.rs`)

- **alef-backend-pyo3: emit `#[pyo3(signature)]` defaults for non-optional constructor parameters.** `replace_constructor_with_serde_rename` emitted non-optional fields of `has_default` config types as bare parameters with no signature default, so the generated `__new__` required them as positional arguments — `TypeError: ExtractionConfig.__new__() missing 9 required positional arguments: 'use_cache', ...`. Since these constructors are only generated for `has_default` types, whose pyclass struct derives `Default`, each non-optional parameter now defaults to `Self::default().<field>`, restoring the all-keyword-argument-optional constructor. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-e2e/php: preserve snake_case fixture keys when emitting `from_json(json_encode(...))` calls.** The PHP e2e codegen was applying camelCase transformation to JSON object keys before passing them to `from_json()`, converting fixture keys like `extract_pages` to `extractPages`. The PHP binding's serde configuration includes `#[serde(default, rename_all = "camelCase")]`, but fixtures are authored in Rust wire format (snake_case). When the transformed keys were deserialized, serde silently ignored them due to `#[serde(default)]`, leaving configuration values in their default state instead of using the fixture's intent. This caused tests like `test_config_pages` to fail because page markers were not inserted. The fix removes the `json_to_php_camel_keys()` function and replaces all three call sites (lines 1429, 1567, 1597) with `json_to_php()`, which preserves the fixture's original keys. The binding's serde deserializer now receives the fixture's snake_case keys and correctly deserializes them. This fixes 1 runtime failure in the PHP e2e suite (`ContractTest::test_config_pages`). Added regression test `php_snake_case_fixture_keys` documenting the fix. (`src/e2e/codegen/php.rs`, `src/e2e/tests/php_snake_case_fixture_keys.rs`)

- **alef-e2e/c: select correct FFI free function for opaque struct-returning getter results.** When a C getter returns an opaque struct pointer (not a C string), the e2e codegen was always freeing it with `<prefix>_free_string(handle)` instead of the appropriate struct-specific free function `<prefix>_<type_snake>_free(handle)`. The accessor code in `emit_nested_accessor` was correctly emitting the typed handle declaration and registering it with the opaque snake_case type name in `intermediate_handles`, but when creating an alias variable for the local scope, the codegen returned `None` from `emit_nested_accessor` instead of returning the type name, causing the caller to not register the alias in `opaque_handle_locals`. The fix returns the snake_case type name from `emit_nested_accessor` when an opaque handle is detected, allowing the caller to distinguish between primitives and opaque handles and register them correctly. The caller (`render_test_function` and `render_engine_factory_test_function`) now checks `is_primitive_c_type` to determine whether the returned type is a primitive or an opaque handle, and adds opaque handles to `opaque_handle_locals` so cleanup uses the correct free function. This fixes `error: incompatible pointer types passing 'KCRAWLCitationResult *' to parameter of type 'char *'` compilation errors in kreuzcrawl C e2e tests. (`src/e2e/codegen/c.rs`)

- **alef-e2e/zig: fix format string escaping in allocPrint wrapper for interact() calls.** The Zig e2e codegen was generating an invalid format string for `std.fmt.allocPrint` when wrapping interact result: `"{{\\\"interaction\\\"{{s}}}}"` which became the Zig string `{\"interaction\"{s}}` — an unterminated placeholder due to the doubled `{{s}}` being interpreted as a literal `{s}` instead of the string placeholder. The format string in Zig should be `"{{\"interaction\":{s}}}"` where `{{` and `}}` are literal braces and `{s}` is the placeholder. In Rust source, this requires double-escaping: `"{{{{\\\"interaction\\\"{{s}}}}}}"` (each `{` → `{{` → `{{{{` in Rust source). The fix updates the writeln! invocation to emit the correct format string, allowing Zig 0.16 to correctly parse and compile the allocPrint call. This fixes `error: expected . or }, found '{'` compilation errors at the allocPrint line in kreuzcrawl Zig e2e tests. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/csharp: emit plain .ToString() instead of null-conditional ?.ToString() for non-nullable value types.** The C# assertion template was unconditionally using the null-conditional operator (`?.ToString()`) for non-empty and is-empty checks, but this operator is invalid on non-nullable value types (like `bool`, `int`, struct types declared as non-nullable, or properties typed with `!` null-forgiving). The codegen now detects whether a field expression is non-nullable by checking for the `!` operator or method-call syntax (which always returns non-nullable) and passes a `field_is_nullable` flag to the template. The template then emits `.ToString()` for non-nullable types and `?.ToString()` only for nullable types. This fixes `error CS0023: Operator '?' cannot be applied to operand of type 'bool'` compilation errors in kreuzcrawl C# e2e tests when assertions access opaque struct handle fields. (`src/e2e/codegen/csharp.rs`, `src/e2e/templates/csharp/assertion.jinja`)

- **alef-backend-pyo3: escape Rust keywords in serde-rename constructor parameters using raw-identifier syntax.** When a struct field carries `#[serde(rename = "type")]` (or any Rust keyword as the wire name), `replace_constructor_with_serde_rename` was emitting the bare keyword as a function parameter name and struct-literal RHS — e.g. `pub fn new(type: String, ...)` and `Self { item_type: type }`. Both are invalid Rust because `type` is a reserved keyword. The fix calls `alef_core::keywords::rust_raw_ident` on every resolved parameter name, emitting `r#type` wherever the wire name collides with a Rust keyword. PyO3 strips the `r#` prefix when deriving the Python-facing kwarg name, so the Python API remains unchanged (`type`). A new `RUST_KEYWORDS` constant and `rust_raw_ident`/`rust_raw_ident_safe` helpers have been added to `alef-core::keywords` and re-exported via `alef-codegen::keywords`. Added regression test `test_serde_rename_rust_keyword_emitted_as_raw_ident` that verifies the emitted constructor uses `r#type`, the struct literal uses `item_type: r#type`, and no bare `type` token appears. (`src/core/keywords.rs`, `src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-backend-pyo3: emit non-optional Duration struct fields as u64 in constructor parameters.** PyO3 binding constructors for `has_default` config types were wrapping non-optional `Duration` fields in `Option<u64>` in the struct definition (to allow unset fields to fall back to the core type's `Default`) but emitting them as `u64` in the constructor parameter list. This mismatch caused `error[E0308]: mismatched types` compilation errors when the constructor tried to assign the parameter value to the `Option<u64>` struct field. The struct field emitter correctly applies `option_duration_on_defaults` logic to wrap the Duration type in `Option`, but the constructor parameter emitter was not applying the same logic. The fix updates `replace_constructor_with_serde_rename` to accept the `RustBindingConfig` and check the same `force_optional` condition used by the struct field generator: when `option_duration_on_defaults` is true, `has_default` is true, the field is not already optional, and the field type is `Duration`, the constructor parameter is wrapped in `Option<...>` with a default value of `None`. This ensures the constructor parameter type matches the struct field type exactly. Affected types: `BrowserConfig`, `RequestConfig`, `DomConfig`, etc. (`src/backends/pyo3/gen_bindings/mod.rs`)

### Fixed

- **alef-cli/version: sync `.version` in `packages/zig/build.zig.zon`.** `alef sync-versions` updated every other per-language manifest (pyproject.toml, package.json, pom.xml, mix.exs, composer.json, pubspec.yaml, etc.) but never `packages/zig/build.zig.zon`, so the Zig package version drifted from the workspace version every release. tslp's `1.9.0-rc.1` release shipped with `.version = "1.8.1"` still committed in the zon, and the `publish-zig` job consumed it. Both the validate path and the sync-all path now match the `.version = "X.Y.Z"` field, anchored to start-of-line with `(?m)^\s*\.version` so the `.minimum_zig_version = "..."` field on the same file is not accidentally matched. Added regression test `zon_version_regex_anchors_to_dot_version_only` covering exactly that anchor case. (`crates/alef-cli/src/pipeline/version.rs`)

### Added

- **alef-backend-kotlin-android: apply com.vanniktech.maven.publish plugin so `publishAndReleaseToMavenCentral` resolves.** The shared `kreuzberg-dev/actions/publish-maven-gradle@v1` action invokes `gradle publishAndReleaseToMavenCentral` by default — a task provided by the **vanniktech maven-publish** plugin, not the vanilla `maven-publish` plugin alef previously emitted. Releases failed with `Task 'publishAndReleaseToMavenCentral' not found in root project` because the generated `build.gradle.kts` only declared `id("maven-publish")` plus a manual `publishing { publications { register<MavenPublication>("release") { ... } } }` block. The emitter now applies `id("com.vanniktech.maven.publish") version "0.36.0"` and replaces the manual publishing/singleVariant blocks with vanniktech's `mavenPublishing { configure(AndroidSingleVariantLibrary("release", sourcesJar = true, publishJavadocJar = true)); publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL); signAllPublications(); coordinates(...); pom { name, description, url, licenses, developers, scm } }`. Pom metadata is sourced from `config.scaffold` via the now-public `scaffold_meta`/`parse_author`/`xml_escape` helpers re-exported from `alef-scaffold`, falling back to the same defaults `scaffold_meta` uses elsewhere (license `MIT`, repository `https://github.com/example/{name}`, no developers when authors is empty). Added `VANNIKTECH_MAVEN_PUBLISH = "0.36.0"` to `src/core/template_versions.rs::maven`. Snapshot updated; regression test `build_gradle_uses_vanniktech_maven_publish_plugin` asserts the plugin id, the `mavenPublishing {` block, and the absence of the old `register<MavenPublication>("release")` and `android { publishing { } }` blocks. (`src/backends/kotlin-android/gen_build_gradle.rs`, `src/backends/kotlin-android/Cargo.toml`, `src/backends/kotlin-android/tests/snapshot_test.rs`, `src/backends/kotlin-android/tests/snapshots/...build.gradle.kts.snap`, `src/core/template_versions.rs`, `crates/alef-scaffold/src/lib.rs`)

- **alef-scaffold/php: emit root composer.json mirroring packages/php for Packagist/PIE.** Packagist indexes the repo-root `composer.json` and PIE reads its `extra.pie.binary.url-template` from the indexed manifest to download prebuilt extension binaries from GitHub Releases. Until now alef only emitted `packages/php/composer.json`, leaving each polyglot repo's root manifest as a hand-written file that drifted from the alef-generated one — most notably missing the `extra.pie` block, which forced PIE to fall back to source builds. The PHP scaffold now also emits a root `composer.json` byte-identical to `packages/php/composer.json` except that the PSR-4 autoload src path is repointed from `src/` to `packages/php/src/`, so the same classes resolve when consumers install the package from the repo root. `alef sync-versions` already updated the root file's `version` field when present (`crates/alef-cli/src/pipeline/version.rs`); generation closes the gap and keeps the two manifests in sync. Verified across all five polyglot repos (kreuzberg, html-to-markdown, liter-llm, kreuzcrawl, tree-sitter-language-pack), each of which previously had a hand-written root `composer.json` divergent from its alef-generated `packages/php/composer.json` in vendor name, extension name, namespace, and `extra.pie` presence. Added regression test `test_scaffold_php_emits_root_composer_json_mirroring_package` that asserts the root manifest equals the package manifest with the autoload src repointed, carries the `extra.pie.binary.url-template`, and uses `<owner>/<repo>` as the Packagist package name. (`crates/alef-scaffold/src/languages/php.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-scaffold: copy workspace-root LICENSE into every per-language package directory.** Ecosystems like pub.dev (Dart) require a `LICENSE` file in the package root and reject uploads without one. The scaffold step now reads `<workspace_root>/LICENSE` and emits it into each per-language package directory (e.g. `packages/dart/LICENSE`, `packages/python/LICENSE`). When no LICENSE file is present the step warns and continues without error. Internal-only languages (Rust, C, FFI, JNI) are excluded. The copy is idempotent — `generated_header: false` means the file is created once and not overwritten on subsequent `alef scaffold` runs, keeping `alef verify` happy. (`crates/alef-scaffold/src/lib.rs`)

- **alef-scaffold/dart: add `repository:` field to pubspec.yaml and emit CHANGELOG.md.** `dart pub publish` flags packages missing a `repository:` field in `pubspec.yaml` and requires a `CHANGELOG.md` at the package root. The generated `pubspec.yaml` now includes `repository: <configured-url>` (and `homepage:` when set). A minimal `CHANGELOG.md` seed keyed to the current version is emitted as a create-once file so consumers update it before publishing. (`crates/alef-scaffold/src/languages/dart.rs`)

### Fixed

- **alef-backend-pyo3: fix regression in cfg-gated field struct literals in constructors.** Commit fe570c1c added support for `serde(rename)` in constructor parameters for has_default config types, but introduced a regression: cfg-gated fields (e.g., `#[cfg(feature = "pdf")]`) were excluded from the constructor parameter list (correct) but still referenced in struct literal shorthand without being defined (incorrect), causing `error[E0425]: cannot find value` compilation errors in kreuzberg-py for fields like `pdf_options`, `keywords`, `html_options`, etc. The fix correctly handles cfg-gated fields in struct literals: they are excluded from constructor parameters (Python doesn't know about Rust feature gates) and initialized with `None` for optional cfg-gated fields or `Default::default()` for required ones. This ensures all struct fields are properly initialized regardless of feature gate status. Affected types: `ExtractionConfig`, `FileExtractionConfig`, `ExtractionResult`, `KeywordConfig`. Added regression test `test_cfg_gated_fields_excluded_from_constructor` verifying cfg-gated fields are omitted from parameters but correctly initialized in struct literals. (`src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-e2e/php: fully qualify config class names to the binding namespace.** Config classes like `EmbeddingConfig` were being emitted as bare names (e.g., `EmbeddingConfig::from_json('{}')`) in generated test code, causing PHP to look for them in the test namespace (`Kreuzberg\E2e`) instead of the binding namespace (`Kreuzberg`), resulting in `Class "Kreuzberg\E2e\EmbeddingConfig" not found` errors. Config references are now fully qualified to the binding namespace (e.g., `\Kreuzberg\EmbeddingConfig::from_json('{}')`). This fixes 3 runtime failures in the PHP e2e suite for the `embed_texts_async` call. The fix also requires passing the `namespace` parameter through `build_args_and_setup()` to enable qualification at the codegen point. (`src/e2e/codegen/php.rs`)

- **alef-e2e/kotlin_android: emit plain string paths instead of `java.nio.file.Path.of()` wrappers.** The Kotlin e2e codegen was always wrapping file path arguments with `java.nio.file.Path.of(...)` because the Kotlin/JVM binding re-exports the Java facade (which takes `Path`), but kotlin_android has a different signature that takes a plain `String`. The fix detects kotlin_android mode via the existing `kotlin_android_style` flag and emits plain string paths for kotlin_android while keeping the `Path.of()` wrapping for Kotlin/JVM. This fixes 5 compilation errors in the kotlin_android e2e suite where `extractFile()` received a `Path` when a `String` was expected. (`src/e2e/codegen/kotlin.rs`)

- **alef-backend-java: fix clear_fn error handling to throw exception with (int code, String message) constructor.** A prior fix added error-code checking to trait-bridge clear_fn functions, but the generated exception throw used a single-String constructor (`new KreuzbergRsException("FFI call failed: " + msg)`) when `KreuzbergRsException` only has constructors `(int code, String message)` or `(String message, Throwable cause)`. The generated code now throws the correct two-argument form `new KreuzbergRsException(primitiveResult, msg)`, matching how all other fallible FFI calls construct the exception. This fixes compilation errors in the Java binding: `no suitable constructor found for KreuzbergRsException(java.lang.String)`. Added regression test `clear_fn_error_throws_exception_with_code_and_message` to verify the exact throw statement format. (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-cli: prepend the native-library directory to the search-path env var instead of replacing it.** The e2e/test runner inlines an `export <VAR>='<dir>'; <cmd>` prefix so freshly-built native libraries are found at runtime — `LD_LIBRARY_PATH` on Linux, `DYLD_LIBRARY_PATH` on macOS, and `PATH` on Windows (Windows resolves DLLs via `PATH`). The export _replaced_ the variable's entire value, which is catastrophic on Windows: `export PATH='...target\release'` wiped out every tool on the path, so `uv: command not found` killed every Windows e2e job. On Linux/macOS the bug was masked because `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH` are normally empty, so replace happened to equal set. The fix emits `export <VAR>='<dir>'"${<VAR>:+:$<VAR>}"` so the directory is prepended to any existing value, with the `${VAR:+:$VAR}` guard avoiding a stray `:` when the variable was unset. (`crates/alef-cli/src/pipeline/helpers.rs`)

- **alef-scaffold/ruby: exclude generated binding code from RuboCop linting.** The `.rubocop.yml` generated for Ruby packages was excluding build artifacts (`ext/**/*`, `tmp/**/*`) but not the generated binding sources (`lib/**/*.rb`), causing RuboCop to lint alef-generated code (`native.rb`, `version.rb`) against project style rules it was never written to satisfy. This resulted in 7 RuboCop offenses across Metrics and Style rules (ModuleLength, OneClassPerFile, CyclomaticComplexity, AbcSize). The fix adds `lib/**/*.rb` to the `AllCops: Exclude:` list, keeping hand-written spec files linted while exempting all generated binding code. (`crates/alef-scaffold/src/languages/ruby.rs`)

- **alef-backend-go: decode streaming sum-type items via the generated `Unmarshal<Type>` discriminator.** A streaming adapter whose item type is a data enum (a Rust enum with fielded variants) is emitted as a Go sealed interface. The streaming-consumer goroutine decoded each event with a bare `json.Unmarshal` into a named-interface variable, which fails at runtime (`json: cannot unmarshal object into Go value of type <Interface>`) — the goroutine bailed after the first event and delivered zero events to the channel. The generator now calls the generated `Unmarshal<Type>` discriminator function when the stream item type is a data enum. (`src/backends/go/src/gen_bindings/methods.rs`, `src/backends/go/src/gen_bindings/mod.rs`, `src/backends/go/templates/streaming_method_body.jinja`)

- **alef-backend-pyo3: honor `serde(rename)` in constructor parameter names and PyO3 signatures.** PyO3 binding constructors for `has_default` config types were using bare Rust field names as parameter names (e.g. `max_characters`) instead of serde-renamed names (e.g. `max_chars`), causing `TypeError: __init__() got an unexpected keyword argument 'max_chars'` when e2e tests passed the wire name. This broke consistency with other language bindings (Node, Ruby, PHP, Go, etc.) which all expose the serde-renamed names as the public API. The fix generates a custom constructor that honors `field.serde_rename` when available, deriving parameter names from the renamed values while keeping struct literal field names as bare Rust identifiers. The fix validates that config-based keyword renames take precedence over serde_rename (e.g. when `class` is keyword-escaped to `class_`). Added regression test `test_serde_rename_in_constructor_and_properties` that verifies constructor parameters, PyO3 signatures, and struct literals are all correct. (`src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-backend-napi: add regression test for TypeScript declaration formatting (no trailing semicolons on interface members).** NAPI-RS generates `.d.ts` files without trailing semicolons on interface member declarations. Alef must emit the same format to avoid churn when `napi build` regenerates the file. Added explicit test assertions to `test_dts_dto_fields_are_readonly` that verify interface fields do NOT have trailing semicolons, ensuring the formatting remains stable across regenerations. (`src/backends/napi/tests/gen_bindings_test.rs`)

- **alef-backend-java: trait-bridge clear_fn functions invoke with out-error parameter and error code checking.** Trait-bridge clear functions (e.g. `clear_ocr_backends`) return `Result<()>` in Rust, which export as `int kreuzberg_clear_X(char **out_error)` in the C FFI. The Java codegen was treating them as `Unit` return types (void) and invoking them with no parameters, producing `java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(MemorySegment)int to ()void`. The fix detects trait-bridge clear_fn functions via the computed `clear_fn_handles` map and generates the same error-code-checking pattern as other fallible FFI functions: allocate an out-error buffer, pass it to the FFI invocation, and throw an exception if the return code is non-zero. This fixes 7 runtime failures in the Java e2e suite (`clearValidators`, `clearOcrBackends`, `clearRenderers`, etc.). (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-backend-rustler: use `Mix.env()` instead of `System.get_env("MIX_ENV")` for the force_build predicate in native_module_header.** The `rustler_precompiled` configuration's `force_build` clause was checking `System.get_env("MIX_ENV") in ["test", "dev"]`, which returns `nil` when the environment variable is unset, causing the predicate to always be false. However, `mix compile` and `mix test` run in the `:dev` and `:test` environments internally (set by Mix) even when `MIX_ENV` is not exported. This caused `rustler_precompiled` to attempt downloading precompiled NIFs from a GitHub release URL that does not exist for unreleased versions (e.g. `v5.0.0-rc.1`), resulting in HTTP 404 and build failure. The fix uses `Mix.env() in [:dev, :test]` to check the actual Mix environment atom instead. (`src/backends/rustler/src/template_env.rs`)

- **alef-backend-csharp: fix trait-bridge clear facade method naming — use `clear_fn` name (plural) not trait name (singular).** A prior fix added `Clear*` facade methods to the wrapper class to forward to trait-bridge registries' `Clear()` methods, but named them from the trait name: `ClearOcrBackend` (singular, from trait `OcrBackend`). The public API and e2e tests expect the plural name from the Rust free-function: `ClearOcrBackends` (from `clear_ocr_backends`). The method name derivation now uses `bridge_cfg.clear_fn` in PascalCase instead of the trait name, generating the correct plural names: `ClearOcrBackends`, `ClearPostProcessors`, `ClearValidators`, `ClearEmbeddingBackends`, `ClearDocumentExtractors`, `ClearRenderers`. This fixes `CS0117: 'KreuzbergLib' does not contain a definition for 'ClearOcrBackends'` compilation errors in C# e2e suites. Added regression test `test_trait_bridge_clear_method_uses_clear_fn_name_not_trait_name` to verify the exact method signatures. (`src/backends/csharp/gen_bindings/methods.rs`, `src/backends/csharp/tests/gen_bindings_test.rs`)

- **alef-e2e/ruby: emit specific `raise_error(RuntimeError)` matchers instead of bare `raise_error`.** RSpec emits a false-positive warning for every bare `raise_error` matcher because it can match NoMethodError, NameError, or ArgumentError unintentionally. The Ruby e2e generator now emits `raise_error(RuntimeError)` for error fixtures, matching the Kreuzberg binding's exception type. (`src/e2e/codegen/ruby.rs`)

- **alef-e2e/typescript: bind `result` variable for calls returning a plain type when an assertion field is not a struct member.** For calls returning a plain type (e.g. `embedTexts`, `detectMimeTypeFromBytes`), assertions on synthetic fields (e.g. `embeddings` on an `Array<Array<number>>`) render against the result directly, but the binding predicate gated solely on `is_valid_for_result(field)`, leaving undefined `result` references in generated test code. Mirrors the prior Rust/Go fix. (`src/e2e/codegen/typescript/test_file.rs`)

- **alef-e2e/typescript: emit extended timeout for embedding e2e tests.** Embedding fixture calls trigger a multi-minute HuggingFace model download on first run. Generated tests now pass a 600-second timeout (e.g. `it(name, fn, 600_000)`) for fixtures tagged with the `embeddings` call name, gating the timeout on the call type rather than duplicating per-fixture configuration. (`src/e2e/codegen/typescript/test_file.rs`)

- **alef-e2e/go: do not emit unused `metadataOutputFormat` bindings for contract calls.** The Go e2e generator emitted `metadataOutputFormat := ...` for certain fixtures but did not consume it in the resulting test, producing `declared and not used` errors. The binding is now only emitted when the output format is actually applied in an assertion. (`src/e2e/codegen/go.rs`)

- **alef-e2e/php: honor `options_type` for async embed calls, and emit defensive null checks for optional metadata properties.** The `embedTextsAsync` call now resolves the config type from the call-level `options_type` (`EmbeddingConfig`) instead of falling back to `ExtractionConfig`. Additionally, smoke tests now emit null-coalescing for optional/nullable metadata properties (e.g. `$metadata->excel ?? null`) to prevent PHP "Attempt to read property" warnings. Finally, the generated test for `embed_texts_different_preset` now declares at least one assertion to satisfy PHPUnit's strict assertion checking. (`src/e2e/codegen/php.rs`)

- **alef-e2e/r: emit non-empty test bodies for mime_utilities fixtures.** The R e2e generator was emitting empty `test_that` blocks for some mime_utilities fixtures, causing testthat to skip them with a "Reason: empty test" warning. Test bodies now always contain at least one meaningful assertion or call. (`src/e2e/codegen/r.rs`)

- **alef-e2e/php: keep the system php.ini when re-execing the PHPUnit harness.** The generated `run_tests.php` re-execs PHP with `-n` (no ini) plus `-d extension=<local build>` so the freshly-built extension is loaded without a stale system copy. But `-n` also drops every _shared_ PHP module, so on distributions that ship `dom`/`mbstring`/`tokenizer`/`xml`/`xmlwriter` as shared extensions (Debian/Ubuntu, hence CI) PHPUnit 13 aborts at startup with "PHPUnit requires the dom, json, libxml, mbstring, tokenizer, xml, xmlwriter extensions … not available". It only passed where those modules are compiled statically (macOS Homebrew). The harness now re-execs without `-n`, keeping the system ini, and still loads the local build via `-d extension=`. (`src/e2e/codegen/php.rs`)

### Added

- **alef-core: `CallConfig.options_type`.** The binding-agnostic config parameter type for an e2e call can now be declared once at call level (`[e2e.calls.<name>].options_type`) instead of being duplicated across every per-language override. The field was already present in `kreuzberg`'s `alef.toml` but silently discarded because the model lacked it. (`src/core/config/e2e.rs`)

### Fixed

- **alef-e2e/java: keep engine handle in streaming adapter calls.** Streaming adapters expose free-function facades (`Kreuzcrawl.crawlStream(engine, req)`) that take the engine handle as the first positional parameter, not methods on the handle itself. The Java e2e generator was filtering out handle-type args whenever an adapter had an `owner_type`, but streaming adapters are special: they still need the handle. The filter now checks the adapter's `pattern` and only drops the handle for non-streaming adapters with `owner_type`. This fixes `cannot find symbol` errors for `crawl_stream` and `batch_crawl_stream` streaming calls. (`src/e2e/codegen/java.rs`)

- **alef-e2e/dart: emit `mock_url_list` arguments for batch streaming calls.** The `batch_crawl_stream` call requires a `urls: List<String>` parameter, but the Dart e2e generator had no handler for the `mock_url_list` arg type — the argument list fell through without emitting the URLs. Added a `mock_url_list` case that builds a Dart list literal with the same mock-server base-URL resolution as `mock_url` (per-fixture env var, then global `MOCK_SERVER_URL`). This fixes `too few positional arguments` errors for batch streaming calls. (`src/e2e/codegen/dart.rs`)

- **alef-e2e/dart: emit scalar integer and numeric arguments positionally.** The Dart e2e generator had no handler for `int`, `float`, `bool`, and other scalar arg types — they fell through to the catch-all `_ => {}` pattern without being added to the argument list. This caused calls with mixed positional args (e.g. `pdf_bytes: bytes`, `page_index: int`) to emit the first arg only, producing "too few positional arguments" errors at Dart compile time. Added handlers for `"int"`, `"integer"`, `"i64"`, `"float"`, `"number"`, `"bool"`, and `"boolean"` types that emit scalar values positionally as Dart literals, with appropriate defaults for missing/null required args. Added regression test `dart_int_positional_arg` that verifies both bytes and int args are emitted in order. (`src/e2e/codegen/dart.rs`, `src/e2e/tests/dart_int_positional_arg.rs`)

### Fixed

- **alef-backend-swift: trait-bridge result enums are emitted as first-class Swift enums without FFI from_json functions.** Trait-bridge result types (e.g. `VisitResult`) are first-class Swift enums that JSON-decode locally and never cross the FFI boundary. Previously, the Rust-crate emitter declared them in the swift-bridge `extern` block, causing swift-bridge to auto-generate a second opaque `class VisitResult` that collided with the enum. The fix skips both the `extern` type declaration and the `from_json` function declaration for result-type enums, while the Swift-side emitter now emits them as native Swift enums without the `intoRust()` extension (since they decode locally, not via a Rust-side function). This resolves "ambiguous type lookup" errors in consumer Swift code. (`src/backends/swift/src/gen_rust_crate/mod.rs`, `src/backends/swift/src/gen_bindings.rs`)

### Fixed

- **alef-e2e: bind `result_is_simple`/`result_is_tree` calls to the result variable, and emit Go `fmt`/`strings` imports, when an assertion's field is not a real struct member of the plain result type.** For calls returning a plain type (e.g. `embed_texts`, `detect_mime_type_from_bytes`), `count_equals`/`contains` assertions naming a non-member field (e.g. `embeddings` on a `Vec`) render against the result variable directly — but the binding/import predicates gated solely on `is_valid_for_result(field)`, so the call was bound to `_` (Rust) or the imports were omitted (Go), leaving `result.len()` / `fmt.Sprint(result)` referencing undefined names. (`src/e2e/codegen/rust/test_file.rs`, `src/e2e/codegen/go.rs`)

- **alef-e2e/php: keep an explicit `null` for an omitted optional middle argument, and honor the call-level `options_type`.** When a fixture omitted an optional positional arg (e.g. `extractFile`'s `mime_type`) followed by a `json_object` config arg that always emits a default, the config object was shifted into the wrong positional slot, causing a PHP `TypeError`. Separately, embed calls now resolve the config type from the call-level `options_type` (`EmbeddingConfig`) instead of falling back to the `ExtractionConfig` heuristic. (`src/e2e/codegen/php.rs`)

- **alef-e2e/dart: emit `mime_type` positionally for facade extract methods.** `extractBytes`/`extractBytesSync`/`extractFile`/`extractFileSync` declare `mime_type` as a positional parameter, but the e2e generator emitted it as a `mimeType:` named argument, producing "too few positional arguments" errors and invalid named-then-positional calls. (`src/e2e/codegen/dart.rs`)

- **alef-backend-csharp: trait-bridge P/Invoke `EntryPoint` names now match the FFI exports.** `register`/`unregister`/`clear` entry points are derived as `{prefix}_{verb}_{trait_snake}` (matching the go and java backends) instead of using the unprefixed/plural `alef.toml` aliases, which name only the host-language wrappers — fixing `EntryPointNotFoundException` for `kreuzberg_clear_renderers` and similar. (`src/backends/csharp/trait_bridge.rs`, `src/backends/csharp/gen_bindings/functions.rs`, `src/backends/csharp/gen_bindings/methods.rs`)

- **alef-backend-java: facade `clear_*` methods now reference the singular FFI-derived `NativeLib` handle constant.** The `KreuzbergRs` facade body derived a free function's `MethodHandle` constant from `func.name`, so a trait-bridge `clear_fn` (the plural core Rust name, e.g. `clear_ocr_backends`) produced `KREUZBERG_CLEAR_OCR_BACKENDS` — but `NativeLib.java` declares the constant from the trait name as the singular `KREUZBERG_CLEAR_OCR_BACKEND` (matching the C FFI export `kreuzberg_clear_ocr_backend`). The mismatch failed Java compilation. The main-class emitter now maps each trait bridge's `clear_fn` to its singular handle constant and the facade body looks it up. (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-backend-kotlin-android: apply float-literal post-processing in the JNI-mode emitter.** The `kotlin-android` backend emits via its own `gen_bindings::emit` path and never ran the `fix_float_literals` post-processor, so a `Double` field default like `32.0` rendered as `32` — invalid Kotlin (`Initializer type mismatch: expected 'Double', actual 'Int'`). (`src/backends/kotlin-android/lib.rs`)

- **alef-scaffold/ruby: stop the scaffold from clobbering the magnus-generated gem entry point.** The Ruby scaffold emitted `packages/ruby/lib/<gem>.rb` containing only `require '<ext>_rb'`, overwriting the correct entry point the magnus backend's `gen_bindings` emits — which `require_relative`s `<gem>/native` (the `module <Module>` re-export) and `<gem>/version`. The on-disk entry point therefore loaded the native extension (`<Module>Rs`) but never defined the public `<Module>` constant, so `require '<gem>'` left `<Module>.convert` undefined and every consumer e2e spec failed with `NameError: uninitialized constant <Module>`. The scaffold no longer emits `lib/<gem>.rb`; the magnus backend is its sole owner. (`crates/alef-scaffold/src/languages/ruby.rs`)

- **alef-backend-kotlin: import `kotlin.time.Duration.Companion.milliseconds` when a Duration default is emitted.** A `Duration`-typed field with an integer default is rendered as `<n>.milliseconds` to match the declared `kotlin.time.Duration` type, but the generated file only imported `kotlin.time.Duration` — so `.milliseconds` was an unresolved reference and the binding failed to compile. The data-class emitter now adds `import kotlin.time.Duration.Companion.milliseconds` whenever a rendered field default uses the `.milliseconds` extension. Applies to the `kotlin-android` backend too (shared `object_wrapper.rs`). (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-swift: emit `@unchecked Sendable` for opaque result types returned across the bridge.** The catch-all `@unchecked Sendable` emission filtered `api.types` by the IR `is_opaque` flag, which is false for a result struct like `ScrapeResult` even though swift-bridge still bridges it as an opaque `RustBridge.ScrapeResult` class — and async forwarders return that class out of a `Task.detached`, so Swift 6 strict-concurrency rejected the binding with `type 'ScrapeResult' does not conform to the 'Sendable' protocol`. The emission now also covers `compute_handle_returned_types` (every type returned across the bridge as an opaque handle), and a shared `sendable_emitted` set prevents the streaming-specific emissions and the catch-all block from declaring the same conformance twice (Swift warns on a redundant conformance). (`src/backends/swift/src/gen_bindings.rs`)

## [0.17.31] - 2026-05-22

### Fixed

- **alef-backend-swift: don't emit a swift-bridge opaque `extern` class for trait-bridge result enums.** A trait-bridge result type like `VisitResult` is a first-class Swift `enum` used only inside the visitor trait protocol — it never crosses the FFI boundary as an opaque handle. It was nonetheless declared as `type VisitResult;` in the swift-bridge `extern` block, so swift-bridge generated a second opaque `class VisitResult: VisitResultRefMut` that collided with the enum, making every `VisitResult` reference ambiguous and breaking compilation of consumer Swift code (e.g. html-to-markdown's Swift e2e suite). The Rust-crate emitter now collects trait-bridge result-enum names and skips their `extern`-block declaration. (`src/backends/swift/src/gen_rust_crate/mod.rs`)

- **alef-backend-extendr: keep generated R within lintr's 120-character line limit.** Two emitters produced over-long lines that failed `lintr` in consumer repos: the `from_json` factory template emitted the whole `.Call(...)` invocation on one line (exceeding 120 chars for longer type names), and roxygen `@field` doc lines concatenated long field descriptions past the limit. The factory template now wraps the `.Call()` body onto its own indented line, and `r_field_one_liner()` truncates over-long `@field` descriptions at a word boundary. (`src/backends/extendr/templates/r_from_json_factory.jinja`, `src/backends/extendr/src/gen_bindings.rs`)

- **alef-e2e/typescript: pin the e2e `vitest` devDependency to the shared `template_versions` constant.** The TypeScript e2e `package.json` template hardcoded `vitest ^3.0.0` while the WASM e2e template already used `template_versions::npm::VITEST` (`^4.1.5`). A regen therefore downgraded `e2e/<consumer>/package.json` out of sync with the WASM suite and with any committed `pnpm-lock.yaml` resolved against vitest 4, breaking `pnpm install --frozen-lockfile` in CI. Both e2e templates now source the same constant. (`src/e2e/templates/typescript/package.json.jinja`, `src/e2e/codegen/typescript/config.rs`)

- **alef-backend-rustler: stop emitting unreachable duplicate clauses for functions with a trailing optional parameter.** A function with N required parameters plus one trailing optional parameter is emitted as two explicit arity clauses (`f(a)` and `f(a, b)`). The longer clause additionally carried a `\\ nil` positional default on the optional parameter, which makes Elixir generate an _implicit_ lower-arity head that collides with the explicitly-emitted shorter clause — `this clause cannot match because a previous clause always matches`, fatal under `mix compile --warnings-as-errors`. Positional defaults are now applied only when the function emits a single clause. (`src/backends/rustler/src/gen_bindings/mod.rs`)

## [0.17.30] - 2026-05-22

### Fixed

- **alef-backend-java: stop emitting broken duplicate streaming methods on the FFI and facade classes.** A streaming adapter with an `owner_type` is bound as an instance method on the owner's opaque-handle class (`types.rs::gen_streaming_method`) — that is the only class with the `handle` field and the `STREAM_MAPPER` / `checkLastFfiError` helpers in scope. The FFI class also emitted the same `streaming_iterator_method.jinja` body (referencing the absent `this.handle` / `STREAM_MAPPER` / `checkLastFfiError`), and the facade emitted a `static` method that delegated to that instance method — both uncompilable. Removed the streaming emission from `ffi_class.rs` and `facade.rs`; the opaque-handle class is the sole owner-typed streaming surface. (`src/backends/java/gen_bindings/ffi_class.rs`, `src/backends/java/gen_bindings/facade.rs`, `src/backends/java/templates/facade_class.jinja`)

## [0.17.29] - 2026-05-22

### Fixed

- **alef-backend-wasm: add `serde` to the generated crate's `cargo-machete` ignore list.** The WASM `Cargo.toml` declares `serde` unconditionally, but `serde` is only referenced when a config type gets an `Input` DTO (`#[derive(serde::Deserialize)]`). Consumers whose config types do not trigger an Input DTO (e.g. types not ending in `Config`) therefore failed `cargo machete` with `serde` flagged as unused. Added `serde` to the `[package.metadata.cargo-machete] ignored` array alongside the other conditionally-used dependencies (`futures`, `serde_json`, …). (`src/backends/wasm/gen_bindings/mod.rs`)

- **alef-e2e/dart: generate compilable `Vec<DTO>` contains assertions.** The Dart `contains` aggregator for `Vec<T>` fields emitted swift-bridge-style method-call accessors (`item.kind()`) and a hardcoded list of 17 candidate accessor names probed under `on NoSuchMethodError`. Dart is statically typed and flutter_rust_bridge renders struct DTOs as plain classes with `final` fields, so `item.kind()` (invoking a non-callable field) and `item.source()` (a field the type lacks) are compile errors, not runtime misses — `process_test.dart` failed to load entirely. Fixed by: (1) deriving the per-call root type from the call config `result_type` override (mirroring `swift_call_result_type`) so the IR-driven stringy-field path is selected instead of the catch-all fallback; (2) emitting property reads with no parens; (3) replacing the uncompilable hardcoded-accessor fallback with a whole-object stringification match; (4) making the substring match case-insensitive so enum/sealed-class fields (`StructureKind.function()`) match PascalCase fixture node-type values (`Function`). (`src/e2e/codegen/dart.rs`)

- **alef-backend-ffi: treat C `<stdint.h>` field-type overrides as primitives, not opaque handles.** `is_primitive_c_type_override` only recognised Rust primitive spellings (`u64`, `i32`, …), so a `fields_c_types` override naming the C type directly (`uint16_t`, `uint32_t`, `int32_t`, …) was misclassified as an opaque handle: the field accessor emitted `-> *mut <crate>::uint16_t` and a bare `null` return, producing dozens of `cannot find type` / `cannot find value` errors in generated `*-ffi` crates. Added the C `<stdint.h>` / scalar spellings to the primitive set, and corrected the opaque-handle null sentinel from the invalid bare `null` to `std::ptr::null_mut()`. (`src/backends/ffi/src/gen_bindings/types.rs`)

- **alef-backend-wasm: fix the camelCase Input DTO for config types with `Named` fields or no `Default` impl.** The data-driven WASM Input DTO had two compile-breaking bugs. (1) `Named` field types were emitted with their bare core name (`AuthHeaderFormat`), unresolved in the binding crate — they are now core-qualified (`{core_import}::{name}`), matching the core type the DTO converts into. (2) The `From<Input>` body unconditionally started from `Self::default()`, so any config type that does not implement `Default` (`CustomProviderConfig`) failed with `no function ... named 'default'`. The body now branches on the IR `TypeDef::has_default` flag: `Self::default()` + field overrides when the core type is `Default`, otherwise a direct struct literal with per-field `.map(Into::into).unwrap_or_default()`. (`src/backends/wasm/gen_bindings/functions.rs`, `src/backends/wasm/templates/gen_input_dto.jinja`)

- **alef-backend-ffi: emit opaque-handle field accessors when the field type is overridden to a struct.** When an e2e config maps a field accessor's return type to a non-scalar struct via `fields_c_types` (e.g. a getter yielding a `CitationResult`), the FFI backend set the C function's return type to `*mut CitationResult` but still generated the body from the original scalar field type — producing `obj.field as i32` against a pointer return, a C `incompatible integer to pointer conversion` error. The body generator now receives the override info and emits a handle-shaped body for opaque-handle overrides. (`src/backends/ffi/src/gen_bindings/types.rs`)

- **alef-backend-swift: place the `async` keyword before the return type in free-function forwarders.** Async free-function forwarders were emitted as `func f(...) throws -> T async {`, which is invalid Swift — `async` must precede `throws` and the return type (`func f(...) async throws -> T {`). The generated Swift module failed to compile, so every `Kreuzcrawl.scrape(...)`-style call reported `module has no member named 'scrape'`. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e/java: fix streaming-adapter call generation.** Java e2e tests for streaming adapters emitted `Module.crawlStream(engine, req)` — passing the engine handle separately even though the streaming facade method takes only the request object (the handle is encapsulated by the adapter) — and typed the collected-chunk `ArrayList` with the default `ChatCompletionChunk` instead of the adapter's declared `item_type`. Both caused `cannot find symbol` compile errors. The handle arg is now filtered out when an `owner_type` adapter is present, and the collected-chunk list is typed from `adapter.item_type`. (`src/e2e/codegen/java.rs`)

- **alef-e2e/csharp: probe a preset `MOCK_SERVER_URL` for reachability before trusting it.** The generated C# `TestSetup.cs` used a preset `MOCK_SERVER_URL` environment variable without checking the server was alive. When a test harness sourced a stale `.mock-server.env` from a previous run, every HTTP fixture request failed with `net::ERR_CONNECTION_REFUSED`. `TestSetup.cs` now makes a short-timeout TCP connection to the preset URL and, if it is unreachable, spawns a fresh mock-server — staying backward-compatible with harnesses that pre-spawn a reachable server. (`src/e2e/codegen/csharp.rs`)

- **alef-backend-csharp: disable NuGet audit in the generated compile smoke-test project.** The
  regression test that builds generated C# bindings should verify generated source compatibility,
  not require network access to nuget.org vulnerability metadata. Setting `NuGetAudit=false` keeps
  the test stable in offline CI/local runs while preserving warnings-as-errors for generated code.
  (`src/backends/csharp/tests/gen_bindings_test.rs`)

- **alef-backend-ffi: ignore the e2e `"skip"` sentinel in field-accessor C-type overrides.** The FFI field-accessor generator consults the e2e `fields_c_types` map for a C-type override. That map also carries a `"skip"` sentinel meaning "omit this field from generated C e2e assertions" — not a C type. The generator treated `"skip"` as an opaque-handle type name and emitted `-> *mut <crate>::skip` plus a bare `null` return, breaking compilation of every `*-ffi` crate whose e2e config skips any field. The sentinel is now filtered out before the override is applied, so the accessor falls through to normal IR-driven generation. (`src/backends/ffi/src/gen_bindings/types.rs`)

- **alef-backend-java: declare streaming-adapter native methods on the generated FFI class.** The Java facade emitted `crawlStream(...)` / `batchCrawlStream(...)` calls against the `*Rs` native-method class, but the `*Rs` class only declared `native` methods for plain functions — streaming adapters were skipped — so the binding failed to compile with `cannot find symbol`. The FFI-class generator now iterates `config.adapters` and emits a matching `native` method declaration (request DTO in, streaming handle out) for every streaming adapter, with all type names derived from `adapter.request_type` / `item_type` / `owner_type` / `name`. (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-backend-kotlin: wrap integer defaults for `Duration`-typed fields.** A config field whose Rust core type is `std::time::Duration` is rendered as `kotlin.time.Duration`, but `render_kotlin_default` returned an `IntLiteral` default as a bare integer, producing `Initializer type mismatch: expected 'Duration', actual 'Int'`. The default for a `TypeRef::Duration` field is now wrapped with the `.milliseconds` extension so the value matches the declared type. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-swift: make generated opaque wrapper classes conform to `Sendable`.** Async free-function forwarders wrap blocking Rust calls in `Task.detached { … }.value`, which requires the returned type to be `Sendable`. Generated opaque wrapper classes (`public class T: TRefMut`) did not conform, so Swift 6 strict-concurrency rejected the binding with `type 'T' does not conform to the 'Sendable' protocol`. The backend now emits an `extension RustBridge.T: @unchecked Sendable {}` for every generated opaque type — sound because the type wraps a `Send + Sync` Rust pointer. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-wasm: convert non-`Into`-bridged Input DTO fields with an explicit constructor.** The camelCase Input DTO's `From<Input>` body assigned every field with a blanket `v.into()`. That works for matching types and for `Option<T>: From<T>`, but not for core fields whose DTO spelling has no `From` impl — `Duration` (DTO `u64` milliseconds) and `PathBuf` (DTO `String`) failed with `the trait From<u64>/From<String> is not satisfied`. Each field's conversion expression is now derived from its IR type: `Duration` via `std::time::Duration::from_millis`, `Path` via `std::path::PathBuf::from`, both wrapped in `Into::into` so they remain valid whether the core field is `T` or `Option<T>`; all other types keep `v.into()`. (`src/backends/wasm/gen_bindings/functions.rs`, `src/backends/wasm/templates/gen_input_dto.jinja`)

- **alef-e2e/dart: silence `clippy::too_many_arguments` on `render_test_file`.** The Dart e2e `render_test_file` helper grew to eight parameters, tripping the workspace `-D warnings` clippy gate. Annotated with `#[allow(clippy::too_many_arguments)]`, matching the convention used elsewhere in the codegen crates. (`src/e2e/codegen/dart.rs`)

## [0.17.28] - 2026-05-22

### Fixed

- **alef-e2e/go: emit the `testify/assert` import only when the rendered body uses it.** The Go e2e generator predicted whether `testify` was needed from a heuristic over fixture assertion shapes and categories. The heuristic produced false positives — importing `testify/assert` into generated test files whose tests only use `t.Errorf`/`t.Fatalf` — which fails Go's `imported and not used` compile check and breaks the entire Go e2e build. Removed the heuristic; the import is now decided purely empirically: the file body is rendered into a buffer first, then the import is emitted only if that buffer contains an `assert.` reference. (`src/e2e/codegen/go.rs`)

- **alef-backend-rustler: name the public streaming wrapper after the adapter, not the owner type.** The high-level `Stream.unfold` wrapper for a streaming adapter was emitted as `{owner}_{adapter}` (e.g. `crawlenginehandle_crawl_stream`), so callers could not reach it as `Module.crawl_stream/2` the way every other binding exposes a streaming adapter — the e2e suite failed with `function Module.crawl_stream/2 is undefined`. The public wrapper is now named after the adapter (`crawl_stream`); the owner-prefixed form is retained only for the internal `_start`/`_next` NIF delegates. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/csharp: coerce nullable string expressions in `not_empty`/`is_empty` assertions.** `string.IsNullOrEmpty({expr})` was emitted for string assertions where `{expr}` may be a nullable chain such as `result.Markdown!.Citations?.ToString()`. The nullable result cannot be passed to `string.IsNullOrEmpty`, producing a C# compile error. Coerce the expression with `?? string.Empty`, matching the pattern already used for the `contains` assertion. (`src/e2e/codegen/csharp.rs`)

- **alef-scaffold/r: stop scaffolding from clobbering the generated R `NAMESPACE`.** `scaffold_r_cargo` unconditionally emitted a `packages/r/NAMESPACE` containing only the `useDynLib(...)` directive — no `export(...)` / `S3method(...)` entries. Because `alef all` runs scaffolding after the extendr backend's `generate_public_api` (which emits the full NAMESPACE via `gen_namespace`), the scaffold stub overwrote the real file on every run, leaving the installed R package with zero exported symbols — `library(htmltomarkdown)` exposed nothing and every call raised `could not find function`. Removed the NAMESPACE `GeneratedFile` from `scaffold_r_cargo`; the extendr backend is the sole owner of NAMESPACE and regenerates it in full each run. (`crates/alef-scaffold/src/languages/r.rs`)

- **alef workspace: restore CI test coverage after backend generator regressions.** Fixed C# enum/default-field emission, Java builder `@JsonProperty` annotations for single-word fields, Kotlin Android trait interface files, Rustler `force_build` line wrapping, FFI field-accessor type overrides, Swift snapshot drift, and Zig e2e JSON wrapping so the affected workspace tests compile and pass again. (`src/backends/csharp`, `src/backends/java`, `src/backends/kotlin-android`, `src/backends/rustler`, `src/backends/ffi`, `src/backends/swift`, `crates/alef-e2e`)

- **alef-backend-wasm/ffi/java: skip top-level emission of trait-bridge `clear_fn` functions to prevent duplicate-symbol errors.** `clear_fn` functions (e.g. `clear_ocr_backends`) have no `dyn Trait` parameter, so `find_bridge_param` returns `None` and the function fell through to the regular top-level emitter in each backend. The trait-bridge layer also emits these functions independently. In the WASM backend this produced two `#[wasm_bindgen(js_name = "clearXxx")]` exports in the same namespace (the bridge module is glob-re-exported via `pub use __alef_wasm_bridge_*::*`), causing wasm-bindgen to error with `"symbol already defined"`. The FFI backend emitted duplicate C symbols, and the Java backend's `trait_bridge_handles` skip-set omitted clear-function handle names, allowing a duplicate method handle through the dedup filter. Applied the `is_trait_bridge_managed_fn` guard at the top of the `api.functions` loop in alef-backend-wasm and alef-backend-ffi, and extended `trait_bridge_handles` in alef-backend-java to pre-register clear-function handle names. The extendr (R) backend was already protected — `collect_trait_bridge_fn_names` includes `clear_fn` names and the functions-loop early-continue covers them. pyo3 and csharp are unaffected. Fixes #100. (`src/backends/wasm/gen_bindings/mod.rs`, `src/backends/ffi/src/gen_bindings/mod.rs`, `src/backends/java/gen_bindings/native_lib.rs`)

- **publish workflow: make crates.io and CLI binary publishing parallel.** Removed `macos-x86_64` and `windows-x86_64` from the CLI matrix, and decoupled `build-cli` from `publish-crates` so crates.io publishing no longer gates binary builds. Homebrew bottle publishing already consumes only `cli-macos-arm64`, so the target removal drops only standalone release binaries and leaves the Homebrew path unchanged. (`.github/workflows/publish.yaml`)

- **alef-backend-go: resolve passthrough-enum struct fields to their generated named type instead of `*json.RawMessage`.** `gen_struct_type` classified any `Named` field type absent from `enum_names`, `data_enum_names`, and `struct_names` as an unresolved external type and emitted the fallback `*json.RawMessage`. Passthrough enums (untagged Rust unions like `StopSequence`, emitted as a `json.RawMessage`-backed named Go type) live only in `passthrough_enum_names`, so they were misclassified. Meanwhile the generated `UnmarshalJSON` raw-mirror struct resolves the same field via `go_optional_type`, which yields the proper `*StopSequence`. The two diverged and `s.Field = raw.Field` failed to type-check (`cannot use raw.Stop (variable of type *StopSequence) as *json.RawMessage value`). Fixed by threading `passthrough_enum_names` into `gen_struct_type` and its `is_unresolved_named` check, so every passthrough-union field resolves to its generated named type and matches the raw mirror. (`src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/src/gen_bindings/mod.rs`)

- **alef-backend-php: emit camelCase `#[php(name = ...)]` on static methods so PSR-12 callers resolve.** The static-method templates used a single `{{ name }}` placeholder for both the `#[php(name)]` attribute and the Rust `pub fn` ident, forcing the PHP-facing method name to the raw snake_case IR name. An explicit `#[php(name)]` overrides ext-php-rs's automatic snake→camel conversion, so the extension class registered e.g. `create_client` while the userland facade and stubs call `createClient` — `Call to undefined method` at runtime. Fixed by computing a separate camelCase `php_name` for the `#[php(name)]` attribute while keeping the Rust ident snake_case, mirroring the existing streaming-adapter pattern. (`src/backends/php/src/gen_bindings/functions.rs`, `src/backends/php/templates/php_{,async_}static_method_definition_{no_,with_}params.jinja`)

- **alef-backend-wasm: suppress `clippy::await_holding_refcell_ref` in generated bindings.** The generated WASM streaming iterator holds a `RefCell` borrow of its `mpsc::Receiver` across the `.await` in `next()` — unavoidable, since `Receiver::next()` borrows the receiver, and sound because WASM is single-threaded. Under `-D warnings` this tripped `clippy::await_holding_refcell_ref` and failed the clippy gate in consumer crates. Added the lint to the crate-level `#![allow(...)]` block the wasm backend already emits. (`src/backends/wasm/gen_bindings/mod.rs`)

- **alef-e2e/c: apply the cbindgen type-name prefix to streaming opaque-handle types.** The C streaming e2e test referenced the opaque handle as `{Pascal}DefaultClientChatStreamStreamHandle`, but cbindgen prepends the configured uppercase `prefix` (e.g. `LITERLLM`) to every exported type — as it already does for ordinary opaque handles. The streaming-handle emission omitted the prefix, producing `unknown type name` compile errors in `test_streaming.c` and `test_smoke.c`. Fixed by prepending `prefix_upper` to the streaming-handle type name. (`src/e2e/codegen/c.rs`)

- **alef-e2e/dart: pass named arguments for FRB-generated client methods.** FRB emits every non-`config` parameter of a generated client method as a Dart named-required parameter (e.g. `retrieveFile({required String fileId})`). The e2e codegen passed required string arguments positionally, which Dart rejects (`Too many positional arguments`), failing whole test files to load. Fixed by detecting the `client_factory` routing and emitting `name: value` for required string args on those calls, while keeping positional args for hand-written facade methods. (`src/e2e/codegen/dart.rs`)

## [0.17.27] - 2026-05-22

### Fixed

- **alef-backend-jni: drop redundant parentheses around optional-param call expressions.** The JNI shim emitter wrapped optional `String` and primitive call-site expressions in parentheses (`(if x.is_empty() { None } else { Some(x) })`). As a complete function argument these parentheses are unnecessary and trip rustc's `unused_parens` lint, which fails the `-D warnings` clippy gate in consumer crates' generated `*-jni` bindings. Emit the bare `if/else` expression instead. (`src/backends/jni/src/gen_shims.rs`)

- **alef workspace: restore clean clippy and pre-commit verification.** Removed an unused Kotlin Android backend dependency and adjusted the R e2e assertion renderer to avoid clippy's argument-count warning while preserving generated assertion behavior. This keeps `cargo clippy --workspace --all-targets --all-features -- -D warnings` and `prek run --all-files` clean before release. (`src/backends/kotlin-android/Cargo.toml`, `src/e2e/codegen/r.rs`)

- **alef-e2e/python: fix F841 unused variable when all assertions are skipped.** Python e2e codegen was assigning the result variable even when every assertion was skipped (because the field didn't exist on the result type), causing ruff F841 "local variable assigned but never used" errors. The bug occurred because the detection of result variable usage was a naive substring search on the rendered assertion buffer, which would match the variable name inside skipped comment text like "# skipped: field 'result' not available on result type". Fixed by checking only non-comment lines when detecting if the result variable is actually used in code: `temp_assertions.lines().any(|line| !line.trim().starts_with('#') && line.contains(result_var))`. When no assertions actually use the variable, it is now assigned to `_` (underscore), which correctly indicates intentional discard. Fixes 8 Python e2e F841 linting errors across test_engine.py, test_filter.py, and test_redirect.py. (`src/e2e/codegen/python/test_function.rs`)

- **alef-e2e: enable `mock_url_list` argument aliasing in streaming call selection.** The `select_best_matching_call()` function was using only the primary field name (e.g., `batch_urls`) when checking if call arguments could be satisfied from fixture input. For `mock_url_list` arguments, fixtures often use aliased names (e.g., `urls` instead of `batch_urls`), so calls with `batch_urls` were marked as unsatisfiable even when the fixture provided `urls`. This caused streaming fixtures like `batch_crawl_stream_events` (which provide `input.urls`) to fail routing to `batch_crawl_stream` (which requires `batch_urls`) and incorrectly fall back to the default call. Fixed by using `resolve_urls_field()` (which already handles aliasing) in `select_best_matching_call()` when the argument type is `mock_url_list`. Now both direct matches and aliased field names are recognized, ensuring correct call routing for all streaming batch functions. (`src/e2e/codegen/mod.rs`)

- **alef-backend-dart: fix streaming adapter method resolution in generated facade.** The Dart facade's streaming methods (e.g., `crawlStream`, `batchCrawlStream`) were calling `rust_bridge.crawlStream(...)` instead of delegating to the correct instance method on the opaque handle parameter. This caused "isn't defined" analyzer errors because FRB emits streaming functions only as instance methods on the abstract API class, not as top-level bridge functions. Fixed by: (1) updating `dart_streaming_method.jinja` to call `{{ owner_param }}.{{ method_name }}(...)` instead of `rust_bridge.{{ method_name }}(...)`; (2) extracting `owner_type` and deriving `owner_param` in `emit_streaming_adapter_methods()` so the template context receives the correct parameter name. The fix is generic across all streaming adapters and does not hardcode language-specific types. (`src/backends/dart/templates/dart_streaming_method.jinja`, `src/backends/dart/src/gen_bindings/mod.rs`)

- **alef-extract: skip underscore-prefixed `pub fn`s when building the API surface.** A leading underscore on a `pub fn` is the Rust convention for "public for technical reasons but not part of the supported API" — for example test-only helpers like `_testing_extract_languages` that are gated behind a `#[cfg(any(test, feature = "test-internals"))]` attribute so integration tests in a separate crate can reach them. The extractor scans source syntactically and does not evaluate `#[cfg]`, so it picked these methods up as ordinary public API and generated bindings (and API-reference docs) for them across every backend. The generated callers then failed to compile because the underlying method does not exist without the test feature enabled. Fixed by skipping both inherent impl methods and free functions whose name starts with `_` during extraction, so they never reach any backend or the docs generator. (`src/extract/extractor/functions.rs`, `src/extract/extractor/mod.rs`)

- **alef-backend-wasm: stop emitting the broken `*Input` config DTO.** The WASM backend generated a camelCase `*Input` deserialization struct for config-like function parameters, but the implementation (`gen_input_dto_for_type`) hard-coded the field list per type instead of reading the real struct definition. For `ProcessConfig` this produced uncompilable code: `#[derive(serde::Deserialize)]` referencing a `serde` crate that is not a WASM-crate dependency, `Option<bool>` fields assigned directly into the core type's `bool` fields, and a `String` field assigned into a `Cow<'static, str>` — eleven compile errors. The Input DTO never compiled and never shipped. Disabled it (`should_have_input_dto` now returns `false`); config-like parameters are deserialized directly into the core type via `serde_wasm_bindgen`, the behavior that shipped before the stub was added. A data-driven Input DTO that reads fields from the `ApiSurface` can be reintroduced later. (`src/backends/wasm/gen_bindings/functions.rs`)

- **alef-backend-kotlin-android: add `LongMethod` to the generated `@file:Suppress` list.** Enum type files emit `toWire()`/`fromWire()` conversion functions as exhaustive `when` expressions — one arm per variant. For wide enums (e.g. `NodeType` with ~70 variants) these exceed detekt's default `LongMethod` threshold of 60 lines (90+ lines observed), failing `detekt --build-upon-default-config` in consumer repos. These functions are inherently long machine-generated mappings that cannot be sensibly split, so `LongMethod` is suppressed alongside the existing `CyclomaticComplexMethod`/`MaxLineLength`/`TooManyFunctions` file-level suppressions. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-e2e/swift: qualify helper function calls with module name and inherit `options_type` from call config.** Swift e2e tests were calling unqualified helper functions like `extractionConfigFromJson(...)` in files that imported both Kreuzberg and RustBridge modules. Since both modules export the same functions, this caused "ambiguous use" compile errors. Additionally, Swift had no per-language override for `options_type`, so embedding tests were incorrectly using `extractionConfigFromJson` instead of `embeddingConfigFromJson`. Fixed by: (1) qualifying all helper function calls in test setup with the module name (e.g., `Kreuzberg.embeddingConfigFromJson(...)`), eliminating name collisions; (2) adding Swift-specific overrides for `embed_texts` and `embed_texts_async` calls that set `options_type = "EmbeddingConfig"`, matching configurations for other languages. Fixes Swift e2e compile errors: 5 "ambiguous use of extractionConfigFromJson", 4 "cannot convert value of type 'ExtractionConfig' to expected argument type 'EmbeddingConfig'". (`src/e2e/codegen/swift.rs`, `alef.toml` in kreuzberg repo)

- **alef-backend-rustler: enable force_build in test/dev to skip artifact downloads for unpublished versions.** The rustler_precompiled library attempts downloading precompiled artifacts before falling back to local compilation. For unreleased versions (e.g., v5.0.0-rc.1), this causes repeated 404 errors and unnecessary network latency. Fixed by adding `System.get_env("MIX_ENV") in ["test", "dev"]` to the `force_build` condition, ensuring test/dev environments skip download attempts entirely and compile from source immediately. The `{{ build_env_var }}` env var override is still available for production use. Ensures fast, reliable e2e test builds even when release artifacts aren't published. (`src/backends/rustler/src/template_env.rs`)

- **alef-backend-swift: re-export JSON deserialization helpers and add ExtractionResult property accessors with String conversion.** Swift e2e test files were calling JSON helper functions (`extractionConfigFromJson()`, `batchBytesItemFromJson()`, `batchFileItemFromJson()`) without importing RustBridge, and accessing ExtractionResult fields as properties (`.mimeType`, `.content`) with String methods (`.contains()`, `.count`). The e2e codegen intentionally avoids `import RustBridge` to prevent name collisions with first-class Swift types like `VisitResult`. Fixed by: (1) adding public re-export forwarders in the Kreuzberg module for all three JSON helper functions; (2) adding computed property accessors on ExtractionResultRef (`var mimeType: String`, `var content: String`) that convert RustString to String via `.toString()`, enabling test code to use Swift String methods directly. Fixes Swift e2e compile errors: "cannot find 'extractionConfigFromJson' in scope", "cannot find 'RustBridge' in scope", "value of type 'RustString' has no member 'contains'", and "value of type 'RustString' has no member 'count'". (`src/backends/swift/src/gen_bindings.rs`)

## [0.17.26] - 2026-05-21

### Fixed

- **alef-e2e/python: fix NameError when assertions reference undeclared result variable.** When Python e2e test fixtures had assertions but the code generator computed `has_usable_assertion = false` (edge case due to upstream miscomputation of assertion usability), the result binding would be discarded into `_` while subsequent assertions still tried to read the `result` variable. This caused NameError: "name 'result' is not defined" at test runtime. Fixed by always binding to `result_var` when there are any assertions in the fixture, even if `has_usable_assertion` is false. The variable check is now: if `fixture.assertions` is non-empty, bind the result; only use `_` discard when there are truly no assertions. Fixes Python e2e NameError on 3 tests: test_embed_texts_async_*, test_embed_texts_different_preset. (`src/e2e/codegen/python/test_function.rs`)

- **alef-backend-kotlin-android: skip trait interface emission for Android AAR.** The Android backend was unconditionally calling `emit_trait_interfaces()`, generating Kotlin trait interface files (e.g., `IDocumentExtractor.kt`, `IRenderer.kt`) in the same package as Java trait interfaces (generated by a different backend or left from a previous build). This caused "Redeclaration" errors when the Gradle compiler encountered both the `.java` and `.kt` versions of the same interface with identical signatures. The Android AAR is a self-contained JNI library without a Java facade, so trait bridge interfaces are not needed — plugin implementations would be handled by a higher-level Kotlin/JVM layer outside the AAR. Fixed by removing the `emit_trait_interfaces()` call and replacing with a clarifying comment. Kotlin trait interfaces are emitted only by alef-backend-kotlin (JVM target). Fixes Kotlin Android e2e interface redeclaration errors for IDocumentExtractor, IRenderer, IOcrBackend, IEmbeddingBackend, IPostProcessor, and IValidator. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-kotlin-android: fix `Vec<u8>` return type mapping to `ByteArray` in public facade functions.** The Android AAR's public wrapper functions were incorrectly mapping `Vec<u8>` (binary data) return types to `String` instead of `ByteArray`. This caused type mismatches at the Kotlin wrapper level when functions like `render_pdf_page_to_png` (which returns `Result<Vec<u8>>` for image bytes) were called. The root cause was that `jni_return_type_str()` didn't special-case `Vec<u8>` the way the JNI bridge emitter does. Fixed by adding explicit `Vec<u8>` → `ByteArray` handling in `jni_return_type_str()`, mirroring the JNI bridge's logic. Fixes Kotlin Android e2e compile error: "Return type mismatch: expected 'Long', actual 'ByteArray'" on renderPdfPageToPng. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-e2e/elixir: emit `func()` not `func(nil)` for zero-arg functions.** E2e test codegen was rendering `Kreuzberg.list_document_extractors(nil)` for registry functions that take no arguments, causing `FunctionClauseError` when the Rustler binding defines `def list_document_extractors()` without an optional parameter. The bug occurred because call configs (e.g., `[crates.e2e.calls.list_document_extractors]` with `args = []`) were falling back to the global `[crates.e2e.call]` args when the call's args were empty. Since `call_config.args = []` explicitly means "no args for this function," there should be no fallback. Fixed by always using the call config's args directly, never falling back to the global default args. Fixes 19 Elixir e2e test failures in registry operations (list_document_extractors, list_ocr_backends, list_validators, list_post_processors, list_renderers, list_embedding_backends, clear_* variants). (`src/e2e/codegen/elixir.rs`)

- **alef-backend-java: use `null` instead of `""` as builder default for Path-typed fields.** Builder field initialization was defaulting all non-optional String-like fields to `""`, including `java.nio.file.Path` fields. Since Path is an interface with no public constructor, assigning an empty string causes a compile error "incompatible types: java.lang.String cannot be converted to java.nio.file.Path". Fixed by special-casing `TypeRef::Path` to default to `null` — Jackson's builder only sets the field when present in deserialized JSON. (`src/backends/java/gen_bindings/types.rs`)

- **alef-e2e/csharp: fix sealed-union enum field assertions and chunk metadata navigation.** Three code generation bugs were emitting invalid C# assertions: (1) `FormatMetadata` (sealed union) was incorrectly configured as an `assert_enum_field` in alef.toml, causing `.Trim()` to be called on sealed union objects (which don't have `.Trim()`). Sealed unions should only be asserted via discriminated-union accessors (e.g., `metadata.format.excel.sheet_count`), not directly. Fixed by removing `assert_enum_fields = { "metadata.format" = "FormatMetadata" }` from alef.toml and removing the corresponding assertion from the smoke fixtures. (2) `chunks_have_heading_context` predicate was accessing `c.HeadingContext` directly instead of `c.Metadata?.HeadingContext`, causing "field not found" errors since HeadingContext lives on ChunkMetadata, not Chunk. (3) `first_chunk_starts_with_heading` used wrong property name; fixed to `c.Metadata?.HeadingContext`. Additionally enhanced `render_discriminated_union_assertion` to accept and apply `assert_enum_fields` for other enum fields within sealed unions. All assertions now correctly route through Metadata. Fixes C# e2e compile from 11 → 0 errors (81/88 tests now pass, runtime failures only). (`src/e2e/codegen/csharp.rs`, `alef.toml` in kreuzberg repo)

- **alef-e2e/python: inherit `options_type` from top-level call config.** The Python test function codegen resolved `options_type` only from per-call overrides, not from the base e2e.call config. This meant `[crates.e2e.calls.embed_texts] options_type = "EmbeddingConfig"` was ignored in Python, causing tests to emit ExtractionConfig instead of EmbeddingConfig. Fixed by falling back to the top-level `[crate] call.overrides.get("python")` options_type when the per-call override is absent, matching the C# codegen pattern. Also applied the same merge strategy to `options_via`. (`src/e2e/codegen/python/test_function.rs`)

- **alef-backend-php: emit `#[php(name = ...)]` attribute on static methods in KreuzbergApi facade.** Methods generated in `#[php_impl]` facades were not being exposed to PHP because they lacked the required `#[php(...)]` annotation. Both sync (`php_static_method_definition_{no_,}with_params.jinja`) and async (`php_async_static_method_definition_{no_,}with_params.jinja`) method templates were updated to emit the annotation. Fixes all 14 PHP e2e method-not-found errors for `embed_texts_async`, `detect_mime_type_from_bytes`, `render_pdf_page_to_png`, and other KreuzbergApi methods. (`src/backends/php/templates/php_static_method_definition_{no_,}with_params.jinja`, `src/backends/php/templates/php_async_static_method_definition_{no_,}with_params.jinja`)

- **alef-backend-kotlin{,-android}: normalize integer-like float literals in Kotlin code.** Kotlin requires float literals to have a decimal point or exponent, but Rust's f64 `Display` trait drops trailing zeros, causing codegen to emit invalid Kotlin like `val field: Double = 32` instead of `val field: Double = 32.0`. Added post-processing in `generate_bindings()` for both JVM and Android backends to fix `: Double = <digit>` → `: Double = <digit>.0` and `: Float = <digit>f` → `: Float = <digit>.0f` via regex. Fixes kotlin_android e2e compile errors in OcrQualityThresholds, TesseractConfig, and other data classes with numeric defaults. (`src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin-android/lib.rs`, `src/backends/kotlin/src/gen_bindings/literal_normalizer.rs`)

- **alef-backend-swift: use `unwrap_or` instead of `unwrap_or_else(|_| …)` in default-construction string-like serde fallbacks.** Clippy's `redundant_closure` lint rejects `unwrap_or_else(|_| Value::String(s))` when the constructor is a cheap eager expression — under `-D warnings` this becomes a compile error in consumer crates' published builds. Switched both `default_field_string_like_optional_serde.jinja` and `default_field_string_like_serde.jinja` to the eager `unwrap_or(::serde_json::Value::String(...))` form. The borrow analysis is sound: `from_str(&s)` releases its borrow before the argument is evaluated. Fixes h2m CI clippy failures in alef-generated `packages/swift/rust/src/lib.rs`. (`src/backends/swift/templates/default_field_string_like_serde.jinja`, `src/backends/swift/templates/default_field_string_like_optional_serde.jinja`)

- **alef-backend-java: skip all streaming adapters in Java facade to prevent invalid method references.** Java streaming is not supported via the FFI layer (Panama FFM is inherently blocking), so the Java backend cannot implement streaming adapters. However, the facade codegen was unconditionally emitting wrapper methods like `crawlStream()` that tried to delegate to non-existent FFI layer implementations, causing compilation errors like "cannot find symbol: method crawlStream(...)". Fixed by replacing the adapter loop in the facade with an explicit skip of all streaming adapters, ensuring no wrapper methods are generated for Java bindings regardless of adapter configuration. (`src/backends/java/gen_bindings/facade.rs`)

- **alef-e2e/csharp: inherit `assert_enum_fields` and `nested_types` from top-level call config when C# override is empty.** The C# codegen resolved `assert_enum_fields` (fields whose values are sealed unions requiring display-helper wrapping) and `nested_types` (fields whose values are custom types needing JsonSerializer.Deserialize) only from per-language overrides, not from the base e2e.call config. This caused (1) fields like `metadata.format` to skip display-helper wrapping, leading to `.Trim()` calls on non-string sealed union objects, and (2) fields like `embedding_model: "default"` to be treated as scalar strings instead of EmbeddingModelType instances. Fixed by falling back to the top-level config for both `assert_enum_fields` and `nested_types`, and building per-call effective mappings that merge top-level + C# overrides. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e/csharp: use `JsonSerializer.Deserialize<T>` for nested-type fields instead of non-existent `.FromJson()`.** When a fixture field was mapped to a `nested_type` (e.g., `embedding_model` → `EmbeddingModelType`), the codegen emitted `.FromJson(json_str)`, assuming a factory method. However, sealed records and custom JsonConverter types don't expose `.FromJson()`; deserialization must route through `JsonSerializer.Deserialize<T>`. Fixed by always emitting `JsonSerializer.Deserialize<{nested_type}>(json_str, ConfigOptions)!` for nested types, which works correctly with both custom converters and sealed unions. (`src/e2e/codegen/csharp.rs`)

- **alef-backend-rustler: collect types from internally-tagged enum variant fields.** The `collect_types_for_nif_derives` helper walks function/method/struct signatures to collect all types that need NifMap/NifStruct derives, then recursively walks their field types for the transitive closure. However, it did not walk enum variant fields, so types referenced only in enum variants (e.g., `TableGrid` in `NodeContent::Table { grid: TableGrid }`) were never included in `types_to_emit`. This caused 39+ `error[E0425]: cannot find type` errors when generating Elixir bindings with internally-tagged enums containing nested struct references. Fixed by seeding enum variant fields during initial type collection, before the transitive-closure loop. (`src/backends/rustler/src/gen_bindings/helpers.rs`)

## [0.17.25] - 2026-05-21

### Fixed

- **alef-backend-rustler: add opaque types to NIF resource wrapper emission set.** The `collect_types_for_nif_derives` helper deliberately filters out opaque types (since they don't need NifMap/NifStruct derives), but the type-iteration loop that emits rustler::Resource wrappers expected them to be present in `types_to_emit`. This caused opaque structs like `CrawlEngineHandle` to never generate their `pub struct CrawlEngineHandle { inner: Arc<...> }` wrapper and `impl rustler::Resource for CrawlEngineHandle {}` impl block, resulting in `error[E0422]: cannot find struct "CrawlEngineHandle"` across all NIF functions that use them. Fixed by re-inserting opaque types into `types_to_emit` after the initial collection, before the type-iteration loop, so Resource wrappers are generated. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-adapters: qualify `owner_type` with `core_import` in streaming NIF + use `Deref` clone.** Streaming NIF wrappers referenced `owner_type` without qualifying it, causing `unresolved import` errors when the wrapper crate uses `core_import` re-exports. Now uses `{core_import}::{owner_type}` and dereferences resources via `&*resource` for the Clone path. (`crates/alef-adapters/src/streaming.rs`)

- **alef-backend-rustler: qualify opaque-type refs with `core_import` in NIF signatures; use `Deref` instead of private `.inner` field.** Opaque types in NIF function signatures were emitted as bare identifiers (`fn foo(handle: &CrawlEngineHandle, …)`), failing to resolve when the wrapper crate uses `core_import`. Now emits `{core_import}::{type}` for the core-side type and `&*resource` for resource extraction (the `.inner` field is private to the wrapper crate). (`src/backends/rustler/src/gen_bindings/`)

- **alef-e2e: prefix unused fn params with underscore to unblock `cargo install`.** Several test-helper functions in the codegen modules had unused params that triggered `-D warnings` only under `cargo install` (which builds release with deny-warnings). Prefixed with `_` to silence. (`src/e2e/codegen/{elixir,zig}.rs`)

## [0.17.24] - 2026-05-21

### Fixed

- **alef-e2e/zig: use binding module name (not registry pkg name) for `root_source_file` path in test_apps build.zig.** The Zig test_app build.zig was referencing `../../packages/zig/src/{pkg_name}.zig` where `pkg_name` is the registry-display package name (e.g., `html_to_markdown`). But the alef-backend-zig emits the binding file at `src/{module_name}.zig` where `module_name` is the Rust crate's snake_case identifier (e.g., `html_to_markdown_rs`). When the two differ — which is the common case for crates whose Rust name carries a `-rs` suffix while the published package drops it — `zig build` fails with "no such file". Now uses `module_name` to match the actual emitted file path. (`src/e2e/codegen/zig.rs`)

- **alef-backend-kotlin/kotlin-android: synthesize `= Name()` default for non-nullable nested struct fields whose type has Rust `Default`.** Jackson's Kotlin module raises `MissingKotlinParameterException` when the wire JSON omits a non-nullable parameter (the common shape of partial-update payloads sent from test fixtures, e.g., `mapper.readValue("{\"autolinks\":false}", ConversionOptions::class.java)`). The previous emitter intentionally refused to synthesize `Name()` for `TypeRef::Named` fields with `DefaultValue::Empty` because it couldn't distinguish data classes from sealed classes and didn't know which data classes had all-default constructor params. Added a `default_constructible_types: &HashSet<String>` parameter threaded through `emit_type_with_imports → kotlin_field_default → render_kotlin_default`, populated by the kotlin-android backend with the set of non-trait, non-opaque types in the IR that have `has_default = true`. For named-type fields in this set the emitter now emits `= Name()`, which invokes the data class's synthesized no-arg constructor (every Kotlin field for these types also gets a default by the same logic). Sealed classes are still excluded — they're tracked in the existing `sealed_class_names` set. New public entry point `emit_type_pub_with_defaults_sealed_and_constructible`. Fixes html-to-markdown Kotlin Android e2e tests that pass partial-options JSON. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/src/lib.rs`, `src/backends/kotlin-android/gen_bindings.rs`)

- **alef-e2e/zig: emit local-path dependencies instead of invalid hashes for registry-mode e2e test apps.** Zig has no official package registry like PyPI/npm/Maven; when registry-mode test_apps were generated, the Zig codegen emitted a placeholder URL + dummy hash (`0000000...`) for the main dependency, which could never fetch. Fixed by always using local `.path` dependencies for Zig, regardless of `DependencyMode`, since Zig packages are resolved via filesystem paths or the Zig package manager (which is primarily git-based). Test apps continue to use relative paths that resolve correctly inside the generated directory structure. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/dart: avoid double-caret in pubspec.yaml version constraints.** When `[crates.e2e.registry.packages.dart]` specifies a version constraint like `version = "^3.5.0"`, the Dart e2e codegen was unconditionally prepending another `^`, resulting in `^^3.5.0` in test_apps pubspec.yaml. The fix checks if the version already starts with a constraint operator (`^`, `~`, `>`, `<`, `=`) and only adds `^` if not. Downstream alef.toml configs can now express constraints directly; single-version specs still get `^` for flexible pinning. (`src/e2e/codegen/dart.rs`)

- **alef-backend-kotlin: emit `@field:JsonSerialize(\`as\` = T::class)` (and `contentAs` for collections) for data-class fields whose declared type is a sealed class.** Jackson default-bean serialization dispatches serializer lookup by runtime variant subtype. Sealed-class variants (tagged or untagged) emit `@JsonSerialize(using = JsonSerializer.None::class)` to break deserializer recursion — which short-circuits to the default POJO serializer, bypassing the parent's custom `{Sealed}Serializer` and dropping the `"type"` discriminator. The new `@field:JsonSerialize(\`as\` = T::class)` annotation forces Jackson to use the declared static type, which still carries the custom serializer. Threaded a `sealed_class_names: &HashSet<String>` parameter through `emit_type_with_imports` via a new public helper `emit_type_pub_with_enum_defaults_and_sealed_classes`; the kotlin-android backend now builds the set from `serde_tag.is_some() || serde_untagged` enums and passes it. Fixes Rust-side `missing field type` failures for `OcrRequest.document`, `ChatCompletionRequest.responseFormat`/`messages`/`stop`/`toolChoice`, `RerankRequest.documents`, `UserMessage.content`, `ModerationRequest.input`, `EmbeddingRequest.input`. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/src/lib.rs`, `src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-kotlin: emit `@JsonIgnoreProperties(ignoreUnknown = true)` on data classes containing `#[serde(flatten)]` fields, and force the flatten field to nullable with default null.** Rust flatten fields collect unknown wire keys into the field's value, but Kotlin has no native equivalent — Jackson's default `FAIL_ON_UNKNOWN_PROPERTIES` made deserialization fail on every sibling key the flatten was supposed to absorb, and the flatten field itself failed `MissingKotlinParameterException` when missing from the wire JSON. The mitigation is lossy (flatten contents aren't captured) but unblocks deserialization. Fixes liter-llm `ResponseOutputItem` / `ResponseTool` 4-test cluster. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-kotlin: explicit-loop serializer for `Vec<SealedClass>` newtype variants of untagged sealed classes.** When `UserContent::Parts(Vec<ContentPart>)` was serialized via `mapper.writeValue(gen, value.value)`, Jackson iterated the list and dispatched each element to its runtime-subtype serializer (which has `@JsonSerialize(using = None::class)` to break deserializer recursion) — losing the `"type"` discriminator. Switch to `provider.findValueSerializer(ContentPart::class.java).serialize(...)` in an explicit per-element loop. Local variables use plain names (`elemSerializer`, `elem`) to satisfy ktlint's `backing-property-naming` rule, which would flag double-underscore local names even though they're not class-level properties. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-kotlin: `DefaultValue::Empty` no longer synthesises constructor calls for `Named` types or zero-values for optional fields.** Two regressions surfaced when the IR started tagging more fields with `DefaultValue::Empty`: (1) `Named(SealedClass)` fields produced `EmbeddingInput()` constructor calls — illegal because sealed classes have protected constructors; (2) optional fields like `dimensions: Int?` and `user: String?` got `0`/`""` defaults instead of `null` because the type-based default match saw the unwrapped inner type, not `Optional`. Fixed by (1) returning `None` from `render_kotlin_default` for non-enum `Named` types (fall through to type-based default), and (2) short-circuiting `kotlin_field_default` to `" = null"` whenever `optional == true && DefaultValue::Empty`. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-jni: empty-string sentinel for optional single-param JSON bodies.** The Kotlin facade emits `query?.let { MAPPER.writeValueAsString(it) } ?: ""` so a `null` query is sent as an empty `JString`. The JNI Rust shim used to call `serde_json::from_str(&req_str)` unconditionally, which fails on empty input with "EOF while parsing a value at line 1 column 0", then wrapped the (impossible) `Ok` in `Some(...)`. Added an `is_optional` parameter to `emit_single_param_unmarshal`; the general-type fallback now emits `let req: Option<T> = if req_str.is_empty() { None } else { Some(serde_json::from_str(&req_str)?) };` and the caller passes the binding through directly without wrapping. Special-case branches (Vec<u8>, Bytes, Path, Vec<String>, String) keep their old `Some(name)` wrapping; the empty-string sentinel only applies to general JSON-decoded payloads. Fixes liter-llm `DefaultClient.listFiles(query = null)` / `listBatches(query = null)`. (`src/backends/jni/src/gen_shims.rs`)

- **alef-e2e/kotlin: wrap setup + `MAPPER.readValue` inside `assertFailsWith` for error tests; trailing `Unit` to keep JUnit-recognised return type.** Two separate fixes to the kotlin/kotlin_android e2e codegen for error tests: (1) `val request = MAPPER.readValue(...)` and client construction were emitted **before** `assertFailsWith<Exception> { ... }`, so Jackson validation errors on bad-input literals (e.g. `purpose: "invalid-purpose"`) escaped the assertion and failed the test instead of being asserted. Moved both into the assertion block. (2) After (1) the lambda's last expression became `assertFailsWith<Exception> { ... }`, which returns `Exception` — Kotlin then inferred the test function's return type as `Exception`. JUnit Jupiter silently skips any `@Test` method whose return type is not `void`/`Unit`, so the affected tests disappeared from the report (162 → 114). Appended a trailing `Unit` literal in both the client-factory and flat-function error-emission branches so the lambda's last expression resolves to `Unit`. (`src/e2e/codegen/kotlin.rs`)

- **alef-backend-jni: add leading dash to `crate_suffix` so build commands use the correct package name.** The JNI backend's `build_config` set `crate_suffix: "jni"` (missing the separator), causing `alef build` to invoke `cargo build -p {name}jni` (e.g., `html-to-markdown-rsjni`) — which doesn't match the actual workspace package name `html-to-markdown-rs-jni`. Matches the pattern used by all other backends: `-php`, `-dart`, `-wasm`, `-rustler`, `-py`, `-node`, `-rb`, `-swift`, `-extendr`. Fixes `alef build` for JNI-enabled projects. (`src/backends/jni/src/gen_shims.rs`)

- **alef-e2e/streaming_assertions: parameterize streaming union type name in `has_event_variant_accessor`.** The function hardcoded `"CrawlEvent"` in every language branch, making it unusable for any consumer with a different streaming union type. Introduced a new public entry point `StreamingFieldResolver::accessor_with_streaming_context` that accepts `item_type: Option<&str>` — the unqualified name of the tagged-union type. Existing callers are unchanged: `accessor` and `accessor_with_module_qualifier` forward to the new function with the legacy default `Some("CrawlEvent")` so kreuzcrawl output is bit-for-bit identical. New consumers pass their own type name; when `item_type` is `None` the `stream.has_*_event` branches return `None` and the assertion is silently skipped. Also fixed four pre-existing test call sites in `go.rs` that were passing 4 arguments to the 5-argument `render_test_function`. (`src/e2e/codegen/streaming_assertions.rs`, `src/e2e/codegen/go.rs`)

- **alef-scaffold: convert hyphens to underscores in wasm package.json main/module/types filenames.** wasm-pack emits files with underscores in the name (e.g., `html_to_markdown_wasm.js`), matching Rust naming conventions. The package.json template was using `core_crate_dir` directly (e.g., `html-to-markdown`), causing `main`, `module`, and `types` fields to reference non-existent files. Added a `core_crate_file` variable that replaces hyphens with underscores before interpolating into the entry-point paths. (`crates/alef-scaffold/src/languages/wasm.rs`)

## [0.17.23] - 2026-05-21

### Fixed

- **alef-backend-kotlin-android: exclude sealed-class enums from `enum_defaults` map.** The Kotlin-Android codegen built an `enum_name → default_variant` lookup table to synthesise constructor defaults for Named enum fields, filtering by `serde_tag.is_none() && !serde_untagged`. This incorrectly classified enums with data-carrying variants (e.g. `Other(String)`) as true Kotlin enums — but those are emitted as **sealed classes** with PascalCase object names, not `enum class` with SCREAMING_SNAKE_CASE. Generated defaults like `DocstringFormat.PYTHON_TRIPLE_QUOTE` referenced non-existent identifiers (the actual object is `DocstringFormat.PythonTripleQuote`), causing Kotlin compile errors. Added `&& en.variants.iter().all(|v| v.fields.is_empty())` to the filter so only true unit-variant enums populate the lookup table. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-kotlin: emit primitive/string/struct defaults for `DefaultValue::Empty`.** When a Rust struct derives `Default`, all its fields are tagged with `DefaultValue::Empty` in the IR. The Kotlin `render_kotlin_default` function previously only handled `Vec` (`emptyList()`), `Map` (`emptyMap()`), and `Optional` (`null`), so primitive (`Long`, `Boolean`, `Float`, …), `String`, and non-enum `Named` fields fell through to `None` — no Kotlin default emitted → data class field had no default → calling `TypeName()` failed with "No value passed for parameter". Added explicit defaults: `String → ""`, `Bool → false`, integer types → `0`/`0L`, float types → `0.0`/`0.0f`, and `Named` non-enum structs → `TypeName()` no-arg constructor (safe because `DefaultValue::Empty` only appears for types that derive `Default`, which guarantees nested defaults). Fixes tslp `CodeChunk()`, `Span()`, `FileMetrics()`, `ChunkContext()` and similar no-arg constructor calls. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-rustler: gate `From`/`Into` impls on types with NIF wrapper structs.** The From/Into conversion loop in `generate_bindings` filtered only by `is_trait` and `exclude_types`, generating `impl From<core::TypeName> for TypeName` blocks for every IR type — including types like `ConversionOptionsUpdate`, `PreprocessingOptionsUpdate`, and `NodeContext` that are not reachable from function signatures and therefore have no NIF wrapper struct generated. This caused `error[E0425]: cannot find type` for the wrapper-side identifier in the `impl From<...> for X` block. Added `types_to_emit.contains(&typ.name)` to the filter so From/Into impls are only emitted when the wrapper struct exists. Fixes html-to-markdown Elixir NIF compilation. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/csharp: emit inline predicates for synthetic chunk assertion fields.** The C# e2e test generator was missing support for synthetic field names `chunks_have_heading_context` and `first_chunk_starts_with_heading`. These are computed predicates over the chunks collection — not real fields on `ExtractionResult`. Added match arms that emit LINQ expressions: `(result.Chunks ?? new()).All(c => c.HeadingContext != null)` for `chunks_have_heading_context`, and `(result.Chunks ?? new()).FirstOrDefault()?.HeadingContext != null` for `first_chunk_starts_with_heading`. Aligns C# with TypeScript, Java, Python, and Ruby codegen patterns. Generic over field names; applies to all projects. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e/csharp: emit `Type.FromJson(...)` for nested complex-type config fields in object initializers.** The `csharp_object_initializer` function previously emitted raw JSON strings for nested struct fields (e.g., `Chunking = "{...}"`), causing C# compile errors when the field type is a complex record like `ChunkingConfig`. Fixed by using the binding-emitted `FromJson` static factory method: nested types now emit `{NestedType}.FromJson("{escaped-json}")`, matching the pattern used for top-level options via `options_via = "from_json"`. All nested types in the `nested_types` map get this treatment. Generic; applies to all config initialization paths. (`src/e2e/codegen/csharp.rs`, `csharp_object_initializer` function)

- **alef-backend-napi: derive streaming iterator struct name from adapter name, not item_type.** The previous fix in 0.17.22 named the wrapper's return type `Js{item_type_name}Iterator` (e.g., `JsCrawlEventIterator`), but the iterator struct generated by the streaming adapter body uses `to_pascal_case(adapter.name) + "Iterator"` (e.g., `CrawlStreamIterator`). The mismatch broke compilation. Fixed by switching `gen_adapter_wrapper`'s `return_iterator_type` to the same `adapter_name.to_pascal_case() + "Iterator"` convention used by `alef_adapters::streaming::iterator_name`. Generic — derives from adapter name; no kreuzcrawl-specific identifiers. (`src/backends/napi/src/gen_bindings/functions.rs`)

- **alef-e2e/csharp: emit sealed-display helper classes driven by `assert_enum_fields`.** Mirrors the java change from 0.17.21 — the C# e2e codegen now reads `assert_enum_fields` from each call's C# override, collects the distinct sealed-union type names, and emits a `{TypeName}Display.cs` helper per type with variant pattern-matching driven from the IR's `EnumDef.variants`. Projects that declare no `assert_enum_fields` get no helper files. Generic; no hardcoded type names. (`src/e2e/codegen/csharp.rs`)

- **alef-backend-kotlin: use `findValueSerializer` for sealed-class Vec fields in untagged unions.** When an untagged-union variant wraps `Vec<SealedClass>`, `mapper.writeValue` dispatched to each element's runtime-subtype serializer (which had `@JsonSerialize` reset to None on tuple variants), losing the discriminator. Switch to `provider.findValueSerializer(ElemType::class.java)` so the sealed-class serializer (which writes the `type` discriminator) is always called. Generic across all sealed-class variants. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `jni_emitter.rs`)

- **alef-e2e/python: emit inline predicates for synthetic chunk assertion fields.** The Python e2e test generator was missing support for synthetic field names `chunks_have_heading_context` and `first_chunk_starts_with_heading`. These fields are computed predicates over the chunks collection — not real fields on `ExtractionResult`. Fixed by adding match arms that emit Python comprehension expressions: `all(c.metadata and c.metadata.heading_context is not None for c in (result.chunks or []))` for `chunks_have_heading_context`, and a boolean check for the first chunk's heading context for `first_chunk_starts_with_heading`. Aligns Python with TypeScript, Java, and Ruby codegen patterns. Generic over field names; applies to all projects using these synthetic fields. (`src/e2e/codegen/python/assertions.rs`)

- **alef-backend-php: emit `#[php(prop)]` for complex field types (structs, enums, `Option<T>`).** The struct field property annotation was conditional on `is_php_prop_scalar_with_enums()`, which only returned true for primitives, strings, and enums — excluding `Option<CustomStruct>`, `Vec<CustomStruct>`, and other complex types. This prevented PHP property access to fields like `Metadata.format: Option<FormatMetadata>`, forcing callers to use getter methods instead. Fixed by emitting `#[php(prop)]` unconditionally for all fields — all generated types implement `FromZval`/`IntoZval`, so ext-php-rs can serialize/deserialize any type. Generic over type names; applies to all PHP classes regardless of field complexity. Unblocks PHP `->format` property access on nested struct fields. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-rustler: gate `from_json` NIF shims and `native.ex` stubs on types that have NIF wrapper structs.** The `from_json` NIF loop in `generate_bindings` and the corresponding stub loop in `gen_native_ex` previously iterated ALL serde-capable non-opaque types in the IR, including types like `ConversionOptionsUpdate`, `PreprocessingOptionsUpdate`, and `NodeContext` that are not reachable from function signatures and therefore have no NIF wrapper struct (`#[derive(NifMap)]`) generated. These unqualified type references compiled fine in the NIF stub `.ex` file but caused `error[E0425]: cannot find type` errors in the Rust NIF `lib.rs` because no wrapper struct was defined for them. Fixed by filtering both loops through `collect_types_for_nif_derives` (which walks the full type closure from function signatures) so `from_json` NIFs are only emitted for types that have corresponding wrapper structs. Fixes html-to-markdown Elixir NIF compilation failures after alef 0.17.21. (`src/backends/rustler/src/gen_bindings/mod.rs`, `src/backends/rustler/src/gen_bindings/helpers.rs`)

## [0.17.22] - 2026-05-21

### Fixed

- **alef-e2e/codegen: generic call selection + url-list field aliasing.** Added `select_best_matching_call` helper in `codegen/mod.rs` that walks the `alef.toml` named calls and picks the one whose required args are all satisfied by the fixture input, instead of always using the initially-resolved call. Enables fixtures with `batch_urls` / `urls` input to route to the batch variant automatically, without hardcoding call names. Added `resolve_urls_field` that tries the declared field name and common aliases (`batch_urls ↔ urls ↔ url_list ↔ urls_list`) to avoid broken empty-list emission when a fixture uses a different field name than the call config declares. Both helpers are generic over type names — no kreuzcrawl-specific identifiers. Python, TypeScript, PHP, and C# codegen wired up. (`src/e2e/codegen/mod.rs`, `python/test_function.rs`, `typescript/test_file.rs`, `php.rs`, `csharp.rs`)

- **alef-backend-napi: derive iterator/event types from adapter IR, not hardcoded names.** `gen_adapter_wrapper` previously used `if name.contains("batch")` to choose between `BatchCrawlStreamIterator` and `CrawlStreamIterator`, and hardcoded `JsCrawlEvent::from(c)` in the event cast. Replaced with `adapter.item_type` introspection: iterator class is `Js{item_type_name}Iterator`, event cast is `Js{item_type_name}::from(c)`. Takes a `core_crate: &str` param (derived from config) so the Rust path qualification is `{core_crate}::{Type}` not `kreuzcrawl::{Type}`. (`src/backends/napi/src/gen_bindings/functions.rs`, `mod.rs`)

- **alef-backend-pyo3: derive streaming wrapper params from IR type fields, not hardcoded type names.** The `emit_adapter_wrapper` function previously matched `"CrawlStreamRequest"` / `"BatchCrawlStreamRequest"` literals to decide parameter shape. Replaced with IR introspection: looks up `param.ty` in `types`, reads the first field's name and arity (`Vec<_>` → `list[str]`, else `str`), constructs the wrapper params and request construction generically. Falls back to original behavior when the type is not found in IR. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-go: emit module-level streaming adapter wrappers.** Added `gen_adapter_wrapper` in `functions.rs` and wired it into `mod.rs` so streaming adapters (declared in `alef.toml [[crates.core.adapters]]`) emit a package-level `func {AdapterName}(engine *{OwnerType}, ...) (<-chan {ItemType}, error)` wrapper. Enables test code to call `pkg.CrawlStream(engine, url)` instead of `engine.CrawlStream(url)`. All type names derived from `AdapterConfig.item_type`, `owner_type`, `request_type` — no hardcoded product names. (`src/backends/go/src/gen_bindings/functions.rs`, `mod.rs`)

- **alef-e2e/go: derive streaming item type from adapters, not literal names.** The Go e2e test renderer now receives the `adapters` slice and resolves the streaming item type by finding the matching adapter via `adapter.name`. Removed hardcoded `"CrawlEvent"` fallbacks; uses `"Item"` as the generic fallback for any adapter that doesn't declare `item_type`. (`src/e2e/codegen/go.rs`)

- **alef-backend-java: emit streaming adapter wrapper methods on facade.** Java facade now emits `Iterator<{ItemType}> {methodName}(...)` wrapper methods for streaming adapters, derived from `adapter.item_type` / `owner_type`. Template updated. (`src/backends/java/gen_bindings/facade.rs`, `mod.rs`, `templates/facade_class.jinja`)

- **alef-backend-dart: emit Stream<{ItemType}> wrapper methods.** Dart backend now emits `Stream<{ItemType}> {methodName}(...)` wrappers for streaming adapters. Uses `dart_streaming_method.jinja` template with `item_type` and `owner_type` derived from `AdapterConfig`. (`src/backends/dart/src/gen_bindings/mod.rs`, `templates/dart_streaming_method.jinja`)

- **alef-backend-jni: map empty/zero JNI null-sentinels to `None` for optional params.** Kotlin passes `""` (empty string) and `0`/`0L`/`0.0` as null-sentinels for nullable params via `value ?: ""` / `value ?: 0`, since JNI primitive signatures cannot express nullability. JNI shims now convert empty strings and numeric zeros to `None` before calling Rust, so optional Rust params receive the correct `Option<_>` value instead of `Some("")`/`Some(0)`. Added `primitive_zero_literal` helper. (`src/backends/jni/src/gen_shims.rs`)

- **alef-backend-kotlin: fix serialization annotation inheritance for newtype/tuple variants.** Kotlin sealed class newtype variants (wrapping a single inner type) do not need `@JsonSerialize(using = None::class)` / `@JsonDeserialize(using = None::class)` reset annotations — adding them defeats the parent's custom serializer, causing incorrect POJO serialization instead of the discriminator-flattened form. Removed the annotation reset from newtype variants; named-field struct variants retain it. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `type_map.rs`)

## [0.17.21] - 2026-05-21

### Fixed

- **alef-e2e/java: drive sealed-interface display helpers from IR variants, not hardcoded type names.** `render_format_metadata_display` hardcoded `"FormatMetadata"` in three places: the gate that decided whether to emit the helper, the generated `{TypeName}Display.java` class body (with every kreuzberg variant name statically listed), and the `is_format_metadata_field` predicate in assertion rendering. All three paths now derive their type names from the `assert_enum_fields` map declared in `alef.toml`. The helper class is renamed `render_sealed_display(type_name, enum_def, type_defs, java_group_id)` and loops over `EnumDef.variants` from the IR — tuple variants whose inner struct has a `format` field emit `.value().format()`, all others emit the lowercased serde variant name. A catch-all `default -> "unknown"` arm keeps generated code forward-compatible when new Rust variants are added. (`src/e2e/codegen/java.rs`)

- **alef-e2e/r: drive `.alef_format_value` wrapping from `assert_enum_fields`, not literal field path.** The R assertion renderer hardcoded `Some(f) if f == "metadata.format"` as the sole trigger for wrapping the field accessor with `.alef_format_value(...)`. Any project that declares an internally-tagged sealed union under a different field path would not get the wrapper. Replaced the literal match with `assert_enum_fields.contains_key(f)` so every field declared in the R override's `assert_enum_fields` map triggers the wrapper, regardless of field name. Threaded the map from `render_test_case` through to `render_assertion`. (`src/e2e/codegen/r.rs`)

- **alef-backend-rustler: derive `NifMap` on the full result-type closure, not just top-level types.** When a function returns `CrawlResult { pages: Vec<CrawlPageResult>, … }`, both the outer and inner types need `#[derive(NifMap)]` for the Rustler encoder to round-trip the nested struct's fields. Previously only types named directly in function signatures got the derive, so inner types like `CrawlPageResult`, `SitemapUrl`, `ActionResult`, and `CookieInfo` were opaque and their fields elided — producing `KeyError: :pages` / `:urls` / `:action_results` / `:cookies` on the Elixir side. Added `collect_types_for_nif_derives` in `helpers.rs` that walks the IR's full type closure (transitive closure over `Optional`, `Vec`, `Map`, and named-type field references) and feeds the result into the struct-derive emission. Generic over type names; uses the existing `binding_fields` + `TypeRef` introspection. Unblocks ~80 kreuzcrawl Elixir e2e tests. (`src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/elixir: route auto-selected fixtures through `resolve_call_for_fixture` unconditionally.** The Elixir e2e emitter skipped `resolve_call_for_fixture()` when `fixture.call` was absent, falling back to the default call config. Interaction fixtures (and any other category relying on `select_when` rules) therefore generated calls against `scrape_async` instead of the correct `interact_async`, asserting on a `ScrapeResult` that doesn't have `:action_results`. Removed the `if fixture.call.is_some()` guard so `select_when` always runs — matches Python codegen behaviour. Generic via existing IR `select_when` machinery. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/swift: include `config:` argument label in generated `createEngine` calls.** Swift requires explicit argument labels matching the declaration; the binding's `createEngine(config: ConfigType?)` signature was being called positionally (`createEngine(config_var)` / `createEngine(nil)`), producing "missing argument label" compile errors across the whole Swift e2e suite. Now emits `createEngine(config: config_var)` / `createEngine(config: nil)`. The `config:` label is the binding's engine-factory convention — applies uniformly to any consumer crate, not kreuzcrawl-specific. (`src/e2e/codegen/swift.rs`)

## [0.17.20] - 2026-05-21

### Fixed

- **alef-e2e/elixir: restore struct-literal emission when `options_type` is set without `options_via`.** Commit `52695749` removed the struct-literal branch on the premise that Rustler facades always need JSON strings, but the auto-generated facade signature is `def f(html, options \\ nil) when is_map(options)` — it requires a map, not a binary, and Elixir structs ARE maps. Without the struct-literal branch, fixtures with `options_type` set fall through to JSON-string emission (`f(html, "{...}")`), which crashes the facade with `FunctionClauseError`. Restored the struct-literal emission and applied the same keyword-opts threshold from `76086428`, so single-optional-arg facades get positional struct (`f(html, options)`) and multi-optional-arg facades get keyword (`f(html, options: options)`). Fixes html-to-markdown elixir e2e (262 tests previously crashing with FunctionClauseError on every options call). (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/ruby: render array literals from fixture data in positional arguments.** When `element_type` was set on a `json_object` argument (e.g., `element_type="String"`), the Ruby codegen emitted empty arrays `[]` instead of the fixture values. The tagged-enum array handler expected all elements to be objects; when the array contained primitives (strings, integers), the `filter_map` call silently dropped them. Fixed by checking that all array elements are objects BEFORE emitting as a tagged-enum array. Otherwise fall through to `json_to_ruby`, which correctly renders primitive arrays. This mirrors the behavior in Python and Node.js codegen. Fixes kreuzberg ruby e2e: `embed_texts`, `embed_texts_async` and similar string-array arguments. Add test: `ruby_array_literals_in_positional_args`. (`src/e2e/codegen/ruby.rs`)

## [0.17.19] - 2026-05-21

### Fixed

- **alef-e2e/elixir: respect keyword-opts threshold to emit valid positional/keyword arg syntax.** Elixir requires all positional arguments before keyword arguments. When a call had 1 trailing optional parameter (e.g., `config`), the code emitted it as positional, but other optional parameters were rendered as keyword form (e.g., `mime_type: "..."`), creating invalid syntax like `extract_file_async(path, mime_type: "...", "{}")` where a positional arg follows keyword args. Implemented the same threshold logic as the Rustler backend: use keyword form for 2+ trailing optional params, stay positional for 1 or 0. This ensures all json_object args (config) respect the positional/keyword decision and syntax is valid. Fixes kreuzberg elixir e2e smoke_test compile errors. (`src/e2e/codegen/elixir.rs`)

## [0.17.18] - 2026-05-21

### Fixed

- **alef-e2e/elixir: always emit options as JSON string when no options_via is configured.** The struct-literal emission path (`%Module.OptionsType{...}`) was unreachable in practice — Rustler NIF facades that don't have an `options_via` constructor expect a JSON string (`Option<String>` decoded by serde_json), not an Elixir map. Removed the struct-literal branch and the keyword-vs-positional split for JSON-encoded args: always emit positional JSON string. Fixes html-to-markdown elixir e2e codegen consistency. (`src/e2e/codegen/elixir.rs`)

- **alef-backend-kotlin: emit `@JsonValue` + `@JsonCreator` on enums for Jackson round-trip serialization.** Jackson's enum deserializer does not use `@JsonProperty` when converting `String → Enum`, only when serializing. Unit enums (e.g., `FinishReason`) now emit a `@JsonValue` method returning the wire discriminator and a `@JsonCreator` companion-object static method that parses wire strings back to variants. This enables full Jackson deserialization support so e2e tests can call `ObjectMapper.readValue(json)` on enum-carrying DTOs. Also strengthened sealed-class variant annotations from bare `@JsonDeserialize` / `@JsonSerialize` to explicit `using = JsonDeserializer.None::class` / `JsonSerializer.None::class` to clearly override inherited parent-class serializers and prevent infinite recursion. Fixes liter-llm kotlin_android e2e enum deserialization failures (65→164 passing). (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-e2e/csharp: fix 17 e2e compile errors for streaming + assertion shape.** (a) `Async` suffix was suppressed for streaming methods, but `CrawlStream`/`BatchCrawlStream` now resolve to `IAsyncEnumerable<T>` instance methods that ARE async — name them `*Async`. (b) `call_target` for streaming switched to the engine handle variable (instance method) instead of static class call. (c) The engine handle arg is stripped from the call arglist when it's the receiver. (d) `mock_url_list` args under streaming adapters are wrapped into the adapter's request type (`new BatchCrawlStreamRequest { Urls = … }`). (e) `KreuzcrawlLib.` prefix dropped from adapter request types — they're in the same namespace. (f) Empty array literals emit `new string[] { }` instead of `new[] { }` so type inference doesn't fail. (g) `is_true`/`is_false` assertions against complex object fields (CitationResult, etc.) emit `Assert.NotEmpty` instead of `Assert.True` so casts to bool don't fail. (h) Template `test_method.jinja` emits `await foreach (var item in …)` for streaming success/error branches. (`src/e2e/codegen/csharp.rs`, `src/e2e/templates/csharp/test_method.jinja`)

- **alef-backend-csharp: snake_case→PascalCase adapter names + emit wrappers for adapters with owner_type.** The `gen_adapter_wrapper` work in `55377232` used `csharp_type_name` (returns identifier as-is) for the adapter name → P/Invoke entry mapping, producing mismatched symbols like `CrawlEngineHandlecrawl_streamStart` against the actual C ABI symbol `kcrawl_crawl_engine_handle_crawl_stream_start`. Switched to `to_csharp_name` which applies snake→Pascal conversion (`crawl_stream → CrawlStream`). Also removed the `owner_type.is_none()` filter so streaming adapters that declare an owner type (e.g. CrawlEngineHandle) still get their public wrapper method emitted on the wrapper class. Unblocks 6 missing-symbol errors in the kreuzcrawl C# e2e suite. (`src/backends/csharp/gen_bindings/functions.rs`, `src/backends/csharp/gen_bindings/methods.rs`)

- **alef-e2e/php: emit default config for optional config args.** Wave 7's `&& !arg.optional` check (7fcb504d) was meant to skip defaults for optional args, but PHP facades always type config as required positional parameters. When a call has `config` marked `optional = true` in alef.toml but the PHP signature requires it, wave 7's logic fell through to the optional-args branch, emitting `null` or skipping entirely. This caused `ArgumentCountError: Too few arguments` for 32+ PHP e2e tests. Removed the `&& !arg.optional` guard so PHP always emits a default config construction (e.g., `ExtractionConfig::from_json('{}')`​) for missing fixture values, regardless of the optional flag. The `options_type` lookup with fallback to arg type correctly handles the embed_texts_async case. Fixes kreuzberg php e2e: `extractBytes`, `batchExtractBytes`, `batchExtractFile` and variants. (`src/e2e/codegen/php.rs`)

- **alef-e2e/python: detect streaming-error calls via declared streaming flag OR function name.** Python e2e tests for streaming calls with error fixtures (e.g., `stream_content_policy_error`, `stream_error_401`) were incorrectly skipping the stream-drain logic because the condition checked `!is_streaming && function_name.contains("stream")`. This was backward: when a call is declared as streaming (`call_config.streaming = Some(true)` in `alef.toml`), `is_streaming` evaluates to `true`, so the AND with `!is_streaming` killed the drain. The fixture has `"stream": true` in the input but no `stream_chunks` in the mock (it errors immediately before chunks arrive), causing the lazy async iterator to never be consumed and the error to never surface. Fixed by adopting the TypeScript logic: `is_streaming_error_call = expects_error && (is_streaming || function_name.contains("stream"))`. This correctly detects both declared streaming calls with errors and heuristic function-name detection, ensuring the drain loop is emitted inside `pytest.raises()` so the exception propagates. Fixes liter-llm python e2e: `test_stream_content_policy_error`, `test_stream_error_401`. (`src/e2e/codegen/python/test_function.rs`)

## [0.17.17] - 2026-05-21

### Fixed

- **alef-e2e: remove hardcoded project-name special-casing across codegen.** Alef is a generic generator — literal consumer-repo or product names in emitted source/output strings are bugs. Sweep across `alef-e2e` codegen: (1) `elixir.rs` derives the per-project Elixir module via `module_path` instead of literal `Kreuzcrawl`; (2) `streaming_assertions.rs` adds `accessor_with_module_qualifier` so the C# crawl-event branch uses the namespace qualifier and the Rust branch uses the cargo crate name; (3) `php.rs` drops the unreachable fallback that hardcoded `\HtmlToMarkdown\ConversionOptions`; (4) `c.rs` replaces the hardcoded `LiterllmDefaultClientChatStreamStreamHandle` with `{pascal_prefix}DefaultClientChatStreamStreamHandle`; (5) `java.rs` templates the `FormatMetadataDisplay` import via `{java_group_id}` and removes `default_java_nested_types()`; (6) `csharp.rs` removes `default_csharp_nested_types()`; (7) `rust/http.rs` doc-comment rewording — emitted code already used the `dep_name` parameter correctly. Downstream impact: kreuzberg/kreuzcrawl `alef.toml` must now declare `nested_types` explicitly under their `[crates.e2e.calls.*]` sections.

- **alef-backend-go: exclude struct types from unresolved-type fallback.** Wave 7 Go fallback emitted `*json.RawMessage` for all Named types not in enum_names or data_enum_names, but legitimate struct types in the same generated package were incorrectly treated as external/unresolved. This caused type mismatches when struct types like `OcrConfig`, `ChunkingConfig` appeared in fields of other structs (e.g., `ExtractionConfig.Ocr` was emitted as `*json.RawMessage` instead of `*OcrConfig`), breaking Go compilation with "cannot use &v as *json.RawMessage" errors. Added a `struct_names` HashSet to track all non-opaque struct type names, passed to `gen_struct_type`, and included in the unresolved check so real struct types fall through to correct typed fields. Unblocks go binding compilation. (`src/backends/go/src/gen_bindings/mod.rs`, `src/backends/go/src/gen_bindings/types.rs`)

- **alef-e2e/java: derive streaming DTO imports from declared adapters instead of hardcoding kreuzcrawl types.** The Java test-file generator unconditionally emitted `import <pkg>.CrawlEvent; import <pkg>.CrawlStreamRequest; import <pkg>.BatchCrawlStreamRequest;` for every project with a streaming fixture, leaking kreuzcrawl-specific type names into the generic polyglot generator. Other consumers (liter-llm, kreuzberg) ship streaming with different DTOs (e.g., `ChatCompletionChunk` + `ChatCompletionRequest`) and never declare any `Crawl*` types, so the maven build failed with three `cannot find symbol` errors on every streaming-touched test class. The fix iterates `adapters`, filters to `AdapterPattern::Streaming`, and imports each adapter's `item_type` (skipping `ChatCompletionChunk` which is already emitted above) plus `request_type` (stripped of its Rust path prefix). For kreuzcrawl this produces the same three imports as before; for liter-llm it produces none extra. (`src/e2e/codegen/java.rs`)

- **alef-e2e/elixir: handle binary/list return types in min/max_length assertions and zero-arg functions.** Elixir e2e tests for binary-returning functions like `render_pdf_page_to_png` failed with `String.length/1` type errors because the assertion logic didn't account for binaries (which use `byte_size/1`) or lists (byte arrays from Rustler). The min/max_length assertion now pattern-matches all three cases: `is_binary → byte_size`, `is_list → length`, `else → String.length`. Additionally, zero-argument functions were incorrectly receiving the harness' internal `setup` dictionary from the fixture input; the argument builder now filters out the `setup` key when no explicit args are configured so functions with no parameters are called with no arguments. Added `alef_e2e_format_to_string/1` helper for FormatMetadata struct display conversion (pattern-matches on nested fields like `metadata.image.format` to extract displayable strings, falling back to struct inspection). Fixes Elixir e2e: pdf_test, smoke_image_png, extractors_list, register_document_extractor. (`src/e2e/codegen/elixir.rs`)

- **alef-backend-csharp: emit `gen_adapter_wrapper` for streaming adapters.** Adds the `IAsyncEnumerable<ItemType>` public method derived from `AdapterConfig`. Wires `&config.adapters` through `gen_bindings::emit`. (`src/backends/csharp/gen_bindings/methods.rs`, `src/backends/csharp/gen_bindings/mod.rs`)

- **alef-backend-rustler: restore `use_keyword_opts = trailing_keyword_count >= 2` threshold.** A single trailing optional param (e.g. `config: Option<T>`) now stays positional with `\\ nil` instead of collapsing to `opts \\ []`. Aligns with the common config-parameter pattern where a single JSON string or `nil` is passed positionally. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-backend-go: emit `*json.RawMessage` for unresolved external-crate Named types.** Go struct fields with unresolved `Named` types (e.g., types from external crates that alef cannot resolve to a struct definition) now fall back to `*json.RawMessage` instead of attempting to reference a non-existent Go struct. This allows JSON round-tripping of opaque external types without requiring the binding generator to understand their shape. Fields are emitted with `omitempty` tag to handle null/absent values gracefully. Prevents compile errors when polyglot repos embed external types via re-exports. (`src/backends/go/src/gen_bindings/types.rs`)

- **alef-e2e/dart: required string args use positional syntax to match hand-written facades.** Commit `2572cb72` reverted the prior `required → positional, optional → named` heuristic in favour of "always emit named", citing FRB v2's all-named convention. The revert was correct for liter-llm (whose `chat`/`embed` calls go through the `from_json` path that emits `req:` named, and the `client_factory` path that hardcodes its own arg shape), but broke every polyglot repo whose Dart surface is a hand-written facade wrapping the FRB bridge: `H2mBridge.convert(String html, {ConversionOptions? options})`, `TreeSitterLanguagePackBridge.process(String source, ProcessConfig config)`, `KreuzbergBridge.extractBytes(Uint8List content, String mimeType, [ExtractionConfig? config])`. All three failed Dart compilation with `Too few positional arguments` because the codegen emitted `process(source: 'x', _config)` against a positional signature. Restored the `required → positional, optional → named` policy (matching the `json_object` handler at line 743 and the `bytes`/`file_path` handlers above), which mirrors the Rust idiom every facade follows. Unblocks tslp, h2m, and kreuzberg Dart e2e compilation. (`src/e2e/codegen/dart.rs`)

- **alef-backend-pyo3: emit `_rust.CrawlStreamRequest` and `_rust.BatchCrawlStreamRequest` in adapter wrappers.** The generated `api.py` streaming adapter wrappers constructed the facade dataclass `CrawlStreamRequest(url=url)` and passed it to `engine.crawl_stream(req)`, but the PyO3 native method enforces strict type identity against `_rust.CrawlStreamRequest`. Tests failed with `TypeError: 'CrawlStreamRequest' object is not an instance of 'CrawlStreamRequest'` (same name, different class object). The wrapper now constructs the native pyclass via `_rust.CrawlStreamRequest(url=url)` (and `_rust.BatchCrawlStreamRequest(urls=urls)`), matching the type the underlying method expects. Unblocks ~8 python streaming e2e tests. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-php: walk raw `typ.fields` (not `binding_fields`) for trait-bridge withers.** Commit `4e18a6a5` added a `with_<field>` wither emission for trait-bridge / opaque-handle optional fields (so PHP callers can attach a visitor after constructing options), but iterated `binding_fields(&typ.fields)` which filters out `binding_excluded` entries — and trait-bridge fields like `ConversionOptions.visitor` are flagged binding-excluded so they don't appear in the generated `__construct` / `from_json` parameter list. Net result: the wither method was never emitted, every html-to-markdown PHP visitor e2e (54 tests) errored with `Call to undefined method ConversionOptions::with_visitor()`. Switched the wither loop to walk `typ.fields.iter()` directly so trait-bridge fields are reached. The wither remains gated on `Option<Named>` where `Named ∈ opaque_types ∪ bridge_type_aliases`, so non-bridge excluded fields are still skipped. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-e2e/zig: omit fixtures whose target language is outside `[crates.zig].languages`.** The Zig binding statically compiles a subset of tree-sitter grammars (it does not currently dynamically load parsers at runtime), but the e2e generator emitted tests for every fixture regardless of the fixture's `input.language`. Fixtures like `smoke_bibtex` therefore generated tests that failed to load their parser. Adds a new `languages: Vec<String>` field to `ZigConfig` and a filter in the Zig codegen that consults both `input.language` and `input.config.language` (mirroring the WASM filter from `f9e0ff50`). When the list is set and non-empty, fixtures whose target grammar is not in the list are omitted entirely from the generated test file — not emitted as `it.skip()` placeholders. Defaults to empty (all fixtures included), preserving prior behaviour for hosts that haven't opted in. tree-sitter-language-pack Zig e2e: every non-static-set fixture (e.g. `smoke_actionscript`, `smoke_bibtex`) auto-excluded. (`src/core/config/languages.rs`, `src/e2e/codegen/zig.rs`)

- **alef-backend-wasm: generate camelCase-aware Input DTOs for config-like function parameters.** WASM function parameters of types like `ProcessConfig` (ending with "Config"/"Options"/"Settings"/"Params") failed to deserialize camelCase JSON from JS because `serde_wasm_bindgen::from_value` expected snake_case field names. The JS test suite passed `{chunkMaxSize: 50}` but the Rust `ProcessConfig { chunk_max_size: ... }` field wasn't found, silently defaulting to `None`, causing chunking to silently fail with empty chunks arrays. The fix generates an Input DTO struct (e.g., `ProcessConfigInput`) with `#[serde(default, rename_all = "camelCase")]` before each function that takes such a parameter, deserializes the JS value into the DTO, then converts to the core type via a generated `From` impl. All config-like parameters now correctly round-trip camelCase JSON from JavaScript. Tree-sitter-language-pack wasm e2e: chunks tests now passing. (`src/backends/wasm/gen_bindings/functions.rs`, `src/backends/wasm/template_env.rs`, new templates: `gen_input_dto.jinja`, `serde_config_required.jinja`, `serde_config_optional.jinja`)

- **alef-backend-php: emit `with_visitor` wither on `ConversionOptions` for trait-bridge opaque types.** E2e PHP tests call `$options->with_visitor($visitorHandle)` to set trait-bridge visitor handles on options objects, but the generator only checked IR-level opaque types and skipped bridge type aliases like `VisitorHandle`. When a struct has an `Option<NamedType>` field where the named type is a trait-bridge alias, the generator now emits a wither method `with_field_name(value: NamedType) -> Self` that accepts the unwrapped type and wraps it in `Some()` before assignment. Collects both IR opaque types and `config.trait_bridges[*].type_alias` into an `all_opaque_types` set before field iteration. Fixes html-to-markdown PHP e2e visitor tests from 208/262 to 262/262 (54 visitor tests now passing). (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-csharp: emit primitive option fields as nullable to preserve Rust defaults.** Option-config types (with `typ.has_default`) were emitting primitive/string fields with C# default values (`false`, `0`, `""`), which when serialized with `WhenWritingNull` would cause issues if the user explicitly set a field to its Rust default value but different C# default. The fix makes all non-optional primitive fields in option-config types nullable in C#, defaulting to `null`. With `WhenWritingNull` serialization, unset fields stay null and get stripped from JSON, letting Rust apply its own defaults; explicitly set fields (even if matching Rust defaults) flow through as JSON, preserving user intent. Combined with the wrapper template change to use `WhenWritingNull` for FFI input serialization, this ensures that explicit user values always reach Rust while unset fields let Rust apply its defaults. Fixes html-to-markdown C# e2e from 175/262 to 262/262 (all tests passing). (`src/backends/csharp/gen_bindings/types.rs`, `templates/wrapper_class_header.jinja`)

- **alef-backend-csharp: restore `[JsonConverter]` attribute on generated enums.** Commit `342a0f0c` (fix alef-backend-java enum serialization) accidentally re-introduced a conditional `if needs_custom_converter` check in the C# enum template and enums.rs converter generation, which disabled `[JsonConverter(typeof(EnumNameJsonConverter))]` emission on standard snake_case enums. Without the converter, System.Text.Json serializes enum variants as numeric values (`0`, `1`) instead of string names (`"function"`, `"method"`), breaking all assertions that depend on serialized JSON containing variant names. The fix restores unconditional converter generation and attribute emission for all enums (not just those with non-standard naming), ensuring enum-to-string conversion works consistently. Fixes 18 tree-sitter-language-pack C# e2e test failures (410/410 now passing). (`src/backends/csharp/gen_bindings/enums.rs`, `src/backends/csharp/templates/enum_header.jinja`)

- **alef-e2e/swift: aggregate every stringy accessor when `contains` asserts against a `Vec<DTO>` field.** `XCTAssertTrue(result.imports().map { $0.source().toString() }.contains("os"))` previously relied on `result_field_accessor` naming a single "primary" accessor (e.g. `imports → source`, `structure → kind`), which fails whenever the asserted value lives on a sibling field (`ImportInfo.items` for `from pathlib import Path`, `StructureItem.name` for `MyConfig`). The codegen now walks the element type's IR fields, classifies every `String`/`Option<String>`/`Vec<String>`/serde-enum field as a "stringy" accessor, and emits a `contains(where: { item in … })` closure that gathers every text-bearing value into a `[String]` before substring-matching the expected value — mirroring python's `_alef_e2e_item_texts`. `Vec<String>` accessors are flattened via `.map { $0.as_str().toString() }` (swift-bridge wraps borrowed RustString elements as `RustStringRef`, which exposes `as_str()` from `SwiftBridgeCore.swift`, not `toString()`). The aggregator only fires when the element type carries ≥2 stringy fields, leaving the existing single-accessor path untouched for trivial cases. Unblocks 2 process tests (`testProcessPythonImportsDetail`, `testProcessRustStructureName`) in tree-sitter-language-pack swift e2e. (`src/e2e/codegen/swift.rs`, `src/e2e/field_access.rs`)

- **alef-e2e/java: gate `FormatMetadataDisplay.java` emission on presence of `FormatMetadata` in Java `assert_enum_fields`.** The helper was previously emitted unconditionally for every Java e2e harness and imports `dev.kreuzberg.FormatMetadata`, a sealed interface that only exists in the kreuzberg binding crate. Other polyglot repos (e.g. tree-sitter-language-pack) without that type failed Java compilation with `cannot find symbol: class FormatMetadata`. The generator now walks the resolved Java call overrides (`call` plus all named `calls`) and emits the helper only when at least one `assert_enum_fields` entry maps to `"FormatMetadata"`. tree-sitter-language-pack Java e2e: 0 errors → 410/410 tests passing. (`src/e2e/codegen/java.rs`)

### Added

- **alef-e2e/c: emit visitor test category for C FFI bindings.** The C e2e generator previously filtered out all visitor fixtures and panicked if any reached `render_test_file`. It now collects visitor fixtures into a separate list, generates `test_visitor.c` with per-fixture static callbacks and the full `HTMHtmVisitorCallbacks` + `htm_visitor_create` + `htm_options_set_visitor_handle` + `htm_convert` + JSON assertion pattern, adds visitor forward declarations to `test_runner.h`, wires all visitor tests into `main.c`, and includes `test_visitor.c` in the Makefile `SRCS`. Adds ~54 visitor tests to the C e2e suite (bringing html-to-markdown C e2e from 208 to 262 total). (`src/e2e/codegen/c.rs`)

### Fixed

- **alef-e2e/r: four codegen fixes to close the kreuzberg R e2e gap.** (1) `build_args_string` now returns an empty argument string whenever `args = []` is declared, regardless of fixture `input` shape — the previous fall-through emitted positional `list(...)` from harness metadata (e.g. `setup.lazy_init_required` for Go's eager-init shim), producing `unused argument` errors on no-arg wrappers like `list_document_extractors()`. (2) Empty `Vec<String>` args (`element_type = "String"`) now emit `character(0)` instead of `c()`; `c()` is `NULL` in R and extendr rejects it with `Expected Strings got Null` for `Vec<String>` Rust signatures. (3) Per-call `extra_args` is now honoured for R (mirroring Ruby/Zig/Swift) — appended verbatim after declared args, so `render_pdf_page_to_png` can fill in extendr-required positionals (`dpi`, `password`) with `NULL` when the fixture omits them. (4) Terminal `metadata.format` accessors are wrapped with a new `.alef_format_value()` helper emitted into `setup-fixtures.R` that collapses the internally-tagged `FormatMetadata` enum (`{image: {format: "PNG", ...}, excel: NULL, ...}` under `simplifyVector = FALSE`) down to the inner format string, matching the assertion expectation. The codegen also threads `result_is_bytes` into `render_assertion` so `min_length`/`max_length` assertions on raw-byte returns use `length()` instead of `nchar()` (raw vectors element-wise on `nchar`, breaking the scalar `expect_true` contract). Combined, the kreuzberg R e2e suite goes from 153/158 to 159/160 with only the env-dependent tesseract-not-registered failure remaining. (`src/e2e/codegen/r.rs`)

- **alef-backend-csharp: strip nulls (not defaults) when serializing config objects to FFI.** Commit `980e6f10` introduced `JsonSerializationOptions` (no `DefaultIgnoreCondition`) for FFI-input serialization so explicit `false`/`0` weren't silently elided, but it then included `null` for every C# nullable field (`PreprocessingPreset? Preset = null`, etc.). When the Rust source declares the corresponding field as non-`Option` (e.g. `PreprocessingOptions { preset: PreprocessingPreset }`), serde deserialisation chokes on `"preset": null` and the whole options object is dropped — regressed html-to-markdown C# e2e from 7 failures to 87. Switched `JsonSerializationOptions` to `DefaultIgnoreCondition.WhenWritingNull`, which drops null-valued nullable fields (so required Rust fields fall back to Rust defaults) while still serialising explicit `false`/`0` (so the original WhenWritingDefault regression stays fixed). (`src/backends/csharp/gen_bindings/types.rs`)

- **alef-backend-jni: treat empty-string complex-param payload as `None` for optional params.** The Kotlin/Java JNI client emits `options?.let { mapper.writeValueAsString(it) } ?: ""` for `Option<DTO>` parameters — an empty string is the legacy host-language sentinel for "caller passed null". The Rust JNI shim previously fed that empty string into `serde_json::from_str::<DTO>` unconditionally, which fails with `EOF while parsing a value at line 1 column 0` and throws `RuntimeException` from every call that omits options (e.g. `HtmlToMarkdownRs.convert("…", null)`). For optional complex params the shim now checks `is_empty()` first and yields `None` without invoking serde, leaving the existing non-empty parse path intact for genuine payloads. Required params remain strict (empty payload still raises). Fixes every kotlin_android `null` options call in html-to-markdown e2e. (`src/backends/jni/src/gen_shims.rs`)

- **alef-backend-kotlin-android: drop trait-bridge `type_alias` field when its `param_name` is in `kotlin_android.exclude_functions`.** When a host configured `[crates.kotlin_android].exclude_functions = ["visitor"]` to suppress the bridge function (e.g. because the JNI trait-handle bridge isn't implemented yet in `alef-backend-jni`), the visitor function was filtered out of the module facade but `ConversionOptions.visitor: VisitorHandle?` (and `ConversionOptionsUpdate.visitor`) was still emitted on the data classes. Since `VisitorHandle` itself has no Kotlin representation in this configuration, every Kotlin file referencing the data class failed to compile with `Unresolved reference 'VisitorHandle'`. The fix collects an `effective_excluded_types` set in `gen_bindings::emit` that includes any `trait_bridge.type_alias` whose `param_name` matches a `kotlin_android.exclude_functions` entry (or whose `exclude_languages` lists `kotlin_android`), then drops both the alias type itself and any field whose `TypeRef` references it before the data-class emission. Mirrors the existing `exclude_types`-driven filter in `alef-backend-kotlin-android/src/lib.rs` for the case where the user opts out of the bridge function alone. Fixes html-to-markdown kotlin_android e2e compilation. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-e2e/kotlin_android: emit `.orEmpty()` for `Map.get(key)` assertions in kotlin_android style.** Kotlin's `Map<K, V>.get(key)` returns `V?`, so calling `.trim()` directly on the result fails kotlin_android compilation with `Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'`. The assertion emitter previously short-circuited `field_is_optional` to `false` for any field path with `has_map_access`, regardless of target. The branch now returns `kotlin_android_style` instead, so kotlin_android assertions on map-access paths coalesce the nullable receiver via `.orEmpty()` before invoking `.trim()`/`.contains()`. The kotlin/JVM target keeps its legacy behaviour to avoid churning unrelated snapshots — Java records' platform types make the missing `.orEmpty()` harmless there. Resolves 10 MetadataTest compile errors in html-to-markdown kotlin_android e2e (`testOgBasicTags`, `testOgMultipleTags`, `testTwitterCardTags`). (`src/e2e/codegen/kotlin.rs`)

- **alef-backend-kotlin: fully-qualify `kotlin.collections.List`/`Map` inside sealed-class data variants whose siblings shadow the stdlib name.** When a sealed enum variant carries the simple name `List` (or `Map`), Kotlin resolves bare `List<T>` inside the sealed body to the nested data class rather than to `kotlin.collections.List`. The `MetadataBlock` variant of `NodeContent` declared `val entries: List<String>`, which the compiler rejected with `No type arguments expected for 'data class List : NodeContent'`. `render_type_ref_disambiguated` now consults `variant_names`: when `"List"` (resp. `"Map"`) is present as a sibling variant, generic emissions use `kotlin.collections.List<…>` / `kotlin.collections.Map<…, …>` so the stdlib type wins over the nested shadow. Fixes html-to-markdown kotlin_android NodeContent.kt compilation. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-e2e/ruby: skip all empty-string config values, not just marked enum fields.** When a fixture's config object contained a key with an empty string (e.g., `embedding_model: ""`), the Ruby codegen only skipped it if that key was registered in the call's `enum_fields` map. For enum-typed fields discovered during fixture rendering but not pre-declared in alef.toml, the empty string was rendered as a literal `''`, causing deserialization errors like "Unknown embedding preset: ". The fix: skip empty-string values unconditionally in the config builder loop — all empty strings are invalid for enum fields, regardless of whether they were pre-declared. Resolves 2 Ruby e2e failures: `embed_texts_async_preset_switch` and `embed_texts_batch`. Ruby: 88→90/91.

- **alef-e2e/swift: fix two visitor-method codegen bugs that left every visitor test silently inert.** (1) `swift_visitor_params` emitted `_ ctx: String` for every callback, but the swift backend declares the protocol method with `_ ctx: NodeContext` (a typealias to `RustBridge.NodeContext`). Swift overload resolution treats the mismatched signature as a brand-new method, so the local visitor class never overrode the protocol's default implementation and every callback silently returned `.continue` — fixtures using `.custom`/`.skip` actions produced unchanged output. Now emits `_ ctx: NodeContext`, matching the protocol declaration exactly so overrides take effect. (2) `swift_action_body` interpolated optional `String?` parameters (e.g. `visit_video`'s `src`) directly via `\(src)`, which Swift renders as `Optional("tutorial.mp4")` for fixtures whose template is `[VIDEO: {src}]` — comparing against `[VIDEO: tutorial.mp4]` always failed. Added a `swift_visitor_param_is_optional` table mirroring the `?` suffix in `swift_visitor_params` so optional-typed placeholders emit `\(src ?? "")` and unwrap to the underlying string. Combined, these two fixes take html-to-markdown swift_e2e from "compiles but 72/262 visitor assertions fail" to **262/262 passing**. (`src/e2e/codegen/swift_visitors.rs`)

- **alef-e2e/swift: emit visitor callback actions with correct case naming and tuple-variant label.** Fixture-driven callback action codegen (`swift_action_body` in `swift_visitors.rs`) emitted `.continue_` and `.custom("payload")`, both inconsistent with how the swift backend declares `VisitResult`: the `Continue` unit variant is `` case `continue` `` (backtick-escaped because `continue` is a Swift keyword), and the `Custom(String)` tuple variant is `case custom(field0: String)` — swift-bridge synthesises `field0:` labels for single-field tuple variants. Without the corrections, every fixture using a custom or continue action produced `'VisitResult' has no member 'continue_'` or `missing argument label 'field0:' in call`. Now emits `` .`continue` `` and `.custom(field0: "payload")`. (`src/e2e/codegen/swift_visitors.rs`)

- **alef-backend-swift, alef-e2e/swift: unbreak Swift e2e visitor compilation across three fronts.** Three independent codegen bugs combined to keep html-to-markdown's swift_e2e suite at hundreds of compile errors. (1) The trait-bridge protocol default extension emitted `return .continue_` (Rust-side trailing-underscore escape style) for every visitor method, but the actual enum cases are emitted via `swift_case_ident` which uses Swift-idiomatic backtick escapes (`` case `continue` ``) — producing `'VisitResult' has no member 'continue_'` at every callback site. The default extension now derives the return literal from the first unit (no-field) variant of the result enum (`swift_case_ident(&variant.name.to_lower_camel_case())`), so the generated `return .` literal matches whatever the enum declaration emits. (2) The trait-bridge `{options_type}FromJsonWith{Field}` shim (e.g. `conversionOptionsFromJsonWithVisitor`) was only emitted as a swift-bridge `extern "Rust"` function in the `RustBridge` module — there was no top-level forwarder in the user-facing module, so e2e tests calling `HtmlToMarkdown.conversionOptionsFromJsonWithVisitor(json, handle)` saw `module 'HtmlToMarkdown' has no member named 'conversionOptionsFromJsonWithVisitor'`. The swift backend now emits a public top-level wrapper alongside the `make{Trait}Handle` factory, forwarding into `RustBridge.{options_fn}`. (3) Every e2e test file imported both the user-facing module and `RustBridge`, but each opaque `extern "Rust" { type T; }` declaration produces a `public class T` in `RustBridge` that collides with the first-class Swift Codable enum/struct of the same name — `VisitResult` ambiguity was the most prominent, blocking every visitor callback signature. With the new top-level forwarder in place, the e2e codegen no longer needs `import RustBridge` (test files only reference public-module symbols), so the line is dropped from `render_test_file`. Combined, these three fixes take html-to-markdown swift_e2e from "fails at module compile" to a runnable test suite. (`src/backends/swift/src/gen_bindings.rs`, `src/e2e/codegen/swift.rs`)

- **alef-e2e/typescript: handle FormatMetadata assertions with display helper function.** TypeScript e2e codegen for optional FormatMetadata fields (e.g., `metadata.format`) was applying `String(...)` to the tagged-enum object, which returns `[object Object]` instead of the format string. Now emits a `_alefE2eFormatMetadataDisplay()` helper that pattern-matches the FormatMetadata tagged-enum variant and extracts the format field if present. Resolves Node e2e smoke_image_png test assertion failure. (`src/e2e/templates/typescript/helpers.jinja`, `src/e2e/templates/typescript/assertion.jinja`, `src/e2e/codegen/typescript/assertions.rs`)

- **alef-backend-java: emit Builders for all serializable types (has_serde=true) in Auto mode, even without has_default.** When a Rust type has `#[derive(Serialize, Deserialize)]` but implements Default manually (not via `#[derive(Default)]`), the alef extractor marks `has_default=false`, causing the Java backend to skip Builder emission. Without a Builder, Jackson deserializes nested fields like `PreprocessingOptions` using the record constructor directly, applying Java defaults (all false/0) instead of Rust defaults. This causes serialized options sent to Rust to override preset-level settings — e.g., `{"preset":"Aggressive"}` deserializes with `removeNavigation=false`, silencing the preset's intent. The fix: in `should_emit_builder()` Auto mode, force Builder emission for any type where `has_serde=true`, regardless of `has_default`. All serde types benefit from a Builder to ensure nested deserialization respects Rust defaults. Additionally fixed import generation to check `will_emit_builder` (instead of only `typ.has_default`) when deciding whether to import `java.util.Optional`, `java.util.List`, `java.util.Map`, `@JsonProperty`, and `@JsonPOJOBuilder` so Builders get required imports even when emitted for has_serde-only types. Fixes 2 failing html-to-markdown Java e2e tests: `testOptionsPreprocessingAggressive` and `testOptionsPreprocessingRemoveForms` (all 262 Java e2e tests now pass). (`src/backends/java/gen_bindings/types.rs`)

- **alef-e2e/zig: skip `chunks_have_heading_context` synthetic-field assertion instead of emitting a derived predicate.** `heading_context` on `TextChunk` is `Option<HeadingContext>` with `#[serde(skip_serializing_if = "Option::is_none")]`, so chunks without a heading context produce no JSON key at all. The previous codegen emitted a Zig predicate that called `c.object.get("heading_context")` and required the value to be non-null for every chunk, which spuriously failed on extraction results where some chunks legitimately have no heading. Matching the Ruby codegen's behaviour, the assertion is now emitted as a `// skipped:` comment. Fixes kreuzberg's `config_chunking_prepend_heading_context` zig e2e (final blocker getting kreuzberg zig to 88/88 passing). (`src/e2e/codegen/zig.rs`)

- **alef-backend-csharp: re-apply separate JsonSerializationOptions for FFI parameter serialization.** The C# generator used a single `JsonSerializerOptions` with `DefaultIgnoreCondition.WhenWritingDefault` for both deserializing FFI responses and serializing input parameters (like `ConversionOptions`) to pass to Rust. When a test explicitly set an option to false/0/null, the serializer skipped writing that field — Rust received incomplete JSON and applied defaults, overwriting the caller's intent. This fix was originally landed but accidentally removed in a refactoring. Restored: added a second `JsonSerializationOptions` (without `WhenWritingDefault`) used when serializing Named parameters and config objects in wrappers, streaming methods, and record-level methods; deserialization continues to use the original `JsonOptions` for sparse response handling. Fixes 7 failing html-to-markdown C# e2e tests: `Test_FormSelectOptions`, `Test_FormInputElements`, `Test_OptionsPreprocessingEnabledFalseSkipsCleanup`, `Test_OptionsCompactTablesTrue`, `Test_OptionsPreprocessingRemoveNavigationFalseKeepsNav`. (`src/backends/csharp/gen_bindings/types.rs`, `methods.rs`, and templates)

- **alef-backend-zig: de-duplicate `VisitorHandle` (trait-bridge `type_alias`) emission.** The zig backend emitted trait-bridge `type_alias` types twice: once at the top of the file as `pub const VisitorHandle = *anyopaque;` (the correct form, referenced by struct fields like `visitor: ?VisitorHandle` and by the bridge factory `html_visitor_handle_from_vtable`), and again later as a struct wrapper `pub const VisitorHandle = struct { _handle: *anyopaque, ... }` through the generic opaque-handle emission loop. Zig rejects the duplicate declaration with a `duplicate struct member` error at file scope, failing every zig e2e test compile. The opaque-handle loop now filters out any type whose name matches a configured `[[trait_bridges]].type_alias` (respecting the bridge's `exclude_languages = ["zig"]` setting), so the trait-bridge contract — a raw `*anyopaque` pointer — is preserved as the single emission. Fixes html-to-markdown zig e2e compilation across all 9 test files. (`src/backends/zig/src/gen_bindings/mod.rs`)

- **alef-e2e/brew: route subcommand based on fixture tags (crawl → "crawl", map → "map", else "scrape").** Brew e2e codegen was rendering every fixture with the default subcommand (hardcoded "scrape"), so fixtures tagged "crawl" or "map" were invoked as `kreuzcrawl scrape URL` instead of `kreuzcrawl crawl URL` or `kreuzcrawl map URL`. This caused fixture failures because the CLI flags and output shape differ across subcommands. Now `render_test_function` inspects the fixture's tags and calls `determine_subcommand()` to route: if tags contain "crawl" use "crawl", if tags contain "map" use "map", else use the default. Fixes brew e2e test routing for all crawl/map fixtures. (`src/e2e/codegen/brew.rs`)

- **alef-e2e/python, ruby, php: emit dict/hash literals for tagged-enum arrays in `build_args_and_setup`.** Python/Ruby/PHP e2e codegen was emitting constructor calls with kwargs (e.g., `PageAction(selector="#open", type="click")`) for array arguments with tagged-enum element types like PageAction, but the bindings (PyO3, Magnus, ext-php-rs) expect dict/hash/array literals (e.g., `{"selector": "#open", "type": "click"}`). Fixed two code paths: (1) in the `options_via == "dict"` branch of `build_args_and_setup`, when `element_type` is set and value is an array of objects, emit dict literals instead of constructor calls; (2) in the `json_object && element_type` branch, when element_type is not BatchBytesItem/BatchFileItem and value is an array, emit array of dict/hash literals. Resolves ~36 Python, Ruby, and PHP e2e failures in interaction tests (interact_click_element, interact_type_field, interact_fill_form, interact_action_sequence, etc.). (`src/e2e/codegen/python/test_function.rs:769-788`, `src/e2e/codegen/ruby.rs:1435-1453`, `src/e2e/codegen/php.rs:1475-1492`)

- **alef-e2e/swift: stop skipping `json_object` args with scalar `element_type`.** Swift e2e codegen flagged every `json_object` arg without `options_via` as unresolvable and emitted `XCTSkipIf(true, ...)` stubs. Args with a scalar `element_type` (`String`, `bool`, `i*`/`u*`, `f32`/`f64`) describe `Vec<T>` Rust parameters that the swift-bridge surface exposes as native Swift `[T]` arrays — these construct cleanly from array literals and never needed the opaque-options path. The unresolvable-arg check now excludes scalar-element json_objects, so tslp's `download_languages` call (`args = [{ name = "names", field = "languages", type = "json_object", element_type = "String" }]`) emits real `download(names: [...])` invocations instead of skip placeholders. Resolves 4 skipped tslp Swift e2e download tests (`download_empty_list`, `download_invalid_language`, `download_multiple_languages`, `download_single_language`). (`src/e2e/codegen/swift.rs`)

- **alef-scaffold/elixir: drop nonexistent `lib/` and `checksum-*.exs` from `mix.exs` `files:` list.** The scaffolded Elixir `mix.exs` unconditionally advertised `~w(lib native .formatter.exs mix.exs README* checksum-*.exs)`, but `lib/` is only written when at least one non-OptionsField trait bridge GenServer is emitted, and `checksum-*.exs` is only produced by `mix rustler_precompiled.download` — which alef does not wire into the publish workflow. `mix hex.publish` validates every entry on disk before contacting the registry and aborted with `Missing files: lib, checksum-*.exs`. The scaffold now omits `lib` unless a bridge populates it, drops `checksum-*.exs` entirely (consumers fall back to building from source via plain `rustler`), and — when `crates.output.elixir` points outside `packages/elixir/lib/` — appends the same relative `*.ex` glob already used for `elixirc_paths` so the externally-located source actually ships in the Hex tarball. Re-applies the fix originally landed as `0d987874` which was lost in a later force-push to `main`. Surfaced on spikard Hex publish (Goldziher/spikard runs 26215742325 and 26222483530). (`crates/alef-scaffold/src/languages/elixir.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-backend-swift: emit custom `init(from decoder:)` for first-class Codable structs whose Rust source has `#[derive(Default)]` or `impl Default`.** Swift's auto-synthesised Codable decoder rejects JSON that omits any non-Optional declared property, so JSON produced by Rust serializers using `#[serde(default)]` or `#[serde(skip_serializing_if = "...")]` (e.g. `{"language":"python"}` decoding into `ProcessConfig`, or empty-`Vec` fields on `ProcessResult` round-tripped via `serde_json`) failed with `keyNotFound`. The Swift backend now emits a custom decoder whenever `TypeDef.has_default` is true; each field uses `decodeIfPresent + ?? <fallback>` with the per-field literal from `FieldDef.typed_default` (`BoolLiteral`/`IntLiteral`/`FloatLiteral`/`StringLiteral`) or a type-based default (`[]`/`[:]`/`false`/`0`/`""`/`nil`). Fields with no safe Swift fallback (e.g. nested `Named` structs) decode without `??` and rely on the nested type's own decoder. `CodingKeys` is force-emitted alongside the custom decoder. Reduces tslp Swift e2e failures from 355 → <10. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e/java: handle FormatMetadata assertions with sealed-interface pattern matching.** Java e2e codegen for optional FormatMetadata fields (e.g., `metadata.format`) was incorrectly applying enum coercion `.map(v -> v.getValue()).orElse("")` which fails because FormatMetadata is a sealed interface, not a Java enum. Now registers FormatMetadata types via `assert_enum_fields` in call overrides, passes the type map to render_assertion, and applies `FormatMetadataDisplay.toDisplayString()` for all FormatMetadata fields instead of enum-specific handling. Resolves Java e2e smoke test compilation errors on metadata.format assertions. (`src/e2e/codegen/java.rs`, `kreuzberg/alef.toml`)

- **alef-e2e/wasm: omit (not auto-skip) fixtures for languages outside static-compiled set.** When `[crates.wasm].languages` was set to a curated list (e.g., 31 languages), the codegen logic set `base_include = false` for fixtures with `input.language` not in that list, but then emitted them as `it.skip()` tests instead of dropping them entirely. This produced 281 skip placeholders in tslp wasm e2e with the message `"language X not in WASM's static-compiled set"`. The fix: replace the auto-skip branch with a simple `continue`, so fixtures for unsupported languages are omitted from the test file entirely — no skip clutter, no spurious test count. Resolves wasm e2e fixture emission. (`src/e2e/codegen/wasm.rs`)

- **alef-backend-rustler: emit default-arg signatures for params with | nil in typespec, and use keyword-opts collapsing for any trailing optionals.** When a function had params marked optional in the @spec (type includes `| nil`) but not in the Rust IR (param not `Option<T>`), the Elixir wrapper counted trailing optionals only via `p.optional`, missing those with `| nil` typespecs. This caused two failures: (1) when emitting arity variants, no defaults were added (e.g., `def embed_texts_async(texts, config)` for config: `String.t() | nil`), breaking intermediate-arity calls like `embed_texts_async(["text"])`; (2) when deciding whether to use keyword-opts collapsing, only 2+ trailing optionals triggered `opts \\ []` form, leaving 1-optional functions with positional signatures that conflicted with e2e codegen's keyword emission. Fix: (1) when counting trailing optionals, check **both** `p.optional || type_str.contains("| nil")` so params with Option<T> Rust types are counted even if IR doesn't mark them `.optional`; (2) lower the keyword-opts threshold from `>= 2` to `>= 1` so e2e tests calling with keyword syntax (e.g., `extract_file_sync(path, config: "...")`) map correctly to the `opts \\ []` form. Fixes 46 Elixir e2e failures across smoke, extraction, and embedding categories. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-backend-csharp: fully-qualify all Marshal calls with global::System.Runtime.InteropServices.** C# codegen was emitting bare `Marshal.*` references in multiple locations: gen_visitor.rs (PtrToStringUTF8 conversions), trait_bridge.rs (StringToCoTaskMemUTF8 callbacks), and gen_bindings/methods.rs (JSON deserialization). Template-based codegen in named_param_handle_from_json.jinja and multiple other templates also had bare Marshal references. Without the global:: qualifier, these break compilation with CS0103: name 'Marshal' does not exist. Now all emissions use `global::System.Runtime.InteropServices.Marshal.*` consistently. Resolves C# e2e compilation errors in ExtractionResult.cs and related wrapper classes. (`src/backends/csharp/gen_visitor.rs`, `src/backends/csharp/trait_bridge.rs`, `src/backends/csharp/gen_bindings/methods.rs`, `src/backends/csharp/templates/*`)

## [0.17.16] - 2026-05-21

### Fixed

- **alef-backend-dart: emit `to_string()` (not Debug format) for borrowed-string optional returns in the FRB bridge.** `build_primitive_result_cast` mapped `Option<&str>` core returns through `.map(|v| format!("{:?}", v))`, producing Debug-quoted values (`"bash"` instead of `bash`). At the dart call site every fixture asserting raw string equality against a borrowed-string return (e.g. tslp's `detect_language_from_extension`, `detect_language_from_path`, `detect_language_from_content`, `get_*_query`) failed with the JSON-encoded literal. Now emits `.map(|v| v.to_string())` so the FRB-exposed `Option<String>` carries the raw value. Resolves ~20 tslp dart e2e language-detection assertions. (`src/backends/dart/src/gen_rust_crate/bridge_fn.rs`)

- **alef-backend-go: handle null values in optional enum deserialization and resolve external crate types.** Go's JSON unmarshalling of optional data enum fields failed when the JSON value was `null` — the code checked `len(raw.field) > 0` but `json.RawMessage` containing `"null"` has length 4. Now explicitly checks `string(raw.field) != "null"` to skip unmarshal dispatch. Additionally, fixed `Option<ProcessResult>` mapping to `*string` instead of `*ProcessResult` by using the re-exported `ProcessResult` type name in kreuzberg's extraction.rs (instead of the fully-qualified `tree_sitter_language_pack::ProcessResult` which alef cannot resolve). These fixes resolve Go e2e test failures: `Test_ConfigChunkingPrependHeadingContext`, `Test_ConfigTreeSitter`, `Test_CodeShebangDetection`, `Test_EmbedTextsAsync*` (4 tests), and `Test_SmokeImagePng`. (`src/backends/go/src/gen_bindings/types.rs`)

- **alef-adapters: install a Tokio runtime for FRB streaming bodies.** `gen_dart_body` in `alef-adapters/src/streaming.rs` wrapped the BoxStream pump in `flutter_rust_bridge::spawn(async move { ... })`. FRB's worker pool is not a Tokio runtime, so the moment the core stream touched `reqwest`/`hyper` it panicked with `there is no reactor running, must be called from the context of a Tokio 1.x runtime`. The streaming body now lazy-initialises a shared `OnceLock<tokio::runtime::Runtime>` (multi-thread, all features enabled) and drives the pump via `rt.spawn(...)`. Companion change in `alef-backend-dart/src/gen_rust_crate/cargo.rs`: emit `tokio = { version = "1", features = ["rt-multi-thread"] }` whenever the API surface has streaming adapters (also consolidates the trait-bridge tokio entry since `rt-multi-thread` includes `rt`). Surfaced on liter-llm Dart e2e — every `chat_stream` fixture failed at the first chunk with the panic above. (`crates/alef-adapters/src/streaming.rs`, `src/backends/dart/src/gen_rust_crate/cargo.rs`)

- **alef-e2e/dart: index-aware optional-path lookup for `List<T>?` accessors.** `render_dart_with_optionals` joined path segments with `.` only, so when `fields_optional` listed an entry containing `[N]` (e.g. `choices[0].message.tool_calls`) the lookup against the dot-only `path_so_far` (`choices.message.tool_calls`) missed and the renderer emitted `toolCalls[0]` instead of `toolCalls![0]`. Now tracks two parallel keys — bare (`choices.message.tool_calls`) and indexed (`choices[0].message.tool_calls`) — and reports the field as optional when either form is registered. Surfaced on liter-llm dart e2e (`tool_choice_required`, `tool_choice_specific`, and other tool-calls assertions) — Dart sound-null-safety rejected `List<ToolCall>?[0]` as a compile error. (`src/e2e/field_access.rs`)

- **alef-e2e/dart: emit FRB-style named string arguments unconditionally.** The dart e2e codegen previously emitted `_client.retrieveFile('file-nonexistent')` for non-optional string args because FRB used to mirror Rust positional signatures. Modern FRB v2 declares every parameter as named (`retrieveFile({required String fileId})`), so positional emission produces `CS9035`-style `Too many positional arguments: 0 allowed, but 1 found`. All string args now emit as `name: value` regardless of `arg_def.optional`. Resolves the remaining batch-id / file-id / response-id error-path tests in liter-llm dart e2e. (`src/e2e/codegen/dart.rs`)

- **alef-e2e/swift: only route through chunks-virtual accessor when fixture is streaming.** `src/e2e/codegen/swift.rs` was checking `is_streaming_virtual_field(f) || is_streaming_usage_path` without first gating on `is_streaming`. For non-streaming `process()` fixtures with `chunkMaxSize` set, `chunks` is a real `ProcessResult` field (`chunks() -> RustVec<CodeChunk>` via swift-bridge), not a streaming-collected local variable, so the codegen emitted `XCTAssertGreaterThanOrEqual(chunks.count, 2)` against an undefined `chunks`. Now guarded with `is_streaming &&` so non-streaming fixtures fall through to the regular field-accessor path and emit `result.chunks().count`. Resolves 4 swift e2e chunking test compile errors in tslp (`python_chunking_medium`, `python_chunking_process_detail`, `rust_chunking_process`, `rust_chunking_process_detail`). (`src/e2e/codegen/swift.rs`)

- **alef-e2e/rust mock-server: skip host-root paths (`/robots*`, `/sitemap*`) in the shared route table.** Every fixture's resolved routes — including those declared at host-root paths like `/robots.txt`, `/sitemap.xml`, or `/sitemap_index.xml` — were inserted into the shared namespaced route table at the same path. Multiple fixtures declaring `/sitemap.xml` (e.g. `sitemap_index`, `sitemap_from_robots_txt`, `engine_map_basic`) and 5+ fixtures declaring `/robots.txt` then collided last-write-wins. Whichever fixture loaded last won the shared route, so calls to `<shared>/sitemap.xml` or `<shared>/robots.txt` returned arbitrary content from an unrelated fixture. `test_sitemap_basic` saw the `sitemap_index` (3 children) instead of the per-fixture sitemap (4 URLs); same shape across every language binding's sitemap suite. Host-root paths now go only into the per-fixture listener that spawns alongside fixtures with at least one host-root route, where there's no namespace collision. Surfaced on kreuzcrawl Rust + Python + Ruby + PHP + Node + WASM + brew sitemap tests (~5 tests per binding). (`src/e2e/codegen/rust/mock_server.rs`)

### Added

- **alef-backend-wasm: env shim arms for `iswlower`, `iswupper`, `iswxdigit`, `towlower`, `memchr`, `strcmp` — required by tree-sitter external scanners under wasm32-unknown-unknown.** `gen_env_shims` previously only emitted Rust shims for `iswspace`, `iswalnum`, `towupper`, `iswalpha`; the other six C-stdlib symbols pulled in by tree-sitter parser scanners (e.g. C grammar's `iswxdigit`, Ruby/Python's `iswlower`/`iswupper`/`towlower`, several grammars' `memchr`/`strcmp`) were silently dropped, leaving the generated WASM module with unresolved `env.*` imports that wasm-pack `--target nodejs` translates to a runtime `require('env')` failure. Each new arm uses Rust's Unicode-aware `char` APIs (`is_lowercase`, `is_uppercase`, `is_ascii_hexdigit`, `to_lowercase`) for the wide-char predicates/conversions and minimal pointer-safe loops with `// SAFETY` comments for `memchr`/`strcmp`. (`src/backends/wasm/gen_bindings/functions.rs`)

### Fixed

- **alef-e2e/dart: required-positional string args no longer emit `paramName: 'value'`.** Follow-up to `cae8d538` which always emitted strings as named parameters on the assumption that all FRB-generated Dart methods take named-only args. Hand-written facade wrappers (e.g. tslp's `process(String source, ProcessConfig config)`) declare required-positional + optional-named signatures, so emitting `process(source: 'x', _config)` produced "Too few positional arguments: 2 required, 1 given." The string handler now mirrors the json_object handler's `required → positional, optional → named` policy. (`src/e2e/codegen/dart.rs`)

- **alef-e2e/dart: fix method call argument passing and enum value stringification.** (1) All FRB-generated Dart methods require named parameters (e.g., `retrieveResponse({required String responseId})`), but the e2e codegen was emitting positional arguments. Fixed by always emitting string arguments as named parameters using camelCase parameter names, matching FRB v2 conventions. (2) Dart enum `toString()` returns `EnumName.variant` (e.g., `FinishReason.stop`), but fixture assertions expect serde wire format (`"stop"`). Added `_alefE2eText()` helper that detects enums by the dot-separated format and converts camelCase variant names back to snake_case wire format (e.g., `toolCalls` → `tool_calls`). Also added `_camelToSnake()` helper for the conversion. (3) Per-language `enum_fields` from `CallOverride` (e.g., `[crates.e2e.calls.create_batch.overrides.dart]`) were not being merged into the effective enum field set. Fixed by collecting per-language override `enum_fields` and merging them with the global `fields_enum`, matching Swift's implementation. (`src/e2e/codegen/dart.rs`)

- **alef-backend-rustler: default-arg signatures for optional params in Elixir wrapper.** The wrapper generator emitted `def extract_file_sync(path, mime_type, config) do` for functions with trailing optional params, supporting only /3 arity. Fixtures calling with intermediate arities (e.g., `/1` or `/2`) failed with "undefined function extract_file_sync/2". Fixed by appending `\\ nil` defaults to trailing optional params when generating positional arity variants, producing `def extract_file_sync(path, mime_type \\ nil, config \\ nil) do` which supports /1, /2, /3 arities. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/java: wire FormatMetadataDisplay into equals assertions on Optional<FormatMetadata> fields.** The Java e2e codegen was wrapping Optional FormatMetadata with `.orElse("")` (intended for string-typed fields), yielding a type mismatch before the `FormatMetadataDisplay.toDisplayString()` conversion could apply. Now equals assertions on optional FormatMetadata fields preserve the Optional and pass the unwrapped value (via `.orElse(null)`) to the helper, which checks for null and returns "" safely. The helper itself now guards against null inputs to handle the unwrapped Optional gracefully. Fixes SmokeTest.java:55 and all other metadata.format assertions. (`src/e2e/codegen/java.rs`)

- **alef-backend-csharp: fully-qualify `Marshal.PtrToStringUTF8` in error-handling code.** The C# code generator emitted bare `Marshal.PtrToStringUTF8(ctxPtr)` in two locations (opaque handle methods with errors), causing `CS0103: The name 'Marshal' does not exist` compile errors when the generated file lacked a `using System.Runtime.InteropServices;` import. Elsewhere in the template (null-result checks) the call was fully-qualified as `global::System.Runtime.InteropServices.Marshal.PtrToStringUTF8(jsonPtr)`. Fixed both sites to use full qualification for consistency and to eliminate the import requirement. (`src/backends/csharp/gen_bindings/types.rs`)

- **alef-e2e/zig: `not_error` assertion no longer emits spurious `result != null` check.** When a fixture's call had `result_is_option = true` (returns `?T`), the `bare_result_is_option` branch in `render_assertion` grouped `not_error` with `not_empty` and emitted `try testing.expect(result != null);`. Paired with the common `{not_error, is_empty}` assertion combo, this produced contradictory `!= null` followed by `== null` checks in the same test — the `!= null` always failed at runtime when the call legitimately returned `null` (e.g., tslp's `error_detect_content_empty`, `error_detect_extension_empty`, `error_detect_path_empty`, `highlights_nonexistent_language`). Split the branch so `not_error` emits a comment-only inert line (`// not_error: covered by try propagation`) — in Zig the error case is already handled by `try` propagation on the call itself, so no runtime assertion is needed. Added regression tests in `src/e2e/tests/zig_not_error_optional_result.rs`. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/go: fix unused `fmt` import in test files.** The import logic was incorrectly combining `needs_fmt` and `needs_strings` with an OR condition, causing `fmt` to be imported whenever `strings` was needed. Since the `contains` assertions on array results use `jsonString()` instead of `fmt.Sprint()`, the import was spurious for categories like `async` and `registry_operations`. Fixed by splitting the conditions so `fmt` is imported only when actually needed (when `needs_fmt` is true), while `strings` is imported based on its own condition. (`src/e2e/codegen/go.rs`)

- **alef-e2e/elixir: handle `mock_url_list` arg type for batch URL params.** Elixir codegen lacked a `mock_url_list` branch, so batch fixtures (`batch_urls` arg in kreuzcrawl) fell through to the generic JSON-array path and emitted bare relative paths like `["/seed1", "/seed2"]`. The Rust HTTP client rejects those, leaving batch ops with `completed_count: 0`. New branch mirrors Go/C#/Python: resolves the per-fixture base URL (`System.get_env("MOCK_SERVER_<FIXTURE_ID>")` with `||` fallback to `System.get_env("MOCK_SERVER_URL") <> "/fixtures/<id>"`) and `Enum.map`'s each relative path with the prefix at runtime while passing absolute `http(s)://` URLs through unchanged via `String.starts_with?(p, "http")`. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/java: handle `mock_url_list` arg type for batch URL params.** Java codegen lacked a `mock_url_list` branch, so batch fixtures (`batch_urls` arg in kreuzcrawl) fell through to the generic JSON-array path and emitted bare relative paths inside a `String[]` literal. The Rust HTTP client rejects those, leaving batch ops with `completed_count: 0`. New branch mirrors Go/C#/Python: resolves the per-fixture base URL (`System.getenv("MOCK_SERVER_<FIXTURE_ID>")` with `getOrDefault` fallback to `MOCK_SERVER_URL + "/fixtures/<id>"`) and emits a typed `java.util.List<String>` built via `Arrays.stream(...).map(...).collect(toList())` that prefixes each relative path while passing absolute `http(s)://` URLs through unchanged. (`src/e2e/codegen/java.rs`)

- **alef-e2e/typescript: handle `mock_url_list` arg type for batch URL params.** TypeScript/Node codegen lacked a `mock_url_list` branch, so batch fixtures (`batch_urls` arg in kreuzcrawl) fell through to the generic JSON-array path and emitted bare relative paths like `["/seed1", "/seed2"]`. The Rust HTTP client rejects those, leaving batch ops with `completed_count: 0`. New branch mirrors Go/C#/Python: resolves the per-fixture base URL (`process.env.MOCK_SERVER_<FIXTURE_ID>` with `??` fallback to `process.env.MOCK_SERVER_URL/fixtures/<id>`) and maps each relative path with the prefix at runtime while passing absolute `http(s)://` URLs through unchanged via `.startsWith("http")`. (`src/e2e/codegen/typescript/test_file.rs`)

- **alef-e2e/php: handle `mock_url_list` arg type for batch URL params.** PHP codegen lacked a `mock_url_list` branch, so batch fixtures (`batch_urls` arg in kreuzcrawl) fell through to the generic JSON-array path and emitted bare relative paths like `['/seed1', '/seed2']`. The Rust HTTP client rejects those, leaving batch ops with `completed_count: 0`. New branch mirrors Go/C#/Python: resolves the per-fixture base URL (`MOCK_SERVER_<FIXTURE_ID>` via `getenv`, falling back to `MOCK_SERVER_URL/fixtures/<id>`) and `array_map`'s each relative path with the prefix at runtime while passing absolute `http(s)://` URLs through unchanged via `str_starts_with`. (`src/e2e/codegen/php.rs`)

- **alef-e2e/ruby: handle `mock_url_list` arg type for batch URL params.** Ruby codegen lacked a `mock_url_list` branch, so batch fixtures (`batch_urls` arg in kreuzcrawl) fell through to the generic JSON-array path and emitted bare relative paths like `['/seed1', '/seed2']`. The Rust HTTP client rejects those, leaving batch ops with `completed_count: 0`. New branch mirrors Go/C#/Python: resolves the per-fixture base URL (`MOCK_SERVER_<FIXTURE_ID>` env var, falling back to `MOCK_SERVER_URL/fixtures/<id>`) and prefixes each relative path at runtime while passing absolute `http(s)://` URLs through unchanged. (`src/e2e/codegen/ruby.rs`)

- **alef-e2e/java: fix FormatMetadata assertions with helper utility class.** Sealed-interface `FormatMetadata` assertions in Java e2e tests were failing with type mismatch: `Optional<FormatMetadata>.orElse("")` returns `Object` (LCM of FormatMetadata and String), which lacks `.trim()` method. Fixed by generating a `FormatMetadataDisplay` utility class in the test package that pattern-matches `FormatMetadata` variants and extracts their display strings (Image variant → `image.format()`, all others → lowercase variant name). The codegen now calls `FormatMetadataDisplay.toDisplayString(...)` instead of `.toString()`, aligning with Rust's Display impl. This unblocks kreuzberg's SmokeTest.java compilation. (`src/e2e/codegen/java.rs`)

- **alef-e2e/python: handle `mock_url_list` arg type for batch URL params.** The `mock_url_list` arg type was unhandled in the Python codegen (`src/e2e/codegen/python/test_function.rs`), causing batch fixtures to fall through to the generic JSON-array conversion and emit bare relative paths like `["/seed1", "/seed2"]`. The downstream HTTP client rejects invalid URLs and all batch operations return `completed_count=0`. The fix resolves the per-fixture base URL (`MOCK_SERVER_<FIXTURE_ID>` → `MOCK_SERVER_URL/fixtures/<id>` fallback), constructs a list comprehension that prefixes each relative path with the base URL (and preserves absolute URLs starting with `http` as-is), and adds `mock_url_list` to the `os` import detection predicate. Mirrors the working implementations in Rust, Go, and C#. Surfaced on kreuzcrawl Python e2e batch tests. (`src/e2e/codegen/python/test_function.rs`, `src/e2e/codegen/python/test_file.rs`)

- **alef-backend-zig: migrate ArrayList API to Zig 0.16+.** Zig 0.16 removed the `.init(allocator)` constructor from `ArrayList(u8)`, moving to an API where the allocator is passed at each operation instead. The streaming method codegen in `src/backends/zig/src/gen_bindings/opaque_handles.rs` was emitting the old API, causing compilation failures in the `streaming_test` e2e suite with "struct 'array_list.Aligned(u8,null)' has no member named 'init'". Migrated to `initCapacity(allocator, 0)` which binds the allocator to the ArrayList during construction, then passes it explicitly to each operation (`.append(allocator, ...)`, `.appendSlice(allocator, ...)`, `.toOwnedSlice(allocator)`, `.deinit(allocator)`). The fix unblocks liter-llm Zig e2e tests: all 21 test modules (148/148 individual tests) now pass. (`src/backends/zig/src/gen_bindings/opaque_handles.rs`)

- **alef-e2e/wasm: also look up `input.config.language` when filtering against `[crates.wasm].languages`.** Follow-up to `f23ae5d3` which only inspected the top-level `input.language` shape. tslp's smoke/process/etc. fixtures use the nested `input.config.language` shape (language is part of the typed `ProcessConfig` DTO), so the auto-skip filter was a no-op for them — all 302 smoke tests including out-of-bundle grammars (`abl`, `yuck`, …) were still emitted. Now checks both shapes in order: `input.language` first (function-call shape), then `input.config.language` (config-object shape). After this fix, tslp's `e2e/wasm/tests/smoke.test.ts` correctly renders 268 `it.skip(...)` for out-of-set grammars and 30 live `it(...)` for the curated set. (`src/e2e/codegen/wasm.rs`)

- **alef-e2e/elixir: wrap assert case expression in parentheses.** The synthetic field assertion `first_chunk_starts_with_heading` emitted `assert case ... do ... end`, which violates Elixir syntax — the `assert` macro does not accept a `do/end` block as a bare argument. Fixed by wrapping the case expression in parentheses: `assert (case ... do ... end)`. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/java: fix three e2e codegen bugs.** (1) Spurious `import dev.kreuzberg.String;` in test files: the codegen added Java built-in types like `String`, `Boolean`, `Integer` to the import collection when processing `json_object` array element types. Fixed by skipping Java built-ins via new `is_java_builtin_type()` helper that checks against the set of boxed primitive types. (2) `.isPresent()` called on `@Nullable` fields: synthetic field assertions `chunks_have_heading_context` and `first_chunk_starts_with_heading` incorrectly called `.isPresent()` on `ChunkMetadata.headingContext()` which returns `@Nullable HeadingContext` (not `Optional<HeadingContext>`). Changed both assertions to check `!= null` instead, matching the Java type contract. (3) `.getValue()` called on `FormatMetadata` sealed interface: `enum_fields = { "metadata.format" = "FormatMetadata" }` treated the sealed-interface variant like a Java enum with a `.getValue()` method, but `FormatMetadata` uses records and pattern matching. Removed `"metadata.format"` from the Java override's `enum_fields` map, allowing assertions on the field to emit the raw accessor without type coercion. (`src/e2e/codegen/java.rs`, `kreuzberg/alef.toml`)

- **alef-e2e/php: deterministic ordering of generated accessor extraction lines.** `render_test_method` in `src/e2e/codegen/php.rs` collected per-fixture array-binding accessors (`$<field> = $result-><getter>();`) into a `HashMap<String, (String, String)>` and iterated `.values()` directly when emitting them, leaking `RandomState`-randomized iteration order into the generated PHP source. Concretely, tslp's `e2e/php/tests/ProcessTest.php` flipped the relative order of `$imports` vs `$structure` bindings between back-to-back `alef e2e generate` invocations, producing noisy diffs and breaking the `CI: regen leaves zero diff` invariant. The fix swaps the map for a `BTreeMap` so iteration is sorted by field name (and updates the `render_assertion` signature to match). Added a regression test (`src/e2e/tests/php_accessor_extraction_ordering.rs`) that renders a two-array-field fixture twice and asserts both byte-equality and lexicographic ordering of the emitted lines. (`src/e2e/codegen/php.rs`, `src/e2e/tests/php_accessor_extraction_ordering.rs`)

- **alef-e2e/csharp: emit `List<string>` for `mock_url_list` args.** The `mock_url_list` arg type was unhandled in `src/e2e/codegen/csharp.rs`, so batch fixtures fell through to the generic `json_to_csharp` path which emitted `new[] { "/url1", "/url2" }` (a `string[]`). The C# binding's `BatchScrapeAsync(handle, List<string> urls)` rejects `string[]` with CS1503. New branch resolves the per-fixture base URL (`MOCK_SERVER_<FIXTURE_ID>` → `MOCK_SERVER_URL/fixtures/<id>` fallback), prefixes each relative path with it, and emits `new System.Collections.Generic.List<string>(new[] {...}.Select(p => p.StartsWith("http") ? p : base + p))`. Surfaced on kreuzcrawl C# e2e — 8 BatchTests CS1503 errors. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e/go: include `os` import when fixtures use `mock_url_list` args.** The `needs_os` predicate matched `mock_url` only; with `mock_url_list` introduced for batch URL args (3ed6aaa4), fixtures that took an array of paths still emitted `os.Getenv(...)` setup lines but the import remained absent. Generated `batch_test.go` then failed to compile with `undefined: os` (10× across batch/scrape_batch tests). Extended the predicate to cover both arg types. (`src/e2e/codegen/go.rs`)

- **alef-e2e/wasm: auto-skip fixtures whose `input.language` is outside `[crates.wasm].languages`.** The override branch in `src/e2e/codegen/wasm.rs` was previously only entered when `should_include_fixture(...)` returned false, but that helper does not inspect `fixture.input.language` — so fixtures targeting grammars absent from the static-compiled wasm bundle (e.g. `{ input: { language: "abl" } }`) were emitted as live tests and failed at runtime against a missing parser. The codegen now consults `fixture.input.language` directly: when `[crates.wasm].languages` is configured and the fixture's language falls outside that set, `base_include` is forced to `false` so the existing else-branch attaches the canonical `SkipDirective { languages: ["wasm"], reason: "language not in WASM's static-compiled set: [...]" }` and the test renders as `it.skip(...)`. Surfaced on tree-sitter-language-pack: configuring `[crates.wasm].languages = ["python", ...]` was emitting all 302 smoke tests including 294 grammars not in the wasm bundle. (`src/e2e/codegen/wasm.rs`)

- **alef-e2e/ruby: fix four e2e codegen bugs.** (1) Functions with no arguments (e.g., `list_document_extractors()`) were incorrectly receiving the fixture input as a positional argument, causing "wrong number of arguments" errors. Fixed by skipping input passing when `args` is empty (input is only for setup/mocking). (2) Empty string enum field values (e.g., `embedding_model: ""`) were passed to Ruby constructors, triggering "Unknown embedding preset: (empty string)" runtime errors. Added filtering to skip empty enum values when building config objects. (3) Synthetic field assertions `chunks_have_heading_context` and `first_chunk_starts_with_heading` referenced non-existent Chunk binding fields. Fixed by emitting skip comments since the feature is not exposed in the Ruby binding. (4) Enum-typed result fields like `metadata.format` are serialized to JSON hashes in Ruby, losing variant information and Display formatting. Fixed by emitting skip comments for `metadata.format` and variant accessors (`metadata.format.excel`) since the serialization differs from other languages. (`src/e2e/codegen/ruby.rs`)

- **alef-scaffold/zig: default scaffolded `packages/<lang>/build.zig` `ffi_path` to `target/release`.** Mirrors the alef-e2e/zig fix — the per-language `before` hook for zig builds `--release` so the scaffold's previous `target/debug` default broke `zig build test` with `unable to find dynamic system library 'kreuzberg_ffi'`. (`crates/alef-scaffold/src/languages/zig.rs`)

- **alef-e2e/go: fix three Go e2e codegen bugs.** (1) Synthetic field assertion `chunks_have_heading_context` emitted `c.Metadata == nil || !c.Metadata.HeadingContext` — but `c.Metadata` is a value type in Go (not a pointer), so `c.Metadata == nil` is a compile error. `HeadingContext` is a pointer `*HeadingContext`, so `!ptr` is also invalid. Fixed to check only `c.Metadata.HeadingContext == nil`. Similarly, `first_chunk_starts_with_heading` emitted `chunks[0].Metadata != nil && chunks[0].Metadata.HeadingContext` which treats the pointer as a boolean; fixed to check `chunks[0].Metadata.HeadingContext != nil`. (2) Variable binding for optional enum fields (e.g., `*result.Metadata.Format` where `Format` is `*FormatMetadata` struct) used `string(*field)` to coerce to string, which fails for struct pointers with "cannot convert X (type *FormatMetadata) to type string". Changed to use `fmt.Sprintf("%v", *field)` which handles both named string types and struct types. (3) The `needs_fmt` import detection did not account for optional string field bindings using `fmt.Sprintf`, causing `async_test.go` and `registry_operations_test.go` to import `"fmt"` but never use it. Added check for optional string field assertions to the `needs_fmt` logic so the `fmt` import is only emitted when actually needed. (`src/e2e/codegen/go.rs`)

- **alef-e2e/swift: fix three Swift e2e codegen bugs surfaced on tree-sitter-language-pack.** (1) Scalar accessors traversing an opaque (`type X;` in swift-bridge, emitted as `typealias X = RustBridge.X`) parent dropped trailing `()` on the leaf — e.g. `result.metrics().totalLines` instead of `result.metrics().totalLines()` — because the codegen's per-segment first-class dispatch advanced `current_type` to a first-class-eligible child without remembering that the chain had already crossed an opaque boundary. Calling a method on `RustBridge.X` always returns the OPAQUE wrapper of the next type, never the first-class Codable struct. Added a `via_opaque` cursor (mirroring the existing `via_rust_vec` cursor) that pins method-call syntax once the chain crosses any opaque segment. (2) `swift_array_contains_expr` unconditionally chained `?.map { … } ?? []`, producing `(result.foo()?.map { … } ?? []).contains(…)` for non-optional `RustVec<T>` accessors. Swift rejects optional chaining on non-optional values with "cannot use optional chaining on non-optional value of type 'RustVec<…>'". Now returns `(expr, is_optional)` and only chains `?.map` + coalesces with `?? []` when the accessor is genuinely optional. (3) The contains-via-map closure emitted snake_case `as_str()` — but swift-bridge auto-renames the Rust `fn as_str(&self) -> &str` method to lowerCamelCase `asStr()` on the Swift side, so the original closure failed with "value of type 'XRef' has no member 'as_str'". All three fixes apply at codegen time only; no IR or backend changes required. (`src/e2e/codegen/swift.rs`, `src/e2e/field_access.rs`)

- **alef-e2e/typescript: add synthetic chunk-heading assertions and enum result field coercion.** Two Node/TypeScript e2e test failures in kreuzberg. (1) Synthetic field assertions `chunks_have_heading_context` and `first_chunk_starts_with_heading` were missing from the TypeScript codegen, causing smoke tests with `prependHeadingContext: true` to fail with "expected undefined to be true". Added predicate rendering for both: `chunks_have_heading_context` checks all chunks have non-null `metadata?.headingContext`, while `first_chunk_starts_with_heading` verifies the first chunk's heading field is present. (2) Enum-typed result fields like `metadata.format` (a Rust tagged enum) are exposed as objects in Node/WASM bindings, not strings. The fixture asserts `metadata.format == "PNG"`, but the generated code was calling `.trim()` on an object, raising "trim is not a function". Added `result_enum_fields = { ... "metadata.format" = "FormatMetadata" }` to the Node call override and updated the TypeScript assertion codegen to wrap enum fields in `String()` before `.trim()` for string equality checks. (`src/e2e/codegen/typescript/assertions.rs`, `src/e2e/templates/typescript/assertion.jinja`, `alef.toml`)

- **alef-e2e/go: fix `mock_url_list` codegen so it emits a valid `[]string{...}` literal.** The earlier `mock_url_list` Go emit (3ed6aaa4) used the format string `range []{{}}{paths_literal}` which expands to `range []{}` followed by the comma-separated raw strings — producing syntactically invalid Go like `for _, p := range []{}` `/good1`, `/bad`, `/good2` `{`. Go's parser rejects it (`expected type, found '{'`). Fixed by changing the format to `range []string{{{paths_literal}}}` so the brace pair around `paths_literal` is preserved literally, yielding `range []string{`/good1`, `/bad`, `/good2`}` — which is valid Go. Surfaced on kreuzcrawl Go e2e batch and stream tests (~13 tests across batch_test.go / stream_test.go that use `mock_url_list` args). (`src/e2e/codegen/go.rs`)

- **alef-backend-java: serialize enums to lowercase JSON by default.** When a Rust enum has no explicit `#[serde(rename_all)]`, it keeps variant names unchanged (PascalCase). However, Rust code often implements custom `deserialize` functions via `parse()` that expect lowercase input. Example: `HighlightStyle` enum variants are `Bold`/`Html`/`None`/`DoubleEqual` in Rust, but the `parse()` function expects lowercase: `bold`/`html`/`none`/`doubleequal`. When Java generates enums without explicit `serde_rename_all`, now applies lowercase normalization to match Rust's actual deserialization expectations. This fixes 7 Java e2e test failures in html-to-markdown (the full `OptionsTest` suite). (`src/backends/java/gen_bindings/types.rs`)

- **alef-e2e/r: emit `invisible(fn())` for `returns_void = true` calls and resolve `function` via the R override.** `render_test_case` in `src/e2e/codegen/r.rs` used `&call_config.function` directly when emitting the call line, ignoring the per-language override. Trait-bridge call configs (e.g. `clear_document_extractors`, `clear_ocr_backends`, `clear_post_processors`, `clear_validators`, `clear_embedding_backends`, `clear_renderers`) set `function = ""` at the top level and expose the real binding name only through `[crates.e2e.calls.<call>.overrides.r]`, which produced invalid R code `result <- ()` — the empty parens parse as a syntax error in R, breaking all six `*_management.R` testthat files with `unexpected ')'`. Now resolves `function_name` via the same override-then-fallback pattern used elsewhere in the file. In addition, when `call_config.returns_void` is true the codegen emits `invisible({function}({args}))` instead of binding a `result <-`, mirroring the Java `returns_void` treatment and matching the Rust unit return contract (R's `NULL`). Surfaced on kreuzberg R e2e — `parse(R/test_document_extractor_management.R)` failed at line 9. (`src/e2e/codegen/r.rs`)

- **alef-backend-rustler: skip free functions whose name collides with a trait-bridge fn.** `gen_bindings/mod.rs` iterated `api.functions` and emitted a `#[rustler::nif]` wrapper for every entry, then iterated trait-bridge configs and emitted a second wrapper for synthesised `register_<trait>` / `unregister_<trait>` / `clear_<trait>` NIFs. When a `clear_<trait>` function was also defined as a `pub fn` in the source crate, the same name was emitted twice, causing 12 compilation warnings (6 functions × 2 clauses each) in `packages/elixir/lib/kreuzberg/native.ex` for `clear_ocr_backends`, `clear_post_processors`, `clear_validators`, `clear_embedding_backends`, `clear_document_extractors`, `clear_renderers`. Added `collect_rustler_trait_bridge_fn_names()` helper (mirrors extendr approach) and filter exclude_functions at the free-function emission site, preventing duplicate Elixir function definitions. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/elixir: emit valid `case` syntax for `first_chunk_starts_with_heading` assertion.** The codegen emitted invalid Elixir syntax `|> case do` (piping into a case expression), which is not valid outside `Code.eval_string`. Rewrite to use proper `case` expression: `case List.first(chunks || []) do` followed by pattern-match branches. Surfaced on kreuzberg Elixir e2e — `contract_test.exs:72` compilation error. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/c: emit 3-arg call for engine-factory functions with `actions` input.** `render_engine_factory_test_function` always emitted `{prefix}_{fn}(engine, url)` regardless of fixture input shape, causing 12 compile errors in `test_interaction.c` for `kcrawl_interact` which the C header declares as `(engine, url, actions)`. The fix serialises `fixture.input["actions"]` to a JSON string literal (snake_case keys, C-escaped), emits `const char* actions_json = "..."` before the call site, and appends `, actions_json` to the argument list whenever the `actions` key is present in the fixture input. (`src/e2e/codegen/c.rs`)

- **alef-e2e/c: handle opaque-struct `raw_c_result_type` in engine-factory codepath.** When a call override sets `raw_c_result_type` to a PascalCase type name (e.g. `"BatchCrawlResults"`) instead of `"char*"`, the codegen now emits `{PREFIX}{Type}* result = ...` and frees with `{prefix}_{type_snake}_free(result)` rather than falling through to the `{PREFIX}{ResultTypeName}*` path which relied on `result_type_name` being set correctly. Previously the only supported value was `"char*"`; any other string caused the opaque-handle path to emit a mismatched type declaration. (`src/e2e/codegen/c.rs`)

- **alef-e2e/c: emit opaque-handle declaration for PascalCase leaf fields in `emit_nested_accessor`.** The leaf branch of `emit_nested_accessor` previously defaulted to `char* local = accessor(handle)` for any field whose type was not a known C primitive or enum. Fields whose `fields_c_types` entry is a PascalCase struct name (e.g. `"markdown_result.citations" = "CitationResult"`) now emit `{PREFIX}{Type}* {field}_handle = accessor(handle)` and register the handle for `{prefix}_{type_snake}_free` cleanup, matching how intermediate opaque handles are emitted. Without this, `kcrawl_markdown_result_citations` (which returns `KCRAWLCitationResult*`) was assigned to `char*`, causing a type mismatch compile error in `test_markdown.c`. (`src/e2e/codegen/c.rs`)

- **alef-e2e/elixir: emit json_object config args as keyword arguments and implement synthetic field assertions.** Two separate bugs in Elixir e2e codegen caused test failures. (1) Optional `json_object` arguments (like `config` in `extract_file`, `extract_bytes`) were emitted as positional struct variables when `options_type` was set (e.g., `ExtractionConfig`), breaking argument order when mixed with optional string arguments. For `extract_file(path, mime_type?, config?)`, the codegen generated `extract_file(path, options, mime_type: "...")` instead of `extract_file(path, mime_type: "...", config: options)`, causing `FunctionClauseError` because the wrapper expects positional args in definition order. Fixed by always emitting json_object arguments as keyword arguments in Elixir (e.g., `config: options`) regardless of `options_via` setting, avoiding positional ordering conflicts with optional parameters. (2) Two synthetic field assertions were missing: `chunks_have_heading_context` (checks all chunks have non-nil metadata with non-nil heading_context) and `first_chunk_starts_with_heading` (verifies first chunk's trimmed content starts with `#` markdown syntax). Both support `is_true` and `is_false` assertion types, mirroring Rust and Java implementations. Affected 38+ test failures in kreuzberg Elixir e2e (extract_file/extract_bytes smoke tests and 8 assertion failures). (`src/e2e/codegen/elixir.rs`)

- **alef-backend-extendr: prefix multi-line `@param`/`@return` continuation lines with `#'`.** `r_roxygen_block` pushed multi-line rustdoc `# Returns` / argument bullets verbatim into the roxygen2 block via `block.push_str(text)`, losing the `#'` comment prefix on continuation lines. When the source rustdoc returned `- \`Ok(...)\` - …\n- \`Err(...)\` if …`, the second bullet emerged as a bare `- \`Err(...)\` if …` line in `R/extendr-wrappers.R`, which R parses as code and fails with `unexpected 'if'`. Added a `push_roxygen_inline_multiline` helper that keeps the first line on the same line as the tag and prefixes every continuation line with `#' `. Applied to both `@param` descriptions and `@return` text. Surfaced on kreuzberg R e2e — `devtools::load_all` aborted on parse error in extendr-wrappers.R line 175. (`src/backends/extendr/src/gen_bindings.rs`)

- **alef-e2e/zig: skip fixtures whose resolved call has `streaming = true`.** The Zig backend (`alef-backend-zig`) does not yet emit streaming free functions on the binding module — only unary entry points are exposed. The Zig e2e codegen, however, used the per-language `overrides.zig.function` (e.g. `crawl_stream`) to emit `kreuzcrawl.crawl_stream(...)` call sites, producing Zig compilation errors like `kreuzcrawl struct missing 'crawl_stream' member` for every streaming fixture (kreuzcrawl: 6 in `stream_test.zig`, 1 in `engine_test.zig`, plus the resulting unused-`result` lint). Consumer-side workarounds required adding `zig` to every `call.streaming.skip_languages` entry in `alef.toml`, which is fragile and easy to forget when new streaming calls are added. The fix lives at the codegen level: filter active fixtures by `call_config.streaming != Some(true)` in `ZigE2eCodegen::generate`, mirroring how other backends gate on call-config flags. Streaming support in the Zig backend is tracked separately (see `streaming-audit (low-tier)` notes — "Zig: last-chunk-only"); until then, streaming fixtures are silently omitted from the Zig e2e suite rather than emitting broken calls. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/go: emit `[]string{}` composite literal for `mock_url_list` batch args instead of JSON-string literal.** The Go e2e codegen's `build_args_and_setup` function treated all array arguments uniformly, converting them to JSON strings and calling `json.Unmarshal` at runtime. For `mock_url_list` arguments (batch seeding with relative fixture-server URLs), this emitted code like `["/seed1","/seed2"]` as a JSON string literal, which Go's compiler rejected with `cannot use ["/seed1","/seed2"] (untyped string constant) as []string`. The Rust e2e codegen already has correct handling in `codegen/rust/args.rs`: extract the JSON array paths, resolve each against the per-fixture mock-server URL (checking `MOCK_SERVER_<FIXTURE_ID>` env var first, then `MOCK_SERVER_URL/fixtures/<id>`), and construct a Rust `Vec<String>` with `map`/`collect`. Applied the same pattern to Go: extract paths, resolve via `os.Getenv` with fallback, and build a Go `[]string` using a `for` loop that appends resolved URLs. Also updated the `needs_strings` import detection to check for `mock_url_list` args (which need `strings.HasPrefix`), ensuring the generated test file imports `"strings"`. Surfaced on kreuzcrawl Go e2e — batch_test.go and engine_test.go compilation errors. (`src/e2e/codegen/go.rs`)

- **alef-e2e/zig: default `ffi_path` in generated `build.zig` to `target/release`.** The Zig e2e codegen emitted `orelse "../../target/debug"` while the per-language `before` hook builds the FFI crate with `--release`. With no caller-supplied `-Dffi_path` override, `zig build test` looked in `target/debug/`, found nothing, and failed with `unable to find dynamic system library 'kreuzberg_ffi'`. Aligning the default with the standard release-only workspace policy. (`src/e2e/codegen/zig.rs`)

- **alef-backend-extendr: skip free functions whose name collides with a trait-bridge fn.** `gen_bindings.rs` iterated `api.functions` and emitted a `#[extendr]` wrapper for every entry, then iterated `collect_trait_bridge_functions()` and emitted a second `#[extendr]` wrapper for the synthesised `register_<trait>` / `unregister_<trait>` / `clear_<trait>` fns. When a `clear_<trait>` function was also defined as a `pub fn` in the source crate (so it appeared in `api.functions`), the same name was emitted twice — Rust's compiler then rejected `lib.rs` with 18 `E0428` duplicate-definition errors on kreuzberg (six `clear_*` lifecycle functions: `clear_ocr_backends`, `clear_post_processors`, `clear_validators`, `clear_embedding_backends`, `clear_document_extractors`, `clear_renderers`). Added `collect_trait_bridge_fn_names()` and applied it as a filter at four emission sites: the per-function `#[extendr]` pass (`gen_bindings`), the `extendr_module!` macro registration, the `extendr-wrappers.R` wrapper file, and `NAMESPACE` exports. The trait-bridge variant is canonical because it resolves to the fully-qualified `kreuzberg::plugins::ocr_backend::clear_ocr_backends` module path rather than the top-level alias. Mirrors how `pyo3` already prefixes the bridge symbol with `_alef_` to avoid the same collision. (`src/backends/extendr/src/gen_bindings.rs`)

- **alef-e2e/r: implement `chunks_have_heading_context` and `first_chunk_starts_with_heading` synthetic field assertions.** These two synthetic field names were only implemented for Rust / Java / Go / Elixir; the R codegen fell through to the regular field-access path and emitted a struct-attribute lookup on the result, producing assertions that failed at runtime with `$ operator is invalid for atomic vectors`. Added R-native predicates mirroring the Java implementation: `chunks_have_heading_context` checks that chunks exist and every chunk has non-empty content (since `prepend_heading_context` adds heading text to the chunk content); `first_chunk_starts_with_heading` checks that the first chunk's content starts with `#` after trimming. Surfaced on kreuzberg R e2e — `test_contract.R::config_chunking_prepend_heading_context`. (`src/e2e/codegen/r.rs`)

- **alef-backend-csharp: suffix public async opaque methods with `Async`.** `gen_opaque_method` and `gen_opaque_streaming_method` derived the public C# method name from `to_csharp_name(&method.name)` directly, producing `Chat`, `Embed`, `ListModels`, `ChatStream`, etc. on opaque handle classes like `DefaultClient`. The free-function path (`gen_function` in `methods.rs:281`) already appended `Async` for async functions per the .NET Framework Design Guidelines (CA1849, VSTHRD200), but the opaque-method path did not — leaving e2e tests that called `client.ChatAsync(...)` to fail with `CS1061: 'DefaultClient' does not contain a definition for 'ChatAsync'`. Now both opaque emit paths derive a separate `public_method_name` that appends `Async` (when `method.is_async` and the resolved name doesn't already end in `Async`), while the native FFI name `{ClassName}{MethodName}` continues to use the unsuffixed identifier so it matches the C ABI export. Streaming methods are unconditionally async (they emit `async IAsyncEnumerable<T>`) so their public name always gets the suffix. Surfaced on liter-llm C# e2e — every test in `SmokeTests`/`ChatTests`/`StreamingTests`/etc. failed to compile because the e2e generator (`alef-e2e/csharp`) emits `ChatAsync`-style calls. (`src/backends/csharp/gen_bindings/types.rs`)

- **alef-e2e/go: fix synthetic field assertions and optional field comparisons.** Three bugs in Go e2e codegen caused test compilation failures: (1) missing `chunks_have_heading_context` and `first_chunk_starts_with_heading` synthetic field assertion implementations (implemented inline predicates mirroring Rust/Java patterns), (2) `less_than_or_equal` assertion never dereferenced optional pointer fields before comparison (e.g., `result.QualityScore > 1.0` tried to compare `*float64` to literal), and (3) non-optional slice fields (`result.Chunks`) were incorrectly dereferenced in `for` loops (`for _, c := range *chunks` → `for _, c := range chunks`). Additionally, when comparing a tagged-union struct field like `*FormatMetadata` to a string value in equals assertions, the codegen now uses `fmt.Sprintf("%v", *field)` instead of the invalid `string(...)` cast, avoiding type errors. Fixed slice dereferencing in `chunks_have_content` and `chunks_have_embeddings` synthetic fields. Surfaced on kreuzberg Go e2e — contract_test.go lines 182, 317, 189–190; smoke_test.go line 129. (`src/e2e/codegen/go.rs`)

- **alef-e2e/java: fix codegen bugs in chunk field predicates, void-returning methods, and enum field coercion.** Three separate compilation failures in Java e2e tests. (1) Synthetic chunk-level predicates (`chunks_have_content`, `chunks_have_heading_context`, `chunks_have_embeddings`, `first_chunk_starts_with_heading`) called `.chunks().orElse(...)` assuming the field returned `Optional<List>`, but `chunks()` is annotated `@Nullable List<Chunk>` — wrapped with `Optional.ofNullable()` before calling `.orElse()`. (2) Void-returning methods like `clearOcrBackends()` were emitted as `var result = clearOcrBackends()`, which is invalid Java syntax — added `returns_void = true` to the call config for all `clear_*` registry functions so the template omits the variable assignment. (3) Optional enum fields like `metadata.format` were using `.orElse("")` on `Optional<Enum>`, which fails type-checking — added `"metadata.format" = "FormatMetadata"` to the Java call override's `enum_fields` so optional enum fields trigger `.map(v -> v.getValue()).orElse("")` coercion. Surfaced on kreuzberg Java e2e — 80+ compilation errors. (`src/e2e/codegen/java.rs`; alef.toml)

- **alef-backend-csharp: emit ArgumentNullException for config params with required fields instead of `new T()`.** The C# code generator emitted `config ?? new CustomProviderConfig()` for config parameters without checking if the target type had any required fields. When `CustomProviderConfig` declared required properties (`Name`, `BaseUrl`, `AuthHeader`), the compiler raised CS9035 ("Required member 'X' must be set in the object initializer"). Added type metadata inspection in `emit_named_param_setup` to detect types with required fields (optional=false, no default); for these types, emit `config ?? throw new ArgumentNullException(...)` instead of silently instantiating with `new T()`. Types with all-optional or defaulted fields continue to use `new T()` to preserve backward compatibility. Surfaced on liter-llm C# e2e — `RegisterCustomProvider` failed to compile with CS9035 on three required fields. (`src/backends/csharp/gen_bindings/mod.rs`, `methods.rs`, `types.rs`)

- **alef-e2e/swift: guard `String.contains("")` assertions with an `isEmpty` short-circuit.** Swift's `String.contains(_:)` returns `false` when the argument is the empty string, unlike every other language we target (`"foo".contains("")` is `true` in Python, JS, Ruby, etc.). Fixtures asserting `contains: ""` on a response field (e.g. `transcribe` returning empty `text` for silent audio) therefore always failed with `XCTAssertTrue failed - expected to contain:`. The swift e2e codegen now emits `XCTAssertTrue({val}.isEmpty || {expr}.contains({val}), …)` for the non-array, non-traversal `contains` cases, preserving the original semantic ("any string trivially contains the empty string") while leaving non-empty containment checks unchanged. Surfaced on liter-llm Swift e2e — `TranscribeTests.testEdgeTranscribeEmptyAudio`. (`src/e2e/codegen/swift.rs`)

- **alef-backend-swift: fall back to a `Value::String` wrapper when `from_str` fails for string-like fields.** The `default_field_string_like_serde.jinja` / `default_field_string_like_optional_serde.jinja` templates wrapped raw `String` constructor arguments in `serde_json::from_str(&v).ok()` → `from_value(v).ok()`. For inputs that are NOT valid JSON tokens — bare values like `tts-1`, `whisper-1`, `dall-e-3` — `from_str` silently returns `Err`, the `if let Ok` branch is skipped, and the target field stays at the struct's `Default` value (empty `String`). The high-level Swift `intoRust()` therefore round-tripped fixtures through a swift-bridge constructor that silently zeroed the `model` field, causing the Rust client to reject the call with `bad request: model must not be empty`. Both templates now first try `from_str`; on parse failure they fall back to `serde_json::Value::String(v)` and then `from_value`, so raw strings deserialize correctly into plain `String` (and string-like enum) fields while structured JSON (objects / arrays / string-like enums encoded as JSON strings) continues to work. Surfaced on liter-llm Swift e2e — 13 failures across SpeechTests, TranscribeTests, ImageGenerateTests where the test request JSON serialized `model` as a bare string. (`src/backends/swift/templates/default_field_string_like_serde.jinja`, `src/backends/swift/templates/default_field_string_like_optional_serde.jinja`)

- **alef-backend-csharp: emit separate JsonSerializationOptions for FFI parameter serialization.** The C# generator used a single `JsonSerializerOptions` with `DefaultIgnoreCondition.WhenWritingDefault` for both deserializing FFI responses and serializing input parameters (like `ConversionOptions`) to pass to Rust. When a test explicitly set an option to false/0/null, the serializer skipped writing that field — Rust received incomplete JSON and applied defaults, overwriting the caller's intent. Surfaced on html-to-markdown C# e2e: 3 tests failed because options like `preprocessing.enabled=false` were omitted from serialization. Added a second `JsonSerializationOptions` (without `WhenWritingDefault`) used when serializing Named parameters and config objects in wrappers and streaming methods; deserialization continues to use the original `JsonOptions` for sparse response handling. (`src/backends/csharp/gen_bindings/types.rs`, `methods.rs`, and templates)

- **alef-backend-java: change ObjectMapper serialization inclusion from NON_NULL to ALWAYS.** When Java code called Rust via FFI, the ObjectMapper's `JsonInclude.Include.NON_NULL` configuration silently omitted all null-valued fields from the JSON sent to `htm_conversion_options_from_json`. The Rust deserializer then saw an incomplete JSON object and applied defaults for all missing fields, overwriting any explicitly-set false/0/null values from Java. Effectively, any test setting an option to its "falsy" default (e.g., `highlightStyle=null`, `includeDocumentStructure=false`) would be ignored because those fields disappeared from the JSON. Changed to `JsonInclude.Include.ALWAYS` so all fields serialize regardless of value, ensuring the Rust FFI layer sees exactly what the Java caller set. Surfaced on html-to-markdown Java e2e tests — 33 tests failed because option setters like `withHighlightStyle()` were being overwritten by Rust defaults; 7 additional tests failed with NullPointerException on `result.document()` because `includeDocumentStructure=false` was omitted. (`src/backends/java/templates/helper_object_mapper.jinja`)

- **alef-backend-csharp: emit custom converters for all enums instead of relying on global policy.** Following the Round 1 fix (separate JsonSerializationOptions), the code still had 87 e2e failures because enum fields weren't using the specified naming policy consistently. Enums with `[JsonPropertyName]` attributes but without class-level `[JsonConverter]` were relying on the global `JsonStringEnumConverter(SnakeCaseLower)` from `JsonSerializationOptions`, which System.Text.Json did not apply uniformly during serialization. Now the C# backend emits a custom `{EnumName}JsonConverter` class for all enums (previously only for those with non-standard names), guaranteeing explicit `[JsonPropertyName]` attribute usage during both read and write. This ensures enum values serialize as lowercase strings matching their Rust serde definitions. (`src/backends/csharp/gen_bindings/enums.rs`, `src/backends/csharp/templates/enum_header.jinja`)

- **alef-e2e/csharp: append `Async` suffix to all non-streaming async method names — including overridden ones.** The earlier pass only appended `Async` when the function name was derived from `call_config.function`; if `alef.toml` declared an explicit `overrides.csharp.function = "Crawl"`, the suffix was skipped and the call site emitted `KreuzcrawlLib.Crawl(...)` — but the C# backend exposes `CrawlAsync(...)`. Consumers like kreuzcrawl set csharp overrides on every named call (`Crawl`, `MapUrls`, `Interact`, `BatchScrape`, …) and saw 200+ CS0117 compile errors after Async was appended only on the unoverridden path. Now both branches (override or derived) get the suffix when `call_config.r#async && !call_config.streaming`. The chat-stream renderer drops its append entirely — streaming methods return `IAsyncEnumerable<T>` and never carry an Async suffix in the C# backend. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e/zig: implement the `equals` assertion path for string results.** The Zig assertion renderer matched on `result_is_simple` strings (`null`, `true`, `false`, …) but had no arm for `equals`, so fixtures asserting an exact string value silently emitted no `try testing.expect…` line — the generated test compiled and passed without ever checking the result. Added an `equals` arm that lowers the JSON value via `json_to_zig` and emits `try testing.expectEqualStrings(expected, result.?)`. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/swift: prevent UInt() wrapping of floating-point numeric literals in comparisons.** The `swift_numeric_literal_cast` function unconditionally wrapped all numeric method-call accessor comparisons (e.g., `result.relevanceScore()`) in `UInt(...)`, assuming all opaque swift-bridge methods return UInt. However, some accessors return `Double` (e.g., `relevanceScore() -> Double`), causing compilation errors when comparing against float literals like `0.9`. Fixed by detecting float-valued literals (containing `.`) and skipping the `UInt()` wrapper; integer literals continue to be wrapped to match UInt-returning accessors. Also applied the numeric-literal-cast function consistently across all numeric comparison assertions (equals, greater_than, less_than, etc.) instead of only some, and refactored the optional-field coalesce paths to use the cast function. Surfaced on liter-llm Swift e2e — RerankTests and similar fixtures with float field assertions. (`src/e2e/codegen/swift.rs`)

- **alef-e2e: treat inline `href="/…"` anchors in fixture HTML as a `has_host_root_route` trigger.** `Fixture::has_host_root_route()` previously only fired for `/robots*` / `/sitemap*` paths or 3xx Location / Refresh / `<meta refresh>` redirects targeting `/…`. Multi-page crawl fixtures that traversed between mock responses via plain `<a href="/page1">` links did not qualify, so language-specific e2e codegens (Python, Node, Ruby, …) emitted the shared `MOCK_SERVER_URL + /fixtures/<id>` URL rather than the per-fixture `MOCK_SERVER_<FIXTURE_ID>` env-var lookup. The crawl engine then resolved linked `/page` paths against the host root and 404'd against the namespaced shared listener. Detecting `href="/` / `href='/` in any `body_inline` mirrors the runtime mock-server's `has_inline_host_link` predicate (60deac38) so compile-time and runtime decisions stay in sync. Surfaced on kreuzcrawl Python e2e — `test_crawl_concurrent_depth` got 1 page instead of 3. (`src/e2e/fixture.rs`)

- **alef-e2e/php: honor `options_via = "from_json"` when constructing visitor options and in fallback builder path.** When a fixture used a visitor callback and the PHP e2e call override specified `options_via = "from_json"`, the test code generator unconditionally emitted `ConversionOptions::builder()->visitor($visitor)->build()`, which does not exist in the ext-php-rs binding. The correct behavior is to honor `options_via` and use `ConversionOptions::from_json(json_encode(['visitor' => $visitor]))` instead. Also added a guard in the fallback builder pattern (`build_args_and_setup`) to prevent emitting builder() when `options_via = "from_json"` and no `options_type` is configured — this now returns a descriptive error instructing users to configure `options_type` for typed config construction. This caused 54 php e2e test failures in html-to-markdown (ConversionTests, OptionsTests, VisitorTests) with "Call to undefined method ConversionOptions::builder()". (`src/e2e/codegen/php.rs`)

### Changed

- **maintenance: record a clean full-project `prek run --all-files` verification.**

## [0.17.15] - 2026-05-21

### Fixed

- **alef-e2e/swift: guard `String.contains("")` assertions with an `isEmpty` short-circuit.** Swift's `String.contains(_:)` returns `false` when the argument is the empty string, unlike every other language we target (`"foo".contains("")` is `true` in Python, JS, Ruby, etc.). Fixtures asserting `contains: ""` on a response field (e.g. `transcribe` returning empty `text` for silent audio) therefore always failed with `XCTAssertTrue failed - expected to contain:`. The swift e2e codegen now emits `XCTAssertTrue({val}.isEmpty || {expr}.contains({val}), …)` for the non-array, non-traversal `contains` cases, preserving the original semantic ("any string trivially contains the empty string") while leaving non-empty containment checks unchanged. Surfaced on liter-llm Swift e2e — `TranscribeTests.testEdgeTranscribeEmptyAudio`. (`src/e2e/codegen/swift.rs`)

- **alef-backend-swift: fall back to a `Value::String` wrapper when `from_str` fails for string-like fields.** The `default_field_string_like_serde.jinja` / `default_field_string_like_optional_serde.jinja` templates wrapped raw `String` constructor arguments in `serde_json::from_str(&v).ok()` → `from_value(v).ok()`. For inputs that are NOT valid JSON tokens — bare values like `tts-1`, `whisper-1`, `dall-e-3` — `from_str` silently returns `Err`, the `if let Ok` branch is skipped, and the target field stays at the struct's `Default` value (empty `String`). Both templates now first try `from_str`; on parse failure they fall back to `serde_json::Value::String(v)` and then `from_value`, so raw strings deserialize correctly into plain `String` (and string-like enum) fields while structured JSON continues to work. Surfaced on liter-llm Swift e2e — 13 failures across SpeechTests, TranscribeTests, ImageGenerateTests. (`src/backends/swift/templates/default_field_string_like_serde.jinja`, `src/backends/swift/templates/default_field_string_like_optional_serde.jinja`)

- **alef-backend-csharp: emit separate JsonSerializationOptions for FFI parameter serialization.** The C# generator used a single `JsonSerializerOptions` with `DefaultIgnoreCondition.WhenWritingDefault` for both deserializing FFI responses and serializing input parameters (like `ConversionOptions`) to pass to Rust. When a test explicitly set an option to false/0/null, the serializer skipped writing that field — Rust received incomplete JSON and applied defaults, overwriting the caller's intent. Added a second `JsonSerializationOptions` (without `WhenWritingDefault`) used when serializing Named parameters and config objects in wrappers and streaming methods; deserialization continues to use the original `JsonOptions`. (`src/backends/csharp/gen_bindings/types.rs`, `methods.rs`, and templates)

- **alef-backend-java: change ObjectMapper serialization inclusion from NON_NULL to ALWAYS.** When Java code called Rust via FFI, the ObjectMapper's `JsonInclude.Include.NON_NULL` configuration silently omitted all null-valued fields from the JSON sent to `htm_conversion_options_from_json`. The Rust deserializer then saw an incomplete JSON object and applied defaults for all missing fields, overwriting any explicitly-set false/0/null values from Java. Changed to `JsonInclude.Include.ALWAYS` so all fields serialize regardless of value. (`src/backends/java/templates/helper_object_mapper.jinja`)

- **alef-e2e/csharp: append `Async` suffix to non-overridden async method names.** `render_test_method` / `render_chat_stream_test_method` derived the C# call site from `call_config.function.to_upper_camel_case()` without appending the conventional `Async` suffix that the C# generator emits on async wrappers. The suffix is now appended automatically when `call_config.r#async` is true and the name doesn't already end in `Async`; explicit `cs_overrides.function` overrides still win. (`src/e2e/codegen/csharp.rs`)

- **alef-e2e/zig: implement the `equals` assertion path for string results.** The Zig assertion renderer matched on `result_is_simple` strings (`null`, `true`, `false`, …) but had no arm for `equals`, so fixtures asserting an exact string value silently emitted no `try testing.expect…` line. Added an `equals` arm that lowers the JSON value via `json_to_zig` and emits `try testing.expectEqualStrings(expected, result.?)`. (`src/e2e/codegen/zig.rs`)

- **alef-e2e/swift: prevent UInt() wrapping of floating-point numeric literals in comparisons.** The `swift_numeric_literal_cast` function unconditionally wrapped all numeric method-call accessor comparisons in `UInt(...)`, assuming all opaque swift-bridge methods return UInt. However, some accessors return `Double`, causing compilation errors when comparing against float literals like `0.9`. Fixed by detecting float-valued literals (containing `.`) and skipping the `UInt()` wrapper. Surfaced on liter-llm Swift e2e — RerankTests. (`src/e2e/codegen/swift.rs`)

- **alef-e2e: treat inline `href="/…"` anchors in fixture HTML as a `has_host_root_route` trigger.** Multi-page crawl fixtures that traversed between mock responses via plain `<a href="/page1">` links did not qualify, so language-specific e2e codegens emitted the shared `MOCK_SERVER_URL + /fixtures/<id>` URL rather than the per-fixture `MOCK_SERVER_<FIXTURE_ID>` env-var lookup. Detecting `href="/` / `href='/` in any `body_inline` mirrors the runtime mock-server's `has_inline_host_link` predicate so compile-time and runtime decisions stay in sync. (`src/e2e/fixture.rs`)

- **alef-e2e/php: honor `options_via = "from_json"` when constructing visitor options.** When a fixture used a visitor callback and the PHP e2e call override specified `options_via = "from_json"`, the test code generator unconditionally emitted `ConversionOptions::builder()->visitor($visitor)->build()`, which does not exist. Now uses `ConversionOptions::from_json(json_encode(['visitor' => $visitor]))` instead. (`src/e2e/codegen/php.rs`)

### Changed

- **maintenance: record a clean full-project `prek run --all-files` verification.**

## [0.17.14] - 2026-05-21

### Fixed

- **alef-backend-magnus: convert `Vec<Named>` free-function params via a `_core` `Vec<core::Type>` rebinding.** Free functions taking `Vec<WrapperType>` (e.g. `interact(engine, url, actions: Vec<PageAction>)`) emitted the call to `core::interact(...)` passing the wrapper Vec directly. The core function expects `Vec<core::Type>`, so compilation failed with `expected core::PageAction, found PageAction`. The named-Vec deser binding (`let actions_core: Vec<core::PageAction> = actions.into_iter().map(Into::into).collect()`) already existed for the serde-recoverable path; this extends it to the non-serde path in both `gen_function` and `gen_async_function`, and forces the let-binding-aware call_args generator (`gen_call_args_with_let_bindings`) so the core call references `actions_core` instead of the raw wrapper. Surfaced on kreuzcrawl Ruby build with `PageAction` as the `interact` arg type. (`src/backends/magnus/gen_bindings/functions.rs`)

- **alef-backend-csharp: emit DllImport declarations for enum streaming item types.** The streaming method walk in `gen_bindings/functions.rs` registered struct item types in `opaque_return_types` but skipped enum item types with `if !enum_names.contains(&meta.item_type)`. The FFI backend, however, emits `<type>_to_json` and `<type>_free` exports for both enums and structs that flow through streaming. The asymmetric C# skip meant the generated streaming wrapper called `NativeMethods.<EnumItemType>ToJson` against a non-existent extern and failed to compile with CS0117. Removed the enum exclusion so enum stream items get the same DllImport pair as struct stream items. Surfaced on kreuzcrawl C# build with `crawl_stream` returning `CrawlEvent` (enum). (`src/backends/csharp/gen_bindings/functions.rs`)

- **alef-e2e/go: OR-coalesce `result_is_simple` against the call-level value.** Serde defaults `result_is_simple` to `false` on `CallOverride`, so a Go override that didn't explicitly restate the flag (e.g. only `returns_result = false` + `result_is_pointer = false`) silently clobbered a true call-level value. The codegen then fell into the "function returns only error" branch and emitted `err := tspack.HasLanguage(...)` (and similar for `LanguageCount`, etc.) — but `HasLanguage` returns `bool` (not `error`), so `if err != nil` failed to compile with `invalid operation: err != nil (mismatched types bool and untyped nil)`. Same fix pattern as the prior `result_is_array` coalesce: `overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple || rust-override-fallback`. Surfaced on tslp `e2e/go/registry_test.go::Test_RegistryHasLanguage*` and `Test_RegistryLanguageCount`. (`src/e2e/codegen/go.rs`)

- **alef-backend-rustler: re-emit top-level flat wrappers for non-streaming opaque-type async methods.** The v0.17.0 cleanup (`13a9a973`) moved opaque-type methods into per-type submodules (`LiterLlm.DefaultClient.chat/2`) and removed the historical top-level flat wrappers, but the streaming-wrapper emit loop kept emitting `defaultclient_chat_stream/2` at the top level. The asymmetry broke the e2e fixture API contract (and any consumer relying on the `{type_lc}_{method}_async/N` convention) — calling `LiterLlm.defaultclient_chat_async(client, req)` died with `function LiterLlm.defaultclient_chat_async/2 is undefined`. New emit loop in the main wrapper module emits `def {type_lc}_{method}[_async]({receiver}, ...)` delegates that forward to the underlying NIF, mirroring the streaming-wrapper convention. Streaming methods are filtered out (already emitted by the dedicated streaming loop). Surfaced on liter-llm Elixir e2e — 147/161 fixtures failed. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e/rust: use Display (`v.to_string()`) instead of Debug (`format!("{:?}", v)`) for optional-field binding accessors.** The optional-field unwrap path in `rust_unwrap_binding` was emitting Debug-formatted strings (`format!("{:?}", v)`) so types without Display impl (e.g. FormatMetadata) could still convert to string. But for enums with snake_case serde rename + a Display impl that defers to serde (e.g. `FinishReason::ContentFilter` → `"content_filter"`), Debug emits the PascalCase variant name (`"ContentFilter"`) and breaks every fixture asserting on the wire-format string. Switched to `.to_string()` (Display) — matches what `assertion_helpers.rs` already does for non-optional fields. Types without Display would need to be excluded from string-equals assertions or have a Display impl added to the core library. Surfaced on liter-llm Rust e2e — `test_finish_reason_*`, `test_parallel_tool_calls`, `test_response_format_*`, `test_stop_sequences`, `test_tool_choice_*` (12 failures). (`src/e2e/field_access.rs`)

- **alef-backend-rustler: strip leading underscore from visitor method parameter names when building visitor args JSON.** Visitor NIF bridge methods like `visit_button` take underscore-prefixed parameters (e.g., `_text: &str`) to distinguish from Rust keywords, but the visitor args JSON object was keyed with these underscore-prefixed names (e.g., `"_text"`). Elixir e2e test codegen expected clean names for template variable interpolation (e.g., `{text}` → `Map.get(args, "text")`), so custom visitor formatters always failed with empty map lookups. New `strip_prefix('_')` on param names ensures the JSON keys match what Elixir codegen expects. Surfaced on html-to-markdown Elixir e2e tests — 59 failures across visitor/options/result/metadata/structure tests. (`src/backends/rustler/src/trait_bridge.rs`)

- **alef-backend-csharp: use `WhenWritingDefault` instead of `WhenWritingNull` in JSON serialization options.** C# wrapper and opaque-handle classes were serializing options/config parameters with `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, which only skips null-valued properties. For record types with bool, int, and enum fields, this caused default-valued options (e.g. `Autolinks = false`, `IncludeDocumentStructure = false`) to be omitted from the JSON when explicitly set to their default values, preventing the Rust FFI from receiving those settings. Changed both `wrapper_class_header.jinja` and `opaque_handle_header.jinja` to use `WhenWritingDefault`, matching the behavior of auto-generated record types (types.rs line 972) and ensuring all options are properly serialized to JSON even when set to default values. Surfaced on html-to-markdown C# e2e tests where 87 assertions failed due to missing options like `include_document_structure` and `autolinks`. (`src/backends/csharp/templates/wrapper_class_header.jinja`, `src/backends/csharp/templates/opaque_handle_header.jinja`)

- **alef-e2e/go: skip nil guard for struct value-type fields.** Go e2e tests failed when a fixture asserted on a field path traversing through a struct value type (e.g. `metadata.document.keywords`). The e2e generator tried to emit `if result.Metadata.Document != nil` to guard access, but since `Document` is a value type (struct, not pointer), this is invalid Go syntax. Fixed by detecting struct value-type guard expressions (simple field access with no array indexing or map access) and skipping the nil guard for them. Assertion code is rendered directly without the guard, allowing direct access to nested fields. Surfaced on html-to-markdown Go e2e `metadata_keywords_meta` test. (`src/e2e/codegen/go.rs`)

- **alef-backend-csharp: prevent trailing garbage past closing brace when two error enums share variant names.** When a crate declared two thiserror enums with overlapping variant names (e.g. spikard's `GraphQLError::ValidationError` + `SchemaError::ValidationError`, same for `DepthLimitExceeded`), the C# backend emitted two `GeneratedFile` entries with the same `{Variant}Exception.cs` path. `write_files` processes the file list with `rayon::par_iter`, so the two payloads racily wrote to the same path: when the shorter payload's `open(O_TRUNC)` happened after the longer payload had already written more bytes, the leftover tail of the longer payload survived past the shorter payload's logical end-of-file. The result was a file that looked like `}\ntring message, Exception innerException) : base(message, innerException) { }\n}\n` — a valid class followed by a truncated leftover constructor line and a stray closing brace. The embedded `alef:hash` matched the corrupt on-disk bytes (so `alef verify` was happy), but the C# compiler tripped on the trailing garbage. Fixed by deduping per class name in `gen_bindings/mod.rs`: the first enum to declare a given variant name wins, subsequent same-named variants are dropped from the file list. Only manifested on updates where the two payloads differed in length by enough bytes to expose the race; full rewrites of the same exception class (i.e. when the class content changed substantially) were unaffected because both writers wrote payloads of similar length. (`src/backends/csharp/gen_bindings/mod.rs`, `src/backends/csharp/tests/gen_bindings_test.rs`)

- **alef-e2e/swift: emit labelled arguments and module-qualify free-function calls.** Swift e2e tests failed with two errors: (1) missing argument labels on free-function calls like `process(source:, config:)` were emitted as positional `process(source, config)`, which Swift rejects; (2) unqualified calls like `languageCount()` caused ambiguity errors when both the high-level `TreeSitterLanguagePack` module and swift-bridge's `RustBridge` namespace exported the same function. Fixed by tracking argument indices in the call-arg builder and reconstructing the call string with labels on return; added module qualification (`TreeSitterLanguagePack.languageCount()`) to all free-function calls. Surfaced on tree-sitter-language-pack e2e (Swift). (`src/e2e/codegen/swift.rs`)

- **alef-e2e/rust: use Display (`.to_string()` / `.as_deref()`) for string-like equals assertions instead of Debug `format!("{:?}", v)`.** The Debug-format fix from v0.17.13 unblocked rare non-Display enum types but broke every string scalar comparison: `format!("{:?}", mime_type)` on a `Cow<'static, str>` field emits `"application/pdf"` (with literal quotes) while the fixture's `field_equals` value is the bare `application/pdf` literal, so the assertion fails. Restored Display formatting for both optional (`.as_deref().unwrap_or("").trim()`) and non-optional (`.to_string().as_str().trim()`) string fields. Surfaced on kreuzberg `test_async_extract_bytes`, `test_smoke_*_basic`, and `test_ocr_image_png` — 8+ tests across `smoke_test`, `async_test`, `code_test`, `contract_test`. (`src/e2e/codegen/rust/assertion_helpers.rs`)

## [0.17.13] - 2026-05-21

### Added

- **alef-e2e: new `mock_url_list` call-arg type for batch-URL parameters.** Mirrors the per-fixture URL resolution of `mock_url` (`MOCK_SERVER_<FIXTURE_ID>` env var with `MOCK_SERVER_URL/fixtures/<id>` fallback) but operates on a `Vec<String>` of relative paths. Each element starting with `http` passes through; every other element is prefixed with the fixture's mock-server base. Without this, kreuzcrawl `batch_crawl` / `batch_scrape` / `batch_crawl_stream` fixtures with `batch_urls: ["/seed1", "/seed2"]` failed all e2e tests because the engine fetched the bare relative paths and the resulting `BatchCrawlResults` reported `completed_count: 0`. Wired into the Rust e2e backend; identical resolution semantics need to land in every backend that supports batch operations. (`src/e2e/codegen/rust/args.rs`)

### Fixed

- **alef-e2e/rust: unwrap `Option<Vec<T>>` chunks field with `.as_ref().map_or(0, |v| v.len())` in count assertions.** When a fixture bound the `chunks` streaming virtual field and tested its count with assertions like `count_min`, the generated code emitted `chunks.len()` against `&Option<Vec<Chunk>>`, failing Rust compilation with `no method named 'len' found for reference '&Option<Vec<Chunk>>'`. The fix gates the Option-aware unwrap on `field_resolver.is_optional()` to distinguish actual streaming `Option<Vec<T>>` fields from non-streaming direct `Vec<T>` bindings. Surfaced on kreuzberg `test_smoke_xlsx_basic`. (`src/e2e/codegen/rust/assertions.rs`)

- **alef-e2e/rust: add synthetic field handlers for `chunks_have_heading_context` and `first_chunk_starts_with_heading`.** These derived assertions check observable chunk properties (`content` non-empty and starts with `#`) rather than non-existent struct fields. Previously unimplemented, they failed with `assertion type not found`. New `render_chunks_have_heading_context()` and `render_first_chunk_starts_with_heading()` functions emit proper checks in the assertion renderer. Surfaced on kreuzberg chunking fixtures. (`src/e2e/codegen/rust/assertion_synthetic.rs`)

- **alef-e2e/rust: use `format!("{:?}", v)` for optional enum fields in equals assertions instead of `.to_string()`.** FormatMetadata and similar enum types without Display trait implementation failed with `no method named 'to_string' found for enum 'Option<FormatMetadata>'`. The fix uses Rust's Debug format (which all types derive) via `format!("{:?}", v)` to render the optional value. Applied consistently in `render_equals_assertion`, `render_gte_assertion`, and `field_access.rs` rust_unwrap_binding. Surfaced on kreuzberg metadata extraction fixtures. (`src/e2e/codegen/rust/assertion_helpers.rs`, `src/e2e/codegen/rust/assertions.rs`, `src/e2e/field_access.rs`)

- **alef-e2e/rust: fix numeric literal default selection for floating-point comparisons.** `quality_score.unwrap_or(0)` failed type checking when `quality_score` was `Option<f64>` because integer `0` doesn't match `f64`. The fix changed `numeric_literal()` to check the JSON representation for a decimal point and emit `_f64` suffix for floats, then updated all comparison operators (`greater_than`, `less_than`, `less_than_or_equal`, `greater_than_or_equal`) to check the rendered literal's type suffix or decimal point to choose `"0.0"` vs `"0"` defaults. This ensures proper type matching for all numeric unwrap patterns. Surfaced on kreuzberg quality scoring assertions. (`src/e2e/codegen/rust/assertion_synthetic.rs`, `src/e2e/codegen/rust/assertions.rs`, `src/e2e/codegen/rust/assertion_helpers.rs`)

- **alef-e2e/go: stop letting a per-language `[crates.e2e.calls.<name>.overrides.go]` table silently mask the call-level `result_is_array` flag.** `CallOverride` derives `serde(default)` for `result_is_array`, so any Go override that didn't explicitly re-state the flag (commonly the case — e.g. `result_is_pointer = false` only) made `overrides.map(|o| o.result_is_array).unwrap_or(call_config.result_is_array)` evaluate to `false`. The Go assertion renderer then chose the `fmt.Sprint(result)` branch for a `[]string` return (e.g. `manifest_languages`) instead of `jsonString(result)`, leaving the generated test file referencing `fmt` without importing it (`./download_test.go:104:23: undefined: fmt`). Switched to a boolean OR (`overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array`) — true wins, override-default-false no longer clobbers. Until `CallOverride.result_is_array` becomes `Option<bool>` this is the only safe coalesce. Surfaced on tslp `e2e/go/download_test.go::Test_DownloadManifestLanguages`. (`src/e2e/codegen/go.rs`)

- **alef-e2e/rust: mock-server `has_host_root` now flips on when any mock-response body inlines a host-absolute anchor (`href="/…"` / `href='/…'`).** The previous predicate only triggered host-root listeners for fixtures with `/robots*` or `/sitemap*` paths or 3xx Location redirects, so most kreuzcrawl crawl fixtures registered under the shared `/fixtures/<id>/` namespace. When the seed HTML contained `<a href="/page1">`, the crawl engine resolved the link against the host (yielding `${MOCK_URL}/page1`) and the follow-up GET 404'd against the shared listener that only served `/fixtures/<id>/page1`. Detecting inline host-absolute anchors auto-promotes such fixtures to a dedicated per-fixture port where relative URLs naturally resolve. This brought rust e2e from 158/255 to 368/368 on kreuzcrawl. (`src/e2e/codegen/rust/mock_server.rs`)

- **alef-e2e/swift: module-qualify swift-bridge-ambiguous DTO type names (`StreamToolCall`, `ToolCall`) in streaming-virtual assertion expressions.** Both the alef-emitted `<Module>` namespace (first-class `public struct StreamToolCall: Codable`) and the swift-bridge generated `RustBridge` namespace export the same identifier. Unqualified usage in assertions like `chunks.flatMap { c -> [StreamToolCall] in ... }` fails Swift compilation with `'StreamToolCall' is ambiguous for type lookup`. `render_test_method` now post-processes each rendered assertion and rewrites bare `[StreamToolCall]` / `[ToolCall]` to `[<Module>.StreamToolCall]` / `[<Module>.ToolCall]`. Mirrors the existing `[ChatCompletionChunk]` replacement on the collect snippet. Surfaced on liter-llm e2e (Swift) `StreamingTests.swift`. (`src/e2e/codegen/swift.rs`)

- **alef-e2e/swift: drop the leading `?.` after array indexing in `render_swift_tool_calls_deep` for first-class `StreamToolCall` structs.** Index access `arr[0]` yields a non-optional `StreamToolCall`, so chaining `[0]?.function` failed Swift compilation with `cannot use optional chaining on non-optional value of type 'StreamToolCall'`. The renderer now tracks per-segment optionality: after an `Index` segment the next field uses plain `.`; once a field segment fires (every `StreamToolCall` property is `Optional`), subsequent segments chain with `?.`. (`src/e2e/codegen/streaming_assertions.rs`)

- **alef-e2e/rust: gate the `chunks` Option-aware unwrap in `render_assertion_with_streaming` on `field_resolver.is_optional(f)` instead of a hardcoded `f == "chunks"` name check.** The previous heuristic always emitted `chunks.as_ref().map_or(0, |v| v.len())` even when `chunks` was a non-optional `Vec<T>` (the common non-streaming case), breaking `fields_array_binding_emitted_before_count_min_assertion_for_non_streaming_fixture` and any downstream fixture binding `chunks` directly. The resolver-driven check correctly preserves `chunks.len()` for non-optional vec fields and `chunks.as_ref().map_or(0, ...)` for actual `Option<Vec<T>>` fields. (`src/e2e/codegen/rust/assertions.rs`)

- **alef-core + alef-e2e/typescript + alef-e2e/wasm: `[crates.e2e.<lang>.harness_extras]` knob for injecting extra dependencies into e2e harness manifests (Phase 1: Node + Wasm `package.json`).** Previously, repos like tslp had to hand-restore `tree-sitter@^0.25.0` in `e2e/node/package.json` on every `alef e2e generate` because the codegen unconditionally rewrote the manifest. New `ManifestExtras` model in `src/core/config/manifest_extras.rs` exposes `dependencies` + `dev_dependencies` buckets, each accepting bare `"name" = "version"` or detailed `"name" = { version = "…", … }` table form. Wired through `E2eConfig.harness_extras: HashMap<String, ManifestExtras>` (keyed by canonical language name) and consumed by `render_package_json` in both `src/e2e/codegen/typescript/config.rs` and `src/e2e/codegen/wasm.rs`. Extras land in the JSON `devDependencies` / `dependencies` maps via `serde_json::Value::Object` merge — last-write-wins so a user pin overrides the baseline (e.g. `vitest`), keys sorted for byte-stable re-renders. Idempotent: re-running `inject_package_json_extras` on the merged output produces no further diff. Empty/absent extras is a no-op — full backward compatibility for repos that don't opt in. Phase 2 (remaining 11 languages) and Phase 3 (`packages/<lang>/` manifests via parallel `[crates.<lang>.package_extras]`) follow. Distinct from the existing `[crates.<lang>].extra_dependencies` knob, which targets the binding crate's `Cargo.toml` only and stays put. (`src/core/config/{manifest_extras.rs,e2e.rs,mod.rs}`, `src/e2e/codegen/typescript/{mod.rs,config.rs}`, `src/e2e/codegen/wasm.rs`)

- **alef-core + alef-e2e/wasm + alef-e2e/typescript: auto-skip wasm e2e fixtures whose languages are not in the static-compiled WASM language set.** New optional `[crates.wasm].languages: Vec<String>` config entry declares the set of languages baked into the WASM build (e.g. tslp's curated 8-language wasm32 bundle). When set, `WasmCodegen` no longer drops fixtures filtered out by `should_include_fixture`; instead it clones each one, attaches a synthetic `SkipDirective { languages: ["wasm"], reason: "language not in WASM's static-compiled set: [...]" }`, and emits it as an `it.skip(...)` block in the generated `.test.ts` file. The TypeScript test_function jinja template gained a `{% if skip_reason %}` branch (driven by a new `skip_reason` context field threaded through `render_test_case → render_test_file`) that renders the skip directive's reason as a comment on the `it.skip` body. Empty/absent `languages` is a no-op — full backward compatibility. Without this, downstream tslp's curated wasm32 build fails CI E2E with `RuntimeError: unreachable` on every fixture whose language is outside the bundled set (e.g. yuck/zig/ziggy/…). (`src/core/config/languages.rs`, `src/e2e/codegen/wasm.rs`, `src/e2e/codegen/typescript/test_file.rs`, `src/e2e/templates/typescript/test_function.jinja`)

- **alef-backend-napi: emit `repository` field in generated `package.json`** so npm publish with provenance attestation succeeds. Previously omitted, causing E422 "repository.url is \"\"" failures.

- **alef-backend-csharp: restore nullable config parameter handling to use default instance instead of throwing.** v0.17.13 changed optional `ExtractionConfig? config` codegen from `config ?? new ExtractionConfig()` to `config ?? throw new ArgumentNullException(nameof(config))`. However, nullable optional parameters should support null to mean "use the default". The fix removes the distinction between required and optional config params—all config params now default to `new T()` when null, allowing callers to pass `null` to use default configuration rather than throwing. (`src/backends/csharp/gen_bindings/mod.rs`)

- **alef-e2e: `FieldResolver::rust_unwrap_binding` now strips the same virtual namespace prefix that `accessor()` strips.** The intermediate-binding emission used `render_accessor` directly on the raw `resolved` path, which kept the namespace prefix in the generated code: e.g. `let interaction_action_results_0_data = result.interaction.action_results[0].data…;`. The resulting code failed Rust compilation with `no field 'interaction' on type 'InteractionResult'`. Now mirrors the prefix-stripping branch from `accessor()` so the binding lands on the real struct path (`result.action_results[0].data…`). Surfaced on kreuzcrawl `interact_execute_js` and `interact_click_nonexistent_element`. (`src/e2e/field_access.rs`)

- **alef-e2e/rust: route `is_true` / `is_false` assertions on `Option<T>` leaf fields through `.is_some()` / `.is_none()`.** The previous emission `assert!(result.x.y, …)` failed Rust compilation with `cannot apply unary operator '!' to type 'Option<Value>'` whenever the leaf field was optional (e.g. kreuzcrawl's `ActionResult.data: Option<serde_json::Value>` under `interact_screenshot` / `interact_screenshot_full_page`). `is_some()` is the broadest correct interpretation that compiles uniformly across `Option<bool>` / `Option<Value>` / `Option<serde_json::Value>`. Re-uses the existing `is_optional_scalar_field` predicate. (`src/e2e/codegen/rust/assertions.rs`)

- **alef-e2e: `FieldResolver::is_optional` now strips a virtual namespace prefix before consulting `optional_fields`, mirroring `is_valid_for_result`.** Fixtures use organizational prefixes like `interaction.action_results[0].data` where `interaction.` is not a real struct field on the call's return type. `is_valid_for_result` already strips such prefixes via `namespace_stripped_path`, but `is_optional` did not, so `Option<T>` leaf fields under a namespace prefix were misclassified as non-optional. This caused per-backend codegen for `not_empty` / `is_true` assertions to emit raw `field.is_empty()` / `assert!(field, …)` against `Option<serde_json::Value>` fields (e.g. kreuzcrawl's `ActionResult.data`), failing compilation with `no method named 'is_empty' found for enum 'Option<Value>'` and `cannot apply unary operator '!' to type 'Option<Value>'`. Factored the existing predicate into `is_optional_direct`, then layered the namespace-stripped fallback on top. (`src/e2e/field_access.rs`)

- **alef-e2e: extend `pages_crawled` alias rewrite to cover `greater_than_or_equal` and `less_than_or_equal` assertion types.** `normalize_assertions` already mapped `equals pages_crawled` (with optional `crawl.` namespace prefix) to `count_equals pages`, but range comparisons (`greater_than_or_equal`, `less_than_or_equal`) passed through unchanged. The rewritten field name (`pages`) is a `Vec<CrawlPageResult>` in kreuzcrawl's `CrawlResult`, so emitted code `result.pages >= 1` failed Rust compilation with `binary operation '>=' cannot be applied to type 'Vec<CrawlPageResult>'`. The branch now also rewrites GTE → `count_min` and LTE → `count_max`, both of which already handle `.len()`-based collection counts in every backend. Surfaced on kreuzcrawl `strategy_adaptive_saturation` / `strategy_adaptive_window` fixtures. (`src/e2e/fixture.rs`)

- **alef-e2e/rust: unwrap `Option<scalar>` leaf fields in `greater_than` / `less_than` / `less_than_or_equal` numeric assertions.** `render_rust_with_optionals` deliberately leaves the leaf field optional (so `equals` against `None` still type-checks via `PartialEq`), but comparison operators (`>`, `<`, `<=`) do not implement `PartialOrd` between `Option<T>` and `T`. `render_gte_assertion` already special-cased this via `.unwrap_or(0)`; the other three operators emitted raw `result.x.y > N` and failed to compile when `y` was optional (e.g. kreuzcrawl's `metadata.word_count: Option<usize>` with the `metadata_word_count` fixture). Extracted `is_optional_scalar_field` helper and routed all four operators through `.unwrap_or(0)` consistently. (`src/e2e/codegen/rust/assertions.rs`)

- **alef-e2e/rust: serialize fixture `json_object` args via `serde_json::from_str(r#"…"#)` instead of `serde_json::json!(…)`.** The `serde_json::json!` macro has a finite expansion-recursion budget; on fixtures whose JSON arg contains more than ~100 array elements (e.g. kreuzcrawl's `interact_max_actions_exceeded`, which submits 101 click actions) the generated test failed to compile with `error: recursion limit reached while expanding $crate::json_internal!`. Switching to `serde_json::from_str` on a raw-string literal sidesteps the macro entirely while keeping the same `serde_json::Value → serde_json::from_value::<T>` typed-deser path. Removed the now-dead `json_value_to_macro_literal` helper and its tests. (`src/e2e/codegen/rust/args.rs`)

- **alef-backend-php: emit `Box::default()` instead of `Box::new(Default::default())` for boxed fallback fields in flat-enum-to-core conversions.** `Box::new(T::default())` trips `clippy::box-default` ("`Box::new(_)` of default value") which is denied as a warning under `cargo clippy -- -D warnings`. The generated PHP umbrella crate (e.g. `kreuzcrawl-php`) failed CI Rust on the `CrawlEvent::Page { result: Box::new(Default::default()) }` fallback emitted by `flat_enum_to_core_match_body`. Replaced all three occurrences in `gen_bindings/types.rs` with `Box::default()` which delegates directly to `<Box<T> as Default>::default`. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-e2e/elixir: prefix unused result variables with `_` in e2e fixtures with no assertions.** When a fixture has zero assertions, the generated pattern match `{:ok, result} = ...` binds a variable that is never referenced, causing `mix compile --warnings-as-errors` to fail with `variable "result" is unused`. The Elixir e2e codegen now detects empty assertion lists and prefixes the result variable with `_` (e.g., `{:ok, _result} = ...`), signaling intentional non-use. Streaming fixtures always use the result (to drain chunks) so they remain unprefixed. Added comprehensive unit test coverage. Fixes #306 (tslp CI failing on Elixir e2e). (`src/e2e/codegen/elixir.rs`, `src/e2e/tests/elixir_unused_variable_prefix.rs`)

- **alef-core: emit `* alef:hash:` (block-comment style) inside C/C-block headers instead of `// alef:hash:`.** `inject_hash_line` infers the comment style from the trimmed marker line. For a block-comment header the marker is `* This file is auto-generated by alef …`; after `trim()` it becomes `* This file …`, which previously did not match `trimmed.starts_with(" *")` (the leading space was already trimmed away). The detection then fell through to the catch-all `format!("// {HASH_PREFIX}{hash}")`, producing a malformed header with `// alef:hash:` lines inside `/* … */` blocks. Replaced the wrong predicate with `trimmed.starts_with('*')`, and strengthened `test_inject_and_extract_c_block` to assert `* alef:hash:` is present and `// alef:hash:` is absent. (`src/core/hash.rs`)

- **alef-backend-csharp: marshal `Named`-type params and `bool` correctly in record methods.** `emit_record_methods` passed `Named`-type parameters directly to P/Invoke (a managed `record` object where `IntPtr` is required) and passed `bool` params as `bool` instead of `int` (P/Invoke FFI convention: 0/1). Fix: use the existing `native_call_arg`, `emit_named_param_setup`, and `emit_named_param_teardown_indented` helpers so record methods now serialize `Named` params to JSON and obtain handles before the FFI call, wrap calls in `try/finally` to free those handles, and convert `bool` params to `(x ? 1 : 0)` at the call site. Fixes #97 (bool→int), fixes #98 (Named-param handle marshaling). (`src/backends/csharp/gen_bindings/mod.rs`, `src/backends/csharp/gen_bindings/types.rs`)

- **alef-backend-csharp: restore nullable config parameter handling to use default instance instead of throwing.** v0.17.13 changed optional `ExtractionConfig? config` codegen from `config ?? new ExtractionConfig()` to `config ?? throw new ArgumentNullException(nameof(config))` based on concerns about `required` struct members. However, nullable optional parameters should support null to mean "use the default". The fix distinguishes: required config params throw on null (since the caller passed `null` to a non-optional signature), while optional config params (e.g. `config?`) default to `new T()`. This allows e.g. `ExtractBytesAsync(bytes, mime, config: null)` to succeed with the default extraction config rather than throwing. (`src/backends/csharp/gen_bindings/mod.rs`)

## [0.17.12] - 2026-05-20

### Fixed

- **alef-backend-kotlin: emit `open val` with zero-value defaults instead of `abstract val` for error introspection properties (`statusCode`/`isTransient`/`errorType`).** The Kotlin Android JNI bridge throws a flat `<App>BridgeException` (see `crates/<lib>-jni/src/lib.rs`'s `throw_jni_error`) rather than constructing sealed-class variants. With `abstract val`, every `data class` variant in the sealed hierarchy failed `Class '<App>Error.X' is not abstract and does not implement abstract base class members: val errorType: String` and broke `assembleRelease`. The new `kotlin_zero_value` helper returns the literal zero for `Boolean`/`Byte`/`Short`/`Int`/`Long`/`Float`/`Double`/`String`/nullable types so the sealed class compiles. Variants can still opt into a concrete override when domain code constructs the error directly. Surfaced on liter-llm CI Mobile (`packages/kotlin-android/src/main/kotlin/dev/kreuzberg/literllm/android/LiterLlmError.kt`). (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `src/backends/kotlin/tests/gen_bindings_test.rs`, `src/backends/kotlin-android/tests/docs_emission_test.rs`)

- **alef-backend-swift: include synthesised `message: _` label in introspection-method switch patterns for error variants that don't carry a real `message` field.** `emit_error_type_with_imports` prepends `message: String` to error case declarations when none of the variant's fields is named `message` (see `seen_message` logic in `gen_bindings.rs`). The companion switch patterns in the `statusCode`/`isTransient`/`errorType` extensions only iterated `variant.fields` and so produced `(endpoint: _, provider: _)` for a 3-element case `(message: _, endpoint: _, provider: _)`, failing Swift compilation with `tuple pattern has the wrong length for tuple type '(message: String, endpoint: String, provider: String)'`. The wildcard pattern now mirrors the case-declaration logic and prepends `message: _` when no real `message` field exists. Surfaced on liter-llm CI E2E (Swift) on `LiterLlmError.endpointNotSupported` and `LiterLlmError.invalidHeader`. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: dispatch `Optional<UnitEnum>` field conversion through `flatMap` on the bridge optional instead of `?.toString().flatMap` on `String?`.** The previous emission `rb.field()?.toString().flatMap { S(rawValue: $0) }` chained `?.` through both `.toString()` and `.flatMap`, so `.flatMap` ran on the unwrapped `String` value. Swift then dispatched to `Sequence.flatMap` (where `String` is a Sequence of `Character`), producing `cannot convert value of type 'String.Element' (aka 'Character') to expected argument type 'String'` and `cannot convert value of type 'EnumType?' to closure result type 'String?'`. Lifting `flatMap` onto the bridge `Optional<RustString>` directly (`rb.field().flatMap { S(rawValue: $0.toString()) }`) forces `Optional.flatMap` selection. Surfaced on liter-llm CI E2E (Swift) on `ImageUrl.detail` (`ImageDetail?`), `Choice.finishReason` (`FinishReason?`), `ToolChoice.toolType` (`ToolType?`). (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: extend `non_throwing_optional_string_uses_json_bridge` to also match `Option<Primitive>` so the JSON-decode body branch fires for non-throwing `Option<f64>` / `Option<i32>` returns.** Per `needs_json_bridge` in `gen_rust_crate/type_bridge.rs`, ALL `Option<T>` returns are JSON-bridged; the bridge surfaces them as non-optional `RustString`. v0.17.11 only covered `Option<String>`, so `liter-llm`'s `completion_cost(model, …) -> Option<f64>` and `completion_cost_with_cache(…) -> Option<f64>` failed Swift compilation with `cannot convert return expression of type 'RustString' to return type 'Double'`. The predicate now also accepts `TypeRef::Primitive(_)` inside the optional, routing those returns through the JSON-decode body that already handles `Option<String>`. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: wrap free-function bridge returns of client-class types via a new internal `init(_ inner: RustBridge.{Class})` on the emitted Swift client class.** Free functions returning a client class (e.g. liter-llm's `createClient(apiKey:baseUrl:timeoutSecs:maxRetries:modelHint:)` and `createClientFromJson(json:)`) emitted `return try RustBridge.createClient(...)` directly, but the bridge yields `RustBridge.DefaultClient` (low-level swift-bridge handle) while the forwarder signature declares the higher-level `DefaultClient` wrapper, failing Swift compilation with `cannot convert return expression of type 'RustBridge.DefaultClient' to return type 'LiterLlm.DefaultClient'`. `emit_client_class` now adds `internal init(_ inner: RustBridge.{Class}) { self.inner = inner }` so the free-function forwarder can wrap the bridge handle via `let _rb = try RustBridge.{fn}(...); return {Class}(_rb)`. A new `client_class_names` set is threaded from the top-level emitter through `emit_free_function_forwarders` → `emit_single_free_function_forwarder` to drive the new body branch. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-scaffold/java: pin `pmd.version` to 7.17.0 to match `pmd-core` bundled in `maven-pmd-plugin` 3.28.0.** Dependabot bumps of `net.sourceforge.pmd:pmd-java` to 7.19.0+ produced `mvn verify` failures with `NoSuchMethodError: 'net.sourceforge.pmd.properties.PropertyBuilder$GenericPropertyBuilder net.sourceforge.pmd.properties.PropertyFactory.conventionalEnumProperty(java.lang.String, java.lang.Class)'` because `maven-pmd-plugin` 3.28.0 ships `pmd-core` 7.17.0, and mixing newer `pmd-java` against the older bundled `pmd-core` triggers the API incompatibility. Pinning `pmd.version` to 7.17.0 in both the hardcoded template (`languages/java.rs:113`) and the renovate-tracked constant (`template_versions::maven::PMD`) keeps the two artifacts in lockstep. (`crates/alef-scaffold/src/languages/java.rs`, `src/core/template_versions.rs`)

## [0.17.11] - 2026-05-20

### Fixed

- **alef-e2e: fix Elixir test helper to handle struct field values.** E2e Elixir test helper `alef_e2e_item_texts` called `to_string(str)` on all non-atom, non-nil field values, which failed when struct types (e.g., `TreeSitterLanguagePack.StructureKind`) were returned from NIF bindings with error: `protocol String.Chars not implemented for <struct>`. Replaced `to_string(str)` with `inspect(str)`, which safely converts any Elixir value to a string. (`src/e2e/codegen/elixir.rs`)
- **alef-backend-extendr: emit `unsafe impl Send`/`Sync` on the visitor bridge struct.** The visitor bridge holds an `extendr_api::Robj` (a raw R `SEXP` pointer), which is `!Send`/`!Sync`. Since the core `VisitorHandle = Arc<Mutex<dyn HtmlVisitor + Send>>` requires the bridge to be `Send`, generated R bindings failed to compile with `the trait 'Send' is not implemented for '*mut extendr_ffi::SEXPREC'`. Mirrors the magnus backend pattern: emit `unsafe impl Send`/`Sync` on the bridge struct with a `// SAFETY:` comment noting that R is single-threaded and the bridge must only be invoked from the R main thread. (`src/backends/extendr/templates/visitor_bridge.jinja`, `src/backends/extendr/tests/gen_bindings_test.rs`)
- **alef-backend-swift: JSON-decode `Option<String>` returns on non-throwing free-function forwarders instead of optional-chaining `.toString()`.** v0.17.10 added a `?.toString()` suffix to coerce the bridge return for `Option<String>` results, but that fix was based on the false premise that swift-bridge surfaces the value as `RustString?`. In reality the bridge crate JSON-serializes ALL `Option<T>` returns (see `needs_json_bridge` in `src/backends/swift/src/gen_rust_crate/type_bridge.rs`: `Optional(_) => true`): for a non-throwing function the bridge declares `-> String` and swift-bridge surfaces it as `-> RustString` (NON-optional). Applying `?.toString()` therefore failed with `cannot use optional chaining on non-optional value of type 'RustString'` — observed in downstream tree-sitter-language-pack on `detectLanguageFromExtension(ext:)`. `emit_single_free_function_forwarder` now detects non-throwing `Option<String>` returns via a new `non_throwing_optional_string_uses_json_bridge` predicate and emits a JSON-decode body (`JSONDecoder().decode(String?.self, from: _rb_data)`) — JSON `null` decodes to `.none`, present values to `.some`. The Optional<String> arm of `forwarder_return_conversion_suffix_inner` now returns an empty suffix to leave the body branch in sole control. The v0.17.10 regression test is rewritten to assert the new JSON-decode body and to negative-assert against the v0.17.10 `?.toString()` pattern. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

## [0.17.10] - 2026-05-20

### Fixed

- **alef-backend-swift: convert `RustString` returns and host-facing DTO arguments in free-function forwarders.** Two related forwarder-codegen gaps surfaced as Swift compile errors in downstream tree-sitter-language-pack: (1) free functions returning `Option<String>` emitted `return RustBridge.detectLanguageFromExtension(ext)` directly, but swift-bridge maps `Option<String>` to `RustString?` regardless of throws-ness, so Swift rejected the body with `cannot convert return expression of type 'RustString?' to return type 'String?'`; (2) free functions taking a Named DTO struct param emitted `return try RustBridge.process(source, config)` where `config` was still the host-facing `ProcessConfig` struct, but the swift-bridge extern requires the low-level `RustBridge.ProcessConfig` class, producing `cannot convert value of type 'TreeSitterLanguagePack.ProcessConfig' to expected argument type 'RustBridge.ProcessConfig'`. `forwarder_return_conversion_suffix_inner` now emits a `?.toString()` suffix for `Optional<String>` returns, and `forwarder_param_signature` now threads `known_dto_names` so it can emit `let _rb_<name> = try <name>.intoRust()` (or the optional-chained variant) and forward the converted local. The function-level `throws` clause widens whenever any param conversion can throw via a new `param_conversion_throws` helper. Three new regression tests pin the three shapes (Optional<String> return, bare Named DTO param, Optional<Named DTO> param). (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

## [0.17.9] - 2026-05-20

### Fixed

- **alef-backend-csharp: pass `bool` directly to P/Invoke methods instead of emitting `(name ? 1 : 0)` at the call site.** The P/Invoke declaration in `gen_bindings::functions.rs` (line 523-525) emits `[MarshalAs(UnmanagedType.U1)] bool <name>` for every `TypeRef::Primitive(PrimitiveType::Bool)` param — i.e., the unmanaged side is a single-byte `bool`, but the managed C# parameter type is `bool`. The wrapper-method body in `gen_bindings::mod.rs::ffi_arg_conversion_for_param` had drifted: it still emitted `(<name> ? 1 : 0)` (the legacy form from when the P/Invoke also took `int`), so generated wrappers like `GraphQLRouteConfig.EnablePlayground` and `SpikardLib.ResponseSetCookie` failed `dotnet build` with `CS1503: Argument N: cannot convert from 'int' to 'bool'`. The non-optional branch now passes `<name>` directly; the optional branch emits `(<name> ?? false)` to collapse `bool?` to `bool` (matching the legacy null-as-false semantics — no FFI sentinel is currently needed for bool? since no method requires distinguishing `Some(false)` from `None`). New regression test `test_bool_param_call_site_matches_pinvoke_bool_decl` pins both sides of the contract: P/Invoke decl keeps `[MarshalAs(UnmanagedType.U1)] bool`, call site passes `<name>` directly with no `? 1 : 0`. (`src/backends/csharp/gen_bindings/mod.rs`, `src/backends/csharp/tests/gen_bindings_test.rs`)

## [0.17.8] - 2026-05-20

### Fixed

- Re-released with the v0.17.7 alef-scaffold/alef-codegen fixes plus pyo3 stub doc-emission cleanups already merged on main. No new functional changes versus 0.17.7; bump exists to give downstream consumers a clean tag after the in-flight csharp bool-call-site fix was deferred.

## [0.17.7] - 2026-05-20

### Added

- **alef-scaffold + alef-core: support per-target replacement of the core-crate dependency feature set in the generated FFI Cargo.toml.** New `[crates.ffi.target_dep_overrides]` array (Vec of `{ cfg, features }`) on `FfiConfig` instructs the FFI scaffold to emit the default branch under `[target.'cfg(not(any(<all-cfgs>)))'.dependencies]` and one `[target.'cfg(<cfg>)'.dependencies]` block per override, instead of a single unconditional `[dependencies]` block. Without it, downstream kreuzberg's `kreuzberg-ffi/Cargo.toml` always pulled `kreuzberg` with the full ORT-dependent feature set (`paddle-ocr`, `layout-detection`, `embeddings`, `auto-rotate`), and `cargo ndk --target x86_64 -- check -p kreuzberg-ffi` failed with `ort-sys: ort does not provide prebuilt binaries for the target x86_64-linux-android`. With the override pointing the Android x86_64 emulator triple at kreuzberg's `android-target` feature set the same check succeeds. The empty-overrides path is byte-identical to the previous output. New regression test `test_scaffold_ffi_target_dep_overrides_emit_cfg_blocks` in `crates/alef-scaffold/src/tests.rs`. (`crates/alef-scaffold/src/languages/ffi.rs`, `src/core/config/languages.rs`, `src/core/config/mod.rs`)

### Fixed

- **alef-codegen: detect rustdoc test-attribute fences (`no_run`, `ignore`, `should_panic`, `compile_fail`, `edition*`) as Rust code, drop bodies for foreign-language targets.** Downstream tslp `index.d.ts` had 32 remaining `use tree_sitter_language_pack::...` / `.unwrap()` leaks because the source rustdoc uses ` ```no_run ` fences that v0.17.5's sanitizer didn't recognise as Rust. `is_rust_fence_tag` now matches the full set of rustdoc test-attribute-only tags (including `edition2018`, `edition2021`, etc.) in both `sanitize_rust_idioms_inner` and `detect_first_fence_lang`. Non-Rust language fences (`python`, `javascript`, `typescript`, `php`, …) are unaffected. 13 new unit tests added. (`src/codegen/doc_emission.rs`)

## [0.17.6] - 2026-05-20

### Fixed

- **alef-readme: configure the template-driven README minijinja `Environment` to match `template_env::make_env()` and trim the multi-line TOML `description` before rendering.** `try_template_readme` in `crates/alef-readme/src/lib.rs` constructed its `Environment` via `Environment::new()` without `set_trim_blocks(true)` / `set_lstrip_blocks(true)` / `set_keep_trailing_newline(true)`, so every Jinja control tag (`{% if %}`, `{% include %}`, `{% endif %}`) leaked its trailing newline into the rendered README and every `{% include 'partials/_*.md' %}` dropped the trailing newline of the partial file. The result was double blank lines around every `## Heading` and a missing blank line between hand-included blocks (e.g. between the badges `<div>` and the description paragraph). Compounding the issue, TOML basic multi-line strings (`description = """..."""`) preserve a trailing newline, so `{{ description }}<blank line>## Installation` rendered as `<desc>\n\n\n## Installation` (one extra blank line) before the next section — independent of `trim_blocks` because `{{ … }}` is an output tag, not a block tag. `try_template_readme` now enables the three whitespace settings on the local environment and `.map(|s| s.trim_end().to_string())` strips the description's trailing whitespace before insertion into the template context. Downstream html-to-markdown's `task generate-readme:check` CI step (which runs `alef readme && rumdl fmt && git diff --exit-code`) failed on every regen because rumdl accepts both spacings but the committed READMEs no longer matched the regenerated output. (`crates/alef-readme/src/lib.rs`)

- **alef-cli: include the resolved workspace version in the IR cache key so `alef sync-versions --set …` (and downstream `alef readme`) invalidate the cache automatically on a version bump.** The IR cache key was `sources_hash` only, but `api.version` is read fresh from `version_from` (Cargo.toml) into the IR at extract time. Bumping just the version did not change source contents, so `extract` returned cached IR with the previous version, and `regenerate_readmes` inside `sync_versions` then rendered README badges/install snippets against the stale version (e.g. `filter=v0.3.0-rc.21` after a bump to `rc.22`), failing the consumer's "Check README freshness" CI step. The cache key is now `format!("{source_hash}:{version}")` in `crates/alef-cli/src/pipeline/extract.rs`, threaded into both `is_ir_cached` and `write_ir_cache`. Verified end-to-end in the kreuzcrawl consumer: `alef sync-versions --set 0.3.0-rc.99` followed by `alef sync-versions --set 0.3.0-rc.23` now refreshes per-binding READMEs in a single pass without `alef cache clear`. (`crates/alef-cli/src/pipeline/extract.rs`)

## [0.17.5] - 2026-05-20

### Fixed

- **alef-backend-napi: sanitize Rust code-block examples from emitted docstrings to prevent leaks in .d.ts.** Downstream tree-sitter-language-pack TypeScript bindings contained 61 Rust-code-block leaks in `index.d.ts` JSDoc comments (15× `use tree_sitter_language_pack::...` imports, 8× `.unwrap()`, 8× `Some/None`, 10× `assert_eq!`, etc.) because NAPI-RS function code generation was calling `emit_rustdoc(func.doc)` directly without sanitizing Rust examples. The fix applies `sanitize_rust_idioms(doc, DocTarget::TsDoc)` before emitting rustdoc in functions, enums, variants, types, and methods in `alef-backend-napi::gen_bindings`, so when NAPI-derive extracts Rust doc comments to generate the `.d.ts`, the examples are already clean. (`src/backends/napi/src/gen_bindings/{functions,enums,types,mod}.rs`)

- **alef-backend-rustler: emit Elixir `Native.<errname>_<method>` stubs alongside Rust NIF shims for error introspection.** The Rust side of `alef-backend-rustler` already emitted `#[rustler::nif] fn <errname>_status_code/is_transient/error_type(_msg: String)` for every non-sanitized error type, and the public `Elixir.Spikard` wrapper called `Spikard.Native.<errname>_<method>(msg)`, but the corresponding `def <errname>_<method>(_msg), do: :erlang.nif_error(:nif_not_loaded)` placeholders were never emitted into `native.ex`. rustler-precompiled checks at on_load time that every NIF it installs has a matching Elixir stub in the named module and bails with `{:error, {:bad_lib, "Function not found 'Elixir.Spikard.Native':<errname>_<method>/1"}}` when one is missing — every downstream consumer with declared error types failed `mix compile --warnings-as-errors`. `gen_bindings/helpers.rs::gen_native_ex` now iterates `api.errors` with the same `!m.sanitized` filter the Rust NIF emitter uses and writes the missing stubs as `@doc false`. Regression test `error_methods_emit_matching_native_ex_stubs` in `tests/gen_public_api_test.rs` pins the two-sided emission. (`src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/tests/gen_public_api_test.rs`)

- **alef-backend-csharp + alef-codegen: route every generated C# `<summary>` through `sanitize_rust_idioms(_, CSharpDoc)` so XML-doc no longer leaks raw Rust idioms.** Generated `GraphQLErrorException.cs` (and every other error/enum/type with rustdoc) shipped raw `# Examples` / ` ```ignore ` code fences / `[`Self::error_code`]` intra-doc links / `Result<T, E>` generics / bare `<` and `>` characters straight into `<summary>` blocks, producing tens of CS1002/CS1519 parse errors. New `DocTarget::CSharpDoc` variant in `src/codegen/doc_emission.rs` drops rust-tagged code fences (matching the TsDoc/JsDoc convention), maps `Vec<u8>` → `byte[]`, `Vec<T>` → `T[]`, `Option<T>` → `T?`, `HashMap<K, V>` → `Dictionary<K, V>`, `Result<T, E>` → `T`, `None` → `null`, strips `# Examples` / `# Arguments` / `# Returns` section headings, and finally XML-escapes remaining `<` / `>` / `&` so the output is safe inside `<summary>`. A companion `sanitize_rust_idioms_keep_sections` preserves headings for callers that have already split sections (so `emit_csharp_doc`'s structured `<param>` / `<returns>` path keeps its rendering). `alef-codegen::error_gen::gen_csharp_error_types` and every doc-emission site in `alef-backend-csharp::gen_bindings::{enums,types,mod}.rs` now route through the helper. 10 new unit tests + a fixture-pinning regression test in `error_gen.rs` + an end-to-end backend test in `src/backends/csharp/tests/gen_bindings_test.rs` cover both the bad GraphQLErrorException input shape and the per-target substitutions. (`src/codegen/doc_emission.rs`, `src/codegen/error_gen.rs`, `src/backends/csharp/gen_bindings/{enums,types,mod}.rs`, `src/backends/csharp/tests/gen_bindings_test.rs`)

- **alef-backend-swift: nullary free-function forwarders that return a named DTO now wrap the bridge call in the DTO converter instead of returning the `RustBridge.<Dto>` directly.** `public func schemaQueryOnly() -> QueryOnlyConfig` previously emitted `return RustBridge.schemaQueryOnly()`, which Swift rejected with `cannot convert return expression of type 'RustBridge.QueryOnlyConfig' to return type 'Spikard.QueryOnlyConfig'` (same shape for `schemaQueryMutation` / `schemaFull` and any other nullary default-config emitter). `emit_single_free_function_forwarder` now recognises bare `Named(DTO)` returns, binds the bridge result to `_rb`, and returns `try DTO(_rb)` via the existing `init(_ rb: <Dto>Ref) throws` converter; the forwarder signature also widens to `throws` so the converter's error path is honoured. Test `nullary_free_function_returning_named_dto_wraps_bridge_call_in_converter` pins the new body shape. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

- **alef-backend-swift: `*FromJson` forwarders for enums route through `RustBridge.<enum>FromJson` whenever `emit_enum` falls back to the typealias form.** Earlier the from-JSON emitter unconditionally used `JSONDecoder().decode(<EnumType>.self, ...)`, but data-bearing enums whose payload types are not bridge-safe (per the `gate codable enum synthesis` work in v0.17.2) fall back to a `typealias <EnumType> = RustBridge.<EnumType>` and never get a `Codable` conformance — every such enum produced a fresh Swift compile error at the forwarder. New `enum_emits_codable` helper mirrors `emit_enum`'s Codable/typealias decision; enum branches now route typealias-fallback enums to `RustBridge.<EnumType>FromJson(json)` directly. Test `data_variant_serde_enum_with_opaque_field_falls_back_to_rust_bridge_from_json` pins the typealias-routing path; `data_variant_serde_enum_with_bridge_safe_fields_emits_codable` pins the positive Codable case. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

- **alef-backend-kotlin: emit one Kotlin coroutine-wrapper file per opaque client type instead of bundling every wrapper into `DefaultClient.kt`.** Generators that produce more than one Kotlin client wrapper (e.g. spikard's `Router` + `GraphQLRouteConfig`) previously emitted every wrapper class into a single `DefaultClient.kt` AND into per-type `<Type>.kt` files, producing K2-LightTree "Redeclaration" errors at `compileKotlin`. `emit_jvm_client_class` / `emit_jvm_client_class_with_package` now return `Vec<GeneratedFile>` and emit one `<Type>.kt` per opaque client type (the bundled `DefaultClient.kt` is no longer produced when multiple types exist; the single-wrapper case still renders to `DefaultClient.kt` for backward compatibility). Two regression tests in `tests/gen_bindings_test.rs` pin the multi-type-per-file shape and the single-wrapper-fallback. Stale `DefaultClient.kt` files in downstream repos must be deleted manually after upgrade because Kotlin output carries no `alef:hash:` marker for `cleanup_orphaned_files`. (`src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/tests/gen_bindings_test.rs`)

- **alef-backend-kotlin: merge `[crates.java].exclude_types` into the Kotlin wrapper's effective exclude set so Kotlin never emits a wrapper referencing a Java facade the Java backend skipped.** Without this propagation, spikard's `dev.spikard.Router` (excluded at the Java layer) still produced a Kotlin coroutine wrapper that imported the missing class, surfacing as `Unresolved reference 'Router'` / `Unresolved reference 'routeCount'` / `Unresolved reference 'close'` at K2 compile. `effective_kotlin_exclude_types` now unions Kotlin's own exclude list with the Java backend's. Test `jvm_client_wrapper_respects_java_exclude_types` pins the merge. (`src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/tests/gen_bindings_test.rs`)

- **alef-e2e/java: emit `JsonUtil.fromJson(json, Type.class)` instead of the removed `Type.fromJson(json)` per-DTO factory.** The Java backend centralised JSON deserialisation in `JsonUtil` (removing per-DTO static `fromJson(String)` methods), but the e2e Java test generator continued to emit `var request = ChatCompletionRequest.fromJson("...")` — symbol not found at `javac` for every generated test using the `from_json` options-via path (OcrTest.java, ParityTest.java, RerankTest.java, ResponsesTest.java, SearchTest.java, SmokeTest.java in liter-llm). `render_test_method` in `codegen/java.rs` now emits `var {var} = JsonUtil.fromJson("{json}", {Type}.class)` for the `options_via = "from_json"` arm. `render_test_file` adds `import {pkg}.JsonUtil` whenever `all_options_types` is non-empty (same condition as existing per-DTO type imports). Two regression tests in `src/e2e/tests/java_from_json_codegen.rs` pin the emission shape and the import. (`src/e2e/codegen/java.rs`, `src/e2e/tests/java_from_json_codegen.rs`)

- **alef-backend-pyo3: emit `def __init__(self, ...) -> None: ...` stub for opaque types that have a `[workspace.client_constructors.TypeName]` entry.** The `.pyi` stub generator for opaque types (`gen_opaque_type_stub`) iterated `typ.methods` for instance and static method stubs but never emitted `__init__`, so mypy inferred `def __init__(self) -> None` (no args) for every opaque handle class. Every `DefaultClient(api_key=..., base_url=...)` call site in downstream `api.py` was rejected with "Too many arguments for `DefaultClient`". The fix passes `config.client_constructors.get(&typ.name)` to `gen_opaque_type_stub`; when a constructor config is present the stub emits `def __init__(self, param: PythonType, ...) -> None: ...` immediately after the class header. Raw Rust param types (`&str`, `String`, primitive integers, `()`) map to Python equivalents via a new `constructor_rust_type_to_python` helper; unknown types fall back to `Any`. Two regression tests in `src/backends/pyo3/tests/gen_stubs_test.rs` pin both the positive and negative cases. (`src/backends/pyo3/gen_stubs.rs`, `src/backends/pyo3/tests/gen_stubs_test.rs`)

- **alef-backend-pyo3: known regression — api.py adapter wrapper params and return values not converted between `options.*` layer types and `_internal_bindings.*` opaque types.** `emit_adapter_wrapper` passes `options.ChatCompletionRequest` directly to `engine.chat()` (which expects `_bindings.ChatCompletionRequest`) and returns `_bindings.ChatCompletionResponse` where the signature promises `options.ChatCompletionResponse`. `_to_rust_*` converters exist for input types but no `from_rust_*` converters exist for return types; adding return-type converters requires a new class of generated helpers out of scope for this patch. Fix tracked for follow-up; downstream mypy errors on adapter wrappers remain. (`src/backends/pyo3/gen_bindings/functions.rs`)

## [0.17.3] - 2026-05-20

### Fixed

- **Re-publish release with current main as binary.** The v0.17.2 git tag was force-pointed to `b539d258` (`fix(swift): gate codable enum synthesis`) after the original v0.17.2 release was cut, but the GitHub Release archive still shipped the binary built from the original pre-retag commit. Downstream consumers using `install-alef@v1` (which downloads the release binary) ran an older alef than the tag suggested, surfacing as `alef docs` output drift between local dev (built from source at the re-tagged commit) and CI (using the stale release binary). v0.17.3 cuts a fresh release on top of the same code so the GitHub Release binary matches the tag. (A stale local `v0.17.3` tag pointing at an earlier release-prep commit was discarded — it never existed on origin and had no associated GitHub Release.) No code changes between v0.17.2 (`b539d258`) and v0.17.3.

## [0.17.2] - 2026-05-20

### Fixed

- **alef-e2e/rust: bind fields_array accessor before len() assertion in e2e tests.** The Rust e2e generator emitted `assert!(chunks.len() >= 2 as usize, ...)` with `chunks` undeclared for non-streaming fixtures whose result struct has a `chunks` (or `imports`, `structure`, etc.) field registered in `fields_array`. The streaming-virtual-field arm in `render_assertion` fires unconditionally on these names (they are in `STREAMING_VIRTUAL_FIELDS`) and hardcodes `chunks` as the variable; for non-streaming calls no such local was bound. The fix emits `let {field} = &{result_var}.{field};` bindings in `render_test_function` for any `fields_array` field that both appears in assertions and collides with a streaming-virtual-field name, matching the shape of the PHP fix in `4a59541e`. A regression unit test in `codegen::rust::test_file::tests` pins the binding-before-assertion ordering. (`src/e2e/codegen/rust/test_file.rs`, `src/e2e/codegen/rust/assertions.rs`)

- **alef-backend-php: use language-neutral phrasing in `php_visitor_interface_start.jinja` class-level docblock.** The template hardcoded `ConversionOptions::$visitor` — PHP static-property-access syntax — to describe an instance property, making the generated `HtmlVisitorInterface.php` class-level comment syntactically invalid PHP. Replaced the `Type::$field` reference with "the `visitor` field of `ConversionOptions`", which is unambiguous in every target language. (`src/backends/php/templates/php_visitor_interface_start.jinja`)

- **alef-backend-go: pin method-level regression test for `Option<&str>` getters returning `*string`.** The earlier free-function snapshot (`option_string_return_null_checks_and_boxes_value`) covered the `binding_content` end-to-end path but did not exercise the method-emission path inside `gen_method_wrapper`. A stale spikard `binding.go` surfaced the same shape on `GraphQLRouteConfig.GetDescription()` — `func (h *GraphQLRouteConfig) GetDescription() *string { … return C.GoString(ptr) }` — and `golangci-lint` rejected the `string`/`*string` mismatch. The fix is downstream-only (regenerating spikard with the existing alef code already produces the corrected nil-check + take-address closure), but a new unit test in `alef-backend-go::gen_bindings::methods::tests` (`test_gen_method_wrapper_optional_string_getter_emits_nil_check_and_address`) now pins the method-level shape so future refactors of `gen_method_wrapper` / `go_return_expr` cannot regress this case for instance getters on opaque handles. (`src/backends/go/src/gen_bindings/methods.rs`)

- **alef-backend-swift: hide internal FFI types (`RustVec<T>`, `RustString`, `intoRust()`) from public Swift API.** Public Swift functions/methods previously exposed swift-bridge transport types in their signatures (e.g. the e2e `batchExtractBytesSync`/`batchExtractBytes`/`batchExtractFilesSync`/`batchExtractFiles` wrappers returned `[ExtractionResultRef]`, leaking the swift-bridge borrowed-Ref class through to user code). The auto-generated free-function forwarders already accepted native Swift types (`[T]`, `String`, `[UInt8]`, `Data`, …) and converted to `RustVec`/`RustString` internally; the four batch e2e wrappers now do the same by draining the returned `RustVec<ExtractionResult>` into an owned `[ExtractionResult]` via `pop()`-and-reverse before returning. `intoRust()` extensions are already emitted under `internal extension Name { … }` (consumed by other generated code, not user code), so their visibility is unchanged. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: emit `Codable`, `Sendable`, and `Hashable` only for data-bearing Swift enums whose associated values can synthesize those conformances.** Data-bearing serde enums now get native Swift protocol conformances when every payload type is bridge-safe, preserving JSON decoding and value semantics. Enums with opaque or non-Codable payloads fall back to a RustBridge typealias instead of generating Swift code that cannot compile. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/templates/swift_enum_header.jinja`)

- **alef-backend-php: emit `Option<T>` getter return type for Duration fields on Default structs.** The struct emitter wraps non-optional `Duration` fields in `Option<i64>` when `option_duration_on_defaults` is enabled (always true for PHP), but the getter emitter did not apply the same `force_optional` condition — it used bare `i64` for the return type while the storage field was `Option<i64>`, causing E0308. The fix adds the matching `typ.has_default && !field.optional && Duration` check to the getter emit so the return type mirrors the storage type. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-ffi: emit `.unwrap_or(default)` instead of manual `match result { Some(val) => val, None => default }` for `Option<Primitive>` returns.** The `gen_owned_value_to_c` helper emitted the manual-match form for every `Optional<T>` return, but when the inner type is a non-bool primitive the match body is just the value itself — clippy flags this as `manual_unwrap_or`. Non-bool primitives in `Optional` position now emit `expr.unwrap_or(null_value)` directly. The bool case still uses the match form because it needs the `as i32` cast. (`src/backends/ffi/src/gen_bindings/helpers.rs`)

- **alef-codegen, alef-backend-napi, alef-backend-wasm: use `.iter()` instead of `.into_iter()` for `&[T] → Vec<U>` clone-and-map emission (clippy::into_iter_on_ref under -D warnings).** The shared `wrap_return`/`wrap_return_with_mutex` helpers and per-backend equivalents emitted `result.into_iter().map(|v| v.clone().into()).collect()` for the `returns_ref` + `Vec<Named(_)>` case. Calling `.into_iter()` on a slice reference yields the same `&T` iterator as `.iter()`, so under `-D warnings` clippy 1.95 promoted the suggestion to a hard error: every alef-managed `liter-llm-{node,py,php,ffi}` (and previously `-php`, which already had a one-off fix) failed `cargo clippy --workspace -- -D warnings`. Switched the `returns_ref` arm in `alef-codegen/src/generators/{functions,binding_helpers}.rs`, `alef-backend-napi/src/gen_bindings/functions.rs`, and `alef-backend-wasm/src/gen_bindings/functions.rs` to `result.iter().map(...).collect()`. The integration test `test_wrap_return_with_mutex_vec_non_opaque_named_ref` was updated to assert the new shape.

- **alef-codegen/doc_emission: strip bare `Some <lowercase noun>` in prose.** The earlier `replace_some_calls` handled only the call form `Some(x)` and `replace_none_keyword` already replaced bare `None` with `null`/`undefined`, but bare `Some` in prose like "(Some values)" or "Some keys leave the previous" slipped through the sanitizer untouched. Real leak in html-to-markdown `PreprocessingOptionsUpdate.java:16`: "Only specified fields (Some values) will override existing options; null values leave the previous" — `null values` was correct (None → null) but `Some values` shouted Rust. New helper `replace_some_keyword_in_prose` runs immediately after `replace_some_calls` and drops `Some` when preceded by a word boundary and followed by a single ASCII space + ASCII-lowercase letter, leaving the following noun intact. `SomeType` (no space), `Some.method()` (followed by `.`), `Some Title` (uppercase follower), and `Some(x)` (already converted upstream) are all preserved. Two new unit tests pin the positive (PreprocessingOptionsUpdate leak) and negative (identifiers / proper nouns / call form) shapes; existing `sanitize_attribute_line_dropped` test relaxed to accept the stripped form alongside the legacy outcomes. (`src/codegen/doc_emission.rs`)

- **alef-backend-swift: emit public-API function and method names in camelCase (was snake_case).** Swift convention is camelCase methods; snake_case names in the userland module made the public API look like a thin Rust transliteration. The swift-bridge `extern "Rust"` block continues to use snake_case Rust idents (ABI requirement); Swift-side public functions and field getters are now generated via lower-camel-case transform of the same Rust ident — the swift-bridge getter extern now emits `#[swift_bridge(swift_name = "<camel>")]` so accessors like `ref.deviceId()` replace `ref.device_id()`, and the inbound-trait `Swift{Trait}Box` delegate protocol now exposes `func processImage(...)` rather than `func process_image(...)`. The internal `alef_<snake>` shim names (matched by swift-bridge `@_cdecl` exports) remain snake_case. Param labels on the delegate protocol/box class methods are also camelCase to match Swift conventions. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/src/gen_rust_crate/extern_block.rs`)

- **alef-backend-pyo3: emit `async def foo(...) -> T: return await engine.foo(...)` for non-streaming DefaultClient methods in api.py.** Previously `emit_adapter_wrapper` applied the streaming `async for item in engine.foo(): yield item` pattern to every adapter regardless of `pattern`, treating `async_method` adapters (which return a single awaitable value) identically to `streaming` adapters (which return an async iterator). The generated api.py wrapped 22 non-streaming methods as async generators with `-> AsyncIterator[()]` return types, producing 70+ mypy errors (`"Coroutine[…]" has no attribute "__aiter__"`, malformed `AsyncIterator[()]` annotation). The fix checks `adapter.pattern`: `AsyncMethod` now emits the `return await` form with the correct return type from `adapter.returns`; `Streaming` retains the `async for … yield` form. The `AsyncIterator` typing import is also gated on having at least one `Streaming` adapter. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-pyo3: map Rust `String`/`&str` to Python `str` in api.py adapter wrapper signatures.** `emit_adapter_wrapper` used the raw type string from `alef.toml` adapter param definitions directly in the generated Python signature, so `type = "String"` produced `url: String` instead of `url: str`. Python has no `String` builtin; mypy reported `Module "liter_llm.options" has no attribute "String"`. The fix routes adapter param type names through a new `adapter_param_python_type` helper that maps `"String"` and `"&str"` to `"str"` and passes other names through unchanged. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-pyo3: also map `bytes::Bytes` / `Vec<u8>` to Python `bytes` and `()` to `None` in api.py adapter signatures (params AND return type).** Methods returning `bytes::Bytes` (e.g. `speech`, `file_content`) previously emitted `-> bytes::Bytes` in the wrapper signature — `::` isn't a Python path operator so mypy aborted with `Invalid syntax`. The return-type emission now also goes through `adapter_param_python_type`, which gained `"bytes::Bytes"`/`"Vec<u8>"`/`"&[u8]"` → `bytes` and `"()"` → `None` mappings. (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-pyo3: import adapter return types into api.py so async wrapper signatures resolve at runtime.** When emitting the api.py for adapter-based bindings, only the adapter param types and the `item_type` of streaming adapters were added to the import list. `AsyncMethod` adapter return types (e.g. `BatchObject`, `ResponseObject`, `BatchListResponse`) were referenced bare in `async def foo(...) -> ReturnType` annotations but never imported, producing 20+ `Name "X" is not defined` mypy errors and F821 at import. The collector now also adds `adapter.returns` to `all_type_imports`, skipping names that map to Python builtins (`str`, `bytes`, `None`, `int`, `float`, `bool`). (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-go: null-check and box `Option<String>` / `Option<Char>` / `Option<Path>` returns instead of dereferencing C pointers directly.** Functions returning `Option<String>` were generating signatures like `func Foo() *string` but emitting bodies with `return C.GoString(ptr)` (which returns bare `string`), causing type mismatches in golangci-lint and govulncheck. The fix adds a closure that null-checks the C pointer and returns either `nil` or a boxed Go string: `func() *string { if ptr == nil { return nil }; defer C.{prefix}_free_string(ptr); s := C.GoString(ptr); return &s }()`. This pattern is consistent with how `Option<Primitive>` returns are already handled and matches idiomatic Go for nullable pointers. New snapshot test `option_string_return_null_checks_and_boxes_value` pins the generated code shape. (`src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/tests/snapshot_test.rs`)

- **alef-backend-swift: emit `*FromJson` forwarder for every `from_json`-eligible type (not just first-class structs).** The previous filter gated emission on `can_emit_first_class_struct`, so types with `from_json` capability but no first-class mirror (LayoutDetectionConfig, PdfConfig, PostProcessorConfig, every enum, etc.) lacked the JSON-decode forwarder. Swift e2e and the new enum `intoRust()` extension (from prior commit) both depend on `RustBridge.{typeName}FromJson(json)` existing. Broaden the filter to include all from_json-eligible IR types. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-codegen/doc_emission: add shared `sanitize_rust_idioms(text, DocTarget)` helper to strip Rust idioms leaking into foreign-language docstrings.** Generated PHPDoc / Javadoc / TSDoc inherited rustdoc verbatim, surfacing Rust-only syntax (`Result<T, E>`, `Option<T>`, `Vec<T>`, `HashMap<K, V>`, `Arc<>` / `Box<>` / `Mutex<>` wrappers, `Send + Sync` trait bounds, `'static` lifetimes, `::` path separator, `[`Type::method`]` intra-doc links, `.unwrap()` / `.expect("…")` calls, `Some(x)` / `None` in prose, `pub fn` / `crate::` / `&self` / `&mut self` mentions, `#[…]` attribute macros, and full ```rust ``` code fences) directly to PHP/Java/TS users for whom none of it parses. The new helper applies per-target transformations outside backtick/code-fence regions: intra-doc links collapse to `Type.method`; `Option<T>` rewrites to `T?` (PhpDoc) / `T | null` (JavaDoc) / `T | undefined` (TsDoc/JsDoc); `Vec<T>` to `T[]` (with `Vec<u8>` → `Uint8Array` for TsDoc/JsDoc, `byte[]` for JavaDoc); `HashMap<K, V>` to `Record<K, V>` (TS) / `Map<K, V>` (Java) / `array<K, V>` (PHP); wrapper types stripped; standalone `None` / `Some(...)` substituted; `::` collapsed to `.`; `rust`-tagged code fences dropped for TS, language tag stripped for Java/PHP. 26 unit tests pin the per-target output shape and confirm backtick-protection invariants. The existing per-backend `alef-backend-java::gen_bindings::helpers::sanitize_rust_syntax` body collapses to a single delegation. (`src/codegen/doc_emission.rs`, `src/backends/java/gen_bindings/helpers.rs`)

- **alef-backend-php: route every PHPDoc emission site through the shared `sanitize_rust_idioms(.., PhpDoc)` helper.** Five sites in `gen_bindings/mod.rs` (type stub at line 1363, enum stub at line 1464, struct class docblock at line 1716, method docblock at line 1747) and the visitor-interface method docstring path in `trait_bridge.rs` (lines 917–921) previously passed `method.doc.lines().next()` raw into Jinja templates, so `VisitorHandle.php` shipped Rust trait-bound prose ("`Arc`-wrapped `Mutex` for thread-safe shared mutation. `Send + Sync` so that types embedding a `VisitorHandle` ... can be shared across threads — required by callers that stash configs inside axum/rmcp/tokio Send-bound contexts.") and class docblocks contained rustdoc intra-doc links of the form `` [`ConversionResult`] ``. After wiring, PHP consumers see PHP-shaped types (`T?`, `array<K, V>`) and clean ``\`Type.method()\`` cross-references. All 39 backend tests + 4 unit tests pass; `task php:lint` clean; e2e pass count preserved (54 pre-existing `ConversionOptionsBuilder`-exclusion failures are unrelated). (`src/backends/php/src/gen_bindings/mod.rs`, `src/backends/php/src/trait_bridge.rs`)

- **alef-backend-java/gen_visitor: sanitize callback docstrings through the shared helper before emitting the visitor interface.** `gen_visitor::files::gen_visitor_interface` passed `spec.doc` raw into the Jinja template, so the generated `Visitor.java` callback methods inherited rustdoc with `Result<T, E>` / `Option<T>` generics, lifetime markers, and Rust path separators. New `sanitize_callback_doc(text)` helper in `gen_visitor::helpers` delegates to `alef_codegen::doc_emission::sanitize_rust_idioms(text, JavaDoc)` and is now called from the visitor-interface emission path. The record/builder/enum paths in `gen_bindings/types.rs` and `gen_bindings/enums.rs` already flow through `escape_javadoc_line` → `sanitize_rust_syntax` (now delegating to the shared helper per the codegen change above), so all DTOs inherit the cleanup automatically. Java backend build remains green; pre-existing `JsonUtil.fromJson` test-compile regression is unrelated and out of scope. (`src/backends/java/gen_visitor/files.rs`, `src/backends/java/gen_visitor/helpers.rs`)

- **alef-backend-wasm: route every rustdoc-emission site through `sanitize_rust_idioms(.., TsDoc)` so generated `.d.ts` files stop leaking Rust idioms into JS/TS tooltips.** Before this change, `src/backends/wasm/gen_bindings/functions.rs::emit_rustdoc` was a verbatim pass-through (template `templates/rustdoc.jinja` just emitted `/// {{ line }}` for each input line), so the generated `crates/html-to-markdown-wasm/pkg/*.d.ts` surfaced Rust intra-doc links (`` [`ConversionOptions::builder()`] ``), `Option<T>` / `Vec<u8>` / `Some(…)` / `None` in prose, `.to_string()` literals, and 100+ lines of `rust`-tagged code-fence examples — none of which help JS callers. The rewritten `emit_rustdoc` calls the sanitizer up-front; all 11 doc-emission call sites in `types.rs` (4), `enums.rs` (2), `methods.rs` (1), `functions.rs` (1 + the dispatcher itself) consume the helper transparently. `cargo test -p alef-backend-wasm` (17 lib tests + 2 visitor-substitution tests) passes; `cargo clippy -p alef-backend-wasm -- -D warnings` clean; regenerated `lib.rs` drops 123 lines of `rust`-tagged code examples that no longer reach the JS consumer. (`src/backends/wasm/gen_bindings/functions.rs`)

- **alef-backend-java: force Builder emission for types with `#[serde(flatten)]` `serde_json::Value` fields, regardless of field count.** The `@JsonAnySetter` method that absorbs unknown sibling keys during deserialization can only live on a builder setter — not on a record component. Previously, `should_emit_builder` under `Auto` mode skipped the builder for types with fewer than 5 fields (e.g. `ResponseOutputItem`, `ResponseTool`), leaving only `@JsonAnyGetter` on the record with no matching `@JsonAnySetter`. Jackson then rejected every sibling field as "Unrecognized field", and any flattened field whose wire value was an array caused `Cannot deserialize...LinkedHashMap from Array value`. The fix adds an early-return in the `Auto` branch: any type that has at least one `serde_flatten && TypeRef::Json` field now always gets a builder, ensuring `@JsonAnySetter` is emitted for correct round-trip deserialization. (`src/backends/java/gen_bindings/types.rs`)

- **alef-e2e/typescript: drain stream with `for await` in NAPI streaming-error tests.** The TypeScript e2e template for `expects_error` fixtures generated `await expect(async () => { await client.chatStream(...); }).rejects.toThrow()`, which awaits only the stream handle creation. The NAPI binding returns the handle lazily — HTTP errors (401, 400 content-policy) only surface when the first chunk is polled. Introducing `is_streaming_error_call` detection (error fixture + function name contains "stream" + no stream chunks), the template now emits `const _stream = await ...; for await (const _ of _stream) {}` inside the `expect` block so the error propagates before the wrapper exits, matching Python's `async for _ in _iterator` pattern. (`src/e2e/codegen/typescript/test_file.rs`, `src/e2e/templates/typescript/test_function.jinja`)

- **alef-cli/pipeline/extract: seed include-list expansion from included functions' signatures.** `expand_include_list` previously walked only `include.types` (and their transitively-referenced field/method types) when computing the keep-set, which silently dropped wrapper return structs declared alongside their returning functions if the user listed only the per-element type. The `sanitize_unknown_types` pass then collapsed the function's `Named(...)` return into `String`, and every binding facade emitted the wrong signature (regression observed on kreuzcrawl `batch_scrape`/`batch_crawl` → `BatchScrapeResults`/`BatchCrawlResults`). The expansion now also seeds itself with types referenced by every `include.functions` entry's return type and parameter types — the user opted into the function, so its public signature must survive the filter. (`crates/alef-cli/src/pipeline/extract.rs`)

- **alef-e2e validate: stop emitting "missing required input field 'input'" warnings when the call's arg uses the top-level 'input' field (the whole fixture.input is the arg's value — no sub-key lookup applies). Sub-path arg checks (e.g., field = "input.path") still fire correctly.**

- **alef-e2e validate: suppress "skipped for all languages" warning when the fixture's category is in [crates.e2e].exclude_categories — those fixtures are intentionally excluded at the category level, so empty skip.languages with no reason is the correct shape.**

- **alef-publish: skip Jni language in validate() — JNI is a transitive (Java/Kotlin) binding with no standalone package directory.**

- **alef-backend-go: prefix every line of multi-line rustdoc comments with `//` — previously continuation lines after a blank line were emitted bare, breaking the Go parser at the next non-comment line.** The `emit_godoc_summary` renderer in `gen_bindings/types.rs` already passes every line through `go_doc_comment_line.jinja` (which prepends `// `), so the renderer was correct. Added a regression test (`godoc_multiline_summary_continuation_lines_are_prefixed`) that drives the full `GoBackend::generate_bindings` path with a two-paragraph summary and asserts every comment line is `//`-prefixed and no true blank line appears inside the doc block. (`src/backends/go/tests/snapshot_test.rs`)

- **alef-backend-php: emit .iter().cloned().map(Into::into) instead of .into_iter().map(|v| v.clone().into()) for slice-to-owned conversions — fixes clippy::into_iter_on_ref on -D warnings.**

- **alef-e2e/php: bind fields_array accessor before count() assertion in e2e tests.** PHP e2e generator was emitting assertions like `count($chunks)` without first binding the variable, resulting in "Undefined variable $chunks" errors. Now emits `$chunks = $result->getChunks();` (and equivalently for `$imports` and `$structure`) before assertions that reference these fields_array items, matching Python/TypeScript behavior. (`src/e2e/codegen/php.rs`, `src/e2e/templates/php/test_method.jinja`)

- **alef-backend-swift: emit `init(_ rb: {Name}Ref) throws` converter on every first-class struct.** First-class Swift struct mirrors lacked an initializer from their FFI-bridge `Ref` counterpart, so swift e2e tests failed with `cannot convert value of type 'LayoutRegionRef' to expected argument type 'LayoutRegion'` (and similar for PageInfoRef, TableRef, etc.). The previous init took the owned `RustBridge.{Name}` class, but `RustVec<Name>` iteration yields the base `{Name}Ref`, so chains like `try rb.layouts().map { try Layout($0) }` did not compile. Change the parameter type to `RustBridge.{Name}Ref`; since swift-bridge emits `{Name}: {Name}RefMut: {Name}Ref`, owned instances still bind via class inheritance. Existing helpers `vec_elem_convert_expr`, `swift_ffi_read_expr`, `forwarder_return_conversion_suffix_with_throws`, and `return_uses_json_bridge` are reused for per-field conversion. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: emit `RustVec<RustString>` for Rust `Vec<String>` (was `RustVec<String>`, which is not `Vectorizable`).** Swift compilation failed with `Type 'String' does not conform to protocol 'Vectorizable'` because swift-bridge's `RustVec<T>` requires `T: Vectorizable`, satisfied only by `RustString` and primitive scalars. Update `emit_vec_arg` so `intoRust()` field-conversion preludes substitute `RustString` for `String` when wrapped in `RustVec` and wrap each pushed element with `RustString(__elem)`. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: emit `intoRust()` extension on every Swift enum mirror.** Swift enums emitted by alef previously lacked any way to convert back to the FFI-bridge `RustBridge.<EnumName>` type, forcing swift e2e tests to fail with `<EnumType>.intoRust() undefined` for ExecutionProviderType, CodeContentMode, OcrElementLevel, etc. Add a generated extension that maps each Swift case to its `RustBridge.<Name>` counterpart — covering both unit-variant and data-variant enums. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e/ruby: emit post-clear `list_*` assertion after `clear_*` calls.** Ruby e2e tests for plugin registry clear operations (`clear_ocr_backends`, `clear_post_processors`, etc.) previously called the method but did not assert that the list is actually empty afterwards, so tests would pass even if `clear` was a no-op. The codegen now detects function names ending in `_clear` and emits a follow-up call to the corresponding `list_*` function with an assertion that the result is empty, ensuring the test actually validates the clear operation's effect. (`src/e2e/codegen/ruby.rs`, `src/e2e/templates/ruby/test_function.jinja`)

- **alef-backend-napi: emit typed `JsonValue` type alias for `serde_json::Value` fields instead of bare `unknown`.** Fields typed `TypeRef::Json` (e.g. `ActionResult.data`, `BrowserExtras.eval_result`) now emit `readonly data?: JsonValue` in the `.d.ts` instead of `readonly data?: unknown`, where `JsonValue` is a recursive type alias representing arbitrary JSON: `string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }`. This improves TypeScript inference and enables strict type checking of JSON return values across the binding API. (`src/backends/napi/src/gen_bindings/errors.rs`)

- **alef-backend-magnus: kwargs-only initializers, typed RBS fields, Enumerator streaming.** Emits idiomatic Ruby code for three long-standing non-idiomatic patterns: (a) All struct initializers now use kwargs-only signature (e.g. `CrawlConfig.new(max_depth: 10, timeout: 30000)`) instead of positional params — RBS stubs declare `def initialize: (?field1: T, ?field2: T, ...)` with all fields as optional kwargs for config types (has_default=true) or required/optional kwargs for result types. (b) JSON fields typed `TypeRef::Json` now emit `JsonValue` type alias (union of primitives/arrays/hashes/nil) instead of bare `untyped` to improve Sorbet inference — introduced `type json_value` + `type JsonValue` alias in module scope. (c) Streaming methods (`crawl_stream`, `batch_crawl_stream`, etc.) now correctly return `Enumerator[CrawlStreamIterator]` instead of String, matching the Magnus runtime implementation that yields events to a block or returns an iterator. RBS generation receives streaming_method_names set and derives iterator class names from method names for Enumerator type annotation. (`src/backends/magnus/{gen_stubs.rs,type_map.rs,gen_bindings/mod.rs}`)

- **alef-cli/validate_versions: drop `"kreuzberg"` literal from go-doc version heuristic.** `read_go_doc_version` was project-scoped, branching on `lower.contains("kreuzberg")` to find version comments. The `"version"` and `"targets"` substrings cover the universal patterns (`// targets <Project> X.Y.Z`, `// version X.Y.Z`); drop the kreuzberg-specific token so alef stays generic across downstream projects. (`crates/alef-cli/src/commands/validate_versions.rs`)

- **alef-backend-go: Map Rust `String` return to Go `string` (was `*string`); `Option<String>` keeps `*string` for nullable distinction.** Functions and methods returning Rust `String` now generate Go signatures with bare `string` type (e.g. `func DetectMimeType() string` not `*string`), matching Go idiom where zero value `""` is meaningful. Rust `Option<String>` still maps to `*string` for struct fields and parameter types to represent nullability. Fixed `go_return_expr` to emit `C.GoString(ptr)` directly for non-optional strings instead of wrapping in a closure that returns `*string`, and updated `go_optional_type` to distinguish optional vs bare scalar types at call site. (`src/backends/go/src/{type_map,gen_bindings/{types,functions,methods}}.rs`)

- **alef-backend-csharp/naming: apply 3+ letter acronym PascalCase rule.** C# convention prefers `Uri`, `Xml`, `Json` over `URI`, `XML`, `JSON` for 3+ letter acronyms. The `csharp_type_name` function now normalizes all-uppercase acronym names (e.g., `URI` → `Uri`, `XML` → `Xml`) to PascalCase before applying preserved-initialism rules (GraphQL, ID, UUID), ensuring type filenames and class names conform to Microsoft's framework design guidelines. (`src/codegen/naming.rs`)

- **alef-backend-csharp: serialize record-type parameters to JSON in static factory methods.** When a static factory method (e.g., `ExtractionResult.FromOcr`) takes a record-type parameter (e.g., `OcrExtractionResult`), the parameter must be serialized to JSON and marshalled through the FFI layer via `{TypeName}FromJson()`, not passed as a raw `nint`. The codegen now detects `TypeRef::Named` parameters in static factories and emits serialize/deserialize wrapper code with try-finally cleanup, matching the pattern used for receiver-based methods. This fixes compile errors when factories receive record-type arguments. (`src/backends/csharp/gen_bindings/types.rs`)

### Added

- **alef-cli**: Add `no_project_special_casing` integration test that fails CI if codegen introduces literal references to specific downstream projects (kreuzberg, kreuzcrawl, html-to-markdown, tree-sitter-language-pack, liter-llm, spikard) outside test/doc contexts.

- **alef-e2e: add `stream.has_page_event`, `stream.has_error_event`, `stream.has_complete_event`, and `stream.event_count_min` virtual fields for streaming-fixture assertions.** Streaming fixtures previously had no way to assert against per-event-type presence or minimum event counts without relying on `not_empty`-style synthetic assertions that every backend had to emit as `// skipped`. The new virtual fields are resolved by `streaming_assertions.rs` with per-language accessors (Python list comprehensions, TypeScript `Array.some`/`.filter`, Rust iterator combinators, etc.), unskipping streaming assertions across every backend's streaming test suite. (`src/e2e/codegen/streaming_assertions.rs`)

- **alef-e2e: normalize `pages_crawled` / `min_pages` assertion aliases to `count_equals` / `count_min` on the `pages` field.** Fixtures commonly use `crawl.pages_crawled` (or bare `pages_crawled`) with `equals` and `crawl.min_pages` with `greater_than_or_equal` to assert the number of pages a crawl yielded. Neither is a struct field on the call's result type, so every backend was emitting `// skipped: field 'crawl.pages_crawled' not available on result type` for these assertions. A new post-load `normalize_assertions` pass rewrites the alias to a `count_*` assertion targeting the `pages` field that the `crawl`-shaped calls already expose via `result_fields`, letting every backend render the assertion through its existing `count_equals` / `count_min` paths. (`src/e2e/fixture.rs`)

### Changed

### Fixed

- **alef-e2e/elixir: emit `mime_type:` keyword-arg for `extract_file_async`.** Elixir NIF signatures for async file extraction expect the optional `mime_type` parameter as a keyword argument, not positional. The codegen was emitting positional arguments, causing `argument error` at runtime. Updated `build_args_and_setup` to format all optional arguments as keyword-list entries (`name: value`) regardless of their primitive type (string, file_path, bytes, etc.), not just for json_object types. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e/csharp, alef-e2e/c: build per-call `FieldResolver` from the resolved fixture call's `result_fields`.** Both backends previously constructed a single top-level `FieldResolver` from `e2e_config.result_fields` and reused it for every fixture, so assertions like `pages.length` on a fixture routed to the `crawl` call (whose `[crates.e2e.calls.crawl] result_fields = ["pages", ...]` includes `pages`) were being skipped because the top-level `result_fields` (scoped to the default `scrape` call) does not list `pages`. This matches the per-call resolver pattern already used by the go/java/swift/dart/zig/kotlin/elixir/php/ruby backends and unskips ~250 assertions across the kreuzcrawl C and C# e2e suites (the C# `CrawlTests` alone gained 60+ `result.Pages.Count` assertions). (`src/e2e/codegen/csharp.rs`, `src/e2e/codegen/c.rs`)

- **alef-backend-php: don't double-wrap `Option<T>` getter return type for already-optional IR fields.** `types.rs:gen_getter_methods` unconditionally applied `mapper.optional(...)` when `field.optional` is true, producing `Option<Option<T>>` getters for fields whose IR type is already `TypeRef::Optional<T>` (e.g. Update DTOs where every field is both `field.optional == true` and `TypeRef::Optional<inner>`). The getter body `self.{name}.clone()` returns the storage type `Option<T>`, which the declared return type `Option<Option<T>>` cannot accept — Rust rejects the implicit `Option<T> → Option<Option<T>>` coercion with E0308. Detect `matches!(field.ty, TypeRef::Optional(_))` and skip the outer wrap in that case. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-napi: exclude `*Builder` / `*Update` DTOs from `From<>` impl emission to match struct-emission filter.** The struct-emission loop already filters `!typ.name.ends_with("Builder") && !typ.name.ends_with("Update")`, but the parallel `From<binding> for core` emission loops did not — they iterated every type and emitted impls referencing `Js{Type}Update`/`Js{Type}Builder` types that were never defined, yielding `cannot find type` E0425 for any DTO with an Update sibling (e.g. `JsConversionOptionsUpdate`). Apply the same filter at all three conversion-emission sites (main loop, pre-seed of `emitted_binding_to_core`, fallthrough loop). (`src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-e2e/dart: drop synthetic `expect(result, isNotNull)` from `not_error` assertion.** Dart rejects `expect(<void>, isNotNull)` with `error: This expression has type 'void' and can't be used`. Trait-bridge `clear_*` fixtures with a `not_error`-only assertion list therefore failed to compile every `*_management_test.dart`. `await` already raises on error, so the explicit `expect` adds nothing — emit only a doc-comment placeholder so the test body remains valid for non-mock fixtures. (`src/e2e/codegen/dart.rs`)

- **alef-backend-swift: emit JSONDecoder body for throwing functions whose return type swift-bridge cannot natively bridge.** swift-bridge transports `Vec<Vec<T>>`, `Map<_, _>`, and `Json` returns from `Result<T, _>` functions through `RustString` (serde-serialised). The previous forwarder emitted `return try RustBridge.fn(...)` directly, which the Swift compiler rejected with `cannot convert return expression of type 'RustString' to return type '[[Float]]'`. Detect those return shapes via a new `return_uses_json_bridge` helper and emit a three-line body that `.toString()`s the RustString, converts to `Data`, then decodes with `JSONDecoder` against the declared Swift type. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: coerce `RustString` to native Swift `String` for throwing functions with `String` return.** swift-bridge maps `Result<String, _>` to `throws -> RustString` (the error path forces the union through the RustString ABI), but the public forwarder declares `-> String`. Without the `.toString()` coercion the Swift compiler rejected the body with `cannot convert return expression of type 'RustString' to return type 'String'`. The previous codegen omitted the suffix based on the incorrect assumption that swift-bridge maps `Result<String, _>` directly to native String — only the non-throwing case actually does. Introduce a `throws` parameter to `forwarder_return_conversion_suffix` and use it at call sites. Snapshot + bytes-overload regression test updated. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: stop emitting broken `RustStringRef.toString()` shim, expand `as_str().toString()` at call sites.** The 0.17.1 shim appended `extension RustStringRef { func toString() }` to swift-bridge's `SwiftBridgeCore.swift`, but Swift rejected it with `non-'@objc' instance method 'toString()' is declared in extension of 'RustStringRef' and cannot be overridden` because `RustString` is a subclass of `RustStringRef` and declares its own `toString()` in a separate extension. Swift cannot resolve the override chain across two non-@objc extensions in the same class hierarchy. Replace every emitted `.map { $0.toString() }` for `RustVec<RustString>` (in `vec_elem_convert_expr`, `forwarder_return_conversion_suffix`, and all three optional/required Vec<String> sites in `swift_ffi_read_expr`) with `.map { $0.as_str().toString() }`. The post-process step now strips any legacy shim previously appended by older alef revisions. Snapshot tests + the bulk-nested constructor assertion updated. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-dart: skip regular emission in userland bridge class for trait-bridge `clear_fn` names.** The Rust-side fix landed earlier still left the Dart userland file (`packages/dart/lib/src/{module}.dart`) declaring `static Future<void> clearXxxs()` twice — once by the regular-function forwarder loop and again by `emit_trait_bridge_methods`. Dart rejected the duplicate with "already declared in this scope". (`src/backends/dart/src/gen_bindings/mod.rs`)

- **alef-backend-zig: drop optional wrapper from `_out_error` in clear/unregister templates.** Templates declared `var _out_error: ?[*c]u8 = null` and passed `&_out_error` to the C function which expects `[*c][*c]u8`. The Zig type checker rejected the implicit `*?[*c]u8 → [*c][*c]u8` coercion. Drop the optional wrapper to match the C signature directly and switch the null-check to a non-optional comparison. (`src/backends/zig/templates/clear_fn_body.jinja`, `src/backends/zig/templates/unregister_fn_body.jinja`)

- **alef-backend-swift: skip regular emission in free-function forwarder pass for trait-bridge `clear_fn` names.** Mirror of the earlier rust-side fix — the userland `Kreuzberg.swift` forwarder pass still emitted `public func clearOcrBackends() throws { try RustBridge.clearOcrBackends() }` alongside the trait-bridge forwarder, causing `invalid redeclaration` for every clear_* across the public Swift module. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-php: skip regular emission in userland facade for trait-bridge `clear_fn` names.** The previous fix skipped the API-stub class, but the userland `Kreuzberg.php` facade still emitted `clearOcrBackends()` from the regular `api.functions` loop alongside the trait-bridge loop's emission, triggering PHP fatal `Cannot redeclare ...` at extension load. (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-backend-magnus: drop duplicate trait-bridge `clear_fn` registration.** The fix in 0.17.1 stopped emitting two `pub fn clear_*` definitions, but the Ruby module registration loop (`module.define_module_function(...)`) still emitted the call twice: once from the regular `api.functions` registration pass and once from the trait-bridge registration loop. Skip trait-bridge-managed names in the regular pass so the function is registered exactly once. (`src/backends/magnus/gen_bindings/functions.rs`)

- **alef-backend-zig: align synthetic `unregister_{snake}` template with `KreuzbergError!void` body.** The bdceae1e migration converted the body and the configured-name signature template to `KreuzbergError!void` style, but the synthetic-name `unregister_fn_signature.jinja` was missed and still emitted `pub fn unregister_{snake}(name: [*c]const u8, out_error: ?*?[*c]u8) i32`. Because the zig backend never instantiates `ZigTraitBridgeGenerator`, only the synthetic path runs, leaving the `out_error` parameter unused and breaking the build with `error: unused function parameter`. Drop the `out_error` parameter and return `KreuzbergError!void` to match the body. (`src/backends/zig/templates/unregister_fn_signature.jinja`)

- **alef-backend-swift: skip regular emission for trait-bridge `clear_fn` names.** Mirror the go/dart/php/napi/magnus/rustler fix. `__swift_bridge__clear_*` was emitted twice — once via the regular function-emission loop and once via the trait-bridge wrappers — breaking `cargo build -p kreuzberg-swift` with `E0428: __swift_bridge__clear_*` is defined multiple times. (`src/backends/swift/src/gen_rust_crate/mod.rs`)

- **alef-backend-go, alef-backend-dart: extend trait-bridge `clear_fn` skip to Go and Dart dispatchers.** Mirror the 0.17.1 magnus/rustler/0.17.2 napi/php fix. Without this filter the Go binding refused to compile (`Clear* redeclared in this block`) and the Dart binding triggered noisy "still multiple objects with same key" warnings from `flutter_rust_bridge_codegen` on every regen. (`src/backends/go/src/gen_bindings/mod.rs`, `src/backends/dart/src/gen_rust_crate/mod.rs`)

- **alef-backend-napi, alef-backend-php: extend trait-bridge `clear_fn` skip to NAPI-RS and ext-php-rs dispatchers.** Mirror the 0.17.1 magnus/rustler fix — call `alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(name, bridges)` in the napi and php backend dispatchers so trait-bridge `clear_*` functions are not emitted twice. Without this filter the napi crate would refuse to compile (`E0428` duplicate definition) and the ext-php-rs crate would emit colliding method names. (`src/backends/napi/src/gen_bindings/mod.rs`, `src/backends/php/src/gen_bindings/mod.rs`)

## [0.17.1] - 2026-05-20

### Fixed

- **alef-backend-magnus, alef-backend-rustler: skip regular emission for trait-bridge `clear_fn` names.** Functions named in any `trait_bridge.clear_fn` (e.g. `clear_ocr_backends`, `clear_post_processors`) were emitted twice: once by the regular function-emission loop (because `clear_*` takes no `dyn Trait` parameter, so `find_bridge_param` does not match), and once by the trait-bridge code path's `gen_clear_fn`. Result: Magnus/ext Ruby builds failed with `E0428: the name 'clear_*' is defined multiple times`, and Rustler NIF builds aborted at runtime with `Duplicate NIF entry for 'Elixir.Kreuzberg.Native':clear_*/0` — taking down the entire NIF (every `Kreuzberg.Native.*` call became `UndefinedFunctionError`). Add `alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(name, bridges)` and use it in both backend dispatchers to skip the regular emission so only the trait-bridge wrapper is generated. (`src/codegen/generators/trait_bridge.rs`, `src/backends/magnus/gen_bindings/mod.rs`, `src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-scaffold/swift: emit canonical `swift-format` output on first write.** The Swift scaffold emitter produced `Package.swift`, `Examples/Demo/main.swift`, `.editorconfig`, and `.swiftformat` with 4-space indentation and a trailing comma on the single-element `products:` array, while the test stub was missing a blank line between `import XCTest` and `@testable import`. `swift-format` (with its 2-space defaults) rewrote all four files on every `prek run --all-files` after regen. Fix: emit 2-space indentation throughout `Package.swift` and `Demo/main.swift`, drop the trailing comma from the single-element `products` array entry, add the blank line between import groups in the test stub, and align `.editorconfig` (`indent_size = 2`) and `.swiftformat` (`indent = 2`) with `swift-format`'s defaults. Five new assertions in `test_scaffold_swift` pin the canonical form. (`crates/alef-scaffold/src/languages/swift.rs`)

- **alef-scaffold: emit JNI Cargo.toml in cargo-sort canonical form.** The JNI scaffold emitter (`scaffold_jni`) emitted `[dependencies]` with the umbrella `{config.name}` dep baked at the head of a static list and `[package.metadata.cargo-machete]` placed after `[dependencies]`. `cargo sort` would rewrite the file on every regen because (a) the umbrella name does not sort first alphabetically for most consumers (kreuzcrawl, tree-sitter-language-pack, liter-llm), and (b) cargo-sort hoists `[package.metadata.*]` sub-tables directly after `[package]`. Fix: collect dep lines into a `Vec<String>`, sort alphabetically, and emit `[package.metadata.cargo-machete]` between `[package]` and `[lib]`. Two regression tests assert section order and alphabetised deps. (`crates/alef-scaffold/src/languages/jni.rs`)

- **alef-e2e/brew: emit 2-space-indented shell scripts so shfmt does not reformat them.** The brew e2e generator emitted `writeln!` literals with 4-space (and 8-space) shell-block indentation. Every `prek run --all-files` cycle invoked `shfmt -i 2` which rewrote the indentation, leaving every regenerated brew runner script in working-tree diff. Fix: emit 2-space (and 4-space) indents directly in the renderer; add a regression test pinning the canonical indentation depth across the entire emitted runner script. (`src/e2e/codegen/brew.rs`)

- **alef-e2e/ruby: restore `expect(result).not_to be_nil` for `not_error` assertions on non-void returns.** Commit `a1a50adc` narrowed the nil-check suppression too broadly: any fixture whose only assertion was `not_error` skipped the explicit `not_to be_nil` check even when the underlying call returned a non-`Result<()>` value. The result was that ruby `not_error`-only fixtures emitted an unused `let result = ...; end` body — the same empty-body / unused-local pattern that the dart fix corrected. Revert that branch of the template so the explicit `not_to be_nil` returns for non-void results; `Result<()>` (true unit returns) still suppress the nil check. (`src/e2e/templates/ruby/test_function.jinja`)

- **alef-scaffold: emit Rustler NIF crate Cargo.toml in cargo-sort canonical section order** (`[package]` → `[workspace]` → `[lib]` → `[dependencies]`). The previous template placed `[lib]` before `[workspace]` and `[workspace]` after `[dependencies]`, causing `cargo sort` to rewrite the file on every post-generation hook run. (`crates/alef-scaffold/src/languages/elixir.rs`)

- **alef-e2e (all 8 backends): thread `adapter.request_type` to `build_args_and_setup` so streaming call sites wrap the mock URL in a typed request object.** Every per-language e2e backend's `build_args_and_setup` helper emitted the raw URL variable as the argument for a `mock_url`-typed call arg, even when the corresponding adapter declares a `request_type` (e.g. `kreuzcrawl::CrawlStreamRequest`). The generated tests passed the bare URL string directly to the streaming adapter, which expects a typed request struct, causing compile/type errors in every language. Fix: thread `adapters: &[AdapterConfig]` from `generate()` through `render_test_file` → `render_test_method/render_test_case` → `build_args_and_setup`. In each `build_args_and_setup`, when the function name matches an adapter with a `request_type`, strip the Rust path prefix and emit a language-idiomatic typed request constructor binding before the call. (`src/e2e/codegen/{python,typescript,go,ruby,java,csharp,elixir,php}.rs`)

- **alef-e2e/php: emit smoke `assertNotNull` for fixtures with no assertions instead of `markTestSkipped`.** The `test_method.jinja` template's `elif skip_test` branch (triggered when `fixture.assertions.is_empty()`) emitted `$this->markTestSkipped(...)`, causing PHPUnit to report the test as skipped rather than actually exercising the function. Fix: replace that branch with the same setup/call/`assertNotNull($result)` pattern used for real smoke tests so zero-assertion fixtures still verify the call does not throw. (`src/e2e/templates/php/test_method.jinja`)

### Fixed (urgent)

- **alef-backend-swift: convert `Optional<RustBridge.{Dto}>` returns to the high-level DTO struct in free-function forwarders, and ship a `RustStringRef.toString()` shim.** Two Swift compile failures surfaced after the recent alef-extract `EmbeddingPreset2 → EmbeddingPreset` disambiguation: (1) the free-function forwarder for `getEmbeddingPreset(name:) -> EmbeddingPreset?` returned `RustBridge.getEmbeddingPreset(name)` unchanged — that bridge call yields `RustBridge.EmbeddingPreset?` (the low-level swift-bridge class), but the enclosing public signature resolves `EmbeddingPreset?` against the high-level `Codable/Sendable` struct emitted in the same module, producing `cannot convert value of type 'RustBridge.EmbeddingPreset?' to expected type 'EmbeddingPreset?'`; (2) the forwarder for `listEmbeddingPresets() -> [String]` emitted `.map { $0.toString() }` over a `RustVec<RustString>`, but `RustVecIterator<RustString>.next -> T.SelfRef` yields `RustStringRef`, and swift-bridge only emits the shortcut on the owned `RustString` class — there is no `RustStringRef.toString()`. Fix: (a) `forwarder_return_conversion_suffix` now consults `known_dto_names` and emits `.map { try {Name}($0) }` for `Optional<Named-DTO>` returns, routing through the symmetric `init(_ rb: RustBridge.{Name}) throws` initializer that the first-class DTO emitter already produces; (b) the forwarder body widens to `throws` when the value-conversion can throw, even when the bridge call itself does not, via a new `return_value_conversion_throws` helper; (c) `emit_swift_bridge_files` post-processes `SwiftBridgeCore.swift` through a new `append_rust_string_ref_to_string_extension` step that idempotently appends `extension RustStringRef { public func toString() -> String { self.as_str().toString() } }`, giving the borrowed-reference companion the same shortcut as the owned class. Pinned by 4 new unit tests (Optional<DTO> conversion suffix, non-DTO pass-through, shim appended on fresh content, shim is idempotent). (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-zig + alef-e2e/zig: zig e2e build failed with mismatched `clear_*` arities and `[]u8` field access.** Two related defects surfaced when running kreuzberg's `task zig:e2e` after the v0.16.14 `OutOfMemory` error-set unification: (1) `clear_fn_signature.jinja` / `clear_fn_body.jinja` and `unregister_fn_configured_signature.jinja` / `unregister_fn_body.jinja` in `alef-backend-zig` emitted `pub fn clear_X(out_error: ?*?[*c]u8) i32` — leaking the C ABI through the binding — while the e2e generator (correctly) called `try kreuzberg.clear_X()` expecting a `KreuzbergError!void` shape; (2) `render_assertion` in `alef-e2e/src/codegen/zig.rs` gated the synthetic `embeddings` JSON-array path on `!is_valid_for_result(f)`, but `is_valid_for_result` is permissive when `result_fields` is empty (the common case for `Vec<T>` / bare-`String` returns whose Zig binding is `[]u8`), so assertions like `field: "embeddings"` and `field: "result"` fell through to literal `result.embeddings.len` / `result.result` access on a `[]u8` slice, failing compilation with `error: no member named 'embeddings' in '[]u8'`. Fix: (a) the four Zig trait-bridge templates now emit `pub fn {name}(...) KreuzbergError!void { var _out_error: ?[*c]u8 = null; const _rc = c.<…>(&_out_error); if (_out_error) |ptr| _free_string(ptr); if (_rc != 0 or c.kreuzberg_last_error_code() != 0) return _first_error(KreuzbergError); }` — matching the idiom used for every other fallible function in the binding; (b) the `embeddings` synthetic path now gates on `has_explicit_field`, and a parallel synthetic `result` path was added covering `contains` / `not_contains` / `equals` / `not_empty` / `is_empty` on bare-string returns. Existing snapshot assertion (`emit_trait_bridge_emits_clear_fn_when_configured`) updated to match the new idiomatic signature; all 15 `alef-backend-zig` unit tests pass. (`src/backends/zig/templates/{clear,unregister}_fn_*.jinja`, `src/backends/zig/src/trait_bridge.rs`, `src/e2e/codegen/zig.rs`)

- **alef-e2e/brew: emit shfmt-canonical 2-space indents in generated `*.sh` files.** `render_run_tests` and `render_category_file` in `src/e2e/codegen/brew.rs` previously hand-emitted shell helper bodies and test-function bodies with 4-space indents (and 8-space for nested levels). Downstream consumers (kreuzcrawl, …) run the `shfmt` pre-commit hook with the default `-i 2` step, so every `e2e/brew/run_tests.sh` and `e2e/brew/test_*.sh` file was rewritten to 2-space indent on the very first `prek run --all-files` after each regen — 29 files in kreuzcrawl alone. Fix: emit shell content with 2-space first-level indent (and 4-space second-level) directly from the codegen so the generator output is byte-identical to `shfmt -i 2` canonical form. Add `tests::render_run_tests_uses_two_space_indent` regression test asserting every leading-whitespace prefix in the emitted runner is a multiple of two spaces, so a future contributor cannot reintroduce 4-space-indent string literals without the test failing. (`src/e2e/codegen/brew.rs`)

- **alef-scaffold: emit trailing newline for `py.typed` and Java `checkstyle.properties` to satisfy POSIX text-file convention.** `scaffold_python` emitted `py.typed` as an empty string (zero bytes), so consumer pre-commit `end-of-file-fixer` hooks added a trailing `\n` on every regen. Fix: emit `"\n"` for both `py.typed` and `checkstyle.properties` so the files are one byte and end-of-file-fixer compliant. Added a regression assertion in `test_scaffold_python` checking `py.typed` content ends with `'\n'`. (`crates/alef-scaffold/src/languages/python.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-scaffold/java: terminate empty `packages/java/checkstyle.properties` with a trailing newline.** `scaffold_java` emitted `checkstyle.properties` as an empty string (zero bytes), so consumer pre-commit `end-of-file-fixer` hooks added a trailing `\n` on every regen. Fix: emit `"\n"` so the file is one byte and end-of-file-fixer compliant. The scaffold test (`test_scaffold_java_checkstyle_suppressions_use_config_location`) now asserts both `properties.content.trim().is_empty()` (still semantically empty — no checkstyle properties needed) and `properties.content.ends_with('\n')` (regression guard against the trailing-newline drop). (`crates/alef-scaffold/src/languages/java.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-scaffold/jni: emit cargo-sort canonical JNI `Cargo.toml`.** `scaffold_jni` baked a static `[dependencies]` order (`{config.name}` first, then `futures-util`, `jni`, `serde_json`, `tokio`) and placed `[package.metadata.cargo-machete]` after `[dependencies]`. cargo-sort rewrites both: deps alphabetised (with the umbrella dep landing wherever `config.name` sorts) and `[package.metadata.*]` sub-tables hoisted directly after `[package]`. Result: prek's `cargo-sort` hook rewrote `<crate>-jni/Cargo.toml` on every regen for any consumer whose `config.name` did not happen to sort first (`kreuzcrawl`, `tree-sitter-language-pack`, `liter-llm`, …). Fix: collect dep lines into a `Vec`, `sort()`, and emit; place `[package.metadata.cargo-machete]` between `[package]` and `[lib]`. Two new regression tests (`scaffold_jni_dependencies_are_alphabetically_sorted`, `scaffold_jni_section_order_matches_cargo_sort`) pin the canonical form. (`crates/alef-scaffold/src/languages/jni.rs`)

- **alef-backend-swift: convert `Optional<RustBridge.{Dto}>` returns to the high-level DTO struct in free-function forwarders, and ship a `RustStringRef.toString()` shim.** Two related Swift compile failures landed after the alef-extract `EmbeddingPreset2 → EmbeddingPreset` disambiguation fix: (1) the free-function forwarder for `getEmbeddingPreset(name:) -> EmbeddingPreset?` returned `RustBridge.getEmbeddingPreset(name)` unchanged, which is `RustBridge.EmbeddingPreset?` (the low-level swift-bridge class) — but the enclosing public signature resolves `EmbeddingPreset?` against the high-level `Codable/Sendable` struct emitted in the same module, producing `cannot convert value of type 'RustBridge.EmbeddingPreset?' to expected type 'EmbeddingPreset?'`; (2) the forwarder for `listEmbeddingPresets() -> [String]` emitted `.map { $0.toString() }` over a `RustVec<RustString>`, but the iterator yields `T.SelfRef = RustStringRef` (per `RustVecIterator<RustString>.next`), and `RustStringRef` has no `.toString()` — swift-bridge only emits the shortcut on the owned `RustString`. Fix: (a) `forwarder_return_conversion_suffix` now consults `known_dto_names` and emits `.map { try {Name}($0) }` for `Optional<Named-DTO>` returns, routing through the symmetric `init(_ rb: RustBridge.{Name}) throws` initializer that the first-class DTO emitter already produces; (b) the forwarder body widens to `throws` when the value-conversion can throw, even when the bridge call itself does not, via a new `return_value_conversion_throws` helper; (c) `emit_swift_bridge_files` post-processes `SwiftBridgeCore.swift` through a new `append_rust_string_ref_to_string_extension` step that idempotently appends `extension RustStringRef { public func toString() -> String { self.as_str().toString() } }`, giving the borrowed-reference companion the same shortcut as the owned class. Pinned by 4 new unit tests (DTO conversion suffix, non-DTO pass-through, shim appended on fresh content, shim is idempotent). (`src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e/dart: emit `expect(result, isNotNull)` for `not_error` assertions instead of an empty body.** The dart codegen treated `not_error` (the assertion attached to registry-management calls like `clear_document_extractors`, `list_document_extractors`, and every streaming fixture) as a no-op, so generated dart tests landed with `final result = await ...;` followed by nothing — the test body was empty and the local was unused, masking real regressions because the test could not actually fail. Every other language generator (python `_ = call()`, ruby `expect(result).not_to be_nil`, node implicit return, java/csharp `assertNotNull`) consumed the result in some form. Fix: both `render_assertion_dart` and `render_streaming_assertion_dart` now emit `expect({result_var}, isNotNull);` for `not_error`. Because every dart e2e call is `await`ed, a thrown error already fails the test on the call line; the explicit expect adds a non-empty body, consumes the local (no `unused_local_variable` hint), and parallels the other backends. Doc comment on `render_streaming_assertion_dart` updated to reflect the new emission. (`src/e2e/codegen/dart.rs`)

- **alef-scaffold, alef-e2e/python, alef-docs: fix pre-commit hook failures from emitted file formatting.** Three alef generators emitted files that failed prek validation on every regen cycle: (1) `py.typed` and `__init__.py` in Python packages were emitted with empty or single-newline content, failing end-of-file-fixer when missing trailing newline; (2) e2e Python `pyproject.toml` lacked blank line between `[tool.setuptools]` and `[tool.ruff]` sections, failing pyproject-fmt; (3) API reference markdown (`docs/reference/api-*.md`) tables started immediately after headings without blank lines, failing rumdl MD058. Fix: (1) always emit text files with POSIX trailing newlines in `py.typed` (now `"\n"`) and implicitly via `__init__.py` emission; (2) ensure blank line before `[tool.ruff]` in pyproject.toml template for both Registry and Local modes; (3) add blank lines before all markdown tables in type/enum/error doc sections. All three backends now emit prek-compliant files on first gen, unblocking consumer regen cycles. Unit tests pass. (`crates/alef-scaffold/src/languages/python.rs`, `src/e2e/codegen/python/config.rs`, `crates/alef-docs/src/lib.rs`)

- **alef-backend-php: emit explicit getter methods on DTO readonly classes.** PHP DTOs with readonly classes expose scalar fields via `#[php(prop)]` making them PHP properties, but readonly class DTOs also need getter methods for the e2e tests which call methods like `$result->getContent()` rather than accessing properties directly. Previously, getter methods were only emitted for non-scalar fields (Named/Vec/Map/Json). Fix: emit getter methods for ALL fields (both scalar and non-scalar) in structs. Each field gets a `get_snake_case` method (e.g., `get_content`) that ext-php-rs surfaces as a callable PHP method (`getContent()`). This matches the e2e accessor dispatch which already expects method-call shape for all fields. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-csharp: emit `[MarshalAs(...)]` attributes on bool P/Invoke parameters.** P/Invoke signatures for `bool` parameters were emitted as bare `int` without marshalling attributes, causing C# compile failures with `cannot convert from 'int' to 'bool'` on every native function accepting a bool. Named DTO types were already correctly mapped to `IntPtr`. Fix: detect bool parameters and emit `[MarshalAs(UnmanagedType.U1)] bool` to match the C FFI convention. (`src/backends/csharp/gen_bindings/functions.rs`)

- **alef-e2e/elixir: emit positional args before keyword args to fix syntax error.** The Elixir e2e generator collected both positional and keyword args into a single vector in input order, producing calls like `function(pos, key: val, pos2)` which Elixir rejects. Fix: separate positional and keyword args into two vectors, then concatenate positional-first so all keyword args appear after positionals, matching Elixir's required syntax. (`src/e2e/codegen/elixir.rs`)

- **alef-codegen/alef-backend-kotlin: emit ktfmt-canonical KDoc comments for Kotlin code.** ktfmt collapses short KDoc comments to single-line format (`/** ... */`) when they fit within the 100-character line width limit. The generated Kotlin code was being formatted after generation, requiring a post-processing step through ktfmt on every `alef all --clean` cycle. New `emit_kdoc_ktfmt_canonical()` function in `alef_codegen::doc_emission` detects short, single-paragraph comments and emits them in single-line format directly, matching ktfmt's canonical form. Comments exceeding 100 chars or containing multiple paragraphs use the multi-line format. The Kotlin backend's `emit_cleaned_kdoc()` helper now uses the new canonical emitter instead of the plain `emit_kdoc()`, so all generated Kotlin files (enums, data classes, sealed classes, interface variants) now pass ktfmt without reformatting. Unit tests cover single-line vs. multi-line thresholds, indent preservation, real-world enum-variant and data-class-field examples, and exact 100-char boundary conditions. The html-to-markdown binding now regenerates with ktfmt-compliant Kotlin (40 .kt files, all enum variants and data-class fields now using collapsed single-line KDoc). (`src/codegen/doc_emission.rs`, `src/backends/kotlin/src/gen_bindings/helpers.rs`)

- **alef-backend-swift, alef-e2e/swift: prepend `// swift-format-ignore-file` to every alef-emitted Swift source so Apple's `swift-format` leaves them byte-identical.** Two regressions surfaced as one failed pre-commit hook on every regen: (1) the swift-bridge output that `emit_swift_bridge_files` copies into `packages/swift/Sources/RustBridge/` (`SwiftBridgeCore.swift`, `{binding}-swift.swift`) landed without the ignore marker, so the consumer repo's `swift-format` hook rewrote it on the first re-stage; (2) the e2e generator (`render_test_helpers_swift`, `render_test_file`) emitted 4-space-indented, XCTest-first-import, unwrapped-long-line Swift that swift-format aggressively reformats (2-space indent, alphabetised imports, 100-char wrap). Neither shape is something we control — swift-bridge owns the bridge output verbatim and the e2e generator uses inline `writeln!("    …")` calls that would require a templater rewrite to canonicalise. Mirror the existing fix in `gen_bindings.rs` for `HtmlToMarkdown.swift`: add the `swift-format-ignore-file` directive in both pathways. The shared `prepend_rust_bridge_c_import` helper now idempotently injects both `import RustBridgeC` and the directive (covering fresh swift-bridge output, previously-prepared files, and the two single-component recovery cases). The e2e backend gains a `SWIFT_FORMAT_IGNORE_DIRECTIVE` constant emitted right after the alef header in both `TestHelpers.swift` and every per-category `<Category>Tests.swift`. Pinned by 4 unit tests in `alef-backend-swift/src/gen_bindings.rs#tests` (idempotence × 4 combinations) and 4 generator tests in `alef-e2e/tests/swift_swift_format_ignore_header.rs` (directive presence on both file types, header positioning, exactly-once invariant). Unblocks the html-to-markdown `prek run --all-files` `swift-format` hook, which previously failed twice on every `alef all --clean` cycle. (`src/backends/swift/src/gen_bindings.rs`, `src/e2e/codegen/swift.rs`, `src/e2e/tests/swift_swift_format_ignore_header.rs`)
- **alef-backend-ffi: emit opaque-handle typedefs for every error type referenced by accessor functions in the generated C header.** `gen_cbindgen_toml` collected forward `typedef struct` declarations for struct types and enum types but not for error types. When an error type carried whitelisted methods (e.g. `status_code`), `gen_ffi_error_methods` emitted `extern "C" fn` accessors whose parameter type was `*const ErrorType` — cbindgen wrote those signatures to the header verbatim, but without a matching `typedef struct ErrorType ErrorType;`, the C compiler (and cgo) rejected the header with `unknown type name`. Fix: include every error type that has at least one whitelisted method in the `entries` list inside `gen_cbindgen_toml`, using the same `{PREFIX_UPPER}{Name}` naming convention as struct and enum forward declarations. (`src/backends/ffi/src/gen_bindings/helpers.rs`)
- **alef-codegen/error_gen: collapse multi-line rustdoc to single-line summary in `gen_go_error_struct`.** The `error_gen/go_error_struct.jinja` template emits `// {{ method_name }} returns {{ doc }}.` for each whitelisted error-introspection method (e.g. `StatusCode()`, `IsTransient()`, `ErrorType()`). When the underlying Rust rustdoc spanned multiple lines (paragraphs, bullet lists, fenced `# Examples`), the template substituted `m.doc` raw, so the continuation lines landed in the Go source unprefixed — producing `expected declaration, found Maps` and `illegal character U+0023 '#'` errors from `gofmt`/`golangci-lint`/`govulncheck` whenever the method's rustdoc had any body beyond the first paragraph (e.g. `GraphQLError::status_code` in spikard). Fix: in `gen_go_error_struct`, preprocess `m.doc` through `doc_first_paragraph_joined()` so only the first paragraph (joined into a single line) is handed to the template; trailing period is stripped because the template appends one. The full multi-line rustdoc body is intentionally dropped for the error-method getter docstring — it is preserved on the underlying method elsewhere. (`src/codegen/error_gen.rs`)
- **alef-backend-java: wrap `@Nullable` setter values with `Optional.ofNullable()` in Builder.** The Builder's private field for an optional record field is declared as `Optional<T>` (and defaulted to `Optional.empty()`), but the `withFoo(@Nullable T value)` setter assigned `this.foo = value` directly — Java rejected this with `incompatible types: T cannot be converted to Optional<T>` for every optional DTO field (DocstringInfo, ImportInfo, ProcessConfig, StructureItem, SymbolInfo, …). Fix: in `gen_builder_nested_class`, extend the visitor-field branch that already wraps with `Optional.ofNullable(value)` to also cover `field.optional` non-visitor fields. (`src/backends/java/gen_bindings/types.rs`)
- **alef-backend-go: fix signature/body mismatch in error-returning methods with scalar value-form returns.** Commit c428c191 changed `gen_method_wrapper` to emit a value-form return signature `(T, error)` for non-reference returns, but the body emitters still produced `*T`-shaped expressions and the error paths still emitted `return nil, err` — Go rejected this with `cannot use nil as T value` and `cannot use *T as T value`, breaking every affected method in the generated `binding.go` (downstream: tslp `packages/go/binding.go` failed `golangci-lint` and `govulncheck`). Fix: (1) narrow the scalar-signature condition in `gen_method_wrapper` to `TypeRef::Primitive(_) | TypeRef::Duration` so only true scalar types use `(T, error)` (Named/String/Json/etc. stay `(*T, error)` to match the still-pointer-form body emitters), (2) align `go_return_expr_inner` Primitive branch with the new signature (plain `T(var)` / `var != 0`) and special-case `Optional<Primitive>` to wrap with `func() *T { v := <expr>; return &v }()`, (3) add `go_zero_value` in `type_map.rs` returning the type-appropriate zero literal (`0`, `false`, `""`, or `nil`), (4) thread it through `gen_method_wrapper` (`err_prefix`) and `gen_param_to_c` (now takes an `err_return_prefix: &str` instead of a `bool` flag) so every `return <zero>, fmt.Errorf(...)` early exit matches the enclosing signature. `gen_streaming_method_wrapper` hardcodes `"nil, "` because streaming returns `(<-chan T, error)` and channels are nil-zero. Test calls updated. (`src/backends/go/src/gen_bindings/methods.rs`, `src/backends/go/src/gen_bindings/functions.rs`, `src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/src/type_map.rs`)
- **alef-backend-dart: replace unsound `Vec<MirrorT> → Vec<CoreT>` whole-collection transmute with element-by-element transmute in `Vec<Named>` bridge-fn lowering.** Clippy's `unsound_collection_transmute` lint denies the previous emission under `-D warnings` because `Vec`'s allocator metadata is not guaranteed to match across same-layout element types (Rust 1.95 explicitly forbids `transmute::<Vec<A>, Vec<B>>` even when `A` and `B` have identical layouts). Kreuzcrawl's `interact(actions: Vec<PageAction>)` bridge fn at `packages/dart/rust/src/lib.rs:1929` triggered the lint. Fix: emit `name.into_iter().map(|x| unsafe { std::mem::transmute::<MirrorT, CoreT>(x) }).collect::<Vec<_>>()` instead (and the `Option<Vec<…>>.map(|v| …)` analogue). Per-element transmute between identical-layout structs is sound and not flagged by clippy. The `&[Named]` raw-pointer transmute branch is unchanged because clippy doesn't flag single-pointer transmutes. (`src/backends/dart/src/gen_rust_crate/bridge_fn.rs`)
- **alef-backend-swift: silence `clippy::type_complexity` and `clippy::should_implement_trait` on generated swift-bridge streaming handles.** `emit_streaming_adapter_shims` emits `pub struct *StreamHandle { rt: ::tokio::runtime::Runtime, stream: ::std::sync::Mutex<Option<::futures_util::stream::BoxStream<'static, Result<T, Box<dyn Error + Send + Sync>>>>> }` followed by `impl *StreamHandle { pub fn next(&mut self) -> Result<String, String> }`. The nested type is required by swift-bridge's opaque-handle convention and the `next` method name deliberately mirrors `Iterator::next` for ergonomic parity but cannot implement the trait because the signature is fallible (`Result<String, String>`). Adding `#[allow(clippy::type_complexity)]` directly above the struct and `#[allow(clippy::should_implement_trait)]` above the `impl` block keeps `-D warnings` green without API changes. Snapshot updated. (`src/backends/swift/src/gen_rust_crate/wrappers.rs`, `src/backends/swift/tests/snapshots/snapshot_test__snapshot_streaming__packages__swift__rust__src__lib.rs.snap`)
- **alef-backend-dart: detect enums with FRB-flattened variants and emit `From<MirrorEnum> for CoreEnum` so bridge fns don'''t transmute.** The element-by-element `Vec<MirrorT> → Vec<CoreT>` transmute above only buys clippy-cleanliness when layouts coincide. FRB mirror enums strip `Option<T>` wrappers from variant fields (kreuzcrawl `PageAction::Scroll { selector: Option<String>, amount: Option<usize> }` becomes `Scroll { selector: String, amount: u32 }`), so the mirror variant is 64-128 bits shorter than core'''s. Wholesale transmute was runtime UB; per-element transmute fails at compile time with E0512. Fix: extend `types_with_direct_sanitized_fields` and `compute_types_containing_sanitized` in `gen_rust_crate/mod.rs` to walk enums alongside structs. Any enum whose variants contain `sanitized` fields, `optional` fields (the FRB-flattened ones), or Duration/Path fields is registered as needing From conversion. The transitive closure now propagates "contains layout-different enum" up through containing structs. Bridge-fn lowering then uses the existing `build_named_in_from` path which calls `CoreT::from(MirrorT)`, backed by the existing `emit_from_mirror_to_core_enum` (already invoked when an enum lands in `types_needing_from_impl`). One missing edge in `enum_variant_field_conv_to_core`: bool fields can'''t use `if x == 0` (Rust rejects `bool == {integer}`), so optional-bool now emits `if {binding} { Some({binding}) } else { None }`. (`src/backends/dart/src/gen_rust_crate/mod.rs`)
- **alef-backend-pyo3: include adapter-wrapper types in `api.py` imports and add `AsyncIterator` to typing imports.** `gen_api_py` collected type names from `api.functions` to compute the import block at the top of `api.py`, but `emit_adapter_wrapper` (the existing helper that emits `async def <adapter>(engine, req)`) references the adapter'''s owner type, item type, and param types as bare names — none of which were in `all_type_imports`. Result: ruff `--check` flagged F821 on every generated `api.py` (`Undefined name '''CrawlEngineHandle'''`, `'''CrawlStreamRequest'''`, `'''BatchCrawlStreamRequest'''`, `'''CrawlEvent'''`) and the module raised `NameError` at import time. Walk `adapters` alongside `api.functions` when building `all_type_imports`, and append `AsyncIterator` to the `from typing import …` line whenever any adapter exists so the return annotation resolves. (`src/backends/pyo3/gen_bindings/functions.rs`)
- **alef-e2e/python: import every Named `element_type` referenced by a call arg, not just the hardcoded `BatchBytesItem`/`BatchFileItem` pair.** The python e2e generator emitted `from kreuzcrawl import …` lines that hardcoded the only two batch-item types known at the time and dropped every other element type. kreuzcrawl'''s `interact` call config has `element_type = "PageAction"`; fixtures emitted `PageAction(...)` constructor calls but `PageAction` was never imported, so ruff flagged F821 on the entire `test_interaction.py` file. Replace the hardcoded allowlist with a primitive denylist (`str`/`int`/`float`/`bool`/`bytes`/`list`/`dict`/`tuple`/`Any`) so every non-primitive element type referenced by any fixture'''s resolved call gets imported. (`src/e2e/codegen/python/test_file.rs`)
- **alef-backend-java: place `@Nullable` at the simple-name segment of a fully-qualified type in Builder setters.** Java'''s type-use annotation grammar rejects `@Nullable java.nio.file.Path` because the annotation precedes the package prefix; the legal form is `java.nio.file.@Nullable Path`. The record-field declaration emitter already handled this via `nullable_at_leading_pos`, but `gen_builder_nested_class`'''s `with{Field}` setter wrote `body.push_str("@Nullable ")` directly before `field_type` regardless of qualification, breaking `mvn verify` on every record with an optional `PathBuf` (e.g. `kreuzcrawl::CrawlConfig.warc_output`'''s `withWarcOutput(@Nullable java.nio.file.Path value)`). Mirror the record-field logic: split on the last `.`, insert `.@Nullable` before the simple name; fall back to the leading-position form for unqualified types. (`src/backends/java/gen_bindings/types.rs`)
- **alef-e2e/ruby: add missing `HashSet` import so `cargo build --release` (and `cargo install`) compile.** `render_assertion` / `render_call_assertions` in `src/e2e/codegen/ruby.rs` accept `&HashSet<String>` parameters but the file only imported `HashMap`. `cargo check` accepted this on the cold path through generic inference, but `cargo build` (which `cargo install --path crates/alef-cli` runs in release mode) failed with E0425 (`cannot find type '''HashSet''' in this scope`). Extend the import to `use std::collections::{HashMap, HashSet};`. (`src/e2e/codegen/ruby.rs`)
- **alef-backend-php: emit `{` opening brace after trait-bridge facade method signatures.** The PHP facade emitter at `gen_bindings/mod.rs` lines 1109, 1154, 1191 wrote the function signature (`): void`) followed directly by the call statement, with no opening brace before the body. Result: PHP syntax error on every register_*, unregister_*, clear_* method, breaking the entire generated `Kreuzberg.php` file. Fix: append `\n    {\n` after each signature string so the method body opens correctly. (`src/backends/php/src/gen_bindings/mod.rs`)
- **alef-backend-php: call camelCase method on `KreuzbergApi` in trait-bridge facade delegation.** The PHP facade's call expression for register/unregister/clear delegated to `\Kreuzberg\KreuzbergApi::{snake_case_fn}(...)`, but ext-php-rs exposes the underlying methods in camelCase (`registerOcrBackend`, `clearValidators`, ...). PHP raised `Call to undefined method` for every trait-bridge facade method. Fix: substitute `{method_name}` (already camel-cased via `to_lower_camel_case()`) into the `call_expr` at lines 1117, 1162, 1199 so the facade calls the actual method name. (`src/backends/php/src/gen_bindings/mod.rs`)
- **alef-backend-pyo3: escape Python `str` method-name collisions in StrEnum variant names.** `StrEnum` inherits from `str`, so member names that match `str` instance methods (e.g. `Title` → `title`, shadowing `str.title()`) produced mypy `[assignment]` errors at the class body. The escape helper `alef_core::keywords::python_ident()` only protected against Python reserved keywords (`del` → `del_`), not against `str` method-name collisions. Add `PYTHON_STR_METHODS` constant listing all 40+ `str` instance methods, and a new `python_str_enum_ident()` helper that escapes both keywords AND method-name collisions with a trailing underscore. Update the pyo3 backend's `to_python_enum_variant()` function in both `gen_bindings/types.rs` and `gen_stubs.rs` to use `python_str_enum_ident` instead of `python_ident`, so the emitted member is `title_ = "title"` instead of `title = "title"`. (`src/core/keywords.rs`, `src/backends/pyo3/gen_bindings/types.rs`, `src/backends/pyo3/gen_stubs.rs`)

### Added

- **alef-core, alef-e2e: per-call assertion metadata (`result_fields`, `fields`, `fields_optional`, `fields_array`, `fields_method_calls`, `fields_enum`, `fields_c_types`).** Previously these seven maps/sets lived only on `E2eConfig` and were implicitly ScrapeResult-shaped (matching the default `[e2e.call]` for kreuzcrawl). When a consumer wanted to route fixtures to a non-default named call returning a different result type (e.g. `crawl` returning `CrawlResult`, `map_urls` returning `MapResult`, `interact` returning `InteractionResult`), every assertion referencing a field absent from the global `result_fields` allowlist was emitted as a commented-out skipped assertion — effectively destroying coverage on the routed-to call. Each of the seven config items is now also accepted on `CallConfig`, with seven matching `E2eConfig::effective_*(call)` resolver helpers that return the per-call override when non-empty else the global default. The ~70 codegen call sites across all per-language backends now consult the resolver against the resolved-for-fixture `CallConfig`, so assertions for the `crawl_basic` fixture (routed to the `crawl` named call) see CrawlResult's field allowlist while assertions for `scrape_basic` (default call) still see the global ScrapeResult allowlist. 14 new unit tests in `alef-core/src/config/e2e.rs` pin each resolver's fallback semantics and the TOML deserialization shape for the per-call fields. `alef-core` (384/384) and `alef-e2e` (247/247) test suites pass. Unblocks downstream consumers from authoring `[crates.e2e.calls.<name>.result_fields]` to scope assertion validity per call. (`src/core/config/e2e.rs`, every file under `src/e2e/codegen/`, snapshot updates in `src/backends/swift/tests/snapshots/`, ergonomic helpers in `src/backends/go/`, `src/backends/java/`, `src/backends/php/`, `src/backends/pyo3/`, `src/backends/wasm/`, `src/backends/dart/`, `src/codegen/error_gen.rs`)

- **alef-backend-pyo3: emit module-level wrapper functions for adapter-based streaming methods in the Python facade.** `crates/kreuzcrawl-py/src/lib.rs:2248` defines `crawl_stream` and `batch_crawl_stream` as PyO3 methods on `CrawlEngineHandle`, but the generated `packages/python/kreuzcrawl/api.py` only re-exported plain functions (scrape/crawl/etc.) — streaming methods were dropped on the floor because `gen_api_py` iterated `api.functions`, not `config.adapters`. The new `emit_adapter_wrapper` helper (called from `gen_api_py` after the regular function loop) emits an `async def <adapter_name>(engine, req)` for each `[[crates.adapters]]` entry, with the canonical async-for-loop body that yields items from the underlying engine method. `gen_init_py` now extends `imports_from_api` and `__all__` to include adapter names so `from kreuzcrawl import crawl_stream, batch_crawl_stream` resolves. New test `test_adapter_wrapper_functions` in `src/backends/pyo3/tests/gen_bindings_test.rs` pins the emission shape (signature, async-for-loop body, `__init__.py` import + `__all__` entry). The same emitter naturally serves every downstream adapter (kreuzberg streaming would behave the same). (`src/backends/pyo3/gen_bindings/functions.rs`, `src/backends/pyo3/gen_bindings/errors.rs`, `src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-core: extend `SelectWhen` from single-variant enum to ANDed-discriminator struct.** `SelectWhen` in `src/core/config/e2e.rs` was an enum with only `InputHas(String)`, meaning the only way to auto-route a fixture to a named call was by checking whether its input contained a specific key. Consumers like kreuzcrawl (8 public API methods: scrape / crawl / map_urls / interact / batch_scrape / batch_crawl / crawl_stream / batch_crawl_stream) cannot route by input shape alone — `crawl` and `scrape` fixtures both have a `url` field. `SelectWhen` is now a struct with five optional discriminator fields (`category`, `id_prefix`, `id_glob`, `tag`, `input_has`) that are ANDed — every set field must match for the call to fire. A `SelectWhen` with no fields set never matches (no accidental greedy match). The legacy TOML syntax `select_when = { input_has = "batch_urls" }` continues to deserialize unchanged. New compound syntax: `select_when = { category = "stream", id_prefix = "batch_crawl_stream" }` routes a fixture iff its resolved category is `stream` and its id starts with `batch_crawl_stream`. `id_glob` supports a minimal `*` glob for prefix/suffix/contains-style matches. `resolve_call_for_fixture` now takes the fixture's `id`, resolved `category`, `tags`, and `input` (signature change propagated to ~70 call sites across the alef-e2e per-language codegens) and iterates named calls in sorted-by-name order so routing is deterministic. Nine new unit tests in `alef-core/src/config/e2e.rs` cover each discriminator individually, the AND composition, legacy backward compat, and the alphabetical-iteration fallback to `[e2e.call]`. Existing `select_when_routes_to_batch_scrape` tests in alef-backend-java and alef-backend-csharp continue to pass against the new struct literal. (`src/core/config/e2e.rs`, `src/e2e/codegen/*.rs`)

- **alef-extract: support cfg-gated generic functions (embed_texts_async).** Added a snapshot test confirming that `pub async fn embed_texts_async<T: AsRef<str> + Send + 'static>(texts: Vec<T>, config: &EmbeddingConfig) -> Result<Vec<Vec<f32>>, E>` is extracted and monomorphized correctly — `Vec<T>` becomes `Vec<String>`, the `'static` lifetime bound on the type parameter is accepted, and the `#[cfg(all(...))]` gate is preserved in the IR `cfg` field. The extractor's `can_monomorphize_to_string` already handled this shape; the test documents and pins the behaviour so downstream fixtures in kreuzberg can drop their global language skip. (`src/extract/extractor/tests.rs`)

### Fixed

- **alef-backend-swift: emit `emit_streaming_free_functions` for orphan streaming adapters whose owner type has no Swift client class wrapper.** `CrawlEngineHandle` in kreuzcrawl has no `client_constructor_body` in `alef.toml`, so `emit_client_class` is never called for it, meaning `emit_streaming_client_method` never runs and `crawlStream`/`batchCrawlStream` were absent from the emitted `Kreuzcrawl.swift`. The new `emit_streaming_free_functions` pass collects all `AdapterPattern::Streaming` adapters whose `owner_type` is not in `client_constructor_types`, emits `@unchecked Sendable` conformances for the owner type and stream-handle type (Swift 6 strict-concurrency requires this for `Task.detached` captures backed by Rust `Send + Sync` types), then emits `public func {method}(_ {ownerCamel}: {OwnerType}, _ req: {ParamType}) async throws -> AsyncThrowingStream<{ItemType}, Error>` free functions. For first-class struct params (Codable structs with `intoRust()`), the call-arg is wrapped in `try {param}.intoRust()`. Also fixed `emit_streaming_client_method` to apply the same `intoRust()` wrapping for first-class struct params in the class-method code path. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-scaffold/swift: use file mtime of `SwiftBridgeCore.h` instead of directory mtime in `read_swift_bridge_headers`.** macOS updates a directory's mtime whenever its contents are read (directory-entry cache invalidation), making `find_swift_bridge_out_dir` / `read_swift_bridge_headers` pick a non-deterministic build-output directory across `alef all --clean` runs — causing `packages/swift/Sources/RustBridgeC/RustBridgeC.h` to be rewritten with content from a different build on every regen cycle. Fix: replace `std::fs::metadata(&out)` (directory) with `std::fs::metadata(&core_h)` (the `SwiftBridgeCore.h` file itself, written by `cargo build`). Cargo only rewrites build artifacts when the build runs, so the file mtime is stable across `alef` invocations that don't trigger a Rust build. (`crates/alef-scaffold/src/languages/swift.rs`)

- **alef-backend-swift: prepend `// swift-format-ignore-file` to `emit_inbound_box_files` output.** `emit_inbound_box_files` generated `Swift{Trait}Box.swift` files (e.g. `SwiftHtmlVisitorBox.swift`) without the `// swift-format-ignore-file` pragma, so Apple's `swift-format` pre-commit hook rewrote them on every regen cycle. The main facade file (`Kreuzcrawl.swift`, `HtmlToMarkdown.swift`) and the swift-bridge output files (via `prepend_rust_bridge_c_import`) already carry the directive. Fix: add `content.push_str("// swift-format-ignore-file\n")` immediately after the generated-by header in `emit_inbound_box_files`. Insta snapshot for `snapshot_trait_bridge_inbound_options_field__Sources__RustBridge__SwiftHtmlVisitorBox.swift` updated to include the new second line. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-go: skip accessor method when its name collides with a struct field on error types.** The `error_gen/go_error_struct.jinja` template emitted both a struct field (e.g. `StatusCode uint16`) and a receiver method with the same name (e.g. `func (e T) StatusCode() uint16`) for every whitelisted introspection property. Go forbids a field and method sharing a name on the same type — `go vet` reports the collision and the method body references the field by name, creating a recursive call that never compiles. Fix: remove the method block from the template entirely; the exported struct field serves as the public accessor under Go's value-semantics convention. New unit test `test_gen_go_error_struct_no_field_method_collision` pins the absence of accessor methods for any property name. (`src/codegen/templates/error_gen/go_error_struct.jinja`, `src/codegen/error_gen.rs`)

- **alef-backend-pyo3: emit `raise NotImplementedError` body for `@property` stubs on Python exception classes so mypy `empty-body` no longer fires.** `ruff check --fix` applies PIE790 ("Unnecessary `...` literal"), which removes a bare `...` that immediately follows a docstring, leaving the `@property` with an empty body. mypy then rejects the file with `error: Missing return statement [empty-body]`. Replacing `...` with `raise NotImplementedError` in all three whitelisted introspection property stubs (`status_code`, `is_transient`, `error_type`) satisfies both ruff (PIE790 does not fire) and mypy (body is non-empty). New unit test `gen_exceptions_py_property_stubs_use_raise_not_implemented_error` asserts all three properties use `raise NotImplementedError` and contain no `        ...\n` body. (`src/backends/pyo3/gen_bindings/errors.rs`)

- **alef-e2e/field_access: strip virtual namespace prefixes in `is_valid_for_result` and `accessor`.** Fixture paths like `browser.browser_used`, `interaction.action_results[0].action_type`, and `crawl.pages_crawled` were emitted as `// skipped: field … not available on result type` comments because `is_valid_for_result` checked only the first path segment (`browser`, `interaction`, `crawl`) against the configured `result_fields` set, where those prefixes were absent. These prefixes are organizational labels used in kreuzcrawl-style fixtures (and other consumers) to group related assertions; they do not correspond to real struct fields. Fix: `is_valid_for_result` now additionally strips a single leading non-bracket segment when that segment is absent from `result_fields` and re-checks whether the remainder's first segment IS present — accepting `browser.browser_used` when `browser_used` is in `result_fields`. The `accessor` method applies the same strip before generating the language-specific expression, so the produced code accesses `result.browser_used` / `result.BrowserUsed` / etc. rather than the non-existent `result.browser.browser_used`. Eight new unit tests cover the strip (Python and C# accessors, indexed array paths, the non-strip guard for real first-segments, and the permissive-when-result_fields-empty invariant). (`src/e2e/field_access.rs`)

- **alef-docs: fix heading hierarchy in generated API reference docs (rumdl MD001/MD025).** Methods within types were emitted at incrementing heading levels (`#### `, `##### `, `###### `) instead of all at the same level. Type doc comments embedding Rust `#` sections were rendered verbatim without demotion, breaking the hierarchy. Fix: add `demote_headings(doc, levels)` to demote markdown headings in doc comments by N levels (respecting code blocks). Demote type and method docs by 2 levels when rendering at H4 to preserve relative structure. Change Methods section from H5 to H3 so all methods are siblings at H4. (`crates/alef-docs/src/doc_cleaning.rs`, `crates/alef-docs/src/lib.rs`)

- **alef-extract: keep original name on non-alef(skip) entry when disambiguating colliding types.** `compute_renames()` sorted colliding entries by `rust_path` alone, so a `binding_excluded=true` entry (from `#[cfg_attr(alef, alef(skip))]`) that sorted first alphabetically would claim the original name while the legitimate type received a numeric suffix (`EmbeddingPreset2`, `NodeContent2`, etc.). Fix: sort by `(binding_excluded ASC, rust_path ASC)` so non-excluded entries always sort first and keep the original name. The bx=true entry is renamed (and subsequently filtered out), making the rename invisible in downstream IR. New test `bx_true_entry_yields_original_name_to_bx_false_entry` pins the behaviour. (`src/extract/extractor/disambiguation.rs`)

- **alef-backend-magnus: replace dynamic re-export pattern with explicit filtered lists in native.rb wrapper.** The `native.rb.jinja` template previously used `{{ native_module_name }}.methods(false).each` and `{{ native_module_name }}.constants.each` to re-export all native extension symbols into the Ruby public module surface. This leaked internal Rust naming (`HtmlToMarkdownRs`), internal partial-update structs (`ConversionOptionsUpdate`, `PreprocessingOptionsUpdate` — meant only for intra-binding serialization, not public API), and builder types when none existed. The fix generates explicit curated lists at codegen time: `public_types` excludes opaque types, trait types, excluded types, and any type ending with `Update` or `Builder`; `public_functions` excludes reserved functions and excluded functions. The template now iterates these explicit lists and emits direct constant assignments and method delegations, ensuring only intended public surface is exposed. A unit test `test_explicit_re_export_list_filters_internal_types` pins the emission: asserts no dynamic pattern is used, confirms expected public types appear, and confirms internal types are filtered. (`src/backends/magnus/gen_bindings/mod.rs`, `src/backends/magnus/templates/native_rb_wrapper.rb.jinja`, `src/backends/magnus/tests/gen_bindings_test.rs`)

- **alef-backend-magnus: filter enums from public re-export list in generate_public_api.** Magnus backend does not register enums as module-level constants in `gen_module_init` (only struct types are registered via `#[magnus::wrap]`). The `generate_public_api` function was including enums in `public_types`, causing generated `native.rb` to call `HtmlToMarkdownRs.const_get(:AnnotationKind)` for enum types that do not exist on the native module, raising `NameError: uninitialized constant`. Fix: exclude enums from the `public_types` collection, iterating only non-opaque, non-trait struct types. Updated test `test_explicit_re_export_list_filters_internal_types` to assert that enums are also filtered from re-export. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-kotlin-android: honour `alef(skip)` annotations in DTO and enum emission.** The Kotlin source emitter iterated `api.types` and `api.enums` filtering only `is_opaque`/`is_trait`, ignoring the `binding_excluded` IR flag set by `#[cfg_attr(alef, alef(skip))]` (or `#[doc(hidden)]`) on the upstream Rust source. As a result, skipped types and enums still produced `.kt` wrapper files (e.g. `OcrTesseractConfig.kt`, `Table2.kt`, `OcrPipelineConfig.kt` in kreuzberg's Android package), even though every other backend (PHP, WASM, NAPI, etc.) already filtered them out. Skip both loops on `binding_excluded` to bring the kotlin-android backend in line with the rest of the polyglot surfaces. (`src/backends/kotlin-android/gen_bindings.rs`)
- **alef-backend-dart: replace unsound raw-pointer transmute in mirror error introspection with safe variant-by-variant `From` conversion.** The `emit_mirror_error` function previously delegated introspection methods (`status_code`, `is_transient`, `error_type`) by casting `self` from `&MirrorEnum` to `&RealEnum` via raw pointer. This is unsound: the mirror uses FRB-widened types (`i64` for all integers, `i64` for `Duration`) while the real type uses native widths (`u16`, `u64`, `Option<Duration>`), so the pointer cast reads garbage bytes at runtime despite `cargo check` passing. The `SAFETY` comment was also incorrect — `#[frb(mirror(T))]` is a codegen directive, not a layout guarantee. Fix: emit an `impl From<&MirrorEnum> for RealType` that reconstructs each variant field-by-field with explicit casts (`*f_status as u16`, `Duration::from_millis(*f_retry_after as u64)`, `Some(f_model.clone())` for optional-in-real-but-bare-in-mirror fields). Variants with sanitized fields (types erased to `String` whose originals cannot be recovered, e.g. `serde_json::Error`) are skipped and covered by an `unreachable!` wildcard arm. Introspection methods now use `let real: CorePath = self.into(); real.method()` — fully safe, no `unsafe` block. (`src/backends/dart/src/gen_rust_crate/mirror.rs`)

- **alef-backend-pyo3: exclude `*Builder` types from emission and emit enum variants in snake_case.** The pyo3 backend was registering `ConversionOptionsBuilder` and other internal Rust-ergonomics builder types as `#[pyclass]` items, exporting them via the `.pyi` stubs. Builder is a Rust idiom; Python callers construct dataclass-style with kwargs and don't need a fluent builder. Extended the existing `Update`-suffix exclusion to also drop any type whose name ends with `Builder`. Separately, enum variants were emitted in PascalCase requiring a runtime `_pascal_to_snake` / `_coerce_enum` shim in the generated `api.py`. The backend now emits enum variants in snake_case directly (via `heck::ToSnakeCase`), matching idiomatic Python enums (`HeadingStyle.atx` instead of `HeadingStyle.Atx`), so the runtime translation shim can be removed from generated code. (`src/backends/pyo3/gen_bindings/errors.rs`, `src/backends/pyo3/gen_bindings/types.rs`, `src/backends/pyo3/gen_bindings/gen_stubs.rs`)

- **alef-scaffold (rustler/magnus/extendr): emit cargo-sort-canonical `[dependencies]` in generated Cargo.toml files.** The Rustler (Elixir), Magnus (Ruby), and extendr (R) scaffold Cargo.toml emitters appended optional dependencies (`async-trait`, `tokio`, `futures`, `futures-util`) as string suffixes after the base deps rather than collecting all deps into a Vec, sorting alphabetically, and joining. The `prek run --all-files` `cargo-sort` hook rewrote these three files on every `alef all --clean` cycle. Fix: each scaffold emitter now builds a `Vec<String>` of all dependency entries, calls `.sort()`, and joins them before interpolating into the Cargo.toml template. Extra deps from `render_extra_deps` are deduplicated before insertion. Five unit tests added to `alef-scaffold/src/tests.rs` (`dep_keys_in_order` helper + tests for each backend with and without trait-bridge flags) assert that the emitted `[dependencies]` keys are alphabetically ordered. (`crates/alef-scaffold/src/languages/elixir.rs`, `crates/alef-scaffold/src/languages/ruby.rs`, `crates/alef-scaffold/src/languages/r.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-scaffold, alef-e2e/python: emit pyproject.toml in pyproject-fmt canonical form.** Generated `packages/python/pyproject.toml`, `e2e/python/pyproject.toml`, and per-language Python scaffold templates were emitting non-canonical TOML formatting: `requires = [ ... ]` came before `build-backend`, keywords were unsorted, nested table sections (`[tool.ruff.lint]`) were used instead of dot-syntax (`lint.select = ...`), and `[project.urls]` sections used bracket notation instead of dot-syntax (`urls.repository = ...`). The `prek run pyproject-fmt --all-files` hook would rewrite these files on every commit, creating friction during CI and local development. Fixes: (1) In `alef-scaffold`, reorder `[build-system]` to emit `build-backend` before `requires`, wrap keywords in sorted-alphabetically order, output `urls.repository` in dot-syntax, and flatten `[tool.ruff]` nested sections into dot-notation (`lint.select`, `lint.mccabe.max-complexity`, etc.). (2) In `alef-e2e/python`, apply identical formatting to the runtime-generated e2e `pyproject.toml` and sort dependencies alphabetically. (3) Add unit tests `test_scaffold_python_pyproject_canonical_format` and `render_pyproject_canonical_format_*` to pin the canonical output form (spaces in arrays, sorted lists, dot-syntax for nested tables, build-backend before requires). Output is now idempotent with respect to `pyproject-fmt`. (`crates/alef-scaffold/src/languages/python.rs`, `crates/alef-scaffold/src/tests.rs`, `src/e2e/codegen/python/config.rs`, `src/e2e/templates/python/pyproject.toml.jinja`)

- **alef-backend-napi: filter `*Builder` and `*Update` types from `#[napi]` class emission.** TypeScript `.d.ts` exposed `JsConversionOptionsBuilder`, `JsConversionOptionsUpdate`, `JsPreprocessingOptionsUpdate` — internal Rust ergonomics types with no idiomatic JS counterpart. JS callers use object literals and `Partial<ConversionOptions>` for partial overrides. Added a name-suffix filter alongside the existing `is_trait`/`exclude_types` checks at the struct emission gate so neither Builder nor Update structs are registered with napi. (`src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-backend-go: scalar `IsValid` returns `(bool, error)` not `(*bool, error)`; skip `*Default` free functions; sanitize Rust syntax in Go doc examples.** Three Rust-leakage bugs in the Go backend: (1) the FFI return-type mapping for scalar booleans wrapped them in pointer-then-Option, producing un-idiomatic `(*bool, error)` signatures for instance validation methods. Map scalars directly to `(bool, error)`. (2) `ConversionOptionsDefault()` and `PreprocessingOptionsDefault()` were emitted as free functions — the Rust `Default::default()` idiom doesn't translate to Go; idiomatic Go uses zero-value struct literals or constructor functions. Skip method names ending in `_default`. (3) Go doc comments contained Rust code examples (`use ...::convert;`, `.unwrap()`); replaced with Go syntax via the shared rustdoc summary-extraction path. (`src/backends/go/src/gen_bindings/methods.rs`, `src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/src/gen_bindings/mod.rs`)

- **alef-backend-csharp: skip `is_valid` and `default` methods in both FFI-function and type-method emission loops.** The C# backend emitted `HeaderMetadataIsValid(IntPtr handle)`, `ConversionOptionsDefault()`, `PreprocessingOptionsDefault()` as static methods on the facade class — Rust idioms with no C# equivalent. Idiomatic C# uses instance methods (`meta.IsValid()`) and parameterless constructors (`new ConversionOptions()`). Added a `should_skip_ffi_method` predicate matching the Rust snake_case `FunctionDef.name` field (`is_valid`, `_default` suffix). Applied to both the standalone function emission loop AND the per-type method emission loop — the second loop was missed in the initial fix, causing the methods to keep appearing under their type's impl block. (`src/backends/csharp/gen_bindings/methods.rs`)

- **alef-backend-rustler: remove Rust-idiom free functions, add `valid?/1` instance method on type module, add `@derive Jason.Encoder` to option structs.** The Elixir binding leaked Rust-isms: `headermetadata_is_valid/1`, `conversionoptions_default/0`, `preprocessingoptions_default/0` as top-level `HtmlToMarkdown` module functions. Elixir idiom is the `%ConversionOptions{}` struct literal and instance-style `HeaderMetadata.valid?/1`. Three changes: (1) skip `*_default` and `*_is_valid` free-function emission in the rustler backend. (2) Emit `def valid?(struct)` on the type's per-module file (`packages/elixir/lib/html_to_markdown/header_metadata.ex`) by detecting `is_valid` methods on type defs and routing them through the dedicated module template instead of the top-level facade. (3) Emit `@derive Jason.Encoder` on every option struct so consumers can pass struct literals through `Jason.encode!` (used by `HtmlToMarkdown.convert(html, options)` to forward to the NIF). (`src/backends/rustler/src/gen_bindings/mod.rs`, `src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/templates/per_type_module.jinja`)

- **alef-e2e elixir codegen: emit struct-literal `%ModulePath.OptionsType{...}` when `options_type` is set but `options_via` is unset.** Paired with the rustler `*_default` removal: previously when `options_via` was unset the codegen fell through to passing a raw JSON string, but the new high-level `HtmlToMarkdown.convert(html, options)` expects a map (which a struct-with-`@derive Jason.Encoder` satisfies). New branch emits idiomatic Elixir struct-literal syntax with enum atoms (`heading_style: :atx`). (`src/e2e/codegen/elixir.rs`)

- **alef-backend-extendr: emit S3 generic wrappers and `S3method()` directives in NAMESPACE.** The R binding exposed only the raw `extendr-wrappers.R` env-class accessors (`meta$is_valid`), un-idiomatic for R callers who expect `is_valid(meta)` dispatching on `class(meta)`. Added per-method S3 generic + dispatcher emission (`r_s3_generic.jinja`, `r_s3_method.jinja`) and a NAMESPACE pass that writes `S3method(is_valid, HeaderMetadata)` entries so the generics are exported and visible to package consumers. (`src/backends/extendr/src/gen_bindings.rs`, `src/backends/extendr/src/template_env.rs`, `src/backends/extendr/templates/r_s3_generic.jinja`, `src/backends/extendr/templates/r_s3_method.jinja`)

- **alef-backend-php: sanitize Rust syntax in facade-method PHPDoc summaries.** The PHP class facade copied raw rustdoc verbatim into PHPDoc comments, leaking `use html_to_markdown_rs::convert;`, `.unwrap()`, `::default()`, `# Arguments`/`# Returns`/`# Errors` markers. Switched to `doc_emission::parse_rustdoc_sections()` to extract only the summary; arguments/returns/errors are rendered as proper `@param`/`@return`/`@throws` tags after the summary. Includes a regression test verifying no Rust syntax leaks through. (`src/backends/php/src/gen_bindings/mod.rs`, `src/backends/php/tests/gen_bindings_test.rs`)

- **alef-backend-java: builder fluent setters accept `@Nullable T` instead of `Optional<T>`; centralize `fromJson` via shared `JsonUtil`; sanitize Rust syntax in JavaDoc.** Three Java-idiom fixes: (1) builder setters had `Optional<String> summary(Optional<String> summary)` — forcing callers to write `Optional.of(...)`. Idiomatic Java accepts the nullable directly and annotates with `@Nullable` (extended `needs_nullable` detection to cover builder method parameters). (2) Every DTO had a static `fromJson(String json)` boilerplate method using its own `ObjectMapper`. Centralized to a single `dev.kreuzberg.{crate}.JsonUtil` class with `<T> T fromJson(String json, Class<T> cls)` and shared mapper; per-DTO method emission removed. (3) JavaDoc contained Rust syntax (`::`, `.unwrap()`, `.expect(...)`); added `sanitize_rust_syntax()` to the JavaDoc escaper translating `::` → `.`, dropping `.unwrap()`/`.expect()`, and rewriting `.unwrap_or(x)` → `.orElse(x)`. (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/gen_bindings/mod.rs`, `src/backends/java/gen_bindings/helpers.rs`, `src/backends/java/template_env.rs`, `src/backends/java/templates/json_util.jinja`, `src/backends/java/tests/gen_bindings_test.rs`)

- **alef-backend-wasm: strip `Wasm` type prefix in JS, replace static `default()` with zero-arg constructor, eliminate `#[allow(non_snake_case)]` workaround.** The wasm binding emitted `WasmConversionOptions`, `WasmHeaderMetadata`, etc. into the generated `.d.ts` — un-idiomatic JS where the language already namespaces via module. Added `js_name = "{{ unprefixed_name }}"` to the `gen_struct.jinja` template so the JS-facing name drops the prefix while the Rust struct retains it for internal disambiguation. Replaced the multi-param `gen_new_method()` (which required a `#[allow(non_snake_case)]` to accept camelCase params) with a zero-arg `new()` returning `Default::default()`, paired with per-field setters; this also makes `static default()` redundant — removed both `gen_default_method()` and its IR-extraction entry. (`src/backends/wasm/gen_bindings/types.rs`, `src/backends/wasm/templates/gen_struct.jinja`)

## [0.17.0] - 2026-05-19

### Added

- **alef-backend-kotlin-android / alef-backend-jni / alef-backend-kotlin**: emit trait-bridge plumbing for every `[[crates.trait_bridges]]` entry so the Android AAR has the same plugin-registration API surface as the JVM Panama backend. The Kotlin JNI bridge object gains one `@Throws external fun nativeRegister<Trait>(impl: <pkg>.I<Trait>)`, `external fun nativeUnregister<Trait>(name: String)`, and `external fun nativeClear<Trait>s()` declaration per configured bridge; a paired `I<Trait>.kt` interface file is emitted alongside the bridge (with canonical `name`/`version`/`initialize`/`shutdown` lifecycle methods when the trait has a super-trait, plus one suspend/normal `fun` per IR method). The Rust JNI shim crate now emits matching `Java_*` extern functions — `nativeUnregister<Trait>` and `nativeClear<Trait>s` delegate to the configured host `unregister_fn`/`clear_fn`, while `nativeRegister<Trait>` is a documented placeholder that throws "not yet implemented" pending the upcall trampoline work. Bridges that list `kotlin_android` in `exclude_languages` are honoured by both emitters. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`, `src/backends/kotlin-android/gen_bindings.rs`, `src/backends/jni/src/gen_shims.rs`)

### Fixed

- **alef-backend-napi: emit `From<binding> for core` impls for nested DTO types and unit-variant enums.** When a DTO appeared only as a field in output structures (not as a direct input/return type or enum variant payload), the binding->core conversion code would emit `.into()` calls without corresponding `impl From<binding> for core`, causing E0277 errors for types like `DbfFieldInfo`, `DbfMetadata`, etc. appearing in `Vec<DbfFieldInfo>` within tagged-enum variants. Similarly, unit-variant enums appearing in optional fields (e.g., `Option<TextDirection>`) needed From impls but weren't generated because they weren't input types. Add two passes: first iterate variant fields of tagged enums and emit From impls; second iterate all remaining convertible types; third collect enums in fields of already-emitted types and emit their From impls. Deduplicates against already-emitted types to prevent conflicting implementations. (`src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-backend-php: emit `From<binding> for core` impls for nested DTO types.** When a DTO appeared only as a field in output structures (not as a direct input/return type or enum variant payload), the binding->core conversion code would emit `.into()` calls for fields of that type, but no corresponding `impl From<binding> for core` would be generated, causing E0277 compile errors (`the trait bound 'Type: From<binding>' is not satisfied`). Affected types: `GridCell`, `CoreProperties`, `DocxAppProperties`, `YearRange`, `StructuredData`, `LinkMetadata`, `ImageMetadataType`, `HeaderMetadata`, `DbfFieldInfo`, `ContributorRole`, and others. Add a second pass after the main conversion loop and tagged-enum variant-data loop that iterates all remaining convertible types and emits their From impls, deduplicating against the set of already-emitted types. (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-backend-dart: emit error types as mirrored Rust enums with introspection method bridges.** When the IR contained an `ErrorDef` (e.g. `LiterLlmError`), the dart backend's `gen_rust_crate` emitter never iterated `api.errors`, so the generated `packages/dart/rust/src/lib.rs` had no `#[frb(mirror(ErrorName))]` enum declaration. flutter_rust_bridge requires a mirrored enum to generate the Dart sealed class and its per-variant subclasses. Add `emit_mirror_error` to `gen_rust_crate/mirror.rs`: emits the `#[frb(mirror(T))]` enum with all variants (unit and data), then an `impl T { #[frb] pub fn method(&self) ... }` block for each whitelisted introspection method (`status_code`, `is_transient`, `error_type`). The body casts `self` to the core type via raw pointer (sound under the FRB mirror-layout invariant) and calls the core method directly, applying `.to_string()` for `&str`→`String` coercions and `as i64`/`as f64` for narrow-integer/float widening so the declared FRB return type is satisfied. Wire the call into `emit_lib_rs` after enum emission. (`src/backends/dart/src/gen_rust_crate/mirror.rs`, `src/backends/dart/src/gen_rust_crate/mod.rs`)

- **alef-backend-dart: stub bridge fns whose return type was sanitized from a Named core type.** When a public function's return type is a Named struct that the Dart binding cannot expose (e.g. `kreuzberg::get_embedding_preset(...) -> Option<EmbeddingPreset>` where the core struct is not in the Dart API surface), the extractor sanitizes the return to `Option<String>` but the bridge body kept emitting `core_fn(...).map(|s| s.to_string())`. The core type does not implement `Display`, so the emitted crate failed to compile with E0599. Detect `FunctionDef::return_sanitized` in `emit_bridge_fn` and emit a default-value stub body matching the sanitized return shape (`None` for `Option<_>`, `String::new()` for `String`, `Vec::new()` for `Vec<_>`, `Default::default()` for Named/Map/Json, etc.), mirroring the existing PyO3/NAPI behaviour where sanitized returns become unimplemented stubs. The function remains callable through the binding surface but returns the zero value, since the Named core type is not visible to Dart callers. (`src/backends/dart/src/gen_rust_crate/bridge_fn.rs`)

- **alef-backend-php: dedup `From<binding> for core` impls across tagged-enum variant payloads.** When a DTO was both a top-level input/return type AND a payload of a tagged-data enum variant (e.g. `CrawlPageResult` used directly and inside `CrawlEvent::Page { result: Box<CrawlPageResult> }`), the main conversion loop and the tagged-enum variant-data loop each emitted an `impl From<Binding> for core::Type`, producing `E0119 conflicting implementations`. Track which types the main loop has already emitted (covering both the plain and enum-tainted branches) and skip them in the variant-data loop. (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-backend-php: drop bogus `#[php_method]` attribute from error info methods.** Methods inside a `#[php_impl]` block need no per-method attribute to be exposed to PHP — the `#[php_impl]` wrapper is sufficient. The `#[php_method]` attribute does not exist in ext-php-rs, causing `cannot find attribute 'php_method' in this scope` compile errors in any consumer crate. Removed the attribute from `status_code`, `is_transient`, and `error_type` method bodies in `gen_php_error_methods_impl`. (`src/codegen/error_gen.rs`)

- **alef-backend-pyo3: emit error introspection via companion class instead of `#[pymethods]` on the exception type.** `pyo3::create_exception!` types are zero-sized marker types and do not implement `PyClass`, so `#[pymethods] impl {ErrorName}` fails with "the trait bound `{ErrorName}: PyClass` is not satisfied". Replaced the broken impl with a `#[pyclass(name = "{ErrorName}Info")]` companion struct whose `#[pymethods]` block exposes `#[getter]` accessors for `status_code`, `is_transient`, and `error_type`. A `#[pyfunction] fn {snake}_info(err: Bound<'_, PyAny>) -> {ErrorName}Info` free function extracts the values from the exception args tuple (indices 1–3, populated by the existing converter). Both the companion class and the free function are registered in the `#[pymodule]` init. (`src/codegen/error_gen.rs`, `src/backends/pyo3/gen_bindings/methods.rs`)

- **alef-backend-rustler: append `visitor` to the trailing args of the emitted `Native.{fn}_with_visitor(...)` call.** The high-level Elixir wrapper module `lib/<crate>.ex` invokes the visitor NIF with arity `base+1` (extra trailing `visitor` parameter), but the args-builder for the call only re-mapped the existing NIF params and forgot to append `visitor`. Generated code was emitting `Native.convert_with_visitor(html, opts)` (arity 2) for an arity-3 NIF, producing `undefined function convert_with_visitor/2 (expected /3)` compile warnings (and runtime `:undef` on the visitor branch). Append the visitor parameter explicitly. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-backend-php: route enum-tainted types through `gen_enum_tainted_from_binding_to_core` in all `From<binding> for core` emission loops.** Two fallback loops — one for tagged-enum variant payloads and one for remaining DTO types — called `gen_from_binding_to_core_cfg` unconditionally, bypassing the `enum_tainted` check. When a type contained a field whose core type is a unit-variant enum stored as `String` in the PHP binding (e.g. `TextDirection`, `LinkType`, `ImageType`, `StructuredDataType`), the standard path emitted `.into()`, which requires a `From<String> for XxxEnum` impl that cannot exist downstream (orphan rule), producing E0277. Route enum-tainted types to `gen_enum_tainted_from_binding_to_core` in both loops, matching the existing main-loop behaviour. (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-backend-php: fix E0282 type inference on newtype Vec fields in enum-tainted From impls.** When `gen_enum_tainted_from_binding_to_core` emitted a Vec field whose elements are a newtype wrapper (e.g. `children: Vec<NodeIndex>`), the inner expression from `field_conversion_to_core_cfg` already ended with `.into_iter().collect()`, making the intermediate collection type ambiguous. The compiler could not infer the type parameter `B` for `collect`, producing E0282. Mirror the fix already present in `binding_to_core.rs`: when the inner expression ends with `.collect()`, rewrite it to `.collect::<Vec<_>>()` before chaining `.into_iter().map(NewtypeT).collect()`. (`src/backends/php/src/gen_bindings/helpers.rs`)

### Added

- **alef-backend-pyo3**: when `ErrorDef.methods` is non-empty, the exception class gains `@property` stubs in the `.pyi` stub file (`status_code -> int`, `is_transient -> bool`, `error_type -> str`) and a `#[pymethods] impl` block in the emitted Rust crate where each `#[getter]` reads the value from the exception args tuple by index (args are stored as `(msg, status_code, is_transient, error_type)` at raise-time). (`src/backends/pyo3/gen_bindings/errors.rs`, `src/backends/pyo3/gen_bindings/mod.rs`, `src/codegen/error_gen.rs`, `src/codegen/templates/error_gen/pyo3_error_converter.jinja`)
- **alef-backend-napi**: when `ErrorDef.methods` is non-empty, emit a `#[napi] pub struct Js{ErrorName}Info` with stored fields and a `#[napi] impl` block exposing `statusCode() -> u16`, `isTransient() -> bool`, and `errorType() -> String` with camelCase `js_name` attributes in the Rust crate, plus a matching `export declare class {ErrorName}Info { ... }` TypeScript declaration. (`src/backends/napi/src/gen_bindings/errors.rs`, `src/backends/napi/src/gen_bindings/mod.rs`, `src/codegen/error_gen.rs`)
- **alef-backend-magnus**: when `ErrorDef.methods` is non-empty, emit a `#[magnus::wrap] pub struct {ErrorName}Info` with `status_code()`, `transient()` (exposed as `transient?` via `define_method`), and `error_type()` methods in the Rust crate; register the class and methods in the module init; add `class {ErrorName}Info` with typed method signatures to the RBS stub file. (`src/backends/magnus/gen_bindings/mod.rs`, `src/backends/magnus/gen_bindings/functions.rs`, `src/backends/magnus/gen_stubs.rs`, `src/codegen/error_gen.rs`)
- **alef-backend-php**: when `ErrorDef.methods` is non-empty, emit a `#[php_class] pub struct {ErrorName}Info` with a `#[php_impl]` block exposing `statusCode(): int`, `isTransient(): bool`, and `errorType(): string` as `#[php_method]` functions in the Rust crate; register the info class in the PHP module builder; add the same three method stubs to the base exception class in the `.php` stub file. (`src/backends/php/src/gen_bindings/mod.rs`, `src/codegen/error_gen.rs`)
- **alef-backend-go**: when `ErrorDef.methods` is non-empty, `gen_go_error_struct` now emits exported struct fields (`StatusCode uint16`, `IsTransient bool`, `ErrorType string`) plus matching receiver methods on the structured error type. (`src/codegen/error_gen.rs`, `src/codegen/templates/error_gen/go_error_struct.jinja`)
- **alef-backend-java**: when `ErrorDef.methods` is non-empty, the base exception class gains private final fields, a zero-defaults simple constructor, an overloaded full constructor with all introspection params, and public getter methods following Java naming conventions (`getStatusCode()`, `isTransient()`, `getErrorType()`). (`src/codegen/error_gen.rs`, `src/codegen/templates/error_gen/java_error_base.jinja`)
- **alef-backend-csharp**: when `ErrorDef.methods` is non-empty, the base exception class gains get-only properties (`StatusCode`, `IsTransient`, `ErrorType`) initialized via a default constructor (with language-native zero values) and an overloaded full constructor. (`src/codegen/error_gen.rs`, `src/codegen/templates/error_gen/csharp_error_base.jinja`)
- **alef-backend-wasm**: when `ErrorDef.methods` is non-empty, emit an opaque `#[wasm_bindgen] pub struct Wasm{Prefix}{ErrorName} { pub(crate) inner: CoreError }` together with a `#[wasm_bindgen] impl` block exposing `statusCode() -> u16`, `isTransient() -> bool`, and `errorType() -> String` with camelCase `js_name` attributes. (`src/backends/wasm/gen_bindings/errors.rs`, `src/codegen/error_gen.rs`)
- **alef-backend-ffi**: when `ErrorDef.methods` is non-empty, emit `#[no_mangle] pub unsafe extern "C"` helper functions `{prefix}_{error}_status_code`, `{prefix}_{error}_is_transient`, and `{prefix}_{error}_error_type` (plus a matching `_error_type_free` deallocator). Each function null-checks the incoming `*const ErrorType` pointer and includes SAFETY comments per the FFI conventions. cbindgen will auto-generate the corresponding C header declarations. (`src/backends/ffi/src/gen_bindings/mod.rs`, `src/codegen/error_gen.rs`)
- **alef-backend-swift**: when `ErrorDef.methods` is non-empty, `emit_error` appends a `public extension {ErrorName}` block after the enum closing brace with one computed property per method (`public var statusCode: UInt16`, `public var isTransient: Bool`, `public var errorType: String`). Each property body is a `switch self` that matches every variant and returns the field value when the variant carries a field of the matching name, or a language-native zero default otherwise (`0`, `false`, `""`). (`src/backends/swift/src/gen_bindings.rs`)
- **alef-backend-dart**: when `ErrorDef.methods` is non-empty, the `error_sealed_class.jinja` template renders abstract getter declarations (`int get statusCode;`, `bool get isTransient;`, `String get errorType;`) inside the sealed class body before the closing `}`. Concrete variant subclasses can override these getters when they naturally carry the relevant field. (`src/backends/dart/src/gen_bindings/errors.rs`, `src/backends/dart/templates/error_sealed_class.jinja`)
- **alef-backend-kotlin**: when `ErrorDef.methods` is non-empty, `emit_error_type_with_imports` appends `abstract val {propertyName}: {KotlinType}` declarations inside the sealed class body before the closing `}` (`abstract val statusCode: Short`, `abstract val isTransient: Boolean`, `abstract val errorType: String`). Shared by both `alef-backend-kotlin` and `alef-backend-kotlin-android` via `emit_error_type_pub`. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)
- **alef-backend-kotlin-android**: inherits error introspection abstract properties from `alef-backend-kotlin` via the shared `emit_error_type_pub` helper; no source changes required beyond the test coverage addition. (`src/backends/kotlin-android/tests/docs_emission_test.rs`)
- **alef-backend-rustler**: when `ErrorDef.methods` is non-empty, emit one `#[rustler::nif] fn {error}_{method}(_msg: String) -> {RustType}` stub per method in the Rust `lib.rs` (returning a language-native zero default until structured error terms are available), plus a matching `@spec {fn_name}(String.t()) :: {elixir_type}` + `def {fn_name}(msg)` wrapper in the public Elixir module. (`src/backends/rustler/src/gen_bindings/mod.rs`)

### Fixed

- **alef-backend-magnus: drop duplicate `abstract!` after `interface!` and emit accessor as endless def with rubocop disable.** `interface!` already calls `T::Private::Abstract::Declare.declare_abstract`, so a following `abstract!` raises `<Module> is already declared as abstract` at file-load time on the very first `require`. Separately, the field-accessor pattern `def field; super; end` was being stripped by `rubocop -a` via the `Lint/UselessMethodDefinition` cop, leaving the preceding `sig { returns(...) }` orphaned and breaking the variant's Sorbet types. Emit `def field = super # rubocop:disable Lint/UselessMethodDefinition` so the def survives autocorrect and the sig attaches correctly. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-magnus: fix `Data.define` symbol args and accessor recursion in emitted sum-type variants.** Two bugs in the Ruby 3.2+ sum-type emission introduced in the prior Data.define migration: (1) `Data.define(level, text)` passed bare field names as undefined local variables instead of symbols — must be `Data.define(:level, :text)`; (2) `def level; level; end` shadowed the Data-generated accessor and recursed infinitely — must be `def level; super; end` to delegate to Data's auto-getter. Both bugs caused immediate `NameError` / `SystemStackError` on any Ruby binding with internally-tagged enums (e.g. `NodeContent` in html-to-markdown). Updated `tagged_enum_public_api_emits_class_hierarchy` and `test_enum_variant_method_yard_docs` assertions to match the corrected output. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-scaffold (java): raise CPD `minimumTokens` from 100 (default) to 200 in the generated `pom.xml`.** alef-emitted streaming methods on opaque handle classes share an identical iterator-driving loop by design (per-stream JNI externs differ, the surrounding plumbing is the same). The shared block is ~106 tokens, which trips PMD-CPD's default threshold and fails `mvn verify` on any binding that exposes two or more streaming adapters on the same handle. 200 is the smallest threshold that lets two streaming methods coexist in one handle class without a false-positive while still catching genuine large-scale duplication. (`crates/alef-scaffold/src/languages/java.rs`)

- **alef-backend-kotlin-android: disable ktlint's `multiline-expression-wrapping` in the generated `.editorconfig`.** Streaming-method emit shapes `fun foo(): Flow<T> = callbackFlow { ... }` as a one-liner; ktfmt (canonical formatter for the emitted tree) keeps it on one line, but ktlint then rejects the form. Mirrors the existing ktfmt-vs-ktlint conflict-rule overrides already in the emitted `.editorconfig` (class-signature, function-signature, function-expression-body, etc.). Add `ktlint_standard_multiline-expression-wrapping = disabled` to both `[*.kt]` and `[*.gradle.kts]` sections. (`src/backends/kotlin-android/gen_editorconfig.rs`)

- **alef-backend-go**: emit `Clear<Trait>s` functions for the trait-bridge family. The trait-bridge config already defined `clear_fn` (e.g., `"clear_ocr_backends"`), and `gen_clear_fn` existed to emit the wrappers, but the generated function names were lowercase (unexported in Go), preventing external consumers from clearing plugin registries. Now converts config-provided snake_case names to PascalCase (e.g., `"clear_ocr_backends"` → `func ClearOcrBackends() error`) for proper Go idiom and export visibility. Also honors `bridge_cfg.exclude_languages` to skip bridges when `"go"` is listed. (`src/backends/go/src/trait_bridge.rs`)
- **alef-backend-wasm**: add `futures` umbrella crate to emitted `Cargo.toml` dependency list. The WASM backend previously only included `futures-util` but the emitted `lib.rs` uses `futures::channel::mpsc` and `futures::sink::SinkExt` from the umbrella crate, causing E0433 compile errors in generated crates. `FUTURES` constant added to `alef-core::template_versions::cargo`; `"futures"` added to the `cargo-machete` ignored list. (`src/core/template_versions.rs`, `src/backends/wasm/gen_bindings/mod.rs`)
- **alef-backend-swift**: clone refs in Vec-of-Named return wrapper when `returns_ref = true`. When a core function returns `&[T]`, the emitted wrapper previously called `.into_iter().map(T)` — but `into_iter()` on `&[T]` yields `&T`, and the tuple constructor `T(...)` does not accept `&CoreT`. The fix emits `.iter().map(|x| T(x.clone()))` when `f.returns_ref` is true. (`src/backends/swift/src/gen_rust_crate/shims.rs`)
- **alef-backend-dart**: clone refs in Vec-of-Named return wrapper when `returns_ref = true`. Same root cause as the Swift fix: `return_transmute_expr` now accepts `returns_ref: bool` and emits `.iter().map(|x| T::from(x.clone()))` (or opaque `.iter().map(|inner| T { inner: inner.clone() })`) when the function returns a reference slice. (`src/backends/dart/src/gen_rust_crate/bridge_fn.rs`)
- **alef-backend-zig**: emit clear_<trait>s pub fns for the trait-bridge family (was previously omitted, leaving plugin registries un-clearable from Zig consumers).
- **alef-backend-csharp**: emit `Clear()` static method on each trait-bridge nested class (was previously omitted, leaving plugin registries un-clearable from .NET consumers).

### Added

- **alef-backend-magnus: emit Ruby 3.2+ `Data.define` sum types with marker modules instead of class inheritance.** Data-carrying internally-tagged enums now generate a marker module with `interface!` and `abstract!` Sorbet annotations, plus a dispatcher `from_hash(hash)` that routes to the correct variant by discriminator tag. Each variant becomes `<Name> = Data.define(:field1, :field2) do include MarkerModule ... end`, gaining Sorbet-typed attribute readers, variant predicate methods (e.g., `characters?`, `tokenizer?`), and per-variant `from_hash` factories. This is the canonical Ruby 3.2+ idiom for sealed sum types: modules can be mixed into Data classes, so instances return `true` for `is_a?(MarkerModule)` while retaining all Data-class ergonomics (immutability, destructuring, equality). The previous class-inheritance hierarchy pattern caused issues with advanced Ruby idioms and prevented safe integration with other Data-based code. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-core, alef-extract: add `ErrorDef.methods` field and extract whitelisted introspection methods from error enums.** `ErrorDef` in the IR now carries `pub methods: Vec<MethodDef>` (serde-defaulted to `[]`). `alef-extract` walks inherent `impl <ErrorType>` blocks and populates this field with a fixed whitelist — `status_code`, `is_transient`, `error_type` — filtering out all other methods (Display helpers, internal utilities, trait impls). Enables downstream backends to expose per-variant introspection on error types across the FFI without accidentally leaking Rust-only ergonomics. (`src/core/ir.rs`, `src/extract/extractor/functions.rs`, `src/extract/extractor/types.rs`)

- **alef-scaffold, alef-backend-jni: add `[crates.jni] crate_dir` override for JNI crate directory naming.** When `config.name` carries a language-specific suffix (e.g. `"html-to-markdown-rs"`) the default `<config.name>-jni/` derivation produces `crates/html-to-markdown-rs-jni/` — inconsistent with every other binding crate (`*-node`, `*-py`, `*-wasm`, etc.). A new optional field `crate_dir` on `JniConfig` allows consumers to override the base name: `[crates.jni] crate_dir = "html-to-markdown"` places the crate at `crates/html-to-markdown-jni/`. Both `scaffold_jni` and `jni_output_path` now call `config.jni_crate_base()` (which reads the override, falling back to `config.name`). The umbrella dep-table key remains `config.name` (the Cargo package name) with `path = "../<core_crate_dir>"` unchanged. Existing consumers without the override retain identical output — the tslp regression (CHANGELOG [0.16.25]) is unaffected. (`src/core/config/languages.rs`, `src/core/config/resolved/ffi.rs`, `src/backends/jni/src/gen_shims.rs`, `crates/alef-scaffold/src/languages/jni.rs`)

### Fixed

- **alef-backend-swift: emit complex DTOs (Vec/nested-struct/enum fields) as first-class Swift structs.** `first_class_field_supported` now accepts `Vec<T>`, `Optional<Vec<T>>`, and `Named(S)` where S is a known first-class struct or a unit serde enum. Unit serde enums gain `: String, Codable, Sendable, Hashable` conformance with proper raw-value annotations (e.g. `case toolCalls = "tool_calls"`) so structs containing them derive `Codable` automatically. `swift_ffi_read_expr` handles the new field shapes: `Vec<Named>` via `.map { try T($0) }`, `Optional<Vec<Named>>` via `?.map { ... }`, and enum fields via `T(rawValue: rb.field().toString())`. The fixed-point iteration that builds `known_dto_names` is seeded with unit serde enum names so dependent structs resolve in the same pass. The `unit_enum_raw_value` helper computes serde-accurate raw values for `snake_case`, `camelCase`, `SCREAMING_SNAKE_CASE`, `PascalCase`, and `kebab-case` rename strategies. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/snapshot_test.rs`)

- **alef-backend-java: suppress PMD CPD on intentional FFI boilerplate via `// CPD-OFF` / `// CPD-ON` markers, and correct `checkLastFfiError` modifier to `static`.** The Java codegen emits two boilerplate blocks that are structurally identical across every opaque-type class by design — the `checkLastFfiError`/`STREAM_MAPPER` pair in `streaming_helpers.jinja`, and the JSON-roundtrip FFI tail in `stream_method_named_result.jinja`. PMD CPD flagged the first as a 35-line/202-token duplication across 6 files (DownloadManager, LanguageRegistry, Node, Parser, Tree, TreeCursor) and the second as a 32-line/172-token duplication between `Node.walk()` and `Tree.walk()` — both inherent to the FFI emission pattern; extracting helpers would just hide the duplication behind a private method without removing the underlying boilerplate. Both templates now wrap their body in PMD line-comment markers (`// CPD-OFF` … `// CPD-ON`) so `mvn verify` passes without weakening duplication detection elsewhere. Additionally, `checkLastFfiError` is now `private static void` instead of `private void` — the method uses zero instance state, the prior modifier was an oversight. (`src/backends/java/templates/streaming_helpers.jinja`, `src/backends/java/templates/stream_method_named_result.jinja`)

- **alef-backend-napi: disambiguate static `From` method via typed let-binding.** When a DTO type emits a static `from` method (e.g., `ConversionOptions::from(update: ConversionOptionsUpdate)`), the NAPI backend previously generated `ConversionOptions::from(update.into())`, which is ambiguous to the Rust compiler: both the explicit `From<ConversionOptionsUpdate>` impl AND the blanket `From<T> for T` match, and `update.into()` can produce either. The fix emits a let-binding with an explicitly-typed inner conversion: `let _arg: html_to_markdown_rs::options::ConversionOptionsUpdate = update.into(); ConversionOptions::from(_arg)`, which disambiguates both the outer call and the inner conversion. The param's core type is looked up from the API surface and emitted with its fully-qualified path. (`src/backends/napi/src/gen_bindings/types.rs`, `src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-backend-kotlin-android: honor `exclude_types` in DTO/enum/error emission.** The Kotlin-Android backend was iterating `api.types`, `api.enums`, and `api.errors` directly without applying the per-language `exclude_types` filter configured via `[crates.kotlin_android].exclude_types` (or inherited from `[crates.ffi].exclude_types`). Types in those lists were still emitted as `.kt` data classes, enums, and sealed-class errors. The fix mirrors the Java backend pattern: `effective_exclude_types` collects names from both the FFI and `kotlin_android` config sections; `api_without_excluded_types` builds a filtered `ApiSurface` removing matching types, enums, errors, and any functions or fields that reference them; the filter is applied at the top of `generate_bindings` before any file emission. (`src/backends/kotlin-android/lib.rs`)

- **alef-backend-napi: deref `Box<T>` when converting tagged-enum variant fields to the flattened binding struct.** The tagged-enum core-to-binding match arms in `gen_tagged_enum_core_to_binding` emitted `Some(field.into())` for Named-typed fields, but for `Box<T>` fields (e.g. an internally-tagged variant `CrawlEvent::Page { result: Box<CrawlPageResult> }`) this fails with `E0277: the trait bound JsT: From<Box<T>> is not satisfied` — `Box<T>: Into<JsT>` is not provided. The fix consults `field.is_boxed` and emits `Some((*field).into())` in both the required-Named and binding-struct-Named branches (mirroring the existing synth-field path). Optional `Option<Box<T>>` fields similarly need `f.map(|v| (*v).into())`. (`src/backends/napi/src/gen_bindings/methods.rs`)

- **alef-backend-ffi: emit `_to_json` and `_free` for enum types used as streaming-adapter `item_type`.** The FFI streaming `_next` helper (emitted via `gen_stream_handle_functions`) returns `*mut item_type`, but `enum_pointer_return` was previously populated only from free-function returns, type-method returns, and struct field types — never from streaming adapters' `item_type` field. When the streaming item was an enum (e.g. kreuzcrawl's `CrawlEvent`), the FFI surface exposed `_next` without the matching `_to_json` / `_free`, leaving downstream language bindings (Go, Ruby, Java, C#, …) unable to consume the returned pointer. The fix adds an explicit pass over `config.adapters` that inserts each streaming adapter's enum `item_type` into `enum_pointer_return` before the enum emission loop. (`src/backends/ffi/src/gen_bindings/mod.rs`)

- **alef-codegen, alef-backend-csharp: split the shared INITIALISMS list and apply Microsoft's framework-design naming for C#.** The 0.16.71 fix added GraphQL handling to a single INITIALISMS list shared by Go and C#. That list also included `JSON`, `HTTP`, `URL`, etc., which produced names like `ProblemDetailsToJSON` for the user-method wrapper while alef's hardcoded helper declarations stayed at `ProblemDetailsToJson` — a hard-fail when the wrapper called the non-existent declaration. C# now uses a narrow `CSHARP_INITIALISMS = ["GraphQL", "ID", "UUID", "URI"]` list (product names + 2-letter abbreviations only), keeping `Json`/`Http`/`Url` consistent with Microsoft framework design guidelines AND with alef's hardcoded `{Type}ToJson`/`{Type}FromJson`/`{Type}Free` helper sites. Go's wider list is unchanged. (`src/codegen/naming.rs`, `src/backends/csharp/type_map.rs`)

- **alef-backend-csharp: emit `{Type}FromJson` and `{Type}ToJson` DllImport declarations for record types whose only user-facing method is an instance wither (`fn with_*(self, ...) -> Self`).** The wrapper synthesises `var selfHandle = NativeMethods.{Type}FromJson(selfJson)` and `var jsonPtr = NativeMethods.{Type}ToJson(nativeResult)` for those methods, but the previous opaque-param/return collection logic only inserted the owner type when one of its method params/returns was a Named type. Types like `ProblemDetails.with_detail(self, detail: String) -> Self` and `SseEvent.with_id(self, id: String) -> Self` therefore skipped the declaration entirely, leaving the wrapper to reference an undeclared symbol. The owner type is now added to both `opaque_param_types` and `opaque_return_types` for every `method.receiver.is_some()` case. (`src/backends/csharp/gen_bindings/functions.rs`)

- **alef-backend-go: apply Go acronym normalization to the inner type name in the JSON-roundtrip default-method emission.** The outer function signature uses `go_type_name(name)` (e.g. `JSONRPCConfig`), but the inline `func() *Foo { ... var result Foo ... }` closure used the raw IR `name` (`JsonRpcConfig`), producing two distinct identifiers that don't resolve. Affected at minimum: `JSONRPCConfigDefault`, `OpenAPIConfigDefault`. Inner references now match the outer signature so Go typechecks cleanly. (`src/backends/go/src/gen_bindings/types.rs::1631`)

- **alef-backend-java: gate all builder-dependent emissions on `should_emit_builder` (not the looser `typ.has_default`).** Three sites were still gated on `typ.has_default` after the conditional builder feature landed, causing records that have defaults but fall below the auto-emit threshold to reference a Builder class that wasn't generated: (1) the Jackson `JsonPOJOBuilder` import — Checkstyle flagged it as `UnusedImports` downstream because the `@JsonPOJOBuilder` annotation is only emitted inside `gen_builder_nested_class`, which already runs only when `should_emit_builder` is true; (2) the `@JsonDeserialize(builder = TypeName.Builder.class)` class-level annotation — pointed at a non-existent nested class; (3) the `public static Builder builder()` factory method — its return type `Builder` no longer exists in scope. All three now match the emission condition, so the three stay in lockstep with the actual nested-Builder presence. (`src/backends/java/gen_bindings/types.rs`)

### Added

- **alef-backend-java: conditional builder emission with `[crates.java.dto] builder = "auto" | "always" | "never"`.** The Java backend now gates builder class generation based on field count instead of unconditionally emitting a builder for every type with defaults. The `builder` knob accepts three modes: `"auto"` (default) emits a builder when field count >= 8 OR (has nested type AND count >= 5); `"always"` unconditionally emits builders for types with defaults; `"never"` disables builder emission entirely. This reduces boilerplate in kreuzberg's Java package: records with 2–7 fields no longer carry unnecessary nested `Builder` inner classes. Idiomatic Java 17+ prefers `record(...)` constructors with optional `withX(...)` methods for simple cases; builders shine when a type has many optional fields and complex dependencies. The threshold of 8 (or 5 with nested types) is a heuristic tuned for document-extraction configs: spot-checks confirm `BoundingBox` (4 fields) and `ExtractionConfig` (25 fields) respond appropriately. Configuration in `src/core/config/dto.rs` adds `JavaBuilderMode` enum and `JavaDtoConfig` struct; per-crate plumbing through `[crates.java.dto]` block in `alef.toml`. Implementation in `src/backends/java/gen_bindings/types.rs` adds `should_emit_builder()` predicate and `BUILDER_AUTO_THRESHOLD` constant.

### Fixed

- **alef-backend-napi: emit `Symbol.asyncIterator` on streaming iterator classes and correct streaming method TS signature.** The NAPI backend emitted `Promise<string>` for streaming methods (e.g. `chatStream`) because the `.d.ts` generator saw the raw IR return type (`String`) without consulting the streaming-adapter map. The iterator struct was a plain `#[napi]` class with only a `.next()` method, making `for await...of` impossible. Two fixes: (1) `gen_node_body` in `alef-adapters` now marks the iterator struct `#[napi(async_iterator)]` and implements NAPI-RS's `AsyncGenerator` trait instead of a bare `#[napi] impl` block — NAPI-RS sets `Symbol.asyncIterator` on every instance automatically at class-init time; (2) `gen_dts` in `alef-backend-napi` now accepts the `streaming_item_types` map, overrides streaming method return types to `Promise<AsyncGenerator<ItemType, void, undefined>>`, and appends a proper class declaration for each iterator (with `next()`, `[Symbol.asyncIterator]()`). TypeScript callers can now use idiomatic `for await (const chunk of client.chatStream(req)) { ... }` without `@ts-ignore`. (`crates/alef-adapters/src/streaming.rs`, `src/backends/napi/src/gen_bindings/errors.rs`, `src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-backend-dart: emit the reverse-direction `From<LocalMirror> for kreuzcrawl::CoreMirror` impl for streaming-adapter request types.** Streaming method bodies use `let core_req: kreuzcrawl::X = req.into();` which requires that direction, but until now alef only emitted `From<kreuzcrawl::X> for LocalX`. Simple request types such as `CrawlStreamRequest { url: String }` have no sanitized fields, so they never entered the `types_needing_from_impl` transitive closure that previously gated `From<LocalT> for CoreT` emission. The fix collects each streaming adapter's param types that are declared as mirror types and emits the mirror-to-core `From` impl for any not already covered. Surfaced as `E0277` in the regenerated `kreuzcrawl-dart` crate's `crawl_stream` / `batch_crawl_stream` methods.

- **alef-backend-napi: emit trait-bridge registration functions (`register_*`) in TypeScript declarations.** The NAPI backend was generating the Rust implementations of trait-bridge registration functions (`#[napi(js_name = "registerOcrBackend")]`, etc.) in `lib.rs` but omitting their declarations from `index.d.ts`, making them invisible to TypeScript callers. The `.d.ts` generator now iterates over `config.trait_bridges`, emitting `export declare function register*(impl: object): void;` declarations for each bridge's `register_fn` alongside the existing `unregister_*` and `clear_*` declarations. Fix in `src/backends/napi/src/gen_bindings/errors.rs::gen_dts`.

- **alef-backend-extendr: emit trait-bridge `register_*` / `unregister_*` / `clear_*` functions in `extendr_module!`, `extendr-wrappers.R`, and `NAMESPACE`.** The R backend already emitted `#[extendr]` wrappers for trait-bridge plugin functions, but they were missing from the three places that make them callable: the `extendr_module!` macro registers `wrap__<symbol>` entry points in the .so, `extendr-wrappers.R` binds them to R functions via `.Call(...)`, and `NAMESPACE` exports them. Because all three iterated only `api.functions` (parsed from Rust source), the synthesised trait-bridge functions were silently dropped — kreuzberg's R package shipped without any plugin registration surface. The fix adds `collect_trait_bridge_functions(config)` which walks `config.trait_bridges` (honouring `exclude_languages = ["r"]`) and is woven into all three emitters. R wrappers are minimal: `register_<trait>(r_backend)` accepts a named list of closures, `unregister_<trait>(name)` accepts a plugin name, `clear_<trait>()` takes nothing. R is dynamically typed so signatures are intentionally untyped; the extendr `Robj` introspection on the Rust side validates required methods. Implementation in `src/backends/extendr/src/gen_bindings.rs`.

- **alef-backend-swift: emit top-level `public func` forwarders for every free function in `api.functions` plus trait-bridge `register_*` / `unregister_*` / `clear_*` entry points.** The Swift host wrapper previously emitted only convenience overloads for functions whose first parameter was `TypeRef::Bytes` or `TypeRef::Path`, hardcoded e2e wrappers for the legacy extraction surface, and `*FromJson` forwarders. Every other public free function on the source Rust crate was reachable only through the auto-generated `RustBridge` module (e.g. `RustBridge.embedTexts(...)`), forcing consumers of the host module to also `import RustBridge` — a violation of the alef binding-parity promise. Similarly, `register_*` / `unregister_*` / `clear_*` plugin entry points were synthesised from `[[trait_bridges]]` and emitted into the swift-bridge Rust crate, but not re-exposed at the host module's top level. The fix adds `emit_free_function_forwarders(api, config, out)` which walks `api.functions`, honours `[crates.swift] exclude_functions`, skips entries already covered by other emission passes (bytes/path overloads, e2e wrappers, JSON forwarders), and emits a `public func {camelName}(...)` forwarder for each remaining entry — converting `String` / `[String]` / `[UInt8]` between Swift-native and swift-bridge runtime types (`RustString`, `RustVec<RustString>`, `RustVec<UInt8>`) at the call boundary. A second pass, `emit_trait_bridge_forwarders(config, out)`, iterates `config.trait_bridges` (filtered to `bind_via = "function_param"` and `exclude_languages` excluding `"swift"`) and emits the matching trio of forwarders, each delegating to `RustBridge.{name}(...)`. Implementation in `src/backends/swift/src/gen_bindings.rs`.

- **alef-backend-php: emit trait-bridge `register_*` / `unregister_*` / `clear_*` as static methods on the `KreuzbergApi` facade class.** The PHP backend was generating the Rust trait-bridge functions with `#[php_function]` attributes as standalone global functions (visible to PHP as `register_ocr_backend()`, `unregister_ocr_backend()`, etc.), but the facade-class approach expected them to be accessible as static methods on the `KreuzbergApi` class (`KreuzbergApi::registerOcrBackend(...)`, etc.) to match all other bindings. The fix adds iteration over `config.trait_bridges` in `gen_bindings/mod.rs`, emitting wrapper static methods that delegate to the underlying `#[php_function]` implementations. Each trait bridge contributes three methods: `register_*` (accepts a PHP object implementing the bridge interface), `unregister_*` (accepts a plugin name string), and `clear_*` (takes no arguments). This ensures kreuzberg's PHP package exposes the full plugin-registration surface on the native `KreuzbergApi` class, maintaining parity with Python, TypeScript, Ruby, Go, Java, C#, Elixir, and R. Implementation in `src/backends/php/src/gen_bindings/mod.rs`.

- **alef-backend-php: drop the invalid `#[php_method]` attribute and rename the `impl` parameter so the trait-bridge facade methods actually compile.** Two follow-up bugs in the prior trait-bridge facade emission: (1) the emitted `#[php_method]` attribute does not exist in `ext-php-rs` (`#[php_impl]` already exposes every `pub fn` to PHP without a per-method annotation), causing 18 `cannot find attribute \`php_method\` in this scope` errors on the downstream `kreuzberg-php` build; (2) the `register_*` body used `impl` as a Rust keyword parameter name, producing 18 `expected identifier, found keyword \`impl\`` parse errors. The fix drops the attribute and renames the parameter to `backend`, and uses a `crate::{name}(...)` qualified call inside the body so the wrapper does not infinitely recurse into itself. (`src/backends/php/src/gen_bindings/mod.rs`)

### Added

- **alef-core: `AdapterConfig` gains a `skip_languages` field that suppresses streaming/callback/async adapter emission for the listed backends.** Mirrors the existing `[[crates.e2e.calls.*]].skip_languages` precedent. Unknown language names fail at config-resolve time with a descriptive error. Useful when a consumer's core crate cannot compile a given binding target (e.g. kreuzcrawl's tokio-runtime engine on `wasm32-unknown-unknown` with no working fetch shim) — instead of force-removing the adapter's surface from `alef.toml` entirely, the adapter stays declared everywhere it works with explicit per-backend opt-out. The filter is applied in `build_adapter_bodies` (so adapter bodies are never inserted for skipped backends) and as a safety early-bail in `streaming::generate_body`. Per-backend collection sites in alef-backend-{php,java,magnus,kotlin,kotlin-android,swift} also skip matching adapters so streaming method registrations and import injections are consistent. Configured as `[[crates.adapters]] skip_languages = ["wasm", "kotlin"]`.

## [0.16.71] - 2026-05-19

### Fixed

- **alef-backend-napi: emit trait-bridge registration functions (`unregister_*`, `clear_*`) in TypeScript declarations.** The NAPI backend was generating the Rust implementations of trait-bridge functions (`#[napi(js_name = "unregisterOcrBackend")]`, etc.) in `lib.rs` but omitting their declarations from `index.d.ts`, making them invisible to TypeScript callers. The `.d.ts` generator now iterates over `config.trait_bridges`, emitting `export declare function unregister*(name: string): void;` and `export declare function clear*(): void;` declarations for each bridge's `unregister_fn` and `clear_fn`. Fix in `src/backends/napi/src/gen_bindings/errors.rs::gen_dts`.

### Added

- **alef-backend-php: emit `HtmlVisitorInterface` for visitor trait bridges so PHP users can implement custom visitors.** The PHP binding previously exposed only the opaque `VisitorHandle` type alias, giving no way for users to implement custom visitor logic. The backend now generates a `HtmlVisitorInterface` with method signatures for every visitor callback (15 methods: `visit_text`, `visit_element_start`, `visit_element_end`, `visit_link`, `visit_image`, `visit_heading`, `visit_code_block`, `visit_code_inline`, `visit_list_item`, `visit_list_start`, `visit_list_end`, `visit_table_start`, `visit_table_row`, `visit_table_end`, `visit_blockquote`). PHP users can now `implement HtmlVisitorInterface` on their callback class and pass instances via `ConversionOptions::$visitor`. The context parameter (`NodeContext`) is automatically hidden from the interface signature — only user-relevant parameters appear. Implementation in `src/backends/php/src/trait_bridge.rs:gen_visitor_interface` and `src/template_env.rs` (template registration).

- **alef-backend-kotlin-android: emit `Flow<T>` wrappers on opaque-handle classes for streaming adapters.** The kotlin-android backend now generates streaming instance methods on handle-only wrapper classes (e.g., `CrawlEngineHandle`) when streaming adapters own them. Each wrapper class gets one `fun {streamName}(req): Flow<ItemType>` instance method per streaming adapter, using `callbackFlow { ... }` to drive the JNI `nativeXxxStart/Next/Free` extern triple. This mirrors the existing `alef-backend-kotlin` (JVM) pattern in `jni_emitter.rs:emit_jni_streaming_client_method`. Previously the JNI externs were emitted but no Kotlin wrapper exposed them — callers had no way to consume the stream without dropping to raw JNI. New collector logic in `emit_module_kt` detects handle-only streaming ownership and emits methods with full Jackson mapper initialization and `withContext(Dispatchers.IO)` for each JNI call. Lives in `src/backends/kotlin-android/gen_bindings.rs:217–350`.

### Changed

- **alef-backend-pyo3: emit `@dataclass(frozen=True, slots=True)` for Python DTO classes.** Modern Python 3.10+ best practice is `slots=True`, which reduces memory overhead and improves attribute access performance with no behavioral change for users. All alef-generated dataclasses now include `slots=True` in the decorator on all `@dataclass` types emitted to `options.py`. The types generated are composed only of primitives and standard collections (no self-referential defaults), so slots compatibility is guaranteed.

### Fixed

- **alef-backend-php: emit `HtmlVisitorInterface` as separate PHP file, not inline Rust code.** The previous implementation (commit 570fa3f4) appended the interface stub to the Rust trait-bridge code via `code.push_str()`, which broke Rust compilation with "expected one of \`!\` or \`::\`, found \`HtmlVisitorInterface\`". The interface must be emitted as a standalone file (`packages/php/src/HtmlVisitorInterface.php`) following the same pattern as opaque-type PHP class stubs in `generate_public_api()`. Also fixed type translation: method signatures now use proper PHP types (`string`, `?string`, `bool`, `int`, `float`) instead of mixing Rust syntax (e.g., `_text: &str`). Fix in `src/backends/php/src/trait_bridge.rs` (new `rust_type_to_php_type()` function) and `src/gen_bindings/mod.rs` (emit interface files in `generate_bindings()` loop).

- **alef-backend-csharp: change visitor-bridge property from `{ get; set; }` to `{ get; init; }`.** Records should enforce immutability semantics. The `IHtmlVisitor?` property in visitor-bridge records was mutable, violating record invariants. Now emitted as init-only (constructor-time initialization only), matching other record properties and enforcing this constraint at compile time. _Breaking change:_ code that was mutating the visitor property after construction will no longer compile — callers must now set it via the property initializer at construction time.

- **alef-backend-java: emit NativeLib MethodHandle for static methods on opaque types.** `gen_native_lib` previously skipped every `method.is_static` entry on opaque types, so the new static-factory emission referenced symbols (`TS_PACK_PARSER_DEFAULT`, `TS_PACK_DOWNLOAD_MANAGER_NEW`, etc.) that NativeLib never declared. Static methods are now emitted alongside instance methods; the only difference is the param-layout vector starts empty rather than with the receiver `ValueLayout.ADDRESS`. Fix in `src/backends/java/gen_bindings/native_lib.rs`.

- **alef-backend-java: use the (String, Throwable) exception constructor in `gen_static_factory_method`.** Emitted `throw new TreeSitterLanguagePackRsException("…")` (single-String arg) but the available constructors are `(int, String)` and `(String, Throwable)` — so the generated Java did not compile. Now emits `new {exception_class}("…", (Throwable) null)` to match the existing instance-method pattern. Fix in `src/backends/java/gen_bindings/types.rs`.

- **alef-extract: skip `From`/`Into`/`TryFrom`/`TryInto` trait method extraction.** These conversion-trait impls reference Rust-only counterpart types (e.g. `impl From<tree_sitter::Point> for Point` references `tree_sitter::Point`, which has no representation in any target language). Emitting them produced nonsensical signatures like `Point.from(Point p)` in Java/C# and uncompilable code in napi where the input type was ambiguous between the JsX wrapper and the Rust counterpart. `Default` is intentionally retained — `Default::default()` is a legitimate preset constructor we want emitted. Fix in `src/extract/extractor/functions.rs::extract_trait_impl_methods`.

### Added

- **alef-extract: HashSet/BTreeSet/AHashSet/IndexSet/FxHashSet now resolve to Vec<T> in the IR so set-returning public APIs become exposed across all bindings (collapsing the wire shape).**

- **alef-backend-java: keyword-safe sanitiser `safe_java_method_name` for Rust impl-method names.** Rust impl blocks use names that are Java reserved words — `default`, `new`, `class`, etc. — and emitting them verbatim produces non-compiling Java (`public static Parser default()`). Sanitiser renames `default` → `defaultInstance` and `new` → `create`; any other `JAVA_KEYWORDS` collision falls back to a trailing-underscore suffix. Replaces the ad-hoc `if method.name == "default"` check in `emit_record_methods` and wires the new opaque static-factory and instance-method emission through the same helper. Lives in `src/backends/java/gen_bindings/helpers.rs`.

- **alef-backend-java: emit static factory methods on opaque-handle classes.** Opaque types in Java had no public constructors / static factories — users could only obtain a `Parser`, `LanguageRegistry`, or `DownloadManager` via top-level static helpers on the main class. Rust impl methods with no `&self` receiver and `-> Self` return (e.g. `Parser::default()`, `LanguageRegistry::default()`, `DownloadManager::new(version)`) now emit as `public static ClassName methodName(...)` on the opaque-handle class itself, with full Panama FFM marshaling. Mirrors the existing `gen_instance_method` pattern. New `gen_static_factory_method` in `src/backends/java/gen_bindings/types.rs`, called from `gen_opaque_handle_class` after instance-method emission.

- **alef-backend-napi: emit DTO impl-method companions as `#[napi]` free functions.** Previously, `#[napi(object)]` structs (DTOs in the Node backend) dropped every `impl` method from the Rust source, because object-style structs can't carry methods on the JS side. The new `gen_dto_method_fns` in `src/backends/napi/src/gen_bindings/types.rs` emits each non-excluded impl method as a free function named `<typeName><MethodName>` (camelCase), serde-JSON round-tripping the receiver for instance "wither" methods. Static preset constructors (e.g. `ProcessConfig::all()`, `::minimal()`, `::default()`) and instance withers (e.g. `with_chunking(usize)`) are now callable from TypeScript. Call site added at `src/backends/napi/src/gen_bindings/mod.rs:438`, gated to the non-opaque branch.

- **alef-backend-napi: document that DTO types do NOT emit fluent builder classes.** NAPI-RS (TypeScript/Node.js) intentionally does NOT emit fluent builder classes for DTO types. TypeScript idiomatically uses object literals (`{ headingStyle: 'atx', wrap: true }`) rather than fluent builders (which are idiomatic in Java as nested `Foo.Builder` classes). DTO methods (withers returning `Self`) are exposed as free functions accepting the instance as the first parameter. Added documentation comment to `gen_opaque_struct_methods` in `src/backends/napi/src/gen_bindings/types.rs` clarifying the pattern, rationale, and contrast with the Java backend (`src/backends/napi/src/gen_bindings/types.rs`).

### Fixed

- **alef-backend-csharp: use `to_csharp_name` (not `csharp_type_name`) on `api.crate_name`.** `csharp_type_name` only applies initialism uppercasing — it does not perform snake_case → PascalCase conversion. Since `api.crate_name` is a snake_case crate identifier (e.g. `ts_pack_core`), the previous initialism fix accidentally regressed exception and wrapper class name generation: callers got `ts_pack_coreException` instead of `TsPackCoreException`. Routed through `to_csharp_name` (PascalCase + initialisms) at the two `api.crate_name` call sites in `src/backends/csharp/gen_bindings/mod.rs`. Includes a `Cow::to_string()` fix in `emit_record_methods` to satisfy the borrow checker. Updated `test_opaque_method_return_wraps_handle_without_to_json` to assert the now-correct `GraphQLRouteConfig` class name.

- **C# backend**: preserve initialisms in type names (e.g. `GraphQLRouteConfig` instead of `GraphQlRouteConfig`), matching Go behaviour. A new `csharp_type_name` function in `alef-codegen/src/naming.rs` applies longest-match initialism restoration to PascalCase IR type names, handling multi-segment initialisms like `GraphQL` that `heck` splits into `Graph`+`Ql`. All C# class name, enum name, and `TypeRef::Named` call sites in `alef-backend-csharp` now route through `csharp_type_name`. `to_csharp_name` (snake_case → PascalCase) also applies the same initialism handling so e.g. `graphql_route_config` → `GraphQLRouteConfig`.

### Documentation

- **README, `skills/alef/`, `.ai-rulez/`: refresh to cover v0.16.69 and capture recurring work patterns.** README adds `[workspace.client_constructors]`, `[crates.<lang>.stubs] emit_docstrings`, source-level `#[alef::skip]`/`#[alef::exclude]` precedence vs config-level filters, polyglot release flow pointer (`.github/workflows/publish.yaml` + `kreuzberg-dev/actions`), the missing CLI subcommands (`docs`, `migrate`, `validate`, `release-metadata`, `check-registry`, `go-tag`, `snippets`), splits Kotlin/JVM and Kotlin/Android into separate rows, and corrects the `--format` default (now `true` on `generate`/`init`/`all`). The `alef` user-facing skill picks up the same `client_constructors`/`emit_docstrings` config sections, the corrected `--format` default, the `cargo install --path crates/alef-cli --force` local-install loop, and a cross-link to the `release-procedure` skill; pre-commit hook revision in the skill bumped to match `.pre-commit-config.yaml` (`v1.1.11`). `.ai-rulez/` gains five new rule files (`binding-audit-pattern`, `upstream-triage`, `sibling-agent-coordination`, `publish-flow`, `local-alef-install`) and a new `binding-audit` slash-command skill; `config.toml` declares both `release-procedure` and `binding-audit` skills explicitly; `CLAUDE.md` regenerated from source.

### Fixed

- **alef-backend-pyo3: .pyi stub generator now emits AsyncIterator[T] return type for streaming methods (was emitting the buffered placeholder return type, mismatching the real async iterator emitted in the Rust shim).** The `gen_stubs` function now builds a lookup from `config.adapters` for `Streaming` pattern entries and overrides the return type of matching methods and free functions. `AsyncIterator` is added to the `from typing import` line automatically when at least one streaming method is present.

- **alef-backend-php: streaming methods now emit a lazy PHP `\Generator` instead of an eager-collected JSON string.** The PHP backend now emits three free functions per streaming adapter (`{owner}_{name}_start`, `_next`, `_free`) that expose an opaque handle to PHP, plus a high-level `public function {method}(...): \Generator` on the owning class that drives the handle via `yield json_decode($chunk, true)`. The originally-emitted eager method body is suppressed for streaming adapters via a `streaming_method_keys` filter in `src/backends/php/src/gen_bindings/types.rs` so callers see only the Generator. Mirrors the Elixir `Stream.unfold` and Python `AsyncIterator` architectures already in place.

- **alef-adapters: Node streaming methods now return a `#[napi]` iterator class instead of materializing the entire stream into a Vec.** Callers consume items via `await iter.next()` (returns `null` at end-of-stream), matching the existing Python AsyncIterator pattern. The previous behavior collected every chunk into a JS Array eagerly, defeating the point of streaming.

- **alef-adapters, alef-backend-wasm: streaming method infrastructure for WASM (requires core library changes).** Added `gen_wasm_body` streaming adapter and return-type override logic in the WASM backend to emit iterator structs via wasm-bindgen. The pattern mirrors Python/Node: an iterator struct with a `RefCell<futures::channel::mpsc::Receiver>` (no threads in WASM) and async `next()` method, driven by a `wasm_bindgen_futures::spawn_local()` task. **NOTE:** kreuzcrawl's core library gates the streaming methods with `#[cfg(not(target_arch = "wasm32"))]`, preventing WASM from calling them. This is a library design limitation, not an alef limitation — the infrastructure is complete and ready when the core library exposes streaming methods for WASM. See `crates/alef-adapters/src/streaming.rs:495` and `src/backends/wasm/gen_bindings/{mod.rs,types.rs,methods.rs}`.

- **alef-backend-zig: Zig streaming methods now return all chunks as a JSON array instead of only the last chunk.** Previously, `_last_json` was overwritten on every iteration, dropping every event except the final one. Now emits `[chunk1,chunk2,...]` so callers receive the full stream.

## [0.16.69] - 2026-05-19

### Added

- **alef-core, alef-codegen: introduce `[workspace.client_constructors.<TypeName>]` config and `gen_opaque_constructor()` shared helper**. New `ClientConstructorConfig` (params + body template + optional `error_type`, default `String`) on `WorkspaceConfig` is plumbed through `NewAlefConfig::resolve` into `ResolvedCrateConfig.client_constructors`. The codegen helper renders `pub fn new(params…) -> Result<Self, ErrType>` from the config body (with `{type_name}` and `{source_path}` placeholders) and accepts a per-backend attribute prefix (`#[new]`, `#[napi(constructor)]`, `#[wasm_bindgen(constructor)]`, `#[php_method]`, `#[frb]`, etc.) so every Rust-host backend can emit a uniform user-defined factory for opaque handles. (`src/core/config/workspace.rs`, `src/core/config/new_config.rs`, `src/core/config/resolved/mod.rs`, `src/codegen/generators/methods.rs`, `src/codegen/generators/mod.rs`)

- **alef-backend-ffi: emit `{prefix}_{type_snake}_new` constructor symbol from `[workspace.client_constructors]` config**. When `client_constructors.<TypeName>` is present, the FFI backend now emits a `#[unsafe(no_mangle)] pub unsafe extern "C" fn {prefix}_{type_snake}_new(params...) -> *mut TypeName` function. The template `type_new.jinja` was missing from the static TEMPLATES list despite being referenced by `gen_type_new()`; it is now registered. (`src/backends/ffi/templates/type_new.jinja`, `src/backends/ffi/src/template_env.rs`, `src/backends/ffi/src/gen_bindings/mod.rs`, `src/backends/ffi/src/gen_bindings/types.rs`)

- **alef-backend-go: emit `func New<TypeName>(params...) (*TypeName, error)` constructor from `[workspace.client_constructors]` config**. When an opaque type has a `client_constructors` entry, the Go backend now emits a `func New<TypeName>(params...) (*TypeName, error)` CGO wrapper. `*const c_char` params are marshalled via `C.CString()` + `defer C.free()`; other FFI types map to their CGO equivalents. (`src/backends/go/src/gen_bindings/mod.rs`)

- **alef-backend-zig: emit `pub fn create_<type_snake>(params...) !TypeName` constructor from `[workspace.client_constructors]` config**. When an opaque type has a `client_constructors` entry, the Zig backend emits `pub fn create_<type_snake>(...)  !TypeName`. String params use `allocator.dupeZ` for null-terminated conversion; the allocator param is prepended automatically when string params are present. (`src/backends/zig/src/gen_bindings/opaque_handles.rs`, `src/backends/zig/src/gen_bindings/mod.rs`)

- **alef-backend-csharp: emit `public static TypeName Create(params...)` factory and `[DllImport] static extern IntPtr TypeNameNew(params...)` P/Invoke from `[workspace.client_constructors]` config**. When an opaque type has a `client_constructors` entry, the C# backend emits a `Create` factory method on the SafeHandle class and a matching `NativeMethods.TypeNameNew(...)` P/Invoke declaration. String params use `[MarshalAs(UnmanagedType.LPStr)]`. (`src/backends/csharp/gen_bindings/types.rs`, `src/backends/csharp/gen_bindings/functions.rs`, `src/backends/csharp/gen_bindings/mod.rs`)

- **alef-backend-jni: emit `pub unsafe extern "system" fn Java_..._nativeNew<TypeName>(params...) -> jlong` constructor shim from `[workspace.client_constructors]` config**. When an opaque type has a `client_constructors` entry, the JNI backend emits a constructor shim whose symbol follows the `nativeNew<TypeName>` convention. `*const c_char` params are received as `JString` and unmarshalled via `jstring_to_string`. Returns `Box::into_raw` pointer as `jlong` on success, `0` on error. (`src/backends/jni/src/gen_shims.rs`)

- **alef-backend-kotlin: emit `external fun nativeNew<TypeName>(params...): Long` and `companion object { fun create(params...): TypeName }` from `[workspace.client_constructors]` config**. When an opaque type has a `client_constructors` entry, the Kotlin JNI emitter declares the `nativeNew<TypeName>` extern in the Bridge object and adds a `create(...)` factory in the DefaultClient companion object. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **alef-backend-pyo3, alef-backend-napi, alef-backend-wasm, alef-backend-php, alef-backend-rustler, alef-backend-dart: emit a per-type constructor `impl` block from `[workspace.client_constructors]` config**. Every Rust-host backend that wraps opaque handles now consults the shared `client_constructors` map and appends a constructor impl beside its main impl block: PyO3 emits `#[pymethods] impl T { #[new] pub fn new(...) -> Result<Self, E> { ... } }`, NAPI emits `#[napi] impl T { #[napi(constructor)] ... }`, wasm-bindgen emits `#[wasm_bindgen] impl T { #[wasm_bindgen(constructor)] ... }`, ext-php-rs emits `#[php_impl] impl T { #[php_method] ... }`, Rustler emits a plain `impl T` body, and the Dart `lib.rs` shim emits `impl T { #[frb] pub fn new(...) ... }`. (`src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/napi/src/gen_bindings/mod.rs`, `src/backends/wasm/gen_bindings/mod.rs`, `src/backends/php/src/gen_bindings/mod.rs`, `src/backends/rustler/src/gen_bindings/mod.rs`, `src/backends/dart/src/gen_rust_crate/mod.rs`)

- **alef-extract: auto-disambiguate types with the same identifier in different modules by prepending PascalCase parent module name to the second+ collisions**. When two source types share the same identifier in sibling modules (e.g. `crate::SseEvent` and `crate::testing::SseEvent`), the second collision is now renamed by prepending its PascalCase parent module segment (`TestingSseEvent`), keeping the first-seen variant under its original name. Three-way (and deeper) collisions walk further up the module tree until a unique name is found. Sort order is by full source `rust_path` so first-seen-first is deterministic across runs. Eliminates the need for per-collision exclusions in consumer `alef.toml` files. (`src/extract/extractor/disambiguation.rs`, `src/extract/extractor/mod.rs`)

- **alef-extract: unwrap `Arc<Mutex<T>>` and `Arc<RwLock<T>>` to `CoreWrapper::ArcMutex`; peek through `Option<...>`**. `detect_core_wrapper` now inspects the inner type of an `Arc<...>` and returns `CoreWrapper::ArcMutex` when it is a `Mutex<T>` or `RwLock<T>` (last-segment match, so `std::sync::Mutex` and `tokio::sync::Mutex` are treated identically). It also peeks through a leading `Option<...>` so `Option<Arc<Mutex<T>>>` resolves the same as the bare form. `Arc<dyn Trait>` is intentionally left as plain `Arc` — trait-object ownership differs. The type resolver's `Box | Arc | Rc` unwrap arm is extended to also unwrap `Mutex` and `RwLock` to their inner type, so the inner `T` no longer leaks out as `TypeRef::Named("Mutex")` in generated bindings. Enables binding generation for structs that share state via Arc-wrapped synchronization primitives. (`src/extract/extractor/helpers.rs`, `src/extract/type_resolver.rs`, `src/codegen/conversions/mod.rs`)

### Fixed

- **alef-codegen: fluent-builder methods with `Json` (or `Option<Json>`) params now auto-delegate instead of stubbing to `unimplemented!()`**. `TypeRef::Json` was previously excluded from both `is_delegatable_param` and `is_simple_non_opaque_param`, so a perfectly delegatable shape like `pub fn with_extension(self, key: String, value: serde_json::Value) -> Self` fell through to the unimplemented stub. The binding takes the JSON value as a string and `gen_call_args` already emits `serde_json::from_str(&value)` at the call site to bridge it into the core `serde_json::Value` parameter, so Json is fully delegatable in both directions (return values are serialized via `.to_string()` by `wrap_return_with_mutex_mapped`). All Rust-host backends already depend on `serde_json` (used by Json field round-tripping), so no new dependency. (`src/codegen/shared.rs`, `src/codegen/generators/binding_helpers.rs`, `src/codegen/tests/integration_test.rs`)

- **alef-codegen: explicit adapter bodies now take precedence over auto-generated delegation in `gen_method`/`gen_static_method`**. The adapter override lookup was previously gated behind `!can_delegate` — a method that the codegen could delegate would silently ignore any user-provided adapter body. Now adapter bodies are checked first and unconditionally win; delegation is a fallback. Required to keep adapter overrides honored as more shapes (e.g. Json params, see above) become eligible for auto-delegation. (`src/codegen/generators/methods.rs`)

- **alef-cli: `alef generate` now runs formatters by default (`--format` defaults to `true`)**. Previously `alef generate` silently skipped the post-generation ruff/mix-format/gofmt/etc. pass unless `--format` was explicitly passed, so the written files were pre-formatter content. Downstream prek hooks then reformatted the files, changing their content after `alef:hash:` was computed, causing `alef verify --exit-code` to report every Python/Elixir/Go binding as stale. `alef all` already defaulted `format` to `true`; `alef generate` and `alef init` now match. Consumers who want to skip formatting can still pass `--format=false`. (`crates/alef-cli/src/main.rs`)

- **alef-backend-pyo3, alef-backend-magnus: gate stub docstring emission behind `[crates.{lang}.stubs] emit_docstrings = true`**. Commit c2f18497 made `alef stubs` and `alef generate` unconditionally emit triple-quoted docstrings in `.pyi` files and `#`-comment blocks in `.rbs` files. Projects with `ruff lint.select = ["ALL"]` break on ruff rule PYI021 ("Docstrings should not be included in stub files"). The new `emit_docstrings` field on `StubsConfig` (default `false`) gates all docstring emission in both backends; consumers opt in by adding `emit_docstrings = true` to their `[crates.python.stubs]` or `[crates.ruby.stubs]` section. (`src/core/config/languages.rs`, `src/backends/pyo3/gen_stubs.rs`, `src/backends/magnus/gen_stubs.rs`, `src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-swift: add `rt-multi-thread` to the emitted swift crate's tokio features**. v0.16.62 switched the swift-bridge async shims from per-call `Builder::new_current_thread()` to a process-wide `Builder::new_multi_thread()` runtime (so `reqwest`'s lazily-attached connection pool stays alive across calls), but the emitted `packages/swift/rust/Cargo.toml` still pinned `tokio = { features = ["rt", "macros"] }` — and `new_multi_thread` only exists when the `rt-multi-thread` feature is enabled. Surfaced on tree-sitter-language-pack `CI Mobile` as `error[E0599]: no function or associated item named 'new_multi_thread' found for struct 'tokio::runtime::Builder'` on both `aarch64-apple-ios-sim` and `aarch64-apple-ios` cargo-check jobs after the v0.16.68 regen. Bump the emitted tokio dep to `features = ["rt", "rt-multi-thread", "macros"]`; iOS/macOS/Linux all support `rt-multi-thread` so no platform-conditional gating is required. (`src/backends/swift/src/gen_rust_crate/cargo.rs`, snapshot fixtures)

### Changed

- **Bump `kreuzberg-dev/pre-commit-hooks` to v1.1.11, `crate-ci/typos` to v1.46.2, `Goldziher/gh-actions-updater` to v0.1.2.** Routine dev-tooling refresh; no behavioral changes. (`.pre-commit-config.yaml`)

## [0.16.68] - 2026-05-19

### Fixed

- **alef-backend-java: emit `checkLastError()` after fallible primitive-return and void-return FFI invocations**. The Java FFI codegen called `checkLastError()` only on the pointer-return path (where a NULL sentinel triggered it); fallible functions whose Rust signature is `Result<T, Error>` with a primitive `T` (e.g. `pub fn download(...) -> Result<usize, Error>` → Java `long`) or with `T = ()` silently dropped errors on the floor because the primitive `0` or the empty return couldn't distinguish "successful no-op" from "FFI set last_error". Now, when `FunctionDef.error_type.is_some()`, both the primitive-return and void-return branches of `gen_sync_function_method` emit `checkLastError()` immediately after the FFI invoke and ptr-cleanup so Java throws the expected `*Exception` on failure. Surfaced on tree-sitter-language-pack `E2E (java)` where `DownloadTest.testDownloadInvalidLanguage` expected an exception for `download(List.of("zzz_definitely_not_a_real_language_xyz"))` but the bound `download` method returned 0 cleanly. (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-e2e: raise vitest `hookTimeout` to 120s and add explicit `teardownTimeout: 30s` in the typescript/wasm vitest.config templates**. Vitest 3.x has an internal worker RPC (`onTaskUpdate`) whose budget tracks `hookTimeout`; long single-file suites (the tslp `smoke.test.ts` runs 305 tests through `process()`, ~63s on macos-arm64) outran the default 10s hook budget and the worker shutdown handshake errored with `[vitest-worker]: Timeout calling "onTaskUpdate"` even though every test passed individually. Raising `hookTimeout` to 120s and pinning `teardownTimeout` at 30s gives the worker enough patience for long alef-generated suites without affecting per-test timing (still 30s via the existing `testTimeout`). (`src/e2e/templates/typescript/vitest.config.ts.jinja`, `src/e2e/templates/wasm/vitest.config.ts.jinja`)

## [0.16.67] - 2026-05-19

### Fixed

- **alef-e2e mock-server: honour `MOCK_SERVER_NO_STDIN_WATCH=1` to wait on SIGTERM instead of stdin EOF**. The standalone mock-server binary's default lifecycle waits for stdin EOF (parent-controlled subprocess pattern, fine for Rust/Node/Python harnesses that spawn it directly). The Swift e2e GitHub Actions job uses a different pattern — it backgrounds the mock-server in a `Start mock-server` step, then expects it to survive across to the next `Run E2E tests` step. When the per-step bash exits the inherited stdin FD closes and the mock-server immediately reads EOF and exits, leaving the test step to fail every smoke test with `error sending request for url (...)`. Add an opt-in env switch that swaps the lifetime watcher to `tokio::signal::ctrl_c()` so the server runs until the job step explicitly terminates it. (`src/e2e/codegen/rust/mock_server.rs`)

## [0.16.66] - 2026-05-19

### Fixed

- **alef-backend-swift: share a single process-wide tokio runtime across all async wrappers instead of building one per call**. swift-bridge functions wrap async source calls with `Builder::new_current_thread().enable_all().build().block_on(...)` — fine for self-contained futures, but breaks `reqwest`. `reqwest::Client::builder().build()` lazily attaches its connection pool to the FIRST tokio runtime it sees, then the pool dies when that runtime drops. Every subsequent call (driven by a fresh runtime) hits an orphaned pool and fails with `error sending request for url (...)` — the exact symptom on liter-llm `E2E (swift)` smoke tests after v0.16.61 unblocked compile. Replace the per-call pattern with `crate::__alef_tokio_runtime()` — a `OnceLock<tokio::runtime::Runtime>` initialized once with `Builder::new_multi_thread()` so the pool stays alive for the process lifetime and works across reentrant host threads. Touches `gen_rust_crate/shims.rs` (top-level definition + accessor const), `gen_rust_crate/trait_bridge.rs`, and `gen_rust_crate/wrappers.rs`. Snapshot tests updated. (`src/backends/swift/src/gen_rust_crate/{mod,shims,trait_bridge,wrappers}.rs`)

## [0.16.65] - 2026-05-19

### Added

- **alef-e2e/swift: emit a shared `TestHelpers.swift` with a `RustString: CustomStringConvertible` extension so XCTest failures print the actual Rust error content**. swift-bridge's `RustString` is an opaque class without a `description`, so any error thrown from a bridge function (`throw RustString(ptr: val.ok_or_err!)`) surfaces in XCTest output as the bare type name — `caught error: "RustBridge.RustString"` — with the actual Rust error message hidden inside the unprinted instance. Every E2E (swift) failure across the polyglot repos was indecipherable as a result. Add a one-time `TestHelpers.swift` per test target with `extension RustString: @retroactive CustomStringConvertible { public var description: String { self.toString() } }`. Diagnostic-only change; no test code modifications, no semantic shift in passing/failing — but failures now print readable error text. (`src/e2e/codegen/swift.rs`)

## [0.16.64] - 2026-05-19

### Fixed

- **alef-e2e/swift: `swift_count_target` consults `SwiftFirstClassMap.vec_field_names` so `.count` on `RustVec` method-call accessors stays put**. v0.16.63's blanket `.toString()` injection (predicated only on the accessor ending in `)`) over-eagerly wrapped Vec-typed method-call accessors too — `result.output().toString().count` fails with `value of type 'RustVec<ResponseOutputItem>' has no member 'toString'`. Track per-IR `Vec<T>` (and `Option<Vec<T>>`) field names in a flat `vec_field_names: HashSet<String>` on `SwiftFirstClassMap`, expose `leaf_is_vec_via_swift_map` on `FieldResolver`, and have `swift_count_target` skip the wrap when the leaf is a Vec field. RustVec already exposes `.count` directly. Surfaced on liter-llm `E2E (swift)` ResponsesTests line 95 after v0.16.63. (`src/e2e/field_access.rs`, `src/e2e/codegen/swift.rs`)

## [0.16.63] - 2026-05-19

### Fixed

- **alef-e2e/swift: pin opaque (method-call) syntax after any `ArrayField` segment + inject `.toString()` before `.count` on opaque method-call accessors**. v0.16.62 dispatched per-fixture first-class/opaque syntax for the root, but two follow-on misses remained on liter-llm `E2E (swift)`: (1) array-indexed elements were emitted with property syntax when the element type's first-class Codable struct existed (`_vec_data[0].object`), even though `RustVec[i]` always yields the opaque `RustBridge.T` (its first-class twin is a separate Codable struct used only at FFI boundaries — never produced by indexing). swift compile error: `method 'object' was used as a property; add () to call it` on `ListModelsTests`. (2) The not-empty / is-empty assertion path emitted `result.id().count` for opaque-rooted scalar string fields, but `RustString` does not expose `.count` — only Swift `String` does. swift compile error: `value of type 'RustString' has no member 'count'` on `ResponsesTests`. Fixes: (1) Carry a `via_rust_vec` flag in `render_swift_with_first_class_map` that flips to true on the first `ArrayField` segment and pins opaque syntax for every following segment (advance still tracks `current_type` so first-class metadata stays accurate for cleanups, but the property/method-call decision is overridden). (2) Add `swift_count_target` helper in the swift codegen that wraps an accessor with `.toString()` when it ends in `)` (the opaque method-call signature), leaving first-class property accessors untouched. Wired into both the `not_empty` and `is_empty` length-expression paths. (`src/e2e/field_access.rs`, `src/e2e/codegen/swift.rs`)

## [0.16.62] - 2026-05-19

### Fixed

- **alef-e2e/swift: thread per-fixture `root_type` into `SwiftFirstClassMap` so each call's accessor dispatch reflects its actual return type**. v0.16.61 wired the per-segment first-class/opaque dispatcher but the `root_type` was derived once at codegen-build time (a single workspace-wide heuristic), so the dispatcher couldn't tell `chat()` (returns opaque `ChatCompletionResponse`) apart from `createFile()` (returns first-class `FileObject`). The map's default (`is_first_class(None) → true` → property access) was correct for first-class result types but emitted bare property syntax (`result.choices[0]`) for opaque types, surfacing as `function value was used as a property; add () to call it` and `method 'message' was used as a property` on liter-llm `E2E (swift)` SmokeTests/BatchesTests/ChatTests. Add `FieldResolver::with_swift_root_type` (clones the resolver with the map's `root_type` replaced) and `swift_call_result_type` in the Swift codegen which reads `call_config.overrides[<lang>].result_type` across `c, csharp, java, kotlin, go, php` (the same language-agnostic IR-name lookup PHP uses). Each fixture's call now gets its own resolver clone with the correct `root_type`, so opaque types route through method-call access and first-class types stay on property access. Also removed a stray `eprintln!` debug line in `render_test_method` that leaked build noise. (`src/e2e/field_access.rs`, `src/e2e/codegen/swift.rs`)

## [0.16.61] - 2026-05-19

### Added

- **alef-e2e/swift: per-segment first-class/opaque dispatch via `SwiftFirstClassMap`**. v0.16.59 switched render_swift to property access for all field segments, fixing first-class Codable struct paths (FileObject, SystemMessage, etc.) but breaking paths that traverse into typealias-to-opaque types (BatchObject, ResponseObject, etc. where swift-bridge exposes fields as methods). E2E (swift) compile errors: `value of type '() -> RustString' has no member 'count'` and `method 'status' was used as a property; add () to call it`. Add a per-type `SwiftFirstClassMap` (analogous to `PhpGetterMap`) populated from the IR's TypeDef list: first-class types use property access, opaque/typealias types use method-call. Renderer tracks a `current_type` cursor through `field_types` so it picks the right syntax at each segment of a nested path like `result.data[0].id`. Surfaced on liter-llm `E2E (swift)` in BatchesTests after v0.16.59. (`src/e2e/field_access.rs`, `src/e2e/codegen/swift.rs`)

## [0.16.60] - 2026-05-19

### Fixed

- **alef-backend-csharp: tagged-union enum-variant defaults now populate `tagged_union_enums` set from `api.enums`**. The v0.16.57 fix branched the default-value renderer on `complex_enums.contains(...)`, but `complex_enums` is intentionally empty (untagged enums emit as JsonElement-wrapper classes), so the new `new Base.Variant()` path never fired. Build a separate `tagged_union_enums` set from `api.enums` filtered to serde-tagged data enums and thread it into `gen_record_type`. Surfaced on liter-llm `E2E (csharp)` continuing to fail CS0119 on `CacheConfig.Backend = CacheBackend.Memory` after v0.16.57. (`src/backends/csharp/gen_bindings/mod.rs`, `src/backends/csharp/gen_bindings/types.rs`)

- **alef-e2e/php: PhpGetterMap.needs_getter falls back to bare-name union when the recorded `root_type` does not declare the field**. The workspace-level `root_type` resolution is best-effort and can pick a wrong type for some fixtures (every call gets the same workspace root, but per-fixture calls have different return types). When a recorded `current_type` does not declare `field_name`, the renderer was returning `false` (no getter), causing `$result->data` instead of `$result->getData()` for non-scalar fields on the correct (per-call) return type. Track `all_fields` per type in `PhpGetterMap` and consult the owner-type classification ONLY when that type actually declares the field; otherwise fall through to the bare-name union. Surfaced on liter-llm `E2E (php)` (76 TypeErrors on `assertCount(N, $result->data)` where the workspace root_type was `ChatCompletionResponse` but `listBatches` returns `BatchListResponse`). (`src/e2e/field_access.rs`, `src/e2e/codegen/php.rs`, plus test data updated to populate `all_fields`)

## [0.16.59] - 2026-05-18

### Fixed

- **alef-e2e/swift: use property access for field segments (no parens), matching first-class Codable struct emission**. `render_swift` and `render_swift_with_optionals` in `src/e2e/field_access.rs` historically added `()` after every field segment because swift-bridge exposed all Rust fields as methods. Since the v0.16.43+ first-class-struct emission, most binding DTOs are `public struct Foo: Codable { public let id: String }` — properties, not methods — and `.id()` raises `cannot call value of non-function type 'String'`. Switch to plain `.id` field access for all segments, drop `()` from array `[N]` and map `[key]` subscripts. Also updated the `.len()` → `.count` assertion paths in `src/e2e/codegen/swift.rs` for non-empty / is-empty checks. The remaining typealias-to-opaque types (request DTOs with Vec/Map/Named fields) are method inputs / streaming outputs in practice, not parents for field-access chains, so property syntax works. A future per-type `SwiftFirstClassMap` (analogous to `PhpGetterMap`) would be needed if a fixture asserts on a field chain rooted in an opaque type. Surfaced on liter-llm `E2E (swift)` with `cannot call value of non-function type 'String'` errors in FilesTests. (`src/e2e/field_access.rs`, `src/e2e/codegen/swift.rs`, plus matching test updates)

## [0.16.58] - 2026-05-18

### Fixed

- **alef-backend-php: revert v0.16.55 camelCase Rust-ident attempt — restore `get_snake_case` and let ext-php-rs convert to camelCase**. v0.16.55 attempted to emit non-scalar field getters as `pub fn getCamelCase(...)` (camelCase Rust ident, no `#[php(getter)]` attribute). But ext-php-rs's `#[php_impl]` macro doesn't surface camelCase Rust idents as callable PHP methods reliably — runtime dispatch still fails with "Call to undefined method getCamelCase()". Revert to the snake_case `get_camel_case_field` Rust ident; ext-php-rs auto-converts that to camelCase `getCamelCaseField()` at the PHP boundary. The `#[php(getter)]` attribute stays dropped (per the v0.16.55 fix description) so the method is callable rather than registered solely as a (broken) property accessor. (`src/backends/php/src/gen_bindings/types.rs`)

## [0.16.57] - 2026-05-18

### Fixed

- **alef-backend-csharp: tagged-union enum defaults emit `new Base.Variant()` instead of `Base.Variant`**. C# emits serde-tagged data enums as `public abstract record Base { public sealed record Variant() : Base; }` — under that shape `Base.Variant` is a TYPE, not an instance, so `public T Field { get; init; } = Base.Variant;` fails to compile (CS0119 "X is a type, which is not valid in the given context"). Detect via the `complex_enums` set (enums whose variants carry data) and emit `new Base.Variant()` instead. Surfaced when exposing `CacheConfig.backend` (default `Memory`) to C# via the Tower config DTOs. (`src/backends/csharp/gen_bindings/types.rs`)

- **alef-e2e/php: key getter-field classification by `(owner_type, field_name)` instead of bare field name**. The previous `php_getter_fields: HashSet<String>` lumped every non-scalar field across every `TypeDef` into a single bare-name set, so when two types declared the same field name with different scalarness — e.g. kreuzcrawl `CrawlConfig.content: ContentConfig` (non-scalar) vs `MarkdownResult.content: String` (scalar) — every `->content` access on EVERY type emitted `->getContent()`, including on types where the field is a real PHP property. Generated assertions then fail with "Call to undefined method MarkdownResult::getContent()" on every fixture that asserts a markdown body. Replaced the flat set with a `PhpGetterMap { getters: HashMap<String, HashSet<String>>, field_types: HashMap<String, HashMap<String, String>>, root_type: Option<String> }`: the renderer tracks a "current type" cursor that starts at the resolved root type and advances through `field_types` at each path segment, looking up `(current_type, field)` in `getters` for the correct per-type classification. `PhpCodegen::generate` builds the map from `type_defs` using the existing `is_php_scalar` predicate (a re-implementation of `is_php_prop_scalar_with_enums` already lived in the codegen) and derives the root type from any backend's `result_type` override (`php`/`c`/`csharp`/`java`/`kotlin`/`go`) or by matching `result_fields` against `TypeDef.fields`. Falls back to the legacy bare-name union when no root type can be determined. Surfaced on kreuzcrawl `E2E (php)` (CI run 26056619876) with 5 errors in `MarkdownTest`. (`src/e2e/field_access.rs`, `src/e2e/codegen/php.rs`)

## [0.16.56] - 2026-05-18

### Added

- **alef-backend-magnus: Ruby `.rbs` stubs now propagate Rust `///` doc comments for fields and methods**. `gen_stubs.rs` was already emitting struct/enum-level doc comments (via `rbs_doc_block.jinja`) but field-level and method-level docs were silently dropped. Field docs now precede each `attr_accessor`/`attr_reader` line and method docs precede each `def ... : (...) -> T` signature. Multi-line docs render as multiple `#` lines; empty lines become bare `#` (matching RBS comment-block conventions). Brings Ruby `.rbs` coverage in line with Python `.pyi` (both now ship every public type/field/method docstring inherited from the upstream Rust source). (`src/backends/magnus/gen_stubs.rs`)

## [0.16.55] - 2026-05-18

### Fixed

- **alef-backend-php: non-scalar field getters now emit as plain PHP methods (`getCamelCase()`), not `#[php(getter)]` property accessors**. ext-php-rs-derive 0.11.7 leaves `get_method_props` as `todo!()`, so `#[php(getter)]` registers ONLY as a property accessor (the runtime callable method is never registered) AND the property accessor itself is broken for non-scalar return types — every `$obj->nonScalarField` and `$obj->getNonScalarField()` raises a fatal "Call to undefined method" error. Drop `#[php(getter)]` from non-scalar field getters and emit a regular `pub fn getCamelCase(&self) -> T` so ext-php-rs surfaces them as callable PHP methods. The Rust ident is now `getPascalCase` (e.g. `getToolCalls`) so the PHP-side name matches the `$obj->getToolCalls()` shape `alef-e2e/src/codegen/php.rs` already emits for non-scalar fields. Property-style `$obj->camelCase` access for non-scalar fields is forgone as a result of the ext-php-rs limitation. Surfaced on liter-llm `E2E (php)` with 76 runtime errors on `getChoices()`, `getData()`, etc. (`src/backends/php/src/gen_bindings/types.rs`)

## [0.16.54] - 2026-05-18

### Fixed

- **alef-backend-csharp: tagged-enum file headers now `using System.Collections.Generic;`**. `gen_tagged_union` in `src/backends/csharp/gen_bindings/enums.rs` emitted `using System; using System.IO; using System.Text.Json; using System.Text.Json.Serialization;` but not `System.Collections.Generic`. Variants with `Dictionary<K, V>` fields (e.g. `CacheBackend.OpenDal { Dictionary<string, string> Config }`) then fail to compile with CS0246 "type or namespace name 'Dictionary<,>' could not be found". Added the missing using statement. Surfaced when exposing `CacheBackend` to C# via the Tower config DTOs. (`src/backends/csharp/gen_bindings/enums.rs`)

- **alef-backend-magnus: unit-variant subclasses no longer emit orphan `sig { void }`**. The generator unconditionally emitted `# @return [void]` + `sig { void }` + `def initialize() super() end` for every variant subclass, but rubocop's `Lint/UselessMethodDefinition` rule strips the trivial `def initialize` body during the regen format pass. The orphan `sig { void }` that remains then trips Sorbet's runtime check ("You called sig twice without declaring a method in between") and the entire `require 'liter_llm'` fails for any consumer of tagged data enums that have unit variants (e.g. `ResponseFormatText`, `ResponseFormatJsonObject`, `ResponseFormatJsonSchema`). Skip the initialize emission entirely when `variant.fields.is_empty()` so unit variants inherit the parent's no-arg initialize, with no orphan sig left behind. Surfaced on liter-llm `E2E (ruby)` (`ResponseFormatText` triggering `RuntimeError: You called sig twice…`). (`src/backends/magnus/gen_bindings/mod.rs`)

## [0.16.53] - 2026-05-18

### Added

- **alef-backend-pyo3: propagate Rust `///` doc comments into Python `.pyi` stubs as `"""..."""` docstrings**. `gen_stubs.rs` previously emitted only signatures (`class Foo:`, `model: str`) without any docstrings, leaving Python IDE hovers / `help()` output blank for every binding type and field. Added a small `pyi_docstring` helper that takes a Rust doc string, extracts the first paragraph, joins multi-line text with spaces, sanitises Unicode quote/dash characters via the existing `sanitize_python_doc` helper, escapes embedded `"""` and backslashes, and emits a single-line `"""…"""` indented for class-body inclusion. Hooked it into struct-class headers, struct field annotations, unit-enum class headers, and unit-enum variant declarations. Liter-llm regen now ships ~140 Python class/field docstrings inherited from the upstream Rust source. (`src/backends/pyo3/gen_stubs.rs`, `src/backends/pyo3/gen_bindings/enums.rs` — `sanitize_python_doc` and `class_name_to_docstring` promoted to `pub(crate)`)

## [0.16.52] - 2026-05-18

### Fixed

- **alef-backend-wasm: tagged-enum variant Map fields now bridge through `serde_wasm_bindgen` in both directions**. v0.16.51 added the core→binding `to_value` path for `tagged_enum_core_to_binding_expr` but the symmetric `tagged_enum_binding_to_core_expr` still emitted `val.field.clone().unwrap_or_default()` for non-optional Map fields, producing E0308 in the opposite direction ("expected `HashMap<String, String>`, found `JsValue`"). Added `TypeRef::Map` arms that deserialise the binding `Option<JsValue>` via `serde_wasm_bindgen::from_value` with a `Default` fallback. Surfaced when exposing `liter_llm::tower::CacheBackend::OpenDal { config: HashMap<String, String> }` as a Wasm DTO — the v0.16.51 fix unblocked the From<core> impl but the From<WasmCacheBackend> impl still failed. (`src/backends/wasm/gen_bindings/enums.rs`)

## [0.16.51] - 2026-05-18

### Fixed

- **alef-backend-wasm: tagged-enum variant Map fields now use `serde_wasm_bindgen::to_value` instead of passing the raw HashMap**. `tagged_enum_core_to_binding_expr` in `src/backends/wasm/gen_bindings/enums.rs` handled `Named` and `Optional<Named>` field shapes for enum variants emitted as flat-struct (the `ConversionConfig::map_uses_jsvalue` path used elsewhere), but `Map<K, V>` (`HashMap`) fell through to the catch-all and the generated `From<core::Enum> for WasmEnum` impl emitted `Some(hashmap)` against an `Option<JsValue>` binding field, producing E0308 ("expected `JsValue`, found `HashMap<...>`"). Added explicit `TypeRef::Map` arms (optional and non-optional, plus the wrapped-Optional case) that serialise the HashMap via `serde_wasm_bindgen::to_value`. Surfaced when exposing `liter_llm::tower::CacheBackend::OpenDal { config: HashMap<String, String> }` as a Wasm DTO. (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-backend-swift, alef-docs: bridge data-enum function parameters without stale docs**. Swift free-function shims now declare `Named(enum)` and `Vec<Named(enum)>` parameters as JSON strings at the swift-bridge boundary and deserialize them back to the source enum before calling Rust, avoiding invalid `.0` wrapper unwrapping for data enums such as `PageAction`. Per-language API docs now respect language-specific and FFI-backed `exclude_functions` / `exclude_types` so generated references no longer advertise functions or types omitted from that language surface. (`src/backends/swift/src/gen_rust_crate/{extern_block,mod,shims}.rs`, `crates/alef-docs/src/lib.rs`)

- **alef-backend-wasm: async functions with `Vec<Named>` parameters now convert binding vectors before calling core Rust**. The async function path already detected `Vec<Named>` as needing a let-binding, but only emitted serde recovery bindings for direct `Named` params. Generated WASM code could therefore pass `Vec<WasmPageAction>` into a core function expecting `Vec<PageAction>`. The binding now emits an `{param}_core` vector by mapping each element with `Into::into`, matching the synchronous path. (`src/backends/wasm/gen_bindings/functions.rs`)

## [0.16.50] - 2026-05-18

### Fixed

- **alef-backend-swift: methods returning first-class DTOs now wrap bridge return in `try T(bridge)` constructor**. v0.16.46 fixed input-side first-class DTOs (call sites correctly pass either `try x.intoRust()` or the raw value depending on classification) but symmetric output handling was missing. When a method returns `T` and `T` is a first-class Swift struct (Codable, all-primitive fields), the bridge function returns the opaque `RustBridge.T` — but the Swift signature returns `LiterLlm.T`, so the bare `return try await RustBridge.foo(...)` fails to type-check ("cannot convert return expression of type 'RustBridge.FileObject' to return type 'LiterLlm.FileObject'"). When the return type is a first-class DTO, wrap the bridge call in `try {ReturnType}(…)` using the existing `init(_ rb: RustBridge.T) throws` extension emitted by `emit_first_class_struct`. The `throws` clause is also added to the method signature when only the return-init needs it. Surfaced on liter-llm `E2E (swift)` after v0.16.47 with 3 errors on `defaultClientCreateFile` / `defaultClientRetrieveFile` / `defaultClientDeleteFile`. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-codegen: gate untagged-enum tuple-form conversion match arms on backend opt-in (`ConversionConfig::binding_tuple_form_for_untagged_variants`)**. v0.16.46 switched the conversion arms to tuple-form whenever `enum_serde_untagged && variant.is_tuple`, but that assumed every backend's enum body emitter does the same. Only Magnus does — every other data-bearing backend (Rustler/Elixir, NAPI, PyO3, etc.) keeps struct-form variants regardless of `serde_untagged`. The mismatch surfaced on liter-llm's E2E (elixir) job with E0164 / E0533 ("expected tuple struct or tuple variant, found struct variant `RerankDocument::Text`"). Add a new `ConversionConfig` field, default false; Magnus is the only backend that sets it true. Counter-regression test asserts backends without the opt-in keep struct-form arms. (`src/codegen/conversions/mod.rs`, `src/codegen/conversions/enums.rs`, `src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-go: method signatures now emit on a single canonical line, removing the dependency on gofmt for test assertions**. `gen_method_wrapper` previously built function signatures across three separate template fragments (receiver, params appended inline, closing paren + return type on a new line), producing `func (r *T) Foo(\n) RetType {` for zero-parameter methods. Tests asserting `contains("func (r *T) Foo()")` then depended on gofmt collapsing the newline — failing on CI macos-latest runners where gofmt availability differed. Two new templates (`method_signature_instance.jinja`, `method_signature_static.jinja`) emit the full signature on one canonical line, matching the existing `function_signature.jinja` and `streaming_method_signature.jinja` patterns. gofmt is now a no-op for method signatures. Also fixes `output_dir` path concatenation: commit 0020ca74 replaced `PathBuf::join` with `format!("{output_dir}{file}")`, but `resolve_output_dir` returned paths without a trailing slash (e.g. `"packages/go"`), producing `"packages/gobinding.go"` instead of `"packages/go/binding.go"`. `PathBuf::ends_with("binding.go")` then returned `false` (last component was `"gobinding.go"`), causing all `go_emission_test.rs` tests to panic on `unwrap()`. `output_dir` is now normalised to always end with `/` before use. Removes the `#[cfg_attr(target_os = "macos", ignore)]` guard on `test_unmarshal_bytes_returns_slice_not_pointer`. (`src/backends/go/src/gen_bindings/methods.rs`, `src/backends/go/src/gen_bindings/mod.rs`, `src/backends/go/src/template_env.rs`, `src/backends/go/templates/method_signature_{instance,static}.jinja`, `src/backends/go/tests/`)

- **alef-e2e php: emit `->getCamelCase()` getter-method syntax for non-scalar fields instead of `->camelCase` property syntax**. ext-php-rs-derive 0.11.7 (the latest release compatible with ext-php-rs 0.15.4) leaves `get_method_props` as `todo!()` in its derive macro, so `#[php(getter)]` methods are never surfaced as magic PHP properties — accessing `$obj->nonScalarField` on a field whose Rust getter is `#[php(getter)] pub fn getNonScalarField()` raises a fatal error at runtime. Scalar fields continue to work because `#[php(prop)]` uses a different, working code path. Prior to this fix, every e2e assertion that accessed a Named-struct, `Vec<Named>`, or `Map` field via dot-notation (`$result->metadata->title`) would fail for any kreuzcrawl consumer that targets ext-php-rs 0.15.4. The fix adds a `php_getter_fields: HashSet<String>` to `FieldResolver` (populated via the new `new_with_php_getters` constructor) and a `render_php_with_getters` function that inspects each path segment: fields present in the set emit `->get{CamelCase}()`, all others emit `->camelCase`. The `PhpCodegen::generate` method now accepts the `type_defs` and `enums` IR slices (previously `_type_defs` / `_enums` unused) to compute the getter field set by running an `is_php_scalar` predicate (mirroring `is_php_prop_scalar_with_enums` from `alef-backend-php`) over every field of every exported type, and passes the set to `FieldResolver::new_with_php_getters`. A public `is_php_prop_scalar` wrapper is also exported from `alef-backend-php` as the canonical source of truth. Four unit tests cover: getter method for non-scalar field, property for scalar field, nested non-scalar + scalar chain, and array-field getter. (`src/e2e/field_access.rs`, `src/e2e/codegen/php.rs`, `src/backends/php/src/gen_bindings/types.rs`, `src/backends/php/src/lib.rs`)

- **alef-cli: `alef all` now always regenerates README files, fixing divergence with `alef readme`**. The `Commands::All` handler passed `overwrite=clean` to `write_scaffold_files_with_overwrite` for README files. When `alef all` is run without `--clean`, `clean=false`, so any README that already exists on disk (e.g. with table columns padded by `rumdl-fmt`) is silently skipped; `finalize_hashes` then re-stamps the hash on the padded content, making it appear valid. The next `alef readme` run (which always uses `overwrite=true`) regenerates compact content from the template and writes different bytes, producing an observable divergence between the two commands. README files are always regenerated from templates — unlike scaffold-once files (Cargo.toml, package.json) they must not preserve user edits — so the fix is to unconditionally pass `overwrite=true`. Regression-covered by `readme_overwrite_false_preserves_existing_content_producing_divergence`. Surfaced on html-to-markdown where `packages/python/README.md` differed between `alef generate` and `alef readme` runs. (`crates/alef-cli/src/main.rs`, `crates/alef-cli/src/pipeline/generate.rs`)

- **alef-backend-rustler: switch NIF trait-bridge templates to `.to_owned()` and bind the outer `spawn_blocking` `JoinResult`**. The four `*_method_body.rs.jinja` templates (`sync_method_body`, `trait_sync_method_body`, `trait_async_method_body`, plus the embedded strings in `src/template_env.rs`) emitted `.clone()` on `&[u8]` / `&str` parameters and left the outer `spawn_blocking(...).await` `Result<(), JoinError>` unused. With consumer `-D warnings` on Windows MSVC (kreuzberg's CI), the resulting `noop_method_call` and `unused_must_use` lints fired as errors. Switch parameter copies to `.to_owned()` — returns `Vec<u8>` / `String` for slice refs and falls back to `Clone` for owned types — and bind the `spawn_blocking` `JoinResult` with `let _` so `must_use` is satisfied. (`src/backends/rustler/src/template_env.rs`)

- **alef-backend-dart, alef-backend-go: emit forward-slash `GeneratedFile.path` values on Windows**. `PathBuf::from(dir).join(file)` uses the OS separator, producing mixed paths like `packages/dart/lib/src\traits.dart` when the directory string already contained forward slashes from `resolve_output_dir`. Tests asserting on `to_string_lossy().ends_with("/src/foo.dart")` then failed on Windows. Replace with `PathBuf::from(format!("{dir}/{file}"))` throughout `alef-backend-dart` (`gen_bindings`, `gen_ffi`, `gen_rust_crate`) and `alef-backend-go` (`gen_bindings`). `GeneratedFile.path` values are repo-relative virtual paths, not OS-native paths; forward slashes work for file I/O on all platforms including Windows. Fixes 6 `windows-latest` test failures across `alef-backend-dart` and `alef-backend-go`. (`src/backends/dart/src/gen_bindings/mod.rs`, `src/backends/dart/src/gen_ffi/mod.rs`, `src/backends/dart/src/gen_rust_crate/{cargo,mod}.rs`, `src/backends/go/src/gen_bindings/mod.rs`)

## [0.16.47] - 2026-05-18

### Fixed

- **alef-backend-magnus: `Default` impl for `#[serde(untagged)]` enums with a tuple-form first variant now emits tuple-form constructor**. Companion to v0.16.46's conversion-arms fix. `first_variant_default` in `src/backends/magnus/gen_bindings/classes.rs` unconditionally formatted the default body as struct-form (` {{ _0: Default::default() }}`), so the generated `impl Default for {Enum}` line read `Self::Single { _0: Default::default() }` for tuple variants — failing to compile with E0559 + E0769 the same way the From/Into match arms did before v0.16.46. Branch on `enum_def.serde_untagged && first.is_tuple` so untagged enums emit `Self::Single(Default::default())` instead. Surfaced on liter-llm regen against v0.16.46 producing 6 fresh Magnus compile errors (one per untagged enum: `EmbeddingInput`, `ModerationInput`, `RerankDocument`, `StopSequence`, `ToolChoice`, `UserContent`). (`src/backends/magnus/gen_bindings/classes.rs`)

- **alef-scaffold node: include `aarch64-pc-windows-msvc` in generated `crates/{name}-node/package.json` `napi.targets`**. The scaffold listed five targets (linux x64/arm64 gnu, darwin x64/arm64, win32 x64-msvc) but omitted `aarch64-pc-windows-msvc`, even though the runtime dispatch `index.js` already routes to `{binary}.win32-arm64-msvc.node` / `{binary}-win32-arm64-msvc` optional dep. Consumer publish workflows (e.g. tslp) build the win32-arm64-msvc artifact and then call `napi create-npm-dirs` + `napi artifacts`, which use `napi.targets` as the source of truth and skip creating the platform package directory. `napi artifacts` then fails with `Internal Error: No dist dir found for .../{binary}.win32-arm64-msvc.node`, blocking the Publish Node.js to npm job. Add the missing triple so the platform package is created and the win32-arm64-msvc binary lands in it. Surfaced on tslp `Publish Release` dry-run (run `26020821545`). (`crates/alef-scaffold/src/languages/node.rs`)

## [0.16.46] - 2026-05-18

### Fixed

- **alef-docs: `clean_doc_inline` double-escaped pipes in logical-OR expressions, causing MD056 table cell count violations**. `clean_doc_inline` applied `.replace('|', "\|")` internally, but every call site in `lib.rs` then immediately passes the result through `escape_table_cell`, which also replaces `|` with `\|`. For a doc string containing `||` (e.g. `rows == 0 || cols == 0`), this two-pass escaping produced `\|\|`: CommonMark parses `\` as an escaped backslash (literal `\`) and the following bare `|` as a cell separator, growing a 4-column table row to 6 cells and triggering rumdl MD056. Removed the `.replace` from `clean_doc_inline`, added a doc comment explaining the invariant, and updated the unit test in `doc_cleaning.rs` to assert that `clean_doc_inline` leaves pipes unescaped while the full pipeline (`escape_table_cell(clean_doc_inline(...))`) escapes them exactly once. Surfaced on html-to-markdown CI Lint (rumdl MD056 in `docs/reference/configuration.md`, `api-*.md`, `types.md`). (`crates/alef-docs/src/doc_cleaning.rs`)

- **alef-codegen: untagged-enum From/Into match arms now emit tuple-form for tuple variants**. Commit a715f378 (v0.16.41) made the Magnus enum body template emit tuple-form `Variant(T)` for `#[serde(untagged)]` enums (required so serde untagged can deserialise scalar variants like `RerankDocument::Text(String)`), but the conversion match arms in `src/codegen/conversions/helpers.rs` continued to destructure/construct as struct-form `Variant { _0 }`. Generated Magnus binding code then failed to compile with E0559 `variant 'X::Y' has no field named '_0'` and E0769 `tuple variant written as struct variant` (28 errors on liter-llm). Threads `enum_serde_untagged: bool` through `binding_to_core_match_arm_ext_cfg` and `core_to_binding_match_arm_ext_cfg`; the single call site in `enums.rs` passes `enum_def.serde_untagged`. When set, both helpers emit tuple-form patterns and constructors for tuple variants; tagged/unit enums retain the existing struct-form behaviour. Regression tests in `conversions/mod.rs` cover both directions plus a counter-regression that tagged enums still get struct-form. Surfaced on liter-llm `E2E (ruby)`. (`src/codegen/conversions/helpers.rs`, `src/codegen/conversions/enums.rs`, `src/codegen/conversions/mod.rs`)

- **alef-backend-swift: `first_class_types` set now matches actual emission decision, fixing missing `intoRust()` and `Decodable` conformance errors**. The `first_class_types` collection used by Swift method/streaming code generation was filtered with `!is_opaque && has_serde && !fields.is_empty()`, but the actual struct-vs-typealias emission decision (`can_emit_first_class_struct`) additionally requires every field to be primitive/optional/string and a bulk constructor to be emittable. Types failing the stricter check (e.g. `ChatCompletionRequest`, `CreateBatchRequest`, `ChatCompletionChunk`, any DTO with Vec/Map/Named fields) were emitted as `typealias X = RustBridge.X` — but `first_class_types` still considered them first-class, so call sites emitted `try req.intoRust()` (no such method on the typealias) and streaming code emitted `JSONDecoder().decode(ChunkType.self, …)` (opaque RustBridge classes can't auto-conform to Decodable). Align both filters by reusing `can_emit_first_class_struct` to build the set. Additionally, optional `Named` parameters that ARE first-class now emit `try query?.intoRust()` instead of `try query.intoRust()` so the call site type-checks against the swift-bridge-emitted `Optional<RustBridge.T>` argument. Surfaced on liter-llm `E2E (swift)` with `value of type 'CreateBatchRequest' has no member 'intoRust'`, `cannot convert return expression`, and `type 'ChatCompletionChunk' does not conform to 'Decodable'`. (`src/backends/swift/src/gen_bindings.rs`)

## [0.16.45] - 2026-05-18

### Fixed

- **alef-backend-rustler: insert blank line between concatenated elixir streaming wrappers**. The `elixir_streaming_start_wrapper.jinja` and `elixir_streaming_next_wrapper.jinja` templates rely on trailing blank lines to separate from the following `@doc`/`def` block, but the shared `end-of-file-fixer` pre-commit hook strips trailing blank lines from template source files. The runtime concatenation in `gen_bindings/mod.rs` therefore produced `end\n@doc "..."` (no blank line), which fails `mix format --check-formatted` ("a blank line is required before each `@doc` that precedes a function definition"). Insert the separator explicitly via `content.push('\n')` between successive `template_env::render` outputs so the boundary is independent of template file contents. Surfaced on liter-llm CI Lint (`packages/elixir/lib/liter_llm.ex`). (`src/backends/rustler/src/gen_bindings/mod.rs`)

## [0.16.44] - 2026-05-18

### Fixed

- **alef-e2e kotlin_android: drop redundant `repositories { mavenCentral(); google() }` block from emitted `build.gradle.kts`**. The v0.16.39 settings.gradle.kts emission sets `dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) }` and declares `google()` + `mavenCentral()` centrally. Leaving the per-project `repositories { ... }` block in `build.gradle.kts` triggers Gradle "Build was configured to prefer settings repositories over project repositories but repository 'MavenRepo' was added by build file 'build.gradle.kts'" at the `e2e/kotlin_android/` configuration phase. Surfaced on liter-llm's `E2E (kotlin_android)` CI job following the v0.16.43 regen. (`src/e2e/codegen/kotlin_android.rs`)

## [0.16.43] - 2026-05-18

### Changed

- **publish workflow: move all Linux jobs to self-hosted runners (`runner-medium`, `runner-medium-arm64`) and reorder so `publish-crates` runs before `build-cli`**. Two related infrastructure changes to `.github/workflows/publish.yaml`. (1) Every `ubuntu-latest` job (prepare, validate-versions, check-cratesio, check-github-release, build-homebrew-bottles, upload-release-assets, publish-crates, publish-homebrew, finalize) now targets the self-hosted `runner-medium` label; the `build-cli` matrix uses `runner-medium` for `linux-x86_64` and `runner-medium-arm64` for `linux-aarch64`. macOS (`macos-latest`, `macos-15-intel`) and Windows (`windows-latest`) matrix entries continue to use GitHub-hosted runners. Motivation: GitHub-hosted Linux runners were queuing for 4+ hours behind unrelated workloads, blocking v0.16.41/42/43 from publishing. (2) `build-cli` now `needs: [..., publish-crates]` with `(publish-crates.result == 'success' || == 'skipped')` gating, so the canonical crates.io artifact lands before any platform binary work begins — a crates.io failure (network, dupe version, manifest error) no longer wastes ~30 min of CLI matrix build time. `upload-release-assets` and `build-homebrew-bottles` already transitively depend on `build-cli` so they inherit the new ordering. (`.github/workflows/publish.yaml`)

### Fixed

- **alef-backend-rustler: `gen_rustler_flat_data_enum_from_core` / `_to_core` now use the enum's full `rust_path` instead of `{core_import}::{name}`**. The two helpers in `src/backends/rustler/src/gen_bindings/types.rs` unconditionally hardcoded `format!("{core_import}::{name}")` for the core-side type path. For enums re-exported at the crate root (e.g. `kreuzberg::FormatMetadata`) the short form happens to compile, but for enums living in a nested module (e.g. `DrawingType` at `kreuzberg::extraction::docx::drawing::DrawingType`) the generated `impl From<kreuzberg::DrawingType>` references a path that does not exist, and the consumer NIF crate fails to build with `cannot find DrawingType in kreuzberg`. The fix routes both helpers through `alef_codegen::conversions::core_enum_path`, which preserves `enum_def.rust_path` when it is already fully qualified (matching the alef-backend-wasm path used by the other discovered data enums). Locked in with `test_flat_data_enum_from_core_uses_full_rust_path` asserting the generated `impl From<…>` carries the full module path and never collapses to `kreuzberg::<Name>`. Surfaced on kreuzberg's `E2E (elixir)` and `Build Elixir NIF` publish jobs failing to compile `packages/elixir/native/kreuzberg_nif/src/lib.rs`. (`src/backends/rustler/src/gen_bindings/types.rs`)

- **alef-cli: `finalize_hashes` no longer applies `normalize_whitespace` to non-Rust files**. Language-native formatters (`gofmt`, `oxfmt`/oxlint, `php-cs-fixer`, `ruff`, etc.) have already produced idempotent canonical output by the time `finalize_hashes` runs; the additional `normalize_whitespace` pass mutated that output (e.g. collapsing blank lines that `php-cs-fixer` intentionally inserts after `<?php`) so the on-disk bytes after finalisation differed from what prek's formatter hooks would produce, causing `alef verify` to report a stale hash on every `prek` run. The fix restricts `normalize_whitespace` to `.rs` files only (where it is needed to match a subsequent `cargo fmt`); all other languages hash the raw stripped on-disk content directly, matching what `compute_file_hash` in the verify path computes. Regression-covered by `test_finalize_hashes_non_rust_skips_normalize_whitespace`. Surfaced on html-to-markdown `alef verify` reporting `crates/html-to-markdown-node/index.d.ts` and `packages/go/visitor.go` as stale after every `prek` run. (`crates/alef-cli/src/pipeline/generate.rs`)

## [0.16.42] - 2026-05-18

### Changed

- **chore: adopt gh-actions-updater** — add `gh-actions-updater` pre-commit hook and `gh-actions-updater --update .` step in the `update` task to keep GitHub Actions pins current. (`.pre-commit-config.yaml`, `Taskfile.yml`)

### Fixed

- **alef-e2e kotlin_android: migrate the emitted `build.gradle.kts` to the Kotlin 2.x `compilerOptions` DSL**. The legacy `android { kotlinOptions { jvmTarget = "<n>" } }` block was removed as an error in Kotlin 2.x ("Using 'jvmTarget: String' is an error. Please migrate to the compilerOptions DSL"), failing the kotlin_android e2e project at script compilation. Drop the legacy block and emit `kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_<n> } }` at the top level — the `org.jetbrains.kotlin.gradle.dsl.JvmTarget` import was already present in the scaffold. Surfaced on liter-llm's `E2E (kotlin_android)` CI job after Kotlin Gradle plugin upgrade. (`src/e2e/codegen/kotlin_android.rs`)

## [0.16.41] - 2026-05-18

### Fixed

- **alef-backend-go: decode `Vec<DataEnum>` and scalar-tuple variants for `#[serde(untagged)]` enums**. Two related bugs in the Go codegen for untagged enums whose variants include both scalar-tuple shapes (e.g. `Text(String)`) and named struct shapes (e.g. `Object { text: String, … }`): (1) the struct-level custom `UnmarshalJSON` only handled `<DataEnum>` and `Option<DataEnum>` fields; `Vec<DataEnum>` fell through and Go's default unmarshal of a JSON array into `[]<sealed-interface>` failed because Go interfaces are opaque to `encoding/json`. Extended `DataEnumField` with `is_slice`, the raw-shadow struct emission to use `[]json.RawMessage` for slice fields, and the decode loop to iterate the array and call `Unmarshal<Enum>(item)` per element. (2) `gen_data_enum_type` unconditionally skipped tuple fields (`_0`, `_1`, …) on the per-variant struct, so a variant like `Text(String)` ended up as `type RerankDocumentText struct {}` with nowhere to land the inner String. Detect single-tuple-field variants of primitive/String/Char/Path type on untagged enums and emit a named `Value` field of the matching Go type, plus a custom `MarshalJSON`/`UnmarshalJSON` that bypasses the tag-wrapping aux struct and emits/reads the bare JSON scalar. Surfaced on liter-llm's `RerankRequest.documents: []RerankDocument` Go e2e suite. (`src/backends/go/src/gen_bindings/types.rs`)

- **alef-backend-magnus: emit `#[serde(untagged)]` + tuple-form variants for tuple variants in untagged enums**. The Ruby native-extension enum template was missing the `#[serde(untagged)]` attribute for `#[serde(untagged)]` enums, and unconditionally emitted struct-form variants (`Text { _0: String }`) for tuple variants — regardless of the serde representation. The result was that an untagged enum like `enum RerankDocument { Text(String), Object { text: String, … } }` serialised on the wire as `{"_0":"hello"}` / `{"text":"hello"}` instead of the expected `"hello"` / `{"text":"hello"}`. The Ruby e2e suite, which passes Ruby Strings into `RerankRequest.new(documents: ['…'])`, then failed because serde's untagged-deserialiser saw a bare JSON string and rejected it (no struct-form variant matched). Thread `enum_def.serde_untagged` into the template context, add `is_tuple` to per-variant context, emit `#[serde(untagged)]` when set, and emit tuple-form (`Variant(T)`) instead of struct-form (`Variant { _0: T }`) for tuple variants _only when_ the enum is untagged. Externally and internally tagged enums keep struct-form so the existing `gen_enum_from_*_match_arm` helpers continue to work. (`src/backends/magnus/gen_bindings/classes.rs`, `src/backends/magnus/templates/enum_magnus.rs.jinja`)

- **alef-backend-kotlin-android: suppress 3 more ktlint rules tripped by generated DTOs**. ktlint 1.x's standard ruleset flags `blank-line-before-declaration`, `no-semi`, and `statement-wrapping` on alef-emitted Kotlin data classes (sealed-class variants without blank lines between them, trailing semicolons inside enums, and chained statements without explicit wrapping). All three are inherent to generated code and not worth a full reformatter post-processing step — extend the existing `@file:Suppress` block to silence them. Surfaced on liter-llm's `packages/kotlin-android/.../LiterLlmError.kt` (24× `blank-line-before-declaration`), `AuthHeaderFormat.kt` (2× `no-semi`, `statement-wrapping`), and `EmbeddingFormat.kt`. Aligned with the existing precedent set by 0fc9719e and bd2f712d. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-jni: allow `let_and_return` clippy lint on `jboolean` shims**. The boolean-conversion shims emit `let result: jboolean = …; result` which clippy's `let_and_return` rejects. Suppress via `#[allow(clippy::let_and_return)]` rather than restructure since the explicit type annotation is load-bearing.

## [0.16.40] - 2026-05-18

### Fixed

- **alef-e2e swift: emit e2e package under `e2e/swift_e2e/` instead of `e2e/swift/` to avoid SwiftPM 6.0 identity collision with `packages/swift/`**. SwiftPM 6.0 derives path-based dependency identity from the path's basename and silently ignores any `.package(name: "...", path: "...")` override (deprecated since 5.5). When the e2e consumer and the binding dep both live in directories named `swift/`, SwiftPM treats them as the same package and resolution fails with `product 'X' required by package 'swift' target '...' not found in package 'swift'`. Surfaced on kreuzberg's `cd e2e/swift && swift build` step. The fix moves the emitted consumer package to `<output>/swift_e2e/`, drops the deprecated `name:` parameter on `.package(path:)`, and uses the path basename (`swift`) for the `.product(package:)` reference — the product `name:` still matches the binding's library declaration (`Kreuzberg`, `LiterLlm`, etc). Consumers must update `[crates.test.swift] e2e` in their `alef.toml` to `cd e2e/swift_e2e && swift test`. Locked in with `package_swift_uses_path_basename_for_product_package_ref` regression test asserting emit path, no `.package(name:`, `.package(path:)`, and basename-based `.product(package:)`. (`src/e2e/codegen/swift.rs`, `src/e2e/tests/swift_client_factory.rs`)

- **alef-backend-php: emit PSR-12-compliant blank lines after `declare(strict_types=1);` and `namespace X;`** in the facade class, stubs file, and every opaque class file. Previously the three code paths in `gen_bindings/mod.rs` concatenated these statements with no intervening blank line, causing php-cs-fixer to rewrite every generated PHP file and diverge from the alef-managed hash. Fixed by inserting `content.push('\n')` after each relevant render call. Surfaced on html-to-markdown CI Lint (`packages/php/src/HtmlToMarkdown.php`, `VisitorHandle.php`). (`src/backends/php/src/gen_bindings/mod.rs`)

- **alef-backend-pyo3: emit formatter-idempotent `__init__.py`, `api.py`, `exceptions.py`, and `options.py`** to fix ruff/isort violations in generated Python bindings. (1) `__init__.py`: added blank line after module docstring; reordered imports so `._native` (underscore) comes before `.api`/`.exceptions`/`.options` (isort underscore-before-letter); case-insensitive sort for native symbols (`VisitorHandle < VisitResult`). (2) `api.py`: blank line between absolute `import X as _rust` and relative `from .Y import` lines (isort). (3) `exceptions.py`: two leading blank lines moved into `exception_variant_class.jinja` (E302). (4) `options.py`: blank line after enum/dataclass docstrings (E303); blank line before relative imports; fixed `referenced_types` collection to skip `*Update` types and non-emitted return types, eliminating phantom `TYPE_CHECKING` imports (F401). (`src/backends/pyo3/gen_bindings/{errors,functions,types}.rs`, `src/backends/pyo3/templates/exception_variant_class.jinja`)

- **alef-backend-rustler: emit mix-format-idempotent Elixir for visitor-bearing functions**: blank line after `Map.pop` before `if is_map(visitor) do`; conditional multi-line formatting for the `{:ok, _} = Module.fn_with_visitor(...)` call when it exceeds 98 chars; blank line before `do_visitor_receive_loop`; 6-space indent for the `else` branch body. Surfaced on html-to-markdown CI Lint (`packages/elixir/lib/html_to_markdown.ex`). (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-e2e python: emit pyproject-fmt-idempotent `e2e/python/pyproject.toml`**: `packages = []` (was `[  ]`); dependency list reordered to put the bound package name first (pyproject-fmt alphabetical ordering). (`src/e2e/codegen/python/config.rs`)

- **alef-backend-rustler: drop unused `mut` on `args` in trait-bridge method bodies when the method takes no parameters**. The four `*_method_body.rs.jinja` templates (`sync_method_body`, `trait_sync_method_body`, `trait_async_method_body` — both as standalone files under `templates/` and as embedded strings in `src/template_env.rs`) unconditionally emitted `let mut args = serde_json::Map::new();`. For Plugin trait methods like `initialize()` and `shutdown()` (and any other no-arg trait method) the `args.insert(...)` loop expands to nothing, so the `mut` binding is never used and rustc/clippy flag every emission with `warning: variable does not need to be mutable`. Surfaced on kreuzberg's generated `packages/elixir/native/kreuzberg_nif/src/lib.rs:5159` (initialize) and `:5196` (shutdown) and propagates to every plugin trait bridge (OcrBackend/PostProcessor/Validator/EmbeddingBackend/DocumentExtractor/Renderer). The templates now gate `mut` on the presence of params (`let {% if params %}mut {% endif %}args = …`), so no-arg methods emit `let args = …` and parameterised methods continue to emit `let mut args = …`. Locked in with a regression assertion in `test_plugin_bridge_with_super_trait_generates_plugin_impl` (which uses no-arg `initialize`/`shutdown` via `super_trait = "Plugin"`). (`src/backends/rustler/templates/{sync,trait_sync,trait_async}_method_body.rs.jinja`, `src/backends/rustler/src/template_env.rs`, `src/backends/rustler/tests/trait_bridge_test.rs`)

## [0.16.39] - 2026-05-18

### Fixed

- **alef-backend-wasm: drop `js_name = "<unprefixed>"` override on emitted struct/opaque/enum types so the JS class name matches the Rust struct name (the prefixed form, e.g. `WasmChatCompletionRequest`)**. Previously, wasm-bindgen exported `Wasm<T>` as `T` (unprefixed) via `js_name`, but alef-e2e's TypeScript test generator imports the prefixed Rust identifier (`WasmChatCompletionRequest`). The mismatch made every wasm e2e import resolve to `undefined`, so the first method call (e.g. `WasmChatCompletionRequest.default()`) failed with `TypeError: Cannot read properties of undefined (reading 'default')`. Removing the `js_name` override aligns the JS API with the e2e import shape. Consumers that want a custom JS class name can still configure `[crates.wasm] type_prefix` (e.g. `""` for unprefixed). Surfaced on every liter-llm wasm e2e fixture. (`src/backends/wasm/templates/{gen_struct,gen_opaque_struct}.jinja`, `src/backends/wasm/gen_bindings/enums.rs`)

- **alef-e2e elixir: pass `base_url` as a keyword option to `create_client/2` instead of a bare positional string**. The generated Elixir wrapper signature is `create_client(api_key, opts \\ [])` with `base_url = Keyword.get(opts, :base_url)`, but the e2e codegen was emitting `Module.create_client("test-key", url_string)` — the bare string got passed into `Keyword.get`, which crashes with `FunctionClauseError: no function clause matching in Keyword.get/3`. The codegen now emits `Module.create_client("test-key", base_url: url_value)` everywhere and handles the env-var-fallback branch (real-API vs mock) by building an `[base_url: …]` keyword list (or `[]` when the real API key is present so no base_url override applies). Surfaced as 161× `FunctionClauseError` across liter-llm's Elixir e2e fixtures. (`src/e2e/codegen/elixir.rs`)

- **alef-e2e kotlin_android: emit `settings.gradle.kts` with `pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }` alongside the existing `build.gradle.kts`**. Without `settings.gradle.kts` declaring the plugin repositories, Gradle cannot resolve `com.android.library` and the e2e project fails at configuration time with `Plugin [id: 'com.android.library', version: '8.7.3'] was not found in any of the following sources`. The new `render_settings_gradle_kotlin_android` helper mirrors the AAR-side `alef-backend-kotlin-android::gen_settings_gradle` template and names the project `<pkg_name>-e2e`. Locked in with a unit test asserting `pluginManagement`, `google()`, `gradlePluginPortal()`, and the correct `rootProject.name`. Surfaced on liter-llm's `E2E (kotlin_android)` CI job. (`src/e2e/codegen/kotlin_android.rs`)

- **alef-e2e wasm: honor `[crates.wasm] type_prefix` from `alef.toml` in TypeScript e2e generator instead of hardcoding `"Wasm"`** (#27): the wasm binding backend already respects `config.wasm_type_prefix()` when generating bindings (default `"Wasm"`, but can be customized via `[crates.wasm] type_prefix`), but the WASM e2e test generator hardcoded the prefix in all `strip_prefix("Wasm")` calls and the `wasm_class_name` helper. Consumers that customized the prefix (e.g. kreuzcrawl set it to `""`) were forced to ship workaround re-export wrappers like `index.js` to alias generated type names. The fix threads `config.wasm_type_prefix()` from `wasm.rs::generate()` into the TypeScript renderer and replaces all hardcoded `"Wasm"` literals with the configurable prefix. Function signatures updated: `render_test_file`, `render_test_case`, `ts_builder_expression`, `ts_builder_expression_inner`, `build_args_and_setup`, `is_tagged_data_enum`, `derive_nested_types_for_wasm`, `collect_transitive_nested_types_for_wasm`, and `wasm_class_name`. Default behavior unchanged; projects using the default `"Wasm"` prefix get identical output. Added regression tests `wasm_test_file_respects_empty_type_prefix`, `wasm_test_file_respects_custom_type_prefix`, and `wasm_test_file_defaults_to_wasm_prefix` to lock in the configurable behavior. (`src/e2e/codegen/typescript/test_file.rs`, `src/e2e/codegen/typescript/mod.rs`, `src/e2e/codegen/wasm.rs`)

- **alef-cli `sweep_orphans` + `cleanup_orphaned_files`: ownership detection requires the `// alef:hash:` line** (#26): both helpers previously accepted any file containing the loose substring `"auto-generated by alef"` or `"DO NOT EDIT"` as alef-owned and therefore deletable. That set matched countless vendored consumer files (cgo headers, swig output, autoconf artefacts, hand-written DO-NOT-EDIT shims), so a stray `alef regen` could silently delete them. Both helpers now defer to `alef_core::hash::extract_hash` — only files carrying the cryptographic `// alef:hash:<hex>` line that alef's own finalizer appends are eligible for sweeping. Surfaced by kreuzcrawl's vendored `packages/go/include/kreuzcrawl.h` and `packages/go/internal/ffi/kreuzcrawl.h` cgo headers being deleted on every regen. (`crates/alef-cli/src/pipeline/cleanup.rs`, `crates/alef-cli/src/pipeline/generate.rs`)

## [0.16.38] - 2026-05-18

> **Note:** the `alef-e2e wasm type_prefix` (#27) and `sweep_orphans` (#26) fixes previously listed in this section were re-categorised to `[Unreleased]` because the publish workflow for v0.16.38 was cancelled before the source-code changes for those fixes were committed. Both fixes ship in v0.16.39.

### Fixed

- **alef-backend-go: trait-bridge interfaces and trampolines fall back to `json.RawMessage` for excluded named types** — when a Rust trait method (e.g. `DocumentExtractor::extract_bytes -> Result<InternalDocument>`, `Renderer::render(&InternalDocument)`) referenced a type that was excluded from the binding via `#[cfg_attr(alef, alef(skip))]`, the Go codegen emitted `InternalDocument` as the parameter/return type without ever defining that Go type, causing `undefined: InternalDocument` build errors. `gen_trait_bridges_file` now collects names from `ApiSurface::excluded_type_paths` and substitutes any `TypeRef::Named(n)` (or nested `Optional`/`Vec`/`Map` of one) with `TypeRef::Json`, which renders as `json.RawMessage` in the Go interface signature and trampoline declarations. Added explicit `TypeRef::Json` handling in `gen_param_conversion` so the trampoline copies the raw JSON payload through verbatim. Surfaced by kreuzberg's `DocumentExtractor` and `Renderer` trait bridges. (`src/backends/go/src/trait_bridge.rs`)

- **alef-backend-go: data-enum (sealed-interface) fields default to `nil` in `New{Config}` constructors instead of invalid `T{}` composite literal** — `gen_config_options` fell through to `format!("{}{{}}", go_type_name(name))` for any non-string-enum, non-passthrough Named field, producing `Sizing: ChunkSizing{}` / `Model: EmbeddingModelType{}`. Since those types are Go interfaces (one per variant struct, sealed via an `is{Trait}()` marker), composite literals are invalid (`invalid composite literal type ChunkSizing`). Added an explicit branch that checks `data_enum_names` and emits `nil` (the interface zero value) instead. (`src/backends/go/src/gen_bindings/types.rs`)

- **alef-backend-go: `MarshalJSON` for optional `[]byte` fields no longer dereferences the slice** — for a Rust `Option<Bytes>` field the IR sets `field.optional = true` and `ty = Bytes`; the Go struct field is `[]byte` (slices are already nullable in Go, no pointer needed). The `struct_marshal_bytes_field_pointer.jinja` template wrongly wrote `*v.Field`, producing `invalid operation: cannot indirect v.Data (variable of type []byte)`. Dropped the dereference so the template works for both pointer and non-pointer bytes fields. Surfaced on kreuzberg's `EmailAttachment.data`. (`src/backends/go/templates/struct_marshal_bytes_field_pointer.jinja`)

- **alef-e2e elixir: honor `returns_void` on calls returning `Result<(), E>`**: `src/e2e/codegen/elixir.rs` unconditionally emitted `{:ok, result} = Module.fn(...)` whenever `returns_result` was true. For unit-returning fallible functions (Rust `Result<(), E>` → Elixir bare `:ok` on success) the pattern match `{:ok, result} = :ok` raises `MatchError` at runtime. The codegen now emits `:ok = Module.fn(...)` when `call_config.returns_void` is set, mirroring the Ruby/Java patterns. Surfaced on tree-sitter-language-pack's `download_configure_custom_dir`, `download_init_default`, `download_clean_cache` Elixir e2e fixtures.

- **alef-e2e java: array-field `contains`/`contains_all`/`not_contains` assertions now match case-insensitively against `String.valueOf(...)`**: `src/e2e/templates/java/assertion.jinja` emitted `{{ string_expr }}.toString().contains({{ java_val }})` when `field_is_array`. Java record `toString()` renders enum-typed fields in their SCREAMING_SNAKE_CASE form (e.g. `kind=FUNCTION`), so a fixture asserting `"Function"` would never match. Switched to `.toString().toLowerCase().contains(String.valueOf({{ val }}).toLowerCase())` so the fixture wording can match the enum variant regardless of casing convention. Surfaced on tree-sitter-language-pack's `process` fixtures asserting `structure` contains `"Function"`/`"Method"`/etc. (`src/e2e/templates/java/assertion.jinja`)

- **alef-e2e php: array-field `contains`/`contains_all` assertions handle object items without throwing on string cast**: `src/e2e/templates/php/assertion.jinja` emitted `(string)$item` inside the `foreach` loop, which raises `Error: Object of class <T> could not be converted to string` when array elements are objects without a `__toString` method (every alef-generated PHP data record). Switched to `is_object($item) ? json_encode($item) : (string)$item` and `stripos(...) !== false` for case-insensitive matching, mirroring the Java fix. Surfaced on tree-sitter-language-pack's `process` fixtures asserting `structure` contains a `StructureItem` kind name. (`src/e2e/templates/php/assertion.jinja`)

- **alef-backend-csharp: check `LastErrorCode()` after numeric `Result`-returning FFI calls in `gen_wrapper_function`**: free-function wrappers like `TreeSitterLanguagePackLib.Download(...)` only consulted `nativeResult == IntPtr.Zero` to detect errors. For functions that return numeric primitives (e.g. `Result<usize, Error>` surfaced as `ulong`) the FFI sets `last_error` and returns the sentinel `0` — which the C# binding silently returned as a legitimate "0 items downloaded" value, swallowing the error. The Rust FFI clears `LastErrorCode` at every call entry, so a non-zero value immediately after the call unambiguously indicates failure. Both the sync and async branches of `gen_wrapper_function` now emit a `LastErrorCode() != 0 → throw GetLastError()` check for non-pointer return types when `func.error_type.is_some()`. Surfaced on tree-sitter-language-pack's `Test_DownloadInvalidLanguage` which expected `TreeSitterLanguagePackException` from `Download(["zzz_definitely_not_a_real_language_xyz"])`. (`src/backends/csharp/gen_bindings/methods.rs`)

- **alef-backend-pyo3: add `# noqa: F401` to `TYPE_CHECKING` import items in generated `options.py`**: the `type_checking_import_item.jinja` template emitted bare `Name,` lines while the runtime `import_item.jinja` already had `# noqa: TC001`. When a TYPE_CHECKING-only import is referenced through `from __future__ import annotations` deferred evaluation but the local @dataclass / TypedDict field annotations don't textually reference the name (e.g. because the field's type was rewritten to `dict[str, Any]` or because the type is reachable transitively but only appears nested), ruff F401 flags the import as unused. Consumers can't hand-edit (alef-generated file), and `ruff --fix` would diverge from the alef hash. The TC import item now carries `# noqa: F401` to mirror the existing TC001 suppression on runtime imports — both fall in the same generated-binding pragma family. Surfaced on html-to-markdown CI Lint ruff F401 (packages/python/html_to_markdown/options.py: `DocumentStructure`, `PreprocessingOptionsUpdate`, `ProcessingWarning`, `TableData`). (`src/backends/pyo3/templates/type_checking_import_item.jinja`)

## [0.16.37] - 2026-05-17

### Fixed

- **alef-backend-rustler: uniform multi-line case arms in opaque-module streaming wrapper and blank lines before `@doc` in template wrappers**: two separate mix-format violations in generated Elixir. (1) `gen_elixir_opaque_module` in `helpers.rs` rendered the `Stream.unfold` inner `case` block with mixed single-line and multi-line arms (`{:ok, nil} -> nil`, `{:ok, chunk} -> {chunk, h}`) alongside a multi-line arm — mix-format requires all arms to use the same multi-line form when any arm is multi-line, and requires blank lines between multi-line arms. Fixed by rewriting all four arms to multi-line form (`{:ok, nil} ->\n  nil`, etc.) with blank lines between each. Also fixed the outer `case` arms wrapping the `Stream.unfold` call (`{:ok, stream}` and `{:error, reason}`) to use multi-line form and blank lines. (2) `elixir_streaming_start_wrapper.jinja` and `elixir_streaming_next_wrapper.jinja` emitted `end` with no trailing blank line, so the following `@doc` attribute (from the next template) appeared immediately after `end` without a blank line — mix-format requires a blank line before each `@doc` that precedes a function definition. Added a blank line at the end of both templates. Surfaced on liter-llm CI Lint mix-format (packages/elixir/lib/liter_llm/default_client.ex + packages/elixir/lib/liter_llm.ex). (`src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/templates/elixir_streaming_start_wrapper.jinja`, `src/backends/rustler/templates/elixir_streaming_next_wrapper.jinja`)

## [0.16.36] - 2026-05-17

### Fixed

- **alef-e2e zig: emit `setCwd` only for consumers with `file_path`/`bytes` fixtures**: commit `a3ba3b40` restored `setCwd(b.path("../../test_documents"))` on every `addRunArtifact` step to fix kreuzberg's PDF/image tests, but this unconditional call breaks mock-server-only consumers like kreuzcrawl that have no `test_documents/` directory. When Zig's `RunStep` executes, it calls `chdir(2)` into the target directory before `execve`-ing the test binary; if the directory does not exist the OS returns `ENOENT`, which Zig surfaces as `error: failed to spawn and capture stdio from .zig-cache/o/<hash>/<name>: FileNotFound` — even though the compile step completed successfully and the binary is present in the cache. The fix mirrors the pattern already used by the Ruby, Go, PHP, TypeScript, and Wasm backends: compute `has_file_fixtures` by checking whether any fixture's resolved call args carry type `file_path` or `bytes`, and only emit the `setCwd` line when that flag is true. Three regression tests in `zig_addtest_use_llvm_install.rs` cover both the file-fixture path (setCwd present) and the mock-server-only path (setCwd absent). (`src/e2e/codegen/zig.rs`, `src/e2e/tests/zig_addtest_use_llvm_install.rs`)

## [0.16.35] - 2026-05-17

### Fixed

- **alef-e2e java: honor `returns_void` on calls returning `()` / `Result<(), E>`**: `java/test_method.jinja` unconditionally emitted `var result = <call_expr>;` which the Java compiler rejected with `cannot infer type for local variable result (variable initializer is 'void')` for calls whose Rust signature is `() -> ()` / `() -> Result<(), E>` (e.g. tree-sitter-language-pack's `init`, `configure`, `clean_cache`). Threaded `returns_void` from `CallConfig` through `render_example` into the template; when true the template emits the bare statement form (`<call_expr>;`) instead of the `var` assignment. Mirrors the existing Ruby/C# patterns. (`src/e2e/codegen/java.rs`, `src/e2e/templates/java/test_method.jinja`)

- **alef-backend-rustler: only emit `alias <App>.Native` in reference-only struct modules when the body actually references `Native`**: the generated `defmodule <App>.<Type> do` block unconditionally pushed `alias <App>.Native` at the top, but it was only referenced in the body for types with `has_default = true` (the `new()` constructor calls `Native.<type>_default()`) or non-empty `methods` (each wrapper calls `Native.<nif>`). For reference-only types like tree-sitter-language-pack's `Language` (only `defstruct [:ref]` + `@type t`), the alias was dead and `mix compile --warnings-as-errors` rejected it with `warning: unused alias Native`. The alias is now gated on `typ.has_default || !typ.methods.is_empty()`. (`src/backends/rustler/src/gen_bindings/helpers.rs`)

- **alef-backend-pyo3 `_coerce_enum`: accept PascalCase / camelCase variant names by also trying UPPER_SNAKE_CASE and snake_case derivations**: the generated `_coerce_enum` helper tried `s`, `s.upper()`, `s.lower()`, and `"".join(part.capitalize() for part in s.split("_"))` candidates against the pyclass enum. None of these maps `"AtxClosed"` (a multi-word PascalCase variant name commonly used in JSON fixtures) to `ATX_CLOSED` (the `#[pyo3(name = "…")]` UPPER_SNAKE_CASE form). Single-word variants like `"Atx"` worked accidentally because `s.upper()` already equals `"ATX"`. Multi-word PascalCase variants (`AtxClosed`, `DoubleEqual`, etc.) raised `ValueError: unknown <Enum> value: '<X>'` at runtime. The coercer now also derives a snake_case form (`_pascal_to_snake` helper inserts `_` between lower→upper transitions) and tries `snake`, `snake.upper()`, and `"".join(part.capitalize() for part in snake.split("_"))` candidates, covering the PascalCase ↔ UPPER_SNAKE_CASE round-trip pyo3 enums use. Surfaced on html-to-markdown e2e Python tests (`test_options_heading_style_atx_closed`, `test_options_highlight_double_equal`). (`src/backends/pyo3/gen_bindings/functions.rs`)

- **alef-backend-pyo3: remove blank line between `class X:` and its docstring (pydocstyle D211)**: `gen_options_py` and `gen_typeddict` pushed an extra `\n` after the `dataclass_header.jinja` / `typeddict_header.jinja` / `str_enum_class_header.jinja` render and before the `class_docstring.jinja` / `enum_docstring.jinja` render, producing `class X:\n\n    """doc"""` in every emitted `@dataclass`, `TypedDict`, and `(str, Enum)` class. pydocstyle D211 ("No blank lines allowed before class docstring") rejected the blank line at every class definition. The spurious `out.push('\n')` calls at all three sites in `types.rs` are removed. Surfaced on liter-llm CI Lint pydocstyle D211 (packages/python/liter_llm/options.py).

- **alef-backend-rustler: emit multi-line case arms in streaming `Stream.unfold` body (mix-format)**: the `elixir_streaming_unfold_wrapper.jinja` template rendered all three inner `case` arms as single-line (`{:ok, nil} -> nil`, `{:ok, chunk_json} -> {Jason.decode!(…), h}`, `{:error, _} -> nil`). mix-format requires that arms whose bodies are tuples or non-trivial expressions be written in multi-line form with the body on its own indented line and a blank line separating consecutive multi-line arms. The template is updated to use the multi-line form for all three arms. Surfaced on liter-llm CI Lint mix-format (packages/elixir/lib/liter_llm/default_client.ex).

- **alef-e2e ruby: honor `returns_void` on calls returning `()` / `Result<(), E>`**: the Ruby codegen emitted `result = TreeSitterLanguagePack.clean_cache()` followed by an unconditional `expect(result).not_to be_nil` fallback whenever `has_usable` was false (the only assertion in the fixture was `not_error`). For unit-returning functions (Rust `()` → Ruby `nil`) that fallback always failed. Threaded `returns_void` from `CallConfig` through `render_example` into the `ruby/test_function.jinja` template, dropped the result-variable assignment, and gated the nil-check fallback on `!returns_void`. Surfaced on tree-sitter-language-pack's `download_clean_cache`, `download_init_default`, `download_configure_custom_dir` fixtures whose calls (`init`, `configure`, `clean_cache`) all return `Result<(), Error>`. (`src/e2e/codegen/ruby.rs`, `src/e2e/templates/ruby/test_function.jinja`)

- **alef-e2e ruby: stop coercing simple-result equality to `String` for numeric/bool expected values**: when `result_is_simple = true` the codegen wrapped the result in `.to_s.strip` so `equals "foo\n"` would absorb trailing-newline drift, but the same wrapper applied for `equals 0` / `equals true`, producing `expect(result.to_s.strip).to eq(0)` (Ruby `String "0" != Integer 0`). The strip wrapper is now gated on the expected value being a JSON string; numeric and bool comparisons use the raw expression and stay typed. Surfaced on tree-sitter-language-pack's `download_empty_list` fixture (`download([])` returns `usize 0`). (`src/e2e/codegen/ruby.rs`)

## [0.16.34] - 2026-05-17

### Fixed

- **alef-scaffold ruby: add `sorbet-runtime` as a runtime gemspec dependency**: the Magnus backend's `native_rb_wrapper.rb.jinja` template unconditionally emits `require 'sorbet-runtime'` at the top of every generated `packages/ruby/lib/{name}/native.rb` wrapper and uses `T::Sig`, `T.nilable(...)`, and `T::Array[...]` throughout the public-API shim. The gemspec produced by `scaffold_ruby` only declared `rb_sys` as a runtime dependency, so consumers running `bundle install --without development` (the standard CI mode) never installed `sorbet-runtime` and received `LoadError: cannot load such file -- sorbet-runtime` the moment the native wrapper was required — causing the entire `spec/` suite to abort before running a single test. The fix adds `spec.add_dependency 'sorbet-runtime', '~> 0.5'` to the generated gemspec, sourced from the new `tv::gem::SORBET_RUNTIME` constant in `alef-core::template_versions`, consistent with how all other pinned gem versions are managed. A regression test `test_scaffold_ruby_gemspec_includes_sorbet_runtime_dependency` asserts that the rendered gemspec contains the dependency, uses `spec.add_dependency` (not `add_development_dependency`), and carries the `~> 0.5` constraint. Surfaced in kreuzcrawl CI E2E run 25997906829, job 76416254666. (`crates/alef-scaffold/src/languages/ruby.rs`, `src/core/template_versions.rs`)

## [0.16.33] - 2026-05-17

### Fixed

- **alef-cli format: `[workspace.format_overrides.<lang>]` custom commands now run even when the language's generated files were not re-written this run**: `format_generated` previously applied the `only_languages` filter uniformly, skipping any language absent from `changed_languages`. For default formatters this optimization is correct, but custom `format_override.command` entries must run whenever the language appears in the generated-file list — even when the content hash matches on-disk content and files were not written. The symptom: adding `[workspace.format_overrides.php]` on an existing repo ran `alef all --format`, got a config-hash cache miss (IR+config changed) but a content-hash hit (generated PHP source is identical), skipped php-cs-fixer because PHP was not in `changed_languages`, computed `alef:hash:` over pre-formatter output, and entered an endless regen loop when prek's own php-cs-fixer hook reformatted and broke `alef verify`. Same bug for any language with a custom override (`[workspace.format_overrides.python]`, etc.). Custom commands now bypass the `only_languages` filter; the filter still applies for built-in default formatters. Two regression tests added. (`crates/alef-cli/src/pipeline/format.rs`)

- **alef-cli generate/all: sweep language package directories after cleanup to remove orphaned alef-generated files in directories the current run no longer writes to**: `cleanup_orphaned_files` only walks directories that had files written in the current run. When a backend stops generating files in a directory — e.g. after `generate_public_api` was removed from `alef-backend-wasm`, leaving `packages/wasm/src/index.ts` behind — that directory is never visited and the stale file persists. The `alef generate` and `alef all` pipelines now run a `sweep_orphans` pass after `cleanup_orphaned_files` covering each language's current `package_dir` and `output_dir`, plus the legacy `packages/wasm/` and `packages/typescript/` paths that alef previously wrote public-API shims into. (`crates/alef-cli/src/main.rs`)

- **alef-readme: emit blank line between performance-context line and table header**: `render_performance_table` rendered the "**platform** · `function` · note" context line via the `performance_context.jinja` template (which ends with a single `\n`) and immediately concatenated the `| Document | Size | … |` header on the next line. The resulting README failed CommonMark/GFM MD058 ("Missing blank line before table") and `rumdl fmt` auto-fixed it on every CI run, so consumers' "regenerate then `git diff --exit-code`" gate failed indefinitely (surfaced on html-to-markdown's `Validate READMEs` step across all 9 polyglot READMEs). A second `\n` is now pushed after the context render so the output is `…note\n\n| Document | …`, matching what rumdl produces and what every other Markdown formatter expects between a paragraph and a following table. Covered by a new assertion in `test_render_performance_table_throughput`. (`crates/alef-readme/src/lib.rs`)

- **alef-core `package_dir` Node/Wasm defaults now point at `crates/{name}-{node,wasm}/` instead of dead `packages/{node,wasm}/`**: `ResolvedCrateConfig::package_dir(Language::Node)` returned `"packages/node"` and `package_dir(Language::Wasm)` fell through the `_ => format!("packages/{lang}")` arm to `"packages/wasm"`. Both directories were dead-weight scaffolds that 0.16.32 stopped emitting (the real NAPI-RS + wasm-bindgen npm publish sources live next to the binding crate at `crates/{name}-{node,wasm}/`). Setup/test/clean defaults therefore ran `cd packages/node && pnpm install` against a non-existent directory and timed out (600s) for every consumer on first CI run — surfaced on html-to-markdown's CI Lint at the `alef setup` step. Defaults now resolve to `crates/{self.name}-{node,wasm}` using the crate name, matching where the manifests actually live and where pnpm-workspace already includes them. (`src/core/config/resolved/lookups.rs`)

- **alef-readme: skip `Language::C` and `Language::Jni` by default — they were silently overwriting the Zig README**: the hardcoded fallback used `Language::C | Language::Jni | Language::Zig => packages/zig/README.md` for all three, so when a consumer's `languages = [..., zig, jni, ...]` list included both, the second one to run (Jni) clobbered the Zig template-rendered README with a 30-line hardcoded "Zig Bindings" stub. C and JNI are FFI glue layers, not publishable bindings; their public surface is the host language's README (kotlin-android, ffi). They now skip silently like Rust does, unless a consumer opts in via `[readme.languages.c]` / `[readme.languages.jni]`. (`crates/alef-readme/src/lib.rs`)

- **alef-backend-kotlin jni client emitter: prepend `import` to the `TypeReference` import insert**: when the generated `DefaultClient.kt` instance-method wrapper needed Jackson's `TypeReference<T>` for a generic-container return type, the `imports` BTreeSet received a bare `"com.fasterxml.jackson.core.type.TypeReference"` entry instead of `"import com.fasterxml.jackson.core.type.TypeReference"`. The assembler that rendered the imports concatenated each entry verbatim, producing a top-level expression `com.fasterxml.jackson.core.type.TypeReference` in `DefaultClient.kt` that ktlint rejected with "Expecting a top level declaration" and broke kotlin-android post-generation formatting. Aligned with the eleven sibling `imports.insert(...)` sites in the same file that already use the `import` prefix. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

## [0.16.32] - 2026-05-17

### Fixed

- **alef-e2e java: honor per-call `streaming = false` opt-out in import + assertion gating**: the Java codegen's `ChatCompletionChunk`-import gate and the streaming-virtual-field assertion accessor both used `is_streaming_virtual_field(fld)` directly, ignoring the call config's `streaming` flag. Consumers like tree-sitter-language-pack expose a non-streaming `process` function whose result struct has a real `chunks` field; the unconditional gate spuriously triggered `import …ChatCompletionChunk` and rewrote `count_min` on `chunks` to `chunks.size() >= N` (referencing a non-existent local aggregator) instead of `result.chunks().size() >= N`. Both sites now route through `resolve_is_streaming(fixture, call_config.streaming)`, threading the resolved `is_streaming` into `render_assertion` so the virtual-aggregator path only activates for genuinely streaming fixtures. (`src/e2e/codegen/java.rs`)

- **alef-scaffold node: drop the false-positive `glibcVersion === undefined` branch from the platform-dispatch loader**: the loader's `isMusl()` heuristic interpreted "report header has no `glibcVersion` field" as "this is a musl system." On Node 22 GitHub-hosted ubuntu-24.04 runners the field is sometimes absent on glibc systems, producing a false positive that drove the loader to attempt `./{binaryName}.linux-x64-musl.node` instead of `…-linux-x64-gnu.node` and fail to load any binding. The `=== undefined` branch is removed; the loader still trusts a string `glibcVersion` as "glibc" but otherwise falls through to the unambiguous filesystem check (`/lib64/ld-musl-x86_64.so.1`). (`crates/alef-scaffold/src/languages/node.rs`)

- **alef-readme node: emit the Node README at `crates/{name}-node/README.md` (publish target) instead of `packages/typescript/README.md`**: both `lang_dir_name(Language::Node)` and the hardcoded fallback in `generate_readme_hardcoded` returned `"typescript"` for the directory portion of the Node README path, while the publishable npm package lives at `crates/{name}-node/`. Result: npm publishes shipped without a README, and the file landed at a directory no consumer used. `lang_dir_name` now returns `"node"`, the hardcoded fallback emits `crates/{name}-node/README.md`, and the test `test_generate_node_readme` is updated. The YAML/TOML config key (`lang_code`) remains `"typescript"` so existing per-language `[readme.languages.typescript]` blocks keep resolving. Consumer `alef.toml` files that set an explicit `output_path` are unaffected. (`crates/alef-readme/src/lib.rs`)

- **alef-scaffold wasm: strip `-node` suffix before appending `-wasm` to derive the npm package name**: `scaffold_wasm` previously appended `-wasm` directly to `node_package_name()`, so consumers following the `-node` naming convention (e.g. `@kreuzberg/html-to-markdown-node`) got `@kreuzberg/html-to-markdown-node-wasm` instead of the correct `@kreuzberg/html-to-markdown-wasm`. The fix strips a trailing `-node` before appending; names without that suffix are unchanged. Three new unit tests cover the scoped case, the unscoped case, and the no-suffix fallback. (`crates/alef-scaffold/src/languages/wasm.rs`)

### Changed

- **alef-readme: skip Rust by default — no more `packages/rust/README.md` stub**: `generate_readme(Language::Rust, ...)` previously emitted a 25-line stub at `packages/rust/README.md` redirecting users to crates.io. That path collides with consumer repos whose canonical Rust crate README is the source-of-truth `crates/<name>/README.md`, and the stub had to be deleted on every regen. `generate_readmes` now silently skips `Language::Rust` unless the consumer has explicitly opted in via `[readme.languages.rust]` with an `output_path` (or `output`) — opt-in matches the existing per-language configuration pattern used by every other language. The internal `generate_readme` signature changed from `Result<GeneratedFile>` to `Result<Option<GeneratedFile>>` to express the skip cleanly; public `generate_readmes` keeps its existing `Result<Vec<GeneratedFile>>` shape. (`crates/alef-readme/src/lib.rs`)

### Removed

- **alef-scaffold node: stop scaffolding the dead `packages/node/` directory**: `scaffold_node` emitted a parallel npm package at `packages/node/{package.json,index.js,index.d.ts,src/index.d.ts,tsconfig.json,.oxfmtrc.json,.oxlintrc.json}` whose `package.json` declared an unscoped `"name": "<crate>"` that no downstream consumer published — the real npm publish target lives at `crates/<crate>-node/` and is built by NAPI-RS. The seven dead emissions are removed; `scaffold_node` now only emits the crate-level `package.json` + platform-dispatch `index.js` at `crates/<crate>-node/`. Now-orphaned helper bindings (`pkg_dir`, `name`, `keywords_json`, `homepage_json`, `authors_json`, `content`, `dts_content`, `js_content`) and the four assertion blocks pinned to the old `packages/node/` paths in `tests.rs` are dropped (the `test_scaffold_node`, `test_scaffold_multiple`, and `test_scaffold_node_production_features` tests are updated; the three `test_node_scaffold_oxfmt_config_content`/`oxlint_config_content`/`uses_oxc` tests are removed since they asserted on now-gone files). (`crates/alef-scaffold/src/languages/node.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-backend-napi: drop `Backend::generate_public_api` (`packages/typescript/src/index.ts` shim)**: the NAPI backend's `generate_public_api` impl emitted a re-export TS module at `packages/typescript/src/index.ts` that no consumer imported from — the real TS types are produced by `napi build` directly into `crates/{name}-node/index.d.ts` next to the published npm package. The shim was dead weight that had to be hand-deleted on every regen. The `generate_public_api` impl is removed (falling back to the default empty impl from `Backend`), the now-orphaned `to_node_name` import is dropped, and the three `test_napi_js_name_*` tests have their `index.ts` assertions removed while retaining the `lib.rs`-side `js_name` assertions. (`src/backends/napi/src/gen_bindings/mod.rs`)

- **alef-backend-wasm: drop `Backend::generate_public_api` (`packages/wasm/src/index.ts` shim)**: the wasm backend's `generate_public_api` impl emitted a re-export TS module at `packages/wasm/src/index.ts` that no consumer imported from — the real `@scope/<pkg>-wasm` npm publish is built directly from `crates/<crate>-wasm/` by `wasm-pack`/`wasm-bindgen` into `dist/`, `dist-node/`, `dist-web/`. The shim was dead weight that had to be hand-deleted on every regen. The `generate_public_api` impl is removed (falling back to the default empty impl from `Backend`), the now-orphaned `to_node_name` import is dropped, the dead `WasmConfig.exclude_reexports` config field is removed from `alef-core` (no consumer set it), and the `test_wasm_index_ts_exports_unprefixed_names` test is dropped. (`src/backends/wasm/gen_bindings/mod.rs`, `src/core/config/languages.rs`)

## [0.16.31] - 2026-05-17

### Fixed

- **alef-backend-napi trait_bridge: build the visitor-only `ConversionOptions` via struct-literal + `..Default::default()` instead of `let mut opts = Default::default(); opts.field = X;`**: the `or_else(|| { ... })` branch in the napi visitor wrapper code constructed a new `ConversionOptions` by `Default::default()` then field-assigned the visitor handle. Clippy's `field_reassign_with_default` lint (denied by default in consumer projects that build with `-D warnings`) rejects this idiom and demands the struct-literal form. The change keeps the same semantics (every other field still falls through to `Default`) but produces clippy-clean Rust on first emission, so consumers don't need to spawn a follow-up "fix clippy" commit after every alef regen. Surfaced on html-to-markdown's CI Rust (crates/html-to-markdown-node/src/lib.rs:1589). (`src/backends/napi/src/trait_bridge.rs`)

## [0.16.30] - 2026-05-17

### Fixed

- **alef-e2e rust cors: emit only the `axum::http` re-exports actually referenced by the rendered `CorsLayer`**: `render_cors_layer` previously emitted `use axum::http::{HeaderName, HeaderValue, Method};` unconditionally, but `HeaderName` is only emitted when `allow_headers` contains a non-prelude header (i.e. anything outside `content-type`/`authorization`/`accept`), `HeaderValue` only when `allow_origins` is non-empty, and `Method` only when `allow_methods` is non-empty. Fixtures that allow no custom headers (the common case) therefore got an unused `HeaderName` import, tripping `-D unused_imports` in the consumer's cargo build. The import group is now computed from actual use and reduced to one of: nothing, a single `use axum::http::X;`, or the appropriate subset. Covered by five new unit tests in `render_cors_layer::tests`. (`src/e2e/codegen/rust/http.rs`)

## [0.16.29] - 2026-05-17

### Fixed

- **alef-backend-pyo3 errors: drop duplicate `\n` after `indented_import_item.jinja` render**: the template already ends in `\n`, but each of the 5 callsites in `errors.rs` appended an extra `out.push('\n')` after `render`, producing `NAME,\n\n` per line — i.e. a blank line between every imported symbol in every multi-line `from .X import (...)` block in the generated `packages/<pkg>/__init__.py`. Ruff format strips these blank lines, breaking the embedded `alef:hash:` line and forcing an endless regen-format cycle on every consumer. Each of the 5 callsites now relies on the template's trailing newline alone. (`src/backends/pyo3/gen_bindings/errors.rs`)

- **alef-backend-rustler `@type t ::` multi-line emission: indent the first arm to match the `|`-prefixed continuation arms**: the `elixir_enum_type_arm_first.jinja` template emitted `{{ arm }}\n` (no indent), placing the first variant at column 0 while every subsequent `| :variant` line sat at column 10. mix-format `--check-formatted` then rejects the file (it wants the bare arm at column 10 too, aligned with the `|`), and the consumer's `mix-format` pre-commit hook rewrites the file — invalidating the embedded `alef:hash:` line. Template now emits `{{ arm }}\n` (10-space indent), matching `elixir_enum_type_arm_rest.jinja` and producing mix-format-clean output. (`src/backends/rustler/src/template_env.rs`)

- **alef-cli pipeline/generate: `format_rust_content` detects crate edition from nearest `Cargo.toml`**: rustfmt was invoked with a hardcoded `--edition 2024`. When the target crate declares a different edition (e.g. `edition = "2021"`) in its `Cargo.toml`, `cargo fmt` would produce different import sort order and formatting than alef's hardcoded edition, causing drift after `alef generate`. `format_rust_content` now accepts the file path, walks up to find the nearest `Cargo.toml`, parses its `[package] edition` field, and passes the detected edition to rustfmt; falls back to `"2024"` when no `Cargo.toml` is found or the edition field is absent. (`crates/alef-cli/src/pipeline/generate.rs`)

## [0.16.28] - 2026-05-17

### Fixed

- **alef-e2e rust: drop unused `use {dep}::{App, RequestContext}` import from http-fixture test files**: `render_test_file` unconditionally emitted `use {module}::{App, RequestContext};` when any fixture in the file declared an `http` block, but `render_http_test_function` references those symbols via their fully-qualified module path (`{dep_name}::App::new()`, `move |_ctx: {dep_name}::RequestContext|`). The use-statement was therefore always unused, tripping `-D unused_imports` in the consumer crate's clippy sweep. The unconditional emission is dropped; the body keeps the fully-qualified paths so no resolution behaviour changes. (`src/e2e/codegen/rust/test_file.rs`)

- **alef-e2e (all backends): `sanitize_ident` now strips underscores left behind after a numeric prefix to avoid `test__foo` names**: fixture ids commonly carry a numeric prefix to control category-file ordering (`24_cookie_samesite_strict.json`). `sanitize_ident` stripped the leading digits but left the underscore separator behind, so generators that prefix the result with `test_` (rust, python, ruby, typescript, elixir, C, …) emitted function names like `test__cookie_samesite_strict`. The function now also strips any leading underscores _that were exposed by stripping digits_, while preserving genuine leading underscores in ids that never had a digit prefix (`_foo` → `_foo`). Covered by four new unit tests in `escape::tests`. (`src/e2e/escape.rs`)

## [0.16.27] - 2026-05-17

### Added

- **alef-scaffold python: `[crates.python] pip_dependencies = [...]`**: declare runtime PyPI dependencies for the alef-scaffolded `pyproject.toml`. Entries are emitted verbatim into `[project] dependencies = [ ... ]` (PEP 508). Needed for downstream crates whose alef-generated `api.py` imports a third-party PyPI package (e.g. `tree-sitter`), so a fresh `pip install <pkg>` resolves the transitive at install time and the sdist smoke-test `import` succeeds. (`src/core/config/languages.rs`, `crates/alef-scaffold/src/languages/python.rs`)

### Fixed

- **alef-cli pipeline/generate: apply `rustfmt` in `finalize_hashes` for `.rs` files so the embedded hash always reflects cargo-fmt-clean content**: `finalize_hashes` previously computed the `alef:hash:` value from the raw on-disk bytes, which could be unformatted when the file was not re-written in the current run (cache hit or generated by an older alef version). A subsequent `cargo fmt` in CI would then reformat the file (reordering imports such as `{c_char, CStr, CString}` → `{CStr, CString, c_char}` and collapsing expanded `if`/`else` blocks), changing the on-disk bytes without updating the hash, causing `alef verify` to report stale. `finalize_hashes` now calls `normalize_content` (which pipes `.rs` files through stdin `rustfmt` with the consumer's `rustfmt.toml`) before computing and injecting the hash, making the recorded hash identical to what `cargo fmt` would produce. (`crates/alef-cli/src/pipeline/generate.rs`)

- **alef-cli pipeline/format: drop the `pnpm dlx oxfmt` step from the FFI and WASM formatter sequences**: the FFI step ran `pnpm dlx oxfmt .` over the entire workspace and the WASM step ran `pnpm dlx oxfmt <manifest>` on the generated wasm crate's Cargo.toml. The shared Kreuzberg pre-commit `oxfmt` hook (`kreuzberg-dev/pre-commit-hooks`, `id: oxfmt`) is scoped to `[javascript, jsx, ts, tsx, json, css]` — i.e. it never touches TOML in CI. But oxfmt's default behaviour DOES format TOML, with 2-space indent and collapsed multi-line arrays. That collides with `cargo-sort`'s preserved-indent canonicalisation and with hand-maintained Cargo.toml style (e.g. html-to-markdown's CLI crate Cargo.toml uses 4-space `features = [...]` blocks): oxfmt rewrites them to 2-space, cargo-sort then preserves the 2-space, the embedded `alef:hash:` line ends up describing the pre-oxfmt content, and every subsequent `alef verify` reports the file as stale. `cargo fmt` and `cargo sort` together are sufficient to canonicalise Rust + Cargo.toml; JS/TS/JSON files have their own per-language scaffold + the consumer's own oxfmt hook. (`crates/alef-cli/src/pipeline/format.rs`)

- **alef-backend-kotlin (and kotlin-android via the same emitter): use Jackson `TypeReference` for generic-container returns**: the JNI emitter's deserialize branch always emitted `MAPPER.readValue(responseJson, {kotlin_ty}::class.java)`, which is rejected by `kotlinc` whenever `kotlin_ty` carries angle-bracketed generics — e.g. functions returning `Vec<String>` emitted `List<String>::class.java` and failed with `Only classes are allowed on the left-hand side of a class literal`. The emitter now switches to `MAPPER.readValue(responseJson, object : TypeReference<{kotlin_ty}>() {{}})` whenever the rendered Kotlin return type contains `<`, mirroring the kotlin-android facade's existing generic-container handling, and threads the `com.fasterxml.jackson.core.type.TypeReference` import in. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

## [0.16.26] - 2026-05-17

### Fixed

- **alef-scaffold: include conditional deps in `cargo-machete` ignored lists for FFI/Node/PHP/Python umbrellas**: when the umbrella crate declares `trait_bridges` or streaming `adapters`, the scaffold conditionally adds `async-trait` / `futures-util` / `tokio` to `[dependencies]` for manifest stability across regens — but the static `[package.metadata.cargo-machete]` block only listed the _unconditional_ deps (`serde_json` and, for FFI/PHP/Python, `tokio`/`pyo3-async-runtimes`). For umbrella crates whose actual API surface doesn't exercise async-trait or streaming codepaths in the generated binding (e.g. html-to-markdown's HtmlVisitor is sync, no streaming adapters), cargo-machete flagged the conditional deps as unused and broke `prek run --all-files`. The ignored list is now built dynamically from `has_trait_bridges` / `has_streaming`, mirroring the dep emission logic exactly so machete stays quiet whenever a dep is emitted unconditionally for codegen stability. (`crates/alef-scaffold/src/languages/{ffi.rs,node.rs,php.rs,python.rs}`)

- **alef-backend-wasm: also emit `getrandom 0.3` with `wasm_js` feature alongside `0.4`**: the WASM Cargo.toml emitter only declared `getrandom = { version = "0.4", features = ["wasm_js"] }`, but kreuzberg-wasm has transitive deps that pull in `getrandom 0.3.4` (e.g. through `rand 0.8`/`uuid 1.x` chains). On `wasm32-unknown-unknown`, getrandom 0.3 requires the `wasm_js` feature to provide its backend; without it the build fails with `compile_error!("The wasm_js backend requires the wasm_js feature for getrandom")`. Cargo's additive feature unification means declaring a direct `getrandom 0.3` dep with `features = ["wasm_js"]` enables the feature on the unified 0.3 instance, fixing the WASM build without affecting non-wasm targets. The 0.3 dep is added under a renamed alias `getrandom_03` so both major versions can coexist in the same `[target.cfg]` table. (`src/backends/wasm/gen_bindings/mod.rs`)
- **alef-e2e zig: restore `setCwd` on test run steps so generated fixture reads resolve**: commit `993ca9ec` removed `setCwd("../../test_documents")` under the assumption that all Zig e2e suites are mock-server-only (kreuzcrawl), but kreuzberg's document-extraction e2e reads fixtures like `pdf/fake_memo.pdf` and `images/test_hello_world.png` via `std.Io.Dir.cwd().readFileAlloc(...)`. Without the chdir every fixture read failed with `error.FileNotFound`, producing 26 test failures across smoke/contract/format_specific/detection/error/async suites. Other languages perform this chdir in a per-suite hook (Go `TestMain`, Python conftest, Kotlin Gradle `workingDir`); Zig has no equivalent so it must happen at the build-step level. `setCwd` is safe here because `.use_llvm = true` already pins the LLVM backend, which resolves the test binary's spawn path to an absolute cache path via `getEmittedBin()` before `convertPathArg` runs — the child cwd only affects fixture lookups, not the binary path. Re-threaded `test_documents_relative_from(0)` into `render_build_zig` and updated the `every_test_artifact_runs_via_addrunartifact_directly` test to assert the chdir is present. (`src/e2e/codegen/zig.rs`, `src/e2e/tests/zig_addtest_use_llvm_install.rs`)
- **alef-backend-zig: null-check raw C pointer before slicing in fallible owned-string returns**: `unwrap_return_expr` for `String`/`Path`/`Json`/`Vec`/`Map` returns built `raw[0.._result_len]` after a `kreuzberg_last_error_code() != 0` check, on the assumption that any error path on the primary `_<fn>` FFI call would still be visible in the thread-local error code when the wrapper queried it. That holds when the FFI implementation calls `set_last_error` on every error branch, but the matching `_<fn>_len()` companion always invokes `clear_last_error()` on entry and some implementations silently return 0 on the error path without re-setting the error code, wiping out the original error. The wrapper then passed the error-code gate and panicked with `attempt to use null value` when constructing the slice over a null pointer (observed in `get_extensions_for_mime("application/x-totally-unknown")`, where `kreuzberg_get_extensions_for_mime_len`'s `Err(_) => 0` arm forgets to call `set_last_error`). Defensive null check now runs first in fallible owned-string returns; threaded the resolved Zig `error_type` into `unwrap_return_expr` so the early return can produce the function's declared error variant. (`src/backends/zig/src/gen_bindings/functions.rs`)

- **alef-e2e java: handle `result_is_option` for bare-result `is_empty`/`not_empty` assertions**: the Java facade exposes `Option<T>`-returning Rust functions as `@Nullable T` (via `.orElse(null)`), so calling `.isEmpty()` on the nullable reference does not compile for record types like `EmbeddingPreset` (`error: method isEmpty() not found on EmbeddingPreset`). The e2e codegen now propagates `result_is_option` through `render_assertion` and emits `assertNull/assertNotNull(result, ...)` for bare-result `is_empty`/`not_empty` assertions on option-returning calls, mirroring the existing `kotlin.rs` and `zig.rs` behaviour. (`src/e2e/codegen/java.rs`)
- **alef-e2e java: coerce nullable numeric fields via `Number::longValue` to fix `long`/`Integer` mismatch**: `Optional.ofNullable(Integer field).orElse(0L)` does not compile because `0L` is a `Long` while the optional value type is `Integer` (e.g. Rust `Option<u32>` maps to Java `@Nullable Integer`). The Java e2e codegen now emits `.map(Number::longValue).orElse(0L)` for numeric comparisons and equality on optional numeric fields, which compiles cleanly for both `Optional<Integer>` and `Optional<Long>`. (`src/e2e/codegen/java.rs`, `src/e2e/templates/java/assertion.jinja`)
- **alef-e2e dart: emit required string args as positional rather than named**: a prior fix indiscriminately switched all string args to named-argument syntax to match the FRB bridge, but the alef-generated Dart facade (e.g. `KreuzbergBridge`) emits required params as positional and only wraps optional params in a `{...}` named block (see `alef-backend-dart/src/gen_bindings/functions.rs`). Calling `KreuzbergBridge.extractBytesSync(content, mimeType: 'application/pdf', config)` produced `Error: No named parameter with the name 'mimeType'` because the facade has positional `mimeType`. The Dart e2e codegen now checks `arg_def.optional` and emits positional literals for required string args, preserving named syntax only for genuinely optional string params. (`src/e2e/codegen/dart.rs`)

- **alef-e2e swift: emit e2e package at `e2e/swift/` with explicit `name:` on `.package(path:)`**: the Swift e2e codegen was emitting under `e2e/swift_e2e/` to avoid a SwiftPM identity collision between the consumer (`e2e/swift/`) and the dep (`packages/swift/`) — both reduce to the path-last identifier `swift`. The stale `e2e/swift/Package.swift` left over from the original layout still declared a `.testTarget` with no source directory, so `cd e2e/swift && swift test` (the canonical CI command, matching every other language) failed with `error: The package does not contain a buildable target.` The emitter now writes back to `e2e/swift/` for consistency with every other language and disambiguates the dep via `.package(name: "{module}", path: "{pkg_path}")`, which SwiftPM honors as the dep's identity for `.product(name:package:)` resolution. (`src/e2e/codegen/swift.rs`)

## [0.16.25] - 2026-05-17

### Changed

- **ci: restrict `Android Emulator (instrumented tests)` workflow to manual dispatch**: the GitHub-hosted arm64-v8a API 34 emulator on `ubuntu-latest` regularly fails to reach `boot_completed` within the 1800s timeout, blocking unrelated merges. Trigger temporarily reduced to `workflow_dispatch` only until a stable runner/image combination is identified. (`.github/workflows/ci-android-emulator.yaml`)

### Added

- **alef sync-versions: manage `CITATION.cff` at the repo root**: a new optional `[workspace.citation]` section in `alef.toml` declares the project's citation metadata (`title`, `abstract`, `authors` — persons via `family-names`/`given-names` and/or entities via `name`, plus `email`/`orcid`, `repository-code`, `url`, `license`, `date-released`, `doi`). When present, `alef sync-versions` renders a full Citation File Format YAML at `CITATION.cff` using these fields plus the canonical workspace version from `Cargo.toml`; the file is rewritten only when its contents actually change, so the working tree stays clean across reruns. When the section is absent but a hand-authored `CITATION.cff` exists at the repo root, alef falls back to updating only the top-level `version:` scalar (preserving the original quote style — unquoted, single-, or double-quoted). New config types `CitationConfig` and `CitationAuthor` in `alef-core`. (`src/core/config/{output.rs,workspace.rs,resolved/mod.rs,new_config.rs,mod.rs}`, `crates/alef-cli/src/pipeline/version.rs`)

### Fixed

- **alef-backend-csharp: emit excluded types as JsonElement instead of named type**: when a field's type was marked with `#[cfg_attr(alef, alef(skip))]` or `#[doc(hidden)]` (e.g., `CoreProperties` in kreuzberg's `DocxMetadata.core_properties`), the C# codegen treated it as a Pascal-cased type name instead of recognizing it as an excluded type and mapping it to `JsonElement`. This caused deserialization failures with `JsonException: Cannot get StartObject as String` when Rust sent an object but C# expected a string. Fixed by passing the excluded types set from `ApiSurface.excluded_type_paths` to `gen_record_type` and checking it alongside complex enums to determine whether to emit `JsonElement`. (`src/backends/csharp/gen_bindings/{types.rs,mod.rs}`)

- **alef-backend-rustler: emit full `rust_path` for types not re-exported at the crate root**: param-type resolution in `gen_nif_function`, `gen_nif_async_function`, `gen_nif_method`, `gen_nif_async_method`, and `build_default_deser_preamble` was using `format!("{core_import}::{short_name}")` (e.g. `kreuzberg::DrawingType`) instead of the type's full `rust_path` (e.g. `kreuzberg::extraction::docx::drawing::DrawingType`). Types not re-exported at the crate root caused `cannot find DrawingType in kreuzberg` compile errors in the generated Elixir NIF crate. Fixed by building a `types_by_name` lookup map and resolving via `alef_codegen::conversions::core_type_path`. (`src/backends/rustler/src/gen_bindings/functions.rs`, `src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-backend-rustler: omit `mut` from `args_map` in visitor methods with no arguments**: the `visitor_method.rs.jinja` template always emitted `let mut args_map = serde_json::Map::new();`. When the visitor method has zero arguments the map is never mutated, triggering an `unused_mut` clippy warning that fails CI under `-D warnings`. The template now conditionally emits `mut` only when `args` is non-empty. (`src/backends/rustler/src/template_env.rs`)

- **alef snippets gaps: fix false-positive for MkDocs `pymdownx.snippets` multi-base-path includes**: `alef snippets gaps` previously resolved every `--8<-- "target"` include relative to the docs root only. Projects that configure `pymdownx.snippets.base_path = ["docs/snippets", "docs", "."]` — such as spikard — have includes like `--8<-- "CHANGELOG.md"` that resolve to the project root (`./CHANGELOG.md`), not `docs/CHANGELOG.md`. Alef reported these as missing references even though MkDocs renders them correctly. Two changes fix this: (1) `GapConfig` gains an `include_base_paths: Vec<PathBuf>` field; `parse_includes` now searches each base path in order and returns the first candidate that exists on disk, falling back to `docs_dir.join(target)` only when none match, so the missing-references report still shows a real candidate path. (2) `alef snippets gaps` gains a repeatable `--include-base-path <PATH>` CLI flag — pass each path from `pymdownx.snippets.base_path` to match the MkDocs resolver exactly. When the flag is omitted, behaviour is identical to before (only the docs root is searched). (`crates/alef-snippets/src/gaps.rs`, `crates/alef-cli/src/commands/snippets.rs`)

- **alef-e2e wasm: do not append `/nodejs` to user-supplied `[e2e.packages.wasm].path`**: `render_package_json` always appended `/nodejs` to the resolved package path, on the assumption it was a `pkg/` directory laid out by `wasm-pack build --target nodejs --out-dir pkg/nodejs`. That assumption only holds for the fallback path from `wasm_crate_path()`. When a project sets `[crates.e2e.packages.wasm].path` explicitly (e.g. html-to-markdown's `../../crates/html-to-markdown-wasm`, the crate root whose root-level `package.json` already declares `"main": "pkg/nodejs/..."`), the resulting `file:.../nodejs` dependency pointed at a nonexistent directory, breaking `pnpm install` with `ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND` and corrupting the lockfile. Now `render_package_json` only appends `/nodejs` for the fallback case; explicit paths are used verbatim. (`src/e2e/codegen/wasm.rs`)

- **alef-backend-wasm: skip `serde_wasm_bindgen::from_value` for external-crate types in tagged enums**: when a mixed-type tagged-enum field's `type_rust_path` resolves to a crate other than `core_import` (e.g. `tree_sitter_language_pack::ProcessResult`), the emitter previously generated `serde_wasm_bindgen::from_value::<ext_crate::T>()` unconditionally. Because the external crate is not in the WASM consumer's Cargo dependency graph, this caused `error[E0433]: failed to resolve: use of unresolved module or unlinked crate` and broke WASM, Python wheel macOS, Swift, and Dart iOS builds. The fix detects whether the type's crate prefix differs from `core_import` and falls back to `Default::default()` for those fields. (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-snippets C validator: use unambiguously invalid token in `syntax_fail` test**: the previous fixture (`return ;` in a non-void function) is treated as a warning by some GCC versions on Linux and exits 0 unless `-Werror` is set, causing the test to return `Pass` instead of `Fail`. Replaced with `@@@` — an invalid preprocessing token that is a hard error on all C compilers. (`crates/alef-snippets/src/validators/c.rs`)

- **publish workflow: include `alef-backend-jni` in the crates.io publish order**: the jni backend was added in commit 831dd99d but was not wired into `.github/workflows/publish.yaml` crate order. Topologically, jni must be published after gleam (which has no jni deps) and before kotlin and kotlin-android (which depend on it). (`.github/workflows/publish.yaml`)

- **alef-backend-napi + alef-e2e: emit tagged-enum discriminator matching Rust serde tag**: NAPI-RS binding generation for tagged enums hardcoded the TS discriminant field as `"kind"` regardless of what the Rust enum declared via `#[serde(tag = "...")]`. This broke deserialization: kreuzcrawl's `AuthConfig` uses `tag = "type"`, so Rust expected payloads like `{ type: "basic", username, password }` but TypeScript was emitting `{ kind: "basic", ... }`, causing deserialization to fail with `Missing field \`type\``. Now alef-backend-napi emits the discriminant name matching the Rust serde tag, and alef-e2e fixture code preserves the actual tag field name instead of rewriting all tags to "kind". Updated test assertions in alef-backend-napi to expect the serde tag name. (`src/backends/napi/src/gen_bindings/enums.rs`, `src/e2e/codegen/typescript/test_file.rs`)

## [0.16.24] - 2026-05-17

### Changed

- **packaging: fold `alef-snippets` into the main `alef` CLI as the `alef snippets` subcommand**: the workspace previously shipped two binaries (`alef-cli` → `alef`, and `alef-snippets`), forcing `cargo install --git ...` callers to disambiguate with `--package alef-cli --bin alef`. The standalone `alef-snippets` bin is removed; its CLI surface (list/validate/parse/audit/gaps) is now reachable as `alef snippets <cmd>`, matching the existing `alef fmt`/`alef lint`/`alef generate` pattern. Companion updates landed in `kreuzberg-dev/actions` (install-alef@v1 drops the `--bin alef` selector; lint-docs@v1 calls `install-alef` + `alef snippets validate`) and `kreuzberg-dev/pre-commit-hooks` v1.1.3 (alef-snippets-validate hook now invokes `alef snippets validate`). (`crates/alef-cli/{Cargo.toml,src/commands/{mod.rs,snippets.rs},src/main.rs}`, `crates/alef-snippets/{Cargo.toml,src/main.rs}`)

- **chore: drop taplo entirely from alef tooling**: CI no longer installs taplo-cli or runs `taplo fmt --check`; `.taplo.toml` is removed. TOML formatting in alef now relies on `cargo-sort` (Cargo.toml ordering) and `cargo-fmt` (inline TOML literals in Rust code) only. (`.github/workflows/ci.yml`, `.taplo.toml`, `.github/copilot-instructions.md`)

### Fixed

- **alef-backend-kotlin-android: also suppress `CyclomaticComplexMethod` in the generated `@file:Suppress` header**: data-class (de)serializers emitted for sealed `NodeContent`-style discriminated unions (with one branch per variant) sit just above detekt's default cyclomatic-complexity threshold of 15 — for html-to-markdown's 14-variant `NodeContent` they hit 16 (`deserialize`) and 15 (`serialize`). The existing file-level suppress block already silenced `MaxLineLength` / `TooManyFunctions` / etc.; adding `CyclomaticComplexMethod` rounds out the set so refactoring the (de)serializer into per-variant helpers isn't required just to get prek clean. Surfaced on html-to-markdown's prek run (`packages/kotlin-android/src/main/kotlin/dev/kreuzberg/android/NodeContent.kt:95,152`). (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-java: wrap the FFI-tail block in `convert` and `convertWithVisitorInternal` with `// CPD-OFF` / `// CPD-ON` markers**: PMD CPD flagged the ~15-line FFI marshalling tail (null-check `resultPtr` → `result_to_json` → free → null-check `jsonPtr` → `reinterpret` → free string → `MAPPER.readValue`) as a duplication between the two methods. The block is intentionally identical and extracting it into a helper would require threading the `Arena`, `MAPPER`, and free-handle through a private method for marginal benefit. Wrap both copies with CPD markers in `ffi_class.rs`, matching the precedent the builder-class emitter already uses. Surfaced on html-to-markdown's prek run (`packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/HtmlToMarkdownRs.java:39,85`). (`src/backends/java/gen_bindings/ffi_class.rs`)

- **alef-backend-kotlin: add `@file:Suppress` headers to JNI Bridge object + JNI client class kotlin files**: `emit_jni_bridge_object` (e.g. `LiterLlmBridge.kt`) and `emit_jni_client_class` (e.g. `DefaultClient.kt`) emit kotlin files that go through their own assembly path (NOT through `assemble_kt_content` in alef-backend-kotlin-android), so the `@file:Suppress` block added there did not cover them. They now emit their own `@file:Suppress(...)` block listing `MaxLineLength`, `ktlint:standard:max-line-length`, `TooManyFunctions`, `LongParameterList` (+ `LongMethod` on the client class), silencing detekt on JNI external-fun signatures and coroutine method signatures that naturally exceed 120 chars. Surfaced on liter-llm's prek run (`DefaultClient.kt:54,121,133` and `LiterLlmBridge.kt:12`). (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **alef-scaffold/ffi: add `futures-util = "0.3"` to `crates/<crate>-ffi/Cargo.toml` when the umbrella crate exposes a Streaming adapter**: the FFI codegen emits `use futures_util::StreamExt; ... stream.next().await` inside the streaming shim, but the FFI scaffold did not add the dep, producing `error[E0432]: unresolved import futures_util` on `cargo check -p <crate>-ffi`. Mirrors the same `has_streaming` check used by the PHP / Python / Ruby / Node scaffolds. Surfaced on liter-llm's prek run (`crates/liter-llm-ffi/src/lib.rs:10609`). (`crates/alef-scaffold/src/languages/ffi.rs`)

- **alef-backend-kotlin-android: suppress `MaxLineLength` violations in generated Kotlin files**: kotlin POJOs / data-class declarations with serde-renamed enum tags and multi-arg constructors regularly exceed the 120-char `MaxLineLength` limit enforced by detekt (and the ktlint `max-line-length` sibling rule). The `@file:Suppress(...)` header emitted at the top of every generated `.kt` file now lists both `MaxLineLength` (detekt) and `ktlint:standard:max-line-length` (ktlint) so the kotlin-android binding no longer flaps on every prek run. Surfaced on liter-llm's prek run (15+ `[MaxLineLength]` violations across `LiterLlm.kt`, `RerankDocument.kt`, `Message.kt`, etc.). (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-scaffold: add `futures-util = "0.3"` to `crates/<crate>-node/Cargo.toml` when the crate exposes a Streaming adapter**: the napi backend emits `use futures_util::StreamExt;` in streaming method bodies (e.g. `chatStream`), but the node Cargo.toml scaffold did not add the dep — only PHP / Ruby / Python had the conditional `has_streaming` check. `cargo check` failed on `unresolved import futures_util` for every umbrella crate with a chat-stream adapter on the node backend. Mirrors the same pattern used in `alef-scaffold/src/languages/php.rs:38`. Surfaced on liter-llm's prek run (`crates/liter-llm-node/src/lib.rs:992`). (`crates/alef-scaffold/src/languages/node.rs`)

- **alef-e2e dart: emit named-arg syntax for required string parameters**: the dart e2e codegen emitted positional literals for required string args (`_client.cancelBatch('batch-id')`) under the assumption the wrapper convention was POSITIONAL-required. The dart bridge backend (`alef-backend-dart`) emits ALL params as named-required (`Future<BatchObject> cancelBatch({required String batchId})`), so the generated tests failed `missing_required_argument` + `extra_positional_arguments_could_be_named` analyzer errors across `batches_test.dart`, `files_test.dart`, `responses_test.dart`. The `extractFile* → extractBytes*` mime_type remap special-case (kreuzberg) still emits positional. Surfaced on liter-llm's prek run (60+ dart-analyze errors). (`src/e2e/codegen/dart.rs`)

- **alef-scaffold: add `[package.metadata.cargo-machete] ignored = [...]` to FFI / PHP / Node / Python / JNI Cargo.toml scaffolds**: the binding Cargo.toml templates emit `serde_json` / `tokio` / `pyo3-async-runtimes` / `futures-util` unconditionally so the manifest is stable across regens (they are pulled in when the umbrella crate's API surface has async fns, streaming adapters, or JSON-marshalled return types). For umbrella crates with none of those — e.g. html-to-markdown's PHP and FFI bindings — those deps are genuinely unused and `cargo machete` flags them as a finding on every prek run. Mirrors the precedent set by `alef-backend-swift` (`async-trait`, `serde`, `tokio`). The conditional deps (`async-trait`, `futures-util` in PHP, `tokio` in Python) that _are_ only emitted under specific feature combinations don't need ignoring since they aren't in the manifest when not used. Surfaced on html-to-markdown's prek run (5 binding crates flagged). (`crates/alef-scaffold/src/languages/{ffi,php,node,python,jni}.rs`)

- **alef-backend-pyo3: silence ruff TC001 on the runtime re-export block in `options.py`**: the generated options module does `from .<native> import (CodeBlockStyle, HeadingStyle, ...)` so that downstream users can `from html_to_markdown.options import NewlineStyle` at runtime. Ruff's TC001 saw the imports referenced only in annotations (the file uses `from __future__ import annotations`) and demanded they move under `if TYPE_CHECKING:`, which would break the runtime re-export. The `import_item.jinja` template now appends `# noqa: TC001` to each enum import line, preserving the runtime re-export while satisfying the lint. Tried `as`-alias re-exports first; ruff TC001 still fires through that form. Surfaced on html-to-markdown's prek run (10 TC001 errors in `packages/python/html_to_markdown/options.py`). (`src/backends/pyo3/templates/import_item.jinja`)

- **alef-backend-jni: migrate generated shims to jni 0.22 — `EnvUnowned` in extern sigs, `AttachGuard` body, `JNIString` for throw_new args, `JString::try_to_string` for decode**: jni 0.22 split `JNIEnv` into FFI-safe `EnvUnowned<'frame>` and `Env<'_>` (which carries the real API surface). The pre-0.22 codegen emitted `use jni::JNIEnv;` plus extern shims with `mut env: JNIEnv` bodies that called `env.get_string(&s)?`, `env.new_string(s)`, and `env.throw_new(ERROR_CLASS, msg)` directly, which now (a) emits deprecation warnings for the `JNIEnv` alias, (b) trips `E0599` for `get_string` / `new_string` / `throw_new` on `&mut EnvUnowned`, and (c) trips `E0277` for `throw_new` because 0.22 requires `S: AsRef<JNIStr>` / `T: Desc<JClass>` (Rust `&str` no longer coerces). The codegen now: (1) emits `use jni::{AttachGuard, Env, EnvUnowned};` and declares extern args as `mut env: EnvUnowned`, (2) upgrades to `&mut Env<'_>` inline via `AttachGuard::from_unowned(env.as_raw())` + `borrow_env_mut()` (avoids `EnvUnowned::with_env`'s `FnOnce(&mut Env) -> Result<T, E>` closure shape which would force rewriting every early-return + sentinel path), (3) replaces `env.get_string(&s)?` with `s.try_to_string(env)`, and (4) wraps both `class` and `msg` args to `env.throw_new` in `jni::strings::JNIString::from(...)`. Helpers (`jstring_to_string`, `string_to_jstring`, `throw_jni_error`, `run_or_throw`) now take `&mut Env<'_>`. `_env`-only shims (destructors, streaming free) declare `_env: EnvUnowned` with no body. Verified clean on `cargo clippy -p html-to-markdown-rs-jni` against the regenerated shim. Snapshots refreshed; the 25-test suite still passes. Surfaced on html-to-markdown's prek run. (`src/backends/jni/src/gen_shims.rs`, `src/backends/jni/tests/{gen_shims_test.rs,snapshots/*.snap}`)

- **alef-hook (`alef-verify` / `alef-sync-versions`): resolve consumer `alef.toml` by walking up from CWD**: `_version()` previously read `_hooks_dir().parent / "alef.toml"`, which points at the prek-cached clone of this repo (the dir where `alef_hook.py` lives), NOT the consumer project being verified. That made consumer bumps of `alef.toml` invisible to the hook — it kept downloading the binary version pinned by the prek `rev:` tag, surfacing as `HTTP 404` when the rev's release had no assets uploaded (e.g. kreuzberg pinning prek `rev: v0.16.23` and bumping `alef.toml` to `0.16.24`). `_find_consumer_alef_toml()` now walks up from `Path.cwd()` until it finds an `alef.toml`, matching how prek invokes hooks (CWD = consumer repo root). (`hooks/alef_hook.py`)

- **alef-backend-rustler: fix `@type t` struct field indentation for mix format**: field lines inside `@type t :: %__MODULE__{...}` were indented with 4 spaces; `mix format` requires 10-space alignment to the opening `{` column. Surfaced on kreuzcrawl prek run where 28+ `.ex` files including `CrawlPageResult`, `ContentConfig`, `JsonLdEntry` etc. were reformatted. (`src/backends/rustler/src/gen_bindings/helpers.rs`)

- **alef-scaffold: emit space-inside-brackets in pyproject.toml inline arrays**: `pyproject-fmt` rewrites inline arrays from `["x"]` to `[ "x" ]`. The scaffold template now emits the spaced form for `requires`, `keywords`, `features`, `python-packages`, `dev`, `lint.select`, and `lint.per-file-ignores` so the generated pyproject.toml is idempotent under pyproject-fmt. Surfaced on kreuzcrawl prek run against `packages/python/pyproject.toml`. (`crates/alef-scaffold/src/languages/python.rs`)

- **alef-backend-kotlin-android: add `@file:Suppress` header to suppress ktlint/detekt violations in generated Kotlin files**: generated Kotlin files violated `ktlint:standard:trailing-comma-on-call-site`, `ktlint:standard:spacing-between-declarations-with-comments`, `ktlint:standard:when-entry-bracing`, `ktlint:standard:annotation`, and detekt's `TooManyFunctions` on ~30 files in kreuzcrawl. A file-level `@file:Suppress(...)` annotation is now emitted at the top of every assembled `.kt` file before the package declaration. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-kotlin-android: extend `@file:Suppress` to cover `trailing-comma-on-declaration-site`, `spacing-between-declarations-with-annotations`, `blank-line-between-when-conditions`, and `chain-method-continuation`**: ktlint flagged additional rules on kreuzcrawl's generated `MapResult.kt` (missing trailing comma on data class constructor) and `AuthConfig.kt` (spacing around annotated declarations, blank lines between when-conditions, chain method continuation). All four new rules are now included in the `@file:Suppress` block. (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-backend-kotlin: add `ktlint:standard:annotation` to `@file:Suppress` in JNI bridge and client files**: `ktfmt` (run by prek) compacts `@Throws(...)\n    external fun` into a single line, which then triggers ktlint's `standard:annotation` rule. Adding `ktlint:standard:annotation` to both the bridge-object and client-class `@file:Suppress` blocks silences the conflict. Snapshots updated. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **alef-backend-swift: add `// swift-format-ignore-file` directive to generated Swift files**: swift-format reformatted `packages/swift/Sources/Kreuzcrawl/Kreuzcrawl.swift` on every prek run. The `// swift-format-ignore-file` magic comment is now emitted at the top of every generated Swift bindings file, instructing swift-format to skip the file entirely. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-java: wrap generated Builder class with CPD-OFF/CPD-ON markers**: PMD's copy-paste detector flagged 64-line (213 token) and 19-line (121 token) duplications across `CrawlPageResult.java` and `ScrapeResult.java` builder classes in kreuzcrawl. `// CPD-OFF` and `// CPD-ON` comments are now emitted around the nested builder class so CPD suppresses these intentional structural duplications. (`src/backends/java/gen_bindings/types.rs`)

- **alef-e2e dart: emit `// ignore_for_file: unused_local_variable` in generated test files**: `final result = await api.method(args)` is emitted for every test case; tests that only assert no error is thrown do not use `result`, producing ~1266 `unused_local_variable` warnings across kreuzcrawl's dart e2e suite. The file-level ignore directive is now prepended after the alef header in every generated `*_test.dart` file. (`src/e2e/codegen/dart.rs`)

- **alef-backend-dart: skip trait-bridge methods that return `alef(skip)`-stripped trait types**: `return_type_references_trait` only inspected `api.types`, which has already had `binding_excluded` items stripped by the time codegen runs. Traits annotated `#[cfg_attr(alef, alef(skip))]` (e.g. kreuzberg's `SyncExtractor`) therefore slipped past the filter, and the resulting `Box<dyn Fn() -> DartFnFuture<Option<kreuzberg::extractors::SyncExtractor>>>` field on the bridge struct failed to compile with `error[E0782]: expected a type, found a trait` (23 errors across `packages/dart/rust/src/{lib.rs,frb_generated.rs}`). A new `ApiSurface::excluded_trait_names: HashSet<String>` is populated alongside `excluded_type_paths` during `strip_binding_excluded`, preserving the `is_trait` flag that would otherwise be lost. The dart trait-bridge filter now consults this set so excluded **traits** are filtered out while excluded **structs** (e.g. `InternalDocument` returned by `extract_bytes`) continue to be emitted by qualified core path. Added unit tests covering in-surface traits, excluded traits, excluded structs, and unrelated named types. (`src/core/ir.rs`, `crates/alef-cli/src/pipeline/extract.rs`, `src/backends/dart/src/gen_rust_crate/trait_bridge.rs`)

- **alef-backend-ffi: allow `clippy::missing_safety_doc` in the generated C-FFI lib.rs**: every `pub unsafe extern "C" fn` emitted by the FFI backend lacks a `# Safety` rustdoc section (the safety contract is documented at the C-header / cbindgen layer instead). Downstream consumers running `cargo clippy -- -D warnings` on the FFI crate therefore tripped 13+ `missing_safety_doc` errors per polyglot library. The crate header now includes `#![allow(clippy::missing_safety_doc, clippy::doc_lazy_continuation, clippy::doc_overindented_list_items)]` alongside the existing allow blocks. Surfaced first on kreuzcrawl CI Rust @ `74021aa7`. (`src/backends/ffi/src/gen_bindings/mod.rs`)

- **alef-backend-swift: disable `cargo test`/`doctest`/`bench` for the generated `*-swift` crate**: the `[swift_bridge::bridge]` `extern "Swift" { ... }` block emits Rust-to-Swift call sites whose symbols (`__swift_bridge__$<Type>$<method>`) are only resolved when the static/cdylib output is linked into a Swift target. `cargo test --workspace` on a pure-Rust runner (windows-latest, ubuntu-latest without swift-toolchain) would therefore fail with `rust-lld: error: undefined symbol: __swift_bridge__$SwiftHtmlVisitorBox$alef_visit_*` (and ~30 more) during the test-harness link step. The generated `Cargo.toml`'s `[lib]` now sets `test = false`, `doctest = false`, `bench = false` — the cdylib/staticlib build path is unaffected, and the swift package's own `swift test` suite remains the authoritative test surface for this crate. (`src/backends/swift/src/gen_rust_crate/cargo.rs`)

- **alef-scaffold: emit `extra.pie.binary.url-template` block in PHP composer.json for explicit PIE binary discovery**: PIE's `pre-packaged-binary` download method relies on asset filename pattern matching, which has known edge cases with architecture aliases and libc detection (kreuzberg-dev/html-to-markdown#333). The PHP scaffold now derives the GitHub repo owner/name from `metadata.repository` and emits an `extra.pie.binary.url-template` entry pointing to the releases' archive pattern, giving PIE explicit binary URLs when pattern matching alone is insufficient. The block is only emitted when the repository URL is valid (github.com-based). Template variables (`{Version}`, `{PhpVersion}`, `{Arch}`, `{OS}`, `{Libc}`, `{TSMode}`) follow PIE 1.4.x naming conventions. Also fixes the Composer package name derivation to use the GitHub repo name (e.g. `html-to-markdown`) instead of the crate name (e.g. `html-to-markdown-rs`), ensuring Packagist package names are correct. (`crates/alef-scaffold/src/languages/php.rs`)

- **alef-backend-dart: pin `flutter_rust_bridge` with `=`-prefixed strict version**: flutter_rust_bridge enforces exact runtime/compile-time version matching — a caret-pin lets `cargo update` pull in a patch release that the generated bridge code can't talk to. The Rust crate's `Cargo.toml` template now emits `flutter_rust_bridge = "=2.12.0"` so `cargo update` cannot drift off the matching version. (`src/backends/dart/src/gen_rust_crate/cargo.rs`)

- **alef-backend-kotlin: drop redundant `${...}` braces in error-message interpolation when the next char is not an identifier continuation**: `interpolate_error_message_template` previously always emitted `${fieldN}` for `{N}` placeholders, producing `"HTML parsing error: ${field0}"` for the common single-field `thiserror`-style template. ktlint's `standard:string-template` rule flags this as redundant when the brace is unnecessary (the next character — `"`, `:`, ` `, `)`, etc. — cannot extend the identifier), and the rule is a CI gate in the polyglot host repos. The helper now peeks at the character following the closing brace and emits `$fieldN` when it would be unambiguous, falling back to `${fieldN}` only when the next char is `[A-Za-z0-9_]`. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-scaffold: scaffold the JNI crate at `crates/<config.name>-jni/` to match the path that `alef-backend-jni::gen_shims` uses for `src/lib.rs`**: `scaffold_jni` derived the JNI crate directory from `config.core_crate_dir()`, while `gen_shims::jni_output_path` derived it from `config.name`. On any workspace where the two differ (e.g. tree-sitter-language-pack with `name = "tree-sitter-language-pack"` and `sources = ["crates/ts-pack-core/src/lib.rs"]`), regen produced an orphan `crates/ts-pack-core-jni/Cargo.toml` with no `src/`, while the actual JNI shim landed under `crates/tree-sitter-language-pack-jni/`. cargo broke the entire workspace with `can't find library 'ts_pack_jni', rename file to 'src/lib.rs' or specify lib.path`, taking cargo-check, cargo-clippy, cargo-fmt, cargo-machete, and cargo-deny down with it. The scaffold now uses `config.name` for the crate directory and the `[package] name`, and uses the cargo package name (`config.name`) as the umbrella dep-table key while keeping `path = "../<core_crate_dir>"` for the on-disk location. Added regression test `scaffold_jni_path_uses_config_name_not_core_crate_dir`. (`crates/alef-scaffold/src/languages/jni.rs`)

- **alef-backend-swift: add `tokio` to the generated Cargo.toml's `[package.metadata.cargo-machete] ignored = [...]` list**: alongside the existing `async-trait` / `serde` entries, `tokio` is now ignored. The Swift backend emits `tokio` unconditionally in `[dependencies]` for stable-manifest reasons (matches the documented "conditionally referenced only when the umbrella crate declares trait bridges or async adapters" pattern). For umbrella crates without trait bridges or async adapters — e.g. tree-sitter-language-pack — `tokio` is genuinely unused, and `cargo machete` correctly flagged it. Listing it in `ignored` matches the same approach the template already uses for the other conditional deps. Surfaced on tree-sitter-language-pack prek run. (`src/backends/swift/src/gen_rust_crate/cargo.rs`)

- **alef-backend-rustler: align the closing `}` of `@type t :: %__MODULE__{...}` to 8-space indent so `mix format` accepts it idempotently**: the typespec block opened at 10-space indent for fields (column 11) but closed with `}` (2 spaces), which `mix format` rewrote to 8 spaces. tslp's 19+ struct files (`ProcessConfig`, `ProcessResult`, `FileMetrics`, `Span`, etc.) flapped between emit and format on every prek run. Closing brace now matches the column `mix format` settles on. Surfaced on tree-sitter-language-pack prek run. (`src/backends/rustler/src/gen_bindings/helpers.rs`)

- **alef-scaffold: bump the JNI Cargo.toml's `jni` dep from `0.21` to `0.22`**: `alef-backend-jni::gen_shims` emits `use jni::{Env, JNIEnv};` (jni 0.22 split `JNIEnv` into FFI-safe `EnvUnowned<'frame>` and the API-carrying `Env<'_>`) but the scaffold template was still pinning `jni = "0.21"`. tslp's regen produced a `tree-sitter-language-pack-jni` crate that failed `cargo check` with `unresolved import jni::Env`. Bumped the scaffold to match the shim emitter. Surfaced on tree-sitter-language-pack prek run. (`crates/alef-scaffold/src/languages/jni.rs`)

## [0.16.23] - 2026-05-16

### Fixed

- **alef-core: sanitize Zig enum tag identifiers**: `zig_ident` previously escaped reserved keywords only; serde-renamed tags such as `og:image` or `Content-Type` flowed through unchanged and emitted invalid Zig (`enum fields do not have types` at the colon). The helper now replaces every non-`[A-Za-z0-9_]` character with `_`, prefixes a leading digit with `_`, then runs the result through `zig_safe_name`. Affects every Zig enum variant whose serde rename contains punctuation. (`src/core/keywords.rs`)

- **alef-backend-php: skip `binding_excluded` fields in `pub fn new(...) -> Self` literals**: the named-constructor emitter iterated every field of the IR type and wrote `name: Default::default()` for fields marked `binding_excluded`, but those fields are stripped from the binding struct itself — producing `error[E0560]: struct X has no field named Y` (seen on `DownloadedDocument.content` and `ScrapeResult.screenshot` in kreuzcrawl-php). The constructor now filters `binding_excluded` fields entirely; only `cfg`-gated fields (which stay in the struct under a `#[cfg]` gate) still receive `Default::default()` initializers. Both the `has_serde` and the non-serde branches of `gen_struct_methods_impl` are fixed. (`src/backends/php/src/gen_bindings/types.rs`)

- **alef-backend-go: emit sealed-interface fields as bare interface, not pointer-to-interface**: struct fields whose type is a tagged-data (sealed-interface) enum were being emitted as `*AuthConfig` for both optional and `has_default` positions, producing `cannot use v (variable of interface type AuthConfig) as *AuthConfig value in assignment: AuthConfig does not implement *AuthConfig (type *AuthConfig is pointer to interface, not interface)` at compile time. Go interfaces are already nullable — the interface zero value is `nil` — so taking a pointer to one is never the right shape. `gen_struct_type` detects sealed-interface fields via `data_enum_names` and emits the bare type for both struct declarations and builder option setters; the custom `UnmarshalJSON` emitter also drops the `&` when assigning the decoded variant. `gen_config_options` now takes `data_enum_names` and skips the `&v` address-of path for these fields. Test `test_parent_struct_with_optional_data_enum_field_emits_custom_unmarshal_json` updated to assert the new shape. (`src/backends/go/src/gen_bindings/{types.rs,mod.rs}`, `src/backends/go/tests/bug_fixes_test.rs`)

### Changed

- **alef-snippets: parser tolerates malformed YAML frontmatter**: `parse_frontmatter` previously returned `Error::Parse` when a snippet file opened with `---\n` but had no closing `\n---\n` delimiter, or when the YAML between delimiters failed to deserialize. Snippet collections that include partial templates or human-edited drafts would short-circuit the entire run. The parser now treats these cases as "no frontmatter": `SnippetMetadata::default()` plus the original `content` are returned, and validation continues against the body. Strict structural reporting still lives in `frontmatter_status`, which downstream `audit` consumers can opt into. (`crates/alef-snippets/src/parser.rs`)

- **alef-snippets: gaps detection recognises binding-specific language dir names**: `gaps::language_group` previously formed the `{language}` placeholder by string-matching the rendered `Language::to_string()` value, so `docs/snippets/kotlin_android/...` produced no group (`kotlin_android` != `kotlin`) and `gaps` could not flag missing variants for kotlin-android consumers. The matcher now passes each path component through `Language::from_dir_name`, which already understands aliases like `kotlin_android`, `kotlin-android`, `c-sharp`, `dotnet`, `golang`, and the `wasm`/`node` → typescript shortcuts. (`crates/alef-snippets/src/gaps.rs`)

- **alef-snippets: accept `kotlin_android` and `kotlin-android` as kotlin in `from_dir_name`**: directory names used by polyglot consumers (`kotlin_android`, `kotlin-android`) now map to `Language::Kotlin`. (`crates/alef-snippets/src/types.rs`)

- **alef-snippets: go/java/csharp validators wrap bare fragments**: parity with the existing `RustValidator::wrap_if_fragment` path. Snippets that omit the `package main` / `public class` / `Main`-method boilerplate now compile cleanly at syntax level. (1) `GoValidator` splits leading `import (...)` / `import "..."` blocks from the body and wraps the body in `package main; func main() { ... }`; comment-only bodies get a placeholder `_ = 0` statement so the entry point still compiles. (2) `JavaValidator` detects an existing top-level class/interface/enum/record and skips wrapping; otherwise it splits leading `import`/`package` declarations and wraps in `public class Snippet { public static void main(String[] args) throws Exception { ... } }`. (3) `CsharpValidator` wraps comment-only bodies with a `return;` placeholder so the top-level-statements compiler entry point is satisfied; the `is_dependency_error` list also picks up `CS5001` ("Program does not contain a static 'Main' method") for the placeholder fall-through. (`crates/alef-snippets/src/validators/{go,java,csharp}.rs`)

- **alef-snippets: C validator broadens dependency-error pattern**: `CValidator::is_dependency_error` now matches `file not found`, `No such file or directory`, `undeclared identifier`, `implicit declaration`, and `unknown type name` — so snippets that legitimately `#include "html_to_markdown.h"` (or any other host-supplied header) downgrade to Pass at syntax level when the include path is not configured in the validator sandbox, instead of failing CI on a legitimate doc example. (`crates/alef-snippets/src/validators/c.rs`)

### Added

- **alef-snippets: add JSON and YAML in-process validators**: snippets in `` ```json `` / `` ```yaml `` fences are now syntax-checked by deserializing into `serde_json::Value` / `serde_yaml::Value`. Both validators run in-process (no external toolchain required) and report concrete parser error messages on failure. (`crates/alef-snippets/src/validators/{json_validator,yaml_validator}.rs`, `crates/alef-snippets/src/validators/mod.rs`)

- **alef-snippets: add R validator (`Rscript -e 'parse(file=...)'`)**: probes for `Rscript` and `R` on PATH; uses `parse(file=...)` for syntax/compile levels and `Rscript <file>` for run level. Returns `Unavailable` when the R toolchain is missing. (`crates/alef-snippets/src/validators/r.rs`)

- **alef-snippets: register documentation-only languages (`text`, `mermaid`, `powershell`, `xml`, `docker`) with no-op pass-through validators**: doc fences in these formats are now recognised by `Language::from_fence_tag` and short-circuit to `Pass` instead of falling through to `Unknown`. Prevents `audit` from reporting them as `UnknownLanguage` errors. (`crates/alef-snippets/src/validators/documentation.rs`, `crates/alef-snippets/src/types.rs`)

- **alef-snippets: add C validator**: closes the last language gap. The `CValidator` probes `cc`, `clang`, then `gcc` for availability and shells out to the first match. Syntax level uses `cc -fsyntax-only`; Compile compiles to a temp binary via `cc -o <out>`; Run additionally executes the compiled binary. (`crates/alef-snippets/src/validators/c.rs`, `crates/alef-snippets/src/validators/mod.rs`)

- **alef-snippets: add 8 missing language validators (c, csharp, dart, go, java, kotlin, swift, zig)**: the validator registry previously covered bash/elixir/php/python/ruby/rust/toml/typescript (8 languages); snippets in any other language returned `Unavailable`. The 8 new validators each gate on toolchain presence via `which::which(...)` and shell out to the upstream toolchain: `cc -fsyntax-only` (Syntax) / `cc -o <out>` (Compile/Run) for C, `dotnet build --nologo -v quiet` (Syntax/Compile) for C#, `dart analyze --no-fatal-warnings` for Dart, `gofmt -e -l` (Syntax) / `go build` (Compile) / `go run` (Run) for Go, `javac -Xlint:none` (with class-name extraction so the temp file matches the public class) for Java, `kotlinc -d <out>` for Kotlin, `swiftc -parse` (Syntax) / `swiftc -o <out>` (Compile/Run) for Swift, `zig ast-check` (Syntax) / `zig build-exe -fno-emit-bin` (Compile) for Zig. (`crates/alef-snippets/src/validators/{c,csharp,dart,go,java,kotlin,swift,zig}.rs`, `crates/alef-snippets/src/validators/mod.rs`)

- **alef-snippets: wire `audit` and `gaps` CLI subcommands**: the `alef-snippets` binary previously exposed only `list`, `validate`, and `parse`. The `audit` (structural integrity report — frontmatter, fences, includes) and `gaps` (coverage gaps — missing includes, unreferenced snippets, missing language variants, unannotated skips, unknown languages) library entry points are now reachable from the CLI. `--snippets <dir>... --docs <dir>... [--require-frontmatter | --required-languages <list>]`. Both commands exit with a non-zero status when issues are found, so they slot into pre-commit / CI gates. (`crates/alef-snippets/src/main.rs`)

- **workspace: register `alef-snippets` crate + add missing workspace deps**: `crates/alef-snippets` is now a `[workspace.members]` entry and a `[workspace.dependencies]` alias, fixing the prior cargo error `cannot specify features for packages outside of workspace`. Added `tempfile = "3"`, `walkdir = "2"`, and `which = "8"` as workspace dependencies — the alef-snippets crate referenced them via `workspace = true` before any workspace-level declaration existed. (`Cargo.toml`)

## [0.16.22] - 2026-05-16

### Fixed

- **alef-e2e: remove vite-plugin-wasm from WASM e2e vitest config**: `wasm-pack build --target nodejs` produces a self-initializing CommonJS bundle that loads the `.wasm` binary via `fs.readFileSync` at import time — it does not require the browser-oriented `vite-plugin-wasm` Vite transform. Including `vite-plugin-wasm` caused all `Wasm*` class exports to be `undefined` when running under wasm-pack 0.13.x (installed by the `curl | sh` installer in CI), because the plugin intercepted the CJS `require()` path and returned an uninitialized `WebAssembly.Module` instead of the module object. Removed `import wasm from 'vite-plugin-wasm'` and `plugins: [wasm()]` from `vitest.config.ts.jinja`, and removed the `vite-plugin-wasm` devDependency from `package.json.jinja`. (`src/e2e/templates/wasm/vitest.config.ts.jinja`, `src/e2e/templates/wasm/package.json.jinja`, `src/e2e/codegen/wasm.rs`)

- **alef-backend-wasm: add `is_sanitized_fixed_tuple_array` predicate for fixed tuple-array fields**: tagged-enum struct fields that were originally `[(K, V); N]` but sanitized to `String` were not being detected as JsValue candidates, causing them to be stored as `Option<String>` in the binding struct rather than `Option<JsValue>`. Added `is_sanitized_fixed_tuple_array` predicate (matching `original_type` that starts with `[(` and contains `);`) and wired it alongside `is_sanitized_tuple_vec` in all three collection sites in `gen_tagged_enum_as_struct`, `gen_tagged_enum_binding_to_core`, and `gen_tagged_enum_core_to_binding`. (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-backend-jni + alef-backend-kotlin: fix JNI exception swallowing in generated Android bridge**: two gaps caused Rust `Result::Err` / panics to be silently lost instead of reaching Kotlin callers as typed exceptions. (1) The generated `Bridge.kt` `external fun` declarations were missing `@Throws(<Bridge>Exception::class)` annotations — without this, JNI-thrown `RuntimeException` is wrapped in `UndeclaredThrowableException` or otherwise bypasses typed `catch` blocks. Every non-destructor `external fun` (top-level functions, instance methods, streaming Start/Next shims) now carries `@Throws`. Destructor shims remain annotation-free (they are infallible drop operations). (2) The generated `throw_jni_error` helper emitted `let _ = env.throw_new(ERROR_CLASS, msg)`, silently discarding failure. If the exception class was not on the classpath (misconfigured AAR) the shim returned a null/zero sentinel with no exception pending, appearing to Kotlin as a valid return value. The helper now checks `is_err()` and falls back to `java/lang/RuntimeException` so the caller always receives an exception. Adds explicit unit tests and updates all snapshots. (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`, `src/backends/jni/src/gen_shims.rs`)

- **alef-backend-swift: preserve `Option<T>` as `T?` in enum-variant associated values**: `emit_variant_with_data` previously called `mapper.map_type(&f.ty)` without checking `field.optional`, so fields where the extractor had unwrapped `TypeRef::Optional(inner)` into `(ty: inner, optional: true)` were emitted as bare `T` instead of `T?`. The same gap existed in `emit_error` for error-variant fields. Both sites now check `f.optional && !already_optional` and append `?` when the flag is set, matching the existing behavior in `emit_first_class_struct`. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/snapshot_test.rs`)

- **alef-backend-kotlin: emit ktfmt-stable data-class declarations**: the Kotlin data-class emitter now applies ktfmt's 100-character heuristic — declarations whose single-line form fits within 100 chars are emitted as a single line (`data class Foo(val a: Int, val b: T)`); longer declarations stay multi-line. Previously the emitter always used multi-line, causing ktfmt in CI to rewrite every short declaration and break the lint step. The same heuristic applies to sealed-class variant data classes and error-variant data classes. Annotations (`@JsonDeserialize` / `@JsonSerialize`) on variant subclasses force multi-line regardless of length. Field-level KDoc also forces multi-line. Adds 6 new unit tests: short/long coverage for top-level data classes, sealed-class variants, and error variants. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, `src/backends/kotlin/tests/gen_bindings_test.rs`)

## [0.16.21] - 2026-05-16

### Added

- **alef-backend-php: emit per-property `@var` PHPDoc on promoted readonly constructor parameters**: generated DTO classes now carry an inline `/** @var T description */` block above every promoted property (and `/** ... @var T */` block for multi-line rustdoc). PHPStan level max now sees the precise generic element type (`@var array<string>`, `@var ?int`) and IDEs surface tooltips on every property. Replaces the prior global `@param` block before the constructor, which only covered array fields and didn't propagate field rustdoc. Adds `php_property_phpdoc()` helper + 153 lines of unit tests covering empty/single-line/multi-line doc forms. (`src/backends/php/src/gen_bindings/mod.rs`, `src/backends/php/tests/gen_bindings_test.rs`)

- **alef-core: `KotlinAndroid` package-directory lookup**: `default_package_directory` now maps `Language::KotlinAndroid` to `packages/kotlin-android` explicitly instead of falling through to the generic `packages/{lang}` formatter (which produced `packages/kotlinandroid`). (`src/core/config/resolved/lookups.rs`)

- **alef-backend-ffi: emit Doxygen comments on C headers**: every `extern "C" fn` declaration, `typedef`, opaque-handle wrapper, enum, and enum variant in the generated `.h` files now carries a `/** ... */` Doxygen block derived from the upstream rustdoc. `# Arguments` items render as `\param`, `# Returns` as `\return`, `# Errors` and `# Safety` as `\note`, `# Example` as `\code`/`\endcode`. C/C++ consumers and IDEs (clangd, Xcode, Visual Studio) now surface tooltips on every public FFI symbol. Adds `emit_c_doxygen` to the shared `alef-codegen::doc_emission` module. (`src/codegen/doc_emission.rs`, `src/backends/ffi/src/gen_bindings/{functions.rs,types.rs,enums.rs}`, snapshot tests)

- **alef-backend-go: emit Godoc on methods + free functions**: every exported method declaration `func (r *Receiver) Method(...)` and every free function `func Name(...)` in generated `.go` files now carries `// Method ...` / `// Name ...` Godoc derived from upstream rustdoc. Comments start with the symbol name (per the `go doc` and `godoc` convention) so IDE tooltips and `pkg.go.dev` render every public binding member. `# Arguments` → `// Arguments:` bullets, `# Returns` → `// Returns ...`, `# Errors` → `// Errors are returned when ...`, `# Example` → `// Example:` indented code block. Also fixes a pre-existing double-prefix glitch where summaries that already started with the symbol name produced `// RootNode RootNode returns ...`. (`src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/tests/snapshot_test.rs`)

- **alef-backend-java: close docstring emission gaps at record components, opaque-handle instance methods, and free-function FFI surface**: per-component Javadoc now renders above each `record` field when the multi-line emit path is taken (single-line records skip per-component docs because inlining `/** ... */` inside a comma-joined parameter list produces invalid Java). Opaque-handle instance methods emitted by `gen_instance_method` now carry their rustdoc above the public signature. Free-function static methods in the raw FFI class (`gen_sync_function_method`) emit Javadoc derived from `FunctionDef.doc`. (`src/backends/java/gen_bindings/{types.rs,ffi_class.rs}`, snapshot tests in `src/backends/java/tests/docs_emission_test.rs`)

- **alef-backend-zig: close docstring emission gaps at struct fields and enum variants**: `///` doc comments now render above each struct field declaration inside `pub const Foo = struct { ... };` and above every enum tag — both for unit-only `enum { ... }` and tagged-union `union(enum) { ... }` shapes (void / single-value / struct-variant cases). Type/method/free-function emission was already covered upstream. (`src/backends/zig/src/gen_bindings/types.rs`, snapshot tests in `src/backends/zig/tests/docs_emission_test.rs`)

- **alef-backend-kotlin-android: close docstring emission gaps at data-class fields, enum variants, and sealed-class variants**: the kotlin-android backend reuses the shared `alef-backend-kotlin` emitter helpers; this change wires `emit_cleaned_kdoc` at every previously-uncovered site (data-class primary-constructor field, plain enum constant, sealed-class nested variant) so KDoc now propagates through to AAR-emitted `.kt` files. The free-function facade, JNI bridge object, and handle-only opaque wrapper class already emitted KDoc at the kotlin-android-local sites. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`, snapshot tests in `src/backends/kotlin-android/tests/docs_emission_test.rs`)

- **alef-backend-rustler: emit `@moduledoc`/`@typedoc`/`@doc` on DTO modules + enum variants**:
  previously only NIF stubs (function-level) carried rustdoc-derived `@doc` heredocs; the
  generated Elixir DTO `defmodule` blocks, unit-enum variant accessors, and data-enum
  per-variant type aliases emitted no doc attributes, leaving `mix docs` with blank ExDoc
  pages for those modules. Now wires a shared `emit_elixir_doc_attr` helper at every
  applicable site so `mix docs` shows complete coverage: full multi-line `@moduledoc`
  heredocs on struct-bearing modules (DTO + opaque resource wrappers) and enum modules,
  `@typedoc` above each `@type t ::` typespec and above each data-enum per-variant type
  alias, and `@doc` above each unit-enum variant accessor function (preserving
  `@doc → @spec → def` ordering for mix-format compliance). Modules with no rustdoc still
  fall back to `@moduledoc false` to silence ExDoc warnings.
  (`src/backends/rustler/src/template_env.rs`,
  `src/backends/rustler/src/gen_bindings/helpers.rs`,
  `src/backends/rustler/tests/elixir_module_doc_test.rs`)

- **alef-codegen: language-aware example filtering (`example_for_target`)**: rustdoc `# Example` blocks fenced as ` ```rust ` (and bare-fenced ` ``` ` which rustdoc treats as Rust) are now suppressed from non-Rust doc renderers — YARD (`render_yard_sections`), JSDoc (`render_jsdoc_sections`), and PHPDoc (`render_phpdoc_sections`). Examples fenced with the target language (` ```php `, ` ```ruby `, ` ```typescript `) flow through verbatim. The prior behavior produced misleading Rust source bodies inside Ruby/PHP/TS doc tooltips. 5 new unit tests plus updated assertions in the existing renderer tests. (`src/codegen/doc_emission.rs`)

- **alef-backend-rustler: full multi-line `@doc` on functions + methods**: `@doc` strings for NIF functions and methods previously truncated to the first line via `func.doc.lines().next()`, dropping all subsequent paragraphs. The emitter now calls `doc_first_paragraph_joined` to preserve the complete first paragraph (joining wrapped lines) and falls back to `"Function"`/`"Method"` only when rustdoc is empty. Closes the gap left by the v0.16.20 single-line truncation noted in the audit. (`src/backends/rustler/src/gen_bindings/mod.rs:682,1172`)

- **alef-backend-magnus: YARD `@param`/`@return` on tagged-enum variant classes**: generated Ruby variant subclasses (`class TextContent < ContentPart`) now carry YARD tags on every site that consumers see in IDE tooltips: each `attr_reader` field, the variant `initialize` parameter list, the predicate method (`def text? = true`), and the class-level `from_hash` factory. The variant's fields use either upstream rustdoc (via `emit_yard_doc`) or a synthesized `# @return [SorbetType]` line so `solargraph` and `yard server` always resolve the type. (`src/backends/magnus/gen_bindings/mod.rs:gen_tagged_enum_ruby_classes`)

### Fixed

- **alef-cli/alef-e2e: use `oxfmt` instead of Taplo for generated TOML normalization**:
  the default post-generation formatters now call `pnpm dlx oxfmt` for WASM
  manifests, FFI/workspace formatting, and Rust e2e `Cargo.toml` normalization,
  aligning generated output with Kreuzberg's shared pre-commit hooks.
  (`crates/alef-cli/src/pipeline/format.rs`, `src/e2e/format.rs`)

- **alef-backend-ffi: escape generated `cbindgen.toml` `after_includes` safely**:
  Doxygen blocks embedded in `after_includes` now render as escaped TOML
  multiline strings, so backslash commands such as `\code` and rustdoc examples
  containing triple quotes no longer make `cbindgen.toml` invalid.
  (`src/backends/ffi/templates/cbindgen_toml.jinja`,
  `src/backends/ffi/src/gen_bindings/helpers.rs`,
  `src/backends/ffi/src/gen_bindings/mod.rs`)

- **alef-backend-wasm: emit current `getrandom` for wasm target dependencies**:
  generated WASM `Cargo.toml` files now use `getrandom` `0.4` with `wasm_js`, so downstream
  aggressive dependency upgrades no longer make generated manifests stale.
  (`src/backends/wasm/gen_bindings/mod.rs`,
  `src/backends/wasm/tests/gen_bindings_test.rs`)

- **alef-e2e: omit non-runnable Python and Node fixtures instead of generating skipped tests**:
  fixtures that have no assertions for the target language, or HTTP 101 upgrade
  fixtures that the standard Python/Node HTTP clients cannot execute, are now
  left out of those generated suites instead of producing skipped placeholders.
  (`src/e2e/codegen/python`,
  `src/e2e/codegen/typescript`)

- **alef-backend-php: promote opaque class parameters after the first optional argument**:
  generated PHP userland class declarations now mirror static extension stubs by
  marking parameters after an optional parameter as nullable with `= null`,
  keeping PHPStan/PHP syntax valid for methods like `TestClient::post`.
  (`src/backends/php/src/gen_bindings/mod.rs`,
  `src/backends/php/tests/gen_bindings_test.rs`)

## [0.16.20] - 2026-05-16

### Fixed

- **alef-backend-kotlin-android: emit resolved workspace version in generated `build.gradle.kts`**: AAR `version` coordinate was hardcoded to `"0.0.0"`, leaving every published Android artifact tagged as v0.0.0 in Maven local. Now reads `config.resolved_version()` (workspace `version =`) so the generated `build.gradle.kts` carries the real release version. (`src/backends/kotlin-android/gen_build_gradle.rs`)

- **release: include all v0.16.19 fixes**: v0.16.19 publish failed on `cargo fmt --check` due to a stray `eprintln!` in a test landed by a concurrent agent. v0.16.20 carries the same fix set plus the fmt scrub and the kotlin-android AAR version fix.

## [0.16.19] - 2026-05-16

### Fixed

- **alef-e2e/typescript: emit synthetic `kind` discriminator on napi serde-tagged-enum fixtures**: `alef-backend-napi` (commit `091514d`) hardcodes `"kind"` as the TypeScript discriminator for every internally-tagged (`#[serde(tag = "...")]`) data enum, but e2e fixture codegen still emitted the original serde-tag name (e.g. `role: "user"` instead of `kind: "user"`), producing `Missing field 'kind'` runtime errors across the entire node smoke/chat suite. Adds `rename_napi_serde_tags_to_kind` + `rename_napi_serde_tags_recursive` helpers that walk fixture JSON and rename any serde-tag key to `"kind"` only when the value matches a known variant of the corresponding tagged-data enum (so plain struct fields sharing the same name are left alone). Wired into both `build_args_and_setup` (node, no `options_type`) and `ts_builder_expression_inner` (node, with `options_type`). WASM fixtures untouched — `serde_wasm_bindgen::from_value` reads original serde attribute names. (`src/e2e/codegen/typescript/test_file.rs`)

- **alef-backend-go: emit custom `UnmarshalJSON` on parent structs containing untagged-enum fields**: structs whose Go-bound fields include any untagged-enum (`#[serde(untagged)]`) named type previously deserialized into the bare struct shape, which left those fields as `nil` because Go's default `json.Unmarshal` doesn't dispatch on shape. The backend now collects every binding field whose type is `TypeRef::Named(n)` or `TypeRef::Optional(TypeRef::Named(n))` where `n` is in `data_enum_names`, and emits a `func (s *Parent) UnmarshalJSON(data []byte) error` that decodes through an anonymous helper struct using `json.RawMessage` for those fields, then dispatches each raw payload through the matching `Unmarshal<EnumName>(raw)` helper. (`src/backends/go/src/gen_bindings/types.rs:gen_struct_type`)

- **alef-backend-swift: call `.intoRust()` on first-class DTO args at call sites**: the Swift binding's `emit_client_class` previously passed Swift first-class DTO arguments directly into Rust-bridge function calls without invoking `.intoRust()`, leaving the Rust side with a Swift wrapper value instead of the materialised Rust DTO and failing the swift-bridge type check. Each first-class DTO argument now flows through `.intoRust()` at the call site, matching the convention already in place for nested constructor extern calls. (`src/backends/swift/src/gen_bindings.rs:emit_client_class`)

- **alef-e2e/kotlin_android: apply Android Gradle Plugin in e2e build.gradle.kts**: the e2e fixture-driven Android test app's generated `build.gradle.kts` referenced `android { ... }` DSL blocks but never applied the `com.android.application` plugin, so `./gradlew assemble` failed with "android extension not found". The codegen now applies the AGP at the top of the test-app gradle script. (`src/e2e/codegen/kotlin_android.rs`)

- **alef-e2e/java: iterate streams via `.iterator()` instead of calling `Iterator` methods on `Stream`**: streaming-assertion codegen called `.hasNext()` / `.next()` directly on `java.util.stream.Stream`, which Java disallows (those methods live on `Iterator`). The codegen now resolves through `.iterator()` first, mirroring the idiomatic Java streaming-consumer pattern. (`src/e2e/codegen/streaming_assertions.rs`)

- **alef-backend-java: unwrap `Optional<T>` in facade returns + propagate to stream-method named results**: facade methods whose Rust return is `Option<T>` now call the raw-class method through `.orElse(null)` so the public facade signature (`@Nullable T`) matches the raw class signature (`Optional<T>`). The bytes path is preserved (raw signature already returns `byte[]`, not `Optional<byte[]>`, so unwrapping would be a compile error). The streaming-method-named-result template now distinguishes optional vs non-optional returns at both the early-null check and the success path, wrapping through `Optional.of`/`Optional.empty()` only when the declared return is `Optional<T>`. (`src/backends/java/gen_bindings/facade.rs`, `src/backends/java/gen_bindings/types.rs`, `src/backends/java/templates/facade_class.jinja`, `src/backends/java/templates/stream_method_named_result.jinja`)

- **alef-backend-kotlin-android: extend generic-container support to `HashMap<_,_>` and `Option<Vec|Map>` in public signatures**: facade param/return type resolution previously only special-cased `Vec<DTO>` → `List<DTO>` for non-opaque named types, leaving `HashMap<_,_>`, primitive vecs, and any optional-wrapped container falling through to the JNI primitive mapping (which served bare `String` for all generics). Now routes any `Vec<_>` / `Map<_,_>` (including `Option<Vec|Map>`) through the recursive `render_kotlin_type` helper, and switches the Jackson deserialization decision from `is_vec_of_dtos` to the broader `is_generic_container` predicate so `TypeReference<...>` is used for every container return. Public Kotlin signatures now match the Jackson-serialized payload regardless of the inner element type. (`src/backends/kotlin-android/gen_bindings.rs`, `src/backends/kotlin-android/tests/gen_bindings_test.rs`, `src/backends/kotlin/src/gen_bindings/mod.rs`)

- **alef-backend-swift: primitive-only direct constructor + shared JSON-fallback predicate**: primitive-only serde DTOs (e.g. `Point { row: u32, column: u32 }`, `ByteRange { start: usize, end: usize }`) now get a positional constructor extern emitted to the swift-bridge `extern "Rust"` block instead of falling through to a JSON-roundtrip path whose Rust-side shim was never emitted. Adds shared `requires_json_fallback` predicate consulted by both the Swift binding side (`gen_bindings.rs`) and the Rust crate side (`gen_rust_crate/mod.rs`) so any type Swift will JSON-encode is guaranteed to have a matching Rust shim. Closes the `RustBridge.pointFromJson` / `byteRangeFromJson` compile gap in tslp's swift binding. (`src/backends/swift/src/gen_rust_crate/extern_block.rs`, `src/backends/swift/src/gen_rust_crate/json_fallback.rs` (new), `src/backends/swift/src/gen_rust_crate/mod.rs`, `src/backends/swift/src/gen_rust_crate/wrappers.rs`, snapshot tests in `src/backends/swift/tests/`)

- **alef-cli: per-language setup workdir for swift/kotlin-android/dart/zig**: setup install commands now run from each binding's manifest directory (`packages/swift`, `packages/kotlin-android`, `packages/dart`, `packages/zig`) instead of the repo root. Closes "Could not find Package.swift" timeouts during `alef setup` invocations for languages whose manifest does not live at the workspace root. Other languages (python, node, ruby, php, go, java, csharp, elixir, wasm, ffi) retain current behavior of running from repo root. (`src/core/config/setup_defaults.rs`, `crates/alef-cli/src/pipeline/commands.rs`, `src/core/tests/setup_config_test.rs` (new))

- **alef-backend-php: promote static stub parameters after the first optional argument**:
  PHP stubs for static API methods now mark parameters after the first optional
  argument as nullable with `= null`, matching the generated PHP facade and
  keeping PHPStan/PHP syntax valid for Rust functions whose required parameters
  follow optional ones. (`src/backends/php/src/gen_bindings/mod.rs`,
  `src/backends/php/tests/gen_bindings_test.rs`)

- **alef-scaffold: place Kotlin sample sources under their declared package path**:
  JVM scaffold now writes `Sample.kt` beneath
  `src/main/kotlin/<package>/sample/`, so Detekt package/path validation passes
  for generated Kotlin package names. (`crates/alef-scaffold/src/languages/kotlin.rs`,
  `crates/alef-scaffold/src/tests.rs`)

## [0.16.18] - 2026-05-16

### Fixed

- **alef-backend-java: import `Optional` for opaque optional method returns**: the opaque-handle import scanner now uses the same public return-type mapper as the generated method signature, so `Option<T>` methods emit both `Optional<T>` and the required `java.util.Optional` import. (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/tests/gen_bindings_test.rs`)

- **alef-backend-kotlin: unwrap Java `Optional` in opaque method wrappers**: Kotlin JVM wrapper classes now append `.orElse(null)` when delegating opaque methods that return Rust `Option<T>`, matching the public Kotlin `T?` signature. (`src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/tests/gen_bindings_test.rs`)

- **alef-cli: avoid .NET query-mode shorthand in C# builds**: the default C# build command now uses `dotnet build --verbosity quiet` instead of `-q`, avoiding SDKs where `-q` exits non-zero when `CoreCompile` is not up to date. (`crates/alef-cli/src/pipeline/commands.rs`)

## [0.16.17] - 2026-05-16

### Added

- **alef-codegen: YARD doc emission (`emit_yard_doc`, `render_yard_sections`) + Magnus backend wiring**: shared module gains a Ruby YARD-flavoured renderer that translates rustdoc `# Arguments` / `# Returns` / `# Errors` sections into `# @param` / `# @return` / `# @raise` tags. The Magnus backend now emits YARD blocks above generated tagged-enum wrapper classes in `packages/<crate>/lib/*/native.rb`, restoring IDE tooltips and RDoc rendering that were previously a no-op when `gen_bindings/classes.rs` ignored `TypeDef.doc`. 4 new unit tests on the shared module, 1 integration test on the Magnus backend. (`src/codegen/doc_emission.rs`, `src/backends/magnus/gen_bindings/mod.rs`, `src/backends/magnus/tests/gen_bindings_test.rs`)

- **alef-backend-extendr: roxygen2 docs on R class envs, enum class envs, and richer free-function blocks**: class envs (`Type <- new.env(parent = emptyenv())`) now carry a roxygen block derived from `TypeDef.doc` with one `#' @field` line per public field; flat data-enum class envs gain a `#' @field` per variant from `EnumVariant.doc`; JSON-passthrough enum class envs gain a title/description block (no `@field` since callers go through the opaque `__inner` JSON blob). Free-function roxygen blocks now parse rustdoc sections via `alef-codegen::parse_rustdoc_sections` + `parse_arguments_bullets` so `# Arguments` bullet bodies drive `#' @param` lines, `# Returns` drives `#' @return`, `# Errors` becomes `#' @section Errors:`, and raw headings stop leaking into the description body. 3 new unit tests. (`src/backends/extendr/src/gen_bindings.rs`, `src/backends/extendr/templates/r_type_class_env.jinja`)

- **alef-backend-kotlin-android: KDoc emission for types, methods, fields, enum variants**: the kotlin-android backend previously had zero `.doc` references in `gen_bindings.rs`, producing undocumented `.kt` files even when rustdoc was rich. Wires `emit_kdoc(...)` calls at every type/method/field/variant emission site, matching the alef-backend-kotlin pattern. Adds 2 unit tests and updates the snapshot fixture. (`src/backends/kotlin-android/gen_bindings.rs`, `src/backends/kotlin/src/gen_bindings/mod.rs`, `src/backends/kotlin/src/lib.rs`, `src/backends/kotlin-android/tests/gen_bindings_test.rs`, snapshot)

- **alef-docs: Kotlin/KotlinAndroid, Swift, Dart, Zig signature renderers**: replaces the `// Phase 1: {lang} backend signature generation` stubs in `signatures.rs` with full `render_kotlin_fn_sig`, `render_swift_fn_sig`, `render_dart_fn_sig`, `render_zig_fn_sig`, plus method-signature arms for all four. Kotlin/KotlinAndroid share a renderer; statics emit `@JvmStatic`, fallible functions emit `@Throws(...)`. Swift emits `public static func` / `public func` with `throws` keyword and trailing `-> Ret`. Dart emits optional params as `[Type? name]` (bracketed positional). Zig emits `pub fn name(...) ErrorTy!Ret` for fallible functions, `pub fn name(...) Ret` otherwise; instance methods include a `self: *const Receiver` first param, statics omit it. Closes the per-language API reference docs (`docs/reference/api-{kotlin-android,swift,dart,zig}.md`) for the v3.5.0 binding additions. Adds 17 new unit tests. (`crates/alef-docs/src/signatures.rs`)

- **alef-core: `FieldDef.original_type: Option<String>`** preserves the pre-sanitization Rust type string (e.g. `"Vec<(String, String)>"`) so backends can reconstruct proper serialization logic for tuple types that the sanitizer downgrades. Populated by `sanitize_unknown_types()` when a field's type is rewritten. (`src/core/ir.rs`, `src/extract/extractor/{helpers,types}.rs`)

- **alef-backend-ffi: emit `*_len()` companion for `c_char` returns**: every `pub unsafe extern "C" fn` returning `*mut c_char` now has a sibling `_len() -> usize` that returns the same string's byte length without allocating. Enables safe slice construction in Zig (drops `[:0]const u8` in favour of `[]const u8`) and Java FFM Panama bindings. (`src/backends/ffi/src/gen_bindings/functions.rs`, `src/backends/ffi/src/gen_bindings/mod.rs`)

### Changed

- **[BREAKING] alef-backend-swift: eliminate JSON roundtrip in DTO `intoRust()`**: Swift DTOs no longer serialize through `JSONEncoder` + Rust-side `from_json` shim. The Swift binding now calls a Rust-side bulk constructor extern via swift-bridge (`#[swift_bridge(init)] fn new(...)`), mirroring the existing direct-field-access pattern in `init(_ rb:) throws`. Internal bridge architecture change; public Swift API unchanged. Affects all DTOs that previously emitted `JSONEncoder().encode(self)` in `intoRust()` — primitive-only, nested-struct (`try self.field.intoRust()`), `Vec<Primitive | String | Named>` (materialised as `RustVec<T>`), and `Option<Primitive | String>` fields all take the direct path. DTOs whose constructor extern is not emitted (no `Default` impl, JSON-bridged Map/Json fields, or fields excluded via `[crates.<crate>.swift] exclude_fields`) keep the JSON fallback. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/src/gen_rust_crate/extern_block.rs`, tests in `src/backends/swift/tests/snapshot_test.rs`)

### Fixed

- **alef-backend-java: preserve `Optional<T>` in opaque method return signatures**: Java opaque-handle methods now use the public return-type mapper instead of the FFI boundary mapper, so methods returning `Option<String>` emit `Optional<String>` signatures to match their generated `Optional.empty()` / `Optional.of(...)` bodies. (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/tests/gen_bindings_test.rs`)

- **alef-backend-go: keep byte-slice returns as `[]byte` in fallible methods**: Go methods that return `TypeRef::Bytes` but can fail while marshaling a non-opaque receiver now return `([]byte, error)` instead of `(*[]byte, error)`, matching the generated `unmarshalBytes` helper and idiomatic Go slice semantics. (`src/backends/go/src/type_map.rs`, `src/backends/go/tests/bug_fixes_test.rs`)

- **alef-backend-go: emit shape-discriminated `Unmarshal` for untagged enums**: `Unmarshal{EnumName}` now splits its emission on `enum_def.serde_untagged`. Tagged enums keep the existing wire-struct-and-switch pattern. Untagged enums (where `enum_def.serde_tag` is `None`) now emit a first-byte shape sniffer (`'"'` → string, `'['` → array, `'{'` → object, else numeric) that gates each variant, then attempts `json.Unmarshal` into each concrete struct in declaration order, returning the first success. Previously the wire struct was empty `struct {}` but the switch unconditionally accessed `wire.Type`, causing `wire.Type undefined (type struct{} has no field or method Type)` Go compile errors on every untagged enum (e.g. `RerankDocument`, `OcrDocument`). (`src/backends/go/src/gen_bindings/types.rs`, `src/backends/go/tests/bug_fixes_test.rs`)

- **alef-backend-kotlin: use payload-derived field name in untagged sealed-class serializer's `when` branch**: the custom serializer for newtype/tuple variants emitted literal `value.field0`, but the data-class declarations for those variants already render with payload-derived field names via `kotlin_field_name_with_type` (e.g. `data class Single(val value: String)`). Kotlin compilation failed with `Unresolved reference 'field0'` on `EmbeddingInput`, `ModerationInput`, `RerankDocument`, `StopSequence`, `ToolChoice`, `UserContent`. The `when`-branch now reuses the same helper, computing the correct field name from the field's type. (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **alef-backend-kotlin-android: null-coalesce nullable primitive bridge args to JNI defaults**: the facade emits public params like `baseUrl: String? = null`, `timeoutSecs: Long? = null` and passes them through to a JNI `external fun` declaration whose signature is non-nullable. The `bridge_args` closure previously only null-coalesced DTO/Vec<DTO>/opaque param types; primitive scalars (`String?`, `Long?`, `Int?`, `Double?`, `Bool?`) fell through to bare `name`, producing `Argument type mismatch: actual type is 'String?', but 'String' was expected` Kotlin compile errors. Now emits `name ?: <zero>` where zero matches the JNI signature (`""` for `String`, `0L` for `i64/u64`, `0` for `i32/u32`, `0.0` for floats, `false` for `bool`). (`src/backends/kotlin-android/gen_bindings.rs`, `src/backends/kotlin-android/tests/gen_bindings_test.rs`)

- **alef-cli: re-stamp `test_apps/` hashes after the `all` pipeline finalizes binding hashes**: registry-mode e2e files in `test_apps/` (generated by `alef e2e generate --registry`) carry `alef:hash:<sha>` markers computed from the sources hash of the upstream Rust crate. Any commit that modifies a watched Rust source file rotates the sources hash — but `alef all` never touched `test_apps/`, so their embedded hashes immediately went stale. `alef verify --exit-code` reported 188-257 stale `test_apps/` files on every CI run. The `all` pipeline now collects every alef-headered file under `e2e_config.registry.output` (default `test_apps/`) and re-runs `finalize_hashes` over them with the current sources hash, re-stamping their `alef:hash:` lines without regenerating their content. (`crates/alef-cli/src/main.rs`, `crates/alef-cli/src/pipeline/generate.rs`, `crates/alef-cli/src/pipeline/mod.rs`)

- **alef-cli: make post-generation formatter warnings actionable**: formatter failures now include both stdout and stderr instead of dropping stdout-only diagnostics. Python package formatting is scoped to the generated stub package when configured, avoiding unrelated hand-written package tests. (`crates/alef-cli/src/pipeline/format.rs`)

- **alef-scaffold: suppress php-cs-fixer runtime-version advisories in generated format commands**: PHP package scaffolds now set `setUnsupportedPhpVersionAllowed(true)` in `.php-cs-fixer.dist.php`, run php-cs-fixer format scripts with `--quiet`, and stop using the deprecated `PHP_CS_FIXER_IGNORE_ENV` composer-script prefix. (`crates/alef-scaffold/src/languages/php.rs`, `crates/alef-scaffold/src/tests.rs`)

- **alef-backend-wasm: `Vec<(K, V)>` enum fields use `JsValue` + `serde_wasm_bindgen`**: tagged-enum variants with fields whose original type was `Vec<(String, String)>` (sanitized to `Vec<String>`) are now stored as `Option<JsValue>` in the wasm struct rather than `Option<Vec<String>>`. Binding-to-core reads via `serde_wasm_bindgen::from_value::<Vec<(String, String)>>`, core-to-binding writes via `serde_wasm_bindgen::to_value`. Wire format `[["k","v"],...]` is preserved. Fixes compile errors in `html-to-markdown-wasm` for `NodeContent::MetadataBlock { entries }`. (`src/backends/wasm/gen_bindings/enums.rs`, `crates/alef-cli/src/pipeline/extract.rs`)

- **alef-backend-swift: drop `-> ()` on unit-returning wrapper fns**: `wrappers.rs:gen_method_wrappers` emitted `pub fn parser_reset(client: &mut Parser) -> () { ... }` for any `&mut self` method returning `()` without an error type, which clippy flags as `unused_unit` (denied in tslp under `-D warnings`). Now omits the return clause entirely when `return_ty == "()"`. Affects every Unit-returning method exposed via swift-bridge wrappers. (`src/backends/swift/src/gen_rust_crate/wrappers.rs`)

- **alef-backend-ffi: `*_len()` companion uses correct fail-path return**: the `_len()` companion's parameter-validation early-return (null pointer, UTF-8 decode error, etc.) was emitting the primary function's `return std::ptr::null_mut();` instead of `return 0;`, causing E0308 type mismatches (`*mut _` vs `usize`) when the underlying function returns `*mut c_char`. `gen_free_function_len_companion` now passes a synthetic `TypeRef::Primitive(PrimitiveType::Usize)` to `gen_param_conversion` so `null_return_value` resolves to `"0"` for the fail-path. Affects every `*_len()` companion emitted for `String`/`Path`/`Json`/`Named`/`Vec`/`Map`/`Bytes` returning functions. (`src/backends/ffi/src/gen_bindings/functions.rs:gen_free_function_len_companion`)

- **alef-backend-swift: emit doc comments on opaque-class methods and first-class struct fields**: methods previously inlined an ad-hoc `for line in method.doc.lines()` loop with a stale comment ("Emit `/// doc` when available."); now route through the shared `emit_doc_comment` helper, matching how types/enums/errors/variants already emit. First-class struct fields surface their rustdoc above the generated `public let` declaration — previously emitted no doc at all. Plain prose passes through verbatim; `# Errors`/`# Returns` flow through as DocC-compatible text. 2 regression tests. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

- **alef-backend-swift: emit bidirectional `From` impls for `OptionsField` newtype wrappers**: the `OptionsField` bind_via path generates a factory (`make_{trait}_handle`) and options-helper (`{opts}_from_json_with_{field}`) that call `TypeAlias::from(inner)`, `<inner>::from(handle)`, and `OptionsType::from(core_opts)`. These three `From` impls were missing — only enums emitted one-direction `From` via match arms. `emit_options_field_from_impls` now emits all three bidirectional newtype-struct impls (`.0` field access) with a deduplication guard so multiple bridges sharing the same alias or options type don't produce duplicate `impl` blocks. Fixes E0308 and E0277 in every consumer using `bind_via = "options_field"` (e.g. html-to-markdown's `HtmlVisitor` bridge). (`src/backends/swift/src/gen_rust_crate/plugin_inbound.rs`, `src/backends/swift/src/gen_rust_crate/mod.rs`)

- **alef-backend-swift: only emit first-class Swift DTOs for directly bridgeable field shapes**: complex serde DTOs with maps, vectors, nested wrappers, paths, bytes, JSON, or duration fields now remain RustBridge typealiases instead of generating uncompilable Codable/Hashable wrappers that called missing bridge accessors or JSON factory shims. Simple primitive/string DTOs keep first-class Codable wrappers and call snake_case RustBridge accessors. (`src/backends/swift/src/gen_bindings.rs`)

- **alef-backend-swift: require direct RustBridge constructors for first-class DTOs**: Swift first-class DTO emission now uses the same constructor/direct-conversion eligibility as `intoRust()`, so serde DTOs without a generated `#[swift_bridge(init)]` constructor remain RustBridge typealiases instead of calling missing `{type}FromJson` shims. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

## [0.16.16] - 2026-05-16

### Fixed

- **alef-backend-java: drop redundant `import java.util.stream.Stream;` from streaming opaque-handle classes**: the streaming method body template (`streaming_iterator_method.jinja`) uses fully-qualified names (`java.util.stream.Stream<T>` in the method signature, `java.util.stream.StreamSupport.stream(...)` in the bridge body), so the unconditional `imports.push("java.util.stream.Stream")` emitted an import that never resolved to a short-form identifier in the file body. Checkstyle's `UnusedImports` rule rejected `DefaultClient.java:12` in liter-llm after regeneration against v0.16.15. Drop the import push; rely on FQNs in the template throughout. Adds a regression test asserting the import is absent in generated opaque-handle facades with streaming adapters. (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/tests/gen_bindings_test.rs`)

## [0.16.15] - 2026-05-16

### Fixed

- **alef-cli: resolve `PostBuildStep::RunCommand` working directory from `base_dir.join(crate_dir)`**: the dispatcher previously ran every backend-emitted `RunCommand` from the workspace `base_dir`, but commands like `flutter_rust_bridge_codegen generate` expect the binding crate root (where `flutter_rust_bridge.yaml` lives, relative to `crate_dir`). The Dart FRB step now runs in the correct directory and finds its config file. Also adds `src/backends/dart/examples/rewrite_lib_dart.rs` so the post-frb sealed-variant rewriter can be applied as a one-shot when callers bypass alef's build pipeline (e.g. running `task dart:codegen` directly). (`crates/alef-cli/src/pipeline/commands.rs`, `src/backends/dart/examples/rewrite_lib_dart.rs`)

- **alef-backend-java: nest Jackson POJO builders inside their owning record**: every DTO record now embeds its `Builder` as `public static final class Builder` rather than emitting `FooBuilder.java` as a sibling top-level class. Cuts the generated file count for the Java package by ~32% (54 fewer `.java` files in liter-llm) and matches idiomatic Java conventions (`Foo.Builder` pattern, mirroring `ImmutableList.Builder`, `LocalDate.Builder`, etc.). The `@JsonDeserialize(builder = ...)` annotation now references `Foo.Builder.class`. (`src/backends/java/gen_bindings/types.rs`, snapshot tests refreshed)

- **alef-backend-dart: pass generated config to flutter_rust_bridge post-build command**: the Dart backend's post-build `RunCommand` now invokes `flutter_rust_bridge_codegen generate --config-file packages/dart/rust/flutter_rust_bridge.yaml`, matching the root-relative post-build working directory and ensuring FRB reads the emitted `rust_input`/`dart_output` settings instead of failing with "Please provide `rust_input`" or "Cannot find config_file". (`src/backends/dart/src/gen_bindings/mod.rs`)

- **alef-e2e/node: use workspace version for registry test apps when no package version is configured**: Node registry-mode `package.json` now falls back to the crate's resolved Cargo version before the `0.1.0` placeholder, matching the Python/Ruby/PHP/Dart generators and preventing generated `test_apps/node` projects from depending on unpublished `@scope/package@0.1.0` versions. (`src/e2e/codegen/typescript/mod.rs`)

- **alef-e2e/elixir: emit version dependencies for registry test apps**: Elixir registry-mode `mix.exs` now uses the package version from `[e2e.registry.packages.elixir]` or the resolved crate version instead of carrying the local `../../packages/elixir` path into standalone `test_apps`. (`src/e2e/codegen/elixir.rs`)

- **alef-backend-magnus: omit binding-excluded fields from explicit `Default` impls**: Ruby DTOs with field-level defaults now filter `binding_excluded` fields consistently with struct and constructor generation, preventing hidden core fields from being emitted into binding-side `Self { ... }` defaults. (`src/backends/magnus/gen_bindings/classes.rs`)

- **alef-codegen/rustler: keep binding-excluded fields out of config constructors and cfg-gated core literals**: shared default-constructor helpers now filter `binding_excluded` fields before emitting keyword/option constructors, and binding-to-core conversion skips cfg-gated hidden fields so internal implementation details like caches and DI containers do not leak into generated Elixir NIF code or fail when the core feature is disabled. (`src/codegen/config_gen.rs`, `src/codegen/conversions/binding_to_core.rs`)

- **alef-backend-swift: omit binding-excluded fields from Rust bridge constructors**: Swift Rust bridge extern declarations and wrapper constructor bodies now share the binding-visible field set, preventing hidden core fields from appearing as unbound constructor parameters or serde assignments. (`src/backends/swift/src/gen_rust_crate/extern_block.rs`, `src/backends/swift/src/gen_rust_crate/wrappers.rs`, `src/backends/swift/src/gen_rust_crate/default_construction.rs`)

- **alef-backend-swift: only emit first-class Swift DTOs for directly bridgeable field shapes**: complex serde DTOs with maps, vectors, nested wrappers, paths, bytes, JSON, or duration fields now remain RustBridge typealiases instead of generating uncompilable Codable/Hashable wrappers that called missing bridge accessors or JSON factory shims. Simple primitive/string DTOs keep first-class Codable wrappers and now call snake_case RustBridge accessors. (`src/backends/swift/src/gen_bindings.rs`)

## [0.16.14] - 2026-05-16

### Fixed

- **alef-backend-rustler: emit positional arity overloads for single-trailing-optional Elixir facade fns**: facade emission for functions with exactly one trailing optional parameter (e.g. `create_engine(config: Option<String>)`) previously collapsed to a single `def create_engine(opts \\ [])` body that called `Keyword.get(opts, :config)` — but the alef-e2e Elixir codegen always emits positional calls (`Kreuzcrawl.create_engine(engine_config)`), passing a string into a function expecting a `keyword()` list. The mismatch surfaced as `FunctionClauseError` on every e2e test that constructed an engine. The keyword-opts collapse threshold in `gen_bindings/mod.rs` is now `trailing_keyword_count >= 2` (was `> 0`): single trailing optional falls through to the existing arity-variants path (`def create_engine()` + `def create_engine(config)` with `nil` filled for the shortest arity), matching the e2e call shape. Functions with 2+ trailing optionals still collapse to the `opts \\ []` keyword form. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-cli: implement `PostBuildStep::RunCommand` dispatcher**: the runtime arm at `crates/alef-cli/src/pipeline/commands.rs` was previously a TODO debug-log that silently skipped every emitted `RunCommand` step (notably the Dart flutter_rust_bridge codegen invocation). Now spawns `std::process::Command::new(cmd).args(args).current_dir(base_dir)`, captures stdout/stderr, and propagates non-zero exit status as an `anyhow::Error` so the pipeline aborts. The dart backend's `RunCommand` step (`flutter_rust_bridge_codegen generate`) now executes automatically during `alef generate`. (`crates/alef-cli/src/pipeline/commands.rs`)

- **alef-backend-dart: wire `rewrite_frb_sealed_variants` into the post-frb pipeline**: introduces a new `PostBuildStep::PostProcessFile { path, processor }` variant in `alef-core::backend` with `PostProcessor::FrbDartSealedVariants`. The dart backend's `build_config_for` appends this step after the `RunCommand` for `flutter_rust_bridge_codegen`, targeting `../lib/src/<crate>_bridge_generated/lib.dart`. The alef-cli dispatcher reads the file, applies `alef_backend_dart::rewrite_frb_sealed_variants`, and writes back only on change. Sealed-class factory params previously emitted as `{required PdfMetadata field0}` now resolve to `{required PdfMetadata metadata}` matching the kotlin/swift binding surface. (`src/core/backend.rs`, `crates/alef-cli/src/pipeline/commands.rs`, `src/backends/dart/src/gen_bindings/mod.rs`, `src/backends/dart/src/frb_rewrite.rs`)

- **alef-backend-rustler: resolve concrete Elixir types for known union payloads**: tagged enum `@type` emission for variants like `FormatMetadata::Pdf(PdfMetadata)` previously emitted `@type pdf :: %{type: :pdf, metadata: map()}` — a loose `map()` typespec that defeats Dialyzer's variant-payload inference. The variant emitter now threads the API surface's known type names through `elixir_field_name_with_type`; payloads matching a known struct emit `Kreuzberg.PdfMetadata.t()`, falling back to `map()` only for opaque/unknown payloads. Field names (`metadata`, `value`, `value0`/`value1`) are unchanged. (`src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/src/gen_bindings/mod.rs`)

- **alef-backend-java: emit `@Nullable` on `Option<T>` parameters in FFI facade methods**: facade method signatures previously rendered `Option<T>` params as bare `String`/`Path`/etc. without nullability annotations, causing static analyzers (IntelliJ, Checker Framework) to flag `null` arguments as false-positives. Now emits `@Nullable T` with an `import org.jspecify.annotations.Nullable;` line whenever any optional parameter is present in the class body. Return types remain `Optional<T>` to preserve existing call sites; switching returns to `@Nullable T` is a follow-up that requires a deeper body refactor. (`src/backends/java/gen_bindings/facade.rs`, `src/backends/java/gen_bindings/ffi_class.rs`, `src/backends/java/templates/facade_file.jinja`)

- **alef-backend-swift: escape Swift reserved keywords with backticks in emitted Swift source**: enum cases, struct field names, function-parameter labels, and associated-value labels that collide with Swift reserved keywords are now wrapped in backticks (`` case `default` ``, `` public let `class`: String ``) — Swift's idiomatic escape — instead of suffixing a trailing underscore (`case default_`). Rust-side identifiers on the swift-bridge boundary continue to use the trailing-underscore form so the two sides agree at the FFI boundary. Adds `swift_case_safe_name` / `swift_case_ident` to `alef_core::keywords` for emitted-Swift escaping; `swift_safe_name` / `swift_ident` remain for Rust-side use. (`src/core/keywords.rs`, `src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`)

- **alef-backend-zig: collapse error unions to single `KreuzbergError` set**: every emitted error set now includes `OutOfMemory` as a variant, so allocator failures can be propagated without a `||error{OutOfMemory}` concat. Return types for fallible functions are now `ErrorSet!T` instead of the verbose `(ErrorSet||error{OutOfMemory})!T`. (`src/backends/zig/src/gen_bindings/errors.rs`, `src/backends/zig/src/gen_bindings/functions.rs`)

### Added

- **alef-backend-extendr: emit roxygen2 `@param`/`@return` on every exported R function**: every free function in the generated `extendr-wrappers.R` previously carried only `#' @export`, so `?<fn>` in an R REPL returned an empty `.Rd` file. The wrapper emitter now produces a complete roxygen2 doc block above each `name <- function(...)` definition: the title line is derived from the first sentence of the Rust doc comment (capitalized, trailing period stripped) — or falls back to the bare function name when the doc is empty; remaining doc text is emitted as the description with paragraph breaks preserved as `#'` blank lines; one `#' @param <name> <type-description>` line is emitted per parameter, with the description derived from the IR `TypeRef` (e.g. `Raw vector of bytes.`, `Optional character string. Defaults to NULL.`, `<TypeName> object (list with class attribute).`); a final `#' @return` and `#' @export` close the block. (`src/backends/extendr/src/gen_bindings.rs`, `src/backends/extendr/templates/r_free_function_wrapper.jinja`)

- **alef-codegen: add acronym-preserving PascalCase conversion helpers**: shared naming utilities now include `pascal_to_snake` and `pascal_to_screaming_snake`, preserving acronym runs such as `Rdfa`, `HTMLParser`, and `XMLHttpRequest` instead of splitting every uppercase letter. (`src/codegen/naming.rs`)

- **alef-backend-magnus: generate aggregated RBS type stub sidecar (`types.rbs`)**: the Magnus backend now generates a comprehensive aggregated RBS sidecar file (`packages/ruby/sig/types.rbs`) containing all public API type signatures when `[crates.ruby.stubs] output = "..."` is configured. The file includes module declarations, class definitions with attr_reader/attr_accessor annotations, method signatures with typed parameters and return types, and enum type aliases. RBS types are mapped from Rust IR: `String` → `String`, integers → `Integer`, floats → `Float`, booleans → `bool`, `Vec<T>` → `Array[T]`, `Option<T>` → `T?`, `Map<K, V>` → `Hash[K, V]`. The generated file is consumable by Steep type checker; clients configure `target :lib do; signature 'sig'; check 'lib'; end` in their `Steepfile`. (`src/backends/magnus/gen_stubs.rs`, existing tests in `gen_stubs_test.rs`)

- **alef-backend-rustler: emit `@type t` typespec above every DTO `defstruct`**: all generated Elixir DTO modules now emit a `@type t :: %__MODULE__{ field: Type, ... }` type specification immediately above the `defstruct` declaration. This enables Dialyzer static type checking on DTO instances and improves IDE autocomplete and type inference in Elixir code that manipulates DTOs. Field types are mapped from Rust types: `String` → `String.t()`, `u64`/`usize` → `non_neg_integer()`, `bool` → `boolean()`, `Vec<T>` → `[T]`, `Option<T>` → `T | nil`, `Map` → `map()`, and named struct types → `map()`. (`src/backends/rustler/src/gen_bindings/helpers.rs`, `src/backends/rustler/tests/elixir_struct_typespec_test.rs`)

### Fixed

- **alef-backend-jni: catch native panics from `runtime().block_on`; propagate via JNI exception**: panics inside `tokio::Runtime::block_on` previously unwound across the C ABI (undefined behaviour), surfacing as `Exception at native frame -2` on the Kotlin side with no payload. Added a `run_or_throw` helper to the emitted JNI module preamble that wraps every `block_on` call in `std::panic::catch_unwind(AssertUnwindSafe(...))`, downcasts the panic payload to a message string, and throws it as a JNI exception prefixed with `"native panic: "`. All shim variants are covered: top-level functions, instance methods, streaming start, and streaming next. (`src/backends/jni/src/gen_shims.rs`)

- **alef-backend-wasm: keep camelCase constructor parameters aligned with snake_case struct fields**: constructors now rewrite both shorthand and explicit field initializers so multi-word fields use JS-facing camelCase parameters on the right-hand side while preserving the Rust field name on the left-hand side (`total_tokens: totalTokens.unwrap_or_default()`, `tool_call_id: toolCallId`). Multi-line constructor parameter lists are handled too, and generated constructors allow the `non_snake_case` parameter names required by wasm-bindgen's JS surface. (`src/backends/wasm/gen_bindings/types.rs`, `src/backends/wasm/tests/gen_bindings_test.rs`)

- **alef-backend-swift: honor extractor-produced optional fields in first-class structs**: Swift DTO generation now treats both `TypeRef::Optional(T)` and extractor-style `(ty: T, optional: true)` fields as optional, emitting `T?`, default-nil initializer parameters, and optional swift-bridge accessors such as `rb.name()?.toString()`. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/snapshot_test.rs`)

- **alef-codegen: omit binding-excluded fields from generated DTO surfaces**: shared struct generation, constructor helpers, Java, NAPI, PyO3, C#, Go, Magnus, PHP, and Rustler emitters now filter out `binding_excluded` fields while conversion code still defaults the hidden core fields when converting back to Rust core types. The CLI keeps excluded fields in IR instead of rejecting non-`Default` core types, so hidden fields like internal cursors can be initialized by conversion code without leaking into host APIs. (`crates/alef-cli/src/pipeline/extract.rs`, `src/codegen/shared.rs`, `src/codegen/generators/structs.rs`, `src/codegen/conversions/`, `src/backends/*/src/gen_bindings/`)

- **alef-backends: close remaining binding-excluded field leaks and generated wrapper noise**: PHP constructors, Java builders, Swift DTOs, Dart mirrors, and WASM DTOs now honor `binding_excluded` fields consistently, while Java opaque methods support string and primitive FFI returns instead of emitting unsupported stubs. Ruby tagged-enum wrappers now require `sorbet-runtime`, call `super()` in variant initializers, and read string hash keys correctly; Zig scaffolding avoids removed `pub usingnamespace` syntax. (`src/backends/php/src/gen_bindings/types.rs`, `src/backends/java/gen_bindings/types.rs`, `src/backends/java/template_env.rs`, `src/backends/magnus/`, `src/backends/swift/`, `src/backends/dart/`, `src/backends/wasm/`, `crates/alef-scaffold/src/languages/zig.rs`)

- **alef-backend-dart: payload-derived sealed variant parameter names**: flutter_rust_bridge generates tagged-union sealed classes with positional parameter names (`field0`, `field1`, ...) for tuple variants, e.g. `const factory FormatMetadata.pdf({required PdfMetadata field0}) = FormatMetadata_Pdf;`. Added a post-frb rewriter (`alef_backend_dart::rewrite_frb_sealed_variants`) that converts these into payload-derived names matching the kotlin/swift binding surface (`metadata`, `value`, `value0`/`value1`, ...). Single-field variants with a struct payload whose name shares the variant's prefix (e.g. `PdfMetadata` for `Pdf`) use the stripped lowerCamel remainder; primitive payloads use `value`; multi-field tuples use `value0`/`value1`; struct variants with named fields are left untouched. Mirrors `kotlin_field_name_with_type` in `alef-backend-kotlin/src/gen_bindings/shared.rs`. (`src/backends/dart/src/frb_rewrite.rs`, `src/backends/dart/Cargo.toml`)

- **alef-backend-csharp: emit newline between consecutive using directives**: the Jinja2 template for opaque handle headers was using `{%- if` with left-side whitespace stripping, which removed the newline separating `using System.Runtime.InteropServices;` from the next conditional block's first `using` statement. This caused two consecutive imports to appear concatenated on a single line (e.g., `using System.Runtime.InteropServices;using System.Text.Json;`). Changed all conditional `{%- if` tags to `{% if` to preserve newlines between using statements. (`src/backends/csharp/templates/opaque_handle_header.jinja`, `src/backends/csharp/tests/gen_bindings_test.rs`)

- **alef-backend-php: apply camelCase conversion to DTO property names in stubs**: PHP 8.2+ idiomatic style requires camelCase property names per PSR-12. The binding layer was correctly using `#[php(prop, name = "deviceId")]` for runtime, but the generated PHP stubs were emitting snake_case property names directly (e.g., `$device_id` instead of `$deviceId`). Applied `to_php_name()` conversion to property names when emitting readonly constructor parameters in DTO stubs. (`src/backends/php/src/gen_bindings/mod.rs`, `src/backends/php/tests/gen_bindings_test.rs`)

- **alef-backend-pyo3: drop redundant `return` on void-returning function calls in `api.py`**: functions with `-> None` return type annotation were emitting `return _rust.init(...)`, where the `return` statement is redundant and causes type checkers to flag the function as returning `None` instead of `None` implicitly. Modified `gen_api_py` to detect `TypeRef::Unit` returns and emit a bare call without the `return` keyword. (`src/backends/pyo3/gen_bindings/functions.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-backend-pyo3: emit PEP 8 blank lines between consecutive top-level function definitions in `api.py`**: consecutive function definitions were jammed together with zero blank lines between them, violating PEP 8's requirement for two blank lines between top-level definitions. Modified the function emission loop to emit two additional newlines after each function body, ensuring proper spacing in generated `api.py`. (`src/backends/pyo3/gen_bindings/functions.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

- **alef-backend-napi: drop unnecessary 'undefined' from nullable `.d.ts` return types**: Optional return types in TypeScript declaration files were emitted as `T | undefined | null` when FFI returns can only be `T | null` (either the value or null, never undefined). Changed `dts_type` for `TypeRef::Optional` to emit `T | null` instead. (`src/backends/napi/src/gen_bindings/errors.rs`, `src/backends/napi/tests/gen_bindings_test.rs`)

- **alef-backend-jni: preserve serialization error messages through JNI exception boundary**: `serde_json::to_string(&v).unwrap_or_default()` on all JSON serialization paths silently returned an empty string when serialization failed, causing the JNI caller to see a null/empty payload with no exception raised. Replaced with explicit `match` that calls `throw_jni_error(&mut env, &format!("serialize: {e}"))` and returns the appropriate null sentinel (`std::ptr::null_mut()`) on failure. Applies to the general return-marshal helper, the streaming `Next` shim (two call sites), and the `emit_function_shim` OK-arm. Regression test `no_unwrap_or_default_on_json_serialization_path` verifies no `serde_json::to_string` + `unwrap_or_default` combination appears in generated shim code. (`src/backends/jni/src/gen_shims.rs`, `src/backends/jni/tests/gen_shims_test.rs`)

- **alef-backend-swift: user-facing HtmlVisitorProtocol now receives typed NodeContext, not raw JSON String**. The adapter (SwiftHtmlVisitorBox / Delegate) handles JSON → NodeContext conversion internally so user implementations work with the structured value. Adapter delegate protocol scoped to `internal` (or underscore-prefixed if cross-module references prevent narrowing) to hide it from public API surface. (`src/backends/swift/src/gen_bindings/swift_inbound_protocol.rs`, `gen_rust_crate/plugin_inbound.rs`)

### Changed

- **[BREAKING] alef-backend-kotlin-android: collapse optional parameter defaults from sentinel zero-values to nullable Kotlin types**: `createClient` and other facade functions with `p.optional == true` parameters previously emitted `timeoutSecs: Long = 0L`, `maxRetries: Int = 0`, `modelHint: String = ""` — sentinel values that collapse with valid zero-value inputs. They now emit `timeoutSecs: Long? = null`, `maxRetries: Int? = null`, `modelHint: String? = null`, which is the idiomatic Kotlin pattern. Callers passing explicit zeros remain source-compatible; callers that relied on default-zero behavior must migrate to `null`. (`src/backends/kotlin-android/gen_bindings.rs`, `src/backends/kotlin-android/tests/gen_bindings_test.rs`)

### Added

- **alef-e2e/kotlin-android: emit `src/androidTest/` instrumented test source set + Gradle Managed Device + emulator CI**: the `KotlinAndroidE2eCodegen` now emits both a host-JVM test file (`src/test/kotlin/.../e2e/ChatTest.kt`) and an instrumented Android test file (`src/androidTest/kotlin/.../e2e/ChatTest.kt`) per fixture group. The instrumented file carries `@RunWith(AndroidJUnit4::class)`, a `@BeforeClass` `System.loadLibrary` block, and one `@Test` per fixture. The emitted `build.gradle.kts` now includes `import com.android.build.api.dsl.ManagedVirtualDevice` and an `android { testOptions { managedDevices { ... } } }` block declaring a `pixel6api34` Gradle Managed Device (API 34, aosp, Pixel 6 profile). A new `.github/workflows/ci-android-emulator.yaml` workflow runs `reactivecircus/android-emulator-runner@v2` with API 34 / arm64-v8a on changes to the kotlin-android/jni/e2e files. Two new regression tests lock in both invariants. (`src/e2e/codegen/kotlin_android.rs`, `src/e2e/tests/kotlin_android_codegen.rs`, `.github/workflows/ci-android-emulator.yaml`)

### Fixed

- **alef-backend-kotlin-android: align `sealed_variant_tuple_params_use_payload_derived_names` regression assertions with actual multi-line emission**: the regression test asserting payload-derived sealed-variant parameter names for the kotlin-android emitter (`Pdf(PdfMetadata)` → `val metadata`, `Custom(String)` → `val value`, `Multi(String, Int)` → `val value0, val value1`, `Struct { reason: String }` → `val reason`) was compiled against a stale IR shape and the body checks were written for a single-line `data class Variant(val name: Type)` form. The android backend's `data_class_header.jinja` actually emits each field on its own indented line, so the test never executed; once compilation was restored the assertions failed against the real (correct) emission. Updated `make_sealed_variants_api` to use the current `FieldDef` / `EnumVariant` / `EnumDef` shape and rewrote the `data class` assertions to match the multi-line layout. Confirms that the payload-derived-name helper from `alef-backend-kotlin::gen_bindings::shared` (`kotlin_field_name_with_type`) reaches the android emitter through the `emit_enum_pub` → `object_wrapper::emit_enum` re-export. (`src/backends/kotlin-android/tests/gen_bindings_test.rs`)

- **alef-backend-magnus: emit real e2e tests for non-HTTP fixtures**: Ruby has direct FFI access to Rust core via Magnus, so it can execute fixtures with `mock_responses` (non-HTTP) without relying on HTTP clients. The generator now only skips fixtures with zero assertions, allowing fixtures with assertions (even if some field references are invalid) to emit real tests with skipped assertion comments, matching Python and Node behavior. E2E skip count reduced from 47 (all non-HTTP fixtures) to 1 (genuinely untestable fixtures). (`src/e2e/codegen/ruby.rs`)

- **alef-backend-magnus: emit `_async` function variants in RBS stubs**: All async functions exported by Magnus bindings now appear in the RBS type stubs as both sync and `_async` variants (e.g., `def self.scrape` and `def self.scrape_async`), allowing type checkers like Steep to validate both calling conventions. (`src/backends/magnus/gen_stubs.rs`)

- **alef-backend-pyo3: remove leaked debug `eprintln!` in `rewrite_capsule_methods`**: a diagnostic `eprintln!("[rewrite_capsule] method=... start_idx=... attr_start=... prefix=...")` introduced alongside the capsule-passthrough impl-header fix was left in place and emitted a line to stderr for every pyo3 capsule method on every `alef generate`. Removed. (`src/backends/pyo3/gen_bindings/mod.rs`)

- **alef-backend-pyo3: preserve `#[pymethods] impl` block header when first method returns a capsule type**: `rewrite_capsule_methods` uses `find_method_attrs_start` to walk backward from a `pub fn` to find the start of its attribute block. The walk accepted any line starting with `#[` — including `#[pymethods]impl Foo {` which minijinja emits as a single concatenated line when whitespace stripping is active. As a result, `attr_start` landed at byte 0, and the string slice `result[..0]` discarded the entire impl block header, leaving the rewritten method floating outside any `impl` block. Fixed by replacing the `starts_with("#[")` heuristic with `is_method_attr_line` — a bracket-depth scanner that accepts a line only when it consists entirely of `#[…]` patterns with no trailing non-whitespace tokens. (`src/backends/pyo3/gen_bindings/mod.rs`, `src/backends/pyo3/tests/gen_bindings_test.rs`)

### Changed

- **[BREAKING] alef-backend-swift: first-class struct/enum emission with Codable+Sendable+Hashable**: non-opaque DTO types that carry `#[derive(Serialize, Deserialize)]` and have at least one field now emit as `public struct Foo: Codable, Sendable, Hashable` with stored `public let` properties, a memberwise `public init` (default-nil for Optional fields), and `CodingKeys` for snake_case↔camelCase wire-key mapping. An `internal extension Foo` provides `init(_ rb: RustBridge.Foo) throws` (reading fields via swift-bridge accessor calls) and `func intoRust() throws -> RustBridge.Foo` (JSON round-trip via `JSONEncoder`). Streaming wrappers whose item type is a first-class struct decode chunks via `JSONDecoder().decode(T.self, from: chunkData)` instead of the `RustBridge.{T}FromJson(json)` shim. Public `{T}FromJson` helpers for first-class types also use `JSONDecoder` directly. Empty structs and opaque handle types are unaffected and continue to emit typealiases or class wrappers. (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/tests/gen_bindings_test.rs`, `src/backends/swift/tests/snapshots/`)

- **[BREAKING] alef-backend-swift: RustBridge becomes internal-only impl detail**: public `typealias Foo = RustBridge.Foo` declarations are no longer emitted for any DTO type that qualifies as a first-class struct. The `RustBridge` module is still imported and used internally (opaque handles, streaming start/next bridge functions, `intoRust()` JSON shim), but no public API surface exposes `RustBridge`-prefixed types. Callers that accessed fields via `.role()` method call syntax must migrate to `.role` property access on the first-class struct. (`src/backends/swift/src/gen_bindings.rs`)

- **[BREAKING] alef-backend-wasm: drop Wasm-prefix from exported TypeScript names**: mirroring the NAPI W3 idiomaticity work, non-opaque and opaque structs now carry `#[wasm_bindgen(js_name = "Foo")]` to export the unprefixed name in the generated `.d.ts`, while unit enums and tagged data enums carry `#[wasm_bindgen(js_name = "Foo")]` to expose the unprefixed variant. The Rust binding struct internally retains its `WasmFoo` identifier (prefixed). The `generate_public_api` `index.ts` re-export uses unprefixed names (`Config`, `Mode`, not `WasmConfig`, `WasmMode`). Callers that imported `WasmConfig`, `WasmParser`, etc. from the generated WASM binding package must migrate to `Config`, `Parser`, etc. (`src/backends/wasm/gen_bindings/mod.rs`, `src/backends/wasm/gen_bindings/types.rs`, `src/backends/wasm/gen_bindings/enums.rs`, `src/backends/wasm/templates/gen_struct.jinja`, `src/backends/wasm/templates/gen_opaque_struct.jinja`)

- **[BREAKING] alef-backend-napi: drop Js-prefix from exported TypeScript names; add `readonly` to DTO fields**: three linked W3 idiomaticity changes: (1) non-opaque structs carry `#[napi(object, js_name = "Foo")]` instead of `#[napi(object)]`, (2) opaque structs carry `#[napi(js_name = "Foo")]` via post-processing of `gen_opaque_struct_prefixed` output, (3) string enums carry `#[napi(string_enum, js_name = "Foo")]` — in all cases NAPI-RS will export the type as `Foo` in the generated `.d.ts` while the Rust struct retains its `JsFoo` identifier internally. The `generate_public_api` `index.ts` re-export uses the unprefixed name to match. DTO interface fields in the handwritten `.d.ts` are now emitted with the `readonly` qualifier. Callers that imported `JsConfig`, `JsMode`, etc. from the generated binding package must migrate to `Config`, `Mode`, etc. (`src/backends/napi/src/gen_bindings/mod.rs`, `src/backends/napi/src/gen_bindings/types.rs`, `src/backends/napi/src/gen_bindings/enums.rs`, `src/backends/napi/src/gen_bindings/errors.rs`)

- **[BREAKING] alef-backend-rustler: Elixir client function arity overloads collapsed to single keyword-opts function**: Functions with trailing optional parameters now emit a single `def f(required, opts \\ [])` definition with `Keyword.get(opts, :param_name)` forwarding instead of one `def` per arity combination. Callers using `create_client/2`, `create_client/3`, etc. must migrate to `create_client(api_key, base_url: "...", timeout_secs: 30)`. (`src/backends/rustler/src/gen_bindings/mod.rs`)

- **[BREAKING] alef-backend-rustler: `defstruct` optional-field defaults changed from `""` to `nil`**: String, Char, Path, Json, and Bytes fields without an explicit `typed_default` now default to `nil` in generated `defstruct` declarations, reflecting Elixir's idiomatic absent-value convention. Callers matching on `""` must migrate to `nil`. (`src/backends/rustler/src/gen_bindings/helpers.rs`)

- **[BREAKING] alef-backend-magnus: Ruby tagged enums emitted as class hierarchies instead of Hash method_missing**: Internally-tagged data enums now emit a sealed base class with per-variant predicate stubs (returning `false`) and one concrete subclass per variant. Each subclass carries Sorbet-typed `attr_reader` fields, a keyword-arg `initialize`, an overridden predicate (returning `true`), and a `self.from_hash` factory. The `Hash#method_missing` / `Hash#respond_to_missing?` monkey-patch that previously lived in `native.rb` is removed entirely. Callers that relied on `result.excel` or `format[:format_type]` style access must migrate to `result.is_a?(MessageSystem) && result.content`. (`src/backends/magnus/gen_bindings/mod.rs`)

- **alef-backend-magnus: Sorbet `sig { }` blocks on all emitted tagged enum methods**: every accessor (`attr_reader`), predicate (`def variant?`), `initialize`, and `from_hash` in the generated tagged-enum class hierarchy now carries a Sorbet-compatible `sig { }` annotation with the correct type (`String`, `Integer`, `Float`, `T::Boolean`, `T.nilable(...)`, etc.). `extend T::Sig` and `extend T::Helpers` are emitted at the top of each class. (`src/backends/magnus/gen_bindings/mod.rs`)

- **[BREAKING] alef-backend-php: emit DTOs as PHP 8.3+ `final class` with readonly constructor property promotion**: Previously, DTO stubs in `packages/php/src/*.php` were emitted as mutable `class` declarations with separate public property fields and redundant `getFoo()` / `getName()` getter methods. Now the backend emits `final class` with all fields promoted into the constructor signature as `public readonly` parameters, which is the idiomatic PHP 8.3+ pattern. Callers that invoked `$obj->getContent()` or `$obj->getName()` must migrate to direct property access `$obj->content` / `$obj->name`. (`src/backends/php/src/gen_bindings/mod.rs`, `src/backends/php/templates/php_record_class_stub_declaration.jinja`, `src/backends/php/tests/gen_bindings_test.rs`)

- **alef-backend-csharp: modern C# features polish — file-scoped namespaces, `IAsyncEnumerable<T>` for streams, collection expressions**: All generated C# files already use file-scoped namespace syntax (`namespace Foo;`, C# 10+). Streaming methods on opaque handle classes return `IAsyncEnumerable<T>` (C# 8+) so consumers can `await foreach`. `byte[]` fields without an explicit default now emit `= []` (C# 12 collection expression) instead of `= Array.Empty<byte>()`. Three regression tests lock in these invariants. (`src/backends/csharp/gen_bindings/types.rs`, `src/backends/csharp/tests/gen_bindings_test.rs`)

- **BREAKING — alef-backend-pyo3: emit `UPPER_SNAKE_CASE` pyclass enum variants; drop runtime SCREAMING aliasing**: Previously, pyclass enum variants were exposed under their Rust PascalCase names (e.g. `BatchStatus.Validating`) and `options.py` monkey-patched `SCREAMING_SNAKE_CASE` aliases on top (`BatchStatus.VALIDATING = BatchStatus.Validating`). Now every variant carries `#[pyo3(name = "UPPER_SNAKE_CASE")]` in the generated Rust binding, so the canonical Python name is directly `UPPER_SNAKE_CASE` (PEP 8-compliant). The `.pyi` stub emits only the `UPPER_SNAKE_CASE` attribute declarations. The `options.py` monkey-patching block is removed. Callers that accessed variants via PascalCase (e.g. `BatchStatus.Validating`) must migrate to `BatchStatus.VALIDATING`. (`src/codegen/generators/enums.rs`, `src/codegen/templates/generators/enums/enum_definition.jinja`, `src/backends/pyo3/gen_stubs.rs`, `src/backends/pyo3/gen_bindings/types.rs`)

- **alef-backend-csharp: emit DTOs as C# 9+ `sealed record` with property bodies and `required` modifier for mandatory fields**: Previously, DTOs were emitted as `public sealed class Foo { public T Field { get; set; } = default; }` (C# 6 style). Now the backend emits `public sealed record Foo { [property: JsonPropertyName("...")] T Field { get; init; } = default; }` — idiomatic C# 9+ record syntax with `[property:]` attribute targets for JSON metadata and immutable `get; init;` accessors. Non-nullable reference-type fields without explicit defaults now emit the `required` modifier (C# 11+), enforcing constructor-time initialization; nullable fields, collections, value types, and fields with defaults do not emit `required`. Opaque handle wrapper classes expose their `Handle` property as `internal` (not `public`) to prevent unsafe direct `DangerousGetHandle()` access by external consumers. This improves consistency with modern C# conventions and enables compiler-driven immutability checking. Internally-tagged enums (with data variants) now use abstract `record` bases with `[JsonDerivedType]` discriminator annotations instead of flat class hierarchies, enabling type-safe sealed-union semantics via inheritance. Enum XML doc comments are now properly formatted with explicit `</summary>` closing tags. (`src/backends/csharp/gen_bindings/types.rs`, `src/backends/csharp/templates/record_class_header.jinja`, `src/backends/csharp/templates/property_with_default.jinja`, `src/backends/csharp/templates/property_required_init.jinja`, `src/backends/csharp/templates/enum_header.jinja`, `src/backends/csharp/templates/opaque_handle_header.jinja`)

- **[BREAKING] alef-backend-java: streaming methods return `Stream<T>` instead of `Iterator<T>`**: `chatStream()` and all adapter-pattern streaming methods on opaque handle classes now return `java.util.stream.Stream<T>` (composable, lazy, parallelizable) instead of `Iterator<T>`. The internal anonymous iterator is still used as the data source and is bridged via `StreamSupport.stream(Spliterators.spliteratorUnknownSize(..., ORDERED | NONNULL), false)`. The import changes from `java.util.Iterator` to `java.util.stream.Stream`; callers that consumed the return value as `Iterator<T>` must migrate to the Stream API (e.g. `.forEach()`, `.collect()`, `.toList()`). (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/templates/streaming_iterator_method.jinja`, `src/backends/java/tests/gen_bindings_test.rs`)

- **[BREAKING] alef-backend-java: tagged enums emitted as `sealed interface ... permits ...` + per-variant `record` (Java 21+ idiom)**: Internally-tagged enums with data variants (e.g. `Message`, `ContentPart`, `AuthConfig`) are now emitted as `public sealed interface Foo permits Variant1, Variant2 { }` with each variant as `public record Variant1(T field) implements Foo { }`. Previously they used plain inheritance hierarchies. Jackson polymorphic deserialization is wired via `@JsonTypeInfo`/`@JsonSubTypes`. Callers using class-cast-based dispatch must migrate to `instanceof` pattern matching (`if (msg instanceof SystemMessage sm) { ... }`). (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/tests/gen_bindings_test.rs`)

- **alef-backend-java: emit fields-only DTOs as Java 17+ records**: All non-opaque structs with only field declarations (no custom methods) are now emitted as `public record Foo(String a, int b) {}` instead of sealed classes. Records automatically generate the constructor, getters (named `a()` not `getA()` per Java records convention), and `equals`/`hashCode`/`toString`. This aligns with modern Java idioms (Java 17 GA, 2021) and mirrors the pattern already used in sealed-union variant declarations. Opaque FFI handles continue to emit as regular classes (implementing `AutoCloseable`) for resource management. Sealed interfaces for sum-types continue to emit variant records implementing the interface. Three regression tests verify the heuristic: fields-only structs emit as records, opaque handles remain classes, and sum-type variants are records within sealed interfaces. (`src/backends/java/gen_bindings/types.rs`, `src/backends/java/tests/gen_bindings_test.rs`)

- **alef-backend-go: emit tagged-data enums as sealed interfaces with per-variant concrete structs**: Previously, externally-tagged data enums (e.g., `AuthConfig` with variants `Basic`, `Bearer`, `Header`) were emitted as a single flat struct with all variant fields as nullable pointers, allowing invalid combinations like `Bearer { username: Some("x"), token: None }`. Now, the backend emits a sealed interface with an unexported marker method `is{EnumName}()` and one concrete struct per variant containing only that variant's fields (no nullables). Each concrete struct implements `MarshalJSON`/`UnmarshalJSON` to emit the discriminator tag. Callers construct `AuthConfigBasic{ username, password }` directly—type-safe and idiomatic Go. An `Unmarshal{EnumName}([]byte)` helper dispatches to the correct variant based on the wire `type` field. This refactor improves type safety and ergonomics for consumers while maintaining full JSON round-trip compatibility. (`src/backends/go/src/gen_bindings/types.rs`)

- **alef-backend-kotlin / alef-backend-kotlin-android: payload-derived parameter names for sealed-class tuple variants**: Previously, tuple variants on sealed-class enums emitted with placeholder field names — `data class Pdf(val field0: PdfMetadata) : FormatMetadata()` — which obscured intent and made pattern-match code awkward. The shared `gen_bindings::shared` helper now derives an idiomatic Kotlin field name from the payload type: named types whose prefix matches the variant name strip the prefix and lower-camel the remainder (`Pdf(PdfMetadata)` → `val metadata`); primitive payloads (`String`, `Int`, `Long`, `Boolean`) use `val value`; multi-payload tuples fall back to indexed `val value0, val value1, …`; struct variants with named fields preserve the original field names. JSON serializers and round-trip codecs were updated to read/write through the new accessor names. (`src/backends/kotlin/src/gen_bindings/shared.rs`, `src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)
- **alef-backend-pyo3: drop SCREAMING_SNAKE_CASE aliases from generated `.pyi` stubs**: `gen_stubs.rs` previously emitted `EnumName.SHOUTY_NAME: EnumName = ...` class attributes alongside the canonical PascalCase variant declarations in the type stubs, mirroring the runtime monkey-patches in `options.py`. The aliases bloated every enum stub by N additional lines and confused IDEs that suggested both forms as valid attribute names. Removed the `to_python_screaming` helper and the alias-emit loop in `gen_enum_stub`; the runtime `_coerce_enum` helper in the binding's `api.py` continues to accept string-form input transparently, so callers using `EnumName("shouty-name")` are unaffected. (`src/backends/pyo3/gen_stubs.rs`)

### Added

- **alef-backend-napi: W3 regression tests — js_name, readonly, no Js-prefix on TS surface**: four new integration tests in `gen_bindings_test.rs` lock in the W3 TypeScript idiomaticity invariants: `test_napi_js_name_on_non_opaque_struct` asserts `#[napi(object, js_name = "Foo")]` and unprefixed `index.ts` re-export; `test_napi_js_name_on_opaque_struct` asserts `#[napi(js_name = "Foo")]` on opaque handles; `test_napi_js_name_on_string_enum` asserts `#[napi(string_enum, js_name = "Foo")]` and unprefixed enum in `index.ts`; `test_dts_dto_fields_are_readonly` asserts `readonly` on both required and optional DTO fields in the `.d.ts` output and verifies the unprefixed interface name. (`src/backends/napi/tests/gen_bindings_test.rs`)

- **alef-backend-napi: regression tests for tagged-enum discriminant and variant naming (D4A/B/C)**: three tests in `enums.rs::tests` lock in the expected napi-rs surface for tagged enums — `js_name = "kind"` is used for the discriminant field (not the Rust `serde_tag` name like `"annotation_type"`), tuple-variant `serde_rename` is preserved in camelCase (e.g. `fontSize`, not `font_size`), and struct-variant named field names pass through unchanged. (`src/backends/napi/src/gen_bindings/enums.rs`)

- **alef-backend-go: regression tests for trait-bridge typed parameters (D1)**: tests verify that trait-bridge method parameters of type `String`, named structs (e.g. `OcrConfig`), and other concrete types emit the correct Go type in the generated interface signature, instead of degrading to `interface{}` or `map[string]interface{}`. (`src/backends/go/tests/gen_bindings_test.rs`)

- **alef-backend-wasm: support `Vec<UnitEnum>` parameter deserialization via `from_api_str`**:
  wasm-bindgen handlers accepting `Vec<UnitEnum>` parameters need to convert each JS string into
  the corresponding Rust enum variant. The unit-enum emitter now also generates a
  `pub fn from_api_str(s: &str) -> Option<Self>` constructor that parses serde wire strings
  (honoring `#[serde(rename_all)]` and per-variant `#[serde(rename)]`). Two new jinja templates
  (`serde_vec_unit_enum_optional.jinja`, `serde_vec_unit_enum_required.jinja`) wire the conversion
  into the binding→core parameter mapping path, returning a `JsValue` error with the offending
  string when an unknown variant is supplied.
  (`src/backends/wasm/gen_bindings/enums.rs`,
  `src/backends/wasm/template_env.rs`,
  `src/backends/wasm/templates/serde_vec_unit_enum_optional.jinja`,
  `src/backends/wasm/templates/serde_vec_unit_enum_required.jinja`)

- **alef-backend-swift: emit `options_field` trait bridge — `HtmlVisitorProtocol` + `SwiftHtmlVisitorBox` + `makeHtmlVisitorHandle` + `conversionOptionsFromJsonWithVisitor`**: the swift backend previously only supported `bind_via = "function_param"` inbound trait bridges. This extends `gen_rust_crate/plugin_inbound.rs` with two new emitters (`emit_options_field_factory`, `emit_options_field_options_helper`) and wires both the `extern "Swift"` declaration and the Rust-side wrapper struct for all bridge modes. `gen_bindings.rs` gains `emit_inbound_protocols` which generates the Swift-side protocol, default extension, and box class from the IR. (`src/backends/swift/src/gen_rust_crate/plugin_inbound.rs`, `src/backends/swift/src/gen_rust_crate/mod.rs`, `src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e/swift: emit fixture-driven visitor test classes via `build_swift_visitor`**: visitor-bearing fixtures previously emitted `XCTSkipIf(true, ...)` stubs in the swift e2e output. A new `codegen/swift_visitors.rs` module generates `final class LocalVisitor_<id>: HtmlVisitorProtocol` classes with per-method overrides, and `codegen/swift.rs` threads the resulting handle through `conversionOptionsFromJsonWithVisitor(json, handle)` replacing the former skip stub. (`src/e2e/codegen/swift_visitors.rs`, `src/e2e/codegen/swift.rs`, `src/e2e/codegen/mod.rs`)

### Fixed

- **alef-backend-napi: keep tagged tuple variant Rust fields snake_case while exposing serde names to TypeScript**:
  Tagged tuple variant payloads now assert the generated Rust field remains snake_case (for
  warning-free Rust) while `#[napi(js_name = "...")]` preserves the serde rename at the
  TypeScript boundary.
  (`src/backends/napi/src/gen_bindings/enums.rs`)

- **alef-backend-wasm: preserve `Option<T>` fields in tagged data enum variants**:
  The flat wasm-bindgen struct representation for serde-tagged data enums stores every variant
  field as optional because a single JS object only carries one variant's fields. When a source
  variant field was already `Option<T>`, the conversion code incorrectly unwrapped it in
  binding→core direction and wrapped it in `Some(...)` in core→binding direction, producing type
  mismatches such as `Option<String>` vs `String`. Optional variant fields now pass through the
  flattened binding field directly, with named inner values still mapped through `Into`.
  (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-e2e/typescript: clean up mock-server timers and process handles in Vitest global setup**:
  The generated TypeScript e2e `globalSetup.ts` now clears the startup timeout after the mock
  server emits its sentinel URL and removes the stdout listener, then waits for the spawned
  mock-server process to close during teardown with a bounded SIGKILL fallback. This prevents
  Vitest from printing hanging-process warnings after otherwise successful generated e2e runs.
  (`src/e2e/templates/typescript/globalSetup.ts.jinja`,
  `src/e2e/codegen/typescript/config.rs`)

- **alef-backend-jni: emit type aliases for streaming handle struct to avoid `clippy::type_complexity`**:
  The `<Type><Adapter>StreamHandle` struct was inlining the full
  `Mutex<Option<BoxStream<'static, Result<T, Box<dyn Error + Send + Sync + 'static>>>>>` type
  in its `stream` field, triggering `clippy::type_complexity` (6 levels of nesting) under
  `-D warnings`. The emitter now generates two type aliases — `<Handle>Item` and `<Handle>Stream`
  — before each struct so the field reduces to `Mutex<Option<<Handle>Stream>>`.
  (`src/backends/jni/src/gen_shims.rs`)

- **alef-backend-wasm: extend tagged-data enum JsValue storage to `Option<T>` and bare `T` fields**:
  The previous fix in commit `c62b16e6` only handled `Vec<TaggedDataEnum>` fields. Fields of type
  `Option<TaggedDataEnum>` and bare (required) `TaggedDataEnum` were still emitting
  `Option<WasmFoo>` / `WasmFoo` storage, causing wasm-bindgen to reject plain JS object literals
  with "expected instance of WasmFoo" at the setter. The emitter now maps all three shapes
  (`T`, `Option<T>`, `Vec<T>`) to `JsValue` / `Option<JsValue>` when T is a tagged-data enum,
  and the From impls use `serde_wasm_bindgen` in all cases. Fixes CI E2E failures for
  `ChatCompletionRequest.responseFormat` and `OcrRequest.document`.
  (`src/backends/wasm/gen_bindings/types.rs`,
  `src/codegen/conversions/binding_to_core.rs`,
  `src/codegen/conversions/core_to_binding.rs`)

- **alef-backend-swift: emit `import RustBridgeC` in bridge files and umbrella header for SwiftPM**:
  Swift bindings generated by swift-bridge require a separate C target (`RustBridgeC`) to hold
  C type declarations; the `RustBridge` Swift target must `import RustBridgeC` at the top of
  every generated `.swift` file, and `packages/swift/Sources/RustBridgeC/RustBridgeC.h` must be
  a real umbrella header (not a placeholder). The scaffold now reads the cargo-build output
  headers (`SwiftBridgeCore.h` + `{crate}.h`) and concatenates them into the umbrella header so
  `swift test` compiles without "module 'RustBridgeC' not found" or "no such module" errors.
  Additionally, Swift 6 strict concurrency requires `@unchecked Sendable` conformances on the
  three opaque swift-bridge types captured across task boundaries (client inner, stream handle,
  and request param). Streaming item types (e.g. `ChatCompletionChunk`) are now decoded via a
  Rust-side `{type}_from_json(String) -> Result<T, String>` bridge function instead of a
  Swift-side Codable struct, avoiding the "ambiguous for type lookup" conflict between the
  opaque class and a generated struct with the same name.
  (`src/backends/swift/src/gen_bindings.rs`, `src/backends/swift/src/gen_rust_crate/mod.rs`,
  `crates/alef-scaffold/src/languages/swift.rs`)

- **alef-e2e: fix Swift streaming accessor for `tool_calls` deep paths and flat collection**:
  The `tool_calls[0].function.name` field path in Swift e2e tests was emitting property access
  syntax (`.function.name`) but swift-bridge exposes all fields as methods returning Optional
  (`.function()?.name()`). Added a Swift-specific deep-path renderer that uses direct `.method()`
  on the non-optional ref after an index and `?.method()` optional chaining thereafter, with
  `?.toString()` on the final string leaf. Also fixed the flat `tool_calls` accessor to use a
  `guard let` closure with an explicit return type annotation (`{ c -> [StreamToolCallRef] in
  guard let … }`) so Swift 6's type checker doesn't select the Optional overload of `flatMap`.
  (`src/e2e/codegen/streaming_assertions.rs`, `src/e2e/codegen/swift.rs`)

- **alef-backend-wasm: emit tuple-variant pattern for newtype enum variants in From impls**:
  tagged-data enums with tuple/newtype variants (e.g. `enum Message { System(SystemMessage),
  User(UserMessage), ... }`) were emitting invalid struct-pattern destructuring
  (`Message::System { _0 }`) in the `From<core::Enum> for WasmEnum` match arm and invalid
  struct-construction syntax (`Self::System { _0: val }`) in the `From<WasmEnum> for
  core::Enum` arm. Both now use the correct tuple syntax (`Message::System(field0)` and
  `Self::System(val)` respectively). When all tuple variants share the same positional field
  name (e.g. `_0`) but carry different `Named` inner types across variants, the binding
  struct stores the field as `JsValue` and per-variant conversion uses
  `serde_wasm_bindgen::from_value`/`to_value` to avoid impossible `WasmA → B` coercions.
  Named-field struct variants in tagged enums now also correctly apply `.into()` when
  constructing or destructuring core Named types at the WASM boundary.
  (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-backend-wasm: use `set_field_N` / `field_N` names for positional `_N` struct fields in tagged-enum wasm-bindgen getters/setters**: the getter/setter pair for positional fields (named `_0`, `_1`, … by the extractor) was emitting `fn set__0` and `fn _0`, which the Rust `non_snake_case` lint rejects as `method 'set__0' should have a snake case name` under `RUSTFLAGS="-D warnings"`. The Rust identifiers are now `field_0` (getter) and `set_field_0` (setter); the JS-visible name is unchanged via the `#[wasm_bindgen(getter/setter, js_name = "...")]` attribute. Adds a regression test asserting the correct identifier names are emitted. (`src/backends/wasm/gen_bindings/enums.rs`)

- **alef-backend-kotlin-android: add `jackson-datatype-jdk8` to library `build.gradle.kts`**: the generated `DefaultClient.kt` registers `Jdk8Module` via `ObjectMapper().registerModule(com.fasterxml.jackson.datatype.jdk8.Jdk8Module())`, but the library scaffold's `build.gradle.kts` only declared `jackson-databind` and `jackson-module-kotlin`. Kotlin compilation failed with "Unresolved reference 'datatype'". Adds `implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:<version>")` to the emitted dependencies block, version-pinned via `maven::JACKSON` (same as the other Jackson deps). (`src/backends/kotlin-android/gen_build_gradle.rs`)

- **alef-backend-wasm: store `Vec<TaggedDataEnum>` fields as `JsValue` to accept plain JS objects**:
  wasm-bindgen type-checks every element of a `Vec<T>` setter against class instances of `T`;
  plain JS object literals (e.g. `{ role: "user", content: "..." }`) were rejected with
  "array contains a value of the wrong type", causing all non-streaming WASM chat tests to
  fail in CI. Struct fields, constructor parameters, getters, and setters for
  `Vec<TaggedDataEnum>` are now emitted as `JsValue`. The `From` impls in both directions
  use `serde_wasm_bindgen::from_value`/`to_value` to deserialize/serialize the plain-object
  array. A new `tagged_data_enum_names` field on `ConversionConfig` controls which named
  types trigger this path; `binding_to_core.rs` and `core_to_binding.rs` emit the
  `serde_wasm_bindgen` conversion when the field is set.
  (`src/backends/wasm/gen_bindings/types.rs`,
  `src/backends/wasm/gen_bindings/mod.rs`,
  `src/codegen/conversions/mod.rs`,
  `src/codegen/conversions/binding_to_core.rs`,
  `src/codegen/conversions/core_to_binding.rs`)

## [0.16.12] - 2026-05-15

### Fixed

- **kotlin-android: drop redundant variant constructor wrap in tagged sealed-class
  deserializer**: the emitter for serde-tagged sealed classes (`#[serde(tag = ...)]`)
  incorrectly wrapped the `readTreeAsValue` result in the variant constructor for
  named-field struct variants (e.g. `ContentPart.Text(ctx.readTreeAsValue(node,
  ContentPart.Text::class.java))` — the outer `ContentPart.Text(...)` call passes a
  `ContentPart.Text` to a constructor expecting `String`, failing with "Argument type
  mismatch"). Named-field variants now emit `ctx.readTreeAsValue<Variant>(node,
  Variant::class.java)` directly. Newtype/tuple variants (`_0` field) correctly retain
  the wrapper since they pass the inner type to the variant constructor. Explicit Kotlin
  type parameters (`<T>`) are added to all `readTreeAsValue` call sites to prevent
  Kotlin inferring `Any!` for the Java generic return type.
  (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **kotlin-android: emit `JavaType` for `List<T>` in untagged sealed-class deserializer**:
  the emitter for serde-untagged sealed classes (`#[serde(untagged)]`) emitted
  `ctx.readTreeAsValue(node, List::class.java)` for `Vec<T>` variants, producing a raw
  `List<*>` at runtime (Jackson cannot reconstruct the element type from a raw class
  literal). The fix uses `ctx.typeFactory.constructCollectionType(List::class.java,
  T::class.java)` to carry the element type through to Jackson. Struct-variant branches
  in the untagged deserializer return `ctx.readTreeAsValue<T>` directly (no constructor
  wrap). Adds regression tests for `List<String>` and `List<NamedType>` variants.
  (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`)

- **kotlin-android: add Jackson runtime deps to `build.gradle.kts`
  template**: alef v0.16.11 added `@JsonDeserialize` to sealed-class Kotlin
  DTOs for serde-tagged polymorphism, but the kotlin-android `build.gradle.kts`
  template still only declared `kotlin-stdlib` + `kotlinx-coroutines-android`.
  The compileDebugKotlin task failed with `Unresolved reference 'com'` /
  `Unresolved reference 'codec'` / `Unresolved reference 'asText'` etc.
  Adds `jackson-databind` (for the annotations + `StdDeserializer`) and
  `jackson-module-kotlin` (for Kotlin data-class deserialization) to the
  emitted `dependencies { ... }` block, versions pinned via
  `maven::JACKSON`. (`src/backends/kotlin-android/gen_build_gradle.rs`)

## [0.16.11] - 2026-05-15

### Fixed

- **wasm: serde-tagged data enums emit as a flat `#[wasm_bindgen]` struct with a `default()`
  factory**: tagged data enums (e.g. `#[serde(tag = "type")] enum AuthConfig { Basic {...}, Bearer {...} }`)
  were previously emitted as wasm-bindgen C-style enums (`#[wasm_bindgen] pub enum`), which
  silently dropped every variant's field data and exposed no JS-callable constructor — JS test
  codegen calling `WasmAuthConfig.default()` crashed with `TypeError: WasmAuthConfig.default is
  not a function`. The wasm backend now detects serde-tagged data enums and emits them as a
  flat struct with a `String` discriminator (named after `serde_tag`), the union of every
  variant's fields as `Option<T>` properties, a `#[wasm_bindgen(constructor)]` `new()`, a
  static `default()` factory, and per-field getter/setter pairs — mirroring the NAPI
  `gen_tagged_enum_as_object` path. Custom `From<Wasm{Enum}> for core::{Enum}` (and reverse)
  impls match on the discriminator field instead of variant identity. Rust reserved keywords
  used as tag/field names (e.g. `type`) are escaped via raw-identifier syntax. Struct getters
  for `Option<WasmTaggedEnum>` fields now clone the binding wrapper directly instead of
  calling the unit-enum-only `to_api_str()` helper.
  (`src/backends/wasm/gen_bindings/enums.rs`,
  `src/backends/wasm/gen_bindings/types.rs`,
  `src/backends/wasm/gen_bindings/mod.rs`)

- **kotlin-android e2e codegen: emit `Flow.toList()` and Kotlin property access
  in streaming assertions**: the `kotlin_android` streaming codegen path in
  `StreamingFieldResolver` was already correct (`result.toList()` via the
  `kotlin_android` branch), but the installed alef binary was stale (0.16.10).
  Added regression test
  `kotlin_android_streaming_collect_uses_flow_to_list_not_as_sequence` covering:
  (a) `result.toList()` used to collect the `Flow<T>` (not `asSequence().toList()`),
  (b) chunk field assertions use Kotlin property access (`it.choices?.firstOrNull()?.delta?.content`
  not Java getter calls), and (c) `import kotlinx.coroutines.flow.toList` is
  emitted. (`src/e2e/tests/kotlin_android_codegen.rs`)

## [0.16.10] - 2026-05-15

### Fixed

- **kotlin-android: bump Java/JVM target from 11 to 17**: the kotlin-android
  backend emitted `JavaVersion.VERSION_11` and `JvmTarget.JVM_11` in the AAR
  `build.gradle.kts`, which is incompatible with Java 16+ records, Java 17+
  sealed classes, and Java 14+ switch expressions that the canonical Java
  backend's DTOs use. AGP 8.x desugars Java 17 down to Dalvik for minSdk 21+
  so 17 is safe across every supported Android target. Bumped via
  `toolchain::ANDROID_JVM_TARGET` so every emitted Android library now
  defaults to Java 17 / JVM 17. (`src/core/template_versions.rs`)

- **cleanup: sweep orphan alef-generated files in sibling subtrees of
  touched directories**: `cleanup_orphaned_files` previously only descended
  into subdirectories that were either themselves touched or contained a
  touched path. Subtrees that a backend used to write to but no longer does
  (e.g. the kotlin-android backend dropped its `src/main/java/` Java DTO emit
  in v0.16.0, leaving stale alef-marked Java files behind in every
  consumer repo) were never visited, so their orphans survived every
  subsequent regen and broke downstream builds (e.g. kreuzcrawl's
  `gradle assembleDebug` failed compiling FFM-tagged Java files with
  `import java.lang.foreign.*` against the Android JDK 17 toolchain).
  The descend logic now also walks any subtree that is a descendant of a
  touched directory; the alef-header gate in `has_alef_header` remains the
  safety net that keeps user-customised files untouched. Touched-dir paths
  are now canonicalized up-front so the new descendant check is portable
  across macOS `/tmp` vs `/private/tmp` symlinks. Regression test
  `cleanup_removes_orphan_in_sibling_subtree_of_touched_dir`.
  (`crates/alef-cli/src/pipeline/cleanup.rs`)

## [0.16.8] - 2026-05-15

### Added

- **Source-level `#[cfg_attr(alef, alef(skip))]` on types, enums, errors, functions, methods, and traits**:
  Alef now honors `alef(skip)` (and `#[doc(hidden)]`) at every item level, not just on
  fields. Excluded items are stripped from the binding API surface before downstream
  codegen; trait-bridge codegen can still resolve their fully qualified Rust paths via
  `ApiSurface.excluded_type_paths`. This lets the source declare what stays internal to
  Rust without per-language `exclude_types`/`exclude_functions` lists in `alef.toml`.
  (`crates/alef-extract`, `crates/alef-cli`, `crates/alef-core`)

### Fixed

- **kotlin-android: handle-only opaque types now get an AutoCloseable wrapper class
  in the facade**: when a top-level free function takes or returns an opaque type
  with no instance methods (the kreuzcrawl `CrawlEngineHandle` shape, distinct
  from the liter-llm `DefaultClient` shape), the `<Module>.kt` facade previously
  emitted `String` for both the parameter and the return type — disagreeing with
  the JNI bridge, which (since the v0.16.7 JNI handle refactor) correctly emits
  `Long`. The facade now emits a sibling `<TypeName>.kt` wrapper class implementing
  `AutoCloseable` (`class <TypeName> internal constructor(internal val handle: Long)
  : AutoCloseable`) whose `close()` delegates to the bridge's
  `nativeFree<TypeName>(handle)` destructor. Top-level fns returning the opaque
  type now return this wrapper class, constructed from the bridge's `Long`; fns
  taking the type as a parameter accept the wrapper and unwrap `.handle` into
  the bridge call. The liter-llm `DefaultClient` (client-with-methods) shape is
  unchanged — `emit_jni_client_class` continues to emit `DefaultClient.kt` for
  that case, and top-level factories now correctly construct it from the
  `Long` bridge return instead of returning a stale `String`. Regression test
  `handle_only_opaque_returns_wrapper_class_and_accepts_wrapper_params` in
  `src/backends/kotlin-android/tests/gen_bindings_test.rs`.
  (`src/backends/kotlin-android/gen_bindings.rs`)

- **codegen/trait_bridge: synthesized `Plugin` methods include new `binding_excluded`
  fields**: the synthetic `MethodDef`s for the Plugin trait's `version`,
  `initialize`, and `shutdown` methods were missing the `binding_excluded` and
  `binding_exclusion_reason` fields added when source-level exclusion metadata
  landed, breaking workspace compilation.
  (`src/codegen/generators/trait_bridge.rs`)

- **kotlin-android: `createClient` wrapper returns `DefaultClient` not `String`**:
  `emit_module_kt` in `alef-backend-kotlin-android` previously passed every
  `TypeRef::Named` return through `jni_return_type_str`, which maps named types
  to `"String"`. When a top-level function returns an opaque handle type (e.g.
  `create_client() -> DefaultClient`), the wrapper facade now detects the opaque
  type name, emits `: DefaultClient` as the return type, and wraps the bridge call
  in the `DefaultClient(handle: Long)` constructor instead of propagating the raw
  `Long`. Regression test added in
  `src/backends/kotlin-android/tests/gen_bindings_test.rs`.
  (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-e2e/kotlin_android: construct `DefaultClient` via `client_factory` before
  calling methods**: when `[crates.e2e.calls.<call>.overrides.java]` (or
  `.kotlin_android`) declares a `client_factory`, `render_test_method` in
  kotlin_android style now picks it up and emits the instance-method call pattern
  (`val client = LiterLlm.createClient(...); client.chat(...); client.close()`)
  instead of the flat function call style (`LiterLlm.chat(...)`).
  Previously the fallback chain only checked a `"kotlin"` override key, so the
  java override was silently ignored. Regression test added in
  `src/e2e/tests/kotlin_android_codegen.rs`.
  (`src/e2e/codegen/kotlin.rs`)

- **alef-e2e/kotlin_android: emit Kotlin property access instead of Java getter
  calls**: `render_test_method` now passes `"kotlin_android"` as the language key
  to `FieldAccessResolver::accessor()`. New `render_kotlin_android_with_optionals`
  and `render_kotlin_android` helpers produce `result.choices.first().message.content`
  (no `()`) rather than `result.choices().first().message().content()`.
  Regression test added in `src/e2e/tests/kotlin_android_codegen.rs`.
  (`src/e2e/field_access.rs`, `src/e2e/codegen/kotlin.rs`)

- **alef-e2e/kotlin_android: serialize enum fields via `.name.lowercase()` not
  `.getValue()`**: assertion codegen now branches on `kotlin_android_style` to emit
  `.name.lowercase()` (or `?.name?.lowercase()` for optional enums) instead of
  `.getValue()`, which does not exist on plain Kotlin `enum class` values. Enum
  fields from `[overrides.java]` call configs are now also merged correctly when
  generating kotlin_android tests. Regression test added in
  `src/e2e/tests/kotlin_android_codegen.rs`.
  (`src/e2e/codegen/kotlin.rs`)

- **Binding surface: exclude source-declared internal fields before codegen**:
  Alef now records field-level exclusion metadata from `#[doc(hidden)]` and
  explicit `#[cfg_attr(alef, alef(skip))]`, then removes those fields from
  generated binding APIs before unknown-type sanitization. Internal caches and
  runtime-only state no longer appear as fake `String` fields in language
  bindings.
  (`crates/alef-extract`, `crates/alef-cli`, `crates/alef-core`)

- **Swift: render `extra_dependencies` into the generated Rust crate**: Swift
  bindings now merge crate-level and `[crates.swift.extra_dependencies]` entries
  into `packages/swift/rust/Cargo.toml`. Multi-crate consumers can reference
  shared core/runtime crates from generated Swift bridge code without manually
  editing the generated manifest.
  (`src/backends/swift/src/gen_rust_crate`)

- **alef-e2e/kotlin_android: streaming assertions use Kotlin property access and
  `Flow.toList()` collect**: `StreamingFieldResolver` previously had no
  `"kotlin_android"` arms — all five streaming virtual fields (`stream_content`,
  `stream_complete`, `tool_calls`, `finish_reason`, `usage`) fell through to
  the `"kotlin"` arm and emitted Java record accessor calls (`.choices()`,
  `.delta()`, `.content()`, `.finishReason()`, `.getValue()`, `.usage()`).
  Kotlin data-class fields (as generated by alef's kotlin-android backend) are
  plain properties, so every one of those method calls produced an
  "Unresolved reference" compile error. Added `"kotlin_android"` match arms for
  all five virtual fields and for `collect_snippet` (now emits
  `val chunks = result.toList()` — `Flow.toList()` — instead of
  `result.asSequence().toList()` which is the `Iterator` API). Deep-path
  tail-segment rendering in `render_deep_tail` also gained
  `(TailSeg::Field, "kotlin_android")` and `(TailSeg::Index, "kotlin_android")`
  arms that omit `()`. Five regression tests added in
  `src/e2e/codegen/streaming_assertions.rs`.
  (`src/e2e/codegen/streaming_assertions.rs`,
  `src/e2e/codegen/kotlin.rs`)

- **kotlin-android: optional facade params emit Kotlin default values**: params
  where `p.optional == true` (mapped from Rust `Option<T>` through the JNI shim
  as non-nullable `jlong`/`jint`/`JString`) previously had no Kotlin default in
  the facade signature, forcing every call site to supply all arguments. E2e
  codegen only passes `apiKey` and `baseUrl`, so `createClient(...)` failed to
  compile. The facade now emits `= 0L` for `Long`, `= 0` for `Int`,
  `= 0.0f`/`= 0.0` for `Float`/`Double`, `= false` for `Boolean`, and `= ""`
  for `String` optional params. Regression test
  `optional_params_get_kotlin_default_values_in_facade` in
  `src/backends/kotlin-android/tests/gen_bindings_test.rs`.
  (`src/backends/kotlin-android/gen_bindings.rs`)

- **alef-scaffold/jni: `[lib] name` uses `config.jni_lib_name()`**: the JNI
  cdylib scaffold derived `[lib] name` from the crate directory (`<crate>` with
  hyphens replaced by underscores), ignoring any `[ffi] prefix` override. The
  Kotlin Bridge emitter, however, uses `config.jni_lib_name()` (which honors
  `ffi_prefix`), so any consumer with a prefix override produced a `.so` whose
  basename disagreed with what `System.loadLibrary(...)` looks up at runtime,
  triggering `UnsatisfiedLinkError` on the first JNI call. The scaffold now
  also calls `config.jni_lib_name()`. Regression test in
  `crates/alef-scaffold/src/languages/jni.rs` asserts both the prefix-set and
  default cases.
  (`crates/alef-scaffold/src/languages/jni.rs`)

- **JNI: method-shim Result match gated on `error_type`**: `emit_method_shim`
  previously wrapped every method call in `match result { Ok / Err }` even when
  the method returned `T` directly. Builder methods returning `Self`
  (e.g. `ConversionOptionsBuilder::strip_tags`) failed to compile with
  `expected Self, found Result<_, _>`. Now mirrors `emit_function_shim`: gate
  on `method.error_type.is_some()` and emit the no-match path when the method
  is infallible. `emit_return_marshal` factored into a shared
  `_with_indent` helper so the no-error path can reuse the marshalling logic
  at the function's top-level 4-space indent.
  (`src/backends/jni/src/gen_shims.rs`)

- **JNI: `Vec<u8>` / `Bytes` params receive `jbyteArray`, `PathBuf` params skip
  JSON decode**: `emit_single_param_unmarshal` fell through to `serde_json::from_str`
  for any type other than `String`, producing invalid Rust for `&[u8]` / `PathBuf`
  params. Added explicit branches: `Vec<u8>` and `Bytes` now receive
  `<name>: jbyteArray` and call `env.convert_byte_array(name)` (the JNI function
  signature is patched to use `jbyteArray` for these types); `TypeRef::Path` receives
  `request_json: JString` and constructs `std::path::PathBuf::from(req_str)`
  without any JSON deserialization.
  (`src/backends/jni/src/gen_shims.rs`)

- **JNI: `&mut self` methods use `*mut T` handle cast**: `emit_method_shim`
  always emitted `&*(handle as *const T)` for the client dereference, causing
  E0596 ("cannot borrow as mutable behind `&` reference") for any method whose
  IR `receiver` is `ReceiverKind::RefMut` (e.g. `Parser::set_language`,
  `Parser::parse`, all `TreeCursor::goto_*`). `emit_client_shims` now inspects
  `method.receiver` and passes `receiver_is_mut` to `emit_method_shim`, which
  emits `&mut *(handle as *mut T)` when the flag is set.
  (`src/backends/jni/src/gen_shims.rs`)

- **JNI: `&[&str]` params coerce from `Vec<String>` JSON**: when a method param
  is `Vec<String>` with `is_ref=true` (representing `&[&str]` in core),
  `emit_single_param_unmarshal` now deserializes into `<name>_vec: Vec<String>`
  and `emit_method_shim` emits `let <name>_refs: Vec<&str>` before passing
  `&<name>_refs` to the core method. Previously `&Vec<String>` was passed
  directly, causing a type mismatch.
  (`src/backends/jni/src/gen_shims.rs`)

- **zig e2e codegen**: emit `build.zig` that runs tests via `addRunArtifact`
  directly (Zig 0.16+ no longer installs test binaries to `zig-out/bin/`).
  The previous `addInstallArtifact` + `run.dependOn(install)` workaround
  produced `FileNotFound` at spawn time against the would-be install path on
  Zig 0.16+ Linux backends. `setCwd("../../test_documents")` is also dropped
  — the generated tests reach the mock server purely through `MOCK_SERVER_*`
  env vars and never read anything cwd-relative, so leaving cwd alone keeps
  `convertPathArg` from re-resolving the cache spawn path.
  (`src/e2e/codegen/zig.rs`)

- **JNI: raw jlong for opaque handle returns**: `emit_function_shim` now returns
  `jlong` directly from opaque-returning top-level functions instead of a
  JSON-encoded `jstring`. Consumer types no longer require `Serialize` /
  `Deserialize` derives — the handle is a raw pointer cast, matching the
  method-on-struct pattern already used by liter-llm.
  (`src/backends/jni/src/gen_shims.rs`)

- **JNI: raw jlong for opaque handle params**: top-level functions whose parameters
  are opaque named types (e.g. `scrape(engine: &CrawlEngineHandle, ...)`) now
  receive `handle: jlong` and dereference via
  `unsafe { &*(handle as *const core_crate::T) }` instead of JSON-deserializing
  from a `JString`. The error-return sentinel in all unmarshal paths is now `0`
  (for jlong returns) rather than `std::ptr::null_mut()`.
  (`src/backends/jni/src/gen_shims.rs`)

- **JNI: emit nativeFreexxx destructors for top-level-function opaque returns**:
  the JNI backend now emits a `nativeFreeXxx(handle: jlong)` destructor shim for
  every opaque type returned by a top-level function that does not already have
  instance methods (those types were already covered by `emit_client_shims`).
  Fixes handle leaks on the kreuzcrawl `CrawlEngineHandle` shape.
  (`src/backends/jni/src/gen_shims.rs`)

- **kotlin-android: Bridge.kt Long for opaque handle params/returns**: Bridge.kt
  `external fun` declarations for top-level functions now use `Long` (not
  `String`) wherever a parameter or return type is an opaque named handle. Covers
  both the constructor return type and handle params like `engine: Long`.
  (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **kotlin-android: Bridge.kt emits nativeFreexxx for client types**: the
  `emit_method_jni_external_funs` helper now appends
  `external fun nativeFreeXxx(handle: Long)` after each client type's method
  declarations so the destructor that `DefaultClient.close()` delegates to is
  properly declared in Bridge.kt.
  (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **kotlin-android: Bridge.kt emits nativeFreexxx for non-method opaque types**:
  opaque types returned by top-level functions but without instance methods now
  also get a `// Destructor external funs` block in Bridge.kt.
  (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **kotlin-android: ByteArray for bytes::Bytes returns in Bridge.kt**: instance
  method `external fun` declarations now emit `ByteArray` (not `Long`) for
  `TypeRef::Bytes` return types (e.g. `speech`, `file_content`), matching the
  `Vec<u8>` → `ByteArray` mapping already present for the JNI Rust side.
  (`src/backends/kotlin/src/gen_bindings/jni_emitter.rs`)

- **dart: filter trait/opaque types from `From<Mirror> for Core` impls**: trait
  types reachable via trait-bridge return-type seeding (e.g. `SyncExtractor` from
  a `make_visitor() -> &dyn SyncExtractor`) were landing in
  `types_needing_from_impl` and producing
  `impl From<SyncExtractor> for SyncExtractor { ... SyncExtractor {} }`, which
  fails to compile (E0574: trait, not struct). The mirror-to-core emission loop
  now filters `is_trait`/`is_opaque` so only mirror structs receive the impl.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **dart: gate sanitized-String From exception on `CoreWrapper::Cow`**: the prior
  exception treated every sanitized `TypeRef::String` field as the safe
  `Cow<'static, str>` round-trip case, but fields whose core type is something
  unrelated (e.g. `Option<BoundingBox>` sanitized down to `Option<String>`
  because BoundingBox is not in the API surface) emitted
  `v.field.map(Into::into)` and failed to compile with
  `String: Into<BoundingBox>` not satisfied. The exception is now gated on
  `core_wrapper == CoreWrapper::Cow`; non-Cow sanitized strings fall back to
  `Default::default()` like other sanitized fields.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **dart: wrap Arc/Box core wrappers in `enum_variant_field_conv_to_core`**:
  enum variant fields previously emitted `.into()` regardless of `core_wrapper`
  or `is_boxed`, but `From<T>` is not provided for `Arc<T>`/`Box<T>` by stdlib.
  Variants now match the struct-field handling: `Arc`/`ArcMutex` core wrappers
  emit `std::sync::Arc::new(_.into())`, `is_boxed` emits `Box::new(_.into())`,
  and plain Named types keep `.into()`. Fixes
  `the trait bound Box<HtmlMetadata>: From<HtmlMetadata> is not satisfied`
  errors on `FormatMetadata::Html`/`FormatMetadata::Docx`.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **dart: round-trip `Option<T>` flattening in enum-variant mirror-to-core**:
  the forward direction collapses enum-variant `Option<String>` /
  `Option<PathBuf>` / `Option<primitive>` to bare values via
  `unwrap_or_default()`. The reverse direction now mirrors that: empty
  `String` → `None`, non-empty → `Some(_)`; `0` primitive → `None`, non-zero
  → `Some(_ as _)`; `newtype_wrapper` is honored on both branches so
  `Vec<NodeIndex>` (mirror `Vec<i64>`) round-trips via `NodeIndex(x as _)`.
  Fixes the `expected Option<String>, found String` / `i64 is not an iterator`
  / `non-primitive cast: i64 as NodeIndex` regressions across
  `NodeContent::{Image,Code,Group,Custom,Title}` and
  `AnnotationKind::{Link,Custom}`.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **dart: honor newtype_wrapper on `Vec<Primitive>` struct fields**: when a
  struct field is `Vec<Primitive>` in the mirror with `newtype_wrapper` set,
  `field_from_expr_to_core` previously emitted `|x| x as _`, breaking core
  fields like `children: Vec<NodeIndex>`. Now emits `|x| {nw}(x as _)` when
  `newtype_wrapper` is set, matching the scalar-primitive branch.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **kotlin-android e2e: emit `import kotlinx.coroutines.flow.toList` for
  streaming test files**: `render_test_file_inner` with `kotlin_android_style=true`
  now detects whether any fixture in the generated file is a streaming call and, if
  so, emits the `import kotlinx.coroutines.flow.toList` statement. Without it,
  `Flow<T>.toList()` is an unresolved reference in Kotlin 1.x/2.x — the
  extension is not part of the Flow API surface without the explicit import.
  Regression test `kotlin_android_streaming_fixture_emits_flow_to_list_import` in
  `src/e2e/codegen/kotlin.rs`.
  (`src/e2e/codegen/kotlin.rs`)

- **kotlin-android e2e: emit `registerKotlinModule()` on ObjectMapper and add
  `jackson-module-kotlin` dependency**: `render_test_file_inner` with
  `kotlin_android_style=true` now imports
  `com.fasterxml.jackson.module.kotlin.registerKotlinModule` and appends
  `.registerKotlinModule()` to the companion-object `MAPPER` initialiser.
  `render_build_gradle_kotlin_android` now includes
  `testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:{jackson}")`.
  Plain Jackson cannot deserialise Kotlin data classes (which have no no-arg
  constructor) without this module, producing `InvalidDefinitionException` at
  runtime. Regression tests `kotlin_android_object_mapper_emits_register_kotlin_module`
  in `src/e2e/codegen/kotlin.rs` and
  `build_gradle_kotlin_android_includes_jackson_module_kotlin` in
  `src/e2e/codegen/kotlin_android.rs`.
  (`src/e2e/codegen/kotlin.rs`,
  `src/e2e/codegen/kotlin_android.rs`)

- **kotlin-android bindings: emit `@JsonDeserialize` + custom deserializer for
  serde-tagged and serde-untagged sealed classes**: `emit_enum` in
  `alef-backend-kotlin` now detects enums with `serde_tag` (internally-tagged,
  e.g. `Message` with `#[serde(tag = "role")]`) or `serde_untagged` (e.g.
  `EmbeddingInput`, `RerankDocument`) and emits a `@JsonDeserialize(using =
  XxxDeserializer::class)` annotation on the sealed class plus a private
  `StdDeserializer<T>` subclass. The tagged deserializer reads the discriminant
  field from an `ObjectNode` and delegates to `ctx.readTreeAsValue(node,
  VariantPayload::class.java)`; the untagged deserializer branches on
  `node.isTextual` / `node.isArray` / `node.isObject`. Without this, Jackson
  throws `InvalidDefinitionException: Cannot construct instance of sealed class`
  when tests call `MAPPER.readValue(...)` against any of these types.
  Regression tests `emit_enum_tagged_sealed_class_emits_json_deserialize_annotation`,
  `emit_enum_untagged_sealed_class_emits_json_deserialize_annotation`, and
  `emit_enum_unit_only_does_not_emit_json_deserialize` in
  `src/backends/kotlin/src/gen_bindings/object_wrapper.rs`.
  (`src/backends/kotlin/src/gen_bindings/object_wrapper.rs`,
  `src/backends/kotlin/Cargo.toml`)

## [0.16.6] - 2026-05-15

### Added

- **alef-backend-jni**: new first-class backend crate that emits the Rust JNI
  shim source (`crates/<crate>-jni/src/lib.rs`) consumed by the Android AAR
  build. Exports `pub unsafe extern "system" fn Java_*` symbols for every
  top-level function, every instance method on opaque client types (with
  `handle: jlong` + optional `request_json: JString`), destructor shims
  (`nativeFree<Type>`), and streaming Start/Next/Free triple shims — one
  symbol per `external fun native*` declaration emitted by
  `alef-backend-kotlin-android`. Symbol names are derived via
  `alef_core::jni::*` so Kotlin bridge names and Rust `Java_*` symbols can
  never drift independently.
  (`src/backends/jni/`, `src/core/jni.rs`)

- **alef-core/jni module**: shared symbol-naming utilities
  (`bridge_class_name`, `bridge_method_name`, `streaming_method_names`,
  `destructor_method_name`, `jni_symbol`) extracted into `alef_core::jni` so
  every backend that touches JNI uses one canonical encoder.
  (`src/core/jni.rs`)

- **Language::Jni**: new language variant that activates `alef-backend-jni`
  emission. Requires `kotlin_android` to be listed in `languages` (validated
  at config resolution time). All existing match arms updated; `alef-scaffold`
  and `alef-cli` dispatch wired accordingly.
  (`src/core/config/extras.rs`, `crates/alef-cli/src/registry.rs`)

### Fixed

- **alef-backend-zig (free-before-use on C-string sentinel params)**: wrapper
  functions that converted a `[]const u8` parameter to a null-terminated C string
  via `allocPrintSentinel` / `dupeZ` were emitting `c_allocator.free(name_z)`
  _before_ the C function call, passing a dangling pointer. Affected all infallible
  top-level functions (e.g. `has_language`, `detect_language_from_content`,
  `detect_language_from_extension`, `detect_language_from_path`) and all infallible
  opaque-handle methods (e.g. `LanguageRegistry.has_language`, `Parser.parse`,
  `Node.child_by_field_name`). Fallible functions (those with an `error_type`) were
  already correct (free came after the error-code check). Fix: emit
  `defer std.heap.c_allocator.free(name_z);` immediately after each
  `allocPrintSentinel` / `dupeZ` alloc — the deferred free runs on function exit
  regardless of control flow, so the buffer is always alive during the C call.
  (`src/backends/zig/src/gen_bindings/functions.rs`,
  `src/backends/zig/src/gen_bindings/opaque_handles.rs`,
  `src/backends/zig/templates/param_string_line2.jinja`,
  `src/backends/zig/templates/param_optional_string_alloc.jinja`)

- **alef-backend-dart (String→Cow From-impl)**: `Cow<'static, str>` struct
  fields were silently dropped in the generated `From<MirrorT> for CoreT`
  implementation. The type resolver resolves `Cow<'static, str>` to
  `TypeRef::Named("str")`, which `sanitize_unknown_types` then replaces with
  `TypeRef::String` and marks `sanitized = true`. The dart backend's
  `emit_from_mirror_to_core_struct` unconditionally emitted `Default::default()`
  for every `sanitized` field — including these perfectly convertible
  `TypeRef::String` ones. Fix: skip the `Default::default()` fallback when
  `field.ty` is `TypeRef::String`; use `field_from_expr_to_core` instead, which
  emits `v.<field>.into()` (valid for `String → Cow<'static, str>`). Applies to
  any `Cow<'static, str>` field in any config struct, not just `language`.
  (`src/backends/dart/src/gen_rust_crate/mod.rs`)

- **alef-backend-swift (Error enum name clash)**: when the Rust error type is
  literally named `Error`, Swift parses `public enum Error: Error` as a circular
  raw-type binding instead of protocol conformance, causing compile errors. The
  codegen now renames such types to `{ModuleName}Error` (e.g.
  `TreeSitterLanguagePackError` for tslp). All error enums now use
  `Swift.Error` (fully-qualified) as the protocol conformance to avoid any
  future clashes with same-named types in scope.
  (`src/backends/swift/templates/error_enum_header.jinja`,
  `src/backends/swift/src/gen_bindings.rs`)

- **alef-e2e (swift)**: three follow-on fixes that make the generated swift
  e2e suite compile and report meaningful results:
  - **Map-field assertion subscript**: `materialise_vec_temporaries` now
    detects string-key subscripts (e.g. `["title"]`) and, for those cases,
    wraps the parent accessor in `JSONSerialization.jsonObject(...) as? [String: String]`
    so subscripting works on JSON-bridged Map fields (e.g. `metadata.document.open_graph[title]`).
    swift-bridge serialises `HashMap<String, String>` as JSON-encoded RustString;
    the previous emission tried to subscript RustString directly and produced
    `value of type 'RustString' has no subscripts`. The rewritten expression
    also signals back that no `.toString()` chain should follow.
  - **Trim character set**: `CharacterSet.whitespaces` does NOT include `\n`
    in Swift. Switched the `equals` and `contains` trim chains to
    `CharacterSet.whitespacesAndNewlines` so assertions like
    `XCTAssertEqual(trimmed, "# Heading 1")` against `# Heading 1\n\n` succeed.
  - **Visitor-fixture skip**: when a fixture has a `visitor` spec and the
    swift binding has no visitor wiring (the matching alef-backend-swift
    inbound emission is also gated below), emit `XCTSkipIf(true, ...)` so
    the test compiles and is recorded as pending rather than asserting
    against transformations the binding cannot perform.
  (`src/e2e/codegen/swift.rs`)

- **alef-backend-swift (inbound plugin emission)**: gate the inbound
  `extern "Swift"` block (`Swift<Trait>Box` declaration + per-method FFI shims),
  the corresponding `extern "Rust"` register/unregister entry points, and the
  Rust-side `Swift<Trait>Wrapper` newtype + `register_<trait>` fn on
  `bind_via = "function_param"`. Previously, _every_ configured trait bridge
  emitted the inbound plugin block unconditionally, including `bind_via = "options_field"`
  bridges (e.g. h2m's `HtmlVisitor`). The swift-bridge crate then declared
  `type Swift<Trait>Box;` in the `extern "Swift"` block, but there was no
  matching Swift class in the binding facade — `swift build` failed with
  `cannot find type 'Swift<Trait>Box' in scope`. `options_field` bridges
  conventionally bind via an options builder method (mirroring the dart/zig
  pattern), not via a separate Rust-side registry, so the inbound plugin
  scaffolding is the wrong shape for them. Until alef-backend-swift grows a
  proper Swift class scaffold + handle-builder shim for `options_field` bridges,
  skip the inbound emission entirely.
  (`src/backends/swift/src/gen_rust_crate/mod.rs`)

- **alef-e2e (swift)**: SwiftPM identifies path-based deps by the path's last
  component, so a consumer at `e2e/swift/` referencing a dep at `packages/swift/`
  collides on identifier `"swift"` and SPM resolves `.product(package:)` against
  the consumer itself, failing with `product '<name>' required by package 'swift'
  ... not found in package 'swift'`. Emit the swift e2e package under
  `<output>/swift_e2e/` (path-last `"swift_e2e"`) so the consumer has a distinct
  SwiftPM identity regardless of where the dep lives. Repos must update
  `[crates.test.swift] e2e = "cd e2e/swift_e2e && swift test"` accordingly.
  (`src/e2e/codegen/swift.rs`)

- **alef-e2e (swift)**: the generated `Package.swift` for swift e2e now uses
  `SWIFT_MIN_IOS` from `template_versions::toolchain` instead of hardcoding
  `.iOS(.v14)`. Previously, when the dependency package required iOS 16+ (the
  current default), SwiftPM hid the dep's products as platform-incompatible and
  failed resolution — the consumer's iOS minimum must be >= the dep's iOS
  minimum for SPM to expose products.
  (`src/e2e/codegen/swift.rs`)

- **alef-e2e/swift (config-from-json helper name)**: the config-arg codegen at
  line 1047 hardcoded `extractionConfigFromJson(...)` for all crates, but should
  derive the function name from the `options_type` configured in `CallOverride`.
  For example, with `options_type = "ProcessConfig"`, emit
  `processConfigFromJson(...)` instead. Derives the camelCased helper name on the
  fly; falls back to `extractionConfigFromJson` if `options_type` is absent
  (preserving backward compatibility for kreuzberg).
  (`src/e2e/codegen/swift.rs`, `src/e2e/tests/swift_config_from_json_helper.rs`)

- **alef-codegen (ruby kwargs constructor)**: Named enum fields with a resolved
  `EnumVariant` typed default now fall through to the explicit-default branch
  (`unwrap_or(EnumType::Variant)`) instead of being emitted as required kwargs
  via `ok_or_else(... "missing required field")`. The previous behaviour
  forced callers to pass every enum-typed config field explicitly even when
  the parent struct's `impl Default` already specified a concrete variant —
  e.g. kreuzberg's `ExtractionConfig.new` (Ruby) required `output_format:` on
  every construction despite `#[serde(default)] pub output_format: OutputFormat`
  on the Rust side. Magnus-wrapped _struct_ fields (which never implement
  `Default`) keep their required-kwarg behaviour. (`src/codegen/config_gen.rs`)

- **alef-backend-swift (capsule/handle passthrough for Option<Named(T)> returns)**:
  method shims and extern-block declarations for `Option<Named(T)>` return types
  now route through the handle-aware path (`bridge_type_with_handles`,
  `needs_json_bridge_with_handles`) instead of unconditionally falling back to
  `serde_json::to_string`. When `T` is a known opaque handle type (returned by any
  public function/method in the API surface), the shim emits `(call).map(T)` and
  the extern block declares `Option<T>` — not `String`. This fixes `E0277` compile
  errors in binding crates whose opaque types (e.g. `Tree`, `Node`) do not
  implement `serde::Serialize`. (`src/backends/swift/src/gen_rust_crate/extern_block.rs`,
  `src/backends/swift/src/gen_rust_crate/wrappers.rs`)

- **alef-backend-swift (arg conversions in method shims)**: method shims now
  insert correct conversions for four parameter patterns that previously generated
  code that failed to compile against the core crate:
  - `Bytes + is_ref` (`&[u8]` receiver): emit `&name` instead of `name`
  - `Path` (non-ref, `PathBuf` receiver): emit `::std::path::PathBuf::from(name)`
    instead of `&name`
  - `Named + is_ref` (`&T` receiver): emit `&name.0` instead of `name.0`
  - `Vec<String> + is_ref` (`&[&str]` receiver): emit
    `&name.iter().map(|s| s.as_str()).collect::<Vec<_>>()` instead of `name`
  (`src/backends/swift/src/gen_rust_crate/wrappers.rs`)

- **alef-backend-zig (opaque handle emission)**: opaque handle types with no
  instance methods (e.g. a bare `Language` newtype returned by `get_language()`)
  were silently excluded from struct emission because the loop filtered on
  `!t.methods.is_empty()`. Zig then rejected any function whose return type or
  body referenced the undeclared type with "use of undeclared identifier". Removed
  the `!t.methods.is_empty()` guard so every opaque/non-serde type receives a
  `pub const T = struct { _handle: *anyopaque, ... };` declaration regardless of
  whether it has methods. (`src/backends/zig/src/gen_bindings/mod.rs`)

- **alef-backend-zig (bool return coercion)**: functions returning `bool` through
  the C ABI (`extern "C" fn(...) -> i32`) generated `return _result;` where the
  Zig wrapper declared return type `bool`. Zig's strict type system rejects the
  implicit `i32 → bool` coercion with a compile error. The `unwrap_return_expr`
  function now emits `_result != 0` for `TypeRef::Primitive(PrimitiveType::Bool)`
  returns, producing a valid Zig boolean. Two new tests cover the infallible and
  fallible (error-union) cases.
  (`src/backends/zig/src/gen_bindings/functions.rs`,
  `src/backends/zig/tests/gen_bindings_test.rs`)

- **alef-e2e/zig (infallible function try emission)**: the Zig e2e codegen
  unconditionally emitted `try` before every call, but functions that are
  genuinely infallible and take no allocating parameters (e.g. `language_count()`
  which returns a plain `u64`) do not declare an error union — `try` on a
  non-error-union expression is a compile error in Zig. The fix defaults
  `call_returns_error_union` to `true` (safer: almost all wrapper functions
  allocate and/or can fail) and requires an explicit per-language opt-out:
  `[crates.e2e.calls.<name>.overrides.zig] returns_result = false`.
  (`src/e2e/codegen/zig.rs`,
  `src/e2e/tests/zig_infallible_no_try.rs`)

- **alef-e2e/zig (streaming-virtual field intercept in non-streaming path)**:
  `render_json_assertion` intercepted field names matching `is_streaming_virtual_field`
  (e.g. `"chunks"`) unconditionally, emitting `chunks.items.len` even when the test
  was not streaming and no `chunks` local was ever declared. Any non-streaming fixture
  whose result type happened to have a field named `chunks` produced an "undeclared
  identifier 'chunks'" compile error. Added `uses_streaming: bool` parameter to
  `render_json_assertion`; the streaming-virtual intercept path now fires only when
  `uses_streaming = true`. The four existing call sites pass the correct flag: the
  streaming path passes `true`, all others pass `false`.
  (`src/e2e/codegen/zig.rs`)

## [0.16.5] - 2026-05-15

### Added

- **alef-backend-kotlin (JNI mode)**: extended Bridge object and `DefaultClient`
  emission to cover the full Panama-emitted method surface. Every visible
  non-static instance method on an opaque client type now produces an
  `external fun native<Owner><Method>(handle: Long, requestJson: String): String`
  Bridge declaration plus a typed Kotlin wrapper that JSON-marshals the request
  via `MAPPER.writeValueAsString` and deserialises the response via
  `MAPPER.readValue`. Wrappers run on `Dispatchers.IO`. `Vec<u8>` IR returns map
  to Kotlin `ByteArray` (pass-through, no base64); `Unit` returns drop response
  handling; zero-param methods omit `requestJson` from both the Bridge external
  fun and the wrapper call. A shared `MAPPER` companion object is emitted on
  the client class when at least one method requires JSON marshalling.

### Fixed

- **alef-e2e/dart**: emit `import 'dart:io';` for non-HTTP fixtures that build mock-server
  URLs (`mock_url` args and `client_factory` configurations). These reference
  `Platform.environment` but were not covered by the prior import condition, causing
  generated Dart tests (e.g., `cookies_test.dart`, `error_test.dart`) to fail to compile
  with `Error: Undefined name 'Platform'`.
- **alef-e2e/zig**: resolve `FileNotFound` at test-binary spawn on Zig 0.16's self-hosted
  aarch64-linux backend. The generated `build.zig` now captures the install step via
  `b.addInstallArtifact(<test>, .{})` and makes each per-test `addRunArtifact` run step
  depend on it. This forces `Compile.installed_path` to be populated before `Run.make`
  reads it, so the spawn argv uses the absolute `zig-out/bin/<name>` install path instead
  of the cwd-relative `.zig-cache/o/<hash>/<name>` path that `convertPathArg` re-resolves
  when `setCwd` is applied. The cache-relative path broke on the self-hosted backend
  because its emitted cache hash disagreed with the path the build system computed
  ahead of time, even though the compile step reported success.

## [0.16.4] - 2026-05-15

### Added

- **alef-core/template_versions**: split `JVM_TARGET` into `JAVA_JVM_TARGET = "25"` (Java/Panama),
  `KOTLIN_JVM_TARGET = "21"` (Kotlin/JVM), and `ANDROID_JVM_TARGET = "11"` (Android, unchanged).
  `JVM_TARGET` retained as a deprecated alias of `JAVA_JVM_TARGET` for one release cycle.
- **alef-core/config**: `KotlinFfiStyle::{Panama, Jni}` enum; `KotlinConfig.ffi_style` field
  defaulting to `Panama`. `ResolvedCrateConfig` gains `kotlin_ffi_style()`,
  `with_kotlin_ffi_style()`, and `jni_lib_name()` accessors.
- **alef-backend-kotlin**: `KotlinFfiStyle::Jni` emission mode. Produces
  `<Module>Bridge.kt` (a Kotlin `object` with `external fun` JNI declarations
  and `init { System.loadLibrary("<crate>_jni") }`) plus a `DefaultClient`
  class holding a `Long` handle. Selectable via `[crates.kotlin] ffi_style = "jni"`.
- **alef-backend-zig**: honor `ffi.exclude_types` and `ffi.exclude_functions`
  (unioned with the zig-scoped lists); filter every type / field / method whose
  signature references an excluded type, matching the java/csharp/go backends.
- **alef-backend-kotlin / alef-backend-kotlin-android**: emit Kotlin `Flow<T>`
  streaming surface via `callbackFlow`. Each streaming method now produces a
  `public fun {method}(...): Flow<ChunkType>` wrapper plus three JNI native
  method declarations on the Java facade (`native{Owner}{Adapter}{Start,Next,Free}`).
  Flips `supports_streaming` to `true` on both backends. JNI Rust shims for
  the new native methods are hand-written in consumer crates (e.g.
  `liter-llm-ffi`) for this release; full Rust-side automation tracked.
- **alef-backend-dart / alef-e2e (dart)**: generate Dart visitor bridge
  factories that take typed `DartFnFuture` callback closures and return the
  configured opaque `type_alias` handle. Visitor fixtures now emit active
  `createHtmlVisitor(...)` setup and attach the handle through generated
  `create<OptionsType>FromJsonWithVisitor(...)` helpers instead of skipping
  visitor tests as pending.
- **alef-backend-dart**: emit `From<Mirror> for Core` impls for trait-bridge
  method return types (e.g. `VisitResult`) so the bridge converts the Dart
  callback's mirror return value back to the core type via `.into()` instead
  of dropping it. Without this, trait methods returning a named mirror type
  previously fell back to `Default::default()`, making every Dart visitor
  callback a silent no-op even though the closures executed.
- **alef-e2e (zig)**: generate active fixture-driven visitor tests. Visitor
  fixtures now emit per-fixture C-callable callback thunks, populate
  `HTMHtmVisitorCallbacks`, attach the visitor to conversion options, and call
  the C FFI directly instead of skipping every Zig visitor fixture.
- **alef-backend-kotlin-android**: add the Ben Manes Gradle Versions plugin
  to generated Android `build.gradle.kts` files.

### Changed

- **alef-backend-kotlin-android**: no longer bundles the Java/Panama facade
  inside the AAR. `src/main/java/` is removed entirely; binding code lives
  in `src/main/kotlin/` only. Forces `KotlinFfiStyle::Jni` regardless of
  configuration. Consumers must ship a `<crate>-jni` Rust crate exporting
  `Java_<package>_<Module>Bridge_native<Method>` symbols per JNI spec §5.11.3
  and link `lib<crate>_jni.so` into `jniLibs/<abi>/`.
- **alef-backend-kotlin / alef-backend-kotlin-android**: streaming JNI
  `external fun` declarations moved from the Java facade class to the new
  Kotlin Bridge object. JNI symbol pattern unchanged:
  `Java_<pkg>_<Module>Bridge_native<...>{Start,Next,Free}`.
- **alef-backend-java**: `gen_streaming_jni_decls` removed; the Java/Panama
  facade no longer carries `static native` JNI shims. Java backend is
  Panama-only now.

### Fixed

- **alef-backend-kotlin**: emit a single JVM client wrapper class into a
  matching Kotlin source file (for example `GraphQLRouteConfig.kt`) instead of
  always writing `DefaultClient.kt`, avoiding ktlint filename violations for
  non-`DefaultClient` opaque client types.
- **alef-backend-kotlin**: backtick-escape data class field names that collide
  with Kotlin hard keywords (`object`, `class`, `fun`, `val`, …). Previously
  fields like `object: String` on `BatchListResponse` produced a parse error
  at ktlint time; the wire name is preserved by wrapping the identifier in
  backticks.
- **alef-cli**: recognize `Generated by alef` headers during orphan cleanup so
  renamed generated files are removed by `alef all --clean`.
- **alef-backend-kotlin-android**: the generated `<Module>.kt` Kotlin facade
  lives in a sub-package of the bundled Java DTOs (e.g.
  `dev.kreuzberg.kreuzcrawl.android` vs `dev.kreuzberg.kreuzcrawl`). Kotlin
  does not inherit symbols from parent packages, so every bare DTO
  reference (`CrawlConfig`, `ScrapeResult`, …) was unresolved at
  `compileDebugKotlin` time. Emit explicit `import <java_pkg>.<Type>` lines
  for every `Named` type referenced by visible free functions. Also add
  `org.jetbrains.kotlinx:kotlinx-coroutines-android` as an `implementation`
  dependency in the generated `build.gradle.kts` so the `withContext` /
  `Dispatchers.IO` / `callbackFlow` calls compile.
- **alef-backend-kotlin**: apply the same explicit-import fix to the JVM
  `DefaultClient.kt` emitter — when the Kotlin and Java packages differ
  (including the `<pkg>.kt` sub-package fallback path), emit explicit
  per-type imports for every `Named` type touched by client method
  signatures and streaming adapter `item_type` / `params`.
- **alef-backend-go**: honor FFI and Go `exclude_types` settings when
  generating bindings, including functions and methods whose signatures
  reference excluded types.
- **alef-backend-java**: honor FFI and Java `exclude_types` settings when
  generating Panama bindings, including record fields, functions, and methods
  whose signatures reference excluded types. Deduplicate generated exception
  classes when multiple Rust error enums contain variants with the same name.
- **alef-backend-csharp**: honor FFI and C# `exclude_types` settings when
  generating P/Invoke bindings, and map named IR types through C# class casing
  so acronym-heavy types resolve to their generated class names. Opaque methods
  that return opaque handles now wrap the returned pointer directly instead of
  calling non-existent `_to_json` exports, and generated error dispatch preserves
  base error class names such as `GraphQLErrorException`.
- **alef-backend-kotlin / alef-backend-swift / alef-backend-zig**: honor FFI
  `exclude_types` / `exclude_functions` for FFI-backed host bindings so
  generated code does not reference C symbols or RustBridge types excluded from
  the FFI layer. Kotlin scaffold now pins `jackson-annotations` to its published
  `2.21` line while keeping `jackson-databind` on `2.21.3`.
- **alef-backend-zig**: emit register/unregister shims only for
  `bind_via = "function_param"` trait bridges. Options-field bridges now emit a
  vtable-to-handle helper instead of referencing missing
  `{prefix}_register_{trait}` / `{prefix}_unregister_{trait}` C symbols.
- **alef-backend-kotlin**: emit a single JVM client wrapper class into a
  matching Kotlin source file (for example `GraphQLRouteConfig.kt`) instead of
  always writing `DefaultClient.kt`, avoiding ktlint filename violations for
  non-`DefaultClient` opaque client types.

## [0.16.3] - 2026-05-15

### Added

- **alef-e2e (dart)**: fixture-driven visitor codegen scaffolding —
  `dart_visitors::build_dart_visitor` emits a `createHtmlVisitorDartImpl(...)`
  setup block from `fixture.visitor.callbacks`, mapping each
  `CallbackAction` to a `VisitResult.skip()` / `VisitResult.continue_()` /
  `VisitResult.preserveHtml()` / `VisitResult.custom(field0: '...')` body
  (with `${placeholder}` interpolation for `custom_template` actions).
  Visitor fixtures currently emit a `test(...,  skip: 'pending dart-binding
visitor wiring (alef issue)')` stub at the call site because the dart
  binding does not yet expose typed callback parameters for the
  `BoxFn...` opaque types, does not emit `VisitResult` factory variants
  (the enum is `#[frb-ignore]`d), and lacks a `HtmlVisitorDartImpl` →
  `VisitorHandle` conversion plus a `visitor:` argument on
  `H2mBridge.convert`. The codegen pattern is now in place so once the
  dart binding catches up, the visitor block can be appended directly to
  `setup_lines`.

### Fixed

- **alef-backend-swift**: emit `extension RustBridge.{Owner}{Adapter}StreamHandle:
@unchecked Sendable {}` for every streaming adapter's opaque handle type.
  swift-bridge generates opaque Swift classes without `Sendable` conformance; the
  generated `chatStream` wrapper captures the handle inside two nested
  `Task.detached` closures, which Swift 6 strict-concurrency rejects with "does
  not conform to the 'Sendable' protocol". The Rust handle wraps a
  `Mutex<BoxStream>` and a `tokio::runtime::Runtime` — both `Send + Sync` — so
  `@unchecked Sendable` is the correct annotation: Rust enforces thread-safety,
  Swift cannot see it through the opaque FFI boundary. The extension is emitted
  immediately after the owning client class declaration.
- **alef-backend-swift**: emit streaming `item_type` types that appear in
  `api.types` with `has_serde: true` and populated fields as native
  `public struct X: Codable { … }` declarations instead of opaque
  `typealias X = RustBridge.X` aliases. The generated streaming wrapper calls
  `JSONDecoder().decode(X.self, from: data)`, which requires `X: Decodable`; an
  opaque swift-bridge class does not implement `Decodable`, causing a compile
  error. The decision is field-gated (falling back to typealias when the IR has
  no fields) so it is safe for types that the IR only partially describes.

- **alef-backend-dart**: skip trait methods whose return type references another
  trait (e.g. `Option<&dyn SyncExtractor>` on `DocumentExtractor::as_sync_extractor`)
  from the generated trait bridge. The IR flattens `&dyn Trait` /
  `Option<&dyn Trait>` / `Box<dyn Trait>` to `TypeRef::Named(name)` — the
  bridge cannot dispatch these methods (the foreign side can't construct a
  Rust trait object across FFI) and the emitted `Option<TraitName>` is not
  even valid Rust (rustc E0782 "expected a type, found a trait"). The
  skipped method must have `has_default_impl = true` so the trait's
  default impl handles the receiver. Surfaced by kreuzberg's
  `DocumentExtractor::as_sync_extractor` breaking `cargo check -p kreuzberg-dart`
  on Android/iOS targets.
- **alef-backend-dart**: emit `.into()` on `String` / `Vec<String>` / `HashMap<String, _>`
  field conversions in generated `From` impls (both `core → mirror` and
  `mirror → core`). The IR collapses wrapped string types (`Box<str>`,
  `Cow<'_, str>`, `Arc<str>`) into `TypeRef::String` and only tracks `Cow`
  on `core_wrapper`, so the previous identity emit (`v.field`, `(k, v)`)
  failed to compile when core fields used unwrapped boxed/arc string
  variants (rustc E0277: `FromIterator<(Box<str>, Box<str>)>` is not
  implemented for `HashMap<String, String>`). The crate-level
  `#![allow(clippy::useless_conversion)]` was extended to absorb the
  no-op `String → String` case. Surfaced by kreuzcrawl's
  `DownloadedDocument.headers: HashMap<Box<str>, Box<str>>` field
  breaking `cargo clippy -p kreuzcrawl-dart`.
- **alef-backend-dart**: honor `[crates.dart.extra_dependencies]` when
  generating the package-local Rust bridge `Cargo.toml`, allowing projects
  to override sibling crate paths that differ from the workspace binding
  crates.
- **alef-backend-dart**: generate compiling Rust bridge conversions for
  optional borrowed string returns, optional JSON string params, optional
  borrowed string params, and named map values.
- **alef-backend-wasm**: make `[crates.wasm.exclude_fields]` remove fields
  from generated Wasm structs and conversions instead of leaving cfg-marked
  fields in the emitted Rust.
- **alef-e2e (zig)**: `field[key]` bracket notation in fixture aliases now
  resolves to a HashMap-style nested object lookup
  (`.object.get("field").?.object.get("key").?`). Previously the bracketed
  form was treated as a single literal JSON key, panicking with "attempt
  to use null value" on accesses like
  `metadata.document.open_graph[title]` (where `open_graph` is a
  `HashMap<String, String>` serialized as a nested object).
- **alef-e2e (zig)**: visitor fixtures now emit `return error.SkipZigTest`
  with a "pending zig-binding visitor wiring" comment, matching the dart
  codegen's pending-skip behaviour. The zig binding exposes a C-vtable
  `Visitor` struct via the FFI bridge but alef-e2e/zig doesn't yet emit
  the per-fixture vtable population + register call, so every visitor
  fixture's `convert(html, null)` call ran without the visitor and the
  assertions checking for custom outputs (e.g. `[AUDIO: podcast.mp3]`)
  failed.
- **alef-e2e (zig)**: `not_contains` assertions with a plural `values: [...]`
  list now emit one `std.mem.indexOf` check per needle. Previously only
  `value` (singular) was emitted; fixtures using `values` (e.g. XSS edge
  cases asserting `["onclick", "onmouseover", "alert(", "steal_data"]`)
  produced uncompilable code with an empty `indexOf(u8, str, )` third arg.
- **alef-e2e (zig)**: `equals` string assertions now `std.mem.trim` the
  JSON `.string` field on both whitespace and newlines before comparing,
  matching the rust/dart pattern. h2m's `convert()` emits a trailing `\n`
  on rendered markdown — the bare `expectEqualStrings(expected, ...)`
  flagged that as a false positive on the `Hello World` smoke fixture.
- **alef-backend-magnus**: generate compiling Ruby bindings for optional
  borrowed string returns, opaque owned builder methods, and `Bytes` data
  enum fields. Magnus now converts `Option<&str>` method results to owned
  `Option<String>`, only treats exact `self.inner` expressions as already
  `Arc`-wrapped, and keeps `TypeRef::Bytes` enum payloads as `Vec<u8>`.
- **alef-scaffold (elixir)**: normalize generated external `elixirc_paths`
  entries so leading slashes in explicit output paths do not produce
  `../..//crates/...` paths, and emit them via `Path.expand(..., __DIR__)`
  so Mix sees an absolute source root during compilation. The generated
  Rustler `Cargo.toml` now also points `[lib].path` at external Rustler
  output directories, so packages with `elixir = "crates/.../src/"`
  compile via Mix.

## [0.16.2] - 2026-05-15

### Fixed

- **alef-e2e (kotlin)**: clear four clippy warnings in
  `src/e2e/codegen/kotlin.rs` so `cargo clippy --workspace
-- -D warnings` is green again. Removed a duplicate
  `#[allow(clippy::too_many_arguments)]` attribute on
  `render_test_method`, added the same allow to the 13-arg
  `render_test_file_android` companion (matches the existing
  convention for these codegen-internal helpers), and collapsed two
  pairs of `if`/`else if` branches in the `not_empty` / `is_empty`
  assertion emitter that had identical bodies into single
  `bare_result_is_option || field_is_optional` arms. Emission is
  byte-identical (no snapshot churn).
- **alef-e2e (dart)**: pass fixture `input.options` through to the bridge
  call. Generic `json_object` args without a special name (e.g. h2m's
  `options: ConversionOptions` on `convert(html, options)`) now build the
  mirror struct via the FRB-generated `create<OptionsType>FromJson(json:
...)` helper and pass it as a named parameter when the arg is optional.
  Previously fixtures providing `input.options.include_document_structure
= true` etc. silently dropped the options, so tests requiring document
  structure, visitor registration, or metadata extraction returned `null`
  for the gated fields.
- **alef-backend-kotlin-android**: alias the Kotlin wrapper's `Bridge`
  import to the `Rs`-suffixed Java facade class (e.g.
  `dev.kreuzberg.KreuzbergRs`) instead of the unsuffixed module name. When
  the Kotlin `object` wrapper shares its simple name with the Java facade
  (both `Kreuzberg`, same package), aliasing `Bridge` to the short name
  resolves back to the Kotlin object itself, producing infinite recursion
  or unresolved members (`Bridge.getEmbeddingPreset(name).orElse(null)`
  failed because the Kotlin wrapper's `getEmbeddingPreset` returns the
  unwrapped `EmbeddingPreset?` and does not have `.orElse`). The
  `Rs`-suffixed alias matches the Java backend's `resolve_main_class`
  convention and guarantees `Bridge.*` calls hit the JNI layer.
- **alef-e2e (kotlin_android)**: wrap every emitted `@Test` body in
  `runBlocking { ... }` and emit nullable `T?` (`== null` / `!= null`)
  rather than `Optional<T>` (`.isEmpty` / `.isPresent`) for bare
  option-returning calls. The kotlin-android AAR exposes most extraction
  entry points as `suspend fun` and unwraps Java `Optional<T>` to Kotlin
  `T?` at the boundary, so the JVM-Kotlin emission style produced 26
  `Suspend function ... can only be called from a coroutine` errors and
  one `Unresolved reference 'isEmpty'` error on every regeneration.
- **alef-e2e (zig)**: pin `.use_llvm = true` on every emitted
  `b.addTest(.{...})` block and add a matching `b.installArtifact(...)`
  call before the run step. Zig 0.16 defaults the self-hosted aarch64
  backend on `aarch64-linux` Debug builds, which emits the test binary
  at a path that does not match what `addRunArtifact` computes via
  `getEmittedBin()`. Every test fails at run time with
  `error: failed to spawn and capture stdio from
.zig-cache/o/<hash>/<name>: FileNotFound` even though the compile
  step reports success. The previous fix (unique `.name` per test, see
  0.16.0) was necessary to avoid cache collisions but did not address
  the backend mismatch. Forcing the LLVM backend on every test pins the
  binary at the cache path the RunStep expects on every supported host;
  the explicit `installArtifact` is a defensive guarantee that the
  build system materialises the binary on disk before the run step
  executes. Other Zig backends (x86_64 macOS/Linux, aarch64-darwin)
  already default to LLVM, so the change is a no-op there.
- **alef-backend-kotlin-android**: stop re-declaring enum types in the
  Kotlin wrapper (`Kreuzberg.kt`) when the bundled Java facade already
  declares them. Previously every enum surfaced from the Rust IR was
  emitted as both a Java `enum` (in `src/main/java/`) and a Kotlin
  `enum class` (in `src/main/kotlin/Kreuzberg.kt`) at the same FQN,
  triggering 268+ `Redeclaration` errors when both source sets were
  compiled together. Kotlin/Java interop is seamless — the Java enum is
  canonical and the Kotlin wrapper now references it directly.
- **alef-e2e (kotlin_android)**: resolve the test package from the
  `[crates.kotlin_android] package` config (the AAR's actual JVM
  package) rather than the generic `config.kotlin_package()` accessor,
  which falls back to a `com.github.<org>` derivation from the GitHub
  URL when only `[crates.kotlin_android]` is configured. The mismatch
  generated tests at `com.github.<org>.e2e` while the AAR shipped types
  at `dev.kreuzberg` (or whatever package the user configured),
  producing ~150 `Unresolved reference 'Kreuzberg'` /
  `'ExtractionConfig'` errors. Precedence:
  `[crates.e2e.packages.kotlin_android].module` (explicit override) >
  `[crates.kotlin_android].package` > derived fallback.
- **alef-backend-kotlin**: extract a
  `emit_jvm_client_class_with_package` variant of
  `emit_jvm_client_class` so the kotlin_android backend can thread the
  AAR's own `[crates.kotlin_android] package` through the client-class
  emitter. The existing `emit_jvm_client_class` keeps the JVM-Kotlin
  semantics and is preserved as a thin wrapper.
- **alef-e2e (dart)**: `equals` / `field_equals` / `not_equals` assertions
  on String-valued expected values now trim both actual and expected,
  matching the rust e2e codegen pattern. h2m's `convert()` emits a
  trailing `\n` on rendered markdown; rust trims via
  `.to_string().as_str().trim()`, but dart was comparing raw, producing
  false-positive failures on tests like "Simple paragraph converts
  correctly" (`Hello World\n` vs `Hello World`).
- **alef-backend-kotlin-android**: emitted `.editorconfig` now disables the
  ktlint rules that conflict with ktfmt formatting (`class-signature`,
  `function-signature`, `function-expression-body`, `no-empty-class-body`,
  `no-empty-first-line-in-method-block`, `indent`, `filename`). The
  alef-emitted Kotlin Android code is canonically ktfmt-formatted; ktlint's
  competing layout opinions were spuriously flagging the output.
- **alef-backend-kotlin-android**: explicitly set Kotlin compile target via
  `kotlin { compilerOptions { jvmTarget = ... } }` so it matches the Java
  `compileOptions` target. Previously the emitted `build.gradle.kts` only
  set Java `sourceCompatibility`/`targetCompatibility` (JVM 11 by default)
  while Kotlin 2.x with AGP 8.x inferred its own JVM target (17), which
  caused `compileDebugKotlin` to abort with "Inconsistent JVM-target
  compatibility detected for tasks 'compileDebugJavaWithJavac' (11) and
  'compileDebugKotlin' (17)". Both compilers now use the
  `ANDROID_JVM_TARGET` toolchain constant (or the per-crate
  `kotlin_android.jvm_target` override) verbatim.

## [0.16.1] - 2026-05-15

### Added

- **alef-e2e (kotlin_android)**: new `KotlinAndroidE2eCodegen` emits
  `e2e/kotlin_android/` as a host-JVM Kotlin project that depends on the
  AAR-bundled Java facade and Kotlin wrapper via gradle sourceSets, loads
  `libkreuzberg_ffi` via JNA, and runs JUnit 5 tests on the host without
  requiring an Android emulator. Validates the Kotlin Android bindings
  end-to-end via the standard e2e fixture suite.

### Fixed

- **alef-docs (kotlin_android)**: emit `api-kotlin-android.md` with title
  "Kotlin (Android) API Reference" for the `kotlin_android` workspace language
  slug. Previously both `kotlin` and `kotlin_android` shared the same
  `api-kotlin.md` filename and "Kotlin API Reference" title, which collided
  when a workspace targets the Android variant only — the JVM-Kotlin doc
  would still appear alongside the Android one. Slug and display name now
  diverge while the code-fence language stays `kotlin` (what syntax
  highlighters expect).
- **alef e2e generate (python)**: preserve newlines in generated HTTP fixture
  tests. Whitespace-trimming Jinja blocks in `python/http_test.jinja` could
  concatenate adjacent statements such as `_headers = {}` and `_req = ...`,
  producing invalid Python syntax.
- **alef-e2e (dart)**: import path for the FRB bridge now matches the
  emitted directory layout. Tests previously imported
  `package:{lib_name}/src/{lib_name}_bridge_generated/frb_generated.dart`
  which doesn't exist — the FRB dir uses the snake_cased crate name (e.g.
  `html_to_markdown_rs_bridge_generated`), not the pubspec `name`. Now
  imports `package:{lib_name}/src/{module_name}_bridge_generated/frb_generated.dart`.
- **alef-e2e (dart)**: dropped unconditional `dart:io` import from
  generated test files (only emitted when HTTP fixtures or chdir-required
  fixtures actually use it) and switched count-style assertions
  (`count_equals`, `count_min`, `min_length`, `max_length`) from
  unconditional `?.length ?? 0` to plain `.length` when the IR marks the
  receiver as non-optional. Clears `unused_import` and
  `invalid_null_aware_operator` warnings on the generated suite.
- **alef-backend-dart**: the FRB bridge class name now honors
  `[crates.dart] lib_name` instead of always deriving from the crate name.
  Previously the dart backend's local `dart_bridge_class_name` helper used
  `config.name` (e.g. `html_to_markdown_rs` → `HtmlToMarkdownRsBridge`),
  while the alef-e2e dart codegen used the
  `ResolvedCrateConfig::dart_bridge_class_name` method which DOES honor
  `lib_name` (e.g. `lib_name = "h2m"` → `H2mBridge`). The mismatch left
  e2e tests referencing an undefined `H2mBridge` symbol. The backend now
  calls the same shared method; the local helper is removed.
- **alef e2e generate (rust)**: replace `cargo sort .` with `taplo fmt Cargo.toml`
  in the default rust e2e formatter, and unconditionally run taplo as a post-pass
  on the e2e crate's `Cargo.toml` regardless of user override. `cargo sort` was
  silently corrupting the e2e crate manifest by relocating the leading alef
  header comments to the bottom of the file (cargo-sort treats the empty
  `[workspace]` table at the file top as the first section and detaches
  preceding free comments). The relocation pushed the embedded `# alef:hash:`
  line past the 10-line detection window used by
  `alef_core::hash::{extract_hash, inject_hash_line}`, so `alef verify` could
  no longer find the hash and silently treated the file as fresh. Worse, when
  downstream `prek` ran taplo on the manifest, the file got reformatted after
  `finalize_hashes` had captured the pre-prek content, leaving
  `e2e/rust/Cargo.toml` flagged as stale by `alef-verify`. Taplo preserves the
  alef header at the top of the file and only normalises array wrapping and
  whitespace, which keeps verification working and makes prek's taplo hook a
  no-op.
- **alef generate --format (wasm/ffi rust binding crates)**: extend the TOML
  format pipeline to run `taplo fmt` after `cargo sort` so downstream `prek`
  setups that include a taplo hook reformat nothing after generation.
  Previously only `cargo sort` ran for these crates' `Cargo.toml`, leaving
  array wrapping/indentation at the cargo-sort default; a subsequent
  `taplo fmt` (run by prek or by the consumer's own CI) would rewrite the
  manifest and invalidate the embedded `# alef:hash:` line. Wasm/FFI Cargo.toml
  files are safe to cargo-sort because their alef header is followed
  immediately by `[package]` (no leading `[workspace]` sentinel to trigger
  cargo-sort's section reordering).
- **alef-codegen / all backends**: restore canonical `https://github.com/kreuzberg-dev/alef`
  URL in generated file headers; 0.16.0 introduced a `https://example.invalid/alef`
  placeholder by mistake. Affects every emitted artifact's `Issues & docs:` line.
- **alef-docs / api reference**: fix MD056 table-cell-count mismatch in
  generated `docs/reference/api-*.md` — some rows emitted 5 cells in a
  4-column table. Visible in TypeScript and Elixir API docs.
- **alef-backend-dart**: trait-bridge shims now preserve `Option<&T>` on reference
  parameters. Previously `Option<&str>` collapsed to `&str` in the emitted method
  signature, breaking `impl Trait` for traits like `HtmlVisitor` with optional
  reference args (E0053). The param-conversion let-bindings now use `.map(...)`
  to propagate the Option through to the Dart closure call.
- **alef-backend-dart**: emitted dart-crate `build.rs` now skips `flutter_rust_bridge_codegen` invocation gracefully when the tool is not on PATH instead of panicking. Downstream CI environments (`cargo check --workspace`) no longer require `flutter_rust_bridge_codegen` to be installed; the committed generated FRB sources are sufficient. Developers with FRB installed still get automatic regen on source changes.
- **alef-extract**: reverted the `Option<Option<T>>` → `Option<T>` IR flatten
  introduced in 0.16.0. The flatten produced cross-backend type mismatches
  (WASM and other passthrough backends saw `expected Option<Option<usize>>,
found Option<usize>` against the core `ConversionOptionsUpdate` shape).
  Nested optionals are now preserved in IR; per-backend renderers decide
  how to surface them.
- **alef-backend-dart**: `Option<Option<T>>` core fields now render as a
  single `Option<T>` in the FRB mirror struct (FRB rejects nested optionals
  at codegen time). The core→mirror conversion injects `.flatten()` and the
  mirror→core conversion injects `.map(Some)` so the bridge round-trips
  consistently. The "no change" vs "explicit clear" distinction is
  collapsed to `None` on the dart side; callers needing both states must
  model them via a dedicated enum.
- **alef-backend-dart**: opaque wrappers now emit `From` impls in both
  directions between the wrapper and its inner core type, so builder
  methods and field conversions referencing `{Wrapper}::from(core_val)` or
  `core_val.map(Into::into)` compile (E0308, E0631, E0277).
- **alef-backend-dart**: non-optional `char` fields convert with
  `.to_string()` in `field_from_expr` (mirror struct is `String`, core is
  `char`). Optional `char` keeps its existing `.map(...)` pattern but is
  now generated via `.to_string()` for parity (E0308).
- **alef-e2e (swift)**: emit Swift-native syntax for streaming-virtual accessors (`stream_content`, `stream_complete`, `tool_calls`, `finish_reason`, `usage`, `chunks.length`). Previously fell through to a TypeScript default that produced uncompilable Swift (`(c: any) =>`, `.length`, `?.[0]?.`, `!= null`, `undefined`). The new arms use swift-bridge method-call chains (`.choices()`, `.delta()`, `.content()?.toString()`, `.finish_reason()`) and Swift collection idioms (`.isEmpty`, `.last!`, `.count`, `.joined()`).
- **alef-backend-dart**: dropped redundant `.into()` on String/Vec<String>
  field, enum-variant, and HashMap key/value conversions
  (clippy::useless_conversion), gated `..Default::default()` struct-literal
  fillers behind `#[allow(clippy::needless_update)]` when the core struct
  has cfg-stripped fields, and replaced
  `.map(|v| T::from(v))` with `.map(T::from)` on bridge-function return
  wrappers (clippy::redundant_closure). The dart bridge crate now passes
  `clippy -D warnings`.

## [0.16.0] - 2026-05-14

### Added

- **alef-backend-kotlin-android**: new standalone backend crate for emitting
  Android library (AAR) projects from alef. Carved out of
  `alef-backend-kotlin/src/gen_android/` into a self-contained crate so the
  emission pipeline owns every file the AAR needs — no scaffold dependency.
  - **`Language::KotlinAndroid`** (slug `"kotlin_android"`, with
    `"kotlin-android"` accepted as a deserialization alias) is a first-class
    target distinct from `Language::Kotlin`. Workspaces can emit both JVM and
    Android Kotlin sets in parallel.
  - **Self-contained emission**: every file required for
    `gradle assembleRelease` is produced by the backend — `build.gradle.kts`
    (AGP plugin with version, `com.android.library`, kotlin("android"),
    maven-publish, namespace, compileSdk/minSdk/jvmTarget),
    `settings.gradle.kts` (pluginManagement with `google()`,
    `mavenCentral()`, `gradlePluginPortal()` + `dependencyResolutionManagement`),
    `src/main/AndroidManifest.xml`, `consumer-rules.pro`, `proguard-rules.pro`,
    `.gitignore`, `src/main/jniLibs/{arm64-v8a,x86_64}/.gitkeep`.
  - **Self-contained Java facade**: the backend re-emits the Java records
    that the Kotlin Android binding depends on into the AAR's
    `src/main/java/{java_pkg_path}/` directory, so the published AAR has no
    runtime dependency on a co-published Java jar.
  - **`KotlinAndroidConfig`** on `ResolvedCrateConfig` adds Android-specific
    knobs (`namespace`, `artifact_id`, `group_id`, `compile_sdk`, `min_sdk`,
    `jvm_target`, `abis`, `exclude_functions`, `exclude_types`). Defaults
    pulled from `template_versions::toolchain` so renovate keeps them current.
  - Snapshot tests cover every emitted file for a representative API
    (struct, enum, error, sync fn, async fn). insta-based, `cargo test
-p alef-backend-kotlin-android`.
  - **ktlint compliance out of the box**: the emitted `build.gradle.kts`
    applies the `org.jlleitschuh.gradle.ktlint` plugin (pinned via
    `template_versions::maven::KTLINT_GRADLE_PLUGIN` / `KTLINT`) and the
    emitted `.editorconfig` disables `ktlint_standard_package-name` so the
    co-located Java facade — whose package is reverse-DNS-derived from the
    repo's `repository` URL and may carry underscores (e.g.
    `com.github.kreuzberg_dev`) — does not trip the strict
    `standard:package-name` rule. All other ktlint rules remain on; after
    `alef generate --format`, `gradle ktlintCheck` passes clean on the
    full emission.
  - **`maven-publish` singleVariant("release")**: `android { publishing }`
    block configures the `release` variant with `withSourcesJar()` so the
    `publishAndReleaseToMavenCentral` task (vanniktech-compatible) finds
    a valid publication.
  - **AGP 8.x compatibility**: builds with `com.android.library` 8.x +
    explicit `kotlin("android")` plugin. The deprecated `kotlinOptions`
    DSL was dropped; AGP's `compileOptions` (sourceCompatibility /
    targetCompatibility) drives the Kotlin compile JVM target.

### Changed

- **BREAKING — alef-backend-kotlin**: the in-band `[crates.kotlin] mode =
"android"` dispatch path has been removed. Consumers that previously
  emitted Android via the kotlin backend must migrate to
  `Language::KotlinAndroid` and `[crates.kotlin_android]`. Setting
  `mode = "android"` now raises a hard error pointing at the new slug.
- **BREAKING — alef-scaffold**: `scaffold_kotlin_android` has been removed.
  Android scaffolding lives in `alef-backend-kotlin-android` so the same
  emitter owns the project tree on every regeneration. The
  `"android"` branch in `scaffold_kotlin` dispatch is gone.
- **alef-backend-swift**: streaming adapters are now implemented end-to-end.
  `Capabilities::supports_streaming` flips from `false` to `true`. For every
  adapter with `pattern = streaming`:
  - **Rust shim** emits a per-adapter opaque `{Owner}{Adapter}StreamHandle`
    that owns the `BoxStream<Result<T, E>>` and a per-stream
    `tokio::runtime::Runtime` (single-worker, multi-thread). swift-bridge's
    `extern "Rust"` block declares the handle plus a `_start` free function
    returning `Result<Handle, String>` (HTTP head validation happens here, so
    pre-stream errors throw before any chunk arrives) and a `fn next(self:
&mut Handle) -> Result<String, String>` method that drains one chunk via
  `runtime.block_on(stream.next())` and `serde_json::to_string`s it.
  Empty-string EOF sentinel.
  - **Swift wrapper** emits a public method returning
    `AsyncThrowingStream<ItemType, Error>` backed by a detached drain `Task`
    that loops `try handle.next()`, decodes each JSON chunk via
    `JSONDecoder().decode(ItemType.self, ...)`, and yields to the continuation.
    `continuation.onTermination` cancels the drain Task on early-break;
    swift-bridge's auto-generated `deinit` frees the handle.
  - **alef-e2e/streaming_assertions.rs (swift)**: replaces the
    `XCTSkipIf` stub with a real `for try await _chunk in result { … }` drain
    so streaming fixtures actually assert against the chunked items.
  - Items must be `Serialize` on the Rust side and `Decodable` on the Swift
    side. swift-bridge 0.1.59 bridges `Result<String, String>` cleanly; the
    JSON-string protocol avoids hitting the gaps in its `Option<OpaqueRust>`
    codegen.
    Trade-offs documented in the shim: one tokio runtime per stream (heavier
    than a shared runtime, but isolates lifetimes); back-pressure is not yet
    implemented (Swift consumer slower than chunk arrival → unbounded
    `AsyncThrowingStream` buffer growth).

- **alef-core/alef-scaffold/backends**: Alef is less organization-specific by
  default. Workspace config can now provide scaffold metadata plus generated
  header and pre-commit defaults, pre-commit scaffolding can point at custom
  shared/Alef hook repositories or omit those blocks, and PyO3/PHP/FFI trait
  bridge templates now use configured error constructors instead of hardcoded
  Kreuzberg error variants.
- **alef-backends/e2e/scaffold**: remove remaining organization-specific
  literals from emitted output. Extendr and Swift plugin bridges now use the
  configured error constructor, C#/Ruby/Wasm e2e scaffolds derive namespaces
  and globals from Alef/project defaults, generated headers fall back to a
  vendor-neutral issues URL, and pre-commit scaffolding defaults Alef hooks to
  local `alef` commands unless a repository is configured.
- **alef-scaffold/backends/e2e**: stop emitting GitHub Actions workflows and
  remove generated package-name literals from FFI streaming callbacks, Zig/PHP
  e2e streaming helpers, and visitor/plugin conversion comments.
- **alef-backend-php**: derive trait-bridge handle paths from the API surface
  instead of emitting a package-specific visitor handle path in generated bridge
  setters and function wrappers.
- **alef config/build commands**: build command defaults now compose with
  workspace and crate overrides field-by-field, so repositories can set global
  defaults while overriding only the commands they need per backend.
- **alef-scaffold**: Dart scaffolding now distinguishes FRB and raw FFI styles,
  uses Dart SDK `>=3.11.0 <4.0.0`, removes generated `BUILDING.md`, derives
  generated analyzer excludes from the crate name, and centralizes pub.dev
  dependency pins for Renovate.
- **alef-scaffold**: Kotlin scaffolding still covers JVM, Native, and Kotlin
  Multiplatform from Kotlin mode/target configuration. **Android scaffolding
  moved into the new `alef-backend-kotlin-android` crate** (see Added). Kotlin
  and ktlint dependency/tool pins are centralized for Renovate, and generated
  JVM text uses generic `native.lib.path` instead of organization-specific
  properties.
- **alef-scaffold**: Zig scaffolding no longer uses organization-specific
  workflow setup actions and now uses the public Zig setup action.

### Fixed

- **alef-backend-napi**: gate the raw `napi_create_external` /
  `napi_type_tag_object` `extern "C"` block with
  `#[cfg_attr(target_os = "windows", link(name = "node", kind = "raw-dylib"))]`.
  Without this, generated capsule_types bindings failed to link on Windows MSVC
  with `LNK2019: unresolved external symbol`, because `napi-build`'s `.def` only
  exposes symbols inside `napi-sys`'s `generate!` allowlist — and these two are
  not in it. `kind = "raw-dylib"` (stable since Rust 1.71) tells the MSVC
  linker to synthesize the import-library entries from the `extern` block
  itself; on Linux/macOS the `cfg_attr` is a no-op and Node's dynamic loader
  continues to resolve at module load. Added `windows-latest` to alef's CI
  test matrix so the new attribute regression-gates against future drift.
- **alef-backend-swift, alef-backend-dart**: emit correct `package = "..."`
  rename in bridge `Cargo.toml`. The rename target previously used
  `core_crate_dir` (the on-disk directory name), but cargo needs the actual
  `[package].name` from the umbrella crate's `Cargo.toml`. The two differ when
  `[[crates]] name` is suffixed for crates.io disambiguation (e.g.
  `html-to-markdown-rs` package at `crates/html-to-markdown/`). Without the
  fix, `cargo check` errors with `no matching package found, searched package
name: "html-to-markdown"`. Use `crate_name` (the `[[crates]]` `name` field)
  for the rename target instead.
- **alef-backend-swift**: emit method receivers from IR
  (`&self`/`&mut self`/`self`) instead of hardcoded `&self` — closes E0053 for
  traits with `&mut self` methods (40+ HtmlVisitor methods in h2m). Adds
  optional and slice param reconstruction in trait impls, emits `Debug` impl
  for non-Plugin inbound wrappers (`HtmlVisitor: Debug` supertrait), detects
  `ReceiverKind::Owned` for inner method calls and clones the inner value to
  support `&self` wrappers calling owned-self inner methods, and skips
  serde-bridging `Vec<T>` when `T` lacks `Serialize`/`Deserialize` (sanitized
  field detection).
- **alef-backend-dart**: `build.rs` invokes `flutter_rust_bridge_codegen` CLI
  at cargo build time (closes D1). Emits `visit_*` method receivers from IR.
- **alef-core**: configurable `error_type` / `error_constructor` read from
  `alef.toml` `[[crates]]` entries instead of hardcoded Kreuzberg error
  variants.
- **alef-extract**: flatten `Option<Option<T>>` in `type_resolver` (closes Z3).
  flutter_rust_bridge rejects nested optionals and zig emits semantically
  muddy `??T`. Structs that wrap already-optional fields (e.g.
  `ConversionOptionsUpdate.max_depth: Option<Option<u64>>`) now extract as a
  single `Option<T>` — `None` means "no change", matching the practical
  semantics across all backends.
- **alef-e2e (zig)**: emit unique `.name = "<test>_test"` on every
  `b.addTest(.{...})` block in the generated `build.zig`. Zig 0.16 hashes the
  output binary path off the artifact name; without an explicit name every
  `addTest` defaulted to `"test"`, colliding in the cache so only one binary
  survived and every other `addRunArtifact` invocation failed with
  `FileNotFound` at its computed `.zig-cache/o/<hash>/test` path.
- **alef-e2e (rust)**: omit `use {crate}::CrawlConfig;` (and other handle-arg
  helper imports) from generated `tests/<category>_test.rs` files when the
  rendered body never references the symbol. Test bodies are now buffered and
  scanned for word-boundary references before optional `use` statements are
  emitted, eliminating `unused_imports` errors under `-D warnings` for handle
  fixtures whose `input.config` is null/empty.
- **alef-backend-kotlin-android**: emit Android build-metadata files
  (`build.gradle.kts`, `settings.gradle.kts`, `consumer-rules.pro`,
  `proguard-rules.pro`, `.gitignore`, `src/main/AndroidManifest.xml`,
  `src/main/jniLibs/<abi>/.gitkeep`, `src/main/java/<java-pkg>/*.java`)
  at the AAR **project root**, not nested inside the Kotlin source
  destination. `[crates.output].kotlin_android` semantically names the
  Kotlin source destination (`src/main/kotlin/<dotted_package>/`); the
  project root is now derived by stripping that suffix. Kotlin source
  (`<Module>.kt`, `DefaultClient.kt`) is emitted directly at the
  configured path with no extra `src/main/kotlin/<pkg>/` nesting.
  Workspaces that pointed `kotlin_android` at the project root continue
  to work — the legacy semantics are preserved when the configured path
  does not end with the Gradle Android source-set suffix.

### Planned

- **alef-scaffold**: evolve the existing scaffold and backend implementations
  instead of adding new Alef crates. Planned follow-up work should align Dart
  scaffolding with FRB versus FFI and pure Dart versus Flutter plugin modes,
  align Kotlin scaffolding with the existing JVM, Native, and KMP
  backend modes (Android now ships as its own `alef-backend-kotlin-android`
  crate — see Added), include Swift Rust subpackage scaffold expectations, remove
  organization-specific assumptions from generated scaffold and workflow text,
  and centralize scaffold dependency and tool versions.

## [0.15.71] - 2026-05-14

### Fixed

- **alef-e2e/rust (test_file.rs, mock_server.rs)**: error fixtures now emit
  `let _mock_server = MockServer::start(...).await;` (underscore prefix)
  when the test body never references `mock_server` again. Previously every
  error test bound the server to `mock_server` purely to keep it alive for
  the duration of the failed call, which tripped `-D warnings` →
  `unused_variables` across `tests/error_test.rs` (12+ sites in kreuzcrawl).
  `render_test_function` now scans the body for a `mock_server.` reference
  and picks the bare or underscored name accordingly.
- **alef-e2e/zig (zig.rs)**: every generated `addModule(...)` /
  `createModule({...})` call now sets `.link_libc = true`. Required by
  Zig 0.16+: any module that transitively references C stdlib (`c.getenv`,
  `c.malloc`, etc.) must declare libc explicitly in its build graph,
  otherwise `zig build test` fails with `error: dependency on libc must be
explicitly specified in the build command`.
- **alef-e2e/wasm (wasm.rs)**: in `DependencyMode::Local`, the generated
  `e2e/wasm/package.json` now declares
  `"<crate>": "file:../../crates/<crate>-wasm/pkg/nodejs"` instead of just
  `pkg`. `wasm-pack build --target nodejs --out-dir pkg/nodejs` writes the
  npm-consumable package (with its own `package.json`) under `pkg/nodejs/`,
  so pnpm resolution requires the `/nodejs` suffix.

## [0.15.70] - 2026-05-14

### Fixed

- **alef-e2e/rust (mock_server.rs)**: emit `#![allow(dead_code)]` on the
  generated `tests/common.rs` module. The mock_server helper exposes a
  small set of test utilities (`MockRoute`, route builders) that not every
  test binary uses; without the allow, `-D warnings` flagged `dead_code`
  whenever a single test file imported only a subset of the module's
  surface.
- **alef-e2e/wasm**: emit a local `pnpm-workspace.yaml`
  (`packages: ["."]`) alongside the generated `e2e/wasm/package.json`.
  Without it, consumer repos whose root `pnpm-workspace.yaml` excludes
  `e2e/wasm` (because `wasm-pack` outputs a different `package.json` shape)
  leave `e2e/wasm` outside any workspace, and `pnpm install` cannot resolve
  the `kreuzcrawl` file-protocol dependency.

## [0.15.69] - 2026-05-14

### Reverted

- **alef-scaffold (swift.rs)**: roll back the `Sources/{module}/{module}.swift`
  stub introduced in 0.15.68 — it overwrote the alef-backend-swift authoritative
  output for the same path (the 500-line typealias module), breaking the public
  Swift API surface in downstream regens. The original SwiftPM "product not found"
  needs a different fix (likely in the codegen path, not the scaffold).

## [0.15.68] - 2026-05-14

### Fixed

- **alef-e2e (typescript/vitest.config.ts.jinja)**: add `testTimeout: 30000` so HTTP
  fixture tests against mock-server do not time out on slow ARM Linux CI runners (Vitest
  default is 5 s).
- **alef-scaffold (swift.rs)**: write `Sources/{module}/{module}.swift` stub so SwiftPM
  can register the `.library` product; without a source file in the target directory
  `swift test` fails with "product not found in package". (Reverted in 0.15.69.)

## [0.15.67] - 2026-05-14

Version bump only — no functional changes.

## [0.15.66] - 2026-05-14

### Fixed

- **alef-readme (performance_context.jinja)**: emit a blank line between the
  performance metadata line (`**Platform** · function · note`) and the
  benchmark table that follows. Without the blank line, `rumdl fmt`
  (Markdown formatter, used by downstream repos that lint the generated
  READMEs) auto-fixes `MD058` (Missing blank line before table) and the
  `git diff --exit-code` README freshness check in CI then fails. Reported
  via kreuzberg-dev/html-to-markdown CI Lint.

## [0.15.64] - 2026-05-14

### Fixed

- **alef-codegen (magnus hash constructor)**: stop emitting `TypeName::default()`
  for `TypeRef::Named` fields that lack an explicit `typed_default`. Magnus-
  wrapped structs (`#[magnus::wrap]`) do not implement `Default`, so the
  generated Ruby binding failed to compile with `E0599: no function or
associated item named 'default' found for struct 'FunctionDefinition'`
  (likewise `FunctionCall`, etc.). The hash constructor now treats such fields
  as required: when the caller does not provide the field, the constructor
  returns `magnus::Error::new(ruby.exception_arg_error(), "missing required
field: ...")` instead of synthesising a fictional default.
- **alef-e2e (swift)**: fix `Package.swift` for path-based local dependencies
  under SwiftPM 6.0. Previously the codegen emitted
  `.package(name: "<ModuleName>", path: ...)` paired with
  `.product(name: "<ModuleName>", package: "<ModuleName>")`, but SwiftPM 6.0
  ignores `.package(name:)` for path-based dependencies and infers the package
  identity from the path's last component (e.g. `packages/swift` → `swift`).
  The generated `Package.swift` therefore failed with
  `error: 'swift': product 'LiterLlm' required by package 'swift' target
'LiterLlmE2ETests' not found in package 'LiterLlm'`. The codegen now drops
  the redundant `name:` parameter and references the inferred identity in the
  product dependency.
- **alef-e2e (kotlin)**: emit `java` plugin alongside `kotlin("jvm")` in the
  generated `build.gradle.kts`. Under Gradle 9.x, `kotlin("jvm")` no longer
  implicitly applies the `java` base plugin, so the `test` task did not exist
  and `gradle test` failed with `Task 'test' not found in root project
'kotlin'.` Applying `java` restores the standard test/check lifecycle.

## [0.15.63] - 2026-05-14

### Fixed

- **alef-backend-magnus**: rename generated RBS type alias from
  `instance` to `value` in unit-variant enum stubs. `instance` is a
  reserved keyword in RBS (it refers to the "instance of self" type),
  so `rbs >= 4.0` rejected the generated `sig/types.rbs` with a hard
  parse error that aborted the entire RBS environment load. Downstream
  projects running `steep check` or `rbs validate` saw every unrelated
  RBS — `ActiveRecord::Base::ClassMethods`, `ActiveSupport::Concern`,
  `SimpleDelegator`, etc. — become "Cannot find type". Reported in
  kreuzberg-dev/html-to-markdown#360.

## [0.15.62] - 2026-05-14

### Added

- **alef-backend-dart**: support per-target Cargo dependency overrides via
  `[[crates.dart.target_dep_overrides]]` in `alef.toml`. Each entry specifies
  a `cfg` predicate, the `features` to enable on the core dep for that
  target, and optionally `default_features = false`. The emitted
  `Cargo.toml` wraps the base core dependency in `[target.'cfg(not(...))'.dependencies]`
  and emits a matching `[target.'cfg(...)'.dependencies]` block per override.
  Required for projects like kreuzberg where the Android x86_64 emulator
  triple cannot link ORT prebuilts and must fall back to a no-ORT feature
  group, while arm64 Android phones and all other targets keep the full set.

## [0.15.61] - 2026-05-14

### Fixed

- **alef-backend-ffi**: add `noop_method_call` to the crate-level
  `#![allow(...)]` block in the generated `lib.rs`. The trait-bridge async
  dispatch emits `let result = result.clone();` on `&[&str]` slices — a no-op
  since slices don't implement `Clone`. Cargo's `[lints.rust]` table doesn't
  override `RUSTFLAGS=-D warnings`, but a file-level inner attribute does.
- **alef-codegen/doc_emission**: take the exception class name as a parameter
  in `emit_phpdoc` and `emit_csharp_doc` instead of hard-coding
  `"KreuzbergException"`. PHP and C# backends now thread the project-specific
  exception type through `<exception>` / `@throws` doc emission so
  kreuzcrawl-style projects no longer see `<exception cref="KreuzbergException">`
  on their generated bindings.
- **alef-e2e/swift**: derive the test target name from the configured module
  (`{module_name}E2ETests`) in both the rendered `Package.swift` and the
  emitted test file directory. Previously hard-coded to `KreuzbergE2ETests`
  regardless of project, which broke kreuzcrawl's swift e2e build.
- **alef-backend-php**: unmask `core_import` parameter on `gen_static_method`
  (renamed from `_core_import` in 0.15.61 WIP) so `Exception` class derivation
  compiles. Fixes a clippy/cargo-check break introduced alongside the
  exception-name refactor.
- **alef-cli**: `alef all --format` now accepts the flag without an explicit
  value (matches the `alef generate --format` UX). Defaults to `true`; pass
  `--format=false` to opt out.

### Internal

- Refresh insta snapshots in `alef-backend-swift` for the cargo-machete
  metadata block and crate-root clippy allows added in 0.15.58.
- Sync `tests/integration_test.rs` and `tests/field_path_array.rs` with the
  current `wrap_return_with_mutex` and swift optional-chain accessor
  behaviour (`&&str` → `to_string()`, `()?[N]` without trailing `?`).
- Drop the outdated `texts:` named-arg assertion in
  `dart_scalar_array_arg.rs` — the dart codegen forces positional args for
  scalar-element json_object types.

## [0.15.60] - 2026-05-14

### Fixed

- **alef-e2e/swift**: actually move generated tests to `<output>/swift/Tests/KreuzbergE2ETests/`.
  The 0.15.59 release renamed the test target but left `tests_base` pointing at
  `output_base.join(pkg_path)` (which normalises to `packages/swift`), so the
  files still landed under `packages/swift/Tests/`. Use `output_base` directly
  and drop the now-unused `normalize_path` helper. Same Package.swift output
  as 0.15.59, just the test files now live where intended.

## [0.15.59] - 2026-05-14

### Fixed

- **alef-e2e/swift**: generate Swift e2e tests into `<output>/swift/Tests/KreuzbergE2ETests/`
  (mirroring every other language) instead of `packages/swift/Tests/`. The previous
  layout claimed SwiftPM 6.0 forbade inter-package `.package(path:)` references —
  not actually true; the real constraint was implicit package identity. The generated
  `e2e/swift/Package.swift` now declares an explicit
  `.package(name: "Kreuzberg", path: "../../packages/swift")` so SwiftPM resolves the
  binding library correctly. Downstream `alef.toml` `[crates.test.swift]` blocks can
  now point `e2e` at `cd e2e/swift && swift test`.
- **alef-e2e/c**: only emit the mock-server orchestration block in the generated
  `Makefile` when at least one fixture actually `needs_mock_server()`. Previously
  every C e2e suite hard-required `../rust/target/release/mock-server` regardless
  of whether any fixture defined `mock_response`/`http` shapes, breaking C FFI
  CI for repositories whose fixtures never exercise HTTP (e.g. html-to-markdown
  where 0 fixtures need a mock backend). Suites with mock-needing fixtures get
  the same behaviour as before.

## [0.15.58] - 2026-05-13

### Added

- **alef-cli**: `alef readme` now includes Rust in the regeneration loop when
  `[crates.readme.languages.rust]` is configured in `alef.toml`. Previously the
  command called `resolve_languages` which excluded Rust (matching the policy
  for binding generation), so the Rust crate's `crates/<lib>/README.md` was
  silently skipped and stayed frozen — drifting the moment templates changed.
  Introduces a dedicated `resolve_readme_languages` helper that mirrors
  `resolve_doc_languages` (Rust is the source language; its crates.io README
  needs the same regen path as every binding's README). Opt-in via the existing
  `[crates.readme.languages.rust]` block in `alef.toml`.

### Fixed

- **alef-e2e/swift**: coalesce `.len()` via `?? 0` when emitting `not_empty` /
  `is_empty` assertions over a `?.` optional-chain accessor. Previously the
  generator emitted `XCTAssertGreaterThan(result.field()?.inner().len(), 0, ...)`
  which Swift rejects with `cannot convert value of type 'UInt?' to expected
argument type 'Int'` because optional chaining propagates `?` through the
  whole expression. The not_empty / is_empty arms in
  `src/e2e/codegen/swift.rs` now check `accessor_is_optional` (the
  same heuristic already used by every other assertion arm) and wrap as
  `(field_expr.len() ?? 0)` when the chain crosses an optional field. Surfaced
  in kreuzcrawl's swift test target where `result.markdown()?.content().len()`
  failed to compile across MarkdownTests.swift.
- **alef-backend-swift**: emit `[package.metadata.cargo-machete] ignored =
["async-trait", "serde"]` in the swift-bridge Cargo.toml. Those deps are
  conditionally referenced (only when the umbrella crate declares trait
  bridges or `Serialize`/`Deserialize`-derived response types); downstream
  consumers without those features (kreuzcrawl) saw cargo-machete false-
  positive flags. Ignoring at the manifest level keeps the manifest stable
  across regens.
- **alef-backend-swift**: allow `clippy::useless_conversion` and
  `clippy::inherent_to_string` at the crate root of the emitted swift-bridge
  Rust crate. The bytes-default `__target.x = x.into();` is a no-op when
  source and target field types match (kreuzcrawl `Option<Vec<u8>>`), and the
  emitted `RustString` wrappers ship inherent `to_string(&self)` that clash
  with Display::to_string. Suppress at the generated-crate header instead of
  requiring per-task clippy flags downstream.
- **alef-backend-rustler**: emit `base_url:` inline in the generated
  `native.ex` instead of wrapped across two lines. Under the kreuzberg-dev
  `line_length: 120` convention, `mix format --check-formatted` collapses
  the wrap, so each `alef e2e generate` drifted against pre-commit.
- **alef-docs**: inject a trailing blank line after every heading at render
  time. The shared `heading.jinja` / `version_heading.jinja` templates can't
  carry a trailing blank line — the pre-commit `end-of-file-fixer` hook
  strips it on every commit — so wrap `template_env::render` to add the
  blank line for those two templates. Resolves the rumdl MD022 cycle that
  added ~1100 fixes across 18 `docs/reference/api-*.md` files per regen.

## [0.15.57] - 2026-05-13

### Fixed

- **alef-backend-wasm**: `gen_opaque_method` now respects `mutex_types` for both `&self` and `&mut self` methods on opaque types whose `inner` field is `Arc<Mutex<T>>`. Previously: (a) `&self` methods on Mutex-wrapped opaque types emitted `self.inner.{method}(...)` which fails to compile against `Arc<Mutex<T>>`, and (b) `&mut self` methods on opaque types fell through `shared::can_auto_delegate`'s blanket `RefMut → false` exclusion and were emitted as no-op `gen_wasm_unimplemented_body` stubs. Concretely surfaced in tree-sitter-language-pack's `WasmTreeCursor::node()`, `field_name()`, `goto_first_child()`, `goto_parent()`, `goto_next_sibling()`. Fix: override `can_delegate` for RefMut methods when `mutex_types.contains(type_name)` (mirrors the existing override in `methods.rs:33`), and dispatch via `self.inner.lock().unwrap().{method}(...)` for both `&self` and Owned receivers on Mutex-wrapped types.

## [0.15.56] - 2026-05-13

### Fixed

- **alef-backend-wasm**: wrap opaque-type constructor returns and `&self`-method returns in `Arc::new(std::sync::Mutex::new(...))` when the target type has `&mut self` methods (i.e. is in the new `mutex_types` set). Previously `gen_function` / `gen_method` / `gen_opaque_struct_methods` emitted bare `Arc::new(...)` regardless of whether the opaque struct field was `Arc<T>` or `Arc<Mutex<T>>`, producing invalid Rust like `inner: Arc::new(Parser::default())` against an `Arc<Mutex<Parser>>` field. Threads a `mutex_types: &AHashSet<String>` set through `gen_bindings/{functions,methods,types,mod}.rs` and uses `generators::wrap_return_with_mutex` at every Arc-wrapping site (parallels the Magnus fix in v0.15.55). Constructors no longer emit a double-nested struct-init (the wrap helper already returns the complete `Self { inner: ... }` expression).
- **alef-e2e/kotlin**: additional sticky-nullability + enum-typed field detection refinements in `src/e2e/codegen/kotlin.rs` plus shared `src/e2e/field_access.rs`; corresponding go/swift/streaming generator updates and `crates/alef-scaffold/src/languages/kotlin.rs` adjustments.
- **all visitor backends**: update all codegen sites that construct a `VisitorHandle` to use `Arc::new(Mutex::new(...))` instead of `Rc::new(RefCell::new(...))`, matching the updated `VisitorHandle` type alias (`Arc<Mutex<dyn HtmlVisitor + Send>>`). Affected backends: PHP, PyO3, NAPI (4 Jinja templates + 2 Rust sites), WASM (Jinja template + 3 Rust sites + `unsafe impl Send + Sync` in visitor bridge), Magnus (Rust + Jinja visitor bridge + `unsafe impl Send + Sync`), Rustler (template env + trait bridge), extendr (trait bridge + gen_bindings use stmts), FFI (gen_bridge_field + gen_visitor VisitorRef + `unsafe impl Send + Sync` on `{Prefix}Visitor` and `VisitorRef`). Also updates the Rust e2e test generator (`alef-e2e`) to emit `Arc::new(Mutex::new(...))` for the test visitor.
- **alef-backend-java**: switch generated instance-method `Arena` from `Arena.ofConfined()` to `Arena.ofShared()` so cross-thread access to `MemorySegment`s doesn't panic in pooled / async caller contexts.
- **alef-e2e/kotlin**: mock URL now resolves via the per-fixture system property set by `MockServerListener`, falling back to `mockServerUrl` / `MOCK_SERVER_URL` env var with the `/fixtures/<id>` suffix appended.

## [0.15.55] - 2026-05-13

### Added

- **alef-backend-gleam**: restore the Gleam backend, e2e codegen, scaffold, publish package, and `Language::Gleam` enum variant that were removed in v0.15.54 by mistake. Gleam is a first-class alef-supported binding language again, with the full backend crate (`alef-backend-gleam`), e2e codegen (`src/e2e/codegen/gleam.rs`), publish package handler, scaffold, readme template, doc emission, and downstream registry/match-arm wiring across `alef-core`, `alef-codegen`, `alef-adapters`, `alef-docs`, `alef-cli`, `alef-publish`, `alef-readme`, and `alef-scaffold`. Note: kreuzberg has independently dropped its own Gleam binding (BEAM users can call the Elixir binding via Erlang interop); restoring Gleam here keeps the language available for downstream alef users who want it.

### Fixed

- **alef-e2e/kotlin**: sticky nullability in `render_kotlin_with_optionals`. Once a `?.` safe-call is emitted for any segment in a Kotlin accessor chain, all subsequent segments are on a nullable receiver and must also use `?.`. The previous code reset `prev_was_nullable` to just `is_optional` on each segment, so non-optional fields after a `?.` call dropped the safe-call, producing uncompilable code (e.g. `toolCalls()?.first().function()` instead of `toolCalls()?.first()?.function()`). Fixed in all three `PathSegment` arms (`Field`, `ArrayField`, `MapAccess`).

- **alef-e2e/kotlin**: auto-detect enum-typed fields from IR `TypeDef` to route through `.getValue()`. Fields whose Rust type is `Named(T)` where `T` is not a known struct (i.e. an enum) must call `.getValue()` in Kotlin assertions. Previously only fields in the global `fields_enum` or a per-call `enum_fields` override were handled; `BatchObject.status` (type `BatchStatus`) was silently treated as `String`, causing `"BatchStatus has no method trim"` at Kotlin compile time. Now `generate()` builds a `type_enum_fields` map from the IR and merges it into `effective_enum_fields` per call. Also extends `field_is_optional` to detect nullability from the accessor expression itself (sticky-`?.` chain makes the whole expression `T?` regardless of what the path-prefix lookup finds).

- **alef-backend-magnus**: WIP: correctly convert `&DTO` and `&[&str]` parameter shapes when calling core functions. Functions and methods accepting references to generated DTO structs and string slices now emit proper type conversions at the FFI boundary. Serde recovery path creates `{name}_core` bindings for Named params; Vec<String> ref params create `{name}_core` intermediate Vec<&str> bindings consumed by `gen_call_args_with_let_bindings`.

- **alef-backend-csharp**: serialize enum values as strings via JsonStringEnumConverter (matches python/node/go/java/ruby/php). All C# enums now emit `[JsonConverter(typeof(JsonStringEnumConverter))]` to serialize as JSON strings (e.g. `"function"`) instead of numeric discriminants (e.g. `0`), matching other language bindings.

## [0.15.54] - 2026-05-13

### Added

### Fixed

- fix(alef-backend-rustler): wrap bare opaque returns and Optional<opaque> returns in ResourceArc<T> in NIF function/method signatures. Functions returning T (opaque type) now emit `ResourceArc<T>` instead of bare `T`, and functions returning `Option<T>` emit `Option<ResourceArc<T>>` instead of `Option<T>`. Methods on opaque types now call inner methods via `resource.inner.as_ref().method()` to properly dereference the Arc and invoke the inner type's methods (required for methods like `clone()`). Fixes compile errors where `Tree`, `Node`, `TreeCursor`, `Parser` were used directly without ResourceArc wrapping, which violated Rustler's Encoder trait bounds.

- fix(alef-e2e/typescript): gate streaming-virtual interception on is_streaming, matching go fix. TypeScript codegen now respects the `streaming = false` opt-out at the call level, skipping the virtual field interception (chunks, stream_content, etc.) and allowing them to be resolved normally through the result struct accessor path. Non-streaming fixtures no longer emit references to undeclared `chunks` variable.

- fix(alef-backend-magnus): delegate all methods through `Mutex` on types with `&mut self` methods. When an opaque type has any `&mut self` methods, ALL its methods (including `&self` methods) must use `.lock().unwrap()` to access the inner value in `Arc<Mutex<T>>`. Previously, only `&mut self` methods got the lock, leaving `&self` methods calling `.method()` directly on `Arc<Mutex<T>>`, which fails because `Arc` has no such methods. Now matches the NAPI and PyO3 patterns: if `has_mut_methods`, emit `.lock().unwrap()` for all delegation.
- fix(alef-backend-pyo3): surface trait-bridge `OptionsField` kwargs in the generated `.pyi` for both struct `__init__` and module-level functions. The cfg-gated bridge field (e.g. `ConversionOptions.visitor`) is stripped by the `gen_stubs.rs` partition (`f.cfg.is_none()`), but the PyO3 `#[new]` macro keeps it via `never_skip_cfg_field_names`, so api.py callers like `_rust.ConversionOptions(visitor=...)` and `_rust.convert(html, options, visitor=...)` failed `mypy --strict` with `Unexpected keyword argument "visitor"`. Stubs now append the bridge kwarg as `{kwarg_name}: {type_alias} | None = None` whenever the options type is referenced, mirroring the wrapper signature in `gen_bindings/functions.rs`.
- fix(alef-backend-wasm): wrap opaque types with `&mut self` methods in `Arc<Mutex<T>>` matching NAPI/PyO3/Magnus pattern. Opaque types now detect `RefMut` receivers and conditionally wrap the inner type in `std::sync::Mutex` so that method delegation can call `.lock().unwrap()` before invoking mutable methods. Previously generated `Arc<T>` unconditionally, causing invalid code when methods tried to call `.lock()` on non-Mutex types.
- fix(alef-backend-wasm): gate the `self.inner.lock().unwrap()` call path on the receiver type actually being opaque. Methods on transparent (named-field) structs whose type happens to have a sibling `&mut self` method (e.g. `WasmDocumentStructure::is_empty` alongside `finalize_node_types`) were emitting `self.inner.lock().unwrap().is_empty()` even though the struct has direct fields and no `inner: Arc<Mutex<T>>`. Codegen now checks `opaque_types.contains(type_name)` before taking the Mutex branch; transparent structs fall back to `Type::from(self.clone()).method(...)`.
- fix(alef-backend-csharp): pass length argument alongside pinned pointer when delegating byte-slice params to native P/Invoke. Wrapper methods now emit both the `AddrOfPinnedObject()` pointer AND the `(UIntPtr)source.Length` argument for `&[u8]` parameters, matching the FFI signature. Previously only the pointer was passed, causing C# compile errors (CS7036: missing required parameter) when parsing byte arrays.
- fix(alef-e2e/kotlin): fix `{kotlin_val}` placeholder in numeric comparison assertion failure messages. The `greater_than`, `less_than`, `greater_than_or_equal`, and `less_than_or_equal` assertions emitted literal text `expected > {kotlin_val}` because the placeholder was doubly-escaped (`{{kotlin_val}}`), causing Kotlin source parse errors (`Expecting an element`).
- fix(alef-e2e): backtick-escape Kotlin hard keywords when emitting field accessors. Field paths like `result.data().first().object()` are syntax errors in Kotlin because `object` is a hard keyword; now emitted as ``result.data().first().`object`()``. Applies to both plain and nullable-aware Kotlin accessor renderers.

## [0.15.53] - 2026-05-13

### Added

### Fixed

- fix(alef-backend-magnus): pass missing `mutex_types` argument in Magnus `gen_function` unit-test call sites after the signature change, fixing `cargo check` failures in `gen_bindings_test`.

### Added

- feat(alef-e2e): add `[e2e.calls.<name>] streaming = false | true` opt-in/out at the call config level. When set to `false`, the streaming-virtual-field auto-detection is disabled — assertions that reference field names like `chunks` / `chunks.length` / `tool_calls` / `finish_reason` on a synchronous result type are treated as plain field accessors rather than streaming adapters. Without this, an API whose return value happens to have a `chunks: Vec<T>` field that is not actually streamed (e.g. a code-chunking result) would be incorrectly emitted with `async for chunk in result` and `@pytest.mark.asyncio` decorators across every backend that supports streaming. `None` (default) preserves the prior auto-detect heuristic so existing LLM-style downstream crates are unchanged. Honored by all backends that previously hard-coded the heuristic: python, typescript (node + wasm), go, java, php, elixir, kotlin, swift, dart. A new `resolve_is_streaming(fixture, call_config.streaming)` helper in `codegen/streaming_assertions.rs` is the single source of truth so future backends pick up the opt-out automatically.

### Fixed

- fix(alef-e2e/go): gate the streaming-virtual-field assertion interception in `render_assertion` on the resolved `is_streaming` flag (threaded through from `render_test_function`). Without this gate, a call with `streaming = false` whose result type happens to have a `chunks` field would still emit `len(chunks)` referencing an undeclared local (streaming chunks-var) instead of the plain `len(result.Chunks)` field accessor. Matches the python/typescript backends which already short-circuit when streaming is disabled.

- fix(alef-backend-magnus): wrap opaque types with `&mut self` methods in `Arc<Mutex<T>>` instead of bare `Arc<T>`, and allow delegation of `&mut self` methods via `.lock().unwrap()`. Previously, opaque structs were always wrapped in `Arc` with no mutex, and methods with `&mut self` receivers were stubbed as "Not implemented". This fix enables real implementations for methods like `Parser::set_language`. Complements equivalent fixes already in NAPI and PHP backends.

- fix(alef-backend-php): allow delegation of `&mut self` methods on opaque types wrapped in `Arc<Mutex<T>>`. Previously the can_delegate check rejected all RefMut receivers (line 396) assuming Arc doesn't support `&mut T`. However, when a type has any RefMut methods (indicating Mutex-wrapping), those methods CAN be delegated by locking the mutex. This fix enables real implementations for methods like `Parser::set_language` instead of stubbed "Not implemented" bodies.

- fix(alef-backend-napi): allow delegation of `&mut self` methods on opaque types wrapped in `Arc<Mutex<T>>`. Previously the opaque_can_delegate check rejected all RefMut receivers (line 418) assuming Arc doesn't support `&mut T`. However, when a type has any RefMut methods (indicating Mutex-wrapping), those methods CAN be delegated by locking the mutex. This fix enables real implementations for methods like `Parser::set_language` instead of stubbed "Not implemented" bodies.

- fix(alef-e2e/csharp): emit `<GenerateAssemblyInfo>false</GenerateAssemblyInfo>` in the e2e test project `.csproj` to prevent duplicate `AssemblyInfo` attributes when a hand-written `Properties/AssemblyInfo.cs` file provides assembly metadata manually. The .NET SDK auto-generates `obj/*/AssemblyInfo.cs` by default, leading to CS0579 errors when both files define the same attributes. Complements the assembly info file previously generated for e2e projects.

- fix(alef-e2e/python): also include per-fixture streaming in the file-level `is_async` calculation used to gate `import pytest`. Previously a file containing fixtures whose `is_streaming` was triggered only by virtual-field assertions (not by call-level `async`) would emit `@pytest.mark.asyncio` decorators without the matching `import pytest`, producing `NameError`/F821 on test collection. Now `needs_pytest` correctly reflects every code path that emits an async test.

- fix(alef-backend-extendr): generate `String` (not `Robj`) for non-options string parameters in `gen_extendr_bridge_field_function`, and build the core-function call from the actual params instead of a hardcoded `convert(&html, …)` literal. The previous output emitted `pub fn convert(html: Robj, options: Robj)` and then called `core::convert(&html, Some(opts))` — `&Robj` doesn't satisfy `&str`, so the generated R binding crate failed to compile. Now string params decode via extendr's `TryFrom<Robj> for String` and the `&name` call site deref-coerces to `&str`.

- fix(alef-e2e/r): wrap array-valued fields in `I(...)` when emitting `jsonlite::toJSON(list(...), auto_unbox = TRUE)` for the `Type$from_json(...)` typed-config code path. Without the `AsIs` marker, single-element vectors were unboxed to scalars (`c("foo")` → `"foo"`) and empty vectors collapsed to `{}`, causing serde to reject `Vec<T>` fields with `invalid type: string "foo", expected a sequence`. Non-empty arrays now emit as `I(c(...))` (→ `[...]`) and empty arrays as `I(list())` (→ `[]`), fixing 9 R e2e failures across `exclude_selectors`, `strip_tags`, `preserve_tags`, and `keep_inline_images_in`.

- fix(alef-e2e/r): visitor-test arg builder now strips any pre-existing `options = …` argument before appending `options = list(visitor = visitor)`, instead of only stripping the literal `options = NULL`. The previous code left `options = ConversionOptions$default()` in place when `build_args_string` emitted a default placeholder, producing duplicated named args that R rejects with `formal argument "options" matched by multiple actual arguments`. New `strip_options_arg` helper walks the args string while tracking paren/quote depth so nested commas inside `options = list(visitor = visitor)` aren't treated as arg separators.

- fix(alef-backend-csharp): emit single `{` / `}` (not `{{` / `}}`) in the `VisitResult.ToFfiJson()` string-concatenation literals for `Custom` and `Error` variants. The template was authored as if the surrounding code were a C# interpolated string (`$"…"`), where `{{` escapes to `{` — but the emitted source is a plain string-concatenation, so `{{` came out as literal double-braces, producing `{{"Custom":"…"}}` which Rust's serde rejects. C# visitor overrides (`VisitResult.Custom("…")`) now round-trip correctly across the FFI boundary, fixing all C# e2e visitor tests (42 fixtures previously regressing).

- fix(alef-backend-csharp): include `api.enums` (alongside `api.types`) when building the `visible_type_names` set passed to trait-bridge codegen. Without this, enum types like `VisitResult` were treated as non-API by `csharp_type_visible`, which substituted them with `string` in generated trait-method signatures (`string VisitFigureEnd(...)`). E2e visitor tests that returned `VisitResult.Continue` then failed to compile with CS0738 "does not have the matching return type of 'string'".

- fix(alef-e2e/csharp): look up `enum_fields` and `nested_types` overrides by both snake_case (fixture JSON key) and camelCase (alef.toml convention) when emitting C# object initializers. Previously fixtures with `code_block_style: "Backticks"` produced `CodeBlockStyle = "Backticks"` (raw string literal) instead of `CodeBlockStyle = CodeBlockStyle.Backticks` (enum constant), causing ~18 CS0029 compile errors. C# is strongly typed and does not accept string-to-enum implicit conversion, unlike Python's ConversionOptions which accepts strings and converts internally.

## [0.15.52] - 2026-05-13

### Added

- feat(scaffold/node): add `futures-util` to generated Node binding `Cargo.toml` so `chat_stream` compiles without requiring a manual dependency addition

### Fixed

- fix(alef-backend-magnus): enums now use the variant marked with `#[default]` instead of always the first variant in source order. This respects Rust's `#[derive(Default)]` semantics. Fixes `CodeBlockStyle` defaulting to `Backticks` (marked `#[default]`) instead of `Indented`, enabling fenced code blocks with language annotations.
- fix(alef-backend-magnus): emit explicit `impl Default` for all binding structs with fields instead of deriving `Default`, which gives all-zeros initialization. Explicit impls use field-level defaults computed by config_gen (same as kwargs constructors), ensuring serde deserialization with missing fields produces correct values (e.g., `PreprocessingOptions.enabled=true` instead of false). Properly handles `Option<T>` fields which always default to `None`.

- fix(napi,pyo3,wasm,php): allow `clippy::should_implement_trait` at the crate level so opaque-type wrappers carrying a `pub fn clone(&self) -> Self` method (lifted from the upstream `Clone` derive on the wrapped Rust type) compile under `-D warnings`. Without the allow, NAPI/PyO3/wasm-bindgen/ext-php-rs surfaces fail clippy because the inherent method shadows `std::clone::Clone::clone`; the bindings still derive `Clone` for the wrapper type so trait dispatch is unaffected.

- fix(alef-backend-magnus): unit-enum `TryConvert` now also accepts the PascalCase Rust variant name (e.g. `"Tildes"`, `"AtxClosed"`) alongside the serde wire form (e.g. `"tildes"`). Fixtures and tests written in either style now deserialize correctly.
- fix(alef-e2e/ruby): emit string-equals assertions as `expr.to_s.strip == expected.strip` to match Python's pattern, masking trailing-newline differences fixture authors don't write into expected values. Drops ~10 ruby e2e failures in html-to-markdown.
- fix(alef-e2e/field_access): render Ruby hash map-access as `["key"]` instead of `.get("key")`. Ruby's `Hash` has no `get` method, so previously-generated assertions like `result.metadata.document.open_graph.get("title")` raised `NoMethodError`.
- fix(alef-e2e/typescript): look up `enum_fields` overrides by both snake_case (fixture key) and camelCase (alef.toml convention) when emitting WASM-class field assignments. Previously fixtures with `code_block_style: "Tildes"` produced `_u.codeBlockStyle = "Tildes"` (raw string) instead of `_u.codeBlockStyle = WasmCodeBlockStyle.Tildes` (enum constant), and wasm-bindgen silently dropped the assignment so options like code-block style, heading style, and highlight style never took effect.
- fix(alef-backend-magnus): in `gen_options_field_bridge_function`, recognise Ruby `ConversionOptions` class instances passed as the options argument and convert them to core via `.clone().into()`. Previously only `Hash` and visitor objects were accepted, so tests doing `HtmlToMarkdownRs::ConversionOptions.new(...)` were misclassified as visitors and the options were dropped (e.g. `include_document_structure` never flowed through, leaving `result.document` nil). Also drives the options type name from the IR so the helper isn't hard-wired to `ConversionOptions`.
- fix(alef-backend-napi): stop clearing `o.visitor = None` before `o.into()` in `gen_options_field_bridge_function`. The From impl now forwards `val.visitor` through `JsHtmlVisitorBridge` (via the existing post-process), so the handle survives the conversion. A separate kwarg-supplied visitor still overrides when present. Without this fix, JS callers passing `convert(html, { visitor: ... })` silently lost the visitor and callbacks never fired.
- fix(alef-e2e/typescript): emit Node visitor tests as `convert(html, options, visitor)` (three positional args) instead of merging `visitor` into the options object literal. napi-rs cannot deserialize `Option<Object<'static>>` from a JS object property, so the visitor field was always `None` on the Rust side; routing through the existing kwarg path wraps the JS object via `JsHtmlVisitorBridge::new`.
- fix(alef-e2e/typescript): wrap WASM e2e visitor tests' visitor argument with `new WasmVisitorHandle(...)`. wasm-bindgen's generated setter for `WasmConversionOptions.visitor` enforces `_assertClass(value, WasmVisitorHandle)` and rejects plain JS objects. Auto-import `WasmVisitorHandle` alongside `WasmConversionOptions`. Also preserve the trailing `html` positional argument when the fixture has no other options (previous IIFE rewrite replaced it).
- fix(e2e/kotlin): derive JAR name by replacing hyphens with underscores (`rootProject.name` uses underscores, producing e.g. `liter_llm-VERSION.jar`); bare hyphenated name caused "Unresolved reference" linking errors
- fix(e2e/kotlin): wrap generated `@Test` bodies in `runBlocking { }` when the binding exposes `suspend fun` methods; previously tests called coroutine methods outside a coroutine scope and crashed with `IllegalStateException`
- fix(e2e/kotlin): append `L` suffix to integer literals for `uint64_t`/`int64_t` fields (e.g. `assertEquals(6L, result.promptTokens())`) to prevent Kotlin long-vs-int type mismatch compilation errors

- feat(backend-php): emit per-opaque-type PHP stub class files under the configured `output` directory. Each opaque type with public methods gets its own `<TypeName>.php` declaring `public function` signatures, so static analysers and IDEs see the type's surface. Mirrors the existing `.rbs` stub pattern in `alef-backend-magnus`.
- feat(backend-rustler): emit idiomatic Elixir `.ex` wrapper modules for opaque types with methods. Each opaque type gets `defstruct [:ref]` plus per-method delegations to `TreeSitterLanguagePack.Native.<type>_<method>(obj.ref, ...)`, so Elixir callers get a typed module-based API instead of raw NIF call sites.

### Fixed

- fix(backend-napi): correctly resolve type names for opaque-vs-capsule types when used as parameters and return types. Capsule types (e.g. `Language` configured under `[crates.node.capsule_types]`) now reference the ecosystem-library type directly with no `Js` prefix; non-capsule opaque types (e.g. user-defined `Parser`/`Tree`/`Node`) get the `Js` prefix. Fixes "cannot find type `JsLanguage`" / "cannot find struct `Node`" errors when a project defines its own wrapper types alongside capsule passthrough.
- fix(backend-napi): for opaque types with any `&mut self` method, wrap the inner value in `Arc<Mutex<T>>` consistently — at struct construction (`Arc::new(Mutex::new(raw))` instead of `Arc::new(raw)`) and in every method body (acquire `.lock()` before the inner call). Resolves "no method named X found for `Arc<Mutex<T>>`" errors and Arc-double-wrap mismatches.
- fix(backend-pyo3): mirror the napi Mutex wrapping fix for opaque types with `&mut self` methods (same root cause, same fix shape).
- fix(backend-php): same Mutex wrapping fix as napi/pyo3 — propagate the computed `mutex_types` set through the codegen pipeline (`gen_opaque_struct_methods` → `gen_instance_method` → `php_wrap_return`) and emit `.lock().unwrap()` for method dispatch on `Arc<Mutex<T>>` opaque fields.
- fix(alef-codegen,core-to-binding): stop wiping cfg-gated trait-bridge fields to `Default::default()` in the core→binding conversion. The previous block in `gen_from_core_to_binding_cfg` forced `visitor: Default::default()` whenever the field's type referenced an opaque wrapper, dropping PHP/Node/WASM visitor objects between the binding builder and the core call. The field now flows through the normal `val.field.into()` conversion (or the `Option<Option<T>>` recursive path), so bindings that wrap visitors as their own opaque types forward them correctly.
- fix(alef-codegen,binding-to-core): forward trait-bridge Arc-wrapper fields instead of emitting `Default::default()` in the binding→core direction. The `is_opaque_no_wrapper_field` branch in `binding_to_core.rs` previously dropped fields like PHP's `Option<VisitorHandle>` (where `VisitorHandle { inner: Arc<core::VisitorHandle> }`). Added a `ConversionConfig::trait_bridge_arc_wrapper_field_names` slice populated from each backend's OptionsField bridges; when a field is in that list the conversion emits `val.{field}.map(|v| (*v.inner).clone())` (or the non-optional equivalent). Fixes 48 PHP visitor e2e failures where the builder set a visitor that was then silently dropped by `.into()`.
- fix(alef-backend-napi): soften the `find_options_field_binding` filter to accept cfg-gated trait-bridge fields whose names appear in `never_skip_cfg_field_names` (e.g. `visitor`). The previous `f.cfg.is_none()` predicate rejected the visitor field outright, causing the NAPI `convert` codegen to fall through to the plain `gen_function` path with no visitor parameter and a `From<JsConversionOptions>` impl that hardcoded `__result.visitor = Default::default()`. With the softened filter the options-field-bridge codepath activates: `convert` exposes `visitor: Option<Object>` and the JS visitor is woven into the core `ConversionOptions` before `core::convert()` is called.
- fix(alef-backend-wasm): make the Phase C `From<Wasm{options_type}>` visitor-forwarding post-process tolerant of indentation variations. The previous exact whitespace match (12-space indent only) silently failed to substitute `visitor: Default::default()` when the surrounding output used different indentation. The substitution now tries 12-, 8-, and 2-space indents so the forwarding (`val.visitor.map(|v| (*v.inner).clone())`) actually lands on every generated `From<Wasm…>` impl. Fixes 62 WASM visitor e2e failures.
- fix(alef-backend-java): align `gen_visitor_bridge` callback-chunking constants. Previously the constructor computed `num_chunks = CALLBACKS.chunks(10).count()` while the per-chunk method emitter used a local `const CHUNK_SIZE: usize = 5`. With 40 callbacks the emitter produced 8 `registerStubsN` methods but the constructor invoked only 4 of them, leaving callbacks 20–39 (`visit_button`, `visit_iframe`, `visit_mark`, `visit_video`, `visit_input`, etc.) NULL in the C struct. Lifted `CHUNK_SIZE = 5` to module scope and used it in both places. Fixes 28 Java visitor e2e failures.
- fix(alef-backend-magnus): emit `require "json"` once in the generated Ruby ext module init, and replace `.unwrap_or_default()` on the options-deserialization funcall with explicit `magnus::Error` propagation. Ruby's `Hash#to_json` only exists after `require "json"`; the gem didn't load the stdlib, so `funcall("to_json", ())` raised `NoMethodError`, the previous `unwrap_or_default()` swallowed it, and the entire options object reverted to `ConversionOptions::default()` (`include_document_structure: false`, no preprocessing, etc.) across 30+ Ruby e2e specs. The module-init `require "json"` makes `Hash#to_json` available, and the error-propagating `.map_err` surfaces any future failures instead of silently defaulting.

#### kreuzcrawl iteration (16-language e2e convergence)

- fix(alef-e2e/c): emit `char*` JSON-roundtrip path (instead of opaque-handle pattern) for FFI functions whose result type is `char*` such as `batch_scrape`. Driven by a new `raw_c_result_type` override threaded through `render_engine_factory_test_function`; the emitter writes `char* result = kcrawl_{fn}(...); kcrawl_free_string(result);` instead of the wrapper-constructor + `*_free` pattern. Previously generated `KCRAWLBatchScrape* result = kcrawl_batch_scrape(...)` referencing an undeclared type.
- fix(alef-e2e/c): parse `MOCK_SERVERS={"<id>":"<url>",...}` from the spawned mock-server's stdout in the generated `Makefile` test target and export `MOCK_SERVER_<UPPER_ID>` env vars for the test binary; the per-call URL setup now prefers `getenv("MOCK_SERVER_<UPPER_FIXTURE_ID>")` over the shared `MOCK_SERVER_URL/fixtures/<id>` fallback. Required for host-root fixtures (robots/sitemap) where a single port can only serve one robots.txt.
- fix(alef-e2e/c): for `expects_error` fixtures, emit a soft early-return when `kcrawl_crawl_config_from_json` or `kcrawl_create_engine` returns NULL rather than `assert(... != NULL)`. Tests asserting that config validation fails no longer abort the suite.
- fix(alef-e2e/dart): resolve `xxx[].yyy` traversal aliases on the full path before splitting around `[]`, so renamed sub-fields like `assets[].category` → `assets[].asset_category` route correctly through `field_resolver`. Previously the head was alias-resolved but the element segment kept the fixture-side name and missed the renamed accessor.
- fix(alef-e2e/dart): construct `FieldResolver` per file; emit `createCrawlConfigFromJson` setup and `createEngine(config: ...)` named-arg syntax to match the flutter_rust_bridge facade; route field accessors through `field_resolver.accessor("dart", ...)` for alias + nullability; `is_empty` matcher emits `anyOf(isNull, isEmpty)` to accommodate FRB nullable strings/lists.
- fix(alef-e2e/field_access): in the dart renderer, `PathSegment::Length` honours `prev_was_nullable` (emits `?.length`) and `PathSegment::ArrayField` only adds `!` when the field is itself optional. Prevents `Null check operator used on a null value` runtime failures on optional list/string fields.
- fix(alef-e2e/kotlin): set `PropertyNamingStrategies.SNAKE_CASE` on the generated test-suite `ObjectMapper` so Jackson deserializes the snake_case JSON wire format from the Rust FFI into the Kotlin record's camelCase fields (matches the Java mapper convention).
- fix(alef-e2e/kotlin): for fixtures with `has_host_root_route()`, emit `System.getProperty("mockServer.<fixture_id>", ...)` as the URL source so host-root fixtures route to their per-fixture listener (matches the Java emitter pattern).
- fix(alef-e2e/swift): for `expects_error` fixtures, hoist setup lines (config-from-JSON, create-engine) INSIDE the `do { } catch { }` block so config-validation errors are caught by the test. Previously the setup `try` was outside the do, so validation failures aborted the test instead of being asserted.
- fix(alef-e2e/swift): hoist `RustVec<T>` subscript temporaries into local variables (new `materialise_vec_temporaries` helper). swift-bridge's `RustVec<T>` subscript returns a `T.SelfRef` holding a raw pointer into the Vec's storage; without a local binding the Vec gets ARC-released before the ref is used, leaving a dangling pointer (observed as empty JSON-LD test data, then SIGSEGV under stress).
- fix(alef-e2e/php): templates/synthetic_assertion.jinja gains a trailing newline. Previously the skip-comment for unavailable fields ran into the following assertion on the same line, making the assertion part of the comment (PHPUnit reported affected tests as "risky" with no assertions, exiting non-zero).
- fix(alef-backend-csharp): filter `compute_handle_returned_types` to types with `has_serde == false`, so only truly-opaque handle types route through the constructor-wrapper pattern (`new T(IntPtr)`). Value records like `ScrapeResult`, `CrawlResult`, `MapResult`, `ContentConfig`, `BrowserConfig`, `CrawlConfig` stay on the JSON-roundtrip path (`*ToJson` → `JsonSerializer.Deserialize` → `*Free`). The earlier opaque-handle change had widened the wrapper-path to all named returns, breaking compilation of every serde-backed value-type return with `CS1729: 'T' does not contain a constructor that takes 1 arguments`.
- fix(alef-e2e/wasm): auto-import nested config types referenced as `handle_config_type` values (e.g. `WasmBrowserConfig`, `WasmAuthConfig`) even when they aren't direct `json_object` args. The codegen now includes `handle_config_type` in `all_options_types` for WASM and derives transitive nested types in `build_args_and_setup` so they're wrapped with proper class constructors at the test call site.
- fix(alef-e2e/wasm): in `templates/wasm/globalSetup.ts.jinja`, parse `MOCK_SERVERS={...}` from the spawned mock-server's stdout and export per-fixture `MOCK_SERVER_<UPPER_FIXTURE_ID>` env vars so host-root fixtures (robots/sitemap) reach their dedicated listener instead of the shared base port.
- fix(alef-e2e/brew): emit `${MOCK_SERVER_<UPPER_FIXTURE_ID>:-${MOCK_SERVER_URL}/fixtures/<id>}` for `mock_url` args so host-root fixtures route correctly when the mock-server allocates a per-fixture listener; the generated `*.sh` tests fall back to the shared `MOCK_SERVER_URL/fixtures/<id>` form for fixtures without isolation.
- fix(alef-e2e/brew): when fixture `input.config` is non-empty, append `--config '<minified json>'` to the generated CLI command so the kreuzcrawl CLI honors fixture-specified config (e.g. `respect_robots_txt: true`, validation fixtures). Pairs with the kreuzcrawl-side `--config` flag added to `scrape`/`crawl`/`map`.

## [0.15.51] - 2026-05-13

### Fixed

- fix(backend-java): emit `@JsonProperty("snake_case_name")` on request type fields where the Java camelCase name differs from the original snake_case IR name; adds `import com.fasterxml.jackson.annotation.JsonProperty` automatically when needed
- fix(e2e-elixir): streaming assertions now use `Enum.at(choices, 0)` instead of invalid `choices[0]` bracket access on Elixir lists
- fix(e2e-elixir): smoke test fallback api_key uses `"test-key"` instead of `nil` when no env var is set, preventing NIF rejection of nil for required String parameter
- fix(wasm): optional-enum getters return `Option<String>` via `to_api_str()` instead of raw C-style enum discriminant; optional `Vec<Struct>` getters return `Option<js_sys::Array>` for correct JS element access
- fix(e2e): mock server serves `data: [DONE]\n\n` SSE response for streaming fixtures with empty `stream_chunks` array, preventing Python SSE parser from hanging

## [0.15.50] - 2026-05-12

### Removed

- chore: drop Gleam language backend, codegen, scaffold, publish support, and `Language::Gleam` enum variant

## [0.15.49] - 2026-05-12

### Added

- feat(core,backends): `TraitBridgeConfig` gains `context_type` and `result_type` fields, plus a `ResolvedCrateConfig::bridge_associated_types()` helper that aggregates them across all configured bridges. Backends use this to skip generic record/POJO codegen for the bridge's associated types instead of hardcoding literal `"NodeContext"` / `"VisitResult"` checks against h2m's HtmlVisitor types. Any project with a callback-style trait can now wire its own context/result types through the bridge codegen.

### Fixed

- fix(codegen): scope cfg-gated trait-bridge field detection to fields whose type references an opaque wrapper (per `cfg.opaque_type_names`). The previous logic in `gen_from_core_to_binding_cfg` + the struct emitters forced `Default::default()` / `#[serde(skip)]` for any cfg-gated field listed in `never_skip_cfg_field_names`, which incorrectly defaulted regular convertible fields like `metadata: HtmlMetadata` on `ConversionResult`. Restores `result.metadata.document.*` access in the Python binding (h2m).
- fix(backend-pyo3,backend-wasm): drive trait-bridge post-process from `core_import` instead of hardcoded crate names. The pyo3 visitor-fallback rewrite and the wasm `From<Wasm*>` impl rewrite previously matched the literal string `html_to_markdown_rs`, breaking codegen for any other project using `bind_via = "options_field"`. Both now build their search/replace patterns via the configured `core_import` plus the per-bridge `type_alias` / `options_type` / `resolved_options_field`. The wasm rewrite also no longer relies on a hardcoded anchor field (`strong_em_symbol`) to scope the From-impl search — it now finds the impl block by its full `impl From<Wasm{options_type}> for ...` header.
- fix(wasm-backend): WASM trait bridge constructor now correctly reads the JS object's `name` property as a plain string. The previous implementation called `dyn_into::<js_sys::Function>()` on the value returned by `Reflect::get(&js_obj, "name")`, which always failed because `name` is a string property, not a method — causing the chain to fall through to `unwrap_or_else(|| "wasm_bridge".to_string())` unconditionally. Fixed by removing the intermediate `dyn_into::<Function>` step and calling `.as_string()` directly on the `JsValue` returned by `Reflect::get`.
- fix(wasm-backend): `build_wasm_arg` no longer uses Rust `{:?}` Debug formatting for non-primitive JS bridge arguments. The previous fallback emitted binary debug repr for `&[u8]` parameters (e.g. `[72, 101, 108, 108, 111]` instead of a `Uint8Array`) and Rust field syntax for complex types, both of which are unrecognisable at the JS call site. Fixed: `TypeRef::Bytes` now emits `js_sys::Uint8Array::from(...)` for correct typed-array interop; all remaining complex types (`Named`, `Vec`, `Map`, etc.) now use `serde_wasm_bindgen::to_value(...)` so they arrive as plain JS objects.
- fix(wasm-backend): async WASM trait bridge methods now correctly await the `Promise` returned by the JS function. `func.apply()` on a JavaScript `async` function returns a `Promise` object, not the resolved value; the previous implementation treated it as the final result and called `.as_string()` on the `Promise` itself, so all async bridge methods silently returned the default value. Fixed by casting via `dyn_into::<js_sys::Promise>()` and awaiting with `wasm_bindgen_futures::JsFuture::from(promise).await`.
- fix(alef-e2e/swift): `[].` traversal element accessor now resolves field aliases before computing the Swift method name (e.g. `assets[].category` → `assetCategory()` not `category()`); enum elements now call `.toString()` not `.to_string().toString()`.
- fix(alef-backend-dart): add `rust_input: crate` to the generated `flutter_rust_bridge.yaml` — omitting it caused `flutter_rust_bridge_codegen generate` to panic.
- fix(alef-backend-go): filter `ffi_skip_methods` in trait bridge generation so FFI-incompatible methods are not emitted in the Go C-VTable.
- fix(alef-backend-java): add a post-generation line-length wrapping pass to keep Checkstyle happy on compound annotation and call-argument lines.
- fix(alef-backend-napi): skip opaque-struct methods whose return type is a capsule type; the capsule shim uses `napi_create_external` directly, so no wrapper class is emitted for the return type and the method codegen path would reference a nonexistent type.
- fix(alef-backend-rustler): refactor method-call rendering to use a single `core_path` template variable instead of separate `core_import`/`struct_name`, fixing static method dispatch.
- fix(alef-e2e/kotlin): resolve `options_type` across language overrides (csharp/c/go/php/python) so Kotlin e2e tests pick up the correct options type without a redundant kotlin-specific override.
- fix(alef-backend-napi): napi capsule passthrough is now runtime-compatible with the `tree-sitter` npm package. The generated `getLanguage`-style shim now uses raw `napi_create_external` (the previous `napi::bindgen_prelude::External::new()` produced a wrapper that `Napi::Value::As<External<T>>` did not recognise), optionally calls `napi_type_tag_object` with a configurable GUID, and the property name is configurable (default `__parser`, set `property_name = "language"` to satisfy `node-tree-sitter`'s `UnwrapLanguage`). `NodeCapsuleTypeConfig` gains two optional fields: `property_name` (default `"__parser"` for back-compat) and `type_tag = { lower = "0x...", upper = "0x..." }`. Verified end-to-end in tree-sitter-language-pack: `getLanguage("python")` round-trips through `new Parser().setLanguage(lang).parse(...)` and yields the correct AST. Method-on-opaque-type capsule path remains a known limitation.

## [0.15.48] - 2026-05-12

### Changed

- chore(alef-core,alef-backend-php,alef-e2e): make `ResolvedCrateConfig::serde_rename_all_for_language` the single source of truth for per-language JSON wire casing. Flipped registry defaults so PHP, Kotlin, Swift, Dart return `camelCase` (matching their idiomatic variable conventions); Python/Ruby/Go/Elixir/R/Rust/Gleam/Zig/C/FFI stay `snake_case`. PHP backend's hard-coded `serde(rename_all = "camelCase")` now reads from the registry via a new `lang_rename_all` parameter threaded through `gen_php_struct`. Replaced `alef-e2e::codegen::normalize_json_keys_to_snake_case(value)` with `transform_json_keys_for_language(value, wire_case)` accepting any serde rename strategy; existing rust/c/kotlin callers pass `"snake_case"`, java codegen's `fromJson(...)` emission now transforms fixture keys to `"camelCase"` to match Jackson `@JsonProperty`, and php codegen's typed `from_json(json_encode([...]))` path uses `json_to_php_camel_keys` (also camelCase wire). This eliminates the band-aid "everything-to-snake" pre-pass and lets each binding receive JSON in the wire case its serde wrapper actually expects.

### Fixed

- fix(alef-e2e/c,alef-e2e/swift,alef-e2e/zig,alef-e2e/dart,alef-backend-zig): five codegen bugs that broke c/swift/zig/dart e2e tests. (1) c.rs: `render_chat_stream_test_function` redeclared `mock_base` for smoke+mock fixtures (those with `api_key_var`) where the env-fallback prologue already emits `api_key`/`base_url_buf` — caused "redefinition of `mock_base`" compile errors on `test_smoke_streaming_openai`. Now takes `api_key_var` and reuses the prologue's `api_key`/`base_url_buf` (matching the non-streaming client-factory path). (2) swift.rs: streaming fixtures fell through to the JS-style streaming_assertions fallback (`chunks.map((c: any) => ...).join('')`, `chunks.length`, `undefined`) because `collect_snippet` returns `None` for swift (swift-bridge doesn't yet expose a typed `chatStream` async sequence). Now emits an `XCTSkipIf` stub when the collect snippet is unavailable so the test target compiles. (3) zig.rs: `json_path_expr` didn't handle `[N]` indexed-segment notation, so `results[0].relevance_score` resolved to `object.get("results[0]")` instead of `.array.items[0]`; greater_than/less_than against fractional values (e.g. `0.9`) emitted `.integer` accessor causing "fractional component prevents float value '0.9' from coercion to type 'i64'" — now branches on whether the assertion value is a float and uses `.float`. (4) zig.rs: `build_args_and_setup` ignored per-call `extra_args` overrides, so `list_files()` / `list_batches()` invocations dropped the trailing optional `query: ?[]const u8` parameter — added merge logic mirroring go/python/swift. streaming_assertions deep-path for `tool_calls[N].xxx` returns None for zig (zig stores chunks as JSON strings, not typed records). (5) alef-backend-zig/opaque_handles.rs: `emit_method_param_conversion` didn't handle `Option<NamedStruct>` parameters — `dupeZ(u8, query)` requires a non-optional slice but the wrapper signature is `?[]const u8`, triggering "cannot convert optional to payload type" at `list_files`/`list_batches`. Now allocates conditionally (`if (query) |v| try dupeZ(...) else null`) and emits a null-aware `_from_json` call so `null` callers see `null` C handles. dart codegen: `field_to_dart_accessor` adds `!` after bracketed names so `choices![0]` / `toolCalls![0]` compile against `List<T>?`; `count_min`/`min_length`/`count_equals`/`max_length` use `?.length ?? 0`; `result_is_simple` config now honored so bytes-returning calls (speech, file_content) assert against the `Uint8List` directly rather than non-existent `.audio`/`.content` accessors.
- fix(alef-e2e/kotlin): test files emitted bareword imports (`import ChatCompletionRequest`, `import LiterLlm`) when the binding `class_name` wasn't fully-qualified, which Kotlin rejects with "Unresolved reference" since the kotlin binding emits typealiases at `kotlin_pkg_id` (e.g. `com.github.kreuzberg_dev`) and the test files live one package deeper at `<kotlin_pkg_id>.e2e` (parent-package symbols don't import implicitly). Now falls back to `kotlin_pkg_id` as the binding package when `import_path` is empty, producing `import com.github.kreuzberg_dev.ChatCompletionRequest` / `import com.github.kreuzberg_dev.LiterLlm`. Same fallback applied to options-type imports and CrawlConfig. Resolves the import layer in liter-llm kotlin e2e (remaining errors are coroutine `suspend fun` wrappers and nullable `Optional` accessors that need follow-up kotlin codegen work).
- fix(alef-e2e/java,alef-e2e/go): codegen bugs that broke streaming e2e tests for the Java and Go bindings. Java: (1) the `collect_snippet` emitted `new ArrayList<ChatCompletionChunk>()` but never imported `ChatCompletionChunk`, so the type erased to `Object` and downstream `c.choices()/ch.delta()` method calls failed with "cannot find symbol" — added an FQN import when any fixture is streaming. (2) `is_streaming` only checked `is_streaming_mock()`, so the `empty_stream` fixture (`stream_chunks:[]`) skipped the collect snippet and left `chunks` undefined at the assertion site — broadened to fire when any assertion references a streaming-virtual field, matching the kotlin/swift/php/python/elixir/typescript pattern. (3) The `not_error` arm for `byte[]` results emitted `Assertions.assertNotNull(...)` but only the static-import form (`assertNotNull`) is in scope — switched to the bare name. Go: (1) same `is_streaming` broadening so `empty_stream` drains the channel into `chunks`. (2) `needs_assert` for the file-level testify import treated streaming-virtual fields as `field_valid=false` (they don't resolve against the result type), so streaming-only files omitted the `github.com/stretchr/testify/assert` import even though every assertion called `assert.X` — now field-valid is `true` when the assertion's field is a streaming-virtual reference on a streaming fixture. (3) `finish_reason` accessor returned `*last.Choices[0].FinishReason` without casting `FinishReason` (a `type FinishReason string` alias) back to `string`, causing E0308-style type mismatches at the assertion site — added an explicit `string(*...)` cast. (4) `tool_calls` accessor used `*ch.Delta.ToolCalls` but the Go binding declares `ToolCalls []StreamToolCall` (slice, not pointer); removed the bogus dereference and changed the IIFE return type to `[]pkg.StreamToolCall` so deep-path accessors like `tool_calls[0].function.name` index into the typed slice. (5) deep-path streaming-virtual equals on Go binding pointer-typed leaves (`StreamFunctionCall.Name *string`) now wraps the expression in a nil-safe deref IIFE so the comparison succeeds against the plain-string assertion value. Drops liter-llm go e2e from a build failure to 161/161 passing; the corresponding fix in `liter-llm/alef.toml` adds the missing `[crates.e2e.calls.chat_stream.overrides.go]` block so `chat_stream` requests are deserialised into the typed Go struct (the binding signature is `func (h *DefaultClient) ChatStream(req ChatCompletionRequest)`).
- fix(alef-e2e/php): four codegen bugs that broke streaming e2e tests for PHP bindings. (1) The `chunks` streaming-virtual-field accessor returned the bareword `chunks` for every language; PHP parses bareword identifiers as constant references, so generated code like `count(chunks)` triggered "Undefined constant" errors — added a PHP arm that emits `$chunks`. (2) `is_streaming` in `php.rs` only checked `is_streaming_mock()`, so fixtures like `empty_stream` (with `stream_chunks:[]`) skipped the collect snippet and left `$chunks` undefined at assertion sites — broadened to fire when any assertion references a streaming-virtual field, matching the elixir/python/kotlin/typescript pattern. (3) PHP binding's streaming method returns a JSON string of chunks (PHP cannot expose Rust iterators via ext-php-rs), but the collect snippet emitted `iterator_to_array(...)` which fails with `TypeError: Argument must be Traversable|array, string given` — now branches on `is_string($result)` to `json_decode` the string into stdClass objects, retaining `iterator_to_array` as a fallback for a future binding that exposes a real iterator. PHP `stream_complete` / `finish_reason` / `tool_calls` accessors and the deep-path tail renderer switched from camelCase to snake_case so they match the JSON wire-format property names on the decoded stdClass values. (4) Streaming fixtures whose only assertion is `not_error` (e.g. `stream_multiple_choices`) produced an empty assertions body, which PHPUnit flags as "risky" — emit a baseline `$this->assertTrue(is_array($chunks), 'expected drained chunks list')` so the test records a real assertion. Drops liter-llm php e2e from 12 errors to 0; all 161 tests pass with 308 assertions.
- fix(alef-e2e/streaming*assertions): three rust-codegen bugs that produced uncompilable e2e tests. (1) `is_streaming_virtual_field` matched the tail-suffix of any field whose chars-after-`root.len()` started with `.` or `[` without first checking `field.starts_with(root)`, so `choices[0].finish_reason` matched root `tool_calls` (chars 10+ begin with `.finish_reason`) and falsely triggered streaming-mode codegen on non-streaming fixtures — added the `starts_with` guard. (2) Rust `collect_snippet` left the chunks vector as `Vec<Result<ChatCompletionChunk, *>>`, so subsequent accessors like `chunks.iter().map(|c| c.choices...)`failed E0609 ("no field`choices`on`&Result<...>`") — now chains `.into_iter().map(|r| r.expect("stream item failed")).collect()`to yield`Vec<ChatCompletionChunk>`. (3) Rust `finish_reason`accessor used`.as_deref()`on`Option<FinishReason>`(an enum, not`String`) which fails E0599 — switched to `.as_ref().map(|v| v.to_string())`. (4) Rust deep-path `tool_calls[0].function.name`chained naive field access through`StreamToolCall { function: Option<StreamFunctionCall>, ... }`causing E0609 — added`render_rust_tool_calls_deep` helper that emits Option-aware chaining (`.nth(0).and_then(|x| x.function.as_ref()).and_then(|x| x.name.as_deref()).unwrap_or("")`). Drops liter-llm rust e2e from 266 compile errors to 0; all 152 tests pass.

### Added

- feat(alef-core,alef-backend-ffi): `ffi_skip_methods` on `TraitBridgeConfig` filters individual methods from the FFI vtable struct and `impl Trait for KreuzbergXBridge`. Methods whose signatures can't traverse the C FFI boundary (e.g. `Option<&dyn Trait>` returns like `DocumentExtractor::as_sync_extractor`) are dropped, falling back to the trait's default implementation. Configured per-bridge in `alef.toml`.
- feat(alef-backend-swift): trait-bridge support for trait methods that take excluded internal types by reference (e.g. `Renderer::render(&self, doc: &InternalDocument)`). Outbound trampolines now deserialise the JSON-bridged `String` parameter back to the source type using its fully-qualified Rust path (resolved via `type_paths`); inbound `impl Trait` blocks emit owned return types (`String` rather than the unsized `str`) so methods returning `Result<String>` compile. Lets the swift backend bridge `Renderer` alongside the other plugin traits.

### Fixed

- fix(alef-backend-extendr): trait-bridge codegen now compiles for R bindings. Six issues were fixed end-to-end: (1) generated `#[extendr]` registration/unregister/clear functions used `Result<(), String>` which is rejected as `extendr_api::Result<T>` only takes one generic parameter — switched to `std::result::Result<(), String>`; (2) `.map_err()` on `registry.write()` for `parking_lot::RwLockWriteGuard` (infallible) — dropped; (3) `Robj` is `!Send`/`!Sync` (raw `*mut SEXPREC`) but `Plugin: Send + Sync` is required — emit `unsafe impl Send + Sync` for the bridge struct with a SAFETY comment noting R's single-threaded contract; (4) async trait methods captured `Robj` directly into `tokio::spawn_blocking` closures which then failed the `F: Send` bound (RFC 2229 disjoint captures grab `r_obj.0` not the whole wrapper) — introduced a `SendRobj` newtype with `into_inner(self)` so the closure captures the whole `Send` wrapper; (5) sync/async complex-return templates called `serde_json::to_string(&val)` against `Robj` (no `Serialize` impl) — switched to expecting JSON-encoded `&str` payloads from R callbacks (callers serialise via `jsonlite::toJSON` on the R side); (6) `build_extendr_arg` had no branch for `TypeRef::Named` params (e.g. `Renderer::render(&self, doc: &InternalDocument)`) — now serialises Named-typed args to JSON strings before crossing the bridge. Unblocks removing `exclude_languages = ["r"]` from `OcrBackend`, `PostProcessor`, `Validator`, `EmbeddingBackend`, `DocumentExtractor`, and `Renderer` trait_bridges in downstream `alef.toml` configurations.
- fix(e2e/rust): skip primitive / std element_types (`String`, `str`, integer/float types, `bool`, `char`) when emitting `use {module}::{T};` statements. Fixtures that declare `element_type = "String"` were emitting `use kreuzberg::String;`, which triggered E0432 ("unresolved import `kreuzberg::String`"). Primitives are already in scope via the Rust prelude.
- fix(e2e/python,e2e/elixir): broaden `is_streaming` detection to also fire when an assertion references a streaming-virtual field (e.g. `empty_stream` has `stream_chunks:[]` so `is_streaming_mock()` returns false, but the fixture still asserts on `chunks`/`stream_content` which need the collect snippet). Mirrors the kotlin Phase-A pattern.
- fix(e2e/typescript,e2e/wasm): broaden `is_streaming` detection in the typescript test renderer (shared by both node and wasm e2e codegen) to also fire when an assertion references a streaming-virtual field. Previously the `empty_stream` fixture (which has `stream_chunks:[]`) skipped the `const stream = await client.chatStream(...)` binding and the `for await (_chunk of stream) chunks.push(_chunk)` collect block, leaving `chunks` undefined at the assertion site (`ReferenceError: chunks is not defined`). Mirrors the same fix already applied to python/elixir/kotlin.
- fix(codegen): backfill ffi_skip_methods field at TraitBridgeConfig construction sites
- fix(alef-backend-ffi): stop duplicating the `Plugin` super-trait impl in `gen_ffi_trait_impl`. The orchestrator (`trait_bridge::mod.rs`) already emits it before calling the trait impl path; emitting it again triggered E0119 ("conflicting implementations of trait `Plugin`") for every FFI trait bridge (`DocumentExtractor`, `Renderer`, `OcrBackend`, `PostProcessor`, `Validator`, `EmbeddingBackend`).
- fix(alef-backend-dart): skip static / associated methods (e.g. `T::default()`, `T::new()`) on opaque impl blocks. The emitter previously generated `pub fn name(&self) -> T { self.inner.name() }` for every method including statics, but calling an associated function with method syntax (`self.inner.default()`) trips E0599 ("this is an associated function, not a method").
- fix(alef-backend-dart): emit explicit conversion wrappers for opaque method returns where the core type doesn't match the FRB-widened bridge type. `&str` → `String`, `&Path` → `String` via `to_string_lossy`, `&[&str]` → `Vec<String>`, and `i32`/`u32`/`usize` → `i64`, `f32` → `f64` per FRB's primitive widening contract. Previously the body emitted bare `self.inner.method()` which produced E0308 ("mismatched types") for these returns.
- fix(alef-backend-dart): filter out non-serde types in the `create_<T>_from_json` shim emission. Types with `has_serde = false` in the IR (e.g. `MergedChunk`, `ResolvedStyle`, `CharShape`, `OdtProperties`, `OcrCacheStats`) can't be deserialised via `serde_json::from_str::<CoreT>` — emitting the shim triggered E0277 ("the trait bound `T: Deserialize` is not satisfied") at compile time.
- fix(alef-backend-dart): emit `cfg = None` on per-field assignments inside `From<CoreT> for MirrorT` impls. The dart bridge crate enables `features = ["full"]` on its kreuzberg dep, so every core-side cfg-gated field is present at compile time. Emitting `#[cfg(feature = "X")]` would gate on the dart crate's own (undefined) features — evaluating to false and leaving the struct literal missing fields like `code_intelligence`, `extracted_keywords`, `html_options`, etc.
- fix(alef-backend-dart): trait-bridge wrapper `supported_mime_types` now returns `&[&str]` instead of `Vec<String>` to match `DocumentExtractor::supported_mime_types(&self) -> &[&str]`. The Dart closure produces an owned `Vec<String>` that's materialised into `&'static [&'static str]` via `Box::leak` per call (same pattern as napi/pyo3 trait bridges, `alef-codegen::generators::trait_bridge::gen_method`).
- fix(alef-backend-swift): skip static / associated methods in `emit_extern_block_for_type_methods` and `emit_type_method_shims`. The shim signature is `pub fn type_method(client: &T)` and the body calls `client.0.method()` — calling associated functions like `T::default()` via the receiver trips E0599.
- fix(alef-backend-swift): handle `TypeRef::Path` returns with `.to_string_lossy().into_owned()` (instead of `.to_string()`, which doesn't exist on `&Path`) and `Vec<String>` + `returns_ref` returns (`&[&str]`) with `.iter().map(|s| s.to_string()).collect()` so methods like `TessdataManager::cache_dir() -> &Path` and `DocumentExtractor::supported_mime_types() -> &[&str]` compile.
- fix(alef-backend-zig): emit async Rust functions as synchronous Zig wrappers. The `!f.is_async` filter in the function-emission loop was re-introduced by the opaque-handle codegen commit, causing `scrape`, `crawl`, and `map_urls` (all `is_async: true` in the IR) to disappear from generated `kreuzcrawl.zig`. The Zig C FFI wraps every async function with `block_on`, so from Zig's perspective all functions are synchronous — the `is_async` flag is irrelevant at the Zig ABI level.
- fix(alef-e2e/dart): handle `[].` array traversal in assertion rendering. Field paths like `links[].url` and `links[].link_type` previously generated invalid Dart syntax (`links()[0].url()` type subscripts that fail `dart format`). Now generates correct `array.any((e) => e.field.toString().contains(val))` expressions for `contains`, `contains_all`, `not_contains`, and `not_empty` assertion types.

- fix(alef-backend-dart): emit `pub(crate) inner: {ty.rust_path}` on the FRB opaque wrapper struct instead of `{source_crate}::{name}`. The naive form only resolves for types re-exported at the crate root; `HwpxExtractor` lives at `kreuzberg::extractors::HwpxExtractor`, `TessdataManager` at `kreuzberg::ocr::TessdataManager`, etc., so the generated bridge failed to compile with `E0425: cannot find type X in crate kreuzberg` for ~12 opaque handles. Falls back to `{source_crate}::{name}` only when `rust_path` is empty. Template `rust_opaque_wrapper_struct.jinja` now takes `inner_path` directly.
- fix(alef-backend-dart): emit `pub use {qualified_path}` at the crate root for excluded types referenced by required trait-bridge method signatures (e.g. `InternalDocument`). FRB strips module paths from `frb_generated.rs` and resolves bare names via `use crate::*`, so these types must be visible at the crate root. The walk filters `trait_source.is_some()` and `has_default_impl` methods to match `emit_trait_bridge`'s required-method filter — avoids re-exporting traits referenced only by default-impl methods (e.g. `SyncExtractor` via `as_sync_extractor`), which would otherwise trigger `E0782: expected a type, found a trait` in FRB-generated closure types.
- fix(alef-backend-swift): gate `create_<type>(api_key, base_url)` constructor emission on `client_constructor_body` override presence in `alef.toml`. The default `(api_key, base_url) -> Result<T, String>` template only matches liter_llm-style clients; for plugin types like `HwpxExtractor::new()` and utilities like `TessdataManager::new(Option<PathBuf>)` it produced calls that didn't match the real Rust signature and broke compilation. Opaque types without an override are returned by Rust APIs, not constructed in Swift, so omitting the shim is the correct default.
- fix(alef-backend-java): filter `api.errors` variants whose generated class names collide with the FFI infrastructure exception names (`InvalidInputException`, `ConversionErrorException`). Both code paths in `JavaBackend::generate` were writing files at the same target paths; for `InvalidInputException` this produced malformed Java with a duplicate constructor block appended after the closing brace. Keeps the hardcoded infrastructure version canonical and drops the conflicting `gen_java_error_types` emission.
- fix(alef-codegen,backends): stop stripping cfg-gated struct fields from the binding. Previously `gen_struct_with_per_field_attrs`/`gen_struct_with_rename` and the napi/wasm backends dropped any field with `#[cfg(...)]`. The result was that `metadata: HtmlMetadata` and `images: Vec<InlineImage>` on `ConversionResult` (and similar feature-gated fields on other crates) disappeared from the binding struct even when the binding crate enabled the same feature. Binding crates already mirror the core crate's features via their `Cargo.toml`, so the cfg gate is redundant — the field is always present at compile time when the feature is on. The conversion sites and constructor helpers are still cfg-aware for trait-bridge `bind_via = "options_field"` fields (force-restored via `never_skip_cfg_field_names`), which carry binding-wrapper types that need `#[serde(skip)]` and `Default::default()` initialization. Restores `result.metadata` / `result.images` access on Python/Node/PHP/WASM bindings.
- fix(alef-codegen): `constructor_parts_with_renames` and `config_constructor_parts_inner` in `shared.rs` now emit a default expression (`None` for `Option<T>` fields, `Default::default()` otherwise) for cfg-gated fields in the struct-literal assignment list, while continuing to omit them from the parameter list. Completes the cfg-gated field story: the binding struct keeps the field (per the prior `gen_struct_*` fix), but the caller can't supply trait-bridge wrapper values, so the constructor body fills them with a default. Without this, Python/WASM/PHP bindings hit `E0063 missing field visitor / metadata / images` because the struct now requires the fields the constructor wasn't initializing. Mirrors the same defaulting pattern already used in `gen_struct_default_impl`. The php `Named` constructor in `alef-backend-php/src/gen_bindings/types.rs` (manual `param_init`) was updated to match.
- fix(alef-backend-pyo3): pass the visitor kwarg through to `_rust.<fn>` in generated api.py wrappers. For `bind_via = "options_field"` bridges, the python wrapper was stuffing the visitor into `options.visitor` via `_to_rust_<snake>(opts, _visitor_override=visitor)` but never forwarding `visitor=` to the underlying Rust function, which is what `gen_bridge_field_function` actually reads to wrap the bridge handle. The visitor was silently dropped; user-supplied visitor implementations had no effect.

### Added

- feat(e2e/swift): wire `StreamingFieldResolver` into swift codegen. Adds `"swift"` arms to `StreamingFieldResolver::accessor` (all seven virtual fields: `chunks`, `chunks.length`, `stream_content`, `stream_complete`, `no_chunks_after_done`, `tool_calls`, `finish_reason`) and `collect_snippet` (emits `var chunks: [ChatCompletionChunk] = []` / `for try await _chunk in result { ... }` drain loop). Adds `"swift"` to `render_deep_tail` field segment dispatch (snake_case method calls). Wires `is_streaming` detection in `render_test_method` and streaming-virtual field interception in `render_assertion` before the field-not-available guard. Eliminates ~30 `// skipped: field '...' not available on result type` comments from `StreamingTests.swift` and `SmokeTests.swift`. Adds 7 swift-specific unit tests.
- feat(alef-e2e/field_access): support user-typed array indices in fixture field paths — `choices[0].message.content` now parses as `Segment::ArrayField{name:choices,index:0}` → `Field(message)` → `Field(content)` instead of being treated as a literal dot-key. Wired through every per-language renderer so explicit indices are emitted correctly: Rust/Python/Go/Zig/C#/Swift/TS/PHP/Ruby/Dart use bracket notation `[N]`, Java emits `.get(N)`, Kotlin emits `.first()` for index 0 or `.get(N)` for others, Elixir emits `Enum.at(expr, N)`. Config-registered array fields without explicit `[N]` continue to default to index 0.
- feat(alef-backend-dart/frb-stream): implement FRB v2 `StreamSink<T>` streaming for the Dart backend. Sanitized methods with a matching `[[adapters]] pattern = "streaming"` entry in `alef.toml` now emit `#[frb] pub fn method_name(&self, params..., sink: StreamSink<ItemType>)` in the Rust bridge crate instead of being skipped. FRB v2 generates a `Stream<ItemType>` on the Dart side automatically. The generated Cargo.toml gains `futures-util = "0.3"` when any streaming adapter is configured. `supports_streaming` is now `true` in `DartBackend::capabilities()`.
- feat(alef-e2e/dart): streaming fixture support in the Dart e2e codegen. Fixtures with `mock_response.stream_chunks` now emit `await _client.method().toList()` instead of `await _client.method()`. Streaming assertions: `count_min` on `chunks` emits `expect(result.length, greaterThanOrEqualTo(N))`, `equals` on `stream_content` concatenates deltas and compares, error cases use `.toList()` in the `expectLater` lambda.
- feat(alef-backend-rustler): emit `#[rustler::nif] pub fn <snake_type>_from_json` shims that deserialize a JSON string via `serde_json::from_str`, convert to the binding type, and return `Result<TypeName, String>`. Enables Elixir e2e tests to deserialize typed request objects from JSON strings.
- feat(alef-e2e/swift): `options_via = "from_json"` support in Swift e2e test generation. When a call override sets `options_via = "from_json"` and `options_type = "<TypeName>"`, `build_args_and_setup` now emits `let _argName = try typeNameFromJson("<fixture-json>")` setup lines for `json_object` args instead of a `XCTSkipIf(true, "swift: json_object request construction requires options_via configuration ...")` skip stub. Handles `field = "input"` (entire fixture input is the request) correctly via `resolve_field`. Eliminates ~140 skip stubs in liter-llm swift e2e tests.
- feat(alef-backend-swift/gen_rust_crate): emit `{type_snake}_from_json(json: String) -> Result<TypeName, String>` free-function shims and extern "Rust" declarations for serde-enabled non-opaque types that appear as method parameters on opaque types. `collect_serde_param_types` scans both free-function params and `api.types[].methods[].params` to find these types. Enables `chatCompletionRequestFromJson`, `embeddingRequestFromJson`, etc. in the RustBridge Swift module.
- feat(alef-e2e/dart): dart assertion renderer reaches parity with rust/kotlin/go — all 24 fixture assertion types handled (was 5). Adds `render_dart()` and `render_dart_with_optionals()` to `FieldResolver` with proper per-segment camelCase conversion (FRB v2 convention), wired into `accessor()` and `render_accessor()` dispatch via a new `"dart"` arm. Replaces the single-pass `snake_to_camel` field path hack with `field_to_dart_accessor` for correct multi-segment path conversion.
- feat(e2e/streaming): support deep-nested paths in `StreamingFieldResolver` — `tool_calls[N].function.name`, `tool_calls[N].id`, and similar paths now resolve against the flat-mapped collected tool-calls list instead of emitting `// skipped:` comments. `split_streaming_deep_path` splits the root virtual field from the tail, `parse_tail` tokenises the tail into `Index`/`Field` segments, and `render_deep_tail` applies per-language navigation conventions (bracket for Rust/Go/TS/Python/PHP/C#/Kotlin/Dart/Swift/Ruby/Zig, `.get(N)` for Java, `Enum.at(expr, N)` for Elixir, `.items[N]` for Zig ArrayList, `.first()` for Kotlin index-0, `.ID`/`.URL` initialism expansion for Go). Covered by three new snapshot tests: `deep_tool_calls_function_name_snapshot_rust_kotlin_ts`, `deep_tool_calls_id_snapshot_all_langs`, and `deep_tool_calls_function_name_snapshot_python_elixir_zig`.

### Fixed

- fix(alef-e2e/java,csharp): assertion type 'not_error' on byte[] result now emits assertNotNull(result) / Assert.NotNull(result) instead of skipping. Eliminates 10 skipped comments across speech/transcribe fixtures.
- fix(alef-e2e/kotlin): substitute the field name in 'skipped: field {f} not available' comments instead of leaking the literal {f} placeholder. (Phase D eliminates these comments entirely for streaming-virtual fields.)
- fix(alef-backend-pyo3): import `has_default` config DTOs from `.options` even when they expose `Self`-returning builder methods. The api.py import classifier built `return_type_names` by walking method return types (and their transitive field references), which pulled config types like `PackConfig` (with `from_toml_file -> PackConfig`) and `ProcessConfig` (with `default`/`with_chunking`/`all`/`minimal` builders) out of `options_type_names`. The generated api.py then imported them from `._native`, breaking `mypy --strict` for any caller passing the re-exported dataclass. Use the IR-level `is_return_type` flag alone (set by alef-extract only for direct free-function returns), removing the over-broad method/field walk. Fixes [#72](https://github.com/kreuzberg-dev/alef/issues/72).
- fix(alef-e2e/java): `not_contains` assertions now handle the plural `values: [...]` array (symmetric to `contains_all`) in addition to the singular `value`. Previously, a fixture using `not_contains` with `values` produced `result.contains()` (empty argument) — a Java compile error. The codegen now wraps a singular `value` into a one-element list, and the jinja template loops over `values_java` emitting one `assertFalse(...contains(val))` call per entry.
- fix(alef-codegen): cfg-gated fields restored via trait-bridge `bind_via = "options_field"` now emit `#[serde(skip)]` and are propagated through both `binding→core` and `core→binding` From impls. Previously, restoring such a field (e.g. `visitor: Option<VisitorHandle>` on `ConversionOptions`) produced uncompilable bindings: the struct derived `serde::{Serialize, Deserialize}` over a non-serde-compatible bridge handle type, and the `From<Core>` impls omitted the field, so `Self { ... }` initializers were missing it. Adds `never_skip_cfg_field_names` to `ConversionConfig`, populated from `trait_bridges` by each backend.
- fix(alef-backend-napi,php): apply the trait-bridge `never_skip_cfg_field_names` mechanism added in alef-codegen so the NAPI (Node) and PHP bindings keep `bind_via = "options_field"` fields (e.g. `visitor` on `ConversionOptions`) instead of dropping them with the rest of the cfg-gated fields. Mirrors the existing PyO3 backend behavior.
- fix(alef-backend-ffi,php): emit `*result` (Copy) for `TypeRef::Char` and `result.to_vec()` for `TypeRef::Vec/Map` slice returns in `gen_method_wrapper`, instead of `result.clone()` (clippy noop on `&char` and unsized slices). Also emit `.to_string()` rather than `.clone()` for `&str` params before moving into `spawn_blocking` closures (avoids E0521 borrow-escape).
- fix(pyo3): emit `error_to_py_err` directly in `.map_err()` (no redundant closure) — avoids clippy `redundant_closure` in generated bindings.
- fix(pyo3): api.py import order — stdlib `from typing import …` first, then `import _rust as _rust`, then capsule-type imports (e.g. `from tree_sitter import Language`), then locals.
- fix(pyo3): api.py wraps capsule-returning calls in `cast("Type", …)` for mypy strict mode; adds `cast` to the typing import only when needed.
- fix(pyo3): skip capsule types in `.pyi` stubs (they live in third-party packages); conditional `from typing import …` based on actual usage — avoids RUF100 for the old unconditional four-name import. Method stubs on opaque types (e.g. `LanguageRegistry.get_language`) also substitute `Any` for capsule-typed params/returns. Suppresses PYI029 on the data-enum class wrappers' `__str__`/`__repr__` — the pyo3 wrapper implements them via Display/Debug and callers rely on `str(value)` returning the serde tag.
- feat(alef-backend-napi): `capsule_types` support for raw-pointer passthrough into external tree-sitter libraries (e.g. the `tree-sitter` npm package). Types listed under `[crates.node.capsule_types]` are excluded from opaque `#[napi]` class emission; functions returning them instead emit a `#[napi]` shim that calls `value.into_raw()`, wraps the pointer in `env.create_external(ptr, None)`, and sets it as `__parser` on a returned `JsObject`. TypeScript declaration stubs emit `import type { T } from "module"` and use the ecosystem type name directly in function signatures.
- fix(pyo3): drop redundant `pass` body for empty-field types in generated `options.py` — `pass` after a docstring triggers ruff PIE790.
- fix(wasm): add `#[allow(clippy::should_implement_trait)]` to inherent `default()` factory method; renaming would change the JS-visible API.
- fix(java): marshal `Path` params via `.toString()` in opaque-type stream-method bodies — previously lumped with `String`/`Char`/`Json` whose template does not call `.toString()`, causing a Java type mismatch.

## [0.15.44] - 2026-05-12

### Added

- feat(alef-core/java): `[java] group_id` and `[java] artifact_id` config overrides for the Maven coords emitted by `alef-scaffold/java`. When unset, `java_group_id()` falls back to the Java package and `java_artifact_id()` falls back to the source crate name. Set these when the published Maven coords need to differ from the Java package / source crate name (e.g. crate `html-to-markdown-rs` publishes as artifactId `html-to-markdown` under groupId `dev.kreuzberg`).

### Fixed

- fix(alef-e2e/zig): drop the misleading `// Note: async functions not yet fully supported; treating as sync` comment from generated tests in all three emission paths (error, no-assertion, happy). The generated tests already exercise the real async code path via `tokio::runtime::block_on` in the FFI shim, so the comment was cosmetic noise.
- fix(alef-e2e/kotlin): remove the dead `call_overrides.is_none()` short-circuit that emitted `Assumptions.assumeTrue(false, "TODO: implement Kotlin e2e test")` for every fixture lacking a per-call override. The existing fallback resolves `function_name`/`result_var`/`args` from `call_config.function.to_lower_camel_case()` and the global `[e2e.call.overrides.kotlin]` block, so the real assertion renderer is now reachable for all kotlin fixtures. Also extend `client_factory` lookup to fall back to the global block (mirrors dart pattern), so per-call kotlin overrides don't lose the global `createClient` factory.
- fix(alef-e2e/dart): emit `expect(...)` assertion calls after the `await` in the non-HTTP test path. Previously the renderer dropped the result on the floor (`final result = await ...;` with no following assertions). Added `render_assertion_dart` helper handling `equals`, `field_equals`, `contains`, `not_null`, `not_error`; unknown assertion types emit a `// skipped:` comment as before.

## [0.15.43] - 2026-05-12

### Fixed

- fix(alef-backend-go): emit gofmt-clean output from the visitor templates — `visitor_preamble.jinja` now ends with a trailing newline (so `gofmt` doesn't insert a blank line between the import block and the first declaration), and `visitor_registry_block.jinja` aligns the `visitorRegistry`/`visitorIDCounter` field types so `gofmt` doesn't realign them. Without this, the alef-verify pre-commit hook fails because `gofmt` reformats the regen output and the resulting hash no longer matches the `alef:hash` header.

## [0.15.42] - 2026-05-12

### Fixed

- fix(alef-backend-go): prepend `C.` to all cgo function calls and the options struct type in the visitor helper template. Previously `convert_with_visitor_helper.jinja` emitted bare identifiers (`htm_conversion_options_from_json(...)`, `htm_options_set_visitor(...)`, `htm_convert(...)`, etc.) which Go treated as undefined package-level functions, breaking the entire Go binding with `undefined: htm_*` errors. Now matches the existing `binding.go` convention of `C.htm_*` for cgo symbol resolution.

## [0.15.41] - 2026-05-12

### Fixed

- fix(alef-backend-wasm): emit a real newline after the rustdoc block so `#[derive(Clone, Default)]` no longer ends up concatenated inside the `///` comment line. Previously every `Wasm*` binding struct silently lost its `Clone`/`Default` derives because the template's `{%- endfor %}` stripped the newline between rustdoc lines and the subsequent attribute, causing `error[E0277]: the trait bound 'WasmDocumentMetadata: Default' is not satisfied` (and many like it) at compile time.
- fix(alef-backend-pyo3): skip the `options.{visitor}` fallback in the convert wrapper when the bridge field is `#[cfg(...)]`-gated and therefore absent from the binding struct. Previously the fallback was inserted unconditionally and produced `error[E0609]: no field 'visitor' on type '&ConversionOptions'`.
- fix(alef-backend-napi): gate `find_options_field_binding` on the bridge field being present in the binding struct. When the core field is cfg-gated and stripped from the binding, fall back to the regular function generator instead of emitting an options-field bridge that references a missing field.

## [0.15.40] - 2026-05-12

### Fixed

- fix(alef-e2e/php): `not_contains` assertions now handle the plural `values: [...]` array (symmetric to `contains_all`) in addition to the singular `value`. Previously, a fixture using `not_contains` with `values` produced `assertStringNotContainsString(, $result)` — an empty-first-argument PHP parse error. The codegen now wraps a singular `value` into a one-element list, and the jinja template loops over `values_php` emitting one `assertStringNotContainsString` call per entry.
- fix(alef-backend-dart): use `From<MirrorT> for CoreT` conversion instead of `unsafe transmute` in opaque-type method bodies for types whose mirror layout differs from the core layout. The `has_layout_mismatch` detection now covers Duration, Path, Json (serde_json::Value → String, 32 vs 24 bytes), and non-identity primitive widening (e.g. u32 → i64, 4 vs 8 bytes). The transitive-closure computation (`compute_types_needing_from_impl`) is extended to process enum variant field types so that types like `SystemMessage` and `ToolChoiceMode` (reachable through `Message` and `ToolChoice` from `ChatCompletionRequest`) also get `From<MirrorT> for CoreT` impls. Additionally, `String` method parameters marked `is_ref` now correctly pass `&param_name` to core methods that take `&str`. `Bytes`-returning methods chain `.to_vec()` to convert `bytes::Bytes` to `Vec<u8>`.
- fix(alef-backend-dart): emit `create_<snake>_from_json(json: String) -> Result<TypeName, String>` free bridge functions for all non-opaque mirror struct types. These allow e2e tests to construct typed request objects from JSON without manually populating every field — required because FRB bridge methods use named parameters that cannot accept bare JSON strings. FRB generates the corresponding `createTypeNameFromJson(json: '...')` top-level Dart function from these annotated Rust stubs.
- fix(alef-e2e/dart): pass `String`-typed method arguments as Dart named parameters (`paramName: 'value'`) instead of positional arguments — FRB v2 generates all bridge method parameters as `{required T name}` named params in Dart, so positional passing produced `extra_positional_arguments_could_be_named` compile errors.
- fix(alef-e2e/dart): add `dart` to `chat_stream` `skip_languages` — the `chat_stream` method has a sanitized `BoxStream` return type that cannot be bridged through FRB, so the method is absent from the generated Dart class and streaming tests cannot be emitted. Skipping them matches the `go` treatment.
- fix(alef-e2e): fix `retrieve_response` and `cancel_response` call configs to use `field = "input.response_id"` — the liter-llm fixtures store the response identifier under `input.response_id`, not `input.id`, so the previous config caused "missing required input field 'id'" warnings and generated invalid no-argument calls in every language backend.

- fix(alef-e2e/swift): emit `try XCTSkipIf(true, ...)` stub for test methods whose call has a `json_object` arg without an `options_via` construction mechanism — previously the codegen passed `nil` or a bare JSON string literal, producing a Swift compile error (`'nil' is not compatible with expected argument type 'ChatCompletionRequest'` / `cannot convert value of type 'String' to expected argument type 'EmbeddingRequest'` etc.). Affects `chat`, `embed`, `speech`, `transcribe`, `image_generate`, `moderate`, `rerank`, `search`, `ocr`, `create_batch`, `create_file`, `create_response`, and `chat_stream` calls in liter-llm. The skipped tests compile and are recorded as XCTest skips rather than compile failures.
- fix(alef-e2e/swift): use `CharacterSet.whitespaces` instead of `.whitespaces` in all `trimmingCharacters(in:)` calls — Swift's type inference cannot always resolve the member shorthand inside `XCTAssertEqual` overloads, producing `error: cannot infer contextual base in reference to member 'whitespaces'`. The explicit `CharacterSet.whitespaces` is always unambiguous.
- fix(alef-e2e/swift): honour `extra_args` from per-call language overrides in the Swift e2e codegen — previously `extra_args = ["nil"]` entries in `[e2e.calls.<name>.overrides.swift]` were silently ignored, causing calls such as `listFiles()` and `listBatches()` to omit the required (but optional-typed) query parameter and fail with `error: missing argument for parameter #1 in call`. The fix appends verbatim extra args after the normal argument list, mirroring the existing behaviour in Go, Java, Ruby, and Gleam backends.
- fix(alef-e2e/swift): correctly handle `[N]` array subscripts in `swift_build_accessor` — segments containing an index (e.g. `data[0]`) were naively appended as `data[0]()` instead of the correct `data()[0]`, producing a Swift compile error. The segment is now split at `[` so the method call `()` is placed before the subscript.
- fix(alef-e2e/swift): read per-language `result_is_simple` override in swift codegen — the flag was only read from the base `CallConfig`, ignoring `[e2e.calls.<name>.overrides.swift] result_is_simple = true` entries. Calls such as `file_content` that return `Data` (not a struct) were incorrectly attempting to access `result.content().toString()`, producing a Swift compile error. The fix mirrors ruby's resolution: `base || override || global`.
- fix(alef-e2e/swift): merge per-call `enum_fields` keys from swift overrides into the effective enum set for each test method — fields like `"status"` (type `BatchStatus`) were not in the global `fields_enum` and so generated `.toString()` on the opaque `RustBridge.BatchStatus` class (which has no such method), producing a compile error. The fix builds a `Cow<HashSet<String>>` that extends the global set with the per-call override keys, then uses `String(describing:)` for those fields.
- fix(alef-e2e/swift): emit `XCTAssertFalse(result.isEmpty, ...)` for `not_empty` assertions when `result_is_simple = true` — previously the code path fell through to `result.toString().isEmpty`, which fails to compile when the result type is `Data` (no `.toString()` method). The fix detects `result_is_simple` before the string-conversion branch and calls `.isEmpty` directly on the result variable, which is valid for both `String` and `Data`.
- fix(alef-e2e/dart): remove erroneous `call_config.id` field references introduced by the dart agent — `CallConfig` has no `id` field, causing a compile error `no field 'id' on type '&CallConfig'`. The `.or_else` fallback branches that used this field were redundant (the primary `call_overrides` resolution already reads the correct per-fixture per-language override) and are now removed.

- fix(alef-backends/visitor): preserve original-case string in the `VisitResult::Custom` variant across `pyo3`, `napi`, `magnus`, `php`, `extendr`, and `wasm` trait-bridge templates. The dispatch match was binding the already-lowercased string to `other` and using it as the `Custom` payload, so user-returned bare strings like `"[Download](url)"` were emitted as `"[download](url)"`. The fallback arm now returns `Custom(s)` (or `Custom(s.to_string())` for `&str` sources). Fixes kreuzberg-dev/html-to-markdown#350.
- fix(alef-scaffold/php): emit `php-ext.download-url-method = ["pre-packaged-binary", "composer-default"]` in the scaffolded `packages/php/composer.json` so PIE 1.4.x discovers pre-built binaries attached to GitHub releases instead of falling through to source-build via `phpize` (which has no `config.m4` for ext-php-rs extensions). Fixes kreuzberg-dev/html-to-markdown#333.
- fix(alef-backend-kotlin): wrap opaque-handle client types as coroutine-friendly Kotlin classes that delegate to a sibling Java instance instead of calling non-existent `Bridge.<type>_<method>(handle, ...)` static methods. The previous generator emitted `class DefaultClient(apiKey, baseUrl)` with `private val handle: Long = Bridge.default_client_new(...)` and `Bridge.default_client_chat(handle, req)` calls — but the Java FFM backend exposes those FFI symbols only as instance methods on the Java `DefaultClient`, not as flat statics on the facade. The wrapper now takes `internal val inner: <java_package>.<ClassName>` and delegates each method as `withContext(Dispatchers.IO) { inner.<method>(args) }`. Construction flows through the existing facade factories (e.g. `LiterLlm.createClient(...)` now wraps the returned Java instance). Methods whose IR signature was sanitized (e.g. `chat_stream` whose real Java return type is a custom `Iterator<Chunk>`) are skipped — they require backend-specific surfaces that the generic generator cannot synthesize. Trait types and non-opaque value types (e.g. kreuzberg's `ExtractionConfig` with only a `default()` static) keep flowing through the Java typealias as before.
- fix(alef-backend-kotlin): exclude types-with-methods from `typealias` emission so the new Kotlin wrapper class does not collide with `typealias DefaultClient = <java>.DefaultClient` in the same package (was producing `Redeclaration: DefaultClient` from `compileKotlin`). The exclusion is gated on `is_opaque && !is_trait && has(non_sanitized, non_static method)` so kreuzberg's value-type configs keep their existing alias surface.
- fix(alef-backend-kotlin): emit `= null` defaults for optional flat-function parameters on the facade `object` so callers can use named-argument syntax (`LiterLlm.createClient(apiKey = "x", baseUrl = "y")`) without spelling out every nullable downstream argument.
- fix(alef-e2e/kotlin): client-factory smoke tests now invoke the configured factory function as `<class>.<client_factory>(apiKey, baseUrl)` (e.g. `LiterLlm.createClient(...)`) rather than calling the class name as a constructor — the latter produced `LiterLlm(apiKey, ...)` against the singleton facade `object` and failed to compile.
- fix(alef-backend-swift): emit `use <trait_path>;` statements in generated `lib.rs` for every trait-provided method on opaque-handle types — without these, `client.0.chat(req)` fails with `no method named 'chat' found for struct DefaultClient; perhaps trait LlmClient is implemented but not in scope`. Imports are collected from `MethodDef.trait_source` (None for inherent methods) and de-duplicated per type.
- fix(alef-backend-swift): correct call-site handling of opaque-method arguments — `Option<Named>` newtype args now emit `arg.map(|v| v.0)` instead of the invalid `arg.0`, `&str`/`&Path`-typed args (marked `is_ref` in IR) now emit `&arg` instead of moving the owned `String`, and `Bytes`-returning methods now chain `.map(|b| b.to_vec())` so `bytes::Bytes` converts to the `Vec<u8>` swift-bridge expects on the bridge boundary.
- fix(alef-backend-swift): allow per-type override of the auto-generated `create_<type>(api_key, base_url)` constructor body via `[crates.<crate>.swift] client_constructor_body."TypeName" = "…"`. Required for source crates whose constructor signature differs from the hardcoded `Type::new(api_key, base_url)` shape — e.g. liter-llm's `DefaultClient::new(ClientConfig, Option<&str>)`.
- fix(alef-backend-swift): only inject the implicit `ocr-wasm` cargo feature when the umbrella source crate actually exposes it in its on-disk `Cargo.toml`. Previously the unconditional injection caused `error: package liter-llm-swift depends on liter-llm with feature 'ocr-wasm' but liter-llm does not have that feature` for crates without an OCR module.
- fix(alef-backend-swift): drop argument labels in the `DefaultClient.init` -> bridge constructor call — swift-bridge emits the free `createDefaultClient(_:_:)` function without parameter labels, so the host wrapper now calls it positionally. Bytes-returning host wrapper methods bind the result locally and wrap in `Data(bytes.map { $0 })` to convert from `RustVec<UInt8>`.
- fix(alef-backend-dart,alef-backend-swift): emit explicit `package = "..."` rename in generated `Cargo.toml` `[dependencies]` block when the umbrella crate's Rust ident form (e.g. `liter_llm`) differs from the on-disk cargo package name (e.g. `liter-llm`). Without this, `liter_llm = { path = "..." }` causes `error: no matching package found — searched: liter_llm; perhaps you meant: liter-llm`. The fix is symmetrical across both backends and only fires when no explicit `core_crate_override` is set. (Swift had the rename logic in place but the new `core_crate_dir` argument was not wired through the cargo emitter — both sides are now consistent.)
- fix(alef-backend-dart): bump generated Dart pubspec SDK constraint to `>=3.3.0 <4.0.0`. `flutter_rust_bridge` 2.x emits `extension type` declarations in the WASM-side `frb_generated.web.dart`, which the Dart analyser rejects on SDKs below 3.3. Lower SDKs analyse cleanly for native-only builds but fail when the `.web.dart` file is in scope (always, since FRB emits it unconditionally). Same constraint now propagates to the alef-e2e dart pubspec template.
- fix(alef-backend-dart): emit static-wrapper signatures with required parameters as positional and optional parameters inside a `{...}` named-parameter block. Previously every param was positional, forcing callers (and the e2e codegen, which emits `createClient('test-key', baseUrl: mockUrl)`) to compile-fail with `not_enough_positional_arguments` / `undefined_named_parameter`. Functions with the special `ExtractionConfig` param (kreuzberg) keep the existing `[ExtractionConfig? config]` optional-positional shape — the new logic only applies to all-other-functions.
- fix(alef-backend-zig): emit iterator-based streaming method bodies — previously `chat_stream` was emitted as a generic method that called the callback-based C symbol `literllm_default_client_chat_stream(client, req_handle)` with only 2 arguments, producing a Zig compile error "expected 4 argument(s), found 2". Streaming adapters (pattern = "streaming") now detect the matching `Streaming` adapter config, derive the item type, and emit a `_start`/`_next`/`_free` iterator body that collects chunks, keeping the last chunk JSON as the return value (or `"{}"` on an empty stream). The detection is fully generic — any method named in a `Streaming` adapter in `alef.toml` receives the iterator body.
- fix(alef-backend-zig): emit multi-out-parameter convention for `Bytes` return types in both flat-function and opaque-method codegen — the C FFI returns `int32_t` status and writes the buffer via three out-params (`uint8_t **out_ptr, uintptr_t *out_len, uintptr_t *out_cap`); the zig wrapper now declares `var _out_ptr: [*c]u8 = undefined; var _out_len: usize = 0; var _out_cap: usize = 0;`, passes `&_out_ptr, &_out_len, &_out_cap` as extra C args, copies the buffer into a caller-owned `[]u8` via `std.heap.c_allocator.dupe`, and releases the FFI allocation via `{prefix}_free_bytes`. `zig_return_type` now maps `TypeRef::Bytes` to `[]u8` (owned) rather than `[]const u8`. Unblocks `speech` / `file_content` and any other library method returning `Result<Vec<u8>, _>`.
- fix(alef-e2e/gleam): fix four compile errors in generated Gleam e2e test files:
  1. `client_factory` calls now emit `option.None` (or `option.Some(base_url)`) instead of `""` for the `base_url` arg, and accept a new `client_factory_trailing_args` key on `[e2e.call.overrides.<lang>]` so consumers can pad extra positional parameters (e.g. `["option.None", "option.None", "option.None"]` for liter-llm's 5-arg `create_client`).
  2. `json_object` args without an `element_constructors` recipe or `json_object_wrapper` now emit a `// skipped` stub instead of a bare JSON-string literal that fails to typecheck against typed record parameters (`ChatCompletionRequest`, `EmbeddingRequest`, etc.).
  3. `extra_args` from per-call language overrides are now appended to the generated argument list; consumers can use `extra_args = ["option.None"]` to supply required-but-fixture-absent parameters such as the optional `query` argument on `list_files` / `list_batches`.
  4. Array-element field assertions (`data[1].field`, `pages[2].field`, etc.) are now correctly skipped — previously only `[0].` and `[].` patterns were matched; the fix uses a regex-free digit scan so any `[N].` index is recognised.
     Additionally: `equals` assertions on fields listed in the effective enum-field set (global `fields_enum` merged with per-call `enum_fields` / `assert_enum_fields`) now emit a `// skipped` comment in Gleam instead of generating a string comparison against a sum-type value that does not typecheck. Per-call `result_is_simple = true` overrides now cause field-access assertions to be skipped when the return type is a primitive (e.g. `BitArray`) with no record fields.
- fix(alef-backend-gleam): skip non-trait types with methods from the regular data-type emission pass — they are now emitted exclusively as opaque NIF resource handles (`pub opaque type T { T(resource: dynamic.Dynamic) }`). Previously such types were emitted twice (once as a phantom record by `emit_type` and once as an opaque resource by `emit_resource_type`), producing a "Duplicate type definition" compile error from `gleam build`.
- fix(alef-backend-zig): emit synthetic `free()` destructor on opaque-handle structs — every opaque handle owns a heap allocation in the FFI and must be released via the matching `{prefix}_{snake}_free` C symbol; `emit_opaque_handle` now appends a `pub fn free(self: *T) void` that calls the destructor, so `defer _client.free();` in generated e2e tests resolves correctly.
- fix(alef-backend-zig): remove spurious leading `!` on opaque-method signatures — the emit template was producing a double error union `!(LiterLlmError||error{OutOfMemory})![]u8` which Zig 0.16 rejects with "type does not support field access"; corrected to a single error union.
- fix(alef-backend-zig): map FFI errors to declared error set via `_first_error` — opaque-method error path was returning `error.FfiError` which isn't in the function's declared error set; aligned with flat-function codegen by calling `_first_error({error_type})`.
- fix(alef-e2e/zig): auto-set `result_is_json_struct` when `client_factory` is configured — opaque-method results are always serialized to JSON `[]u8` by the zig backend, so the e2e test must parse them with `std.json.parseFromSlice` rather than expecting a typed Zig struct.
- fix(alef-backend-zig): emit async opaque-handle methods — the `is_async` guard in `emit_opaque_method` was incorrectly skipping all methods whose Rust source is `async` even though the C FFI wraps them as synchronous functions via `block_on`; removed the guard so all non-static methods are emitted.
- fix(alef-backend-zig): correct optional-integer FFI marshalling — `?u64`/`?u32` (and any `Optional(Primitive)`) parameters now emit `if (x) |v| v else std.math.maxInt(T)` to pass the sentinel value the Rust FFI uses for `None`, instead of passing the `?T` type directly to a non-optional C parameter.
- fix(alef-backend-zig): wrap opaque-handle function return in Zig struct — `create_client` and similar functions that return an opaque C pointer now emit `TypeName{ ._handle = _result.? }` instead of returning the raw nullable C pointer, matching the `_handle: *anyopaque` field type.
- fix(alef-backend-zig): fix unreachable-code in opaque-method error block — `_ = _msg;` was emitted after `return error.FfiError;`; reordered to suppress the unused-variable warning before the early return.
- fix(alef-backend-zig): use `{prefix}_last_error_context` (not `_message`) in opaque-method error path — the C FFI exposes `_last_error_context`, not `_last_error_message`.

### Added

- feat(alef-e2e/visitor): `CallbackAction::CustomTemplate` gains a `return_form` field (`"dict"` (default) or `"bare_string"`). When set to `"bare_string"`, the generated test visitor method returns the rendered template directly as a string (e.g. `return f'…'` in Python, `` return `…` `` in TS/WASM, `"#{…}"` in Ruby, the raw string in PHP/R) so the trait-bridge's bare-string return path is exercised — instead of the dict/object wrapper that all existing fixtures relied on. Default behaviour is unchanged.
- feat(zig): client-object/opaque-handle codegen — types with non-empty `TypeDef.methods` that are opaque or non-serde now emit a Zig `pub const TypeName = struct { _handle: *anyopaque, ... }` with one `pub fn` per non-static, non-async method, dispatching via `c.{prefix}_{snake_type}_{snake_method}`. Driven by `CallOverride.client_factory` in e2e; no special-casing per library.
- feat(zig e2e): `[e2e.call.overrides.zig].client_factory` support — when set, generated Zig test functions instantiate a client via `module.factory_fn("test-key", mock_url, ...)` and call methods on the `_client` instance instead of calling the top-level module function directly. Mirrors the Go/Swift/Kotlin/Dart client-factory pattern.

- feat(gleam): opaque-resource codegen — types with non-empty `TypeDef.methods` now emit `pub opaque type TypeName { TypeName(resource: dynamic.Dynamic) }` plus one `@external` NIF binding per method. Mirrors the Swift/Kotlin/Dart client-object pattern.
- feat(gleam e2e): `[e2e.call.overrides.gleam].client_factory` support — when set, generated Gleam tests call the factory function and pass the client as the first argument to the method under test.
- fix(gleam e2e): Erlang startup shim now uses a `case` expression with a graceful `{error, _} -> ok` fallback when starting the Elixir application, avoiding test failures in environments without an Elixir runtime dependency.

- feat(dart e2e): `[e2e.call.overrides.dart].client_factory` support — when set, generated `package:test` tests call `await {BridgeClass}.{factory}('test-key', baseUrl: mockUrl)` before the assertion and dispatch the method on the resulting `_client` instance. Mirrors the Go/TypeScript/Zig/Swift/Kotlin client-factory pattern; no special-casing per library.

### Changed

- refactor(dart): `[crates.dart].stub_methods` is now a config-driven list of method names whose Rust bridge body is replaced with `unimplemented!()`. Previously this behaviour was hardcoded to kreuzberg's `batch_extract_bytes` / `batch_extract_bytes_sync`. **Migration**: if your `alef.toml` relied on the hardcoded list, add `stub_methods = ["batch_extract_bytes", "batch_extract_bytes_sync"]` (or the relevant names) under `[crates.<name>.dart]`.

### Known limitations

- FRB codegen still requires a manual post-step after `alef generate`: `cd packages/dart/rust && cargo run --bin flutter_rust_bridge_codegen`. Implementing a `PostBuildStep` for this is deferred to a future release.
- The dart backend does NOT yet emit instance methods on opaque-type wrappers in the FRB Rust crate. When a type has `TypeDef.methods` (e.g. `DefaultClient::chat`, `::embed`, `::list_models` in liter-llm), only the empty `#[frb(opaque)] pub struct DefaultClient { inner: ... }` wrapper is emitted — FRB then generates an `abstract class DefaultClient implements RustOpaqueInterface {}` with no callable methods. The swift/kotlin/zig/gleam backends already implement this (driven by `TypeDef.methods` + `[e2e.call.overrides.<lang>].client_factory`); the dart equivalent — emitting `impl DefaultClient { #[frb] pub fn chat(&self, req: ChatCompletionRequest) -> Result<ChatCompletionResponse, String> { ... } }` blocks so FRB surfaces them as Dart instance methods — is the natural next step but is deferred. Until then, dart e2e tests for libraries with stateful clients (liter-llm) will not compile past the `_client.chat()` call sites even though the static factory (`createClient`) and all data-type mirrors generate correctly.

### Added

- feat(swift): client-object class wrapper — types with non-empty `TypeDef.methods` now emit a `public final class TypeName` with an `init(apiKey:baseUrl:)` constructor and one `public func method(...)` per method, backed by free-function shims in the swift-bridge crate (`create_<type>` / `<type>_<method>`). Driven by `TypeDef.methods` in the IR; no special-casing per library.
- feat(swift e2e): `[e2e.call.overrides.swift].client_factory` support — when set, generated XCTest methods instantiate `DefaultClient(apiKey:baseUrl:)` against the mock server URL and call `client.<method>(args)` instead of a free function. Mirrors the Go/TypeScript/Zig client-factory pattern.
- feat(swift e2e): `Package.swift` now always emits `.iOS(.v14)` alongside `.macOS(...)` in the platforms array; swift-bridge supports both targets.
- feat(kotlin): client-object class codegen — types with non-empty `TypeDef.methods` now emit a `DefaultClient.kt` with a `class DefaultClient(apiKey, baseUrl?)` constructor, one method per `MethodDef`, and `AutoCloseable.close()` delegating to `Bridge.<type>_free(handle)`. Driven entirely by IR; flat-function Kotlin (kreuzberg) is unaffected.
- feat(kotlin e2e): `[e2e.call.overrides.kotlin].client_factory` support — when set, generated JUnit 5 tests instantiate `DefaultClient(apiKey, baseUrl)` against the mock server URL and call `client.<method>(args)` followed by `client.close()`, mirroring the Go/TypeScript/Zig pattern.
- feat(kotlin kmp): KMP `build.gradle.kts` now emits `iosX64`, `iosArm64`, `iosSimulatorArm64` (framework binaries) and `androidNativeArm64` (sharedLib) targets with cinterop blocks; corresponding `iosMain` and `androidNativeArm64Main` sourceSets wired to `nativeMain`.
- feat(kotlin mode): `KotlinConfig` gains `pub mode: Option<String>` field — accepted values `"jvm"` (default), `"kmp"`, `"android"`. Setting `mode = "android"` emits an Android library project under `packages/kotlin-android/` (minSdk 21, compileSdk 35, `AndroidManifest.xml`).

### Note

- XCFramework `binaryTarget` codegen (pre-built `.xcframework` distribution for SwiftPM) is deferred as a follow-up; the current output requires local `cargo build` to produce the Rust dylib.

## [0.15.39] - 2026-05-11

### Added

- feat(alef-backend-pyo3): `[crates.python.extra_init_imports]` re-exports hand-written Python symbols (e.g. literal type aliases generated by user scripts) through `__init__.py` without alef culling them. Schema: `{ "<module>" = ["<symbol>", ...] }`. Symbols are appended to `__all__` and the source modules are skipped by the cleanup pipeline.

### Fixed

- fix(alef-backend-pyo3): capsule-typed return values now Python-construct the declared `python_type` instead of returning a bare `PyCapsule`. Both the `Capsule(...)` and `ConstructFrom { python_type, construct_from }` config variants now produce real `tree_sitter.Language` / `tree_sitter.Parser` instances at the call site, restoring `parser.parse(bytes)` semantics in downstream Python bindings. Also filters capsule types out of generated `from ._native import …` statements in `api.py` and `__init__.py`, since they have no native Rust class to import.

## [0.15.38] - 2026-05-11

### Fixed

- fix(hooks): `alef_hook.py` now honours `[crate].version_from = "Cargo.toml"` in `alef.toml` and resolves the version from the referenced Cargo.toml's `[workspace.package]` (or `[package]`) `version` field. Previously the hook always read the top-level `version = "..."` line in alef.toml, which is a stale lazily-bumped fallback — downstream consumers tried to download `alef-<host>.tar.gz` for that stale version and 404'd. Falls back to the inline `version` line if the referenced Cargo.toml is missing.

## [0.15.37] - 2026-05-11

### Fixed

- fix(alef-backend-dart): `doc_comment.jinja` now preserves newlines between doc-comment lines. Under `trim_blocks = true`, `{%- endfor %}` was stripping the newline before the tag, collapsing multi-line doc comments into a single line in the generated Dart wrapper class (e.g. `packages/dart/lib/src/<module>.dart`). Same class of bug as the prior `alef-backend-zig` fix in 0.15.36 (`error_doc_block.jinja`, `trait_method_doc_lines.jinja`).

## [0.15.36] - 2026-05-11

### Changed

- refactor(alef-backend-java,alef-backend-swift,alef-backend-wasm,alef-backend-zig): migrate remaining backend doc-code emission and opaque-bridge interpolation from `push_str(&format!(...))` to dedicated Jinja templates (`javadoc_lines.jinja`, `doc_comment.jinja`, `rustdoc`, `param_opaque_config_from_json.jinja`).

### Fixed

- fix(alef-backend-zig): opaque C FFI handle types (`is_opaque = true` / `has_serde = false`, e.g. `CrawlEngineHandle`) are now excluded from `struct_names`. Previously they were treated as JSON-serializable structs and the generated wrappers called non-existent `_from_json`/`_to_json` C helpers. Functions taking an opaque handle now accept `?[]const u8` config JSON and internally call the creator function (discovered from the IR) to build, use, and free the handle per call.

- fix(alef-e2e/swift): `Optional<RustString>` fields now emit `({field_expr}?.toString() ?? "")` in Swift assertions instead of `{field_expr}.toString()`, fixing compile errors when the field is nullable. `not_empty`/`is_empty` assertions on array fields (`RustVec<T>`) now emit `{field_expr}.isEmpty` directly instead of routing through `.toString()` — `RustVec<T>` has no `.toString()`.

- fix(alef-e2e/zig): `handle` arg type in `build_args_and_setup` is now an explicit case: emits `null` when the fixture omits the engine config, or a JSON string literal when a config value is present. Aligns with the updated Zig binding that accepts `?[]const u8` for engine parameters.

- fix(alef-backend-zig): `error_doc_block.jinja` and `trait_method_doc_lines.jinja` now emit the trailing newline after the final doc-comment line. Under `trim_blocks = true`, `{%- endfor %}` was stripping the newline before the tag, merging the doc comment and the following `pub const` declaration onto one line — a Zig compile error.

- fix(alef-e2e/zig): `render_json_assertion` now resolves fixture field-path aliases through `FieldResolver` before building the JSON traversal chain. Previously, `content.detected_charset` was traversed as `result.object.get("content").?.object.get("detected_charset")` instead of the correct `result.object.get("detected_charset")`, causing runtime panics on fields that only exist at the top level.

## [0.15.35] - 2026-05-11

### Added

- feat(alef-backend-pyo3): `capsule_types` in `[crates.python]` is now wired into codegen. Types listed there are emitted as PyCapsule pass-through (via `PyCapsule_New` / `PyCapsule_GetPointer`) instead of opaque `#[pyclass]` wrappers. Supports two TOML forms: a bare string (`Language = "tree_sitter.Language"`) for capsule round-trips, and a struct (`Parser = { python_type = "tree_sitter.Parser", construct_from = "Language" }`) for Python-side construction (e.g. `tree_sitter.Parser(language)`).

### Fixed

- fix(alef-backend-dart): `flutter_rust_bridge.yaml` now pins `rust_input: crate` so FRB scans every top-level `pub fn` in the binding crate. Previously the key was omitted (under the assumption FRB 2.x had dropped it) and FRB defaulted to a narrower scope, silently skipping plugin lifecycle helpers (`unregister_*`, `register_document_extractor`, `register_renderer`, etc.) and leaving the generated wrapper referencing undefined bridge functions.

- fix(alef-backend-java): per-extractor Java wrappers (e.g. `HwpxExtractor.java`) now emit `import java.util.{List, Optional, Map}` when the trait's instance methods reference those types. Previously the import set was hard-coded to MemorySegment/Arena/ValueLayout/ObjectMapper, so `List<String> supportedMimeTypes()` failed javac with `cannot find symbol: class List`.

- fix(alef-core): `PythonConfig.capsule_types` schema is now `HashMap<String, CapsuleTypeConfig>` (was `HashMap<String, String>`). Existing `alef.toml` files using bare string values (e.g. `Language = "tree_sitter.Language"`) continue to deserialize correctly via `#[serde(untagged)]`.

### Changed

- refactor(alef-backend-zig,alef-backend-gleam,alef-backend-extendr): migrate additional interpolated `push_str`/`writeln!` emission paths to Jinja templates for trait bridge docs, error docs, import line emission, and visitor bridge assembly.
- refactor(alef-backend-csharp): replace inline `push_str`/`writeln!` visitor record emission for generated `NodeContext.cs` and `VisitResult.cs` with Jinja templates (`node_context.jinja`, `visit_result.jinja`).
- refactor(alef-backend-go): migrate additional `gen_visitor.rs` emission from inline `push_str` blocks to Jinja templates for visitor interface/registry/helper/trampoline/control-flow; generated `ConvertWithVisitor` now routes through `convertWithVisitorHelper` for shared logic.
- refactor(alef-backend-ffi): continue migrating parameterized trait-bridge code generation to Jinja templates for vtable error messages and async registration body lines (`ffi_vtable_not_initialised_msg.jinja`, `ffi_nul_byte_*_param_msg.jinja`, `ffi_vtable_null_out_result_msg.jinja`, plus async cached-name/clone/`map_err` templates).
- refactor(alef-backend-magnus): move more trait-bridge generation into Jinja templates (`trait_bridge_async_method_body.rs.jinja`, `trait_bridge_constructor.rs.jinja`, `trait_bridge_registration_fn.rs.jinja`, `trait_bridge_return_conversion.rs.jinja`), keeping behavior unchanged.

### Fixed

- fix(alef-e2e/gleam): `render_assertion` now resolves field aliases before calling `is_optional`, fixing incorrect non-optional treatment of fields accessed via fixture path aliases (e.g. `og.title` → `metadata.og_title`). Import pre-pass also uses resolved paths for optional detection.
- fix(alef-e2e/gleam): `not_empty` and `is_empty` assertions on non-array, non-optional String fields now emit `string.is_empty` instead of `list.is_empty`. Import pre-pass updated accordingly.
- fix(alef-e2e/gleam): Array element field assertions using indexed access (`[0].` and `[].`) are now both skipped with a comment, not just `[].` paths.
- fix(alef-backend-zig): Async Rust functions are no longer silently skipped in Zig binding generation. The C FFI exports synchronous wrappers for all functions (including those async in Rust), so Zig can call them directly.
- fix(alef-e2e/gleam): `render_assertion` now also resolves field aliases before setting `field_is_optional`, fixing optional wrapping for aliased fields at the render stage.
- fix(alef-e2e/gleam): `not_empty`/`is_empty` assertions on non-array, non-optional fields now emit `string.is_empty` instead of `list.is_empty` in `render_assertion`.
- fix(alef-e2e/gleam): Indexed array element fields (`[0].`) are now skipped alongside `[].` paths in `render_assertion`.
- fix(alef-e2e/gleam): Fields with a `.length` segment (e.g. `links.length`) now import `gleam/list` in the test file header.

## [0.15.34] - 2026-05-11

### Changed

- refactor(alef-backend-zig,alef-backend-dart,alef-backend-php): move remaining generated-code emission blocks for Zig parameter/return handling, Dart trait bridge defaults, and PHP vector binding conversion into Jinja templates.
- refactor(alef-scaffold,alef-docs,alef-readme): render generated pre-commit YAML, scaffold Cargo env rows, API Markdown blocks, and README performance rows through Jinja templates.
- refactor(alef-backend-napi,alef-backend-extendr): move additional trait bridge wrappers, function bodies, serde bindings, and async parameter clone fragments into Jinja templates.

### Fixed

- fix(alef-e2e/kotlin): generate `MockServerListener.kt` and `META-INF/services/org.junit.platform.launcher.LauncherSessionListener` when any fixture needs the mock-server. Add `junit-platform-launcher` to `build.gradle.kts` when the listener is emitted. Previously `MOCK_SERVER_URL` was never set, causing all mock-server-dependent Kotlin e2e tests to fail.

- fix(alef-e2e/kotlin): generate Kotlin-native collection access and nullable assertion expressions for optional fields, arrays, maps, and JNA library path setup.

- fix(alef-e2e/zig): replace `std.posix.getenv` with `std.process.getenv` in generated test preambles. `std.posix.getenv` was removed in Zig 0.16.0; `std.process.getenv` is the correct API and works across all supported Zig versions.

- fix(alef-e2e/gleam): `build_args_and_setup` now handles `handle` and `mock_url` argument types. `handle` args emit `let assert Ok(<name>) = module.create_<name>(option.None)` setup and `option` is added to imports. `mock_url` args emit an `envoy.get("MOCK_SERVER_URL")` URL construction and `envoy` is imported via `gleam_envoy`. Previously both fell through to the catch-all, producing a single-argument JSON call instead of the correct multi-argument form.

- fix(alef-e2e): `field_access.rs` now emits `list.length` for `PathSegment::Length` in Gleam (was missing), and handles nested map/array access chains correctly across all e2e codegen backends.

- fix(alef-backend-swift): Duration-typed struct fields now bridge correctly through swift-bridge. Constructors emit `std::time::Duration::from_millis(<param>)` (or `.map(std::time::Duration::from_millis)` for optional), and getters emit `self.0.<field>.as_millis() as u64` (or `.map(|d| d.as_millis() as u64)` for optional). Previously both fell through to the generic field assign template, causing `E0308 expected Duration, found u64` in every swift binding crate that has timeout fields.

## [0.15.33] - 2026-05-11

### Changed

- refactor(alef-backend-swift): move remaining Swift backend generated-code blocks for inbound plugin bridges, JSON factory shims, and Swift convenience overloads from inline string assembly into Jinja templates.

- refactor(alef-backend-magnus,alef-backend-php,alef-backend-rustler): move more generated function-body, registration, serde-binding, and enum-conversion emission from inline `format!` assembly into Jinja templates while preserving generated output behavior.

- refactor(alef-backend-csharp,alef-backend-dart,alef-backend-ffi,alef-backend-java,alef-codegen): move additional generated-code emission blocks from inline `push_str`/`format!` assembly into Jinja templates while preserving the existing generated output contracts.

### Added

- feat(alef-e2e/rust): the C# and Java e2e codegen now calls `resolve_call_for_fixture` (which honours `select_when = { input_has = "..." }` in `[crates.e2e.calls.*]`) instead of the no-input `resolve_call`. Fixtures with `input.batch_urls: []` (and no explicit `call` override) now route to `batch_scrape()` / `BatchScrape()` instead of the default `scrape()`. Go and Python codegen already used the correct resolver; C# (`csharp.rs:775, 3032`) and Java (`java.rs:458, 498, 961`) were the only callers of the wrong variant.

- feat(alef-e2e/rust): the Rust e2e codegen now emits a `tests/common.rs` module whenever the fixture suite requires a standalone mock server. The module exposes `pub fn mock_server_url() -> &'static str` backed by `std::sync::OnceLock`, spawning `target/release/mock-server` with the fixtures directory on first call, parsing its `MOCK_SERVER_URL=<url>` and `MOCK_SERVERS={...json...}` stdout lines, and setting `std::env::set_var` for each. Generated test files that previously panicked with `MOCK_SERVER_URL not set` now import `mod common;` and call `common::mock_server_url()`. Mirrors Python (`conftest.py`), Node (`globalSetup.ts`), and Elixir (`test_helper.exs`) session-level orchestration.

### Fixed

- fix(alef-backend-java): extract the repeated bytes-FFI result block (rc check, outPtr read, free, return) from `stream_method_bytes_result.jinja` into a `readBytesResult` helper method emitted once per class by `streaming_helpers.jinja`. Eliminates the 17-line CPD duplication flagged when two or more byte-returning methods (e.g. `speech`, `fileContent`) are generated in the same class.

- fix(alef-backend-swift,alef-backend-napi,alef-codegen): harden remaining trait bridge generation edge cases. Swift trait bridge JSON envelopes now serialize success and error payloads with `serde_json`, Swift inbound wrappers handle `returns_ref` `Vec<String>` methods without overriding default trait methods, NAPI trait bridge parse errors include plugin context, and excluded type paths no longer overwrite visible binding type paths with the same short name.

- fix(alef-backend-dart): the core→mirror `From` impl for struct fields of type `Duration` now emits `v.{name}.as_millis() as i64` (or `.map(|d| d.as_millis() as i64)` for optional) instead of the invalid `v.{name} as _`. `Duration` is not a primitive type and cannot be coerced with `as`; the previous codegen produced `E0605 non-primitive cast: Duration as i64` for any crate that has `Duration`-typed fields (e.g. `BrowserConfig.timeout`, `CrawlConfig.request_timeout`).

- fix(alef-e2e/ruby): fixtures with only a `not_error` assertion now emit a real test body (`expect { }.not_to raise_error`) instead of `skip "Non-HTTP fixture cannot be tested via Net::HTTP"`.

- fix(alef-codegen,alef-backend-php): `gen_lossy_binding_to_core_fields_inner` and `gen_php_lossy_binding_to_core_fields` now skip fields whose `field.cfg` is `Some(...)`. Previously, cfg-gated fields (e.g. `pdf_options`, `keywords`, `layout`) were emitted in the binding→core struct literal even though those fields are absent from the binding struct (they are filled by `..Default::default()`). The generated code produced `E0609 "no field X on type &ExtractionConfig"` in downstream PyO3 and PHP crates after alef regen.

- fix(alef-backend-napi): three related NAPI codegen correctness fixes: (1) `gen_struct` now filters cfg-gated fields when emitting the binding struct definition and its manual `Clone` impl, preventing `E0063 "missing fields in initializer"` for structs like `JsExtractionConfig`; (2) `sync_method_non_unit_return.jinja` and `async_method_non_unit_return.jinja` now return `Err({{ error_parse }})` instead of `Ok(Default::default())` for null/empty JSON in error-returning methods, avoiding `E0277` caused by `InternalDocument` not implementing `Default`; (3) `field_conversion_from_core` and `field_conversion_to_core` now handle `Map(_, Bytes)` explicitly — `HashMap<String, Vec<u8>>` core fields (e.g. `tessdata_bytes`) are correctly round-tripped through `HashMap<String, Buffer>` in the NAPI binding using `.to_vec().into()`.

- fix(alef-backend-swift): `emit_extern_block_for_trait_bridge` and `emit_trait_bridge_wrapper` no longer emit `Result<T, String>` return types for error-returning trait bridge methods. swift-bridge 0.1.59 panics with "Type must be declared with `type >`" when it encounters `Result<T, E>` inside an `extern "Rust"` block. Error-returning methods now use a plain `String` return carrying a JSON envelope (`{"ok": <value>}` / `{"err": "<message>"}`) so swift-bridge can parse the extern block. The Rust trampoline body serialises the `Result` to this envelope; the Swift caller deserialises it.

- fix(alef-e2e/rust): `needs_mock_server` in the Rust e2e codegen (`rust/mod.rs`) and the per-file `file_needs_mock` condition in `test_file.rs` were gated on `f.mock_response.is_some()` — the legacy liter-llm single-response shape. Fixtures using the array form (`input.mock_responses: [...]`, the kreuzcrawl shape) returned `false` for that check, so the entire kreuzcrawl suite had `needs_mock_server = false`. Consequently: (1) `tests/common.rs` was never generated (gated on `needs_mock_server`), (2) `mod common;` was never emitted in test files. Test files called `common::mock_server_url()` without the module declaration, causing `E0433 cannot find module or crate 'common'` and a compilation failure. Fix: use `f.needs_mock_server()` (which already handles all three shapes: liter-llm, http, and kreuzcrawl array form) in both detection sites.

- fix(alef-backend-java): the compact-constructor emission in `gen_record_type` no longer coerces an explicit `0` to the Rust-default value for `Duration`-typed (boxed Long) fields. Previously the generated Java record contained `if (requestTimeout == null || requestTimeout == 0) requestTimeout = 30000L;` — callers who intentionally passed `request_timeout: 0` (which the Rust core rejects via `validate()` as invalid) had their value silently replaced with the default before the FFI boundary, so the validation error was never surfaced. The fix drops `|| requestTimeout == 0`; only absent JSON fields (Jackson deserialises boxed numerics as `null` when the key is missing) still receive the default.

- fix(alef-e2e/dart): `json_object` args that contain a plain JSON array (e.g. `batch_urls: ["/p1", "/p2"]`) now emit `final {name} = (jsonDecode(r'...') as List<dynamic>).cast<String>();` + pass the variable to the call, instead of silently dropping the argument. Previously any `json_object` without an explicit `element_type` of `BatchBytesItem`/`BatchFileItem` and without the name `config` was skipped entirely.

- fix(alef-e2e/dart): the Dart e2e codegen now correctly handles `handle` and `mock_url` arg types. Previously both arg types fell through to the `_ => {}` catch-all, so generated tests had no arguments and called the wrong `KreuzbergBridge` class (a kreuzberg-specific hardcode). Fixes: (1) `handle` args emit engine construction — `CrawlConfig.fromJson(jsonDecode(...))` then `await {BridgeClass}.createEngine(config)` — before the main call; (2) `mock_url` args emit `Platform.environment["MOCK_SERVER_URL"]` URL construction; (3) the receiver class now derives from `[dart] lib_name` (converted to PascalCase + "Bridge") via a new `dart_bridge_class_name()` method on `ResolvedCrateConfig` instead of the hardcoded `"KreuzbergBridge"` literal; (4) error-expecting fixtures with setup lines wrap the full setup + call in an `async` lambda so any exception at any step is caught by `throwsA(anything)`; (5) `import 'dart:convert'` is now emitted when any non-HTTP fixture uses a `handle` arg (previously only emitted for HTTP fixtures).

- fix(alef-e2e/zig): the `mock_url` arg type in the Zig e2e codegen now emits `const {name} = try std.fmt.allocPrint(allocator, ...)` + `defer allocator.free({name})` instead of the previously invalid `var {} = try allocator.alloc(u8, std.fmt.bufPrint(undefined, ...) catch 0)`. The old pattern produced a Zig syntax error (`expected ';' after statement`) and an incorrect allocation size. The new pattern uses `std.fmt.allocPrint` which allocates exactly the right number of bytes for the formatted URL string.

- fix(alef-backend-dart): Vec<Json> (e.g. `MarkdownResult.tables: Vec<serde_json::Value>`) fields in core→mirror `From` impls now emit `.map(|j| serde_json::to_string(&j).unwrap_or_default())` instead of falling through to the identity return (`v.{name}`). Previously, `TypeRef::Json` inside a `Vec` was treated as a pass-through, producing `E0308 mismatched types: expected Vec<String>, found Vec<Value>`.

- fix(alef-backend-swift): `Duration`-typed struct fields now emit correct constructor and getter code. The constructor body emits `__target.{name} = std::time::Duration::from_millis({param});` (or `.map(std::time::Duration::from_millis)` for optional) via two new Jinja templates (`default_field_duration_assign.jinja`, `default_field_optional_duration_assign.jinja`). Getters emit `self.0.{name}.as_millis() as u64` (or `.map(|d| d.as_millis() as u64)` for optional) via `getter_duration.jinja` and `getter_optional_duration.jinja`. Previously both paths fell through to generic templates that emitted `__target.timeout = timeout` / `self.0.timeout.clone()`, producing `E0308 mismatched types` for any struct with a `Duration` field (e.g. `BrowserConfig.timeout`, `CrawlConfig.request_timeout`).

- fix(alef-backend-dart): opaque handle types (e.g. `CrawlEngineHandle`) now emit a `#[frb(opaque)] pub struct {Name} { pub(crate) inner: source::{Name} }` wrapper instead of `#[frb(mirror({Name}))] pub struct {Name} {}` (an empty zero-sized struct). The empty mirror pattern caused `E0308` for return values (`CrawlEngineHandle::from(v)` with no `From` impl) and would have silently destroyed the engine value via an unsound zero-sized transmute. Bridge functions now use `&engine.inner` (input) and `|inner| Name { inner }` (return) instead of transmute/From.

- fix(alef-backend-dart): struct types that contain `Duration` or `Path` fields are now included in `types_needing_from_conversion` (and transitively in `types_needing_from_impl`), causing them to use `{core_ty}::from(mirror_val)` at call sites instead of `unsafe { transmute }`. Previously, `Duration` fields (16-byte `std::time::Duration` in core vs 8-byte `i64` in mirror) and `Path` fields were not considered sanitized by the IR, so structs like `CrawlConfig` used a transmute that caused `E0512 cannot transmute between types of different sizes` (6720-bit mirror vs 6848-bit core).

- fix(alef-codegen): `gen_bridge_trait_impl` now wraps `Vec<String>` bodies in a `Box::leak` pattern when the trait method signature declares `returns_ref = true` (i.e. `&[&str]`). Previously, `supported_mime_types()` bridge bodies returned `Vec<String>` but the trait requires `&[&str]`, causing `E0308` in generated Python, Node.js, PHP, and Swift binding crates after `alef generate`.

- fix(alef-backend-swift): two additional Swift trait bridge fixes: (1) `emit_trait_method_body` generated `format!("{\"ok\": ...}")` which is invalid Rust format syntax because `{` must start a format spec, not a literal; doubled the outer braces so the generated code emits literal `{` and `}` characters correctly. (2) `emit_extern_block_for_trait_bridge` and `emit_trait_bridge_wrapper` now skip methods with `has_default_impl = true` (e.g. `as_sync_extractor` returning `Option<&dyn SyncExtractor>`) — these cannot be expressed in swift-bridge and rely on the trait's own default impl.

- fix(alef-backend-php): `gen_struct_methods_impl` now filters cfg-gated fields (`field.cfg.is_some()`) when building constructor parameter lists, `let_binding` loops, and `param_init` struct literals. `gen_enum_tainted_from_binding_to_core` also skips cfg-gated fields in its field loop. Previously, cfg-gated fields like `pdf_options` were emitted as constructor parameters and in `From` impls even though they are absent from the PHP binding struct, producing `E0560 "struct has no field named pdf_options"`.

- fix(alef-backend-napi): `gen_struct` now maps `Bytes` fields to `Vec<u8>` in struct field position via a `map_bytes_field_type` closure instead of `mapper.map_type`. `napi::bindgen_prelude::Buffer` does not implement `Clone`, `Serialize`, or `Deserialize`, causing derive failures on structs with bytes fields (e.g. `JsOcrConfig`). `Buffer` is still used for function parameters. The `has_bytes_field` manual-`Clone` workaround is removed since `Vec<u8>` is `Clone`.

- fix(alef-e2e): Java mock server URL lookup now checks `System.getProperty("mockServerUrl", ...)` with env fallback so the URL can be injected via JVM system property. Ruby client factory calls switched from keyword args (`api_key: ..., base_url: ...`) to positional args to match the actual generated factory method signatures.

- fix(alef-e2e/rust): the generated `common.rs` `BufReader` now takes ownership of `ChildStdout` (`BufReader::new(stdout)`) instead of borrowing it (`BufReader::new(&mut stdout)`). The previous pattern caused `E0597` on Rust 2024: the drain thread (`std::thread::spawn(move || reader.into_inner())`) requires `'static` bounds, but a `BufReader<&mut ChildStdout>` wraps a local reference that is not `'static`. Owned `BufReader<ChildStdout>` satisfies the bound.

- fix(alef-backend-magnus): streaming adapter (`gen_iterator_struct`) now derives the core-crate prefix from the configured `core_import_name` rather than the hardcoded literal `liter_llm::`. The `StreamingAdapter` struct gains a `core_crate` field populated from `core_import` (computed at the top of `generate_bindings`) and threaded through `from_config`. Fixes the no-special-casing rule: any downstream crate (not only liter-llm) that wires a `streaming` adapter now gets `{core_crate}::{ItemType}` / `{core_crate}::{ErrorType}` in the emitted iterator struct instead of a crate-name assumption.

- fix(alef-e2e/wasm): the emitted `vitest.config.ts` now sets `testTimeout: 30000` globally for the WASM e2e suite. Vitest's default 5 s deadline is too tight for fixtures that exercise liter-llm's retry path (504 / 429 / 500 / 502 are retryable with backoff); those tests all timed out at the default, masking real pass/fail outcomes. A 30 s timeout matches the rest of the suite's retry window.

### Added

- feat(alef-backend-ffi): emit `{prefix}_{enum}_to_string(*const Enum) -> *mut c_char` for unit-variant enums (`has_serde = true`) that are returned as heap-allocated pointers. The function uses `serde_json::to_value(val).as_str()` to extract the bare variant name (e.g. `"completed"`) without surrounding JSON quotes, so C/Zig/Dart e2e callers can string-compare an enum field accessor against a fixture string without reaching for `_to_json` (which yields a JSON-quoted form). Sibling helper to existing `_to_json`/`_free`; emitted only when the enum is in `enum_pointer_return` AND `can_generate_enum_conversion` (gates out compound enums whose serde shape is not a plain string).

### Fixed

- fix(alef-e2e/c): when a fixture assertion targets a field that is registered in `[crates.e2e] fields_enum` AND the field's resolved type in `[crates.e2e] fields_c_types` is a non-primitive PascalCase enum name (e.g. `BatchStatus`), emit an opaque-handle declaration plus a `{prefix}_{enum_snake}_to_string({handle})` conversion call rather than declaring the accessor return as `char*`. The previous output (`char* status = literllm_batch_object_status(result); assert(str_trim_eq(status, "completed") == 0);`) treated the FFI's `LITERLLMBatchStatus*` opaque pointer as a C string, causing immediate `Abort trap: 6` / NULL-deref in every C e2e fixture that compared an enum field. Applied to all four accessor sites: `render_test_function` (default-client and legacy paths), `render_engine_factory_test_function`, and the leaf branch of `emit_nested_accessor`. Cleanup: the opaque handle is registered in `intermediate_handles` so the existing reverse-order free loop calls `{prefix}_{enum_snake}_free(...)`; the `to_string` result is a heap `char*` freed by `{prefix}_free_string` like any other accessor result.

- fix(alef-backend-ffi): `_free`/`_to_json`/`_to_string` are now also emitted for enum types that are returned by _struct field accessors_ (not just by free functions or method returns). Previously the `enum_pointer_return` set was built from `api.functions` and `typ.methods` only — fields like `BatchObject.status: BatchStatus` produced an opaque-pointer accessor (`literllm_batch_object_status`) without a matching `_free` / `_to_string`, leaking memory and forcing C callers to treat the return as an unfreeable handle. The detection now walks `typ.fields` too. The pre-existing comment ("Also check struct field accessors and method returns…") matched the intent but the code was missing the field-walk half.

- fix(alef-e2e/c): the iter11 sentinel fix `(uint64_t)-1, (uint32_t)-1` for `*_create_client` only landed in `render_chat_stream_test_function`. The default-client path in `render_test_function` and the bytes-result path in `render_bytes_test_function` still emitted literal `0, 0` — passing them as `Some(0)` to the FFI yielded `Duration::from_secs(0)` and aborted every non-streaming HTTP fixture. Both call sites now also emit the sentinel. Mirror of 0.15.28 java/csharp marshalling fix.

- fix(alef-e2e/c): `emit_nested_accessor` now correctly handles `field[N]` numeric array indexing in chained paths (e.g. `data[0].index`, `choices[0].message.tool_calls[0].function.name`). Previously the bracket branch returned early after emitting `alef_json_get_string(parent, "0")` — looking up literal key `"0":` instead of the Nth array element — so any path with array indexing returned NULL and aborted on the first downstream assertion. New runtime helper `alef_json_array_get_index(json, idx)` extracts the Nth top-level element of a JSON array; the codegen emits it whenever it sees a numeric bracket key (both at top-level and inside `json_extract_mode`). Bare `[]` still uses substring search semantics. Required for fixtures with multi-element result arrays where each element's fields are asserted independently (e.g. `data[0].index == 0`, `data[1].index == 1`).

- fix(alef-e2e/c): `alef_json_get_string` now falls through to `alef_json_get_object` when the value at the resolved key is a JSON object/array, and returns the raw token string when the value is a primitive (number / bool / null). Previously it strictly required string values, so leaf accessors over collection-typed fields (`Vec<T>`, `Option<Vec<T>>`) and numeric leaves accessed via `json_extract_mode` (e.g. `data[0].index` of type `u32`) returned NULL and broke `not_empty` / `count_equals` / `equals` assertions. Adds a runtime helper `alef_json_get_object` that uses balanced-bracket matching to extract `{...}` / `[...]` substrings; codegen also uses it directly for intermediate object hops in `json_extract_mode`.

- fix(alef-e2e/c): "expected error" fixtures (assertion type `error`) now treat a NULL return from `*_request_from_json` as the expected failure path instead of asserting non-null. Mirrors Java's `assertThrows(Exception.class, () -> { … })` and Python's `pytest.raises(...)` patterns: when the fixture's input contains an invalid enum value (e.g. `"purpose":"invalid-purpose"` for `FilePurpose`), serde's strict deserialization rejects it before the request leaves the binding, and that counts as the expected error. Without this, every C error fixture whose error originates in the input layer crashed at the build step rather than reaching its `assert(result == NULL)` final assertion.

- fix(alef-e2e/c): smoke/live fixtures gated on a required env var (`fixture.env.api_key_var`, e.g. `OPENAI_API_KEY`) now emit a `if (getenv(VAR) == NULL) return;` short-circuit at the top of the test body. Previously the C suite hard-failed on any missing-credentials run; now CI without provider creds gracefully skips these tests, matching Python's `pytest.skip(...)` and Java's `Assumptions.assumeTrue(...)`.

- fix(alef-e2e/c): streaming tool_calls assertions (`tool_calls`, `tool_calls[0].function.name`) are now emitted as skip comments instead of attempting to parse them out of the last SSE chunk's `choices`. The OpenAI streaming wire format distributes a single tool call's fields across many delta chunks, and the inline C SSE parser only inspects the last chunk — which carries `finish_reason=tool_calls` but no payload — so the assertions could never evaluate. Mirrors Python's `# skipped: field 'tool_calls' not available on result type` for the same fixture. A delta-merge accumulator is the proper long-term fix; tracked separately.

- fix(alef-e2e/wasm): generate `globalSetup.ts` (which spawns the mock-server and exports `MOCK_SERVER_URL`) for any fixture that needs the mock server, not just `is_http_test()` (which only matched the consumer-style `http: { ... }` shape and missed the entire `mock_response: { ... }` set used by liter-llm). Without globalSetup, every fixture that interpolates `${process.env.MOCK_SERVER_URL}/fixtures/<id>` into a base URL hit `undefined/fixtures/<id>` and `reqwest::Client::builder().build()` resolved to `Err(builder error)` because the URL parser rejected the constructed `Url`. Predicate now uses `Fixture::needs_mock_server()` which covers both schemas; the surrounding comment block already stated the same intent. Lifts liter-llm WASM e2e from 56/161 → 153/161 passing in a single config change.

- fix(alef-backend-java): revert kreuzcrawl-specific `dispatchCrawlError` typed-exception dispatch in the FFI `checkLastError` helper. Commit `44507046` had hardcoded references to `TimeoutException`, `ConnectionException`, `GoneException`, `BadGatewayException`, `BrowserTimeoutException`, `CrawlErrorException` etc. — exception classes that exist in kreuzcrawl but not in any other downstream — into the shared template. Regenerating the Java binding for liter-llm (or any downstream other than kreuzcrawl) produced a `LiterLlmRs.java` referencing undefined symbols and broke `mvn package`. The case-2 branch is now back to the pre-44507046 emission `throw new ConversionErrorException(msg)`. The proper fix — config-driven typed-exception dispatch keyed by error-code mapping — is tracked under iter15's "remove downstream-crate special-casing" plan; kreuzcrawl can re-add its dispatcher via that mechanism (or via a post-process step in its own toolchain) without polluting the shared backend.

## [0.15.30] - 2026-05-10

### Fixed

- fix(alef-backend-ffi): async trait method bodies with `&str` params now capture via `.to_string()` instead of `.clone()`. `.clone()` on `&str` returns `&str` — the borrow escaped into the `spawn_blocking` closure, causing E0521 ("borrowed data escapes outside of method"). The fix is in `gen_async_method_body` in `registration.rs`: `TypeRef::String | TypeRef::Char` with `is_ref=true` now emit `let x = x.to_string()` before the closure.
- fix(alef-backend-ffi): async trait method bodies whose trait return type is an excluded Named type (present in `api.excluded_type_paths`, e.g. `InternalDocument`) now correctly emit `serde_json::from_str::<QualifiedPath>(&json)` in the closure body and `Result<QualifiedPath, _>` in the method signature. Previously a stale IR cache (written before the `sanitize_unknown_types` trait-method exemption was added) rewrote `Named("InternalDocument")` to `TypeRef::String`, causing the codegen to emit `Ok(cs.to_string_lossy().into_owned())` — a `String` where the trait expected `InternalDocument`. The root fix was `alef generate --clean` which invalidates the stale cache; the code generator itself was already correct (verified by new regression test `bug6_async_excluded_type_return_signature_and_deserialization`).

- fix(alef-e2e/rust mock-server): emit `MOCK_SERVERS={...}` (possibly `{}`) unconditionally as a sentinel line so parent-process parsers — Python's conftest, Ruby's spec_helper, etc. — that read until they observe MOCK_SERVERS never block on a `readline()` that the server was never going to emit. Previously the line was only printed when `fixture_urls` was non-empty (host-root fixtures present), and downstreams without any host-root fixtures (liter-llm) timed out their entire e2e suites at conftest setup. The empty `{}` is parsed as a no-op JSON object so no per-fixture env vars get set.
- fix(alef-e2e/ruby): assertion codegen now consults the per-call `[crates.e2e.calls.<x>.overrides.ruby] enum_fields = { ... }` override (in addition to the global `[crates.e2e] fields_enum`) when deciding whether to coerce the Ruby field expression via `.to_s` for `equals` comparisons. Magnus binds Rust enums as Ruby Symbols, so an assertion `expect(result.status).to eq("completed")` fails against a returned `:completed` Symbol — the per-call override is already populated for the C#/Java/Python sides; threading the same map into the Ruby render preserves the single-source-of-truth contract instead of forcing a Ruby-only duplicate of every enum field.
- fix(alef-e2e/elixir): same per-call enum_fields lookup as ruby — Rustler binds Rust enums as Elixir atoms (`:in_progress`), so `String.trim(result.status)` raises `FunctionClauseError`. Threading the per-call map alongside the global `fields_enum` set lets the existing `to_string/1` coercion fire when the operator labels e.g. `status = "BatchStatus"` for the elixir side.
- fix(alef-e2e/elixir): emit the `{:ok, client} = create_client(...)` setup line inside the expects_error branch as well. The non-error path emits this client binding then references `client` in the call expression; the expects_error path was missing the binding, so generated test bodies failed compilation with `undefined variable "client"` for every error_handling fixture.
- fix(alef-e2e/c): pass the FFI's `None` sentinel (`(uint64_t)-1` / `(uint32_t)-1`) — not literal `0` — for the `timeout_secs` and `max_retries` parameters of the generated `*_create_client(...)` invocation, mirroring the alef-backend-{java,csharp} marshalling fix from 0.15.28. Passing `0` made the FFI shim resolve to `Some(Duration::from_secs(0))`, wiring an immediate-deadline reqwest client that aborted every HTTP fixture in the C suite. Adds `<stdint.h>` to the test-file header block so the cast types resolve.

## [0.15.29] - 2026-05-10

### Fixed

- fix(alef-backend-ffi): sync trait method bodies emitting static error messages now coerce bare string literals to `String` via `.to_string()`. Previously, `gen_vtable_call_body(inside_closure=false)` passed the literal directly to `spec.make_error(...)`, producing e.g. `KreuzbergError::Other("nul byte in serialized param doc")` — a `&'static str` — which fails E0308 when the error variant wraps `String`. The fix is in the `make_err` closure in `call_body.rs`: when not inside the async `_SendFn` closure and the message is a quoted string literal, `.to_string()` is appended before passing to `make_error`. The async closure path (`Box::from(...)`) is unaffected.

## [0.15.28]

### Fixed

- fix(alef-backend-java,alef-backend-csharp): pass the FFI `None` sentinel (`{prim}::MAX`, `NaN`) — not zero — when an `Optional<numeric_primitive>` parameter is null. The FFI binding generated by `alef-backend-ffi` decodes `if x == {prim}::MAX { None }` to recover the absent case; the Java/C# marshallers were instead coercing `null` to `0`/`0L`/`0.0`, so a caller passing `null` ended up handing the host crate `Some(0)` — silently colliding with legitimate zero values (e.g. `timeout_secs=Some(0)` was treated as "no timeout" but instead produced an immediate-deadline reqwest client, breaking ~30 e2e tests per language in liter-llm with "error sending request"). Sentinel choice mirrors `alef-backend-ffi/src/gen_bindings/functions.rs::param_optional_numeric_conversion`: unsigned ints use bitwise `-1` (all-bits-set = `u{N}::MAX`), signed ints use the boxed type's `MAX_VALUE`, floats use `NaN`. Bool optionals retain the existing fall-through (separate FFI path).
- fix(alef-e2e/c): align generated Makefile mock-server invocation with the rest of the suite — point `MOCK_SERVER_BIN` at `../rust/target/release/mock-server` (matches python conftest, php bootstrap, java MockServerListener) instead of `../../target/release/mock-server`, and pass the fixtures directory as a positional argument rather than `--fixtures …` (mock-server's CLI takes a single positional `[fixtures-dir]`, so the flag form was being parsed as the fixtures-dir literal — `loaded 0 routes from --fixtures`).
- fix(alef-e2e/typescript): `not_empty` assertion is now polymorphic — strings/arrays still use `length > 0`, but objects use `expect(_v).toBeDefined()` + `.not.toBeNull()`. The previous template assumed string-like fields and emitted `(field ?? "").length > 0`; for object-typed fields like Cohere's `JsRerankResultDocument` (a `{text: string}` wrapper, not a bare string) `({text: "…"} ?? "").length` evaluates to `undefined`, and Vitest reports "actual value must be number or bigint, received undefined". The runtime branch on `typeof === "string"` / `Array.isArray()` keeps the existing string-and-array semantics intact for all current fixtures while extending to object payloads.

## [0.15.26] - 2026-05-10

### Added

- feat(alef-backend-gleam): emit `unregister_fn` and `clear_fn` external Gleam declarations from `TraitBridgeConfig` when the fields are set; short-circuits to no output when `None`. Closes the gap where Gleam silently ignored the optional unregister/clear lifecycle config that other backends already honored.
- feat(alef-backend-go): emit `unregister_fn` and `clear_fn` Go wrappers from `TraitBridgeConfig` when set; short-circuits empty when `None`. Generated wrappers delegate to the host crate's C-exported `kreuzberg_unregister_*(name, &err)` and `kreuzberg_clear_*(&err)` symbols via cgo.
- feat(alef-backend-csharp): replace hardcoded `Unregister{Trait}` P/Invoke generation with config-driven lookup of `bridge_config.unregister_fn`. Previously every C# trait-bridge always emitted an `Unregister*` declaration regardless of host capability; now the declaration and the static `Unregister(name)` C# method are conditional on the config field being set, matching the contract every other backend already honored.
- feat(alef-backend-java): emit `unregister_fn` and `clear_fn` Panama FFM downcall handles + Java methods from `TraitBridgeConfig` when set. Each emits a `Method.invoke(...)` over the configured C symbol with the `FunctionDescriptor.of(JAVA_INT, ADDRESS)` shape and drains the local Java-side bridge map on clear.
- feat(alef-backend-dart): emit Dart-side `unregisterXxx(name)` and `clearXxxs()` static methods on the generated bridge class when `unregister_fn` / `clear_fn` are set, plus a Rust-side `clear_*` forwarder for FRB to bridge. Previously the Rust-side unregister forwarder existed but the Dart caller had no way to invoke clear (clear was emitted in Rust but never surfaced to Dart).
- feat(alef-backend-zig): emit `unregister_fn` and `clear_fn` Zig wrappers from `TraitBridgeConfig` when set. Generated wrappers are thin `extern "C"` passthroughs over the host crate's exported `kreuzberg_unregister_*(name, out_error)` and `kreuzberg_clear_*(out_error)` symbols, returning Zig `i32` return codes.
- feat(alef-backend-swift): emit `unregister_fn` and `clear_fn` swift-bridge `extern "Rust"` declarations + `pub fn` bodies from `TraitBridgeConfig` when set. Generated bodies access the registry directly via `registry_getter()` + `guard.remove(name)` / `guard.clear()` and adapt errors to Swift-friendly `Result<(), String>`. Refactor extracts the trait-bridge codegen into a `SwiftBridgeGenerator: TraitBridgeGenerator` impl, keeping the wrapper-emission path consolidated with the other backends.
- feat(alef-backend-kotlin): introduce `KotlinJvmBridgeGenerator: TraitBridgeGenerator` impl emitting `unregisterXxx(name)` / `clearXxxs()` thin Kotlin wrappers that delegate to the JVM-side `XxxBridge.unregisterXxx` / `clearAllXxx` static methods. Methods short-circuit to nothing when the corresponding config field is `None`.
- feat(alef-backend-wasm): emit a synthetic `pub fn default()` static factory on every wasm-bindgen wrapper struct that derives `Default`. wasm-bindgen mirrors the Rust `(constructor)` arity, so structs with non-Optional fields (e.g. `WasmChatCompletionTool { tool_type, function }`) can only be instantiated with positional args from JS — `new WasmChatCompletionTool()` throws. The factory delegates to `<Self as ::core::default::Default>::default()` so JS callers can obtain a fresh instance and drive it via setters. Skipped automatically when the IR already exposes an explicit `default` method to avoid impl-block conflicts.

### Fixed

- fix(alef-cli): `sync-versions` now writes to root `package.json` and every `crates/*-node/package.json` — both manifests are already validated by `validate-versions`, but were silently absent from the sync writer, so polyrepos with a private pnpm-workspace root manifest (e.g. `kreuzberg-root`) had to bump their version manually before every release. Sync is idempotent and only top-level `"version"` is rewritten — nested dependency specs and `pnpm.overrides` are untouched.
- fix(alef-e2e/typescript): use `.default()` factory for all wasm class instantiations in test bodies, not just `*Config` types. The previous `new WasmFoo()` pattern only worked for structs whose fields were all `Option<T>`; structs with required fields (e.g. `WasmChatCompletionTool`, `WasmFunctionDefinition`, `WasmResponseTool`) caused ~9 e2e tests to fail with "expected instance of WasmFoo" or constructor-arity TypeError. Combined with the new synthetic `default()` factory, `_u = WasmFoo.default()` now works uniformly.
- fix(alef-e2e/python): error-assertion compares the fixture value against EITHER `str(exc_info.value)` OR `type(exc_info.value).__name__`. Different downstream crates use different fixture-shape conventions — kreuzcrawl fixture values are message substrings (`"max_depth"`, `"proxy"`), liter-llm fixture values are class-name prefixes (`"Authentication"`, `"BadRequest"`). The disjunction lets a single codegen path satisfy both without a config flag.
- fix(alef-e2e/csharp): streaming `lastFinishReason` accumulator now uses `JsonNamingPolicy.SnakeCaseLower.ConvertName(...)` instead of `.ToString().ToLower()`. The latter collapses compound PascalCase enum names like `ToolCalls` to `toolcalls` (no underscore), causing equality assertions against fixture wire-form values like `"tool_calls"` to fail. The new emission matches the policy used by the global `JsonStringEnumConverter` and the non-streaming assertion path at `csharp.rs:2087-2094`.
- refactor(alef-e2e): drop hardcoded downstream-crate names from dart/zig/r e2e codegen. dart now emits `import 'package:{pkg_name}/{pkg_name}.dart'` driven by the resolved `[e2e.packages.dart]` config; zig's `build.zig` resolves `pkg_name`, `module_name`, `ffi_lib_name`, and `ffi_crate_path` from config (mirroring the FFI helper pattern) instead of literal `kreuzberg`/`libkreuzberg_ffi`/`crates/kreuzberg-ffi`; the R `setup_fixtures.jinja` template and `r.rs` codegen renamed `.kreuzberg_test_documents` to the alef-internal generic `.alef_test_documents`. None of these change behavior for kreuzberg (defaults preserve existing names) but removes the assumption that the downstream crate is named "kreuzberg".
- feat(alef-core): introduce `[e2e] test_documents_dir` config knob (default `"test_documents"`) and a centralised `E2eConfig::test_documents_relative_from(emission_depth)` helper that computes the relative path from a backend's emission directory to the configured fixture-binary directory. Replaces hardcoded `"../../test_documents"` / `"../../../test_documents"` literals across all 10 backends (dart, ruby, zig, r, gleam, elixir, go, php, csharp, swift, wasm, typescript, java pom, python conftest, rust args) with calls into the helper. Defaults preserve kreuzberg's behaviour; downstreams whose fixtures don't reference files (liter-llm) can leave the default in place — backends already emit the chdir/setup hook conditionally on `has_file_fixtures`. Hand-rolled `Default` impl on `E2eConfig` ensures the field receives `"test_documents"` even when constructed via `..Default::default()` rather than serde-deserialised.
- fix(alef-backend-ffi): use `config.core_import` (already plumbed into the FFI param-conversion path) as the type-name prefix in `type_ref_to_rust_type` instead of hardcoding `"kreuzberg::"`. The prefix governs how named struct/enum types from the consumer crate are referenced in the generated FFI shim; baking the literal name in meant any non-kreuzberg downstream produced unresolved type paths in the emitted Rust.
- refactor(alef-e2e/gleam): drop kreuzberg-specific Gleam codegen helpers. The OTP-application-startup shim now binds the configured `pkg_name` (snake-cased crate name from `[e2e.packages.gleam] name`) so `application:ensure_all_started/1` adapts to the downstream binding; the helper function name is now `start_app/0` (was `start_kreuzberg/0`) — a fixed, downstream-agnostic identifier. The `BatchFileItem`/`BatchBytesItem` `element_type` branches in `build_args_and_setup` and the `build_gleam_extraction_config` / `build_gleam_default_extraction_config` helpers are removed entirely. The `json_object` arg-type arm now falls back to a generic `json_to_gleam` JSON-string emission. `render_tagged_union_assertion` takes the package qualifier as a parameter (resolved by the caller from the gleam package config) instead of hardcoding `"kreuzberg"`. Note: the FFI plugin shim templates `plugin_impl_initialize.jinja` and `plugin_impl_shutdown.jinja` still construct `kreuzberg::KreuzbergError::Plugin { ... }` literals — that's a kreuzberg-specific error contract that needs a separate refactor (would require defining a generic plugin-error trait in alef-core, which downstreams implement). Tracked for iter16.
- feat(alef-core): introduce `[ffi] plugin_error_constructor` config knob — a Rust expression with access to a `msg: String` local that the FFI plugin shim templates (`plugin_impl_initialize`, `plugin_impl_shutdown`) interpolate verbatim when constructing the error value to return. When unset, the shim falls back to `<core_import::error_type as ::core::convert::From<String>>::from(msg)` so any error type implementing `From<String>` works out of the box. Removes the hardcoded `kreuzberg::KreuzbergError::Plugin { ... }` literal that previously locked the FFI plugin path to kreuzberg's specific error variant shape.
- feat(alef-core,alef-e2e/gleam): introduce `[crates.gleam] element_constructors` and `[crates.gleam] json_object_wrapper` config knobs to restore Gleam record-constructor support without baking downstream-specific knowledge into the codegen. `element_constructors` is a list of recipes keyed by the fixture-side `element_type` (e.g. `"BatchFileItem"`) — each declares a Gleam constructor identifier and a typed list of fields (`file_path` for fixture-relative paths, `byte_array` for `<<n1, n2, …>>` BitArrays, `string` with default, `literal` for constants). `json_object_wrapper` is a single template (`{json}` placeholder) used when no element_type matches; downstreams whose Gleam binding parses a JSON string into a config record (e.g. `kreuzberg.config_from_json_string({json})`) declare it once instead of having alef know the field shapes. Replaces the iter15-removed `BatchFileItem`/`BatchBytesItem`/`build_gleam_extraction_config` helpers.

## [0.15.25] - 2026-05-10

### Fixed

- fix(alef-e2e/typescript): emit `globalSetup` when any fixture uses mock-server — Vitest config now includes the global-setup hook for HTTP-bound or `mock_response`-tagged fixtures so the mock-server binary actually spawns before the test run starts.
- fix(alef-e2e/python): wrap `arg_bindings` in `pytest.raises(...)` and assert on `str(exc_info.value)` — error fixtures that include argument-construction expressions now catch deserialize-time failures inside the `with` block instead of letting them escape before the assertion.
- fix(alef-e2e/typescript): recursively walk nested type fields for wasm imports — class types referenced two or more levels deep (e.g. `WasmChatCompletionRequest.tools[].function: WasmFunctionDefinition`) were emitted in test bodies via `new WasmFunctionDefinition()` but missing from the import statement, causing `ReferenceError: WasmFunctionDefinition is not defined` at runtime. The single-level `derive_nested_types_for_wasm` is now wrapped by `collect_transitive_nested_types_for_wasm`, a BFS over the wasm class graph that follows every struct-typed field through `Vec`/`Optional`. Terminates on cycles via a `seen` set on wasm class names.
- fix(alef-cli): merge `excluded_type_paths` from each per-crate extraction in the multi-source extract pipeline so trait_bridge codegen can resolve qualified paths for excluded types referenced across crate boundaries.
- chore(alef): add `excluded_type_paths` field to all `ApiSurface { ... }` test literals across the workspace (~50 backend test/bench files) and `serde_flatten: false` to the matching `FieldDef` literals in the bench/snapshot suites. Mass-fix follow-up to the `feat(extract): preserve excluded type paths for trait_bridge codegen` IR change so `cargo check` and `cargo test` both stay green.

### Changed

- chore: cargo fmt sweep and sync `Cargo.lock` to 0.15.24 release pin.

## [0.15.24] - 2026-05-10

### Added

- feat(alef-core): `SelectWhen` enum and `resolve_call_for_fixture` — named call configs in `[e2e.calls.*]` can now declare `select_when = { input_has = "<key>" }` to auto-route fixtures whose input contains that key, without requiring an explicit `"call"` field on every fixture. All per-language e2e generators now use `resolve_call_for_fixture` instead of `resolve_call` so auto-routing applies everywhere.
- feat(alef-e2e/go): wrap engine creation in error-return for validation fixtures — when a test fixture has `type=error` assertions, `build_args_and_setup` now emits `return` instead of `t.Fatalf` if `CreateEngine` itself fails, so validation errors from engine creation satisfy the error assertion rather than failing the test.

### Changed

- alef-e2e/typescript: auto-derive nested-type wrapping from IR field types — `ts_builder_expression_inner` now resolves class-typed fields (`TypeRef::Named` and `TypeRef::Vec(Named)`) from the type registry, removing the need for manual `nested_types` mappings in alef.toml call overrides. Explicit overrides still win on collision.

### Added

- feat(alef-docs): render enums in shared `types.md` and `configuration.md` reference files. Previously enum variant tables only appeared in per-language `api-{lang}.md`, so downstream guides could not deep-link to enum sections from the language-neutral shared pages. `types.md` now gets every public enum under a new `### Enums` section; `configuration.md` gets the subset referenced as field types of any rendered config struct (matching on `TypeRef::Named` recursing through `Optional`/`Vec`/`Map`). Kreuzberg's `LayoutClass`, `HtmlTheme`, `TableModel`, `KeywordAlgorithm`, `OutputFormat`, `ResultFormat` (and analogous enums in any other downstream crate) now have stable `#enumname` anchors in both shared files. The shared rendering also adds a `Wire value` column when the enum carries `#[serde(rename_all = "...")]` or any per-variant `#[serde(rename = "...")]`, so users see the actual JSON/TOML token (e.g. `"default"`, `"slanet_wired"`) alongside the Rust variant name.

- feat(alef-core/ir): plumb `FieldDef.serde_flatten: bool` from the Rust source through the IR. Backends use this to emit language-native flatten support — see `feat(java-backend, csharp-backend)` below. The extractor recognises `#[serde(flatten)]` (also under `#[cfg_attr(...)]`) by anchoring on the `flatten` token boundary in the token-stream string.

- feat(java-backend, csharp-backend): emit language-native `#[serde(flatten)] serde_json::Value` support — Java records get `@JsonAnyGetter Map<String, Object>` on the record component plus a matching `@JsonAnySetter <field>Entry(String, Object)` on the builder (with `@JsonIgnore` on the regular `with<Field>(Map)` setter so a wire field of the same name is not miscast as a Map). C# classes get `[JsonExtensionData] Dictionary<string, JsonElement>` instead of `[JsonPropertyName(...)]`. Both implement the serde flatten semantic for types like `ResponseTool { tool_type, #[serde(flatten)] config: Value }` whose wire JSON is `{"type":"function","name":"f","description":"d"}` — without this, Jackson rejected `Unrecognized field "description" (class ResponseToolBuilder)` on every chat-tools request.

- alef-core: `CallOverride.assert_enum_fields` per-call override for routing result-field enum types in assertions.
- alef-e2e/python: thread per-call `assert_enum_fields` through `render_assertion` so same-named fields (e.g. `status`) can be enum in one call and string in another.

### Fixed

- alef-codegen, alef-docs: add `serde_flatten: false` to test-site `FieldDef` literals that were missing the field after iter10's plumbing.
- alef-e2e/java: add TCP-readiness probe to MockServerListener — polls the bound mock-server URL until accepting (max 5s, 50ms backoff) before releasing the JUnit launcher session, preventing intermittent `error sending request` failures under Surefire parallel execution.
- alef-e2e/csharp: add TCP-readiness probe to TestSetup `[ModuleInitializer]` — same polling logic; eliminates intermittent failures under xUnit class-parallel default.
- alef-e2e/typescript: emit numeric bracket access for digit-only JSON pointer segments in test assertions — e.g. `result.results[0]` instead of `result.results["0"]`. The string-keyed access returned `undefined`, breaking array-element assertions.

- fix(csharp-backend): branch sealed-union `JsonConverter<T>` Read on variant shape — struct variants (named fields like `OcrDocument::Url { url: String }`) skip the `"Value"` wrap so `JsonSerializer.Deserialize<Variant>(...)` sees `{"url":"..."}` directly and can match the `[JsonPropertyName("url")]` annotation on the variant record's positional component. Tuple variants (single-field tuple of named struct, e.g. `Message::User(UserMessage)`) keep the `"Value"` wrap as before. Without this, every struct-variant tagged union failed to round-trip — Rust serde rejected the FFI request with `missing field 'url' at line 1 column 73` because the C# layer dropped the field on serialize when the converter could not even deserialize it on the way in. The Read method now produces both `flatJson` (no wrap) and `wrappedJson` and dispatches per variant via the `is_tuple` IR flag.

- fix(e2e/csharp): env-gate live smoke fixtures (no `mock_response`, no `http` override, but `env.api_key_var` set) via early-return when the env var is unset. Without the gate, `task csharp:e2e:test` failed on `smoke_chat_anthropic`, `smoke_chat_gemini`, `smoke_chat_openai`, `smoke_streaming_openai`, `smoke_embed_openai`, `smoke_list_models_openai`, `smoke_provider_routing`, `smoke_cache_memory` with `not found: No mock route for /fixtures/<id>/...` because the mock-server has no fixture for them — they target real provider APIs. Mirrors the Elixir / Python conftest pattern. Both the regular `render_test_method` and `render_chat_stream_test_method` paths gain the gate.

- fix(java-backend): wrap `<Type>.fromJson(json)` deserialization failures in `<Crate>Exception` (the binding's checked exception class) instead of bare `RuntimeException`. The catch block now emits `throw new <Crate>Exception("Failed to parse <Type> from JSON: " + e.getMessage(), e)` and the method signature gains `throws <Crate>Exception` so callers must handle/declare it. Lets error fixtures asserting `assertThrows(<Crate>Exception.class, ...)` catch deserialize-time failures (e.g. `Cannot construct instance of FilePurpose, problem: Unknown value: invalid-purpose`) the same way they catch FFI failures. Mirror change in `untagged_union_wrapper.jinja`.

- fix(csharp-backend): emit a `public static T FromJson(string json)` factory on every generated record/class that wraps `System.Text.Json.JsonException` (and any other deserialization failure) in `<Crate>Exception`. The factory uses a `JsonOptions` field that mirrors the e2e-harness `ConfigOptions` (`JsonStringEnumConverter(SnakeCaseLower)` + `WhenWritingDefault`), so round-trips stay consistent with the FFI request-serialization path. Without this, malformed input JSON surfaced as a raw `JsonException` that error fixtures' `Assert.ThrowsAny<<Crate>Exception>(...)` did not catch. The e2e codegen now emits `<OptionsType>.FromJson("...")` instead of inlined `JsonSerializer.Deserialize<T>("...", ConfigOptions)!` so the wrap actually intercepts the parse step.

- fix(csharp-backend): generate a custom `JsonConverter<T>` for enums whose `#[serde(rename_all)]` is non-snake (`kebab-case`, `SCREAMING-KEBAB-CASE`, `camelCase`, `PascalCase`), not just for enums with explicit per-variant `serde_rename`. The global `JsonStringEnumConverter(SnakeCaseLower)` previously emitted `"fine_tune"` for `FilePurpose::FineTune` (`#[serde(rename_all = "kebab-case")]`), producing `"could not convert to FilePurpose"` on every read of `"fine-tune"`. The custom converter now explicitly maps `"fine-tune" → FilePurpose.FineTune` and writes `"fine-tune"` on the way back. Property-level `[JsonConverter(typeof(<Enum>JsonConverter))]` is also emitted on enum-typed fields so the override wins over the global naming policy.

- fix(e2e/java): wrap `setup_lines` (which may include `<Type>.fromJson(...)` calls that throw on malformed JSON) plus the `call_expr` inside the `assertThrows` lambda for `expects_error` fixtures. Without this, deserialize-time failures escaped before `assertThrows` could catch them — `var request = OcrRequest.fromJson("...invalid-purpose...")` threw outside the assertion. Mirrors the C# `Assert.ThrowsAnyAsync(() => client.X(Type.FromJson(...)))` pattern.

- fix(php-backend): do not append "Async" suffix to async method names in wrapper facade. The PHP binding blocks on async internally via `block_on`, presenting a synchronous API to callers. Wrapper facade methods now use the exact name from the IR (e.g. `scrape()` instead of `scrapeAsync()`), matching the configured function name in alef.toml e2e overrides. The underlying extension methods retain the "\_async" suffix for internal delegation.

- fix(magnus-backend): emit valid serde attribute `#[serde(default = "default_timeout")]` (function path) instead of `#[serde(default = "30000")]` (literal string) for Duration-typed timeout fields. The invalid literal syntax caused compilation errors in Ruby e2e bindings. Added helper function `default_timeout() -> u64` to struct templates.

### Added

- feat(e2e/java): emit a JUnit Platform `LauncherSessionListener` (`MockServerListener`) and the matching SPI manifest under `src/test/resources/META-INF/services/` to spawn the mock-server binary once per launcher session whenever any fixture is HTTP-bound or carries `mock_response`. Mirrors the Ruby `spec_helper.rb` / Python `conftest.py` / Node `globalSetup.ts` / C `Makefile` spawn patterns. The listener parses the `MOCK_SERVER_URL=...` line from mock-server stdout, exposes it as the `mockServerUrl` JVM system property, and tears the child down via stdin-close on `launcherSessionClosed` (with a 2s timeout fallback to `destroyForcibly`). A pre-set `MOCK_SERVER_URL` env var skips the spawn entirely (CI / external orchestration). The pom.xml template gains a `dependencyManagement` import of `org.junit:junit-bom` and an explicit `org.junit.platform:junit-platform-launcher` test dependency so the SPI lookup actually finds the listener interface. Generated test bodies now read `System.getProperty("mockServerUrl", System.getenv("MOCK_SERVER_URL"))` so external overrides still work without going through JNI's lack of `setenv`. Without this, every fixture-bound Java test failed with `LiterLlmRsException: error sending request for url (null/fixtures/<id>/...)` whenever `task java:e2e:test` ran standalone, because nothing in the Java toolchain spawned the mock-server.

- feat(e2e/csharp): extend the `[ModuleInitializer] TestSetup.Init` block to spawn the mock-server binary before the assembly loads whenever any fixture is HTTP-bound or carries `mock_response`. Walks ancestor directories of `AppContext.BaseDirectory` to locate the repo root (the directory containing `test_documents/`), resolves `e2e/rust/target/release/mock-server` (`.exe` on Windows), starts it via `Process` with redirected stdin/stdout/stderr, parses the `MOCK_SERVER_URL=...` line, and calls `Environment.SetEnvironmentVariable("MOCK_SERVER_URL", url)`. Drains stdout/stderr in background daemon threads to keep the child unblocked, and registers an `AppDomain.CurrentDomain.ProcessExit` handler that closes the child's stdin (mock-server treats stdin EOF as a shutdown signal) and falls back to `Process.Kill(true)` after a 2s grace. Honors a pre-set `MOCK_SERVER_URL` by skipping the spawn (CI / external orchestration). Without this, `task csharp:e2e:test` standalone produced 47/161 with 114 `LiterLlmException : builder error` failures because reqwest rejected the relative path `"" + "/fixtures/<id>"` when `Environment.GetEnvironmentVariable("MOCK_SERVER_URL")` returned null.

- feat(csharp-backend): emit a proper `Write` method for sealed-union `JsonConverter<T>`. The previous implementation threw `NotSupportedException` from `Write`, so any C# binding that serialized a sealed union (e.g. a `Message.User` instance inside a `ChatCompletionRequest`) failed at the FFI marshalling step with `Message serialization is not supported`. The new converter mirrors the Java sealed-union serializer pattern: switch on the variant, write the discriminator tag, then flatten the inner record's fields alongside the tag (so `Message.User(UserMessage value)` round-trips as `{"role":"user","content":...}` not `{"value":{...}}`). Emitted via a new `sealed_union_converter.jinja` template that owns both the Read and Write methods and replaces ~190 lines of inline `out.push_str` calls.

### Fixed

- fix(csharp-backend): null-marshal `Optional<&T>` (`TypeRef::Named` + `optional: true`) parameters by passing `IntPtr.Zero` instead of round-tripping the literal string `"null"` through `<Type>FromJson`. Without this, `client.ListBatches(null)` against `Option<&BatchListQuery>` failed with `invalid type: null, expected struct BatchListQuery`. Mirrors the Java fix for the same IR shape. Emitted via a new `named_param_handle_from_json_optional.jinja` template.

- fix(csharp-backend): respect `#[serde(rename = "...")]` when emitting `[JsonPropertyName("...")]` on record properties. Previously the generator hardcoded `field.name` as the JSON wire name, so fields like `tool_type` (renamed via `#[serde(rename = "type")]`) round-tripped as `"tool_type"` on the wire — Rust serde then rejected the request with `unknown field tool_type, expected type or function`. Now uses `field.serde_rename` when set, falling back to `field.name`.

- fix(csharp-backend): retrieve the actual FFI error message instead of throwing a generic `"<NativeMethod> failed"`. The `null_result_throw.jinja` template now reads `LastErrorCode()` + `LastErrorContext()` and surfaces the underlying Rust error (e.g. `"missing field 'role'"`, `"invalid type: null, expected struct BatchListQuery"`) so callers see the real cause instead of a meaningless wrapper.

- fix(csharp-backend): check the return value of `<Type>FromJson` and throw the actual error before passing the (potentially-null) handle to the next FFI call. Without this, a malformed serialized JSON silently returned `IntPtr.Zero`, then the next FFI call failed with `Null pointer passed for parameter 'req'` — masking the real serialization error. The `named_param_handle_from_json.jinja` template now checks the handle and throws with the underlying error context. Same pattern as the existing Java emission.

- fix(e2e/csharp): use `JsonNamingPolicy.SnakeCaseLower.ConvertName(value.ToString())` for enum-equality assertions instead of `.ToString()?.ToLower()`. C# enum members are PascalCase (`InProgress`, `ContentFilter`, `ToolCalls`), so `.ToLower()` produces `inprogress`, `contentfilter`, `toolcalls` — none of which match the snake_case wire format (`in_progress`, `content_filter`, `tool_calls`) that the global `JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower)` actually emits.

- fix(java-backend, csharp-backend): handle `#[serde(rename_all = "kebab-case")]` (and `SCREAMING-KEBAB-CASE`) on enums. Both `apply_rename_all` helpers had no `kebab-case` arm, so they fell through to `name.to_lowercase()`, producing `FineTune` → `"finetune"` instead of `"fine-tune"`. JSON values like `"fine-tune"` then failed to deserialize with `Cannot construct instance of FilePurpose, problem: Unknown value: fine-tune` (3 Java tests, all C# file-purpose tests).

- fix(java-backend): null-marshal `TypeRef::Named` parameters with `optional: true`. The IR represents `Option<&BatchListQuery>` as `TypeRef::Named("BatchListQuery") + optional: true` after FFI extraction strips the `Option`. The Java instance-method emitter only branched on `TypeRef::Optional(Named)`, taking the unconditional path for `Named + optional`: `STREAM_MAPPER.writeValueAsString(null)` yielded the literal `"null"`, then `LITERLLM_BATCH_LIST_QUERY_FROM_JSON("null")` failed inside Rust serde with `invalid type: null, expected struct BatchListQuery at line 1 column 4`. The path now branches on `p.optional` for `TypeRef::Named` and emits `if (param == null) { cParam = MemorySegment.NULL; } else { ... }`, mirroring the existing `TypeRef::Optional(Named)` arm. Fixes `client.listBatches(null)` / `client.listFiles(null)` against `Option<&BatchListQuery>` / `Option<&FileListQuery>` (4 Java tests).

- fix(java-backend): emit `@JsonProperty(<wire-name>)` on builder field declarations when the core field has explicit `#[serde(rename = "...")]`. Without this, fields like `StreamToolCall.call_type` (renamed from `r#type` to `call_type` to avoid Java's contextual-keyword conflict, with `#[serde(rename = "type")]` to keep wire-name `"type"`) deserialize JSON like `{"type":"function"}` via Jackson's `BuilderBasedDeserializer` and fail with `Unrecognized field "type" (class StreamToolCallBuilder), not marked as ignorable (4 known properties: function, index, id, call_type)`. The builder field declarations now mirror the record-component annotation so wire-name lookup succeeds.

### Added

- feat(csharp-backend): emit JsonElement-wrapper records for `#[serde(untagged)]` enums, mirroring the Java backend's Stage A wrapper pattern. Untagged unions like `EmbeddingInput = Single(String) | Multiple(Vec<String>)`, `UserContent = String | Vec<UserContentBlock>`, `ToolChoice = String | Object`, `StopSequence = String | Vec<String>` previously emitted as plain C# `enum`s; System.Text.Json's default enum converter rejected any non-variant-name value (e.g. `"Hello world"` for `EmbeddingInput`) with `JsonException: The JSON value could not be converted to LiterLlm.EmbeddingInput`. The new emission produces a `sealed class T : IEquatable<T>` holding a `JsonElement Value`, with a paired `JsonConverter<T>` that round-trips the raw JSON. Static factories (`Of(string)`, `Of(IEnumerable<string>)`, `OfObject(object)`, `FromJson(string)`) and probe accessors (`AsString()`, `AsList()`, `AsObject()`) keep ergonomic construction available. Field references stay strongly typed (e.g. `EmbeddingRequest.Input` is `EmbeddingInput`, not `JsonElement`) since `gen_bindings/mod.rs.complex_enums` is now empty — the wrapper class itself is the proper field type. Emitted via a new `untagged_union_wrapper.jinja` template registered in `template_env.rs`.

### Fixed

- fix(e2e/csharp): merge per-call C# `enum_fields` into the effective field-enum set used by `render_assertion`. The C# codegen resolved `enum_fields` once from the file-level `[crates.csharp].enum_fields` (top-level) and ignored per-call overrides like `[crates.e2e.calls.retrieve_batch.overrides.csharp].enum_fields`, so call-specific enum-typed result fields (e.g. `status` returning `BatchStatus` on retrieve_batch / cancel_batch / create_batch) never triggered the enum-coercion branch (`?.ToString()?.ToLower()`). The result was `result.Status!.Trim()` against `BatchStatus?` — a compile error (`'BatchStatus?' does not contain a definition for 'Trim'`). `render_test_method` now merges `cs_overrides.enum_fields` keys into the effective set before passing it to `render_assertion`, mirroring the Java fix from 5692d176.

- fix(java-backend): mark the untagged-union wrapper boilerplate (`asString` / `asList` / `asObject` / `equals` / `hashCode` / `toString` block, ~170 tokens) with `// CPD-OFF` / `// CPD-ON` sentinels so PMD's copy-paste detector does not flag the intentional duplication across `EmbeddingInput`, `ModerationInput`, `RerankDocument`, and other untagged-wrapper classes. The duplication is the price of avoiding a generic base class; the sentinels are the standard CPD suppression mechanism. Liter-llm's `packages/java/pom.xml` separately bumps `<minimumTokens>` to 200 to clear the pre-existing 124-token DefaultClient FFI byte-buffer block that the maven-pmd-plugin's `cpd-check` goal also flags.

- fix(e2e/brew): emit `jq -r '<path> // empty'` for `is_empty` assertions so a JSON `null` value becomes an empty string instead of the literal `"null"`. Without this, fixtures that asserted `is_empty` on optional fields (e.g. `metadata.og_title` on a page that omits Open Graph tags) failed because `jq -r .metadata.og_title` prints `null` and the bash assertion treated that as a non-empty value. Restores brew/CLI e2e parity with the other backends, which compare against language-native null.

- fix(e2e/java): merge per-call `enum_fields` into the file-level java enum_fields used by `render_assertion`. The Java codegen resolved enum_fields once from `[crates.e2e.call.overrides.java].enum_fields` (top-level) and ignored per-call overrides like `[crates.e2e.calls.chat.overrides.java].enum_fields`, so call-specific enum-typed result fields (e.g. `choices[0].finish_reason` returning `FinishReason`, `status` returning `BatchStatus` on retrieve_batch/cancel_batch) never triggered the Optional<Enum> coercion path added in 51fb7656. The result was `Optional.ofNullable(x).orElse("")` against `Optional<FinishReason>` (`incompatible types: java.lang.String cannot be converted to FinishReason`) and `BatchStatus.trim()` (`cannot find symbol: method trim()`). `render_test_method` now merges `call_overrides.enum_fields` over the file-level map before passing it down so per-call enum classifications take precedence.

- fix(napi-scaffold): include `napi/serde-json` feature for crates with Json fields. The NAPI scaffolder emitted `napi = { version = "3", features = ["async"] }` unconditionally, but when the API surface contained any `TypeRef::Json` field (e.g. `data: Option<serde_json::Value>`), the binding failed to compile with `error[E0277]: the trait 'ToNapiValue' is not implemented for 'std::option::Option<Value>'` because the `napi/serde-json` feature is required to marshal JSON types. The scaffolder now detects Json types recursively across struct fields, method parameters/returns, function parameters/returns, and enum variants; when any Json type is found, it adds `"serde-json"` to the napi features list. This restores kreuzcrawl-node and other bindings with Json fields to compatibility without hand-edits.

- fix(e2e/rust): support array-form `input.mock_responses` schema in mock-server template. The Rust e2e test template's `render_mock_server_setup` only recognized the single-response schema (`mock_response: { status, body, stream_chunks, headers }`), silently discarding the multi-response array schema (`input.mock_responses: [{ path, status_code, headers, body_inline, body_file }, ...]`) used by kreuzcrawl and other fixture-heavy projects. The kreuzcrawl standalone mock-server (`tools/mock-server/src/main.rs`) correctly loaded 452 routes from 246 fixture files, but the alef-generated `e2e/rust/src/main.rs` loaded only 20, because the template never examined `fixture.input.get("mock_responses")`. The fix extends `render_mock_server_setup` to first check for the array schema (extracting `path`, `status_code`, `headers`, and `body_inline` from each element; defaulting path to "/" and status to 200), fall back to the single-response schema if absent, emit multiple `MockRoute` objects when the array contains multiple elements, and emit a single route when either schema produces exactly one response. The standalone binary's `load_routes_recursive` function continues to handle both schemas including `body_file` (fixture-relative file loading), while the test-function template (which lacks fixture-dir context) emits placeholder bodies for `body_file` entries — the binaries will be tested separately against the real file paths. Restores e2e fixture coverage for array-based mock-response projects.

## [0.15.23] - 2026-05-09

### Fixed

- fix(scaffold-swift): drop `swift-actions/setup-swift@v2` from the generated Swift CI workflow and rely on the macos-latest runner's bundled Xcode toolchain. The third-party action installs a Swift 6.0 toolchain that is binary-incompatible with Xcode 16.4's XCTest framework, so swiftpm test bundles fail to load `XCTestCore.framework` at runtime (`Library not loaded: @rpath/XCTestCore.framework/Versions/A/XCTestCore`). The macos-latest runner already ships Xcode 16+ with Swift 6 on `PATH`, so no extra setup step is required. The step is replaced with a diagnostic `swift --version` step.

## [0.15.22] - 2026-05-09

### Fixed

- fix(backend-swift): re-wire `emit_e2e_wrappers` and `emit_json_factory_shims` so the kreuzberg-style e2e helpers (`extractionConfigFromJson`, `batchBytesItemFromJson`, `batchFileItemFromJson`) are emitted again. Emission is gated structurally on the api surface exposing all three serde-enabled types (`ExtractionConfig`, `BatchBytesItem`, `BatchFileItem`) — this keeps the helper out of binding crates that don't expose those types while restoring the symbols the alef-e2e Swift codegen still calls (`src/e2e/codegen/swift.rs:762`). Without the helpers, every kreuzberg-derived Swift test suite failed at compile with `cannot find 'extractionConfigFromJson' in scope`. Also drops the broken multi-line `out.push_str` in `batchExtractBytesSync` that produced stray indentation.

## [0.15.21] - 2026-05-09

### Fixed

- fix(scaffold-swift): replace the unsafe `printf "import RustBridgeC\n$(cat …)"` form in the generated Swift workflow and BUILDING.md template with `{ echo "import RustBridgeC"; cat …; }`. printf interprets `%` and `\` sequences in its format string, so the substituted swift-bridge source content was being silently corrupted whenever it contained those characters — surfacing in CI as `cannot find type 'RustString' in scope` errors in hand-authored Swift sibling files (e.g. `Plugins.swift`) because the copied `SwiftBridgeCore.swift` lost its `RustString` declaration. The new echo+cat form has no format-string interpretation and is robust against arbitrary byte content. Applied to the `swift.yml` workflow Copy step and to both the debug and release sections of `BUILDING.md`. Regression assertions added to `test_scaffold_swift`.

## [0.15.20] - 2026-05-09

### Added

- feat(backend-swift): emit `extern "Swift"` plugin trait bridges so a Swift class can implement a Rust plugin trait and have Rust call back into Swift. For each configured `[[crates.trait_bridges]]` entry the swift bridge crate now produces, alongside the existing outbound `{Trait}Box` glue: an `extern "Swift" type Swift{Trait}Box` declaration with one FFI shim per method (plus `name`/`version`/`initialize`/`shutdown` Plugin shims), a Rust `Swift{Trait}Wrapper` newtype with `OnceLock<String>` name cache and `unsafe impl Send + Sync`, a `Plugin` super-trait impl forwarding to the Swift box (mapping `Result<_, String>` to `KreuzbergError::Plugin`), an `#[async_trait] impl {Trait}` forwarding each method through the Swift FFI shims with JSON marshalling for complex types, and `register_*`/`unregister_*` free functions exposed via an additional `extern "Rust"` block (`#[swift_bridge(swift_name = "registerXxx")]`) that wrap the wrapper in `Arc` and insert it into the configured `registry_getter`. Complex types are JSON-bridged at the FFI boundary (every `Named` type, all `Optional`/`Map`/non-leaf `Vec`); primitives, `String`, `Vec<u8>`, and `Vec<leaf>` pass through directly. ARC lifecycle: swift-bridge's `extern "Swift" type` declaration retains the Swift instance via `Unmanaged<T>.passRetained` and releases it on `Drop`, so storing the wrapper in a process-wide `Arc<dyn Trait>` registry is ARC-safe — the Swift instance lives until the registry's last `Arc` clone is dropped. The generated `Cargo.toml` gains an `async-trait = "0.1"` dependency since the inbound trait impl uses `#[async_trait]` to mirror the source crate's async trait shape. User-facing `Sources/<Module>/Plugins.swift` (Swift protocols + `Swift{Trait}Box` adapter classes) is left for the consuming binding to author by hand — swift-bridge requires a Swift class named exactly `Swift{Trait}Box` with the matching `alefSwift*` method names, which the Rust-side codegen documents in struct doc comments.

## [0.15.19] - 2026-05-09

### Fixed

- fix(backend-swift): drop the bogus `.toString()` suffix on `String` and `Json` returns from convenience overloads. swift-bridge maps a Rust `String` return (bare or inside `Result<String, _>`) directly to a Swift-native `String` — there is no `RustString` wrapping to unwrap. Calling `.toString()` on a Swift `String` failed at the call site with `error: value of type 'String' has no member 'toString'`. The conversion suffix is now only emitted for `RustVec<T>` returns (Bytes, `Vec<primitive>`), where `.map { $0 }` is still needed to materialise the `Sequence` into a Swift `Array`.

- fix(backend-swift): skip emitting convenience overloads when the public wrapper name would collide with the bridge function name (i.e. when the IR function name has no `_sync` suffix to strip). The v0.15.18 fix incorrectly assumed Swift overload resolution would disambiguate the labeled overload from the unlabeled bridge function based on argument-label inclusion — in practice Swift's resolver still treats the in-file labeled candidate as a match for the unlabeled positional inner call, producing `error: no exact matches in call to global function 'renderPdfPageToPng'` and `'detectMimeTypeFromBytes'`. Module-prefix qualification (`RustBridge.fn(...)`) is rejected by Swift for free functions imported from another module, so there is no clean disambiguation path. We now skip the convenience overload entirely when `wrapper_name == swift_inner`; the bridge function remains callable directly with `makeByteVec(...)`. Restores local `swift build` against `kreuzberg/packages/swift` to a clean compile.

## [0.15.18] - 2026-05-09

### Fixed

- fix(backend-swift): drop the bogus `RustBridge.` qualifier introduced in v0.15.16. Swift rejects `ModuleName.functionName` qualification of free functions imported from another module — the v0.15.16 emission `RustBridge.detectMimeTypeFromBytes(...)` failed at the call site with `error: module 'RustBridge' has no member named 'detectMimeTypeFromBytes'`, even though the function is `public func` at module scope in the swift-bridge-generated source. The original shadowing diagnosis was wrong: convenience overloads use a labeled first parameter (`content: String` / `content: [UInt8]` / `path: String`), the underlying bridge function uses an unlabeled first parameter (`_ content: RustVec<UInt8>`), and Swift overload resolution disambiguates by argument-label inclusion. A positional inner call `detectMimeTypeFromBytes(makeByteVec(...))` resolves unambiguously to the bridge function regardless of which other same-named labeled overloads exist in the same module. The codegen now always emits the unqualified inner call, restoring local `swift build` against `kreuzberg/packages/swift` to a clean compile.

## [0.15.17] - 2026-05-09

### Added

- feat(backend-dart): emit `register_*` / `unregister_*` forwarders for `[[trait_bridges]]` entries that set `register_fn` (and optionally `unregister_fn`) plus `registry_getter`. The Dart backend previously emitted only the `{Trait}DartImpl` opaque struct, the trait impl, and the `create_*_dart_impl` factory — leaving Dart consumers no way to actually install their callbacks into the host registry. The codegen now appends `pub fn {register_fn}(impl_: {Trait}DartImpl) -> Result<(), String>` (wrapping `impl_` in `Arc<dyn Trait>` and calling `registry_getter().write().register(arc{register_extra_args})`) and, when `unregister_fn` is set, `pub fn {unregister_fn}(name: String) -> Result<(), String>` (calling `registry_getter().write().remove(&name)`). Both forwarders stringify the host-typed error since FRB requires owned, FFI-safe error types. FRB auto-bridges these `pub fn` items, so Dart sees `Future<void> registerOcrBackend(OcrBackendDartImpl impl_)` / `Future<void> unregisterOcrBackend(String name)` directly. Going through the registry handle (rather than the host crate's `register_*` free function) sidesteps `pub(crate)` / `#[cfg(test)]` restrictions on trait registration wrappers, notably for `EmbeddingBackend`. Closes the last gap for Dart plugin trait callbacks.

## [0.15.16] - 2026-05-09

### Fixed

- fix(backend-swift): qualify the convenience-overload inner call with `RustBridge.` when the wrapper has the same Swift name as the underlying bridge function. When the IR function name lacked a `_sync` suffix to strip, `wrapper_name` equalled `swift_inner` and the same-module convenience overloads (e.g. `detectMimeTypeFromBytes(content:)` and `renderPdfPageToPng(content:pageIndex:dpi:password:)`) shadowed the imported bridge declarations from `RustBridge`. The inner call `try detectMimeTypeFromBytes(makeByteVec(...)).toString()` resolved to the convenience overload itself — returning a Swift `String` that has no `.toString()` member — and `try renderPdfPageToPng(makeByteVec(content), pageIndex, dpi, password)` produced `error: no exact matches in call to global function 'renderPdfPageToPng'` because the same-module candidates require labels (`content:pageIndex:dpi:password:`). The codegen now emits `RustBridge.<name>(...)` whenever the wrapper would otherwise self-shadow, routing the call to the positional swift-bridge wrapper without relying on import-shadow resolution order.

## [0.15.15] - 2026-05-09

### Fixed

- fix(scaffold-swift): generated `.github/workflows/swift.yml` now builds the `{crate}-swift` Rust crate and copies the swift-bridge generated headers and Swift sources into `Sources/RustBridgeC/` and `Sources/RustBridge/` before invoking `swift build`. Previously the workflow ran `swift build` directly with `working-directory: packages/swift`, but `Sources/RustBridgeC/RustBridgeC.h` is only a placeholder until `cargo build -p {crate}-swift` produces the real combined header in `target/debug/build/{crate}-swift-*/out/`. CI failed with hundreds of `cannot find '__swift_bridge__$Vec_<Type>$<method>' in scope` errors because the C symbols and Swift bridge wrappers were never copied into the SwiftPM targets. The new workflow installs the Rust toolchain before Swift, runs `cargo build -p {crate}-swift` at the repo root, concatenates `SwiftBridgeCore.h` and `{crate}-swift/{crate}-swift.h` into `Sources/RustBridgeC/RustBridgeC.h`, prepends `import RustBridgeC` to the generated `SwiftBridgeCore.swift` and `{crate}-swift.swift` files (writing them into `Sources/RustBridge/`), then runs `swift build` and `swift test` from `packages/swift`. Mirrors the canonical sequence documented in the scaffolded `BUILDING.md`.

## [0.15.14] - 2026-05-09

### Fixed

- fix(scaffold-kotlin): exclude the alef-emitted Kotlin binding-class file (e.g. `Kreuzberg.kt`) from the ktlint check in the generated `packages/kotlin/build.gradle.kts`. The alef-backend-kotlin emits the binding object with parameters on a single line, no expression bodies, and blank lines inside method blocks — patterns that ktlint flags as `parameter-list-wrapping`, `function-expression-body`, and `no-blank-lines-in-block` violations. The scaffold's `ktlint { filter { ... } }` block now adds `exclude { entry -> entry.file.toString().endsWith("/<BindingClass>.kt") }` (where `<BindingClass>` is `config.name` pascal-cased) so the generated file passes CI without hand-edits while still linting all user-authored Kotlin sources.

## [0.15.13] - 2026-05-09

### Fixed

- fix(scaffold-gleam): bump `gleam-version` in the generated `.github/workflows/gleam.yml` from `1.4` to `1.14`. The latest `gleam_stdlib` requires Gleam `>= 1.14.0`, so CI failed with `Incompatible Gleam version` on every fresh scaffold using the default toolchain pin.

- fix(scaffold-swift): bump `swift-version` in the generated `.github/workflows/swift.yml` from `5.10` to `6.0`. The scaffolded `Package.swift` declares Swift tools 6.0, so CI failed with `package 'swift' is using Swift tools version 6.0.0 but the installed version is 5.10.1` whenever the workflow ran.

- fix(scaffold-kotlin): emit the generated `packages/kotlin/build.gradle.kts` with 2-space indentation matching the `[*.gradle.kts] indent_size = 2` rule already declared in the scaffolded `.editorconfig`. The template previously emitted 4-space indent, which ktlint flagged via `ktlintKotlinScriptCheck` against the build script itself. Also drops the redundant `${rootDir}/...` braces in the JNA `libPath` assignment to silence the matching ktlint warning.

## [0.15.9] - 2026-05-09

### Fixed

- fix(e2e/elixir): coerce enum atom fields via `to_string/1` before string comparisons. Rustler binds Rust enums as Elixir atoms (e.g. `:stop` for `FinishReason::Stop`), so `String.trim/1` on `result.choices[0].finish_reason` raised `FunctionClauseError: no function clause matching in String.trim/1` on every chat fixture asserting `finish_reason == "stop"`. `render_assertion` now consults `[crates.e2e].fields_enum` (matched against both the raw fixture path and the resolved alias) and wraps the accessor in `to_string(...)` for `equals` assertions, mirroring the existing Ruby/Python coercion paths.

- fix(e2e/elixir): drop `_async` suffix for streaming entry-points. `[crates.e2e.calls.chat_stream]` is marked `async = true`, but the Elixir binding exposes the streaming wrapper as `defaultclient_chat_stream/2` (no `_async` suffix — it drives the FFI iterator handle synchronously, returning a `Stream`). The codegen unconditionally appended `_async`, producing `LiterLlm.defaultclient_chat_stream_async/2 is undefined` on every stream fixture. The two suffix sites (`render_test_files` and `render_test_case`) now skip the suffix when the resolved base function name ends with `_stream`.

- fix(e2e/elixir): plumb `extra_args` from per-language overrides. `[crates.e2e.calls.list_files.overrides.elixir]` and `[crates.e2e.calls.list_batches.overrides.elixir]` configure `extra_args = ["nil"]` to fill the trailing `Option<String> query` slot, but the Elixir codegen ignored them — emitting `defaultclient_list_files_async(client)` against a `/2` arity binding (`UndefinedFunctionError: did you mean defaultclient_list_files_async/2`). `render_test_case` now reads `call_overrides.extra_args` and appends them after configured args before prefixing the client variable, mirroring the Ruby/Go/Node/C# generators.

- fix(e2e/elixir): env-gate live smoke fixtures via `OPENAI_API_KEY in [nil, ""]`. Smoke fixtures without `mock_response` (e.g. `smoke_chat_openai`, `smoke_streaming_openai`) target real provider APIs; absent env keys yielded `MatchError: {:error, "no mock route ..."}` rather than a clean skip. The codegen now emits an `if System.get_env("<api_key_var>") in [nil, ""] do :ok else <test body> end` wrapper when `fixture.env.api_key_var` is set and there's no mock_response/http override, mirroring the Python conftest skip pattern. Both the early-return `expects_error` path and the assertion path close the `else` block.

- fix(e2e/elixir): support `result_is_simple` for raw-byte returns (`speech`, `file_content`). The Elixir binding returns the audio/content bytes directly as a binary, but the codegen emitted `assert result.audio != ""` against the binary, raising `BadMapError: expected a map`. `render_test_case` now reads `result_is_simple` from the resolved call override, and `render_assertion` bypasses the field accessor (and the `is_valid_for_result` skip) when set, asserting against the bound `result` variable directly. Mirrors the existing Ruby/PHP `result_is_simple` path. Combined with adding `Default` to `OcrRequest` (allowing the rustler backend to marshal it as `Option<String>` JSON), drops Elixir e2e failures from 68 → 0.

- fix(e2e/typescript+wasm): assert against `EnumClass.Variant`, wrap numeric assertions in `Number(...)`, plumb `extra_args`, and replace empty-input `{}` with no positional args. Four WASM-specific assertion regressions made cross-language e2e fail with `result.choices[0].finishReason.trim is not a function` (wasm-bindgen exposes Rust enums as numeric discriminants, not strings), `expected 15n to be 15` (`u64`/`i64` getters return BigInt — comparing against a JS Number with `.toBe()` always fails on `Object.is` equality), `expected instance of WasmFileListQuery / WasmBatchListQuery` (zero-arg `list_files()`/`list_batches()` was emitted as `listFiles({}, undefined)` because the codegen always dumped the fixture's `{}` input as the first arg), and `extra_args` configured per-language but never consumed by the typescript/wasm generator. The fix: (1) adds `result_enum_fields: HashMap<String, String>` to `CallOverride`, used by `render_assertion` to emit `expect(field).toBe(EnumClass.Variant)` (PascalCase-converted) when the assertion field path matches — the enum class is auto-imported alongside the request type. (2) For numeric `equals` / `<` / `<=` / `>` / `>=` on WASM, wraps the field expression in `Number(...)` so BigInt and Number compare equal. (3) Plumbs `extra_args` through `render_test_case` for both `node` and `wasm` lang keys. (4) When a call has no configured args and the fixture input is an empty object, returns an empty `args_str` so `extra_args = ["undefined"]` becomes the sole call argument. Drops liter-llm's WASM e2e failures from 51 → 18 (143/161 passing).

- fix(e2e/c): pass `MOCK_SERVER_URL/fixtures/<id>` as `base_url` and spawn the mock-server from the generated Makefile. Every `mock_response`/`http` fixture now has its `client = create_client("test-key", base_url, ...)` constructed against the per-fixture mock-server route, mirroring the Python/Ruby/Java/Dart codegen pattern. Without this, every C test that asserts `result != NULL` failed at the FFI boundary because the default OpenAI URL with a fake `"test-key"` could never serve a mocked response (e.g. `test_edge_batch_empty_list` aborted with `assert(result != NULL && "expected call to succeed")` at the first non-error fixture). The Makefile `test:` target now spawns `../rust/target/release/mock-server ../../fixtures` over a FIFO (so stdin stays open), parses the printed `MOCK_SERVER_URL=...` line, exports it for the test process, and tears the server down on exit. Honors a pre-set `MOCK_SERVER_URL` by skipping the spawn. The C `render_test_function`, `render_bytes_test_function`, and `render_chat_stream_test_function` paths all consult `Fixture::needs_mock_server()` and emit the per-fixture `getenv("MOCK_SERVER_URL")` + `snprintf` setup before the `create_client` call.

- fix(e2e/java): emit FQN imports for binding types and split imports onto separate lines. Previously the Java codegen derived the binding package only when `class_name` itself was fully-qualified (rare); the more common unqualified case (e.g. `class_name = "LiterLlm"`) left `import_path = ""`, so every dependent import branch — `all_options_types`, `enum_types_used`, `nested_types_used`, `CrawlConfig`, visitor types — fell back to bare type names like `import ChatCompletionRequest;`, which javac rejects with `'.' expected`. The fix threads the resolved `[crates.java] package` (via `config.java_package()`) into `render_test_file` as `binding_pkg` and routes every binding-type import through that package, with the per-class FQN derived from `import_path` retained as a secondary fallback. The class-itself import (`import {binding_pkg}.{ClassName};`) is now emitted when `class_name` is unqualified, fixing `cannot find symbol: class LiterLlm` on every test file. The `templates/java/test_file.jinja` import block also dropped the `{%-` whitespace-strip on `endfor`, which had collapsed every `{{ imp }}` and the `package` line into a single physical line — imports now render one per line as required by Java syntax. Bonus: the `templates/java/assertion.jinja` `equals` branch handles `value: null` fixtures (which deserialize to `Option<Value> = None` under `#[serde(default)]`) by emitting `assertTrue(...isEmpty(), "expected null")` against the unwrapped Optional accessor instead of `assertEquals(, ...)` — the latter was an empty-token-list compile error in every fixture asserting on a nullable field.

- fix(php-backend/enum-tainted): emit `..Default::default()` in `From<Binding> for Core` impls when the core type has cfg-stripped fields. The enum-tainted From-impl emitter (`gen_enum_tainted_from_binding_to_core`) iterates `typ.fields`, but cfg-gated fields are absent from the IR, so the resulting `Self { … }` initializer was missing those fields entirely — `error[E0063]: missing field 'browser_pool' in initializer of kreuzcrawl::CrawlConfig`. The two templates `php_impl_from_begin.jinja` / `php_impl_from_end.jinja` now accept a `has_stripped_cfg_fields` flag (passed through `typ.has_stripped_cfg_fields`); when set, the begin template adds `#[allow(clippy::needless_update)]` and the end template emits `..Default::default()` before closing the struct literal. Mirrors the existing handling in `binding_to_core_impl.jinja` for the non-tainted path.

- fix(codegen/structs): exclude data-enum opaque wrappers from `#[serde(skip)]` on parent fields. The PyO3 (and any future serde-aware) backend used to add `#[serde(skip)]` to every field whose type referenced any name in `opaque_type_names`, which now also includes data-enum wrappers (e.g. `Message`, `StopSequence`, `ToolChoice`, `ResponseFormat`). Those wrappers are emitted via `gen_pyo3_data_enum` with hand-rolled forwarding `serde::Serialize`/`Deserialize` impls that delegate to the core type, so the skip silently dropped the field on JSON round-trips: `ChatCompletionRequest::from_json('{"messages":[…]}').messages` produced `[]`, breaking every binding-level `from_json` round-trip and surfacing as `BadRequestError: Invalid 'messages': empty array` against real provider APIs. The fix introduces a new `serializable_opaque_type_names: &[String]` field on `RustBindingConfig`; `gen_struct_with_per_field_attrs` and `gen_struct_with_rename` exclude fields referencing names in this slice from the skip rule. The PyO3 backend populates it with `data_enum_names` (cloned before the existing `opaque_names_vec.extend(data_enum_names)` move) — fields referencing data-enum wrappers now serialize/deserialize through the forwarding impls instead of defaulting to empty.
- fix(e2e/c): emit `uint64_t` / `int32_t` / `double` / `bool` for primitive leaf accessors instead of `char*`. The C codegen previously hardcoded `char* {local} = {prefix}_{type}_{field}({handle});` for every leaf field, then routed every assertion through `strcmp` / `str_trim_eq`. FFI signatures like `uint64_t literllm_usage_total_tokens(...)` and `double literllm_rerank_result_relevance_score(...)` thus failed to compile (`incompatible integer to pointer conversion initializing 'char *' with 'uint64_t'`, `invalid operands to binary expression ('char *' and 'double')`). The new path consults a `[e2e.fields_c_types]` lookup for `"{parent_snake_type}.{field}"` — when the resolved type is a primitive C scalar (`uint64_t`, `int32_t`, `uint32_t`, `int64_t`, `double`, `float`, `bool`, …), the codegen emits the typed local declaration, switches `equals` assertions to `==` (mapping JSON booleans to `1` / `0` for `bool` slots), and skips the `char*` free at end-of-test. Both `emit_nested_accessor` (returning `Option<String>` for the leaf primitive) and the non-nested accessor branch consult the lookup. `render_assertion` gains a `primitive_locals: &HashMap<String, String>` parameter to dispatch numeric vs string comparisons.
- fix(e2e/c): support `string` and optional pointer args in the client-factory pattern. The C codegen previously processed only `arg_type == "json_object"` args in the client pattern (constructing handles via `_from_json` and passing them positionally), silently dropping every other arg type — so `cancel_batch(client, batch_id)`, `retrieve_file(client, file_id)`, etc. emitted `cancel_batch(client)` and failed to compile (`too few arguments to function call, expected 2, have 1`). The new path adds an `else if arg.arg_type == "string"` branch that reads `fixture.input.<field>`, escapes via `escape_c`, and emits the value as a C string literal `"value"` inline; an `else if arg.optional` branch passes `NULL` for missing optional arg slots; and the method-call argument string concatenates request handles + inline args together. Resolves the `cancel_batch` / `retrieve_batch` / `retrieve_file` / `delete_file` / `retrieve_response` / `cancel_response` compile errors.
- fix(e2e/c): honor `extra_args` from per-language overrides. The C codegen now reads `extra_args` from `[crates.e2e.calls.<name>.overrides.c]` (mirroring Rust/Go/Ruby/Node) and appends them verbatim after configured `args`. Used to pass `NULL` for trailing optional pointer slots in FFI signatures the fixture cannot supply directly (e.g. `*const FileListQuery` on `list_files`, `*const BatchListQuery` on `list_batches`).
- fix(e2e/c): emit numeric/boolean comparisons for map-access fields. JSON-extracted leaves (e.g. `results[0].relevance_score`, `data[0].index`) come back as `char*` from `alef_json_get_string`, so `strcmp(p, 0)` / `p > 0.9` previously failed type-check. `render_assertion` now detects the map-access flag on the field and re-routes `equals` / `greater_than` / `less_than` / `greater_than_or_equal` / `less_than_or_equal` through `atof(p)` (numbers) or `strcmp(p, "true"/"false")` (booleans) when the expected value is non-string.
- fix(php-backend): generated `#[php(constructor)]` named constructors now match binding-side field types. Two regressions produced uncompilable PHP for any core type that satisfies `has_serde && has_default && contains_non_optional_Duration && contains_Vec<serde_json::Value>`: (1) `Duration` fields stored as `Option<i64>` in the binding (via the serde-skip-serializing-if-none + `option_duration_on_defaults` interaction) had constructor parameters typed as plain `i64`, breaking the implicit `Self { timeout, … }` init with `expected Option<i64>, found i64`. The fix flips `optional=true` on the `ParamDef` for Duration fields when `has_serde && typ.has_default`. (2) `Vec<serde_json::Value>` fields were treated as constructor parameters, but `Vec<serde_json::Value>` does not implement `FromZval`, so ext-php-rs could not generate marshalling code (`the trait FromZval is not implemented for std::vec::Vec<Value>`). The fix excludes `TypeRef::Vec(TypeRef::Json)` from `field_can_be_param`, so the constructor signature drops the JSON-array parameter entirely and the field initializer falls through to `Default::default()`. Both fixes target `gen_struct_methods_impl` in `alef-backend-php/src/gen_bindings/types.rs`. Affects kreuzcrawl's `BrowserConfig.timeout` / `CrawlConfig.request_timeout` and `MarkdownResult.tables`.

- fix(codegen/binding-to-core): preserve `Option<T>` layer for genuinely-optional fields in the `has_optionalized_duration` builder branch. The branch routed every `field.optional == true` field through `gen_optionalized_field_to_core(..., field_is_ir_optional=true)`, whose body emits `.unwrap_or_default()` for primitives, `String`, `Path`, `Duration`, etc. — producing `T` from a binding `Option<T>` and breaking the assignment to a core `Option<T>` destination. Genuinely-optional fields now fall through to `field_conversion_to_core_cfg(name, ty, true, config)` regardless of `optionalize_defaults`, mirroring the non-builder branch's logic. Both `optionalize_defaults=false` (e.g. PyO3) and `optionalize_defaults=true` (e.g. NAPI) configurations are now correct: in both cases a genuinely-optional core field maps to a single-Option binding field with passthrough conversion. Affected any crate where the parent type has both a non-optional `Duration` (triggering builder mode via `option_duration_on_defaults`) and at least one `Option<T>` field — e.g. kreuzcrawl's `BrowserConfig` (`endpoint`, `wait_selector`, `extra_wait`) and `CrawlConfig` (`max_depth`, `max_pages`, `browser_profile`, `warc_output`).

- fix(pre-commit): prefer pre-installed `alef` binary on PATH (when `--version` matches the pinned `alef.toml` version) before falling back to the cached release tarball download. Speeds up local development (no network round-trip when `cargo install --path crates/alef-cli` already produced a binary) and avoids the "No such file or directory" failure mode when a downstream `.pre-commit-config.yaml` overrode `entry: alef verify` — overrides are no longer needed because the script-language hook itself dispatches to the right binary. Documented in `.pre-commit-hooks.yaml` that `entry:` should not be overridden.
- fix(e2e/csharp): emit `?.ToString()?.ToLower()` for enum-typed `equals` assertions instead of `!.Trim()`. `[crates.e2e].fields_enum` entries (e.g. `choices[0].finish_reason`, `status`) resolve to typed C# enums (`FinishReason?`, `BatchStatus?`) in the binding surface, but the C# assertion path unconditionally suffixed `!.Trim()` — a string-only API — yielding `error CS1929: 'FinishReason?' does not contain a definition for 'Trim'` on every chat/batch fixture. `render_assertion` now consults `e2e_config.fields_enum` (resolved through `FieldResolver::resolve` for nested paths like `choices[0].finish_reason`) and bypasses the assertion template for enum equality, emitting `Assert.Equal("stop", result.Choices[0].FinishReason?.ToString()?.ToLower())`. The lowercase comparison matches the JSON form attached via `[JsonPropertyName("stop")]` on each enum variant.
- fix(e2e/csharp): drive `chat_stream` tests via `await foreach` over `IAsyncEnumerable<ChatCompletionChunk>`. The C# binding emits `IAsyncEnumerable<ChatCompletionChunk> ChatStream(req)` (not `Task<T>`), but `render_test_method` previously emitted `var result = await client.ChatStream(req)` — `error CS1061: 'IAsyncEnumerable<ChatCompletionChunk>' does not contain a definition for 'GetAwaiter'`. The new `render_chat_stream_test_method` branch detects `function = "chat_stream"` up front and emits a dedicated body that loops `await foreach (var chunk in client.ChatStream(req))`, building local aggregator vars (`chunks`, `streamContent`, `streamComplete`, plus optional `lastFinishReason` / `toolCallsJson` / `toolCalls0FunctionName` / `totalTokens` resolved from fixture assertions). Pseudo-fields (`chunks`, `stream_content`, `stream_complete`, `no_chunks_after_done`, `finish_reason`, `tool_calls`, `tool_calls[0].function.name`, `usage.total_tokens`) translate to assertions on those locals; `error` fixtures wrap the foreach in `Assert.ThrowsAnyAsync` so the producer is actually consumed. Mirrors the Stage 3 Ruby/C streaming codegen pattern.

### Changed

- feat(e2e/java): emit real test bodies parallel to Python codegen — drop `Assumptions.assumeTrue(false, ...)` stubs. The Java e2e generator now resolves `client_factory` and `options_via` from java overrides (with file-level fallback) and emits real bodies for every non-HTTP fixture. With `client_factory` set, tests instantiate a client via `{ClassName}.{factory}("test-key", mockUrl, null, null, null)` (when fixture has `mock_response`/`http`) or via the env-key-or-skip pattern, then dispatch the call as a method on the client. With `options_via = "from_json"`, json_object args are built via `{OptionsType}.fromJson(jsonString)` instead of the builder expression path. The `if call_overrides.is_none() { assumeTrue(false) }` stub branch in `render_test_method` is removed.

### Fixed

- fix(e2e/csharp): preserve `List<T>` element types and construct `JsonElement` for untagged-union fields. The C# e2e codegen previously emitted `new List<string> { ... }` for every JSON array nested inside an options object initializer, regardless of the property's actual type — so `ChatCompletionRequest.Messages` (typed `List<Message>`) and any `JsonElement?` field (untagged unions like `tool_choice` / `stop` / `EmbeddingRequest.Input`) failed with `CS0029` at compile. The fix adds `options_via = "from_json"` plumbing to the C# codegen (resolved per-call with file-level fallback through `[crates.e2e.call.overrides.csharp]`): when set, the generator emits `JsonSerializer.Deserialize<{OptionsType}>("...", ConfigOptions)!` for `json_object` args, sidestepping per-field type inference entirely. Discriminator fields in nested objects are sorted-first (mirroring the existing handle-config path) so System.Text.Json polymorphic deserialization sees the discriminator before the data fields.
- fix(e2e/elixir): build typed maps with atom keys instead of JSON strings — actually solved by routing default-typed (has*default) struct method params through `Option<String>` JSON in the rustler backend (`gen_nif_method`/`gen_nif_async_method` plus the streaming `_start` post-processor), mirroring the existing free-function pattern. Rustler's `NifMap` derive is strict (`try_decode_field` returns `Err` whenever any keyed atom is absent), so partial Elixir maps like `%{messages: …, model: …}` failed to decode and surfaced as `ArgumentError` at every method call site (148 of 161 e2e tests in liter-llm). The fix accepts `Option<String>` JSON, deserializes via `serde_json::from_str` (so `#[serde(default)]` fills missing fields), and forwards the resulting `core*\*` local. The Elixir wrapper layer (`liter_llm.ex`) already typed `req`as`String.t() | nil` so no wrapper change was needed; e2e tests that already emit JSON strings now decode correctly.
- fix(e2e/ruby): drop architectural skip for streaming fixtures, emit `chat_stream` block iteration. The Ruby e2e codegen previously emitted `skip 'Non-HTTP fixture cannot be tested via Net::HTTP'` for every non-HTTP fixture whose assertions referenced pseudo-fields (`chunks`, `stream_content`, `stream_complete`, ...) the public response type does not expose, leaving the entire `streaming_spec.rb` suite pending. The new path detects `function = "chat_stream"` up front, suppresses the Magnus `_async` suffix (the streaming method is `chat_stream`, not `chat_stream_async`), and emits a dedicated test body that drives `client.chat_stream(req) do |chunk| ... end`, building local aggregator vars (`chunks`, `stream_content`, `stream_complete`, plus optional `last_finish_reason`, `tool_calls_json`, `total_tokens`) inside the block. Fixture pseudo-field assertions are translated to expectations on those locals; `error` fixtures assert the call raises.
- fix(e2e/wasm): drop the `WasmTypeUpdate` builder pattern from the WASM e2e codegen and construct the main wasm-bindgen type directly. `alef-backend-wasm` does not emit per-type `*Update` builder classes, so the previous `new WasmChatCompletionRequestUpdate()` + `WasmChatCompletionRequest.fromUpdate(_u)` IIFE blew up at runtime with `WasmChatCompletionRequestUpdate is not a constructor`. Every wasm-bindgen-emitted struct exposes an all-optional positional constructor and per-field setters, so the WASM branch of `ts_builder_expression_inner` now emits `new T()` followed by setter assignments and returns the instance directly. Imports drop the `*Update` value imports as well.
- fix(e2e/wasm): emit BigInt literals for `u64`/`i64` setter assignments and resolve per-call options classes. Previously the codegen wrote plain numeric literals (`_u.maxTokens = 100;`) into wasm-bindgen setters whose Rust types are `u64`/`i64`, which wasm-bindgen now exposes as JS `bigint` — leading to `TypeError: Cannot convert 100 to a BigInt`. The configured `bigint_fields` list under `[e2e.call.overrides.wasm]` was already accepted by the schema but never consumed; `ts_builder_expression_inner` now treats listed field names as bigint sites and emits `100n` (or `BigInt(...)` for non-literal numeric expressions). At the same time, the WASM `options_type` had to be resolved per-fixture instead of globally — every `[e2e.calls.<name>.overrides.wasm].options_type` is now picked up so embeddings tests construct `WasmEmbeddingRequest`, files tests construct `WasmCreateFileRequest`, and so on (the previous global-only resolution forced `WasmChatCompletionRequest` for every call and surfaced as `expected instance of WasmEmbeddingRequest` from wasm-bindgen). `bigint_fields`, `nested_types`, `enum_fields` are similarly merged from the per-call wasm override, with the file-level wasm override as fallback. Imports aggregate every options class referenced across the file's fixtures so a test file covering chat + chat_stream pulls both.
- fix(e2e/wasm): generate `globalSetup.ts` whenever any non-HTTP fixture is present so function-call e2e tests (which interpolate `${process.env.MOCK_SERVER_URL}/fixtures/<id>` into the client base URL) actually find a running mock server. Previously `globalSetup` was conditional on `has_http_fixtures` only; for fixture sets that contain only function-call tests (no raw HTTP fixtures) the mock server never spawned and every test failed at request-builder time with `Unknown Error: builder error` (the wasm-bindgen url builder rejected `undefined/fixtures/...`).
- fix(wasm-backend): evaluate `any(...)`/`all(...)`/`not(...)` cfg gates instead of stripping items whose first listed feature is disabled. `is_gated_behind_disabled_feature` previously parsed only the first `feature = "name"` token in a cfg string, so a type gated `#[cfg(any(feature = "native-http", feature = "wasm-http"))]` was treated as disabled whenever the first feature (`native-http`) was absent — even when the WASM feature set legitimately enabled `wasm-http`. This silently dropped `DefaultClient` (and every method/adapter hung off it) from the generated WASM `lib.rs`. The evaluator now mirrors the alef-cli extractor's existing parser: `feature = "name"`, `any(...)`, `all(...)`, `not(...)`, with whitespace normalisation for proc-macro2 token-stream output.
- fix(e2e/wasm): import the local `wasm-pack` artifact directly instead of rewriting test imports to a non-existent `<pkg>/dist-node` subpath. The post-processor was tailored to a multi-distribution layout (`dist/`, `dist-node/`, `dist-web/`) that alef's own `wasm-pack build` step does not produce — its flat `pkg/` is already a self-initializing CJS module when built with `--target nodejs`. Removing the rewrite lets the generated `import … from "<pkg_name>"` resolve via the package's `main` entry. Projects that still need a multi-distribution layout should configure their pkg `package.json` `exports` map at build time.
- fix(e2e/c): drive streaming tests via FFI iterator handle instead of non-existent `_chunks` / `_stream_content` / `_stream_complete` accessors. The C codegen previously emitted calls to invented per-chunk accessor functions and then comment-skipped every assertion, leaving the C streaming suite uncompilable against the actual FFI surface. The new path detects `function = "chat_stream"` in the client-pattern branch and emits a dedicated test body that calls `{prefix}_default_client_chat_stream_start`, loops over `_next` until null (treating `last_error_code() == 0` as clean end-of-stream, non-zero as error), and aggregates per-chunk data into local variables (`chunks_count`, `stream_content`, `stream_complete`, `last_choices_json`, `total_tokens`). Fixture pseudo-fields (`chunks`, `stream_content`, `stream_complete`, `no_chunks_after_done`, `finish_reason`, `tool_calls`, `tool_calls[0].function.name`, `usage.total_tokens`) are translated to assertions on those locals; `error` fixtures assert that `_chat_stream_start` returned NULL.
- fix(e2e/c): emit byte-buffer out-pointer pattern for `speech` / `file_content` / `transcribe`-bytes-style methods. The C codegen previously treated every client method as returning a `*Response` opaque handle, so methods whose actual FFI shape is `int32_t fn(client, req, uint8_t **out_ptr, uintptr_t *out_len, uintptr_t *out_cap)` (e.g. `literllm_default_client_speech`, `literllm_default_client_file_content`) were emitted with a non-existent `LITERLLMSpeechResponse*` cast plus invented `_audio` / `_content` accessor calls and a `_response_free` invocation. The new branch in `render_test_function` detects `result_is_bytes = true` (resolved from `[e2e.calls.<name>]` or the per-language override) and routes to a dedicated `render_bytes_test_function` that declares the three out-params, calls the FFI method with `&out_ptr, &out_len, &out_cap`, asserts on `status` (`== 0` on success, `!= 0` on expected error) and `out_len > 0` for any `not_empty` / `not_null` field assertion (the field name is a pseudo-field — the buffer itself is the value), and frees with `<prefix>_free_bytes`. The matching `[crates.e2e.calls.{speech,file_content}.overrides.c]` blocks in `liter-llm/alef.toml` flip from `result_type = "..."` to `result_is_bytes = true`.

### Added

- feat(java-backend): emit `DefaultClient` instance methods (chat, embed, moderate, rerank, list*models, image_generate, transcribe, speech, ocr, files API, batches API, responses API, search) over FFI MethodHandles. The Java backend previously emitted only the streaming `chatStream` iterator on opaque handle classes, leaving every other public method missing — Java e2e tests calling `client.chat(req)` failed to compile. `alef-backend-java` now iterates `typ.methods` for every opaque type and emits, for each non-static, non-streaming method, (a) a `MethodHandle` in `NativeLib.java` whose `FunctionDescriptor` prepends `ValueLayout.ADDRESS` (the receiver) to each param's layout (and appends three `ADDRESS` out-pointers + `JAVA_INT` return for `Result<Vec<u8>>` methods like `speech` and `file_content`), and (b) a public instance method on the owning opaque handle class that marshals each `Named` param via the per-type `_from_json` helper, marshals `String`/`Path` params via `arena.allocateFrom`, calls `NativeLib.<PREFIX>*<OWNER>\_<METHOD>.invoke(this.handle, …)`, deserializes `Named`returns via the existing`\_to_json`helper + Jackson`STREAM_MAPPER`, and frees both response and param pointers. Bytes-result methods unpack the `(out_ptr, out_len, out_cap)`triple and free via`<prefix>\_free_bytes`. Null returns or non-zero status routes through the existing `checkLastFfiError` helper to raise the binding's standard exception type. Streaming-adapter method names are excluded from the iteration so they remain handled by the streaming codegen path.
- feat(check-registry): add `pub`, `zig`, and `swift` adapters. `pub` queries pub.dev's `/api/packages/{name}/versions/{version}` endpoint. `zig` and `swift` have no central registry — both delegate to `check_github_release` since they consume packages directly from GitHub release tags (Zig via `zig fetch --save <tarball-url>`; Swift Package Index auto-discovers new tags from Git). Required for kreuzberg's publish workflow which now ships these languages and needs proper existence checks instead of stub jobs.
- feat(magnus-backend): emit `chat_stream` method returning Ruby Enumerator/block-yield. For every `[[crates.adapters]]` entry with `pattern = "streaming"` and `owner_type = <opaque>`, `alef-backend-magnus` now emits (a) a `{PascalName}Iterator` opaque struct wrapping `Arc<tokio::sync::Mutex<Option<BoxStream<…>>>>` plus a private tokio runtime, with `next_chunk` (sync `block_on` over `StreamExt::next`, returns `nil` at end-of-stream) and `each` (yields each chunk to the block, or returns an `Enumerator` via `enumeratorize` when no block is given) inherent methods, and (b) a public instance method on the owning opaque type (e.g. `DefaultClient#chat_stream(req)`) that drives the Rust core stream natively — block on `inner.{core_path}(core_req).await` to materialize the stream, then yield chunks to the caller's block (returning `nil`) or wrap the stream in the iterator type and return it for `Enumerable` consumption (`.to_a`, `.lazy`, `.map`, …). The default `chat_stream_async` stub previously emitted by `gen_opaque_async_instance_method` (which raised `NotImplementedError` because the IR represents `BoxStream` returns as `String`) is suppressed for streaming adapter names. The iterator class is registered with `Enumerable` mixed in via `class.include_module(ruby.module_enumerable())`. `alef-scaffold` now adds `futures = "0.3"` to the magnus crate's `Cargo.toml` whenever a streaming adapter is present.
- feat(csharp-backend): emit `ChatStream` as `IAsyncEnumerable<ChatCompletionChunk>` over FFI chat-stream iterator handle. The C# backend previously filtered every method whose adapter pattern was `Streaming`, leaving the streaming surface unreachable from .NET. With the iterator-handle exports in place (`{prefix}_{owner}_{name}_start` / `_next` / `_free`), `alef-backend-csharp` now emits, for every streaming adapter, three new P/Invoke decls in `NativeMethods.cs` (`{Owner}{Name}Start` / `Next` / `Free`, all `IntPtr`-typed) plus the request `{Request}FromJson` / `Free` and item `{Item}ToJson` / `Free` accessors, and a public `async IAsyncEnumerable<{Item}> {Method}({Request} req, [EnumeratorCancellation] CancellationToken cancellationToken = default)` method on the owning opaque handle class. The body marshals the request via `JsonSerializer` + `_from_json`, drives `_start`, then loops `_next` inside `try`/`finally` (yielding deserialized chunks until null; null + `LastErrorCode() != 0` is rethrown as the binding's exception type, null + `0` is a clean `yield break`), and frees both the stream handle and the request handle in `finally`. The opaque-handle template now pulls in `System.Threading` and `System.Runtime.CompilerServices` whenever a streaming method is emitted.
- feat(java-backend): emit `chatStream` returning `Iterator<ChatCompletionChunk>` over FFI iterator handle. For each `[[crates.adapters]]` entry with `pattern = "streaming"` and `owner_type = <opaque>`, `alef-backend-java` now emits (a) three downcall `MethodHandle`s in `NativeLib.java` for the iterator-handle FFI trio (`_start` / `_next` / `_free`) plus the request `_from_json`/`_free` and the chunk-item `_to_json`/`_free` accessors, and (b) a public instance method on the owning opaque handle class (e.g. `DefaultClient.chatStream(req)`) that marshals the request via Jackson + `_from_json`, drives `_start`, and returns a `java.util.Iterator<Item>` whose `next()`/`hasNext()` pull through `_next`, JSON-deserialize each chunk via the existing per-type `_to_json` / `_free` helpers, and `_free` the stream handle on clean end-of-stream or error. FFI errors (non-zero `last_error_code` after a null `_next` return) are surfaced as the binding's standard exception type wrapped in a `RuntimeException` so the iterator contract is preserved.
- feat(go-backend): emit `ChatStream` method consuming FFI chat-stream iterator handle. The Go backend previously skipped every method whose adapter pattern was `Streaming` because the callback-based FFI export cannot be driven from CGO. With the iterator-handle exports now available (`{prefix}_{owner}_{name}_start` / `_next` / `_free`), `alef-backend-go` emits a dedicated Go method per streaming adapter that returns `(<-chan ItemType, error)`: it marshals the request, calls `_start` to obtain the handle, spawns a goroutine that loops over `_next` (treating null as either clean end-of-stream or stream error), deserializes each chunk via the existing `_to_json` helper, frees the chunk + JSON pointer, sends the typed value on the channel, and `defer`s `_free` on the handle for cleanup.
- feat(rustler-backend): emit `chat_stream` NIF as a pair of standalone `start`/`next` functions backed by a per-adapter handle resource, plus a high-level `Stream.unfold/2` Elixir wrapper. For every `[[crates.adapters]]` entry with `pattern = "streaming"`, `alef-backend-rustler` now emits a `{Owner}{Name}Handle` resource (Tokio runtime + `Mutex<Option<BoxStream>>`), `{owner_lc}_{name}_start` (decodes the request, drives `core_path` once, returns the handle), and `{owner_lc}_{name}_next` (blocks dirty-CPU on a single `stream.next()`, returns chunk JSON or `nil`). The matching `Stream.unfold` wrapper exposes the iterator as an `Enumerable` of decoded chunk maps. The original async method is suppressed in the regular method-iteration loop, native.ex stub list, and main wrapper module to avoid double-emitting a NIF for the same name; the new handle types are registered as resources in `on_load`.
- feat(ffi): emit chat-stream iterator handle for FFI consumers (Go/Ruby/Java/C#/Elixir/C). For every `[[crates.adapters]]` entry with `pattern = "streaming"`, `alef-backend-ffi` now emits three exported C functions alongside the existing callback-based wrapper: `{prefix}_{owner}_{name}_start` (creates an opaque handle wrapping a tokio runtime + `BoxStream`), `{prefix}_{owner}_{name}_next` (advances the stream, returns a heap-allocated chunk or null; null + error-code 0 means clean end, non-zero means error), and `{prefix}_{owner}_{name}_free` (null-safe drop). The opaque handle struct is emitted inline in `lib.rs`; all unsafe blocks carry SAFETY comments.
- feat(e2e): `exclude_categories` filter omits non-binding-surface fixtures from cross-language codegen. Set `exclude_categories = ["cache", "proxy", ...]` under `[crates.e2e]` (or a top-level `[e2e]` block) and every per-language e2e generator skips fixtures whose resolved category matches an entry in the set — no test, no skip directive, no commented body. The fixture files stay on disk and remain available to the consumer's own Rust integration tests, so middleware-only fixtures (cache, proxy, budget, hooks, ...) can keep their existing Rust coverage without polluting bindings whose public API does not expose those layers. The validate pass treats excluded categories as expected-empty and no longer warns about them.
- feat(release-metadata): recognise `dart`, `swift`, `gleam`, `zig`, and `kotlin` as release targets. They join the existing 14 targets in `ALL_RELEASE_TARGETS`, are emitted as `release_dart` / `release_swift` / `release_gleam` / `release_zig` / `release_kotlin` boolean fields in the JSON output, and are accepted by `--targets`. Aliases: `flutter`/`pub` → `dart`, `spm` → `swift`, `kt` → `kotlin`. Required for kreuzberg's publish workflow which now ships these languages.

## [0.15.8] - 2026-05-09

### Fixed

- fix(pre-commit): resolve pre-commit hook binary in extraction subdirectory. The release tarball top-level entry (e.g. `alef-aarch64-apple-darwin/`) extracts into the version cache directory, placing the binary at `~/.cache/alef-hooks/{version}/{target}/alef`. The hook was previously looking for the binary one level up, resulting in `FileNotFoundError` when executing `alef-verify` or `alef-sync-versions` hooks in downstream projects.

## [0.15.7] - 2026-05-09

### Fixed

- fix(e2e/python): emit positional arguments instead of `kwarg=var` in test call sites. Reverts a regression introduced by 0.15.5 (`fix(e2e/php): spawn mock server when fixtures use mock_response schema`) which inadvertently re-added the `{kwarg_name}={var_name}` form across all branches in `build_args_and_setup`. The kwarg name was sourced from `alef.toml` and did not match the binding's actual pyo3 parameter name, producing `TypeError: ... got an unexpected keyword argument 'request'` for every chat/embed/moderate test. Restores positional emission so the call works regardless of binding-side parameter naming.
- fix(php-backend): map `TypeRef::Bytes` params to PHP `String` and convert to `Vec<u8>` via `into_bytes()` at constructor / call sites (PHP strings are binary-safe). Avoids the missing `FromZval for Vec<u8>` in ext-php-rs that previously surfaced as compile errors on speech / file-content endpoints.
- fix(php-backend): remove unused `gen_php_from_core_to_binding` helper that was added in 0.15.6 but never called; was triggering `dead_code` lint error under `-D warnings` during `cargo publish` verification, blocking the 0.15.6 release at `alef-backend-php`.
- fix(codegen): fix malformed rustdoc list continuation in `ConversionConfig::untagged_data_enum_names` that triggered `doc_lazy_continuation` lint under `-D warnings`.
- fix(napi-backend): add `#[allow(clippy::too_many_arguments)]` to `gen_opaque_instance_method` (8 params, over the 7-arg limit).
- fix(e2e/python): remove now-unused `kwarg_name` parameter from `emit_handle_arg`, `emit_json_object_arg`, and `emit_bytes_arg`; drop the corresponding `arg_name_map` derivation from `build_args_and_setup`.

## [0.15.6] - 2026-05-09

### Fixed

- fix(e2e/wasm): redirect generated test imports to `<pkg>/dist-node` sub-path so that vitest (Node.js) resolves the self-initialising CJS bundle instead of the bundler-only `dist/` entry that fails without Vite/webpack.
- fix(e2e/c): normalise hyphens to underscores in the `-l` linker flag of generated Makefiles; Rust cdylib output always uses underscores (`libhtml_to_markdown_ffi.dylib`) regardless of the crate name.

## [0.15.5] - 2026-05-09

### Fixed

- fix(napi-backend): only treat enums as untagged when they have `#[serde(untagged)]`, not just any enum without `#[serde(tag = "...")]` — was misclassifying `VisitResult` and similar externally-tagged enums as untagged, emitting an unusable `serde_json::Value` wrapper instead of a `#[napi(string_enum)]`.
- fix(go-backend): same untagged-enum heuristic fix applied to the Go backend.
- fix(csharp-backend): same untagged-enum heuristic fix applied to the C# complex_enums filter.
- fix(java-backend): same untagged-enum heuristic fix applied to the Java complex_enums filter.
- feat(core/ir): add `serde_untagged: bool` field to `EnumDef` (with `#[serde(default)]`) so backends can correctly distinguish `#[serde(untagged)]` from the default externally-tagged serialization.
- feat(extract): parse `#[serde(untagged)]` attribute on enum definitions and populate `EnumDef::serde_untagged`.

## [0.15.4] - 2026-05-09

### Fixed

- fix(java-backend): underscore-prefix the unused `package` parameter on
  `gen_sealed_union_deserializer` to satisfy `RUSTFLAGS=-D warnings` in the
  publish pipeline (was the v0.15.3 release blocker).
- fix(java-backend): strip tag field before deserializing inner type.
- fix(e2e/c): use `options_type` override for request type when set.
- fix(e2e/wasm): derive bg.wasm filename from the actual crate name.
- fix(e2e/elixir): resolve `field=='input'` to entire fixture input.
- fix(php-backend): emit camelCase parameter names in struct constructor
  field init.

## [0.15.3] - 2026-05-09

### Fixed

- fix(codegen): silence `dead_code` lint on `gen_magnus_positional_constructor`,
  intentionally retained even though `gen_magnus_kwargs_constructor` always
  delegates to the hash-based form. Without `#[allow(dead_code)]` the publish
  workflow's `RUSTFLAGS="-D warnings"` failed `cargo publish`'s package verify
  step and all CLI binary builds, blocking the v0.15.2 release.
- fix(php-backend): pass `opaque_types` to all `field_can_be_param` call sites
  that were missed when the parameter was added.
- fix(php-backend): exclude `Vec<NonOpaqueCustomType>` from constructor params.
- fix(php-backend): emit `Vec<u8>` as slice and `Vec<T>` with manual `FromZval`
  conversion.
- fix(e2e/go): keep mock-server stdin pipe open so it does not exit on EOF.
- fix(e2e/go): honor `fixture.env.api_key_var` for live-API skips.
- fix(e2e/php): bootstrap.php spawns the mock HTTP server when fixtures use
  the `mock_response` schema.

### Added

- feat(e2e/go): support `api_key_var` skip + tweak liter-llm skip lists.

## [0.15.2] - 2026-05-09

### Fixed

- fix(rustler-backend): remove `{%- raw %}` whitespace strippers from Elixir
  use the `mock_response` schema (in addition to the `http` schema). Switched
  the trigger from `is_http_test()` to `needs_mock_server()` to match the
  Ruby/Python behavior; otherwise `MOCK_SERVER_URL` was unset and reqwest
  failed with "builder error" on every call.
- fix(e2e/go): keep the mock-server stdin pipe open so the server does not
  exit on EOF. Go's `exec.Command` with `Stdin == nil` connects the child
  to /dev/null, so the mock-server (which blocks on `stdin.lock().lines()`)
  exited immediately and every HTTP fixture failed with "error sending
  request for url". Now opens a `StdinPipe` like Python's `stdin=PIPE`.
- fix(e2e/go): honor `fixture.env.api_key_var` by emitting a `t.Skipf` when
  the named env var is unset, and threading the env value as the api*key
  with a nil base_url for live-API fixtures. Previously every `smoke*\*`live-API fixture failed with "no mock route" because Go always pointed
at`MOCK_SERVER_URL/fixtures/<id>`regardless of`api_key_var`.

## [0.15.2] - 2026-05-09

### Fixed

- fix(rustler-backend): remove `{%- raw %}` whitespace strippers from Elixir
  visitor templates that were collapsing pattern-match clauses onto single
  lines. With `trim_blocks` and `lstrip_blocks` enabled in the env, the raw
  blocks were unnecessary and the leading `-` strippers ate the newlines and
  indent that separated `receive do` arms, generating syntactically invalid
  Elixir like `receive do      {:visitor_callback, ...} ->        result =`.
  Affects `elixir_visitor_call.jinja` and the visitor receive loop / apply
  callback case clauses in `elixir_visitor_helper_functions.jinja`.
- fix(e2e/php): treat `field == "input"` as the entire fixture input object
  in PHP arg resolution (mirroring the shared `resolve_field` helper used by
  Python/Ruby/Go). Previously every call whose argument bound the whole input
  (e.g. `chat`, `embed` with `req=input`) generated `client->methodAsync(null)`,
  causing ~99 of 128 PHP e2e errors with "Invalid value given for argument".
- fix(napi-backend): represent `#[serde(untagged)]` data enums as a thin
  `serde_json::Value` wrapper struct with manual `FromNapiValue`/`ToNapiValue`
  impls. Previously the variants were flattened to a `#[napi(string_enum)]`
  and the inner data was lost, so JS callers couldn't pass either side of
  `Single(String)` / `Multiple(Vec<String>)`-style unions.
- fix(napi-backend): map `Json` fields to `serde_json::Value` (relying on
  napi-rs's `serde-json` feature) so JS callers can pass arbitrary
  objects/arrays/scalars instead of having to pre-serialize to a string.
  Threaded a new `json_as_value` flag through `ConversionConfig`.
- fix(napi-backend): per-field `#[serde(rename = "...")]` is now emitted as
  `#[napi(js_name = "...")]` so JS-side property names match the wire format
  (e.g. `tool_type` with rename `"type"` is exposed as `type`). Per-variant
  `#[serde(rename = "...")]` is now emitted as `#[napi(value = "...")]` on
  the corresponding string-enum variant.
- fix(napi-backend): streaming adapter methods (e.g. `chat_stream`) now
  return `Vec<Js{Item}>` directly so JS callers receive a typed array of
  chunks; previously the body called `serde_json::to_string` and the
  IR-declared return type was `String`, forcing JS callers to parse JSON
  manually.
- fix(e2e/typescript): added `result_is_simple` support so that
  bytes-returning methods (e.g. `speech`) can be tested with length-only
  assertions on the result directly, mirroring the Python codegen. Also
  added `api_key_var` env-skip support so live-API smoke tests skip when
  the API key is not set instead of falling through to the mock server.
- fix(e2e/python): client setup is now appended to the test function body
  instead of being written to the outer file buffer; the Jinja migration
  regression placed `client = create_client(...)` lines outside the test
  functions, breaking every mock-driven test.
- fix(e2e/python): use positional args when invoking the binding method
  instead of keyword args (`client.chat(request)` rather than
  `client.chat(request=request)`), since the binding's keyword name may
  differ from the alef.toml-declared call arg name (e.g. core uses `req`
  while the call is named `request`).
- fix(java-backend): wrap `readJsonList` null-check inside try-catch so that
  `checkLastError()`'s `Throwable` is caught and rethrown as `KreuzbergRsException`,
  resolving Java compile error on FFI method calls.
- fix(e2e/gleam): `contains_any` assertion now compiles to OR logic via
  `gleam/list.any` + `string.contains`, instead of N independent
  `string.contains` calls AND-ed together. Adds `gleam/list` to required
  imports for any test using `contains_any`. Without this, fixtures with
  alternative expected substrings (e.g. error-message variants) emitted
  always-false assertions.
- fix(e2e/zig): `setCwd` for test runs, `json_path_expr` skips
  `FormatMetadata` variant-name segments (internally-tagged enum),
  `contains_any` uses OR logic, bytes arg uses `std.Io.Dir.cwd()` /
  `std.testing.io` / `.unlimited` (Zig 0.16 API).
- fix(e2e/swift): `doc_lazy_continuation` now handled correctly in
  field-access codegen; `render_swift_with_optionals` added.
- fix(dart-backend): `#[allow(dead_code)]` on in-progress dart binding
  errors/types/idents; clippy fixes for `gen_rust_crate`.

- fix(rustler-backend): emit `From<Local> for core::Enum` for flat data
  enums when they appear in input position. Flat data enums (data variants
  that are all single-tuple) are encoded as a flat NifStruct on the Elixir
  side; the local→core direction was previously skipped (commented as
  "output-only"). `Vec<Message>` parameters relied on the missing impl,
  producing E0277 "the trait `From<Message>` is not implemented for
  `liter_llm::Message`". Generator dispatches on the discriminator field
  (`role`, `type`, etc.) and threads `.into()` / iter-map for `Vec<Named>`
  payloads.

- fix(rustler-backend): in `gen_rustler_flat_data_enum_from_core`,
  convert `Vec<core::T>` payloads via `_0.into_iter().map(Into::into).collect()`
  rather than the bare `_0.into()`, since `From<Vec<core::T>> for Vec<T>`
  is not blanket-impled.

- fix(go-backend): emit untagged-union Marshal/Unmarshal for enums with no
  `#[serde(tag)]`. Previously the Go backend always assumed a tag field
  named `Type` exists, but only emitted that field when `serde_tag` was
  Some. Untagged enums (`#[serde(untagged)]`, e.g. `ToolChoice`) now get
  marshalers that dispatch on the active variant pointer and unmarshalers
  that try each variant in declaration order. Also avoids the broken
  `&T{}` literal for variants whose payload type is a string alias.

- fix(csharp-backend): handle `Result<bytes::Bytes>` methods on opaque
  handle classes (e.g. `DefaultClient.Speech`). `gen_opaque_method`
  previously fell through to the generic pointer-return path, calling
  `NativeMethods.X(handle, req)` and JSON-deserialising into `byte[]`;
  but the FFI declaration uses out-params (`out IntPtr ptr, out UIntPtr
len, out UIntPtr cap`). Now emits a dedicated body that pins the
  out-params, copies the bytes, and frees them, throwing a fully
  qualified exception (the wrapper-class `GetLastError` helper is private
  and not visible from sibling classes).

- fix(adapters/php): emit lowerCamelCase parameter names in `async_method`
  PHP body (`call_args_cloned`). PHP signatures camelCase param names via
  `gen_php_function_params`, so the body must reference the camelCased
  identifier. Adapter methods with `String` params (e.g. `file_id` →
  `fileId`) previously emitted `file_id.as_str()` against a parameter
  named `fileId`, producing E0425 "cannot find value `file_id` in this
  scope".

- fix(adapters/node): wrap `bytes::Bytes` adapter return in
  `napi::bindgen_prelude::Buffer::from(b.to_vec())` instead of bare
  `.to_vec()`. The function signature returns `Result<Buffer, Error>`,
  so `Vec<u8>` produced an E0308 type mismatch.

- fix(ffi-backend): convert via `Vec::<u8>::from(val).into_raw_parts()`
  in `bytes_result_match.jinja` so the bytes return path works for both
  `Vec<u8>` and `bytes::Bytes` (and any other type implementing
  `Into<Vec<u8>>`). `bytes::Bytes` does not have `into_raw_parts`.

- fix(csharp-backend): rename tagged-union variant accessor properties from
  `{Pascal}` to `As{Pascal}` in `variant_accessor_property.jinja`. C# 12
  records cannot have a property whose name matches a nested type, so
  emitting both `public sealed record Pdf(string Value) : FormatMetadata`
  and `public string? Pdf => …` produced CS0102 ("type already contains a
  definition for 'Pdf'") for every variant. `AsPdf` is the idiomatic C#
  pattern-matching helper convention and avoids the collision.

- fix(e2e/php): handle `arg_type = "bytes"` in
  `src/e2e/codegen/php.rs::build_args_and_setup`. Previously
  bytes args fell through to the default render and passed the raw
  fixture-relative path string to `extractBytesSync()`, which ext-php-rs
  rejected with `Invalid value given for argument content`. Mirror the
  go/python convention: emit a setup line that calls
  `file_get_contents()` to load the file at runtime and binds a local
  variable to the resulting binary string. Inline byte arrays are
  encoded as `\xNN` escape strings.

## [0.15.1] - 2026-05-08

### Fixed

- fix(rustler-backend): remove 7 orphan template files in
  `src/backends/rustler/templates/` whose filenames embedded literal newlines and
  template content (residue from the c6856f8c Jinja migration). The clean `*.jinja` files
  with the same logical names were already committed and referenced by `include_str!`,
  but the duplicates were tracked in git. They blocked `cargo publish` (`cannot package
a filename with a special character`) and Windows builds (`invalid path` on git
  checkout). 0.15.0 published partially as a result; 0.15.1 republishes the affected
  downstream crates (alef-backend-rustler/swift/zig/dart/extendr/java/csharp/kotlin/gleam,
  alef-e2e, alef-readme, alef-scaffold, alef-publish, alef-cli).

## [0.15.0] - 2026-05-08

### Fixed

- fix(java-backend): drop the `SerializationFeature.WRITE_BYTE_ARRAY_AS_BASE64` configure
  call from `helper_object_mapper.jinja`. Jackson 2.x has no such enum constant — the
  actual `SerializationFeature` enum (verified against `jackson-databind:2.21.0`) does not
  contain it, so any backend that wires this feature in fails to compile with `cannot
find symbol`. If a future caller really wants to disable base64 byte-array
  serialization the correct path is a custom `JsonSerializer<byte[]>`, not a serialization feature flag.

- fix(php-backend): wrap the `php_visit_result_with_template` `format!` arg in
  `{% raw %}…{% endraw %}` so Jinja stops eating the literal `{` / `}` braces. The
  template wrote `format!("{{{}}}", k)` intending the Rust string `"{...}"`, but minijinja
  parsed `{{{}}}` as an interpolation and rendered `format!("{}", k)` instead. Clippy
  flagged the result with `useless_format` (`-D warnings`).

- fix(java-backend): rebuild `convertWithVisitorInternal` so it actually compiles. Three
  cascading bugs in `gen_convert_with_visitor_internal_method`: (1) the try-with-resources
  parenthesis stayed open across `cHtml` (a `MemorySegment`, not `AutoCloseable`); (2) the
  bridge variable referenced by `bridge.callbacksStruct()` and
  `bridge.rethrowVisitorError()` was never declared in this method — it now lives in the
  resource list as `var bridge = new VisitorBridge(options.visitor())`;
  (3) `ffi_conversion_options_invoke.jinja` hardcoded `defaultJson` as the JSON argument,
  so the `if (options != null)` branch (which allocates `optJson`) emitted
  `…invoke(defaultJson)`. Template now takes a `var_name` context arg.

- fix(scaffold/php): exclude the `stubs/` directory from the emitted
  `.php-cs-fixer.dist.php` finder. PHP CS Fixer's `@PHP82Migration` rule promotes
  class-level property declarations into constructor parameters and deletes the explicit
  `public Type $name;` lines. Stub files rely on those declarations for phpstan to know
  what fields native classes expose, so the rewrite silently breaks static analysis.
  Adding `->notPath('stubs')` keeps stubs untouched.

- chore(scaffold/php): rename emitted PHP CS Fixer config from `php-cs-fixer.php` to
  `.php-cs-fixer.dist.php`. The dotted name is the default `php-cs-fixer` looks up
  without `--config`, so users (and the kreuzberg-dev pre-commit `php-cs-fixer` hook)
  pick up the alef-managed config instead of any hand-rolled `php-cs-fixer.php` left
  over from earlier scaffolds.

- fix(error-gen/java): wrap class doc comments in `/** ... */` and stop swallowing the
  newline after the `package …;` declaration. Templates now use `{% if doc -%}` /
  `{% endif -%}` so the package separator is preserved.

- fix(ffi-backend): emit a string literal instead of `format!("include/{header_name}")` in
  the generated `build.rs` go-copy step. Clippy's `useless_format` rewrote the call to
  `"...".to_string()` and broke the alef-verify hash check on every fresh regen.

- fix(ffi-backend): drop `.clone()` on owned named returns. `gen_owned_value_to_c` was
  emitting `Box::into_raw(Box::new(result.clone()))` for any `Named` return, which fails
  for non-Clone opaque handles. New `named_owned` template arm moves the value into the
  Box; `Optional` inner conversions now use `optional_owned` so the inner `val` is owned.

- fix(magnus-backend): suppress redundant `native.rb` re-export when the public Ruby
  module name and the native extension's module name collide. The generated
  `define_singleton_method(m) { Module.public_send(m, ...) }` loop overwrote each native
  function with a self-call and triggered `SystemStackError` on first invocation.

- fix(magnus-backend): exclude thread-unsafe bridge handle types (`VisitorHandle`) from
  binding ↔ core From impls via `ConversionConfig::exclude_types` instead of post-process
  line filtering. The line filter silently failed whenever the alef extractor had already
  stripped the field's `cfg` for an active feature — the IR then carried a `cfg: null`
  field referencing `VisitorHandle`, so codegen emitted `visitor: val.visitor.map(...)`
  lines into both directions of the From impl, and the generated Ruby gem refused to
  compile against a binding `ConversionOptions` struct that (correctly) had no `visitor`
  field. Mirrors the Rustler backend's approach.

### Added

- feat(java-backend): consume the FFI `Result<Vec<u8>>` out-param convention. Java now detects byte-buffer return shapes from IR (`is_bytes_result`), declares the symbol with three trailing `ADDRESS` out-params and a `JAVA_INT` return, allocates `outPtr`/`outLen`/`outCap` slots in a confined `Arena`, checks the return code, copies the bytes via `outPtr.reinterpret(outLen).toArray(JAVA_BYTE)`, and frees them through a bound `{prefix}_free_bytes` method handle. Templates: `bytes_result_call.jinja`, `method_handle_free_bytes.jinja`. Existing Go and C# byte-buffer support unchanged.

### Changed

- refactor(backends): migrate all parameterized code emission in every `alef-backend-*` crate to
  minijinja template `render()` calls. `writeln!(out, "...")` and `push_str(&format!(...))` are no
  longer used for interpolated output in any backend; all variable substitution goes through named
  `.jinja` templates registered in each crate's `template_env.rs`. Static `push_str("literal\n")`
  calls with no interpolation are unchanged. Templates use `trim_blocks = true`,
  `lstrip_blocks = true`, and `keep_trailing_newline = true`. Affected crates: `alef-backend-ffi`,
  `alef-backend-napi`, `alef-backend-pyo3`, `alef-backend-wasm`, `alef-backend-csharp`,
  `alef-backend-java`, `alef-backend-go`, `alef-backend-magnus`, `alef-backend-php`,
  `alef-backend-rustler`, `alef-backend-extendr`, `alef-backend-swift`, `alef-backend-zig`.

### Fixed

- fix(taskfile): make `task set-version` idempotent and tolerant of a leading `v` in the input
  argument. `task set-version -- v0.15.0` previously wrote `version = "v0.15.0"` verbatim into
  `Cargo.toml`/`alef.toml` (rejected by cargo as semver), and the dep-pin regex required strict
  `[0-9]+\.[0-9]+\.[0-9]+` so a re-run could not repair its own bad output. The task now strips a
  leading `v`, validates semver shape up front with a clear error, and tolerates an optional `v`
  in the dep-pin and `ALEF_REV` regexes so subsequent runs are self-healing.
- chore(clippy): clear `clippy::if_same_then_else`, `clippy::format_in_format_args`,
  `clippy::dead_code`, `clippy::nonminimal_bool`, and `clippy::collapsible_match` warnings across
  `alef-codegen`, `alef-backend-csharp`, `alef-backend-ffi`, `alef-backend-java`,
  `alef-backend-napi`, and `alef-e2e`. Removes one dead helper in `alef-backend-csharp`
  (`native_call_args`) and one dead method in `alef-backend-ffi` (`vtable_fn_ptr_field`); collapses
  redundant identical `if`/`else` branches in `alef-codegen::config_gen` and
  `alef-backend-java::gen_visitor::files`; drops a never-read `FieldEntry { decl, doc }` struct in
  `alef-backend-java::gen_bindings::types` whose only observed use was `.len()`.
- diagnostics(backend-magnus): add `[ALEF BUG]` / `[ALEF OK]` instrumentation around
  `gen_visitor_bridge` and `gen_trait_bridge` when emitting `impl std::fmt::Debug` for visitor
  bridges. Prints rendered output to stderr when the count diverges from 1, added after a
  duplicate `impl std::fmt::Debug for RbHtmlVisitorBridge` was observed in `html-to-markdown`
  Ruby gem builds (E0119 conflicting trait impl). Test `test_visitor_bridge_debug_not_duplicated`
  extended to 40 visitor methods to better mirror the real `HtmlVisitor` trait surface.
- fix(codegen/config_gen): drop trailing `,` from rendered field assignments in
  `magnus_hash_constructor.jinja` and `rustler_kwargs_constructor.jinja`. The `field.assignment`
  strings already terminate in `,`, so the templates were emitting `,,` and Rust rejected the
  generated kwargs constructors with `expected identifier, found ','`.
- fix(jinja-env): set `keep_trailing_newline` on every backend's minijinja `Environment`. With
  `trim_blocks` + `lstrip_blocks` already on, `keep_trailing_newline=false` (the minijinja default)
  stripped the final `\n` from each rendered template, so the struct/impl builders' `out.push_str`
  concatenations collapsed onto one line. `///` doc comments then swallowed the rest of the file,
  producing "unclosed delimiter" errors throughout `pyo3`, `napi`, `php`, and `wasm` bindings.
  Brings `alef-codegen`, `alef-e2e`, and the `extendr`, `kotlin`, `napi`, `rustler`, `wasm`
  backends in line with the `csharp`/`dart`/`gleam`/`java`/`magnus`/`php`/`pyo3`/`swift`/`zig`
  backends that already set this.
- fix(e2e/java): replace `${{project.basedir}}` with `${project.basedir}` in `pom.xml.jinja`. The
  doubled braces were intended to escape the literal Maven property syntax but Jinja parses the
  inner `{{project.basedir}}` as an undefined-variable expression and refuses to render the
  template. Single braces pass through untouched and the emitted pom still contains the correct
  `${project.basedir}` Maven reference.
- fix(php-backend): align `php_vec_string_refs_let_binding.jinja` to the `param_name` context key
  used by `helpers.rs`. The template referenced an undefined `php_name` and rendered as empty
  string, emitting `let _refs: Vec<&str> = .iter().map(...)` (no receiver) for sanitized
  `Vec<String>`/`Vec<&str>` parameters and breaking compilation of the PHP binding.

## [0.14.36] - 2026-05-08

### Changed

- refactor(e2e-codegen): remove all hardcoded `tree-sitter-language-pack` special-cases from production codegen. C, PHP, WASM, and brew backends now derive every project-specific value from config: `ffi_header_name()`, `ffi_lib_name()`, and new `ffi_crate_path()`/`wasm_crate_path()` helpers on `ResolvedCrateConfig` (derived from `[crates.output]` paths). C method-result assertions use generic `{ffi_prefix}_{method}(result_var)` dispatch; PHP uses instance dispatch `$var->method()`; brew converts `method_name` snake_case to kebab-case and calls `{binary_name} {subcommand} "$output"`. A new `return_type = "string"` fixture field opts into heap-allocated `char*` handling in C codegen; the default is primitive value dispatch.
- refactor(streaming-ffi): generalise FFI streaming body generation. `gen_ffi_body` now receives `&ResolvedCrateConfig` and uses `ffi_prefix()` for error-message symbol prefixes instead of the hardcoded `literllm_` string. The request type is no longer hardcoded as `liter_llm::ChatCompletionRequest`; callers must set `request_type = "my_crate::MyRequest"` in the `[[crates.adapters]]` block for any streaming adapter that targets the FFI backend — codegen fails with a clear error if the field is absent.
- refactor(e2e-rust): make visitor trait name config-driven instead of hardcoded. `resolve_visitor_trait` now reads `visitor_trait` from `[e2e.call.overrides.rust]` in `alef.toml` and returns `Option<String>`. Fixtures that declare a `visitor` block without a configured `visitor_trait` now fail at codegen time with a clear error. Callers must add `visitor_trait = "MyTrait"` to their Rust e2e override to keep visitor fixtures working.
- refactor(docs): remove project-specific `html_to_markdown` string from `is_rust_code_fence` heuristic in `alef-docs`. The generic Rust signals (`use `, `unwrap()`, `assert!`, etc.) are sufficient.
- refactor(backend-php, backend-pyo3): replace hardcoded `html_to_markdown_rs` fallback in visitor bridge `core_crate` derivation with a panic that reports the misconfiguration instead of silently using a wrong crate name.

### Added

- feat(php-backend): emit `FromZvalMut` for `Vec<NamedStruct>` parameters so PHP arrays of struct values cross the ext-php-rs boundary correctly.

### Fixed

- fix(ffi-backend): parameterize `core_import` so generated FFI code uses the configured crate name instead of the hardcoded `kreuzberg` string. Also adds `Result<Vec<u8>>` return support via out-params (`out_ptr`, `out_len`, `out_cap`) returning `i32` (-1 = error, 0 = ok), with a companion `{prefix}_free_bytes` deallocator.
- fix(swift-backend): emit `Vec<u8>` / `Path` convenience overloads from the IR shape (first-param `TypeRef::Bytes` / `TypeRef::Path`) instead of matching by hardcoded function names. The backend now derives wrapper names by stripping the `Sync` suffix and works for any library, not just kreuzberg.
- chore(php-backend): fix `clippy::useless_format` warning in test helper.
- fix(napi-backend): initialize all binding-struct fields in tagged-enum `From` and `Default` impls. The previous codegen only emitted the matching variant's slot plus shared fields, leaving the synthesized variant-data fields (e.g. `excel`, `docx`, `html` on `JsFormatMetadata`) unset and producing E0063 ("missing fields"). The struct-literal builders now initialize every variant-data field — `None` for non-matching variants, `Some(...)` only on the active variant. Boxed tuple variants (`FormatMetadata::Html(Box<HtmlMetadata>)`) now deref before calling `.into()` since `From<HtmlMetadata>` is derived for `JsHtmlMetadata`, not `Box<HtmlMetadata>`.
- fix(csharp-backend): emit length parameters for byte slice and batch FFI calls. The C# P/Invoke signatures for `extract_bytes_sync` and related functions were missing `UIntPtr contentLen` and `UIntPtr itemsLen` parameters that the Rust FFI requires, causing host crashes. The codegen now expands `TypeRef::Bytes` parameters into two FFI arguments (pointer + length), and the wrapper methods pass `(UIntPtr)content.Length` when calling the native methods.

## [0.14.35] - 2026-05-08

Re-roll of 0.14.34: the v0.14.34 tag captured an early commit, missing the long fix list
below. Bumped to 0.14.35 to ship them on crates.io.

### Fixed

- fix(napi): emit tagged-enum variant data as optional struct properties instead of getter methods. NAPI-RS does not surface per-variant getters in the generated `.d.ts` types, so consumers calling `result.metadata.format.excel.sheetCount` got `undefined`. The backend now emits each single-tuple Named variant as a top-level `Option<JsXMetadata>` field on the binding struct (e.g. `excel: Option<JsExcelMetadata>`) and the `From<CoreEnum>` impl populates these optional fields per variant, so direct property access works in TypeScript.
- fix(e2e-zig): emit real `extract_bytes_sync` / `extract_file_sync` signatures and mark unused locals. The zig codegen previously emitted calls to `extract_bytes_sync_default` / `extract_file_sync_default` (driven by per-language `function = "..._default"` overrides) — symbols that no Zig binding exposes — and bound `result` / `err` captures with bare `const result = ...` / `catch |err|` patterns that Zig 0.16 rejects with "unused local constant" / "error set is discarded". The codegen now (a) calls the real `extract_bytes_sync` / `extract_file_sync` functions, (b) constructs the required `ExtractionConfig` value via `std.mem.zeroInit(...ExtractionConfig, .{ .output_format = ...OutputFormat{ .plain = {} } })` for the `config` argument (the binding's struct mirror has no JSON-loading helper and the tagged-union `output_format` field rejects `std.mem.zeroes`), (c) serializes non-config `json_object` fixture values into JSON-string literals (Vec/Map params cross the FFI as JSON strings), (d) passes explicit `null` for optional arguments since Zig has no default-argument support, (e) uses `catch { ... }` (no captured `err`) for error-path tests and `_ = result;` to discard unused locals, and (f) emits `_ = try fn(...)` for assertion-free or `not_error`-only tests so `result` never lingers as an unused local.
- fix(e2e-php): fix config argument handling and field naming. Reordered match arms in codegen to check config json_object args BEFORE optional check, ensuring `ExtractionConfig::from_json('{}')` is emitted even when config is marked optional in `alef.toml` — fixes `ArgumentCountError` when no fixture config provided. Additionally, changed config field serialization to use `json_to_php()` (snake_case) instead of `json_to_php_camel_keys()` so Rust's serde field naming conventions are respected (Rust defaults to snake_case via `#[serde(rename_all = "snake_case")]`), fixing unrecognized `output_format` field errors.
- fix(swift-backend): repair extraction wrapper codegen so `Kreuzberg.swift` compiles. The previous `emit_extraction_wrappers()` emitted three constructs that do not exist in the swift-bridge runtime: `RustVec<T>([T])` (the runtime only exposes `init()` + `push(value:)`), `ExtractionConfig()` (no zero-arg initializer is generated; the only init is the 33-parameter `init(ptr:)`/full constructor), and `JSONDecoder().decode(ExtractionConfig.self, ...)` (`ExtractionConfig` is an opaque swift-bridge proxy class, not a `Decodable` Swift struct). The wrappers now build `RustVec<UInt8>` via a `makeByteVec` helper that pushes bytes one at a time, and accept a fully-built `ExtractionConfig` parameter — Swift-side JSON config parsing is dropped because the proxy type cannot be Codable. Batch wrappers (`batchExtractBytes`/`batchExtractFiles`) are no longer emitted because `BatchBytesItem`/`BatchFileItem` are getter-only proxies with no `#[swift_bridge(init)]` constructor and therefore cannot be instantiated from Swift; callers must invoke `RustBridge.batchExtract*Sync` directly.
- fix(sync-versions): self-heal corrupted `gleam.toml` dependency ranges. Earlier alef releases routed `gleam.toml` through the `SEMVER_RE.replace_all` catch-all, rewriting `gleam_stdlib = ">= 0.34.0 and < 2.0.0"` into `>= {workspace_version} and < {workspace_version}` (an empty range gleam refuses to resolve with `error: Dependency resolution failed ... has no versions in the range .`). `sync_versions` now restores the canonical `gleam_stdlib` and `gleeunit` ranges from `template_versions::hex` whenever it touches a `gleam.toml`, so a single `alef sync-versions` heals affected manifests in-place.
- fix(magnus-backend): close enum braces correctly in per-variant accessor emission. The enum template was missing the closing `}` after the variant loop, causing generated enums to have unclosed delimiters and preventing the code from compiling. The enum method registration code that attempted to expose accessor methods as Ruby methods was removed, as Magnus's `method!` macro cannot handle functions returning `Option<T>` with complex types. Accessor functions are still generated in Rust (for internal use) but are no longer registered as Ruby methods.
- fix(java-backend): use `allocateFrom` for byte[] marshalling (JDK 22+ Panama FFM). The generated Java code now calls `arena.allocateFrom(ValueLayout.JAVA_BYTE, content)` instead of the removed JDK 21 API `arena.allocateArray(ValueLayout.JAVA_BYTE, content)` to copy byte arrays into off-heap arena memory for FFI calls.
- fix(extendr-backend): sanitize error messages to satisfy R's `condition` class constraints. The previous `extendr_api::Error::Other(e.to_string())` propagation produced messages containing `:`, `/`, and `-` characters and unbounded length, which extendr-api rejects when constructing the R-side condition object — the Rust panic propagated through the FFI boundary instead of converting to an R `stop()`. Error messages are now sanitized (`:`, `/`, `-` replaced with `_`, truncated to 255 chars) so `expect_error()`-style tests observe a clean R error.
- fix(java-backend): allocate `byte[]` parameters off-heap via Arena for Panama FFM. The marshal step previously emitted `MemorySegment.ofArray(name)` which only works for on-heap segments and is rejected when the FFI call requires a native (off-heap) segment. Generated code now uses `arena.allocateArray(ValueLayout.JAVA_BYTE, name)` so byte arrays are copied into the active arena's off-heap memory before being passed to `extern "C"` functions.
- fix(java-backend): annotate sealed-interface tagged unions with `@JsonIgnoreProperties(ignoreUnknown = true)`. Variants that flatten an inner type's fields onto the JSON object surface produced extra discriminator/payload properties that the sealed interface itself cannot map to record fields, so Jackson's strict default rejected them with `UnrecognizedPropertyException` before dispatching to the variant record. The annotation lets Jackson resolve the discriminator and forward the unknown properties to the matching subtype.
- fix(e2e-php): `chdir` into `test_documents/` so fixture paths resolve from PHPUnit's working directory; supply default `ExtractionConfig::from_json('{}')` for required `config` args missing from fixtures; wrap stringly-typed file-path inputs to `extract_bytes`-shaped calls in `file_get_contents(...)` so the bytes argument actually contains the file's contents instead of the path string.
- fix(swift-backend): emit string/bytes/JSON-config wrapper helpers. `emit_extraction_wrappers()` previously returned without emitting anything, forcing e2e tests to call `RustBridge.extract_*` directly with `RustVec<UInt8>` and pre-parsed `ExtractionConfig`. The backend now emits public Swift `extractBytes`/`extractFile` (sync + async) wrappers that accept `String`/`[UInt8]` content and an optional JSON config string, parse the JSON via `serde_json` on the Rust side, and delegate to the underlying RustBridge calls.
- fix(napi-backend): emit per-variant getters on `#[napi(object)]` tagged-enum structs. `JsFormatMetadata` is emitted as a flat struct with `<tag>_tag: String` plus optional fields per variant, but `#[napi(object)]` on its own does not expose getters that match `result.metadata.format.excel.sheetCount`. Each variant of every tagged enum that maps every non-empty variant to a single Named field now has a `#[napi(getter)]` method on a `#[napi]` impl block: it checks `<tag>_tag`, then deserializes the variant's JSON payload and returns it.
- fix(magnus-backend): emit per-variant accessor methods on tagged-enum classes. The Ruby `FormatMetadata` class was previously emitted with flat optional fields but no per-variant accessors. Each non-empty variant now has a Ruby method (`#excel`, `#docx`, …) that returns the variant payload when the discriminator matches, or `nil` otherwise.
- fix(e2e-gleam): unwrap `Result(_, _)` before field-access assertions. The codegen previously emitted `result.field |> should.equal(...)` even when `result` was a `Result`, failing Gleam's type checker. The codegen now emits `let assert Ok(r) = result` once after the call and uses `r` as the access base for every assertion.
- fix(zig-backend): port to zig 0.16. Replaced `std.fmt.allocPrintZ` (removed in 0.16) with `std.fmt.allocPrintSentinel(allocator, ..., 0)` returning `[:0]u8`, simplified the matching `free` calls, and updated `@typeInfo(E).ErrorSet` to `@typeInfo(E).error_set` (snake_case rename in 0.16).
- fix(scaffold/node): generate proper platform-dispatch `index.js` at `crates/{X}-node/index.js` instead of a single-file stub. Previously the scaffold did not emit this file, so a `napi build` (without `--platform`) stub `module.exports = require("./{X}-node.node");` would persist in the source tree. Per-target CI builds with `napi build --platform --target X` regenerate the file locally on each runner, but only `*.node` artifacts get uploaded — the platform-aware `index.js` is discarded. The committed stub then ships in the npm tarball, even though the bundled binaries are platform-suffixed (`{X}-node.darwin-arm64.node`, `{X}-node.linux-x64-gnu.node`, etc.), so `require("./{X}-node.node")` fails for every consumer at install time. The new generator emits a self-contained dispatcher covering linux x64/arm64 (gnu+musl), darwin x64/arm64, and win32 x64/arm64 (msvc), with a fallback to optional `{packageName}-{platformArchABI}` deps.
- fix(e2e-swift): handle optional string fields in trimming assertions. When a Rust `Option<String>` maps to Swift `String?`, calling `.trimmingCharacters(in:)` directly fails because the method is unavailable on the optional type. Generated assertions now coalesce optional strings with `?? ""` before trimming, enabling fixtures to assert on optional metadata fields like `output_format`.
- fix(rustler-backend): always convert rustler::Binary to owned Vec<u8> in NIF deser to avoid escape-into-spawn lifetime errors. Previously, the `is_ref` branch emitted `let content: &[u8] = content.as_slice();`, which borrows from the input Binary and cannot satisfy the `'static` requirement of `std::thread::spawn`. Always cloning to `Vec<u8>` is correct and the call site re-borrows the slice when the underlying core function takes `&[u8]`.
- fix(e2e-swift): emit RustBridge-qualified function calls in generated tests. Since wrapper functions were disabled in Phase 2D (commit 6bdbd0e9), e2e tests must call `RustBridge.extractFileSync(...)` instead of bare `extractFileSync(...)`. The codegen now qualifies all function calls with the RustBridge module prefix.
- fix(java-backend): suppress checkstyle LineLength on all generated classes. Extended the suppression added for e2e test classes to cover all alef-generated Java classes (records, enums, tagged unions, opaque handles, builders, facades, FFI classes, exception classes, and trait bridges). All classes now emit `@SuppressWarnings("checkstyle:LineLength")` to acknowledge that generated code may exceed the 140-character limit.
- fix(elixir-backend): use rustler::Binary for NIF binary parameters. Rustler 0.37 cannot marshal `Vec<u8>` from Erlang binaries, causing ArgumentError on every NIF call. NIF functions now accept `rustler::Binary` parameters and convert to `Vec<u8>` with `.as_slice()` or `.as_slice().to_vec()` when calling core functions.
- fix(e2e-dart): add missing kreuzberg package import in test files. Generated Dart e2e tests were emitting `KreuzbergBridge.extractBytesSync(...)` calls without importing the kreuzberg package, causing "Undefined name 'KreuzbergBridge'" errors. Test files now include `import 'package:kreuzberg/kreuzberg.dart'` to make the API accessible.
- fix(swift-backend): emit type aliases for all struct types, not just those referenced in enum variants. swift-bridge doesn't reliably expose all referenced types in generated Swift modules, so the backend now unconditionally emits `public typealias StructName = RustBridge.StructName` for all non-trait types. This ensures metadata types (JatsMetadata, EpubMetadata, PstMetadata, etc.) are accessible in Swift code.
- fix(swift-backend): add lightweight wrapper functions for extraction methods. The Swift package now includes public `extractFile`, `extractBytes`, `extractFileSync`, and `extractBytesSync` convenience functions that delegate to RustBridge equivalents, providing idiomatic Swift entry points for e2e tests and common use cases.
- fix(e2e-java): suppress checkstyle LineLength violations on generated test classes. Auto-generated e2e test methods contain byte array literals (for testing error paths) that are unavoidably long; annotate test classes with `@SuppressWarnings("checkstyle:LineLength")` to indicate that these violations are acceptable.
- fix(csharp-backend): remove duplicate accessor properties from discriminated-union sealed records. The code generator was creating both a sealed record type (e.g., `Pdf`) and a property with the same name (`Pdf => ...`), causing CS0102 "already contains a definition" errors. Sealed records don't need these accessors; pattern matching and property access via the record field itself is idiomatic C#.
- fix(e2e-zig): wire kreuzberg module into test build.zig. Each Zig test module now imports the kreuzberg module via `addImport`, resolving `@import("kreuzberg")` failures. The build script also declares ffi_path and ffi_include options for linking kreuzberg_ffi.
- fix(e2e-dart): emit receiver class and arguments in non-HTTP test calls. Dart e2e tests now emit `KreuzbergBridge.extractBytesSync(File(...).readAsBytesSync(), ...)` instead of bare `extractBytesSync()`, with fixture inputs correctly loaded and passed as arguments.
- fix(e2e-dart): convert snake_case function names to camelCase in generated test code. Dart conventions require camelCase method names; the test code generator now converts function names like `extract_bytes_sync` to `extractBytesSync`, matching idiomatic Dart API surface.
- fix(swift-backend): emit native Swift enums with unit variants only instead of typealiasing to non-existent RustBridge enum types. swift-bridge's automatic code generation doesn't reliably expose all enum types; emitting them directly as Swift enums avoids brittle typealias dependencies and enables pattern matching.
- fix(e2e-wasm): inject initSync() call in test files for Node.js test environments. vitest running wasm-pack output requires synchronous initialization of the WASM module using initSync with the bundled binary, preventing `TypeError: Cannot read properties of undefined (reading '__wbindgen_add_to_stack_pointer')` and fetch failures in Node.js.
- fix(csharp-backend): remove stray opening brace after class headers in SafeHandle and wrapper classes. The template already included the brace; the code was pushing an extra one, resulting in all C# generated files having `class Foo {` followed by a stray `{` on the next line. All 30+ C# compilation errors now clear.
- fix(e2e-dart): remove HTTP-only assumption from non-HTTP fixture codegen. Dart e2e tests now render direct-API call tests using `[e2e.call.overrides.dart]` function overrides, eliminating skip-stubs.
- fix(e2e-gleam): remove call-override check for non-HTTP fixtures. Gleam e2e tests now render all non-HTTP fixtures using the default or overridden call config, eliminating skip-stubs.
- fix(e2e-kotlin): fix `build.gradle.kts` dependency declaration. Registry mode now uses correct `groupId:artifactId:version` format; local mode references kreuzberg binding JAR from `target/release/`.
- fix(e2e-swift): document test placement path. Clarified that tests are placed in `packages/swift/Tests/` (not `e2e/swift/`) due to SwiftPM 6.0 limitations, with rationale in comments.
- fix(e2e-zig): implement proper error union and async function handling. Error-path tests now use `catch` syntax; async functions emit informational notes; all test variants (error/no-assertions/success) now compile and run.

## [0.14.33] - 2026-05-07

### Fixed

- **Go module**: emit `#cgo CFLAGS: -I${SRCDIR}/include` (instead of monorepo-relative `-I${SRCDIR}/.../crates/<ffi-crate>/include`), and have the FFI backend's generated `build.rs` copy the cbindgen header into the Go module's `include/` directory at build time. This unblocks downstream consumers that install the Go module via `go get` (where everything outside `packages/go/` is absent). Thanks to @structuredmerge for the report.
- **Note**: 0.14.31 was a tag-only release whose changes never reached crates.io because Cargo.toml wasn't bumped. 0.14.33 carries forward the intended fix.

## [0.14.32] - 2026-05-07

### Added

- feat(release): x86_64-apple-darwin release binaries to support downstream macOS x86_64 CI runners that cannot rely on Rosetta

## [0.14.30] - 2026-05-07

### Fixed

- fix(java,csharp,magnus,php-backends): set `keep_trailing_newline(true)` on the minijinja `Environment`. Minijinja strips trailing newlines from rendered templates by default, which caused multi-template concatenation in the Java marshalling helpers (`helper_object_mapper.jinja` + `helper_read_json_list.jinja`) to emit `MAPPER = createObjectMapper();    private static <T> readJsonList(...)` smashed onto a single 294-character line. With the trailing newline preserved, helper templates concatenate with proper separators. Also applied defensively to the csharp/magnus/php backends, which use the same template environment idiom.

## [0.14.29] - 2026-05-07

### Changed

- fix(rustler-backend): remove `x86_64-pc-windows-gnu` from default rustler_precompiled `targets:` list. The target was declared in the alef-generated `native.ex` but no canonical CI build job produces a windows-gnu NIF (rustler precompiled binaries normally target windows-msvc, and most Elixir projects don't ship windows-gnu artifacts). Including the missing target broke `mix rustler_precompiled.download --all` during release prep, which in turn blocked Hex publish for downstream packages. New default target list is `aarch64-apple-darwin`, `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-gnu`.

### Fixed

- fix(pyo3,wasm,php-backends): add `clippy::arc_with_non_send_sync` to crate-root allow lists. Rust 1.95.0 promoted this lint to default-on; binding crates wrap a non-Send/Sync `ConversionOptionsBuilder` (or similar core type) in `Arc` for ergonomic chaining, which is sound in single-threaded VM contexts. The napi backend already had this allow; the other three now match for consistent behavior on Rust 1.95+.

### Added

- feat(extendr-backend): flat data enums (e.g. `OutputFormat`) now generate a `From<BindingStruct> for CoreEnum` impl in addition to the existing `From<CoreEnum> for BindingStruct`. Previously the binding→core conversion fell back to `Default::default()` for all flat data enum fields, so setting `output_format = "markdown"` on an `ExtractionConfig` was silently discarded and every extraction ran as `Plain`. The reverse impl is only generated for enums where all tuple-variant data types are `String` or `Option<String>` (primitives); complex output-only enums like `FormatMetadata` are excluded.
- feat(extendr-backend): input types (structs that appear as function/method parameters) now emit a `from_json(json: String) -> Result<Self>` static factory method in their `#[extendr]` impl block. The factory deserializes via the core Rust type (which carries `#[serde(default)]` and string-enum aliases), then converts to the binding type. This allows R callers to construct configs from partial JSON (`ExtractionConfig$from_json('{"output_format":"markdown"}')`). The R-side `.Call` wrapper is emitted in `extendr-wrappers.R` automatically. Only input types receive the factory; output-only types (e.g. `Metadata`, `ExtractionResult`) are excluded.

### Changed

- fix(csharp-backend): replace bespoke `FormatMetadataJsonConverter` with .NET 7+ standard `[JsonPolymorphic]` + `[JsonDerivedType]` attributes. The custom converter was removed entirely; JSON deserialization for tagged unions now uses the idiomatic .NET 7+ approach. Generated C# code is simpler and maintains the same runtime behavior.

### Fixed

- fix(java-backend): visitor bridge `invokeCallbacks` methods now split into chunks of 5 callbacks instead of 10. Checkstyle enforces a 150-line `MethodLength` limit; at ~22 lines per callback, chunks of 10 produced ~232-line methods that failed checkstyle. Chunks of 5 produce ~114 lines, safely under the limit.
- fix(java-backend): visitor interface `jinja` template now emits `default` methods with braced bodies on separate lines. Previously the body was on a single line; checkstyle's `DesignForExtension` rule requires overridable `default` methods to have an explicit block body.

### Fixed

- fix(magnus-backend): `version.rb` now emits `VERSION = '...'` with single quotes to match the quote style that `alef sync versions` normalizes to, fixing a persistent `alef verify` staleness false-positive on the Ruby gem version file.
- fix(scaffold/node,php,python): remove `[lints] workspace = true` from the generated Cargo.toml for NAPI-RS, ext-php-rs, and PyO3 binding crates. These crates legitimately use `unsafe` code generated by their proc-macros; inheriting `unsafe_code = "forbid"` from the workspace caused compile errors (`E0453`) that cannot be suppressed by `#![allow(unsafe_code)]` in source when the workspace uses `forbid`. Cargo does not allow mixing `workspace = true` with individual lint overrides in the same section, so workspace lint inheritance is omitted for these crates.

### Added

- feat(e2e-c): added `raw_c_result_type`, `c_free_fn`, and `result_is_option` fields to `CallOverride` for C e2e codegen. When `raw_c_result_type` is set (e.g. `"char*"`, `"int32_t"`, `"uintptr_t"`), the C test generator uses the raw type directly instead of the opaque `{PREFIX}Type*` pattern, emits the correct assertion style per type, and calls the appropriate free function for `char*` returns. This enables e2e tests for all non-process C FFI calls (registry, detection, query, download).

### Fixed

- fix(magnus-backend): `TryConvert` for binding struct types now accepts plain Ruby `Hash` and `String` values via JSON fallback. Previously only already-wrapped typed-data objects were accepted, causing `TypeError: no implicit conversion of Hash into {StructName}` when callers passed a plain hash. The generated impl now tries typed-data unwrap first; on failure it calls `.to_json` (or accepts a String directly) and deserializes via `serde_json`. Symptom: Ruby process/init/configure e2e tests failed with `TypeError: no implicit conversion of Hash into ProcessConfig`.
- fix(magnus-backend): `gen_options_field_bridge_function` now accepts a Ruby `Hash` as the options argument in addition to a visitor object. When the second argument is a `RHash`, the bridge deserializes it as `ConversionOptions` via JSON; otherwise it wraps it as a visitor bridge handle. Previously only the visitor path existed, so passing plain option hashes silently produced default-constructed `ConversionOptions`.
- fix(magnus-backend): visitor method result parsing now detects Ruby `Hash` returns with a `:custom` key (e.g. `{ custom: '--- {text} ---' }`) and extracts the value as `VisitResult::Custom(s)`. Previously `val.to_string()` on a Hash yielded the Ruby inspect string (`{custom: "..."}`) which was then used verbatim as the Custom template, producing garbled output instead of the intended replacement string.
- fix(ruby-e2e-codegen): `count_equals` and `count_min` assertions on named fields (e.g. `field: "warnings"`) now correctly emit `result.warnings.length` even when `result_is_simple = true`. Previously `result_is_simple` caused the field path to be dropped unconditionally, generating `result.length` which raised `NoMethodError` on `ConversionResult`.
- fix(ruby-e2e-codegen): `CallbackAction::CustomTemplate` visitor callbacks now emit Ruby double-quoted strings with `#{param}` interpolation (e.g. `{ custom: "--- #{text} ---" }`) instead of single-quoted literals. Single-quoted Ruby strings do not interpolate `{text}` placeholders, so the template was returned verbatim and the Rust core received a literal `{text}` instead of the substituted value.
- fix(r-e2e-codegen): non-empty `json_object` args (e.g. `options = {"preprocessing": {"remove_forms": false}}`) are now emitted as R `list(...)` expressions rather than defaulting to `NULL`. Previously the codegen collapsed any `is_object()` value to `r_default_for_config_arg()`, so fixture options containing nested structs like `PreprocessingOptions` were silently discarded and tests ran with default options instead.
- fix(r-e2e-codegen): `json_to_r` now converts PascalCase enum strings to snake_case (e.g. `"DoubleEqual"` → `"double_equal"`, `"AtxClosed"` → `"atx_closed"`) instead of plain `.to_lowercase()`. Plain lowercasing produced `"doubleequal"` which the R binding's `parse_highlight_style` rejected; snake_case matches the expected `"double_equal"`.
- fix(r-e2e-codegen): `CallbackAction::CustomTemplate` visitor callbacks now emit `paste0(...)` expressions for R string interpolation (e.g. `list(custom = paste0("[BTN:", text, "]"))`) instead of a plain double-quoted string literal. R has no built-in string interpolation so `"{text}"` in a string literal was passed verbatim to the Rust core instead of the substituted value.
- fix(r-e2e-codegen): `visit_details` callback parameter renamed from `is_open` to `open` to match the R binding's named argument passing (`("open", ...)` in the Pairlist). R's named-argument dispatch raised `unused argument (open = FALSE)` when the function declared `is_open` instead.
- fix(ffi-backend): remove unnecessary parentheses around `*vtable` dereference in visitor bridge constructor. The generated `(*vtable).clone()` caused a clippy `unused_parens` warning; simplified to `*vtable`.
- fix(pyo3-backend): `_coerce_dict_{name}` helper functions now annotate the `value` parameter as `dict[str, Any]` instead of bare `dict`. mypy `--strict` flags bare `dict` as missing type arguments (`type-arg` error).
- fix(c-e2e-codegen): remove unused `failed` counter from generated C test runner `main()`. The counter was declared and initialized to 0 but never incremented (C test failures abort the process), causing cppcheck to flag `failed > 0` as "comparison is always false". Changed return to `return 0;` and updated result printf to hardcode `0 failed`.
- fix(c-e2e-codegen): `not_empty` assertion now guards against NULL before calling `strlen` (e.g. `assert(var != NULL && strlen(var) > 0 && ...)`). Previously `strlen(var)` was emitted without a prior null check; when a `count_min` assertion on the same variable followed with `var != NULL`, cppcheck flagged `[nullPointerRedundantCheck]` — "either the condition is redundant or there is a possible null pointer dereference". Fixed in both the raw-C-result path and the `render_assertion` path (field-based assertions).
- fix(magnus-backend): `MAGNUS_RESERVED_FN_NAMES` no longer includes `"init"`. The generated entry-point function is now `fn ruby_init(ruby: &Ruby)` (not `fn init`), so `"init"` no longer conflicts with generated API function names and API functions named `init` are now emitted correctly.
- fix(codegen/structs): `gen_struct_default_impl` now uses `typed_default` from the IR field definition when generating `Default` impls for binding structs. Previously all non-optional fields fell through to `Default::default()`, emitting `false` for bool fields even when the core's `impl Default` set them to `true`. Now `BoolLiteral`, `IntLiteral`, and `StringLiteral` defaults are reproduced verbatim, fixing `ProcessConfig::default()` returning `structure: false` in Ruby/Python bindings when the core default is `true`.
- fix(pyo3-backend): data enum variant accessor keyword check now correctly uses the snake_case variant name for comparison against `RUST_KEYWORDS` instead of the original CamelCase name. Previously `Struct`, `Enum`, `Type` variants always passed the keyword check (CamelCase != lowercase keyword) and emitted invalid function names like `fn struct` / `fn enum` / `fn type` that failed to compile. Now `"Struct"` → `"struct"` → `fn r#struct`, matching what Python sees as a plain `struct` attribute.
- fix(pyo3-backend): data enum variant accessor methods now use the correct `pyo3::types::PyDict` path (was `pyo3::PyDict`, which does not exist in pyo3 0.28). Return type is `PyResult<Option<pyo3::Py<pyo3::types::PyDict>>>`.
- fix(pyo3-backend): data enum variant accessor methods now call `.downcast_into::<pyo3::types::PyDict>()?` followed by `.unbind()` to obtain an owned `Py<PyDict>`. Previously `.downcast()` returned `&Bound<'_, PyDict>` (a reference into a temporary), which could not be converted to `Py<PyDict>`, causing an `E0277: From<&pyo3::Bound<'_, PyDict>>` compile error.
- fix(pyo3-backend): data enum single-tuple variants whose inner type is a Named binding struct now return the typed struct directly (via `From`) instead of a Python dict. Multi-field or non-Named variants continue to use the JSON-to-dict path.

- feat(pyo3-backend): data enums now emit `#[getter]` accessor properties for each variant. For example, `FormatMetadata` now exposes `excel`, `docx`, `pdf` properties that return `Option<dict>` containing the variant's data fields. Enables ergonomic pattern: `result.metadata.format.excel["sheet_count"]`. Variant names are converted to snake_case for Python idioms; tag field names that are Rust keywords (e.g. `type`) are emitted with `r#` prefix (`fn r#type`) which PyO3 exposes to Python without the `r#`.
- feat(napi-backend): tagged unions now emit `#[napi(getter)]` accessor methods for each variant on the flattened struct. For example, `JsFormatMetadata` now includes `excel()`, `docx()`, `pdf()` getters that return `Option<napi::bindgen_prelude::Object>` containing the variant's field values. Enables ergonomic pattern: `result.metadata.format.excel?.sheetCount` with optional chaining in JavaScript/TypeScript.
- feat(pyo3-backend): data enums with a `serde_tag` field (e.g. `FormatMetadata`) now emit a `#[getter]` for the tag field on the opaque `#[pyclass(frozen)]` wrapper. The getter serializes `self.inner` to JSON and extracts the tag string (e.g. `format.format_type`). If the tag field name is a Rust keyword (e.g. `type`), the function is emitted with an `r#` prefix (`fn r#type`) — PyO3 exposes it to Python without the `r#`. The companion pyi stub is now a `class` with the tag attribute declared, rather than a `TypeAlias`, so type checkers see the attribute.
- feat(r-e2e-codegen): add `result_is_r_list` flag to `CallOverride`. When `true`, the R generator emits the call result directly without wrapping in `jsonlite::fromJSON()`, fixing test failures for bindings (like html-to-markdown's extendr R binding) that already return a native `Robj` list instead of a JSON string. Field-path assertions (`result$content`, etc.) are unaffected — the flag only suppresses the JSON parse wrapper.

### Fixed

- fix(php-backend): config params are now null-coalesced at call sites (`$config ?? new ConfigType()`) only when the config type can be constructed with zero arguments (all fields are optional in the IR). Previously any param whose type name ended with `Config` triggered null-coalescing unconditionally, which caused `new ProcessConfig()` calls in generated PHP facades — invalid because `ProcessConfig` has 8 required constructor fields. The codegen now pre-computes the set of all-optional-field types and restricts the null-coalesce rewrite to members of that set.
- fix(java-backend, csharp-backend): suppress `clippy::too_many_arguments` on `gen_sync_function_method` and `gen_wrapper_function`; both functions require all their parameters to drive code generation and cannot reasonably be refactored below the 7-argument limit without losing clarity.
- fix(extendr-backend): fix overindented doc list items in `gen_extendr_wrappers_r` doc comment.
- fix(e2e-typescript): collapse duplicate `if`/`else if` branches that both pushed `"undefined"` into a single `if` with an `||` condition.
- fix(e2e-typescript): revert vitest `singleFork` config — running all tests in one worker amplifies visitor teardown crashes across unrelated test files; default pool isolation is safer.
- fix(e2e-node): options are now constructed as a plain object literal with a type assertion (`{ key: val } as ConversionOptions`) and imported type-only. The previous `ConversionOptions.fromUpdate(new ConversionOptionsUpdate({...}))` pattern failed at runtime because `ConversionOptions` is a TypeScript interface, not a class.
- fix(e2e-wasm): options are now constructed via empty constructor + setter assignments wrapped in an IIFE (`(() => { const _u = new WasmConversionOptionsUpdate(); _u.key = val; return WasmConversionOptions.fromUpdate(_u); })()`). The previous pattern passed an object literal to a positional constructor (40+ args), silently landing it as `heading_style` and ignoring all intended values.

### Fixed

- fix(extendr-backend): types whose fields include `Vec<Vec<T>>` or `Option<Vec<Vec<T>>>` are now excluded from `#[extendr]` class and function generation. `extendr_api` has no `TryFrom<&Robj>` implementation for nested vectors, so emitting `#[extendr]` on such types produced a compile error (`error[E0277]: the trait bound … is not satisfied`). Affected types (`ExcelSheet`, `OcrTable`, `Table`, `RecognizedTable`) are now skipped from the extendr class list and the derived function list; their struct definitions are preserved so serde serialization still works. Symptom: `kreuzberg-r` failed to compile with `error[E0277]: Option<Vec<Vec<String>>>: extendr_api::TryFrom<&extendr_api::Robj> not satisfied`.
- fix(php-backend): `ConversionOptionsBuilder.visitor()` now uses `Rc<RefCell<PhpHtmlVisitorBridge>>` instead of `Arc<PhpHtmlVisitorBridge>` for the `VisitorHandle` wrapper. `html_to_markdown_rs::visitor::VisitorHandle` is a type alias for `Rc<RefCell<dyn Visitor>>`, which is `!Send`; using `Arc` produced a compile error (`error[E0308]: mismatched types: expected Rc<RefCell<dyn Visitor>>, found Arc<PhpHtmlVisitorBridge>`). Also removed the now-redundant `&` in `Some(&handle)` (Rc is already an owned handle).
- fix(php-backend): `gen_enum_tainted_from_binding_to_core` now accepts a `bridge_type_aliases` set and, for sanitized fields whose type is a bridge alias, emits `(*val.{name}.inner).clone()` instead of `Default::default()`. Previously bridge-type fields in the tainted-enum conversion always fell back to `Default::default()`, so the actual PHP-supplied bridge object was silently discarded — the `ConversionOptions` carrying a non-None `visitor` always produced a `None` visitor when bridging from PHP binding to Rust core.
- fix(php-backend): the null-coalescing eligible set now also includes types that derive `Default` (i.e. `has_default == true`), not just types whose every IR field is `Optional`. Previously, struct types with `bool` fields carrying `#[serde(default = "…")]` (such as `ExtractionConfig`) were excluded from null-coalescing because their `bool` IR fields are not tagged `optional`, even though calling `new ExtractionConfig()` in PHP works fine via `Default`. This caused config parameters to lose their `?T = null` nullable signature and break callers that omit the config.
- fix(php-backend): `ConversionOptionsBuilder.visitor()` method now correctly accepts a `&mut ZendObject` parameter, creates a `PhpHtmlVisitorBridge` from it, wraps it in `Arc::new()`, and passes `Some(&handle)` to the inner builder — instead of ignoring the parameter and always passing `None`. The PHP visitor bridge can now be invoked through the builder API.
- fix(php-e2e-codegen): `CallbackAction::CustomTemplate` placeholder interpolation now converts `{key}` to `{$key}` in PHP double-quoted strings, enabling variable expansion in generated visitor callback templates. Template `"~{text}~"` now generates `return ['custom' => "~{$text}~"];` so PHP's string interpolation evaluates `$text` variables correctly.
- fix(java-backend): builder classes now carry `@JsonIgnoreProperties(ignoreUnknown = true)` so flattened tagged-enum discriminators (e.g., `format_type` from `FormatMetadata` flatten on `Metadata`) and forward-compatible additions don't fail Jackson's strict deserialization. Without this, every Java SmokeTest errored with `UnrecognizedPropertyException: Unrecognized field "format_type" (class dev.kreuzberg.MetadataBuilder)` because Rust core's `#[serde(flatten)] format: Option<FormatMetadata>` puts the tag at the top level of the metadata JSON and Builder + `@JsonTypeInfo` + `@JsonUnwrapped` are mutually incompatible in Jackson.
- fix(go-backend): bytes parameters no longer panic on empty slices. The previous emission `(*C.uint8_t)(unsafe.Pointer(&content[0]))` panics with `index out of range [0] with length 0` when callers pass `[]byte{}`. The codegen now emits a nil-or-pointer guard so `kreuzberg.ExtractBytesSync(nil, ...)` / `kreuzberg.ExtractBytesSync([]byte{}, ...)` reach the FFI layer cleanly and surface the Rust-side validation error instead of crashing the test binary.
- fix(extract/defaults): const-fold integer/float binary arithmetic in `fn default()` bodies. Patterns like `500 * 1024 * 1024`, `1024 * 1024`, and `100 * 1024 * 1024` (used pervasively for byte-size limits) previously fell through `expr_to_default_value` to `DefaultValue::Empty`, so generated bindings emitted `0` instead of the real default. With `<int> OP <int>` and `<float> OP <float>` now folding through `+ - * / % << >> | & ^` (with overflow checks), `SecurityLimits` and similar config types finally surface their actual default-byte-limits to Python/Java/Go/etc., fixing `RuntimeError: Validation error: ZIP archive text content exceeds limit: 3045 bytes (max: 0 bytes)`.
- fix(napi-backend/default-optional): Named function params whose type derives `Default` (e.g. `ExtractionConfig`) are now promoted to `Option<T>` in the binding signature, with the body emitting an `unwrap_or_default()` coercion before the rest of the let-bindings run. JS callers can now omit the slot or pass `undefined` and get a default-constructed value, matching the wasm-bindgen path's serde behaviour. Without this, idiomatic `batchExtractBytesSync(items, undefined)` failed at NAPI's struct deserializer with `TypeError: Cannot convert undefined or null to object`.
- fix(codegen/promoted-optional): promoted-optional Named params (those that are required in the IR but appear after a naturally-optional param, so the binding signature carries `Option<T>`) now emit `unwrap_or_default()` instead of `.expect("'…' is required")` when bridging to core. JS/Python callers idiomatically pass `undefined`/`None` for these slots, which previously triggered a Rust panic that vitest reported as `Error: Panic in async function` / `'config' is required`. Binding types always derive `Default`, so this is a clean drop-in.
- fix(napi-backend): structs containing `Buffer` fields now skip the `Clone` derive (Buffer doesn't impl Clone) and emit a manual `Clone` impl that copies via `to_vec().into()`. Bytes fields are tagged `#[serde(skip)]` so the rest of the struct can still derive Serialize/Deserialize (Buffer's `Default` impl is used to reconstruct on deserialize). Without this, generated `JsBatchBytesItem`/`JsExtractedImage`/etc. fail to compile with `the trait Clone is not implemented for Buffer` and `the trait serde::Serialize is not implemented for Buffer`.
- fix(napi-backend): function return type `TypeRef::Bytes` now wraps the core `Vec<u8>` (or `bytes::Bytes`) result with `.into()` so it converts cleanly into the `napi::bindgen_prelude::Buffer` return type. Free functions like `render_pdf_page_to_png` (`Result<Vec<u8>>`) previously emitted a bare `core_call.map_err(...)` body, producing `expected Result<Buffer, Error>, found Result<Vec<u8>, Error>` at compile time once `Vec<u8>` struct fields started mapping to `Buffer`.
- fix(codegen/bytes-dedup): `binding_to_core` and `core_to_binding` `apply_*_wrapper` helpers now recognise the new `val.{name}.to_vec().into()` and `val.{name}.map(|v| v.to_vec().into())` forms emitted by `TypeRef::Bytes` field-conversions and pass them through unchanged. Without this dedup, `CoreWrapper::Bytes` re-wrapped them, producing `(val.x.to_vec().into()).into()` and `val.x.map(|v| v.to_vec().into()).map(|v| v.to_vec().into())` — which fail with `cannot infer type of the type parameter T` and `cannot find function .into for type T`.

- fix(java-e2e-codegen): nested-record builder methods on primitive-fields types (e.g. `SecurityLimitsBuilder.withMaxContentSize(long)`) now receive the bare numeric literal instead of `Optional.of(1L)`. Earlier the codegen wrapped every numeric in `Optional.of()` unless the camelCase field name matched a hardcoded plain-numeric whitelist; that whitelist missed all SecurityLimits fields, so generated tests failed `javac` with `incompatible types: no instance(s) of type variable(s) T exist so that java.util.Optional<T> conforms to long`.
- fix(go-backend): emit `MarshalJSON` on structs whose fields include `[]byte` (Vec<u8>) so the bytes serialize as a JSON array of integers — matching Rust serde's expected `Vec<u8>` deserializer shape. Go's default `json.Marshal([]byte)` emits a base64 string, which Rust's deserializer rejected with `invalid type: string "SGVsbG8=", expected a sequence`.
- fix(napi-backend): `Vec<u8>` struct fields now emit as `napi::bindgen_prelude::Buffer` rather than the bare `Vec<u8>`. NAPI-rs 3.x treats `Vec<u8>` as a JS `Array`, calling `napi_get_array_length` on the value — which fails with "Failed to get Array length" when the JS side passes a `Buffer` or `Uint8Array` (the canonical byte-payload types). Using `Buffer` accepts both and avoids a copy.
- fix(java-e2e-codegen): enum-typed builder method parameters now receive correctly-typed enum constants (e.g., `withCodeBlockStyle(CodeBlockStyle.Backticks)`) instead of string literals. The codegen now uses a configuration-driven enum field mapping (camelCase field name → enum type name) to determine which string values are enums and emit them as qualified enum constants. Fixture JSON like `"code_block_style": "Backticks"` is now converted to `withCodeBlockStyle(CodeBlockStyle.Backticks)` rather than `withCodeBlockStyle("Backticks")`. The mapping is configurable via `[crates.e2e.call.overrides.java.enum_fields]` in `alef.toml` (e.g., `headingStyle = "HeadingStyle"`, `codeBlockStyle = "CodeBlockStyle"`).
- fix(java-e2e-codegen): numeric Optional fields are no longer incorrectly wrapped in `Optional.of()`. Fields like `list_indent_width` and `wrap_width` that have plain `long` signatures now emit bare numeric literals (e.g., `withWrapWidth(40L)`), while truly optional numeric fields are wrapped (e.g., `withMaxDepth(Optional.of(100L))`). The codegen maintains a whitelist of plain numeric field names and distinguishes between the two cases.
- fix(java-e2e-codegen): nested options types like `PreprocessingOptions` are now correctly imported when used in builder expressions. The codegen now detects nested type usage via a configuration-driven mapping (`[crates.e2e.call.overrides.java.nested_types]` in `alef.toml`, e.g., `preprocessing = "PreprocessingOptions"`) and emits the corresponding import statements, preventing compilation errors when nested options builders are referenced.
- fix(php-e2e-codegen): empty config objects (`{}`) in fixture `input.config` are now passed as `null` to config constructors instead of being serialized and passed to `ExtractionConfig::from_json(json_encode([]))`. Empty objects cause Rust serde deserialization errors when required fields have enum defaults (e.g., `hierarchy_mode` expecting `"unified"` or `"element_based"`). The codegen now detects empty-after-filtering config objects in the `json_object` / `options_type` code paths and directly passes `null` to match the nullable parameter type. Symptom: 10 PHP e2e smoke/format/contract tests failed with `Exception: unknown variant '', expected 'unified' or 'element_based' at line 1 column 397`.
- fix(java-e2e-codegen): typed-record nested config fields (e.g. `SecurityLimits`, `OcrConfig`, `ChunkingConfig`) are now automatically detected and used to instantiate the correct Java record type instead of being referenced as a fictitious `TypeNameOptions` builder. The codegen now includes built-in type mappings for all Kreuzberg config fields (security_limits, chunking, ocr, etc.), allowing users to omit explicit `nested_types` configuration in alef.toml for common cases. Previously unmapped fields like security_limits were reconstructed with a hardcoded `Options` suffix, causing `error: cannot find symbol SecurityLimitsOptions` when compiling e2e tests.
- fix(pyo3-backend): Python API wrapper now correctly excludes data enums (tagged unions like `OutputFormat`) from `_coerce_enum` calls when converting dict inputs to dataclass instances. Data enums are represented as TypedDict unions in Python and should be passed through to the PyO3 constructor as dicts, not looked up as enum class attributes. Additionally, nested has*default struct types (e.g., `SecurityLimits`, `OcrConfig`) in dict inputs are now recursively converted via their respective `\_to_rust*\*`converters before constructing the dataclass, preventing`TypeError: argument 'field': 'dict' object is not an instance of 'Type'`when callers pass nested dicts instead of constructed instances. Symptom: 3 Python e2e tests failed with`ValueError: unknown OutputFormat value: 'markdown'`and`TypeError: argument 'security_limits': 'dict' object is not an instance of 'SecurityLimits'`.
- fix(csharp-e2e-codegen): typed-record nested config fields (e.g. `SecurityLimits`, `OcrConfig`, `ChunkingConfig`) are now automatically detected and deserialized via `JsonSerializer.Deserialize<T>(...)` instead of being passed as raw JSON strings. The codegen now includes built-in type mappings for all Kreuzberg config fields (security_limits, chunking, ocr, etc.), allowing users to omit explicit `nested_types` configuration in alef.toml for common cases. Previously only explicitly-configured nested_types fields were deserialized, causing `error CS0029: Cannot implicitly convert type 'string' to 'Kreuzberg.SecurityLimits'` for unmapped fields like security_limits.
- fix(java-backend): generated Java source files now comply with the 120-character line-length limit enforced by Checkstyle. Multi-line record fields with long type annotations are now wrapped such that (a) each annotation appears on its own line, and (b) the type and field name are further split if necessary to keep each line ≤120 characters. Trait bridge registry fields (`ConcurrentHashMap<String, {BridgeClass}>`) are now emitted on two lines instead of one when they would exceed 120 characters. Symptom: `mvn package -DskipTests` was failing with 5 Checkstyle [LineLength] violations in generated Java sources (HtmlMetadata.java, PostProcessorBridge.java, JatsMetadata.java, EmbeddingBackendBridge.java, ExtractionResult.java).
- fix(go-backend): Go bindings for `ExtractionResult.tables` now correctly declare the field as `[]Table` instead of `[]string`. The exclude_types list was using unqualified type names (`"Table"`, `"TableCell"`, `"TableRow"`), which incorrectly excluded the public `kreuzberg::types::tables::Table` type. Changed to fully-qualified names (`"kreuzberg::extraction::docx::parser::Table"`, etc.) to exclude only the internal docx parser types, allowing the public table types to be correctly generated in language bindings.
- fix(go-e2e-codegen): pass `file_path` args directly as fixture-relative strings — the e2e/go `TestMain` in `main_test.go` already `os.Chdir`s into the repo-root `test_documents/` directory, so additional `filepath.Abs(filepath.Join("../../test_documents", path))` resolution would land outside the kreuzberg repo. Reverted the earlier path-rewriting attempt and removed the now-unnecessary `path/filepath` import.
- fix(go-e2e-codegen): `bytes` args whose fixture value is a string are now loaded via `os.ReadFile(path)` at test-run time (matching the rust e2e codegen's `std::fs::read` convention) instead of `base64.StdEncoding.DecodeString`. Fixture strings like `"data": "pdf/fake_memo.pdf"` are fixture-relative paths into `test_documents/` (TestMain chdirs there), not base64 — the previous decoder produced garbage bytes that the parser rejected as invalid PDF/DOCX/etc.
- fix(csharp-backend): trait bridge code generation now emits a generic `FfiJsonExtensions.ToFfiJson<T>()` extension method for serializing trait method return values; methods returning named types (enums, records, classes) were generating calls to `.ToFfiJson()` on the result object, but the method was never emitted, causing CS1061 compilation errors. The extension method is now always generated as part of the TraitBridges.cs file.
- fix(elixir-e2e): drop `optional: true` from the generated e2e `mix.exs` rustler dep — `RustlerPrecompiled` falls back to `rustler` only when `force_build: Mix.env() in [:test, :dev]`, but `optional: true` makes mix skip fetching the package, so the build-from-source path used by every e2e run failed with "the dependency is not available, run mix deps.get".
- fix(rust-e2e): `bytes` args resolved as fixture file paths now use `../../test_documents/` (two levels up) — `CARGO_MANIFEST_DIR` for an e2e test crate is `e2e/<lang>/`, so reaching the repo-root `test_documents/` directory needs two parent steps, not one. Also added explicit `file_path` arg handling so `extract_file*` calls receive an absolute path instead of a fixture-relative string.
- fix(typescript-e2e-codegen): `visit_input` visitor method now declares the first non-context parameter as `input_type` (snake_case) instead of `inputType`, matching the fixture template placeholder `{input_type}`. The previous camelCase name caused a `ReferenceError: input_type is not defined` in template-literal visitor callbacks, crashing the vitest worker.
- fix(r-e2e-codegen): `options` call argument now defaults to `NULL` instead of the non-existent `HtmlOutputConfig$default()`. The `"options" | "html_output"` arm was merged; splitting it makes `"options"` emit `NULL` (R NULL, passed directly) while `"html_output"` retains the class default. Previously all 239 R e2e tests failed with `Error: could not find function "HtmlOutputConfig$default"`.
- fix(r-e2e-codegen): visitor test call no longer emits a duplicate `options` parameter. When a fixture has both a visitor spec and an `options` argument that defaults to `NULL`, the codegen now strips the `options = NULL` from the base argument string before appending `options = list(visitor = visitor)`, preventing the `Error: formal argument "options" matched by multiple actual arguments` R error.
- fix(java-e2e-codegen): generated test files now import only the nested config types (e.g. `PreprocessingOptions`) that are actually referenced in fixture builder expressions, rather than all entries in the `nested_types` defaults map. The previous approach imported product-specific types like `ChunkingConfig` and `OcrConfig` unconditionally, causing `error: cannot find symbol` javac failures in bindings that do not expose those types (e.g. html-to-markdown).
- fix(php-backend): visitor bridge methods now use snake_case method names when calling into the PHP object (e.g. `visit_link` instead of `visitLink`), matching PHP's conventional naming for method names. Previously the bridge called camelCase methods which PHP userland implementations would not define, causing all visitor callbacks to silently fall through to `Continue`.
- fix(php-backend): visitor bridge result parsing now handles PHP array returns `['custom' => 'replacement']` in addition to string returns. Added `php_zval_to_visit_result()` helper that tries `val.string()` first (for `skip`/`continue`/`preserve_html`/custom strings) and then `val.array().get("custom")` (for `['custom' => ...]` returns), using the ext_php_rs 0.15.12 `Zval::array()` and `ZendHashTable::get()` APIs.
- fix(scaffold/node): also emit `packages/<crate>/index.js` (CommonJS shim that re-requires from `crates/<crate>-node/index.js`). Previously only `index.d.ts` was emitted, but `package.json` declares `"main": "index.js"`, so vitest/node failed to resolve the package entry from downstream consumers.
- fix(extract): `extract_serde_rename_all` now properly consumes sibling values inside `serde(...)` and `cfg_attr(..., serde(...))`; previously a leading sibling key (e.g. `tag = "..."` before `rename_all = "..."`) left the parser cursor mid-value, aborting the parse and dropping `rename_all`. Switched the skip path from `let _ = meta.value();` to `let _: syn::Expr = meta.value()?.parse()?;` so the value is actually consumed. Symptom: tagged enums emitted lowercase discriminators (`"listitem"`) instead of `snake_case` (`"list_item"`) in C#/Python/etc. bindings.
- fix(extendr-backend): `generate_public_api` now emits `extendr-wrappers.R` directly from the IR plus a regenerated `NAMESPACE` listing every free function and class env. Previously the package only shipped a `useDynLib` stub, expecting `rextendr::document()` to fill in the wrappers — but rextendr never runs at install time, so every R caller saw `Error: could not find function "extract_file_sync"` (and every other binding). Wrappers cover free functions, static methods, instance methods (with `self` captured by `$.Type` dispatch), and class env definitions for every type registered in `extendr_module!`.
- fix(csharp-e2e-codegen): `JsonElement?` typed fields (discriminated unions like `output_format`) are now wrapped with `JsonDocument.Parse(…).RootElement` instead of emitted as plain string literals; `output_format = "markdown"` now generates `OutputFormat = JsonDocument.Parse("\"markdown\"").RootElement`, avoiding `error CS0029: Cannot implicitly convert type 'string' to 'System.Text.Json.JsonElement?'`.
- fix(rust-e2e-codegen): element types from `json_object` args with `element_type` specified are now imported in the generated test file; batch operations that use `serde_json::from_value::<Vec<{elem}>>()` previously failed to compile with E0425 (cannot find type) because types like `BatchFileItem` and `BatchBytesItem` were not in scope.
- fix(csharp-backend): `JsonOptions` now uses `DefaultIgnoreCondition = WhenWritingNull` instead of `WhenWritingDefault`; bool fields explicitly set to `false` (CLR default) were silently dropped when serializing options to Rust FFI, so e.g. `remove_forms: false` was never sent and Rust fell back to its own default of `true`.
- fix(csharp-backend): generated `VisitResult` record now includes a `ToFfiJson()` method that emits Rust-serde-compatible JSON for visitor return values (`"Continue"`, `"Skip"`, `"PreserveHtml"`, `{"Custom":"…"}`, `{"Error":"…"}`); the previous `JsonSerializer.Serialize` produced `{}` for unit-variant records, which Rust could not deserialize.
- fix(csharp-backend): visitor bridge entry point names now use `to_snake_case()` instead of `to_lowercase()` (e.g. `htm_html_visitor_bridge_new` not `htm_htmlvisitorbridgenew`); the wrong name caused `EntryPointNotFoundException` at runtime.
- fix(csharp-backend): `catch (Exception ex)` → `catch (Exception)` in options-field trait bridges; the `ex` variable was unused in that code path, triggering CS0168 compile error.
- fix(csharp-e2e): options arguments now emitted as idiomatic C# object initializers (`new ConversionOptions { Prop = Value }`) with PascalCase property names instead of `JsonSerializer.Deserialize<ConversionOptions>(json)` calls; the JSON path used camelCase keys that did not match `[JsonPropertyName("snake_case")]` attributes.
- fix(csharp-e2e): add `nested_types` field to `CallOverride`; maps fixture nested-object field names to C# type names so nested structs are generated as `Preprocessing = JsonSerializer.Deserialize<PreprocessingOptions>(…)` rather than inlined JSON.
- fix(cli/build): `cargo` build dispatch no longer collapses to `cd  && cargo build --release` when a language has no explicit `output` path (e.g. Dart in FRB style, whose Rust crate lives at `packages/<lang>/rust/` while generated sources go to `packages/<lang>/lib/src/`). The cargo branch now short-circuits to `cargo build -p <crate-name><crate_suffix>` when `crate_dir` is empty and `BuildConfig::crate_suffix` is set, matching the workspace-member case.
- fix(scaffold/node): generated `kreuzberg-node` `Cargo.toml` now always includes `serde = { version = "1", features = ["derive"] }` and suppresses the `cargo-machete` false positive with `[package.metadata.cargo-machete] ignored = ["serde"]`; machete flagged serde as unused because derive macros reference it via fully-qualified paths not detected by static analysis.
- fix(napi): NAPI type map now emits `Vec<u8>` instead of removed `napi::Buffer` for `TypeRef::Bytes`; `napi::Buffer` was removed in NAPI v3 and JS `Uint8Array` ↔ `Vec<u8>` conversion is now automatic.
- fix(php): `gen_enum_tainted_from_binding_to_core` now emits `#[allow(clippy::useless_conversion)]` before the generated `impl From` block; enum-tainted structs with `Vec<u8>` or `String` fields triggered clippy errors for no-op `.into()` conversions.
- fix(csharp-backend): `ConvertWithVisitor` code path now uses `ConversionResultToJson` + `ConversionResultFree` on the returned handle instead of treating the pointer as a direct JSON string, matching the updated FFI API.
- fix(go-e2e-codegen): visitor test fixtures now correctly emit `opts := &htmd.ConversionOptions{}; opts.Visitor = visitor; result, err := htmd.Convert(html, opts)` instead of an undeclared `options` variable and passing `nil` to Convert. The codegen now creates a fresh `opts` variable with the visitor attached and correctly replaces the trailing `, nil` with `, opts` in the function call.
- fix(java-e2e-codegen): visitor test fixtures now correctly emit `convert(html, new ConversionOptions().withVisitor(visitor))` instead of losing the html argument. The codegen now properly detects and replaces optional arguments when visitor is present.
- fix(java-e2e): generate idiomatic builder expressions instead of MAPPER.readValue for json_object args; e.g., `ProcessConfig.builder().withLanguage("go").withChunkMaxSize(Optional.of(50L)).build()` instead of `MAPPER.readValue("{\"language\":\"go\",\"chunk_max_size\":50}", ProcessConfig.class)`. Builder methods wrap number/boolean values in Optional.of() to match Optional<T> parameter types. Removes MAPPER dependency for options deserialization.
- fix(go-backend): `*C.char` returns now check `if ptr == nil { return nil }` before calling `C.GoString`; previously `C.GoString(nil)` returned `""` and `&v` was returned as a non-nil `*string`, so `Option<String> = None` was misrepresented as a non-nil pointer to an empty string.
- fix(go-backend): `{ffi_prefix}_last_error_context()` is now used in all receiver/param `from_json` error messages; the `kreuzberg_` prefix was hardcoded, causing a CGo "could not determine what C.kreuzberg_last_error_context refers to" build error when the FFI prefix differs.
- fix(java-backend): Java builder classes now derive boolean field defaults from the extracted `impl Default` block (`typed_default`) instead of a hardcoded type-name list; previously `ProcessConfig.structure`, `.imports`, and `.exports` defaulted to `false` in the builder while Rust defaults them to `true`, causing the Java binding to explicitly pass `false` to Rust and suppress all structural analysis.
- fix(java-backend): `createObjectMapper()` now calls `.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)` so Jackson maps snake_case JSON keys (e.g. `total_lines`) to camelCase builder `with`-methods during deserialization.
- fix(java-e2e): `contains` / `contains_all` / `not_contains` assertions on array fields now call `.toString().contains(value)` instead of `List.contains(value)`; `List.contains` uses `equals()` and always returns false when comparing a `String` against complex record types such as `StructureItem`.
- fix(e2e/csharp): config arguments are now generated as typed C# object initializers (`new ProcessConfig { Language = "abl" }`) with PascalCase property names instead of raw `JsonSerializer.Deserialize` calls; this matches the `[JsonPropertyName]`-annotated C# API surface and avoids snake_case key mismatches.

## [0.14.23] - 2026-05-05

### Added

- feat(hooks): add `hooks/alef_hook.py` so consumers can run `alef` as a pre-commit hook without a local Rust toolchain — the hook reads the version from `alef.toml`, downloads, sha-verifies, caches, and execs the matching pre-built `alef` binary for the host platform.

### Changed

- chore(scaffold/rustler): generated Elixir NIF wrappers now use `RustlerPrecompiled` unconditionally with a `force_build:` option pointing at `<APP>_BUILD_FROM_SOURCE` / `Mix.env() in [:test, :dev]`, instead of branching on `if force_build do use Rustler else use RustlerPrecompiled end`. The branching pattern broke `mix compile` in published packages because `RustlerPrecompiled.__using__` cannot be invoked dynamically.
- chore(scaffold/java): generated Checkstyle config tightens line length from 200 → 120, adds `UnusedImports`, and caps `MethodLength` at 150 lines, matching the wider Kreuzberg Java conventions.
- chore(scaffold/php): generated `composer.json` emits a single-escaped `psr-4` namespace (`"Foo\\": "src/"`) and a lowercased package `name`; the previous double-escaped form produced an invalid `composer.json`, and uppercase names violate Composer's name regex.

### Fixed

- fix(csharp-backend): remove public `ConvertWithVisitor` method and embed visitor handling into single `Convert(string html, ConversionOptions? options)` method; `ConversionOptions.Visitor` is now typed as `IHtmlVisitor?` (managed interface) with `[JsonIgnore]` instead of `VisitorHandle?`, achieving full API parity across all language bindings (one convert function per language, visitor passed via options).
- fix(java-backend): mirror the C# visitor fix on the Java FFI class — `convertWithVisitor` is no longer emitted as a parallel public method; the visitor dispatch is folded into the main `convert` body, with the `has_visitor_bridge` flag plumbed through `gen_sync_function_method`.
- fix(rustler): generated Elixir visitor wrappers now pattern-match `{:ok, _}` instead of `:ok` on the `_with_visitor` NIF call; Rustler encodes `Result<(), String>` as `{:ok, {}}`, so the bare `:ok` match always failed at runtime.
- fix(php): `from_binding_skip_types` field added to `ConversionConfig`; the PHP backend populates it with trait bridge type aliases (e.g. `VisitorHandle`) so their fields emit `Default::default()` in the binding→core `From` impl instead of `val.field.map(Into::into)`, which failed to compile because no `From` impl exists for the PHP-side bridge handle.
- fix(e2e/rust): `not_error` assertions on `Result`-returning calls now emit `.expect("should succeed")` so the test actually panics on errors; previously the result was bound to `_` and silently discarded, making the assertion a no-op.
- fix(php-backend): PHP method names now mirror the Rust source name verbatim (camelCased) without an extra `Async` suffix; the suffix was breaking parity with `alef.toml` call overrides.
- fix(e2e/go): byte-array JSON arguments are now base64-encoded for Go `json.Unmarshal` compatibility; previously the raw `[0, 1, 2, …]` integer array failed to unmarshal into `[]byte`.
- fix(e2e/go): `not_error` and `error` assertions are excluded from the "only nil assertions" short-circuit, so a single `not_error` / `error` assertion no longer collapses the entire test body into a no-op.
- fix(e2e/go): `has_deref_value` (which selects `value` vs `result_var` as `effective_result_var`) now also excludes `not_error` / `error` assertions; previously a `not_error + is_empty` fixture emitted a nil check against undefined `value` while `result` was declared-but-unused, causing a compile error.
- fix(e2e/r): `run_tests.R` now resolves script and test directories relative to its own path before changing the working directory to `test_documents/`; also sets `testthat::set_max_fails(Inf)` to surface all failures during triage.
- fix(scaffold/r): R scaffold `Makevars` files now use the Rust crate name (`{core}_r`) for the static library path rather than the R package name; the two differ when the R package uses a custom user-facing name.
- fix(napi): `Vec<u8>` / `Bytes` parameters now emit a `let name: Vec<u8> = name.to_vec();` conversion in the generated function body so JS `Buffer` arguments round-trip through `napi::Buffer` to the Rust core. Adds a dedicated `gen_napi_buffer_conversion_bindings` helper and `is_bytes_param` predicate, and gates `use_let_bindings` on bytes params so the conversion always runs.
- fix(magnus/trait_bridge): `default_types` parameters in bridge functions are now passed as the binding type and converted to the core type via `.into()` (the Magnus binding already accepts the typed value), instead of being typed `Option<String>` / `String` and parsed via `serde_json::from_str`. The JSON-string detour silently dropped non-string fixture values and forced callers to pre-encode every nested struct.
- fix(codegen/config_gen): `gen_magnus_kwargs_constructor` now accepts kwargs as `Option<magnus::RHash>` via `scan_args`, so callers can omit the kwargs hash entirely (matching idiomatic Ruby) instead of always passing an empty `{}`.
- fix(codegen/shared): `constructor_parts_with_renames` no longer double-wraps already-`Optional<T>` fields in another `Option<…>`; previously fields whose IR type is already optional became `Option<Option<T>>` in the generated constructor signature.
- fix(e2e/wasm): `globalThis.process.chdir(testDocumentsDir)` replaces `process.chdir(testDocumentsDir)`; the WASM module exports a `process` function that shadowed the Node.js global `process` object, making `.chdir` unavailable at test setup time.
- fix(e2e/c): C generator now reads `options_type` from the call override (`[e2e.call.overrides.c]`) instead of hardcoding `ConversionOptions`; generated code uses the configured type name (e.g. `ProcessConfig`) for both the `*_from_json` constructor and `*_free` calls.
- fix(e2e/csharp): when a visitor is present but no options argument exists, the generator now emits `new OptionsType { Visitor = visitorVar }` instead of appending the visitor as a bare extra argument.
- fix(csharp-backend): `is_convert_with_visitor` early-return path now emits the closing brace and returns immediately, preventing the method body from falling through into the standard param-setup path.
- fix(hooks): `alef_hook.py` now reads the `alef` version from `alef.toml` at hook-cache root instead of a static `hooks/VERSION` file; the `hooks/VERSION` file is removed. This eliminates version-skew bugs where `Cargo.toml` was bumped without a matching `VERSION` update, causing the hook to download a non-existent release artifact.
- fix(.pre-commit-config.yaml): add `default_stages: [pre-commit]` to prevent hooks without explicit `stages:` from running in the `commit-msg` stage (where they receive the commit message file instead of source files).

## [0.14.22] - 2026-05-04

### Fixed

- fix(e2e/wasm): `inject_wasm_init` now loads the WASM binary via `readFileSync` and passes the buffer directly to `init()`; Node.js `fetch` does not support `file://` URLs, so the previous `await init()` (no arguments) failed with `TypeError: fetch failed`.

## [0.14.21] - 2026-05-04

### Fixed

- fix(e2e/go): remove `needs_json_stringify` from `needs_json` gating — `jsonString()` lives in `helpers_test.go` which has its own `encoding/json` import; individual test files no longer emit a spurious `import "encoding/json"` that the Go compiler rejects as unused.
- fix(e2e/typescript): `json_object` args with array values (e.g. batch items) are no longer cast to the `options_type` — only non-array object args receive the interface cast.
- fix(e2e/typescript): empty-object `json_object` args now emit `{} as OptionsType` (interface cast) instead of `new OptionsType()` (class instantiation); TypeScript options types are interfaces, not classes.
- fix(e2e/csharp): `json_array_to_csharp_list` now handles typed class element types (e.g. `BatchBytesItem`) by emitting `JsonSerializer.Deserialize<T>(json, ConfigOptions)!` per element; previously fell through to `List<string>` which caused a compile error when the C# binding expects `List<T>`.

- fix(e2e/wasm): `inject_wasm_init` now searches backward from `} from "pkg_name"` to locate the correct `import {` block; previously it matched the first `import {` in the file (the vitest import), corrupting the vitest import line.
- fix(pyo3-backend): inject `from_json` staticmethod into the existing `#[pymethods]` block instead of emitting a second one; avoids requiring the `multiple-pymethods` pyo3 feature which is not enabled by default.
- fix(napi-backend): visitor and plugin method calls with N>0 arguments now use `FnArgs::from(tuple)` instead of passing the raw tuple directly; the blanket `JsValuesTupleIntoVec` impl was treating a `(Unknown, Unknown)` tuple as a single NAPI value, causing all extra arguments to arrive as `undefined` in JavaScript callbacks.
- fix(e2e/csharp): `equals` assertions on optional string fields now emit `field!.Trim()` (null-forgiving) instead of `field.Trim()` to suppress CS8602 nullable warnings.
- fix(csharp-backend): typed error classes (e.g. `ErrorException`) now inherit from the library's generic fallback exception class (e.g. `TreeSitterLanguagePackException`) instead of `Exception` directly; `Assert.ThrowsAny<LibException>()` now correctly catches typed errors.
- fix(csharp-backend): `TreeSitterLanguagePackException` (the generic fallback) gains `(string message)` and `(string message, Exception innerException)` constructors so derived error classes can call them without a numeric code.
- fix(e2e/csharp): use `Assert.ThrowsAny<T>()` / `Assert.ThrowsAnyAsync<T>()` instead of exact `Assert.Throws<T>()` for `is_error` assertions; the base exception class now matches derived exception subclasses (e.g. `ErrorException` from `TreeSitterLanguagePackException`).
- fix(e2e/csharp): `contains`, `contains_all`, `not_contains`, and `contains_any` assertions on list fields use `JsonSerializer.Serialize(field)` instead of `field.ToString()`; `List<T>.ToString()` returns the type name rather than the contents.
- fix(e2e/csharp): `result_is_array = true` (simple list results such as `manifest_languages()`) now also serialize via `JsonSerializer.Serialize` for substring assertions, not `result.ToString()`.

## [0.14.20] - 2026-05-04

### Fixed

- fix(rustler-backend): `gen_nif_async_function` no longer double-appends `_async` when the Rust function name already ends with `_async` (e.g. `embed_texts_async` → NIF was `embed_texts_async_async`); the generated NIF name now matches the Elixir native.ex stub.
- fix(napi-backend): add `#![allow(unsafe_code)]` inner attribute to generated Node.js bindings; NAPI-RS bridge code for trait visitors emits unsafe blocks that were previously rejected by the workspace-level `-D unsafe_code` lint.
- fix(e2e/wasm): `inject_wasm_init` now adds `init` as the **default** export (`import init, { ... }`) instead of a named export (`import { ..., init }`); wasm-pack exports `init` as a default export, so the named-import form produced `TypeError: init is not a function`.
- fix(e2e/go): `contains`, `contains_all`, `not_contains`, and `contains_any` assertions on optional array fields no longer emit `jsonString(*field)` (invalid dereference of a Go slice); Go slices are nil-able value types, so `jsonString(field)` is emitted directly with the nil guard on the field unchanged.
- fix(csharp-backend): `DefaultValue::EnumVariant` for fields whose C# type is `JsonElement` or `JsonElement?` (complex tagged-union enums) now emits `null` instead of `JsonElement.VariantName`, which does not exist in the .NET API.

### Added

- feat(pyo3-backend): generate `from_json(json_str: String) -> PyResult<Self>` staticmethod on all non-opaque struct types with serde and a core→binding `From` conversion. Deserializes via the core type to correctly handle fields with `#[serde(skip)]` (e.g. `Vec<Message>` in `ChatCompletionRequest`). Requires PyO3 ≥ 0.21 (multiple `#[pymethods]` blocks allowed by default).
- feat(e2e/python): add `"from_json"` mode for `options_via`. When set, generates `OptionsType.from_json(json_str)` instead of a dict or kwargs, allowing typed construction from fixture JSON for types that cannot be constructed via kwargs.
- feat(e2e/python): add `from_json_module` field to `CallOverride`. When set alongside `options_via = "from_json"`, the `options_type` is imported from this module (e.g., `liter_llm._internal_bindings`) instead of the main public module, supporting PyO3 native types whose `from_json()` exists on the native class only.
- feat(e2e/python): add `async` field to `CallOverride`. When set, overrides the call-level `async` flag for Python tests only — useful when a binding returns a sync iterator from a call marked async at the call level (e.g., `chat_stream`).
- fix(e2e/python): use `_alef_e2e_text().lower()` for `equals` assertions on enum fields instead of `.strip()`. PyO3 enum values do not have `.strip()` and their string representation may differ in case from the fixture assertion value.

### Fixed

- fix(e2e/python): resolve `options_type` and `options_via` per-fixture from the per-call Python override (`[crates.e2e.calls.X.overrides.python]`), falling back to the file-level global override. Previously only the global override was used, so per-call overrides had no effect.
- fix(csharp-backend): `Optional<String>` return types — FFI returns a raw C string, not JSON-encoded; use `Marshal.PtrToStringUTF8` directly instead of `JsonSerializer.Deserialize`, which failed with raw strings not being valid JSON.
- fix(csharp-backend): `Optional(_)` return types — null pointer means `None` (not found), not an FFI error; generate `return null` instead of `throw GetLastError()` / `throw new ExceptionName(...)` in both top-level methods and opaque type methods.
- fix(java-backend): use `org.jspecify.annotations.Nullable` instead of `org.jetbrains.annotations.Nullable` in generated record types; aligns with the JSpecify annotations used elsewhere in the Java bindings.
- fix(magnus-backend): skip `&mut self` methods when registering methods in `gen_module_init`; Magnus's `method!` macro doesn't support mutable receivers, causing a compile error.
- fix(napi-backend): import `async_trait` when trait bridges are configured; the generated bridge code uses `#[async_trait::async_trait]` but the dependency was missing from the generated Cargo.toml.
- fix(php-backend): allow `unsafe_code` in generated PHP bindings; `ext-php-rs` macros expand to unsafe blocks, causing clippy to reject with `-D warnings`.
- fix(e2e/go): move `jsonString` helper into a dedicated `helpers_test.go` file emitted once per package, eliminating duplicate function definition errors when multiple test files needed it.
- fix(e2e/go): `needs_fmt` calculation now checks `field_resolver.is_valid_for_result(field)` to avoid emitting an unused `fmt` import when an assertion references a field that doesn't exist on the result type.
- fix(e2e/python): when a fixture argument is an array of objects and `element_type` is declared, construct typed instances (`ElementType(key=value, ...)`) instead of raw dict literals; matches the binding's type-safe API.
- fix(scaffold/csharp): add `<Compile Include="../src/**/*.cs" />` to the generated `.csproj` so source files from the shared `src/` directory are included in the project build.
- fix(scaffold/node): add `async-trait = "0.1"` to Node.js Cargo.toml scaffold when trait bridges are configured; the generated bridge code uses `#[async_trait::async_trait]` but the dependency was missing.
- fix(rustler-backend): declare `visitor_owned_env` as `mut` so `send_and_clear` (which takes `&mut self`) compiles without E0596 borrow error.
- fix(php-backend): use `!is_php_prop_scalar_with_enums` instead of `type_ref_has_named` when computing `has_named_params` in struct methods codegen, correctly gating named-param constructors on non-scalar fields.
- fix(pyo3-backend): correct boolean logic in `is_native` computation in `gen_api_py` — options types are now excluded first, then native membership is checked, preventing options types from incorrectly landing in native imports.
- fix(rustler-backend): replace double-nested `OwnedEnv::run` + `send_and_clear` with a single `send_and_clear` call in the trait bridge field function spawn closure, eliminating a double-borrow that caused BEAM message delivery failures.
- fix(e2e/wasm): remove `vite-plugin-top-level-await` from generated WASM e2e `package.json` and vitest config; top-level await is natively supported in modern Vite/Vitest without the plugin.
- fix(napi-backend): force `is_param_optional = true` in bridge functions so options parameters are always treated as `Option<T>` regardless of whether the IR marks them as non-optional.
- fix(php-backend): add `#[serde(default)]` struct attribute when `has_serde` is enabled, so `from_json()` accepts partial JSON and missing fields use `Default` values instead of failing deserialization.
- fix(php-backend): always emit `from_json` for has_default structs when `has_serde` is true, preventing broken `__construct` methods with invalid Rust enum defaults (e.g. `BrowserMode::Auto`) that don't exist in the PHP binding's string-mapped enum model.
- fix(e2e/go): `is_empty` assertions on pointer-to-struct results now generate `if result != nil { ... }` instead of dereferencing and calling `len()` on a struct, which was both invalid and panicked on nil.
- fix(csharp-backend): add `using System.Collections.Generic;` to generated opaque type source files when any method has a `Vec<T>` parameter or return type.
- fix(csharp-backend): pass full set of opaque type names to `gen_opaque_handle` so that methods on one opaque type returning another opaque type (e.g. `LanguageRegistry::get_language` → `Language`) wrap the pointer with `new Language(ptr)` instead of incorrectly calling the non-existent `LanguageToJson` FFI.

## [0.14.19] - 2026-05-04

### Fixed

- fix(scaffold/python): add `async-trait` dependency to Python Cargo.toml scaffold when trait bridges are configured.
- fix(scaffold/php): add `async-trait` dependency to PHP Cargo.toml scaffold when trait bridges are configured.
- fix(e2e/c): derive header name from `ffi.header_name` config and lib name from `ffi.lib_name` when package-level overrides are absent.
- fix(e2e/go): fix array-assertion detection to correctly check `result_is_array` when no field is specified, without falling through to field-level `is_array` check.
- fix(e2e/ruby): call `.to_s` before `.strip` on simple result fields; call `.to_s` before `not_to be_empty` for simple result fields.

## [0.14.18] - 2026-05-04

### Fixed

- fix(scaffold/ffi): add `async-trait` dependency to FFI Cargo.toml scaffold when trait bridges are configured. When the config has trait bridges, the generated FFI `lib.rs` uses `#[async_trait::async_trait]` for trait bridge implementations, but the dependency was missing from the generated Cargo.toml, causing compilation errors.

## [0.14.17] - 2026-05-04

### Fixed

- fix(scaffold/rustler): remove unused `futures-util` dependency from Rustler NIF Cargo.toml scaffold. When `has_async=true`, the dependency was added speculatively but the generated Rustler code never imports it, causing `cargo-machete` to flag it as unused.

## [0.14.16] - 2026-05-04

### Fixed

- fix(scaffold): `alef all --clean` now force-regenerates scaffold files (Cargo.toml templates, gemspec, etc.) matching the behaviour of README and e2e file writes. Previously scaffold files were never overwritten by `--clean`, leaving stale content (e.g. removed `[lints]` section) on disk indefinitely.

- fix(scaffold/php): downgrade `ext-php-rs` template version from `0.15.12` back to `0.15.4`. Version 0.15.12 introduced an `ext-php-rs-clang-sys` fork that conflicts with `clang-sys` when `kreuzberg-php` is a member of a shared Cargo workspace.

- fix(pyo3): coerce enum fields in dict input before constructing dataclass. When a config dict (e.g. `ExtractionConfig`) contains an enum-typed field, the dict→dataclass path now converts the string value to the correct Python enum member before calling `DataClass(**value)`.

- fix(rustler): extend visitor callback return handler to recognise all `CallbackAction` variants. Previously only `is_binary(result)` was checked; `:continue`, `:skip`, `:preserve_html`, and `{:custom, value}` tuples are now handled correctly.

- fix(e2e/csharp): normalise enum field values to lowercase before serialising fixture input JSON. C# `JsonStringEnumConverter` emits lowercase names; passing PascalCase values (e.g. `"Tildes"`) from fixtures caused deserialization mismatches.

- fix(e2e/elixir): include fixtures that use `client_factory` override when computing `has_mock_server_tests` in the test module header. Previously only fixtures that called `needs_mock_server()` were counted, causing the mock server setup to be omitted for client-factory fixtures.

- fix(e2e/python): include fixtures that use a `client_factory` Python override when computing `has_http_fixtures` in `conftest.py`. Same root cause as the Elixir fix above.

## [0.14.15] - 2026-05-04

### Fixed

- fix(e2e/go-codegen): exclude mock-only fixtures from `needs_pkg` import check. Mock fixtures with no real function calls were incorrectly triggering conditional imports.

- fix(e2e/ruby-scaffold): remove `[lints] workspace = true` from excluded-workspace Magnus Cargo.toml. The lints section is inherited from workspace config, but packages/ruby/ext/kreuzberg_rb/ is excluded from the workspace, so `workspace = true` failed with "cannot find workspace root". Removed the lints section from the generated Cargo.toml template.

- fix(e2e/php-codegen): strip namespace prefix from class names in use statements. When class override contains a namespace (e.g. `Kreuzberg\Kreuzberg`), extract just the class name to avoid triple-nested namespaces like `use Kreuzberg\Kreuzberg\Kreuzberg;`. Now correctly emits `use Kreuzberg\Kreuzberg;`.

## [0.14.14] - 2026-05-04

### Fixed

- fix(java-backend): `Duration` fields map to boxed `Long` in Java; compact constructor defaults emitted as bare integer literals (e.g. `30000`) failed to compile because `int` does not auto-box to `Long`. Now appends `L` suffix so the literal is a `long` that Java auto-boxes correctly.

- fix(magnus-backend): emit a `_refs: Vec<&str>` intermediate binding when a `Vec<String>` parameter is passed by reference to a core function expecting `&[&str]`. Previously the generated code passed `&Vec<String>` directly, which mismatches the `&[&str]` signature and fails to compile.

- fix(rustler-backend): same `Vec<String>` → `&[&str]` refs intermediate for Rustler/Elixir bindings (mirrors the Magnus fix above).

- fix(napi-backend): use `.into()` (`FnArgs`) when calling visitor JS callbacks with more than one argument. Single-argument calls are unaffected; multi-argument calls previously passed a raw tuple that NAPI-RS did not unpack correctly to JavaScript.

- fix(cache): include alef binary identity (mtime + size) in `compute_lang_hash` and `compute_stage_hash` so locally rebuilt binaries always invalidate stale caches without requiring a version bump. Previously a rebuild with code changes would reuse the old cached output.

- fix(e2e-elixir): rewrite `alef_e2e_item_texts` helper to use `Enum.flat_map` with a `case` expression instead of `Enum.map(fn attr -> item |> Map.get(attr) |> to_string() end)`. `to_string/1` raises `ArgumentError` on atom values (e.g. enum variants); the new form guards `is_atom` and capitalises separately.

- fix(e2e-go): use `len(slice)` instead of `len(*slice)` for optional slice-typed fields in `not_empty`, `empty`, `min_elements`, and `max_elements` assertions. Go slice optionals are value types, not pointer-to-slice, so dereferencing them was a compile error.

- fix(e2e-typescript): convert `{placeholder}` syntax to `${placeholder}` in `CustomTemplate` visitor method bodies so the emitted string is a valid JavaScript template literal.

- fix(e2e-go): include fmt import when `needs_json_stringify` is true, since the `jsonString` helper uses `fmt.Sprint`. Previously tests with array field `contains` assertions failed to compile with "undefined: fmt".

- fix(e2e-csharp): add missing `using static {namespace}.{class_name};` directive to test file headers so static method calls to the binding class resolve without full qualification.

- fix(e2e-java): add `org.jetbrains:annotations:24.1.0` dependency to generated pom.xml. E2e tests use `@NotNull`/`@Nullable` annotations from this package.

- fix(e2e-elixir): skip batch functions in generated tests with `@tag :skip`. Batch functions (`batch_extract_*`) are excluded from the Elixir binding due to unsafe NIF tuple marshalling; tests now emit a documented skip rather than failing to compile.

## [0.14.13] - 2026-05-04

### Fixed

- fix(e2e-go): propagate call-level `result_is_array` to Go generator. Previously only Go-specific overrides were checked, so call-level `result_is_array = true` was ignored, causing `value := *result` dereference errors for slice return types like `[]string`.

## [0.14.12] - 2026-05-04

### Fixed

- fix(pyo3): use `val.kind.into()` instead of `Default::default()` for data enum fields in `From` impls. Data enums (like `StructureKind`) were added to `opaque_names_set` which caused the conversion generator to treat their fields as opaque and emit `Default::default()`. A separate `conversion_opaque_set` now excludes data enum names so their fields convert correctly with `.into()`.

- fix(e2e-ruby): use per-item text check for array field `contains` assertions. Previously `result.structure.to_s` produced an object-repr string that never matched the expected value. Now generates `array.any? { |item| alef_e2e_item_texts(item).any? { |t| t.include?(val) } }` using a shared helper method.

- fix(e2e-elixir): use `Enum.any?` for array field `contains` assertions instead of `to_string` on list. `to_string/1` on an Elixir list raises `ArgumentError`; now generates a per-item traversal using a shared `alef_e2e_item_texts/1` helper.

- fix(e2e-go): skip nil check and pointer dereference for `result_is_array` slice results. Go slice types (`[]string`) are not pointers, so the generated `if result == nil` check and `value := *result` dereference were compile errors. Array results now use `value := result` directly.

- fix(magnus-backend): add `options_field` visitor bridge support. The Magnus Ruby backend now generates proper wrapper functions for trait bridges using `bind_via = "options_field"` binding style. Previously only `bind_via = "function_param"` was supported, causing e2e tests to fail when trying to pass a visitor as a secondary argument to functions like `convert(html, visitor)`.

## [0.14.11] - 2026-05-03

### Fixed

- fix(e2e-python): fix `_to_rust_*()` converter field access when `python_output = "typed-dict"`. Previously the converter used `value.get("field")` dict-style access even for dataclass input types, causing `AttributeError` when called with `ProcessConfig` instances. Converters now use `value.field` attribute access (matching the input style) rather than the output style.

- fix(e2e-go): propagate `call_config.result_is_simple` to Go e2e generator. The generator was only checking per-language overrides, so call-level `result_is_simple = true` was ignored, causing `*string` returns to be treated as `error` and printing pointer addresses as failure messages.

- fix(e2e-php): add `options_type` support for PHP. When a call override sets `options_type = "ClassName"`, PHP e2e tests now generate `ClassName::from_json(json_encode([...]))` to construct typed config objects. Removes hardcoded `\HtmlToMarkdown\ConversionOptions::builder()` placeholder.

- fix(e2e-elixir): fix 0-arity functions (e.g. `language_count`, `available_languages`) called with `%{}` or `nil` when `args = []` and fixture input is empty. Now correctly emits no-arg calls.

- fix(e2e-ruby): same empty-args fix for Ruby — 0-arity functions no longer receive `nil` or `{}` from empty fixtures.

- fix(csharp-backend): store delegate objects in field array to prevent GC collection. The trait bridge generated code was creating 41+ `UnmanagedFunctionPointer` delegates as local variables in `BuildVtable()`, then immediately discarding them. The GC could collect these delegates, leaving dangling function pointers in the vtable and causing `AccessViolationException` on native callbacks. Now delegates are stored in `private readonly object[] _delegates` initialized in the constructor and kept alive for the bridge's lifetime.

## [0.14.10] - 2026-05-03

### Added

- feat(e2e-go): `client_factory` support in Go e2e codegen. When `[e2e.call.overrides.go] client_factory = "CreateClient"` is set, the generated test creates a client via `pkg.CreateClient("test-key", baseURL)` and calls API methods as `client.Method(args)` instead of `pkg.Function(args)`. Also fixes `fixture_has_go_callable` to return `true` when `client_factory` is configured.

- feat(e2e-python): `client_factory` support in Python e2e codegen. When `[e2e.call.overrides.python] client_factory = "create_client"` is set, the generated test imports only the factory function, creates a client, and dispatches calls as `client.method(args)`.

- feat(e2e-csharp): `client_factory` support in C# e2e codegen. When `[e2e.call.overrides.csharp] client_factory = "createClient"` is set, the generated test calls `ClassName.CreateClient("test-key", baseUrl)` and dispatches as `client.Method(args)`.

- feat(e2e-ruby): `client_factory` support in Ruby e2e codegen. When `[e2e.call.overrides.ruby] client_factory = "create_client"` is set, the generated spec creates a client via `ModuleName.create_client('test-key', ENV.fetch('MOCK_SERVER_URL'))` and calls methods on it.

- feat(e2e-elixir): `client_factory` support in Elixir e2e codegen. When `[e2e.call.overrides.elixir] client_factory = "create_client"` is set, the generated test calls `{:ok, client} = ModuleName.create_client("test-key", base_url)` and passes `client` as the first argument to API functions.

### Fixed

- fix(e2e): include fixture id in mock server base URL for C#/Ruby/Elixir e2e backends. Previously `client_factory` generated code pointed clients at bare `MOCK_SERVER_URL`; now appends `/fixtures/{fixture_id}` so API calls route correctly through the mock server's prefix-based routing.

- fix(e2e-php): parametrize extension name in generated `run_tests.php`; previously hardcoded to `libhtml_to_markdown_php`, now derived from the crate's extension name so generated test runners work across projects.

- fix(e2e-wasm): inject `initSync` initialization block in generated test files for Node.js environments. Uses `fs.readFileSync` + `initSync` to load the WASM binary from the local `node_modules` path, replacing the broken async fetch-based approach that fails in vitest's Node.js runner.

- fix(e2e): remove language suffix from generated e2e package names. TypeScript packages now use `{pkg}-e2e` instead of `{pkg}-e2e-typescript`; Python packages use `{pkg}-e2e` instead of `{pkg}-e2e-tests`. Python `pkg_name` now defaults to the call module name (replacing a stale hard-coded project name).

- fix(e2e-wasm): remove `initWasm()` call from generated test files. The bundler target auto-initializes when imported, so explicit initialization is unnecessary and causes `TypeError: initWasm is not a function`.

- fix(backend-java): use `LINKER.defaultLookup()` for Panama FFM symbol resolution. The `SymbolLookup.loaderLookup()` API returns symbols from Java's class loader, not the native linker; `LINKER.defaultLookup()` correctly returns symbols from all loaded native libraries.

## [0.14.9] - 2026-05-03

### Fixed

- fix(backend-pyo3): `_coerce_enum` helper now returns `_E` (not `_E | None`) by separating
  the `isinstance` early-return from the `None` case (which now raises `ValueError`). This
  eliminates the `[return-value]` mypy error and removes now-unused `# type: ignore` comments
  that newer mypy versions flag as `[unused-ignore]`.

- fix(backend-napi): `find_options_field_binding` now unwraps Optional types when matching
  options parameters against `options_type` in trait bridge configs. When a function parameter
  is `Option<ConversionOptions>` and a trait bridge specifies `bind_via = "options_field"`,
  the NAPI backend now correctly recognizes the parameter and generates the visitor bridge code.
  Fixes 45 failing Node.js visitor e2e tests where `options.visitor` was being discarded by
  the generated From impl.

- fix(e2e-php): emit `ConversionOptions::from_json()` instead of direct property assignment,
  since ext-php-rs doesn't support writable #[php(prop)] fields; fixes "No setter available for
  this property" errors in 7 e2e tests with preprocessing/metadata options

- fix(backend-go): initialize string-alias enum zero values as `EnumType("")` instead of `EnumType{}` (composite literal syntax only applies to structs/slices/maps, not string aliases)

- fix(backend-go): option setter functions for slice and map fields now assign `v` directly instead of `&v`, matching the `[]T` field types introduced in v0.14.5

- fix(backend-pyo3): fix `_coerce_enum` return type from `_E` to `_E | None` to allow None values
  in optional enum fields; remove unnecessary type: ignore comments that mypy flags as unused

- fix(backend-java): use short annotation names (`@JsonDeserialize`, `@JsonPOJOBuilder`) instead of
  fully-qualified names in generated record and builder classes so that the accompanying import
  statements are actually used and checkstyle's UnusedImports rule passes.

- fix(codegen): `gen_lossy_binding_to_core_fields` and `gen_lossy_binding_to_core_fields_mut` now
  accept `cast_uints_to_i32` and `cast_large_ints_to_f64` flags. All callers updated (magnus,
  integration tests). Extendr backend passes `true` for both to correctly cast R's i32/f64 types
  back to core Rust int types in generated method bodies.

- fix(e2e-php): do not append `_async` suffix to function name when a language-specific override
  explicitly provides the function name. Previously, async PHP functions with overridden names
  were incorrectly suffixed with `_async`.

## [0.14.8] - 2026-05-03

### Fixed

- fix(backend-ffi): `type_ref_to_rust_type` now prefixes named types with `kreuzberg::` so
  `Vec<BatchFileItem>` turbofish annotations in FFI bodies are emitted as
  `Vec<kreuzberg::BatchFileItem>`. The v0.14.6 fix added turbofish annotations without the
  crate prefix, causing "cannot find type X in this scope" E0425 errors in the FFI crate for
  all `Vec<T>`-parameter functions whose element type is a core library struct.

## [0.14.7] - 2026-05-03

### Fixed

- fix(backend-extendr): add `skip_impl_constructor` flag to `RustBindingConfig` and set it
  `true` for the extendr backend. The in-class `impl T { fn new(...) }` constructor is now
  suppressed; callers must use the kwargs-style free-function constructor (already generated
  by `gen_extendr_kwargs_constructor`). This prevents "TryFrom<&Robj> not satisfied" compile
  errors when struct parameters cannot be auto-converted by extendr.
- fix(backend-extendr): methods whose parameters take owned non-opaque structs by value, or
  whose return type is an enum, are now excluded from the `#[extendr]` impl block. Extendr
  generates `TryFrom<&Robj>` for references (`&T`) but not for owned values (`T`), and has
  no `ToVectorValue` impl for enum types; including such methods caused E0277 and E0277
  compile errors in the R binding.
- fix(backend-extendr): methods whose parameters take `Vec<T>` where T is a non-opaque,
  non-enum struct are also excluded from the `#[extendr]` impl block. There is no automatic
  R-list→Vec<ExternalPtr<T>> conversion in extendr.
- fix(codegen/extendr-kwargs): struct-typed fields (non-opaque, non-enum named types) are
  now omitted from the kwargs constructor parameter list and body. Extendr generates
  `TryFrom<&Robj>` only for `&T` (reference), not for owned `T`; including them as kwargs
  parameters caused "T: TryFrom<&Robj> not satisfied" compile errors.
- fix(codegen/extendr-kwargs): fields whose type is already `Option<T>` are no longer
  double-wrapped — the parameter type is now the same as the field type rather than
  `Option<Option<T>>`.
- fix(codegen/core-to-binding): `Vec<u8/u16/u32/i8/i16>` fields with `cast_uints_to_i32`
  are now converted element-wise (`v.iter().map(|&x| x as i32).collect()`) so R receives
  integer vectors instead of raw byte arrays.
- fix(backend-napi/pyo3/php): set `skip_impl_constructor: false` explicitly in those
  backends' `RustBindingConfig` initializers (no behaviour change; required after the new
  field was added to the struct).

## [0.14.6] - 2026-05-03

### Fixed

- fix(backend-ffi): `Vec<T>` parameters now always emit a concrete turbofish type annotation
  (`serde_json::from_str::<Vec<String>>(...)`) in generated FFI functions. Previously the
  annotation was only emitted when `is_ref || is_mut`; without it, calls to Rust core functions
  with generic bounds (e.g. `fn f<T: AsRef<str>>(v: Vec<T>)`) failed to compile with E0283
  "type annotations needed".

## [0.14.5] - 2026-05-03

### Fixed

- fix(backend-extendr): add `cast_uints_to_i32` and `cast_large_ints_to_f64` flags to
  `ConversionConfig` so the R binding maps u8/u16/u32/i8/i16 → `i32` and u64/usize/isize → `f64`
  in both binding→core and core→binding From impls. Without these casts, primitive type
  mismatches caused ~100 E0308 errors in the generated R binding.
- fix(backend-extendr): generate `#[extendr]` on impl blocks (previously `method_block_attr: None`)
  so generated structs register as R classes and satisfy the `ToVectorValue` trait bound.
- fix(backend-extendr): import `std::collections::HashMap` and `extendr_api::Result` in the
  generated binding so HashMap fields compile and `Result<T>` is unambiguously the extendr alias.
- fix(codegen/extendr-kwargs): optional struct fields (where `field.optional=true`) in the
  kwargs constructor now assign `Some(v)` instead of `v` so `Option<T>` fields are set correctly.
- fix(codegen/extendr-kwargs): enum fields in kwargs constructors now accept `Option<String>` and
  parse via serde_json instead of `Option<EnumType>`, which extendr cannot convert from Robj.
- fix(backend-extendr): skip opaque types that use `Rc` internally (identified by having a cfg
  gate) from struct generation and from field inclusion in non-opaque structs. `VisitorHandle` is
  `Rc<RefCell<dyn HtmlVisitor>>` and cannot be wrapped in `Arc` — its binding struct would
  violate `Send + Sync` bounds. Affected fields (e.g. `visitor` on `ConversionOptions`) are
  excluded from struct generation and conversion impls.
- fix(trait_bridge/extendr): use `List::from_pairs(attrs_pairs).into()` instead of the invalid
  `collect::<List>()` pattern for building named R lists from attribute key-value pairs.

- fix(backend-napi): NAPI options_field_bridge now preserves visitor handle across
  options conversion. The From<JsConversionOptions> impl was unconditionally setting
  visitor to Default::default() due to opaque field handling. Fixed by extracting
  the visitor before conversion and re-assigning it after the Into call.
- fix(backend-pyo3): PyO3 convert wrapper now reads visitor from options.visitor
  when the separate `visitor=` kwarg is None. This allows Python callers to pass
  the visitor via ConversionOptions without requiring a separate keyword argument.
- fix(e2e/wasm): replace static package imports with dynamic imports + top-level
  await to defer WASM module loading until after vite plugins have processed it.
  Fixes "failed to initialize WebAssembly bundle" errors in vitest e2e tests.
- fix(e2e/php): when `result_is_simple = true`, generated assertions now access the
  `content` property of the ConversionResult object instead of passing the object
  directly to assertion methods. Fixes TypeError when trimming or comparing results.
- fix(backend-pyo3): plain data structs now emit `#[pyclass(frozen)]` instead of
  `#[pyclass(unsendable)]`. The previous code used the transitive-closure set
  (`opaque_names_set`) to decide between frozen and unsendable, causing any struct
  whose fields transitively referenced an opaque type to be marked unsendable. Data
  structs are `Send + Sync` and crossing thread boundaries in async Python code must
  not panic with "<TypeName> is unsendable". Only truly-opaque Arc/Rc-wrapped handles
  need the unsendable annotation.
- fix(backend-go): `Vec<T>` function return types now emit `[]T` (a value slice) instead
  of `*[]T` (a pointer to a slice). Pointer-to-slice is unidiomatic in Go, breaks
  `len(result)` calls, and is unnecessary since Go slices are already reference types.
- fix(e2e/csharp): generate `List<string>` parameter type for VisitTableRow method
  (was `IReadOnlyList<string>`), emit `new VisitResult.XXX()` instead of
  `VisitResult.XXX()` to correctly instantiate sealed record types, and call
  `ConvertWithVisitor` instead of `Convert` when a visitor is present in fixtures.

## [0.14.4] - 2026-05-03

### Fixed

- fix(backend-pyo3): Required parameters whose Rust type is a has-default struct
  (e.g. `ExtractionConfig`) are now treated as optional at the Python wrapper layer.
  Callers may pass `None` and the wrapper substitutes a Rust default-constructed
  instance. Previously the PyO3 binding panicked with `.expect("'param' is required")`.
- feat(backend-pyo3): emit pass-through wrappers for trait-bridge `unregister_*` and
  `clear_*` functions in `api.py` so callers can use the public package path.
- fix(backend-go): Rust newtype-tuple enums (e.g.
  `enum OutputFormat { Plain, Markdown, Custom(String) }`) are now rendered as Go
  string types with const variants and a Custom fallthrough, instead of empty
  structs. Fixes JSON unmarshaling of config fields like `output_format: "markdown"`.
- fix(e2e/go): callable detection no longer skips fixtures with reason "non-HTTP
  fixture: Go binding does not expose a callable for the configured `[e2e.call]`
  function" — Go does expose the canonical 27 fns. Real test bodies are now
  emitted from the e2e generator, mirroring the Rust backend pattern.
- fix(backend-ffi): emit `visitor_create`/`free`/`convert_with_visitor` when both
  `options_field` and `visitor_callbacks` are configured.
- fix(wasm-visitor): use marker-relative search for visitor replacement; use
  bounded `replace` within impl blocks to avoid cross-impl pollution.
- feat(backend-napi): add `gen_options_field_bridge_function` for visitor
  embedding in options.

### Fixed (carry-forward from unreleased v0.14.3 follow-ups)

- fix(e2e/elixir): inject visitor into the options map argument instead of passing it as a
  separate positional argument, so the Elixir facade's `convert/2` can properly extract it
  via `Map.pop(options, :visitor)` and dispatch to the NIF with visitor callbacks.
- fix(e2e/elixir): read `returns_result` from the language override when available instead of
  always using the base CallConfig value, enabling `returns_result = true` in Elixir overrides
  to generate proper `{:ok, result} =` pattern matches in assertions.
- fix(backend-napi): when `bind_via = "options_field"` is configured for a trait bridge,
  extract the visitor from the options struct before conversion, create the bridge, and
  manually inject it into the converted core options. This ensures visitor callbacks
  are preserved across the NAPI FFI boundary instead of being dropped during the
  JsConversionOptions -> ConversionOptions Into conversion.
- fix(backend-pyo3): emit a `_coerce_enum(enum_cls, value)` helper in the generated
  api.py wrapper and use it in `_to_rust_*` converters instead of attempting
  `_rust.<Enum>(value)`. PyO3 unit-enum pyclasses do not expose a string-accepting
  `__new__`, so the previous codegen raised `TypeError: cannot create '<Enum>'
instances` when callers passed string aliases like `'atx'`. The helper coerces
  strings, snake_case, UPPER_CASE, and CamelCase to the canonical class attribute.
- fix(backend-java): stop emitting illegal `Optional<String>` accessor methods that
  shadow the canonical record component accessor. Java records auto-generate the
  accessor with the field's declared type and disallow overrides with a different
  signature. Callers that want `Optional` can wrap with `Optional.ofNullable(...)`.
- fix(e2e/java): wrap nullable record-component accessors with `Optional.ofNullable`
  before calling `.orElse(...)`, so the existing assertion templates work whether the
  underlying type is `Optional<X>` or `@Nullable X`.
- fix(cli/build): resolve mvn working directory by walking up from `[crates.output].java`
  to the nearest `pom.xml`, so configurations that point output at `src/main/java/`
  still build via maven from the project root.
- fix(cli/build): when the cargo `[crates.output].<lang>` directory is itself inside a
  standalone crate that declares `[workspace]` (extendr's `packages/r/src/rust/`), `cd`
  into that crate and run `cargo build` instead of treating it as a workspace member.
- fix(cli): keep the test module after all items so clippy's `items_after_test_module` check
  passes in the full pre-commit suite.
- fix(e2e/typescript): simplify generated `is_empty` assertion emission so clippy remains
  warning-clean under `-D warnings`.
- fix(backend-pyo3): import native unit enums referenced only through data-enum aliases in generated
  `options.py`, keeping runtime aliases such as `ToolChoice = ToolChoiceMode | str | SpecificToolChoice`
  import-safe and ruff-clean.
- fix(e2e/go): derive generated Go e2e imports and local `replace` paths from `[go]` package
  configuration when no explicit `[e2e.packages.go]` entry is present, import `testify/assert`
  for every assertion kind that emits `assert.*`, and only emit direct Go calls when an explicit
  Go callable override exists.
- fix(cli/test): resolve e2e-only test languages in `alef test --e2e` without leaking the
  test-only resolver into scaffold generation.
- fix(e2e/elixir): emit fixture IDs as generated ExUnit test labels so long fixture
  descriptions cannot exceed Elixir's test name limit.
- fix(scaffold/ruby): omit workspace lints from standalone Ruby native extension crates.
- fix(e2e/csharp): default generated project references to Alef's nested C# package
  project path.
- fix(e2e/csharp): read namespace from `[crates.csharp].namespace` in alef.toml when
  generating test imports, fixing mismatch with generated package namespace.
- fix(e2e/go): render string containment assertions through `fmt.Sprint` so structured
  slices such as `[]StructureItem` compile.
- fix(backend-csharp): emit free P/Invoke declarations for all true opaque handle
  wrappers, including handles that only appear in generated wrapper classes.
- fix(backend-magnus): borrow field types when detecting thread-unsafe visitor
  handles so the backend compiles cleanly.
- fix(e2e/wasm): declare Rollup in generated WASM Vitest harnesses so Vite plugins
  can load in isolated installs.
- fix(e2e/go): compute imports from executable Go fixtures only so skip-only files
  compile without unused imports.
- fix(backend-go): decode Rust data-enum JSON into Go discriminated structs with
  a printable variant name instead of silently returning nil conversion results.
- fix(e2e/go): stringify structured array fields as JSON for containment assertions
  so pointer fields compare by value instead of address.
- fix(backend-go): marshal decoded data-enum variants back to their variant names so
  generated JSON-based e2e assertions can inspect them.

## [0.14.3] - 2026-05-03

### Added

- feat(codegen/doc-emission): shared `parse_rustdoc_sections`,
  `parse_arguments_bullets`, and `replace_fence_lang` helpers in
  `alef-codegen::doc_emission`. Rustdoc Markdown H1 headings (`# Arguments`,
  `# Returns`, `# Errors`, `# Panics`, `# Safety`, `# Example`/`# Examples`)
  are parsed into a typed `RustdocSections` struct that every host renderer
  consumes. ` ```rust ` fences inside `# Example` bodies are rewritten to
  host-native language tags (`typescript`, `php`, `csharp`, …); pound signs
  inside non-Rust fences (e.g. shell `# install …`) are preserved.
- feat(codegen/doc-emission): per-host section renderers
  `render_jsdoc_sections`, `render_javadoc_sections`,
  `render_csharp_xml_sections`, `render_phpdoc_sections`, and
  `render_doxygen_sections`. JSDoc emits `@param` / `@returns` / `@throws` /
  `@example` with ` ```typescript ` fences. JavaDoc emits `@param` / `@return`
  / `@throws KreuzbergRsException`. C# XML doc emits `<param>` / `<returns>`
  / `<exception cref="…">` / `<example><code language="csharp">`. PHPDoc emits
  `@param` / `@return` / `@throws` / ` ```php ` example fences. Doxygen emits
  `\param` / `\return` / `\code` / `\endcode`.
- feat(codegen/trait-bridge): shared `host_function_path` helper in
  `alef-codegen::generators::trait_bridge` replaces the duplicated per-backend
  `<lang>_host_function_path` inline helpers. pyo3, napi, magnus, ext-php-rs,
  rustler, extendr, and wasm now consume the same resolution logic.
- feat(backend-magnus/unregister-clear): emit `pub fn unregister_<name>(name:
String) -> Result<(), magnus::Error>` and `pub fn clear_<plural>() ->
Result<(), magnus::Error>` when `bridge_config.unregister_fn` / `clear_fn`
  are set. Errors are wrapped through `Ruby::exception_runtime_error`.
- feat(backend-php/unregister-clear): emit `#[php_function] pub fn
unregister_<name>(name: String) -> ext_php_rs::prelude::PhpResult<()>` and
  the matching `clear_<plural>()` function. Errors are mapped through
  `PhpException::default(format!("{}", e))`.
- feat(backend-rustler/unregister-clear): emit `#[rustler::nif] pub fn
unregister_<name>(env: rustler::Env<'_>, name: String) -> rustler::Atom`
  and `clear_<plural>()` returning `:ok` / `:error` atoms.
- feat(backend-wasm/unregister-clear): emit `#[wasm_bindgen] pub fn
unregister_<name>(name: String) -> Result<(), JsValue>` and matching
  `clear_<plural>()` wrappers; surfaced via `gen_bindings/mod.rs` so the
  generated bridge re-exports them in `index.ts`.
- feat(backend-extendr/unregister-clear): emit `#[extendr] pub fn
unregister_<name>(name: String)` / `clear_<plural>()` for R bindings.
- feat(backend-pyo3/host_function_path): pyo3 register/unregister/clear
  bridges now share the centralised host-function path resolver, eliminating
  the duplicated inline helper and matching the resolution rules used by all
  other backends.
- feat(backend-napi/host_function_path): napi register/unregister/clear
  bridges now share the centralised resolver.

### Fixed

- fix(backend-napi): preserve Rust `Default` values when converting omitted
  fields on defaultable JavaScript config objects instead of falling back to
  per-field zero values.
- fix(backends): remove dead PyO3 and Magnus backend code paths so workspace
  builds are warning-clean.
- fix(e2e): make TypeScript assertions handle `null` empty results and string
  containment checks against arrays of structured objects.
- fix(backend-napi/errors): JSDoc rendering for `KreuzbergError` variants now
  goes through the shared per-host renderer instead of leaking raw rustdoc
  Markdown headings into emitted error class JSDoc blocks.

## [0.14.2] - 2026-05-02

### Added

- feat(extract/normalize-rustdoc): doc comments parsed from Rust source are now run through
  a `normalize_rustdoc` pass before any backend sees them. Two specific rustdoc artefacts
  that previously leaked into every binding are removed:
  - Rustdoc-hidden lines inside `rust /`rust,no_run fences (`# tokio_test::block_on(async {`,
    `# Ok::<(), Error>(())`, `# }`, `# async fn example()`) are dropped.
  - Intra-doc-link syntax `[`crate::Foo`]` / `[`super::Bar`] /`[`self::X`]` is rewritten to
  plain `` `Foo` `` / `` `Bar` `` / `` `X` ``.
Per-host renderers (Javadoc, KDoc, PHPDoc, JSDoc, Dartdoc, Swift-DocC, roxygen2, Doxygen)
continue to translate `# Errors`/`# Returns`/etc. as before — full per-host code-fence
  translation deferred to v0.14.3.

- feat(error-gen/strip-thiserror-placeholders): added `strip_thiserror_placeholders` and
  `acronym_aware_snake_phrase` helpers in `alef-codegen::error_gen`. Display strings emitted
  by binding error renderers (Go sentinels, Dart `String get message`) no longer contain raw
  `{name}` substitution markers (`OCR error: {message}` becomes `OCR error`, `extraction
cancelled` is preserved). Variant names with technical acronyms (IO, OCR, PDF, URL, HTTP, …)
  render as `IO error` / `OCR error`, not `iO error` / `oCR error`.

- feat(backend-dart/error-render): Dart sealed-class exceptions now use the new placeholder
  stripping so `String get message => 'Parsing error: {message}'` is emitted as
  `'Parsing error'`. Surviving prose still has `'`, `\`, and `$` escaped per Dart string
  literal rules.

- feat(backend-zig/trait-bridge): the `make_<trait>_vtable` doc comment was missing a
  `format!()` call, leaking the literal `{snake}` template string. Substitution now
  happens correctly so generated docs read `make_post_processor_vtable(MyType, …)` etc.

- feat(trait-bridge/unregister-clear): `[[trait_bridges]]` schema gains optional
  `unregister_fn` and `clear_fn` fields. When set, alef emits matching host-language
  wrappers alongside `register_fn`. Pyo3 and napi backends opt in this release; remaining
  backends (magnus, php, rustler, gleam, extendr, dart, swift, kotlin, csharp, java,
  wasm) fall through to the default `None` (no emission) — those will be added in v0.14.3.
  The host-language wrappers call the user's plugin module function (e.g.
  `kreuzberg::plugins::ocr_backend::unregister_ocr_backend(&name)`); the path is derived
  from the bridge's `registry_getter` (`get_*_registry` → `*::*_fn_name`).

### Fixed

- fix(error-gen/go-sentinel): Go sentinel error strings no longer contain literal
  `{message}` / `{plugin_name}` / `{elapsed_ms}` placeholders. Variants like `IoError`,
  `OcrError`, `PdfError` render as `errors.New("IO error")`, `errors.New("OCR error")`,
  `errors.New("PDF error")` instead of the previous `errors.New("iO error")`,
  `errors.New("oCR error: {message}")`.

### Deferred to v0.14.3

- Per-host doc-comment section translation (`# Arguments` → `@param`, `# Returns` →
  `@returns`, `# Errors` → `@throws`, etc.) for JSDoc, JavaDoc, C# XML, PHPDoc, dartdoc,
  roxygen2, KDoc, Swift-DocC, Doxygen, Elixir doctests. Foundation in
  `alef-codegen::doc_emission` is unchanged.
- `gen_unregistration_fn` / `gen_clear_fn` for magnus, php, rustler, gleam, extendr,
  dart, swift, kotlin, csharp, java, wasm. Schema is in place; backends opt in incrementally.

### Fixed (carry-forward from unreleased v0.14.1 follow-ups)

- fix(cli): fix alef-verify ↔ host-formatter circular drift in `generate` and `all` commands.
  `format_generated` was called with only the bindings slice, so languages where only stubs
  changed (cache-hit bindings) were never formatted before `finalize_hashes` ran. Host
  formatters (ruff, mix format, php-cs-fixer, gofmt) would then reformat the unformatted
  stubs, making the embedded `alef:hash:` line stale and causing `alef verify` to report
  drift on every run. Fix: pass `bindings + stubs` combined to `format_generated` in both
  `Commands::Generate` and `Commands::All`, and track stub-changed languages in
  `changed_languages` in `Commands::All`.

- fix(e2e): add `node`/`wasm` default formatters (`npx oxfmt {dir}`) to `alef_e2e::format`.
  `run_formatters` had no entry for TypeScript e2e output, so hashes were embedded over raw
  codegen bytes; prek's oxfmt hook then reformatted those files, making `alef verify` report
  28 stale TS files after every `alef e2e generate` / `alef e2e generate --registry` run.

- fix(cli,e2e): add `cargo sort` to Ffi/Wasm/rust-e2e formatter pipelines before hash
  finalisation. `cargo-sort` normalises `Cargo.toml` dependency-table ordering and feature
  indentation; without it, prek's `cargo-sort` hook reformatted those files after
  `finalize_hashes` ran, causing `alef verify` to report `crates/*-wasm/Cargo.toml`,
  `e2e/rust/Cargo.toml`, and `test_apps/rust/Cargo.toml` as stale on every run.
  - `Language::Ffi` formatter now runs `cargo fmt --all` then `cargo sort -w` (workspace).
  - `Language::Wasm` formatter now runs `cargo fmt --manifest-path` then `cargo sort <crate>`.
  - E2e rust default formatter now runs `cargo fmt --all && cargo sort .` inside the crate dir.

- fix(php): respect namespace config in php_autoload_namespace. After v0.14.0, the namespace
  emission logic regressed and ignored the `[crates.php] namespace` configuration, instead
  splitting the extension name on underscores. Now respects explicit namespace override.

- fix(backend-wasm): bridge type aliases (e.g. `VisitorHandle`) are now treated as opaque in
  WASM binding-to-core `From` conversions. Fields of these types now emit `Default::default()`
  instead of `val.visitor.map(Into::into)` (which failed E0277 because `WasmVisitorHandle`
  has no `From` impl to the core `Rc<RefCell<dyn HtmlVisitor>>`). Builder methods on
  has-default structs with bridge parameters also use `.visitor(None)` instead of
  `.visitor(visitor.as_ref().map(|v| &v.inner))` to avoid E0308 type mismatch.

- fix(backend-php): bridge type aliases (e.g. `VisitorHandle`) are now included in
  `opaque_type_names` so that `gen_php_struct` emits `#[serde(skip)]` for fields of those
  types. Without this, structs like `ConversionOptions` that derive `serde::Serialize` +
  `serde::Deserialize` failed E0277 because `VisitorHandle` (an `Arc<Rc<RefCell<dyn
HtmlVisitor>>>` wrapper) does not implement those traits. Builder methods with bridge
  parameters also use `.visitor(None)` to avoid E0308 type mismatch.

- fix(backend-pyo3): fixed three remaining codegen bugs for PyO3 bindings: (1) `PyVisitorRef::extract` now uses `Borrowed::to_owned()` instead of non-existent `Borrowed::clone()` to avoid E0507 "cannot move out of dereference"; (2) builder methods on has_default structs no longer attempt to access `.inner` for bridge type parameters—visitor parameter skips the core builder's `.visitor()` call and uses `None` instead to avoid E0308 type mismatch; (3) wrapper functions that pass has_default parameters to core functions now wrap the deserialized value in `Some()` when core expects `Option<T>` to fix E0308 "expected Option, found T".

- fix(backend-pyo3): `PyVisitorRef` now uses PyO3 0.28 API correctly. Updated `FromPyObject<'a, 'py>` to accept two lifetimes and use `extract(Borrowed<'a, 'py, PyAny>)` instead of the deprecated `extract_bound()`. Fixed `IntoPyObject` error type to use `Infallible` (conversion cannot fail). Resolves E0407, E0107, E0277, E0308 errors when building against PyO3 0.28.3.

- fix(backend-pyo3): trait types now map to `PyVisitorRef` (a custom wrapper) instead of `Arc<Py<PyAny>>`.
  `Arc<Py<PyAny>>` broke PyO3's `IntoPyObject`/`FromPyObject` field traits (E0277). `PyVisitorRef`
  wraps `Py<PyAny>` in `Arc<>` for cheap cloning while maintaining PyO3 compatibility at struct
  field and parameter boundaries. The wrapper implements Clone via Arc, FromPyObject/IntoPyObject
  for binding integration, and exposes `.inner: Arc<Py<PyAny>>` for trait bridge code generation.

- fix(backend-go): `NodeContext.NodeType` field type is now emitted as `NodeType` (the package-defined
  type alias for `string`) instead of the raw `string` type. The redundant `type NodeType = string`
  declaration in `visitor.go` that caused a redeclaration compile error with `binding.go`'s
  `type NodeType string` has been removed.

- fix(backend-rustler): `gen_native_ex` now emits a `convert_with_visitor/3` NIF stub when
  a `bind_via = "options_field"` bridge is present. Previously only `FunctionParam` bridges
  triggered the visitor-variant stub, causing an `on_load` failure at runtime.

- fix(backend-rustler): `gen_bridge_field_function` now correctly handles optional options
  params. When the core function expects `Option<ConversionOptions>`, the plain NIF no longer
  calls `.unwrap_or_default()` (preserving `None`), and the visitor NIF wraps `options_core`
  in `Some(...)` before passing to the core function.

- fix(alef-codegen/enums): resolved merge-conflict marker in `gen_enum` that broke builds.
  The `#[default]` attribute is now placed on the `is_default`-marked variant (falling back to
  the first variant), matching the Rust core's explicit `#[default]` placement.

- fix(napi): `core_to_binding` conversion now emits `Default::default()` for opaque Named fields
  with `CoreWrapper::None` (e.g. `visitor: Object<'static>`) instead of trying to wrap them
  with `Arc::new` — mirrors the same fix previously applied to `binding_to_core`.

- fix(napi): `gen_opaque_struct_methods` no longer emits `#[napi]` methods whose parameters
  include opaque-typed values by value; NAPI class types don't implement `FromNapiValue`
  and cannot appear as plain method params. Methods without an explicit adapter body are skipped.

- fix(napi/types): opaque Named types in `#[napi(object)]` struct fields are now mapped to
  `napi::bindgen_prelude::Object<'static>` instead of the NAPI class type, which fails the
  `FromNapiValue` bound required by `#[napi(object)]`.

- fix(napi/dts): opaque trait types (e.g. `JsHtmlVisitor`) are now included in the generated
  `index.d.ts`; previously they were excluded by the `!is_trait` filter.

- fix(backend-java): optional record fields now use `@Nullable` annotation instead of
  `Optional<T>` wrapper, making the Java API idiomatically correct. Fields are typed as
  nullable boxed types (e.g. `@Nullable Long` instead of `Optional<Long>`), improving
  compatibility with IDE inspections and code generators.

- fix(backend-java): Javadoc generation now strips Rust-specific syntax (intra-doc links
  like `[`Type`]`, code block fences, and Rust sections like `# Arguments`/`# Errors`)
  to produce valid Java documentation. The `emit_javadoc` helper function applies the
  transformation uniformly across all public API methods.

- fix(backend-java): builder `build()` methods now correctly extract values from `Optional`
  fields using `.orElse(null)` before passing to nullable record constructors.

- fix(backend-java): `convertWithVisitor` method now constructs `ConversionResult` with
  nullable fields instead of `Optional.of()` and `Optional.empty()` calls.

- fix(napi/dts): optional fields in `#[napi(object)]` structs are now correctly marked `?`
  when the field type is `TypeRef::Optional`, `field.optional` is set, or the parent type
  has `has_default = true`.

- fix(e2e/node): removed `"strictNullChecks": false` from e2e tsconfig; strict mode is now
  fully enabled.

- fix(e2e/node): visitor method template double-brace bug fixed and string literals in
  `Custom` callback returns are now properly quoted.

- fix(e2e/node): visitor method parameters are annotated `: any` to satisfy `noImplicitAny`.

- fix(java): bare `catch (Throwable ignored) { return 0; }` in VisitorBridge upcall stubs swallowed
  visitor exceptions silently. The catch clause now captures the first throwable in a sticky
  `volatile Throwable visitorError` field, returns `VISIT_RESULT_ERROR` (4), and surfaces the
  exception via `rethrowVisitorError()` in the `finally` block of `convertWithVisitor`.

- fix(java): `encodeVisitResult` allocated `Custom` markdown and `Error` message buffers into
  `Arena.global()`, leaking them permanently. The method is now a non-static instance method
  that allocates into `this.arena` (the bridge's confined arena), so all buffers are freed on
  `VisitorBridge.close()`.

- fix(java): `checkLastError()` always threw the base exception class. The method now
  dispatches on the native error code: code 1 throws `InvalidInputException`, code 2 throws
  `ConversionErrorException`, and any other code throws the base `{Class}Exception`.

- fix(java): `createObjectMapper()` was called once per FFI invocation, allocating a new
  `ObjectMapper` on every `convert()` call. The factory is now called once at class-load time
  to populate a `private static final ObjectMapper MAPPER` field.

- fix(java): stale generated files `IHtmlVisitor.java`, `HtmlVisitorBridge.java`,
  `TestVisitor.java`, `TestVisitorAdapter.java`, and `VisitContext.java` were emitted by the
  trait-bridge and gen-visitor paths. They are superseded by `Visitor.java` and
  `VisitorBridge.java` in the Panama FFM visitor pattern. All five files are removed from
  generation and deleted from committed output.

- fix(java): `has_visitor_pattern` was evaluated using `config.ffi.visitor_callbacks` which is
  always `None` in the Java backend's `ResolvedCrateConfig`. The check now also activates when
  any `[[trait_bridges]]` entry uses `bind_via = "options_field"`, which is the Java-specific
  visitor activation path.

- fix(java): output path defaulted to `packages/java/` from the workspace template, causing
  generated files to land in `packages/java/dev/kreuzberg/htmltomarkdown/` instead of the
  correct `packages/java/src/main/java/dev/kreuzberg/htmltomarkdown/`. The Java output path is
  now resolved via an explicit `java = "packages/java/src/main/java/"` entry in `[crates.output]`.

## [0.14.1] - 2026-05-02

### Added

- feat(trait-bridge): `[[trait_bridges]]` schema now supports optional `unregister_fn` and
  `clear_fn` fields. When present, alef emits the corresponding host-language wrappers
  alongside the existing `register_fn` codegen. Pyo3 backend implements both new methods;
  remaining backends fall through to the default `None` (no emission).

- feat(error-gen): error-Display strings now have template placeholders (`{message}`,
  `{plugin_name}`, `{elapsed_ms}`, `{limit_ms}`, `{0}`-style positional, etc.) stripped
  before emission. Acronym-aware variant-name splitting recognizes 40+ technical
  acronyms (IO, OCR, PDF, URL, HTTP, TCP, …) and preserves them in the rendered text:
  `IoError` → "IO error", not "iO error". Applies across all error-handling backends.

- feat(pyo3): `register_*` plugin docstrings now use a humanized noun derived from the
  trait name. Previously emitted `"""Register a register_ocr_backend backend."""`
  (placeholder leaked); now emits `"""Register a OCR backend implementation as a runtime plugin."""`.

### Fixed

- fix(extendr): `generate_public_api` no longer emits a duplicate `convert()` wrapper calling
  `.Call("htm_convert", ...)`. extendr generates `extendr-wrappers.R` with the correct
  `.Call(wrap__convert, ...)` symbol; the backend now emits only `@useDynLib` + `options.R`.

- fix(extendr): generated `options.R` now includes all `ConversionOptions` fields from the IR,
  including `exclude_selectors`, `max_image_size`, `capture_svg`, `infer_dimensions`, `max_depth`,
  `skip_images`, `link_style`, `output_format`, `include_document_structure`, `extract_images`,
  and `visitor` (previously missing).

- fix(extendr): visitor pairlist keys no longer carry a leading underscore. Rust uses `_ctx` /
  `_href` for unused default-impl parameters; R callers write `function(ctx, ...)` without the
  prefix. Keys are now trimmed of leading `_`.

- fix(e2e/r): `CallbackAction::Custom` / `CustomTemplate` output values are now quoted in generated
  R: `list(custom = "[AUDIO: podcast.mp3]")` instead of the previous unquoted form that caused a
  parse error.

- fix(e2e/r): `visitor` is now passed inside `options = list(visitor = visitor)` rather than as a
  top-level parameter to `convert()`.

- fix(backend-wasm): use `is_default` variant for generated `Default` impl on wasm enums instead
  of always using the first variant, fixing incorrect defaults for `HeadingStyle`, `CodeBlockStyle`,
  and `PreprocessingPreset`.

- fix(backend-wasm): visitor bridge methods now dispatch `{custom: "..."}` JS objects to
  `VisitResult::Custom(s)` and `{error: "..."}` objects to `VisitResult::Continue`, making the
  full `VisitResult` variant set reachable from JavaScript.

- fix(e2e/wasm): merge visitor into the options object (2nd arg) rather than appending it as a
  standalone 3rd argument, matching the wasm binding's single-object options API.

- fix(backend-napi): use generated `From` conversions for named reference parameters instead of
  JSON round-tripping when let-binding delegation is possible.

- fix(backend-pyo3): generate `__str__` and `__repr__` for Rust-backed enum wrappers so returned
  enum values are inspectable from Python.

- fix(e2e/python): render string containment assertions over configured array fields by checking common DTO text
  attributes instead of comparing directly against the object list.

- fix(alef-cli/format): format WASM binding crates with `cargo fmt --manifest-path`
  derived from the resolved output path so renamed or workspace-excluded crates are handled correctly.

- fix(codegen): use generated `From` conversions for named reference parameters instead of JSON round-tripping when a
  direct let-binding is possible. This preserves fields such as Python `ProcessConfig.language` in free functions.
- fix(codegen): convert Cow-backed string fields with `.into()` when reconstructing `core_self` for binding methods.

- fix(cli): run language-native formatters on stubs before finalising the embedded `alef:hash:` line.
  `alef stubs` previously skipped the format step and computed the hash over raw codegen output.
  When host-language tools (ruff, php-cs-fixer, mix format, cargo fmt, …) reformatted those files,
  the embedded hash no longer matched the on-disk content, causing `alef verify` to report them as stale.

- fix(cli): track stub-changed languages in `changed_languages` during `alef generate`.
  When stubs changed but no bindings changed for a language, the formatter gate
  (`if any_written && !changed_languages.is_empty()`) would skip formatting entirely,
  leaving stub files unformatted and the hash stale after the next formatter run.

- fix(cli): always add stub file paths to `current_gen_paths` before the orphan-sweep pass
  in `alef generate`. When the stub cache was warm, stub paths were not registered,
  causing the sweep to delete freshly-generated stub files (e.g. `.pyi`) as orphans
  on the second generate run.

- fix(csharp): `ConversionOptionsBuilder.Dispose()` and `VisitorHandle.Dispose()` were
  no-op stubs. Opaque handles now emit a `{Name}SafeHandle : SafeHandle` inner class for
  deterministic, exception-safe cleanup. The public wrapper delegates `Dispose()` to it.

- fix(csharp): `optionsHandle` and `visitorHandle` were leaked when `Convert()` or
  `ConvertWithVisitor()` threw. Both handles are now freed in a unified `try/finally` block.

- fix(csharp): `Marshal.PtrToStringAnsi` used throughout — replaced everywhere with
  `Marshal.PtrToStringUTF8` so non-ASCII characters (e.g. Unicode URLs, CJK text) are
  decoded correctly on all platforms.

- fix(csharp): typed exceptions (`InvalidInputException`, `ConversionErrorException`) were
  never thrown. `GetLastError()` now dispatches on the native error code and emits `if`
  branches for each registered exception type.

- fix(csharp): `NodeContext.NodeType` was typed `int` instead of the `NodeType` enum.
  `DecodeNodeContext` now casts `Marshal.ReadInt32` to `(NodeType)`.

- fix(csharp): stale generated files `IVisitor.cs` and `VisitorCallbacks.cs` were still
  emitted by `gen_visitor_files`. They are superseded by `IHtmlVisitor`/`HtmlVisitorBridge`
  in the trait-bridge pattern. Both files are removed from generation and deleted from
  committed output.

- fix(csharp): bare `catch { return 0; }` in visitor upcall thunks swallowed exceptions
  silently. The catch clause now captures the exception and returns the Error discriminant
  (4) with the message encoded via `EncodeString`.

- fix(csharp): outer `packages/csharp/HtmlToMarkdown.csproj` was a NuGet wrapper that
  double-included the inner project's sources, producing duplicate type definitions.
  Added `<Compile Remove="HtmlToMarkdown/**" />`, `<ProjectReference>`, and
  `<GenerateAssemblyInfo>false</GenerateAssemblyInfo>`.

- fix(csharp): `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` was absent from
  `Directory.Build.props`. Added alongside `<Nullable>enable</Nullable>`.

- fix(csharp): `using System.Threading.Tasks` was emitted unconditionally. It is now
  conditionalized on whether the API has any `async` functions or methods.

- fix(csharp): `ConvertWithVisitor` accepted `IVisitor` (old generated interface) instead
  of `IHtmlVisitor` (trait-bridge interface from `TraitBridges.cs`). The method now takes
  `IHtmlVisitor` and wraps it in `HtmlVisitorBridge` instead of the removed `VisitorCallbacks`.

- fix(napi): `mapper.map_type` returns `String`, not `Cow<str>`; removed stale `.into_owned()`
  calls in `src/backends/napi/src/gen_bindings/types.rs`.

- fix(extendr): removed dead reference to `gen_conversion_options_r` (kreuzberg-specific dead
  code from an earlier project-coupled state).

- fix(codegen): collapsed clippy `if-same-then-else` warning in `config_gen.rs` by combining
  enum-variant and Named-type branches that emit the same body.

- chore(deps): dropped unused `tracing` dep from `alef-core` (only `tracing-test` is used,
  and that's a dev-dep).

## [0.14.0] - 2026-05-02

### Changed

- **BREAKING (config schema): `alef.toml` is now multi-crate.** The old single-crate `[crate]` schema is rejected on load with a curated migration message. Run `alef migrate --write` to convert. Internally the loader returns `Vec<ResolvedCrateConfig>` and every backend, codegen pass, scaffold step, e2e generator, and publish step now consumes `&ResolvedCrateConfig` instead of `&AlefConfig`. The `Backend` trait signature changed accordingly. Workspace-wide settings live under `[workspace]`; per-crate settings live under `[[crates]]`.
- alef-cli now iterates every crate in the workspace by default. Add a top-level `--crate <name>` (repeatable) to restrict commands to a subset.
- `alef-backend-swift` / `alef-backend-kotlin`: an explicit `[crates.output] {swift,kotlin}` value is now treated as the verbatim package directory; only the template-derived default constructs the canonical `Sources/<Module>/` (swift) or `src/main/kotlin/<package>/` (kotlin) layout. Predictable rule: explicit override = exactly that path; default template = backend builds the canonical structure.

### Added

- feat(alef-cli): `alef migrate --write` rewrites legacy `alef.toml` in place using an atomic temp-file + rename and refuses to overwrite symlinks. The dry-run default prints a unified diff.
- feat(alef-cli): top-level `--crate <name>` CLI filter (repeatable) selects a subset of the workspace; absent processes every crate. New `crates/alef-cli/src/dispatch.rs` exposes `select_crates` / `is_multi_crate`.
- feat(alef-core): new `WorkspaceConfig` / `RawCrateConfig` / `ResolvedCrateConfig` / `OutputTemplate` types with documented per-field precedence; `OutputTemplate::resolve` rejects path traversal segments and NUL bytes; `WorkspaceConfig`, `RawCrateConfig`, and `NewAlefConfig` use `#[serde(deny_unknown_fields)]` so typos surface as parse errors.
- feat(alef-core): per-crate `compute_crate_sources_hash(&ResolvedCrateConfig)` replaces the legacy `&[PathBuf]` overload so multi-source-crate workspaces produce a single stable hash across all contributing source files.

### Fixed

- fix(e2e-go): avoid double-wrapping `len(...)` in `min_length` / `max_length`
  assertions and generate valid guards for array element length checks.
- fix(e2e-rust): honor call-level `result_is_simple` and `result_is_option`
  flags so optional scalar results are unwrapped before equality assertions.
- fix(codegen): preserve sanitized `Cow<str>` fields in Rust binding DTO serde and binding-to-core conversions.
  Python bindings previously dropped required string fields such as `ProcessConfig.language`, causing generated e2e
  tests to fail with missing-field deserialization errors.
- fix(alef-backend-dart, alef-backend-swift): honor `[crates.dart] frb_version` and `[crates.swift] swift_bridge_version` overrides — the fields were deserialized but every callsite hardcoded the compiled-in default constant.
- fix(alef-cli/migrate): preserve legacy `[crate]` scalars at the top of the generated `[[crates]]` entry by recursively clearing toml_edit position metadata so the resulting document is well-formed even when many language sub-tables follow.
- fix(alef-cli/version-pin): write `[workspace] alef_version` after a successful generate instead of a top-level `version =` line. The legacy detector rejected the top-level form on the next run, breaking re-generation.
- fix(alef-cli/cache): scope IR / lang / stage caches per-crate (`.alef/<crate>/ir.json`, `.alef/<crate>/hashes/<lang>.{hash,manifest}`, `.alef/<crate>/hashes/<stage>.{hash,manifest}`) so multi-crate workspaces no longer poison or overwrite each other's cache entries. `validate_cache_crate_name` rejects path separators, NUL, `..`, `.`.

## [0.13.10] - 2026-05-02

### Added

- feat(codegen): auto-exclude trait-bridge registration functions via `collect_trait_bridge_registration_fn_names()`.
  Each binding backend now automatically excludes `register_*` / `unregister_*` functions to prevent double-emit
  compile errors — downstream consumers no longer need manual `exclude` entries in `alef.toml` for trait-bridge registration fns.

- feat(e2e-go): support disk-path fixture loading in Go e2e harness in addition to HTTP URLs.
  New test infrastructure in `src/e2e/tests/go_bytes_file_path_fixtures.rs` demonstrates the pattern.

### Fixed

- fix(cli): qualify type-name exclusion entries by Rust path. `apply_filters` previously matched
  `[exclude].types`/`enums`/`errors` entries against the short type name only, so a fully-qualified entry
  like `kreuzberg::core::config::formats::OutputFormat` never actually filtered anything when an
  ambiguously-named type appeared in two pub-use chains. Entries containing `::` are now matched against
  the type's `rust_path` (with hyphens normalised); plain entries continue to match by short name.
  Three regression tests cover plain-name match, qualified-path match, and hyphen normalisation.

- fix(codegen): resolve C# signature mismatch in generated binding code that surfaced during trait-bridge auto-exclusion wiring.

- fix(backend-rustler): emit `if(...)` function-call form instead of `(if ...)` outer-parens form for JSON-encoding options.
  `mix format` rewrites `(if x, do: y, else: z)` → `if(x, do: y, else: z)`, so the outer-parens form caused a hash drift
  on every prek run. The generator now emits the formatter-stable `if(...)` form directly.

- fix(e2e): honor call-level Rust option result flags and use the crate version for registry-mode Rust e2e dependencies when no package override is set.
- fix(e2e): generate valid Go array guards for length assertions on array element fields.
- fix(e2e): honor call-level simple/optional result flags in Go tests and render contains assertions for struct slices.
- fix(e2e): emit nil checks for Go optional simple-result empty assertions.
- fix(backend-java): correct four compile errors in the generated Java convert wrapper
  when `bind_via = "options_field"` is used with an opaque-handle bridge field:
  - B1: the generated `convert()` no longer calls the non-existent
    `htm_conversion_options_from_json` FFI symbol; options are now serialised to JSON
    and passed as a C string directly via `coptionsJsonSeg`.
  - B2: the bridge attachment code now detects that the bridge Java type is an opaque
    handle (e.g. `VisitorHandle`) and calls `options.visitor().handle()` to pass the
    raw `MemorySegment` to the FFI setter, instead of constructing a `VisitorHandleBridge`
    (which does not exist and would require a `Visitor` interface argument).
  - B3: the FFI call argument for the options param is now `coptionsJsonSeg` (matching
    the emitted variable name), not the stale `cOptions` name from the old `_FROM_JSON`
    path.
  - B4: `gen_builder_class` now only emits `.orElse(null)` for optional fields that
    are bridge fields (where the record stores the raw handle type); regular
    `Optional<T>` fields are passed directly to avoid "incompatible types" errors.
- fix(backend-go): preserve `None` as nil for optional string returns.
- fix(backend-go): decode externally tagged Rust enums with string payload fallback
  variants (for example `Other(String)`) as string-like Go enums instead of empty
  structs, and match Rust serde's default unit-variant wire names.
- fix(e2e-go): render `contains` assertions for structured values through JSON
  instead of `%v`, so pointer fields are checked by value.

- fix(backend-go): correct two codegen bugs that produced uncompilable Go bindings when
  `bind_via = "options_field"` is used with an options type that has an Update sibling:
  - Bug A: `ffi_c_struct_name` previously inserted an extra PascalCase prefix segment,
    producing double-prefixed C type names (e.g. `HTMHtmNodeContext`) that do not exist
    in the cbindgen header; the formula now maps Rust struct basenames directly to their
    cbindgen names (e.g. `HTMNodeContext`).
  - Bug B: visitor.go is no longer emitted when all `[[trait_bridges]]` entries use
    `bind_via = "options_field"`; the stale FFI symbols `htm_conversion_options_from_json`,
    `htm_visitor_create`, `htm_visitor_free`, and `htm_convert_with_visitor` are absent
    from the options-field bridge API and caused undefined-symbol link errors.
  - Additionally, non-opaque Named parameters and method receivers whose type has a
    corresponding `{Name}Update` sibling now use the two-step
    `{prefix}_{snake}_update_from_json` + `{prefix}_{snake}_from` pattern instead of the
    removed `{prefix}_{snake}_from_json` helper.

- fix(backend-pyo3): emit SCREAMING_SNAKE_CASE aliases alongside PascalCase variants in
  `.pyi` enum stubs so mypy resolves `CodeBlockStyle.BACKTICKS` without `attr-defined`
  errors. Previously only the raw PyO3 names (e.g. `Backticks`) were declared in the stub
  while `options.py` adds the aliases at runtime.

- fix(backend-pyo3): use `object | None` for bridge fields in the `__init__` stub parameter
  list, matching the field annotation. Previously the constructor parameter was typed
  `VisitorHandle | None`, causing a mypy `arg-type` error when passing `value.visitor`
  (typed `object`) from the generated `api.py` conversion helper.

- fix(backend-rustler): wrap inline `if/do/else` JSON-encoding expressions in parentheses
  so the generated NIF call args parse as Elixir when the conditional is followed by a
  visitor parameter. Previously emitted `convert_with_visitor(html, if ..., do: ..., else: ..., visitor)`
  which is invalid syntax — Elixir keyword lists must be the final argument.

- fix(backend-php): null-guard visitor access and correct PSR-4 namespace in generated PHP:
  - `generate_public_api` now emits `$options?->visitor` (null-safe operator) instead of
    `$options->visitor` when the options parameter is optional, eliminating the PHPStan
    "Cannot access property on null" error.
  - Added `generate_scaffold` to `PhpBackend` that emits a `functions.php` convenience
    wrapper using the namespace from `config.php_autoload_namespace()` (which honours
    `[php].namespace` in `alef.toml`), replacing the previously hard-coded wrong
    namespace derived by mechanical case-splitting of the crate name.

- fix(backend-php): resolve compile errors when `bind_via = "options_field"` is used
  in `[[trait_bridges]]` with an options type that has an opaque handle field:
  - `gen_php_struct` now receives `opaque_type_names` from `mod.rs` so
    `gen_struct_with_per_field_attrs` can emit `#[serde(skip)]` on fields whose type
    is an opaque handle (e.g. `VisitorHandle`), fixing 8 `E0277` serde trait errors.
  - `gen_bridge_field_function` now emits `let mut {name}_core` for optional params,
    enabling `options_core.as_mut()` in the generated `visitor_attach` call.
  - `find_bridge_field` (alef-codegen) now sets
    `param_is_optional: is_optional || param.optional`, covering the IR pattern where
    the options param is stored as `ty: Named("T") + optional: true` rather than
    `ty: Optional(Named("T"))`. This caused the wrong `visitor_attach` branch to be
    selected, producing a `no field 'visitor' on type Option<...>` compile error.
  - `gen_php_call_args` now emits `visitor.map(|v| (*v.inner).clone())` for optional
    opaque params, replacing the former `visitor.as_ref().map(|v| &v.inner)` which
    produced a type mismatch (`Option<&Arc<...>>` vs `Option<Rc<...>>`).

- fix(backend-wasm): resolve four compile errors when `bind_via = "options_field"` is
  used in `[[trait_bridges]]` with an options type that has a handle field:
  - `gen_new_method` now appends `..Default::default()` to the struct literal when
    bridge fields are excluded from constructor params, fixing missing-field errors.
  - `gen_opaque_struct_methods` now skips methods whose params contain a
    `type_alias` of an `options_field` bridge (e.g. the builder's `visitor()` method),
    preventing uncompilable Arc-vs-Rc type mismatches in the generated call args.
  - `field_conversion_from_core_cfg` (alef-codegen) now emits `None` for sanitized
    `Named` / `Optional(Named)` fields in WASM mode (`map_uses_jsvalue = true`)
    instead of the Debug-format fallback that produced `Option<String>` rather than
    `Option<JsValue>`.
  - `bridge_fields_map` is extended to cover `{options_type}Update` types so that
    binding→core `From` impls for Update types default the bridge field via
    `Default::default()` instead of calling `val.visitor.map(Into::into)`, which
    fails because `WasmVisitorHandle` has no `Into<VisitorHandle>` impl.
    Also fixes `convertible_types` (alef-codegen) to allow optional sanitized fields
    whose type is `Named(T)` with `optional = true`: the binding stores `Option<T>`
    which is always `Default`, so these types correctly remain in the convertible set.

- fix(backend-ffi): suppress duplicate `{prefix}_convert` symbols in options-field
  bridge mode. When `bind_via = "options_field"` is set, the free-function loop now
  skips ALL `convert` variants (not just sanitized ones), and `gen_convert_no_visitor`
  - `gen_visitor_bindings` are not emitted. The single authoritative `{prefix}_convert`
    comes exclusively from `gen_convert_with_options_field_bridge`. Removes the three
    `htm_convert` definitions that caused a duplicate `#[no_mangle]` compile error in
    html-to-markdown. Also fixes `gen_convert_with_options_field_bridge` to call
    `core::convert(html, options)` with 2 arguments matching the current API (visitor
    is embedded in options, not passed as a third argument).

- fix(e2e): visitor codegen now assigns the synthesized visitor object to
  `options.visitor` (language-idiomatic field assignment) instead of passing it
  as a third positional argument. This aligns e2e test generation with the h2m
  API change where `convert(html, options)` accepts visitor as an optional
  ConversionOptions field rather than a separate parameter. Implemented for:
  - Rust: mutate options binding after construction to attach visitor
  - Ruby: insert visitor key into options hash inline
  - PHP: merge visitor as separate options object
  - Python, TypeScript: already correct (kwargs/object-field pattern)
    Remaining languages (Go, Java, C#, Elixir, R, Gleam, Kotlin, Zig, Swift,
    WASM, Dart, C) require equivalent updates to move visitor from positional
    arg to options field assignment (patterns will vary per language syntax).

- fix(backend-napi): resolve all compile errors when `bind_via = "options_field"`
  is used in `[[trait_bridges]]` with a NAPI-RS binding:
  - `gen_struct` bridge-field substitution (emit `Option<Object<'static>>` instead of
    `Option<JsVisitorHandle>`) is now applied BEFORE the `field.sanitized` guard, so it
    correctly fires for bridge fields that are NOT sanitized (e.g. `visitor` in
    `ConversionOptions` has `sanitized: false`).
  - `gen_struct` now also builds a set of all bridge `type_alias` values and applies the
    `Option<Object<'static>>` substitution to ANY field whose type is a bridge alias
    (e.g. `ConversionOptionsUpdate.visitor`), not only fields in the declared
    `options_type`. This prevents `JsVisitorHandle: FromNapiValue not satisfied` errors
    in Update-pattern structs.
  - `bridge_fields_by_type` (used to populate `force_default_fields` for From
    conversions) is extended by case (b): any type with a field whose type is a bridge
    type alias is now included, so binding→core and core→binding `From` impls emit
    `Default::default()` instead of `val.visitor.map(Into::into)` for those fields.
  - `gen_opaque_struct_methods` now skips methods whose params contain a `type_alias`
    of an `options_field` bridge (e.g. `ConversionOptionsBuilder::visitor()`),
    preventing uncompilable Arc-vs-Rc type mismatches in the generated call args.

- fix(backend-pyo3): resolve all compile errors when `bind_via = "options_field"`
  is used in `[[trait_bridges]]` with a PyO3 binding:
  - `extra_field_attrs` now returns `vec![]` for bridge fields, preventing
    duplicate `#[pyo3(get)]` and `#[serde(skip)]` attributes.
  - `rewrite_bridge_field_type` now accepts `type_alias: Option<&str>` and
    also rewrites `Option<{alias}>` patterns (e.g. `Option<VisitorHandle>`),
    not only the legacy `Option<String>` placeholder.
  - `rewrite_bridge_field_impl` now rewrites constructor parameter types
    (`visitor: Option<VisitorHandle>` → `visitor: Option<Py<PyAny>>`).
  - Struct generation for both the primary options type and related types
    (e.g. `ConversionOptionsUpdate`) that share the same bridge field name
    now applies the full set of rewrites: field type override, `frozen` →
    `unsendable`, and manual `Clone` impl using `Python::attach`/`clone_ref`.
  - `bridge_field_name_for_type` extends From-conversion post-processing to
    cover related types (`ConversionOptionsUpdate`) that are not listed as
    `options_type` in `alef.toml` but share the bridge field name.
  - Opaque types whose name matches a bridge `type_alias` (e.g. `VisitorHandle`)
    and builder types for bridge options types (e.g. `ConversionOptionsBuilder`)
    now use `#[pyclass(unsendable)]` because they transitively contain
    `Rc<RefCell<dyn Trait>>`.
  - Builder opaque impl: `visitor.as_ref().map(|v| &v.inner)` rewritten to
    `visitor.map(|v| (*v.inner).clone())` to pass `Rc<RefCell<...>>` (not a
    reference into Arc) to the core builder's `.visitor()` method.
  - `gen_bridge_field_function` in `trait_bridge.rs` now uses
    `Python::attach(|py| v.clone_ref(py))` to extract the visitor from
    `Option<Py<PyAny>>`, replacing the former `.clone()` call that requires
    the `py-clone` feature (not enabled in most consumers).

- fix(backend-csharp): drop stale `[DllImport]` declarations and fix the visitor
  attach path when `bind_via = "options_field"` is active:
  - `gen_native_methods` no longer emits `{prefix}_conversion_options_from_json`
    for options types owned by an options-field bridge; those types are constructed
    via the `UpdateFromJson` + `FromUpdate` pair that still exists in the FFI surface.
  - `gen_native_methods_visitor` now returns an empty string when an options-field
    bridge is present, suppressing the stale `{prefix}_convert_with_visitor`,
    `{prefix}_visitor_create`, and `{prefix}_visitor_free` P/Invoke declarations.
  - `gen_wrapper_function` now emits `{opts}.{Field}._vtable` directly instead of
    double-wrapping the already-constructed `{bridge_type}Bridge` in a second `new
{bridge_type}Bridge(...)`, eliminating the `CS1503` type mismatch compile error.
  - `ffi_symbol()` on `OptionsFieldBridgeInfo` now produces
    `{prefix}_options_set_{field}` (matching `alef-backend-ffi::gen_bridge_field`)
    instead of the former `{prefix}_{opts_snake}_set_{field}` mismatch.

### Added

- feat(backend-pyo3): wire `bind_via = "options_field"` bridge support. When a
  `[[trait_bridges]]` entry sets `bind_via = "options_field"`, the PyO3 backend now:
  - Emits `visitor: Option<Py<PyAny>>` on the `ConversionOptions` pyclass (overriding
    the IR-sanitized `String` type via a targeted post-process rewrite).
  - Switches the `ConversionOptions` pyclass attribute from `frozen` to `unsendable`
    because the struct embeds `Rc<RefCell<dyn HtmlVisitor>>` via `Py<PyAny>` and is
    `!Send`; `unsendable` enforces single-thread GIL access.
  - Generates a `convert` wrapper that extracts `options.visitor`, builds a
    `PyHtmlVisitorBridge`, sets it on the core `ConversionOptions.visitor` field via
    `std::rc::Rc::new(std::cell::RefCell::new(bridge)) as VisitorHandle`, then calls
    core convert — no separate `convert_with_visitor` export.
  - Updates `.pyi` stubs: the visitor field on `ConversionOptions` is typed as
    `object | None` instead of `str | None`.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the pyo3 `trait_bridge` module.

- feat(backend-napi): wire `bind_via = "options_field"` bridge support — visitor field on
  `JsConversionOptions` is emitted as `Option<Object<'static>>`, the `convert` wrapper
  extracts it, builds the `JsHtmlVisitorBridge`, sets it on core options, and calls core;
  the `.d.ts` interface shows `visitor?: HtmlVisitor`; no separate `convertWithVisitor`
  export is emitted in this mode.
- feat(backend-extendr): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the R/extendr backend now:
  - Renders the bridge field as `Option<extendr_api::Robj>` on the binding options struct
    (with `#[serde(skip)]` so JSON round-trips ignore it).
  - Emits a custom `From<BindingOptions> for core::Options` impl that leaves the bridge
    field at `Default::default()`; the convert wrapper sets it explicitly after building
    the bridge from the R object.
  - Updates the `new_<options>` kwargs constructor to accept the visitor as
    `Option<extendr_api::Robj>` and assign it via `Some(v)`.
  - Emits a `convert` wrapper (via `gen_bridge_field_function`) that takes
    `options: Option<ConversionOptions>`, pulls the visitor field off the binding,
    constructs `RHtmlVisitorBridge`, attaches it to `options_core.visitor` as
    `Rc<RefCell<...>> as VisitorHandle`, and calls core convert.
  - Does NOT emit a separate `convert_with_visitor` extendr export.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the extendr `trait_bridge` module.
- feat(backend-rustler): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the Elixir Rustler backend
  now emits:
  - A regular `convert` NIF (ignores any visitor field in the options JSON).
  - A `convert_with_visitor` NIF with the visitor appended as `Option<rustler::Term<'_>>`,
    generated by the new `gen_bridge_field_function` helper.
  - A `visitor` field (nil default) on the `ConversionOptions` Elixir struct.
  - A `convert/2` Elixir wrapper that calls `Map.pop(options, :visitor)` and dispatches
    to either the async NIF+receive-loop (visitor present) or the plain NIF (no visitor).
  - `convert_with_visitor` is not re-exported as a public Elixir function.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the `trait_bridge` module.
- feat(backend-ffi): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a bridge is configured in options-field mode, `alef-backend-ffi` now:
  - Emits `{prefix}_options_set_{field}(options, visitor)` — a setter that wraps
    the vtable bridge in a thin `Rc<RefCell<VtableRef>>` and stores it on the
    options struct's visitor field.
  - Emits `{prefix}_convert(html, options)` (options carries the embedded visitor)
    instead of the generic sanitized stub.
  - Does NOT emit `{prefix}_convert_with_visitor` — the single `{prefix}_convert`
    entry point replaces it.
  - Suppresses `{prefix}_{options_type}_from_json` for types that own a bridge
    field, because the visitor cannot survive a JSON round-trip.
    New symbol emitted for h2m: `htm_options_set_visitor`.
- feat(docs): render trait-bridged fields as struct fields on options types when
  configured with `bind_via = "options_field"`. The visitor bridge type is rendered
  with language-appropriate syntax (e.g. `HtmlVisitor(Protocol)` for Python,
  `HtmlVisitor (interface)` for TypeScript), and the field description is
  auto-generated if not explicitly documented.
- feat(core/codegen): trait bridges may now declare `bind_via = "options_field"`
  on `[[trait_bridges]]` (with `options_type` and optional `options_field`)
  to indicate that the bridge handle lives as a struct field on a function
  parameter rather than as its own positional argument. Adds the
  `BridgeBinding` enum, the `find_bridge_field` codegen helper, and updates
  `find_bridge_param` to skip bridges configured for options-field binding.
  Existing configs default to `function_param` (the legacy mode), so this is
  fully backwards compatible.
- feat(backend-wasm): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the WASM/wasm-bindgen backend now:
  - Emits the bridge field as `Option<JsValue>` on the binding struct (e.g. `visitor` on
    `WasmConversionOptions`) with a matching `#[wasm_bindgen(getter)]` / `setter`.
  - Excludes the bridge field from the `new()` constructor — callers set it via the setter.
  - Marks the bridge field `sanitized` in the cloned IR so the auto-generated
    `From<WasmConversionOptions>` emits `Default::default()` for it.
  - Generates a `convert` wrapper (via `gen_bridge_field_function`) that extracts the
    `Option<JsValue>` from the binding options, converts the rest to core options via
    `From`, builds `WasmHtmlVisitorBridge`, wraps it as
    `Rc<RefCell<...>> as VisitorHandle`, sets it on core options, and calls core convert.
  - Does NOT emit a standalone `convertWithVisitor` function in this mode.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the wasm `trait_bridge` module.
- feat(backend-csharp): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the C# backend now:
  - Emits a `[JsonIgnore] public {Trait}Bridge? {Field}` property on the options
    record type so the bridge object is excluded from JSON serialization.
  - Declares an `internal static extern void {Options}Set{Field}(IntPtr options, IntPtr vtable)`
    P/Invoke entry-point in `NativeMethods.cs` whose FFI symbol is
    `{prefix}_{options_snake}_set_{field_snake}`.
  - Injects a bridge-attachment block in the wrapper `Convert` method: when
    `options.Visitor != null`, creates the bridge, calls the setter, then calls the
    standard two-arg `convert(html, options)` FFI function.
  - Does NOT emit a separate `ConvertWithVisitor` overload in this mode.
- feat(backend-php): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the PHP/ext-php-rs backend now:
  - Renders the bridge field as `?HtmlVisitor` on the `ConversionOptions` type stub
    (overriding the IR-sanitized `?string` type).
  - Generates a `convert` wrapper (via `gen_bridge_field_function`) that takes all original
    params plus a hidden `{field}_obj: Option<&mut ZendObject>` extra param, builds
    `PhpHtmlVisitorBridge::new`, wraps it in `Rc<RefCell<...>> as VisitorHandle`, attaches it
    to the core options struct, and calls core convert.
  - The PHP facade passes `$options->visitor` as the extra hidden arg when delegating to the
    native extension class.
  - Does NOT emit a standalone `convertWithVisitor` function.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the PHP `trait_bridge` module.
- feat(backend-go): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the Go/cgo backend now:
  - Emits a lightweight `type {Trait} interface` (trait methods only, no plugin lifecycle) in `binding.go`.
  - Injects a synthetic `{Field} {Trait} \`json:"-"\``field on the Go options struct,
skipping the raw IR`Option<VisitorHandle>` field so JSON marshaling ignores it.
  - Adds a `WithConversionOptions{Trait}` functional-option constructor.
  - Adds `"runtime/cgo"` to the imports when any options-field bridge is active.
  - In the `Convert` wrapper, after the C options handle is created, checks
    `options.{Field} != nil`, creates a `cgo.NewHandle`, defers `handle.Delete()`,
    and calls `C.{prefix}_options_set_{field}(cOptions, ...)` to pass the visitor to Rust.
  - Does NOT emit a separate `ConvertWithVisitor` function in this mode.
- feat(backend-java): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the Java/Panama FFM backend now:
  - Emits the bridge field as the bridge interface type (e.g. `HtmlVisitor`) annotated with
    `@JsonIgnore` on the `ConversionOptions` record so Jackson excludes it from serialization.
  - Emits a `static final MethodHandle {PU}_OPTIONS_SET_{FIELD}` in `NativeLib.java` using
    `orElse(null)` so class initialization succeeds even when the dylib lacks the symbol.
  - Generates a `convert` wrapper that marshals the options record normally (bridge field is
    skipped by `@JsonIgnore`), then—if both the setter handle and `options.visitor()` are
    non-null—creates the `{Trait}Bridge`, converts it to a `MemorySegment` via `toSegment`,
    and calls the setter before the main FFI invocation.
  - Does NOT emit a standalone `convertWithVisitor` method in this mode.
- feat(backend-magnus): support `bind_via = "options_field"` in `[[trait_bridges]]`.
  When a visitor bridge is configured in options-field mode the Ruby/Magnus backend now:
  - Renders the bridge field as `Option<magnus::Value>` on the binding options struct
    (with `#[serde(skip)]` so JSON round-trips ignore it).
  - Generates a custom `From<BindingOptions> for core::Options` impl that skips the
    bridge field and lets the convert wrapper set it explicitly.
  - Emits a `convert` wrapper (via `gen_bridge_field_function`) that deserializes the
    options hash via `to_json`/`serde_json`, extracts the visitor Ruby object, builds
    `RbHtmlVisitorBridge`, wraps it in `Rc<RefCell<...>>`, and sets it on the core
    options before calling the core function.
  - Does NOT emit a standalone `convert_with_visitor` function.
    Re-exports `find_bridge_field` and `BridgeFieldMatch` from `alef-codegen` through
    the Magnus `trait_bridge` module.

### Fixed

- fix(backend-java): public facade and opaque-type Javadocs now use the shared
  Javadoc escaping path, preventing Rustdoc examples from being emitted as raw
  `&...`/fenced-code content that breaks `mvn verify` Javadoc generation.
- fix(backend-php): generated public PHP wrapper files now include the
  formatter-required blank line after `<?php` before the Alef header, keeping
  `php-cs-fixer` from mutating generated files after hashes are finalized.
- fix(scaffold/java,scaffold/php): generated Java Checkstyle suppressions now
  use a repo-root path in `checkstyle.xml` so both Maven and repo-root
  pre-commit invocations find the suppressions file, and the PHP CS Fixer
  scaffold is itself fixer-clean while still tolerating packages without a
  `tests/` directory.
- fix(cli/format): the default WASM formatter now derives the generated crate
  package from Alef's core crate directory (`core_crate_dir-wasm`) instead of
  the public Rust crate name. This fixes repos where the public crate
  (`tree-sitter-language-pack`) differs from the internal core crate directory
  (`ts-pack-core`), avoiding `cargo fmt -p <missing-crate>` during generation.
- fix(scaffold/generated-output): generated downstream projects now pass common pre-commit checks after trimming a public API surface: Python `options.py` no longer imports native data-enum aliases before redefining their Python-side aliases, Elixir/Ruby Cargo scaffolds omit `async-trait`/`tokio` when no generated code uses them, Java Checkstyle suppressions resolve from both Maven and repo-root hook invocations, PHP CS Fixer tolerates packages without a `tests/` directory, and Rustler `native.ex` target lists are emitted in `mix format` style.
- fix(cli/format): the default Node formatter path now uses the Oxc toolchain (`npx oxfmt .` followed by `npx oxlint --fix .`) instead of invoking Biome. The Node scaffold, lint defaults, and generated pre-commit config already used `oxfmt`/`oxlint`; this removes the remaining stale Biome fallback that `alef all --clean` could hit in downstream repos with vendored Biome configs.
- fix(backend-napi): `gen_dts` now applies the same filtering as `gen_function` (drops names listed in `[node].exclude_functions`, drops `sanitized` functions without a trait_bridge). Previously every public function in the API surface was declared in `index.d.ts`, even when its NAPI binding was filtered out of `lib.rs` because it took a tuple-typed param like `Vec<(Vec<u8>, String, Option<FileExtractionConfig>)>`. The mismatch surfaced as TS2614 / TS2345 in downstream e2e suites that imported the phantom names. The d.ts is now generated from the same filtered set as the lib.rs, keeping the two in lockstep.
- fix(e2e/go): json_object args with `element_type` now emit a Go slice type derived from the element (e.g. `Vec<String>` → `[][]string`). Previously the codegen always declared `var parts []string` regardless of `element_type`, producing `cannot use parts (variable of type []string) as [][]string value` on `kreuzberg.GenerateCacheKey([][]string)` and similar nested-slice signatures.
- fix(backend-java): generate `TestVisitor`, `TestVisitorAdapter`, and `VisitContext` types for visitor-pattern bindings. `TestVisitor` is a test-friendly interface using `VisitContext` (same fields as `NodeContext`) instead of the raw FFI `NodeContext`, and `TestVisitorAdapter` converts `NodeContext` → `VisitContext` before delegating. `VisitResult` gains a `continue_()` alias used by generated e2e tests. `VisitorBridge.encodeVisitResult` now uses `Arena.global()` (not the try-with-resources arena) so allocated buffers survive past the callback return, and calls `.reinterpret(ValueLayout.ADDRESS.byteSize())` before `.set()` on the 0-byte upcall `outCustom`/`outLen` segments. The public facade's `generate_public_api` now forwards `has_visitor_pattern` so `HtmlToMarkdown.convert(html, options, TestVisitor)` is emitted. Trait bridge register/unregister handles in `NativeLib` use `.map(...).orElse(null)` instead of `.orElseThrow()` to avoid `ExceptionInInitializerError` when the symbol is absent.
- fix(e2e/java): `CustomTemplate` actions now extract `{placeholder}` names, convert them to camelCase Java variable names, replace each `{name}` with `%s`, and emit `String.format("fmt", var1, ...)` instead of the unformatted literal. `visit_blockquote`'s `depth` parameter is now `long` (matching the `JAVA_LONG` C layout) instead of `int`. Visitor e2e test files now import `TestVisitor`, `VisitContext`, and `VisitResult` from the binding package when any fixture uses visitor callbacks.
- fix(backend-pyo3): emit `setattr(Cls, "FOO", getattr(Cls, "Bar"))` instead of bare `Cls.FOO = Cls.Bar` when the Rust variant name is a Python keyword. Previously enum variants like `HighlightStyle::None` rendered as `HighlightStyle.NONE = HighlightStyle.None` in `options.py` — `None` is a reserved word, so this produced a `SyntaxError: invalid syntax` at import time, breaking every Python e2e test for any binding whose enum has a `None`/`True`/`False`/etc. variant.
- fix(backend-pyo3): use the python-ident-escaped runtime name in the `getattr` call for keyword-named enum variants. PyO3's `#[pyclass]` enum derive renames variants whose name collides with a Python keyword by appending `_` (e.g. `None` → the runtime attribute is `None_`), so `getattr(HighlightStyle, "None")` raises `AttributeError`. The alias emission now resolves the runtime name via `alef_core::keywords::python_ident` and also triggers the `setattr`/`getattr` form when the SCREAMING_SNAKE_CASE alias itself is a Python keyword (e.g. a variant `As` would alias to `AS` which is fine, but `Is` → `IS` and a hypothetical `Continue` → `CONTINUE` are guarded for symmetry).
- fix(e2e/rust): pass visitor arg and unwrap Result for content access. Generated tests now call `convert(html, Some(options), None)` (3-arg form) instead of the stale 2-arg form, emit `.expect("should succeed")` so field access is on the unwrapped `ConversionResult` not on `Result<ConversionResult, ConversionError>`, derive `Debug` on `_TestVisitor` structs (required by the `HtmlVisitor: Debug` bound), import `HtmlVisitor` from the `::visitor` sub-module where it lives, and fix all `HtmlVisitor` method parameter types (e.g. `u8` → `u32` for heading level, `&str` → `Option<&str>` for audio/video/iframe src, `usize` for blockquote depth). `CustomTemplate` actions now bind the referenced parameter identifiers so `format!("{text}")` etc. compile; `Option<&str>` parameters referenced in a template are unwrapped via `let name = name.unwrap_or_default()`. The `alef.toml` Rust override for html-to-markdown gains `wrap_options_in_some = true`, `options_type = "ConversionOptions"`, `extra_args = ["None"]`, and `returns_result = true`.
- fix(e2e/elixir): the e2e mix.exs now uses `[elixir].app_name` as the dep atom (e.g. `:html_to_markdown`), not the crate name with `-` → `_` replacement (e.g. `:html_to_markdown_rs`). When the dep atom doesn't match the path-dep's own `app:` value, mix's resolution silently misroutes — the path-dep's transitive deps (notably `:rustler_precompiled`) are not loaded during compilation of the path-dep itself, and the parent build fails with `(CompileError) cannot compile module ... module RustlerPrecompiled is not loaded and could not be found`.

- fix(backend-php): add `[php].namespace` field to `PhpConfig` that overrides the derived PHP namespace verbatim. Previously the namespace was always derived from `extension_name` by splitting on `_` and converting each segment to PascalCase (e.g. `html_to_markdown` → `Html\To\Markdown`), which was inconsistent with the single-segment `HtmlToMarkdown` namespace expected by `[e2e.call.overrides.php].module`. The new field is used verbatim in class registration (`#[php(name = ...)]`), the PHP facade, type stubs, PSR-4 autoload keys, and e2e test imports — ensuring all four places agree. The three duplicated inline namespace-derivation blocks in `gen_bindings/mod.rs` are collapsed to a single `config.php_autoload_namespace()` call that honors the override.

- fix(backend-extendr): emit `impl From<BindingType> for core::Type` (and the reverse direction) for binding↔core struct and enum conversions, mirroring the existing pyo3/magnus emission paths. Without these, every `*_core: core::Type = binding.into()` site in the generated R lib.rs failed to compile with E0277 unsatisfied trait bounds (e.g. `From<PreprocessingOptions> not implemented for html_to_markdown_rs::PreprocessingOptions`). Lossy `From` impls are now also emitted for data-variant enums, since the extendr binding flattens them to unit variants but containing structs still call `.into()` on the variant value.
- fix(codegen/extendr-kwargs): the `gen_extendr_kwargs_constructor` helper used to emit Rust function-parameter defaults (`name: String = "default"`), which is a syntax error — Rust does not accept default values in function signatures. The constructor now accepts each field as `Option<T>`, instantiates the type via its `Default` impl, and overlays caller-supplied values, producing valid Rust source that the `#[extendr]` macro accepts. Existing `extendr(default = …)` semantics on the R side are preserved by the `generate_public_api` wrapper.
- fix(e2e/c): emit shfmt-compliant case statement style in `download_ffi.sh` so the generated file passes the `shfmt` pre-commit hook without modification and `alef verify` stays consistent.
- fix(e2e/elixir): the e2e mix.exs now uses `[elixir].app_name` as the dep atom (e.g. `:html_to_markdown`), not the crate name with `-` → `_` replacement (e.g. `:html_to_markdown_rs`). When the dep atom doesn't match the path-dep's own `app:` value, mix's resolution silently misroutes — the path-dep's transitive deps (notably `:rustler_precompiled`) are not loaded during compilation of the path-dep itself, and the parent build fails with `(CompileError) cannot compile module ... module RustlerPrecompiled is not loaded and could not be found`. Fixes the elixir e2e suite for any binding whose `[elixir].app_name` differs from the kebab-case crate name.
- fix(e2e/typescript): `options_type` casts and imports are now resolved per-fixture. Previously the renderer used the top-level `[e2e.call.overrides.<lang>].options_type` for every fixture in a test file, even fixtures that overrode the call (`fixture.call = "chunk_text"`) and declared their own per-call `options_type` (e.g. `JsChunkingConfig`). The renderer now reads `e2e.calls.<name>.overrides.<lang>.options_type` for fixtures that override the call, and only the top-level value is used for fixtures using the default call. The header import block emits `type X` declarations for the _union_ of all options types referenced by the active fixtures (e.g. both `JsExtractionConfig` and `JsChunkingConfig` when one file mixes `extract_file` and `chunk_text` fixtures), eliminating TS2304 "Cannot find name 'JsChunkingConfig'" on chunking fixtures and stopping the wrong cast (`JsExtractionConfig` applied to a `JsChunkingConfig`-shaped object).
- fix(e2e/typescript): `as <OptionsType>` casts are now emitted only for _object_-shaped json_object args. Previously every json_object arg (including `paths: [...]`, `texts: [...]`, `parts: [...]`) was cast to the call's options type, producing `["a.pdf"] as JsExtractionConfig` on `batchExtractFileSync(paths, config)` calls — TS2352 "Conversion of type 'string[]' to type 'JsExtractionConfig' may be a mistake". The renderer now inspects the fixture value: arrays/scalars are emitted plain (typed as `Array<string>` / etc by the binding), only objects receive the cast.
- fix(e2e/typescript): `bytes` arg type now classifies the fixture string value (file path / inline / base64) and emits a runtime load instead of passing the string as-is. Previously fixtures like `extract_bytes` with `data: "pdf/fake_memo.pdf"` produced `extractBytes("pdf/fake_memo.pdf", ...)` — TS2345 "Argument of type 'string' is not assignable to parameter of type 'Uint8Array<ArrayBufferLike>'". The renderer now classifies via the same rules as python/rust codegens (`<`/`{`/`[`/whitespace → inline-text → `Buffer.from(s, "utf-8")`; `dir/file.ext` → file-path → `readFileSync(path)`; otherwise → base64 → `Buffer.from(s, "base64")`). When any active fixture's resolved call uses a bytes file-path arg, the test file imports `{ readFileSync } from 'node:fs'`. The existing `setup.ts` chdir to `test_documents/` is preserved, so relative paths resolve at runtime.
- fix(e2e/typescript): cast through `unknown` (`as unknown as <Type>`) for both inline configs and the empty-default placeholder. NAPI-RS marks Rust `Option<T>` config params as `T | undefined | null` but bare `T` as required, while the alef.toml `optional` flag does not always match — fixtures like `extract_file_with_no_config` produced calls with no third argument and TS2554 "Expected 3 arguments, but got 2". When the json_object arg is missing (or `null`) and an `options_type` is configured, the renderer now emits `{} as unknown as <Type>` to satisfy the binding's required parameter; partial config literals are also cast through `unknown` to avoid TS2352 "neither type sufficiently overlaps". The runtime binding still validates fields.
- fix(e2e/typescript): `result_is_simple = true` is now honored. Previously the flag was only used by the rust/csharp/java/ruby/r codegens, so fixtures whose binding returns a primitive (e.g. `generateCacheKey: string`, `validateCacheKey: boolean`) emitted `result.<field>` access and triggered TS2339 "Property 'result' does not exist on type 'string'". The TypeScript renderer now resolves the per-call `result_is_simple` flag from `[e2e.calls.<name>.overrides.<lang>]` and (a) uses the bare `result_var` for assertions, (b) skips assertions on non-`result` fields with a `// skipped:` comment.
- fix(e2e/typescript): `arg_order` per-language override is now honored. The flag has been documented in `alef-core` for some time but no codegen reordered the `args` slice — so kreuzberg's `[e2e.call.overrides.node] arg_order = ["path", "config", "mime_type"]` was silently ignored and the renderer emitted args in the canonical Rust order, producing TS2345 "Argument of type 'string' is not assignable to parameter of type 'JsExtractionConfig'" on every `extractFile(path, mime, config)` call (NAPI's binding takes `path, config, mime_type?`). The renderer now reorders the per-fixture call args via the language override before passing to `build_args_and_setup`.
- fix(e2e/typescript): per-fixture `options_type` resolution now falls back to the top-level `[e2e.call.overrides.<lang>]` when a per-call override doesn't declare its own. Most kreuzberg per-call entries (e.g. `extract_bytes`) rely on the same `JsExtractionConfig` shape as the default `extract_file` call and therefore intentionally omit the per-call `options_type` — but the prior renderer treated this as "no options_type", emitted no cast, and produced TS2345 on inline config literals. The fallback chain is: per-call `options_type` → top-level `options_type` → no cast.
- fix(e2e/typescript): emit a bare call (no arguments) when the resolved `[e2e.call]` has `args = []` and the fixture has no input. Previously the renderer fell through to `json_to_js(input)` and stringified the entire input object as a single positional arg, producing `listExtractors({})` for `args=[]` calls and triggering TS2554 "Expected 0 arguments, but got 1".
- fix(e2e/typescript): support per-call `result_is_vec = true` by indexing `[0]` into the result variable for field-targeted assertions. Mirrors the existing csharp behavior so fixtures asserting `mime_type` / `content` on `batch_extract_*` calls (which return `Array<JsExtractionResult>`) compile cleanly under strict tsconfig.
- fix(e2e/typescript+wasm): honor per-call `skip_languages` when filtering active fixtures. The CallConfig field has been documented in alef-core for some time but no codegen consulted it — so kreuzberg's `[e2e.calls.batch_extract_*] skip_languages = ["wasm"]` was silently ignored and the wasm renderer emitted test files importing `batchExtractBytesSync` etc. that the wasm binding doesn't export, producing TS2614 / TS2724. Both the typescript (node) and wasm renderers now drop fixtures whose resolved call's `skip_languages` contains the target language.
- fix(e2e/typescript): `not_empty` / `is_empty` on non-array fields no longer emits `.length`. The renderer cannot statically know whether a struct field (e.g. `metadata` on JsExtractionResult — type `JsMetadata`) is a string or an object, so it now emits a runtime IIFE that branches on `typeof v === 'string' || Array.isArray(v)` (uses `v.length`) vs object (uses `Object.keys(v).length`). Casting through `unknown` avoids TS narrowing the unreachable branch to `never`. Behavior on array fields (declared in `fields_array`) is unchanged.
- fix(e2e/typescript): `has_later_arg_value` now also returns true for optional `json_object` args with `options_type` configured, since those emit a typed `{} as unknown as <Type>` placeholder. Previously a fixture with only `path` set (no `mime_type`, no `config`) on a 3-arg `extractFile(path, mime_type?, config)` produced `extractFile(path, {})` (2 args), tripping TS2554 against the wasm signature `extractFile(path, mime_type, config)`. The fix makes preceding optional args emit `undefined` placeholders so positional order is preserved.

### Changed

- feat(e2e/codegen): result-shape flags (`result_is_simple`, `result_is_vec`, `result_is_array`, `result_is_bytes`, `result_is_option`) are now read from the call-level `[e2e.calls.<n>]` (and the default `[e2e.call]`) in addition to per-language overrides. The rust, csharp, ruby, r, and typescript codegens all OR the call-level value with their per-language override, so a single call-level declaration applies consistently to every binding (return-type shape is a property of the Rust core function, not of any binding). Per-language overrides remain accepted for backwards compatibility but are no longer required.

### Changed

- feat(core/config): `result_is_simple`, `result_is_vec`, `result_is_array`, `result_is_bytes`, and `result_is_option` are now first-class fields of `CallConfig` itself, not only `CallOverride`. These flags describe the Rust core's return type and therefore apply identically to every binding — declaring them at call level eliminates the need for redundant per-language `[e2e.calls.<n>.overrides.<lang>] result_is_simple = true` blocks (kreuzberg's alef.toml had ~30 such call entries duplicated across rust/node/csharp/java/python/wasm). The typescript codegen now ORs the call-level value with the per-language override to preserve backwards compatibility; other backends will adopt the same pattern in subsequent releases. Per-language overrides remain accepted but should be removed in favor of the call-level field.

## [0.13.6] - 2026-05-02

### Added

- feat(scaffold): manage workspace `.cargo/config.toml` via opt-in `[scaffold.cargo]` section in `alef.toml`. When the section is present, alef writes the full canonical file with hash-based drift detection (same `# alef:hash:` pattern as other alef-managed files). Default config produces a 6-target template covering macOS dynamic*lookup (required for PyO3/ext-php-rs cdylibs to link on macOS — the symptom is `ld: symbol(s) not found for architecture arm64` on `\_zend*_`/`ext_php_rs::zend::_`and PyO3 symbols at runtime since macOS ld is strict and these symbols are resolved at extension-load time, not link time), Windows MSVC`rust-lld`for x86_64 + i686,`aarch64-linux-gnu-gcc`for ARM64 Linux cross-compile,`x86_64-unknown-linux-musl`for static Linux, and the`wasm32-unknown-unknown`bulk-memory + getrandom_backend cfg. Per-target opt-out via`[scaffold.cargo.targets]`; repo-specific `[env]`entries via`[scaffold.cargo.env]`(supports both bare strings and`{ value, relative }`form). When`[scaffold.cargo]`is absent, the legacy create-if-missing wasm32-only behavior is preserved unchanged. New types in`alef-core::config`: `ScaffoldCargo`, `ScaffoldCargoTargets`, `ScaffoldCargoEnvValue`. New public function `alef_scaffold::render_cargo_config(&ScaffoldCargo) -> String` for deterministic rendering, with 11 unit tests covering golden output, env injection, per-target opt-out, deterministic re-render, key sorting, escaping, and the legacy-fallback gate.

### Added

- feat(e2e/elixir,e2e/gleam): migrate the BEAM languages onto the shared `TestClientRenderer` + `render_http_test` driver, completing Phase 2B. Elixir defines an `ElixirTestClientRenderer<'a>` (carrying `fixture_id` and `expected_status` so `render_call` can build `/fixtures/<id>` URLs and disable Req's auto-redirect for 3xx fixtures) implementing all 8 trait methods; the `FINCH_UNSUPPORTED_METHODS` (TRACE/CONNECT) skip stub stays as a thin pre-hook before delegating to the shared driver. Gleam defines a unit `GleamTestClientRenderer` whose `render_call` uses `gleam_httpc` and whose `render_assert_partial_body` / `render_assert_validation_errors` use `string.contains` (Gleam's stdlib has no JSON decoder); the renderer also overrides `sanitize_test_name` to strip leading `_`/digits so gleeunit names are valid Gleam identifiers. Behavioral notes: elixir's `describe`/`test` labels are now `(fixture_id, description)` rather than `("METHOD PATH - description", "")`, matching all reference impls (cosmetic only); both languages now emit `body_partial`/`validation_errors` assertions when fixtures carry them (old monoliths never called these); WASM was already routing through the migrated TypeScript renderer and required no changes.
- feat(e2e/go,e2e/java,e2e/csharp,e2e/kotlin): migrate the JVM/Go/.NET languages onto the shared `TestClientRenderer` + `render_http_test` driver. These languages cannot expose the consumer's `TestClient` cleanly via FFI, so they continue to drive the binding's `App.serve()` over a TCP-loopback `baseUrl` env var, but the per-test rendering now goes through the shared driver. Per-language deltas: go now unconditionally emits `_ = bodyBytes` to silence the unused-var compile error on tests with no body assertion (the old monolith conditionally omitted `io.ReadAll`); java/kotlin/csharp now correctly emit `<<present>>`/`<<absent>>`/`<<uuid>>` header-token assertions (old code silently skipped them); java now appends query params to the URI string (old code never handled query params); kotlin/go/csharp now sort headers and cookies for deterministic output (old code used HashMap insertion order); go/kotlin/csharp/java now also emit `body_partial` and `validation_errors` assertions when fixtures carry them (old monoliths never called these).
- feat(e2e/php,e2e/dart,e2e/swift,e2e/zig): migrate the second batch of TestClient-friendly languages (php, dart, swift, zig) onto the shared `TestClientRenderer` + `render_http_test` driver. Each language now defines a `<Lang>TestClientRenderer` struct + `impl client::TestClientRenderer` capturing only the language-native primitives (PHPUnit `public function test*(): void` + Guzzle, Dart `test('...', () async {})` over `dart:io`, XCTest `func testFoo() throws` over `URLSession`, Zig `test "..."` over `std.http.Client`). Per-language deltas: php now emits validation-error assertions even when the fixture also has a `body` (the old gate suppressed them); dart's `DartTestClientRenderer` carries a small `Cell<bool>` for `is_redirect` + `in_skip` because `CallCtx` doesn't carry expected-status; swift binds `_resp`/`_responseData` in `render_call` and references those names from subsequent assertion methods (matching the synchronous-via-DispatchSemaphore wrapper); zig's `render_assert_header` and `render_assert_partial_body`/`render_assert_validation_errors` are stubs (Zig's stdlib HTTP client doesn't expose response headers from `client.fetch(...)` results — same TODO pattern already used for `method_result`/`matches_regex` in the file).
- feat(e2e/python,e2e/typescript,e2e/ruby,e2e/rust): migrate the four pilot languages (python, typescript/node, ruby, rust) onto the shared `TestClientRenderer` + `render_http_test` driver. Each language now defines a small `<Lang>TestClientRenderer` struct + `impl client::TestClientRenderer` capturing only the language-native primitives (`def test_X(client):` / `it(...)` / RSpec `it` block / `#[tokio::test] async fn`); the canonical sequence (open → call → status → headers → body → validation → close), header sorting, content-encoding stripping, skip-directive handling, and `null`/empty-string body sentinels are now driven once from the shared driver. The four files `src/e2e/codegen/{python,typescript,ruby,rust}.rs` lose ~145-line monolithic HTTP-test renderers in favor of trait impls, leaving `render_http_test_function` (or equivalent) as a thin pre-hook + delegate to `client::http_call::render_http_test`. Behavioral notes: rust now also emits body/header/validation assertions from the shared driver (previous code only asserted status); typescript request headers are now emitted in deterministic sorted order rather than HashMap insertion order; python's one fixture with both `body` and `validation_errors` (`content_type_validation_invalid_type`) now emits both assertions instead of suppressing the latter (the validation assertion is a strict subset of the body equality, so no false failures).
- feat(e2e): shared `TestClientRenderer` trait and `render_http_test` driver in `src/e2e/codegen/client/`. Per-language e2e codegen previously duplicated the structural shape of every HTTP test (function header, request build, status/header/body assertions) across 18 monolithic per-language files (359-2965 lines each); the shape only differed in syntax. The new `client::TestClientRenderer` trait captures the canonical sequence — `render_test_open` → `render_call` → `render_assert_status` → per-header `render_assert_header` → `render_assert_json_body` / `render_assert_partial_body` → `render_assert_validation_errors` → `render_test_close` — and the shared `client::http_call::render_http_test` driver invokes those primitives in order, with built-in handling of skip directives, content-encoding header stripping, deterministic header ordering, and the `null`/empty-string body sentinels. Languages migrate one at a time: each implements the trait, replaces its monolithic `render_http_test_function` with a one-line call to the shared driver, and may optionally split its file into a `<lang>/{mod,client,ws,helpers}.rs` directory. 9 new unit tests use a tag-emitting mock renderer to lock down the driver's call sequence and special-case handling. A companion `client::ws_script::WebSocketScriptRenderer` trait is stubbed out for Phase 2B/2C.

### Changed

- chore: scrub library-specific terminology from alef source comments, doc-strings, test fixtures, and skill references. alef is a generic polyglot bindgen tool used by multiple downstream consumer libraries; comments, examples, and test fixture data that previously named one specific consumer ("spikard", `dev.spikard`, `:spikard`, etc.) are renamed to neutral placeholders (`mylib`, `mylib-core`, `consumer_core`, `com.example`, `:mylib`, `MyHandler`). Changes are limited to documentation/test data — no production code logic or generator output is affected. Historical published `## [0.x.y]` CHANGELOG entries are intentionally not rewritten (they describe shipped releases).
- fix(adapters/pyo3): callback-bridge codegen for Python no longer emits hardcoded `prepare_request`/`interpret_response` stub methods referencing a specific consumer's `handler_trait::RequestData` type. The stubs were dead code (always returning `Err(...)` "not implemented") that imposed a 3-method trait shape on every consumer; other language bridges (Node, Ruby, PHP, ...) only implement the user-defined method and were already correct. The Python branch now matches that pattern. If a consumer's actual trait requires additional methods, they must declare default impls in their trait definition or extend the AdapterConfig schema.

### Fixed

- fix(taskfile): `task set-version` now also bumps the workspace `alef.toml` (alef dogfoods its own config). Also switched from BSD-only `sed -i ''` to portable `sed -i.bak ... && rm -f` so the task works on Linux CI as well as macOS.
- fix(scaffold/node): emit `index.d.ts` at the package root in addition to `src/index.d.ts`. The generated `package.json` declares `"types": "index.d.ts"` (and lists `"index.d.ts"` in `files`), but the scaffold previously only emitted the type-shim under `packages/{node}/src/`, leaving the npm package without discoverable declarations at the path consumers actually resolve. Downstream `import 'kreuzberg'` (or any other alef-managed npm package) typed under the strict tsconfig produced `TS7016: Could not find a declaration file for module 'kreuzberg'` and degraded to implicit `any`, propagating to every test/file that referenced binding-emitted types like `JsExtractionConfig`. The fix emits both copies — the `src/` copy is preserved for in-tree tsconfig "include": ["src"] tooling; the new root copy is what npm exposes via `package.json#types`. Scaffold tests updated to assert both files are present (file count rises from 7 → 8 in `test_scaffold_node` and 10 → 11 in `test_scaffold_multiple`).
- fix(backend/pyo3): correct `Arc<T: !Copy>` field codegen for clone. When a struct field had type `Option<Arc<serde_json::Value>>` (or any non-String `Arc`-wrapped type), the core→binding `From` impl chained an extra `.map(|v| (*v).clone().into())` on top of a base conversion that already handled the `Option<Arc<T>>` via Display/Deref coercion (e.g. `val.X.as_ref().map(ToString::to_string)`). This produced `(*String).clone()` at the call site — dereferencing a `String` to `str`, then calling `str::clone()` which fails since `str: !Clone`. The fix detects when the base conversion is already a complex expression (not a simple `val.{name}` passthrough) and returns it unchanged, relying on `Arc<T>: Display + Deref<Target=T>` to correctly produce the binding-side value.
- fix(backend/pyo3): emit `Arc<tokio::sync::Mutex<T>>` (instead of `Arc<std::sync::Mutex<T>>`) for opaque types whose `&mut self` methods are all `async`. `std::sync::MutexGuard` is `!Send`, so holding a guard across `.await` makes the surrounding future `!Send`, which fails to compile against PyO3's `Send` future bound on `pyo3_async_runtimes::tokio::future_into_py`. `tokio::sync::MutexGuard` IS `Send`. New helper `alef_codegen::generators::type_needs_tokio_mutex` returns true only when every `&mut self` method on a mutex-needing type is async (mixed sync+async `&mut self` methods stay on `std::sync::Mutex` since `tokio::sync::Mutex::lock()` returns a future). The PyO3 backend rewrites the rendered struct + impl block to swap the lock type and `.lock().unwrap()` → `.lock().await` for these types. Verified end-to-end against an opaque type wrapping `axum-test`'s `TestWebSocket` in `Arc<Mutex<>>` with all-async `&mut self` methods: with the previously-required exclusions removed from `alef.toml`, the consumer's PyO3 cdylib compiles cleanly and the generated future satisfies the `Send` bound.
- fix(e2e/rust): rust e2e codegen now honors `owned = true` on `bytes` and `string` arg configs. Pre-this-change the renderer always emitted `&{name}` for bytes (via `.as_bytes()`) and a bare `&str` literal for string, regardless of whether the underlying kreuzberg-core function took the value by reference or by ownership (e.g. `detect_image_format(data: Vec<u8>)`, `detect_mime_type(path: String, ...)`). The codegen now flips both binding type (`Some(...).to_vec()` / `String::new()`) and pass expression (`name.to_string()` / `name.to_vec()` / `name`) when `owned = true`, so call sites match the function signature without forcing every owned-by-value Rust API into a borrowed wrapper. Two regression tests added in `src/e2e/tests/rust_function_call_fixtures.rs` (owned bytes file-path, owned string).
- fix(backend-magnus): functions registered via the trait*bridge path now use fixed arity in `function!(...)` instead of `-1` (variadic). The bridge generator emits a fixed-arg signature like `fn convert(html: String, options: Option<String>, visitor: Option<Value>)`, but the registration loop previously emitted `function!(convert, -1)` whenever any param was optional. Magnus's `function!` macro requires the fn to take `&[Value]` for arity `-1`, so the trait-bridge fn failed to satisfy `FunctionCAry<*>`/`Fn<(&[Value],)>`, breaking compilation for every binding that exposed a top-level function with an `HtmlVisitor`-style trait param (e.g. `html-to-markdown`'s ruby gem failed to build on linux/aarch64/macos with E0599 "method `call_handle_error`exists but trait bounds were not satisfied"). Bridge functions now register with`function!(name, params.len())`; non-bridge optional-param functions still use variadic `-1`with`scan_args` as before.

## [0.13.5] - 2026-05-02

### Fixed

- fix(e2e/rust): rust e2e codegen no longer short-circuits all non-HTTP, non-mock-server fixtures to a `// TODO: implement when a callable API is available` stub. Pre-0.13.5 the renderer assumed any fixture without an `http` block or a `mock_response` was a schema/spec validation fixture (asyncapi/grpc/graphql_schema) with no callable Rust API, so libraries whose fixtures invoke a plain function (e.g. `kreuzberg::extract_file(path, mime, config)`) emitted zero real test bodies. The stub gate now triggers only when the resolved call config has no function name; fixtures pointing at a configured `[e2e.call]` (or `[e2e.calls.<n>]`) render real function invocations with the right imports.
- fix(e2e/rust): `use {crate}::{fn};` import lines are now emitted for any non-HTTP fixture whose resolved call config has a function name, not only `mock_response`-bearing fixtures. Previously plain function-call fixtures rendered the call site but omitted the `use` line, producing E0425 "cannot find function" on every test.
- fix(e2e/rust): `bytes` arguments whose fixture value is a relative file path (e.g. `"pdf/fake_memo.pdf"`, classified the same way as in the python codegen) are now loaded at runtime via `std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../../test_documents/", "<path>")).expect(...)` and passed by reference. Previously the path string itself was embedded as `r#"…"#` and `.as_bytes()`-ed, which compiled but always passed the path text as the file contents.

## [0.13.4] - 2026-05-01

### Added

- feat(e2e/wasm): the wasm e2e codegen now emits real `extractFile`/`extractBytes`-style test cases for non-HTTP fixtures, not just HTTP-server fixtures. Pre-0.13.4 the wasm renderer filtered `f.http.is_some()` and dropped every function-call fixture, leaving `e2e/wasm/` with only `globalSetup.ts`+`package.json` and zero tests against the binding's core async API. The wasm codegen now reuses the typescript renderer (parameterised by `lang = "wasm"`) and emits a `setup.ts` chdir to `test_documents/` when any active fixture takes a `file_path` or `bytes` arg, plus `globalSetup.ts` only when at least one HTTP fixture is in scope. Function-call resolution now goes through `[e2e.calls.<name>.overrides.wasm]` (auto-camelCasing the rust snake_case name when no explicit override is set), so wasm-specific renames work the same way they do for node.

### Changed

- chore(codegen): `resolve_node_function_name` (in `alef-e2e::codegen::typescript`) now takes a `lang: &str` parameter so the wasm codegen can reuse it. Existing call sites unaffected — typescript codegen passes `"node"` and behaves identically.

## [0.13.3] - 2026-05-01

### Fixed

- fix(backend/php): always emit `#![cfg_attr(windows, feature(abi_vectorcall))]`
  in the generated PHP binding lib.rs. ext-php-rs requires the `vectorcall`
  calling convention for PHP entry points on Windows, but the generator
  previously only emitted the cfg_attr when `[php].feature_gate` was set.
  Without it, downstream crates fail with E0658 ("the extern \"vectorcall\"
  ABI is experimental"). The cfg_attr is a no-op on non-windows so it costs
  nothing on Linux/macOS builds.

## [0.13.2] - 2026-05-01

### Fixed

- fix(publish/php): use `[php].extension_name` (composer.json's `php-ext.extension-name`)
  for PIE archive filenames and inner `.so`/`.dll` name, not the binding crate name. PIE
  installs binaries by extension-name, so `package_php` previously emitted
  `php_{crate}_php-...` archives that PIE couldn't find. Cargo's compiled artifact filename
  (still based on the crate name with `-php` suffix) is now resolved separately and renamed
  to `{ext_name}.so` / `{ext_name}.dll` inside the archive.

- fix(backend/ffi): honor `[ffi] exclude_types` in generated lib.rs. Previously the `FfiConfig::exclude_types`
  field was parsed from `alef.toml` but never applied during code generation — all types in the API surface
  were emitted regardless. The FFI backend now filters `api.types` through `ffi_exclude_types` before emitting
  struct wrappers, field accessors, and method wrappers, matching the behavior of the elixir and wasm backends.

## [0.13.1] - 2026-05-01

### Fixed

- fix(cli/e2e): `alef e2e generate` now sweeps orphan alef-generated files left under the e2e (and `--registry` test_apps) output root when a category or fixture leaves the generation set (e.g. all fixtures in a category resolve to skipped for the active binding surface, or wasm fixture-filtering drops a file). Previously these files lingered on disk with stale `alef:hash:` headers and `alef verify --exit-code` reported them as stale forever, blocking pre-commit hooks even after a clean regen. The sweep only deletes files whose first 10 lines contain the `auto-generated by alef` marker, so user-owned fixtures, scaffolded manifests, and lockfiles are never touched. Dependency directories (`target`, `node_modules`, `_build`, `vendor`, `deps`, `.venv`, `build`, `dist`, `.git`, `Pods`) are skipped during the walk.

## [0.13.0] - 2026-05-01

### Changed (BREAKING)

- feat(publish/php): `alef publish package --lang php` now produces PIE-conventional archives.
  Filename: `php_{ext}-{ver}_php{phpVer}-{arch}-{os}-{libc}-{ts}.tgz` (Unix) /
  `php_{ext}-{ver}-{phpVer}-{ts}-{compiler}-{arch}.zip` (Windows).
  Archive contains only `{ext_name}.so`/`.dll` at the archive root — no `composer.json`,
  `pie.json`, `INSTALL.md`, or `ext/` subdirectory. Requires new flags:
  `--php-version` (required), `--php-ts` (default `nts`), `--php-libc` (auto-detected),
  `--windows-compiler` (required on Windows targets). A SHA-256 sidecar
  `{archive}.sha256` is always written alongside the archive.

### Added

- feat(template-versions): `ENVOY_VERSION_RANGE` for the `envoy` Hex package — used by gleam e2e tests to read `MOCK_SERVER_URL` since neither `gleam_stdlib` nor `gleam_erlang` expose env-var access.
- feat(e2e/gleam): inject `gleam_http` as a direct dep and emit `import gleam/result` so generated tests can compile under gleam_stdlib >= 1.0.

### Fixed

- fix(codegen/conversions): preserve `Some(...)` wrap when the binding optionalizes a non-optional core field of type `Cow<'_, str>` (e.g. NAPI `optionalize_defaults`); previously the Cow `to_string()` branch dropped the upstream `Some(...)` wrap, producing `String` where `Option<String>` was required and breaking compile of NAPI bindings whose core type holds `Cow<'static, str>` (`ArchiveMetadata.format`, `ExtractionResult.mime_type`)
- fix(e2e/wasm): drop non-HTTP fixtures, fixtures with a literal `Content-Length` header, and fixtures using TRACE/CONNECT before code generation; node `fetch` (undici) rejects pre-set Content-Length and disallows TRACE/CONNECT, and empty `describe(...)` blocks fail vitest

- fix(e2e/elixir): emit a `@tag :skip` ExUnit test for non-HTTP non-`mock_response` fixtures (AsyncAPI, WebSocket, OpenRPC schema-only) so generated suites compile without referencing a non-existent binding `handle_request`/`handle_request_async` callable.
- fix(e2e/elixir): drop `content-encoding` and `connection` from generated header assertions — Req auto-decompresses gzip/brotli responses and strips the encoding header, and `connection` is a hop-by-hop header stripped by the response pipeline.
- fix(e2e/gleam): import `gleam/httpc` (not the package name `gleam_httpc`) and call `httpc.send/1`; the previous import path produced "unknown module" errors at compile time.
- fix(e2e/gleam): replace `gleam/os.get_env` (removed from gleam_stdlib >= 1.0) with `envoy.get/1` for `MOCK_SERVER_URL` lookup.
- fix(e2e/gleam): annotate the `list.find` predicate with `fn(h: #(String, String))` so the gleam type checker can resolve tuple field access on `resp.headers`.
- fix(e2e/gleam): use `result.is_ok` (not `option.is_some`) for header-presence assertions — `list.find` returns `Result(a, Nil)`, not an Option.
- fix(e2e/gleam): strip leading underscores/digits from HTTP test function names so numeric-prefixed fixture IDs (`13_json_with_charset_utf16`) produce valid gleam identifiers.
- fix(e2e/gleam): always emit the `e2e_gleam_test.gleam` entry module so `gleam test` discovers the per-category test files.
- fix(e2e/gleam): drop WebSocket-upgrade fixtures (request advertises `Upgrade: websocket`) — gleam_httpc cannot follow HTTP/1.1 protocol upgrades and errors with `ResponseTimeout`.
- fix(e2e/wasm): drop non-HTTP fixtures, fixtures with a pre-set `Content-Length` header, and fixtures using `TRACE`/`CONNECT` methods — node's undici fetch rejects the first two and refuses to dispatch the latter.

## [0.12.16] - 2026-05-01

### Added

- feat(backend-csharp,core): support `[languages.csharp.exclude_functions]` to skip generating selected free functions from the C# binding surface
- feat(backend-java): emit `JsonInclude(NON_NULL)` on optional fields and `JsonInclude(NON_ABSENT)` at the class level so Jackson omits null/empty optionals when serializing Java DTOs across the FFI boundary
- feat(backend-java): wrap optional FFI parameters as `TypeRef::Optional` for null-safety; non-zero integer defaults emit a compact constructor that initializes them when callers pass null

### Fixed

- fix(backend-magnus): use variadic arity (-1) with `scan_args` for free functions that have optional or promoted parameters; previously such functions were registered with a fixed arity equal to the total parameter count, causing Ruby callers that omit trailing optional arguments to get an argument count error
- fix(e2e/ruby): emit `nil` placeholder for skipped optional positional args when a later arg is present; previously omitting an optional String arg before a json_object arg produced a 2-arg call where the config object landed in the string slot, causing a `TypeError: no implicit conversion` runtime error
- fix(e2e/elixir): add kreuzberg path dep and rustler direct dep to generated mix.exs so NIF force-build works in e2e tests
- fix(e2e/elixir): replace `map_or(false, ...)` with `is_some_and(...)` to clear `clippy::unnecessary_map_or`
- fix(e2e/java): set `workingDirectory` in the generated maven-surefire plugin block so tests run from the project basedir, fixing relative fixture path resolution
- fix(backend-extendr): derive R wrapper output path as packages/r/R/ instead of packages/r/src/rust/src/
- fix(backend-pyo3): emit `pass` body for empty structs that pass through the codegen; combined with the v0.12.15 fix this allows empty structs to be referenced in tagged-union aliases without import-time `NameError`
- fix(backend-ffi): preserve numeric error code metadata on the FFI error type through the type generators so host-side error conversions can attach `code`/`message` consistently
- fix(scaffold/r): include `<R_ext/Visibility.h>` in generated entrypoint.c to define `attribute_visible`
- fix(scaffold/php): align scaffolded PHP package metadata and dev tooling with current backend expectations
- fix(cli/extract): drop debug `eprintln` from the extract pipeline so generation output is no longer polluted on stderr
- fix(e2e/go): include `mock_response` in unit-test fixture builder so `test_go_method_name_uses_go_casing` exercises the real codegen path instead of the non-HTTP skip stub
- fix(e2e/rust): include `mock_response` in unit-test fixture builders so `rust_call_overrides` regression tests exercise the real codegen path
- fix(scaffold): assert on `config.ext_dir = 'native'` (the current `rb_sys/mkmf` API) instead of the deprecated `cargo_manifest` form

## [0.12.15] - 2026-05-01

### Fixed

- fix(backend-pyo3): emit empty Python dataclasses for empty Rust structs (e.g. `pub struct ExcelMetadata {}`) when they have `Default`. Previously, the codegen skipped them entirely, leaving dangling references in tagged-union type aliases (`FormatMetadata = ... | ExcelMetadata | ...`) and breaking module import with `NameError: name 'ExcelMetadata' is not defined`.
- fix(e2e/escape): escape ASCII control characters (`\x00`–`\x1f`) in Python string literals using `\xHH` so generated Python source remains valid. Previously, fixtures with embedded NUL bytes (e.g. `clean_text_basic`) produced source files Python's parser rejected with `SyntaxError: source code string cannot contain null bytes`.
- fix(e2e/csharp): use PascalCase HTTP method names for `System.Net.Http.HttpMethod` static properties; route header assertions to `.Headers` vs `.Content.Headers` correctly to avoid "Misused header name" runtime errors; uniquify per-assertion variable names so `out var` does not redeclare in method scope.
- fix(e2e/go): clarify the non-HTTP stub message and document why it fires (Go bindings without a callable matching `[e2e.call].function`).
- fix(e2e/go): emit `os` import when any HTTP fixture is present (HTTP tests read `MOCK_SERVER_URL` via `os.Getenv`); previously only mock_url args triggered the import.
- fix(e2e/go): only emit `io` import when at least one HTTP fixture has a body assertion; eliminates `"io" imported and not used` build errors in test files where no fixture asserts on the body.
- fix(e2e/go): only declare `bodyBytes` when the fixture has a body assertion; eliminates `declared and not used: bodyBytes` build errors.
- fix(e2e/go): use a separate `want :=` declaration plus `%q` formatting in the body-mismatch `t.Fatalf` instead of interpolating the (already-Go-quoted) literal into the format string; previously emitted unparsable Go for fixtures whose expected body contained double-quotes (e.g. JSON-as-string responses) and for headers whose names/values contained backticks.
- fix(e2e/go): decode HTTP response and expected bodies into `any` (not `map[string]any`) so JSON arrays in the expected body decode without `cannot unmarshal array into ... map` runtime errors.

## [0.12.14] - 2026-05-01

### Fixed

- fix(e2e/rust): translate synthetic assertion fields (`embeddings`, `embedding_dimensions`, `embeddings_valid`, `embeddings_finite`, `embeddings_non_zero`, `embeddings_normalized`, `chunks_have_embeddings`, `keywords`, `keywords_count`) to correct Rust expressions instead of treating them as struct field accesses; `keywords`/`keywords_count` map to `extracted_keywords` on `ExtractionResult`.
- fix(e2e/python): intercept synthetic embedding and keyword assertion fields before struct field emission; skip `keywords`/`keywords_count` (no Python binding for `extracted_keywords`).
- fix(e2e/typescript): intercept synthetic embedding and keyword assertion fields; emit Vitest `expect` assertions using array methods; skip `keywords`/`keywords_count`.
- fix(e2e/go): intercept synthetic embedding and keyword assertion fields; emit inline immediately-invoked functions for predicate checks on `*[][]float32`; skip `keywords`/`keywords_count`.
- fix(e2e/ruby): intercept synthetic embedding and keyword assertion fields in the binding assertion path; emit RSpec `expect` assertions using Ruby enumerable methods; skip `keywords`/`keywords_count`.
- fix(e2e/php): intercept synthetic embedding and keyword assertion fields; emit PHPUnit assertions using `array_reduce`/`array_filter`; skip `keywords`/`keywords_count`.
- fix(e2e/csharp): intercept synthetic embedding and keyword assertion fields; emit xUnit assertions using LINQ; skip `keywords`/`keywords_count`.
- fix(e2e/elixir): intercept synthetic embedding and keyword assertion fields; emit ExUnit assertions using `Enum` functions; skip `keywords`/`keywords_count`.
- fix(e2e/r): intercept synthetic embedding and keyword assertion fields; emit testthat `expect_*` assertions using `sapply`/`all`; skip `keywords`/`keywords_count`.
- fix(e2e/wasm): intercept synthetic embedding and keyword assertion fields (defensive, embed is WASM-skipped); skip `keywords`/`keywords_count`.
- fix(e2e/ruby): use title-cased `Net::HTTP` request class names (`Delete`, `Head`, `Patch`, `Put`) instead of all-caps variants that don't exist in Ruby's stdlib; skip `content-encoding` header assertions; handle plain-string and empty bodies correctly; skip HTTP 101 WebSocket upgrade tests; emit `skip` stubs for non-HTTP fixtures (WebSocket, SSE) that cannot be tested via Net::HTTP; avoid `JSON.parse(nil)` on 204 No Content responses; deduplicate `Content-Type` header when body is already set.
- fix(backend-magnus): use `.to_vec()` instead of `.into()` when wrapping `&Bytes` return values from non-opaque struct methods; `Vec<u8>: From<&Bytes>` is not implemented so `.into()` failed to compile for methods like `as_bytes()` on `UploadFile`.
- fix(e2e/gleam): filter out HTTP-type fixtures and call-based fixtures without a Gleam-specific override; emit a `src/` placeholder module (required by the Gleam toolchain) and a smoke test when no fixture-driven tests are generated; strip leading digits from test function names.
- fix(e2e/kotlin): generate proper `java.net.http.HttpClient` HTTP tests for all 559 HTTP fixtures; hit `MOCK_SERVER_URL/fixtures/<id>` per the mock server protocol; URL-encode query params at runtime to handle JSON array values; escape `$` in string literals to prevent Kotlin string interpolation; skip Java's restricted headers (`Connection`, `Content-Length`, `Expect`, `Host`, `Upgrade`); skip `content-encoding` header assertions (mock server doesn't compress); skip 101 status tests (Java HttpClient doesn't handle protocol upgrades); emit `assumeTrue(false)` stubs for non-HTTP fixtures without a Kotlin-specific call override; add `kotlin("test")` to build.gradle.kts dependencies.
- fix(escape): add `escape_kotlin` function that escapes `$` in addition to standard Java escapes to prevent Kotlin string interpolation on embedded values.

## [0.12.13] - 2026-05-01

### Fixed

- fix(extract): `validate_call_export` now accepts method-on-type references (e.g., `function = "chat"` where `chat` is a method on a public type). Previously rejected legitimate method-style entry points used by `liter-llm` and similar libraries.

## [0.12.12] - 2026-05-01

### Fixed

- fix(codegen/binding_helpers): apply `k.into()` to Map keys in `gen_lossy_binding_to_core_fields` so the binding-side `is_empty()` helper round-trips wrapped string keys (`Cow`, `Box<str>`, `Arc<str>`) correctly. Previously the helper emitted `(k, …)` directly, which broke when the core key type was `AHashMap<Cow<'static, str>, Value>`.
- fix(backend-wasm): also deserialize `Vec<Vec<T>>` parameters in the `can_delegate` branch via `serde_wasm_bindgen::from_value`. Previously only the Result-returning serde-recovery branch handled this, so non-Result functions like `generate_cache_key` received the raw `JsValue`.
- fix(codegen/binding_to_core): emit `k.into()` for non-Json keys in the generic `Map` arm, completing the Cow-key round-trip fix.

### Changed

- feat(e2e/go): emit `t.Skip("TODO: ...")` stubs for all fixtures that lack a `mock_response`; omit the package import when no test uses it to avoid the Go "imported and not used" compile error.
- feat(e2e/java): emit `Assumptions.assumeTrue(false, "TODO: ...")` stubs for all fixtures that lack a `mock_response` so the test class compiles and is skipped cleanly.
- feat(e2e/csharp): emit `[Fact(Skip = "TODO: ...")]` stubs for all fixtures that lack a `mock_response` so the test class compiles and all tests are reported as skipped.
- fix(e2e/php): skip JSON decode and body assertions when `expected_response.body` is the empty-string sentinel, avoiding `JsonException` on empty bodies.
- fix(e2e/php): skip `markTestSkipped` for non-HTTP fixtures with no assertions; add `allow_redirects => false` to Guzzle client; skip HTTP 101 WebSocket upgrade tests; handle plain-string response bodies with raw `(string)$response->getBody()` comparison; suppress redundant `validation_errors` assertions when a full `body` assertEquals is generated; skip `content-encoding` header assertions since the mock server always returns uncompressed bodies.
- fix(e2e/rust): handle `<<absent>>` sentinel by omitting that header from mock responses; handle `<<uuid>>` sentinel by generating a real UUID v4 value; use `text/plain` default content-type when body is not JSON.
- fix(e2e/python): skip content-encoding assertions; handle plain-string and empty/null bodies without json.loads; skip HTTP 101 tests; disable redirect following; fix validation errors field name ("errors" not "detail").
- fix(e2e/typescript): skip content-encoding assertions; handle plain-string and empty/null bodies; skip HTTP 101 tests; add redirect: 'manual'; suppress import when no-assertion non-HTTP fixtures; emit it.skip stubs; fix validation errors field name.

## [0.12.11] - 2026-05-01

### Changed

- feat(e2e/swift): emit test files into `packages/swift/Tests/<Module>Tests/` instead of a standalone `e2e/swift` package. SwiftPM 6.0 forbids local `.package(path:)` references between packages in the same git repository; placing the generated tests inside the existing library package sidesteps the restriction while retaining a skeletal `e2e/swift/Package.swift` for CI reference.
- feat(e2e/zig): use `std.heap.DebugAllocator(.{}) = .init` (Zig 0.16+); emit compilable stub test bodies (`_ = testing;`) for fixtures with no assertions instead of calling a non-existent `handle_request` function; omit allocator setup when no setup lines are needed.
- feat(scaffold/swift): upgrade generated `Package.swift` to `swift-tools-version: 6.0`; remove `unsafeFlags` from `RustBridge` target's `linkerSettings` so the package can be used as a SwiftPM dependency.

### Fixed

- fix(e2e/rust/tests): pass the new `needs_http_tests` flag through `cargo_toml_generation.rs` test fixtures so the suite compiles after the `render_cargo_toml` signature was extended.
- fix(codegen/conversions): emit `k.to_string()` for Map keys in core→binding conversion and `k.into()` for the reverse direction so `Cow<'_, str>`/`Box<str>`/`Arc<str>` keys (which the type resolver normalizes to `TypeRef::String`) round-trip correctly. Without this fix the generated `From` impls produced `(Cow<'_, str>, …)` iterators feeding `HashMap<String, String>::from_iter`, breaking pyo3/napi/php bindings on `Metadata.additional`.
- fix(e2e/elixir): add missing commas between deps entries in generated `mix.exs`; without them Elixir emits a syntax error before deps like `{:req, ...}`.
- fix(e2e/kotlin): fall back to `alef_config.resolved_version()` when `[e2e.packages.kotlin]` has no explicit `version`, avoiding a stale `0.1.0` jar reference in `build.gradle.kts`.
- fix(e2e/gleam): strip leading underscores from generated test-function names; fixture IDs with numeric prefixes (e.g. `13_json_...`) produced `_json_..._test()` which Gleam rejects.
- fix(backend-dart): emit `lib/<pkg>.dart` barrel file re-exporting `src/<pkg>.dart` so `package:<pkg>/<pkg>.dart` imports resolve correctly in e2e tests.
- fix(e2e/elixir): normalize scientific-notation floats for Elixir syntax (`1e-10` → `1.0e-10`, strip `e+` → `e`); use `response.body` directly instead of `Jason.decode!` since Req auto-decodes JSON; fall back to `Req.request(method: :METHOD, ...)` for non-standard HTTP verbs like OPTIONS/TRACE; extract first value from Req's `[{name, [values]}]` header lists; preserve original JSON map keys instead of snake_casing them.
- fix(e2e/rust): skip `content-encoding` headers in mock server responses — server returns uncompressed bodies so forwarding a `content-encoding` header would cause clients to attempt decompression and fail; also skip setting default `content-type: application/json` when the fixture already specifies one.
- fix(extract/type_resolver): treat bare `Value` as `TypeRef::Json` to match the idiomatic `use serde_json::Value;` re-import. Without this, `HashMap<String, Value>` fields (e.g. `ProblemDetails.extensions`) sanitized to `Map<String, String>` and downstream From-impls emitted `.into_iter().collect()` between mismatched value types, breaking pyo3/napi bindings. `serde_json::Value` (full path) was already handled.

## [0.12.10] - 2026-05-01

### Added

- feat(backend-wasm/config): add `custom_rust_modules`, `exclude_fields`, and `source_crate_remaps` options for hand-written Rust modules, per-type field exclusions for `cfg(not(target_arch = "wasm32"))`-gated source fields, and rewriting `<original_crate>::TypeName` references to `<override_crate>::TypeName` when `core_crate_override` is set.
- feat(codegen/conversions): introduce `core_type_path_remapped` / `apply_crate_remaps` so generated `From` impls reference the override crate when `source_crate_remaps` is configured, avoiding orphan-rule violations across re-export facades.

### Fixed

- fix(extract): preserve `Map<K, V>` structure during sanitization. Previously a `Map<Cow<'static, str>, serde_json::Value>` field was flattened to `TypeRef::String` whenever the key resolved through a sanitized `Named` type; the field now stays a Map so binding backends emit the correct iterator-based conversion (or `serde_wasm_bindgen::to_value` for WASM) instead of `format!("{:?}", val.<field>)`. Fixes `Metadata.additional` mismatches in pyo3, napi, php, and wasm bindings.
- fix(backend-napi): widen nested `Vec<Vec<primitive>>` element-wise in return conversion when the binding declares the wider type (`f32` → `f64`, `u64`/`usize`/`isize` → `i64`). Mirrors the existing single-Vec arm so `embed_texts` and similar functions returning `Vec<Vec<f32>>` compile.
- fix(backend-php): widen nested `Vec<Vec<u64/usize/isize>>` element-wise in return conversion to match the i64 cast emitted for single-Vec return types.
- fix(backend-php): `gen_stub_return` now respects the function's actual error variance — non-Result functions get a type-appropriate default (e.g. `String::new()`, `Vec::new()`, `None`) instead of an `Err(PhpException::default(...))` body that violates the function signature.
- fix(backend-wasm): emit `serde_wasm_bindgen::from_value` deserialization for `Vec<Vec<T>>` parameters that arrive as `JsValue`. Without it, `generate_cache_key` and similar functions taking nested-vec composites passed the raw `JsValue` into the core fn.
- fix(core/version): add `to_r_version()` converting SemVer prereleases to CRAN-compatible four-component form.
- fix(scaffold/r): generate `packages/r/src/Makevars`, `Makevars.in`, `Makevars.win.in`, and `src/entrypoint.c`.
- fix(scaffold/r): change scaffolded `Cargo.toml` crate-type from `["cdylib"]` to `["staticlib", "lib"]`.
- fix(scaffold/r): generate `packages/r/NAMESPACE` with `useDynLib` bootstrap.
- fix(scaffold/r): use `to_r_version()` in `DESCRIPTION` `Version:` field.
- fix(backend-extendr): `bridge_imports()` returns bare paths, not full `use` statements.
- fix(cli/sync-versions): write CRAN-compatible version to `packages/r/DESCRIPTION`.
- fix(cli/validate-versions): compare R DESCRIPTION against CRAN-compatible version.

## [0.12.9] - 2026-04-30

### Fixed

- fix(e2e/go): add `returns_void` field to `CallConfig` so functions returning only `error` (e.g. `validate_host`, `validate_port`, `validate_language_code`) emit `err := func()` instead of `_, err := func()` in generated Go tests.
- fix(e2e/go): Go override `returns_result = true` now respected for `dedup_text` and similar functions returning `(value, error)`; previously the call-level default (`false`) overrode the per-language override.

## [0.12.8] - 2026-04-30

### Fixed

- fix(backend-dart): convert nested `Vec<Vec<f32>>` to `Vec<Vec<f64>>` in bridge so embed_texts return type matches binding declaration.

## [0.12.7] - 2026-04-30

### Fixed

- fix(backend-rustler): `force_build` honors `Application.compile_env(:rustler_precompiled, [:force_build, :<app>], false)` in addition to env var + Mix.env() heuristics, so consumers can opt into source builds via mix config.
- fix(extract): empty struct with `#[derive(Default, Serialize, Deserialize)]` is treated as a transparent NifMap data type rather than an opaque resource handle (e.g. `ExcelMetadata{}` is data, not a `ResourceArc`).
- fix(scaffold/precommit): generated `.pre-commit-config.yaml` includes the shared `kreuzberg-dev/pre-commit-hooks` repo (shfmt/shellcheck/hadolint/textlint), tracked via the new `KREUZBERG_PRECOMMIT_HOOKS_REV` template version.
- fix(e2e/java): per-fixture `options_type` overrides now collected for ObjectMapper imports/usage; previously only class-level options_type was honored, leaving overrides un-imported.
- fix(backend-magnus): optional Ruby parameters typed as `Option<magnus::Value>` (not `magnus::Value`) — handler now matches `Some(_v) if !_v.is_nil()` before calling `.funcall(...)`, fixing a compile error when an optional struct arg is omitted.
- fix(backend-pyo3): union alias types used directly in `options.py` runtime expressions (e.g. `FormatMetadata = str | ExcelMetadata | ...`) are now imported unconditionally instead of under `TYPE_CHECKING`. Because union alias RHS is evaluated at module load, all names must resolve at import time; previously the TYPE_CHECKING guard caused `NameError` for data-enum payload struct types not defined locally.
- fix(codegen/core-to-binding): `Map<String, Json>` fields now emit `.to_string()` per-value conversions (same as `Map<String, String>`). Previously the map-value branch only matched `TypeRef::String`, so `serde_json::Value` values were not converted, causing a type mismatch in NAPI-RS and other backends.
- fix(e2e/go): `go.mod` now includes `github.com/stretchr/testify` as a required dependency so generated Go test modules compile without a missing-import error.
- fix(e2e/go): `bytes`-type args now decode from base64 at runtime via `base64.StdEncoding.DecodeString`; the `encoding/base64` import is emitted only when needed.
- fix(e2e/go): optional `string` args are now passed as `*string` (address of a local) matching Go binding signatures that take pointer-to-string for nullable strings.
- fix(e2e/go): functions that return only `error` (no result value) now emit `err := fn(...)` instead of `_, err := fn(...)`, fixing a compile error when the function signature is `func(...) error`.
- fix(e2e/go): `result_is_simple` functions (returning `*string`, `*bool`, etc.) now dereference the pointer into a `value` local before assertions and emit a nil guard, rather than asserting directly on a pointer type.
- fix(e2e/go): `greater_than` assertions on optional (pointer) fields now emit a nil guard before the comparison, preventing a nil-pointer dereference.
- fix(e2e/go): `contains` assertions on optional array fields now use `strings.Join(*field_expr, " ")` instead of incorrectly dereferencing an array pointer as a string.
- fix(e2e/go): `contains_any` is now recognized as a string-assertion type that requires the `strings` import.
- fix(e2e/typescript): `package.json` local dependency now uses `file:<pkg_path>` instead of `workspace:*`, fixing resolution when the e2e package is not inside a pnpm workspace.
- fix(e2e/typescript): options-type imports now collect all distinct types across all per-fixture call overrides, not only the default call's options type; each type is imported exactly once.
- fix(e2e/typescript): `resolve_node_function_name` now converts snake_case function names to camelCase (NAPI-RS convention) when no explicit node override is set, eliminating `undefined is not a function` errors at runtime.
- fix(e2e/typescript): optional config args with no fixture value now emit `{} as unknown as <OptionsType>` instead of being skipped, matching NAPI-RS binding signatures that require the config parameter.
- fix(e2e/typescript): config `json_object` args are now cast via `as unknown as <T>` to suppress TS2352 type-overlap errors for partial config objects.
- fix(e2e/typescript): `bytes`-type args now emit `Buffer.from("<b64>", "base64")` so base64 fixture values are decoded to `Uint8Array` at test time.
- fix(e2e/typescript): `not_empty` assertions now handle array fields with `.length` checks and non-string struct fields with `!= null` presence checks, rather than emitting `.length` on every type unconditionally.
- fix(e2e/core): `CallOverride` gains an `arg_order` field to allow per-language parameter reordering when a binding's function signature differs from the canonical `args` list order.
- fix(e2e/escape): `escape_python` now escapes NUL (`\x00`) and other ASCII control characters (U+0001–U+001F) as `\xNN` sequences, preventing malformed Python string literals from non-printable fixture bytes.

## [0.12.6] - 2026-04-30

### Fixed

- fix(e2e/rust): `Option<Vec<T>>` fields in `fields_array` now emit `as_deref().unwrap_or(&[])` instead of `unwrap_or("")` in the unwrap binding. Previously, `as_deref()` on `Option<Vec<T>>` yields `Option<&[T]>`, so the `&str` fallback produced an `E0308` type mismatch (e.g. `detected_languages`, `metadata.sheet_names`).
- fix(e2e/rust): `greater_than_or_equal` assertions on optional non-array fields (e.g. `Option<usize>`) now emit `.unwrap_or(0) >= N` instead of a bare `>= N` comparison. Comparing `Option<usize> >= 2` directly is a type error (e.g. `metadata.sheet_count >= 2`).
- fix(e2e/rust): `equals` assertions with string values on optional string fields that were not pre-unwrapped (e.g. inside a `result_is_vec` loop) now emit `.as_deref().unwrap_or("").trim()` instead of `.trim()` directly on `Option<String>`, fixing `E0599` no method `trim` on `Option<T>` (e.g. `metadata.output_format`).
- fix(e2e/rust): `field: "result"` used as a sentinel to refer to the whole return value no longer emits `result.result`. When the field path exactly matches the result variable name, the codegen now references the result variable directly.
- fix(e2e/c): `result_type_name` fallback now derives from the base `call.function` name instead of the C-overridden function name. When `[e2e.call.overrides.c]` sets `function = "htm_convert"` and `prefix = "htm"`, the fallback type was `HtmConvert`, which combined with the `HTM` prefix produced `HTMHtmConvert` (doubled prefix). It now correctly produces `HTMConvert`.
- fix(backend-go): `TypeRef::Bytes`-returning methods and functions no longer emit `defer C.{prefix}_free_string(ptr)`. Bytes pointers alias internal FFI storage and must not be freed by the caller; freeing via `_free_string` (which expects `*C.char`) would corrupt the parent handle and caused cgo type errors. The `unmarshalBytes` helper copies the data without freeing the source pointer.
- fix(backend-php): PHP type stub methods with non-void return types now emit `throw new \RuntimeException('Not implemented.');` instead of an empty `{ }` body. Empty bodies are invalid for non-void methods under PHPStan level 9 (`implicitlyImpureMethod` / missing return), which broke `composer check` on downstream repos (e.g. `getErrorCode(): int`, getter methods, and `*Api` static stubs).

## [0.12.5] - 2026-04-30

### Added

- **Per-language `core_import` derivation.** When `[wasm|dart|swift].core_crate_override` is set, the wasm backend now also routes generated Rust `use` paths and `From`/`Into` impls through the override (e.g. `spikard_core::ProblemDetails` instead of `spikard::ProblemDetails`). Without this, the override only flipped the Cargo dep key but left source paths pointing at the umbrella crate, producing E0433 unresolved-crate errors at compile time. New helper: `AlefConfig::core_import_for_language(lang)` in `src/core/config/mod.rs`. Defaults to `core_import()` when no override is set, so existing configs are unaffected.
- **Wasm backend respects `exclude_types` for error converters.** `gen_wasm_error_converter` calls and the WASM exports list now skip any error whose `name` is listed in `[wasm].exclude_types`, mirroring the existing exclusion behavior for structs/enums/functions. Lets a wasm-safe surface (e.g. schema validation only) drop sibling-crate errors like `GraphQLError` or `SchemaError` without the binding referencing the unlinked crate.

## [0.12.4] - 2026-04-30

### Fixed

- fix(backend-php): inherent-method delegation helper (`gen_php_lossy_binding_to_core_fields`) now applies per-value `.into()` and preserves the Option layer for `Map<K, Named>` and `Option<Map<K, Named>>` fields. Same bug class as 0.12.3 fix in `alef-codegen::binding_helpers`, but PHP has its own helper that needed the same patch.

## [0.12.3] - 2026-04-30

### Fixed

- fix(codegen/binding-helpers): `gen_lossy_binding_to_core_fields` now applies per-value `.into()` for `Map<K, Named>` and `Option<Map<K, Named>>` fields. The 0.12.2 round only patched `field_conversion_to_core` (used by the `From<Binding> for Core` impl), but the inherent-method delegation path (used for `with_chunking`, `all`, `minimal`-style wrappers on PyO3/PHP/NAPI/Magnus/Rustler) is a parallel codegen path that constructs `core_self` directly. Both paths now emit per-value `(k, v.into())` so a binding-wrapper Map value converts to the core type.

## [0.12.2] - 2026-04-30

### Fixed

- fix(e2e/rust): `not_empty` / `is_empty` assertions on a field that is BOTH `fields_optional` AND `fields_array` now emit `as_ref().is_some_and(|v| !v.is_empty())` and `as_ref().is_none_or(|v| v.is_empty())` respectively, so `Option<Vec<T>>` struct fields (e.g. `chunks`, `images`, `pages`, `extracted_keywords`) compile against the assertion. Previously the codegen emitted `result.<field>.is_empty()` against the bare `Option<Vec<_>>`, which failed `E0599`.
- fix(e2e/rust): when a call returns `Vec<T>` (`result_is_vec` rust override), the call-site optional-string unwrap pass is skipped so per-element iteration emitted by `render_assertion` is the only place fields are accessed; previously the call-site emitted `let metadata_x = result.metadata.x.as_deref().unwrap_or("")` against `Vec<ExtractionResult>` (no `metadata` field on the Vec).
- fix(e2e/rust): non-optional `string` arguments are no longer prefixed with `&` in the call expression. The fixture binding is already a `&'static str` literal; adding another reference produced `&&str`, mismatching `&str` parameter slots (e.g. `extract_email_content(data, mime_type, ...)`). For `impl AsRef<Path>` parameters both forms still satisfy the bound thanks to `&&str: AsRef<Path>`.
- fix(e2e/rust): `not_empty` on a bare `Option<T>` result (no field) now emits `result.is_some()` when the call is annotated `result_is_option`, matching the existing `is_some()`/`is_none()` mapping for field-level checks.
- fix(backend-pyo3): python_field_type now resolves Named(DataEnum) types correctly per emit context (options.py dataclass field vs \_native.pyi stub) — introduces EmitContext enum and threads it through all collection branches (Map, Vec, Optional) so that in the OptionsModule context the bare name resolves to the locally defined union type alias rather than to the native module import, eliminating the mypy type mismatch between caller-supplied `dict[str, options.ExtractionPattern]` and the annotation `dict[str, _native.ExtractionPattern]`.
- fix(codegen/core-to-binding): emit explicit arms for `Map<K, Named>`, `Option<Map<K, Named>>`, `Vec<Named>`, `Option<Vec<Named>>` instead of falling through to the binding-to-core helper (was emitting wrong-direction conversions; broke every backend that uses the shared converter for high-level `Option<Map<Named>>` fields).
- fix(backend-rustler): split native.ex `force_build:` keyword across three lines so `mix format` accepts it without reformatting (was 114 chars, exceeded Elixir's 98-char default).
- fix(codegen/binding-to-core): apply per-value `.into()` when emitting `Option<Map<K, Named>>` field conversions (was dropping the wrapper conversion, causing rustc type mismatch in PyO3/PHP/Magnus/Rustler).
- fix(codegen/binding-to-core): preserve `Option` layer in optionalized-field path when field is genuinely IR-optional (was using `unwrap_or_default` and dropping `Option`, breaking NAPI `Option<Map<K, Named>>` round-trip).

### Added

- **Per-language `core_crate_override`.** `[wasm].core_crate_override`, `[dart].core_crate_override`, and `[swift].core_crate_override` let a Rust binding crate point at a sub-crate other than `[crate.name]` (e.g. a wasm-safe `spikard-core` instead of the umbrella `spikard` facade). When set, the binding's generated `Cargo.toml` depends on `<override> = { path = "../<override>" }` (wasm) or the equivalent `crates/<override>` path (dart/swift) and the override key replaces the umbrella crate as the core dep. (`src/core/config/languages.rs`, `src/core/config/mod.rs`, `src/backends/wasm/gen_bindings.rs`, `src/backends/dart/src/gen_rust_crate/cargo.rs`, `src/backends/swift/src/gen_rust_crate/`)
- **Per-language `exclude_extra_dependencies`.** `[wasm|dart|swift].exclude_extra_dependencies` filters specific keys out of the merged `[crate.extra_dependencies]` set for that language only — useful when shared sibling crates (e.g. `spikard-http`, `spikard-graphql`) cannot be linked into the wasm target. (`src/core/config/mod.rs::extra_deps_for_language`)

Both fields default to unset/empty so existing configs produce byte-identical output.

- test(backend-wasm): round-trip test for `Map<K, Named>` field through `serde_wasm_bindgen`, locks in the contract that Named values inside a Map must have symmetric serde impls (`src/backends/wasm/tests/gen_bindings_test.rs`).

### Changed

- BREAKING(cli): `alef generate` now runs language formatters by default; pass `--no-format` to skip. The previous `--format` flag is removed (was opt-in and easily forgotten, causing generated output to fail downstream linters like `mix format`). `alef all` behaves the same way — pass `--no-format` to suppress formatters.
- refactor(codegen): introduce `TypeMapper` trait in `alef-codegen`; every backend now implements it with exhaustive `TypeRef` matching. The Go, Java, C#, and FFI backends have been migrated to `GoMapper`, `JavaMapper`/`JavaBoxedMapper`, `CsharpMapper`, and `FfiParamMapper`/`FfiReturnMapper` structs respectively — all implementing `TypeMapper`. Adding a new `TypeRef` variant now produces a compile error in every backend that hasn't handled it, retiring the silent-fallthrough bug class that previously hid `AHashMap` as `Named("AHashMap")`. The dead `extendr/type_map.rs` stub (comment-only file) has been removed; `ExtendrBackend` already implemented `TypeMapper` in `gen_bindings.rs`.

## [0.12.1] - 2026-04-30

### Fixed

- **e2e mock-server respects fixture `expected_response.headers`.** The alef-generated mock HTTP server (both the test-embedded `mock_server` module and the standalone `mock-server` binary used by cross-language e2e suites) now applies fixture-declared response headers to the served response. Previously, headers from `http.expected_response.headers` (spikard schema) and `mock_response.headers` (liter-llm schema) were silently dropped, causing consumer header assertions (CORS, request-id, auth challenge, compression, etc.) to come back as `null`. `MockResponse` gained a `headers: HashMap<String, String>` field, `Fixture::as_mock_response()` bridges headers from both schemas, and the mock-server route handlers iterate the map and apply each entry via `Response::builder().header(name, value)`. Repeated `.header()` calls preserve multi-value semantics for headers like `Set-Cookie`. (`src/e2e/fixture.rs`, `src/e2e/codegen/rust.rs`)

- **go: skip method emission on opaque error types.** When a `TypeDef` is both opaque and registered as an error, `gen_go_error_struct` already emits it as a value-type struct with `Code`/`Message` fields (no `ptr`). Previously the codegen still tried to emit method bodies that dispatch through `h.ptr`, producing uncompilable Go (e.g. `GraphQLError.StatusCode` referencing `h.ptr` against a value struct). Methods on these types are now skipped. (`src/backends/go/src/gen_bindings.rs`)

### Added

- **Rust e2e call-override fields.** `[e2e.call.overrides.rust]` learns `wrap_options_in_some`, `extra_args`, `returns_result`, `result_is_vec`, and `result_is_option` to support fallible signatures whose options slot is owned `Option<T>` and which take additional trailing positional args. (`src/core/config/e2e.rs`, `src/e2e/codegen/rust.rs`)

### Reverted

- **Wasm HTTP-fixture auto-skip** (introduced in fa2d03b1 then reverted in d8ff34c7). The wasm e2e target needs a real fix — a wasm-safe dispatch entrypoint that exercises HTTP fixtures in-process — rather than skipping them.

## [0.12.0] - 2026-04-30

### Fixed

- fix(extract): recognise `AHashMap`, `IndexMap`, and `FxHashMap` as map types in the type resolver (previously fell through to `TypeRef::Named`, causing every binding backend to emit a string/opaque type instead of a real map).

### Added

- **Blocker A: HTTP fixture mock-server support for spikard-style fixtures.** The e2e generator now emits the `mock-server` binary and per-language bootstrap code for projects whose fixtures use the `http.expected_response` schema (spikard shape), not just `mock_response` (liter-llm shape). Changes:
  - `fixture.rs`: `needs_mock_server()` returns `true` for HTTP fixtures; new `as_mock_response()` bridges both schemas to a unified `MockResponse`.
  - `codegen/rust.rs`: embedded `Fixture` struct in the mock-server binary now deserializes both `mock_response` and `http.expected_response`; route-loading uses `as_mock_response()` accessor.
  - `codegen/typescript.rs`: HTTP test cases now call `fetch(MOCK_SERVER_URL/fixtures/<id>)` instead of `app.request(bare_path)`.
  - `codegen/python.rs`: conftest.py now spawns the mock-server binary and exposes `MOCK_SERVER_URL`; HTTP test functions use `urllib.request` against `/fixtures/<id>`.
  - `codegen/ruby.rs`: generates `spec/spec_helper.rb` that starts the mock-server; HTTP examples use `Net::HTTP` against `/fixtures/<id>`.
  - `codegen/php.rs`: `bootstrap.php` spawns the mock-server; HTTP tests use `MOCK_SERVER_URL` and `/fixtures/<id>`.
  - `codegen/elixir.rs`: `test_helper.exs` starts the mock-server via Port; HTTP tests use `Req` against `/fixtures/<id>`.

### Changed

- Bump templated `ext-php-rs` pin to `0.15.12` for PHP 8.5 compatibility (downstream consumers regenerate to pick up upstream's PHP 8.5 build fixes).
- **BREAKING: `[e2e.calls.X].returns_result` default flipped from `true` to `false`.** Most e2e fixture call configs target functions whose Rust signatures do not return `Result<T, E>` (e.g. `String`, `Cow<'_, str>`, `bool`, builder types). The previous default emitted `.expect("should succeed")` unconditionally, producing `no method named expect` compile errors against non-Result returns. Existing configs that genuinely call Result-returning functions (`extract_*`, `batch_extract_*`, `chunk_*`, `render_*`, `validate_*`, `detect_languages`, `blake3_hash_*`, `compute_hash`, etc.) must now set `returns_result = true` explicitly. (`src/core/config/e2e.rs`)

### Added

- **`alef_extract::validate_call_export(surface, module_path, function_name)` public API.** Returns `ExportValidation::Ok` when the function is exported at the declared path, `WrongPath` when the function exists but not at the given module path (includes all actual `rust_path`s found), or `NotFound` when the function is absent from the surface entirely. Used by `alef generate` to fail fast on C1 (function not re-exported at crate root) and C2 (wrong definition selected by dedup). (`src/extract/lib.rs`)
- **`alef_extract::return_type_fields(surface, function_name)` public API.** Returns the public fields of the struct returned by a named free function. Returns `None` for primitive, `String`, `Vec`, unit, or opaque (no-field) return types. Allows the Rust codegen to validate `result.field` assertions at generation time. (`src/extract/lib.rs`)
- **`[e2e.calls.X].args[].owned` flag.** When `true`, the Rust codegen emits the argument as an owned binding and passes it by value rather than by reference. Use for parameters whose Rust signature is `Vec<T>` (not `&Vec<T>` / `&[T]`) — for example `batch_extract_file(items: Vec<(PathBuf, Option<FileExtractionConfig>)>, config: &ExtractionConfig)`. Defaults to `false`. (`src/core/config/e2e.rs`)
- **`[e2e.calls.X].args[].element_type` field.** For `json_object` args whose Rust target is `&[T]`, set to the element type literal (`"String"`, `"f32"`, etc.) so the codegen emits `let name: Vec<element_type> = serde_json::from_value(...).unwrap();`. Without this annotation `serde_json::from_value` cannot infer the unsized slice type and the generated test fails to compile with E0277. (`src/core/config/e2e.rs`)

### Fixed

- **Go backend: duplicate sentinel error declarations across multiple `ErrorDef`s.** When two error enums in the same crate shared variant names (e.g. `GraphQLError::ValidationError` and `SchemaError::ValidationError`), the Go binding emitted two top-level `var (...)` blocks each declaring `ErrValidationError`, breaking compilation with `redeclared in this block`. The Go backend now emits a single consolidated sentinel block; colliding variant names are disambiguated by qualifying with the parent error's stripped base name (`ErrGraphQLValidationError` and `ErrSchemaValidationError`). Unique variant names continue to use the unqualified `Err{Variant}` form. New public APIs `alef_codegen::error_gen::gen_go_sentinel_errors` and `gen_go_error_struct` allow callers to control sentinel/struct emission independently. (`src/codegen/error_gen.rs`, `src/backends/go/src/gen_bindings.rs`)

- **Magnus backend: typed-options params now accept `magnus::Value`.** Functions/methods with `Option<Named>` (or `Named`) parameters previously generated a `Option<String>` ABI that forced Ruby callers to `Hash#to_json` explicitly — and any failure to do so raised `TypeError`. The binding now accepts `magnus::Value` and calls `to_json` internally before `serde_json` deserialization, so a plain Ruby Hash works directly. Closes the upstream regression in `kreuzberg-dev/html-to-markdown#334`.

- **Phase 1: Rust e2e codegen A1/A3/A4/A5 fixes** — Eliminate `E0308 expected &T found &Option<_>`, `E0308 expected Vec<T> found &_`, and `E0277 trait bound` errors via correct optional handling, owned-param passing, slice-type annotation, and simple-return-type detection.
  - **A1:** No longer wraps optional `json_object` args in `Some(...)`; desers as `T` directly, passes `&T`. (`src/e2e/codegen/rust.rs`)
  - **A3:** Respects `owned = true`, passes by value instead of reference. (`src/e2e/codegen/rust.rs`)
  - **A4:** Emits `Vec<element_type>` annotation for slice args when `element_type` is set. (`src/e2e/codegen/rust.rs`)
  - **A5 (partial):** `result_is_simple = true` in call overrides redirects field-access assertions to the result variable directly. (`src/e2e/codegen/rust.rs`)

- **Phase 2: Per-language e2e codegen fixes** — Parallel fixes across Python, TypeScript, Go, Java, C#, Ruby, PHP, Elixir, and other languages to match Rust A1-A5 patterns.
  - **A1 (Python):** No longer wraps optional `json_object` args; passes values directly. When optional arg value is null, skip argument (function default handles None).
  - **A2 (Python, TypeScript, and all languages):** Respect `returns_result=false` — skip error-handling try/except/await for non-Result calls.
  - **Python codegen (`src/e2e/codegen/python.rs`):** A1 and A2 fixes.
  - **TypeScript codegen (`src/e2e/codegen/typescript.rs`):** A2 fix (A1 already handled correctly).

- **Phase 2: Validation hardening in `validate_fixtures_semantic()`** — Add semantic checks to catch configuration errors at `alef generate` time rather than downstream build failures.
  - **D1:** Validate call arg arity and types against IR function signatures (planned: integrate in follow-up with alef-extract).
  - **D2:** Validate return-type field paths against IR struct definitions; reject fixture assertions on fields not in the return type (planned: integrate in follow-up with alef_extract::return_type_fields).
  - **D3:** Integrate module-path validation into generate flow (D3 already implemented in alef-cli; validate.rs updated for consistency).

- **alef-cli: fixed `replace('-', "_")` Rust 2024 edition char-literal compile error.** The Rust 2024 edition requires `&str` for the first argument of `str::replace`; passing `'-'` (char) caused an E0308 in the validate-call path. (`crates/alef-cli/src/main.rs`)

## [0.11.26] - 2026-04-30

### Fixed

- **PyO3 stubs now emit `async def` for async functions and methods so mypy accepts the `await` in the generated `api.py` wrapper.** The 0.11.24 fix changed `api.py` to use `async def fn(...): return await _rust.fn(...)` for pyo3-async functions, but the corresponding `.pyi` stub kept declaring the underlying `_rust.fn` as plain `def fn(...) -> T`. mypy then errored at every wrapper call site with `Incompatible types in "await" (actual type "T", expected type "Awaitable[Any]")`. The stub generator now reads `FunctionDef.is_async` / `MethodDef.is_async` and emits `async def` so the underlying `_rust` symbol is typed as a coroutine; the generated `api.py` wrapper's `await` then type-checks. Both free functions and instance methods (including static methods) are covered. (`src/backends/pyo3/gen_stubs.rs`)
- **E2E TypeScript generator now auto-produces `globalSetup.ts` for HTTP test fixtures.** The generator was only creating `globalSetup.ts` when `client_factory` was configured, leaving HTTP test suites without proper mock server setup. Tests would fail with "app is not defined" at runtime. The generator now checks `has_http_fixtures` and generates `globalSetup.ts` unconditionally when HTTP tests are present (regardless of `client_factory`). The setup creates a fetch-wrapped HTTP client (`createApp`) and exposes it as `global.app` to all test suites. Vitest's `globalSetup` config is also auto-enabled whenever `needs_global_setup` is true. (`src/e2e/codegen/typescript.rs`)

## [0.11.25] - 2026-04-30

### Fixed

- **C# JSON converter switch-case block braces now match `dotnet format`'s default style.** The `gen_csharp_json_converter` writer emitted block braces aligned with the `case` keyword (col 12), but `dotnet format` reformats them one level deeper (col 16). On every commit the `csharp-format` pre-commit hook reshuffled the braces, which then broke `alef-verify`'s per-file hash check on the very next run. The generator now matches the formatter's expected output, eliminating the hook ping-pong. (`src/backends/csharp/gen_bindings.rs`)

## [0.11.24] - 2026-04-30

### Fixed

- **PyO3 backend: sanitized struct fields now emit `#[serde(skip)]` to prevent JSON round-trip failures (`#44 item 1`).** Fields whose types were sanitized to `String` placeholders (e.g. `CancellationToken → String`) were included in the derived `serde::Deserialize` impl. Deserializing a binding struct from JSON would fail with "unknown field 'cancel_token'" because the core type never expects that field. The fix adds `serde(skip)` alongside the existing opaque-field skip logic, so sanitized fields are excluded from JSON serialization/deserialization. (`src/codegen/generators/structs.rs`)
- **PyO3 backend: sanitized enum-like fields no longer cause "unknown variant ''" round-trip errors (`#44 item 2`).** A non-`Option` enum field sanitized to `String` (e.g. `result_format: OutputFormat → String`) derived `Default` as `String::default()` → `""`. Round-tripping through serde then failed with "unknown variant ''". The same `serde(skip)` fix from item 1 applies: the field is excluded from JSON, so the in-memory default is used silently. (`src/codegen/generators/structs.rs`)
- **`api.py` wrappers now forward arguments by keyword to match pyo3 signature order (`#44 item 3`).** The Python wrapper called `_rust.fn(path, config, mime_type)` in wrapper-declaration order, which misaligned with the pyo3 `#[pyo3(signature = (path, mime_type=None, config=None))]` declaration. The call is now `_rust.fn(path=path, mime_type=mime_type, config=config)` so slot alignment is independent of order. (`src/backends/pyo3/gen_bindings.rs`)
- **Async pyo3 functions now produce `async def` + `await` wrappers in `api.py` (`#44 item 4`).** Functions with `is_async = true` were emitted as plain `def fn(...)` wrappers that returned a coroutine directly. Callers who assigned the result without awaiting got a coroutine object instead of the resolved value, and type checkers saw the wrong return type. The generated wrapper is now `async def fn(...): ... return await _rust.fn(...)`. (`src/backends/pyo3/gen_bindings.rs`)
- **Trait-bridge `register_*` helpers are now exported via `api.py` and included in `__init__.py` `__all__` (`#44 item 5`).** `register_embedding_backend` and `register_ocr_backend` were added as `#[pyfunction]` to the pyo3 module but were absent from `api.py` and `__all__`, so `kreuzberg.register_ocr_backend(...)` raised `ImportError`. Pass-through wrappers are now emitted in `api.py` for all trait bridges that declare `register_fn`, and both `__init__.py` imports and `__all__` are updated accordingly. (`src/backends/pyo3/gen_bindings.rs`)

### Changed

- **PyO3 and NAPI generated bindings suppress pedantic/nursery clippy lints that don't apply to autogenerated FFI code.** Downstream projects that opt into `clippy::pedantic = "deny"` and `clippy::nursery = "deny"` (e.g. spikard's `[workspace.lints.clippy]`) were getting 240+ errors in `crates/<crate>-py/src/lib.rs` and `crates/<crate>-node/src/lib.rs` — none of them real bugs, all of them stylistic complaints about generated wrappers (every accessor wanting `#[must_use]`, every `-> CrateName` wanting `-> Self`, every `as` cast at the JS/Python boundary, every `Deserialize` derive on a type that has unsafe FFI methods). Fixing each in the generators would require per-template rewrites with no functional impact, so both backends now emit a documented `#![allow(clippy::*)]` block covering the FFI-specific false-positives: `unsafe_derive_deserialize`, `must_use_candidate`, `return_self_not_must_use`, `use_self`, `missing_const_for_fn`, `missing_errors_doc`, `needless_pass_by_value`, `doc_markdown`, `derive_partial_eq_without_eq`, `uninlined_format_args`, `redundant_clone`, `implicit_clone`, `redundant_closure_for_method_calls`, `wildcard_imports`, `option_if_let_else`, `too_many_lines`. NAPI also picks up the cast-family lints (`cast_possible_wrap`, `cast_possible_truncation`, `cast_sign_loss`) that pyo3 already had. Each entry is annotated with the rationale in the source. (`src/backends/pyo3/gen_bindings.rs`, `src/backends/napi/src/gen_bindings.rs`)

## [0.11.23] - 2026-04-30

### Fixed

- **C# csproj `<None Include="../../../LICENSE" />` had three `../` segments; flat csproj layout (`packages/csharp/<Namespace>.csproj`) only needs two to reach the repo root.** With three `../` segments, `dotnet pack` resolved the LICENSE path to one directory _above_ the repo and bailed with `error NU5019: File not found`. The csproj is scaffold-once so existing repos keep their hand-fixed value, but new scaffolds now emit `../../LICENSE`. (`crates/alef-scaffold/src/languages/csharp.rs`)

## [0.11.22] - 2026-04-29

### Fixed

- **Rustler NIF emission for `&self` opaque methods no longer requires `T: Clone`.** The 0.11.21 fix replaced `resource.inner.as_ref().clone().method(...)` with `(*resource.inner).clone().method(...)` to silence `noop_method_call`, but that pattern requires the underlying opaque type to implement `Clone` — and many real opaque types (e.g. tree-sitter-language-pack's `DownloadManager`) intentionally don't, since they wrap non-cloneable resources like dynamic-library handles. Compilation failed with `error[E0599]: no method named clone found for struct DownloadManager` across every emitted opaque-method call. The emit is now `ReceiverKind`-aware: `ReceiverKind::Ref` produces `resource.inner.method(...)` (Arc<T> derefs to &T, no clone needed); `ReceiverKind::RefMut` and `ReceiverKind::Owned` keep `(*resource.inner).clone().method(...)` (still requires `T: Clone`, but those receiver kinds are uncommon for opaque types and callers can use `[<lang>] exclude_functions` if the type isn't cloneable). Same fix applied to the sync (`gen_nif_method`) and async (`gen_nif_async_method`) call paths.

## [0.11.21] - 2026-04-29

### Fixed

- **Rustler NIF instance-method calls no longer trip `noop_method_call` under `clippy -D warnings`.** `gen_bindings/functions.rs` emitted `resource.inner.as_ref().clone().method(...)` for opaque instance methods. `Arc::as_ref()` returns `&T`; `.clone()` then resolves to `<&T as Clone>::clone` (a pointless reference-clone returning another `&T`), and the lint flagged every emitted NIF call site under `-D warnings`. tree-sitter-language-pack's Elixir NIF builds failed across all 3 platforms (linux-x86_64, linux-aarch64, macos-arm64) with dozens of `error: call to .clone() on a reference in this situation does nothing` at every generated method. The emission now uses `(*resource.inner).clone().method(...)` — the `*` dereferences `Arc<T>` to `T`, so `.clone()` resolves to `<T as Clone>::clone` and produces an owned `T` the method can consume. Same fix applied to both the sync (`gen_nif_method`) and async (`gen_nif_async_method`) emission paths.

## [0.11.20] - 2026-04-29

### Fixed

- **`alef sync-versions` no longer skips work based on a stale `.alef/last_synced_version` cache.** The previous warm-path short-circuit returned early whenever the cached version matched `Cargo.toml`'s, on the assumption that "same version → all manifests still in sync." That assumption breaks in three real cases: a manifest hand-edited to the wrong version, a _new_ manifest added after the last sync (e.g. `e2e/rust/Cargo.toml` introduced by `alef e2e generate`), or a stale `alef:hash:` line whose content drifted. CI runs without the cache and re-derives the correct state, so the local hook stayed silent while the `alef-sync-versions` pre-commit hook failed in CI for downstream consumers (most recently: liter-llm rc.14, kreuzcrawl, html-to-markdown, tree-sitter-language-pack — all required `rm -rf .alef` to reproduce the diff). The function now always walks every manifest. The scan is sub-second on kreuzberg-sized repos and the underlying writes are idempotent when nothing is actually stale, so the cost is invisible. The `.alef/last_synced_version` stamp is still written for forward-compatible introspection but is no longer consulted as a gate. (`crates/alef-cli/src/pipeline/version.rs`)

### Added

- **`[csharp].package_id` config field for NuGet `<PackageId>`, decoupled from `[csharp].namespace`.** The csharp scaffold previously emitted both `<RootNamespace>` and `<PackageId>` from `csharp_namespace()`, conflating two distinct identifiers. That conflation silently broke html-to-markdown's NuGet publish: the consumer-facing namespace `HtmlToMarkdown` is owned on nuget.org by an unrelated third party (Enrico Rossini), but the historical `KreuzbergDev.HtmlToMarkdown` package was owned by us. The alef migration overwrote the published-artifact id, and every release returned `403 (does not have permission to access the specified package)` until the csproj was hand-edited. Setting `[csharp] package_id = "KreuzbergDev.HtmlToMarkdown"` now lets the project publish to the owned coordinate while the in-code namespace stays the short, idiomatic form. When unset, `package_id` defaults to `namespace` — existing configs keep their behaviour. New accessor: `AlefConfig::csharp_package_id()`.

### Changed

- **PHP composer name and Swift Package URL no longer hardcode `kreuzberg`.** The PHP e2e composer.json `"name"` field now derives `<vendor>/e2e-php` from the consumer-binding pkg vendor, and the PSR-4 autoload namespace uses the configured PHP namespace (`<configured>\E2e\\`). The PHP README's `composer require <vendor>/<crate>` line uses the same derivation. The PHP e2e `pkg_name` fallback for `[e2e.packages.php].name` derives `<org>/<module>` from `[scaffold] repository` (instead of `kreuzberg/<module>`). The Swift e2e `Package.swift` `.package(url: ...)` for registry mode now uses the configured repository URL with a `.git` suffix, falling back to a vendor-neutral `https://example.invalid/<module>.git` placeholder. New `derive_repo_org()` helper exposed in `alef_core::config`.
- **`alef init` no longer writes `dev.kreuzberg` / `kreuzberg-dev` literals into freshly generated `alef.toml`.** When invoked in a project whose `Cargo.toml` declares `[package] repository = "..."`, the generated config now seeds `[scaffold] repository`, derives `[go] module` from it, and derives `[java] package` via the same reverse-DNS rule as `AlefConfig::java_package()` — so `repository = "https://github.com/foo-org/bar"` produces `module = "github.com/foo-org/bar"` and `package = "com.github.foo_org"`. When the repository is unset, the affected sections are emitted with the offending fields commented out and a `# TODO: set this` marker, instead of writing a plausible-looking but wrong default. When `[go].module` is unset, alef now derives the module path from `[scaffold] repository` / `[e2e.registry] github_repo` by stripping the `https://` scheme (`https://github.com/foo/bar` → `github.com/foo/bar`). When no repository is configured at all, the fallback is a vendor-neutral `example.invalid/<crate>` placeholder that fails `go build` loudly. The internal `package_name()` helper's last-resort `unwrap_or("kreuzberg")` is now `"binding"` (this branch is unreachable in practice — `next_back()` on a non-empty `split('/')` always returns `Some` — but the literal is gone). New `try_go_module()` accessor returns `Result<String, String>` for callers that should error rather than emit the placeholder.
- **Java/Kotlin package fallback no longer hardcodes `dev.kreuzberg`.** When `[java].package` / `[kotlin].package` is unset, alef now derives a reverse-DNS package from the configured repository URL (`[scaffold] repository` or `[e2e.registry] github_repo`): `https://<host>/<org>/<rest>` becomes `<reversed-host>.<org>` (host labels reversed, hyphens replaced with underscores). For kreuzberg-dev consumers (`https://github.com/kreuzberg-dev/<crate>`) this produces `com.github.kreuzberg_dev` — _different_ from the previous `dev.kreuzberg` literal. **Action required:** kreuzberg-dev consumers should set `[java] package = "dev.kreuzberg"` and `[kotlin] package = "dev.kreuzberg"` in their `alef.toml` to keep the existing namespace. When no repository URL is configured at all, `java_package()` / `kotlin_package()` fall back to `unconfigured.alef` so the build fails loudly. New `try_java_package()` / `try_kotlin_package()` accessors return `Result<String, String>` for callers that should error rather than emit the placeholder.
- **README scaffolding no longer falls back to `https://github.com/kreuzberg-dev/<crate>` when no repository is configured.** Alef is meant to be vendor-neutral; consumers outside the kreuzberg-dev org were silently picking up that URL in 13 places across `alef-readme` (the README header link plus 12 per-language "See <repo> for usage examples." pointers). The 13 inline `format!("https://github.com/kreuzberg-dev/{name}")` calls now route through a single `AlefConfig::github_repo()` accessor whose fallback is `https://example.invalid/<crate>` — an obviously-broken URL that surfaces in code review instead of smuggling another organization's link into the output. Set `[scaffold] repository = "..."` (or `[e2e.registry] github_repo`) in your `alef.toml` to resolve. A new `AlefConfig::try_github_repo() -> Result<String, String>` accessor is available for callers that should fail hard on missing config.

### Fixed

- **C# wrapper class methods now thread `IntPtr handle` for non-static methods.** `gen_pinvoke_for_method` and `gen_wrapper_method` previously only emitted the visible parameter list, ignoring the receiver. The cbindgen-emitted FFI signature for an instance method is `fn(this: *const T, ...)`, so the C# P/Invoke and the wrapper call site were one argument short — `dotnet build` failed with `CS7036: There is no argument given that corresponds to the required parameter 'ptr'`. Both functions now detect `!method.is_static && method.receiver.is_some()` and prepend `IntPtr handle` (P/Invoke signature) / `handle` (wrapper-to-native call argument) so the surfaces line up.
- **Kotlin error typealiases now use the `Exception` suffix to avoid colliding with same-named structs.** When an `errors` variant shares a name with a struct in `api.types` (e.g. an error variant `Foo` and a struct `Foo`), the previous `typealias Foo = pkg.FooException` would clash with the struct's own `typealias Foo = pkg.Foo`, and `compileKotlin` failed with `Redeclaration:`. The error alias now uses `typealias FooException = pkg.FooException`, mirroring the Java facade's class name; struct typealiases are unchanged.
- **Kotlin scaffold sample object renamed to plain `Sample` so its filename is not project-specific.** ktlint's `filename` rule requires a file with a single top-level declaration to match the declaration name. The sample object is now named `Sample` (matching `Sample.kt`) instead of `<ProjectName>Sample` (which would require `<ProjectName>Sample.kt`); the project name still appears in the body's `println` for context.
- **Gleam scaffold emits a valid `manifest.toml`.** Previous output was a comment-only stub, which gleam's TOML parser rejects with `missing field 'requirements'` on every `gleam check` / `gleam build` invocation. The scaffolded manifest now contains the minimum gleam expects (`packages = []` plus an empty `[requirements]` table); `gleam build` repopulates it on the first run.
- **Java scaffold's checkstyle config now resolves `${checkstyle.suppressions.file}`.** The pom.xml `<configuration>` block references a checkstyle config that requires the `checkstyle.suppressions.file` property to be set, but the config never told maven-checkstyle-plugin where to load that property from. Adds `<propertiesLocation>${project.basedir}/checkstyle.properties</propertiesLocation>` to the plugin config. Also fixes the property's value in `checkstyle.properties` to be relative to packages/java/ (the maven cwd) rather than to repo root.

## [0.11.19] - 2026-04-29

### Fixed

- **`sync-versions` now updates `packages/ruby/Gemfile.lock` alongside the gemspec.** After a version bump the gemspec received the new version but the lockfile was not touched, causing `bundle install` to abort with "The gemspecs for path gems changed, but the lockfile can't be updated because frozen mode is set" on every CI run. `sync_versions` now textually replaces all `<gem-name> (<old-version>)` entries for path gems in Gemfile.lock (both the `PATH > specs` block and the `CHECKSUMS` block) with the new RubyGems-format version. The replacement is idempotent — a second sync with the same version is a no-op.
- **`has_derive` now recognises namespaced derive paths** like `#[derive(serde::Serialize, serde::Deserialize)]`. The bare-ident form (`#[derive(Serialize)]`) was already detected, but the namespaced form fell through and IR types ended up with `has_serde: false`. Backends that gate `_to_json` / `_from_json` emission on `has_serde` (which is correct) then produced binding code referencing FFI functions that don't exist (e.g. `C.spikard_sse_event_to_json` for spikard's `SseEvent`, which derives `serde::Serialize`). The cfg_attr branch already used last-segment matching; the direct-derive branch now mirrors that.
- **Go backend C type references now preserve all-caps abbreviations.** Mirror of the FFI cbindgen-forward-decl fix: `alef-backend-go` ran type names through `heck::ToPascalCase` in seven places (free-method receiver, opaque-handle marshalling, return-value unwrapping, trait bridge trampolines, the `type_name` helper used in unmarshalFoo function names). For types like `GraphQLError` this produced `C.SPIKARDGraphQlError` in the generated `binding.go`, which doesn't match cbindgen's actual `SPIKARDGraphQLError` and causes `go build` to fail with "could not determine what C.SPIKARDGraphQlError refers to". IR type names are already PascalCase from Rust source — the conversion was both unnecessary and harmful for acronym-bearing types. Field-name renaming (`field_name.to_pascal_case()` for `serde_rename_all = "PascalCase"`) and method-name conversion are unchanged.
- **FFI cbindgen forward declarations now match cbindgen's actual emit for types with all-caps abbreviations.** The forward-declaration block in the generated `cbindgen.toml` ran type names through `heck::ToPascalCase`, which mangles abbreviations: `GraphQLError` becomes `GraphQlError`, but cbindgen emits the actual struct in the C header as `SPIKARDGraphQLError` (literal Rust name + prefix). Consumers compiling against the header saw two different type names for the same struct and the build failed (`unknown type name 'SPIKARDGraphQLRouteConfig'; did you mean 'SPIKARDGraphQlRouteConfig'?`). The pre-declarations now use the IR type name verbatim.
- **Java/Kotlin e2e codegen now respects `[java].package` and `[kotlin].package`.** Previously the e2e generators hardcoded `dev.kreuzberg` in three places: the pom.xml `<groupId>`, the test-file `package` declarations, and the generated filesystem path (`src/test/{java,kotlin}/dev/kreuzberg/e2e/...`). Projects whose Java/Kotlin package config used a different group id (e.g. spikard's `dev.spikard`) ended up with package declarations that disagreed with their filesystem location, breaking compilation. The generators now use `alef_config.java_group_id()` / `alef_config.kotlin_package()` consistently — pom.xml `<groupId>`, gradle `group =`, the `package` line, and the path segments all derive from the configured value.

## [0.11.18] - 2026-04-29

A patch release that fixes Javadoc emission so HTML inside backticks survives the Eclipse-formatter Spotless pipeline.

### Fixed

- **Javadoc `{@code …}` content now HTML-escapes its inner `<`, `>`, `&` characters.** A Rust doc comment like ``/// Determines how code blocks (`<pre><code>`) are rendered`` previously emitted `{@code <pre><code>}` with raw HTML inside the tag. Eclipse-formatter Spotless (used by html-to-markdown's `packages/java/pom.xml`) interprets the inner `<pre>` as a real block-level HTML element and shatters the doc comment across multiple `* ` rows — which then breaks `alef-verify` on the very next prek run. The codegen now emits `{@code &lt;pre&gt;&lt;code&gt;}` so Spotless leaves the line alone; readers see the same text since Javadoc renders `{@code}` literally regardless. Both `alef-codegen::doc_emission::escape_javadoc_line` and `alef-backend-java::gen_bindings::helpers::escape_javadoc_line` carry the fix.

### Fixed

- **Python e2e codegen no longer triggers ruff `F401` on `import pytest`.**

## [0.11.17] - 2026-04-29

PHP backend: fix flat-enum codegen to emit correct code instead of no-op conversions.

### Fixed

- **PHP flat-enum `From` impls no longer emit no-op `.into()` for primitive and `String` fields.** `flat_enum_core_to_binding_field_expr` and `flat_enum_binding_to_core_field_expr` previously fell through to `.map(Into::into)` / `.into()` for all types not explicitly handled. For primitives (`u8`, `u16`, `u32`, `i32`, `bool`, `f32`, `f64`, etc.) and `String`, the PHP binding type equals the core type, so `Into::into` is a no-op and triggered `clippy::useless_conversion`. Both functions now emit direct assignment for these same-type cases.
- **PHP flat-enum getter methods no longer call `.clone()` on `Copy` types.** Getters for `Option<u32>`, `Option<u8>`, `Option<bool>`, `Option<i64>` etc. previously always emitted `self.field.clone()`, which triggered `clippy::clone_on_copy`. Getters for Copy fields (`is_php_copy_type` helper: `Primitive` and `Option<Primitive>`) now emit `self.field` directly.
- **PHP flat-enum `From` impls no longer emit `..Default::default()` when all struct fields are covered.** When a variant's explicit field assignments already cover every field in the flat struct, the trailing `..Default::default()` is redundant and triggered `clippy::needless_update`. The codegen now pre-computes the complete set of flat field names and omits the struct update when the variant sets all of them. When `e2e.call.async = true` (or any test in the file is async/skipped/has error assertions), the python e2e generator emits `import pytest` at module level. Pytest is needed for `pytest.fixture` / `pytest.mark.*` decorators, but ruff's `F401` rule strips the import when no symbol is statically referenced in the file body — which then causes `alef verify` to fail on subsequent runs because the file's hash no longer matches the generated content. The import is now suppressed with `# noqa: F401`.

## [0.11.16] - 2026-04-29

WASM trait bridges and FFI multi-crate codegen fixes.

### Added

- **`[wasm.extra_dependencies]` table in `alef.toml`.** Mirrors `[crate.extra_dependencies]` but applies only to the WASM binding `Cargo.toml`. Initial use case: `async-trait = "0.1"` so generated wasm bridges can use `#[async_trait::async_trait(?Send)]` without callers having to declare it manually.

### Fixed

- **WASM trait bridge async impls now correctly emit `#[async_trait::async_trait(?Send)]`.** `gen_bridge_trait_impl` in `alef-codegen` always emitted `#[async_trait::async_trait]` (the default `+ Send` variant), which produced `error[E0053]: method ... has an incompatible type for trait` on `wasm32-unknown-unknown`: the underlying trait future was not `Send`-bounded but the macro-rewritten impl signature was. Added `async_trait_is_send() -> bool` (default: `true`) on the `TraitBridgeGenerator` trait; the wasm backend overrides it to `false`, and `gen_bridge_trait_impl` selects between `#[async_trait::async_trait]` and `#[async_trait::async_trait(?Send)]` accordingly. WASM bindings (kreuzberg-wasm and any other wasm-target generator) now compile clean for `OcrBackend`, `PostProcessor`, `Validator`, and `EmbeddingBackend` bridges.
- **FFI scaffold now merges `[crate.extra_dependencies]` into the generated `crates/<crate>-ffi/Cargo.toml`.** Previously the scaffold emitted only the umbrella crate (e.g. `kreuzberg = { path = "../kreuzberg" }`) plus `serde_json` and `tokio`, which worked when the public API surface lived in a single workspace crate. For multi-crate workspaces (e.g. spikard's `spikard-core`, `spikard-http`, `spikard-graphql`), the FFI bindings codegen emits qualified paths like `spikard_http::ServerConfig` and `spikard_graphql::QueryOnlyConfig` — the cdylib failed to compile because those crates were not direct dependencies. The scaffold now merges entries from `[crate.extra_dependencies]` (sorted, alphabetised) into the `[dependencies]` block, matching the behaviour of the wasm backend.
- **FFI codegen no longer double-prefixes sibling-crate paths in field accessors.** The path-qualification logic in `gen_field_accessor` previously only treated `module_prefix` as already-qualified when it equalled `core_import` or started with `core_import::`. For sibling workspace crates whose names share the project's prefix (e.g. `core_import = "spikard"`, sibling crate `spikard_http`), `module_prefix.starts_with("spikard::")` was false and the codegen emitted `spikard::spikard_http::openapi::OpenApiConfig` — a path that doesn't exist. The check now also accepts `{core_import}_` as a sibling-crate marker, so sibling-crate paths render verbatim.
- **FFI field accessors now look up Named types in the per-binding `path_map`.** When a struct field's `type_rust_path` is `None` in the IR (which alef emits when the type is referenced by short name), the field accessor previously fell back to `core_import` and produced paths like `spikard::ContactInfo` — which fails when the type lives in a sibling crate (`spikard_http::ContactInfo`) and is not re-exported through the umbrella crate. `gen_field_accessor` now threads the `path_map` built in `gen_bindings::generate` and uses `c_return_type_with_paths`, matching the behaviour of `gen_method_wrapper` and `gen_free_function`.
- **FFI codegen now uses `.map(str::to_owned)` for `Option<&str>` returns.** Methods returning `Option<&str>` (e.g. a `get_description() -> Option<&str>` accessor) previously emitted `let result = result.cloned();`, which fails to compile because `str: !Sized` and `Option::cloned` requires `T: Clone` on a sized type. The codegen now special-cases `TypeRef::Optional(TypeRef::String)` to emit `.map(str::to_owned)` instead. The existing `.cloned()` path still applies to `Option<&NamedType>`, `Option<&Vec<...>>`, and `Option<&char>`.
- **FFI codegen now uses `.to_owned()` for `&str` returns (was `.clone()`, a no-op).** Methods returning `&str` previously emitted `let result = result.clone();` — which compiles but is a no-op (str: !Sized doesn't impl Clone, so the call is a noop on the reference) and triggers Rust's `noop_method_call` lint. The codegen now emits `.to_owned()`, producing the owned `String` the FFI layer needs.
- **FFI no longer emits duplicate `to_json` / `from_json` exports when the type defines those methods.** Types that derive both `serde::Serialize` and a manual `to_json` / `from_json` method (e.g. `ProblemDetails::to_json`) caused two FFI functions with the same C name (`{prefix}_{type}_to_json`) — one from the auto-serde codegen path and one from `gen_method_wrapper` — and the cdylib failed with `E0428: defined multiple times`. The auto-serde codegen now skips emitting `to_json` / `from_json` when the type already defines a method of that name.

## [0.11.15] - 2026-04-29

PHP backend: fix codegen errors in tagged data enum `From` impls.

### Fixed

- **PHP flat data enum `From` impls now handle all field kinds correctly.** `gen_flat_data_enum_from_impls` previously emitted `.into()` unconditionally for every variant field, which compiled only when `From<BindingType>` happened to exist. Four cases were missing:
  - **`sanitized: true` fields** (e.g. `TableGrid`, `ImageMetadata`, `PdfMetadata`, `ProcessResult`, `[(u32,u32);4]`): now emits `None` / `Default::default()` instead of trying to convert an opaque or complex core type through a String.
  - **`is_boxed: true` fields** (e.g. `FormatMetadata::Docx(Box<DocxMetadata>)`, `::Html(Box<HtmlMetadata>)`): now wraps the core→binding result in `Some((*val).into())` and the binding→core result in `Box::new(...)`.
  - **`TypeRef::Path` fields** (e.g. `ChunkSizing::Tokenizer { cache_dir: Option<PathBuf> }`): now uses `PathBuf::from(s)` for binding→core and `.to_string_lossy().into_owned()` for core→binding instead of `.into()` (no `From<PathBuf> for String` exists).
  - **`TypeRef::Primitive(Usize | U64 | Isize)` fields** (e.g. `EmbeddingModelType::Custom { dimensions: usize }`): now emits `v as usize` / `v as i64` explicit casts instead of `.into()` (no `From<i64> for usize` exists).
  - Struct variant destructuring patterns for sanitized fields now use `field: _field` syntax (not `_field` alone) to satisfy the Rust struct-pattern completeness requirement.

## [0.11.14] - 2026-04-29

CLI ergonomics, generation performance, and live output for long-running commands. Warm `alef generate` on a 16-language consumer (kreuzberg) drops from ~14s to ~1s.

### Added

- **Standard CLI affordances.** `alef --version` (and `-V`) prints the binary version. New global flags `--verbose` / `-v` (repeatable: `-v` info, `-vv` debug, `-vvv` trace), `--quiet` / `-q` (errors only), and `--no-color` (disable ANSI in log output). Tracing now defaults to `info`-level output on stderr — previously the CLI was effectively silent unless `RUST_LOG` was set, which meant users had no signal during long-running commands.
- **Live streamed output for long-running commands.** `alef setup`, `alef update`, `alef update --latest`, `alef lint`, `alef fmt`, `alef test`, and `alef clean` now stream stdout/stderr to the terminal in real time, line-prefixed with `[<lang>]` when multiple languages run in parallel. Previously output was captured and only re-emitted via `tracing::info!` after the command finished — producing a multi-minute blackout for `pnpm install` / `bundle install` / `cargo update`. Failures are now also surfaced via an explicit `✗ <command> failed: <lang> — <error>` summary line on stderr.

### Changed

- **`alef generate` no longer runs formatters by default.** Formatting was the dominant cost of `alef generate` on multi-language projects (e.g. `cargo fmt --all`, `ruff check --fix .`, `biome format --write .`, `dotnet format`) and ran on the full package directory every invocation — even when only one language regenerated. The behaviour is now opt-in: pass `--format` to `alef generate`, `alef all`, or `alef init` to run formatters. When `--format` is passed, formatters run _only_ for languages that actually regenerated this run (other languages skip), making warm `alef generate --format` proportional to changed source files. The tradeoff: projects that previously relied on the implicit format pass to keep `alef verify` green should either (a) pass `--format`, (b) keep formatters in pre-commit hooks, or (c) run `alef fmt` explicitly after generate.

### Performance

- **Public API codegen is now cached.** `pipeline::generate_public_api` output is hashed and skip-written like binding files; previously every warm run rewrote 200+ Python `api.py` / `options.py` / `__init__.py` files for no net change.
- **Deterministic Python public-API imports.** The PyO3 backend's `gen_api_py` collected import names into an `AHashSet` then emitted them via `.join(", ")` — `AHashSet` iteration order is non-deterministic, so the codegen output flipped between runs and the content-hash cache always missed. Imports are now sorted before emit.
- **Idempotent `sync_versions`.** `replace_version_pattern` previously returned `Some(new_content)` whenever its regex matched, even when the rewrite was byte-identical (e.g. when Magnus emits `VERSION = "x"` with double-quotes and the replacement template uses single-quotes). Each `alef generate` then ping-pong-rewrote `version.rb`, marked it as drifted, and triggered a full README regeneration. The function now extracts the version literal from the match and short-circuits when it already equals the target — quote style irrelevant.
- **`sync_versions` short-circuits on warm runs.** Stamps `.alef/last_synced_version` after each successful sync; the next warm run with no `--bump` and an unchanged canonical version skips the entire glob+regex+stat pass over package manifests. When a sync does run, only `readme/docs/scaffold` stage caches are invalidated; the IR cache and per-language binding hashes are preserved.
- **`sources_hash` mtime-prefilter.** A per-source `(mtime_nanos, size)` memo at `.alef/sources_hash.cache` lets warm runs return the previous aggregate hash directly when nothing in the source tree has changed — skipping the read+blake3 pass over every `[crate].sources` file.
- **Hot extractor optimisations.** `extract_serde_rename_all` parses with `attr.parse_nested_meta` instead of stringifying the entire token stream and substring-scanning. `normalize_type_string` now scans bytes directly instead of materialising a `Vec<char>` per call. Combined, these halve allocations on the cold extraction path for large API surfaces.
- **Single-pass `to_pep440`.** Rewritten to build the output in one pre-allocated `String` instead of chaining five `.replace()` calls, each of which allocates a fresh intermediate.
- **`extract_version` regex cache.** The verify path's `extract_version` helper now caches compiled regexes in `OnceLock<Mutex<HashMap>>` so the ~15 verify patterns aren't recompiled per file.
- **Gleam variant collision map** uses `ahash::AHashMap` with `&str` keys; only colliding names allocate owned `String`s (was: `HashMap<String, _>` with per-variant clone).

## [0.11.13] - 2026-04-29

A patch release that addresses two regressions surfaced by the tree-sitter-language-pack v1.8.0-rc.14 release: copy-paste duplication in generated Java FFI methods and Spotless/`alef-verify` hash drift.

### Fixed

- **Java backend deduplicates the FFI Vec-return path through a shared `readJsonList` helper.** Every Vec-returning method previously inlined an identical ~15-line null-check → reinterpret → free → JSON deserialize block, which `cpd` correctly flagged as duplication. The boilerplate now lives in a single private static helper emitted by the helper-emitter; per-method call sites collapse to one line.
- **Java auto-format prefers Spotless when `packages/java/pom.xml` configures `spotless-maven-plugin`.** When detected, `alef generate` runs `mvn -f packages/java/pom.xml spotless:apply -q` instead of `google-java-format`, so the embedded `alef:hash:` value matches what the project's `mvn spotless:check` will see at verification time. Previously the hash drifted on every `alef generate` → prek cycle for any project whose Spotless config diverges from google-java-format defaults (e.g., Eclipse formatter).

## [0.11.12] - 2026-04-29

A patch release that fixes the Java backend so functions returning `PathBuf` emit a properly-typed `java.nio.file.Path` value instead of a raw `String`.

### Fixed

- **Java backend now wraps FFI string returns with `java.nio.file.Path.of(str)` when the declared return type is `Path`.** Previously, methods like `cacheDir()` were declared to return `java.nio.file.Path` but the body returned the raw `String` result of the FFI call, causing `javac` to reject the generated code with "incompatible types". Both `Path` and `Optional<Path>` are now handled correctly.

## [0.11.11] - 2026-04-29

A patch release that adds a `returns_result: bool` field to e2e `[CallConfig]` so the Rust generator can skip the `.expect("should succeed")` unwrap for native non-Result-returning calls.

### Added

- **`[e2e.calls.<name>] returns_result = false`** lets fixture authors mark a call whose Rust function signature returns `T` directly (e.g. `String`, `Vec<u8>`, `bool`, a hash) rather than `Result<T, _>`. The Rust code generator now binds the value with no unwrap when this is set; previous behavior always emitted `.expect("should succeed")`, which failed to compile against any non-Result function. Defaults to `true` to preserve existing behavior.

## [0.11.10] - 2026-04-29

A patch release that fixes the NAPI/Node backend so functions whose only sanitized parameter is a configured `[[trait_bridges]]` param (e.g. `Option<VisitorHandle>`) are emitted via the bridge instead of being silently skipped.

### Fixed

- **NAPI backend now emits top-level functions that take a configured trait-bridge parameter, even when the function is marked sanitized.** Previously the sanitized check ran before `find_bridge_param`, so html-to-markdown's top-level `convert(html, options, Option<VisitorHandle>)` was dropped from the Node binding, causing downstream `import { convert }` to fail at TypeScript compile time. The check now skips a function only when it's sanitized AND no trait-bridge param applies — matching how the PyO3 backend already handles this case (which is why Python had `convert` and Node didn't).

## [0.11.9] - 2026-04-29

A patch release that fixes the PHP backend's lowering of tagged enums with **tuple variants** holding distinct types.

### Fixed

- **PHP backend now gives each tuple variant of a tagged enum its own distinct field instead of collapsing them all to a shared `_0` field.** Previously `pub enum Message { System(SystemMessage), User(UserMessage), … }` lowered to a single `_0: Option<SystemMessage>` (the first variant's type) and the From impls then tried to assign `UserMessage`/`AssistantMessage`/etc to that one field, producing N trait-bound errors per non-first variant. Single-field tuple variants now use the variant's snake_case name as the field name (`system: Option<SystemMessage>`, `user: Option<UserMessage>`, …); multi-field tuple variants get `{variant}_0`, `{variant}_1`, …. Struct variants are unchanged.

## [0.11.8] - 2026-04-29

A patch release that fixes the PHP and Rustler/Elixir backends to preserve struct-variant fields when lowering tagged enums, plus two smaller release-pipeline fixes.

### Fixed

- **PHP backend now generates a real wrapper class for tagged enums with struct variants instead of emitting only string constants.** Previously `pub enum SecuritySchemeInfo { Http { scheme, bearer_format }, ApiKey { location, name } }` was lowered to a few `pub const` strings, the surrounding struct field was demoted to `HashMap<String, String>`, and the generated `From<core::Outer> for php::Outer` impl emitted `(k, v.into())` against the original enum value — which fails to compile with `the trait bound \`String: From<SecuritySchemeInfo>\` is not satisfied`. The backend now emits a flat php_class with a`type_tag`discriminator plus optional fields for every variant, full`#[php_impl]`getters, a`from_json`constructor, and proper`From`impls in both directions. Fixes spikard-php builds with`--features extension-module`.
- **Rustler/Elixir backend now preserves struct-variant fields in tagged enums instead of silently dropping them.** Previously every enum was lowered to `rustler::NifUnitEnum`, so `SecuritySchemeInfo::Http { scheme, bearer_format }` became unit variants `Http`/`ApiKey` with the inner fields gone, and the generated `From` impls fabricated `Default::default()` for every missing field — round-tripping any real value through Elixir produced empty defaults (silent data corruption). The backend now detects struct-variant enums and lowers them to `rustler::NifTaggedEnum` with the original variant fields preserved; the From impls now destructure variants instead of fabricating defaults. Unit-only enums continue to use `NifUnitEnum`.
- **WASM binding crate generation now sets `default-features = false` on the core dep when `[wasm].features` is configured.** Cargo would otherwise OR the explicit feature set with the core crate's defaults, sneaking host-only defaults like `download` back into the `wasm32-unknown-unknown` build and failing on `getrandom`/`mio`.
- **`alef validate-versions` now normalizes per-format before comparing.** It applies `to_pep440` to the canonical version when reading `pyproject.toml`, applies `to_rubygems_prerelease` for Ruby `version.rb`, and skips manifests that exist but don't declare a `version` field (e.g. private pnpm workspace roots). Eliminates spurious mismatches like canonical `1.4.0rc8` vs Python `1.4.0rc8` vs Ruby `1.4.0.pre.rc.8`.

## [0.11.7] - 2026-04-28

A patch release that fixes Rust e2e codegen so optional string/bytes args are passed via `.as_deref()` (yielding `Option<&str>`/`Option<&[u8]>`) rather than `&Option<...>`.

### Fixed

- **Rust e2e codegen no longer generates `&mime_type` for an `Option<String>` argument when the target signature expects `Option<&str>`.** Previously `let mime_type = None;` followed by `extract_file(&path, &mime_type, ...)` produced an `expected Option<&str>, found &Option<_>` E0308 mismatch on every fixture using the `extract_file` call. Optional string args now bind to a typed `Option<String>`/`Option<Vec<u8>>` (so `None` resolves) and pass via `.as_deref()` (or `.as_ref().map(|v| v.as_slice())` for bytes). Non-string optional non-string args use `.as_ref()` to avoid moving the binding.

## [0.11.6] - 2026-04-28

A patch release that prevents the python e2e codegen from emitting test files for categories whose fixtures are 100% skipped for python.

### Fixed

- **Python e2e codegen no longer generates a `tests/test_<category>.py` file when every fixture in that category has `skip.languages` containing `"python"`.** Previously the emitter would still write the file with `@pytest.mark.skip` markers, but the file's `from kreuzberg import ...` line would then reference APIs that aren't bound in Python — failing at module import before pytest could honor the skip marker. The other language emitters (node, ruby, java, go, csharp, php, elixir, r, wasm, dart, gleam, kotlin, swift, zig, typescript) already filtered skipped fixtures upstream and were not affected.

## [0.11.5] - 2026-04-28

A patch release that fixes the GitHub-release existence check so the publish pipeline no longer skips its own builds when only a release tag exists with no binaries attached.

### Fixed

- **`alef check-registry --registry github-release` now optionally verifies asset presence.** Two new flags — `--asset-prefix <STR>` and `--required-assets <a,b,c>` — make the check fail (return `exists=false`) when the release tag exists but the requested binaries have not been uploaded yet. Without this, alef's own `check-github-release` job returned `exists=true` for v0.11.4's empty release (the tag had been created by `gh release create` before the build matrix uploaded artifacts) and the dependent build jobs skipped — producing another tag with no binaries. The action wrapper (`kreuzberg-dev/actions/check-registry`) already exposed `asset-prefix` and `assets` inputs but was previously dropping them on the floor.

## [0.11.4] - 2026-04-28

A patch release that ships v0.11.3's content plus a fix for alef's own publish pipeline. v0.11.3 tagged but never produced binaries because validate-versions aborted on a half-empty `alef.toml`.

### Fixed

- **`alef.toml` in this repo now carries the minimal `[crate]` section and `languages = []` that `load_config` requires.** `alef validate versions` (run by the publish workflow's pre-flight) was failing with `missing field crate`, causing the whole publish run to skip without ever building binaries. Without a `[crate]` table the parser refuses the file even though the subcommand only needs the canonical version from `Cargo.toml`. v0.11.0 through v0.11.3 all share this issue — none of those tags has binary assets on the release page; v0.11.4 is the first installable build of the v0.11.x line.

## [0.11.3] - 2026-04-28

A patch release that turns alef.toml's `version` field into a real lifecycle pin: writes are stamped on every successful generate, and a config pointing to a future alef now refuses to run instead of silently producing stale output.

### Added

- **`alef generate` and `alef all` enforce alef.toml ↔ CLI version compatibility.** Before doing any work, both commands parse the top-level `version = "X.Y.Z"` field (if set) and compare it semver-style against the running CLI. If the pin is greater than the CLI, the command aborts with `alef.toml pins version = "..." but installed alef CLI is X.Y.Z. Upgrade alef ...`. This catches the case where a downstream repo bumps `alef.toml` and tries to regenerate against an older binary still on disk — the regenerate would otherwise quietly skip new emitters and corrupt the output.
- **`alef generate` and `alef all` stamp alef.toml with the CLI version after a successful run.** The top-level `version = "..."` line is rewritten (or inserted if missing) to match `env!("CARGO_PKG_VERSION")`. Downstream consumers (install-alef, CI verify) now have an authoritative record of which alef produced the on-disk artifacts, so a mismatch between alef.toml and the headers in generated files becomes impossible. The rewrite is line-anchored, so dependency `version = "..."` specs inside inline tables are never touched.

## [0.11.2] - 2026-04-28

A patch release fixing the alef self-publish bootstrap and rolling up several major dependency upgrades.

### Fixed

- **`alef`'s own publish workflow no longer races against itself.** The `prepare`, `validate-versions`, and `check-registry` jobs use the alef-backed composite actions, which previously defaulted `alef-version: latest` — install-alef would resolve that to the just-bumped `alef.toml` pin (the very version being published) and try to download a binary that didn't exist yet (it's built later in the same run). All four alef-side action calls now pass `alef-version: main` so install-alef builds from source via `cargo install --git --locked`. Costs ~2-3 min per job but breaks the chicken-and-egg loop.

### Changed

- **`alef check-registry` ported to ureq v3.** The previous v2 API (`AgentBuilder`, `RequestBuilder::set`, `Error::Status(404, _)`, `Response::into_string`) was rewritten to the v3 equivalents (`Agent::config_builder().new_agent()`, `RequestBuilder::header`, `Error::StatusCode(404)`, `Response::into_body().read_to_string()`).

### Bumped (template_versions)

- `JUNIT` 5.14.4 → 6.0.3 (Maven scaffold templates)
- `MICROSOFT_NET_TEST_SDK` 17.14.1 → 18.4.0
- `XUNIT_RUNNER_VISUALSTUDIO` 2.8.2 → 3.1.5
- `PRE_COMMIT_HOOKS_REV` v0.9.5 → v6.0.0
- `TYPOS_REV` v0.7.10 → v1.45.2
- Plus auto-merged Renovate PRs landed since v0.11.1: `criterion` 0.7 → 0.8, `extendr-api` 0.8 → 0.9, `microsoft.net.test.sdk` 17.12.0 → 17.14.1, JVM tooling pins, pre-commit hook revisions.

## [0.11.1] - 2026-04-28

A patch release fixing path discovery and version-extraction edge cases in the new `alef validate versions` subcommand surfaced when running it against kreuzberg's repo layout.

### Fixed

- **`alef validate versions` now skips manifests that don't exist** rather than treating absence as a mismatch. Previously, every repo was flagged as having "missing" Ruby/Go/PHP manifests if it didn't follow alef's default lib-based layout. The check is opt-in per file: only manifests that physically exist are validated.
- **Ruby version files are discovered via globs** matching the same patterns alef's `sync-versions` writes to: `packages/ruby/lib/*/version.rb`, `packages/ruby/ext/*/src/*/version.rb`, `packages/ruby/ext/*/native/src/*/version.rb`. Repos that ship the Ruby gem with rb-sys-style ext layout no longer produce false negatives.
- **PHP composer.json is only validated when a `version` field is actually declared**. Composer relies on Git tags for versioning, and most polyglot manifests omit it; the validator no longer flags missing-by-design as a mismatch.
- **`mix.exs` reader now accepts both `@version "X.Y.Z"` (module-attribute form) and `version: "X.Y.Z"` (keyword form inside `def project do`).** The previous reader only matched the `@version` constant style.
- **Root `package.json` and `crates/{name}-{wasm,node}/package.json` are now part of the validation set.** Repos that ship a top-level npm package or per-crate package.json no longer silently bypass the check.

## [0.11.0] - 2026-04-28

A minor release that absorbs a large slice of polyglot publish-pipeline machinery into alef so consumers (kreuzberg, html-to-markdown, liter-llm, …) can stop duplicating it across `kreuzberg-dev/actions` shims and per-repo `scripts/publish/` shell. `alef-publish` now owns end-to-end packaging for seven languages it previously didn't, and the `alef` binary gains four new top-level subcommands that consolidate cross-manifest validation, release-event metadata extraction, multi-registry version checks, and Go submodule tagging.

### Added

- **`alef-publish` end-to-end packagers for Python, Wasm, Node, Ruby, Elixir, Java, and C#.** Each adds a `package_<lang>` function under `crates/alef-publish/src/package/` and is dispatched from `alef publish package --lang <lang> --target <triple>`. Python invokes maturin for wheels and sdist; Wasm runs `wasm-pack` then `npm pack`; Node runs `napi build` and emits per-platform npm sub-packages following the `napi.triples.additional` convention (with the platform list configurable via the new `[publish.languages.node].npm_subpackage_platforms`); Ruby compiles via rb-sys/`bundle exec rake compile` and assembles a platform-tagged `.gem`; Elixir produces RustlerPrecompiled-style NIF tarballs (one per `nif_versions` × target combination) plus a `checksum-Elixir.{App}.exs` aggregator; Java stages the JNI shared library under `native/{classifier}/` and runs Maven to produce a classifier-suffixed JAR; C# stages `runtimes/{rid}/native/` and runs `dotnet pack`.

- **`alef validate versions`** — cross-manifest version consistency checker. Reads `alef.toml` to discover each language's manifest location (Cargo.toml / pyproject.toml / package.json / gemspec / mix.exs / pom.xml / .csproj / composer.json / gleam.toml / build.zig / Package.swift / pubspec.yaml / build.gradle.kts / DESCRIPTION) and compares declared versions to the canonical Cargo.toml workspace version. Exits non-zero with a clear mismatch list; emits JSON with `--json`. Replaces the Python-based `kreuzberg-dev/actions/validate-versions` action and the per-repo shell scripts that did the same job.

- **`alef release-metadata`** — emits release metadata JSON in the exact shape consumed by GitHub Actions matrix dispatch (`tag`, `version`, `npm_tag`, `is_tag`, `is_prerelease`, `release_<target>` flags, …). Args: `--tag`, `--targets`, `--git-ref`, `--event`, `--dry-run`, `--force-republish`, `--json`. The set of valid targets is discovered from `alef.toml`'s `languages` array. Replaces `kreuzberg-dev/actions/prepare-release-metadata`.

- **`alef check-registry`** — version-existence check across PyPI, npm, crates.io, RubyGems, Hex, Maven Central, NuGet, Packagist, a Homebrew tap, and GitHub Releases. Output: `exists=true|false` (and JSON with `--json`). Replaces `kreuzberg-dev/actions/check-registry` plus the per-registry `scripts/publish/check_*.sh` scripts.

- **`alef go-tag`** — creates and pushes the two Git tags Go's submodule-versioning convention requires (the top-level `vX.Y.Z` and the `packages/go/v<major>/vX.Y.Z` submodule tag). Supports `--dry-run` and `--remote`.

- **`PublishLanguageConfig` schema additions** (all `Option<...>`, fully back-compatible): `npm_subpackage_platforms`, `cibuildwheel_environment`, `jni_classifier`, `csharp_rid`, `wheel`, `sdist`. Existing `[publish.languages.*]` tables continue to load unchanged.

### Fixed

- **Scaffolded GitHub workflows for Swift, Dart, Gleam, Kotlin, and Zig were broken on first run.** The Swift workflow referenced `actions/setup-swift@v4`, which doesn't exist (the working action is `swift-actions/setup-swift@v2`). The Dart, Gleam, Kotlin, and Zig workflows passed unsupported flags to the per-language CLIs (`--working-dir`, `--project-path`, `--directory`) instead of `cd`-ing into the package directory. All five workflow templates now use `defaults.run.working-directory: packages/<lang>` and let each CLI run from its own root, and Swift uses `swift-actions/setup-swift@v2`.

## [0.10.4] - 2026-04-27

A patch release fixing three orchestration / e2e-codegen bugs that surfaced during a clean `alef all --clean` regenerate of every downstream polyglot repo: standalone e2e Rust crates were getting absorbed into parent workspaces, the orchestrated `alef all` skipped the language-default formatter pass, and the Rust visitor codegen was producing unparsable trait-impl blocks.

### Fixed

- **e2e Rust crate's `Cargo.toml` was silently inherited by any parent workspace**, so running `cargo fmt`/`cargo build` from inside `e2e/rust/` failed with `current package believes it's in a workspace when it's not` whenever the consuming repo had a top-level workspace `Cargo.toml` that didn't explicitly list `e2e/rust` in `members` or `exclude`. The default formatter `(cd {dir} && cargo fmt --all)` therefore exited 1 and left `e2e/rust/tests/*.rs` unformatted, breaking the next `alef verify`. The generated `e2e/rust/Cargo.toml` now starts with an empty `[workspace]` table so the e2e crate is its own workspace root and is unaffected by any parent — consumers no longer have to remember to add `e2e/rust` to `workspace.exclude`.

- **`alef all` skipped `format_generated`**, leaving every language-default formatter (mix format, ruff format, biome format, php-cs-fixer, etc.) unrun on the freshly emitted bindings. Prek's check-mode formatting hooks (`mix format --check-formatted`, etc.) therefore failed against `packages/elixir/lib/<lib>/native.ex` and similar files in every repo that ran the orchestrated `alef all` instead of `alef generate`. `Commands::All` now invokes `pipeline::format_generated` before `fmt_post_generate`, mirroring the order in `alef generate`.

- **e2e Rust visitor codegen emitted untyped trait method parameters** (`fn visit_custom_element(&self, ctx, tag_name, html) -> VisitResult`), producing files that could not parse. Three coupled fixes:
  - Each visitor parameter is now bound to a `_` pattern with the explicit `&NodeContext` / `&str` / `bool` / `u8` / `u32` / `Option<&str>` / `&[String]` type from the `HtmlVisitor` trait, so the body needn't introduce unused bindings.
  - The receiver is now `&mut self` to match the trait, not `&self`.
  - `CallbackAction::Custom` was missing the surrounding string-literal quotes — `VisitResult::Custom([CUSTOM WIDGET].to_string())` was the literal output for `output: "[CUSTOM WIDGET]"`. The codegen now wraps the escaped value in `"…"` before calling `.to_string()`, so the emitted expression is a well-formed `&str` literal.
  - The test file now imports `HtmlVisitor`, `NodeContext`, and `VisitResult` whenever any fixture in the file declares a `visitor` block.

## [0.10.3] - 2026-04-27

A patch release fixing `alef e2e generate` so it only emits test projects for languages the consumer has actually scaffolded, plus two `alef verify`-vs-`prek` drift fixes uncovered by a clean v0.10.2 regenerate of the downstream polyglot repos.

### Fixed

- **`alef e2e generate` emitted test projects for every supported backend** when neither `--lang` nor `[e2e].languages` was set, including languages with no scaffolded binding (e.g. `gleam`, `kotlin`, `dart`, `swift`, `zig`, `brew`). The resulting `e2e/<lang>/` directories couldn't compile because the package they reference doesn't exist. The default now mirrors `alef generate` / `alef scaffold`: derive the e2e language list from the top-level `[languages]` array, mapping `Language::Ffi` → the `c` e2e harness and always including `rust` for the source-crate suite. Generators without a matching `Language` variant (`brew`) require explicit opt-in via `[e2e].languages`.

  Migration: after upgrading, run `alef e2e generate` once, then manually delete any stale `e2e/<lang>/` directories for languages you never scaffolded — the cleanup pass only revisits dirs the current run touched, so untouched stale dirs from prior runs are not auto-removed.

- **e2e Rust default formatter command was invalid**: v0.10.2 introduced a built-in default of `cargo fmt --manifest-path {dir}/Cargo.toml`, but `--manifest-path` is not a stable global flag for `cargo fmt` (cargo prints `Specify message-format: short|json|human / --all / --check` and exits 1). The default is now `(cd {dir} && cargo fmt --all)`, which formats the standalone e2e crate from inside its own directory and works regardless of whether the crate is a workspace member. Without this fix, `e2e/rust/tests/*.rs` were left unformatted by `alef e2e generate`, and prek's cargo-fmt hook then rewrote them post-finalisation, breaking `alef verify`.

- **`normalize_content` skipped trailing-whitespace stripping for `.rs` files** when rustfmt could not parse them — for example, cextendr's `packages/r/src/rust/src/lib.rs` uses the non-standard `name: T = "default"` parameter-default syntax, which rustfmt rejects, so `format_rust_content` falls back to the raw codegen output. The raw output contains trailing whitespace on blank lines (e.g. `    \n` between `#[must_use]` and the next `pub fn`), which prek's `trailing-whitespace` hook then strips, breaking `alef verify`. `normalize_content` now always pipes through `normalize_whitespace` after rustfmt, so the embedded `alef:hash` always reflects whitespace-clean content regardless of whether rustfmt parsed the file.

### Added

- **`alef_e2e::default_e2e_languages(&[Language])`** — the public helper that maps the scaffolded language list to e2e generator names. Exposed so consumers and downstream tooling can resolve the same default the CLI uses.

## [0.10.2] - 2026-04-27

A patch release fixing four codegen and pipeline bugs surfaced by a clean regenerate of the five downstream polyglot repos against v0.10.1.

### Fixed

- **Swift binding `Cargo.toml` was missing `serde_json`** even though the generated `lib.rs` emitted `::serde_json::to_value(...)` / `::serde_json::from_value(...)` calls for `Codable` propagation, breaking compilation with ~1k+ `E0433: cannot find serde_json in the crate root` errors. The Swift backend now always includes `serde_json = "1"` in the binding crate's `[dependencies]`.
- **Dart binding `Cargo.toml` listed an unused `anyhow = "1"`** — the Dart trait-bridge codegen returns `source_crate::Result<T>` directly and never imports anyhow, so `cargo machete` rejected the crate. The Dart backend's `extra_deps` no longer emits anyhow.
- **Go e2e codegen emitted fixture strings containing NUL bytes inside raw string literals** (`` `…\0…` ``), which `gofmt` rejects with `illegal character NUL`. Affected ~50 generated test files in repos with NUL-bearing fixture data. The `go_string_literal` helper now switches to interpreted (double-quoted) form whenever a string contains characters Go raw strings cannot represent (NUL, `\r`, or backtick), and emits `\xNN` hex escapes for any ASCII control byte.
- **e2e generated files drifted from their embedded `alef:hash` after `prek run --all-files`** because `alef e2e generate` skipped trailing-newline / trailing-whitespace normalization and didn't run a Rust or Python formatter on standalone e2e manifests by default. Three coupled fixes:
  - `pipeline::write_scaffold_files_with_overwrite` now runs every emitted file through `normalize_content` (ensure exactly one trailing newline, strip trailing whitespace per line) before hash finalization, matching what `prek`'s `end-of-file-fixer` and `trailing-whitespace` hooks would have done.
  - `alef-e2e::format::run_formatters` now falls back to a built-in default formatter set (`cargo fmt` inside `e2e/rust/`, `ruff format` on `e2e/python/`) when `[e2e].format` is unconfigured, instead of silently no-oping.
  - `pipeline::sync_versions` now invokes `finalize_hashes` on every version-synced file (e.g. `version.rb`) so the embedded `alef:hash` line stays consistent with the rewritten content.

A patch release reworking `alef verify` to be **idempotent across alef versions** and bundling six small generator/scaffold fixes that landed since v0.10.0. The verify hash no longer encodes the alef CLI version or `alef.toml`; it is now a per-file fingerprint derived purely from the rust sources and the on-disk file content, so a green `alef verify` stays green after upgrading the alef CLI as long as nothing else changed.

### Changed

- **`alef verify` is now per-file source+output deterministic**. The `alef:hash:<hex>` line embedded in every generated file is computed as `blake3(sources_hash || file_content_without_hash_line)`, where `sources_hash = blake3(sorted(rust_source_files))` — no alef version dimension, no `alef.toml` dimension. `alef generate` finalises the hash _after_ every formatter has run (so the embedded hash describes the actual on-disk byte content), and `alef verify` is now a pure read+strip+rehash+compare with no regeneration and no writes. Previously the hash incorporated the alef CLI version, which forced every consumer repo to re-run `alef generate` after every alef bump even when nothing else had changed.
- **`alef_core::hash::compute_generation_hash` is removed**; use `compute_sources_hash` + `compute_file_hash` instead. The IR cache (`.alef/ir.json`) now keys on `compute_sources_hash` alone — pass `--clean` to bust the cache when the alef extractor itself has changed.
- **`pipeline::write_files` / `write_scaffold_files` no longer take a `generation_hash` argument**; the hash is finalised separately by the new `pipeline::finalize_hashes(paths, sources_hash)` after formatters run.

  Migration: after upgrading, every existing alef-generated file still carries the old input-deterministic hash. Run `alef generate` once (then `alef e2e generate` if the repo uses `[e2e]`) to refresh embedded hashes to the new per-file scheme.

### Fixed

- **`alef scaffold` emitted Dart and Swift `Cargo.toml` files without a `license` field and pulled in an unused `serde_json` dep** — both languages now emit the same `license = "..."` line as the other backends, and the spurious `serde_json = "1"` is gone.
- **Go e2e fixtures used raw `to_pascal_case` for method names** instead of routing through `to_go_name`, so generated test code referenced `result.Html` while the binding declared `result.HTML` (golangci-lint rejected the package). All Go method emission now goes through `alef_codegen::naming::to_go_name`.
- **Dart codegen emitted reserved keywords / numeric idents / unescaped `$` and `\`** in field names, parameter names, and string literals — Dart now escapes reserved keywords (`async`, `class`, …), prefixes leading-digit identifiers with `_`, and escapes `$` / `\` / `"` inside generated string literals.
- **`alef readme` panicked when a snippet section was missing from the language template** — missing snippet keys default to an empty Tera object instead of raising.
- **`fmt_post_generate` did not run the configured formatters for WASM, C#, and Java** because the lint/format dispatch was hardcoded to a static set of languages — the dispatch is now driven entirely by `LintConfig::format` so any backend can opt in.
- **NAPI `.d.ts` emission placed optional parameters before required ones**, which TypeScript rejects with `TS1016`. NAPI generators now reorder optional parameters last in the emitted `.d.ts` signatures.

## [0.10.0] - 2026-04-27

A major release expanding alef's target-language coverage from 11 to 16 backends. Adds full code-generation support for **Kotlin** (JNA), **Swift** (swift-bridge), **Dart** (flutter_rust_bridge), **Gleam** (Rustler-on-BEAM), and **Zig** (C ABI), each with scaffold, lint, format, build, test, setup, update, and clean defaults wired through `alef-core`. The five new backends share the existing IR, trait-bridge round-trip API, and `alef-docs` language-native doc emission paths — there is no per-language fork of the codegen pipeline. Also adds language-native doc comments (PHPDoc, C# XML, `@doc`, roxygen2) to the existing PHP/C#/Elixir/R backends, and fills in numerous correctness gaps surfaced by the kreuzberg verification worktree.

### Added

- **Five new language backends** — `alef-backend-kotlin`, `alef-backend-swift`, `alef-backend-dart`, `alef-backend-gleam`, `alef-backend-zig`. Each implements the `Backend` trait, ships per-crate snapshot tests via `insta`, and is wired into `alef-cli`'s dispatch table. Trait bridges are supported across all five (Dart abstract classes, Gleam per-method response shims, Zig comptime `make_*_vtable` helpers, Kotlin/Swift host-side typealiases + Codable propagation).
- **Language-native scaffolds for the new backends** — `alef-scaffold` writes Kotlin (Gradle KTS + Detekt + ktlint), Swift (Swift Package Manager + swift-format + SwiftLint), Dart (pub + flutter_rust_bridge + lints), Gleam (`gleam.toml` + gleeunit), and Zig (`build.zig` + `build.zig.zon`) toolchain configs, README stubs, and CI templates. `alef-publish` gains per-language packaging and validation modules for the same five.
- **`alef-docs` language emitters for PHP/C#/Elixir/R/Kotlin/Swift/Dart/Gleam/Zig** — PHPDoc, XML `<summary>`/`<param>`/`<returns>`, Elixir `@doc` heredocs, roxygen2 `@param`/`@return`, KDoc, Swift markdown, Dart `///`, Gleam `///`, and Zig `///` are now emitted for all public functions/methods. Shared `doc_emission` module handles language-specific escaping (e.g. `*/` in PHP, XML entities in C#, triple-quote escapes in Elixir).
- **`has_serde` IR flag on `EnumDef`** — extracted from `derive(Serialize)` + `derive(Deserialize)` so backends can decide whether to JSON-roundtrip or pattern-match conversion. Used by Swift to emit `Codable` and propagate it to non-derived enum references whose containing struct is `Codable`.
- **`return_sanitized` IR flag on `FunctionDef`** — laid down by `alef-extract` when the return type was sanitized by the unknown-type pass. Backends can use it to decide whether a roundtrip is recoverable for the return value (currently consumed by snapshot/doc tests; full backend wiring is a follow-up).
- **`alef-readme` v0.10.0 README templates** for the new languages.

### Changed

- **Backend count: 11 → 16** in README, skill docs, and the supported-languages table. Kotlin/Swift/Dart/Gleam/Zig are no longer flagged "in progress".
- **Trait bridges span all 16 backends** — round-trip callbacks (host language implements a trait, Rust core invokes it through the bridge) work end-to-end for every supported language.
- **`alef-core::config` defaults** for `LintConfig`, `BuildConfig`, `TestConfig`, `SetupConfig`, `UpdateConfig`, and `CleanConfig` now cover Kotlin/Swift/Dart/Gleam/Zig with per-language preconditions and command sequences. The Phase-1 skip clauses in the per-language test loops are gone.
- **Centralized version pins** — `alef-core::template_versions` gains `pub_dev`, `toolchain`, and Kotlin/Swift entries in `cargo`/`maven` modules. Toolchain pins (Zig 0.16, Dart SDK `>=3.0 <4.0`, JVM 21, swift-bridge `0.1.59`, flutter_rust_bridge `2.12.0`, Kotlin 2.1.10, kotlinx-coroutines 1.9.0, JNA 5.14.0) are now single-source.

### Fixed

- **Swift typealias closure for #59** — non-trait Kreuzberg types are emitted as `typealias Kreuzberg.X = RustBridge.X`, host-language wrapper functions are removed, and consumers call `RustBridge` functions directly. `RustString`/`RustVec` returns and `Data → [UInt8]` arguments are converted at the wrapper boundary. Conversion initializers (`init(rustVal: RustBridge.X)`) are emitted for non-`Codable` enums; `Codable` enums skip them and use JSON. `swift_name(camelCase)` is emitted on every bridge fn so swift-bridge generates idiomatic Swift symbols. Reduces `kreuzberg-swift` build errors from ~190 to 0.
- **Swift unbridgeable surfaces** — getters/wrappers for fields that can't survive the swift-bridge round-trip (excluded fields, JSON containers with non-serde inners, `Vec<non-Primitive>` in non-serde structs) now skip emission entirely with a `// alef: skipped` marker instead of emitting `unimplemented!()`.
- **Swift constructor / lints / keyword escaping** — constructor is omitted (instead of `unimplemented!()`) for structs that need `Default` but don't derive it. Generated `lib.rs` carries an `#![allow]` block for the swift-bridge codegen pattern's clippy artifacts. Reserved Swift keywords used as field/param names are escaped at the extern block, wrapper, and constructor sites. A phantom `Vec<FooBox>` accessor is forced per trait so swift-bridge emits the C symbols its Vec<T> accessors call.
- **Dart trait abstract classes + unbridgeable surfaces** — Dart-side `abstract class` is emitted per trait for the round-trip API. Functions with unbridgeable params (e.g. `Vec<(Vec<u8>, …)>`, lossy through JSON) are skipped instead of emitting panicking shims. Tuple-variant fields are renamed `_N` → `fieldN` to survive flutter_rust_bridge's underscore stripping. Generator-only clippy lints (`map_identity`, `let_and_return`, `collapsible_match`, `manual_flatten`, `too_many_arguments`, `unit_arg`, `type_complexity`) are silenced via `#![allow]` in the emitted `lib.rs`.
- **Gleam per-method response shims** — each trait method gets a typed `{trait}_{method}_response(call_id, Result(T, E))` shim so callback modules can reply through the Rustler reply-registry without raw JSON.
- **Zig vtable codegen** — comptime `make_{trait}_vtable(comptime T, *T)` helper generated per trait bridge; thunks reconstruct byte slices, fallible methods return `i32 + out_error`, lifecycle stubs (`name_fn`, `version_fn`, `initialize_fn`, `shutdown_fn`) emitted when `super_trait` is configured. Trait-bridge thunks now discard unused `out_result` params, avoid shadowing method params named `result`, and only emit `return 0;` when the success path actually flows through.
- **WASM `useless_conversion`** — generated `lib.rs` adds the lint to its crate-wide allow list, since the `kreuzberg::T::from(t_core).into()` identity conversion is only an artifact of code generation.
- **Rustler doc emission** — NIF Rust source emits standard `///` Rustdoc instead of literal Elixir `@doc """…"""` heredocs (which produced 380 parse errors when run through `cargo build`). Reserved Elixir module-attribute names (`@behaviour`, `@callback`, …) are no longer used as GenServer attributes. The lossy `Vec<String>` → `Vec<&str>` conversion is dropped — `&Vec<T>` already derefs to `&[T]`, so the simpler emission preserves caller signatures like `&[String]`.
- **Swift scaffold indent** — Swift test stubs use 2-space indent to match swift-format defaults.

## [0.9.2] - 2026-04-27

A patch fixing three generator bugs that surfaced when alef v0.9.1 was used to regenerate downstream consumer repos. Each one blocked a different language's pre-commit hook from passing on freshly regenerated bindings; with these fixes a full `alef generate` + `alef e2e generate` produces output that passes mix credo (Elixir), golangci-lint (Go), and `dotnet format` / `dotnet build` (C#) without further hand-editing.

### Fixed

- **Elixir trait-bridge module names emitted broken PascalCase for hyphenated crate names**: `alef-scaffold`'s Elixir generator used `capitalize_first(&app_name)`, which only uppercased the first character and left underscores in place. For a crate named `html-to-markdown` the generated Rustler trait bridge declared `defmodule Html_to_markdownHtmlVisitorBridge do`, which `mix credo` correctly rejects ("Module names should be written in PascalCase."). All three call sites (mix.exs `defmodule`, the bridge `module_name`, and `native_mod`) now use `heck::ToPascalCase`, splitting on both `-` and `_`. `html-to-markdown` → `HtmlToMarkdown`, `tree-sitter-language-pack` → `TreeSitterLanguagePack`.
- **Go binding and Go e2e tests disagreed on initialism casing**: `alef-backend-go` already routes Go field/parameter names through an idiomatic-initialism transformer (`HTML`, `URL`, `ID`, `JSON`, …) but `alef-e2e`'s Go fixture renderer used a plain `to_pascal_case` helper, so generated test code referenced `result.Html` and `result.Url` while the binding declared `HTML` and `URL`. golangci-lint rejected the generated package with "result.Html undefined". Both crates now route through `alef_codegen::naming::to_go_name` and `go_param_name`; the duplicate `GO_INITIALISMS` table in `alef-e2e/src/codegen/go.rs` was deleted.
- **C# `.csproj` scaffold was placed inside the source subdirectory instead of the package root**: `alef-scaffold` wrote the project file at `packages/csharp/{Namespace}/{Namespace}.csproj`, but consumer Taskfile targets and the prek `csharp-format` / `csharp-lint` hooks expected it at `packages/csharp/{Namespace}.csproj` (alongside `Directory.Build.props`, matching the convention used for the gemspec at `packages/ruby/*.gemspec`). The path is now `packages/csharp/{Namespace}.csproj`, with `<ItemGroup>` `Include` paths adjusted (one level up the relative path) so `LICENSE` and `runtimes/**` still resolve correctly. The scaffold remains create-once.
- **C# async docs/signature emitted invalid `Task<void>`**: `alef-docs::render_csharp_fn_sig` formatted async return types as `Task<{ret}>` unconditionally, so for void-returning async functions the documented signature read `public static async Task<void> ...`, which doesn't compile under `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`. It now emits `Task` for void returns and `Task<{ret}>` otherwise.

## [0.9.1] - 2026-04-27

A patch release fixing the publish pipeline. v0.9.0 (and prior releases since `alef-publish` was added) failed to publish `alef-cli` to crates.io because `alef-publish` was missing from the publish workflow's crate list — `alef-cli` declares `alef-publish = "^X.Y.Z"` and crates.io still served `alef-publish 0.7.2`, so the upload of `alef-cli` errored with `failed to select a version for the requirement alef-publish = "^0.9.0"`. The 19 other workspace crates published cleanly each time; only `alef-cli` (the binary used by `cargo install` / `cargo binstall`) was affected. v0.9.0 was rescued by publishing `alef-publish` and `alef-cli` manually with `cargo publish`; v0.9.1 makes the workflow self-sufficient.

### Fixed

- **`alef-publish` was missing from the Publish workflow's crate list**: `.github/workflows/publish.yaml` listed 20 crates but not `alef-publish`, so the workflow never uploaded it to crates.io. `alef-cli`'s dependency on `alef-publish ^X.Y.Z` therefore failed to resolve at publish time. The crate is now listed in dependency order (after `alef-scaffold`, before the backends), matching the rest of the workspace.

## [0.9.0] - 2026-04-27

A major fix release eliminating ~40 generated "Not implemented" stubs across the PHP, Ruby, C FFI, and R backends. Every batch extraction API (`batch_extract_file_sync`, `batch_extract_bytes_sync`, plus async variants), `extract_file`/`extract_file_sync`, and most of the Ruby gem's surface previously failed at runtime with error code 99. Five distinct generator bugs collapsed into a single class of bad output. Also makes `alef verify` input-deterministic so downstream formatters can reformat generated content freely without breaking verify, and exposes the canonical input-hash recipe via `alef_core::hash::compute_generation_hash`.

### Changed

- **`alef verify` is now input-deterministic**. The embedded `alef:hash:<hex>` line in every generated file is no longer a per-file hash of the normalised generated content; it is a single fingerprint of the **inputs** that produced the run:
  - `blake3(sorted(rust_source_files) + alef.toml + alef_version)`

  `alef generate` computes this hash once and writes the same value into every alef-headered file. `alef verify` recomputes the same input hash and compares it to the disk hash without inspecting any file body. As a result, downstream formatters (rustfmt, rubocop, dotnet format, spotless, biome, mix format, php-cs-fixer, taplo, ruff, …) can reformat alef-generated content freely without breaking verify — only changes to the generation inputs invalidate the embedded hash. The previous output-deterministic semantics caused `alef verify` to flag bindings as stale on every commit in repos with active language formatters.

  Migration: after upgrading, every existing alef-generated file still carries the old per-file hash. Run `alef generate` once (then `alef e2e generate` if the repo uses `[e2e]`) to refresh all embedded hashes — a single regenerate pass writes the new uniform input hash everywhere.

- **`alef verify` ignores `--lang`, `--compile`, `--lint`**. The flags are still accepted for backwards compatibility but no longer affect the check, since verify no longer regenerates per-language. Use `alef build`, `alef lint`, `alef test` for those concerns.

### Added

- **`alef_core::hash::compute_generation_hash(sources, config_path, alef_version)`** — public function exposing the canonical input-hash recipe so other consumers (cache invalidation, custom build tools) can reuse it. `alef-cli`'s `cache::generation_hash` is now a thin wrapper passing `env!("CARGO_PKG_VERSION")` for the version dimension.

### Fixed

- **Bare `Path` resolved to `Named("Path")` and silently sanitized to `String`**: `alef-extract`'s `resolve_path_type` only recognized `PathBuf` (and `&Path` via the reference path). Functions taking `path: impl AsRef<Path>` resolved the inner generic arg `Path` to `Named("Path")`, then `sanitize_unknown_types` (which considers stdlib `Path` an unknown user type) replaced it with `TypeRef::String` and marked the param as sanitized. The result was that every binding emitted "Not implemented" stubs for `extract_file`, `extract_file_sync`, and any other API using `impl AsRef<Path>`. Bare `Path` now maps to `TypeRef::Path` like `PathBuf`.
- **PHP backend stubbed sanitized functions even when the params were JSON-roundtripable**: `alef-backend-php`'s `gen_function_body` short-circuited to `gen_stub_return` whenever `func.sanitized` was true, ignoring the existing serde-roundtrip helper. `gen_php_serde_let_bindings`, `gen_php_named_let_bindings`, and `gen_php_call_args_with_let_bindings` now handle `Vec<String>` params that came from `Vec<tuple>` (each element is a JSON-encoded tuple), and the function/method body generators fall through to the serde path when a recovery is possible. Affects all batch APIs (`batch_extract_*`).
- **Magnus (Ruby) backend stubbed every extraction function**: the shared `can_auto_delegate_function` rejected functions with a non-opaque Named ref param (e.g. `&ExtractionConfig`), so `extract_bytes`, `extract_bytes_sync`, and 19 other functions emitted "Not implemented" stubs even though the existing deser-preamble could handle them. `alef-backend-magnus` now adds a magnus-specific `magnus_serde_recoverable` gate that allows JSON-roundtrip on Named non-opaque params and on sanitized `Vec<String>` (originally `Vec<tuple>`), and uses `gen_call_args_with_let_bindings` so the call site borrows `&{name}_core` correctly.
- **C FFI backend stubbed all functions with sanitized params**: `alef-backend-ffi` set `will_be_unimplemented = func.sanitized` and bailed out before parameter conversion, even though the existing `Vec` JSON deserialization path could handle `Vec<String>` params with type inference at the call site. The FFI now exempts sanitized functions from the unimplemented bail-out when every sanitized param is a recoverable `Vec<String>` (with `original_type` set), routing them through the standard JSON-roundtrip Vec conversion. Affects `kreuzberg_batch_extract_*` and other tuple-batch FFI exports consumed by the Go, Java, and C# bindings.
- **R (extendr) backend panicked with `todo!("async not supported by backend")` for every async function**: extendr was configured with `AsyncPattern::None`. R is single-threaded but async functions are still callable from R via a per-call tokio runtime. The backend now uses `AsyncPattern::TokioBlockOn` and reports `supports_async: true`, generating `tokio::runtime::Runtime::new()?.block_on(...)` wrappers like the other sync-on-async backends.

### Known limitations

- Functions whose return type is a Named struct from outside the API surface (e.g. `get_preset` returning `&'static EmbeddingPreset`) are still stubbed: alef has no way to know whether the core type derives `Serialize`, so it can't safely JSON-roundtrip the value back to the binding side. The new `return_sanitized` IR field is laid down for a future fix.

## [0.8.7] - 2026-04-27

A patch fixing a CI-hang in the C# `alef setup` default and replacing the `// TODO: add usage example` placeholder in alef-generated READMEs with a pointer to the main repository docs.

### Fixed

- **`alef setup --lang csharp` walks the entire repo when no .csproj is at the top level**: `dotnet restore packages/csharp` searches for project files recursively under the given directory, including `target/`, `node_modules/`, and other artifact dirs. On CI this took longer than the 600s timeout. The C# setup default now applies the same precondition + `find` strategy as the upgrade default — both `dotnet` and a discoverable `.sln`/`.csproj` (depth 3) must exist, otherwise setup is skipped, and the resolved project path is passed explicitly to `dotnet restore`.
- **README usage examples were `// TODO: add usage example` placeholders**: the alef-generated per-language READMEs in the FFI/WASM crate dirs and `packages/{lang}/` for hardcoded fallbacks now redirect readers to the main repository's documentation instead of emitting a literal TODO.

## [0.8.6] - 2026-04-27

A patch hardening the C# upgrade default introduced in v0.8.5. The original `$(ls ... || find ... || echo …)` chain didn't actually skip when no project file existed — `ls foo/*.csproj 2>/dev/null` always exits 0 with empty stdout, so the `||` fallbacks never triggered and `dotnet outdated` ran with no path argument and errored out at the repo root.

### Fixed

- **C# upgrade falls through and fails when no `.csproj` exists**: alef-core's `update_defaults.rs` C# precondition now requires BOTH `dotnet` AND a discoverable `.sln`/`.csproj` under the output dir (depth 3). When no project exists, the upgrade is skipped (precondition warning) instead of erroring out. The command itself is simplified to just `find … | head -1`, which the precondition has already validated returns a path.

## [0.8.5] - 2026-04-27

A patch fixing two `alef update --latest` (`task upgrade`) failures observed across all four consumer repos.

### Fixed

- **Java `versions:use-latest-releases` aborted on missing rules file**: `alef-core/src/config/update_defaults.rs` unconditionally appended `-Dmaven.version.rules=file://${PWD}/{output_dir}/versions-rules.xml`, but most consumers don't ship that file. Maven aborted with `ResourceDoesNotExistException`, failing the whole upgrade pipeline. The flag is now wrapped in `$([ -f .../versions-rules.xml ] && echo "...")` so it's only appended when the file exists.
- **C# `dotnet outdated packages/csharp` rejected the directory**: `dotnet outdated` requires a `.sln` or `.csproj` path, or a directory that contains one at the top level. Most consumers nest projects under `packages/csharp/<ProjectName>/`, so the call failed with "The directory 'packages/csharp' does not contain any solutions or projects." The default now resolves to the first `.sln`/`.csproj` found under the output dir (depth 3) before invoking `dotnet outdated`.

## [0.8.4] - 2026-04-27

A follow-up to v0.8.3 fixing two cases where downstream tooling reformatted alef-generated `crates/{lib}-wasm/Cargo.toml` and silently invalidated the alef hash header.

### Fixed

- **WASM `Cargo.toml` cargo-sort canonical layout**: `alef-backend-wasm`'s `gen_cargo_toml` previously emitted `[lib]`/`[dependencies]` _before_ the `[package.metadata.*]` blocks and listed dependencies in declaration order. cargo-sort, run as a pre-commit hook in every consumer, then rewrote the file in canonical alphabetical / TOML-section order — invalidating the embedded `alef:hash:` line, so the next `alef verify` reported the wasm Cargo.toml as stale on every commit. The template now emits sections and dependencies in cargo-sort canonical order (`[package]` → `[package.metadata.*]` → `[lib]` → `[dependencies]` sorted alphabetically including the dynamic core crate dep), so cargo-sort is a no-op.
- **WASM cargo-machete unused-dep flag for `serde_json`**: the wasm `Cargo.toml` always declared `serde_json = "1"` because trait-bridge / function generators may use it, but consumers without those code paths would fail `cargo-machete` on every commit. Added `serde_json` to the existing `[package.metadata.cargo-machete] ignored` list alongside the already-ignored `futures-util` and `wasm-bindgen-futures`.

## [0.8.3] - 2026-04-26

A follow-up to v0.8.2 making `alef verify` formatter-agnostic and fixing the Ruby version-sync footgun. Verify is now a pure hash comparison: alef computes the canonical hash at generation time, embeds it in the file header, and on `alef verify` only compares the embedded hash against a freshly-computed hash of the canonicalised generated content. External formatters (php-cs-fixer, rubocop, ruff, biome, …) can reformat the body freely without ever causing a verify diff.

### Changed

- **`alef verify` is now hash-only.** The legacy "diff against on-disk content" path is removed. Generated files carry an `alef:hash:<blake3>` header line; verify reads that hash and compares against the hash of the freshly-generated canonical content. No file body inspection, no normalization-against-disk, no formatter sensitivity. Files without an alef hash header (user-owned scaffolds like `Cargo.toml`, `composer.json`) are skipped — alef has no claim over them.
- **`alef generate` auto-runs `sync_versions`** at the end of every run, so user-owned manifests (gemspec, composer.json, package.json, etc.) track `Cargo.toml` automatically without a separate `alef sync-versions` invocation.

### Fixed

- **Ruby version.rb double-conversion via `[sync].extra_paths`**: when consumers listed `version.rb` in `[sync].extra_paths` (a common pattern for non-default extension layouts), the generic `SEMVER_RE.replace_all` matched the `0.3.0` prefix of the gem-formatted `0.3.0.pre.rc.2` and replaced it with the cargo dash-form `0.3.0-rc.2`, producing the corrupted `VERSION = "0.3.0-rc.2.pre.rc.2"`. The extra-paths handler now recognises `version.rb` and `*.gemspec` filenames and applies the gem-aware replacer (`to_rubygems_prerelease` + targeted regex) instead of the generic semver regex.
- **Ruby gemspec/version.rb gem format**: scaffold (`alef-scaffold`) and binding writer (`alef-backend-magnus`) now both call `alef_core::version::to_rubygems_prerelease` so cargo prereleases like `0.3.0-rc.2` are emitted as RubyGems-canonical `0.3.0.pre.rc.2`. RubyGems rejects the dash form (`gem build` raises `Gem::Version "0.3.0-rc.2" is not a valid version`).
- **PHP stub PSR-12 blank line**: generated `.phpstub` files now emit `<?php\n\n` so php-cs-fixer doesn't reformat the file on every commit.

### Added

- **`alef_core::version` module**: `to_rubygems_prerelease(version: &str) -> String` extracted from the CLI's `pipeline::version` so backends and scaffolds can reuse it without duplicating the conversion logic.

## [0.8.2] - 2026-04-26

A follow-up release to v0.8.1 fixing post-generation formatter invocation. Generated bindings now reliably pass `cargo fmt --check` and `ruff check` in CI.

### Fixed

- **FFI cargo fmt silently no-op'd**: `format_generated` ran `cargo fmt` in `packages/ffi/`, which has no `Cargo.toml`. Generated `crates/{lib}-ffi/src/lib.rs` was therefore never formatted, and CI's `cargo fmt --all -- --check` failed on diffs like `use std::ffi::{CStr, CString, c_char};` vs the rustfmt-canonical `use std::ffi::{c_char, CStr, CString};`. Now runs `cargo fmt --all` from the project root, which formats every generated Rust crate (FFI, PyO3, NAPI-RS, Magnus, ext-php-rs, Rustler, wasm-bindgen) in the consumer workspace.
- **Python ruff missed lint autofixes**: only `ruff format` was run, so generated stubs/wrappers retained `I001` unsorted imports, `F401` unused imports (e.g. `Any`), stale `# noqa: F401` comments on used imports, and `TC008` missing `TypeAlias` annotations on union aliases. `format_generated` now runs `ruff check --fix .` before `ruff format .` in `packages/python/`.
- **Ruby formatter wrong tool**: ran `cargo fmt` in `packages/ruby/`, which has no `Cargo.toml` (the Magnus crate lives under `packages/ruby/ext/{lib}_rb/native/` and is covered by the FFI-driven `cargo fmt --all`). Now runs `rubocop -A --no-server` in `packages/ruby/` to auto-correct generated `.rb` and `.gemspec` files.
- **R formatter wrong tool**: ran `cargo fmt` in `packages/r/`. Now runs `Rscript -e "styler::style_pkg('packages/r')"`.
- **WASM formatter scope tightened**: was `cargo fmt` in `packages/wasm/` (works because the wasm-bindgen crate lives there). Now uses `cargo fmt -p wasm` so the explicit package selection survives if package layout changes.

### Changed

- `FormatterSpec` now holds a `&'static [FormatterCommand]` instead of a single command, so a language can run multiple formatter steps in sequence (used by Python's `ruff check --fix` → `ruff format`). On first failure within a sequence the remaining steps are skipped (warning logged) — formatter errors never fail `alef generate`.
- `work_dir: ""` is now treated as the project root (no path join), so language-agnostic invocations like `cargo fmt --all` can run from the consumer's workspace root.

## [0.8.1] - 2026-04-26

A follow-up release to v0.8.0 focused on closing remaining clippy/build-correctness gaps surfaced by Kreuzberg's full workspace build. All alef-generated bindings (Python, Node, Ruby, PHP, FFI, WASM, Elixir, R, Go, Java, C#) now compile cleanly with `-D warnings` against the kreuzberg verification worktree. Adds an `is_copy` IR flag so the FFI backend can correctly distinguish Copy enums from Clone-but-not-Copy data-bearing enums.

### Added

- **`is_copy` IR flag on `TypeDef` and `EnumDef`**: extracted from `#[derive(Copy)]`. Lets backends distinguish Copy types from Clone-but-not-Copy types when emitting field accessors and method returns.

### Fixed

- **WASM trait-bridge regression**: shared `gen_bridge_trait_impl` emitted `#[async_trait::async_trait]` unconditionally, but `kreuzberg-wasm` has no `async_trait` dep. On non-wasm32 targets the trait declaration uses `#[cfg_attr(not(target_arch = "wasm32"), async_trait)]`, producing E0195 lifetime mismatches and E0433 missing-crate errors. WASM trait bridges now compile inside a private `#[cfg(target_arch = "wasm32")]` mod with a public re-export — the bridge is wasm-only by nature (wraps `wasm_bindgen::JsValue`), so host workspace builds skip the impl entirely. The same `cfg(target_arch="wasm32")` gate is also applied to bridge-using free functions.
- **WASM scaffold deps**: generated `kreuzberg-wasm/Cargo.toml` now ensures `js-sys` is present, and serde-style `Vec<&str>` parameters are correctly bridged via `serde_wasm_bindgen` instead of failing JsValue→Vec deserialization.
- **FFI clone-on-copy lints (~25 occurrences)**: field accessors and method returns previously emitted `Box::into_raw(Box::new(obj.field.clone()))` for every Named-typed field, tripping `clippy::clone_on_copy` on the `Copy` enums (`TableModel`, `ChunkerType`, `LayoutClass`, `BBox`, etc.). Codegen now consults `enum.is_copy` / `type.is_copy` to emit auto-copy/deref for Copy types and `.clone()` for Clone-but-not-Copy types. Non-Clone opaques fall back to a raw-pointer alias.
- **FFI move-out-of-borrow errors (E0507)**: data-bearing enums (`Provider`, `HtmlTheme`, `PdfBackend`, `NodeContent`, `OcrBoundingGeometry`, etc.) are not Copy; previous codegen emitted `Box::new(obj.field)` (no clone), causing E0507. Now correctly emits `.clone()` when `is_clone && !is_copy`.
- **FFI let_and_return in vtable wrappers**: infallible primitive/Duration vtable returns previously emitted `let _rc = unsafe { fp(args) }; _rc`, tripping `clippy::let_and_return`. Skip the binding for that case and emit `unsafe { fp(args) }` as the tail expression.
- **FFI useless conversion in PathBuf::from on Option<PathBuf>**: codegen unconditionally wrapped `cache_dir_rs.map(std::path::PathBuf::from)` even when `cache_dir_rs` was already `Option<PathBuf>`. Now passes through directly.
- **PHP plugin bridge clippy cleanup**: 33 lint errors eliminated across `alef-backend-php`'s trait bridge generator — `let_and_return` in async wrappers, `clone_on_copy` on `*mut _zend_object` (raw ptr is Copy), `useless_conversion` on `&str`/`PhpException` self-conversions, `vec_init_then_push` replaced with `vec![…]` literals, and a duplicate `let texts = texts;` redundant local.
- **Rustler/Elixir useless `.into()`**: NIF function wrappers no longer wrap owned `String` / `Vec<u8>` returns in `.into()` (which clippy flags as `String → String` / `Vec<u8> → Vec<u8>` identity). The `gen_rustler_wrap_return` helper now consults `returns_ref` to decide.
- **Rustler reserved Elixir attribute names**: emitted GenServer modules no longer use Elixir-reserved module-attribute names (`@behaviour`, `@callback`, etc.), preventing `mix compile` errors.
- **Rustler/Go/scaffold misc**: Elixir builtins handling fix, Go cbindgen prefix correction for re-exported types, Cargo `tokio` dep added when async functions are present.
- **Rustler test fixture**: `EnumVariant` test data updated for the new `is_tuple` field added in v0.8.0.
- **PyO3 / WASM static method wrappers**: `wrap_return_with_mutex` now wraps the inner core type in the binding wrapper for `default()` / `from()` static methods on non-opaque types (was previously skipping the conversion via the broken `n == type_name` shortcut, producing 24 `mismatched types` errors in PyO3).
- **PyO3 useless `.into()` on owned String returns**: `wrap_return` in the shared serde path no longer emits `.into()` for `TypeRef::String | TypeRef::Bytes` (both are identity in all backends). Eliminates `.map(Into::into)` chains on `Result<String, _>` returns in generated PyO3 functions like `reduce_tokens`, `serialize_to_toon`, `serialize_to_json`.
- **`useless_conversion` on extracted `From<X> for Y` static methods**: kreuzberg's `From<html_to_markdown_rs::HtmlMetadata> for HtmlMetadata` impl is extracted as a static method on `HtmlMetadata` whose param type is normalized to `Self`. The generated FFI/PyO3 wrapper calls `Y::from(arg: Y)`, which Rust resolves to the blanket `From<T> for T` (identity). The wrapper is preserved for ABI stability; `clippy::useless_conversion` is suppressed at the FFI and PyO3 file level.
- **C# nullable double-coalesce**: visitor field accessors emitted `x?.Property ?? null ?? defaultValue` on already-nullable types. The redundant `?? null` is now dropped.
- **C# Name property on visitor interface**: `IVisitor` was missing the `Name` property on some codegen paths; now always emitted so trait bridge dispatch works.
- **C# csproj scaffolding**: `Kreuzberg.csproj` is scaffolded once into the package subdir (not the language root) and not overwritten on subsequent runs. User-tuned project settings (target framework, package metadata) survive regeneration.
- **Elixir NIF Cargo.toml**: emits `[workspace]` block so the NIF crate is its own cargo workspace, plus `futures-util` dep for the async bridge dispatch path.
- **Java primitive trait method parameters**: trait bridges now correctly box primitives (`int` → `Integer`, `bool` → `Boolean`) before passing through `Object[]` callbacks; primitive return values are unboxed via `.intValue()` / `.booleanValue()` rather than direct cast.
- **Magnus tagged-union `Vec<Named>` field marshalling**: Vec-of-Named fields preserve `Vec<Named>` type in generated Rust enum variants instead of collapsing to `String`. JSON array round-trip now works for tagged unions like `Multi(Vec<Item>)`. Map fields still collapse to `String` for serde_json indirection.
- **Magnus Option<T> double-wrap in kwargs**: kwargs builders no longer emit `Option<Option<T>>` for nullable struct fields when the field type is already `Option<T>` and the kwarg flag also makes it optional. Single-Option emission across kwargs and getters.
- **Go/WASM NodeContext cbindgen prefix**: cbindgen-emitted C struct names use the configured crate prefix (e.g. `KZBNodeContext` instead of `NodeContext`), matching the declared FFI symbol convention.
- **Go binding `futures-util` dep**: generated Go FFI's `Cargo.toml` declares `futures-util` when the IR exposes async functions.

## [0.8.0] - 2026-04-26

This release closes a long-standing gap in alef's polyglot generator: bindings for every supported language now compile cleanly with zero errors and zero warnings against real-world Rust crates that exercise trait bridges, tagged-union enums, async functions, and feature-gated modules. Most of the surface area changes are codegen-internal — public APIs are unchanged — but the cumulative effect is that downstream projects (Kreuzberg in particular) can now consume alef-emitted code without any post-generation patching across Java, C#, Go, Ruby, Elixir, R, WASM, and the previously-stable Python/Node/PHP/FFI bindings.

### Added

- **Trait bridges (Java, C#, Magnus/Ruby)**: full plugin-style register/unregister codegen with proper FFI-side vtable struct names and method dispatch. Previously gated behind `exclude_languages = ["..."]` per-bridge; the generated code is now compile-clean across all backends.
- **Trait bridges (Rustler/Elixir)**: new `LocalPid`-based dispatch using `OwnedEnv::send_and_clear` plus a global oneshot reply registry (`TRAIT_REPLY_COUNTER`, `TRAIT_REPLY_CHANNELS`) for synchronous and asynchronous trait method calls. The `alef-scaffold` Elixir generator emits a companion GenServer module per trait so consumer code can `use` the bridge directly.
- **Auto-format**: `alef generate` now runs the language-native formatter (`ruff format`, `mix format`, `cargo fmt`, `biome format --write`, `gofmt -w`, `php-cs-fixer fix`, `dotnet format`, `google-java-format -i`) on the emitted output by default. `--no-format` opts out. Per-language overrides via `[format.<lang>]` in `alef.toml`.
- **Setup timeouts**: `alef setup` accepts `--timeout <seconds>` (default 600 per language). Per-language `timeout_seconds` settable via `[setup.<lang>]` in `alef.toml`. Hangs in tooling that ignores Ctrl-C now fail cleanly via `try_wait` deadline + `child.kill`.
- **Sync-versions (RubyGems prerelease format)**: `alef sync-versions` now writes Bundler-canonical prerelease strings (`1.8.0-rc.2` → `1.8.0.pre.rc.2`) to gemspecs and `version.rb` files. Previously emitted SemVer dashes were rejected by RubyGems.
- **Sync-versions (README regeneration)**: after manifest updates, alef extracts current IR and regenerates per-language READMEs so embedded version strings (e.g. `<version>1.8.0</version>` in pom.xml snippets) refresh without a separate `alef readme` invocation.
- **Sync-versions (cache invalidation)**: removes `.alef/` after manifest updates so subsequent generation steps see fresh IR.
- **Publish `after` hooks**: `[publish.<lang>] after = "..."` is now executed at the success-path end of `prepare`, `build`, and `package` stages, mirroring the existing `before`/`precondition` hook contract. The `after` field on `PublishLanguageConfig` was previously dead config.
- **Feature-gated extraction**: `alef-extract` now captures `#[cfg(feature = "...")]` annotations on `pub use` re-exports and `pub mod` declarations, propagating the cfg to all re-exported items.
- **WASM feature filtering**: `alef-backend-wasm` reads enabled feature flags from `alef.toml` and automatically excludes types/functions/enums whose cfg references a disabled feature, preventing broken references like `ServerConfig` (gated behind `api`) from leaking into the WASM crate.
- **Orphan file cleanup**: after `alef generate` writes the current binding output, alef scans the per-language output directories for files containing the alef-generated header that are not in the current run's emission set, and deletes them. Prevents stale alef-emitted files from blocking builds when types are removed from the public Rust API.
- **Magnus tuple variant codegen**: tagged-union enum variants like `Multiple(Vec<String>)` now emit valid `Multiple(Vec<String>)` Rust syntax. Previously emitted invalid `Multiple { _0: ... }` struct-style braces.
- **FFI enum from_json/free**: enums that flow through FFI as opaque pointer parameters now get `*_from_json` and `*_free` exports so backends (C#, Java, etc.) can construct enum values across the FFI boundary.
- **C# Directory.Build.props**: emitted on every `alef generate` (always overwritten) with `<Nullable>enable</Nullable>` and `<LangVersion>latest</LangVersion>`. MSBuild auto-imports it from the package directory so the setting survives user edits to `Kreuzberg.csproj`.

### Changed

- **Magnus serde marshalling for tagged-union enum fields**: `Vec<Item>` (where `Item` is a Named type) is now emitted as `Vec<Item>` in the generated Rust enum — preserving JSON array round-trip — instead of being collapsed to `String`. Map fields still collapse to `String` for serde_json indirection. Vec of primitives stays `Vec<T>`.
- **Java facade Optional unwrap**: facade methods declared with non-Optional return types (e.g. `String getPreset(...)`) now call `.orElseThrow()` on FFI results that come back as `Optional<T>`, eliminating the type mismatch between the facade and the Panama FFM layer.
- **C# wrapper `result` shadowing**: emitted local `result` variables are renamed to `nativeResult` to avoid CS0136/CS0841 collisions when the wrapper method takes a parameter named `result` (e.g. `SerializeToJson(ExtractionResult result)`).
- **C# tagged-enum string defaults**: when a serde-tagged enum field maps to `string` in C#, the default is emitted as the variant's JSON tag (`"Plain"`) rather than the non-existent `string.Plain` static.
- **C# async unit returns**: `async Task` methods with unit returns no longer emit `return await Task.Run(() => …);` — the `return` is dropped to satisfy CS1997.
- **Go trait_bridges.go**: now generated independently of the `visitor_callbacks` flag so plugin-style bridges work without enabling visitor codegen. The vtable struct name uses cbindgen's `{CRATE_UPPER}{CratePascal}{TraitPascal}VTable` convention; register/unregister calls use `{prefix}_register_{trait_snake}` / `{prefix}_unregister_{trait_snake}`. Function pointer fields are now wrapped via `(*[0]byte)(unsafe.Pointer(C.export))` so cgo treats them as C function pointer types. JSON-decoded `interface{}` parameters are type-asserted to `map[string]interface{}` before being handed to the impl method. `bool` parameters are converted to `C.uchar` via a conditional rather than direct cast.
- **C# stale-visitor cleanup**: when `visitor_callbacks = false`, alef now deletes `IVisitor.cs`, `VisitorCallbacks.cs`, `NodeContext.cs`, and `VisitResult.cs` from the output dir if they remain from a prior run.
- **Hash injection (XML)**: `alef_core::hash::inject_hash_line` now correctly handles `<!-- … -->` comment style for XML/csproj files; `pipeline::write_files` and `pipeline::diff_files` respect the per-file `generated_header` flag rather than always assuming Rust line comments.
- **alef-cli helpers**: `run_command_captured_with_timeout` uses a deadline-driven `try_wait` poll loop to enforce setup timeouts without adding new dependencies.

### Fixed

- **Magnus**: emits `Vec<T>` and `Map<K,V>` as their actual JSON-shaped types in serde-marshalled enum variants. Collapsing to bare `String` previously broke deserialization of tagged-union variants like `StopSequence::Multiple(Vec<String>)`.
- **Magnus**: deprecated `magnus::value::qnil()` and `magnus::RString::new()` API calls replaced with `Ruby::qnil()` / `Ruby::str_new` in trait bridge and binding emit. Crate-level inner attributes silence the remaining cosmetic warnings.
- **Java**: `gen_facade_class` now wraps FFI Optional returns with `.orElseThrow()` for non-Optional facade signatures (`getPreset`, `detectLanguages`).
- **Java**: `*_TO_JSON` symbol duplicates in `NativeLib.java` are deduplicated via a tracking set, mirroring the existing `_from_json` and `_free` dedup paths.
- **Java**: output-path doubling fixed when `output.java` already contains the `dev/<group>/` prefix.
- **C#**: `_vtable` field promoted to `internal` so cross-class dispatch from the wrapper compiles.
- **C#**: `sizeof(IntPtr)` replaced with the const `IntPtr.Size` so the call site no longer requires `unsafe`.
- **C#**: `bool` parameters cast to `int` (`(arg ? 1 : 0)`) at FFI call sites.
- **C#**: F32 default values emit the `f` suffix; F64 emits the `d` suffix where required for unambiguous typing.
- **C# scaffold**: removed `<NoWarn>` suppression — every generated `.cs` file now has `#nullable enable` and the project enables nullable reference types via `Directory.Build.props`.
- **Rustler**: trait bridge dispatch was previously stubbed; now wired to the reply registry with proper error_constructor template substitution. Atom creation uses `rustler::types::atom::ok()` (the previous `Env::new()` call did not exist).
- **Rustler**: error constructor template now substituted (`{msg}`) into `Err(...)` paths in both sync and async method bodies.
- **CLI**: `alef sync-versions` now updates `packages/ruby/lib/{gem}/version.rb` files in addition to gemspec/pyproject/package.json/pom.xml, completing the previously-incomplete Ruby version sync.
- **e2e/rust**: dropped the unconditional `[workspace]` block from generated `e2e/rust/Cargo.toml`. Parent workspace `[workspace.exclude]` now governs whether cargo trips on two-workspace-roots.
- **Go visitor.go**: regenerated visitor.go now compiles (`HTMHtmNodeContext`/`goVisitText` errors fixed at the alef-backend-go source).
- **Setup pipeline**: closure no longer fights `?`-propagation against rayon's parallel iterator; setup errors now surface with proper context.
- **alef-cli**: setup timeouts now actually kill the spawned process group on deadline rather than logging a spurious "skeleton" error.

### Removed

- C# scaffold no longer emits `<NoWarn>CS1591,CS8618,CS0168,CS0219,CS8625,CS0414,CS8632,CS8866</NoWarn>`. All formerly-suppressed warnings are now fixed at codegen.

## [0.7.11] - 2026-04-26

### Changed

- **E2E**: bump pinned `vitest` in generated TypeScript and WASM `package.json` from `^3.0.0` to `^4.1.5`, matching the version dependabot already pulls into the alef repo's own e2e lockfile.
- **Codegen/Scaffold**: all hardcoded third-party dependency version strings used in scaffold and e2e templates are centralized in `alef_core::template_versions` (110 constants grouped by ecosystem: npm, cargo, maven, gem, packagist, nuget, hex, pypi, cran, precommit). Each const that should auto-bump is annotated with a `// renovate: datasource=...` marker, and a new `renovate.json` at the repo root wires up the custom regex manager so Renovate can open version-bump PRs. Pure refactor — no value changes.
- **Tooling**: `task set-version` now targets the centralized `ALEF_REV` constant in `src/core/template_versions.rs` instead of the stale path in `precommit.rs`.

### Fixed

- **Update**: `alef update --latest` no longer hangs when Node and Wasm are both configured. Commands shared across multiple language default configs (e.g. `pnpm up --latest -r -w`) are now deduplicated — only the first language claiming a command runs it, preventing pnpm lockfile races under parallel execution.
- **Update**: R `upgrade` command now passes `ask = FALSE` to `remotes::update_packages()`, preventing an interactive prompt that blocked the non-interactive runner.
- **E2E (Python)**: skip-reason strings on generated `@pytest.mark.skip` decorators are now escaped before interpolation, so reasons containing quotes or backslashes no longer produce syntactically invalid Python test files.

### Removed

- **Hooks**: `alef-fmt` and `alef-lint` pre-commit hooks. Both used `pass_filenames: false` with a broad `files:` regex, so any matching commit cold-started every configured language toolchain (mvn, dotnet, mypy, etc.) regardless of which file changed — making the hooks unusably slow. The `alef fmt` and `alef lint` CLI commands are unchanged. Scaffold's generated `.pre-commit-config.yaml` no longer emits the removed hook ids.

## [0.7.10] - 2026-04-25

### Fixed

- **Validation**: redundant-default warnings no longer fire for `precondition` fields when the section has custom main commands (format/check/typecheck/command/e2e). Previously, `alef verify` would warn to remove the precondition while simultaneously requiring it — a contradiction.
- **Verify**: legacy README comparison now normalizes whitespace before hashing, preventing false-positive "stale bindings" reports from trailing-space or blank-line differences.

## [0.7.9] - 2026-04-25

### Added

- **Backends**: Kotlin/JVM backend (`alef-backend-kotlin`) — emits `data class` for IR structs, `enum class` / `sealed class` for IR enums, and an `object` wrapping top-level functions. Consumes the same Java/Panama FFM `.so` produced by the Java backend. Function bodies are `TODO()` stubs; FFI bridge wiring lands in Phase 1C. Kotlin/Native and Multiplatform paths deferred to Phase 3.
- **Backends**: Gleam backend (`alef-backend-gleam`) — emits `pub type Foo { Foo(field: T, ...) }` records and `@external(erlang, "<nif>", "<fn>")` declarations targeting the Rustler-emitted Erlang NIF. No new Rust crate generated; Gleam shims an existing Rustler NIF library.
- **Backends**: Zig backend (`alef-backend-zig`) — emits `pub const T = struct {}` types, `pub const E = enum {}` / `union(enum)` enums, and thin wrappers calling into the C ABI via `pub const c = @cImport(@cInclude("<header>.h"))`. Marshalling for non-trivial return types lands in Phase 1C.
- **Core**: `BuildDependency` enum (`None | Ffi | Rustler`) replaces `BuildConfig.depends_on_ffi: bool`. All 11 existing backends migrated; `BuildConfig::depends_on_ffi()` accessor preserved for callers.
- **Core**: `PostBuildStep::RunCommand { cmd, args }` variant — needed by Phase 2 Dart's flutter_rust_bridge codegen step.
- **Core**: Five new `Language` enum variants — `Kotlin`, `Swift`, `Dart`, `Gleam`, `Zig`. All per-language match arms across `src/core/config/` (build/clean/lint/setup/test/update defaults, naming, scaffold, publish, docs, adapters) wired with sensible defaults. `Swift` and `Dart` panic in the registry pending Phase 2.
- **CLI**: `alef verify` now checks README freshness — regenerated READMEs are compared with on-disk files.
- **Hooks**: `alef-readme` pre-commit hook to regenerate README files for all configured languages.
- **Scaffold**: scaffolded `.pre-commit-config.yaml` includes `alef-readme` hook.
- **Config**: top-level `[tools]` section selects per-language tool variants — `python_package_manager` (uv | pip | poetry), `node_package_manager` (pnpm | npm | yarn), and `rust_dev_tools` (list of `cargo install` targets). Per-language pipeline defaults dispatch on these choices.
- **Pipelines**: every per-language default (lint, test, build, setup, update, clean) now declares a POSIX `command -v <tool>` precondition so steps gracefully warn-and-skip when the underlying tool is missing on the user's system.
- **Setup**: Rust `setup` default now installs the full polyrepo dev-tool set — `cargo-edit`, `cargo-sort`, `cargo-machete`, `cargo-deny`, `cargo-llvm-cov` — plus `rustfmt` and `clippy` rustup components. The list is overridable via `[tools].rust_dev_tools`.
- **Init**: `alef init` emits a commented `[tools]` block in the generated `alef.toml` with all alternatives documented.
- **Validation**: `alef.toml` is validated at load time. Custom `[lint|test|build_commands|setup|update|clean].<lang>` tables that override a main command field must declare a `precondition` so the warn-and-skip behavior is preserved on user systems.
- **Scaffold**: FFI Cargo.toml gains a `[dev-dependencies]` block with `tempfile`. Dependency pins audited and policy documented.
- **Config**: per-language `run_wrapper`, `extra_lint_paths`, and `project_file` knobs reduce override boilerplate. `[python] run_wrapper = "uv run --no-sync"` prefixes default tool invocations across lint and test; `[python] extra_lint_paths = ["scripts"]` appends paths to default lint commands; `[csharp] project_file = "Foo.csproj"` (and `[java] project_file = "pom.xml"`) makes default lint/test/build commands target the file instead of the package directory. Each absorbs a common override pattern observed in consumer repos without forcing a full `[lint.<lang>]` redefinition.
- **Validation**: `alef.toml` is now scanned for redundant defaults at load time. When a user-supplied `[lint|test|build_commands|setup|update|clean].<lang>` field equals the built-in default verbatim, alef emits a `tracing::warn!` naming the section and field so users can keep the file minimal.

### Changed

- **Pipelines**: `default_*_config` functions now take a `&LangContext` argument bundling `&ToolsConfig`, `run_wrapper`, `extra_lint_paths`, and `project_file` — replacing the prior `&ToolsConfig`-only signature so per-language defaults can dispatch on every relevant knob.
- **Generate**: post-generation formatting is now best-effort. `alef generate` (and `alef all`) call a new `fmt_post_generate` that swallows three classes of post-gen formatter trouble — a missing tool (precondition miss → "Skipping" warning, language skipped), a failing `before` hook (warning, language skipped), and a non-zero formatter exit (warning, formatter loop continues). Formatters are _expected_ to modify generated files, so non-zero exits there must not abort the run. The explicit `alef fmt` command keeps strict failure semantics.

### Fixed

- **Python**: classify enum imports correctly in generated `api.py` — data enums (tagged unions) and enums not referenced by `has_default` config structs now import from the native module instead of `options.py`, fixing missing-attribute errors at runtime.
- **Python**: emit converter locals in required-first, optional-last order so positional calls to the native function match the pyo3 signature.
- **Python**: generate runtime conversion for data-enum and `has_default` parameters in `api.py` wrappers, including `Vec<…>` and `Optional<Vec<…>>` shapes — list comprehensions are emitted to coerce each element. Previously only scalar (`Named` / `Optional<Named>`) parameters were converted, leaving collection parameters silently un-coerced.
- **Performance (pyo3)**: maintain a parallel `AHashSet` for the opaque-types transitive-closure dedupe check, restoring O(1) membership testing in the hot loop.

## [0.7.8] - 2026-04-25

### Changed

- **Codegen**: extract shared trait bridge helpers (`bridge_param_type`, `visitor_param_type`, `prim`, `find_bridge_param`, `to_camel_case`) into `alef-codegen::generators::trait_bridge`, removing ~600 lines of duplication across 7 backend crates.
- **Codegen**: remove duplicate `format_type_ref` from extendr backend in favor of the shared implementation.
- **Codegen**: consolidate `PYTHON_KEYWORDS` constant — delete duplicate from `alef-codegen::generators::enums`, import from `alef-core::keywords`.
- **E2E**: extract shared `resolve_field()` helper from `rust.rs` and `python.rs` into `codegen/mod.rs`.

### Fixed

- **Codegen**: avoid redundant `.into()` on owned String/Bytes return values in generated bindings.
- **Codegen**: use `to_string_lossy()` for Path return values in generated method wrappers.
- **Codegen**: optimize generated `.map(|val| val.into())` to `.map(Into::into)` in function/method wrappers.
- **Go**: skip functions/methods that use enum types lacking FFI JSON helpers (`_from_json`/`_to_json`/`_free`).
- **Go**: respect `exclude_functions` from FFI config when generating Go bindings.
- **Go**: fix `visitor_callbacks` in trait bridge test config.

## [0.7.7] - 2026-04-25

### Added

- **Config**: top-level `version` field in `alef.toml` to pin the alef CLI version per project. `alef init` now emits this field automatically.
- **Install action**: `install-alef` reads the pinned version from `alef.toml` when input is `"latest"`, falling back to the latest GitHub release if not specified.

### Fixed

- **Config**: use top-level `version` key instead of `[alef]` section to avoid TOML scoping issues where subsequent keys were captured inside the section.
- **Python bindings**: sanitize doc strings with `sanitize_python_doc()` consistently across all generated code (options enums, dataclasses, TypedDict, API functions, exceptions) to prevent ruff RUF001/RUF002 lint errors.
- **Python stubs**: fix test assertions for builtin-shadowing parameter names (`input`, `id`) that now generate multi-line signatures with `# noqa: A002`.

## [0.7.6] - 2026-04-25

### Added

- **Config**: `[alef]` section in `alef.toml` with `version` field to pin the alef CLI version per project (superseded by top-level `version` in 0.7.7).

### Fixed

- **Python bindings**: sanitize doc strings with `sanitize_python_doc()` consistently across all generated code.

## [0.7.5] - 2026-04-25

### Fixed

- **Codegen**: suppress `clippy::redundant_closure` and `clippy::useless_conversion` lints in generated `From` impl blocks.
- **Python bindings**: add `noqa` comments to generated `typing` imports.
- **Python bindings/stubs**: force multi-line function signatures when a parameter name shadows a Python builtin (e.g. `id`, `type`, `input`), and annotate those parameters with `# noqa: A002`.

## [0.7.4] - 2026-04-25

### Added

- **E2E assertions**: `min_length`, `max_length`, `ends_with`, and `matches_regex` assertion types across all language codegen backends (Bash/Brew, C, C#, Elixir, Go, Java, PHP, Python, R, Ruby, WASM).

### Fixed

- **Python bindings**: prevent duplicate `| None` annotation on optional parameters whose base type already contains `| None` (e.g. `Option<Option<T>>`).
- **Python stubs**: use `ends_with("| None")` instead of `contains("| None")` for the same check in type-init, method, and function stubs.
- **Scaffold(C#)**: XML-escape author names in `.csproj` to avoid malformed XML.
- **Scaffold(Java)**: parse `"Name <email>"` author strings into separate `<name>` and `<email>` elements; XML-escape both; omit placeholder email when not provided.
- **Scaffold(PHP)**: derive Composer vendor from GitHub repository URL instead of hardcoding `kreuzberg-dev`.
- **Scaffold(Ruby)**: use double-quoted strings for gemspec authors array.
- **Scaffold**: default repository URL uses `example` org instead of `kreuzberg-dev`.
- **Elixir bindings**: escape parameter names that collide with Elixir reserved words (`do`, `end`, `fn`, etc.) by appending `_val` suffix.

## [0.7.3] - 2026-04-25

### Added

- **Keyword-aware field renaming for Python bindings** — struct fields whose names collide with Python reserved keywords (e.g. `class`, `from`, `type`) are automatically escaped in the generated binding struct (`class_`) while preserving the original name via `#[pyo3(get, name = "class")]` and `#[serde(rename = "class")]` attributes. Configurable per-field via `rename_fields` in language configs.
- Shared keyword list in `alef-core::keywords` with `python_ident()` helper — single source of truth for Python reserved words, replacing duplicated lists in `gen_bindings.rs` and `gen_stubs.rs`.
- `ConversionConfig::binding_field_renames` for keyword-escaped field name substitution in `From`/`Into` conversion impls.
- `gen_struct_with_rename` and `gen_impl_block_with_renames` codegen generators for per-field attribute customization.
- `AlefConfig::resolve_field_name()` for config-driven field name resolution per language and type.
- Scaffold: emit `rust-toolchain.toml` (Rust 1.95) and `.cargo/config.toml` with `wasm32-unknown-unknown` target for WASM projects.
- Scaffold: include WASM target in clippy pre-commit hook instead of excluding it.
- Config: `corepack up` added to Node.js update defaults; `corepack use pnpm@latest` for pnpm upgrade.

### Fixed

- Publish: derive PHP extension name from output path instead of hardcoding.
- Publish: derive crate names from output paths instead of hardcoding.
- FFI: replace deprecated `clippy::drop_ref` lint with `dropping_references`.
- Scaffold: Ruby `Rakefile` template uses correct Bundler 4 API.

## [0.7.2] - 2026-04-24

### Added

- **`alef publish` command group** — vendoring, building, and packaging artifacts for distribution across language package registries (issue #9).
  - `alef publish prepare` — vendors core crate (Ruby, Elixir, R) and stages FFI artifacts (Go, Java, C#).
  - `alef publish build` — cross-compilation-aware build with `--target` support, auto-selects cargo/cross/maturin/napi/wasm-pack per language.
  - `alef publish package` — creates distributable archives (C FFI tarball with pkg-config/CMake, PHP PIE archive, Go FFI tarball).
  - `alef publish validate` — checks version readability, package directory existence, and manifest file presence.
- New `alef-publish` crate with `platform::RustTarget` for parsing Rust target triples and mapping to per-language platform conventions (Go, Java, C#, Node, Ruby, Elixir).
- `[publish]` config section in `alef.toml` with per-language `vendor_mode`, `nif_versions`, `build_command`, `package_command`, `precondition`, `before` hooks, `pkg_config`, `cmake_config`.
- `vendor::vendor_core_only()` — copies core crate, rewrites Cargo.toml via `toml_edit` to inline workspace inheritance and dependency specs, removes workspace lints, generates vendor workspace manifest.
- `vendor::vendor_full()` — core-only + `cargo vendor` of all transitive deps with test/bench cleanup.
- `ffi_stage::stage_ffi()` / `stage_header()` — copies built FFI shared library and C header to language-specific directories.

### Fixed

- CLI: `alef build` now respects `[build_commands.<lang>]` overrides for non-Rust languages.
- CLI: `before` hooks in lint/fmt now fire even when a language has `check`/`typecheck` commands but no `format` commands.
- CLI: `before` hook output (stdout/stderr) is now logged instead of silently discarded.
- Config: Ruby lint defaults use `cd {output_dir} && bundle exec rubocop` instead of running from project root.
- Config: FFI lint defaults search entire output directory instead of assuming `tests/` subdirectory.
- Scaffold: pre-commit config now uses `alef-fmt` + `alef-lint` hooks instead of per-language hooks.
- Codegen(Go): local variable naming, error string conventions, enum acronym rules, `nodeTypeFromC` acronym naming, visitor doc comments, golangci scaffold config.

## [0.7.1] - 2026-04-24

### Added

- Config: `precondition` field on all command configs (`LintConfig`, `TestConfig`, `SetupConfig`, `UpdateConfig`, `BuildCommandConfig`, `CleanConfig`) — a shell command that must exit 0 for the main command to run; skips the language with a warning on failure.
- Config: `before` field on all command configs — command(s) that run before the main command; aborts on failure. Supports `StringOrVec` (single string or list).
- Config: `GOWORK=off` in default Go setup command to avoid workspace interference.
- Config: Maven version rules file reference in default Java update commands.
- CLI: Rust is now a first-class language in `alef build` — builds via configurable `[build_commands.rust]` instead of panicking on missing backend.
- FFI: derive `Copy` and `Clone` on generated vtable structs.

### Fixed

- FFI: trait bridge generation fixes for kreuzberg integration.
- Scaffold: pre-commit config simplification — removed per-language hooks.

## [0.7.0] - 2026-04-24

### Added

- CLI: `alef fmt` command — run only format commands on generated output.
- CLI: `alef update` command — orchestrate per-language dependency updates with sensible defaults. `--latest` flag for aggressive upgrades (incompatible/major version bumps).
- CLI: `alef setup` command — install dependencies per language using per-language defaults or `[setup.<lang>]` config.
- CLI: `alef clean` command — clean build artifacts per language using per-language defaults or `[clean.<lang>]` config.
- CLI: `alef test --coverage` flag — run coverage commands defined in `[test.<lang>].coverage`.
- Config: `StringOrVec` type for lint/update/test/setup/clean/build commands — supports both `format = "cmd"` and `format = ["cmd1", "cmd2"]` in alef.toml.
- Config: `[update.<lang>]` sections in alef.toml with `update` (safe) and `upgrade` (latest) commands.
- Config: `[setup.<lang>]` sections in alef.toml with `install` commands for dependency installation.
- Config: `[clean.<lang>]` sections in alef.toml with `clean` commands for removing build artifacts.
- Config: `[build_commands.<lang>]` sections in alef.toml with `build` and `build_release` commands (replaces hard-coded tool invocations).
- Config: `coverage` field on `[test.<lang>]` — `TestConfig` fields migrated to `StringOrVec`; `command` and `e2e` also accept arrays.
- Config: default lint commands for all 12 languages with autofixes enabled (ruff --fix, rubocop -A, clippy --fix, oxlint --fix).
- Config: default update commands for all 12 languages (cargo update, pnpm up, uv sync, bundle update, composer update, go get, mvn versions, dotnet outdated, mix deps.update, etc.).
- Config: default setup and clean commands for all 12 languages.
- Config: add `exclude_languages` field to `TraitBridgeConfig` for per-language trait bridge opt-out.
- Config: add `exclude_functions` and `exclude_types` fields to `RubyConfig` for per-type/function exclusion.
- Magnus: honor `exclude_functions`/`exclude_types` from `[ruby]` config in binding, conversion, and module init generation.
- Magnus: detect absent `Named` types in enum variant fields and route them through `serde_json` deserialization.
- Rustler: filter trait bridges by `exclude_languages` so excluded bridges are omitted from Elixir output.
- Codegen: handle `EnumVariant` defaults for `String`-mapped fields in Magnus hash constructors.
- Codegen: deduplicate `Bytes` conversion when base conversion already emits `.into()` or `.map(Into::into)`.
- Codegen: handle sanitized and excluded-type fields in enum binding-to-core match arms with `serde_json` deserialization and `Box` wrapping.
- Scaffold (Node): generate `.oxfmtrc.json` (120 printWidth, tabs, import sorting) and `.oxlintrc.json` (correctness=error, suspicious=warn, style=off, typescript+import plugins).

### Changed

- CLI: `alef init` now runs full project bootstrap — generates alef.toml, extracts API, generates bindings, scaffolds manifests, and formats in one command.
- CLI: `alef lint` refactored with `LintPhase` enum, uses `config.lint_config_for_language()` with per-language defaults instead of raw map lookup.
- CLI: `alef generate` and `alef all` use built-in `fmt` instead of prek for post-generation formatting.
- Scaffold (Node): replace Biome with Oxc toolchain — `oxfmt` + `oxlint` in package.json devDeps and scripts.
- Scaffold (Java): strip cosmetic whitespace checks from checkstyle.xml (Spotless handles formatting). Remove WhitespaceAfter, WhitespaceAround, GenericWhitespace, EmptyBlock, NeedBraces, MagicNumber, JavadocPackage.
- Scaffold (Go): update `.golangci.yml` to v2 format with full settings (errcheck exclude-functions, govet enable-all, misspell locale, revive rules, exclusions, formatters).
- Scaffold (pre-commit): replace biome-format/biome-lint hooks with oxlint + oxfmt local hook.

### Removed

- CLI: `run_prek()` and `run_prek_autoupdate()` — prek is no longer auto-invoked during generation. Users can still use prek independently.

### Fixed

- Java: remove dead code left over from modularization (duplicate helpers in `mod.rs`, `helpers.rs`, `marshal.rs`).
- Java: fix empty-line-after-doc-comment clippy warnings in `marshal.rs` and `types.rs`.
- Java: suppress `too_many_arguments` clippy warnings on `gen_main_class` and `gen_facade_class`.
- Java: add missing `is_ffi_string_return()` and `java_ffi_return_cast()` marshal functions.
- Java: fix `RECORD_LINE_WRAP_THRESHOLD` visibility (`pub(crate)`).
- Docs: fix `render_method_signature` line length by splitting parameters across lines.
- Tests: add missing `exclude_languages` field to all `TraitBridgeConfig` test initializers.
- Tests: add missing `exclude_functions`/`exclude_types` fields to `RubyConfig` test initializers.

## [0.6.1] - 2026-04-23

### Fixed

- FFI: generate `{prefix}_unregister_{trait_snake}` extern C function for trait bridges (was documented but missing).
- Codegen: use fully-qualified `std::result::Result` in trait bridge return types.
- Codegen: tuple sanitization, Go `gofmt` column alignment, error gen cleanup.
- Go: `gofmt`-compliant struct field alignment with tab-separated columns.
- Go: apply Go acronym uppercasing (`to_go_name`, `go_type_name`, `go_param_name`) per Go naming conventions.

### Changed

- Tests: align all backend test assertions with actual codegen output (Go, Magnus, NAPI, PHP, PyO3, Rustler, FFI, scaffold).
- Tests: remove unimplemented Magnus plugin bridge tests (visitor bridge tests retained).
- Scaffold: update file count expectations for new tsconfig.json (Node), .golangci.yml (Go), and Java config files.

## [0.6.0] - 2026-04-23

### Added

- Scaffold: add ruff + mypy config and `[dependency-groups] dev` to Python `pyproject.toml`.
- Scaffold: add `biome.json`, `tsconfig.json`, and `@biomejs/biome`/`typescript` devDeps to Node/TypeScript.
- Scaffold: add `.golangci.yml` for Go with standard linter configuration.
- Scaffold: add `.php-cs-fixer.php` for PHP with PSR-12 and PHP 8.2 migration rules.
- Scaffold: add `.credo.exs` for Elixir with strict mode and cyclomatic complexity limit.
- Scaffold: add `.lintr` for R with 120-char line length; add `lintr`/`styler` to DESCRIPTION Suggests.
- Scaffold: add `.editorconfig` for C# with 120-char line length.
- Scaffold (Java): add `versions-rules.xml` filtering alpha/beta/RC/milestone/snapshot versions.
- Scaffold (Java): add `pmd-ruleset.xml` with standard PMD categories and exclusions.
- Scaffold (Java): add `maven-enforcer-plugin`, `maven-checkstyle-plugin`, `maven-pmd-plugin`, `versions-maven-plugin`, `jacoco-maven-plugin` to `pom.xml`.
- Scaffold (Java): add `jspecify` and `assertj-core` dependencies.

### Changed

- Scaffold (Java): update `maven-compiler-plugin` 3.14.0 → 3.15.0, `maven-source-plugin` 3.3.1 → 3.4.0, `maven-site-plugin` 3.21.0 → 4.0.0-M16.
- Scaffold (Java): update surefire argLine with `-XX:-ClassUnloading`, parallel test execution, and 600s timeout.
- Scaffold (Java): change javadoc doclint from `none` to `all,-missing`.
- Scaffold (Java): add whitespace, block, and coding checks to `checkstyle.xml`; add FFI and test suppressions.

### Fixed

- Codegen: fix `Map` with `Named` value conversion in optionalized `binding_to_core` path.
- Codegen: fix WASM `js_sys` import generation, Go `gofmt` compliance, Rust e2e `is_none` assertions, PHP builder pattern.
- Codegen: fix C# bytes pinning and `IntPtr.Zero` guard, Go `gofmt` output, Rust e2e `is_none`, Java e2e camelCase naming.
- Codegen: fix `Vec<String>` `_refs` binding and sanitized tuple-to-vec clone in `core_to_binding`.
- Codegen: fix `Vec<Primitive>` sanitized passthrough and enum sanitized serde round-trip.
- Magnus: fix `funcall` API usage, visitor bridge argument passing, `Vec` conversion, optional field flattening, and `default_types` handling.
- Magnus: simplify `gen_trait_bridge` signature — remove inline error type/constructor args, derive from config.
- PyO3: pass enum fields directly instead of `str()` + map lookup.
- PyO3: add `license`/`credits`/`copyright` to Python builtin names list.
- Rustler: fix Elixir `@spec` line wrapping to respect 98-char formatter default.
- Elixir: add `.formatter.exs` with 120-character line length; update stub wrap threshold to 120.
- Scaffold: add `steep`, `rubocop-performance`, `rubocop-rspec` to Ruby Gemfile template.

## [0.5.9] - 2026-04-23

### Fixed

- Rustler: replace `Pid::spawn_monitor` with `env.call` for sync NIF dispatch; add `Encoder` import.
- Rustler: visitor bridge receive loop — emit `_with_visitor` NIF variant and Elixir-side `do_visitor_receive_loop/1` for trait bridge callbacks.
- Rustler: fix double-optional struct fields — skip outer `Option` wrap when `field.ty` is already `Optional`.
- Rustler: add explicit type-annotated let bindings for named params in static NIF methods to resolve ambiguous `From` conversions.
- PyO3: strong typing for data enums in `options.py` — use concrete union type aliases instead of `dict[str, Any]`.
- PyO3: transitively expand `needed_enums` so nested data enums (e.g. `ContentPart` inside `UserContent`) are defined in `options.py`.
- PyO3: topological sort of data enum union aliases to ensure dependencies are emitted before dependents.
- PyO3: `needs_any` check now scans for `TypeRef::Json` instead of data enum fields.
- PyO3: assert non-optional converters for data enum variant fields.
- PyO3: escape single quotes in string literals and add `noqa` comments for assertion expressions.
- Go: remove zero-argument `New{TypeName}()` constructors from opaque types — handles are created by factory functions, not bare allocators.
- WASM: add `type_contains_json` helper for data enum field scanning.
- E2E (TypeScript): escape single quotes in string literals, handle large integers with `BigInt()`, add `noqa` assertions.
- CLI: simplify post-generation hash logic — store generation hashes from in-memory content pre-formatter instead of re-extracting and re-hashing on-disk files.
- Scaffold: fix Ruby `Rakefile` for Bundler 4 — use `Bundler::GemHelper.install_tasks` instead of `require 'bundler/gem_tasks'`.
- Core IR: add `TypeRef::references_named` method for recursive named type detection.
- Codegen: add `type_contains_json` helper to `binding_helpers` for checking `TypeRef::Json` presence in nested types.

## [0.5.8] - 2026-04-23

### Added

- E2E: `is_false`, `method_result` assertion types and `resolve_call` per-fixture call routing across all 14 language generators.

### Fixed

- PyO3: exclude return types from `local_type_names` in `options.py` (prevents false re-exports).
- PyO3/Magnus: remove stale `type: ignore` comments; add `VERSION` constant to RBS type signatures.
- E2E (Python): add `ruff` per-file-ignores to generated `pyproject.toml`; add `noqa` comments for B017 and S108; add `method_result` helper imports.
- E2E (C): add missing `find_nodes_by_type` and `is_error` check types to `run_query`.
- E2E (Rust): add top-level `is_false` assertion support.
- C#/Elixir: format generated code to pass `dotnet format` and `mix format`.
- FFI: fix doc comment prefixes, duplicate imports, and optional param types.
- Codegen: use serde round-trip for tuple-to-vec `From` impls.
- Codegen: fix async return type wrapping, `Vec` element conversion, and `as_deref` option handling.
- Codegen: fix serde derives for opaque fields, `error_constructor` propagation, `BridgeOutput` fields, and trait bridge generation.
- Codegen: fix `Result` qualification, non-`Result` method returns, complex return types, PHP raw pointer handling, WASM scaffold paths.
- Scaffold: wire `scaffold_output` from config; remove stale `serde(default)` on non-serde structs.
- Python: dedup exception classes in `gen_exceptions_py`.
- NodeContent derive forwarding and test fixture resolution.
- Java: add `final` params, javadoc, `eclipse-formatter.xml`, `checkstyle.xml` scaffold with 120 line width, `spotless-apply` pre-commit hook.
- C#: fix `VisitResult.Continue()` to `new VisitResult.Continue()`.
- Elixir: add `ELIXIR_BUILTIN_TYPES` deny-list, suffix colliding type names with `_variant`.
- PHP: fix double-escaped quotes in `get_property` calls in trait bridge.

## [0.5.7] - 2026-04-22

### Fixed

- Python stubs (.pyi): add `Any` to typing imports (used by `dict[str, Any]` fields).
- Python stubs (.pyi): rename tagged union TypedDict variants with `Variant` suffix to avoid name collision with enum classes (e.g. `ToolChoiceModeVariant` instead of `ToolChoiceMode`).

## [0.5.6] - 2026-04-22

### Fixed

- Go backend: skip streaming adapter methods whose FFI signature uses callbacks (CGO incompatible).
- C# backend: skip streaming adapter methods in P/Invoke declarations and wrapper class generation.
- PHP backend: scaffold generates `[features] extension-module = []` for ext-php-rs compatibility.
- Map value conversion: fix optional fields with Named values in binding-to-core and core-to-binding conversions.

## [0.5.5] - 2026-04-22

### Added

- FFI backend: callback-based streaming via `LiterLlmStreamCallback` function pointer pattern. Replaces the `compile_error!` stub with a working implementation that invokes a callback per JSON chunk.
- FFI backend: `gen_streaming_method_wrapper` generates correct C signature (`client`, `request_json`, `callback`, `user_data` → `i32`).

### Fixed

- WASM backend: streaming adapter return type overridden to `Result<JsValue, JsValue>` (was `Result<String, JsValue>`, causing type mismatch with `serde_wasm_bindgen::to_value`).
- WASM backend: don't underscore-prefix params when adapter body is present (`_req` → `req`).
- Python backend: move type imports out of `TYPE_CHECKING` block — types used in function signatures must be available at runtime.
- Go backend: suppress CGO const warnings for generated enum-to-string functions.
- PHP backend: skip undelegatable methods instead of emitting broken stubs.
- Trait bridge: propagate `error_constructor` to all backends.

## [0.5.4] - 2026-04-22

### Fixed

- NAPI, Magnus, PHP, Rustler backends: skip sanitized methods entirely instead of emitting stubs, panics, or `[unimplemented: ...]` placeholders. Matches PyO3 behavior.
- Scaffold: Ruby `extconf.rb` now sets `cargo_manifest` path for `rb_sys`.
- Scaffold: Ruby and Elixir `Cargo.toml` `[lib]` section now includes `name` and `path` pointing to alef-generated binding source.
- E2E: TypeScript codegen no longer escapes `$` in double-quoted strings (fixes Biome `noUselessEscapeInString` lint).
- Go backend: remove invalid `?` syntax from vtable function pointer assignments.
- Rustler: keep `force_build` on single line for `mix format` compatibility.

## [0.5.3] - 2026-04-22

### Added

- CLI: `alef version` command and `alef --version` flag
- Scaffold: generate `py.typed` marker for Python, `Steepfile` for Ruby, `.d.ts` barrel for TypeScript
- Test coverage: ~500 new tests across alef-docs, alef-codegen, alef-extract, alef-readme, and all backends

### Fixed

- Adapters: streaming body fixes for all backends, core type let bindings
- Magnus: use snake_case for Ruby file paths in `generate_public_api`
- Scaffold: use snake_case for Ruby file paths regardless of `gem_name`
- Go: remove colons from Go parameter syntax in trait bridge
- Fix `error_type` field propagation across all test initializers

## [0.5.2] - 2026-04-22

### Added

- Trait bridges: plugin-pattern support across all 11 backends (issue #4)
  - PyO3: de-hardcoded plugin bridge via shared `TraitBridgeGenerator`
  - NAPI, WASM, PHP, Magnus, Rustler, Extendr: implement `TraitBridgeGenerator` for plugin bridges
  - FFI: vtable + `user_data` ABI with `#[repr(C)]` function pointer struct
  - Go: cgo trampolines wrapping C FFI vtable via `cgo.Handle`
  - Java: Panama FFM upcall stubs wrapping C FFI vtable
  - C#: P/Invoke declarations for C FFI trait bridge registration
- Codegen: shared `TraitBridgeGenerator` trait with `type_paths`, flexible registration, configurable super-trait
- Codegen: `format_param_type` respects `is_ref` for reference-typed params (`&str`, `&[u8]`, `&Path`)
- E2E: HTTP server fixture support for testing HTTP endpoints
- E2E: HTTP test codegen for Python, Node, Ruby, PHP, Elixir
- Scaffold: add `[lints] workspace = true` to all generated Cargo.toml templates
- Extendr: opaque type support with Arc wrappers and mutex types
- Skills: add designing-alef-toml guide with real-world configuration patterns

### Fixed

- Codegen: `gen_bridge_all` now emits constructor so `Wrapper::new()` exists for registration
- Codegen: `gen_bridge_trait_impl` filters `trait_source` methods (super-trait handled separately)
- Codegen: `gen_bridge_plugin_impl` strips body indentation to prevent double-indent
- PyO3: remove `.expect()` panics in `gen_registration_fn`, use graceful fallback
- PyO3: fix async double-`Result` flattening (`spawn_blocking` → `??`)
- NAPI/WASM/PHP/Magnus/Rustler/Extendr: remove `.expect()` panics in `gen_registration_fn`
- Magnus: implement actual registration logic (was a non-functional stub)
- Magnus: add missing async `String` clone for blocking closures
- Magnus/Rustler: fix `build_arg` ordering for optional `&str` (unreachable branch)
- Rustler: replace `spawn_monitor` with direct `Term::apply` for sync dispatch
- PHP: fix `&*self.php_obj` to `&mut *self.php_obj` for `try_call_method`
- WASM: handle poisoned `RwLock` in registration instead of panicking
- FFI: cache `version()` at construction (was leaking via `Box::leak` every call)
- FFI: return error on NUL bytes in serialized params (was `unwrap_or_default`)
- FFI: add `Sync` impl on async `_LocalBridge`
- Go: use correct `ffi_prefix` parameter instead of deriving from `register_fn`
- Go: fix `unsafe.Pointer(handle)` → `unsafe.Pointer(uintptr(handle))`
- Go: fix `outResult` ABI mismatch (emit when `return_type != Unit`, not just `error_type`)
- Go: fix syntax error in `C.GoString(` missing closing paren
- Go: add `FreeUserData` trampoline to prevent `cgo.Handle` leak
- Java: implement actual result serialization (was returning `MemorySegment.NULL`)
- Java: fix use-after-free by storing bridge in static map (not `try-with-resources`)
- Java: uncomment and implement FFI registration call
- Java: conditionally emit super-trait vtable slots based on config
- C#: add `[MarshalAs(UnmanagedType.LPUTF8Str)]` for UTF-8 string marshaling
- C#: change `ref IntPtr outError` to `out IntPtr outError`
- Docs: use filtered IR for API reference generation
- Docs: use `dict[str, Any]` instead of bare `Any` for Python JSON types
- Docs: box Java primitives in `Optional` (e.g. `Optional<Integer>` not `Optional<int>`)
- Docs: emit `async Task<T>` for C# async method signatures
- Docs: use `&str`/`&[u8]` for Rust method params matching free-function style
- Docs: pluralize nouns after stripping `_count` suffix
- Docs: thread `Language` param through `clean_doc_inline`
- Codegen: skip `Default`/`Serialize`/`Deserialize` derives for structs with opaque fields
- Codegen: add serde recovery path to Rustler and PHP backends
- Adapters: emit core type let bindings in Python streaming adapter
- C#: deterministic DllImport ordering in NativeMethods.cs
- CLI: use filtered IR for docs, remove dead unfiltered extraction code

## [0.5.1] - 2026-04-21

### Added

- Extract: per-crate extraction for multi-crate workspaces via `source_crates` config
- IR: `original_rust_path` field for orphan-safe `From` impls
- Codegen: dedup after path mapping, generic/enum generation improvements

### Fixed

- Codegen: Python workspace source path, C# paths/TFM, Go version resolution
- Codegen: only omit opaque inner field for unresolvable generic types (not all sanitized types)
- Codegen: skip serde derives for structs with opaque (Arc-wrapped) fields
- Codegen: use post-mapping `rust_path` for `From` impls, treat data enums as known types
- PyO3: don't import streaming `item_type` that shadows wrapper structs
- PyO3: async stub inference, Mutex default, trait type handling, error path fixes
- PyO3: group consecutive opaque type stubs in `.pyi` output
- Cache: strip all whitespace in content hash for formatter agnosticism
- CI: add `contents: write` permission to publish-homebrew job for bottle upload

## [0.5.0] - 2026-04-21

### Added

- Scaffold: render `extra_dependencies` in scaffold, auto path mappings
- CI: rewrite CI and Publish workflows using shared `kreuzberg-dev/actions` composite actions
- CI: add `typos` (spelling) and `actionlint` (GitHub Actions linting) pre-commit hooks

### Changed

- Publish: replace GoReleaser with native per-platform CLI builds via `build-rust-cli@v1`
- Publish: phased publish pipeline (prepare, validate, check, build, publish, finalize)
- Publish: OIDC authentication for crates.io via `publish-crates@v1`
- Publish: draft-then-undraft GitHub release pattern

### Fixed

- Codegen: opaque `Default` fallback uses `todo!()` instead of `Default::default()` for opaque return types
- PyO3: scan opaque type methods for `HashMap` usage to ensure import is added
- NAPI: pass `opaque_types` to `gen_unimplemented_body` calls
- E2E: make tokio dependency conditional in Rust e2e generator
- Core: remove unused `ahash` dependency, fix `clippy::manual_strip` warning

## [0.4.6] - 2026-04-21

### Added

- Config: `exclude_functions` and `exclude_types` support for Python (PyO3), FFI, and Node (NAPI) backends

### Fixed

- WASM: always import `js_sys` (error converters use `js_sys::Object`/`js_sys::Reflect`)
- WASM: skip free functions referencing excluded types (prevents undefined type references)
- WASM: add `clippy::unused_unit` to allowed lints for generated code
- Node: wire `exclude_functions`/`exclude_types` from config into NAPI gen_bindings
- Python: wire `exclude_functions`/`exclude_types` into both struct generation and pymodule init
- FFI: wire `exclude_functions` into free function generation loop
- Rustler: fix f32/f64 stub return values (`0` → `0.0`)
- Rustler: add `clippy::unused_unit` to allowed lints
- PyO3: add `clippy::map_identity` to allowed lints
- Codegen: use `format!("{:?}", i)` instead of `.to_string()` for sanitized `Vec<String>` conversions (fixes Display trait bound failures)
- Codegen: extend `gen_serde_let_bindings` to handle `Vec<Named>` params (fixes undefined `_core` variables)
- Rustler: Elixir module spec line-wrapping for long type signatures

## [0.4.5] - 2026-04-21

### Added

- Codegen: visitor bridge generation for all backends (Go, Java, C#, Python, Ruby, Elixir, etc.)
- Codegen: `gen_visitor.rs` for Go, Java, C# backends
- E2E: visitor e2e test codegen for 8 languages

### Fixed

- Codegen: NAPI trait_bridge for napi-rs v3 compatibility
- Codegen: PHP trait_bridge for ext-php-rs 0.15 compatibility
- E2E: NodeConfig test fixes

## [0.4.4] - 2026-04-21

### Changed

- CLI: parallelize code generation, build, lint, and test loops with rayon
- CLI: parallelize file writing and diff checks across languages
- CLI: build commands within independent and FFI-dependent groups run concurrently
- CLI: switch rustfmt to stdin-based piping with explicit `--config-path` (thread-safe, respects `rustfmt.toml` from any CWD)
- CLI: lazy-compile fixed regex patterns in version sync with `LazyLock`
- Docs: replace `push_str(&format!())` with `write!()` macro to eliminate temporary string allocations
- Docs: pre-allocate `String::with_capacity(8192)` for markdown output buffers
- PyO3: use `ahash::AHashSet<&str>` instead of `std::collections::HashSet<String>` in code generation hot paths

### Added

- Docs: Rust language API reference generation
- Docs: tuple type detection and per-language rendering (`tuple[A, B]` for Python, `[A, B]` for TypeScript, `(A, B)` for Rust/Go/C#, `Tuple<A, B>` for Java)

### Fixed

- CLI: remove redundant double rustfmt pass on generated Rust files
- CLI: fix post-build file patch race condition by running patches sequentially after parallel builds
- CLI: fix lint/test output truncation — all language output is now printed before reporting errors
- E2E: resolve `field='input'` correctly in TypeScript codegen
- E2E: Tree method mapping, bytes arg type, features, json_object ref
- E2E: Option return handling, `default-features`, `is_none`/`is_some`
- Java: escape `*/` in Javadoc comments; TypeScript codegen updates
- PHP: per-fixture call routing and `json_encode` arg passing in PHP e2e
- PHP: add let-binding support for `Named` ref and `Vec<String>` ref params
- Python: resolve call config per fixture for correct function dispatch
- C#: fix `CS0029` type shadowing, `CS1503` nullable unwrap, `Vec<String>` serde conversion
- C#: add `using System`, fix `CS8910` copy constructor clash for newtype variants
- Rustler: use `_rustler` crate name in `native.ex` (not `_nif`)
- Adapters: use `_core` locals in streaming adapter body (not `.into()` on moved param)
- Codegen: fix codegen for opaque types, ref params, and `Vec<&str>`
- FFI: unnecessary clone removal; NAPI/Rustler/scaffold improvements
- Sync: don't blanket-replace versions in `pyproject.toml` `extra_paths`

### Removed

- Codegen: remove unused `create_env()` and dead `templates` module
- Codegen: remove unused `minijinja` dependency from alef-codegen

## [0.4.3] - 2026-04-20

### Added

- Codegen: trait bridge generation for NAPI, Magnus, Rustler, WASM, and PHP backends
- Codegen: `gen_error_class` for structured Python error hierarchies
- E2E: C codegen backend for e2e test generation
- E2E: Python e2e improvements (fixture handling, codegen updates)

### Fixed

- E2E: various codegen fixes across C and Python backends
- PHP: binding function generation fixes

## [0.4.2] - 2026-04-20

### Added

- Codegen: trait bridge code generation — auto-generate FFI bridges for Rust traits (PyO3 backend)
- Codegen: shared `TraitBridgeGenerator` trait and `TraitBridgeSpec` for cross-backend bridge generation
- Config: `[[trait_bridges]]` table for configuring trait bridge generation per trait
- IR: `super_traits` field on `TypeDef` for trait inheritance tracking
- IR: `has_default_impl` field on `MethodDef` for default method detection
- IR: `CoreWrapper::ArcMutex` variant for `Arc<Mutex<T>>` wrapping
- E2E: standalone mock server binary generation for cross-language e2e tests
- E2E: WASM e2e spawns standalone mock server via `globalSetup`
- E2E: `client_factory` override for WASM instance-method calls
- E2E: `count_equals` and `is_true` assertion types across all language targets
- Verify: blake3 output content hashing for idempotent verify

### Fixed

- PyO3: resolve `Named` types via `rust_path` lookup instead of constructing paths
- PyO3: use `py.detach()` instead of removed `allow_threads` (PyO3 0.28 compat)
- PyO3: fix 5 public API codegen bugs (python_safe_name, string escaping, param ordering, import filtering)
- PyO3: re-export return types from native module, not `options.py`
- NAPI: export enums as types for `verbatimModuleSyntax` compatibility
- C#: rename `record` param to `Value` when it clashes with variant or type name
- C#: add `using System` to `NativeMethods.cs` for `IntPtr`
- Go: marshal non-opaque receivers to JSON instead of using `r.ptr`
- Ruby: normalize dashes to underscores in ext dir paths, add extra `../` for native nesting
- Rustler: use `_rustler` suffix instead of `_nif` for crate name and path
- Codegen: don't apply Vec conversion on non-Vec enum variant fields
- E2E: WASM `globalSetup` paths, `BigInt()` wrapping for u64/i64, `new` constructor usage
- E2E: PHP async function naming, phpunit bump, namespace fixes
- E2E: resolve `input` field correctly for `options_type` import detection
- Scaffold: remove readme from scaffold output
- Sync: version regex matches pre-release suffix to prevent double-append
- Verify: write stubs manifest so output hashes cover stubs

## [0.4.1] - 2026-04-19

### Added

- E2E: `MockResponse` struct on `Fixture` for mock HTTP server support
- E2E: Rust mock server codegen — generates Axum-based mock servers from fixture `MockResponse` data
- E2E: always add `[workspace]` to generated Cargo.toml (prevents workspace inheritance issues)

### Fixed

- Codegen: upstream codegen fixes for liter-llm migration (multiple backends)
- Codegen: additional prek compliance fixes across all backends
- Codegen: add `clippy::allow` attrs to PyO3 and FFI backend generated code
- Codegen: Rustler NIF parameter type conversions and param handling refinement
- Codegen: NAPI — wrap boxed enum variant fields with `Box::new` in binding-to-core conversion
- Codegen: Python `options.py`/`api.py` docstrings on all public classes (D101)
- Codegen: Python D101 docstrings, mypy `dict` type args, exception docstrings
- Codegen: Java tagged union newtype variants use `@JsonUnwrapped`
- Codegen: PHP facade void return
- Codegen: WASM — prefix unused parameters with `_` in unimplemented stubs
- Codegen: handle `Vec<Optional<Json>>` field conversion (`Value` → `String`)
- Codegen: async unimplemented returns `PyNotImplementedError`
- E2E: add `cargo-machete` ignore for `serde_json` in Rust e2e Cargo.toml
- Scaffold: phpstan.neon generation for PHP
- Scaffold: safe `Cargo.toml` handling in `extra_paths` (prevents dep version corruption)
- Scaffold: WASM `package.json` + `tsconfig`, Duration lossy conversion fix
- Scaffold: remove `wasm-pack` `.gitignore` from `pkg/` after build
- Scaffold: don't gitignore `pkg/` — npm needs it for WASM publish
- Version sync: scan workspace excludes correctly
- Overwrite READMEs, e2e, and docs when `--clean` or standalone commands

## [0.4.0] - 2026-04-19

### Added

- Codegen: `Option<Option<T>>` flattening — binding structs use `Option<T>` with `.map(Some)` / `.flatten()` in From impls
- Codegen: RefMut functional pattern — `&mut self` methods on non-opaque types generate clone-mutate-return (`&self → Self`)
- Codegen: auto-derive `Default`, `Serialize`, `Deserialize` on all binding types and enums
- Codegen: builder-pattern methods returning `Self` now auto-delegate instead of emitting `compile_error!`
- Codegen: `can_generate_default_impl()` validation — skip Default derivation for structs with non-Default fields
- Config: configurable `type_prefix` for WASM and NAPI backends, default WASM to `Wasm`
- E2E: Brew/Homebrew CLI test generator (13th e2e language target)
- IR: `returns_cow` field on `FunctionDef` for Cow return type handling in free functions
- IR: `is_mut` field on `ParamDef` for `&mut T` parameter handling
- NAPI: conditional serde derives gated on `has_serde` for structs, enums, and tagged enum structs
- NAPI: tagged enum From impls distinguish binding-struct fields from serde-flattened String fields
- Scaffold: generate CMake config for FFI, add `build:ts` to Node scaffold
- Scaffold: add serde dependency to NAPI and Rustler scaffold templates
- WASM: `exclude_reexports` config for public API generation
- WASM: TypeScript re-export support for custom modules

### Fixed

- Codegen: `TypeRef::Char` in `gen_lossy_binding_to_core_fields` — use `.chars().next().unwrap_or('*')` instead of `.clone()`
- Codegen: optionalized Duration field conversion — handle `has_default` types where Duration is stored as `Option<u64>`
- Codegen: enum match arm field access — use destructured field name instead of `val.{name}`
- Codegen: Default derive conflicts in extendr/magnus backends
- Codegen: convert optional Named params with `.map(Into::into).as_ref()` for non-opaque ref params
- Codegen: pass owned `Vec<u8>` when `is_ref=false` for Bytes params
- Codegen: add explicit type annotations in let bindings to resolve E0283 ambiguity
- Codegen: skip `apply_core_wrapper_from_core` for sanitized fields (fixes Mutex clone)
- Codegen: replace `compile_error!` with `Default::default()` for Named returns without error variant
- Codegen: generate `Vec<Named>` let bindings for non-optional `is_ref=true` params
- Codegen: fix `Option<&T>` double-reference — don't add extra `&` when let binding already produces `Option<&T>`
- Codegen: correct float literal defaults (`0.0f32`/`0.0f64`) in unimplemented body for float return types
- Codegen: handle `&mut T` parameters via `is_mut` IR field — emit `&mut` refs instead of `&`
- Codegen: parse `TypeRef::Json` parameters with `serde_json::from_str()` instead of passing raw String
- Codegen: skip auto-delegation for trait-source methods on opaque types (prevents invalid Arc deref calls)
- Codegen: convert `Vec<BindingType>` to `Vec<CoreType>` via let bindings before passing to core functions
- Codegen: handle `Vec<Optional<Json>>` field conversion (`Value` → `String`)
- Codegen: async unimplemented returns `PyNotImplementedError` instead of `py.None()`
- Codegen: handle `Optional(Vec(Named))` in sync function return wrapping
- Codegen: `#[default]` on first enum variant for `derive(Default)` to work
- Extract: skip `#[cfg(...)]`-gated free functions during extraction
- Extract: prune non-re-exported items from private modules
- FFI: use `std::mem::drop()` instead of `.drop()` for owned receiver methods
- FFI: clone `&mut String` returns before CString conversion
- FFI: deserialize `Optional(Vec/Map)` JSON params and pass with `.as_deref()`
- FFI: handle `Option<&Path>` / `Option<PathBuf>` conversion from `Option<String>`
- FFI: handle `&Value` params by deserializing into owned Value then passing reference
- FFI: add explicit `Vec<_>` type annotations for serde deserialization of ref/mut params
- FFI: handle `returns_cow` — emit `.into_owned()` for `Cow<'_, T>` returns before boxing
- FFI: unwrap `Arc<T>` fields in accessors via `core_wrapper` check
- FFI: respect `is_ref` for `Path` and `String`/`Char` parameters
- FFI: correct codegen for `Option<Option<primitive>>` field getters
- FFI: collapse nested match for `Option<Option<T>>` getters
- NAPI: `Optional(Vec(Named))` return wrapping for free functions
- NAPI: U32 in tagged enum core→binding stays as u32, only U64/Usize/Isize cast to i64
- NAPI: detect and import `serde_json` when serde-based parameter conversion is needed
- NAPI: enum variant field casts use `.map()` for Optional wrapping
- PHP: float literals in `gen_php_unimplemented_body` (`0.0f32`/`0.0`)
- PHP: BTreeMap conversion via `.into_iter().collect()` in binding→core From
- PHP: `Map(_, Json)` sanitized fields use `Default::default()`
- PHP: enum returns use serde conversion instead of `.into()`
- PHP: exclude `PooledString` (ext-php-rs incompatible methods)
- PHP: filter excluded types from class registrations in `get_module`
- PHP: flatten nested `Option<Option<T>>` to single nullable type in stubs
- PHP: optionalized Duration in lossy binding→core conversion
- Scaffold: skip existing files, align Java `pom.xml` with kreuzberg
- Scaffold: remove `compilers` directive for Rustler 0.34+
- Scaffold: remove unused license format arg from C# template
- Scaffold: use `PackageLicenseFile` for C# (NuGet rejects non-OSI licenses)
- WASM: don't add Rust `pub mod` for custom modules
- Docs: add trailing newlines and wrap bare URLs (MD034)
- Docs: shift heading levels down for frontmatter compatibility

### Documentation

- Skills: add `--registry` flag, `[readme]` config, `[e2e.registry]` block, `custom_files` key, Brew e2e target
- Skills: update `alef all` description to include e2e + docs generation
- Skills: add `--set` flag to `sync-versions` documentation
- Skills/READMEs: update pre-commit rev tags to v0.3.5
- README: add `init` and `all` to alef-cli command list

## [0.3.5] - 2026-04-16

### Fixed

- Codegen: deserialize sanitized fields in binding-to-core enum variant conversion
- Codegen: skip trait types in all backend type generation loops
- Codegen: replace glob imports with explicit named imports in PyO3 and FFI backends
- Codegen: generate `From` impls for function return types and nested params
- FFI: generate `from_json`/`to_json` for types with sanitized fields
- FFI: remove unused explicit core imports from FFI backend
- FFI: normalize dashes to underscores in IR `rust_path` for code generation
- CLI: format generated Rust content before diffing in verify/diff commands
- E2E: use configured `class::function` for PHP calls regardless of `result_is_simple`
- Extract: check `lib.rs` as parent module for reexport shortening
- Resolve trait sources in post-processing to handle module ordering
- Normalize dashes to underscores in rust path comparison for `From` impl generation
- Scaffold: add `compilers` directive to Elixir `mix.exs` template
- Bump cbindgen scaffold version from 0.28 to 0.29

## [0.3.4] - 2026-04-15

### Added

- `alef sync-versions --set <version>` — set an explicit version (supports pre-release like `0.1.0-rc.1`)
- `alef verify` now checks version consistency across all package manifests
- `alef-sync-versions` pre-commit hook for automatic version propagation on Cargo.toml changes
- PEP 440 pre-release conversion for Python (`0.1.0-rc.1` → `0.1.0rc1`)
- `reverse_conversions` config flag to gate binding-to-core `From` impls
- E2E registry mode for test_apps generation across all 12 languages
- `alef-docs` fallback description generator for struct fields and enum variants
- `alef all` now includes e2e and docs generation; scaffold reads workspace version
- Elixir `ex_doc` support in scaffold
- Java scaffold: switch to `central-publishing-maven-plugin`; Node scaffold: `optionalDependencies`
- Enriched PHP/Python/Java scaffolds for CI and publishing
- PHP composer.json scaffold: `scripts` section with `phpstan`, `format`, `format:check`, `test`, `lint`, and `lint:fix` commands

### Fixed

- Docs generator: critical type, default value, enum, and error generation fixes across Go, C#, Rust
- Go doc signatures with empty return type; C# double `Async` suffix removed
- FFI codegen cast corrections; README version template rendering
- PHP scaffold autoload and backslash escaping in `composer.json` PSR-4 namespace
- PHP stubs: generate public property declarations on classes (ext-php-rs exposes fields as properties, PHPStan needs them declared)
- PHP stubs: camelCase naming; remove hardcoded `createEngineFromJson` from facade and stubs
- PHP codegen: remove needless borrow in `serde_json::to_value` calls (clippy fix)
- Python stubs: add `# noqa: A002` to constructor parameters that shadow Python builtins (e.g. `id`)
- Python stubs: place `noqa` comment after comma in multi-line `__init__` params
- Python scaffold: removed `[tool.ruff]` section — linter config belongs in root `pyproject.toml`
- Python duration stubs: correct mypy annotations
- Ruby: replace `serde_magnus` with `serde_json`, handle sanitized fields in `From` impls
- Ruby gemspec version sync: match single-quote `spec.version = '...'` (was only matching double quotes)
- Java: `checkLastError` throws `Throwable`; correct Jackson version to 2.19.0
- WASM: `option_duration` handling; added missing `wasm-bindgen-futures` dependency
- WASM codegen: remove unused `HashMap` import
- Node/FFI scaffolds: removed unused `serde` dependency
- Node scaffold: removed unused `serde_json` dependency
- Rustler backend: output path uses `_nif` suffix instead of `_rustler`
- Version sync: recursive `.csproj` scanning, WASM/root `composer.json`, FFI crate name
- Only generate binding-to-core `From` impls for input types, not output-only types
- `path_mismatch` false positive on re-exported types (same crate root + name)
- Add `Language::Rust` to all match arms across codebase
- Rust e2e: `assert!(bool)` for clippy, `is_empty` for len comparisons, unqualified imports
- E2e: add `setuptools packages=[]` to Python registry `pyproject.toml`
- Clippy `field_reassign_with_default` suppressed for Duration builder pattern
- Scaffold Cargo.toml templates: removed unused deps — `pyo3-async-runtimes` (Python), `serde_json` (Node), `tokio` (PHP, FFI), `wasm-bindgen-futures` (WASM), `serde`+`tokio` (Elixir/Rustler) — only include what generated binding code actually uses

## [0.3.3] - 2026-04-14

### Added

- Distributable Claude Code skill for alef consumers

### Fixed

- PHP 100% coverage — `createEngineFromJson`, JSON config e2e support
- PHP snake_case properties, enum scalars, serde casing, camelCase facade
- PHP class registration, property access, per-field attributes
- PHP setters documented, enum serde casing via `serde_json`
- PHP tests updated for `Api` class pattern (no more standalone `#[php_function]`)
- Correct DTO style naming (`typed-dict` not `typeddict`), add `serde_rename_all` to config docs
- Add Credo to Elixir scaffold, PHPStan + PHP-CS-Fixer to PHP scaffold
- Drop unused `tokio` from Python scaffold, add e2e license, split assertions
- Remove unused `serde` dep from Node.js and FFI scaffolds
- PHP and Rustler miscellaneous fixes

## [0.3.2] - 2026-04-13

### Fixed

- Disable `jsonschema` default features (`resolve-http`, `tls-aws-lc-rs`) to remove `reqwest`/`aws-lc-sys` from dependency tree, fixing GoReleaser cross-compilation

## [0.3.1] - 2026-04-13

### Fixed

- Elixir resource registration and enum conversion fixes
- C# opaque handle wrapping, error handling, `Json` to `object` type mapping, `WhenWritingDefault` serialization
- Dropped `x86_64-apple-darwin` from GoReleaser targets (`aws-lc-sys` cross-compile failure)

## [0.3.0] - 2026-04-13

### Added

- Pre-commit hook support for consumer repos (`alef-verify` and `alef-generate` hooks via `.pre-commit-hooks.yaml`)
- Blake3-based caching for stubs, docs, readme, scaffold, and e2e generation — all commands now skip work when inputs are unchanged
- `cargo binstall alef-cli` support via `[package.metadata.binstall]` metadata
- `alef-docs` and `alef-e2e` added to crates.io publish workflow

### Changed

- All workspace dependencies consolidated to root `Cargo.toml` — every crate now uses `{ workspace = true }` for both internal and shared external deps
- Bumped all crates from 0.2.0 to 0.3.0

### Fixed

- Replaced `.unwrap()` with `.extend()` in `to_snake_case` (`alef-adapters`)
- Replaced runtime `todo!()`/`panic!()` with `compile_error!()` in generated code for unimplemented adapter patterns
- Made `PrimitiveType` matches exhaustive, removing `unreachable!()` (`alef-codegen`)
- Added inline SAFETY comments to FFI pointer dereferences (`alef-backend-ffi`)
- Clamped negative Duration values before `u64` cast in NAPI bindings
- Fixed rustler public API test assertions to match Elixir conventions (no parens on zero-arg defs, keyword defstruct)

## [0.2.0] - 2026-04-13

### Added

- `alef e2e` CLI command with `generate`, `list`, `validate`, `scaffold`, `init` subcommands
- E2E test generators for 12 languages (Rust, Python, TypeScript, Go, Java, C#, PHP, Ruby, Elixir, R, WASM, C)
- `alef-e2e` crate with fixture loading, JSON Schema validation, and `FieldResolver` for nested field paths
- `options_type`, `enum_module`, `enum_fields`, `options_via` config support for flexible argument passing

### Fixed

- crates.io version specifier fixes for path dependencies
- Backend-specific e2e test generation fixes across multiple languages

## [0.1.0] - 2026-04-09

### Added

- Initial release with 20 crates in a Cargo workspace
- Full CLI: `extract`, `generate`, `stubs`, `scaffold`, `readme`, `docs`, `sync-versions`, `build`, `lint`, `test`, `verify`, `diff`, `all`, `init`
- 11 language backends: Python (PyO3), TypeScript (NAPI-RS), Ruby (Magnus), Go (cgo/FFI), Java (Panama FFM), C# (P/Invoke), PHP (ext-php-rs), Elixir (Rustler), R (extendr), WebAssembly (wasm-bindgen), C (FFI)
- Async bridging and adapter patterns: streaming, callback bridge, sync function, async method
- Method delegation with opaque type wrapping via `Arc<T>`
- Error type generation from `thiserror` enums with cross-language exception mapping
- Type alias and trait extraction from Rust source
- Blake3-based caching for `extract` and `generate` commands
- CI pipeline: cargo fmt, clippy, deny, machete, sort, taplo
- GoReleaser-based publish workflow with cross-platform binaries and Homebrew tap

## [0.15.34] - 2026-05-11

- fix(magnus): remove problematic re-export loop in native.rb wrapper
- fix(magnus): handle optional String parameters via magnus::Value conversion
