Ash to Mesh: where each Ash concept went

Ash to Mesh: where each Ash concept went

Updated: 2026-10-08. Mesh is modelled on Ash, the declarative framework for Elixir. Until 2026-10-05 Mesh’s vocabulary copied Ash’s DSL (ADR-0034, superseded). It is now Mesh’s own, informed by Ash (ADR-0049): what Ash calls a resource is an entity, and every declaration in an entity file is kind :name options (ADR-0050). This page tells an Ash user where each Ash concept went, and which have no equivalent yet.

Two spellings on this page

The code on main still reads the vocabulary of the M1 alignment, which copied Ash (resource, attribute="title" type="string", defaults, change, calculations, aggregates, policy=action_type("read")), until the realignment task (ADR-0064). Section 3 keeps that spelling in its own column, and its “Contract check” column, because a test in packages/compiler/test reads both to check the contracts on main (roadmap M1, acceptance test 7). The 110-row mapping and its fixture are historical, machine-checked and unchanged here. For current spelling, use Entities and ADR-0067; the realignment rewrites the mapping’s Mesh column when the code moves to v4.

0. How to read this page

  • Section 1 is the short version: the Ash constructs a typical resource uses, and the Mesh line for each.
  • Section 2 lists the Ash concepts Mesh has no equivalent for yet, and why.
  • Section 3 is the full mapping, 110 rows, one per Ash section, entity or option that Mesh v1 touches, with the source in the research for each Ash fact.
  • Section 4 lists where Mesh’s design deliberately differs from Ash, then keeps the record of the M1 alignment as history.
  • Sections 5 to 7 and the appendices are the M1 alignment’s exceptions, Ash lookups and fixture, kept because they record checked facts about Ash and describe the contracts on main.

In current files, :title declares a name, &title refers to a member and an imported identifier names another entity. Expressions use TypeScript plus Mesh’s member and atom spellings.

1. At a glance

Ash examples follow Ash features, section 12; the Mesh lines follow the reference file in ADR-0050.

Ash Mesh Record
defmodule MyApp.Blog.Post do use Ash.Resource, domain: MyApp.Blog entity :Post table="posts" in src/domain/blog/post.mesh.mx; the folder is the module ADR-0049, ADR-0057
postgres do table "posts" end table="posts" on the entity line ADR-0050
uuid_primary_key :id uuid :id primary-key ADR-0050
attribute :title, :string, allow_nil?: false string :title (required by default) ADR-0050
attribute :notes, :string (nullable by default) string :notes nullable ADR-0050
attribute :state, :atom, constraints: [one_of: [:draft, :published]] enum :state values=[:draft, :published] ADR-0050
constraints: [max_length: 2000], constraints: [min: 0] max=2000, min=0 on the line (never a check) ADR-0053
create_timestamp :inserted_at timestamp :insertedAt on=:create ADR-0050
belongs_to :author, MyApp.Accounts.User Import User by relative path, then belongs-to :author entity=User (nullable makes it optional) ADR-0050
has_many :comments, Comment Import Comment, then has-many :comments entity=Comment ADR-0050
calculate :excerpt, :string, expr(...) string :excerpt() { ... } in computed ADR-0050
count :comment_count, :comments count :commentCount of="comments" in computed ADR-0050
defaults [:read, :destroy] actions auto=[:read, :destroy] ADR-0052
update :publish do ... end update :publish ADR-0052
argument :paid_at, :utc_datetime input section: datetime :paidAt ADR-0067
change set_attribute(:state, :published) do then set then &state=:published ADR-0053
validate present(:title) string :title is required by default; input takes &title ADR-0067
a custom validation module with a message check :label [ that=fn code=... message=... ] ADR-0053
resource-level changes and validations always types=[...] under actions ADR-0053
require_atomic? false nothing: the build infers the write strategy ADR-0054
filter expr(state == :published) filter=() => &state === :published ADR-0056
^actor(:id), ^arg(:name), ^context(:key) actor.id, input.name, context.key ADR-0059
authorizers: [Ash.Policy.Authorizer] nothing: policies are core, and an entity without them forbids every action ADR-0055
policy action_type(:read) do authorize_if ... end policy :reads types=[:read] then authorize-if=... ADR-0055
bypass actor_attribute_equals(:admin, true) none: write isAdmin(actor) || ... in each policy ADR-0055
Ash.can? a generated can function per action ADR-0022
actor:, tenant:, context: options on a call one ActionContext argument: payInvoice(input, { actor, tenantId }) ADR-0059

2. Ash concepts with no Mesh equivalent yet

Grouped by why. Row numbers refer to section 3.

  • Not planned: generic actions (55), manual actions (61), many-to-many and join resources (40), NewType and embedded resources (35), bypass and policy groups (97, rejected because a passing bypass fails open and depends on order), a policy solver.
  • After v1: identities beyond unique on one attribute (78, 79), upserts (62), bulk actions (63), multitenancy as an extension (12, 109), relate for managing related records (47), after-commit and other hooks (71), a raw-SQL fragment (103, 104).
  • Not scheduled: the resource options of row 6, preparations (9, 70, 72), pipelines (11, 76), temporal resources (13), uuid_v7 and integer keys (16, 17), update_default (24), description, sensitive, source (26), selection and filterability options (28, 29), arrays (34), relationship options (42 to 46), action options (50, 58, 64, 65, 77), field policies (98), access types (99), authorize_unless and forbid_unless (94), calculation options and arguments (83, 84), aggregate options (88), ^ref (108).
  • Replaced by something different: public? (22, open in ADR-0035), require_atomic? (59, inferred), code_interface (7, generated functions), data_layer and authorizers (3, 5, configuration or core).

3. The mapping

This section preserves the M1 fixture’s syntax and the then-proposed v2 spelling as historical, machine-checked columns. It is not a current entity-file reference; use Entities and ADR-0067. The fixture fence and all table columns stay unchanged until realignment.

The “Contract check” column describes the M1 contracts on main, not syntax v2. It is machine-readable and is what the acceptance test reads (contracts.test.ts, describe “roadmap M1 acceptance test 7”). Every row whose status says “on main” carries one or more specs separated by ;, or an explicit n/a (reason) when the row is not about a tag or attribute (today only row 110, recorded deviation D37: an expression’s contents are not vocabulary); any other row carries -. The test pins the list of n/a rows, so a row cannot leave the check by being relabelled. The realignment task moves it and the test to the v4 spelling together (roadmap, Realignment). A spec is tag: tokens: a plain token is an attribute the contract declares and a clean fixture uses; >name is a child tag the contract declares and a clean fixture nests in that tag; attr=v1,v2 is an attribute that a clean fixture sets to each of those values. For policies, @action and @action_type require clean fixtures for a single string argument, a one-item list and a two-item list. A row marked “on main” whose cell does not parse fails the test, and so does a tag, attribute, child or value no clean fixture uses.

The “M1 spelling” column is what the contracts on main declare after the M1 alignment (“on main (aligned)” in the status column means it has a positive and a negative test). The “Mesh spelling (syntax v2)” column records the spelling proposed at the time (ADR-0050); it replaced a column that recorded the contracts before the M1 alignment (a3b52f4), which is in this page’s Git history. “none yet” means Mesh has no equivalent; section 2 lists them.

3.1 Resource level

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
1 use Ash.Resource, domain: (an option of the resource, a module) Ash features §1.1 after the table resource domain="blog" none: the folder under src/domain/ is the module (ADR-0057) on main; aligned (the value is a name, not a module) resource: value domain
2 Ash.Domain DSL: sections domain, resources, execution, authorization Ash features §1.3 none none: one domain per app, src/domain/ not in roadmap -
3 data_layer option (default Ash.DataLayer.Simple) Ash features §1.1 none in the resource file; the project configuration names the adapter (roadmap M1, “Project configuration”) none in the file: data: in mesh.config.ts not planned as a tag -
4 table, which Ash puts in the data layer’s own section, for example postgres do table "users" end Ash DSL and extensions §3.3 (line 666), §4 (line 750) resource table="posts" entity #Post table="posts" on main; exception X1 resource: table
5 authorizers, extensions, notifiers options of use Ash.Resource (the policy authorizer is opt-in per resource) Ash features §1.1, §6.8 project configuration, not a tag extensions: in mesh.config.ts; no authorizer option, policies are core M6, M8; exception X2 -
6 resource section options: description, base_filter, default_context, trace_name, short_name, plural_name, require_primary_key?, others (defaults: not in the research) Ash features §1.1 row 5 the Ash names in kebab-case none yet not in roadmap -
7 code_interface (generates callable functions) Ash features §1.1 row 4 none; generated action functions replace it (my reading of roadmap M2) none: the generated action functions not planned as a tag -
8 changes section (a change applied to every create/update/destroy) Ash features §1.1 row 7 and note changes always under actions, with a do block (ADR-0053) not in roadmap (M6 named reusable changes are the nearest) -
9 preparations section (applies to every read) Ash features §1.1 row 8 preparations none yet not in roadmap -
10 validations section (on: default [:create, :update]) Ash features §1.1 row 9, §9 validations always under actions, with a validate block not in roadmap -
11 pipelines (named bundle, pipe_through) Ash features §1.1 row 10 pipelines, pipe-through none yet not in roadmap -
12 multitenancy Ash features §1.1 row 13 multitenancy none in the file: an ActionContext key an extension declares (ADR-0059) after v1 (roadmap section 6; placement settled by ADR-0059) -
13 temporal Ash features §1.1 row 14 temporal none yet not in roadmap -

3.2 Attributes

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
14 section attributes Ash features §1.1 row 1 attributes attributes on main attributes: >attribute
15 uuid_primary_key name (sets writable? false, public? true, primary_key? true, type :uuid, default &Ash.UUID.generate/0; allow_nil? is false and is not accepted as an option; no generated?; checked, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/dsl-ash-resource.html) Ash features §2.3 uuid-primary-key="id" uuid #id primary-key on main (aligned) uuid-primary-key: value
16 uuid_v7_primary_key Ash features §2.3 uuid-v7-primary-key none yet not in roadmap -
17 integer_primary_key (type integer, generated? true) Ash features §2.3 integer-primary-key none yet not in roadmap -
18 create_timestamp name (writable? false, match_other_defaults? true, allow_nil? false, default &DateTime.utc_now/0, type Ash.Type.UTCDatetimeUsec; public? is not overridden, so it stays false; primary_key? false; checked, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/dsl-ash-resource.html), used as create_timestamp :inserted_at Ash features §2.3, §12.1 create-timestamp="insertedAt" timestamp #insertedAt on="create" on main (aligned): create-timestamp create-timestamp: value
19 update_timestamp name (same options as create_timestamp, plus update_default &DateTime.utc_now/0; public? false, allow_nil? false, writable? false; checked, Ash 3.34.0, checked 2026-10-04), used as update_timestamp :updated_at Ash features §2.3, §12.1 update-timestamp="updatedAt" timestamp #updatedAt on="update" on main (aligned): update-timestamp update-timestamp: value
20 attribute name, type Ash features §2.2, §12 attribute="subject" type="string" the type is the tag: string #title on main attribute: value type
21 option allow_nil? (default true) Ash features §2.2 allow-nil=false required by default; nullable marks the exception on main (aligned): allow-nil=false attribute: allow-nil
22 option public? (default false) Ash features §2.2 public none (ADR-0035, open) on main; recorded in the model as Ash records it, nothing in v1 reads it (roadmap M1; ADR-0035) attribute: public
23 option default (no default; “Value set on create”) Ash features §2.2 default default= on main Checked (G5, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/dsl-ash-resource.html): default’s type is (-> any) | mfa | any: a zero-arity function, an MFA tuple or a literal. The contract keeps literals only (R14); a function default is for the milestone that evaluates defaults. attribute: default
24 option update_default Ash features §2.2 update-default none yet not in roadmap Checked (G5): update_default has the same type (-> any) | mfa | any. -
25 option constraints (type-specific, for example one_of, max_length) Ash features §2.2, §2.4 constraints shape rules on the line (min, max, match); values on enum on main (aligned): constraints with one_of on atoms; other constraints not in roadmap attribute: constraints
26 options description, sensitive? (false), source Ash features §2.2 description, sensitive, source none yet not in roadmap -
27 options primary_key? (false), writable? (true), generated? (false) Ash features §2.2 primary-key, writable, generated primary-key; the others none yet not in roadmap -
28 options select_by_default? (true), always_select? (false) Ash features §2.2 select-by-default, always-select none yet not in roadmap -
29 options filterable? (true, or :simple_equality), sortable? (true), match_other_defaults? (false) Ash features §2.2 filterable, sortable, match-other-defaults none yet not in roadmap -
30 types string, boolean, uuid, datetime (short names in the registry) Ash features §2.1 type="string" etc. tags string, boolean, uuid, datetime on main attribute: type=string,boolean,uuid,datetime
31 types integer, float Ash features §2.1 type="integer", type="float" tags from the type registry (integer, float, decimal, …) on main (aligned): integer, float attribute: type=integer,float
32 enumerated values: :atom with constraints [one_of: [...]] (Ash features §12, get-started 236-261), or a module using Ash.Type.Enum (declares values/0, Ash features §2.1) Ash features §2.1, §12 type="atom" constraints={ one_of: ["draft", "published"] } enum #state values=["draft", "published"] on main (aligned): type="atom" with constraints={ one_of: [...] } attribute: constraints type=atom
33 the other built-in types (decimal, date, map, utc_datetime, ci_string, and so on; 31 short names) Ash features §2.1 the Ash names in kebab-case tags decimal, date, timestamp; the others none yet not in roadmap -
34 {:array, type} composite Ash features §2.1 none none yet not in roadmap -
35 NewType, embedded resources Ash features §2.1, §2.4 none none (not planned) not planned (roadmap section 6) -

3.3 Relationships

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
36 section relationships Ash features §1.1 row 2 relationships relationships on main relationships: >belongs-to >has-many
37 belongs_to name, destination (the destination is a module) Ash features §3, §12 belongs-to="author" destination="user" belongs-to=User #author on main aligned: resource= became destination= (G9) belongs-to: value destination
38 has_many name, destination Ash features §3, §12.1 has-many="comments" destination="comment" has-many=Comment #comments on main aligned: resource= became destination= (G9) has-many: value destination
39 has_one name, destination Ash features §3 has-one has-one=Payment #payment M7 -
40 many_to_many with a join resource (through, join_relationship, …) Ash features §3.1, §3.3 many-to-many none (not planned) not planned (roadmap M7 out of scope) -
41 belongs_to creates <name>_id automatically (example: representative_id) Ash features §12 (get-started 561-567) implied foreign-key attribute, an author-level name (a value, Section 0); Ash generates representative_id for belongs_to :representative belongs-to=User #author adds authorId M7 (roadmap M7: “adds its foreign-key attribute”) Checked (G4, Ash 3.34.0, checked 2026-10-04): the default source_attribute of belongs_to is <name>_id; its destination_attribute defaults to id. -
42 belongs_to option allow_nil? (on the generated attribute; default not in the research) Ash features §3.1 allow-nil none yet not in roadmap Checked (G4): allow_nil? on belongs_to defaults to true; it also exists on has_one (default true) and does not exist on has_many. -
43 belongs_to options define_attribute?, attribute_type, attribute_writable?, attribute_public?, attribute_always_select?, primary_key? Ash features §3.1 the Ash names in kebab-case none yet not in roadmap -
44 shared options source_attribute, destination_attribute (guessed by a transformer when absent) Ash features §3.1; Ash DSL and extensions §2.7 rows 7-8 the Ash names in kebab-case none yet not in roadmap (M7 needs the derivation rule) Checked (G4): has_one/has_many source_attribute defaults to id; their destination_attribute has no default value in the option table and is guessed by a transformer from the last segment of the source resource’s module name, underscored, plus _id (MyApp.Blog.Post gives post_id; source src/lib/ash/resource/transformers/has_destination_field.ex, https://github.com/ash-project/ash). Other defaults on all three: public? false, writable? true, filterable? true, sortable? true. -
45 shared options public?, description, sort, default_sort, filterable?, sortable?, writable?, read_action, domain, and a nested filter entity; has_many option limit Ash features §3.1 the Ash names in kebab-case none yet not in roadmap -
46 manual, no_attributes?, through (traversal), from_many?, offset, could_be_related_at_creation? Ash features §3.1-3.2 the Ash names in kebab-case none yet not in roadmap -
47 manage_relationship change Ash features §3.4 none the relate= step, planned (ADR-0053) not in roadmap (roadmap M7 out of scope: “managing related records in a write”) -

3.4 Actions

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
48 section actions Ash features §1.1 row 3 actions actions on main actions: >create >update >destroy >read
49 section option defaults (list of action types; “creates a simple action of each specified type, with the same name as the type”) Ash features §1.2 attribute of actions: actions defaults=["read", "destroy"] actions auto=["read", "destroy"] (ADR-0052) on main (aligned): attribute of actions actions: defaults
50 section option default_accept (Ash 3 default: no attributes accepted) Ash features §1.2 default-accept on actions none yet not in roadmap -
51 create name Ash features §4.1 create="open" create #open on main create: value
52 update name Ash features §4.1 update="close" update #close on main update: value
53 destroy name Ash features §4.1 destroy="archive" destroy #archive on main destroy: value
54 read name Ash features §4.1 read="published" read #published on main read: value
55 generic action (returns, run, constraints) Ash features §4.1 none none (not planned) not planned (roadmap section 6) -
56 option accept (create, update, destroy only; :* = all public attributes; accept [] is valid) Ash features §4.1, §12 (get-started 317-335) accept=["title", "body"] accept=[...] on main (aligned): accept on create, update and destroy create: accept; update: accept; destroy: accept
57 option primary? (defaults are primary unless one exists) Ash features §1.2, §4.1 primary auto actions are primary; on:load= names the read Mesh uses not in roadmap -
58 options description, public?, skip_unknown_inputs, touches_resources, transaction? (reads default false, writes true) Ash features §4.1 the Ash names in kebab-case none yet not in roadmap -
59 option require_atomic? (update and destroy, default true; require_atomic? false appears in Ash features §12.1) Ash features §4.9, §12.1 none (require-atomic was M5 vocabulary, never on main) none: the build infers the strategy (ADR-0054) M5; nothing in the vocabulary, the build infers (ADR-0054) -
60 options atomic_upgrade? (false), atomic_upgrade_with Ash features §4.9 the Ash names in kebab-case none not in roadmap -
61 manual, manual? Ash features §4.10 the Ash names in kebab-case none (not planned) not planned (roadmap section 6) -
62 create options upsert?, upsert_identity, upsert_fields, upsert_condition, return_skipped_upsert? Ash features §4.7 the Ash names in kebab-case none yet (after v1) after v1 (roadmap section 6) -
63 bulk actions (Ash.bulk_create/update/destroy) Ash features §4.8 none (a run-time call, not a tag) none yet (after v1) after v1 (roadmap section 6) -
64 destroy option soft? Ash features §4.13 soft none yet not in roadmap -
65 options error_handler, notifiers, action_select, require_attributes, allow_nil_input, delay_global_validations?, skip_global_validations?, multitenancy Ash features §4.1 the Ash names in kebab-case none yet not in roadmap (notifiers: outbox, after v1) -
66 nested change entity (24 built-ins such as set_attribute, relate_actor; a module; change {Module, opts}) Ash features §4.3, §2.5, §12 change= taking a call or an arrow function steps in a do block: set, when, load, run (ADR-0053) on main; the expression form is deviation D16 change: value
67 nested validate entity (24 built-ins; message inside the block); allowed in create, update, destroy, read and generic Ash features §4.4, §1.2, §12 validate= with message= a validate block: require=[...], check :label [ that code message ] on main (aligned): in create, update, destroy and read validate: value; create: >validate; update: >validate; destroy: >validate; read: >validate
68 validation options: resource-level validate has where, on, only_when_valid?, message, description, before_action?, always_atomic?; the action-level options are not in the research Ash features §9 where, on, only-when-valid, message, description, before-action, always-atomic when nests checks; code and message on check; the others none message on main; always_atomic? relates to the M5 protocol; the rest not in roadmap; G7 Checked (G7, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/dsl-ash-resource.html): the action-level validate options are where (default []), only_when_valid? (false), message, description, before_action? (false; not with atomic actions) and always_atomic? (false); there is no on. The action-level change options are where, only_when_valid?, description, always_atomic?; no message, no on. on exists only on the resource-level validate and change (default [:create, :update]), because an action already fixes its type. None of these is added to the contracts now. validate: message
69 nested argument name, type; options description, constraints, allow_nil?, public?, sensitive?, default (defaults of these: not in the research) Ash features §4.2, §1.2 argument="title" type="string" an arguments section: datetime #paidAt M5 -
70 nested prepare on read (built-ins: set_context, build, before_action, after_action; “preparations take no action-input arguments, they rewrite a query”) Ash features §4.5, §1.2 prepare= none yet M5 -
71 hooks written as built-in changes: before_action(fn changeset, context), after_action(fn changeset, record, context), before_transaction, after_transaction (and around_* on the changeset API) Ash features §4.6 change=before_action(...), change=after_action(...), change=before_transaction(...), change=after_transaction(...) (call names are JavaScript identifiers) the run step inside the transaction; after-commit planned M5 for hooks inside the transaction and after commit (roadmap M5); which Ash hook maps to which roadmap phase is G6; before_transaction not in roadmap Checked (G6, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/Ash.Changeset.html): inside the transaction run before_action (after validations and changes, before the data layer action) and after_action (after the data layer action, success only), and around_action; outside run before_transaction (before it starts) and after_transaction (after it ends, whether it committed or rolled back; it receives {:ok, record} or {:error, reason}). So after_transaction is not “after commit”. Signatures: change before_action(fn changeset, context), change after_action(fn changeset, record, context), change before_transaction(fn changeset, context), change after_transaction(fn changeset, result, context). Mapping these to the roadmap’s phases is M5 work. -
72 read-side hooks written as built-in preparations: prepare before_action(fn query, context), prepare after_action(fn query, records, context) Ash features §4.5, §4.6 prepare=before_action(...), prepare=after_action(...) (call names are JavaScript identifiers) none yet not in roadmap (M5 names preparations, not their hooks) -
73 nested pagination on read: keyset? (false), offset? (false), via_data_layer?, default_limit, countable (true), max_page_size (250), stable_sort (primary key), required?, paginate_by_default? Ash features §4.11, §1.2 pagination child of read with offset, keyset, default-limit, max-page-size, countable, stable-sort, required none yet (M3 adds pagination) M3 (offset and keyset; the other options not in roadmap) -
74 nested filter on read Ash features §1.2 filter= filter=({ self }) => ... on main; expression form D16 filter: value
75 sorting a read: Ash’s read action has no sort entity; relationships have sort/default_sort options; the build preparation exists, its options are not in the research Ash features §1.2, §3.1, §4.5 sort=["-insertedAt"] until G1 is answered sort=["-insertedAt"] on main; G1 Checked (G1, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/Ash.Query.html): build passes its keyword list to Ash.Query.build/2, whose options include filter, filter_input, sort, sort_input, default_sort, distinct_sort, limit, offset, load, strict_load, select, ensure_selected, aggregate, calculate, distinct, context; example prepare build(sort: [song_rank: :desc], limit: 10). Sort strings: no prefix or + is ascending, ++ ascending nulls first, - descending, -- descending nulls last; comma separated or a list. So the way to sort a read in Ash is a prepare build(sort: ...) call, not a sort entity; Mesh’s sort=["-insertedAt"] uses the - prefix as Ash does. Whether sort stays a tag or becomes prepare=build(...) is decided in M3/M5 (D17); the contracts keep sort. sort: value
76 nested pipe_through, metadata Ash features §1.2 pipe-through, metadata none yet not in roadmap -
77 read options get?, get_by, modify_query, timeout, manual Ash features §4.1 the Ash names in kebab-case none yet not in roadmap -

3.5 Identities

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
78 section identities; identity name, keys (example identity :email, [:email]) Ash features §9, §12.1 identities; identity="email" keys=["email"] unique on an attribute line; declared identities after v1 after v1 (rulings of 2026-10-04, “After v1”) -
79 identity options where, nils_distinct? (true), eager_check?, eager_check_with, pre_check?, pre_check_with, description, field_names, message, all_tenants? Ash features §9 the Ash names in kebab-case none yet (after v1) after v1 -

3.6 Calculations

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
80 section calculations Ash features §1.1 row 12 calculations computed on main calculations: >calculate
81 calculate name, type, expression, for example calculate :full_name, :string, expr(...) (the third positional’s name is not in the research) Ash features §5.1 calculate="excerpt" type="string" plus the expression a typed field with a body in computed: string #excerpt({ self }) { ... } on main; G2 Checked (G2, Ash 3.34.0, checked 2026-10-04, https://hexdocs.pm/ash/dsl-ash-resource.html): the signature is calculate name, type, calculation \\ nil; the third positional is named calculation, is optional (a do block or option can carry it instead), and takes expr(...), a module, {module, opts} or a function (records, context) -> results. A multi-line form is a do block holding options, not a multi-line expression. The contract keeps the child tag value for the body until the milestone that implements calculations designs it (D27, M7); this task does not restructure calculate. calculate: value type >value
82 module-based calculation (calculate :duration, :string, Module) Ash features §5.1, §12.1 a named, reusable calculation a helper imported into the entity file M6 (roadmap M6) -
83 options async?, constraints, description, public?, sensitive?, load, allow_nil?, filterable?, sortable?, field?, multitenancy Ash features §5.1 the Ash names in kebab-case none yet not in roadmap -
84 nested argument Ash features §5.1 argument none yet not in roadmap -

3.7 Aggregates

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
85 section aggregates Ash features §1.1 row 11 aggregates computed on main aggregates: >count
86 count name, relationship_path (count :assigned_ticket_count, :reported_tickets; relationship_path is Ash’s name for the second positional, G9, Ash 3.34.0, checked 2026-10-04) Ash features §5.2 count="comment_count" relationship-path="comments" count #commentCount of="comments" on main (aligned): relationship-path count: value relationship-path
87 kinds exists, first, sum, list, max, min, avg, custom (example sum :duration_seconds, :tracks, :duration_seconds) Ash features §5.2, §12.1 the Ash names (kebab-case), with field sum, avg, min, max, e.g. sum #total of="lines.amount" M7 (“count and the other aggregates”; which ones is not stated) -
88 shared options relationship_path, read_action, filter, description, default, public?, filterable?, sortable?, sensitive?, authorize?, multitenancy; kind options field (all but exists), uniq?, include_nil?, sort, join_filter Ash features §5.2 the Ash names in kebab-case none yet not in roadmap -

3.8 Policies (Ash.Policy.Authorizer, a separate extension)

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
89 section policies; option default_access_type (:filter) Ash features §1.4, §6.1-6.2 policies policies, part of core (ADR-0055) on main; stays in core (ADR-0055) policies: >policy
90 policy condition do ... end (options description, access_type, condition, error_message; a list of condition checks means all must match) Ash features §6.1 policy=action_type("read"); all: policy=[action_type("update"), action("publish")] policy #name types=[...] actions=[...]; neither means every action on main (aligned): a check call or a non-empty list of checks, D22 Checked (G8, Ash 3.34.0, checked 2026-10-04): Ash’s condition is optional; Mesh keeps it required until M8 (D24). policy: value >authorize-if @action @action_type
91 built-in checks, including action(atom or list of atoms) and action_type(type or list of types); a list argument matches any listed name Ash 3.34.0 source, checked 2026-10-04 action("publish"), action(["publish", "archive"]), action_type("read"), action_type(["read", "destroy"]) (JavaScript call names) actions=[...] and types=[...] on policy action and action_type on main; other checks not in roadmap policy: value @action @action_type
92 authorize_if check Ash features §1.4, §6.1 authorize-if= authorize-if= on main authorize-if: value
93 forbid_if check Ash features §1.4, §6.1 forbid-if= forbid-if= M8 -
94 authorize_unless, forbid_unless Ash features §1.4, §6.1 the Ash names in kebab-case none yet not in roadmap -
95 decision rule: checks run top to bottom, the first decisive one decides; no applicable policy means forbidden Ash features §6.1, §6.8 same rule every policy covering an action must pass; none covering means forbidden M8 (ADR-0055) -
96 the authorizer is opt-in per resource (authorizers: [Ash.Policy.Authorizer]) Ash features §6.8 see row 5 none: no policies section forbids every action M8; exception X2 -
97 bypass, policy_group Ash features §1.4, §6.1 the Ash names in kebab-case none (bypass rejected) out of M8 (roadmap M8) -
98 field_policies (field_policy, field_policy_bypass) Ash features §1.4, §6.1 the Ash names in kebab-case none yet out of M8 -
99 policy access_type (:strict, :filter, :runtime) Ash features §6.2 access-type none yet out of M8 -
100 Ash.can? and related Ash features §6.7 a generated can function per action a generated can function per action M8 (roadmap M8) -

3.9 Expressions, and the references inside them

Ash writes a translatable expression as expr(...) and refers to the caller and the call with templates (Ash features section 5.5). Inside a resource file an expression is JavaScript, parsed by Babel (MX project notes, getting-started section 1), so it must be valid JavaScript and the naming rule of Section 0 does not apply to it. Mesh’s roadmap starts from an arrow function’s parsed form (roadmap M4): a translatable expression may use only its declared parameters and registered functions, and a free variable is a build error. The registry of functions and operators is Mesh’s own and starts small (roadmap M4: attribute and parameter references, literals, comparison and boolean operators, string length, assignment to an attribute). The exact expression form is designed in M4; this table records what Ash has and which Mesh element corresponds.

# Ash section / entity / option (default, type) Source M1 spelling (code on main) Mesh spelling (syntax v2) Status Contract check
101 expr(...) wrapper, for example calculate :full_name, :string, expr(first_name <> " ") and authorize_if expr(public == true) Ash features §5.1, §6.1 the arrow function’s body; whether an expr(...) call wraps it is designed in M4 a body that is one expression is translated; anything else is plain code (ADR-0056) M4 (conversion from MX’s parsed form); D16 -
102 operators: 15 plus is_nil; registered IsNil, Eq, NotEq, In, LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual, and + * - / <> || and &&; and/or are boolean expressions; aliases equals, not_equals, gt, lt, gte, lte Ash features §5.3 JavaScript’s operators (valid JavaScript is required); the registry entry for each is designed in M4 JavaScript operators in the supported subset M4 registry: comparison and boolean operators, numeric + and - (roadmap M4); the rest not in roadmap -
103 functions: 39 registered, by module name (Length, StringLength, Contains, Now, Ago, If, Round, …); the names used inside expr(...) are not in the research Ash features §5.4 one call per registry entry, named with an Ash name as a JavaScript identifier (snake_case) registry functions, named in M4 M4 registry: string length only; G3 Checked (G3, Ash 3.34.0, checked 2026-10-04, src/lib/ash/filter/filter.ex and each use Ash.Query.Function, name: :...): the 39 are module names; the call names inside expr(...) are: ago, at, composite_type, contains, count_nils, date_add, datetime_add, fragment, from_now, get_path, has, is_distinct_from, is_nil, is_not_distinct_from, if, intersects, lazy, length, - (module Minus), now, range_adjacent, range_contains, range_lower, range_overlaps, range_upper, error, rem, round, today, type, start_of_day, string_downcase, string_ends_with, string_join, string_length, string_position, string_split, string_starts_with, string_trim; custom_expressions application config can add more. Built-in changes, validations and checks are not functions inside expr(...): they are DSL calls (change set_attribute(...), validate present(...), authorize_if action_type(:read)). That the validation and check builtins are plain JavaScript-style identifiers in Mesh is not verified (open, G3). -
104 special forms exists/2, path.exists/2, parent/1, lazy/1, error expressions, inline aggregates Ash features §5.4 (guide references) calls with Ash’s names as JavaScript identifiers traversal self.customer.userId (M7); the others none yet M7 for relationship traversal (exists); the others not in roadmap -
105 template ^actor(:key) and ^actor([:key1, :key2]) Ash features §5.5 the arrow function’s declared parameter actor: ({ post, actor }) => ... actor.id the actor parameter M4 (the actor is bound from the scope, roadmap M4); exact form designed in M4; D37 -
106 template ^arg(:name) Ash features §5.5 a declared parameter carrying the action’s arguments; exact form designed in M4 the input parameter M5 (arguments) -
107 template ^context(:key) Ash features §5.5 the declared parameter context, from the scope (ADR-0007); exact form designed in M4 the context parameter M4 -
108 template ^ref(:key), ^ref([:path], :key) Ash features §5.5 none none not in roadmap -
109 template ^tenant() Ash features §5.5 none a declared key: context.tenantId after v1 (multitenancy) -
110 a reference to an attribute inside expr(...) is a bare name (public == true) Ash features §6.1 a property of a declared parameter: post.state self.state on main; recorded deviation D37 n/a (the contracts do not look inside an expression; the form is designed in M4)

That is 110 rows.

4. Deviations

4.0 Where Mesh’s design differs from Ash

These are deliberate. Each has its record.

# Mesh Ash Record
V1 One line shape, kind #name options; the type, relationship kind or action type is the tag Positional arguments and per-entity options ADR-0050
V2 Attributes are required unless nullable allow_nil? defaults to true ADR-0050
V3 One domain per app; folders are modules; no attribute names the domain Many domains; each resource names its own ADR-0057
V4 validate runs before the steps; self is the record with the accepted input applied; one-field rules go on the attribute line, check only for cross-field or stored-state rules Changes and validations interleaved in one list ADR-0053
V5 The write strategy is inferred and printed by mesh explain require_atomic? declared per action ADR-0054
V6 Policies are core; checks inside a policy combine without order; every covering policy must pass; no policies means forbidden; no bypass The authorizer is opt-in per resource; bypass policies ADR-0055
V7 A function whose body is one expression is translated; anything else is plain code; the record is self expr(...) with bare attribute names and ^actor templates ADR-0056
V8 One flat action context argument actor:, tenant:, context: options, or Ash.Scope ADR-0059
V9 A has-one target gets a unique index Truncates silently ADR-0045 (proposed)
V10 Generated code carries the behaviour and is committed Behaviour lives in the library ADR-0003

4.0.1 The M1 alignment (history)

History: the M1 alignment as decided on 2026-10-04. The vocabulary decisions that superseded it are records 0049 to 0056 (section 4.0); where a sentence below disagrees with them, the records win.

The rest of this section, and sections 5 to 7, record the alignment of the contracts with Ash’s DSL done in M1, when the vocabulary still copied Ash. They describe the code on main. “Now” below means the contracts before that alignment.

“Now” is what contracts.ts or the roadmap has today. “Alignment” is the change the ruling implies. Exceptions go to Section 5; open research to Section 6.

4.1 Names and structure

# Mesh now Ash Alignment
D1 kebab-case names (uuid-primary-key, belongs-to, authorize-if, action-type) and booleans without ? (public, required) against Ash’s snake_case and ? uuid_primary_key, belongs_to, authorize_if, action_type, allow_nil?, public? Not a deviation: the naming rule of Section 0 maps one to the other. Only the names that differ by more than the rule (required against allow-nil, timestamps, number, enum) are listed below. Done: the names in contracts.ts follow the rule; a test checks every tag and attribute name.
D3 attribute="title" type="string": name as the default attribute, type as a named attribute attribute :subject, :string (Ash features §12) Aligned in the only way MX holds positionals (Section 2).
D4 uuid-primary-key="id" (338-342) uuid_primary_key :id, with writable? false, public? true (Ash features §2.3) The name follows the rule; the model applies the same defaults.
D5 one timestamps tag (355) two entities, create_timestamp name and update_timestamp name (Ash features §2.3, §12.1). Whether Ash also has a combined entity: not in the research. Align: replace with the two entities, each with an explicit name. Done: create-timestamp and update-timestamp, each optional and at most once, each with a required name.
D6 required (348), a flag meaning “not null” allow_nil?, default true (Ash features §2.2) Align: replace with allow-nil=false. The default (nullable) is the same. Done: allow-nil=false; required is gone.
D7 public flag (349) public?, default false (Ash features §2.2) The name follows the rule. The roadmap already treats the meaning as Ash does: recorded, not read in v1 (roadmap M1; ADR-0035). Done: public is unchanged.
D8 type number (45) integer, float, decimal (Ash features §2.1); no number Align: replace number with integer and float. decimal is not in the roadmap. Done: integer and float; number is gone.
D9 type enum with values (47, 348); calculate.type excludes it (57, 459) an atom with constraints [one_of: [...]] (Ash features §12), or an Ash.Type.Enum module with values/0 (Ash features §2.1) Align: type="atom" with constraints={ one_of: [...] }. MX can express it: a contract attribute may omit its type (contracts.ts:351 does), and analyze reads the object literal’s Babel node. Whether the untyped attribute may keep literalOnly for an object literal is not checked; if not, drop literalOnly for constraints and check it in analyze. The calculate.type list (R9) then excludes atom. Done: type="atom" with constraints={ one_of: [...] }; values and enum are gone. An untyped contract attribute keeps literalOnly for an object literal: the negative fixture literal-only-constraints shows an identifier is rejected and every positive fixture with constraints parses (Appendix B). analyze reads the object literal’s Babel node, rejects a constraints that is not an object literal, has no one_of or has an unknown constraint, and rejects one_of that is not a list of non-blank, non-repeated strings, at least one.
D10 belongs-to="author" resource="user" (366) belongs_to name, destination, the destination being a module (Ash features §12) Align: Ash’s name for the second positional is destination (G9, Ash 3.34.0, checked 2026-10-04). Done: destination= on belongs-to and has-many; resource= is gone.
D11 has_one absent (358-362); roadmap M7 adds it has_one (Ash features §3) Align: add has-one, same shape as has-many, in M7.
D12 foreign key: roadmap M7 says a belongs-to adds its foreign-key attribute (the fixture’s authorId) belongs_to :representative creates representative_id (Ash features §12, get-started 561-567) Not vocabulary: the generated name is a value (Section 0). Ash’s rule is <name>_id. Settled: the relationship’s name plus Id (belongs-to=List #list creates listId), ADR-0050.
D13 defaults=[...] is a child tag of actions (378, 385-389) an option of the actions section (Ash features §1.2) Align: an attribute of actions; remove the defaults tag. Done: defaults is an attribute of actions; the defaults tag is gone.
D14 no accept on destroy (404) accept on create, update and destroy (Ash features §4.1) Align: add accept to destroy. Done: accept on destroy, checked like the others.
D15 validate allowed in update and destroy only (399, 405, 419); none in create (393) or read (411) validate nested in create, read, update, destroy and generic (Ash features §1.2) Align: allow validate in create and read. The review of PR #1 records no reason for the omission. Done: validate allowed in create and read.
D16 change, validate, filter, authorize-if take an arrow function with destructured parameters, ({ post, actor }) => ... (78, 415-416, 419, 424, 447); the roadmap converts arrow functions (roadmap M4) change/validate take a built-in call (24 changes, 24 validations, Ash features §4.3-4.4), a module, or {Module, opts} (Ash features §12); filter, calculations and policy checks take expr(...) (Ash features §5.1, §6.1) Align in form where the research documents the helper: built-in calls with Ash’s snake_case names as JavaScript identifiers (set_attribute(...)). MX can express it: a call is accepted by function attributes (MX project notes, contract-extensions §5, table, last row). The form of a translatable expression (arrow function with declared parameters, or an expr(...) call around one) is designed in M4 (D37); an arrow function stays the form of code that is not a built-in (Ash’s custom change module, Ash features §2.5). Because roadmap M4 and M5 are written around arrow functions, Section 7 keeps arrow functions until then. Roadmap edits: Section 4.3.
D17 sort=["-insertedAt"] as a child of read (411, 427-431) no sort entity on a read action (Ash features §1.2) Open: G1. Checked (G1): Ash sorts a read through prepare build(sort: ...), with - for descending as in Mesh’s strings; see row 75. The sort tag stays until M3/M5 decides the form.
D18 pagination named only as “new vocabulary” (roadmap M3) nested pagination with 9 options (Ash features §4.11) Align: a pagination child of read with Ash’s option names; M3 implements offset and keyset.
D19 preparations (roadmap M5): no name given nested prepare on read (Ash features §1.2, §4.5) Align: a prepare child of read.
D20 arguments (roadmap M5): no shape given argument name, type with options (Ash features §4.2) Align: an argument child of every action.
D21 roadmap M5: an action with an opaque change, or a change or validation that reads the stored record, is not atomic and must say Ash’s require_atomic? set to false require_atomic?, default true, update and destroy (Ash features §4.9) Already aligned in the roadmap; spelled require-atomic=false (Section 0). The proposal put require-atomic=false on publish, which validates post.title; the contracts add the attribute in M5, and the fixture of Section 7 does not carry it yet (it is M5 vocabulary).
D22 policy takes action or action-type as string attributes (440-441) the policy’s condition is a check call, action_type(:read), action(:name), “a check or list of checks” (Ash features §6.1, §6.3) Align: policy=action_type("read"), a list for several (call names are JavaScript identifiers, Section 0). MX can express it: the default attribute may be untyped and take a call (MX project notes, contract-extensions §5; contracts.ts:351 for an untyped attribute); analyze reads the call. Done: policy=action_type("read"), policy=action("publish"), or a list. analyze accepts the two checks on main (row 91), each with one string literal or a non-empty array of string literals (any listed name matches); blank or repeated array items are errors. An unknown check, a non-call, a call with another argument count or type, and an empty condition list are errors. A condition list means all checks must match.
D23 exactly one of action, action-type; both is an error (analyzePolicy, 233-244) conditions combine as a list (Ash features §6.1) Align: the “not both” rule goes; a list of checks replaces it (D22). Done: the “not both” rule is gone; a list of checks replaces it.
D24 authorize-if required, at least one, in every policy (443) a policy’s entities are four check forms (Ash features §6.1); whether a policy may have zero checks is not in the research (G8) Align at M8: “at least one check of any of the four kinds” as an analyze rule. Until then keep. Checked (G8, Ash 3.34.0, checked 2026-10-04): in Ash a policy may have no condition (it then always applies) but needs at least one check of the four kinds; the source error is “Policies must have at least one check.” (src/lib/ash/policy/policy.ex). So the M8 rule “at least one check of any kind” is Ash’s, and Mesh’s required condition is a deviation to revisit in M8 (Ash’s condition is optional).
D25 forbid-if (roadmap M8) forbid_if (Ash features §6.1) The name follows the rule. authorize-unless and forbid-unless are not in the roadmap.
D26 action-type limited to create, read, update, destroy (ACTION_TYPES, 40) five action types including generic action (Ash features §4.1) Align when generic actions arrive (not planned, roadmap section 6).
D27 calculate="excerpt" type="string" with a child tag value holding the body (455-467; fixture lines 36-39) calculate :name, :type, expr(...) in one line (Ash features §5.1) Open: G2. Checked (G2): Ash’s third positional is calculation, optional, and takes expr(...), a module, {module, opts} or a function; no multi-line expression form exists. Alignment follows in M7 with the child tag value’s replacement; not done in this task.
D28 count with attribute relationship (475) count name, relationship_path (Ash features §5.2; the positional’s name is inferred, Section 2) Align: rename the attribute to relationship-path. Done: relationship-path.
D29 only count (471-472); roadmap M7 says “count and the other aggregates” 9 kinds (Ash features §5.2) Align: M7 names the kinds it implements from Ash’s list, with field on all but exists.
D30 table on resource (317) table in the data layer’s section (rows 3-4) Exception X1. (domain is aligned: row 1.)
D31 attributes required in a resource (320); no primary-key rule ValidatePrimaryKey and VerifyPrimaryKeyPresent verifiers exist (Ash DSL and extensions §2.7); when the key is required is not in the research Done in M1 (PR #12): the checks step rejects a resource with no primary-key attribute at the resource name, with a fix to declare uuid-primary-key (roadmap M2 selects by key). There is no opt-out in M1.
D32 a has-one target gets an implicit unique index on the foreign key, with no new vocabulary; declared identities stay after v1 (roadmap M7; ADR-0045, Proposed) Ash does not enforce it; it truncates silently (research synthesis §6, item 7). identity name, keys is Ash’s way to declare uniqueness (Ash features §9) Recorded deviation from Ash, in ADR-0045. It is an emitted-schema rule, not vocabulary, and it is ruling-neutral, so it is not an operator exception. It adds a guarantee Ash lacks.
D33 camelCase names the author chose in the fixture (commentCount, insertedAt, authorId; post.mx:25, 17, 42) snake_case atoms (inserted_at, representative_id; Ash features §12) Not vocabulary: names an author chooses are values and stay as written (Section 0). The fixture in Section 7 keeps its camelCase. Lead ruling, 2026-10-04: a column is named exactly like its attribute, with no transform (rulings before M2).
D34 policies are in ext-policies, “first-party, on by default”; deny by default arrives with it (roadmap section 3, M8; ADR-0036) the authorizer is opt-in per resource: “a resource has policy authorization only if it declares authorizers: [Ash.Policy.Authorizer]” (Ash features §6.8) Exception X2.
D35 hooks: roadmap M5 names no tag hooks are changes: change before_action(fn) (Ash features §4.6) Align: change=before_action(...) (rows 71-72; the call name is a JavaScript identifier, Section 0). MX can express it (a call, MX project notes, contract-extensions §5). Checked (G6): before_action and after_action run inside the transaction, before_transaction before it, after_transaction after it ends (committed or rolled back); see row 71.
D36 the contract has only message on validate (420) where, on, only_when_valid?, before_action?, always_atomic? on resource-level validate (Ash features §9); action-level options not in the research Align when the roadmap needs them (always-atomic and only-when-valid bear on the M5 protocol); G7. Checked (G7): the action-level validate options are where, only_when_valid?, message, description, before_action?, always_atomic? (no on); the action-level change options are where, only_when_valid?, description, always_atomic?. Not added to the contracts; M5 adds what the protocol needs.
D37 expression references go through a declared parameter (post.state, actor.id) bare attribute names inside expr(...) and the templates ^actor(:id), ^arg(:name), ^context(:key), ^ref(...) (Ash features §5.5, §6.1) Recorded deviation, kept (the scope rule of roadmap M4 and ADR-0010): JavaScript has no implicit record scope, and roadmap M4 allows a translatable expression only its declared parameters and registered functions (a free variable is a build error). The templates have no JavaScript spelling; they map to declared parameters (rows 105-107), exact form designed in M4. Not aligned.
D38 the M4 registry is Mesh’s own and small 15 operators, 39 functions (Ash features §5.3-5.4); call names inside expr(...) are not in the research Align: each registry entry takes an Ash name written as a JavaScript identifier; G3. Checked (G3): the call names are listed in row 103.

4.2 Rules the developer inferred or review required

Each row is a rule in contracts.ts that is not a name. “Ash” is what the research says; many rules are not in the research at all. “Origin” cites PR #1 (https://github.com/svallory/mesh/pull/1), whose description records the developer’s list “Inferred by me” and rounds 1 to 4 of its review; it does not record who proposed each review finding.

# Rule and origin Mesh now Ash Alignment
R1 values required for enum, and rejected for any other type. Origin: “required” from MX project notes, contract-extensions §10; the reverse is the developer’s inference analyzeAttribute, 178-186 constraints depend on the type (Ash features §2.4); whether an inapplicable one is an error: not in the research Replaced by D9: constraints allowed only for types that accept them. Done: analyzeAttribute checks constraints by type.
R2 policy exactly one of action, action-type. Origin: “one of” in MX project notes, contract-extensions §10; “exactly one” is the developer’s 233-244 see D23 Align (D23). Done: D23.
R3 closed type list of six and closed action list of four. Origin: the developer (“the notes say ‘fixed list’ but give no list”) 43-50, 40 31 short names plus custom types (Ash features §2.1) Align (D8, D26). Done: the type list (D8). The action list stays four (D26).
R4 authorize-if required in policy; child value required in calculate. Origin: the developer 443, 461 not in the research Keep for now; D24 and G2 revisit them.
R5 table optional on resource. Origin: the developer, from MX project notes, contract-extensions / the MX data-target note 317 not in the research Keep; X1.
R6 accept allowed on update; change and validate repeatable on update. Origin: the developer 398-399 accept on update (Ash features §4.1) Already consistent.
R7 every contract closed: closed() fills attributes, attributeTags, children (leaf tags take no children, no attribute tags). Origin: review of PR #1, round 1 correction and round 2 items 1, 3, 4 62-64; header 18-21 not in the research Keep: an MX-contract rule with no Ash counterpart; M6 composes contracts for extensions (ADR-0021).
R8 every non-function attribute is literalOnly, so resource=post is an error. Origin: review of PR #1, round 2 item 2 18-23, 66-77 not in the research Keep: MX’s static tree cannot evaluate an identifier.
R9 calculate.type excludes enum. Origin: review of PR #1, round 2 item 5 57, 459 not in the research Keep; follows D9. Done: calculate.type excludes atom.
R10 destroy contract with change and validate. Origin: review of PR #1, round 2 item 6 402-407 destroy is an Ash action type (Ash features §4.1) Align (adds accept, D14). Done: destroy has accept.
R11 one resource per file, not in the contracts (MX has no root cardinality); enforced by the model. Origin: review of PR #1, round 2 item 7, round 3 item 2 header 34-36 “one module per resource” (research synthesis §1, first paragraph) Already consistent.
R12 defaults items must be action types, no repeats, no blanks, not empty. Origin: rounds 2 (item 8), 3 (items 3, 5), 4 (item 2) 288-307, 267-276 defaults names action types (Ash features §1.2) Align the type check; keep the rest (not in the research). Done: the defaults type check is unchanged; it reads the actions attribute.
R13 list items: no blank, no repeats, in one_of (was values), defaults, accept, sort; sort=[], defaults=[] and one_of: [] rejected, accept=[] accepted 247-264, 266-276, 188-190 accept [] is valid in Ash (Ash features §12, get-started 317-335); the rest not in the research Keep.
R14 default: a string, number or boolean literal; negative numbers read; null, arrays, objects rejected; checked against the type; an atom default must be in one_of. Origin: round 2 item 9, round 3 item 1 106-124, 193-224 default is “Value set on create” (Ash features §2.2); whether it can be a function: not in the research (G5) Keep; the enum check follows D9. Checked (G5): Ash allows a function (zero-arity), an MFA tuple or a literal for default and update_default; Mesh keeps literals.
R15 names may not be empty or whitespace; empty sections allowed; validate message may not be empty. Origin: round 2 item 10, round 3 item 4, round 4 item 3 146-159, 32-36, 421 not in the research Keep.
R16 unknown tags rejected at any depth (unknownTags: "reject"). Origin: PR #2 (https://github.com/svallory/mesh/pull/2), MX e65707a0 (MX project notes, updates) tests not in the research Keep.

4.3 Roadmap text that changes with the alignment

These were the edits to the roadmap the alignment implied on 2026-10-04. The roadmap has since been rewritten for syntax v2 (revision 4), so the list is history.

  • Done in the alignment pull request: the roadmap’s section 1 item 6 and M1’s “Vocabulary alignment first” bullet now say the alignment is done, and the M1 model bullet quotes the aligned names (seven types, allow-nil, constraints, create-timestamp, update-timestamp, the defaults attribute). The “M1, model” item below is kept as the record of what changed.
  • M1, alignment. The alignment is the first part of M1; its acceptance test (“every row marked ‘on main’ has a contract and a fixture that match it”) is checked against Section 3 with the naming rule of Section 0.
  • M1, model. Replace “attributes of the six types” with the aligned type list (D8, D9); required becomes allow-nil (D6); timestamps becomes create-timestamp and update-timestamp (D5); “the four action kinds with accept and defaults” becomes defaults as an attribute of actions (D13) with accept on destroy as well (D14); add the primary-key verifier (D31); the not-implemented list uses the aligned names; names an author chooses stay as written (D33); the registry/contract drift test (ADR-0037) covers the new type list.
  • M3. “New vocabulary” for pagination becomes the pagination child (D18); sort waits on G1.
  • M4. The expression form is designed here: arrow functions with declared parameters stay (D37), built-in calls take Ash’s snake_case names as JavaScript identifiers (D16), the first registry takes Ash’s names (D38); validate in create and read adds positions to classify (D15).
  • M5. Hooks are change=before_action(...) and the other three, as JavaScript calls (D35), with read-side hooks as prepare (rows 71-72); prepare and argument named (D19, D20); before_transaction is not scheduled; validation options (D36) if the protocol needs them.
  • M7. has-one (D11); the generated foreign-key name is settled (D12); aggregate kinds (D29) and relationship-path (D28); the unique-index rule stays (D32).
  • M8. forbid-if (D25); policy conditions are check calls (D22, D23); the rule “at least one check of any kind” (D24); if the operator rules against X2, roadmap section 3 (“on by default”) and ADR-0036 change.

5. Exceptions for the operator (both closed)

History: the M1 alignment recorded two deviations from Ash for the operator. Both are now closed by later records.

# Deviation How it closed
X1 Where the table name lives. In Ash, table sits in the data layer’s own section (postgres do table "users" end, Ash DSL and extensions §3.3 and §4). The operator ruled on 2026-10-04 that table stays on the root tag and would move to a data-layer section at M6 or the post-v1 vocabulary review (rulings before M2). Syntax v2 keeps it on the entity line, entity #Post table="posts" (ADR-0050); the M6 move is withdrawn.
X2 Policies on for every entity, where Ash’s policy authorizer is opt-in per resource (Ash features §6.8). Closed by ADR-0055: policies are core, not an extension, and an entity without a policies section forbids every action.

6. Ash’s form: the lookups, checked

These were gaps in the research. Each was looked up in Ash’s documentation and source and the answer was then fact-checked. All facts below are Ash 3.34.0, checked 2026-10-04; the documentation is https://hexdocs.pm/ash/ (dsl-ash-resource.html, dsl-ash-policy-authorizer.html, Ash.Query.html, Ash.Changeset.html) and the source is https://github.com/ash-project/ash (main). Only checked facts are recorded; an unchecked sub-claim stays listed as open. “Applied” says whether the fact changed the contracts; vocabulary of a later milestone is recorded and the code is left alone.

# Fact Source Where it lands Applied
G1 build is a pass-through to Ash.Query.build/2; its options are filter, filter_input, sort, sort_input, default_sort, distinct_sort, limit, offset, load, strict_load, select, ensure_selected, aggregate, calculate, distinct, context. Sort strings: none or + ascending, ++ ascending nulls first, - descending, -- descending nulls last; comma separated or a list. Example: prepare build(sort: [song_rank: :desc], limit: 10) https://hexdocs.pm/ash/Ash.Query.html; lib/ash/resource/preparation/builtins.ex, build.ex row 75, D17; M3 no: later milestone
G2 calculate name, type, calculation \\ nil: the third positional is calculation, optional, and takes expr(...), a module, {module, opts} or a function of the records and the context. The multi-line form is a do block holding options https://hexdocs.pm/ash/dsl-ash-resource.html row 81, D27; M7 no: calculate is not restructured here
G3 The 39 registered functions are module names; the call names inside expr(...) are listed in row 103 (is_nil, string_length, if, - for Minus, …). Built-in changes, validations and checks are DSL calls, not functions in expr(...). Open: the validation and check builtins were not fetched, so how Mesh spells them is not settled lib/ash/filter/filter.ex (@functions), lib/ash/query/function/*.ex (use Ash.Query.Function, name: ...), lib/ash/resource/change/builtins.ex row 103, D16, D38; M4 no: later milestone
G4 belongs_to: allow_nil? true, destination_attribute id, source_attribute <name>_id. has_one/has_many: source_attribute id; destination_attribute has no default value and is guessed from the last segment of the source module’s name plus _id. allow_nil? exists on has_one (true) and not on has_many. Also on all three: public? false, writable? true, filterable? true, sortable? true https://hexdocs.pm/ash/dsl-ash-resource.html; lib/ash/resource/transformers/has_destination_field.ex, relationships/{belongs_to,has_one,has_many}.ex rows 41-44; M7 no: no on-main option contradicts them (the contracts have none of these options)
G5 default and update_default have type (-> any) | mfa | any: a zero-arity function, an MFA tuple or a literal. Open: that {MyModule, :my_func, []} appears as an example on the page https://hexdocs.pm/ash/dsl-ash-resource.html rows 23-24, R14 no: Mesh keeps literal defaults
G6 Inside the transaction: before_action (after validations and changes, before the data layer action), after_action (success only), around_action. Outside: before_transaction (before it starts), after_transaction (after it ends, committed or rolled back; receives {:ok, record} or {:error, reason}) https://hexdocs.pm/ash/Ash.Changeset.html; lib/ash/resource/change/builtins.ex row 71, D35; M5 no: later milestone
G7 Action-level validate: where, only_when_valid?, message, description, before_action?, always_atomic?. Action-level change: where, only_when_valid?, description, always_atomic?. on exists only on the resource-level validate and change https://hexdocs.pm/ash/dsl-ash-resource.html (actions.create.validate, actions.create.change) row 68, D36; M5 no: later milestone
G8 A policy needs at least one check of authorize_if, forbid_if, authorize_unless, forbid_unless (error “Policies must have at least one check.”); its condition is optional (policy condition \\ nil), and with none it always applies lib/ash/policy/policy.ex (transform/1), lib/ash/policy/authorizer/authorizer.ex; https://hexdocs.pm/ash/dsl-ash-policy-authorizer.html row 90, D24; M8 no: Mesh’s required condition stays until M8
G9 The positional names are in Section 2. In particular belongs_to name, destination, has_many name, destination, create name, attribute name, type, count name, relationship_path https://hexdocs.pm/ash/dsl-ash-resource.html Section 2, rows 37-38, 86, D10 yes: resource= became destination= on belongs-to and has-many
model uuid_primary_key: public? true, writable? false, primary_key? true, allow_nil? false (not accepted as an option), type uuid. create_timestamp and update_timestamp: writable? false, allow_nil? false, public? false (not overridden), primary_key? false https://hexdocs.pm/ash/dsl-ash-resource.html; lib/ash/resource/dsl.ex rows 15, 18, 19 no change needed: the contracts take only a name for each

The fact-check said the first lookups’ quoted sentences for G1, G2, G3, G5, G6, G7 and G8 were not literal text of Ash’s documentation; none of those sentences is used here.

7. The fixture on main (M1 vocabulary)

This is post.mx as it is on main after the alignment: packages/compiler/test/fixtures/post.mx, and the identical examples/blog/post.mx. It carries every alignment of Section 4 that is on main and the naming rule of Section 0 (tag names and attribute names only). Expressions keep arrow functions with declared parameters until M4 is designed (D16, D37); the author-chosen names (authorId, insertedAt, commentCount) are values and stay as in the file before the alignment (D33). Concise syntax.

resource="post" table="posts" domain="blog"
  attributes
    uuid-primary-key="id"
    attribute="title" type="string" allow-nil=false public
    attribute="body" type="string" public
    attribute="state" type="atom" constraints={ one_of: ["draft", "published"] } default="draft"
    create-timestamp="insertedAt"
    update-timestamp="updatedAt"

  relationships
    belongs-to="author" destination="user"
    has-many="comments" destination="comment"

  actions defaults=["read", "destroy"]

    create="create" accept=["title", "body"]
      change=({ post, actor }) => { post.authorId = actor.id }

    update="publish"
      change=({ post }) => { post.state = "published" }
      validate=({ post }) => post.title.length > 0 message="title required"

    read="published"
      filter=({ post }) => post.state === "published"
      sort=["-insertedAt"]

  policies
    policy=action_type("read")
      authorize-if=({ post }) => post.state === "published"
      authorize-if=({ post, actor }) => post.authorId === actor.id

    policy=action("publish")
      authorize-if=({ post, actor }) => post.authorId === actor.id

  calculations
    calculate="excerpt" type="string"
      value({ post }) {
        return post.body.slice(0, 200)
      }

  aggregates
    count="commentCount" relationship-path="comments"

One difference from the proposal this section first held. The proposal put require-atomic=false on publish (D21). require-atomic is vocabulary for M5, not on main, so the fixture keeps the pre-alignment form of publish (no such attribute); the contracts reject the attribute until M5 adds it. action_type("read") and action("publish") are Ash’s check calls action_type(:read) and action(:publish). They sit inside an expression, so they are JavaScript identifiers and keep Ash’s snake_case (Section 0). Differences from the file before the alignment (Appendix A): allow-nil, create-timestamp and update-timestamp; type="atom" with constraints; defaults as an attribute of actions; the policy condition as a check call; relationship-path; destination on the two relationship tags.


Appendix A. The vocabulary on main, after the alignment

History: the contracts as the M1 alignment left them on 2026-10-04, which are still the code on main until the realignment task. The vocabulary decisions that superseded them are records 0049 to 0056.

Everything here is read from contracts.ts as it is now (26 tags). Line numbers are not given: the file is the authority, and the tests in packages/compiler/test/contracts.test.ts pin each row. Helpers: str() is a string attribute with literalOnly; strings() is an array of strings, literalOnly; flag() is a boolean, literalOnly; code() is a function attribute, required; name() is the default attribute value, a required literal string. literalOnly means the authored value must be a literal, not an identifier (header comment of the file). It accepts an object or array literal, which is what constraints={ one_of: [...] } relies on. The exceptions to literalOnly are the function attributes and the policy condition, which is a call. closed() makes every contract list its attributes, attributeTags and children explicitly; an empty record means none. Child cardinality: {} is optional and at most once; { repeatable: true } any number; { required: true } exactly once; both flags at least once. Analyze helpers: nonEmpty(...) rejects an empty or whitespace string; nonEmptyList rejects []; listItems/checkItems reject blank or repeated items.

Tag Parents Attributes (type, required, literal-only) Children (cardinality) Analyze
resource #root value string, required, literal; table string, literal; domain string, literal attributes required once; relationships, actions, policies, calculations, aggregates optional once nonEmpty("value", "table", "domain")
attributes resource none uuid-primary-key, create-timestamp, update-timestamp optional once; attribute optional, repeatable none
uuid-primary-key attributes value string, required, literal (name()) none nonEmpty("value")
attribute attributes value string required; type string required, enum of ATTRIBUTE_TYPES (string, integer, float, boolean, atom, uuid, datetime); constraints untyped, literal; allow-nil boolean, literal; public boolean, literal; default untyped, literal none nonEmpty("value") and analyzeAttribute: constraints only for atom and required for it, an object literal naming one_of and nothing else, one_of a non-empty list of non-blank, non-repeated strings; default a string, number or boolean literal that fits the type (an integer for integer, one of one_of for atom)
create-timestamp, update-timestamp attributes value string, required, literal (name()) none nonEmpty("value")
relationships resource none belongs-to repeatable; has-many repeatable none
belongs-to, has-many relationships value required; destination string, required, literal none nonEmpty("value", "destination")
actions resource defaults array of string, literal create, update, read, destroy repeatable nonEmptyList("defaults"); analyzeDefaults: each item one of create, read, update, destroy (ACTION_TYPES), no blank or repeated items
create, update, destroy actions value required; accept array of string, literal change repeatable; validate repeatable nonEmpty("value"); listItems("accept", "accept")
read actions value required filter optional once; sort optional once; validate repeatable nonEmpty("value")
change create, update, destroy value function, required none none
validate create, update, destroy, read value function, required; message string, literal none nonEmpty("message")
filter read value function, required none none
sort read value array of string, required, literal none nonEmptyList("sort"); listItems("sort")
policies resource none policy repeatable none
policy policies value untyped, required (a check call or a list of them) authorize-if repeatable, required (at least one) analyzePolicy: each check is a call to action or action_type with one string literal or a non-empty string-literal array, no blanks or repeats; every action_type name is in ACTION_TYPES; action is not empty; a condition list is non-empty and means all checks
authorize-if policy value function, required none none
calculations resource none calculate repeatable none
calculate calculations value string, required; type string, required, enum of CALCULATION_TYPES (the attribute types without atom) value required once nonEmpty("value")
value calculate value function, required none none
aggregates resource none count repeatable none
count aggregates value required; relationship-path string, required, literal none nonEmpty("value", "relationship-path")

Rules that are not in any tag: exactly one resource per file, and “an empty file” are not in the contracts; MX has no root cardinality, so Mesh’s model stage enforces them (header comment of the file; PR #1 review round 3, item 2, with two tests that pin MX accepting both).

The fixture

The fixture is Section 7: packages/compiler/test/fixtures/post.mx, identical to examples/blog/post.mx. It parses with zero diagnostics. Its first version was a copy of MX’s own fixture; the alignment replaced that form (required, type="enum" values=[...], timestamps, defaults as a child, policy action-type="read", relationship="comments", resource="user" and resource="comment" on the relationship tags).

How expressions are treated is now ADR-0056: a body that is one expression is translated when Mesh can translate it, otherwise it runs in memory, and only a filter, a sort or a policy requires SQL.

Appendix B. What this page did not verify

  • Checked by the alignment (no longer open): an untyped contract attribute keeps literalOnly for an object literal (D9). An identifier is rejected (literal-only-constraints) and an object literal is accepted (every fixture with constraints).
  • The two values @mesh/model records where this page was silent are now checked (Section 6, row “model”): uuid-primary-key is allow-nil false and public true; create-timestamp and update-timestamp are allow-nil false and public false. They match what the model records.
  • The two sub-claims Section 6 keeps open: how Mesh spells the validation and check builtins (G3), and the {MyModule, :my_func, []} example of G5.
  • Where adapters and extensions are enabled for a project in the Ash sense (rows 3 and 5): the roadmap places it in the project configuration, M1 and M6.
  • Who proposed each finding in rounds 1 to 4 of the review of PR #1: the PR records what changed, not the author of each finding.

Appendix C. The M1 naming rule and introduction (history)

This was the opening of the page while the vocabulary copied Ash (ADR-0034, superseded by ADR-0049). It still explains the spelling of the M1 contracts in section 3.

C.0 The naming rule

Mesh names are Ash’s names in kebab-case with the trailing ? dropped. belongs_to becomes belongs-to, uuid_primary_key becomes uuid-primary-key, allow_nil? becomes allow-nil, require_atomic? becomes require-atomic. The mapping is mechanical and one-to-one: _ becomes -, a trailing ? is dropped, nothing else changes. A trailing ? is disallowed by ruling, not by MX. The MX maintainers first measured on MX main at 7a404916 that an attribute name takes letters, digits and ._:- and never ? (recorded in rulings of 2026-10-04, section “Lead decisions of the architecture-docs brief”), and later confirmed that the data target could allow it in attribute names; MX still allows it in tag names, as Marko does, and MX decision 144 reserves ?= as a future attribute operator and bans a trailing ? in attribute names on every target. The operator ruled on 2026-10-04 (table “Rulings after the decision review”, row “Trailing ? in attribute names”) that Mesh does not use it, in attribute names or in tag names. Reasons: 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 (public, allow-nil=false). The spelling is unchanged: Ash names in kebab-case with ? dropped. Mesh declares no tag and no attribute whose name ends in ? or contains _; a test asserts it (contracts.test.ts, “no tag name and no attribute name ends in ? or contains _”). _ would be accepted by MX in tag and attribute names; Mesh does not use it. Scope of the rule. It applies to tag names and attribute names only: the vocabulary. Three things are not vocabulary and the rule does not touch them. (1) Attribute values are strings the author writes: insertedAt in create-timestamp="insertedAt" is a value. (2) The names a resource author chooses for attributes, relationships and actions (the fixture’s authorId, commentCount) are values too; they stay as the author writes them. (3) Everything inside an expression is JavaScript, parsed by Babel, and must be valid JavaScript: a call name keeps Ash’s snake_case as an identifier (action_type("read"), before_action(...), set_attribute(...)), because action-type("read") would parse as a subtraction. Ash’s ^actor, ^arg, ^context, ^ref and ^tenant templates have no valid JavaScript spelling; Section 3.9 maps them to the declared parameters of the arrow function.

Decision record. 2026-10-04, recorded in rulings of 2026-10-04, section “Lead decisions of the architecture-docs brief”; a working decision (not an operator ruling); the operator may overrule. Alternative considered: Ash’s exact spelling with _ where MX allows it (belongs_to) and a different spelling only for booleans, which would have renamed every tag on main and left two conventions. Kebab-case matches what is on main, avoids renaming twice, and the operator revisits naming for MX after v1 (ADR-0034). Consequence: names that differ from Ash only by this rule are not deviations and are not listed in Section 4.

Attribute-name safety (M2). The builder rejects these fixed built-in object property names for every attribute-declaring tag, even when no action accepts the field: __proto__, constructor, prototype, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString, toString, valueOf, __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__. This is a restriction on author-chosen field names, not a vocabulary spelling change; it prevents prototype-sensitive input validation from silently losing or misreading fields.

C.1 Introduction

The vocabulary of Mesh is the set of tag and attribute names a resource file may use. A resource file is a .mx file: Marko syntax, parsed by MX, a separate project. Mesh invents tag names, not syntax. For each tag name there is one MX tag contract (CustomTag) that says which attributes, children and parents the tag allows, plus analyze hooks for rules a declaration cannot express. MX is core in Mesh, not an adapter (ADR-0043), so the 26 contracts live in the compiler package, packages/compiler/src/contracts.ts, with tests in packages/compiler/test/. (M0 moved them there. The alignment kept the count at 26: defaults and timestamps left, create-timestamp and update-timestamp arrived. The contracts.ts:N line numbers on this page are those of the file before the alignment, at a3b52f4; rows marked aligned describe the new form in the Mesh name column and the old one in the “On main today” column.) They are the only vocabulary code so far. PR #1 (https://github.com/svallory/mesh/pull/1) added them; PR #2 (https://github.com/svallory/mesh/pull/2) adopted MX’s unknownTags option; PR #4 (https://github.com/svallory/mesh/pull/4) changed one test.

Where the 26 came from. The names were copied from an MX test fixture (packages/targets/data/fixtures/ash-resource/post.mx in the MX repository; MX project notes, getting-started section 1). The Mesh copy is packages/compiler/test/fixtures/post.mx; it has 31 tag occurrences and 25 distinct names. The 26th contract, destroy, was added in round 2 of the review of PR #1. Several rules were inferred by the developer in PR #1 or required in four rounds of review of PR #1. The operator never ruled on them.

The ruling. On 2026-10-04 the operator ruled (rulings of 2026-10-04, table “Rulings after the decision review”, row “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.

It is recorded in ADR-0034 (copy Ash now, optimise after v1) and, for the third sentence, ADR-0041. The consequence stated in ADR-0034: where the contracts or the roadmap deviate from Ash in names, defaults or inferred rules, they are aligned with Ash unless MX cannot express it. Only those exceptions go to the operator. A second reason is used on this page for the exceptions that remain: the deviation conflicts with a decision record (the record is named). After v1 the vocabulary is reviewed for what reads naturally in MX; that is a breaking rename for every resource file, planned separately (ADR-0034, Consequences).

What “copy Ash’s DSL” means here. Ash is the Elixir resource framework Mesh is modelled on. Its DSL is organised in sections (attributes, actions, …), each holding entities (attribute, create, …) that take options. Section 3 maps every Ash section, entity and option that Mesh v1 touches to a Mesh tag or attribute, with its status. Section 4 lists each place where contracts.ts or the roadmap differs from Ash and the alignment that follows. Section 5 holds the two deviations that go to the operator. Section 6 holds the points where Ash’s form is not in the research, so the alignment cannot be written yet. Section 7 shows the fixture after alignment. Appendix A is the vocabulary on main, as it is.

Sources used on this page: Ash features (cited by section), Ash DSL and extensions, research synthesis, roadmap, rulings of 2026-10-04, MX project notes, getting-started, MX project notes, contract-extensions, MX project notes, updates, and the contracts file contracts.ts:N (line N of packages/compiler/src/contracts.ts). Where a detail (a default, an option name) is not in the research, the page says “not in the research” and does not fill it from memory.

C.2 How to read the mapping

Positional arguments. Ash writes attribute :subject, :string: name and type are positional (Ash features section 12 examples). An MX tag has one default attribute (written attribute="subject", and arriving in the tree as an attribute named value) plus named attributes (MX project notes, getting-started section 1). The rule used here: the first positional becomes the default attribute; a later positional becomes a named attribute carrying the name Ash gives it (type for attribute and argument, keys for identity name, keys, destination for a relationship, relationship_path for an aggregate), and otherwise the name Mesh has now, marked “inferred”. The positional names were checked against Ash’s documentation (Ash 3.34.0, checked 2026-10-04, G9 in Section 6, https://hexdocs.pm/ash/dsl-ash-resource.html): attribute name, type; belongs_to name, destination; has_many name, destination; has_one name, destination; create name (also read, update, destroy); calculate name, type, calculation (the third is optional); count name, relationship_path (the other aggregates add field; custom adds type); identity name, keys; argument name, type; policy condition (optional); validate validation; change change; prepare preparation. The two names that were inferred before the check, relationship_path and the relationship’s destination, are Ash’s own: the relationship’s destination is destination= on belongs-to and has-many. policy and calculate are the two on-main tags whose positional Mesh does not name as Ash does (the policy’s condition arrives as the default attribute value; calculate’s third positional is the child tag value); see D22, D27 and G2, G8.

Concise syntax. Every example is concise syntax, as in the fixture: indentation nests, no angle brackets (ADR-0041).

Names. The rule of Section 0. Ash atoms (:read) become strings ("read"). A bare boolean attribute means true (public); allow-nil=false is the explicit form (contracts.ts:116-122 reads a BooleanLiteral).

Status values. On main (with line); M3, M5, M7, M8 (the milestone the roadmap adds it in); M4 (the expression milestone); M6 (extension host); after v1 (roadmap section 6 or a ruling); not planned (roadmap section 6, “Not planned at all”); out of M8 (named as out of scope of that milestone); not in roadmap (neither scheduled nor excluded).