Rulings of 2026-10-04

Rulings of 2026-10-04

Dated log

The decision records are authoritative. This page is the dated log of rulings they quote, kept verbatim, corrections included.

The operator’s answers to the eight open design questions in
research synthesis section 19, and to the Elysia question (section 11).
These are decisions, not proposals. The implementation plan follows them.

# Question Ruling
1 Measure the agent hypothesis before building? No. Mesh is built regardless. The measurement is not a gate.
2 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.
3 Atomic and bulk writes in v1 Atomic single-record updates (changes folded into the statement) plus bulk actions with the per-record stream strategy. Batched-atomic comes later.
4 Mandatory data-layer capabilities Select, insert, update, delete, transactions, filters, sort, pagination. Joins, aggregates, upserts and atomic expressions are declared capabilities; using one an adapter lacks is a build-time error, never a silent in-memory fallback.
5 May extensions contribute to each other’s part of the model? Only through contribution points the owning extension publishes and the contributor declares in its manifest. Anything else is a build error.
6 Policy engine depth Simple tier first: ordered allow/deny checks, read policies as query filters, structured breakdown output. Keep the policy formula representation so a SAT solver can be added later.
7 Durable workflows Compare durable engines now, with the goal of defining Mesh’s workflow adapter interface. The in-process runner is the first adapter.
8 First server adapter Neither Elysia nor Hono first. Mesh is not to be tied to web apps, or to any kind of application: like Ash, it must serve a CLI, a daemon, a web app or an API equally. The operator’s words: “let’s test Mesh with a leaner thing that will require less wiring.” This is a statement about how to test first, not a ruling that a CLI is the first transport. (Corrected 2026-10-04; the earlier text said “The first transport is a CLI”, which was the lead’s misreading.)

Elysia

Agreed: Elysia is not core. The operator’s earlier weight on Elysia should not shape the
transport contract. Elysia stays a candidate HTTP adapter once the generic contract exists.

Consequences for the proposal (synthesis Step 3)

  • Section 15 “Server and API protocol” adapter: no transport is chosen as first. The core’s
    interface is an in-process function call; every transport is an optional adapter over it.
  • Section 17 phase 1 “Enter”: every transport, the CLI included, obtains the caller’s scope
    through the actor-resolver adapter contract. Mesh core defines no flags, no tenant and no
    login; tenant belongs to the multitenancy extension. (Corrected 2026-10-04: an earlier line
    here made the CLI define actor/tenant/context, which pulled app concerns into core.)
  • A new research document (durable engines, adapter-interface focus) feeds the jobs and
    workflows adapter contract.

Lead decisions (operator may overrule)

  • Composed contracts module. Mesh generates one self-contained mx.contracts module from
    core plus the enabled extensions, so extensions add tags without any MX change. Follows
    ruling 5 (contributions only through declared points: a closed resource.children must
    accept children an extension contributes). Stated to the MX lead in
    Mesh’s answers to MX on mx.contracts, 2026-10-04; no decision-142 addendum needed.

Implementation-plan rulings (2026-10-04)

Q Ruling
Q1/Q13 v1 = M0–M14, CLI only, SQLite and Postgres. HTTP adapter (M15) after v1.
Q3 Rely on established tools wherever possible (operator’s standing position). SQL adapters use Drizzle for queries and drizzle-kit for migrations, behind Mesh’s data-layer contract. Mesh still compiles its own expression tree into Drizzle’s SQL builder.
Q4 Withdrawn: it brought app concerns into core. The CLI transport resolves scope via the actor-resolver adapter; tenant is the multitenancy extension’s.
Q8 Build and test locally; no CI until MX packages are published.
Q2, Q5–Q7, Q9–Q12, Q14 Lead decided: take the plan’s recommended option for each (plan section 9).

Review note (2026-10-04, later)

The operator asked for a review of every decision above. Until that review is closed, read the
tables with these corrections:

  • “Plan ruling Q4” was never an operator ruling. The operator said only that app-specific
    concerns must not enter Mesh’s architecture. The actor-resolver adapter, “scope = actor and
    context” and “tenant belongs to the multitenancy extension” were the lead’s inventions.
  • “Plan ruling Q3” records the operator’s position (“the more we can rely on well-established
    tools the better”); choosing Drizzle and drizzle-kit specifically was the lead’s application
    of it.
  • Q2, Q5–Q7, Q9–Q12 and Q14 were accepted by the lead in one line without analysis. They are
    provisional.
  • “CLI only” in Q1/Q13 follows the misreading of ruling 8 and is void; “M0–M14, SQLite and
    Postgres, HTTP after” is what the operator chose.

Rulings after the decision review (2026-10-04, operator)

Topic Ruling
Expressions One expression tree, evaluated both in memory and in SQL (as Ash does). Replaces the plan’s “translatable expressions only ever run as SQL” (plan Q10/N2).
v1 line v1 = M0–M8 plus M10: workspace, build skeleton, run skeleton, data-layer contract, expressions, action lifecycle, extension host, relationships/calculations/aggregates, policies, migrations and Postgres.
After v1 M9 (bulk, identities, upserts; overrides the bulk half of ruling 3), M11 and M12 (outbox, jobs, workflows; replaces ruling 7’s “in-process runner first”; the adapter interface stays a design document), M13 (agent and test surface), M14 (Node parity, single binary), M15 (HTTP).
CI None until the MX lead says the @mxlang packages are published. The operator handles publishing with the MX lead.
Architecture process The final architecture is designed following the engineering:architecture skill (decision records).
Docs site apps/docs in the repo, built with docmd (docmd.io). Two top-level sections: Docs (for users) and Architecture (for contributors: roadmap, every decision, overviews, in-depth pages, research results). Rule for Architecture: document everything that cannot be understood by looking at a single code file.
Runtime Node is dropped. Mesh runs on Bun only (old M14 disappears). The run-time library keeps to web-standard APIs where it can.
MCP Not built. The operator prefers CLIs made for agents (fewer tokens). Agent surface: the generated rules file, later a CLI adapter generated from the action list.
Research The research documents move into the docs site under Architecture / Research. The repo docs become the source of truth; notes/ stops being it.
Docs deployment The docs site is deployed with Coolify on netcup at mesh.saulo.tech.
Order of work Framework code (M0 onward) starts once the roadmap and decision records are published; nothing else gates it.
Licence and visibility Mesh is fully open source under the MIT licence. Nothing in the docs site is private.
Vocabulary Copy Ash’s DSL for now (names and structure). After v1, review it and optimise for what feels natural in MX. Resource files and every example always use MX concise syntax.
User docs as live spec Pages under Docs are written before the implementation, and sometimes before the architecture, to model the intended developer experience and expose problems the architecture pages hide. Replaces the earlier practice “Docs describe only what exists today”. Every Docs page opens with a warning that it is a live spec of how things will be and that Mesh is not released. First set: installation, usage, project structure, a todo-list example.
MX MX is not replaceable: it is core, not an adapter. No “front end” adapter slot and no frontend-mx package; the tag contracts live in the core compiler package.
Repository shape A bun workspace monorepo (packages/, apps/, examples/*), started 2026-10-04 as roadmap milestone M0.
Trailing ? in attribute names Not enabled, although the MX lead confirmed the data target could allow it. In TypeScript name? means optional, so allow-nil? reads wrong; it would permanently block a future name?=expr syntax; it diverges from Marko’s translator; and a bare boolean attribute already carries the predicate meaning. Spelling stays: Ash names in kebab-case with ? dropped. The same holds for tag names: Mesh declares no tag ending in ? (MX still allows it, as Marko does).

Implementation note, 2026-10-04: the first live-spec set grew to seven new pages. Using your domain, Configuration and the command line supplement the four named in the ruling to make their examples complete. The ruling above is preserved verbatim. The command-line page was later merged into Configuration, then split again into Configuration and Command line in the structural review of 2026-10-09.

Lead decisions of the architecture-docs brief (2026-10-04)

Decided by the lead; the operator may overrule. Quoted from the brief the lead gave for the roadmap and the decision records.

  • Scope is a plain argument. The caller passes { actor, context } on every action call, as Ash’s actor: option. Drop the actor-resolver contract, the actor-dev package, the action registry and the transport contract from v1. N4 is moot.
  • Tenant: open. In Ash, tenancy is in core. Nobody ruled where it lives in Mesh and multitenancy is not scheduled. Write it as a Proposed ADR with options; drop the “scope contribution point” from the extension host.
  • Walking skeleton ends in a function call: a test and a ~20-line script in examples/blog call the generated handlers against SQLite. No transport-cli, no exit codes, no --stdin, no worker command.
  • public and default-deny before policies (old D10, D6): no transport exists in v1, so public has nothing to filter yet, and the authorization: "none" flag is wiring for nothing. Drop both from M2; deny-by-default arrives with the policies extension. Record what public will mean as a Proposed ADR.
  • Outbox relay and rule R3 are deferred with the workflow milestones. durable engines stays the design input for a future workflow/job adapter contract; a small spike “DBOS worker on Bun” precedes any engine choice. No engine is a clear winner (DBOS best fit on paper for workflows, pg-boss for plain jobs on Postgres).
  • Validations are classified like changes now that an in-memory evaluator exists: a translatable validation folds into the atomic statement; rework the atomic-by-default rule and the publish example accordingly.
  • N1 Zod behind Standard Schema: accepted. N3 OpenTelemetry API direct: accepted. N2 (in-memory data adapter vs SQLite :memory:): reconsider now that the in-memory evaluator exists; decide, with reasons, in an ADR.
  • The vocabulary on main is provisional. The 26 tag contracts were copied from an MX test fixture; several rules were inferred by the dev or ruled by the lead during code review (PR #1 report, “Inferred by me”; rounds 1–4). Write a Proposed ADR plus a “Vocabulary design” page listing every open vocabulary question (inferred rules, review rulings, and the new tags each v1 milestone adds) so the operator can rule on them in one pass before M1 builds a model on them.

Later the same day

Further decisions by the lead on 2026-10-04, given while the roadmap and the decision records were written. The operator may overrule each.

  • public attributes. public is recorded in the model, as Ash records public?, and nothing in v1 reads it. It is not a not-implemented build error.
  • Atomic updates in v1. “In v1 a validation or change that reads the stored record makes the action non-atomic, and the action must say require_atomic? false, as in Ash. Folding record-reading validations into the statement, with the failure protocol you designed, becomes a Proposed ADR for after v1.” This replaces the brief’s “a translatable validation folds into the atomic statement”. Translatable validations that need no stored record still run before the statement on either path; the in-memory evaluator runs validations on the non-atomic path.
  • Write policies on atomic actions. A record-reading write policy on an atomic action folds into the statement as a filter; a forbidden row reports not found. Accepted first as Ash’s behaviour; the research shows Ash compiles the check as an expression that raises and reports forbidden, so the lead kept not-found as a deliberate deviation and a working assumption, and made the point a Proposed record for the operator to rule.
  • has-one. An implicit unique index for v1, recorded as a Proposed deviation from Ash, with the alternative “a build error unless the foreign key is declared unique” once identities exist.
  • Spelling of names. The MX maintainers measured on MX main (7a404916) that _ is accepted in tag and attribute names and that a trailing ? in an attribute name is not accepted and never will be (Marko’s syntax rule: letters, digits and ._:-). “Mesh names are Ash’s names in kebab-case with ? dropped: belongs_to → belongs-to, allow_nil? → allow-nil, require_atomic? → require-atomic. The mapping is mechanical and one-to-one, it matches what is on main, and it avoids renaming twice.”
  • Adapter-contributed commands. mesh db push and mesh migrate are contributed by the SQL adapters. The rule that MX is imported only by packages that declare tag contracts is checked by verify from M1.
  • Expression semantics. The record stays Proposed and must be ruled before M4.
  • Deciders on public pages are named by role only: operator, lead, roadmap author.
  • In-depth pages for policies and for relationships, calculations and aggregates are written in the milestones that build them, not before: the rule is to document what cannot be understood from one code file, and that code does not exist yet.
  • Published history. The rulings log is published verbatim, corrections included, with a banner saying the decision records are authoritative. Plan revision 2 is published as a superseded page, excluded from search and from llms.txt.

Rulings before M2 (2026-10-04, operator)

Topic Ruling
Connection handling (DX finding 1) Generated code exports a factory, bind(dataLayer), that returns every action function bound to that data layer. connect() applies the factory to the configured adapter and backs the top-level exports, so application code keeps createTodo(input, scope); a test binds its own database and gets its own functions. Touches ADR-0007.
table placement (mapping exception X1) table stays an attribute of resource for now. It moves to a data-layer section, as in Ash, once the extension host exists (M6) or at the post-v1 vocabulary review.

Lead decisions for M2 (operator may overrule): validate sees the record as it will be after the
changes, with the input as a second parameter (DX finding 3; amends ADR-0017, Proposed); a create
policy sees the proposed record, and reading a related record is a query inside the transaction
before the insert (DX finding 4; ADR-0022, Proposed); in-process schema for tests is an adapter
function if drizzle-kit’s push API runs in process on Bun, otherwise emitted DDL the adapter runs
(DX finding 2; decided by a spike); a column is named like its attribute (finding 13); two
resources exporting the same action function name is a build error naming both (finding 21).

Recorded consequences and spike ruling (2026-10-04)

  • Connection handling is ADR-0047, Accepted, operator. The data layer is not in { actor, context }.
  • Column naming has no transform: the column is named exactly like its attribute (data layer, project structure). Duplicate action function names fail the build and name both resources (generated code). X1 is closed for now (vocabulary mapping).
  • From M2 generated code imports zod, drizzle-orm and @opentelemetry/api from the user’s project. They must be application dependencies; mesh build checks that they resolve (quick start).
  • After the spike, the lead accepted ADR-0048, reported to the operator: pin drizzle-orm@0.45.3 and drizzle-kit@0.31.11; use pushSQLiteSchema behind one SQLite adapter function containing the call and cast. Dynamically import drizzle-kit as a project development dependency, fail clearly if absent, and keep schema push to tests/development; production uses migrations. The stable pair works on Bun :memory:; 1.0.0-rc.4 has no drizzle-kit/api. Revisit at Drizzle v1 release or M7, using guarded emitted DDL unless programmatic push is restored. This later ruling changes ADR-0048 from the brief’s requested Proposed status to Accepted.

Rulings on the user docs, layout and terms (2026-10-04 evening, operator)

Topic Ruling
User docs voice The pages under Docs are written as if Mesh 1.0 were released. No “exists today”, no milestone notes, no “not decided yet” callouts. Each page carries one line saying Mesh is not released yet. Contributor material (open decisions, what exists, findings) moves to Architecture. Replaces the “live spec with warnings” wording of the earlier row; the practice (docs before code) stands.
Introduction The intro shows a full todo.mx and, with graphics, what each part gives the user. Value first, not the generated code.
Generated folder Named .mesh, committed, guarded, marked linguist-generated. Users should not have to care about it; it is not hidden from them. Less weight on “what is generated” in the docs.
Import specifier Generated code is imported as #mesh (package.json imports), never by folder path.
Term resource becomes entity (overrides “copy Ash” for this word).
Project structure .mx files live under src/domain/<domain>/; migrations/ and .mesh/ at the root; src/extensions/ for project-local extensions. Details (where actor.ts and the program live, domain derived from the folder) pending. (Superseded on the two details: the file is src/context.ts, and the domain is the folder itself.)
Domain and grouping Deviate from Ash: an app (or package) has one domain, src/domain/. Inside it, entities are grouped in folders; the folder is the group, and no attribute repeats it in the file. The group is called a module (src/domain/accounts/ is the accounts module). (Superseded on the last sentence by the next row: there is no scope and no nested context bag.)
Hold All development is on hold until the operator approves the user docs. “A better DX for users and agents is the only chance Mesh has of making it.” Only the docs rewrite proceeds.
Action context The second argument of every action is the action context: createTodo(input, context). Its type, ActionContext, is one flat object the user declares once by declaration merging in src/context.ts. actor is the one key Mesh reads; every other key (tenant, locale, …) is the user’s. No scope, no nested context bag, no Register interface. An extension that needs a key states which one it reads; a clash is a build error (this settles tenant placement). In .mx functions: the record, actor as a shortcut, and context. Replaces “scope {actor, context}” in ADR-0007 and ADR-0047.
npm organisation meshfw, at least for now (mesh is taken on npm). Packages are @meshfw/* (@meshfw/cli, @meshfw/runtime, …); the product is still called Mesh and the binary mesh. Registered by the operator on 2026-10-04. ADR-0040 (package names) follows in the rename task.

Entity file syntax (2026-10-05, operator)

Supersedes the vocabulary rulings above where they differ (“copy Ash” no longer holds: the
vocabulary is Mesh’s own, informed by Ash). Depends on MX features the operator is adding:
#id after a space, :val sugar, unknown-child pattern mapping (escape hatch only).

Topic Ruling
Principle Rely on existing MX syntax wherever possible; minimise the rules a user must remember; exactly one way to write each thing. Pattern mapping of unknown tags is an escape hatch, not the basis of the syntax.
Line shape Every declaration is kind #name options: the tag is the kind, #name (the id shorthand) names it. Names are unique within their scope; Mesh checks that. One sigil only; :name is not used in entity files.
Entity entity #Invoice table="invoices".
Attributes The type is the tag: uuid #id primary-key, string #number unique, enum #status values=[...] default="draft", timestamp #insertedAt on="create". Required by default; nullable marks the exception.
Relationships The destination is the tag’s value: belongs-to=Customer #customer, has-many=InvoiceLine #lines, has-one=Payment #payment.
Computed fields One computed section replaces calculations and aggregates. A calculation is a typed field with a method body: boolean #isOverdue({ self }) { return ... }. A rollup is count #lineCount of="lines", sum #total of="lines.amount". of takes a path string, checked at build time against generated path types; a function form is allowed where a path cannot express it.
The record Functions receive the record as self (fixed key), beside actor, input, context. Not a name derived from the entity.
Actions Written actions are type #name ..., always named. actions auto=["read", "destroy"] generates the plain action of each listed type, named after the type and primary. auto replaces defaults. Bare read / destroy lines are not valid.
Arguments A nested arguments section with the attribute line shape: datetime #paidAt.

Entity file syntax, continued (2026-10-05 morning, operator)

Topic Ruling
File extension Entity and step files end in .mesh.mx (invoice.mesh.mx). Not .mesh: a search for “mesh language” finds another project.
Policies policy #name with scope attributes actions=[...] (action names) and types=[...] (action types); neither means every action. Checks authorize-if / forbid-if. Every policy covering an action must pass; none covering it means forbidden. No bypass in v1 (it fails open and is order-dependent); write isStaff(actor) || ... with a helper.
Automatic actions actions auto=["read", "destroy"] only generates the plain actions. Which action Mesh uses when it acts on its own is a separate attribute with a modifier: on:load="visible". Without it, the job falls back to the auto action of the type it needs; neither present is a build error. v1 key: load.
Steps (do) The word change is gone. An action’s steps go in a do block, run in written order; the tag is the kind of step. Planned kinds: set (child lines #field=value, a plain value or a one-expression function), when=cond with nested steps (not if: MX treats it as control flow), load=[...], run({ self, input, actor }) { ... } (one-off plain code inside the transaction; makes the action non-atomic), later lock="version", relate=..., after-commit(...) { }. v1: set, when, load, run.
Reusable steps Defined in MX, one file per step (step #slugify with an options section and a body), under src/domain/; Mesh derives the tag’s contract from the definition, and entity files use it as a tag: slugify from="title" to="slug". Extensions contribute steps the same way. Planned now, built later.
Validations Shape of one field (length, pattern, range) goes on the attribute or argument line. Rules of an action go in a validate block: require=[...] and check :name [ that=fn code=... message=... ]; when nests. The name is a label (:name sugar), code is the caller-facing code (string or number). validate runs before do and sees the stored record plus input (replaces the lead’s earlier “sees the record after the changes”).
Shared body actions > always [types=... actions=...] takes the same body as an action (validate, do) and applies it to every action in scope. Replaces Ash’s top-level changes and validations.
Expressions Translated expressions are one-expression arrows; a block body is never translated. Their parameter types expose only what translates; unsupported constructs are diagnosed in the editor through the contracts’ analyze hook. Before building the translator, research whether an existing project lets one write ordinary TypeScript expressions bound to a data model and run them both in memory and as SQL (operator, 2026-10-05); if none fits, Mesh implements it. A raw-SQL escape hatch (like Ash’s fragment) is planned, not in v1.
Confirmed (2026-10-05) Attributes are required unless marked nullable. A check runs only before the steps. No increment step for now: set with an expression covers it (to revisit: which further declared steps would read better). v1 steps are set, when, load, run; lock, relate, after-commit and reusable steps are planned and not shown to users. A create policy sees the proposed record.
Generators Mesh’s generators become Jig templates (the operator’s template engine for code generation). mesh export generators copies them into the project; a project template overrides Mesh’s own per template, when it exists. Emitters split in two: TypeScript computes a typed view of the model, the template only renders it.
Direct dependencies A project depends directly on the validation library, drizzle-orm and @opentelemetry/api, for now. The validation library is to be re-chosen: research alternatives to Zod that are more readable and closer to TypeScript and pair well with Drizzle; pick the best.

Decisions delegated to the lead (2026-10-05)

The operator, before going offline: “Make all the decisions for me and record them.” He expects the user
docs and the contributor docs fully updated with everything above. Decisions taken under that mandate
(he may overrule any):

Topic Decision
Validation library Zod 4 stays for v1 (operator agreed). Research: notes/research/10-validation-library.md recommends ArkType 2 on readability; rejected for Mesh because nobody hand-writes validators, its measured costs (type-check time, startup) grow with the number of entities, and it has one maintainer. Switching later is one emitter behind Standard Schema, or one overridden template.
Entity without policies Every action of an entity with no policies section is forbidden (fail closed), by the same rule as “an action no policy covers is forbidden”. The error says which policy is missing and how to write an open one.
Policies are core policies is a section of the entity file, so authorization is part of core, not an extension. The ext-policies package and the policies() entry in mesh.config.ts disappear. Supersedes ADR-0036’s packaging.
Generated folder Stays .mesh/ (the rename to .mesh-out was only considered together with a .mesh file extension, which was rejected).
MX host Mesh ships an MX host package named mesh on the data target, so .mesh.mx resolves in MX tooling (MX decision 148). Until MX’s side lands, files run through MX tooling stay .mx.
Order of work after approval (1) realignment task: syntax v2, entity, .mesh/#mesh, src/domain, ActionContext, @meshfw/*, policies in core, across contracts, model, compiler, CLI, example; (2) Jig templates for the existing emitters; (3) M2 resumes.
Architecture records Past decision records are not rewritten. New records are added for the new decisions and the old ones are marked superseded or amended, each linking to its successor.
Expression language Mesh implements its own (lead, delegated). Research notes/research/09-expression-language.md (fact-check in progress, 2026-10-05) found no project that reads ordinary TypeScript arrows bound to a data model and both evaluates them in memory and emits SQL; the closest, tinqer, is SQL-only and tiny. Designs to copy: one expression tree with two interpreters (memory and SQL) as in CASL/ucast and Remult; a small enumerated node vocabulary with an allow-list of functions; relationship traversal rewritten into joins in a pass before SQL generation; a declared supported-subset check with source positions; parameters only, never closures; policies folded into the query.

MX highlighting on the docs site (2026-10-05, agreed with the MX lead; MX decision 150)

Question Decision Why
How does the Mesh docs site highlight mx fences? With MX’s own highlighter: the tree-sitter grammar @mxlang/tree-sitter-mx run at docs build time through web-tree-sitter 0.26.9. Shiki stays for every other language. MX has one grammar by operator ruling (tree-sitter); no TextMate grammar will exist, so Shiki can never highlight MX itself. Shiki’s Marko grammar does not know :label, #id after a space or the data-target forms.
How is it shared before the @mxlang packages are published? Mesh vendors the built highlighter under apps/docs/plugins/mx/ (plugin module, tree-sitter-mx.wasm, the two query files, the TypeScript grammar wasm and its highlights), with a header naming the MX commit. The wasm files are committed so the Coolify Docker build needs no tree-sitter CLI. The docs image is built from the public repo and cannot reach the MX checkout.
And after? When @mxlang/tree-sitter-mx is published (subpath @mxlang/tree-sitter-mx/docmd), Mesh imports it and deletes the vendored copy. One source for the grammar.
When is the Mesh task done? After the docs/syntax-v2 branch merges, as one docs task; it must probe a regex-literal attribute value, which MX did not test. Both touch apps/docs; MX asked for the regex check.
Rejected A @mxlang/textmate package, a Mesh-specific grammar. Nothing to put in the first; Mesh adds tag names, not syntax.

Open, operator’s call: a private registry (Verdaccio on netcup, lead’s recommendation over GitHub
Packages) to share unpublished @mxlang and @meshfw packages. It needs a Coolify permission the
lead does not have.

Expression language, after the fact-check (2026-10-05, lead under delegation)

The fact-check of notes/research/09-expression-language.md (review file
notes/research/reviews/09-expression-language-review.md: 47 claims confirmed, 15 corrected,
7 not verifiable) found one project the research missed: Greffon
(https://github.com/PhenX/Greffon). It captures TypeScript lambdas at build time and runs one
expression tree in memory and as Postgres or SQLite SQL, which is the design Mesh chose. It was
created on 2026-08-14, has no stars and four weekly downloads; its docs were read, it was not run.

Question Decision Why
Does Mesh still implement its own expression language? Yes. Greffon is seven weeks old with no users; Mesh cannot put its core on it. The research conclusion changes from “no project” to “no established project”.
What does Mesh do with Greffon? The dev who designs the expression compiler (M3) reads Greffon’s source and docs first and reports what to copy and whether depending on it later is realistic. Same design, so its choices and mistakes are free lessons.

Rulings on the contributor-docs author’s choices (2026-10-05, lead under delegation)

The author of PR #24 made eleven choices where the rulings were silent
(notes/team-lead-2026-10-04/reports/arch-v2.md, “Decisions I made”). The lead accepts them, pending
the independent review; the operator may overrule any of them.

Question Decision Why
Is there a require-atomic flag? No. The build infers whether an action can run as one statement (ADR-0054). The reference file has no such flag and run is non-atomic by definition; a flag is one more rule to remember. An opt-in guarantee can be added later without breaking files.
What is self in a create’s validate? The record built from input and defaults. Same as the create policy ruling (it sees the proposed record).
What does a step in do see? self as the earlier steps left it. Steps run in written order; anything else surprises.
What does load=[...] do? Loads relationships or computed fields onto the returned record. Ash’s meaning; the name says it.
When does always run? Before the action’s own body, in written order. Shared rules first, specific ones after.
May a translated expression call helpers? Yes, when the call does not read self: it is evaluated once in memory and bound as an SQL parameter. Policies need isStaff(actor) and today(); research 09 shows this is how the established tools do it.
How do checks inside one policy combine? In order; the first that applies decides; a policy where nothing decides fails. Every policy that covers the action must pass. Ruling 6 (“ordered allow/deny”) and Ash’s semantics.
Where does the Greffon reading go? First task of the expressions milestone, which is M4 in the current roadmap (an earlier line here said M3, the old numbering). Numbering only.

Rulings on the user-docs author’s choices (2026-10-05, lead under delegation)

The author of PR #25 made ten choices the syntax did not cover (notes/team-lead-2026-10-04/reports/docs-v2.md).
The operator may overrule any of them.

Question Decision Why
What attribute does belongs-to=List #list create? listId: the relationship’s name plus Id. The user never writes the foreign key; the name is predictable from the line.
How is a relationship made optional? nullable, the same word as on an attribute. One word for one idea.
Where does paging go? In the caller’s input (limit, offset). A cap declared on the entity is planned, not v1. The v1 steps do not page.
Which attribute types exist? uuid, string, integer, float, decimal, boolean, enum, date, datetime, timestamp. The reference file had no whole-number type; using decimal for counts is wrong for storage and for the generated TypeScript type.
What do mesh inspect and mesh explain take? The entity’s name as written: mesh inspect Todo, mesh explain Todo complete. The entity is #Todo.
What does a failed check put on the error? InvalidInputError.code is always invalid_input; each issue carries the check’s label, its declared code, path, message and position. Several checks can fail at once; an error-level code taken from one of them is ambiguous.
How is always scoped? types= and actions=, as on a policy. One spelling.
Must sections be in a fixed order? No. Only the order of lines inside a section matters. Fewer rules.
Which rollups exist? count, sum, avg, min, max. The standard five; an open-ended list in a reference page is not a reference.
Does the Entities page open with “the eleven rules”? No. The rules live in their sections; the page ends with a short table. The eleven rules were the lead’s briefing device. A wall of rules on the first screen contradicts “few rules to remember”.

Rulings after the review of the user docs (2026-10-05, lead under delegation)

The independent review of PR #25 (notes/team-lead/reviews/pr25-docs-v2.md) found that the reference
file and the rules contradicted each other in five places. These rulings settle them; each one changes
or sharpens an earlier row, named in the last column. The operator may overrule any of them, and the
first three change behaviour he will see in every entity file.

Question Decision Why Replaces
What is self inside validate? The record with the caller’s accepted input applied: stored values for every field the caller did not send, the sent value for each accepted field. On a create, the stored values are the defaults. Nothing from do has run. input is still there for arguments. Reading a field’s value from before the change is planned, not v1. One rule for create and update. A cross-field check (self.dueOn >= self.issuedOn) and a state check (self.status === "sent") both read naturally. With self as the stored record only, every cross-field check on an update would have to merge input by hand. Sharpens “sees stored record + input” and the create row above.
What is self elsewhere? In a policy: the row for a read, the stored record for an update or destroy, the proposed record for a create. In a do step: the record as earlier steps left it. In a computed field: the loaded record. The reader needs one table. New.
How do the checks inside one policy combine? Without order. A policy passes when none of its forbid-if is true and, if it has any authorize-if, at least one is true. Every policy that covers the action must pass. The reference file’s policy #neverDestroyPaid has only a forbid-if; under “first check that applies decides, nothing decides fails” it would forbid every destroy. Order-dependence is also the reason bypass was dropped. An exemption is written in the condition (forbid-if=({ self, actor }) => self.status === "paid" && !isStaff(actor)). Replaces the row “How do checks inside one policy combine?” above; narrows Ruling 6’s word “ordered” (kept: allow/deny checks, read policies as filters, breakdown output).
Which functions does Mesh translate to SQL? A function whose body is one expression: an arrow (...) => expression, or a method body that is a single return expression. Anything else is plain code and runs in memory only. Using a plain-code computed field in a filter, a sort, a policy or another translated expression is a build error that names the field. The reference file’s boolean #isOverdue({ self }) { return ... } is used by a filter; “block body is never translated” made the reference fail its own build. Sharpens rule 6.
Where do rules about one field go? On the attribute line, always: min, max (length for a string, value for a number), match. A check is only for rules across fields or about stored state. The reference file’s amountNotNegative check becomes decimal #amount min=0, and its always example becomes a cross-field check. “Never two ways to write the same thing.” Fixes the reference file.
What does on:load name? A read action of this entity that exists; Mesh uses it when the entity is loaded through a relationship. Without it, the auto read. Naming a read that does not exist is a build error. The pages explained it backwards. Sharpens rule 7.
Do accepted fields have to be sent? On a create, every accepted field that is required and has no default; on an update, none. Standard; it was unstated. New.
Where do an action’s arguments go? In the same input object as accepted fields; names may not collide (build error). One input object per call. New.
Question Decision Why Replaces
What if a one-expression computed field cannot be translated (it calls a helper on self, such as formatMoney(self.total))? It is not an error. A computed field is translated when its body is one expression Mesh can translate; otherwise it runs in memory. The build error comes only where SQL is required: a filter, a sort, a policy, or a translated expression that uses that field. The error names the field and the part that could not be translated. mesh explain shows which computed fields are translated. Nobody writes a second statement to opt out. Without this, string #label({ self }) { return formatMoney(self.total) } fails the build and the only fix is an artificial two-statement body. Sharpens “Which functions does Mesh translate to SQL?” above.
And an untranslatable one-expression body in a check, a when or a set value? Not an error either. It runs in memory, which makes the action read the record first and then write (not one statement); mesh explain says so and names the expression that caused it. It is the same rule as the inferred write strategy (no require-atomic): what translates folds into the statement, what does not makes the action read-then-write. An error here would contradict it. Completes the row above.

Rulings after the review of the contributor docs (2026-10-05, lead under delegation)

Review file: notes/team-lead/reviews/pr24-arch-v2.md (verdict: ready after fixes).

Question Decision Why
When does an action run as one statement in v1? A create always does (one INSERT). An update or destroy does when its check and when conditions read only input, actor and context, and its set values translate. A check or when that reads self makes the action read the row first (locked, in the same transaction) and then write. Folding a translated self condition into the statement’s WHERE is after v1 (ADR-0044). self in validate is the record with the input applied, and on an update any accepted field may be unsent, so reading self can need the stored row. One criterion, stated once, used by ADR-0054, the lifecycle page and the roadmap.
What may a translated expression reference? Its parameters, registered functions, and calls to imported pure functions that do not read self (evaluated once, bound as a parameter). A bare captured value (a variable from the file) is a build error. The two sentences in the pages contradicted each other; policies need isStaff(actor) and today().
The reference file’s check :invoiceHasNoLines with self.lineCount > 0 Renamed invoiceHasLines. The label said the opposite of the condition.
How is ActionContext declared and typed? @meshfw/runtime exports an empty interface ActionContext {}. The project adds its keys, actor included, by declaration merging in src/context.ts. Generated functions take context: ActionContext; the parameter is optional when the merged interface has no required key. In rules, actor has the type the project declared, or unknown when it declared none. Declaring actor in the runtime’s interface would make the project’s own declaration a duplicate-property error.
Who decided the details of “one domain” (config key domain: "src/domain", names unique across modules, entity files import only relative files inside the domain folder)? The lead, under delegation; the record must say so. The operator ruled the model, not these details.

Rulings after the second review of the user docs (2026-10-05, lead under delegation)

Review file: notes/team-lead/reviews/pr25-docs-v2-pass2.md (verdict: ready after fixes).

Question Decision Why
Where is accept written? On the action’s own line, always: create #create accept=["title", "listId"]. Never on a line of its own under the action. One way to write it; a line under an action is a section or a step.
What message and code does a rule on an attribute line produce? Standard ones, fixed by Mesh: for example min=1 on a string gives code too_short and the message “must be at least 1 character”. A custom message or code for a one-field rule is planned, not v1. The pages printed a message no file declared. Letting a check restate the rule to change its message would be a second way to write it.
When is a value an argument and when is it accepted? A value stored in a field as sent is accepted (accept=[...]). An argument is an input the action needs that is not stored as sent. The reference file’s update #pay therefore uses accept=["paidAt"]; the arguments example is update #applyDiscount with decimal #percent min=0 max=100 and set #amount=({ self, input }) => self.amount * (1 - input.percent / 100). The reference broke the rule the page states.
How is a check labelled? With the rule that must hold, not the failure: invoiceIsSent, invoiceHasLines, dueAfterIssue, notDoneYet. A label that names the failure reads as its opposite next to the condition.
May run call a function that has effects? Yes. “Imported helpers must be pure” applies to translated expressions only; run is plain code. The two rules were stated as one.
May a rollup filter the rows it counts? Not in v1; planned. The tutorial pointed the reader at it.

Open with the operator: the computed-field method form (2026-10-05, raised with the MX lead)

Probe of MX 41da2fde against syntax v2: every form parses with the tree Mesh needs except
boolean #isOverdue({ self }) { ... }. MX decision 146 says a name sugar in attribute position takes no
value, and a method is a value. The glued form boolean#isOverdue({ self }) { ... } parses. The MX lead
puts two options to the operator: (A) keep 146 strict, Mesh changes the form; (B) amend 146 so a sugar
immediately followed by =value or (params) { body } supplies the tag’s default value, at the cost of
the :x=1 typo guard. Both leads recommend B. The published Mesh syntax is not changed until he rules.
No form of syntax v2 depends on MX’s patched htmljs-parser (MX decision 151).

Ruled by the operator on 2026-10-05, as relayed by the MX lead (not heard by the Mesh lead directly):
kind #name(params) { body } is valid; a sugar immediately followed by (params) { body } or =value
sets the tag’s default attribute. Mesh’s published syntax stands; MX implements it as decision 146 PR 4.
Also relayed: Verdaccio on netcup is approved (MX decision 153) for @mxlang/* pre-release packages, and
the parser patch ships as forks @mxlang/htmljs-parser and @mxlang/marko-compiler (MX decision 152). The
Mesh lead has not created the registry: its Coolify access is limited to the mesh-docs app, and a relayed
ruling does not widen it.

Pre-release registry (2026-10-05, operator)

Verdaccio at https://npm.saulo.tech, approved by the operator (“go ahead with verdaccio. hostname is good.
make sure only we can publish (anyone can download)”). Set up and tested the same day; details in
notes/infra/verdaccio.md. Lead’s choices within that: scopes @mxlang and @meshfw only; one publish
account; no self-registration; no uplink to npmjs (so it cannot be used as an open proxy, and projects
scope it instead of making it their default registry); its own Coolify project npm-registry.

Open with the operator: MX atoms and the Mesh syntax (2026-10-05, heads-up from the MX lead)

MX decision 156 (approved by the operator, not implemented): :name in a value position is an atom, a
name that represents itself, distinct from a string in the tree parseData returns. A contract can type
an attribute as one of a fixed set of atoms, or as a reference to something declared elsewhere in the
file (an attribute, an action), with errors, completion and rename coming from MX. The MX lead relays
that the operator prefers :id to #id for Mesh. That preference did not reach the Mesh lead directly,
so nothing in the published syntax changes until the operator rules. What a ruling would touch:

Today (published) With atoms Kind
uuid #id primary-key, entity #Invoice, update #pay, policy #owner uuid :id primary-key, … the name of a declaration
accept=["title", "listId"], require=[...], load=["customer"], sort=["insertedAt"] accept=[:title, :listId] reference to an attribute or relationship
auto=["read", "destroy"], types=["create", "update"], on="create" auto=[:read, :destroy] one of a fixed set
actions=["pay"], on:load="visible" actions=[:pay], on:load=:visible reference to an action
enum #status values=["draft", "sent"] default="draft" values=[:draft, :sent] default=:draft the lead’s view: yes, they are names
check :invoiceIsSent [...] unchanged already a label
of="lines.amount", code="invalid_state", message="...", table="invoices" unchanged text or a path, not a name

Questions for him: (1) :name or #name for declarations (one of them only); if :name, the label on
check and the name of a declaration become the same notation, which is simpler. (2) Atoms for every
reference and fixed-set value, with strings then an error there. (3) Enum values as atoms. The lead
recommends yes to all three once MX emits atom nodes: the user learns one notation for “a name”, the
editor completes and checks every reference, and a typo in accept becomes a positioned error.
Cost: every mx sample on the docs site and ADR-0050 are rewritten once more, before any code depends
on the syntax.

Ruled by the operator on 2026-10-05 (evening), on the atoms questions above: (1) yes, declarations are
written :name, not #name (uuid :id primary-key, entity :Invoice, update :pay); (3) yes, enum
values are atoms (values=[:draft, :sent] default=:draft). (2), atoms required for every reference and
fixed-set value with a string there an error, is open: he asked what happens when a user wants to pass
the value from a variable; the lead answered that Mesh never evaluates an entity file, so a variable is
already rejected there, and recommends yes. Nothing is rewritten until MX’s parseData emits atom nodes.

MX atoms: what MX agreed to provide (2026-10-05, MX decision 156 addendum 1)

The Mesh lead reviewed MX’s atoms design record (mxlang 516cfdaf) and sent nine findings; the MX lead
accepted all nine. What Mesh can rely on once it is built:

  • An atom inside any expression is a StringLiteral node carrying extra.mxAtom = { span }, as public
    API, so the SQL translator can tell :sent from "sent".
  • A name given by the sugar (string :title, and :status="paid" under set) arrives from parseData
    as an attribute of kind atom; a contract can type name as an atom or as a reference, and a quoted
    name="title" is then an error.
  • { type: "atom" } accepts any name; values restricts it; an optional pattern constrains its form.
  • References resolve in two phases (collect declarations, including those added from analyze with
    ctx.declare(kind, name, { span, scope }), then check). A declaration states how far it is visible:
    declares: { kind, from: "id" | "name", scope?: <ancestor tag> }, default the file’s root tag;
    resolution is innermost scope first.
  • A reference may accept several kinds; duplicate declarations are a core error; the same kind declared
    by two extensions merges; an atom where a string is declared, or the reverse, is an error.

Still Mesh’s own checks: references across files, on:load naming a read action, an enum’s default
being one of its values, and an attribute and a computed field not sharing a name.

Rulings of 2026-10-05, late evening (operator)

Question Decision
MX’s interim parser bundle, so the next @mxlang alpha accepts atoms and a :name after a value Yes (answered to the Mesh lead, relayed to the MX lead as such). Mesh’s realignment is then one step on that alpha.
Static tree plus compiled module as Mesh’s design The target, but not now: only when MX2 exists. Until then the current plan stands (Mesh reads the tree and emits the code).
Update the stale space-root CLAUDE.md Yes.
Are the user docs approved? Not yet. They must first be rewritten from #name to :name and atoms; the home page must be beautiful; the Introduction’s file must be shown uninterrupted, with markers in the code and the notes appearing on hover, with a fast, good animation.

Lead’s reading of the still-open atoms question (2): atoms are required in every reference and fixed-set
position and a variable there is an error. The operator’s “yes” to atoms in the docs and his deferral of
the compiled module to MX2 leave no way to resolve a variable before then. Reference:
notes/team-lead-2026-10-04/briefs/syntax-v3.md, including three choices the lead made inside the
ruling (a relationship’s destination is an atom, of= stays a string, error codes stay strings).

Rulings after the review of the atoms rewrite (2026-10-05, lead under delegation)

Review file: notes/team-lead/reviews/pr31-docs-atoms.md (verdict: ready after fixes).

Question Decision Why
How does an entity file say a read is sorted, and descending? A sort section under the read, one line per field, in order: asc :dueOn, desc :insertedAt. No sort=[...] option, no - prefix. The caller’s TypeScript input is unchanged. The pages had -:dueOn, an expression on a name, where the same page says expressions are errors. A list plus a second option naming the descending fields would repeat names. A line asc :name is the form every other line in the file has. The reference file’s sort=[:dueOn] changes with it; the operator may prefer to keep the option and say so.
What may a function in an entity file use? It is ordinary TypeScript. A translated one (a filter, a sort key, a policy, and any one-expression body Mesh translates) may use its parameters, registered functions and calls to imported pure functions that do not read self; a run body may use anything. “Functions may use anything” contradicted the translation ruling.
Who checks a reference inside one file (accept=[:titel])? MX, from Mesh’s contracts (MX decision 156 addendum 1). Mesh’s own checks are the ones across files and the four listed in “MX atoms: what MX agreed to provide”. ADR-0066 had called in-file references Mesh’s check.
Do extensions’ options take atoms too? Yes: an option that names fields or actions takes atoms (audit fields=[:status, :amount]). Same rule everywhere.

Positioning and the home page (2026-10-05, operator)

  • Mesh “is for any kind of app as it builds the core domain and business logic as a module that users
    can connect to anything”. The home page must not narrow it to backends.
  • The headline “Describe it once. Call a function.” was unclear and undersold Mesh. Working headline,
    from his “Describe your app once (or your domain if app is too much)”: “Describe your domain once.
    Mesh builds the rest.” (lead’s pick of the two; he has not confirmed it).
  • The right side of the build seam shows one box per thing Mesh builds, as a flow diagram (React Flow if
    docmd uses React; otherwise an island with the lighter of React Flow and Svelte Flow).
  • The home page is full width, with no sidebar.
  • AI agents get a section of their own, carrying “Ready for your agent” and “The less your agent writes,
    the less it can get wrong”: the agent writes one small file, tested deterministic code does the rest,
    which means fewer mistakes, easier review and fewer tokens. “Applications with an AI agent in them”
    was confusing and goes.
  • Introduction figure: the note appears in a box floating over the code block at its right; on a phone
    it slides in from the bottom or top in portrait and from the right in landscape.

MX facts for the realignment and the MX host package (2026-10-06, MX lead)

  • MX alpha.8: parseData never throws and reports every error, earliest first; DataExpr.node is
    Expression | null (narrow before use); option imports: "pass" | "reject".
  • MX main e389c383d (alpha.9 to follow): the target descriptor takes an optional builtOn?: string.
    The Mesh host package (ADR-0051, MX decision 148) must declare builtOn: "data": it tells MX the
    host extends the data target, which turns on data’s config checks and mx.data.defaultTag for
    .mesh.mx files in mx-tsc, the language server, the TS plugin and the Vite plugin. The name must be
    a registered target; the host’s own load() still runs; where the host’s own namespace key and
    mx.data.<key> differ, the host’s wins with a positioned warning.
  • The alpha.8 error for a modifier on a bound attribute (v:fn:=q) is reversed in alpha.9 (operator’s
    decision on the MX side): it is Marko’s refinement again, with an additive field on data’s bound
    attribute. Mesh uses neither form.

MX decisions 182 to 185: Mesh is a vocabulary, and the 1.0 acceptance test (2026-10-08, relayed by the MX lead)

Source: notes/mx/language-extensions-for-mesh.md (MX docs design-notes/language-extensions/, MX PR #425).

  • Mesh 1.0 acceptance (MX decision 185, operator, 2026-10-07 23:45): Mesh’s first release is done when
    the Hyper engine can be ported to it: the Elixir/Ash version at ~/work/hyper/engine/code/ex
    (svallory/hyper-engine-ex, parity reference of the D8 spike), with the wire protocol and conformance
    suite in ~/work/hyper/engine/code/spec. Every Mesh feature and every MX request is ranked by whether
    that port needs it. Consequence for the roadmap: after the realignment, the Jig port and M2, the
    milestone order is re-cut against what the Hyper port needs; recorded as a roadmap revision 5 item,
    written once the engine’s checkout is readable from the Mesh side (the path is Mac-only, owned by the
    operator; on netcup ~/work/hyper/hyper/worktrees/main is the hyper space).
  • What the mesh package carries (decisions 182 to 184; MX decision 148 stands, confirmed by the MX
    lead 2026-10-08 after a corrected note).
    Mesh ships the mesh host package registered through
    mx.host with builtOn: "data" (ADR-0051, ADR-0060 unchanged); .mesh.mx resolves in mx-tsc, the
    language server and Vite through that specifier today. Layer 1, today: the package carries the
    mx.contracts module, and Mesh calls parseData with customTags, structural: "reject",
    unknownTags: "reject". Layer 2, later: the same package gains a package.json#mx.syntax entry
    (syntax table plus lowerTrigger, afterLower, productName), project-scoped, where atoms and the
    :name sugar move; MX writes it and opens the PR against Mesh. Until then atoms and :name keep
    working in MX core.
  • What MX wants kept: packages/compiler/test/contracts.test.ts (the “31-tag fixture”: Mesh’s
    contracts as CustomTag objects through parseData; MX holds no copy) is the acceptance test for the
    layer-2 move. Tell the MX lead when the vocabulary changes (resource to entity, atoms everywhere) so
    the move is briefed against the new names; this is a step of the realignment task. When the
    syntax-table PR opens, Mesh reviews the trigger shapes for atoms and :name against real entity files
    before they freeze. MX publishes a fresh alpha once its attributeTags["*"] and transform-output PRs
    merge; the realignment pins that one.
  • Answers received: tag :Todo name sugar yields {"kind":"atom","name":"name","value":"Todo"};
    any-name children are children["*"] with pattern (decision 147), never defaultTag;
    attributeTags["*"] does not exist (MX gap 5, scheduled), declare each attribute tag by name;
    parseData reports every error and never throws (6b97b8efe) but returns no tree for a file with
    errors (MX gap 4, after the parser port’s core-error-recovery).
  • mx-tsc: Mesh does not invoke it; when it does, it runs under Bun. MX queued mx-tsc-bun-ts18003
    (dist bin fails TS18003 under bun) to land before that.

Operator rulings of 2026-10-08 (morning review of the home example)

  • Relationship lines are kind :name entity=Identifier (operator, 2026-10-08 08:39: “go”):
    belongs-to :list entity=List, has-many :lines entity=InvoiceLine, has-one :payment entity=Payment.
    Replaces belongs-to=:List :list (syntax v3 brief, line 62). Why: every other line in a file is
    kind :name options; the old form was the only one with a value before the name, and MX carried a
    parser rule just for it. entity= is the same attribute on all three kinds; of= stays a path string.
    belongs-to still creates the key column on this entity; has-one expects it on the other one.
  • An entity is referenced by a TypeScript identifier that the file imports, never by an atom and
    never by a qualified name (import { List } from "./list.mesh.mx"; entity=List). Rule: an atom is a
    name declared in this file or a value from a fixed set; an identifier is something imported. Reverses
    the v3 ruling that a destination is an atom (belongs-to=:Customer). Why: atoms resolved against a
    project-wide index collide across domain modules and give the editor nothing to jump to; imports use
    the MX mechanism that exists (parseData imports: "pass", the mesh host package) and make scope
    per file. The module stays the folder under src/domain/ (project-structure.md); no module= line,
    no top-level MyApp name, no fully qualified references (a second way).
  • Pending the operator’s ruling (same review): how an action declares its input; accept=[...] plus an
    arguments section is under challenge as two places for one thing. Decision 2 (relationship entity=)
    and the import rule go to the MX lead with the vocabulary notice of the realignment.
  • Idea parked, not ruled: a library of Concepts (string :email concept=:Email): notes/ideas/concepts.md.
  • MX decision 187 (operator, 2026-10-08 04:00, relayed by the MX lead 09:18): the target Mesh builds on
    is renamed data to tree (same thing: parseData, static tree, control flow as nodes or rejected).
    Package stays @mxlang/data. Lands in the next alpha with MX PRs #427/#428; the realignment then sets
    builtOn: "tree" on the mesh host package and mx.target: "tree" wherever "data" was. data is
    reserved for a future evaluated (input) => Tree target, after Mesh 1.0. Wait for the alpha
    announcement in notes/mx/updates.md.
  • MX decision 188 (operator, relayed by the MX lead 2026-10-08 20:36): Mesh does no work until its DX is
    fully defined; the operator says when Mesh is ready. Supersedes “on hold until the user docs are
    approved” as the gate: the docs approval is part of the DX definition, not the whole of it.
  • Question the DX work must answer (MX lead, same note): on the tree target, does Mesh give <if> and
    <for> a meaning of its own (engine-evaluated, declared as a layer-2 trigger or a contract), or does
    it want them evaluated at compile time (the future data target, after Mesh 1.0)? Today Mesh rejects
    control flow (structural: "reject"). Owed to the operator with the syntax rulings in progress
    (entity=, imports, input section, member references $name).
  • Operator’s order via the MX lead (2026-10-08 20:39): design Mesh against what MX will be, not what the
    code does today; notes/mx/language-extensions-for-mesh.md opens with the target state, gaps as
    footnotes. Consequence for the syntax rulings in progress: atoms, :name and any $name member
    sugar are Mesh’s own layer-2 triggers in the mesh package’s mx.syntax module (hooks
    lowerTrigger, lowerBlockTag, lowerFilter, afterLower, productName); MX core is Marko’s
    grammar plus the structural tags only. So “can MX parse $title on a line” is a question about what
    the SyntaxTable can express, which Mesh then declares, not a change MX core makes for Mesh.
    Mesh’s base target is tree; builtOn: "tree".
  • Member-reference sigil, exploration (operator and lead, 2026-10-08 evening; MX lead’s answer 20:47,
    against the target syntax table): a sigil for “a member of this entity” distinct from the :name
    declaration atom, in three positions: after a kind (asc &dueOn), at the start of a tagless line
    (&title under input, &amount=expr under set) and inside expressions (load=[&customer],
    () => &status !== :cancelled). @ dropped (attribute tags). MX: after a kind and inside
    expressions are covered by attributeTriggers and expressionTriggers (atoms are the first entry;
    standIn hands the TypeScript parser a same-width identifier; triggers arm at operand positions, so
    a & b stays an operator); a tagless line start is NOT in the table and needs a lineTriggers field
    (decision 182 addendum, S on the port, operator’s ruling). $: scriptlet at line start and steals
    every $-prefixed identifier in expressions, MX advises against. @.: expression and attribute
    position only. MX prefers &, claimed nowhere by core. Pending the operator: (i) lineTriggers,
    (ii) the sigil, (iii) whether self.x stays allowed beside &x in expressions.
  • Rulings (operator, 2026-10-08 23:17):
    1. lineTriggers goes into the MX syntax table (decision 182 addendum): a tagless line may start
      with a trigger, with or without =value.
    2. The member sigil is &. &name is a member of this entity (attribute, relationship, action,
      check, policy) in every position: after a kind (asc &dueOn), at a tagless line start (&title
      under input, &amount=expr under set), inside expressions (load=[&customer],
      () => &status !== :cancelled). :name declares and names fixed-set and enum values; an imported
      identifier is another entity. Replaces :name in reference positions everywhere (accept, load,
      sort, set, actions=[...] on a policy); types=[:create], on=:create, default=:draft stay
      atoms (values, not members).
    3. self.x stays legal beside &x inside expressions; &x lowers to self.x. Rule of thumb (not
      gospel): Mesh should not make legal syntax illegal.
    4. Action input: one input section, one line per field: &title takes a declared attribute or
      belongs-to as declared (no options allowed on that line; rules stay on the attribute); kind :name options declares an argument (attribute shape). accept=[...], the arguments section and
      require=[...] are gone. The section is input because expressions read ({ input }). A name
      declared twice in one input is a build error.
      Open, for the docs rewrite: an input line for a field with no record yet (create) and the
      policy/validate positions use the same &; set under do becomes &amount=....
  • MX recorded the above as decision 182 addendum 1 (MX lead, 2026-10-08 23:18): lineTriggers joins
    the syntax table (lands with lang-ext-syntax-table on the parser port); & is Mesh’s member sigil
    in all three positions, lowering to self.x; MX drops the “:name after a value” form. MX waits for
    the syntax v4 brief to brief the layer-2 move.
  • Correction to the sigil reasoning (MX lead, 2026-10-08 23:36; ruling unchanged): a scriptlet is $
    plus whitespace, so $title at line start was never a scriptlet, only a tag-name error. The real
    objection to $ is expression position: $x and $.x are legal TypeScript, and a trigger must not
    change the meaning of legal syntax (the operator’s rule applied to MX’s expression language). &x
    at an operand position and @.x are TypeScript syntax errors, so both qualify; @. would need a
    carve-out where core recognises @name; & needs none. Dispatch for the v4 brief: in a state with
    triggers the lexer tries the trigger’s anchored matcher first; a match wins and the default grammar
    never sees those characters; overlap with the default grammar is refused when the manifest’s table
    is validated, not at parse time; Mesh’s lowerTrigger/afterLower run after the parse.
  • Entity files are static (operator, 2026-10-08 23:54). No if/for as data anywhere: a
    condition is an attribute of the line it conditions (when= on a policy or a check) or lives in the
    expression; a rule over a has-many is an expression (&lines.every(...)). structural: "reject"
    stays; Mesh builds on tree and does not ask for the evaluated data target. <let> and <const>
    have no use (& names a member once); <define> is not control flow Mesh adopts: a reusable
    fragment, if the engine port needs one, is Mesh vocabulary (use :timestamps). A for over static
    data to save typing (address1..3) gets Mesh syntax if ever needed, not a loop. Why: Mesh already
    raises the abstraction level; an input-dependent tree on top is a footgun.
  • Docs rewrite ordered (operator, 23:54): update the user docs to the 2026-10-08 syntax and
    republish; the operator will have friends review the published docs. Reference for the rewrite:
    notes/team-lead-2026-10-04/briefs/syntax-v4.md. Docs show members as &name; self appears only
    when the whole record is passed to a function.
  • MX decision 187 addendum 2 (MX lead, 2026-10-08 23:56): Mesh builds on tree only; control flow
    stays rejected; no <let>/<const>/<define>; the data target stays reserved and unscheduled.
    Highlighting of layer-2 syntax (&, imports) will come from semantic tokens driven by the resolved
    syntax table or a Mesh-shipped grammar (MX item mesh-syntax-highlighting-route, after the table
    lands); unhighlighted is accepted for the docs review round. Reference for devs: briefs/syntax-v4.md.
  • input section name stands (MX lead, 2026-10-09 00:03). HTML void-ness is a per-target taglib option
    (openTagOnly), neutralised on the tree target for the 19 HTML names; parseData, the contracts and
    the language server (after data-target-tooling-dispatch) all accept input with children. Only the
    static @mxlang/tree-sitter-mx scanner (TAG_VOID) treats it as void, so the docs highlighter
    mis-nests input until the Mesh highlighting route exists (MX TODO mesh-syntax-highlighting-route,
    now carrying this case). The docs bridge it with one counted allowance.
  • MX gap raised (2026-10-09 00:21): on alpha.5, structural: "reject" refuses a // comment line
    (“the data tree is static; this file’s consumer does not evaluate comments”). Mesh allows comments
    (v3 rule 6) and mesh build uses structural: "reject", so a commented entity file would not
    build today. Asked MX for comments to pass on the tree target (or a comments option like
    imports); the realignment pins the answer. Confirmed: imports: "pass" already works on alpha.5
    at runtime, so Mesh passes it from the realignment on.
  • MX decision 131 addendum 5 (2026-10-09 00:23): comments are never structural; under structural: "reject" a // line stays in the tree as a Comment node and Mesh ignores it. No new option. Lands
    with the next alpha (tree-comments-not-structural) together with the tree rename. Realignment
    pins: comments pass, imports: "pass", structural: "reject", builtOn: "tree". Docs keep blanking
    leading comments until the alpha is announced in notes/mx/updates.md.

Docs review notes (operator, 2026-10-09 01:35) and the lead’s rulings for the docs-structure round

  • Package names (lead, checked on npmjs.org 2026-10-09: meshfw and create-mesh free, mesh taken):
    the CLI ships as the unscoped package meshfw with the binary mesh; the starter is
    create-mesh, so bun create mesh todo-app works (Bun resolves create-<name>). The rest of the
    framework stays under @meshfw/* (ADR-0060, amended: @meshfw/cli becomes meshfw). In a project
    meshfw is a dev dependency and the docs write mesh build; bunx mesh build runs the local binary
    without a global install; bun add -g meshfw gives a global mesh. Operator act owed: register
    meshfw and create-mesh on npm.
  • Quick start = requirements, installation, run. A Requirements section (Bun, git, an editor; one link
    per install page) with a callout holding a prompt a reader pastes into an agent to check and install
    them; bun create mesh todo-app; the starter is a minimal runnable app (it builds, pushes the schema,
    and one bun run demo script calls one action and prints the record), so “run it” proves the setup;
    a short “Use your domain” section (import from #mesh, call one function) pointing to the tutorial.
    Everything else that overlapped with the tutorial moves to the tutorial.
  • Introduction: show the import (import { createTodo } from "#mesh") and say the naming rule once:
    an action create on entity :Todo is the function createTodo (action name + entity name).
  • “Calling actions” becomes “Using your domain”, with three sections (one page for now, subpages when
    the integrations exist): from a command line (oclif), over HTTP (Elysia), in a web app. Web app
    framework (lead’s pick, operator may overrule): SolidStart: MX already targets a Solid host, the
    Bun/TypeScript crowd overlaps most with Solid’s, and its router/server-function model maps one to one
    onto “call a function”. Marko Run was the other candidate (MX’s lineage) and loses on community size;
    framework samples are (excerpt) fences (not type-checked) until the integrations exist.
  • Configuration page splits into “Configuration” (project) and “Command line” (the mesh reference).
    CLI installation lives in the Quick start; “The starter installs everything below” links to the tutorial.
  • Navigation: Introduction, Quick start, Working with AI agents, Tutorial, then a group Your first
    project
    : Project structure, Entities, Using your domain, Testing, Configuration; then Command line,
    Customising generated code.