An Elixir module stored as a row in the node database — its code, the hash of
that code and the hash this node has accepted — compiled into the VM and
reachable through the scripts tool and by the node itself; Script.<Name>,
named after its module.
Acceptance is a comparison, not a flag
accepted?/1 is accepted_hash == code_hash and nothing else. There is no
boolean to set, so no write can accept a row by forgetting to clear something:
changing the code changes code_hash, and the row stops being runnable in the
same write. accepted_hash is nullable because a row can legitimately exist
unaccepted — that is what a synced row will be — and nil never equals a
hash, so an unaccepted row is refused by the same comparison. accepted/1 is
that comparison as a query, so no reader of the table spells it a second time.
What accepting means, and which doors may do it, is stated once in YmerNode's
Scripts section.
The denormalised pair
description and declarations are copied out of the compiled module at
every write. They are not the truth — the code is — and a boot compile
rewrites them when the module has come to say something else. They exist so
that list and the references seam are one query each: classifying a
reference must not need a compiled module, and YmerNode.References.Sources
runs on every read.
declarations is stored as JSON and comes back with string keys, which is
the shape the seam wants; YmerNode.Script.declarations/0 is the atom-keyed
shape a script's callback returns, and YmerNode.Scripts.Compiler is where
the two meet.
What the changeset checks, and what it cannot
Everything here is a value check on data the node itself produced one function
earlier — the compiler derived the name, hashed the code and read the
callbacks — so the changeset is a backstop against a bug in this codebase
rather than against a caller. The checks that matter to a caller live
upstream, in the compiler's parse and in acceptance's five refusals
(YmerNode.Scripts).
Summary
Types
Which door a script row arrived through — authored (an MCP write), pushed
(the CLI), synced (registry sync, later), shipped (the build: the example
script, planted on a fresh node database by a migration).
Functions
Narrows a query to the rows accepted?/1 answers true for — the same
comparison, spelled once for the database. Script.accepted() is the whole
table so narrowed; pass a query to narrow one already built.
Whether this row may run: its accepted hash equals its code hash.
The sha256 of a script's code, lowercase hex — the value both hash columns hold.
The origins a row may carry, in the order they were introduced.
Types
@type origin() :: String.t()
Which door a script row arrived through — authored (an MCP write), pushed
(the CLI), synced (registry sync, later), shipped (the build: the example
script, planted on a fresh node database by a migration).
Provenance, never permission: what a row may do is decided by accepted?/1,
and this only records how it got here. It is a plain string rather than an
Ecto enum so that a value the node does not know reads back as itself instead
of raising on load.
Functions
Narrows a query to the rows accepted?/1 answers true for — the same
comparison, spelled once for the database. Script.accepted() is the whole
table so narrowed; pass a query to narrow one already built.
Whether this row may run: its accepted hash equals its code hash.
Examples
iex> YmerNode.Scripts.Script.accepted?(%YmerNode.Scripts.Script{code_hash: "a", accepted_hash: "a"})
true
iex> YmerNode.Scripts.Script.accepted?(%YmerNode.Scripts.Script{code_hash: "a", accepted_hash: "b"})
false
iex> YmerNode.Scripts.Script.accepted?(%YmerNode.Scripts.Script{code_hash: "a", accepted_hash: nil})
false
The sha256 of a script's code, lowercase hex — the value both hash columns hold.
Examples
iex> YmerNode.Scripts.Script.hash("")
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
The origins a row may carry, in the order they were introduced.