Plan revision 2 (superseded)
Plan revision 2 (superseded)
This is revision 2 of the implementation plan, kept as history because superseded decision records cite it. It does not describe what Mesh will build. The current plan is the roadmap.
Date: 2026-10-04. Author: squad leader impl-plan, for the lead mesh-lead and the operator
(Saulo Vallory). Status: revision 2, after the operator’s rulings on the plan’s questions
(section 9) and two independent reviews (section 11). Nothing here is built yet except PR #1.
0. What this document is, and what it rests on
Mesh is a planned TypeScript framework modelled on Ash, the declarative resource framework
for Elixir. One resource file declares a piece of data, the operations on it and the rules
around it; Mesh derives types, handlers, database schema, a command-line interface and other
outputs from that file. This plan turns the architecture into ordered milestones that a team of
developers (human or agent) can execute and review one at a time.
“The architecture” means the proposal in Step 3 of the Synthesis (below) as amended by the
operator’s rulings of 2026-10-04. the project status file (not published), next steps 1 and 2, records
the architecture as agreed on that date. The project’s the project’s agent instructions (CLAUDE.md) still says the proposal “is
not yet agreed”; that sentence predates the rulings.
Every reference below is a path relative to the Mesh space root (/Users/svallory/work/mesh),
and links are relative to this file.
| Short name used below | File | What it is |
|---|---|---|
| Rulings | rulings of 2026-10-04 | The operator’s binding decisions. Three parts are cited: the table of eight answers to the open design questions (“Ruling 4” means row 4), the “Consequences for the proposal” list, and the “Implementation-plan rulings” table, which answers this plan’s own questions (“plan ruling Q3”). It also holds the Elysia ruling and one lead decision. |
| Synthesis | research synthesis | Summary of seven research documents on Ash and on TypeScript tools. Its Step 3 (sections 14–19) is the proposed architecture: three rings, an eight-stage build pipeline, an eight-phase run-time lifecycle, and the extension points. |
| Durable engines | durable engines | Comparison of eleven workflow and job tools, ending in a proposed adapter interface (its section 6), adopted as the contract for M11 and M12 by plan ruling Q14. |
| PR #1 | https://github.com/svallory/mesh/pull/1, report PR #1 and its report | The only Mesh code so far: the resource vocabulary written as 26 MX tag contracts, with 103 passing tests. Merged to main on 2026-10-04 (commit bd62652). |
| MX notes | MX project notes, getting-started, MX project notes, updates, Mesh’s answers to MX on mx.contracts, 2026-10-04 |
How Mesh consumes MX, what has landed in MX, and what Mesh told the MX lead. |
| Old plan | the first plan of 2026-10-01 (not published) | Written 2026-10-01, before the research. Input only; where it disagrees with the Rulings, the Rulings win. |
| Status | the project status file (not published), the team status log of 2026-10-04 (not published) | Where the project stands; this plan is step 7 of the project status file (not published). |
Terms used throughout:
- MX is a separate project that parses Marko-syntax files. Mesh resource files are
.mx
files. Mesh callsparseDatafrom the package@mxlang/data, which returns a static tree of
tags and attributes plus a list of diagnostics; nothing in the file is executed (MX notes,
getting-started section 1). Mesh never parses.mxtext itself. - A tag contract (
CustomTag) tells MX which attributes, children and parents a tag name
allows. With the optionstructural: "reject", MX also rejects control flow (<if>,<for>),
text and comments, so a resource file can only contain declared tags. - Core, adapter, extension are the three rings of the architecture (Synthesis section 15 and
the definitions at the top of its Step 2). Core is what Mesh cannot work without, at build
time or at run time. An adapter is one replaceable implementation of a contract the core
owns (a database, a transport). An extension is an optional feature built on the core’s
extension points. - An action is one named operation on a resource (create, read, update, destroy). A
transport is whatever lets the outside world call an action: a command line, an HTTP
server, a job runner. - The scope is the explicit value passed on every action call. In core it has two parts:
the actor (who is calling) and a context (extra data for the call). It is never
ambient (Synthesis section 8, “Do differently”, row “Ambient actor and tenant”). Core has no
notion of a tenant: that belongs to a multitenancy extension, which adds to the scope through
a declared contribution point (Rulings, “Consequences”, second bullet). - An actor resolver is the adapter that produces the scope for a call from whatever the
transport received (command-line arguments and environment, or an HTTP request). Every
transport gets its scope this way; core defines no flags and no login (same bullet). - A capability is an optional feature of an adapter (joins, upserts, signals) that the
adapter declares as static data, so the build can check it without starting the adapter. - Drizzle is an established TypeScript query builder with drivers for SQLite and Postgres;
drizzle-kit is its companion tool that generates SQL migrations from a schema. By plan
ruling Q3, Mesh’s SQL adapters are built on both.
1. Summary
- Sixteen milestones, M0 to M15, in dependency order. M0 to M2 are the walking skeleton:
a.mxresource file is parsed, turned into a model, emitted as committed TypeScript, and a
command-line transport runs its actions against SQLite. Everything after widens that path. - The first release (“v1”) is M0 to M14: command line only, SQLite and Postgres. The HTTP
transport, the typed client and OpenAPI (M15) come after (plan ruling Q1/Q13). - The package layout is a bun workspace with four core packages (three libraries and the
mesh
developer command), and one package per adapter and per extension (section 3). - Established tools first (plan ruling Q3, the operator’s standing position). Section 8.1
lists, for every place this plan builds something itself, the tool it uses instead or the
one-line reason no tool fits. - All fourteen questions of the first version are decided (section 9.1). This revision raises
four smaller ones for the lead, each with a recommendation (section 9.2).
2. Principles that every milestone is checked against
These come from the Rulings and the Synthesis. A reviewer rejects a milestone that breaks one.
- Generated code carries the behaviour; the run-time library stays thin (Ruling 2). The
test used in this plan: the run-time library never reads the resource model. Every decision
that depends on the model (which fields are accepted, which checks run, in what order) is made
at build time and written into the generated file. The reason is in Synthesis section 6, item
2, and section 8, “Do differently”, row “Keeps behaviour in the library”: because Ash keeps
behaviour in the library, stack traces are unhelpful and test coverage of a user’s own
resource reads 0%. - No silent fallback. A feature the chosen adapter lacks, a tag or attribute the compiler
does not implement yet, an expression the database cannot run: each is a build error that
names the file, line and fix (Ruling 4; Synthesis section 8, “Silent fallbacks”). - Conservative defaults: deny unless allowed, accept only listed inputs, private unless
exposed (Synthesis section 14, goal 4), and atomic unless stated otherwise. Ash reversed every
permissive default in its 3.0 release, “atomic required by default” among them (Synthesis
section 6, last paragraph). - “It was in the plan” is never a reason to hardcode something (the project’s agent instructions (
CLAUDE.md), “Architecture
in brief”). Each package in section 3 states why it sits in its ring. - Every vocabulary addition follows the PR #1 pattern: one closed contract per tag name,
one negative fixture per rule, each asserting the exact message, line and column (PR #1
report, “How verified”). - The committed generated tree is guarded: the
verifyscript regenerates it and fails on
any difference (Synthesis section 16, stage 8, and section 8, gap 8).verifyruns locally
until MX is published and continuous integration (CI) becomes possible (plan ruling Q8). - Rely on established tools; keep application concerns out of core. Before building
anything, look for a well-established tool and put it behind a Mesh contract (plan ruling
Q3). Anything an application decides for itself (how a caller is identified, tenants, login)
is an adapter or an extension, never core (plan ruling Q4).
3. Package layout
A bun workspace in the svallory/mesh repository (worktrees/main). bun is the package manager
by project rule (the project’s agent instructions (CLAUDE.md), “Commands”). Package names use the scope @mesh/ and the command
name mesh as working names (plan ruling Q9); nothing is published yet and npm availability was
not checked.
packages/
model/ core plain-data resource model, vocabulary registries, diagnostics
compiler/ core build pipeline, extension host, core emitters
runtime/ core thin run-time library: scope, errors, contracts, outbox, worker
cli/ core the `mesh` developer command (build, check, inspect, explain)
frontend-mx/ adapter .mx files -> declarations, via parseData; core tag contracts
data-drizzle/ adapter shared code of the SQL adapters: Mesh queries and expressions -> Drizzle
data-sqlite/ adapter data layer on SQLite (file or in-memory), on Drizzle
data-postgres/ adapter data layer on Postgres, on Drizzle
transport-cli/ adapter runs actions from a command line
transport-http/ adapter one Fetch handler per action (after v1)
actor-dev/ adapter actor resolver for development and tests
workflow-inprocess/ adapter job queue and workflow runner inside the app's own database
ext-policies/ extension authorization rules (first-party, on by default)
ext-workflows/ extension multi-step operations declared in resource files
ext-agent/ extension rules files and agent tools generated from the model
ext-testing/ extension generated test data builders and a seed path
ext-client/ extension typed client, one function per action (after v1)
ext-openapi/ extension OpenAPI document (after v1)
examples/
blog/ the fixture resources (post, user, comment), grown milestone by milestone
3.1 Core, and why
Core has a build-time half (model, compiler, cli) and a run-time half (runtime). A
deployed application contains only the run-time half.
| Package | Why it is core |
|---|---|
model |
Every build-time package reads the resource model, so it cannot be replaced. It holds plain data types only (JSON-serialisable): the resource model, the vocabulary registries (attribute types, expression functions, check kinds) and the build diagnostic type. Synthesis section 8, “Copy from Ash”: one registry per vocabulary and a plain-data model that every tool reads. No I/O, no dependency on MX. It imports contract types from runtime (the query and expression-tree types), never the other way round. |
compiler |
The build pipeline (Synthesis section 16) is what makes Mesh a framework instead of a library. It owns the stage order, the extension host and the emitters that every project needs (types and action handlers). It runs only at build time. |
cli |
The mesh command is the only entry point to the build pipeline; a project cannot be built or guarded without it. It is a thin shell over compiler. It is listed apart because it is easy to confuse with the command-line transport: mesh builds a project; transport-cli lets an application’s users run its actions. |
runtime |
The small library that generated code imports: the scope type (actor and context), the run-time error classes, the contracts (data layer, transport, actor resolver, job queue, workflow) with the query and expression-tree types that cross them, the transaction helper, the outbox and the worker loop. It is split from compiler so a deployed binary does not contain the compiler (the Old plan’s goal of a small single binary; Synthesis section 10, “Runtime” row). It imports nothing from model or compiler, and nothing from Drizzle. |
3.2 Adapters, and why
| Package | Contract it implements | Why it is an adapter |
|---|---|---|
frontend-mx |
“Front end”: files in, declarations with source positions out (Synthesis section 16, stage 1) | The Synthesis ring table puts the authoring syntax in the adapter ring, and names TypeScript declarations as a possible second front end (section 10). The code from PR #1 moves here. Only this front end is built. |
data-sqlite, data-postgres, with data-drizzle |
Data layer (Synthesis section 11, “Data access”; Ruling 4) | A project picks its database. The contract is Mesh’s own, at the level of resources, because query libraries disagree on filters, joins and aggregates (Synthesis section 11); “the query library inside an adapter is that adapter’s private choice” (same section). That choice is Drizzle (plan ruling Q3). data-drizzle holds what the two dialects share and is not usable alone. Drizzle is imported only in these three packages. |
transport-cli, transport-http |
Transport: turn an outside request into (action, input), obtain the scope from the actor resolver, return a typed result or classed error |
Ruling 8: Mesh is not tied to web applications, so no transport is core. The command line is first. |
actor-dev |
Actor resolver: transport request in, scope out | How a caller is identified is an application decision (plan ruling Q4), so even the simplest resolver is an adapter. This one serves development and tests; real resolvers (a token check, Better Auth for HTTP) are application or later adapters. |
workflow-inprocess |
JobQueueAdapter and WorkflowAdapter (Durable engines section 6.2) |
Ruling 7: workflows run behind an adapter interface and the in-process runner is the first adapter. External engines (DBOS, Temporal, pg-boss for jobs) are later adapters. |
Three adapter slots from the Synthesis ring table (section 15) have no package in v1, on
purpose:
- Runtime host (Bun or Node): handled by keeping core on web-standard APIs and putting
runtime-specific drivers inside data adapters; M14 proves it on Node. - Expression parser: filled by MX.
parseDataalready hands each expression over as a
parsed Babel syntax node (MX notes, getting-started section 1, “nodeis a Babel
Expression”), so Mesh needs no parser of its own while.mxis the only front end. This
closes Synthesis section 8, gap 6, for now. - Tracer: no Mesh contract is needed. Generated code calls the OpenTelemetry API, one of
the four neutral standards the Synthesis says to use as the contract (section 9); that API
does nothing until an application installs an OpenTelemetry SDK (see N3 in section 9.2).
3.3 Extensions, and why
| Package | Why it is an extension |
|---|---|
ext-policies |
The Synthesis (section 10, last paragraph, and the ring table in section 15) makes authorization “a fixed slot in the core lifecycle, filled by a first-party policy extension”. A project could fill the slot with something else, so the rules engine is not core; the slot is. It is on by default. PR #1 declares the policy tags next to the core ones; M8 moves them here (section 8, decision D3). |
ext-workflows |
Most resources have no multi-step operation. The contract a runner implements is core (runtime); the tags that declare a workflow and the code generated from them are optional. |
ext-agent, ext-testing, ext-client, ext-openapi |
Outputs derived from the model that a project can do without. Each is emitters plus verifiers, which is what the extension points offer (Synthesis section 18). The last two need a wire transport and come with M15. |
Other extensions named in the Synthesis ring table (multitenancy, audit trail, event log, state
machine, soft delete, encryption, rate limits) are not scheduled; section 6 lists them.
4. Dependency order at a glance
M0 workspace
└ M1 build skeleton ─ M2 run skeleton ← walking skeleton ends here
└ M3 data-layer contract
└ M4 expressions
└ M5 action lifecycle, atomic updates
├ M10 migrations, Postgres
└ M6 extension host, composed contracts
├ M7 relationships, calculations, aggregates
│ └ M8 policies
│ ├ M9 bulk actions, identities, upserts
│ └ M13 agent and test surface
└ M11 outbox, notifiers, jobs
└ M12 workflows, in-process runner
M14 Node parity and single binary: last in v1, after every other v1 milestone
M15 HTTP transport, typed client, OpenAPI: after v1
What can run in parallel: M10 alongside M6 and everything after it; after M6, two more tracks
(M7 → M8 → M9 and M13; M11 → M12). M9 and M13 can run side by side once M8 is done.
Capability rule across tracks. A milestone that adds an optional data-layer capability (M5
atomic expressions, M7 joins and aggregates, M9 upserts) implements it in every data adapter
that is merged when the milestone itself is merged. M10 implements, on Postgres, every
capability in the conformance suite on the day M10 is merged. Whichever of two parallel
milestones merges second owes the missing combination. Most of each capability lives in
data-drizzle, shared by both dialects, which keeps that debt small. M14 closes any gap:
before v1, SQLite and Postgres must each pass the full suite with all four optional
capabilities.
Sizes: S is one pull request; M is two to four pull requests; L is five or more and
gets its own squad leader, who splits it into tasks. No calendar dates: sizes are scope, not
time.
5. Milestones
Each milestone lists goal, scope in and out, packages with their ring, acceptance tests, size
and risks. “Vocabulary” means tag names or attributes added to the resource-file language in
that milestone. “verify” is the local script from M0; wherever a test says “checked by
verify”, it moves to CI unchanged once CI exists.
M0 — Workspace and conventions (S)
- Goal. A repository layout the later milestones can add packages to, with one command
that runs every check. - In scope. Turn the repository into a bun workspace; move PR #1’s
packages/compiler/src/contracts.ts,
tests and fixtures intopackages/frontend-mx. The only edits are the ones the move forces:
themx.contractsandmx.targetentries inpackage.jsonfollow the file. Oneverify
script that runs tests and the type check for every package. A project the project’s agent instructions (CLAUDE.md) section
with the commands (PR #1 report, “Instructions updated”, lists them). Skeleton
examples/blogholding PR #1’spost.mx. - Out of scope. Any new behaviour. CI: the MX packages are linked from a local checkout
(link:@mxlang/data, MX notes, getting-started section 2), which a hosted runner does not
have, and the operator ruled out a self-hosted runner (plan ruling Q8). CI is added when MX
is published; asking for that is the lead’s request to the MX lead (the project’s agent instructions (CLAUDE.md), “Working
with MX”). - Packages.
frontend-mx(adapter); workspace root. - Acceptance tests.
bun run verifypasses with the same 103 tests PR #1 has, from a
clean clone with MX linked. - Risks. With no CI, nothing enforces
verifyexcept the review protocol: the lead’s
verifier runs it once per delivery before a merge. A merge without that run is the failure
to watch for.
M1 — Build skeleton: resource file to model to emitted types (M)
- Goal.
mesh buildreads.mxfiles and writes a committed model and TypeScript types.
Build stages 1, 2, 3, 7 and 8 of Synthesis section 16 exist in their simplest form. - In scope.
- Load and check structure (stages 1–2):
frontend-mxcalls
parseData(source, file, { customTags, structural: "reject", unknownTags: "reject" })with the contracts imported
directly, not discovered by scanning, so a user’s stray local tag file cannot change the
build (MX notes, mesh-answers, request 1). Diagnostics are printed with file, line and
column. - An undeclared tag at any depth is rejected by MX itself: the
unknownTags: "reject"
option landed on MXmainate65707a0(MX notes, updates, entry fore65707a0: “an
authored tag at any depth with no entry incustomTagsis a positioned error”), closing the gap PR #1’s report
lists under “MX gaps”, item 1. One root rule stays Mesh’s to enforce: exactly one
resourceper file (same report, Round 3, item 2). - Build model (stage 3): one plain-data document per resource, with source positions kept
for every element. Scope of the model in M1: resource name,tableanddomain(an
optional grouping name, stored in the model; a resource with a domain gets its generated
files undergenerated/<domain>/<resource>/, one without undergenerated/<resource>/);
attributes of the six types PR #1’s contracts allow (string,number,boolean,
enum,uuid,datetime) withrequired,publicanddefault;uuid-primary-key;
timestamps; the four action kinds withacceptanddefaults. - The MX tag contracts are the source of truth for tag names; the value vocabularies live in
the core registries (plan ruling Q2). The six attribute type names are read from the type
registry inmodel; the contract’stypeenum infrontend-mxis built from that
registry, and a test fails if the two lists differ. - Cross-file checks that need no extension host: duplicate resource names,
acceptnaming an
attribute that does not exist. These run in a plain “checks” step that M6 turns into the
Verify stage. - Emit (stage 7):
generated/model.jsonand atypes.tsper resource (record type, public
record type, one input type per action). Emitters build text from templates and pass it
through an established formatter, pinned to an exact version, so output is deterministic:
same input, same bytes. - Guard (stage 8):
mesh build --checkregenerates in memory and exits non-zero on any
difference from the committed files;verifyruns it.mesh inspectprints the model as
JSON (Old plan, “Agent legibility principles”). - Not-implemented rule. A tag or attribute the contracts accept but the compiler does
not handle yet is a build error that names it and the milestone that will implement it. In
M1 that coversrelationships,belongs-to,has-many,change,validate,filter,
sort,policies,policy,authorize-if,calculations,calculate,value,
aggregatesandcount. Nothing is ever ignored (principle 2).
- Load and check structure (stages 1–2):
- Out of scope. Handlers, expressions, the extension host.
- Packages.
model,compiler,cli(core);frontend-mx(adapter). - Acceptance tests. (1) A reduced
examples/blog/post.mxbuilds; the emitted types pass
tsc --noEmit. (2) Building twice gives byte-identical output. (3) A hand edit to a generated
file makesmesh build --checkfail and name the file. (4) An empty file, a file with two
resources, a duplicate resource name and an unknownacceptname each fail with Mesh’s
expected message and position; an unknown top-level tag fails with MX’s diagnostic. (5) The full PR #1 fixture fails with the
not-implemented error for its first unsupported tag. (6) The registry-against-contract drift
test. - Risks.
parseDatastops at the first error in a file (MX notes, getting-started section
1: “one error, fail-fast”), so an author fixes structural errors one at a time. Mesh’s own
checks should collect all errors per file to offset that. Emitters cannot build output
through a TypeScript compiler API, because the TypeScript version PR #1 pins (7.x) has none
that is stable (Synthesis section 10, “Expression parser” row); hence templates plus a
formatter, with tests type-checking the result through thetsccommand.
M2 — Run skeleton: generated handlers, SQLite, command-line transport (L)
- Goal. The walking skeleton, end to end: after
mesh build, a user runs
post create,post read,post update,post destroyfrom a shell and the rows are in a
SQLite file. - In scope.
- Generated handlers. One file per resource with one exported function per action. The
function body contains the steps in order: validate and cast input (only accepted fields
pass), open a transaction, call the data layer, commit, return a typed result. Written out
per action, not a call into a genericrunAction(principle 1; Ruling 2). - Input validators. Generated per action as schemas of an established validation library,
and seen by the rest of Mesh only through Standard Schema, the shared validator interface
the Synthesis recommends as a neutral contract (section 9; section 10, “Validation” row).
Mesh writes no cast or validation functions of its own (N1 in section 9.2 names the
library). - Run-time library. The
Scopetype (actor and context, nothing else); a first,
deliberately small data-layer contract (insert, select by primary key, select all, update
by key, delete by key, transaction); the transport contract; the actor-resolver contract;
the first run-time error classes (invalid input, not found, forbidden, framework).
Synthesis section 8 lists an error class hierarchy as a v1 need (gap 4). - Transport contract. A transport receives a generated action registry (for each
action: name, input validator, handler). For each call it hands what it received to the
configured actor resolver and passes the scope that comes back to the handler; a
transport never builds a scope itself. The registry is generated data, so the transport
does not read the model either. - Private unless exposed. A handler called in process returns the full record. A handler
called through a transport returns only attributes markedpublic; the registry carries
the public result type. This givespublic(already in PR #1’s contracts) its meaning. transport-cli. Maps<resource> <action> --input <json>to a handler call. It
defines no flag about the caller: the remaining arguments and the environment go to the
actor resolver untouched (Rulings, “Consequences”, second bullet; plan ruling Q4). It
prints the result as JSON on standard output and errors on standard error, with one exit
code per error class;--helpis generated from the registry. Arguments are parsed with
the standard library’sparseArgs, not a parser of Mesh’s own.actor-dev. The first actor resolver: it returns the actor and context named in a file
the project config points to. It is for development and tests (N4 in section 9.2). With no
resolver configured, every transport call fails; there is no default actor.data-sqliteon Drizzle (plan ruling Q3). The adapter has a build-time half, which
emits the Drizzle table definitions for the project’s resources as a guarded generated
file, and a run-time half, which implements the data-layer contract with Drizzle over
Bun’s built-in SQLite driver. Tables are created in development with drizzle-kit’s schema
push, wrapped asmesh db push; versioned migrations wait for M10. Generated handlers call
the Mesh contract and never import Drizzle. Exact versions of Drizzle and drizzle-kit are
pinned in the adapter packages.- Default deny, before policies exist. Until M8, an action runs only when the project
config sets an explicitauthorization: "none"flag; otherwise every call fails as
forbidden. This keeps principle 3 true from the first runnable build.
- Generated handlers. One file per resource with one exported function per action. The
- Out of scope. Filters, sorting, pagination, expressions, hooks, policies, Node.
- Packages.
runtime(core),compiler(core: handler and registry emitters),
transport-cli,actor-dev,data-sqlite,data-drizzle(adapters). - Acceptance tests. (1) An end-to-end test spawns the built example as a child process:
create, read back, update, destroy, read again gives “not found” with the documented exit
code. (2) A field not inacceptis rejected, not dropped. (3) A failing input validation
produces a stack trace in which the generated handler file is the first frame that is neither
innode_modulesnor in a Mesh package (packages/runtimeand the adapters). (4) An import
rule checked byverify: no file inruntimeimports@mesh/model,@mesh/compileror
Drizzle; no generated handler imports them orgenerated/model.json; Drizzle is imported
only underpackages/data-*and in the adapter-emitted schema file (principles 1 and 7).
(5) Without theauthorization: "none"flag every action is forbidden. (6)mesh build --checkcovers handlers, registry and the emitted Drizzle schema. (7) A non-public attribute
is absent from command-line output and present in an in-process call. (8) The scope returned
by a test actor resolver reaches the handler unchanged; with no resolver configured the call
fails with a classed error;transport-cli’s generated--helplists no actor, tenant or
context flag. - Risks. The first contract shapes (data layer, transport, actor resolver) will change in
M3 and M5; they are marked unstable until M6.bun:sqliteties the skeleton to Bun until
M14. Drizzle v1 is a release candidate (Synthesis section 12, risk 1); the mitigation is in
section 10, risk 3.
M3 — Data-layer contract and capabilities (L)
- Goal. The data-layer contract of Ruling 4, implemented on Drizzle, with a shared test
suite that every data adapter must pass. - In scope. The mandatory set: select, insert, update, delete, transactions, filters,
sort, pagination.data-drizzletranslates a Mesh query (plain data) into Drizzle’s query
builder; nothing of Drizzle shows through the contract. A capability manifest: static
data published by each adapter listing the optional capabilities it has (joins, aggregates,
upserts, atomic expressions), as a closed union of names (Synthesis section 8, “A capability
probe on the data layer”). The build reads the manifest without starting the adapter. The
check that fails the build when a resource uses a capability the adapter lacks (Ruling 4:
“never a silent in-memory fallback”) is written here, in the checks step from M1, and first
exercised by a real consumer in M5. A conformance suite: one set of tests every data
adapter must pass. An in-memory mode ofdata-sqlite(SQLite’s own:memory:database)
for tests and prototypes, in place of a hand-written in-memory adapter (N2 in section 9.2).
Read actions implementsort(already in PR #1’s contracts) and gain pagination, offset and
keyset (new vocabulary onread). Filters in M3 are plain data (field, operator, literal),
which is also the form a caller supplies at run time; filters written as arrow functions
arrive in M4. - Out of scope. Joins, aggregates, upserts, atomic expressions (declared, not implemented).
Postgres (M10). - Packages.
runtime(core: contract and query types),compiler(core: capability
check),data-drizzle,data-sqlite(adapters). - Acceptance tests. (1)
data-sqlitepasses the conformance suite in file and in-memory
mode. (2) A manifest that names a capability outside the closed union fails the build; the
capability check is unit-tested with a test-only requirement. (3) A transaction that throws
leaves no row behind. (4) Keyset pagination under concurrent inserts: every row that existed
when paging began is returned exactly once, and no row is returned twice. (5) The M2 import
rule still passes: no Drizzle type appears inruntime’s contract. - Risks. Until M10 the contract has one real implementation, so nothing proves it is not
shaped by SQLite. That is why M10 may start as soon as M5 is merged. The contract is the most
expensive thing to change later: Ash’s has 46 callbacks and 47 capability names, applied
inconsistently (Synthesis section 2.2), which is the failure to avoid.
M4 — Expressions (L)
- Goal. Arrow functions in resource files become either a portable expression tree or an
emitted TypeScript function, decided at build time (Synthesis section 16, stage 6). - In scope. The expression tree type (in
runtime, because it crosses the data-layer
contract) and the registry of functions and operators (inmodel). Conversion from the Babel
node MX supplies to the tree, infrontend-mx. Two classes. Translatable expressions
convert fully to the tree. Opaque ones are emitted as TypeScript functions by slicing the
authored text at the span MX reports (MX notes, getting-started section 1: “slicespanfor
authored text”; this is copying, not re-parsing). Which class each position needs:
filter(and laterauthorize-ifand calculations used in queries) must be translatable,
or the build fails at the offending node;validateis opaque; achangeis classified:
translatable when its body is only assignments of translatable expressions to the record’s
attributes, opaque otherwise. In M4 every change still runs in process; the classification
is recorded in the model for M5. Scope rule: a translatable expression may only use its
declared parameters and registered functions; a free variable is a build error (Synthesis
section 8, gap 5, on closures).data-drizzlecompiles the tree into Drizzle’s SQL builder
(plan ruling Q3: “Mesh still compiles its own expression tree into Drizzle’s SQL builder”).
A translatable expression has SQL’s semantics where SQL and JavaScript differ, and the
documentation says so (plan ruling Q10). There is no second, in-process evaluator of the
tree: a translatable expression always runs in the database.filteron read actions;
validatewithmessage;change. A first small function list, documented from the
registry so documentation cannot drift from it (Synthesis section 8, first “Copy” row). - Out of scope. Relationship traversal in expressions (M7); atomic changes (M5).
- Packages.
model,compiler,runtime(core);frontend-mx,data-drizzle,
data-sqlite(adapters). - Acceptance tests. (1) Each registered function and operator has a conformance test with
written-out expected values, including the null cases; every data adapter must produce them.
(2) A translatable expression using an unsupported construct fails the build pointing at that
node. (3) A free variable in a filter fails the build. (4)post read publishedfrom the
command line returns only published rows. (5) The M4 example resource, which declares every
attribute it assigns, has one change that is a single assignment and one with a
multi-statement body;model.jsonrecords the first as translatable and the second as
opaque. - Risks. This is the piece nobody else has built in reusable form (Synthesis section 8, gap
5); it can grow without limit, so the function list is kept small and documented. Because of
plan ruling Q10,===in afilterand in avalidatecan disagree on null; that must be
taught, and a lint for the common cases is worth adding. AshPostgres went the other way,
making database semantics match Elixir by installing SQL functions, and paid a 30-fold
slowdown on one filter (Synthesis section 2.2, last bullet).
M5 — Action lifecycle and atomic updates (L)
- Goal. The eight-phase run-time lifecycle of Synthesis section 17, generated per action,
with the atomic half of Ruling 3. - In scope.
- Phases in generated code: enter, cast, plan, pre-check, transaction, data layer, commit,
after commit. A plan chosen once at build time per action and printable with
mesh explain <resource> <action>(Synthesis section 8, “Copy from Ash”, row “The onion
lifecycle”). - Validations collect all errors. Write hooks before and after the data-layer call inside the
transaction, and after commit (new vocabulary). Preparations: read-side hooks that
adjust a query before it runs (new vocabulary; Synthesis section 8, gap 4, and section 17,
phase 3). Action arguments: inputs that are not attributes (gap 4). - The authorizer slot: one place before the transaction for checks that need no data and
one place inside it for checks that read data; the Synthesis found Ash authorizes writes in
six places, one with a check-then-act gap (section 2.2). The slot is empty until M8. - Tracing: generated handlers emit one span per lifecycle phase (Synthesis section 17,
phase 8, and section 10, “Observability”) through the OpenTelemetry API, which does
nothing unless the application installs an SDK. - Atomic single-record updates. A change classified translatable in M4 is folded into
theUPDATEstatement, using theatomic-expressionscapability. Update and destroy
actions are atomic by default (principle 3): an action with an opaque change, or with a
validation that reads the stored record, cannot be folded into one statement and must say
atomic=false(new attribute onupdateanddestroy); it then reads and writes inside
one transaction. Withoutatomic=falsesuch an action is a build error, never a silent
read. The example’spublishaction validatespost.title, so the example’s copy of
post.mxgainsatomic=falseon it in this milestone, as does every update or destroy
in the examples and test resources from earlier milestones that has an opaque change (the
M4 example among them).explainshows which strategy the action got. A change is never
run twice (Synthesis section 8, “Do differently”, row 2, on Ash bug #2969). - The full run-time error class hierarchy. Where a declared rule can fail at run time, the
generated code carries the resource-file position as data, so the error names the.mx
line. No source maps in v1 (plan ruling Q11).
- Phases in generated code: enter, cast, plan, pre-check, transaction, data layer, commit,
- Out of scope. Bulk actions (M9); policies (M8); notifications (M11).
- Packages.
compiler,runtime,model(core);frontend-mx(adapter: new vocabulary);
data-drizzle,data-sqlite(adapters: atomic expressions). - Acceptance tests. (1) Two concurrent atomic increments of one row both apply. (2) An
update with an opaque change, and one with a validation that reads the record, each fail the
build withoutatomic=false; with it, the build passes andexplainsays “read then
write”. (3) An atomic change on a fake adapter without the capability fails the build with
the resource-file position (the first real use of the M3 check). (4)mesh explainoutput
for every action in the example is committed and guarded like generated code. (5) A hook that
throws inside the transaction rolls the write back. (6) With a test OpenTelemetry SDK
installed, one call produces the eight phase spans in order; with none installed, no error.
(7) A preparation that adds a filter changes the rows a read returns. (8) The M2 import rule
still passes. - Risks. Ruling 2 is tested hardest here: the temptation is to move the lifecycle into
runtime. Generated handlers will be long; they must stay readable (“generated code is
boring”, Old plan). If a pattern repeats in every handler, the fix is a small pure helper in
runtimethat takes values, never the model. Telemetry cost Ash 15–23% of a create
(Synthesis section 6, item 7); the cost of the spans with no SDK installed must be measured.
M6 — Extension host and composed contracts (L)
- Goal. Extensions can add vocabulary, transform the model, verify it, emit files and
supply run-time behaviour, through one typed manifest; Ruling 5 and the composed-contracts
decision are enforced. - In scope.
- The manifest (Synthesis section 18). Build-time points: tags added; model transforms
and the named phase each runs in; verifiers; emitters; expression functions; attribute
types; tooling hooks (an extension may add ameshsubcommand); what the extension
requires from adapters; the contribution points it publishes and the points of other
extensions it contributes to. An attribute type entry names its validator and its column
type per data adapter. An expression function entry names its implementation per data
adapter; the build wires these into generated code, so no adapter importsmodel, and a
function with no implementation for the configured adapter is a build error. Run-time
points: named, reusable changes, validations, preparations and calculations that a
resource file refers to by name, and notifiers and policy checks (the last two are declared
here and first used in M11 and M8). Transports, data layers and actor resolvers are
adapters and are named in the project config, not in a manifest. - Core’s own contribution point for the scope. An extension may declare typed fields it
adds to the scope’s context and the resolver output it needs for them. This is how a
multitenancy extension will carry a tenant without core knowing the word (plan ruling Q4). - Named phases with a hard error on a cycle and a printed order (Synthesis section 16,
stage 4; Ash’s silent ordering failures are in section 2.1). The checks step from M1
becomes the Verify stage; a verifier failure stops the build (stage 5). - Ruling 5: a transform that writes to a part of the model owned by another extension
without a declared contribution fails the build. - Composed contracts (Rulings, “Lead decisions”): the tag contracts handed to
parseData
are composed in memory from core plus enabled extensions, andmesh buildalso writes one
self-contained contracts module for MX tooling, named inpackage.json#mx.contracts(MX
notes, mesh-answers, Q3 and Q7). Because that module containsanalyzefunctions, it is
produced with an established bundler, not by printing. - The core and data-adapter emitters from M1–M5 are re-registered through the same emitter
interface. Contracts from M2–M5 are declared stable here.
- The manifest (Synthesis section 18). Build-time points: tags added; model transforms
- Out of scope. Loading extensions by discovery: an extension takes part only where the
project config names it (MX notes, mesh-answers, Q10). A strictness setting for verifiers:
they are always fatal (section 6). - Packages.
compiler,model,runtime(core);frontend-mx(adapter: composition). - Acceptance tests. (1) A test extension adds a child tag to
resourcethrough a declared
contribution point; the composed contract accepts it; with the extension disabled the same
file fails. (2) An undeclared cross-extension write fails the build and names both
extensions. (3) A phase cycle fails the build and prints the cycle. (4) The generated
contracts module loads through MX’s own scan and yields the same tag names as the in-memory
composition (PR #1 has this test for the static module). (5) It is under the guard. (6) A
test extension supplies a custom attribute type, an expression function and a named change;
a resource file uses all three and the example runs ondata-sqlite; against a fake adapter
the function has no implementation for, the build fails. (7) A test extension adds a typed
field to the scope through the scope contribution point; a handler reads it with its type; a
resolver that does not supply it fails the call with a classed error. - Risks. This is the widest milestone; its squad leader should split build-time and
run-time points into separate tasks. Bundling the contracts module leans on Bun’s built-in
bundler; on Node another bundler is needed (noted for M14). MX’s editor support for data
files is deferred (MX notes, getting-started section 1), so the generated module has no
consumer until MX ships that; the in-memory composition is what the build depends on.
M7 — Relationships, calculations, aggregates (L)
- Goal. Resources refer to each other, and derived values can be queried.
- In scope.
belongs-to,has-many(already in the contracts),has-one(new). A
belongs-toadds its foreign-key attribute to the model (the fixture’sauthorId).
Cross-file verification: unknown resource names and inverse relationships (PR #1 report, “Out
of scope”), and cycles (Old plan, pipeline stage 4). Loading related records by explicit
request. Relationship traversal in translatable expressions, using thejoinscapability.
Mesh compiles relationships to joins itself and does not use Drizzle’s relations API, which
is being replaced (Synthesis section 12, risk 1).calculatewith a translatable value (in
the query) or an opaque value (after load, never usable in a filter).countand the other
aggregates, using theaggregatescapability. Both capabilities implemented in every merged
data adapter (section 4, capability rule). - Out of scope.
many-to-manyand join resources; managing related records inside a
write action (section 6). - Packages.
model,compiler(core);frontend-mx; data adapters. - Acceptance tests. (1) The example’s
post.mx(PR #1’s fixture withatomic=falseon
publish, see M5) without itspoliciesblock, plus newuserandcommentresources,
builds with no not-implemented error. (2) Reading a relationship that was not requested is a
type error in generated code, and a load that cannot be done is a run-time error, never
silently ignored (Synthesis section 6, item 7). (3) An opaque calculation used in a filter
fails the build. (4) Aggregates pass the conformance suite on every merged adapter. (5) A
relationship to an unknown resource fails the build at the tag. - Risks.
has-oneover a many-row match: Ash silently truncates (Synthesis section 6,
item 7); Mesh must define the rule (an identity on the foreign key, checked at build time).
M8 — Policies extension, simple tier (L)
- Goal. Authorization as declared data, per Ruling 6.
- In scope. Move the policy tags (
policies,policy,authorize-if) out of the core
contracts intoext-policies, contributed through the M6 mechanism, and addforbid-if
(new vocabulary) so that checks can deny as well as allow. Ordered checks per action or
action type; no matching policy means forbidden. A verifier that a policy’sactionnames a
real action (PR #1 report, “Out of scope”). Read policies become query filters, so a
forbidden row is simply absent. Write policies run in the M5 authorizer slot; a write check
that needs the record is run as a query inside the transaction. A structured breakdown:
for any decision, which policies applied, which check decided, and why (Synthesis section
2.2 calls Ash’s breakdown “the best debugging tool in the framework”). Acanfunction per
action that answers “may this actor do this” without doing it. The policy is kept as a
boolean formula in the model so a solver can be added later (Ruling 6). The M2
authorization: "none"flag stops being needed; it stays as an explicit option. - Out of scope. A solver; field-level policies (Synthesis section 8, gap 4, “likely
deferrable”); bypass rules, policy groups and Ash’s three access types (section 6). - Packages.
ext-policies(extension);compiler,runtime(core: slot only). - Acceptance tests. (1) The example’s policies: a non-author cannot publish; a reader sees
only published posts and their own. (2) A resource with no policy for an action forbids it.
(3) The breakdown for a denied call is asserted as data, not as text. (4) A policy expression
that cannot be translated to a filter fails the build on a read action. (5) The policy
formula round-trips throughmodel.json. (6) The example’s fullpost.mx(PR #1’s fixture
withatomic=falseonpublish) now builds with no not-implemented error. (7) A policy
naming an action that does not exist fails the build. - Risks. The line between a check that needs data and one that does not decides where it
runs; the build must classify it andexplainmust show it. Established authorization tools
were inventoried, not compared (Synthesis section 13); Ruling 6 fixes the first tier as
Mesh’s own, and section 8.1 says why.
M9 — Bulk actions, identities, upserts (M)
- Goal. The bulk half of Ruling 3 and the unique-key features the Synthesis lists as v1
needs (section 8, gap 4). - In scope. Bulk create, update and destroy with the per-record stream strategy: each
record goes through the full lifecycle, with its changes, hooks and policies, and the result
reports per-record success or error (Synthesis section 2.2: only this strategy keeps
per-record behaviour). One transaction per record by default, with an explicit
all-or-nothing option, shown inexplain(plan ruling Q12). Identities (declared unique
keys; new vocabulary) and upserts on an identity, using theupsertscapability. A--stdin
mode intransport-clithat streams records in and results out. - Out of scope. The batched-atomic strategy (Ruling 3: “comes later”).
- Packages.
compiler,runtime(core);frontend-mx,transport-cli, data adapters. - Acceptance tests. (1) A bulk update of 1,000 rows where 3 fail validation changes 997 and
reports 3 positioned errors; with the all-or-nothing option it changes none. (2) Policies
apply per record. (3) An upsert on an adapter without the capability fails the build.
(4) Streaming 100,000 records: peak memory is within 20% of the peak for 1,000 records.
(5) An identity produces a unique index in the emitted Drizzle schema, and in a generated
migration once M10 is merged. - Risks. A long stream of per-record transactions is slow on SQLite; the all-or-nothing
option is also the fast path, which may tempt users into it for the wrong reason.
M10 — Migrations and Postgres (M)
- Goal. A production database and a safe way to change its schema (Postgres is in v1 by
plan ruling Q1/Q13). May start as soon as M5 is merged. - In scope. Migrations by drizzle-kit (plan ruling Q3):
mesh migrate generateruns
drizzle-kit over the adapter-emitted Drizzle schema and commits the SQL migration it writes;
drizzle-kit owns the snapshot and the diff. Migrations are generated and reviewed, never
applied automatically (Old plan, “Risks”, last row);mesh migrate applyis an explicit
command. Replaces the M2mesh db pushshortcut for anything but throwaway databases. One
Mesh addition on top: before calling drizzle-kit,mesh migrate generatecompares the old
and new model and refuses a destructive or ambiguous change (drop a column, change a type,
rename, make an existing column required) unless a flag names it, because an interactive
prompt cannot be answered by an agent or a script.data-postgreson Drizzle, passing the
conformance suite as it stands when M10 is merged (section 4, capability rule); almost all of
it isdata-drizzleplus a driver and dialect differences. - Out of scope. Data migrations; automatic handling of renames.
- Packages.
cli(core: themigratecommands),data-postgres,data-sqlite,
data-drizzle(adapters). - Acceptance tests. (1) Add an optional column, add a resource: each generates the expected
migration for both dialects. (2) A destructive or ambiguous change fails with instructions
unless its flag is given. (3) Postgres passes the conformance suite locally. (4) Applying
the generated migrations to an empty database gives the same schema asmesh db push. - Risks. drizzle-kit is mid-rewrite (Synthesis section 10, “Migrations” row); see section
10, risk 3, for the mitigation. How drizzle-kit behaves without a terminal on an ambiguous
change has not been checked; the first task of M10 is to test it, and the Mesh pre-check
above exists so that the answer does not block the milestone. Local Postgres tests need a
server or an embedded Postgres on the developer’s machine; which one is a task for M10’s
squad leader.
M11 — Outbox, notifiers and jobs (M)
- Goal. Events and background work that commit together with the data they describe.
- In scope. The outbox, in core (plan ruling Q7): a
mesh_outboxtable written in
the action’s transaction and a relay that delivers rows after commit, at least once (Durable
engines section 6.1, item 4). A row older than the engine’s dedupe window is refused with an
operator error, not delivered (same ruling). Notifiers: after-commit events per action. The
JobQueueAdaptercontract inruntime(Durable engines section 6.2, adopted by plan ruling
Q14) and its implementation inworkflow-inprocess: running an action in the background,
with retry, delay and a dedupe key. Job capabilities checked at build time like data-layer
ones. Mesh’s own tables are declared to the data adapter like a resource’s, so they are in
the emitted Drizzle schema and in migrations. The worker loop (relay pluswork()of the
configured adapters) lives inruntime, so an application with any transport, or none, can
run it;transport-clionly exposes it as aworkercommand. A job runs under the scope
that enqueued it, stored with the job; it does not pass through an actor resolver again.
The in-process job queue declaresenqueueInTransaction: true, so its enqueues join the
action’s transaction and never pass through the relay (Durable engines section 6.1, item 6).
The relay exists for adapters that cannot do that, and for notifiers. - Out of scope. Multi-step workflows (M12); external queues (section 6: pg-boss is the
first candidate). - Packages.
runtime(core),workflow-inprocess(adapter),transport-cli(adapter). - Acceptance tests. (1) An action that enqueues a job and then fails leaves no job behind.
(2) Against a test adapter that declaresenqueueInTransaction: falseand a finite dedupe
window: a relay killed between delivery and acknowledgement delivers again and the job runs
once; a row older than the window is refused with an operator error. (3) A job runs with the
scope that enqueued it. (4) A notifier fires only after commit and not at all on rollback. - Risks. A second process (the worker) sharing a SQLite file needs care with locking. The
relay’s only v1 consumers are notifiers and a test adapter; its real test is the first
external adapter.
M12 — Workflows extension and in-process runner (L)
- Goal. Multi-step operations declared in resource files, running on the first adapter of
Ruling 7. - In scope. The
WorkflowAdaptercontract of Durable engines section 6.2 with its rules
6.1 and 6.3 (plan ruling Q14), with one change: its context type drops thetenantfield,
because core has no tenant (plan ruling Q4); a tenant travels as an extension’s scope field.
Workflow tags (new vocabulary, contributed byext-workflows); loops use step families
only, nofor-eachorparalleltags (plan ruling Q5). Generated code: each declared step
is a top-level named function and the workflow body calls steps by name (Durable engines
section 6.1, item 2). Verifier rules from its section 6.3: step, sleep, signal and child
names unique and declared; the body deterministic. The in-process runner with its journal
tables in the app’s database; per the document’s section 6.4 it declares every capability
true, includingstepInTransaction, so a step’s writes and its checkpoint commit together.
Rule R3 (its section 6.1, item 5) is for adapters that cannot do that: each action a step
calls records an effect key in amesh_step_effectstable inside its own transaction, so a
step that runs twice applies its effect once. R3 is built in core now, because it shapes the
generated step code, and tested with a test adapter (plan ruling Q14, option (a)). Workflow
capabilities checked at build time. The guard hashes each released workflow’s generated
body, step names and step-function inputs (its section 6.6) and fails when the hash changes
without a version entry: a line in a committed file recording the old hash, the new hash
and what happens to runs in flight. No automated migration of running workflows (plan
ruling Q6). - Out of scope. DBOS, Temporal and other external adapters.
- Packages.
ext-workflows(extension),workflow-inprocess(adapter),runtime(core:
contract, R3). - Acceptance tests. (1) A three-step workflow killed after step two resumes and does not
re-run steps one and two. (2) Against a test adapter that declaresstepInTransaction: false: a step that calls an action, killed after the action commits and before the step is
recorded, applies the action once. (3) A workflow using signals built against a fake adapter
withsignals: falsefails the build. (4) Cancel from a second process stops a running
workflow between steps. (5) Start from the command line with no HTTP server anywhere
(Ruling 8). (6) Changing a released workflow’s body without a version entry fails
mesh build --check. - Risks. The in-process runner’s design is “a design claim; no code exists yet” (Durable
engines section 6.4), and it is the largest thing in this plan that Mesh builds where
established engines exist; Ruling 7 orders it first, and section 8.1 records why no engine
can take its place in v1. Compensation and undo for sagas (Synthesis section 4, “Sagas”) are
not in the adapter interface and need vocabulary design in this milestone.
M13 — Agent and test surface (M)
- Goal. The outputs that make a Mesh project legible to coding agents and easy to test
(Synthesis section 14, goal 6). - In scope.
ext-agent: a generated rules file describing the project’s resources and the
Mesh vocabulary, and each action exposed as an agent tool, served with the official Model
Context Protocol SDK, that obtains its scope from the configured actor resolver like any
transport, so policies still apply (Synthesis section 5, item 6).ext-testing: test data
builders derived from each resource, built on an established generator library, and a seed
path that writes rows without running actions (Synthesis section 4, “Testing”). Error
messages audited against “errors name the fix”. - Out of scope. Measuring agent performance: Ruling 1 says the measurement is not a gate.
- Packages.
ext-agent,ext-testing(extensions). - Acceptance tests. (1) The rules file is generated, committed and guarded. (2) An agent
tool call by an actor without permission is denied with the policy breakdown. (3) A generated
builder produces a record that passes the resource’s own validations. - Risks. The agent-tool protocol is a transport in its own right; it must use the M2
transport and actor-resolver contracts, not a side door.
M14 — Node parity and single binary (M)
- Goal. The runtime adapter promise: Mesh runs on Bun and on Node (the project’s agent instructions (
CLAUDE.md)). Last
milestone of v1, because it runs every other milestone’s tests. - In scope. The whole test suite on Node. A Node SQLite driver inside
data-sqlite
(better-sqlite3does not run on Bun, so drivers are per runtime: Synthesis section 10,
“Runtime” row). The build tool on Node, including the contracts bundling step from M6. A
single-binary build of the example withbun build --compile, with its idle memory measured
(unmeasured so far: Synthesis section 12, risk 5). - Out of scope. Deno and edge runtimes; a memory target (the number is recorded, not
gated). - Packages. All; mainly
data-sqlite(adapter) andcli(core). - Acceptance tests. (1)
verifygreen under Bun and under Node. (2) The compiled binary
runs the M2 end-to-end test. (3) Averifycheck thatruntimeuses no Bun-only API.
(4)data-sqliteanddata-postgreseach pass the full conformance suite with joins,
aggregates, upserts and atomic expressions (section 4, capability rule). - Risks. Tests are written for Bun’s test runner (PR #1 uses it), which does not run under
Node; M14 must either run the suites through a runner that works on both or drive a
Node-built example from Bun’s runner. Deciding that at M0 instead would be cheaper; it is
flagged for the M0 squad. Under Node, MX tools need a restart after a contracts module
changes unless it is emitted as.cjs(MX notes, updates, 2026-10-03 19:51).
M15 — HTTP transport, typed client, OpenAPI (L, after v1)
- Goal. The second transport, built on the generic contract the command line proved.
- In scope.
transport-http: one Fetch handler(Request) => Responseper action
(Synthesis section 9). It obtains the scope from the same actor-resolver contract as the
command line, given the request. Then mounting inside Elysia as an adapter over the same
handlers (Rulings, “Elysia”: a candidate HTTP adapter once the generic contract exists).
ext-client: a typed client with one function per action and per-action permission metadata
(Synthesis section 8, “Do differently”, row “Client with no authorization metadata”).
ext-openapi: an OpenAPI document, derived from the generated validators with an established
converter. - Out of scope. JSON:API, GraphQL, authentication strategies (an actor resolver built on
an established library such as Better Auth is a later adapter). - Packages.
transport-http(adapter),ext-client,ext-openapi(extensions). - Acceptance tests. (1) The M2 end-to-end scenario passes over HTTP with no change to
generated handlers. (2) The same handlers mounted in Elysia pass it too. (3) The client’s
types reject an input field not inacceptat compile time. (4) The client’s permission
metadata matchescanfor a sample of actors. (5) The OpenAPI document validates against
the OpenAPI schema and is guarded. - Risks. Elysia’s plugin model was read from documentation, never tested (Synthesis
section 13).
6. Not scheduled
Listed so that their absence is a decision, not an oversight. Each has a one-line reason.
| Item | Source | Why not now |
|---|---|---|
| Batched-atomic bulk strategy | Ruling 3 | The ruling defers it |
| Policy solver | Ruling 6 | The ruling defers it; the formula is kept |
Bypass rules, policy groups, access types (strict, filter, runtime), field policies |
Synthesis sections 1 and 8 | Simple tier first (Ruling 6); access types are a “Copy” recommendation worth revisiting after M8 |
| External workflow adapters (DBOS first, then Temporal) | Durable engines section 7 | DBOS’s worker must be tested on Bun before it is chosen |
| pg-boss as a job-queue adapter | Durable engines section 7 | The first external job adapter once Postgres is in; it enqueues in the caller’s transaction and documents Bun support. Not in v1 only because v1 must also work on SQLite |
| Multitenancy extension | Synthesis section 15; plan ruling Q4 | An extension; M6 gives it the scope contribution point it needs |
| Generic actions (Ash’s fifth action type) | Synthesis section 1; Old plan, open questions | The tag shape is undecided; an opaque step in a workflow covers the first needs |
many-to-many, join-resource options, managing relationships in a write |
Synthesis section 1 (relationships row) and section 8, gap 4 | Join-resource options are deferrable per gap 4; the other two depend on them and no v1 example needs them |
Embedded resources, NewType, manual actions |
Synthesis section 8, gap 4 | Deferrable per gap 4 |
| Audit trail, event log, state machine, soft delete, encryption, rate limits | Synthesis section 15 | Extensions; none blocks the core |
| A hand-written in-memory data adapter | Synthesis section 10, “Data layer” row | SQLite’s in-memory mode covers tests and prototypes (N2) |
| GraphQL, admin interface | Synthesis section 8, “Do not build yet” | Explicitly deferred there |
| JSON:API | Synthesis section 13 | No TypeScript library for it was evaluated; it needs the HTTP transport first |
| A second front end (TypeScript declarations) | Synthesis section 10 | No user for it yet (plan ruling Q2) |
mesh watch |
Old plan, compiler pipeline | Convenience; mesh build is fast enough to start |
| Verifier strictness as a setting | Synthesis section 8, “Do differently” | Verifiers are always fatal in v1, the strict end of that recommendation |
| Staged migration flags with a named removal version | Synthesis section 8, “Copy” | Needed at the first breaking release, not before |
| Source maps from generated code to resource files | Old plan, pipeline stage 7 | Plan ruling Q11: embedded positions in v1; revisit at M14 |
| Continuous integration | Plan ruling Q8 | Waits for MX to be published |
7. Traceability: ruling to milestone
| Ruling (Rulings file) | Where it lands | How it is checked |
|---|---|---|
| 1. No measurement gate | No milestone measures agents; M13 scope says so | Nothing blocks on a measurement |
| 2. As much generated logic as Ash, or more; thin engine | M2 (generated handlers), M5 (generated lifecycle), every later milestone | Import rule (M2 test 4); stack-trace test (M2 test 3) |
3. Atomic single-record updates and bulk stream in v1; batched-atomic later |
M5 (atomic), M9 (bulk stream); section 6 (batched-atomic) | M5 tests 1–3; M9 tests 1–2 |
| 4. Mandatory data-layer set; other capabilities declared; missing one is a build error | M3 (set, manifest, check); M5, M7, M9 (atomic expressions; joins and aggregates; upserts) | Conformance suite; fake-adapter build failures (M5 test 3, M9 test 3) |
| 5. Cross-extension contributions only through declared points | M6 | M6 tests 1–2 |
| 6. Simple policy tier, solver-ready | M8 | M8 tests 1–7; formula kept in the model |
| 7. Workflow adapter interface from the engine comparison; in-process runner first | M11 (outbox, jobs), M12 (interface, runner) | M11 and M12 tests; capabilities checked at build time |
| 8. First transport is a command line; Mesh not tied to web applications | M2 (transport-cli, generic transport contract); M12 test 5 |
End-to-end test with no HTTP anywhere |
| Elysia is not core; later HTTP adapter | M15, after v1 | M15 test 2 |
| Consequence 1: the first “server and API protocol” adapter is a CLI transport | M2 | M2 test 1 |
| Consequence 2: every transport, the CLI included, obtains the scope through the actor-resolver adapter contract; core defines no flags, no tenant, no login; tenant belongs to the multitenancy extension | M2 (contract, actor-dev, scope = actor and context); M6 (scope contribution point); M12 (contract without tenant) |
M2 test 8; M6 test 7 |
| Consequence 3: the durable-engines research feeds the jobs and workflows adapter contract | M11, M12 | M11 and M12 tests |
Lead decision: one generated, self-contained mx.contracts module from core plus enabled extensions |
M6 | M6 tests 1 and 4 |
| Plan ruling Q1/Q13: v1 = M0–M14, command line only, SQLite and Postgres | Section 1; M10; M15 after v1 | M14 test 4 |
| Plan ruling Q3: established tools; Drizzle and drizzle-kit behind the data-layer contract; Mesh compiles its expression tree into Drizzle’s builder | M2, M3, M4, M10; section 8.1 | M2 test 4 (isolation); M3 test 5; M10 tests |
| Plan ruling Q4 (withdrawn question): no application concerns in core | Principle 7; M2; M6 | M2 test 8; M6 test 7 |
| Plan ruling Q8: local build and test, no CI until MX is published | M0; principle 6 | M0 test |
| Plan rulings Q2, Q5–Q7, Q9–Q12, Q14 | M1 (Q2); M12 (Q5, Q6, Q14); M11 (Q7, Q14); section 3 (Q9); M4 (Q10); M5 (Q11); M9 (Q12) | The tests of those milestones |
8. Decisions made in this plan, and why
These are the squad leader’s choices where the inputs left room. The lead or operator may
overrule any of them.
- D1. The walking skeleton uses a SQLite file. A command line starts a new process per
call, so an in-memory store would lose every row betweencreateandread. - D2. Core is split by when the code runs.
compilerandmodelat build time,runtime
in the deployed application. Types that cross a run-time contract (scope, errors, query and
expression tree) live inruntime, so the deployed half depends on nothing from the build
half and the import rule in M2 can be checked mechanically. - D3. Policy tags move from the core contracts to
ext-policiesin M8. The Synthesis
(sections 10 and 15) makes policies a first-party extension. Moving real tags through the
contribution mechanism is also the best test of that mechanism. - D4. The extension host comes after the lifecycle (M6 after M5), not first. Building the
host before there is a pipeline to extend would mean guessing its needs. The cost is that
M1–M5 contracts are unstable until M6, which is stated in M2. - D5. A tag or attribute that is valid but not implemented is a build error. PR #1’s
contracts already accept the whole vocabulary; the compiler grows into it milestone by
milestone. Silently ignoring apoliciesblock would be the worst kind of fallback. - D6. Default deny holds from M2, through an explicit opt-out flag, instead of arriving
with policies in M8. - D7. Opaque expressions are emitted by slicing authored text at MX’s span. MX documents
this use. It is not re-parsing, which the project forbids. - D8. Relationships come before policies (M7 before M8). The example’s policies compare
post.authorIdwith the actor, andauthorIdexists only throughbelongs-to. - D9. Updates and destroys are atomic by default, with
atomic=falseas the stated
exception. It follows principle 3 and the direction Ash 3.0 took. - D10.
publicmeans “may leave through a transport”. In-process callers get the full
record. This is the smallest reading of “private unless exposed” that can be tested in M2. - D11. Drizzle is confined to the
data-*packages and the schema file they emit.
Generated handlers call Mesh’s contract. A project can change database, or Mesh can change
query library, without touching handlers; the import rule enforces it. - D12. Postgres stays in M10 but may start after M5, so the data-layer contract meets its
second dialect as early as the team can staff it. - D13. Mesh does not use Drizzle’s relations API. Relationships compile to joins in
data-drizzle. The relations API is the part of Drizzle being rewritten.
8.1 Build or reuse
Plan ruling Q3 asks for established tools wherever possible. For each thing this plan could
have built itself:
| Need | Decision | Tool, or the one-line reason to build |
|---|---|---|
| SQL queries, drivers, type mapping | Reuse | Drizzle (plan ruling Q3) |
| Schema migrations: snapshot and diff | Reuse | drizzle-kit (plan ruling Q3); Mesh adds only a refusal of destructive changes without a flag |
| In-memory database for tests | Reuse | SQLite’s :memory: mode through data-sqlite (N2); was a hand-written adapter in revision 1 |
| Input validation and casting | Reuse | An established validation library behind Standard Schema (N1); was Mesh’s own cast functions in revision 1 |
| Tracing | Reuse | OpenTelemetry API (N3); was a Mesh tracer contract in revision 1 |
| Formatting emitted code | Reuse | An established formatter, pinned; was a Mesh printer in revision 1 |
| Command-line argument parsing | Reuse | The standard library’s parseArgs |
| Bundling the contracts module | Reuse | Bun’s bundler; another established bundler on Node |
| Agent tools | Reuse | The official Model Context Protocol SDK |
| Test data generation | Reuse | An established generator library under the generated builders |
| OpenAPI document | Reuse | A converter from the validation library’s schemas |
Parsing .mx and expressions |
Reuse | MX (parseData, Babel nodes) |
| Test runner, type checker | Reuse | Bun’s test runner, tsc |
| Expression tree and its compilation to Drizzle | Build | Nothing reusable exists (Synthesis section 8, gap 5); plan ruling Q3 says Mesh compiles its own tree |
| Build pipeline, extension host, emitters, guard | Build | This is Mesh itself; Synthesis section 16 |
| Generated action lifecycle | Build | Ruling 2: the behaviour must be in generated code |
| Policy engine, simple tier | Build | Ruling 6 fixes the tier; it must turn a policy into a query filter over Mesh’s own expression tree, which needs the tree, not an external rule engine. Established authorization tools were inventoried, not compared (Synthesis section 13) |
| Outbox and relay | Build | It must commit in the action’s own transaction through Mesh’s data-layer contract; small (one table, one loop) |
| In-process job queue and workflow runner | Build | Ruling 7 orders the in-process runner first. No compared engine runs on SQLite without a server: DBOS and pg-boss need Postgres, the rest need a server or a cloud (Durable engines section 3). They come as adapters after v1 |
| Conformance suite, capability manifest | Build | They test and describe Mesh’s own contract |
9. Questions
9.1 Decided
All fourteen questions of revision 1 are answered in the Rulings, “Implementation-plan rulings”.
| # | Question | Decision | By |
|---|---|---|---|
| Q1, Q13 | Where is the v1 line; is Postgres in v1? | v1 = M0–M14, command line only, SQLite and Postgres; HTTP (M15) after | Operator |
| Q2 | Source of truth for the tag vocabulary | The MX contracts, for tag names; value vocabularies in the core registries, with a drift test | Lead (recommended option) |
| Q3 | Drizzle, or SQL printed by Mesh? | Reversed from the plan’s recommendation. Drizzle for queries, drizzle-kit for migrations, behind Mesh’s contract; established tools wherever possible | Operator |
| Q4 | How does the command line establish actor, tenant and context? | Withdrawn. The question itself put application concerns in core. Scope = actor and context, from an actor-resolver adapter; no flags, tenant or login in core | Operator |
| Q5 | Loops in workflows | Step families only | Lead (recommended option) |
| Q6 | Running workflows when code changes | The guard fails without a version entry; no automated migration | Lead (recommended option) |
| Q7 | Outbox placement; stale relay rows | Core; stop and alert | Lead (recommended option) |
| Q8 | CI without a published MX | No CI until MX is published; build and test locally | Operator |
| Q9 | Names @mesh/* and mesh |
Kept as working names | Lead (recommended option) |
| Q10 | Expression semantics where SQL and JavaScript differ | SQL’s, documented | Lead (recommended option) |
| Q11 | Source maps or embedded positions | Embedded positions in v1; revisit at M14 | Lead (recommended option) |
| Q12 | Bulk transaction default | Per record, all-or-nothing as an option | Lead (recommended option) |
| Q14 | Adopt Durable engines section 6.2 as the contract | Adopted as is; R3 and the relay built now | Lead (recommended option) |
9.2 Raised by this revision, for the lead
Reworking the plan for plan rulings Q3 and Q4 forced four choices. Each has a recommendation,
which is the working assumption; none blocks M0 or M1.
| # | Question | Options | Recommendation | Blocks |
|---|---|---|---|---|
| N1 | Which validation library do generated validators use? | (a) Zod; (b) Valibot; © keep Mesh’s own cast functions | (a): the most established of the three, and the rest of Mesh sees only Standard Schema, so it can be swapped. (b) is smaller, which matters for the single binary; M14 measures that. Not checked: the size of either in a compiled binary. | M2 |
| N2 | Drop the hand-written in-memory data adapter in favour of SQLite’s in-memory mode? | (a) drop it; (b) keep a separate evaluator | (a): it removes a second implementation of expression semantics that Mesh would have to keep identical to SQL’s by hand. Cost: the contract has a single implementation until Postgres lands, hence D12. The Synthesis listed “in-memory” among the first adapters (section 10). | M3 |
| N3 | May generated code depend directly on the OpenTelemetry API package? | (a) yes; (b) keep a Mesh tracer contract with OpenTelemetry as an adapter | (a): the Synthesis names that API as a neutral standard to use as the contract (section 9). Cost: one small dependency in every generated application. | M5 |
| N4 | What is the development actor resolver? | (a) actor-dev reads actor and context from a file named in the project config; (b) no shipped resolver, each application writes its own from the first day |
(a): the walking skeleton and every test need some resolver, and a file keeps “who is calling” out of the command line’s own flags. It must be plainly unfit for production (it trusts the file). | M2 |
10. Risks across milestones
- PR #1’s vocabulary is partly inferred. M0 starts from it. Its vocabulary rules include
several the dev inferred (PR #1 report, “Conditional rules”, “Inferred by me”); they should
be confirmed when M1 builds the model on them. - MX is consumed from
mainwith nothing pinned (MX notes, getting-started section 5). A
breaking MX change stops Mesh the same day. The conformance and contract tests are the
alarm. - Drizzle v1 is a release candidate, its relations API is being replaced, and drizzle-kit
is mid-rewrite (Synthesis section 12, risk 1; section 10, “Migrations” row). Mitigation,
not avoidance: exact version pins in thedata-*packages; Drizzle imported nowhere else
(M2 test 4), so an upgrade touches three packages; an upgrade is its own pull request and
must pass the conformance suite; Mesh does not use the relations API (D13); committed
migrations are plain SQL files, so they survive a drizzle-kit change. - No CI (plan ruling Q8). Every gate in this plan runs through the local
verifyscript,
so a skipped run is invisible. The lead’s verifier runs it once per delivery; the guard
(mesh build --check) is part of it. - Thin engine against readable output. Ruling 2 pushes logic into generated files; if they
become unreadable, the benefit is lost. M5 is where to watch. - Scope of v1 is large: fifteen milestones, eight of them L. The parallel tracks after M5
and M6 are where calendar time is recovered, and each L needs its own squad leader. - The workflow interface is unproven, and the in-process runner is the largest thing Mesh
builds itself where established engines exist. - The vocabulary is what a coding agent has not seen before (Synthesis section 7), so
every new tag should come with a generated rules-file entry from M13 onward. - New vocabulary is spread over many milestones (pagination, hooks, preparations,
arguments,atomic,has-one,forbid-if, identities, workflow tags). A vocabulary owner
should review them together so the language keeps “one way to do each thing” (Old plan,
“Agent legibility principles”).
11. Review
Review file for every round: the review of plan revisions 1 and 2 (not published).
The squad leader’s report is the report on plan revision 1 (not published).
Revision 1 (reviewed by a separate Opus agent, three rounds, 2026-10-04):
- Round 1: accept with fixes; 2 high, 12 medium, 7 low. All applied: dependency graph redrawn
from the acceptance tests (relationships before policies; M9 and M13 after M8; M14 last);
preparations, tracing and run-time extension points scheduled;publicanddomaingiven a
meaning; five decisions moved into the question table; error classes and scope placed in
runtimewith a concrete import rule; M3 tests corrected; change classification and the
atomic default defined; in-process capability manifests stated; the worker loop moved to
runtime; counts and eight citations corrected. - Round 2: all 21 confirmed fixed; 5 medium and 7 low new findings, all applied (self-contained
M4 example; atomic rule covers validations that read the record; capability rule keyed to
merge time and closed by an M14 test; index test moved to M9; extension types and functions
name their per-adapter half; seven wording and citation fixes). - Round 3: eleven of twelve confirmed fixed, the twelfth fixed when the report was written;
three new low findings applied.
Revision 2 (this text; changes after the operator’s rulings on the plan):
- Plan ruling Q3:
data-sqliteanddata-postgresare built on Drizzle, migrations on
drizzle-kit, with a shareddata-drizzlepackage; M2, M3, M4 and M10 reworked; M10 is now
size M and may start after M5; the Drizzle risk is recorded with its mitigation (section 10,
risk 3). Section 8.1 “Build or reuse” is new; five things revision 1 built are now reused
(in-memory database, input validation, tracing, code formatting, argument parsing). - Plan ruling Q4: the scope is actor and context; the actor-resolver contract and
actor-dev
arrive in M2;transport-clidefines no caller flags; tenant is left to a multitenancy
extension through a scope contribution point in M6; the workflow contract dropstenant. - Plan ruling Q8: CI removed from M0; every “CI check” is a
verifycheck. - Questions: Q1–Q14 moved to “Decided” (section 9.1); four new ones for the lead (N1–N4).
- Review of revision 2: recorded in the review file under “Revision 2”.