Skip to content

Releases: xberg-io/alef

v0.71.0

Choose a tag to compare

@Goldziher Goldziher released this 26 Aug 23:51
v0.71.0
bcc7425

Fixed

  • Four independent trait-bridge stub defects, one per backend. Go emitted a bare identifier as
    an enum's default return, which is valid only for a constant-backed enum — a sealed-interface or
    struct-shaped enum has no such constant, so the identifier named a type and the compiler
    rejected it as "not an expression"; the default is now constructed according to the enum's real
    Go representation. Java computed its excluded-type set with an empty enum registry, which cannot
    tell a real enum from one the crate's own exclude_types marshals as String; it now passes the
    IR enum names minus the configured exclusions. Swift skipped default-body methods when stubbing,
    but the production backend declares every trait method as a required protocol member (alef cannot
    carry a Rust default body through the IR), so the stub never conformed — and its
    import Foundation heuristic matched only a constructor call, missing a bare type annotation.
    TypeScript gated every import on a plain substring test, so an enum whose name is a prefix of
    another correctly-used name was spuriously imported; import gating is now word-boundary aware.

  • Zig snippet validation could not reach a debug-profile FFI library. Zig snippets are built
    through zig build against the consumer's real build.zig, whose ffi_path build option
    defaults to the release profile. The synthesized snippet build only ever threaded
    .target/.optimize into its b.dependency("binding", .{…}) call, and a top-level -D cannot
    set an option on a .path dependency — so there was no mechanism at all to redirect that path.
    With alef build producing a debug artifact, every Zig snippet failed with unable to find dynamic system library. The validator now resolves the library itself — release preferred,
    debug fallback, never crediting a deps/-only copy — and splices the resolved option into the
    dependency call. When neither profile holds a real library it returns no override rather than
    guessing a path.

  • An Iterate docs operation resolved its per-item fields against the call's result type
    instead of the collection's element type.
    When a per-item field name was also reachable as a
    nested path from the result — e.g. content, reachable as results[].content — the
    result-anchored resolver reproduced that whole path underneath the already-peeled loop variable,
    emitting for result in result.results.iter() { println!("{}", result.results[0].content) }.
    That does not compile in Rust and does not typecheck in TypeScript. Because every backend shares
    one presentation layer, the same wrong accessor shipped in Rust, Python, TypeScript and WASM.
    Per-item fields now resolve through an element-anchored resolver.

  • A TypeScript trait-bridge stub declared Promise<string> for a struct-typed method. A stub
    returning a non-enum struct annotated itself string and returned a bare "{}", so it could not
    satisfy the interface it was passed to regardless of its body. The stub now names the real struct,
    casts through as unknown as, and the import sweep reaches struct names referenced only by that
    cast.

  • alef build staged an FFI library from a profile it never built. find_built_artifact
    hardcoded the release profile, so a plain alef build — which runs cargo build and produces
    the debug artifact — could not find what it had just built. It instead fell through to
    target/release/deps/, whose contents come from whatever other cargo invocation happened to
    compile that crate, under that invocation's feature unification. The staged library was fresh but
    feature-incomplete: linking a consumer program against it failed with dozens of undefined symbols.
    find_built_artifact now takes an explicit BuildProfile and searches only the two uplifted,
    profile-scoped directories; deps/ is consulted solely to name a rejected copy in the error.
    StageFfiLibrary passes the profile the current invocation actually built. Callers with no build
    of their own (alef generate's post-build pass, alef test's e2e staging) try release then debug
    explicitly. Packaging passes Release, matching its always-release contract.

  • A snippet run could report success while most of the corpus was never checked at the level it
    asked for.
    RunSummary now tracks fully_verified — results that reached their requested level
    with no downgrade or capability cap — and the summary leads with Checked at requested level: N/Total (P%). alef snippets check now fails, unconditionally rather than only under --strict,
    when not one single result reached its requested level; alef docs/alef all warn loudly on the
    same condition instead of bailing, because that pipeline cannot guarantee a build ran in the same
    invocation. Related: the Java validator no longer counts a package does not exist symbol cascade
    as real failures when the package was simply never built, the "no snippet session configured"
    diagnostic now names the session target rather than the language, and Python typecheck runs
    the interpreter's own compile check ahead of pyrefly, so a hard IndentationError can no longer
    pass.

  • Four generated-snippet type defects. Python field access now narrows an Optional before
    subscripting it instead of indexing it bare; the C free-function call site routes omitted optional
    arguments through resolve_optional_sentinel, so an IR-declared handle parameter gets the 0
    sentinel rather than NULL; an iterate operation with an empty fields list renders a fallback
    print(item) rather than an empty — and therefore syntactically invalid — Python loop body; and a
    Python adapter wrapper now converts its native return value when options.py publishes that type
    only as a return-only TypedDict, matching the wrapper's own annotation.

  • Generated output that is not formatter-canonical by construction. Three emitters produced
    output a consumer's own formatter rewrites: binding.go's stdlib import block was assembled by
    manual insert-position juggling that mis-ordered one real combination (declared errors, no sync
    functions or non-static methods); e2e main_test.go had an unsorted import block, two one-line
    if err != nil { panic(err) } checks, a one-line go func() { for … } }() drain, and
    gofmt-incorrect + spacing; and the Elixir GenServer template carried a double blank line and a
    pre-joined when clause one column past mix format's limit. Each drift let a consumer's
    gofmt -w or mix format rewrite the file after alef hashed and stamped it, permanently
    stranding it outside alef's ownership. All three now round-trip byte-identically through the real
    formatter, asserted by tests that invoke gofmt/mix format and self-skip when absent.

  • Ownership markers alef itself refused to recognise. The PHP install.sh, R install.R, and
    Node/napi e2e .npmrc emitters hand-spelled an alef-generated marker string that alef's own
    content_has_alef_marker guard does not match. All three are generated_header: false, so the
    hand-written text was the only ownership signal — these files were permanently stranded as
    unowned. They now source their marker from hash::header/hash::STANDARD_HEADER_LINE, and each
    has a test asserting through the real guard rather than a copied literal.

  • package_dir no longer leaks a trailing slash into every path built from it.
    ResolvedCrateConfig::package_dir returned a user's configured [crates.output]/scaffold_output
    string verbatim, so a trailing / produced double-slash paths that alef adopt could never match
    against the real on-disk file. Fixed at the source, which protects the ~35 format!("{pkg_dir}/…")
    call sites across scaffold/languages/ and publish/; scaffold_license_files also now builds its
    LICENSE path with Path::join.

  • Trait-bridge test stubs now satisfy the interfaces they claim to implement, in four
    backends.
    Four independent causes, each a generator re-deriving or hardcoding a fact another
    part of the pipeline already had:

    • Go dropped all four required super-trait methods whenever the super trait was declared in a
      private module and re-exported, because the lookup matched on the extracted rust_path rather
      than the configured, publicly visible path. Java had already hit and fixed this; Go now has the
      same synthetic fallback.
    • Java forced every enum-returning method to String, because the exclusion helper it used
      cannot see enums (they live in a separate registry). It now uses the enum-aware helper, as C#
      already did.
    • Kotlin-Android fell back to a hardcoded, project-specific variant table and otherwise called a
      bare constructor, which is invalid for both of Kotlin's enum lowerings. It now reads the real
      enum registry and emits Type.CONSTANT or Type.Variant as appropriate.
    • Dart took the first variant unconditionally and naively lowercased it, producing a constructor
      tear-off when that variant carried fields and the wrong casing regardless.
      The dispatcher also passed an empty enum slice to the Dart and Swift emitters, so their existing
      enum-default lookups could never succeed at all. Where no fieldless variant exists, the stub now
      warns naming the type and language instead of guessing a value the target compiler rejects.
  • Snippet session locks are keyed by fingerprint, not by config name. alef.toml can point two
    differently-named sessions (a language fallback such as typescript and an explicit
    binding-package target such as node) at the same cwd and manifest. They resolve to one
    physical workspace directory but each name got its own Mutex, so two batch groups that both
    believed they held the session lock wrote into the same snippet_batch_N.ts files concurrently.
    The corruption was worse than the lost work i...

Read more

v0.70.0

Choose a tag to compare

@Goldziher Goldziher released this 26 Aug 07:08
v0.70.0
a6a8f69

[0.70.0] - 2026-08-26

Changed (BREAKING)

  • E2eCodegen::render_snippet_body_with_functions no longer has a default implementation. The
    default discarded the function registry, and the one backend that never overrode it
    (kotlin_android) silently dropped every field of a call's result as a result. Making the method
    required turns forgetting it into a compile error. Migration: any out-of-tree E2eCodegen
    implementation must now implement this method explicitly; the previous default's body is
    equivalent to ignoring the functions argument.
  • Generated Go and Java wrappers change shape for a function returning Option<scalar>. They
    now consult a presence companion before trusting the returned value (see below). Go call sites
    that relied on the returned pointer being non-nil will now correctly receive nil for an
    absent result; Java call sites will now correctly receive Optional.empty(). Code that treated
    the old always-present value as meaningful was already reading a fabricated zero.

Fixed

  • A direct Option<scalar> return can now distinguish None from Some(0). Such a return
    crosses the C ABI as a bare scalar, so absence and a legitimate zero were the same bytes. The C
    ABI now exports a {fn}_has_result presence companion, and Go, Java and Kotlin/JVM consult it
    before trusting the value. Go's defect was worse than ambiguity: its wrapper built a pointer that
    could never be nil, so every None arrived as a real Some(0); Java's Optional.of(result)
    could never be empty, so None reached the facade as Optional[0]. Whether a companion exists
    is asked of ffi::type_map::result_presence_companion_exists — the same predicate that decides
    whether the symbol is exported — so a host binding can never reference a companion the FFI crate
    never emitted. This includes the deliberate owned-receiver exclusion, where the companion's second
    call would find the handle already consumed. C#, Zig and the opt-in Dart ffi style are
    audited but not yet wired
    ; a stance ledger over every Language now pins that set so a new
    backend cannot compile without declaring where it stands.
  • A foreign crate's #[cfg] no longer leaks into generated code. codegen::cfg deliberately
    excludes foreign-crate features from Cargo feature forwarding — forwarding them names a feature
    the core crate does not define and breaks resolution — while variant emission copied the same cfg
    verbatim, so the two halves disagreed. Both now ask one authority,
    is_host_owned_rust_path. A foreign-owned cfg-gated variant's arm is dropped with a named
    warning rather than emitted behind an undeclared feature. Host-owned variants keep their gate
    unchanged. Swift's __alef_{enum}_from_swift_string helper turned out to have carried no cfg
    guard at all, for host or foreign variants — an unguarded reference to a variant that may not
    exist — and now gates host variants and drops foreign ones.
  • alef built only on Rust 1.95 or newer while advertising 1.85 and declaring 1.88. Three e2e
    assertion emitters used if let guards, stable only from 1.95, and rust-toolchain.toml pins a
    far newer toolchain, so no CI job ever compiled the crate at its declared floor. Installing
    0.68.0 from crates.io failed with E0658 on any toolchain below 1.95. The guards are rewritten
    (each was the first arm of its match with a _ pattern and a returning body, so an early
    if let is equivalent), the README now matches Cargo.toml, and a new msrv CI job compiles at
    the version it reads from the manifest.
  • Ownership records that predate the committed manifest are now migrated when queried, not only
    when written.
    A path whose ownership predates the committed .alef-ownership.toml and whose
    content never changed could live only in the gitignored .alef/scaffold-owned-paths.manifest
    indefinitely; clearing .alef then lost the record and alef refused to regenerate the file,
    having no durable proof it had ever written it. One consumer hit this for 48 outputs. Every
    ownership-gated write and alef verify's frozen-file scan already query every unmarkable managed
    path, so an ordinary run now migrates the whole legacy ledger before the cache can be cleared out
    from under it.
  • Compile validation is bounded. A before hook that wandered into a pathological state ran
    until a human killed it — one consumer interrupted a Gradle build after 34 minutes. Diagnostics
    are now truncated head-and-tail with an explicit dropped count rather than streamed in full;
    Swift's module-path resolution, which spawned swift build --show-bin-path with no deadline and
    no process-group teardown while every sibling subprocess had both, is now bounded like the rest;
    and a new docs.snippets.before_timeout_secs lets a package build have its own budget instead of
    sharing one number with every individual snippet compile. Truncation is never silent.
  • alef adopt no longer costs more than the problem it recovers from. Recovering 48 paths
    emitted ~124k tokens. Target matching compiled a fresh glob per candidate path, classification
    and diff rendering repeated per target, and the ownership manifest was read-modify-written once
    per target. One session is now shared across every target of an invocation, and under
    --converged-only — where a drifted file cannot be adopted at all — diff bodies are bounded, with
    the withheld files still named and counted. Without --converged-only nothing is bounded, because
    there the diff is the consent document for a write the command performs. Which files are adopted
    and which create-once seeds are refused is unchanged.
  • Go generated the wrong C symbol for any name that defeats snake-casing. Go composed
    {prefix}_{type_snake}_{method_snake} while the FFI backend exports through
    c_consumer::method_symbol, which leaves the method component verbatim — so parseURLPath,
    utf8Length and _Internal linked against symbols that are exported nowhere. Go now asks
    c_consumer. Separately, Option<Duration> had no arm at all in the return lowering and fell to
    a catch-all emitting unmarshalU64, a helper the generated package never declares, and
    Option<Option<T>> declared one more pointer level than its expression could produce.

Added

  • Every language reference page now documents how to register a trait-bridge plugin. Backend
    gained trait_bridge_registration_surface, implemented by 16 backends, and the docs layer asks
    the backend rather than restating naming. Six templates were parameterised so the emitted name
    and the documented name come from one place and cannot drift; each backend has a test asserting
    the reported surface names a symbol the generated output actually declares. Kotlin/JVM and JNI
    deliberately report nothing — the former emits no registration function at all today, and the
    latter's Java_..._nativeRegister* shims are an ABI the Kotlin/Java side links against rather
    than an API a consumer calls.
  • Docs-only e2e fixtures, for documentation content with no single-call shape — configuration
    discovery, standalone pipelines, multi-step handoffs. Every API reference in such a fixture is
    resolved against the real surface, so a renamed field fails the run, but the fixture is never
    executed or generated into test code. A docs-only fixture is structurally unable to be counted as
    runtime coverage: it is a separate type with no conversion into Fixture, and it publishes under
    its own slug.
  • The generated Android project enables the Gradle build cache. The configuration cache is emitted
    commented out, with the reason: the generated buildAndroidJniLibs task reads gradle.taskGraph
    from inside onlyIf and assigns System.err to Exec.errorOutput, both of which Gradle 9
    rejects by failing the build rather than degrading.

Removed

  • Four enum conversion arm functions with no callers.

v0.69.0

Choose a tag to compare

@Goldziher Goldziher released this 26 Aug 04:28
v0.69.0
2331650

Fixed

  • alef docs parsed docs.snippets.required_languages through a fence-tag-only parser while alef snippets gaps parsed the same key through a session-target-aware one. An entry of node, wasm or kotlin_android was therefore accepted by one command and rejected by the other, so alef all aborted with unknown language: node on a config its own sibling command had already validated. The resolver is now a single authority in snippets::types that both call sites use. The abort also short-circuited docs/snippet validation, so it was masking every finding behind it.
  • napi, wasm: a payload-bearing variant on a default-representation enum silently dropped its payload. An enum with no #[serde(tag/content/untagged)] — for example enum Label { A, B, Custom(String) } — was emitted by napi as a #[napi(string_enum)] with a bare Custom, variant and by wasm as a plain C-style Custom = 1,, discarding the field in both. Both backends now route such enums through the same discriminated-object emitter an explicitly tagged enum already used, so the payload round-trips in both directions. pyo3 was already correct, which is why Python preserved payloads while Node and WASM did not.
  • Generated DTOs for pyo3, napi, magnus, rustler and extendr now deserialize a container-level #[serde(from/into/try_from/transparent)] struct by delegating to the core type's own Deserialize and converting via Into. The derived field-by-field object Deserialize silently disagreed with the real wire shape, which is commonly a positional array. Delegation makes no positional assumption; a struct whose fields cannot round-trip (an unwrapped opaque field, a sanitized non-Cow field, a cfg-gated field) falls back to the derived impl rather than guessing. wasm and the seven FFI-derived backends were already unaffected. Serialize symmetry is deliberately unchanged.
  • The C ABI now emits a has_<field> presence companion for every optional struct field whose return type has no null representationOption<i32/u64/f32/f64/bool/…> and Option<Duration>. Previously None and a legitimate zero-valued Some both returned the same 0/0.0 sentinel with no way to tell them apart, so a field meaning "explicitly disabled" was indistinguishable from one meaning "apply the default". Pointer-shaped types already had a real null and are unchanged.
  • PHP now throws, naming both the field and the offending value, when a string-backed enum field does not match a known variant. It previously substituted the default or first variant, which a consumer's own core-side validate() then ran against and could not detect — an unknown value became a real, plausible, wrong one.
  • Swift's generated enum-from-string helper returns Result instead of panicking. An unrecognised wire string used to panic! inside __alef_<enum>_from_swift_string, unwinding across the swift-bridge FFI boundary, which is undefined behaviour. Every call site now propagates the error, forcing an otherwise-infallible wrapper's return type to Result<_, String> so the failure has somewhere to go.
  • Generated binding↔core conversions no longer silently drop Vec elements that fail to (de)serialize. A failed element keeps its slot instead of vanishing and shifting every later index — a shrinking collection is undetectable from the output shape, while a preserved slot is at least positionally honest.
  • alef snippets check no longer tells you to run alef build for a language that has no docs.snippets.sessions target configured at all. That advice was false — no build could change the result — and it was why running alef build and then alef snippets check produced byte-identical unresolved-dependency counts. The rollup now separates "No session configured" from the real build-ordering case, with distinct remediation for each.
  • Fixed-size arrays of primitives ([u8; N], [f64; N]) now resolve losslessly to Bytes/Vec<T> instead of being sanitized to a lossy String placeholder. resolve_type had no syn::Type::Array arm at all, so every fixed array fell through to a stringified Named type.
  • Sanitized public-API diagnostics are now driven by the sanitizer's recorded rewrite rather than by pattern-matching the String placeholder, so a field genuinely declared String is never conflated with one rewritten to String. Sanitized parameters also record original_type for the first time, which several backends already gated on and which had therefore been inert.
  • The Zig reference pages document []const u8/[]u8 at the wrapper boundary instead of a DTO's Rust type name, by asking the Zig emitter rather than re-deriving the shape. The same mapping error had Zig strings documented as [:0]const u8 — a sentinel-terminated slice that appears nowhere in generated output — for parameters, returns and struct fields alike.
  • A trait implemented from a foreign crate no longer contributes methods to the binding surface. An impl SomeFramework::Trait for Config { fn schema() … } — written to serve OpenAPI generation, a serializer, or any other tool — had its methods lifted into the public binding API, where they sanitized lossily and aborted generation. The trait filter was a denylist of std traits, so every other trait passed. A fully-qualified trait path rooted in a crate that is neither the one being extracted nor any crate contributing a type to the surface is now foreign. A single-segment path (an imported trait) is deliberately unchanged: resolving it depends on module visit order and would drop real methods.
  • The C ABI from_i32 reconstruction helper now carries a variant's #[cfg]. A variant behind #[cfg(feature = "…")] does not exist in a build without that feature, so an ungated match arm naming it was a hard compile error in the consumer's crate. Discriminants stay reserved, so numbering is stable across feature subsets.
  • C# no longer declares a scalar optional return as IntPtr. A function returning Option<u64> is exported by the FFI crate as a raw u64; C# declared the same symbol as IntPtr, read the integer bit pattern as a UTF-8 string pointer and passed it to FreeString — an arbitrary-address free. The pointer-vs-scalar decision now has one owner in ffi::type_map that C# asks; the private copy it replaced had already drifted on Option<Option<Duration>>. A direct Option<scalar> return still cannot distinguish None from Some(0); a presence channel for that position is not yet implemented.
  • kotlin_android documentation snippets no longer silently drop every field of a call's result. It was the one language backend without a render_snippet_body_with_functions override, so it fell to a trait default that discards the function registry; the call then anchored to an unrelated struct sharing its name and the field oracle correctly rejected everything beneath it.
  • Generated reference pages no longer corrupt a URL followed by punctuation. Proxy URL (e.g. "http://proxy:8080", …) rendered as <http://proxy:8080\",> because quotes were not excluded from the autolink match. Balanced parentheses and trailing slashes are still treated as part of the URL.
  • Cross-page reference links are no longer hardcoded to a .md suffix. New [docs].reference_link_style selects suffixed (default, unchanged) or extensionless for documentation sites that route without file extensions.
  • A fenced code block naming a language alef does not generate bindings for — astro, mdx, hcl — no longer fails documentation validation. It is reported at warning severity instead, naming the tag, so a typo like pythn stays visible rather than passing silently. A tag that claims a real binding target and still fails to resolve remains an error.
  • MCP prompts and resources constructed at runtime can now be declared in configuration. docs.mcp.declared covers surfaces built through calls like Prompt::new(…) rather than declared by attribute, which attribute extraction cannot see and therefore reported as nothing missing. Attribute-derived surfaces win on a name collision, and every dropped duplicate is reported once with a count. Consumers who declare nothing see identical output.
  • Sanitized method and function parameters now record original_type, mirroring the field path, so a diagnostic can name the original Rust type. Several backends already gated behaviour on that value and had been inert for parameters.

Added

  • A regression test proving that the functions alef generate actually consults to decide skip-vs-regenerate report a cache miss for byte-identical inputs once the compiled-in alef version changes. Prior coverage only compared two cache keys in isolation and never tied that difference to the real on-disk read path.

v0.68.0

Choose a tag to compare

@Goldziher Goldziher released this 25 Aug 16:15
v0.68.0
bef7c79

Changed (BREAKING)

  • Generated bindings for a &mut DTO parameter now return the updated value. A core function with a &mut T parameter on a non-opaque (serde DTO) type was emitted as an owned by-value parameter returning void: the binding converted the caller's object into an owned intermediate, mutated the intermediate, and dropped it. The call compiled, raised nothing, and silently did nothing observable, in Python, Node, PHP, Go, Java, Kotlin, Dart and Swift. The wrapper now returns the mutated value in all eight.
  • Migration. For a core signature fn tag_record(record: &mut Record), assign the result back over the value you passed in — Python tag_record(record) becomes record = tag_record(record); Node tagRecord(record) becomes record = tagRecord(record); PHP tagRecord($record) becomes $record = tagRecord($record); Go err := TagRecord(record) becomes record, err := TagRecord(record), where record is now a *Record; Java and Kotlin tagRecord(record) become record = tagRecord(record); Dart await tagRecord(record) becomes record = await tagRecord(record); Swift try tagRecord(record: record) becomes record = try tagRecord(record: record). A call site that ignored the previously-void result was already silently broken and needs the assignment added. No call shape keeps working unchanged.
  • Generation now fails, naming the function, for the two &mut DTO shapes a binding has no room to express: more than one &mut parameter, and a &mut parameter on a function that already returns a value. Both previously emitted a binding that accepted the argument and discarded the mutation. Change the core signature to return the updated value itself, or fold both results into one returned type.
  • Unchanged by design: a &mut parameter on an opaque handle type still mutates through the handle, which was already correct; and &mut on String, Vec<T> or a scalar still surfaces as a compile error in the generated Rust rather than a silent no-op. Neither shape was ever silently lossy.

Added

  • alef build --strict: fail the run when a language was skipped because its toolchain is not on PATH, naming each skipped language and the precondition that failed. Off by default (a missing local toolchain still leaves the rest of the build clean); pass it in CI so a skipped-and-never-built language surfaces as a non-zero exit instead of a log line nobody read.
  • Added unit tests pinning is_valid_for_result's intentional permissive/anchored asymmetry directly at the FieldResolver layer (src/e2e/field_access/resolver/classify.rs), so a future accidental over-anchoring of the permissive check is caught by cargo test --lib without needing the full presentation-layer suite.
  • alef e2e snippets-migrate and its coverage driver gained regression coverage for two curated_snippets path-resolution edge cases: an existing_root equal to the configured snippets.output now compares correctly, and a bare * glob that crosses a / into alef's own generated output is refused by name.
  • Kotlin e2e assertions now lower a tagged-union field path (<union>.<variant>.<field>) for ANY single-payload variant the IR resolves, not only the one hand-maintained fixture shape, narrowing via a real when (val v = …) { is <Union>.<Variant> -> { … } } block on both the kotlin and kotlin_android targets. The payload property name is computed from the IR through kotlin_field_name_with_type — the same helper the Kotlin binding backend itself uses — so it can never drift from the emitted binding. Detection reuses FieldResolver::tagged_union_split, the generic primitive Gleam/Dart/Swift already consult; FieldResolver::union_variant_payload is new.
  • FieldSkip::UnionTraversalNotImplementedForKotlin (GeneratorGap): a tagged-union boundary Kotlin detects but cannot yet lower (a multi-field variant, or a union type the IR never anchored) now emits a loud, named, counted skip instead of silently falling through to a flat accessor chain against a sealed class — code that does not compile.
  • TypeDef now records a struct's container-level serde conversion (serde_container_conversion, holding from/into/try_from/transparent), read from #[serde(...)] including through #[cfg_attr(...)]. These attributes were previously parsed for no purpose — extraction read only serde_rename_all — so a struct with a hand-written wire shape (commonly a tuple or array for a small value type) generated an object-shaped binding DTO that silently failed to round-trip at runtime.
  • New ValidationCode::SerdeContainerConversionUnsupported: a struct carrying any of those attributes now raises a named diagnostic instead of quietly generating a binding whose JSON shape disagrees with the core type's real one. Deliberately Warning severity — it never aborts a build, because the remedy today is to exclude the type — and scoped to the languages it actually affects (pyo3, napi, magnus, wasm, rustler, extendr, which re-derive their own local binding struct). The FFI-derived backends (Go, Java, C#, Dart, Swift, Kotlin, Zig) deserialize through the core type's own serde impl and are unaffected, so it does not fire for a consumer targeting only those.
  • A declared since that names a release newer than the crate's own version now raises a Warning (since_newer_than_crate_version) naming the item, the declared since, and the crate version it exceeds; an unparseable since raises a distinct since_version_unparseable rather than passing silently. Both #[alef(since = "...")] and #[deprecated(since = "...")] are checked, across all seven item kinds that carry version metadata. Comparison uses semver::Version::cmp_precedence, not the derived Ord — the latter orders build metadata (1.2.0+build > 1.2.0), which the SemVer spec forbids from affecting precedence.
  • Added codegen::mut_writeback, the single policy module every backend consults to decide whether a &mut parameter needs writing back, which type the binding must return in place of (), and which &mut shapes are unsupported. Backends no longer each answer that question their own way; the generated Rust reference asks it too, so the docs cannot describe a signature the binding does not emit.

Changed

  • validate_call_arg_signatures (unknown fixture arg / missing required parameter) is now Severity::Error and aborts e2e generation; a consumer-fleet survey found zero legitimate call sites it would have flagged.
  • validate_call_module_overrides's Go check (a bare-word overrides.go.module/module is never a resolvable Go import path) is now Severity::Error and aborts e2e generation. The equivalent Java check (a module override that looks like a class, not a package) stays Severity::Warning, since no consumer in the surveyed fleet currently sets that field.
  • kotlin/discriminated.rs::render_discriminated_union_assertion takes the sealed-class variant's payload property name as a parameter instead of assuming a literal name; existing callers pass the previous literal unchanged, so behavior for the hand-maintained fixture shape is identical.
  • The Python api.py facade and the <module>.pyi stub now derive parameter existence, order and optionality from one shared decision (backends::pyo3::py_signature) instead of re-deriving it independently, so the two artifacts cannot drift apart. New agreement tests render both from one fixture and assert identical parameters in identical order, in both the required and the defaulted direction.
  • Extracted the free-function delegation predicate that the WASM, NAPI and shared function generators each need into codegen::generators::can_auto_delegate_function_with_named_let_bindings, replacing two byte-identical private copies.
  • A String/Bytes parameter the source declared by value is no longer documented as &str/&[u8] on the Rust page; the borrow forms are emitted only when the IR records a borrow.
  • Added backends::php::layout::{php_class_output_dir, php_psr4_target} as the single authority for where the PHP userland classes live. The php backend, the scaffolded root composer.json and the e2e composer.json now all read it instead of each re-deriving the directory, so the root and e2e PSR-4 targets cannot name two different trees. The root manifest also honours [crates.php.stubs] output, which it previously ignored while the backend wrote the classes there.
  • FieldResolver::result_relative_path returns Cow<'_, str> instead of &str: the envelope projection it can now prepend is a computed path, not a slice of its input.

Fixed

  • .ai-rulez/skills/binding-audit/SKILL.md and the (now-removed, folded into that skill) binding-audit-pattern rule documented a grep for intentional binding-removal attributes that matched only #[alef::skip] and #[doc(hidden)]. The extractor (src/extract/extractor/helpers/attributes.rs:304-333) accepts three spellings — #[alef::skip], the list form #[alef(skip)], and either nested in #[cfg_attr(...)] (the form in common use, e.g. #[cfg_attr(alef, alef(skip))]) — so the documented grep missed the dominant real-world spelling and would misclassify a correctly-excluded item as a binding gap. The grep now matches all three spellings.
  • Ruby e2e snippet and spec generation no longer double-prefixes an options_type (or adapter request_type) that already names a module. Both generators share one constructor builder (ruby/args.rs::build_args_and_setup), which unconditionally prepended the call's module regardless of whether the configured name already carried one, turning e.g. "Sample::DocumentRequest" into Sample::Sample::DocumentRequest.new(...) in both outputs identically. values::qualify_ruby_type now prepends the module only when the name has no :: already, matching how csharp/go take options_type verbatim.
  • Fixed a false-positive `field's #[serde(default)] value disagrees with its #[...
Read more

v0.67.6

Choose a tag to compare

@Goldziher Goldziher released this 25 Aug 05:59
v0.67.6
ef52e10

Added

  • Codegen::cfg::expand_configured_features, which resolves a configured feature list through the core crate's own [features] table (transitively, skipping dep: and crate/feature tokens) and falls back to the list verbatim when the core manifest cannot be read. The JNI shim generator uses it for both its default-target feature set and each per-target override, so gate evaluation agrees with the manifest alef itself scaffolds.
  • BuildAndroidJniLibs derives its target list from [crates.kotlin_android] abis (the same list that scaffolds the jniLibs/<abi>/ directories) and its manifest from [crates.jni] crate_dir, so the directories alef creates and the directories alef fills cannot name two different sets.
  • Added: buildAndroidJniLibs derives its target list from [crates.kotlin_android] abis (the same list that scaffolds the jniLibs/<abi>/ directories) and its manifest from [crates.jni] crate_dir, so the directories alef creates and the directories alef fills cannot name two different sets.
  • Added: codegen::cfg::expand_configured_features, which resolves a configured feature list through the core crate's own [features] table (transitively, skipping dep: and crate/feature tokens) and falls back to the list verbatim when the core manifest cannot be read. The JNI shim generator uses it for both its default-target feature set and each per-target override, so gate evaluation agrees with the manifest alef itself scaffolds.
  • Add [e2e.call(s).*.overrides.java] module validation: warns when the value's last dot-segment starts with an uppercase letter (looks like a Java class, not a package) — the reported regression that produced import io.xberg.Xberg.*; in generated snippets.
  • Add [e2e.call(s).*] module / overrides.go.module validation: warns when the effective Go import path (override, then [go].module, then the base field) is a bare word with no . or /, since only the standard library resolves that way and this field never names it.
  • Add fixture args vs. IR signature validation: warns when a fixture's effective args (its own, or its resolved call's) name a parameter the Rust function/method signature does not declare, or omit a required parameter with no default. Resolves through the same CallIr/TargetParams seam e2e codegen already uses for argument type lowering, so it silently no-ops when the call is unresolvable or the resolved function is binding_excluded rather than claiming a false positive.
  • Both new checks land as warnings only, not errors — see src/e2e/validate_call_module.rs and src/e2e/validate_call_args.rs doc comments for the consumer-fleet measurements behind that choice.
  • alef snippets audit accepts --config and gained a curated-versus-generated accounting pass: a snippet under an audited root that no coverage ledger records as generated and no curated_snippets declaration claims is reported as UnaccountedSnippet (warning), a declared file is reported positively as curated, and a declaration that claims a path alef generates is an error. The pass is named as skipped, rather than silently omitted, when --config is unset or no coverage ledger records anything as generated.
  • alef snippets check carries the same accounting through its configured audit pass.
  • Added [crates.e2e.snippets].curated_snippets: glob patterns (relative to output) declaring hand-authored snippet files as curated on purpose rather than alef-generated. Resolved into SnippetGenerationReport::curated_paths and into migration::MigrationEntry::curated, so both the generation report and alef e2e snippets-migrate can distinguish a declared, intentional absence of a generated equivalent from a genuine coverage gap.
  • Implemented render_snippet_body for the brew (shell) e2e code generator: documentation snippets for CLI-based bindings now render a single binary subcommand "<url>" --flags line, built from the same call-config resolution the executable brew e2e suite already uses.
  • Add [crates.verify].ignore_ephemeral, a glob-pattern opt-out so alef verify never reports intentionally ephemeral, gitignored generated output (e.g. registry-mode test_apps/) as a permanent "missing generated files" failure; every excluded path is still counted and reported in alef verify's coverage output.
  • Added [crates.e2e.snippets].sample_base_url: the public base URL generated documentation snippets bind for a fixture's mock_url / mock_url_list arguments. It is documentation-only — the executable e2e suite keeps binding the per-fixture mock server — so a project can publish snippets a reader can actually run without changing what its tests talk to. Relative fixture paths ("/pdf/report.pdf") resolve against the mock server for tests and against the configured host for docs, from the same fixture, with no per-fixture edit. An explicit $mock_url placeholder resolves against it too.
  • Add [crates.node].excluded_default_features; scaffold_node_cargo now drops excluded names from both the wrapper's own [features] default = [...] array and the core dependency's explicit features = [...] line, matching the fix already shipped for Ruby/Swift/Dart. Same defect: a target_dep_overrides entry excluding a feature for one cfg target was defeated by the wrapper's own unconditional default forwarding.
  • Add [crates.elixir].excluded_default_features; scaffold_elixir_cargo fixed the same way.
  • Add [crates.php].excluded_default_features; scaffold_php_cargo fixed the same way. The function-gated feature set PHP must always request (php_function_gated_core_features_to_add) is deliberately NOT filtered against the exclusion -- those are hard compile-time requirements of an unconditionally-emitted function, not a default-features convenience.
  • Add [crates.ffi].excluded_default_features; effective_ffi_default_features (the single derivation scaffold_ffi and warn_on_ffi_feature_drift both read) now excludes these names from both the FFI crate's own [features] default = [...] list and the core dependency's explicit features = [...] line, while still declaring them so cargo build --features <name> keeps working.
  • Added: a warning when a field path declared in [e2e].fields, fields_optional, fields_array, fields_method_calls or result_fields is refused for a target because that target's result type declares no such member. The warning names the field, the target language and the config key that declares it. Paths nobody declared — assertion groupings, streaming pseudo-fields, virtual namespace prefixes — stay silent, and no target fails its build over a per-target shape difference.
  • alef snippets gaps now prints a gap-coverage report on every run — snippet roots and files discovered, documentation roots and pages actually opened, references found versus supplied by configuration, and required languages against snippet groups compared — so a "No gaps found." result can no longer read as a wider claim than the check made. A consumer that omitted required_languages, docs_dirs and include_base_paths from its alef.toml previously read a clean gap report for a run in which the language-parity check never executed and not one documentation page was opened.
  • alef snippets gaps now names every unset input (docs_dirs/--docs, required_languages/-L, include_base_paths/--include-base-path) together with the check class its absence disables.
  • alef snippets gaps gained --strict, which fails the run when an unset input left a check class with nothing to compare, so a CI job whose purpose is gap detection cannot go green by being unconfigured. An unset include_base_paths is reported but deliberately not strict-fatal: it makes include targets over-report rather than manufacture a false clean.
  • alef verify now reports its own coverage on every run. Every finding verify produces is a negative claim, so a green result was indistinguishable from a run that examined nothing -- and consumer CI reads it under job names like "Alef-generated bindings freshness" as a whole-tree freshness gate. It is a far narrower claim: only files carrying an alef marker on disk are held to a hash; markerless generated output (.json, .jar, lockfiles) is checked for PATH PRESENCE only, so a present-but-wrong file passes; and anything outside the ownership walk's scan set is never opened at all. Each run now prints the managed surface split into content-verified / present-but-not-content-verified / absent, the files opened versus never examined, unmarked create-once seeds, and marked files the surface does not claim. Follows the alef snippets audit precedent of naming the check class a run skipped instead of printing a bare clean result.
  • Added: [crates.ruby].excluded_default_features, mirroring SwiftConfig/DartConfig. scaffold_ruby_cargo previously forwarded every collect_cfg_features name into the generated wrapper crate's [features] default = [...] array unconditionally, which re-enabled a feature a [crates.ruby].target_dep_overrides entry excluded for a specific cfg target one layer down (Cargo unions feature requests across every dependency edge to the same resolved package regardless of target). The excluded name stays declared (so cargo build --features <name> keeps working) but is dropped from default and from the core dependency's own explicit features = [...] line.
  • The gating itself is unchanged, and is now pinned by tests rather than argued from doc comments. For an unmarkable seed (LICENSE, mvnw, gradlew, .gitkeep -- paths marker_comment_style answers None for), alef adopt --write --clobber-create-once-seeds writes no byte of the file: stamp_for yields None, so the entire adoption is one entry in the committed .alef-ownership.toml. That entry is precisely what write_scaffold_files_report accepts as proof of ownership for an unmarkable path (`owned = has_marker |...
Read more

v0.67.5

Choose a tag to compare

@Goldziher Goldziher released this 24 Aug 15:52
v0.67.5
68084f3

Added

  • Added [crates.e2e.snippets].curated_snippets: glob patterns (relative to output) declaring hand-authored snippet files as curated on purpose rather than alef-generated. Resolved into SnippetGenerationReport::curated_paths and into migration::MigrationEntry::curated, so both the generation report and alef e2e snippets-migrate can distinguish a declared, intentional absence of a generated equivalent from a genuine coverage gap.
  • A curated_snippets pattern that matches zero files, or that matches a path alef itself generates, now fails the run instead of being silently accepted.
  • Implemented render_snippet_body for the brew (shell) e2e code generator: documentation snippets for CLI-based bindings now render a single binary subcommand "<url>" --flags line, built from the same call-config resolution the executable brew e2e suite already uses.

Fixed

  • docs.snippets validation now fails fast, before any toolchain runs, when a language needs a compiled artifact (compile/typecheck/run) but has no configured session that could plausibly have produced one yet -- no session at all, an ambiguous session, or a session with an empty before list. Warns always; under strict, bails immediately instead of spending an hour validating snippets that were doomed from the start (GH #256).

  • alef snippets check --lang <language> (and any other filtered run_validation call) now prepares only the configured sessions its filtered snippet set actually needs, instead of running every configured before build hook regardless of the filter -- a single-language diagnostic no longer pays for every other language's build. Sessions sharing a working directory with a needed one are still prepared together, so the scratch sweep never treats a cohabiting session's live build cache as abandoned.

  • resolve() on an alef.toml with zero [[crates]] entries now returns ResolveError::NoCratesConfigured instead of Ok(vec![]), so alef go-tag, alef validate versions --exit-code, and alef publish validate can no longer silently process zero crates and exit 0.

  • alef check-registry --registry github-release now warns when it verified only that the release exists (no --asset-prefix or --required-assets given), so a CI variable that expanded to nothing is no longer indistinguishable from "all N artifacts are attached"; --registry zig/--registry swift are unaffected since they intentionally check existence only.

  • alef e2e validate now applies the same [e2e].languages fallback (to the crate's scaffolded languages) that alef e2e generate --snippets-migrate and alef test-apps run already applied, so an unset [e2e].languages no longer silently disables the "0 test functions" and "unsupported language" checks. The success message also now distinguishes "no fixtures found" from "N fixtures validated".

  • alef lint now fails when poly is not on PATH instead of warning and reporting a clean run; poly is the entire implementation of alef lint, so there was no partial coverage to report.

  • alef release-metadata --targets now rejects a CSV that trims to non-empty but splits into zero real target tokens (e.g. ,,, or a lone ,), which used to resolve identically to the deliberate --targets none (release_any: false, exit 0, no diagnostic).

  • Fixed the FFI feature-drift warning comparing each binding's configured feature set against [crates.ffi]'s configured set instead of the effective default set ([crates.ffi]'s configured features unioned with every feature discovered from emitted #[cfg(feature = "X")] gates) that scaffold_ffi actually writes into the generated FFI crate's Cargo.toml. Two configured lists could match while the effective set was a strict superset, and the warning stayed silent through that drift.

  • Added codegen::cfg::effective_ffi_default_features as the single derivation of the FFI crate's effective default feature set, used by both scaffold_ffi and warn_on_ffi_feature_drift so the two can no longer disagree.

  • warn_on_ffi_feature_drift now distinguishes unsafe host-only features (configured for a binding but absent from the FFI crate's effective default set, which can produce glue referencing a symbol the shipped library was never built with) from safe parity gaps (in the effective default set but undeclared by a binding, which the binding simply omits).

  • Fixed FfiTargetDepOverride.default_features (per-target [crates.jni] target_dep_overrides) being ignored by the JNI Cargo.toml scaffolder: a per-target default_features = false never reached the generated [target.'cfg(...)'.dependencies] block, unlike the equivalent FFI crate scaffolding.

  • Fixed snippet session identity so multiple configured targets that resolve to the same Language (e.g. kotlin + kotlin_android, or typescript/node/wasm) no longer collide when they validate the same physical package/working directory. resolve_session_claim now only reports SessionClaim::Ambiguous when same-language candidates validate genuinely different working directories; candidates sharing one directory collapse to a single deterministic SessionClaim::Claimed instead (issue #255).

  • Added SessionIdentity trait (src/snippets/runner/session_resolution.rs) implemented for ValidationSession and SessionSpec, giving session-claim resolution access to a session's working directory alongside its language.

  • Added regression coverage asserting session count collapses to one for kotlin + kotlin_android over one directory and for typescript + node + wasm over one package, plus a control case proving genuinely different directories still resolve as ambiguous.

  • Swift snippet validation now resolves a session's SwiftPM module directories once per run.
    Every snippet previously launched its own swift build --show-bin-path; 32 concurrent warm
    lookups measured 1.33 seconds wall and 9.22 seconds CPU, versus 0.33 seconds wall and 0.26
    seconds CPU for a single lookup, before any swiftc validation work began. The cache is
    keyed on the resolved lookup inputs (package root plus environment) rather than on session
    identity, and holds no global state.

  • alef validate versions --exit-code now asks checks_pass for its verdict instead of
    re-deriving it. The local copy disagreed in both directions: it exited 0 for a crate whose
    check set was EMPTY — the vacuous pass checks_pass explicitly refuses — and it exited 1
    on a blocked_on_publish row, which checks_pass deliberately tolerates because such a
    row is a lockfile entry pinning the crate at the very version being released and cannot
    resolve until that version is published. Failing it made the gate unsatisfiable by
    construction for any repo with a registry-depending test app. --json already reported
    checks_pass, so a single invocation could print "ok": true and still exit 1. The
    blocked_on_publish doc comment, which asserted the opposite and contradicted both
    checks_pass and its tests, is corrected.

  • Fixed alef build/alef generate running the umbrella gradle build (and gradle build -Prelease) for kotlin_android when no [workspace.build_commands.kotlin_android] overlay is declared, instead of the intended gradle assembleDebug/gradle assembleRelease. build_command_for's "gradle" arm matched on the shared bc.tool string, which cannot distinguish Kotlin from KotlinAndroid; it now asks a new shared build_defaults::gradle_build_task(Language, bool) helper, the same one default_build_config uses, so both derivations agree (GH #259). An explicit [workspace.build_commands.kotlin_android] overlay is unaffected and continues to win.

  • Fixed frb_version_check.rs's test module failing to compile on Windows (std::os::unix::fs::PermissionsExt, Permissions::from_mode) by gating the four unix-only items behind #[cfg(unix)], while keeping the four platform-neutral tests running unconditionally on every OS.

  • Fixed swift_shim_return_marshal (the Swift trait-box FFI shim) wrapping an enum-typed trait method return directly in RustString(...), which requires a String argument and does not compile against an enum value; the shim now JSON-encodes the enum via JSONEncoder before wrapping it, decided by consulting ApiSurface::enums rather than the TypeRef::Named discriminant. Struct-typed (JSON) Named returns are unchanged.

  • Fixed the Swift trait-bridge default method stub emitting return "{}" for a has_default_impl method with a non-excluded enum-typed return, which does not type-check against the enum's own declared Swift return type; it now constructs a real case of the enum when the IR has a fieldless variant, falling back to the prior placeholder body otherwise.

  • Fixed the packaging template environment (src/publish/package/template_env.rs) never calling strip_keep_markers, the only built-in render path that did not, so a ~keep marker left in a packaging template would have shipped verbatim into a consumer's package tree.

  • Corrected the swift e2e integration test count_min_on_optional_vec_of_named_uses_native_optional_count, which shipped red through 0.67.3 and 0.67.4. Its premise was stale rather than the codegen: field_needs_json_bridge has no dependence on the parent's opacity and returns true for any optional Vec<_> field, so the JSON-bridged .toString().count shape is correct for both parent kinds. Split into paired opaque-parent and first-class-parent tests so a fix that corrects one shape while regressing the other is caught by whichever arm it breaks.

v0.67.4

Choose a tag to compare

@Goldziher Goldziher released this 24 Aug 12:22
v0.67.4
f3a0323

Fixed

  • a snippet session's before hook is now run once per package instead of once per configured session target. kotlin and kotlin_android both resolve to Language::Kotlin, and typescript/node/wasm all resolve to Language::TypeScript, so several targets routinely describe one physical package and each carried its own copy of that package's hook. Every copy was executed, sequentially, before a single snippet could validate — and when the hook outran timeout_secs, the run paid that whole timeout once per target. Within an activation group a hook whose command and environment match one already attempted now replays that attempt's outcome; failures replay too, preserving the timeout classification every affected target is reported with.

  • run_command no longer outlives the timeout it was given. The budget covered only the wait for the direct child; once that child exited, output collection waited for end of stream on pipes every descendant had inherited, so any process outliving the command — a Gradle daemon, an MSBuild node, an unwaited background job — held the call open indefinitely. A one-second budget was measured taking twenty seconds and still returning success. Output readers now buffer as bytes arrive and the drain gives up at a fixed grace, reporting everything the command actually wrote and tearing down the process group of anything still holding the pipes.

  • SIGINT, SIGTERM and SIGHUP are now forwarded to every snippet subprocess group before alef exits. Snippet children are spawned into their own process group so a timeout can kill the whole tree, which also removed them from the terminal's foreground group: Ctrl-C reached alef and nothing else, so alef exited 130 while the entire hook tree — shell, build wrapper and build daemon — survived and reparented to PID 1, where a stale daemon goes on to poison the next run. A signal already ignored on entry stays ignored.

  • alef verify refuses --compile, --lint and --lang instead of discarding them. All
    three are visible, documented flags (--compile reads "Also run compilation check") that
    the command destructured away, so alef verify --compile exited 0 having compiled
    nothing — indistinguishable from a passing compile check. They now fail with a message
    naming alef build --lang and alef lint --lang, which do implement that work.
    --exit-code is unaffected: it is a hidden, documented no-op. Nothing in the polyrepo
    passes the refused flags today.

  • alef --version no longer reports tree: DIRTY for every binary installed with cargo install --git. Cargo drops a .cargo-ok completion marker into each checkout it creates, and the build stamp classified the working tree with git status --porcelain, which counts untracked files. Every git-installed binary therefore printed the "not reproducible from commit" warning, and a warning that fires on every install is one nobody reads — which is how genuinely dirty output ends up attributed to a commit it cannot be reproduced from.

  • alef snippets gaps now prints a gap-coverage report on every run — snippet roots and files discovered, documentation roots and pages actually opened, references found versus supplied by configuration, and required languages against snippet groups compared — so a "No gaps found." result can no longer read as a wider claim than the check made. A consumer that omitted required_languages, docs_dirs and include_base_paths from its alef.toml previously read a clean gap report for a run in which the language-parity check never executed and not one documentation page was opened.

  • alef snippets gaps now names every unset input (docs_dirs/--docs, required_languages/-L, include_base_paths/--include-base-path) together with the check class its absence disables.

  • alef snippets gaps gained --strict, which fails the run when an unset input left a check class with nothing to compare, so a CI job whose purpose is gap detection cannot go green by being unconfigured. An unset include_base_paths is reported but deliberately not strict-fatal: it makes include targets over-report rather than manufacture a false clean.

  • alef snippets check no longer skips its gap pass silently. With neither docs_dirs nor required_languages configured under [crates.docs.snippets] the pass is still skipped, but the unset keys are now warned about by name, and under strict the skipped pass fails the run instead of reporting no failure.

  • Split src/snippets/gaps.rs unit tests into src/snippets/gaps/tests.rs, dropping the file under the repository's 1,000-line cap and removing its file-size ratchet baseline entry.

  • Java visitor bridge now derives the context struct layout from the IR.
    VisitorBridge.java hardcoded a six-field context — tagName, depth, indexInParent,
    parentTag, isInline — with fixed offsets, a fixed MemoryLayout, and a fixed six-argument
    decodeContext return. Generated Java only compiled when the configured context_type happened
    to be exactly (enum|i32, ptr, i64, i64, ptr, i32); every other shape failed javac with
    constructor <Context> in record <Context> cannot be applied to given types. The layout,
    the field offsets, the Panama value layout per field and the constructor arguments are now
    derived per context type, so any field count, order, and scalar width compiles.

  • The visitor context C ABI is derived once, in codegen::visitor_context_abi.
    The FFI backend's context_c_type / context_field_specs decided the #[repr(C)] shape and
    which fields have no C representation; the Java bridge re-stated that shape by hand and the two
    drifted. Both backends now read the same derivation — field order, scalar widths, #[repr(C)]
    padding, struct size, and the skip decision.

  • Context fields the C struct cannot carry are decoded as Java's own zero value.
    The FFI backend drops fields with no C representation (floats, collections, nested structs, any
    optional that is not Option<String>). The record component still exists, so the Java bridge
    passes null for reference components and the primitive zero for value components rather than
    fabricating a value or refusing to emit the bridge at all — the options record that holds the
    callback references the visitor interface whether or not the bridge exists.

  • A payload-carrying context enum is no longer decoded from its discriminant. The Java binding
    emits tagged and untagged unions as sealed interfaces with no values(), so an ordinal cannot
    reconstruct a variant; such a component now takes the absent value instead of emitting Java that
    does not compile.

  • FieldResolver::accessor and FieldResolver::rust_unwrap_binding each carried a private copy of
    the virtual-namespace strip decision, gated on result_fields.contains(..) where the shared
    result_relative_path asks the broader is_valid_for_result(..). The copies could place the same
    fixture field somewhere the classifiers did not — the defect shape that emitted
    string(result.ActionResults) into a generated Go package. Both now call result_relative_path,
    so accessor emission, is_array, and the zig/brew/C serialized-path navigation share one
    definition of where a field's value lives.

  • An accessor whose virtual prefix hides a field the IR reaches but a hand-maintained result_fields
    omits now strips that prefix, instead of emitting a member access against the virtual label.

  • A result_fields entry the IR marks binding_excluded no longer strips its virtual namespace
    prefix in accessor emission. with_ir_fields already warns that such an entry is a config bug and
    no binding emits an accessor for the field, so neither spelling compiles; the accessor now agrees
    with is_array and the serialized-path generators rather than keeping a private answer.

Investigated whether alef adopt's --clobber-create-once-seeds over-gates an
unmarkable create-once seed. It does not: the gate is correctly protective. Only the
timing stated in the warning was wrong. Bullets below, for ### Fixed.

- `alef adopt`'s create-once-seed warning no longer names the wrong command as the moment of
  loss. It said adopting a seed consents to alef "replacing its contents with a placeholder
  seed on the next generate", but `write_scaffold_files_report`'s `can_skip`
  (`!overwrite && !generated_header && exists && !is_alef_derived_output`) runs before the
  ownership guard and consults no ownership signal, so a plain `alef generate` skips an
  adopted seed exactly as it skips an unadopted one. The replacement lands on the next write
  that passes `overwrite: true` -- an `alef version` scaffold regen, or
  `alef all --clobber-create-once-seeds`. An operator who tested the warning by running
  `alef generate`, saw the file untouched, and concluded the warning was false would have been
  reading accurate output; the loss was simply still days away. The flag help, the
  `NOT ADOPTED -- create-once seeds` stdout block, the per-path `warn!` and the seeds-only
  `bail!` now all name the overwriting regen and say a plain generate skips these paths.

- The gating itself is unchanged, and is now pinned by tests rather than argued from doc
  comments. For an unmarkable seed (`LICENSE`, `mvnw`, `gradlew`, `.gitkeep` -- paths
  `marker_comment_style` answers `None` for), `alef adopt --write --clobber-create-once-seeds`
  writes no byte of the file: `stamp_for` yields `None`, so the entire adoption is one entry in
  the committed `.alef-ownership.toml`. That entry is precisely what
  `write_scaffold_files_report` accepts as proof of ownership for an unmarkable path
  (`owned = has_marker || (!is_markable && is_owned_by_ownership_record(..))`), so the
  adoption is what clears the guard for the next overwriting write. Five tests in
  `cli::commands::adopt::tests::create_once_seeds` measure the bytes on both si...
Read more

v0.67.3

Choose a tag to compare

@Goldziher Goldziher released this 24 Aug 07:45
v0.67.3
93bb512

Fixed

  • e2e/swift: a getter's bridged shape is now read from the binding backend instead of
    re-derived. build_swift_first_class_map tracked Vec<Vec<_>>/Map<_> plus two hand-enumerated
    Option<Vec<Named(..)>> cases, so every other optional Vec was called countable —
    Option<Vec<String>> among them, which really emits fn og_locale_alternates(&self) -> String,
    making the generator emit ?.count against a RustString. It now calls
    field_needs_json_bridge, the same predicate wrappers::getters::emit_getters uses to pick a
    getter's return type, so the two generators can no longer disagree about one field.

  • e2e/swift: two assertion bugs with one cause — the renderer was asked to describe a leaf it
    does not model. The JSON-bridge guard was keyed on the trailing accessor's spelling (a
    length/count/size suffix), so it refused a count on a bridged leaf while emitting an
    indexed accessor against that same leaf — the generator wrote the correct "JSON-bridges it to
    RustString" skip and a broken assertion on adjacent lines. Keying on whether the path steps past
    a bridged leaf at all collapses the suffix, index and wildcard cases into one. Separately,
    field_expr.contains("?.") proves an ANCESTOR was optional and never the leaf, yet took
    precedence over the leaf's own optionality, emitting article()?.publishedTime().toString()
    where publishedTime() returns Optional<RustString>. The leaf's optionality now comes from the
    type cursor.

  • e2e: namespace_stripped_path no longer drops a real struct segment the result_fields
    config omits. Any leading segment absent from that hand-maintained list was treated as a virtual
    namespace prefix and removed, so a consumer who listed a nested leaf without also listing its
    parent had the parent silently stripped and the accessor built on the wrong receiver
    (result.favicons() against a result type with no such field). The IR is now asked instead: the
    enum and collection maps already anchor the call's declared result type, so a first segment that
    type declares as a struct field is a real nested step whatever the config omits. Absent IR still
    answers false, leaving the config-only behaviour intact.

  • e2e/zig: JSON-mode assertions no longer navigate a virtual namespace prefix as a real JSON
    key. A fixture field like batch.completed_count emitted
    result.object.get("batch").?.object.get("completed_count").?, force-unwrapping a key absent
    from every real payload and aborting the generated zig test. The conditional namespace
    stripping — previously duplicated in the brew and C e2e generators — is now
    FieldResolver::result_relative_path, shared by all three. A genuinely nested path
    (metrics.total_lines) still keeps its full chain.

  • docs: rustdoc fence attributes are no longer copied verbatim into generated markdown. A doc
    comment fence of ```rust,no_run produced a page whose fence language was the literal
    rust,no_run — a markdown info string's language is its first whitespace-delimited token —
    which alef snippets audit --docs correctly rejected as an unknown fence language. Recognised
    rustdoc attributes (no_run, ignore, should_panic, compile_fail, test_harness,
    standalone_crate, edition####, ignore-<target>, E####) are dropped; unrecognised comma
    tokens move into the fence's meta slot so the language token stays intact. Consumers could not
    fix this at the source: dropping no_run makes the doctest actually execute.

  • cli: alef snippets audit now names its coverage when no --docs root is given. A
    snippets-only invocation printed a bare Audit clean: no issues found. while the
    documentation-page checks (fence languages, include targets) never ran, so a CI job that
    omitted --docs read green for a check class it had skipped.

  • Wire src/codegen/config_gen/tests/generators.rs into the module tree
    (src/codegen/config_gen/tests.rs was missing mod generators;), so its 18 config-generator
    unit tests actually compile and run. Fixed 14 stale FieldDef/TypeDef struct literals
    predating the version and has_private_fields IR fields, and one test function missing its
    own #[test] attribute -- all silently dead until now (#211).

  • Fix a stale assertion in the Rustler kwargs-constructor test, which asserted the pre-fix
    buggy output (unwrap_or_default(), silently producing "" for a String field with a real
    default) rather than the already-correct unwrap_or("default".to_string()). The generator was
    right; only the expectation was wrong.

  • Remove three dead, never-compiled test files under src/codegen/generators/trait_bridge/tests/
    (spec.rs, type_formatting.rs, helpers.rs). All 41 of their #[test] bodies are byte-identical
    to ones in the wired spec_and_formatting.rs; helpers.rs carried no tests at all.

  • alef generate/alef build now fail loudly, before invoking flutter_rust_bridge_codegen,
    when the flutter_rust_bridge_codegen binary on PATH reports a version that disagrees with
    the project's declared [crates.dart] frb_version pin. Previously the locally installed
    codegen binary's version was baked into generated Dart/Rust bridge output with no check at
    all, so two developers (or a developer and CI) with different flutter_rust_bridge_codegen
    installs produced different committed bytes from identical input (#204).

  • snippets: a snippet that does not compile is no longer reported as unavailable. Every
    is_dependency_error implementation that could not distinguish "the binding package was never
    built" from "the generated code is wrong" now accepts only diagnostics that can mean nothing
    else: Rust E0432/E0433/E0463/E0583 (no longer E0425, E0308, E0599, E0609,
    E0061, or the could not compile summary rustc prints on every failed build), Java package ... does not exist (no longer bare cannot find symbol), C# CS0246/CS0234 (no longer
    CS0103/CS5001), Go cannot find package/no required module (no longer bare undefined:),
    Swift no such module (no longer cannot find ... in scope). Rust, Java and C# additionally
    require every diagnostic in the output to be a dependency diagnostic, matching the TypeScript
    validator. Reclassification took real failures out of the failure tally entirely — 283 Rust and
    51 Java snippets in two consumer repos were counted unavailable, so nothing went red.

  • e2e: a docs snippet no longer emits an accessor for an assertion field that is not a member
    of the call's result. The operations derived from a fixture's own assertions (added in 0.66.x)
    are now filtered through the oracles the assertion renderers already consult, so an error-path
    fixture, a result_is_simple/result_is_bytes call, a streaming pseudo-field
    (stream.has_page_event), an assertion grouping prefix (rate_limit.) and a field the
    availability oracle rejects all fall back to showing the whole result instead of emitting
    result.error(), result.Audio, result.CostTracked or result.stream.hasPageEvent.

  • e2e/rust: a snippet presenting derived fields now binds the result it references and
    unwraps a Result-returning call first. Fixture::has_docs_presentation — the one predicate
    the call emitter consults — could not see assertion-derived operations, so the emitter wrote
    let _ = convert(...) while the snippet printed result.content (E0425).

  • e2e/csharp: indexing an optional collection now emits the same null-forgiving operator as
    reading its .Count, so a single snippet no longer contains both
    result.Metadata.Headings!.Count and result.Metadata.Headings[0].Level (CS8602).

  • e2e/brew: the generated run_tests.sh harness reported PASS when any assertion but the
    last one failed. run_test invoked each test function as the condition of an if, which
    disables errexit for the entire call, so a failing assertion's return 1 no longer aborted
    the function and the function's exit status was just its last command's. Assertion helpers now
    record failures in a per-test counter that run_test consults alongside the exit status, and
    the harness core is emitted from a Minijinja template. Treat every historical brew pass as
    unverified. (#227)

  • e2e/brew: namespace-prefixed fixture fields produced jq paths that never matched the CLI
    payload. Brew built its path from FieldResolver::resolve, which only applies aliases, so a
    field like batch.completed_count — where batch is a virtual grouping label rather than a
    JSON object — became .batch.completed_count, null against every real payload. Brew now
    applies the same namespace stripping the C backend uses; genuinely nested paths whose first
    segment is a declared result field are unchanged. (#228)

  • Vendoring no longer strips a crate's inherited lint configuration. alef publish prepare
    (both VendorMode::CoreOnly and VendorMode::Full, the latter being R/CRAN's default) copied
    the core crate out of its workspace and deleted its [lints]\nworkspace = true without
    inlining anything, so the vendored crate compiled under a different lint configuration than
    the sources it was copied from. The [workspace.lints.rust] unexpected_cfgs check-cfg
    allowlist went with it, which is what declares the crate's own #[cfg(...)] gates as expected
    cfg names — every gate in the vendored copy then became an unexpected_cfgs diagnostic. That
    is silent in a default build and a hard error under the RUSTFLAGS="-D warnings" CI sets, so
    the breakage was invisible to every local run and only ever surfaced in CI. Vendoring now
    materializes the whole [workspace.lints] sub-tree into the vendored manifest verbatim; a
    crate that spells out its own [lints] instead of inheriting is left untouched, and a
    workspace that declares no lints still just has the inheritance mark...

Read more

v0.67.2

Choose a tag to compare

@Goldziher Goldziher released this 23 Aug 16:32
v0.67.2
3f485ff

Fixed

  • Java: a non-optional Vec/Map field carrying #[serde(default, skip_serializing_if = "...")]
    no longer emits @Nullable on the generated record component. The builder already defaulted such
    fields to List.of()/Map.of(), but the record component was independently marked @Nullable
    because has_serde_default alone drove that decision -- so a payload omitting the key (which
    skip_serializing_if guarantees for an empty collection) passed null into the record's
    canonical constructor, throwing NullPointerException on .isEmpty() downstream even though the
    underlying Rust Vec<T>/HashMap<K, V> is never null. The record now emits a compact-constructor
    line normalizing null to the same empty-collection literal the builder uses, and both
    generators now read that literal from one shared function
    (serde_default_collection_literal) rather than each deriving it. Changes generated Java output
    for any consumer with such a field.

  • Dart generation now uses flutter_rust_bridge 2.13 and bypasses its redundant dependency
    preflight.
    Alef emits the bridge dependencies itself, while FRB's check rejected valid Dart
    prereleases such as freezed 4.0.0-dev.3 before generation could complete.

  • Swift tagged-enum parameters are deserialized before the source call. A data-carrying enum
    crosses swift-bridge as a JSON string; a referenced parameter was emitted as &param.0, treating
    the bridge String as an opaque wrapper and failing to compile with E0609.

  • The generated FFI crate builds by manifest path rather than package ID. cargo build -p <crate>-ffi assumes the emitted crate is a member of the invoking workspace; a standalone
    generated manifest is not, and cargo rejected the package spec outright.

  • The alef all format gate and the publish-asset guard are hermetic across platforms. The
    format gate installs its own stub formatter on PATH instead of depending on poly being
    present, and the publish-asset guard's Unix-only shell helpers are cfg-gated so the suite
    compiles on Windows.

  • Dart FRB: frb_generated.rs no longer diverges between alef build and alef generate on
    identical input. alef build's CarryFrbCfgGates post-build step wrote
    flutter_rust_bridge_codegen's raw, unformatted output straight to disk, while alef generate
    additionally ran a separate poly fmt pass over the same file afterward -- two alef commands
    regenerating unchanged input then disagreed on the committed bytes (e.g. use import grouping
    order), producing spurious diffs on every regeneration. CarryFrbCfgGates now normalizes the
    file through the same normalize_content pass the guarded generator path hashes against, so
    both commands converge on one canonical form. (#179)

  • alef verify now detects Dart FRB frb_generated.rs drift. The file is written by an external
    tool and rewritten in place by CarryFrbCfgGates, so it never carries alef's own embedded hash
    marker and was structurally invisible to alef verify's per-file staleness check -- it could
    silently fall behind (stale #[cfg(...)] gates, or non-canonical formatting) with zero signal.
    alef verify now recomputes the same canonical form CarryFrbCfgGates would write and reports
    a difference as drift. (#179)

  • e2e/java: stop inlining large fixture values as a single Java string literal. The JVM caps
    a CONSTANT_Utf8 constant-pool entry (and javac a string literal) at 65535 bytes, a limit no
    amount of escaping can raise; a fixture body long enough to threaten it made the generated Java
    doc snippet, e2e test method, or HTTP mock body fail to compile. java_string_literal (new,
    src/e2e/codegen/java/values.rs) renders short values exactly as before and splits longer ones
    into +-concatenated literal chunks, each safely under the cap. Wired through
    json_to_java_typed, emit_java_object_array, java_builder_expression, the doc-snippet
    json_object setup (snippet.rs + snippet_json_object_setup.jinja), the e2e test method's
    from_json builder path (test_method.rs), the HTTP mock request body (http.rs), the
    equals assertion literal (assertions.rs), and the handle/IR-typed-struct JSON embeds in
    args.rs. Task #180.

  • e2e/kotlin: apply the identical fix to the Kotlin backend. Kotlin compiles to the same JVM
    bytecode as Java and shares the exact 65535-byte CONSTANT_Utf8 cap, so it had the same live
    defect. kotlin_string_literal (new, src/e2e/codegen/kotlin/values.rs) mirrors
    java_string_literal. Wired through json_to_kotlin, both snippet_json_object_setup.jinja
    call sites (the handle-config and json_object paths in args.rs), the streaming-request
    from_json builder path shared by snippet.rs and test_method.rs, the HTTP mock request body
    (http.rs), the equals assertion literal, and the array-element json_object embed.

  • alef build no longer silently discards PostBuildOutcome::skipped_missing_tools: both
    post-build call sites in build_with_environment now route through
    record_post_build_outcome, which warns per language and adds a "post-build tool(s) skipped
    (not on PATH)" count to the backend build summary, matching the signal alef generate/alef all already gave via run_resolved_post_builds. A missing post-build tool remains non-fatal
    (falling back to committed generated output is intentional), but is no longer indistinguishable
    from a clean run.

  • alef test-apps run --lang <target> now fails with a clear error when the requested target(s)
    matched no crate's configured [e2e].languages, instead of silently exiting 0 with no test
    apps run. Mirrors ensure_requested_suites_will_run's semantics for alef test. A run with no
    --lang filter and no [e2e].languages configured anywhere is unaffected (still a legitimate
    non-fatal no-op).

  • e2e/java: an equals assertion carrying a literal null against a non-optional collection
    field no longer renders assertEquals(null, result.field()) -- a comparison the generated
    binding can never satisfy, because its Jackson builder defaults an absent, serde-defaulted
    collection to List.of(). with_ir_collection_map was wired into the csharp, kotlin, swift and
    rust e2e generators but never java, so java's assertion side had no IR-backed view of which
    result fields are collections. Task #200.

  • e2e: a docs-tagged fixture with neither docs.shows nor docs.presentation no longer emits
    a snippet that bottoms out at a bare print(result). Field access is derived from the fixture's
    own assertions, which already anchor on the same field paths the assertion resolver renders
    against. Python and Rust additionally resolved presentation after clearing assertions; both are
    hoisted above the clear. Task #199.

  • alef scaffold now allowlists bare cfg(alef) in [workspace.lints.rust], not just
    feature = "alef-meta".
    #[cfg_attr(alef, alef(skip))] is alef's documented and far more
    common exclusion marker, but cfg(alef) is never a real declared cfg, so rustc's
    unexpected_cfgs fired on every use and any lane compiling with -D warnings denied it.

  • A user [e2e.format] override's {dir} placeholder now expands to a path a POSIX shell can
    cd into on Windows. canonicalize returns the extended-length form \\?\C:\..., and sh
    reads every \ as an escape, so the cd in the conventional (cd {dir} && ...) override
    failed before the formatter ever ran. The shell then exited 1, which is not the
    command-not-found status 127, so an absent formatter was misclassified as "the formatter ran
    and rejected the code" and killed the run instead of being recorded as a deferred
    environment gap. run_in_dir's built-in residual steps already avoided this by never going
    through a shell; the override path, which must go through one, now normalises the path.

  • The generated FFI crate's build.rs nested its stale-backup cleanup inside if had_destination, which clippy rejects as collapsible_if under -D warnings. Because the
    file carries generated_header: true, no consumer edit survived regeneration, so a consumer
    had to suppress the lint in its own CI — and any lint pass alef did not know about (poly runs
    its own whole-project clippy) hit it anyway. Flattened with an early return rather than a
    let-chain, so the emitted crate's edition does not matter.

  • Emit checksum-Elixir.*.exs in mix format's canonical wrapped form so regeneration no longer
    produces pure-reformat diffs. Each map entry was written on a single line; mix format (the sole
    formatter for generated .ex/.exs) then moved every over-width digest onto its own continuation
    line and dropped the trailing comma, so the file was dirty after every alef build. The emission
    now wraps per entry exactly where the formatter would — honouring line_length from the package's
    .formatter.exs, falling back to Elixir's default of 98 — and renders through a Minijinja template
    instead of push_str(&format!(...)).

  • Tests that shell out to git no longer inherit the ambient global git configuration. Fixture
    repositories are now built through a single hermetic test_support::git_command helper that
    neutralizes GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM and pins identity, signing, excludes and the
    default branch name. Previously a developer with commit.gpgsign = true set globally signed
    every fixture commit, making the suite depend on a working gpg-agent, and the same tests would
    behave differently on CI, which has no signing key.

  • go_tag's fixture builder asserted only that git could be spawned, not that it succeeded, so a
    failed commit or annotated tag left the tests asserting against an empty repository instead of
    the fixture they name. Each step now checks the child's exit status.

  • Four #[test] functions had empty bodies and had passed unconditionally...

Read more

v0.67.1

Choose a tag to compare

@Goldziher Goldziher released this 23 Aug 10:16
v0.67.1
9b60ac2

Fixed

  • Generated FFI build scripts no longer rewrite tracked headers during ordinary Cargo builds.
    Header export is now explicit via ALEF_EXPORT_GENERATED_HEADERS=1; cbindgen output is buffered,
    validated as UTF-8, and published atomically to the canonical and Go destinations with rollback
    on failure. This prevents failed workspace lint or build commands from truncating generated files.