V30, committed codegen: make the diagram the source of truth

September 18, 2026 · ALA · V30 · LiveView · codegen

The V36 vernacular core held ALA by discipline and leaned on a linter to catch drift. V30 makes the opposite bet. Instead of trusting a convention and checking it after the fact, it makes the ALA diagram a literal artifact in the repository, generates the wiring from it, commits the generated code, and fails the build the moment the code and the diagram disagree. The guarantee stops being social and becomes mechanical.

Spray’s ALA says the diagram is the source of truth and the code is a faithful transcription of it. V30 takes that at its word.

The diagram is a data file

A page’s wiring lives in a Manifest: plain Elixir data, no macros, no use. Reading it answers the questions the diagram answers. Which features are on this page? Who reacts to what? What renders where?

defmodule ZeroCoupledWeb.CartPage.Manifest do
  alias ZeroCoupled.Features.{CartItems, Undo, SavedItems, Wishlist, CheckoutFlow, PageUI}
  alias ZeroCoupled.Features.CartItems.Facts, as: CartFacts
  alias ZeroCoupled.Features.Undo.Facts, as: UndoFacts

  # slot → feature (one field per feature on the page)
  def features do
    [cart: {CartItems, render: :render_data}, undo: Undo, saved: SavedItems,
     wishlist: Wishlist, checkout: CheckoutFlow, ui: PageUI]
  end

  # a typed fact from one feature → a reaction on another, wired here and only here
  def reactions do
    [
      {CartFacts.ItemRemoved, to: {:undo, Undo.Intents, :capture_removed}},
      {UndoFacts.ItemRestored, to: {:cart, CartItems.Intents, :receive_item}},
      {CartFacts.PromoRejected, to: {:ui, PageUI.Intents, :set_promo_error}}
    ]
  end

  def action_views, do: [index: IndexView, checkout: CheckoutView]
end

This is the whole cross-feature story of the page in one readable place. No feature calls another; a fact one feature emits is routed to a reaction on another here, in the manifest, which is the composition. And because the manifest is ordinary Elixir, a typo’d fact alias or a renamed feature module is a plain compile error, not a runtime surprise.

The glue is generated, and committed

You do not hand-write the boilerplate that turns that manifest into a working page. You run mix zc.gen, and it writes generated.ex: the session struct with one field per slot, the fact-routing dispatch, the reaction runners. The generated file starts with a banner and is checked into the repository:

# ══════════════════════════════════════════════════════════════════════
# GENERATED FILE — do not edit.
# Source of truth: ZeroCoupledWeb.CartPage.Manifest (change it first, then run
# `mix zc.gen`). `mix zc.gen --check` fails the build on drift.
# ══════════════════════════════════════════════════════════════════════
defmodule ZeroCoupledWeb.CartPage.Session do
  defstruct cart: nil, undo: nil, saved: nil, wishlist: nil, checkout: nil, ui: nil

  def new(opts \\ []) do
    struct!(__MODULE__,
      cart: ZeroCoupled.Features.CartItems.init(opts),
      undo: ZeroCoupled.Features.Undo.init(opts),
      # ... one per slot, straight from the manifest
    )
  end
end

Committing generated code is a deliberate choice, and it is the interesting one. A macro would produce the same wiring at compile time, invisibly. V30 instead makes the wiring a file you can open, read, grep, set a breakpoint in, and diff in a pull request. Nothing about the page expands behind your back. The cost of that transparency is that the file can go stale, which is exactly what the next piece prevents.

CI fails on drift

The manifest is the source of truth only if the code cannot quietly disagree with it. So mix zc.gen --check regenerates each page in memory and fails if the committed file differs, and that check is wired into the test alias:

mix zc.gen              # regenerate every page's generated.ex
mix zc.gen --check      # fail if any committed file is stale (runs in CI)

Now the guarantee is real. You change the manifest and forget to regenerate: CI red. You hand-edit the generated file: CI red. The only green state is one where the committed glue is exactly what the diagram produces. The diagram is the source of truth because the build refuses to let it not be.

Why this reads as ALA, and where it sits

The layering is clean and enforced. Features are pure, zero-coupled modules that emit typed facts and never name each other. The manifest is the application layer, the one place that knows the page is made of these features wired this way. The generated glue is derived composition, not a new abstraction, so it does not muddy the diagram. When ala_lint runs against V30 with a layer map it reports zero peer coupling and zero upward edges, and its committed-codegen banner means the linter skips the generated file so a manifest-to-glue pair is not mistaken for hand-authored duplication.

There is a sharper, information-theoretic way to say why the generated glue does not muddy the diagram, and it lets you rank frameworks on one axis. Treat the code a feature adds as a message and measure its content in bits — not the raw Shannon entropy of the source text, which only rewards terse syntax, but the conditional information of the new code given the codebase already present, estimated by a language model’s cross-entropy (its per-token log-loss, which is Shannon information in bits). Measured across the variant corpus, the manifest runs dense at about 4.01 bits per token (it is the decisions), a hand-written LiveView sits at 2.63, and the generated glue falls to 1.12 bits per token standalone and 0.44 once the generator is in its context — derivative, near-predictable, close to zero essential information. The lesson generalizes past V30: the floor is domain entropy, which is architecture-invariant, so every framework competes only on its gap to that floor and none beats it, and mix zc.gen --check is really a boilerplate detector — it certifies that the committed glue carries no information beyond the manifest.

On the familiarity scale V30 sits at 4, a notch below V36’s 5 and above the variants that come after. The manifest is data and the features are plain modules; the single new idea a developer must hold is “change the diagram, then regenerate.” That is a smaller concept than a manifest DSL or a bespoke macro, which is why V30 is the most approachable variant that still gives a mechanical guarantee rather than a linted one.

It is also honest about what it does not solve. V30 was the frontier before the later refinements, and it left two things weak. Calibration constants (shipping rates, promo codes) still sat scattered in the features rather than hoisted to the composition, and some cross-boundary agreements were still silent string contracts. Those are precisely the gaps V35 and its siblings closed, building directly on V30’s committed-codegen spine. On the ALA Checklist V30 scores 88 out of 100, strong on coupling and state, weaker on those requirement-locus and contract axes.

So the choice between V30 and V36 is not about how ALA-compliant they are; both drive feature coupling to zero. It is about where the guarantee lives. V36 keeps the code vernacular and puts enforcement in a CI linter. V30 puts a small amount of machinery into the workflow (a data manifest and a codegen step) and gets a stronger promise in return: the diagram is not merely documented as the source of truth, it is mechanically incapable of being anything else. If your team wants the design pinned by the build rather than by review, and can accept a generation step in the toolchain, V30 is the one to reach for.

← all posts