The .mox language reference
Phero Lang compiles .mox modeling sources (inspired by Eclipse Xcore) into
a resolved Core IR artifact, from which backends generate Rust models, JSON
Schemas, and canonical instance JSON. This document is the normative language
reference; the pherolang CLI and crate docs cover implementation.
Grammar authority. The normative EBNF for every Phero Lang surface lives
exactly once, in the parser module docs
(crates/phero-syntax/src/parser/mod.rs, rendered as
phero_syntax::parser —
sections: .mox model sources, the shared actors_block, .actor
policy files, .ddd design sources, .evt event-contract sources,
.deploy deployment sources, .uc use case sources, and .cmmn
case management sources).
The sections below link to it
instead of restating the grammar; when this page and the parser disagree,
the parser wins. The .ifml surface’s authority is the EBNF in
crates/phero-ifml/src/parser/mod.rs (see docs/IFML.md); the
expression language is normative in docs/EXPRESSIONS.md.
Lexical rules
Section titled “Lexical rules”- Comments:
//to end of line,/* ... */block comments. Preserved verbatim bypherolang fmt. - Doc comments:
///line comments and/** ... */block comments on their own lines directly above thepackagedeclaration, a declaration, feature, or enum literal become that element’s description. Contiguous///lines join into one description separated by newlines (markdown source stays line-shaped); a blank line or an intervening non-doc comment detaches the run. Descriptions are carried in the IR and surface asdescriptionkeywords in generated JSON Schema and as doc comments in generated code (the package description is carried in the IR only). The.dddsurface joins doc runs identically (seedocs/DDD.md). - Strings: double-quoted with
\"and\\escapes. - Integers: decimal, optional leading
-. - Identifiers:
[A-Za-z_][A-Za-z0-9_]*. Any keyword can be escaped with a leading^(^classis an identifier). - Keywords:
package annotation as class extends interface enum type wraps opaque contains refers container opposite op derived true false vocabulary from version key facet actors actor agent capability grant permit forbid when obligation on never_both delegation purpose cedar import. Contextual words usable as plain identifiers:id readonly get set String int ... date— among themschema, which is special only directly after animportkeyword of a.moxfile (see Importing JSON Schema types), andsigil, special in the same position (see Importing Rune models), andto, which is special only on a delegation’stoline, andhuman, which is special only in a grant or delegation effect entry’s session-kind modifier position (see Session kinds).
Model structure
Section titled “Model structure”A file holds one package and any number of declarations. The full EBNF —
model, package_decl, annotation_decl, import_schema_decl,
import_sigil_decl, and every declaration form below — lives in the parser
module docs under .mox model
sources;
the sections that follow keep the language-level rules.
doc marks the optional doc-comment run described under Lexical rules; it
becomes the package’s description in the Core IR.
annotation_decl attaches a package-level annotation: annotation "<source>"
with an optional as <name> target. The source string and (empty) details
map are carried on the package as Annotation { source, details } in the
Core IR; the as <name> target is currently parsed but not persisted —
it is reserved for future per-declaration attachment. Annotations are
top-level only: they cannot appear inside class, enum, datatype, or actors
bodies.
Inline actors blocks
Section titled “Inline actors blocks”A .mox model may declare actors blocks alongside its classes, enums, and
vocabularies — the block grammar is the shared actors_block production of
the parser module docs, identical to the one in
.actor files (actor/agent with extends,
capability <name> on <type_ref>, purpose, grant with permit/forbid
entries with optional session-kind modifiers (see
Session kinds), when conditions, obligations, cedar
bodies, delegation, never_both). Each block lands on its declaring
package as an ActorsDef in the Core IR (Package.actors).
Blocks compile and validate exactly like the standalone surface: one block
per name per file, and across files of a multi-file model same-named blocks
pool their actors at validation (separation of duty and delegation checks
span the union). A block’s capabilities resolve against the union namespace,
so a block may bind to a class declared in another package, and every when
condition is type-checked against its capability’s class.
package nz.example.support
class Ticket { id readonly String ticketNo boolean internal int refundCents}
actors Support { actor Agent actor Manager
capability RaiseRefund on Ticket capability ResolveTicket on Ticket
grant Agent { permit RaiseRefund when (refundCents > 0 && refundCents <= 5000) }
grant Manager { permit ResolveTicket }
never_both { RaiseRefund, ResolveTicket }}The same block in a standalone Support.actor file (with
import "support.mox") produces an identical ActorsDef — the two surfaces
differ only in where the block lives and how the artifact is emitted (one
multi-package Model vs a standalone ActorModel).
A model may span multiple .mox files — one package per file, named by its
package declaration. Compiling several files produces one Core IR
artifact whose packages follow the input order (for the CLI, the sorted path
order of the expanded inputs). Declarations may reference types from
any package of the compilation:
- A bare single-segment name must be unique across all packages; if
several declare it, the reference is an error (
ambiguous type \X`; qualify as `p.X“) whose help lists the matching packages. - A qualified
<package>.<Name>must match exactly one package.
Cross-package refers, extends, and attribute types (enums, datatypes,
vocabularies) are allowed; constraint family and enum-literal checks see
through the package boundary. contains/container targets and opposite
pairings must live in the declaring class’s package: cross-package ownership
and cross-package opposites are errors.
Importing JSON Schema types
Section titled “Importing JSON Schema types”A .mox package can import JSON Schema types into its namespace — the
import_schema_decl production of the grammar authority
(import schema "<path>" (as <name>)?):
package nz.example.todos
import schema "../tests/conformance/models/import_schema/todo_item.json" as TodoItemimport schema "../tests/conformance/models/import_schema/todo_list.json"
/// The aggregate root, referring to two imported schema types.class TodoList { refers TodoItem[] items refers todo_list meta}The imported name is the as alias when present, otherwise the import
path’s file stem ("schemas/todo_list.json" imports as todo_list).
A stem-derived name must be a valid identifier (.mox identifier rules) —
anything else needs an explicit alias. The path resolves relative to the
declaring .mox file’s directory, like .actor imports.
v1 semantics are opaque. An import registers a nominal, feature-less
class in the importing package’s namespace: refers TodoItem resolves to
TypeRef::Class in that package, and the lowered IR carries an empty
ClassDef named TodoItem. No features, enums, or datatypes are derived
from the JSON content; structural lowering is future work. The content is
still validated — it must parse as JSON, and a declared import must be
provided at all.
The schema word is a contextual keyword (like format inside
datatype blocks and to on delegation lines): it is an ordinary
identifier everywhere else, and existing models may keep using schema as
a class, feature, or parameter name. Escaped ^schema is never the
keyword.
Collisions are errors naming both sources:
- an import name (alias or stem) that matches a declared class, interface, enum, datatype, vocabulary, or actors block of the same package;
- the same name imported twice (whether from the same or different paths);
- two alias-less imports whose file stems collide.
Malformed declarations are syntax errors: a missing path literal after
import schema, or a missing name after as.
Resolution contract. The driver stays filesystem-free: import content
arrives as text. pherolang check/ir/gen read each import file from disk
(relative to the model, as above) and hand it to the driver; a missing file
is a clean CLI error naming the path. Embedded callers (tests, language
servers) pass content through phero_driver::SchemaImports, keyed by the
(mox file path, import path) pair exactly as named; compile_str and
compile_files provide nothing, so a model with import schema
declarations compiled through them errors with
imported schema '<path>' was not provided.
pherolang fmt canonicalizes import-schema declarations into a section
directly after the package declaration, before every other declaration —
one per line, no blank lines between them (the .actor import rule).
Importing Rune models
Section titled “Importing Rune models”A .mox package can import a Rune DSL (.rosetta) file, whose namespaces
lower into synthetic Phero Lang packages — the import_sigil_decl production
of the grammar authority (import sigil "<path>", no alias):
package nz.example.pricing
import sigil "../examples/pricing/pricing.rosetta"
/// The lowered `pricing.model` package carries the Rune namespace's/// functions (signature-only operations with verbatim `"rosetta"` bodies);/// the `zonedDateTime` reference pulls in the `com.rosetta.model` builtin/// package. See `examples/pricing/pricing.mox` — the canonical example this fence/// mirrors.class Quote { String symbol double notional}The path resolves relative to the declaring .mox file’s directory,
like import schema. A whole namespace set is imported, not one type, so
the declaration takes no as alias (an as clause after import sigil
is a syntax error). Beyond the named file, pherolang check/ir/gen also
read every other *.rosetta file in the named file’s directory (walking
subdirectories, skipping dot and target directories) as candidate
files, so rosetta-internal namespace imports (import a.b.*,
import a.b.C) resolve transitively; the driver lowers only the namespace
closure the named files actually need, and candidates that end up
unselected are never diagnosed — an unrelated broken file next to the
import must not block the model.
Lowering. Each rosetta namespace becomes one synthetic Phero Lang package
named after the namespace, appended after every declared .mox package of
the compilation (the same namespace across files merges into one package).
Elements map:
| Rune | Phero Lang |
|---|---|
type T: <"…"> |
class T — attributes → attribute features, extends kept, the definition string → the class’/feature’s description |
choice C: <"…"> |
interface C (choice options do not survive: a Phero Lang interface declares no features); a type whose super type is a choice extends the interface |
enum E: <"…"> |
enum E — literals in declaration order with synthesized integer values 0..n-1 (Phero Lang enums require ints; this is the lowering contract); displayName "…" → the literal’s as label, definition → description |
typeAlias X: <"…"> t |
type X wraps t when t maps to a Phero Lang primitive, an opaque datatype otherwise |
Attribute cardinalities map verbatim ((1..1), (0..1), (0..*),
(2..10), …). Attribute overrides lower as ordinary features. Conditions,
annotation references, labels, rule references, and doc references are
skipped in v1.
A func or library function declaration lowers to a signature-only
operation on the namespace package’s functions (phero_ir::Operation —
the node domain ops use, minus a receiver): inputs → parameters with
their cardinalities (the required (1..1) stays unset, the domain-operation
convention), the output → the return type and the output’s cardinality
→ the operation’s return multiplicity (returnMultiplicity on the wire;
(0..1) and (0..*) lower verbatim so call sites type the call as
Option/List and backends emit the lifted return shape — phero-expr and
the Rust backend do), the definition string → the
description, and a library function’s array parameters carry a 0..*
multiplicity (a library function’s return carries no cardinality in the
sigil AST, so its return multiplicity always stays unset). A func without
an output cannot become an operation (Phero Lang
operations always return) and is skipped with a warning naming it. The
declaration’s complete Rune source is embedded ends-trimmed verbatim
(the declaration’s text without trailing whitespace — sigil spans consume
trailing blank lines; interior indentation is never reformatted) as the
operation’s "rosetta" body target. A statement-bearing function (a
dispatch head, or any non-empty conditions, aliases, or set/add
statements) additionally carries its lowered, type-checked statement body
beside it as the "rune" target — one canonical JSON string whose
expression slots are phero-expr programs; a fragment phero-expr cannot type
defers with a warning and the verbatim text stands alone (the generation-time
deferral contract). Making the statements executable is the Rune body
translation design, docs/RUNE-BODIES.md — landed: the
Rust backend emits a checked "rune" body as straight-line Rust and keeps
the todo!() stub only for functions without one (statement-less,
driver-deferred, or library functions); a present-but-invalid "rune"
payload is a generate-time error naming the function. Functions are
not types: they never join the .mox-side resolution namespace (an
attribute typed by a function stays an error), and builtin
com.rosetta.model functions stay implicit.
The builtin com.rosetta.model namespace — which sigil implicitly
wildcard-imports into every model — maps to Phero Lang primitives where
counterparts exist (boolean → boolean, string → String, int →
int, number → double, date → date); everything else it declares
without one (time, pattern, the dateTime/zonedDateTime family,
calculation, the SerializationFormat enum) lowers to an opaque
datatype/enum in the synthetic com.rosetta.model package so references
resolve. That package is emitted only when something from it is actually
referenced by lowered content. Only the imported user namespaces join
the .mox-side resolution namespace: a .mox reference to number or
time stays an unknown-type error (the coinciding string/int/
boolean/date resolve as the Phero Lang primitives they always were).
Skipped elements in v1: rule, report, external rule sources,
schema/body/corpus/segment, metaType, and annotation
declarations.
Collisions are errors naming both sources: a synthetic package name
(the rosetta namespace) that matches a declared .mox package of the
compilation is rejected, whether from a user namespace or the referenced
com.rosetta.model builtin. Everything lowered is ordinary Model
content — downstream resolution, .actor capability targets, .ddd
designs, and backends see it unchanged.
Sigil parse and resolution diagnostics render against the rosetta source, tagged with its path (the way multi-file models render per file); any error blocks the artifact. The CLI reads the rosetta files from disk (a missing file is a clean CLI error naming the resolved path).
Resolution contract. The driver stays filesystem-free: rosetta content
arrives as text. Embedded callers (tests, language servers) pass content
through phero_driver::SigilImports, keyed by the (mox file path, import
path) pair exactly as named — additional entries for the same mox path
are the candidate files described above. Every compile entry point takes
one phero_driver::DomainImports bundle holding the SchemaImports and
SigilImports maps; the empty DomainImports::default() provides
nothing, so a model with import sigil declarations compiled through it
errors with imported sigil '<path>' was not provided. (For the
.actor/.ddd entry points the bundle carries the .actor/.ddd
file’s domains’ import content, keyed by each domain’s path.)
pherolang fmt canonicalizes both import kinds into a section directly
after the package declaration, before every other declaration.
Classes and features
Section titled “Classes and features”The declaration forms — class_decl and the six feature kinds with their
modifiers, multiplicities, defaults, op bodies, and constraint blocks — are
normative in the parser module docs under .mox model
sources;
what follows is what they mean.
contains— by-value ownership (Ecore containment). The child’s container back-pointer is itsopposite.refers— a cross reference: a link, serialized as{"$ref": "id"}.container— the owner slot; always0..1, never written with a multiplicity. Opposites are mutual and validated:containspairs withcontainer,referswithrefers.- Modifiers:
readonlysuppresses setters (and marks schema propertiesreadOnly);iddeclares identity (carried; semantics land with Tier 1). Both warn when applied toop. opbodies are per-target:{ rust { ... } csharp { ... } }blocks whose text is embedded verbatim by the backend that owns the target (rustin Tier 1).expris the Tier-2 pseudo-target: it holds the neutral expression language, which every backend lowers (seedocs/EXPRESSIONS.md). Arustand anexprbody on the same operation conflict. Body-less operations stay abstract hooks, implemented by hand in target code.derivedfeatures are computed, not stored; a body must be the neutral expression language in a singleexpr { ... }block (Tier 2), e.g.derived String citation { expr { title } }. Backends lower it into a getter; a bare{ ... }body is rejected.- Defaults: string/int/boolean literals or an enum literal name (for enum-typed attributes only).
- Constraints (attributes only) declare value bounds, e.g.
String sku { pattern "[A-Z]{3}-[0-9]{4}" minLength 3 maxLength 12 }orint stock { minimum 0 maximum 1000 }. Each keyword may appear at most once. A constraint family is admitted by the most explicit type knowledge available: the string family (pattern/minLength/maxLength) requires astringprimitive; the numeric family (minimum/maximum) requires a numeric primitive — the integer primitives and the IEEEfloat/doublealike (bounds are declared as integers and hold for the floating values they bound; IEEE semantics apply, so the expression language’s checked-overflow rule R1 is not a concern here). A datatype-typed attribute defaults to the string family — its platform type is opaque — and rejects numeric bounds. A vocabulary-typed attribute follows itskeyfacet’s declared primitive type (Stringadmits the string family, a numeric key the numeric family); constraints are rejected outright when the key facet cannot be resolved. An enum-typed attribute carries a dual value space: all five keywords apply — the string family bounds literal names, the numeric family bounds literal values — and both may coexist; a numeric or length bound that admits zero literals is a compile error, whilepatternis allowed but never statically validated (descriptive only). Class and interface types take no constraints. Length bounds must be non-negative andmin≤maxin both families. On a many-valued attribute the value bounds apply to the elements. The sixth keyword,unique, takes no value (String[] tags { unique }) and is the one collection-level constraint: it is admitted only on many-valued attributes — of any element type — and rejected on single-valued ones. It is schema-only: it surfaces asuniqueItems: trueon the attribute’s array schema, with element equality following JSON value equality, and implies no runtime validation. They surface as the JSON Schema keywords of the same names (pattern,minLength,maxLength,minimum,maximum,uniqueItems).
Multiplicity shorthand: [] = [0..*]; absent: attributes are 1..1,
contains/refers are 0..*, container and derived are 0..1.
A class may declare its storage table right after the name:
class PayRunStatus table "pay_run_status" { ... } (the table_annotation
production of class_decl, normative in the parser module docs). The
driver carries the override verbatim as the IR’s
ClassDef.tableName; without it, consumers derive the table from the
class’s stripped module name, exactly as they derive the module path
itself. table is an ordinary identifier everywhere else — a class,
feature, or enum may still be named table — and the annotation is class
grammar only (enums have no table).
Downstream, consumers derive one module path per declaration: the name
with one trailing Type suffix removed, then snake_case. PayRun and
PayRunType both strip to pay_run; TypeWriter keeps its name (the
suffix strips only at the end, once); PayRunTypeType strips to
pay_run_type. Two declarations in one package that strip to the same
identifier — classes or enums, in any combination — would generate the
same module path and one overwrites the other, so the compiler rejects
the pair (E0300, spanning the later declaration and naming both).
Classes that map to the same effective table name (a declared table
override, else the derived default) are rejected the same way (E0301) —
without an override this is implied by a stripped-name collision, so
E0301 fires only when an override forces two distinct module names onto
one table. A pair where either class is a containment target (some
class declares a contains feature targeting it) is exempt: a contained
value object shares its owner’s module downstream by design, so the
entity + value-object pairing (PayRun containing PayRunType)
compiles. Rename one of the declarations to resolve an E0300; give one
class a distinct table "…" to resolve an E0301.
Register classes
Section titled “Register classes”A register declaration declares a register class — an org/role noun
space the generated app can list, join, and reference (issue pherolang#181):
package nz.example.org
/// A role in the organisation.register Role { /// The role's display name. String display_name String[0..1] badge = "none" String code { pattern "[A-Z]{2,8}" minLength 2 }}
/// A person holding zero or more organisational roles.class Person { id String email refers Role[] roles}The register_decl production is normative in the parser module docs under
.mox model sources.
A register body holds the attribute production only — attributes with
their modifiers, multiplicities, defaults, and constraint blocks, exactly
like a class body. Relationship features are not part of the v1 register
grammar: contains, refers, container, op, and derived in a
register body are parse errors, as are extends and the table "…"
annotation. (Other classes may still refers/contains a register class;
the flat noun space means a register class itself declares no relations.)
A register declaration lowers onto the standard Core IR as a plain
ClassDef — its attributes as attribute features with the same stable-ID
contract — plus the additive kind marker (phero-ir wire-contract rule 16):
"kind": "register" on the serialized class. The marker is the only
difference: ordinary classes carry no kind field at all, so models without
register declarations compile to byte-identical artifacts, and every
backend treats register classes as ordinary classes.
Register classes join every existing class-level rule unchanged: the
package namespace (a register class is a class for refers/extends
targets and for the union namespace), the duplicate-declaration check, the
stripped-name and effective-table uniqueness rules (E0300/E0301), and
feature-id assignment.
register is a contextual word, not a keyword: it is special only at
the top-level item position (no other .mox item starts with a bare
identifier). A class, feature, enum, or datatype may still be named
register, and the word stays a plain name inside every body. Escaped
^register is always an ordinary identifier.
Primitives and the date type
Section titled “Primitives and the date type”Attribute, parameter, and facet positions admit the built-in primitives
String int long short float double boolean byte char and, since issue #9,
the calendar date: a timezone-less, time-less civil day that
serializes as an ISO-8601 YYYY-MM-DD string. In expressions a date value
is written date("2026-09-17") and carries typed ordering plus the
calendar algebra (plus_days, plus_months, diff_days) — the normative
rules are R5–R8 in docs/EXPRESSIONS.md. A declared type shadows the
like-named built-in, so a pre-existing type Date wraps opaque { ... }
datatype keeps working; write lowercase date for the primitive. Dates
take no declared defaults (a = "…" default on a date attribute is a
compile error; generated required date fields anchor at the epoch
1970-01-01) and admit no constraint keywords. datetime does not exist in
this milestone, and the expression language has no clock: “as at date D”
queries take dates as parameters or attributes. The date facet type
(facet date …) is likewise not supported yet.
Enums, datatypes, interfaces, vocabularies
Section titled “Enums, datatypes, interfaces, vocabularies”The declaration forms — enum_decl (with literals), type_decl (with the
datatype block’s bindings, format entry, and create/convert bodies),
interface_decl, and vocabulary_decl — are normative in the parser module
docs under .mox model
sources.
- Enums carry integer values (required) and optional labels:
Mystery as "M" = 0. Backends emit real enums; canonical JSON uses the literal name. - Datatypes wrap platform types opaquely:
type Date wraps opaque { rust "chrono::NaiveDate" ... }. Bindings are per-target hints, never generated dependencies. The block may also declare at most onecreateand oneconvertentry — target-tagged raw bodies of the same shape asopbodies, carried on the IR’sDatatypeDefnext to the bindings. A datatype may also declare oneformathint:type Email wraps String { format "email" }.formatis a reserved key inside the datatype block: the unescaped key declares the format (never a target binding), at most once, and a binding target literally namedformat(only writable escaped,^format "…") is an error. The hint surfaces as the JSON Schemaformatkeyword on that datatype’s schema in both profiles; it is a wire-level contract only — no runtime validation is implied. - Vocabularies reference well-known external code sets. The declaration
names the source, version, key facet, and typed facets. Snapshots are
vendored to
vocab/<source>@<version>.jsonand pinned by sha256 inmodel.lock— onlypherolang vocab fetchtouches the network; compilation is hermetic. Entries are embedded in the IR, and every backend materializes the vocabulary as a closed set of keys with facet accessors.
Actor policy files (.actor)
Section titled “Actor policy files (.actor)”Actor policy surfaces can also live in standalone .actor files, compiled
separately from the domain and emitted as their own ActorModel artifact for
the Cedar backend. The file grammar (actor_file, import_decl) and the
shared actors_block production it consists of are normative in the parser
module docs under .actor policy
files;
what follows is what it means.
The actors block grammar is identical to the inline actors block of a
.mox model. Import paths are resolved relative to the .actor file’s
directory and may name any .mox domain; multiple imports are allowed and
duplicates are collapsed. An imported domain’s own import schema and
import sigil declarations are provided too (the CLI reads that content
from disk relative to the domain file), so capabilities may target lowered
JSON-Schema and Rune types like any other class. The compiled policy set is the union of the
actor file’s blocks followed by every imported domain’s inline blocks
(in that order), so capabilities typecheck against the imported domain’s
classes (permit EscalateTicket when (amount > 0) resolves amount on
Ticket), and the separation-of-duty checks span files. when conditions
are restricted to a decidable subset at Cedar generation time — literals,
primitive attributes, and the operators — and anything without a Cedar
mapping (let, if, lambdas, collection algebra, ?., ?:, null, list
literals, operation calls, and date attributes or date("…") literals,
since Cedar has no date type) is a generate-time error naming the
capability.
agent declares an autonomous LLM agent; the plain actor remains the
human principal. Both take the same optional extends clause, and a
declared kind is inherited: an actor that declares no kind takes the
nearest ancestor’s declared kind, defaulting to human. extends confers
authority: an agent’s effective permits are its own grants plus its
ancestors’ (the same rule gen tools and the delegation containment
check use), so generated Cedar output addresses every holder — the
action’s principalTypes list the actors whose effective permits (or
forbid bindings) reach it, and each grant entry emits one policy per
holding actor, because Cedar’s is matches the exact principal type and
hierarchy membership (memberOfTypes) alone would leave an agent’s
request validated but unauthorized.
Session kinds
Section titled “Session kinds”A grant or delegation effect entry may carry a session-kind modifier between
the effect and the capability name — permit human SubmitPayRun when (payRunStatus == PayRunStatus.Draft) (human sessions only),
forbid agent IngestHours (automation sessions only), permit SubmitPayRun
(unchanged: any session). In context:
package payroll
enum PayRunStatus { Draft = 0 Approved = 1 }
class PayRun { id String id PayRunStatus payRunStatus}
actors Payroll { actor Finance agent Helper extends Finance capability SubmitPayRun on PayRun capability IngestHours on PayRun
grant Finance { permit human SubmitPayRun when (payRunStatus == PayRunStatus.Draft) forbid agent IngestHours }}The semantics are a closed three-valued set on the entry:
| Modifier | Meaning | IR (GrantEntry.sessionKind) |
|---|---|---|
human |
human sessions only | "human" |
agent |
automation (LLM-agent) sessions only | "agent" |
| (absent) | any session kind — the default | any (omitted on the wire) |
The modifier is modeling-only in v1: it is carried in the IR so models
can express session-kind intent, but no enforcement mapping exists yet. The
Cedar backend refuses any entry whose kind is not any as a generate-time
error naming the capability and the grant authority (emitting a restricted
entry as an unconditional policy would widen the grant), and the downstream
enforcement semantics — how a human- or agent-session restriction joins the
codegraph v1 API-key authentication model — are explicitly deferred pending
the codegraph security sign-off (pherolang#181). Until that sign-off lands,
session kinds change nothing about how credentials authorize sessions.
agent is the real lexer keyword — a capability named agent needs the
^agent escape, so an unescaped agent in the modifier position is always
the modifier. human is contextual: it is the modifier iff the next token
is a valid capability-name token, so
permit human Xrestricts the permit ofXto human sessions;permit human when (...)permits a capability literally namedhumanwith a guard (whenis a keyword, not a name);permit human obligation doneattaches an obligation to the capabilityhuman;permit robot Xis a parse error — no other kind words are accepted.
Delegation entries reuse the grant-entry grammar, so a delegated entry may
carry the same modifier. The formatter renders the modifier on the entry’s
line (permit human X).
Blocks declare obligations with obligation <name> lines — the named
follow-through actions entries attach — under the same discipline as
purposes: several per block, duplicates within a block are errors, and the
declared set pools union-wide across blocks. Grant and delegation entries
may only name declared obligations; an undeclared name is a compile error
naming the grant (by actor) or delegation and the obligation.
A delegation is a named transfer of authority from one actor to another:
from and to are required, in that order, and the body holds only effect
entries (cedar { ... } is rejected inside a delegation; an empty body is
legal). Blocks declare purposes with purpose <name> lines: several per
block are allowed, duplicates within a block are errors, and the same name
in different blocks is legal. A delegation may carry one optional purpose
line — after to, before the entries — naming a purpose declared in the
union of the file’s blocks and every imported domain’s inline blocks; an
undeclared name is a compile error naming the delegation and the purpose.
The driver enforces a containment invariant: every capability a
delegation permits must already be an effective permit of the target agent
— through its own grants, inherited ones included — so a delegation cannot
confer authority the target does not already hold, and to must name an
agent. never_both exclusivity is checked over grant and delegation
permits combined. In Cedar output delegations surface only as evidence
comments (// delegation Name: from -> to (N capabilities), suffixed
purpose: <P> when a purpose is declared); like never_both, the driver
enforces them at compile time, and runtime enforcement — of permits and
purpose alike — belongs to the authorization gateway. pherolang gen tools
projects this authorization model into each agent’s tool manifest — the
agent’s effective grant permits plus the delegation bindings scoped to it;
human actors are omitted.
pherolang fmt formats .actor files with the same canonical layout rules
(imports first, one per line; then blocks).
Canonical instance JSON
Section titled “Canonical instance JSON”The cross-language exchange format ($type/$id/$ref):
{ "$type": "phero.instance", "formatVersion": 2, "model": "nz.example.library", "objects": [ { "$type": "Library", "$id": "library/0", "name": "Default Name", "books": [ { "$type": "Book", "$id": "book/0", "title": "Dune", "authors": [ { "$ref": "writer/0" } ] } ] }, { "$type": "Writer", "$id": "writer/0", "name": "Frank", "books": [ { "$ref": "book/0" } ] } ]}Rules: ids are <singular>/<n> in per-class insertion order; containment is
inlined; container features are never serialized (reconstructed from
nesting on load); cross references are $ref links; enums serialize the
literal name, datatypes their inner string, date attributes their
ISO-8601 YYYY-MM-DD string (loading strict-parses it); optional attributes
are omitted when unset; loading is strict (unknown features, serialized
containers, duplicate ids, unresolved refs are errors). load(save(x)) == x
is a tested property, not an aspiration.
| Command | Purpose |
|---|---|
pherolang check <file>... |
validate; ariadne-rendered diagnostics grouped per file (every surface: .mox, .actor, .ddd, .evt, .deploy, .uc, .cmmn, .ifml; a directory argument enters project mode: every supported surface under it is gathered into one snapshot and checked independently, each file’s imports resolving map-first from the snapshot and sibling artifacts reused compile-once) |
pherolang ir <file>... -o <out> |
emit the Core IR artifact (.actor: the ActorModel artifact; .ddd: the DDD design artifact; .evt: the EventModel artifact; .uc/.cmmn: the standalone artifact; several .mox: one multi-package model) |
pherolang build <paths>... -o <dir> |
one JSON artifact per supported input file at <dir>/<input path>.json — the pherolang ir artifact for that surface; the output directory defaults to target/phero; directories enter project mode like check; exits 1 when any file fails, while compiling files still emit |
pherolang gen rust <file>... -o <dir> |
arena-based Rust models |
pherolang gen json-schema <file>... --profile wire|api -o <dir> |
JSON Schema |
pherolang gen cedar <file>... -o <dir> |
Cedar policies + schema (.mox: inline blocks; .actor: file + imported domains) |
pherolang gen tools <file>... |
per-agent tool manifest JSON on stdout (.mox: inline blocks; .actor: file + imported domains) |
pherolang vocab fetch <file> |
vendor + pin vocabulary snapshots |
pherolang fmt [--check] <files>|- |
canonical formatting (comments kept) |
pherolang lsp |
language server (stdio) |
Surfaces beyond this reference
Section titled “Surfaces beyond this reference”Every surface follows the same pipeline — check, ir, and fmt dispatch
on its extension; each compiles to its own versioned artifact consumed by
downstream tools. This page is the .mox/.actor reference; the others:
.ddd— a Sculptor-style DDD design layer (applications, modules, services, repositories, searches, and entity state machines) compiled against the domain models it imports. See docs/DDD.md..evt— event contracts: versioned events, the channels that publish them, and the subscriptions that deliver them, typed against the imported domains. See docs/EVENTS.md..deploy— deployment projections: one logical application bound to its designs and flows, projected onto capability-described targets through profiles and policies. See docs/DEPLOY.md..uc— SysML v2 use case patterns: subjects, actors, anchored messages, performed steps, and successions, composed across all four import buckets. See docs/USECASES.md..cmmn— CMMN 1.1 case management: cases, typed case files, plans, and sentry criteria, lowered byphero-backend-cmmnto the cmmn-rs engine’sCaseDefinitionJSON. See docs/CMMN.md..ifml— interaction flows over the reference pattern library, type-checked against the imported domains. See docs/IFML.md.
The examples directory (examples/, see its README) carries a canonical
example for every surface — the support-desk stack exercises seven of them
over one domain.