Rulings of 2026-10-04
Rulings of 2026-10-04
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.contractsmodule from
core plus the enabled extensions, so extensions add tags without any MX change. Follows
ruling 5 (contributions only through declared points: a closedresource.childrenmust
accept children an extension contributes). Stated to the MX lead in
Mesh’s answers to MX onmx.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’sactor:option. Drop the actor-resolver contract, theactor-devpackage, 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/blogcall the generated handlers against SQLite. Notransport-cli, no exit codes, no--stdin, noworkercommand. publicand default-deny before policies (old D10, D6): no transport exists in v1, sopublichas nothing to filter yet, and theauthorization: "none"flag is wiring for nothing. Drop both from M2; deny-by-default arrives with the policies extension. Record whatpublicwill 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
publishexample 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
mainis 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.
publicattributes.publicis recorded in the model, as Ash recordspublic?, 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 pushandmesh migrateare contributed by the SQL adapters. The rule that MX is imported only by packages that declare tag contracts is checked byverifyfrom 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-ormand@opentelemetry/apifrom the user’s project. They must be application dependencies;mesh buildchecks that they resolve (quick start). - After the spike, the lead accepted ADR-0048, reported to the operator: pin
drizzle-orm@0.45.3anddrizzle-kit@0.31.11; usepushSQLiteSchemabehind 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.4has nodrizzle-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
StringLiteralnode carryingextra.mxAtom = { span }, as public
API, so the SQL translator can tell:sentfrom"sent". - A name given by the sugar (
string :title, and:status="paid"underset) arrives fromparseData
as an attribute of kindatom; a contract can typenameas an atom or as a reference, and a quoted
name="title"is then an error. { type: "atom" }accepts any name;valuesrestricts it; an optionalpatternconstrains its form.- References resolve in two phases (collect declarations, including those added from
analyzewith
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:
parseDatanever throws and reports every error, earliest first;DataExpr.nodeis
Expression | null(narrow before use); optionimports: "pass" | "reject". - MX main
e389c383d(alpha.9 to follow): the target descriptor takes an optionalbuiltOn?: string.
The Mesh host package (ADR-0051, MX decision 148) must declarebuiltOn: "data": it tells MX the
host extends the data target, which turns on data’s config checks andmx.data.defaultTagfor
.mesh.mxfiles in mx-tsc, the language server, the TS plugin and the Vite plugin. The name must be
a registered target; the host’s ownload()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/mainis the hyper space). - What the
meshpackage carries (decisions 182 to 184; MX decision 148 stands, confirmed by the MX
lead 2026-10-08 after a corrected note). Mesh ships themeshhost package registered through
mx.hostwithbuiltOn: "data"(ADR-0051, ADR-0060 unchanged);.mesh.mxresolves in mx-tsc, the
language server and Vite through that specifier today. Layer 1, today: the package carries the
mx.contractsmodule, and Mesh callsparseDatawithcustomTags,structural: "reject",
unknownTags: "reject". Layer 2, later: the same package gains apackage.json#mx.syntaxentry
(syntax table pluslowerTrigger,afterLower,productName), project-scoped, where atoms and the
:namesugar move; MX writes it and opens the PR against Mesh. Until then atoms and:namekeep
working in MX core. - What MX wants kept:
packages/compiler/test/contracts.test.ts(the “31-tag fixture”: Mesh’s
contracts asCustomTagobjects throughparseData; MX holds no copy) is the acceptance test for the
layer-2 move. Tell the MX lead when the vocabulary changes (resourcetoentity, 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:nameagainst real entity files
before they freeze. MX publishes a fresh alpha once itsattributeTags["*"]and transform-output PRs
merge; the realignment pins that one. - Answers received:
tag :Todoname sugar yields{"kind":"atom","name":"name","value":"Todo"};
any-name children arechildren["*"]withpattern(decision 147), neverdefaultTag;
attributeTags["*"]does not exist (MX gap 5, scheduled), declare each attribute tag by name;
parseDatareports every error and never throws (6b97b8efe) but returns no tree for a file with
errors (MX gap 4, after the parser port’score-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.
Replacesbelongs-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-tostill creates the key column on this entity;has-oneexpects 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 (parseDataimports: "pass", themeshhost package) and make scope
per file. The module stays the folder undersrc/domain/(project-structure.md); nomodule=line,
no top-levelMyAppname, no fully qualified references (a second way). - Pending the operator’s ruling (same review): how an action declares its input;
accept=[...]plus an
argumentssection is under challenge as two places for one thing. Decision 2 (relationshipentity=)
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 renameddatatotree(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 themeshhost package andmx.target: "tree"wherever"data"was.datais
reserved for a future evaluated(input) => Treetarget, after Mesh 1.0. Wait for the alpha
announcement innotes/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 futuredatatarget, after Mesh 1.0)? Today Mesh rejects
control flow (structural: "reject"). Owed to the operator with the syntax rulings in progress
(entity=, imports,inputsection, 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.mdopens with the target state, gaps as
footnotes. Consequence for the syntax rulings in progress: atoms,:nameand any$namemember
sugar are Mesh’s own layer-2 triggers in themeshpackage’smx.syntaxmodule (hooks
lowerTrigger,lowerBlockTag,lowerFilter,afterLower,productName); MX core is Marko’s
grammar plus the structural tags only. So “can MX parse$titleon a line” is a question about what
theSyntaxTablecan express, which Mesh then declares, not a change MX core makes for Mesh.
Mesh’s base target istree;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
(&titleunderinput,&amount=exprunderset) and inside expressions (load=[&customer],
() => &status !== :cancelled).@dropped (attribute tags). MX: after a kind and inside
expressions are covered byattributeTriggersandexpressionTriggers(atoms are the first entry;
standInhands the TypeScript parser a same-width identifier; triggers arm at operand positions, so
a & bstays an operator); a tagless line start is NOT in the table and needs alineTriggersfield
(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) whetherself.xstays allowed beside&xin expressions. - Rulings (operator, 2026-10-08 23:17):
lineTriggersgoes into the MX syntax table (decision 182 addendum): a tagless line may start
with a trigger, with or without=value.- The member sigil is
&.&nameis a member of this entity (attribute, relationship, action,
check, policy) in every position: after a kind (asc &dueOn), at a tagless line start (&title
underinput,&amount=exprunderset), inside expressions (load=[&customer],
() => &status !== :cancelled).:namedeclares and names fixed-set and enum values; an imported
identifier is another entity. Replaces:namein reference positions everywhere (accept,load,
sort,set,actions=[...]on a policy);types=[:create],on=:create,default=:draftstay
atoms (values, not members). self.xstays legal beside&xinside expressions;&xlowers toself.x. Rule of thumb (not
gospel): Mesh should not make legal syntax illegal.- Action input: one
inputsection, one line per field:&titletakes a declared attribute or
belongs-toas declared (no options allowed on that line; rules stay on the attribute);kind :name optionsdeclares an argument (attribute shape).accept=[...], theargumentssection and
require=[...]are gone. The section isinputbecause expressions read({ input }). A name
declared twice in oneinputis a build error.
Open, for the docs rewrite: aninputline for a field with no record yet (create) and the
policy/validatepositions use the same&;setunderdobecomes&amount=....
- MX recorded the above as decision 182 addendum 1 (MX lead, 2026-10-08 23:18):
lineTriggersjoins
the syntax table (lands withlang-ext-syntax-tableon the parser port);&is Mesh’s member sigil
in all three positions, lowering toself.x; MX drops the “:nameafter 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$titleat line start was never a scriptlet, only a tag-name error. The real
objection to$is expression position:$xand$.xare 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@.xare 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’slowerTrigger/afterLowerrun after the parse. - Entity files are static (operator, 2026-10-08 23:54). No
if/foras 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 ontreeand does not ask for the evaluateddatatarget.<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). Aforover 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;selfappears 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
treeonly; control flow
stays rejected; no<let>/<const>/<define>; thedatatarget 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 itemmesh-syntax-highlighting-route, after the table
lands); unhighlighted is accepted for the docs review round. Reference for devs:briefs/syntax-v4.md. inputsection 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 (afterdata-target-tooling-dispatch) all acceptinputwith children. Only the
static@mxlang/tree-sitter-mxscanner (TAG_VOID) treats it as void, so the docs highlighter
mis-nestsinputuntil the Mesh highlighting route exists (MX TODOmesh-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) andmesh buildusesstructural: "reject", so a commented entity file would not
build today. Asked MX for comments to pass on the tree target (or acommentsoption 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 aCommentnode and Mesh ignores it. No new option. Lands
with the next alpha (tree-comments-not-structural) together with thetreerename. Realignment
pins: comments pass,imports: "pass",structural: "reject",builtOn: "tree". Docs keep blanking
leading comments until the alpha is announced innotes/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:
meshfwandcreate-meshfree,meshtaken):
the CLI ships as the unscoped packagemeshfwwith the binarymesh; the starter is
create-mesh, sobun create mesh todo-appworks (Bun resolvescreate-<name>). The rest of the
framework stays under@meshfw/*(ADR-0060, amended:@meshfw/clibecomesmeshfw). In a project
meshfwis a dev dependency and the docs writemesh build;bunx mesh buildruns the local binary
without a global install;bun add -g meshfwgives a globalmesh. Operator act owed: register
meshfwandcreate-meshon 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 onebun run demoscript 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 actioncreateonentity :Todois the functioncreateTodo(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
meshreference).
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.