v0.24.17
Added
-
Phase C cross-cutting plumbing: per-language override resolution, surface scan helpers, emitter traits, and backend stubs. Four items land together as the seam framework that wave-2 per-backend specialists drop real Jinja-template work into:
-
RegistrationVariant::resolved_for(language, base_handler_shape) -> ResolvedVariant<'_>(and companionmethod_name_for) onsrc/core/ir/service.rs. The helper performs a single authoritative lookup intovariant.language_overrides[language]and returns the effective(style, handler_shape, method_prefix)triple — per-language override wins, missing fields fall through to variant-level and registration-level defaults transparently. The PyO3 (registration_variants.rs) and NAPI (typescript.rs) backends are updated to callresolved_for("python"/"napi", reg.handler_shape)instead of readingvariant.styledirectly; all other backends continue to work unchanged since no language override is yet configured for them. -
ApiSurfacegate predicates (src/core/ir/surface.rs):has_lifecycle_hooks() -> bool,has_websocket_routes() -> bool,has_sse_routes() -> bool,has_error_types() -> bool. Each wraps a simple!self.<collection>.is_empty()so backends can guard emission blocks without duplicating the same collection check everywhere. -
Emitter traits (
src/core/ir/codegen_hooks.rs):LifecycleHookEmitter,WebSocketRouteEmitter,SseRouteEmitter,ErrorTypeEmitter. Each declares one method (emit_lifecycle_hooks,emit_websocket_routes,emit_sse_routes,emit_error_types) with documented stub contract (log atracing::debug!, walk without panicking, returnString::new()). Backends implement these when they write real emission. -
Per-backend stub modules (
new_ir_stubs.rsfor subdirectory backends, inline stubs for flat-file backends): pyo3, napi, magnus, php, rustler, go, jni, csharp, kotlin, dart, swift, zig. Each backend exposes anemit_new_ir_sections(api: &ApiSurface) -> Stringaggregate that forwards all four IR sections and returns empty output until a Phase-C specialist replaces the stub body.#[allow(dead_code)]suppresses the unused-function lint until callers are wired in.
Unit tests:
resolved_fortests cover the per-language override path, the fallback-to-variant-defaults path, partial overrides (style-only, shape-only), and themethod_name_forprefix concatenation. Surface predicate tests assert empty-collection →false, single-item →truefor all four predicates. Stub tests assert that all four stubs return""for the empty surface and do not panic on non-empty inputs. -
-
PHP backend Phase C: fluent verb-decorator API with lifecycle hooks. The ext-php-rs service-API codegen (
src/backends/php/gen_bindings/service_api/php.rs) now emits an idiomatic Slim-style fluentAppclass aligned with theverb_decorator+request_responsehandler shape: zero-arg constructor, HTTP verb methods (get,post,put,patch,delete,head,options) that return$thisfor method chaining,config(ServerConfig $config): selfto set server configuration, five lifecycle hook methods (onRequest,preValidation,preHandler,onResponse,onError) forwarding to native, andrun(): voidentrypoint. Generated class isfinalwithdeclare(strict_types=1). Foundation for Request/Response helper classes, ServerConfig DTO, error hierarchy, attribute scanner, and WebSocket/SSE routes established but deferred to follow-on iterations. (src/backends/php/gen_bindings/service_api/php.rs) -
NAPI backend Phase C: config method forwarding, lifecycle hook registration, and skeleton for future phases. The TypeScript service-API codegen now emits special handling for the
configmethod to forward configuration to the native runtime via JSON serialization (fixes the long-standing config no-op bug). All lifecycle hooks declared in the IR now generateapp.onRequest(fn),app.preValidation(fn),app.preHandler(fn),app.onResponse(fn),app.onError(fn)registration methods that forward to native. Async-by-default hook handlers useThreadsafeFunctioninternally. Three new templates (service_ts_configurator_config_forward,service_ts_lifecycle_hook) emit the wrappers. Foundation for Context-object handler shape, WebSocket/SSE routes, and error classes established but deferred to follow-on Phase C iterations per language. (src/backends/napi/gen_bindings/service_api/typescript.rs,src/backends/napi/template_env.rs) -
Go backend Phase C: chi-style fluent API with config struct, error types, lifecycle hooks, WebSocket/SSE routes, and helpers. The cgo service-API codegen (
src/backends/go/gen_bindings/phase_c.rs) now emits full Phase C surface forverb_decorator+request_responseshape: (1)Configstruct with all ServerConfig fields (Host, Port, Workers, Compression, Cors, RateLimit, RequestTimeout, MaxBodySize, JwtAuth, ApiKeyAuth, StaticFiles, EnableRequestId, EnableHttpTrace, GracefulShutdown, ShutdownTimeout); (2) error types for eachErrorTypeDefin the IR, each implementingerrorinterface withStatusCode() intandToProblemDetails() map[string]interface{}methods (e.g.NotFoundError,ValidationError); (3) lifecycle hook registration methodsapp.OnRequest(fn),app.PreValidation(fn),app.PreHandler(fn),app.OnResponse(fn),app.OnError(fn)forwarding to native; (4) skeletonWebSocket(path, handler)andSSE(path, producer)methods for future implementation; (5)app.Run(config Config) errormethod with defaults and C entrypoint call; (6) helper functionsPathParam(),QueryParam(),HeaderParam(),BindJSON(),RespondJSON()for chi-style HTTP handler convenience. New module refactored to keepservice_api.rsunder line-count ceiling. (src/backends/go/gen_bindings/phase_c.rs, five new Jinja templates undersrc/backends/go/templates/) -
C# backend Phase C: lifecycle hooks, WebSocket/SSE routes, and error exception hierarchy. The P/Invoke service-API codegen (
src/backends/csharp/gen_bindings/service_api.rs) now emits Phase C surface alongside the existing registration and entrypoint methods: (1) exception classes for eachErrorTypeDefin the IR under theSpikard.Errorsnamespace, each extendingSpikardExceptionbase withStatusCodeproperty returning the HTTP status code andToProblemDetailsJson()method returning RFC 9457 ProblemDetails JSON (e.g.NotFoundError,ValidationError,UnauthorizedError, etc.); (2) lifecycle hook registration methods injected into the service class (OnRequest,PreValidation,PreHandler,OnResponse,OnError) each acceptingAction<RequestData>and forwarding via P/Invoke to the native layer with GCHandle marshalling; (3)WebSocket(string path, Func<WebSocketConnection, Task> handler)andSse(string path, Func<IAsyncEnumerable<SseEvent>> producer)route registration methods marshalling async handlers via GCHandle. Error type file is emitted separately underSpikard/Errors/SpikardException.csfor clean namespace segregation. (src/backends/csharp/gen_bindings/service_api.rs)
Fixed
-
JNI codegen:
Vec<String>parameters now respectvec_inner_is_refand emit aVec<&str>materialisation when the core function takes&[&str]. Previously the JNI function/method shims unconditionally emitted&{name}forVec<String>slots, which coerces to&[String]but not&[&str]. Core fns declared asdownload(names: &[&str])failed to compile with E0308expected reference &[&str], found reference &Vec<String>. The shim now checksp.vec_inner_is_ref && Vec<String>and emits&{name}.iter().map(|s| s.as_str()).collect::<Vec<_>>(), mirroring the existing Dart codegen branch. (src/backends/jni/gen_shims/function_shims.rs,src/backends/jni/gen_shims/method_shims.rs) -
JNI return-marshal:
Option<T>for any inner type now short-circuitsNoneto a nulljstring. Previously onlyOption<String>had a dedicatedreturn_optional_string.rs.jinjabranch; every otherOption<T>(e.g.Option<EmbeddingPreset>fromget_embedding_preset) fell through to the genericreturn_json.rs.jinjaarm, which serialisesNoneto the four-character JSON literal"null". Kotlin then saw a non-nullStringcontaining those four characters, so consumer tests likeassertTrue(result == null)failed even when the Rust side returnedNone. The newTypeRef::Optional(_)arm inemit_return_marshal_with_indentwraps the serialise step in amatch v { None => std::ptr::null_mut(), Some(inner) => /* serialise inner */ }, so anyOption<T>return now produces a real null at the JVM boundary. (src/backends/jni/gen_shims/marshalling.rs) -
Kotlin-Android wrapper object name drops the appended
Convertersuffix.kotlin_android_wrapper_object_nameused to return<Crate>Converter(sokreuzberg→KreuzbergConverter), but the bridge file emitted by the JNI scaffold is<Crate>Bridge(KreuzbergBridge) and every e2e test calls the wrapper as<Crate>.method(...)(Kreuzberg.extractFile(...)). The mismatch producedUnresolved reference 'KreuzbergConverterBridge'in every wrapper that delegated to the bridge, andUnresolved reference 'Kreuzberg'in every alef-emitted test that used the public surface. Return the bare PascalCase stem (still stripping theRsbinding-crate suffix) so the wrapper and bridge file names line up. (src/codegen/naming.rs) -
Struct-field doc strings now flow through
sanitize_rust_idiomsbefore reaching the generated Rust source. The sharedgen_structcodegen insrc/codegen/generators/structs.rs(three call sites at lines 179, 293, 369) and the NAPI-specific struct emitter insrc/backends/napi/gen_bindings/types.rs:195were passingfield.docverbatim toStructBuilder::add_field_with_doc, bypassing the prose sanitizer that strips[X](crate::X)explicit-link targets. The core crate's rustdoc carried validcrate::DataNode/crate::ProcessResult::dataintra-doc links — propagated as-is intots-pack-core-node,ts-pack-core-py, andts-pack-core-php, those references becamebroken_intra_doc_links/redundant_explicit_linkserrors becausecrate::resolves in the originating crate but not in the binding crate. A newsanitize_field_dochelper applies the same target-agnostic transform that method/struct-level docs already go through; the NAPI emitter does the same inline. Method/struct-level docs already routed throughsanitize_rust_idioms, so this closes the field-doc gap. (src/codegen/generators/structs.rs,src/backends/napi/gen_bindings/types.rs) -
Dart FRB bridge crate now emits
#![allow(missing_docs)]. Every#[frb]-attributed function inpackages/<crate>/dart/rust/src/lib.rsis named from a paired Dart binding and carries no human-authored docstring — the name and the bridge contract are the spec. Without the allow, the sharedrustdoc-lintpre-commit hook (cargo doc -- -D missing-docs) blocked every consumer commit that regenerated the dart bridge. Mirrors the same fix landed earlier for the JNI shim. (src/backends/dart/gen_rust_crate/mod.rs)
Added
- Service-API IR: three new
RegistrationVariantStylevariants —Decorator,Attribute,Dsl. The pre-existingHybrid/VerbDecorator/Builderenum is extended so backends can emit per-language idiomatic surfaces beyond the original method-pair shape:Decoratoris the Python-style overloaded method that doubles as both direct registration (app.get(path, handler)) and decorator factory (@app.get(path)) by returning a callable when the handler argument is absent;Attributeemits annotation/attribute types (e.g.#[Get('/path')],@GetMapping(...),[HttpGet(...)]) paired with anapp.mount(ControllerClass)reflection scanner;Dslemits a nested routing-DSL entry point (e.g. Kotlin Ktorrouting {}, Elixiruse Routermacro block). Default remainsHybridso consumers without explicitstyledeclarations get the previous behaviour. Backends that cannot express a requested style (Attributein reflection-less languages,Dslin non-block-friendly targets) should fall back toHybrid. (src/core/ir/service.rs) - Service-API IR:
HandlerShapeenum onRegistrationDef. A new field controls how the generated wrapper binds the incoming request DTO to the host-language callback. Four variants:BareCallable(default — preserves existing behaviour: handler receives the full request DTO directly),ContextObject(handler receives a single ergonomic context handle exposing typed accessors — idiomatic in Hono, Vapor, Javalin, ASP.NET Minimal API),RequestResponse(handler receives separate request and response objects, with the response written to rather than returned — idiomatic in Express, chi), andIntrospectParams(handler signature is introspected at registration time so type-annotated parameters bind from path/query/body by name — idiomatic in FastAPI / Litestar). Backends dispatch on the shape when selecting the emission template. (src/core/ir/service.rs) - Service-API IR:
PathConstraintwithparse_pathnormalization. Three input syntaxes are now accepted and normalized uniformly to the canonical{name}form:{name:type}(typed),{name}(untyped), and:name(colon-prefix). A new fieldpath_param_constraints: Vec<PathConstraint>onRegistrationDefcarries(name, type_constraint)pairs extracted at extract time, while the normalized path string is emitted to the router unchanged. Supportedtype_constraintvalues:"int","uuid","slug","path"(greedy), and pass-through custom regexes;Nonemeans no structural constraint. Backends that don't support a particular constraint fall back to a plain parameter binding and let the native layer validate. (src/core/ir/service.rs) - Service-API IR: per-language style overrides via
RegistrationVariantLanguageOverride. A newlanguage_overrides: HashMap<String, RegistrationVariantLanguageOverride>map onRegistrationVariantlets each backend look up its canonical language name and override the variant-globalstyle,handler_shape, andmethod_prefix. The override mechanism is purely additive — absent keys fall through to the variant-global defaults. Enables alef.toml entries like[crates.services.registrations.variants.languages.csharp] style = "attribute"method_prefix = "Map"to produceMapGet/MapPostfor ASP.NET Minimal-style emission, or[…languages.kotlin] style = "dsl"for Ktor-style routing blocks, without disturbing other backends. (src/core/ir/service.rs) - Application IR:
LifecycleHookDeffor callback-slot registration. A new top-level IR entry describes a named lifecycle hook the service owner exposes to host-language code (e.g.on_request,pre_validation,pre_handler,on_response,on_error). Each hook carries a name, acallback_contractcross-referencing an existingHandlerContractDef::trait_name, anis_asyncflag controlling whether async-first languages emitasynccallback types, and documentation. Backends generate the matchingapp.on_<name>(fn)registration method (or its idiomatic equivalent) alongside the existing service entrypoints. (src/core/ir/application.rs) - Application IR: first-class
WebSocketRouteDefandSseRouteDef. Promotes WebSocket and SSE handler contracts from the previously-skipped category to first-class IR entries. Because both contracts useimpl Futurereturn types that are not object-safe (and so cannot be wrapped inArc<dyn Trait>), each entry names a concrete wrapper struct type (handler_wrapper_typefor WebSocket,producer_wrapper_typefor SSE) so backends emit a concrete monomorphisation instead of a trait-object bridge. The socket type and event type are also named so backends generate typed annotations in the handler signature. (src/core/ir/application.rs) - Application IR: cross-binding
ErrorTypeDef+HttpStatusenum. A new top-level IR section describes the exception hierarchy that every backend must emit as native error classes. EachErrorTypeDefcarries a PascalCase name (e.g.NotFoundError,ValidationError), anHttpStatusdiscriminating the response status (with named variants for the common 4xx/5xx codes plus aCustom(u16)escape hatch), an optional RFC 9457ProblemDetailstypeURI, and documentation. Backends generate native exception classes whose serialization produces the standardized ProblemDetails JSON body, so handler-thrown errors round-trip uniformly across every language binding. (src/core/ir/application.rs) - IR module split. The previously monolithic
src/core/ir.rsis reorganized into a module:application.rs(lifecycle hooks, WebSocket/SSE, error types),service.rs(service definitions, registrations, handler shapes, path constraints),items.rs(the pre-existing TypeDef/EnumDef/FunctionDef/MethodDef/etc.),metadata.rs(deprecation, version, default-value annotations),surface.rs(ApiSurface),type_ref.rs(TypeRef + PrimitiveType). Re-exports inmod.rspreserve the existing public API exactly — no consumer touch required. (src/core/ir/)
Fixed
-
NAPI options-field bridge: drop unused
muton theOption<JsOptions>.map(|o| …)closure binding. The generatedconvert(html, options)shim movedostraight intoo.into()without mutating the binding, so themutkeyword in the template triggeredwarn(unused_mut)on the consumer crate (e.g.crates/html-to-markdown-node/src/lib.rs:1775). Themutwas a copy-paste from the wasm-side template, where the closure does mutateo.<field>before theinto()call. Template: dropmutfrom the napi closure binding; the wasm template is unchanged. (src/backends/napi/templates/options_field_bridge_body.jinja) -
Magnus Ruby gemspec:
required_ruby_versionnow emits the array form[">= 3.2.0", "< 4.0"]instead of the comma-joined single string">= 3.2.0, < 4.0". RubyGems'Gem::Requirement.parsetreats a single requirement string as one expression and rejects embedded commas withGem::Requirement::BadRequirementError: Illformed requirement [">= 3.2.0, < 4.0"], sobundle install/gem buildagainst the v0.24.16-generated gemspec failed before reaching any source. The array form is the canonical RubyGems syntax for multiple bounds and parses cleanly on Ruby 3.2–3.5 as well as 4.x preview builds. The platform-gemspec propagation path (read_required_ruby_version+generate_platform_gemspec) was rewritten to capture the raw RHS (single-string or array literal) and re-emit it verbatim, so the upper-bound constraint is preserved on cross-compiled platform gems. Closes liter-llm v1.5.1 prek failure. (src/scaffold/languages/ruby.rs,src/publish/package/ruby.rs)