Releases: xberg-io/alef
Release list
v0.71.0
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 ownexclude_typesmarshals asString; 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 Foundationheuristic 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
throughzig buildagainst the consumer's realbuild.zig, whoseffi_pathbuild option
defaults to the release profile. The synthesized snippet build only ever threaded
.target/.optimizeinto itsb.dependency("binding", .{…})call, and a top-level-Dcannot
set an option on a.pathdependency — so there was no mechanism at all to redirect that path.
Withalef buildproducing a debug artifact, every Zig snippet failed withunable to find dynamic system library. The validator now resolves the library itself — release preferred,
debug fallback, never crediting adeps/-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
Iteratedocs 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 asresults[].content— the
result-anchored resolver reproduced that whole path underneath the already-peeled loop variable,
emittingfor 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 itselfstringand 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 throughas unknown as, and the import sweep reaches struct names referenced only by that
cast. -
alef buildstaged an FFI library from a profile it never built.find_built_artifact
hardcoded thereleaseprofile, so a plainalef build— which runscargo buildand 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_artifactnow takes an explicitBuildProfileand searches only the two uplifted,
profile-scoped directories;deps/is consulted solely to name a rejected copy in the error.
StageFfiLibrarypasses 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 passesRelease, 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.RunSummarynow tracksfully_verified— results that reached their requested level
with no downgrade or capability cap — and the summary leads withChecked at requested level: N/Total (P%).alef snippets checknow fails, unconditionally rather than only under--strict,
when not one single result reached its requested level;alef docs/alef allwarn 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 apackage does not existsymbol 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 Pythontypecheckruns
the interpreter's own compile check ahead ofpyrefly, so a hardIndentationErrorcan no longer
pass. -
Four generated-snippet type defects. Python field access now narrows an
Optionalbefore
subscripting it instead of indexing it bare; the C free-function call site routes omitted optional
arguments throughresolve_optional_sentinel, so an IR-declared handle parameter gets the0
sentinel rather thanNULL; aniterateoperation with an emptyfieldslist 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 whenoptions.pypublishes that type
only as a return-onlyTypedDict, 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); e2emain_test.gohad an unsorted import block, two one-line
if err != nil { panic(err) }checks, a one-linego func() { for … } }()drain, and
gofmt-incorrect+spacing; and the ElixirGenServertemplate carried a double blank line and a
pre-joinedwhenclause one column pastmix format's limit. Each drift let a consumer's
gofmt -wormix formatrewrite 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 invokegofmt/mix formatand self-skip when absent. -
Ownership markers alef itself refused to recognise. The PHP
install.sh, Rinstall.R, and
Node/napi e2e.npmrcemitters hand-spelled analef-generatedmarker string that alef's own
content_has_alef_markerguard does not match. All three aregenerated_header: false, so the
hand-written text was the only ownership signal — these files were permanently stranded as
unowned. They now source their marker fromhash::header/hash::STANDARD_HEADER_LINE, and each
has a test asserting through the real guard rather than a copied literal. -
package_dirno longer leaks a trailing slash into every path built from it.
ResolvedCrateConfig::package_dirreturned a user's configured[crates.output]/scaffold_output
string verbatim, so a trailing/produced double-slash paths thatalef adoptcould never match
against the real on-disk file. Fixed at the source, which protects the ~35format!("{pkg_dir}/…")
call sites acrossscaffold/languages/andpublish/;scaffold_license_filesalso now builds its
LICENSEpath withPath::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 extractedrust_pathrather
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 emitsType.CONSTANTorType.Variantas 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.
- Go dropped all four required super-trait methods whenever the super trait was declared in a
-
Snippet session locks are keyed by fingerprint, not by config name.
alef.tomlcan point two
differently-named sessions (a language fallback such astypescriptand an explicit
binding-package target such asnode) at the samecwdand manifest. They resolve to one
physical workspace directory but each name got its ownMutex, so two batch groups that both
believed they held the session lock wrote into the samesnippet_batch_N.tsfiles concurrently.
The corruption was worse than the lost work i...
v0.70.0
[0.70.0] - 2026-08-26
Changed (BREAKING)
E2eCodegen::render_snippet_body_with_functionsno 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-treeE2eCodegen
implementation must now implement this method explicitly; the previous default's body is
equivalent to ignoring thefunctionsargument.- 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-nilwill now correctly receivenilfor an
absent result; Java call sites will now correctly receiveOptional.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 distinguishNonefromSome(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_resultpresence 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 benil, so everyNonearrived as a realSome(0); Java'sOptional.of(result)
could never be empty, soNonereached the facade asOptional[0]. Whether a companion exists
is asked offfi::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 Dartffistyle are
audited but not yet wired; a stance ledger over everyLanguagenow 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::cfgdeliberately
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_stringhelper 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 usedif letguards, stable only from 1.95, andrust-toolchain.tomlpins a
far newer toolchain, so no CI job ever compiled the crate at its declared floor. Installing
0.68.0 from crates.io failed withE0658on 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 letis equivalent), the README now matchesCargo.toml, and a newmsrvCI 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.tomland whose
content never changed could live only in the gitignored.alef/scaffold-owned-paths.manifest
indefinitely; clearing.alefthen 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 andalef 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
beforehook 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 spawnedswift build --show-bin-pathwith no deadline and
no process-group teardown while every sibling subprocess had both, is now bounded like the rest;
and a newdocs.snippets.before_timeout_secslets a package build have its own budget instead of
sharing one number with every individual snippet compile. Truncation is never silent. alef adoptno 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-onlynothing 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 — soparseURLPath,
utf8Lengthand_Internallinked 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 emittingunmarshalU64, 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
gainedtrait_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'sJava_..._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 intoFixture, 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 generatedbuildAndroidJniLibstask readsgradle.taskGraph
from insideonlyIfand assignsSystem.errtoExec.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
Fixed
alef docsparseddocs.snippets.required_languagesthrough a fence-tag-only parser whilealef snippets gapsparsed the same key through a session-target-aware one. An entry ofnode,wasmorkotlin_androidwas therefore accepted by one command and rejected by the other, soalef allaborted withunknown language: nodeon a config its own sibling command had already validated. The resolver is now a single authority insnippets::typesthat 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 exampleenum Label { A, B, Custom(String) }— was emitted by napi as a#[napi(string_enum)]with a bareCustom,variant and by wasm as a plain C-styleCustom = 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 ownDeserializeand converting viaInto. The derived field-by-field objectDeserializesilently 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-Cowfield, 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 representation —Option<i32/u64/f32/f64/bool/…>andOption<Duration>. PreviouslyNoneand a legitimate zero-valuedSomeboth returned the same0/0.0sentinel 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
Resultinstead of panicking. An unrecognised wire string used topanic!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 toResult<_, String>so the failure has somewhere to go. - Generated binding↔core conversions no longer silently drop
Vecelements 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 checkno longer tells you to runalef buildfor a language that has nodocs.snippets.sessionstarget configured at all. That advice was false — no build could change the result — and it was why runningalef buildand thenalef snippets checkproduced 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 toBytes/Vec<T>instead of being sanitized to a lossyStringplaceholder.resolve_typehad nosyn::Type::Arrayarm at all, so every fixed array fell through to a stringifiedNamedtype. - Sanitized public-API diagnostics are now driven by the sanitizer's recorded rewrite rather than by pattern-matching the
Stringplaceholder, so a field genuinely declaredStringis never conflated with one rewritten toString. Sanitized parameters also recordoriginal_typefor the first time, which several backends already gated on and which had therefore been inert. - The Zig reference pages document
[]const u8/[]u8at 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_i32reconstruction 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 returningOption<u64>is exported by the FFI crate as a rawu64; C# declared the same symbol asIntPtr, read the integer bit pattern as a UTF-8 string pointer and passed it toFreeString— an arbitrary-address free. The pointer-vs-scalar decision now has one owner inffi::type_mapthat C# asks; the private copy it replaced had already drifted onOption<Option<Duration>>. A directOption<scalar>return still cannot distinguishNonefromSome(0); a presence channel for that position is not yet implemented. kotlin_androiddocumentation snippets no longer silently drop every field of a call's result. It was the one language backend without arender_snippet_body_with_functionsoverride, 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
.mdsuffix. New[docs].reference_link_styleselectssuffixed(default, unchanged) orextensionlessfor 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 likepythnstays 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.declaredcovers surfaces built through calls likePrompt::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 generateactually 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
Changed (BREAKING)
- Generated bindings for a
&mutDTO parameter now return the updated value. A core function with a&mut Tparameter 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 — Pythontag_record(record)becomesrecord = tag_record(record); NodetagRecord(record)becomesrecord = tagRecord(record); PHPtagRecord($record)becomes$record = tagRecord($record); Goerr := TagRecord(record)becomesrecord, err := TagRecord(record), whererecordis now a*Record; Java and KotlintagRecord(record)becomerecord = tagRecord(record); Dartawait tagRecord(record)becomesrecord = await tagRecord(record); Swifttry tagRecord(record: record)becomesrecord = 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
&mutDTO shapes a binding has no room to express: more than one&mutparameter, and a&mutparameter 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
&mutparameter on an opaque handle type still mutates through the handle, which was already correct; and&mutonString,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 onPATH, 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 theFieldResolverlayer (src/e2e/field_access/resolver/classify.rs), so a future accidental over-anchoring of the permissive check is caught bycargo test --libwithout needing the full presentation-layer suite. alef e2e snippets-migrateand its coverage driver gained regression coverage for twocurated_snippetspath-resolution edge cases: anexisting_rootequal to the configuredsnippets.outputnow 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 realwhen (val v = …) { is <Union>.<Variant> -> { … } }block on both thekotlinandkotlin_androidtargets. The payload property name is computed from the IR throughkotlin_field_name_with_type— the same helper the Kotlin binding backend itself uses — so it can never drift from the emitted binding. Detection reusesFieldResolver::tagged_union_split, the generic primitive Gleam/Dart/Swift already consult;FieldResolver::union_variant_payloadis 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.TypeDefnow records a struct's container-level serde conversion (serde_container_conversion, holdingfrom/into/try_from/transparent), read from#[serde(...)]including through#[cfg_attr(...)]. These attributes were previously parsed for no purpose — extraction read onlyserde_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. DeliberatelyWarningseverity — 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
sincethat names a release newer than the crate's own version now raises a Warning (since_newer_than_crate_version) naming the item, the declaredsince, and the crate version it exceeds; an unparseablesinceraises a distinctsince_version_unparseablerather than passing silently. Both#[alef(since = "...")]and#[deprecated(since = "...")]are checked, across all seven item kinds that carry version metadata. Comparison usessemver::Version::cmp_precedence, not the derivedOrd— 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&mutparameter needs writing back, which type the binding must return in place of(), and which&mutshapes 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 nowSeverity::Errorand 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-wordoverrides.go.module/moduleis never a resolvable Go import path) is nowSeverity::Errorand aborts e2e generation. The equivalent Java check (amoduleoverride that looks like a class, not a package) staysSeverity::Warning, since no consumer in the surveyed fleet currently sets that field.kotlin/discriminated.rs::render_discriminated_union_assertiontakes 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.pyfacade and the<module>.pyistub 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/Bytesparameter 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 rootcomposer.jsonand the e2ecomposer.jsonnow 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_pathreturnsCow<'_, 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.mdand the (now-removed, folded into that skill)binding-audit-patternrule 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 adapterrequest_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"intoSample::Sample::DocumentRequest.new(...)in both outputs identically.values::qualify_ruby_typenow prepends the module only when the name has no::already, matching howcsharp/gotakeoptions_typeverbatim. - Fixed a false-positive `field's #[serde(default)] value disagrees with its #[...
v0.67.6
Added
Codegen::cfg::expand_configured_features, which resolves a configured feature list through the core crate's own[features]table (transitively, skippingdep:andcrate/featuretokens) 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.BuildAndroidJniLibsderives its target list from[crates.kotlin_android] abis(the same list that scaffolds thejniLibs/<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:
buildAndroidJniLibsderives its target list from[crates.kotlin_android] abis(the same list that scaffolds thejniLibs/<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, skippingdep:andcrate/featuretokens) 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] modulevalidation: 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 producedimport io.xberg.Xberg.*;in generated snippets. - Add
[e2e.call(s).*] module/overrides.go.modulevalidation: 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
argsvs. IR signature validation: warns when a fixture's effectiveargs(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 sameCallIr/TargetParamsseam e2e codegen already uses for argument type lowering, so it silently no-ops when the call is unresolvable or the resolved function isbinding_excludedrather than claiming a false positive. - Both new checks land as warnings only, not errors — see
src/e2e/validate_call_module.rsandsrc/e2e/validate_call_args.rsdoc comments for the consumer-fleet measurements behind that choice. alef snippets auditaccepts--configand gained a curated-versus-generated accounting pass: a snippet under an audited root that no coverage ledger records as generated and nocurated_snippetsdeclaration claims is reported asUnaccountedSnippet(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--configis unset or no coverage ledger records anything as generated.alef snippets checkcarries the same accounting through its configured audit pass.- Added
[crates.e2e.snippets].curated_snippets: glob patterns (relative tooutput) declaring hand-authored snippet files as curated on purpose rather than alef-generated. Resolved intoSnippetGenerationReport::curated_pathsand intomigration::MigrationEntry::curated, so both the generation report andalef e2e snippets-migratecan distinguish a declared, intentional absence of a generated equivalent from a genuine coverage gap. - Implemented
render_snippet_bodyfor the brew (shell) e2e code generator: documentation snippets for CLI-based bindings now render a singlebinary subcommand "<url>" --flagsline, built from the same call-config resolution the executable brew e2e suite already uses. - Add
[crates.verify].ignore_ephemeral, a glob-pattern opt-out soalef verifynever reports intentionally ephemeral, gitignored generated output (e.g. registry-modetest_apps/) as a permanent "missing generated files" failure; every excluded path is still counted and reported inalef verify's coverage output. - Added
[crates.e2e.snippets].sample_base_url: the public base URL generated documentation snippets bind for a fixture'smock_url/mock_url_listarguments. 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_urlplaceholder resolves against it too. - Add
[crates.node].excluded_default_features;scaffold_node_cargonow drops excluded names from both the wrapper's own[features] default = [...]array and the core dependency's explicitfeatures = [...]line, matching the fix already shipped for Ruby/Swift/Dart. Same defect: atarget_dep_overridesentry 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_cargofixed the same way. - Add
[crates.php].excluded_default_features;scaffold_php_cargofixed 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 derivationscaffold_ffiandwarn_on_ffi_feature_driftboth read) now excludes these names from both the FFI crate's own[features] default = [...]list and the core dependency's explicitfeatures = [...]line, while still declaring them socargo build --features <name>keeps working. - Added: a warning when a field path declared in
[e2e].fields,fields_optional,fields_array,fields_method_callsorresult_fieldsis 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 gapsnow 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 omittedrequired_languages,docs_dirsandinclude_base_pathsfrom itsalef.tomlpreviously 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 gapsnow 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 gapsgained--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 unsetinclude_base_pathsis reported but deliberately not strict-fatal: it makes include targets over-report rather than manufacture a false clean.alef verifynow 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 thealef snippets auditprecedent of naming the check class a run skipped instead of printing a bare clean result.- Added:
[crates.ruby].excluded_default_features, mirroringSwiftConfig/DartConfig.scaffold_ruby_cargopreviously forwarded everycollect_cfg_featuresname into the generated wrapper crate's[features] default = [...]array unconditionally, which re-enabled a feature a[crates.ruby].target_dep_overridesentry excluded for a specificcfgtarget one layer down (Cargo unions feature requests across every dependency edge to the same resolved package regardless of target). The excluded name stays declared (socargo build --features <name>keeps working) but is dropped fromdefaultand from the core dependency's own explicitfeatures = [...]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-- pathsmarker_comment_styleanswersNonefor),alef adopt --write --clobber-create-once-seedswrites no byte of the file:stamp_foryieldsNone, so the entire adoption is one entry in the committed.alef-ownership.toml. That entry is precisely whatwrite_scaffold_files_reportaccepts as proof of ownership for an unmarkable path (`owned = has_marker |...
v0.67.5
Added
- Added
[crates.e2e.snippets].curated_snippets: glob patterns (relative tooutput) declaring hand-authored snippet files as curated on purpose rather than alef-generated. Resolved intoSnippetGenerationReport::curated_pathsand intomigration::MigrationEntry::curated, so both the generation report andalef e2e snippets-migratecan distinguish a declared, intentional absence of a generated equivalent from a genuine coverage gap. - A
curated_snippetspattern that matches zero files, or that matches a path alef itself generates, now fails the run instead of being silently accepted. - Implemented
render_snippet_bodyfor the brew (shell) e2e code generator: documentation snippets for CLI-based bindings now render a singlebinary subcommand "<url>" --flagsline, built from the same call-config resolution the executable brew e2e suite already uses.
Fixed
-
docs.snippetsvalidation 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 emptybeforelist. Warns always; understrict, 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 filteredrun_validationcall) now prepares only the configured sessions its filtered snippet set actually needs, instead of running every configuredbeforebuild 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 analef.tomlwith zero[[crates]]entries now returnsResolveError::NoCratesConfiguredinstead ofOk(vec![]), soalef go-tag,alef validate versions --exit-code, andalef publish validatecan no longer silently process zero crates and exit 0. -
alef check-registry --registry github-releasenow warns when it verified only that the release exists (no--asset-prefixor--required-assetsgiven), so a CI variable that expanded to nothing is no longer indistinguishable from "all N artifacts are attached";--registry zig/--registry swiftare unaffected since they intentionally check existence only. -
alef e2e validatenow applies the same[e2e].languagesfallback (to the crate's scaffolded languages) thatalef e2e generate --snippets-migrateandalef test-apps runalready applied, so an unset[e2e].languagesno 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 lintnow fails whenpolyis not on PATH instead of warning and reporting a clean run;polyis the entire implementation ofalef lint, so there was no partial coverage to report. -
alef release-metadata --targetsnow 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) thatscaffold_ffiactually writes into the generated FFI crate'sCargo.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_featuresas the single derivation of the FFI crate's effective default feature set, used by bothscaffold_ffiandwarn_on_ffi_feature_driftso the two can no longer disagree. -
warn_on_ffi_feature_driftnow 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-targetdefault_features = falsenever 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, ortypescript/node/wasm) no longer collide when they validate the same physical package/working directory.resolve_session_claimnow only reportsSessionClaim::Ambiguouswhen same-language candidates validate genuinely different working directories; candidates sharing one directory collapse to a single deterministicSessionClaim::Claimedinstead (issue #255). -
Added
SessionIdentitytrait (src/snippets/runner/session_resolution.rs) implemented forValidationSessionandSessionSpec, 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_androidover one directory and fortypescript+node+wasmover 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 ownswift 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 anyswiftcvalidation 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-codenow askschecks_passfor 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 passchecks_passexplicitly refuses — and it exited 1
on ablocked_on_publishrow, whichchecks_passdeliberately 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.--jsonalready reported
checks_pass, so a single invocation could print"ok": trueand still exit 1. The
blocked_on_publishdoc comment, which asserted the opposite and contradicted both
checks_passand its tests, is corrected. -
Fixed
alef build/alef generaterunning the umbrellagradle build(andgradle build -Prelease) forkotlin_androidwhen no[workspace.build_commands.kotlin_android]overlay is declared, instead of the intendedgradle assembleDebug/gradle assembleRelease.build_command_for's"gradle"arm matched on the sharedbc.toolstring, which cannot distinguishKotlinfromKotlinAndroid; it now asks a new sharedbuild_defaults::gradle_build_task(Language, bool)helper, the same onedefault_build_configuses, 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 inRustString(...), which requires aStringargument and does not compile against an enum value; the shim now JSON-encodes the enum viaJSONEncoderbefore wrapping it, decided by consultingApiSurface::enumsrather than theTypeRef::Nameddiscriminant. Struct-typed (JSON)Namedreturns are unchanged. -
Fixed the Swift trait-bridge default method stub emitting
return "{}"for ahas_default_implmethod 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 callingstrip_keep_markers, the only built-in render path that did not, so a~keepmarker 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_bridgehas no dependence on the parent's opacity and returns true for any optionalVec<_>field, so the JSON-bridged.toString().countshape 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
Fixed
-
a snippet session's
beforehook is now run once per package instead of once per configured session target.kotlinandkotlin_androidboth resolve toLanguage::Kotlin, andtypescript/node/wasmall resolve toLanguage::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 outrantimeout_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_commandno 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,SIGTERMandSIGHUPare 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 verifyrefuses--compile,--lintand--langinstead of discarding them. All
three are visible, documented flags (--compilereads "Also run compilation check") that
the command destructured away, soalef verify --compileexited 0 having compiled
nothing — indistinguishable from a passing compile check. They now fail with a message
namingalef build --langandalef lint --lang, which do implement that work.
--exit-codeis unaffected: it is a hidden, documented no-op. Nothing in the polyrepo
passes the refused flags today. -
alef --versionno longer reportstree: DIRTYfor every binary installed withcargo install --git. Cargo drops a.cargo-okcompletion marker into each checkout it creates, and the build stamp classified the working tree withgit 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 gapsnow 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 omittedrequired_languages,docs_dirsandinclude_base_pathsfrom itsalef.tomlpreviously 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 gapsnow 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 gapsgained--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 unsetinclude_base_pathsis reported but deliberately not strict-fatal: it makes include targets over-report rather than manufacture a false clean. -
alef snippets checkno longer skips its gap pass silently. With neitherdocs_dirsnorrequired_languagesconfigured under[crates.docs.snippets]the pass is still skipped, but the unset keys are now warned about by name, and understrictthe skipped pass fails the run instead of reporting no failure. -
Split
src/snippets/gaps.rsunit tests intosrc/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.javahardcoded a six-field context —tagName,depth,indexInParent,
parentTag,isInline— with fixed offsets, a fixedMemoryLayout, and a fixed six-argument
decodeContextreturn. Generated Java only compiled when the configuredcontext_typehappened
to be exactly(enum|i32, ptr, i64, i64, ptr, i32); every other shape failedjavacwith
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'scontext_c_type/context_field_specsdecided 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 notOption<String>). The record component still exists, so the Java bridge
passesnullfor 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 novalues(), so an ordinal cannot
reconstruct a variant; such a component now takes the absent value instead of emitting Java that
does not compile. -
FieldResolver::accessorandFieldResolver::rust_unwrap_bindingeach carried a private copy of
the virtual-namespace strip decision, gated onresult_fields.contains(..)where the shared
result_relative_pathasks the broaderis_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 callresult_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_fieldsentry the IR marksbinding_excludedno longer strips its virtual namespace
prefix in accessor emission.with_ir_fieldsalready 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
withis_arrayand 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...v0.67.3
Fixed
-
e2e/swift: a getter's bridged shape is now read from the binding backend instead of
re-derived.build_swift_first_class_maptrackedVec<Vec<_>>/Map<_>plus two hand-enumerated
Option<Vec<Named(..)>>cases, so every other optionalVecwas called countable —
Option<Vec<String>>among them, which really emitsfn og_locale_alternates(&self) -> String,
making the generator emit?.countagainst aRustString. It now calls
field_needs_json_bridge, the same predicatewrappers::getters::emit_gettersuses 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/sizesuffix), 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, emittingarticle()?.publishedTime().toString()
wherepublishedTime()returnsOptional<RustString>. The leaf's optionality now comes from the
type cursor. -
e2e:
namespace_stripped_pathno longer drops a real struct segment theresult_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
answersfalse, 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 likebatch.completed_countemitted
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_runproduced a page whose fence language was the literal
rust,no_run— a markdown info string's language is its first whitespace-delimited token —
whichalef snippets audit --docscorrectly 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: droppingno_runmakes the doctest actually execute. -
cli:
alef snippets auditnow names its coverage when no--docsroot is given. A
snippets-only invocation printed a bareAudit clean: no issues found.while the
documentation-page checks (fence languages, include targets) never ran, so a CI job that
omitted--docsread green for a check class it had skipped. -
Wire
src/codegen/config_gen/tests/generators.rsinto the module tree
(src/codegen/config_gen/tests.rswas missingmod generators;), so its 18 config-generator
unit tests actually compile and run. Fixed 14 staleFieldDef/TypeDefstruct literals
predating theversionandhas_private_fieldsIR 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 aStringfield with a real
default) rather than the already-correctunwrap_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 wiredspec_and_formatting.rs;helpers.rscarried no tests at all. -
alef generate/alef buildnow fail loudly, before invokingflutter_rust_bridge_codegen,
when theflutter_rust_bridge_codegenbinary onPATHreports a version that disagrees with
the project's declared[crates.dart] frb_versionpin. 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 differentflutter_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_errorimplementation 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: RustE0432/E0433/E0463/E0583(no longerE0425,E0308,E0599,E0609,
E0061, or thecould not compilesummary rustc prints on every failed build), Javapackage ... does not exist(no longer barecannot find symbol), C#CS0246/CS0234(no longer
CS0103/CS5001), Gocannot find package/no required module(no longer bareundefined:),
Swiftno such module(no longercannot 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 countedunavailable, 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, aresult_is_simple/result_is_bytescall, 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.CostTrackedorresult.stream.hasPageEvent. -
e2e/rust: a snippet presenting derived fields now binds the result it references and
unwraps aResult-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 printedresult.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!.Countandresult.Metadata.Headings[0].Level(CS8602). -
e2e/brew: the generated
run_tests.shharness reportedPASSwhen any assertion but the
last one failed.run_testinvoked each test function as the condition of anif, which
disableserrexitfor the entire call, so a failing assertion'sreturn 1no 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 thatrun_testconsults 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 fromFieldResolver::resolve, which only applies aliases, so a
field likebatch.completed_count— wherebatchis a virtual grouping label rather than a
JSON object — became.batch.completed_count,nullagainst 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
(bothVendorMode::CoreOnlyandVendorMode::Full, the latter being R/CRAN's default) copied
the core crate out of its workspace and deleted its[lints]\nworkspace = truewithout
inlining anything, so the vendored crate compiled under a different lint configuration than
the sources it was copied from. The[workspace.lints.rust]unexpected_cfgscheck-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 anunexpected_cfgsdiagnostic. That
is silent in a default build and a hard error under theRUSTFLAGS="-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...
v0.67.2
Fixed
-
Java: a non-optional
Vec/Mapfield carrying#[serde(default, skip_serializing_if = "...")]
no longer emits@Nullableon the generated record component. The builder already defaulted such
fields toList.of()/Map.of(), but the record component was independently marked@Nullable
becausehas_serde_defaultalone drove that decision -- so a payload omitting the key (which
skip_serializing_ifguarantees for an empty collection) passednullinto the record's
canonical constructor, throwingNullPointerExceptionon.isEmpty()downstream even though the
underlying RustVec<T>/HashMap<K, V>is never null. The record now emits a compact-constructor
line normalizingnullto 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 asfreezed 4.0.0-dev.3before 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¶m.0, treating
the bridgeStringas 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>-ffiassumes 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 allformat gate and the publish-asset guard are hermetic across platforms. The
format gate installs its own stub formatter onPATHinstead of depending onpolybeing
present, and the publish-asset guard's Unix-only shell helpers arecfg-gated so the suite
compiles on Windows. -
Dart FRB:
frb_generated.rsno longer diverges betweenalef buildandalef generateon
identical input.alef build'sCarryFrbCfgGatespost-build step wrote
flutter_rust_bridge_codegen's raw, unformatted output straight to disk, whilealef generate
additionally ran a separatepoly fmtpass over the same file afterward -- two alef commands
regenerating unchanged input then disagreed on the committed bytes (e.g.useimport grouping
order), producing spurious diffs on every regeneration.CarryFrbCfgGatesnow normalizes the
file through the samenormalize_contentpass the guarded generator path hashes against, so
both commands converge on one canonical form. (#179) -
alef verifynow detects Dart FRBfrb_generated.rsdrift. The file is written by an external
tool and rewritten in place byCarryFrbCfgGates, so it never carries alef's own embedded hash
marker and was structurally invisible toalef verify's per-file staleness check -- it could
silently fall behind (stale#[cfg(...)]gates, or non-canonical formatting) with zero signal.
alef verifynow recomputes the same canonical formCarryFrbCfgGateswould write and reports
a difference as drift. (#179) -
e2e/java: stop inlining large fixture values as a single Java string literal. The JVM caps
aCONSTANT_Utf8constant-pool entry (andjavaca 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_objectsetup (snippet.rs+snippet_json_object_setup.jinja), the e2e test method's
from_jsonbuilder path (test_method.rs), the HTTP mock request body (http.rs), the
equalsassertion literal (assertions.rs), and thehandle/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-byteCONSTANT_Utf8cap, so it had the same live
defect.kotlin_string_literal(new,src/e2e/codegen/kotlin/values.rs) mirrors
java_string_literal. Wired throughjson_to_kotlin, bothsnippet_json_object_setup.jinja
call sites (thehandle-config andjson_objectpaths inargs.rs), the streaming-request
from_jsonbuilder path shared bysnippet.rsandtest_method.rs, the HTTP mock request body
(http.rs), theequalsassertion literal, and the array-elementjson_objectembed. -
alef buildno longer silently discardsPostBuildOutcome::skipped_missing_tools: both
post-build call sites inbuild_with_environmentnow 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 signalalef generate/alef allalready gave viarun_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. Mirrorsensure_requested_suites_will_run's semantics foralef test. A run with no
--langfilter and no[e2e].languagesconfigured anywhere is unaffected (still a legitimate
non-fatal no-op). -
e2e/java: an
equalsassertion carrying a literalnullagainst a non-optional collection
field no longer rendersassertEquals(null, result.field())-- a comparison the generated
binding can never satisfy, because its Jackson builder defaults an absent, serde-defaulted
collection toList.of().with_ir_collection_mapwas 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.showsnordocs.presentationno longer emits
a snippet that bottoms out at a bareprint(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 scaffoldnow allowlists barecfg(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, butcfg(alef)is never a real declared cfg, so rustc's
unexpected_cfgsfired on every use and any lane compiling with-D warningsdenied it. -
A user
[e2e.format]override's{dir}placeholder now expands to a path a POSIX shell can
cdinto on Windows.canonicalizereturns the extended-length form\\?\C:\..., andsh
reads every\as an escape, so thecdin 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.rsnested its stale-backup cleanup insideif had_destination, which clippy rejects ascollapsible_ifunder-D warnings. Because the
file carriesgenerated_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.*.exsinmix 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 everyalef build. The emission
now wraps per entry exactly where the formatter would — honouringline_lengthfrom the package's
.formatter.exs, falling back to Elixir's default of 98 — and renders through a Minijinja template
instead ofpush_str(&format!(...)). -
Tests that shell out to
gitno longer inherit the ambient global git configuration. Fixture
repositories are now built through a single hermetictest_support::git_commandhelper that
neutralizesGIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEMand pins identity, signing, excludes and the
default branch name. Previously a developer withcommit.gpgsign = trueset 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 thatgitcould 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...
v0.67.1
Fixed
- Generated FFI build scripts no longer rewrite tracked headers during ordinary Cargo builds.
Header export is now explicit viaALEF_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.