0003. Generated code carries the behaviour
0003. Generated code carries the behaviour
Status
Accepted
Date
2026-10-04
Deciders
operator (Saulo Vallory), Ruling 2. The test and the guard are the roadmap author’s way of applying it.
Context
From a resource file Mesh produces TypeScript: types, handlers (one function per action) and database schema. A design question is how much of what an action does (cast input, run validations and changes, check policy, open a transaction, call the data layer) is written into that generated file, and how much stays in a shared library that the generated file calls.
Ash, the Elixir framework Mesh is modelled on, keeps behaviour in its library. The research ties two complaints to that: “Behaviour lives in the library, so traces are unhelpful and test coverage of your own resource reads 0%” (research synthesis, section 6, item 2). The synthesis flagged the question as an open gap: if generated handlers are thin calls into a shared core, “Mesh inherits Ash’s coverage and stack-trace problems” (same file, section 8, gap 1).
Decision
The operator ruled on 2026-10-04, in Ruling 2 of rulings of 2026-10-04 (question “How much logic is generated per resource?”):
As much as Ash puts in its resources, or more. Generated code carries the behaviour; the shared engine stays thin.
The roadmap turns this into a test: the run-time library never reads the resource model. Every decision that depends on the model is made at build time and written into the generated file (roadmap, section 2, principle 1). The generated tree is committed, and the verify script regenerates it and fails on any difference (same section, principle 6; research synthesis, section 16, stage 8). One clarification, this record’s own reasoning: M4 emits expression trees into the generated file as data literals. A literal is generated code, not the model, so the test still holds as long as generated code never imports model or model.json (roadmap, M2 test 4).
Options considered
Option A: Generate the whole lifecycle per action (chosen)
| Dimension | Assessment |
|---|---|
| Complexity | Higher: emitters per phase, plus the guard |
| Cost | Large generated output to review and keep deterministic |
| Debuggability | Best: the first stack frame is the generated handler |
| Reversibility | Low once users commit generated trees |
Pros: A user’s resource shows in stack traces and coverage. Output is plain code.
Cons: The risk is named in the roadmap: “If generated handlers become unreadable, the point of Ruling 2 is lost” (roadmap, section 9, risk 6). A repeated pattern tempts authors to move it into the library; the roadmap allows only small pure helpers that take values, never the model (M5 risks). A fix in the library does not change already generated handlers until they are regenerated (this follows from the design; not in the sources).
Option B: Thin generated handlers calling a shared engine
| Dimension | Assessment |
|---|---|
| Complexity | Lowest generated output |
| Cost | Cheapest to build and to review |
| Debuggability | Poor: Ash’s shape |
| Reversibility | High |
Pros: Small diffs; library fixes reach every app on upgrade. This is how Ash works.
Cons: Ash’s measured results: 0% coverage of the user’s resource, unhelpful traces (research synthesis, section 6, item 2).
Option C: Interpret the model at run time, no code generation
| Dimension | Assessment |
|---|---|
| Complexity | Low: no emitters, no guard |
| Cost | Lowest build step |
| Debuggability | Poor: the model is data, not code |
| Fit with the guard | None: nothing to commit |
Pros: Prior art exists. ZenStack v3 produces SQL at run time on every query, and Prisma 8 states that “query compilation happens at runtime” (TypeScript prior art, Summary).
Cons: Same traces and coverage problem as B, and no readable output for an agent to inspect.
Trade-off analysis
Option A is the only one that fixes Ash’s two named defects. Its price is volume and the discipline to keep output boring. The ruling says “or more” than Ash puts in its resources, so the volume cost is accepted.
Consequences
- Easier: stack traces start in generated code; coverage tools see per-resource code; behaviour can be read without understanding a framework.
- Harder: the emitters carry the lifecycle; the guard must keep output byte-identical (roadmap, M1 tests 2 and 3).
- Revisit: after M5, check whether helper functions have crept in (roadmap, section 9, risk 6).
Action items
- M2: generate one function per action with the steps in order, not a call to a generic
runAction. - M2: add the import-rule check and the stack-trace test (roadmap, M2 tests 3 and 4).
- M5: generate the full eight-phase lifecycle; review helpers added to
runtime.