All notable changes to Caravela are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.11.0] — 2026-04-19
Ships the Caravela ↔ caravela_svelte integration
(phase_c1_generator_integration.md).
Entities now declare frontend: :rest (optionally with
realtime: true); the generator branches to emit either a
LiveView tree (:live) or a Phoenix controller (:rest), both
targeting caravela_svelte's shared client bundle. Generated
code delegates field-access and changeset-error handling to
CaravelaSvelte.Caravela helpers, so the same prop shape
reaches every Svelte component regardless of transport.
Added
entity :name, frontend: :rest | :live do … end— new entity-level option on theCaravela.DomainDSL. Defaults to:live, so existing domains are unaffected.:restmarks the entity for rendering throughcaravela_svelte's HTTP/Inertia transport instead of LiveView + WebSocket. Invalid values and unknown entity options raiseCaravela.DSLErrorwith actionable suggestions.entity :name, frontend: :rest, realtime: true do … end— opts the entity into SSE-driven real-time updates on top of the:resttransport. Generated controllers callCaravelaSvelte.Caravela.broadcast_patch/3on create, update, and delete, scoped per-actor viaentity_topic/2. Rejected with a clear error on:liveentities (LiveView's WebSocket already covers that case).Caravela.Schema.Entity{:frontend, :realtime?}— IR carries both the declared transport and the realtime flag. Exposed asentity.frontend(string) andentity.realtime(boolean) onCaravela.IR.of/1output, visible through thecaravela__describe_entityMCP tool response.Caravela.Gen.RestController— new generator. Emits one Phoenix controller perfrontend: :restentity atlib/<app>_web/controllers/<entity>_controller.ex. Renders Svelte components throughCaravelaSvelte.render/3; wiresCaravelaSvelte.Caravela.put_field_access/2andCaravelaSvelte.Caravela.errors/1on every action; addsbroadcast_patch/3call sites whenrealtime: true. Custom code below# --- CUSTOM ---markers is preserved on regeneration (same convention as the rest of Caravela's generators).mix caravela.gen.live --frontend <mode>— blanket override flag.--frontend restor--frontend liveapplies to every entity in the domain, ignoring declared values. Useful for a quick preview of generator output for a different mode without editing the DSL.
Changed
Caravela.Gen.LiveView.render_all/2now skips entities whosefrontendis:rest. A domain where every entity is:restproduces zero LiveView files.Caravela.Gen.LiveRoute.render/1now emits up to two router blocks per domain: alive …block for:liveentities (as before) and acaravela_rest …block for:restentities. Entities declared withrealtime: truegetrealtime: trueappended to theircaravela_restline so the router registers the SSE endpoint. The emitted snippet mentions the requiredimport CaravelaSvelte.Router.mix caravela.gen.livenow invokesRestControllerin addition toLiveViewandSvelte, so a single command produces the full frontend layer (controllers for:restentities, LiveViews for:liveentities, Svelte components for both). The next-steps message adapts to whichever modes are present.caravela__describe_frontend_mode— new MCP tool. Reports the render-mode configuration for a domain (or a single entity whenentity: "..."is provided):frontend,realtime, and short transport-specific notes an LLM host can use before suggesting code. Registered inCaravela.MCP.Tool.registry/0.@caravela-*metadata header on generated Svelte files — everyBook{Index,Show,Form}.sveltenow carries@caravela-entity,@caravela-mode, and (when applicable)@caravela-realtimetags in its top-of-file HTML comment. Lets the new MCP tool and any other static analysis pick up the render mode from the file without re-parsing the DSL.caravela_svelteas optional dep (~> 0.1). Caravela does not force the dep on consumers, but generated code referencesCaravelaSvelte.*modules, so apps using the generators must now pull in{:caravela_svelte, "~> 0.1"}alongside Caravela.
Changed
Generated LiveViews now mount via
<CaravelaSvelte.svelte>, not<LiveSvelte.svelte>. The prop and slot contract is identical, but both render modes now flow through the same client bundle (@caravela/svelte). Existing apps that regenerate must add{:caravela_svelte, "~> 0.1"}tomix.exs— see the new next-steps output frommix caravela.gen.live.Generated LiveView forms delegate
changeset_errors/1toCaravelaSvelte.Caravela.errors/1— a single authoritative implementation shared with the:restcontroller template, so error shapes stay aligned across modes.mix caravela.gen.livenext-steps output was rewritten for both modes to referencecaravela_svelteinstead oflive_svelte. The cold-start warning when the dep is missing now prints whenCaravelaSvelteisn't loaded, regardless of which modes are in play.
Deferred (post-1.0 ergonomics)
Caravela.Gen.svelte_component/2/ igniter recipe for scaffolding a single Svelte file per entity. Open question — whether this belongs here or incaravela_svelte.- Regenerating
caravela_demo's snapshot fixtures to use the newCaravelaSvelte.sveltemount. Done in the demo repo, not this package.
[0.10.0] — 2026-04-19
Ships §9 of llm_friendliness.md —
the MCP server. An LLM host (Claude Code, Cursor, Zed) can now call
Caravela tools directly instead of guessing DSL syntax. Shipped as
part of the main caravela package (not a separate repo) because
every tool is a thin wrapper over Caravela internals shipped in
0.9.x.
Added
Caravela.MCP— Model Context Protocol server over stdio transport. JSON-RPC 2.0 framing, one message per line. Protocol version2024-11-05.mix caravela.mcp— launch the server in a Caravela project. Compiles the project first (so domains are loadable for introspection) and serves on stdin/stdout. Intended to be spawned by an MCP host:// Claude Code ~/.claude/claude_desktop_config.json { "mcpServers": { "caravela": { "command": "mix", "args": ["caravela.mcp"], "cwd": "/absolute/path/to/your/project" } } }Four tools:
caravela__describe_domain— full IR for a domain module.caravela__list_entities— entity names (fast discovery).caravela__describe_entity— single entity + inbound / outbound relations.caravela__validate_dsl— compile a candidate DSL source and return either the resulting IR (on success) or a structuredDSLError/GenError/CompileErrorpayload (on failure). Rescues all exceptions — the tool never crashes the server.
Caravela.MCP.Toolbehaviour. Third-party tools slot into the same registry; shipped tools are all first-class citizens. SeeCaravela.MCP.Tool.ListEntitiesfor a minimal reference implementation.Caravela.MCP.Protocol— JSON-RPC 2.0 builders (response/2,error/4,method_not_found/2, etc.) and encode / decode for stdio framing.Caravela.MCP.Router— pure method dispatch (map → map).Caravela.MCP.Serverwraps it with the stdio IO loop, split so tests exercise the router without touching real IO.
Notes
- Stdio transport only in 0.10. HTTP (for remote / team MCP) and auth gating land in 1.1 per the plan.
caravela__validate_dslcompiles arbitrary Elixir source. Safe on localhost stdio (the host supplying the source is the user's own LLM client running on their machine). When HTTP transport arrives, this tool MUST move behind auth + a proper sandbox. Documented in the tool's moduledoc.- Specs enforced. All 10 new MCP modules (
Caravela.MCP,Caravela.MCP.Protocol,Caravela.MCP.Tool,Caravela.MCP.Router,Caravela.MCP.Server, plus the 4 tool modules) are on the.credo.exsallowlist — every public function carries a@spec, enforced by CI. - 42 new tests: protocol round-trip, router dispatch, each tool's
happy + error paths, full stdio handshake round-trip via
StringIO. 462 total now.
[0.9.2] — 2026-04-19
Kicks off the §10 spec pass from
llm_friendliness.md —
every public function on the stable-API allowlist now carries a
@spec. Wires Credo into CI as a hard gate so new public functions
without specs fail the pipeline.
Added
@specon every public function in ten stable-API modules:Caravela.IR,Caravela.Errors,Caravela.Error,Caravela.Types,Caravela.Naming,Caravela.Tenant,Caravela.Auth,Caravela.Policy,Caravela.Schema,Caravela.Gen.Custom. 85 specs added across the set.Type aliases where the same shape recurred:
Caravela.Naming.domain_or_module :: Domain.t() | module()Caravela.Naming.entity_name :: atom()Caravela.Naming.kind :: :index | :show | :formCaravela.Types.dsl_type :: atom()Caravela.Gen.Custom.style :: :elixir | :ts | :svelte
Credo dep (
~> 1.7, dev/test only) +.credo.exswithCredo.Check.Readability.Specsenabled and scoped viafiles.includedto the stable-API allowlist. Running a plainmix credo --strictlocally also surfaces advisory hygiene checks (nesting depth, function complexity) that aren't hard- gated yet.CI hard gate:
.github/workflows/ci.ymlnow runsmix credo --strict --only Readability.Specsbetween compile and test. New public functions added to allowlisted modules without a@specfail the pipeline. Modules outside the allowlist (mix tasks, generator internals,Live.*macros) are intentionally excluded until they stabilize — see the comments in.credo.exsfor the rationale.
Notes
- The allowlist grows as modules stabilize. Intended pattern for
future additions: add the file path to
.credo.exs, runmix credo --strict --only Readability.Specs, add the missing specs it flags, done. No per-module config beyond the include list. - This is presence enforcement (every public fn has a spec), not correctness enforcement. Spec correctness is Dialyzer's job; wiring Dialyzer into CI is deferred to the 1.1 phase per the LLM plan.
[0.9.1] — 2026-04-19
Second pass of the LLM-friendliness roadmap
(llm_friendliness.md).
Completes the §4 error-message rewrite across every remaining DSL and
generator raise site, and ships the §10 mix caravela.info companion
to mix caravela.ir.
Added
mix caravela.info— human-readable summary of a compiled domain. Prints domain-level flags (multi-tenant, default policy, api version), a per-entity block with fields / relations / policy summary / auth config, and a footer with hook + relation counts. Companion tomix caravela.ir(which emits JSON); useinfofor humans scanning "what's in this domain?" andirfor tooling consuming the IR programmatically.mix caravela.info MyApp.Domains.Library mix caravela.info MyApp.Domains.Library --no-color # for CI / pipingExposed as a library call via
Mix.Tasks.Caravela.Info.render/2so tests and external tools can consume the same rendering without shelling out.
Changed (breaking)
Remaining DSL error sites migrated from
ArgumentError/CompileErrortoCaravela.DSLError. In particular every raise routed throughCaravela.Domain.compile_error!/2andCaravela.Compiler.compile_error!/2now carries the structured four-part message shape. Covers:- Policy DSL: duplicate scope rules, malformed
field :x, optsinsidepolicy,policywith a non-atom entity, action-gate arity validation. - Authenticatable DSL: missing strategies, missing email field
for
:password, manual declaration of auto-injected fields, invalidapi_token :ttlshape, multiple authenticatable entities per domain. - Live runtime DSL:
Caravela.Live.Domain.on_event/updaterarity + shape errors. - Live form DSL:
Caravela.Live.Form.visible/validate_asyncfield / arity / opts errors. - Compiler-level validations: entity / field / relation /
hook cross-checks emitted from
Caravela.Compiler.
- Policy DSL: duplicate scope rules, malformed
Generator error sites migrated to
Caravela.GenError. The threeGen.Auth*"no entity with anauthenticatableblock" raises now carry a structured message with a canonical fix suggestion.Tests asserting
ArgumentErrororCompileErroragainst any of these surfaces need to switch toCaravela.DSLErrororCaravela.GenError. The regex from the old assertion still matches the new message.
Kept as ArgumentError (intentionally)
Caravela.IR.of/1bad-argument errors (runtime API, not DSL).Caravela.Flow.start/3runtime "no flow named" lookup failures.Caravela.Live.Templateruntime updater-not-found / arity-mismatch errors (not compile-time — fire whenapply_updateris called with a bad name at runtime).
These sites are documented in-code; they don't benefit from the structured format since there's no compile-time suggestion to make.
Fixed
- Pre-existing
ArgumentErrorassertions in the test suite updated for 9 sites that exercised migrated paths. No behavior change; assertions now name the correct exception type.
[0.9.0] — 2026-04-19
First phase of the LLM-friendliness roadmap (llm_friendliness.md). Delivers structured errors, a unified validation command, and a public IR export — the "close the iteration loop" half of the plan. Eval harness, natural-language scaffolder, and MCP server land in subsequent releases.
Added
Caravela.IR— public, JSON-serializable view of a compiled domain. CallCaravela.IR.of(MyApp.Domains.Library)to get a plain map of entities, fields, relations, policies, hooks, and auth config. Anonymous functions inside policies are not included; only their metadata (existence, arity, target). The shape is semver-stable starting from this release.mix caravela.ir— print the IR as JSON (or write it to a file). Intended for consumption by editors, LLMs, and external tooling that wants a structured view of the domain without parsing Elixir source.mix caravela.ir MyApp.Domains.Library > library.json mix caravela.ir MyApp.Domains.Library --output docs/library.json mix caravela.ir MyApp.Domains.Library --no-prettymix caravela.check— single-command validation oracle. Compiles the project, discovers every module usingCaravela.Domain, runs all applicable generators in dry-run mode, and optionally runsmix test(--tests) andmix dialyzer(--dialyzer). Exits 0 on green, non-zero with a per-domain / per-generator summary otherwise. Targeted at LLM iteration loops and CI — one command, one pass/fail signal.mix caravela.check mix caravela.check --only MyApp.Domains.Library mix caravela.check --tests --dialyzer mix caravela.check --quietCaravela.DSLError/Caravela.GenError— structured exceptions with a four-part message: what went wrong, what Caravela got (code snippet), suggested fix, docs URL. Every migrated DSL error now spells out both the problem and a canonical example of the right shape.Caravela.Errors.dsl/2andsnippet_from_env/1are convenience helpers for macro authors.Example rendered output:
** (Caravela.DSLError) `scope` must be called inside a `policy` block Suggestion: policy :books do scope fn q, actor -> where(q, [b], b.tenant_id == ^actor.tenant_id) end end See: https://hexdocs.pm/caravela/policies.html#scope
Changed (breaking)
DSL errors now raise
Caravela.DSLErrorinstead ofArgumentError. Migrated sites:default_policyoption,versionoption,scope/allow/fieldinside policy blocks, everyauthenticatablesub-macro (strategy,session,confirm,reset, hook blocks, unknown strategy names),flowname validation, andCaravela.Types.ecto_type/1/postgres_type/1on unknown field types.If you had
assert_raise ArgumentErrortests against any of these surfaces, update toassert_raise Caravela.DSLError. The structured message is strictly more informative; the regex from the old assertion will still match the new message.Caravela.Gen.SvelteForm.render/2raisesCaravela.GenErroron a missing-entity reference (previouslyArgumentError) and includes the list of known entities in the suggestion.
Migration
No source changes required for domain files; the migration is only
visible if you had tests asserting on the old exception type. Run
mix caravela.check after upgrading to confirm everything parses
cleanly.
Not every raise ArgumentError site has been migrated yet — this
release covers the DSL and generator surfaces that users hit most
often. Runtime Caravela.Live.* errors and a handful of generator
edge cases still raise ArgumentError and will migrate
incrementally in 0.9.x / 1.0.
[0.8.1] — 2026-04-19
Added
Checksum header on every generated file. Every regenerable file now starts with a line of the form
# caravela-gen: generator=context version=0.8.1 above_sha256=<hex>(or the
//- /<!-- -->-wrapped equivalent for TS and Svelte outputs). The sha256 covers the content above theCUSTOMmarker. On regeneration the hash is recomputed against what's on disk; if the above-marker region was edited by hand, the generator aborts viaMix.raise/1and points the user at three remediation steps (move edits below the marker, re-run with--force, or--dry-runto inspect). Named-block bodies are excluded from the hash, so edits inside a per-function# --- CUSTOM :name ---block don't trigger a mismatch.Per-function named CUSTOM blocks. Every Elixir-style generator now emits pairs like
# --- CUSTOM :list_books --- # --- END :list_books ---at natural extension points — after each CRUD function in the context, after each action in the controller, around the changeset in the Ecto schema, per entity in the GraphQL types/queries/ mutations, after
mount/3and beforerender/3in LiveViews, and around register/login/logout/reset/confirm in the auth context. Regeneration merges content from these blocks by name: add user code inside:list_books, regen freely, code is preserved.Orphan detection. If a named block exists on disk but no longer has a counterpart in the generator output (e.g. an entity was renamed), regen emits a
Mix.shell/0warning listing every orphan name and discards the content. The file-tailCUSTOMmarker's contents still carry through for freeform user code.--forceflag on everymix caravela.gen.*task now threads through toCaravela.Gen.Custom.verify_existing!/2, printing a yellow warning with the stored/current hashes and proceeding to overwrite. Previously the flag only controlled the file-exists prompt.
Fixed
- Merge bug when the marker string appeared inside a docstring.
The old
String.split(…, marker, parts: 2)cut on the first occurrence, which in Elixir templates was the moduledoc prose (Custom code placed below the \# --- CUSTOM ---` marker is preserved.). Regeneration then produced garbled output for any template whose moduledoc mentioned the marker.Gen.Custom.merge/3and the hash splitter now use:binary.matches/2 |> List.last/1so they always latch onto the real tail marker. ### Changed -Caravela.Gen.Customrewritten from the file-tail-only merge helper into a full verification + merge pipeline with pluggable comment styles (:elixir,:ts,:svelte). Public API additions:namedempty/2,extract_named_blocks/1,merge_named/3,stamp_header/2,verify_existing!/2,verify_contents/2,parse_header_line/2.marker/1now takes an optional style. - The ad-hoc@ts_marker/@svelte_marker/merge_ts/2/merge_svelte/2duplicated acrossGen.Svelte,Gen.SvelteForm, andGen.AuthSveltehas been removed — all three now delegate toCaravela.Gen.Custom.merge_with_file/3withstyle: :tsorstyle: :svelte. ### Migration No source changes required. The first time you run anymix caravela.gen.` task after upgrading, Caravela treats existing files as "unheadered legacy" and silently stamps a header. Subsequent regens enforce the checksum. If you had been editing above the# --- CUSTOM ---marker, the first post-upgrade regen after that edit will abort with a clear message; re-run with--force(to overwrite) or move the edits below the marker (to keep them). ## [0.8.0] — 2026-04-18 ### Changed (breaking) - *Deny-by-default policy fallback. Domains now default todefault_policy: :deny. Any entity without a declaredpolicyblock has its scope filtered to zero rows, every field masked out, and every write denied. The prior permissive behavior is still available asuse Caravela.Domain, default_policy: :allow. Motivation: a forgotten policy on a new entity used to silently ship unscoped data. With deny-by-default, the same forgetful moment produces an obviously-empty list instead of a leak. If you want per-entity control, add a minimal `policy :entity do scope fn q, -> q end end— declaring *any* policy block for an entity makes its undeclared rule types permissive for that entity (the "per-entity fallback" tier of the cascade). - **Generatedcomputefieldaccess/2routes every field through the policy dispatch function.** Previously, unruled fields were hardcoded to literaltruein the generated context; the hardcoding hid the domain-leveldefaultpolicyfrom reaching them. Now every field callscaravela_policy_field_visible/3and the clause cascade on the domain module (specific rule → per-entity fallback → module-level fallback) decides the result. No visible runtime difference for domains without policies that stayed on:allow. ### Added -Caravela.Schema.Domain.default_policy/1helper returning:denyor:allow. -use Caravela.Domainnow importsEcto.Query.where/3andEcto.Query.from/2into the calling module, soscope fn q, actor -> where(q, [b], b.published) endworks without a manualimport Ecto.Query(previously a silent runtime failure). ### Migration - If any of your domains rely on the old permissive fallback, adddefault_policy: :allowto theuse Caravela.Domaincall. - If you want to tighten up an existing domain, remove the:allowoption and add apolicy :entity do …block per entity. Missing rule types inside the block stay permissive, so partial declarations are safe. ## [0.7.0] — 2026-04-18 ### Removed (breaking) -can_read/can_create/can_update/can_deletemacros and the__caravela_permission/2,3,4dispatch functions they emitted. These ran in parallel with phase 9'spolicyblocks, producing an implicit intersection of two authorization systems with no documentation of the interaction. Since the library has no published users yet, the simpler path was to delete outright instead of deprecating. -Caravela.Schema.Permissionstruct and thepermissionsfield onCaravela.Schema.Domain. - Generated context helpersapply_read_permission/3,authorize_create/2,authorize_update/3,authorize_delete/3. Read paths now go throughapply_scope/3; write paths go throughpolicy_authorize/3,4. Both resolve against thepolicyblock's compiled dispatch functions on the domain module. ### Migration Port everycandeclaration into apolicy :entity do … endblock. Rule functions now receive the **actor** (context.current_user), not the raw context map: ```elixir # before can_read :books, fn q, ctx -> where(q, [b], b.published) end can_create :books, fn ctx -> ctx.current_user.role == :admin end can_update :books, fn b, ctx -> ctx.current_user.id == b.author_id end can_delete :books, fn _b, ctx -> ctx.current_user.role == :admin end # after policy :books do scope fn q, actor -> if actor.role == :admin, do: q, else: where(q, [b], b.published) end allow :create, fn actor -> actor.role == :admin end allow :update, fn actor, record -> actor.id == record.author_id end allow :delete, fn actor -> actor.role == :admin end end ``` The generated context is the one that did the dispatch, so the only runtime change is the shape of the predicate's first argument. ## [0.6.0] — 2026-04-18 ### Added - **Phase 7 —authenticatabletrait.** Declareauthenticatableon an entity andmix caravela.gen.authemits the full email/password + API-token server stack: auth context (register/login/logout, sessions with TTL +remember_me+max_sessions, email confirmation, password reset, scoped API tokens), session schema, Plug pipeline (fetch_current_user,require_auth,require_role,require_scope), LiveViewon_mounthooks, auth controller, and a session-tokens migration.on_register/on_loginlifecycle callbacks. Validated compile-time: one authenticatable entity per domain,:emailfield required when:passwordstrategy is used, no collision with auto-injectedhashed_password/confirmed_at/api_tokens. - **Phase 8 — LiveSvelte auth UI.**mix caravela.gen.authalso emits six Svelte components —LoginForm,RegisterForm(inputs derived from the user entity's public fields),ResetPasswordForm(two-phase: request + confirm),ConfirmEmail,TokenManager(scopes +max_tokensreflected from the DSL),SessionList— plus matching LiveView pages (AuthLive.), and prints a ready-to- paste router snippet (public auth routes + authenticatedlivesessionwithon_mounthook + authenticated API scope). Generated User schema gainsregistration_changeset,password_changeset,api_tokens_changeset,confirm_changesetwith Argon2 password hashing. TS types addCurrentUser,ApiToken,Session. Flags--skip-ui/--skip-routerfor server-only use. - **Phase 9 — Triple-target policies.** Newpolicy :entity do … endblock in the domain DSL compiles a single declaration into three simultaneous enforcement layers: (1) EctoWHEREclauses viascope fn query, actor -> query end, (2) field projection that strips invisible fields from everylist/get_response viafield :name, visible: fn actor[, record] -> bool end, and (3) a typedfieldaccessSvelte prop (<Entity>FieldAccessTypeScript interface) that the generated index/show/form components use to gate ruled columns, fields, and inputs with{#if field_access.*}.allow :create | :update | :delete, fn actor[, record] -> bool endextends the Phase 2 permission check with record-aware gates. Arity-1 rules resolve to booleans at request time; arity-2 rules resolve to a'per_record'sentinel and evaluate per row, with denied fields served asnull. Unpolicied entities fall through to permissive defaults — fully backward-compatible with existingcanhooks. -Caravela.Policymodule withEntry,Scope,FieldRule,ActionGateIR structs, andDomain.policyfor/2/auth_entity/1lookups. - New guides: [docs/auth.md](docs/auth.md), [docs/policies.md](docs/policies.md). ### Changed - Generated TypeScript type imports are now combined (import type { Book, BookFieldAccess, LiveHandle } from '…') instead of one line per type. - GeneratedUserEcto schema excludeshashed_password,confirmed_at,api_tokensfrom the genericcastpath — they flow only through the specialised changesets above. -Caravela.Gen.Svelte.public_fields_for/1also strips credential fields (hashed_password,api_tokens) from the Svelte-bound representation, so they can never reach the browser through either the typed TS interface or a CRUD form. ## [0.5.3] — 2026-04-18 ### Added -Caravela.Error— uniform error struct (:unauthorized,:not_found,:invalid,:internal) so upstream code can pattern- match once instead of threading the four shapes individually generated contexts return today.wrap/1lifts a plain reason into the struct;message/1renders a default flash phrase. -Caravela.Live.OnMount—on_mountcallback that assigns a:contextmap (%{current_user, tenant}) to the socket, so LiveViews can read from@contextinstead of each re-rollingbuild_context/1.put/3exposes a merge helper for downstream hooks. -Caravela.Gen.Contextnow emits adelete<entity>(id, context)variant alongside the existing(struct, context)form, so delete-from-index is one round trip (get |> delete). Returnswhen the id is missing or hidden. The generated index LiveView uses the shorter path. ### Changed - Generated--with-domainform LiveView now passes keyword args (applyupdater(:load, entity: e, attrs: a, errors: er)andapply_updater(:put_attr, field: f, value: v)) instead of anonymous tuples. The emittedFormDomainmatches withKeyword.fetch!/2. Self-documenting and survives adding a new field without silently shifting positional args. ### Fixed - Generated Svelte components now destructure thelivehook handle LiveSvelte ≥ 0.18 actually injects, and calllive.pushEvent(...). Previously every generated component pulledpushEventout of$props()— which never existed — so every Edit / Delete / New / Save / Cancel button silently threwTypeError: pushEvent is not a functionand the corresponding server event never fired. -Caravela.Gen.Contextnow emitspreload([:assoc, …])onlist/get*/get!for everybelongstoassociation declared on the entity. Rows no longer ship the raw%Ecto.Association.NotLoaded{}sentinel (withowner/field/cardinalityinternals) to the browser, and dereferencingbook.author.nameon the Svelte side now works without a hand-written preload. - Generated--with-domainand plain form LiveViews replace theMap.fromstruct(entity) |> Map.drop([:meta])attrs seed with a narrowentityattrs/1helper that keeps only the declared entity fields and normalises%Decimal{}values to their string form. Form inputs on an Edit screen no longer render[object Object]for decimal / money fields, and the attrs map has stable types across validate round-trips. ### Changed - Generated LiveViewrender/1now calls<LiveSvelte.svelte>(passingsocket={@socket}) instead of the deprecated<LiveSvelte.render>component. Silences the deprecation warning on every request and re-enables SSR for connected sockets. - NewCaravela.Live.Encodersmodule providesLiveSvelte.Encoderprotocol implementations forDecimal(→ normalised string) andEcto.Association.NotLoaded(→nil), guarded so the file compiles even when LiveSvelte < 0.18 is in use (the protocol is absent there). - Generated TypeScript types file now exports aLiveHandleinterface describinglive.pushEvent/pushEventTo/handleEvent, which every generated component imports for itsliveprop. ## [0.5.2] — 2026-04-18 ### Changed - Generated Svelte components and doc examples now use Svelte 5's$props()rune for prop declarations instead of the deprecatedexport letsyntax. LiveSvelte 0.19 ships with Svelte 5 runtime;export letstill worked but produced compile-time warnings. ## [0.5.1] — 2026-04-18 ### Fixed - Generated--with-domainform LiveView no longer crashes withKeyError :errorson mount. The template now seeds the domain's default state (viaCaravela.Live.Template.__assign_defaults/2) before the firstapply_updater(:load, ...)call. -Caravela.Flow.Runner—raceadvances as soon as the first task resolves instead of waiting the full timeout (was relying onTask.yield_many/2's "wait for all, take first"). -Caravela.Gen.Contextemits simplifiedauthorize/rundelete_hookfunctions when no correspondingcan/ondeleterule is declared, eliminating the "clause will never match" warnings that appeared on every compile of a fresh CRUD generation. -Caravela.Gen.SvelteFormandCaravela.Gen.Sveltenow emit Svelte 5 event attribute syntax (onchange={...},oninput={...},onclick={...},onsubmit={...}) instead of the deprecatedon:eventdirective form. ### Added -Caravela.Flow—:tagstart option. When set, every notification is delivered wrapped as, letting a single listener driving many flows demultiplex without forwarder processes. ### Changed -Caravela.Live.Domain/Caravela.Live.Form— when theupdater/onevent/visiblemacros reject a value they can't arity-check at compile time, the error message now points the reader at the accepted shapes (fn ... endor&Module.fun/N) and the wrap-it-in-fnworkaround for bound function variables. -Caravela.Gen.Migrationmoduledoc now documents the:timestampoption for deterministic output (snapshot tests / demo pages). Behavior unchanged; only documentation. ## [0.5.0] — 2026-04-18 Phase 5 — dynamic Svelte forms with server-driven visibility and async validation, plus theCaravela.FlowGenServer runtime for composable async workflows. ### Added -Caravela.Live.Form— DSL layered onCaravela.Live.Domainfor form-visibility predicates (visible/2) and async field validators (validateasync/3). Each form-domain module exposescaravela_form/0,caravela_form_visibility/1,_caravela_form_visible/2, and__caravela_form_validate_async/3for introspection and runtime dispatch. -Caravela.Gen.SvelteForm— generator that reads a form-domain module plus its owningCaravela.Schema.Domainand emits<Entity>FormDynamic.svelte. The component declaresfield_visibility/async_errorsprops, wraps guarded fields in{#if field_visibility.}, and debounces async-validationpushEventcalls client-side. -Caravela.Flow/Caravela.Flow.DSL—use Caravela.Flowplusflow/3,sequence,repeat,wait,waituntil,debounce,set_state,run,parallel,race, andeachmacros. Compiles to nested step-tree structs inCaravela.Flow.Steps. -Caravela.Flow.Runner— GenServer interpreting step trees. Supports retry/backoff (linear + exponential),wait_untilthat unblocks onsignal/2,debouncethat resets on state change during the pause, parallel/race task orchestration, and per-itemeachiteration with{:ok|:skip|:error, }returns. -Caravela.Flow.Supervisor— optional DynamicSupervisor for flow runners.Caravela.Flow.start/3attaches runners when the supervisor is running, falls back to unsupervisedstartlinkotherwise (useful in tests and tooling). - Top-level API:Caravela.Flow.start/3,Caravela.Flow.signal/2,Caravela.Flow.get_state/1,Caravela.Flow.stop/1,2. Flows deliver{:flow_state, },, andmessages to the:notifypid. -docs/flows.md— new guide covering the flow DSL, primitives, the real-time loop through LiveView + LiveSvelte, and the scope boundary (no event sourcing). -docs/liveruntime.md— extended withCaravela.Live.FormandCaravela.Gen.SvelteFormsections. ### Scope - Flows are strictly ephemeral: in-memory state, no persistence, no event sourcing. Teams needing durable event streams should use [Commanded](https://github.com/commanded/commanded). ## [0.4.0] — 2026-04-17 Phase 4 — LiveView + typed Svelte component generation,Caravela.Live.runtime for composable state, and a docs restructure. ### Added -mix caravela.gen.live— generates three LiveView modules per entity (index / show / form) plus matching typed Svelte components and a TypeScript interfaces file. LiveViews mount components via<LiveSvelte.render>and delegate CRUD to the generated context, so authorization, hooks, and multi-tenant scoping apply for free. -Caravela.Gen.LiveView+Caravela.Gen.Svelte— EEx-backed generators that emit index/show/form templates. Both respect the# --- CUSTOM ---marker (TypeScript uses// --- CUSTOM ---, Svelte uses<!-- --- CUSTOM --- -->). -Caravela.Gen.LiveRoute— prints aliverouter scope snippet with four routes per entity (index,:new,:show,:edit), analogous toCaravela.Gen.RouterScopefor the JSON API. ---with-domainflag onmix caravela.gen.live— also emits aCaravela.Live.Domaincompanion module per entity and regeneratesform.exfrom a Template-backed variant. Index and show stay plain. Useful as an onramp to theCaravela.Live.runtime. -Caravela.Live.Updater— composable assigns-transformer helpers:run/2,3,compose/2,embed/2, and the~>pipe operator. -Caravela.Live.Domain—usemacro withstate,updater,onevent, andoninfoDSL for server-side state machines. Compile-time checks enforce updater arity (1or2) and require string event names. Theuseblock sets@caravelalivedomain MODULEsoapply_updater/2,3resolves inside domain bodies without an explicit module argument. -Caravela.Live.Template—use Caravela.Live.Template, domain: Modbinds a LiveView to aLive.Domainmodule, injectingmount/3,handle_event/3,handle_info/2, andapply_updater/2,3. Unknown events log a warning instead of crashing; all callbacks aredefoverridable. - Naming helpers:live_module/3,live_file_path/3,svelte_component_name/2,svelte_component_ref/3,svelte_file_path/3,svelte_types_file_path/1— all version-aware. - Documentation split into topic guides underdocs/(getting_started, dsl, generators, multi_tenancy, versioning, graphql, livesvelte, live_runtime, regeneration), wired intomix docsas ex_doc extras. README trimmed to a minimal entry point. - GitHub Actions workflow (docs.yml) that deploysmix docsoutput to GitHub Pages on every push tomain. HexDocs continues to publish on tag release. ### Changed - **BC-preserving rename.**Caravela.Live.Updater.apply/2,3→run/2,3to avoid shadowingKernel.apply/2,3.apply/2,3remains as an undocumented alias. - Generated SvelteBookShow.sveltenow renders fields with the same null-safe expression as the index, so a missing field prints—instead ofundefined. - Generated SvelteBookIndex.sveltenow includes a "New book" button that dispatchespushEvent('new', {}); the matchinghandle_eventnavigates to the form route. ### Fixed -Caravela.Live.Domaindocstring previously showed an example that wouldn't compile:apply_updater/2,3was invoked insideon_eventbodies but the macro required@caravela_live_domain, which was only set byuse Caravela.Live.Template. Now set byCaravela.Live.Domaintoo. - Removed an unuseddirtylocal from the generated Svelte form. ## [0.3.0] — 2026-04-17 Phase 3 — multi-tenancy, API versioning, Absinthe/GraphQL generation. ### Added -use Caravela.Domain, multi_tenant: true— opts into row-level multi-tenancy.Caravela.Tenantauto-injects a:tenant_id(:binary_id,null: false) field into every entity, and the generated context gainsscope_tenant/2+inject_tenant_id/2helpers driven bycontext.tenant.id. - Migrations in multi-tenant domains add thetenant_idcolumn and composite[:tenant_id, :<fk>]indexes alongside each FK index, plus a standalone[:tenant_id]index on tables with no FKs. -version "v1"DSL directive — all generated Elixir modules and file paths are namespaced under the version segment (MyApp.Library.V1.Book,MyAppWeb.V1.BookController,lib/my_app/library/v1/book.ex). The router snippet is emitted atscope "/api/v1", MyAppWeb.V1. Table names stay version-free so rows are shared across versions. - Two new compile-time validations: invalid version format (must match~r/^v\d+$/) and manual:tenant_iddeclarations colliding with auto-injection. -Caravela.Gen.GraphQL— renders Absinthe object types, query object, and mutation object (with typed input objects) for the domain. Every resolver delegates to the generated context, so authorization, hooks, and tenant scoping apply to GraphQL for free. Tenant-injected fields are hidden from both object and input types. -mix caravela.gen.graphqltask — checks for Absinthe at runtime and prints an actionable error if the optional dependencies are missing. - Generated controllers readconn.assigns[:tenant]into the context when the domain is multi-tenant. ## [0.2.0] — 2026-04-17 Phase 2 — hooks, permissions, Phoenix context + JSON API generators. ### Added - Hook DSL:on_create/2,on_update/2,on_delete/2on any entity. Hooks run between authorization and the finalRepocall in the generated context.on_deletemay returnto abort the delete. - Permission DSL:can_read/2,can_create/2,can_update/2,can_delete/2.can_readis applied as an Ecto query filter; the other three return booleans and afalseshort-circuits the context function with. - Compiled domain modules expose__caravela_hook/4and__caravela_permissiondispatch functions with safe fallbacks. - Three new compile-time validations: hook / permission arity, unknown entity references, duplicate (action, entity) declarations. -Caravela.Gen.Context— Phoenix context generator with CRUD functions per entity (list,get,get!,change,create,update,delete). -Caravela.Gen.Controller— JSON controller generator (REST actions, standard status codes, changeset → 422 translation). -Caravela.Gen.RouterScope— prints thescope "/api", MyAppWeb do … endsnippet to paste into the host app's router. -Caravela.Gen.Custom— preserves user code below the# --- CUSTOM ---marker across regenerations. Schemas, contexts, and controllers all ship with the marker. - Mix tasks:caravela.gen.context,caravela.gen.api, and the all-in-onecaravela.gen. ## [0.1.0] — 2026-04-17 Initial public release. Phase 1 — DSL, compiler, and schema/migration generators. ### Added -Caravela.DomainDSL:entity,field,relation. -Caravela.Compilerwith six compile-time validations: unknown field types, numeric-constraint/type mismatches, duplicate entities, dangling relation targets, incompatible cardinality, circular requiredbelongs_tochains. -Caravela.Gen.EctoSchema— Ecto schema generator (with changeset, required/format/length/numeric validations). -Caravela.Gen.Migration— Ecto migration generator, topologically sorted, with foreign-key indexes and appropriateon_deleterules derived fromrequired:. -mix caravela.gen.schema MyApp.Domains.<Module>task with--dry-runand--forceoptions. -:binary_id` primary and foreign keys by default. [Unreleased]: https://github.com/rsousacode/caravela/compare/v0.4.0...HEAD [0.4.0]: https://github.com/rsousacode/caravela/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/rsousacode/caravela/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/rsousacode/caravela/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/rsousacode/caravela/releases/tag/v0.1.0