Generated code and the guard

Generated code and the guard

Status: design; the model and types are built in M1, handlers and the Drizzle schema in M2, expression forms in M4, the contracts module in M6. Nothing described here is generated today. Tag names are today’s working names (ADR-0034).

The rule

Generated code carries the behaviour; the run-time library stays thin (ADR-0003). The project owner’s ruling: “As much as Ash puts in its resources, or more. Generated code carries the behaviour; the shared engine stays thin” (rulings of 2026-10-04, table row 2).

Why: Ash keeps behaviour in its library, so stack traces are unhelpful and test coverage of a user’s own resource reads 0% (research synthesis, section 6, item 2). If generated handlers were thin calls into a shared engine, Mesh would inherit both problems (section 8, “Gaps”, item 1).

The test: the run-time library never reads the resource model (roadmap, section 2, principle 1).

What Mesh generates and commits

The generated tree is committed and guarded (roadmap, section 2, principle 6).

File What it is Milestone
generated/model.json One file holding one document per resource, with source positions M1
types.ts per resource TypeScript types for the resource M1
One handler file per resource One exported function per action, for example createPost(input, scope) M2
Input validators Zod schemas, seen by the rest of Mesh only through Standard Schema (ADR-0028) M2
Drizzle schema file Table definitions emitted by data-sqlite’s build half M2
Expression forms For each translatable expression, the tree as a data literal and the in-memory form as TypeScript; the class (translatable or opaque) is recorded in model.json M4
Contracts module One self-contained module for MX tooling (ADR-0021) M6

(roadmap, M1, M2, M4, M6.) The example’s mesh explain output is also committed, as a guarded fixture; it is not stated that every project commits it (M5, acceptance test 5). The directory layout beyond generated/model.json and the rule that domain sets the output directory is not given by the roadmap. Whether migrations written by mesh migrate generate (M9) are under the guard is not stated.

Why the tree is a data literal

Each translatable expression is written into the generated file twice (roadmap, M4):

  1. As the tree itself, a data literal that the data adapter compiles into Drizzle’s builder when a query runs. Queries are assembled at run time from the action’s filter, the caller’s filter and, later, policies, so the SQL cannot be fixed at build time.
  2. As its in-memory form, emitted TypeScript that calls the registered functions’ in-memory implementations in runtime.

The build checks that every function used has a SQL form in the configured adapter. See expressions.md.

What a handler contains

The handler body holds the lifecycle steps in order: validate input, open a transaction, call the data layer, commit, return a typed record. They are written out per action, not delegated to a generic runAction (roadmap, M2). From M5 the body covers all eight phases, the plan is chosen at build time, and a span is opened per phase through the OpenTelemetry API (ADR-0029; action-lifecycle.md).

A reader should expect to see:

  • The scope as the required second argument, { actor, context } (ADR-0007). A call without one is a type error (M2, acceptance test 5).
  • Changes and validations as TypeScript. Translatable ones have an in-memory form emitted here; opaque ones are the authored text sliced from the resource file (M4).
  • For atomic updates, the change folded into the UPDATE statement; for require-atomic=false actions, read the row with a write lock, run in memory, write, in one transaction (M5; ADR-0017).
  • Calls to the data-layer contract and never to Drizzle (M2).

What the run-time library may contain

runtime holds the scope type; error classes; the data-layer contract with the query and expression-tree types; the transaction helper; and the in-memory implementations of registered expression functions (roadmap, section 3, runtime row). That is all.

  • No model. runtime imports nothing from model, compiler, MX or Drizzle.
  • Helpers take values. When a pattern repeats across handlers, it may become a small pure helper in runtime that takes values, never the model (M5, risks).

If handlers become unreadable, the point of the ruling is lost (section 9, risk 6). The concrete check is that a failing validation’s first stack frame outside node_modules and Mesh packages is the generated handler (M2, acceptance test 3).

The import rule

Checked by verify from M2 (roadmap, M2, acceptance test 4):

  • Nothing in runtime imports model, compiler or Drizzle.
  • No generated handler imports them or model.json.
  • Drizzle is imported only under packages/data-* and in the emitted schema file.

Reasons are in three-rings.md.

The guard

Hand edits to generated files, or a stale tree after a resource file changes, would make committed code disagree with its source. Wasp’s checksum manifest protects a gitignored directory and would not catch hand edits to a committed one, so Mesh needs its own check (research synthesis, section 8, “Gaps”, item 8).

How it works:

  1. mesh build --check runs the whole pipeline but regenerates in memory, writing nothing.
  2. It compares the result with the committed files.
  3. Any difference fails the check and names the file (roadmap, M1, acceptance test 3).
  4. verify, the one local script that runs every check, runs it (M1).

The guard depends on determinism: same input, same bytes, from templates plus a pinned formatter (see build-pipeline.md). It grows with the milestones: handlers and the schema in M2, the example’s explain output in M5, the contracts module in M6 (M2 test 6, M5 test 5, M6 test 4).

There is no continuous integration until the MX packages are published, so the guard runs only when someone runs verify (ADR-0031). A skipped run is invisible (roadmap, section 9, risk 4).

Run-time errors point at resource files

A declared rule that fails at run time (for example a validate with its message) should report its .mx position. The roadmap’s working assumption is that the position is carried as data in the generated code (roadmap, M5). That is open: ADR-0039 is Proposed and records the choice against source maps. One argument against source maps is that Bun’s findSourceMap returns undefined (research synthesis, section 12, risk 3).

Errors fall into a class hierarchy built in M5. M2 starts with three classes: invalid input, not found, framework. A read after destroy throws the not-found class (M2, acceptance test 1). A load that cannot be done is a run-time error, never ignored (M7, acceptance test 2).

Reading generated code

The aim of the ruling is that a reader can follow a handler top to bottom without opening model.json; this is an inference from the ruling, not a stated rule. Hand edits to a generated file fail the guard, so change the resource file or the emitter instead.