Skip to content

Appendix A: Grammar

This appendix is dense lookup. For learning the language, read the book starting at the Foreword.

// single-line
/* multi-line
comment */
identifier = letter (letter | digit | "_")*
stable_id = "#" alphanumeric+
qualified_name = identifier ("." identifier)*
quoted_string = '"' (any_char - '"' | '\"')* '"'
| '"""' (any_char* - '"""') '"""' (* triple-quoted: no escapes *)
type, in, export, use, from,
aspect, aspects, required, cascade, append,
override, drop,
process, subprocess, view,
each, if, else, select, one, try, catch, parallel, branch,
join, race, out, await, reversible, unwind, on, do, go, as, dist, fail, finish,
focus, group, by, layout, include, exclude,
true, false

aspect is the hard keyword heading every membership/classification form (aspect k: v, aspect { … }, aspect k for …). aspects still lexes as a keyword but is reserved-with-error: the old aspects { … } / aspects.x forms still parse (for recovery) but raise a 'labels' was renamed to 'aspect' diagnostic; no type/interface/module may be named aspects.

focus is now the view emphasis clause (§View Declarations) — it highlights matching elements and never changes which nodes the view selects. layout, include, and exclude remain reserved but are legacy: each still parses, but only to raise a targeted diagnostic pointing at its replacement (layout → pin a position via style; include/exclude → select with show/hide).

export, use, from, and as are valid both in a .arch file (file-scoped use … from imports — as renames locally — and export type / export interface / export subprocess) and in a package.archspace manifest.

package, name, version, widgets, repo, commit, and dependencies are recognized as field/block names in the manifest grammar but are not reserved words — they don’t shadow identifiers anywhere else.

Module types (service, database, system, …), surface types, and interface types (rest_create, rest_read, kafka, …) are not reserved keywords — they’re user-defined identifiers introduced by type module …, type surface …, type interface … declarations.

Base types. module, surface, interface are pre-registered base types; they’re identifiers, not keywords. process and view are pre-registered base types and reserved keywords because their bodies have specialized grammar productions.

{ } ( ) [ ] : , ; . * > = | \

The process-step arrow is > (single GT); = binds a step result (order = Customer > Orders.CreateOrder); * is the cascade-group marker (cascade *); ; separates steps inline within a block/body; | (fan) and \ (snake) are the leading line glyphs in a process body — | writes the step against the previous sibling’s head, \ against its tail. The grammar fragments below reflect this.

program = declaration* ;
declaration = use_decl
| type_decl
| module_decl
| process_decl
| view_decl
| subprocess_decl
| process_step (* v0.7: a bare step at root — an anonymous
single-step process; caller is explicit at
root, no enclosing module *)
;
(* `use` is valid at the top of a `.arch` file (file-scoped) and in a
`package.archspace` manifest (package- or space-scoped). *)
use_decl = "export"? "use" use_targets "from" dotted_name ;
use_targets = "*" | use_import ("," use_import)* ;
use_import = identifier ("as" identifier)? ;
dotted_name = identifier ("." identifier)* ;
type_decl = "export"? "type" stable_id? parent_type identifier type_body? ;
parent_type = qualified_name ; (* validator rejects 'process' and 'view'; may be space-qualified *)
type_body = "{" type_member* "}" ;
type_member = type_field_decl
| aspect_decl (* replaces type_label_block *)
| type_aspect_blank
| type_subdecl
;
type_field_decl = field_modifier* ( "required" field_path
| field_path ":" value field_body?
) ;
field_modifier = ( "cascade" "*"? ) | "append" ;
field_body = "{" field_body_member* "}" ;
field_body_member = ( "required" | "cascade" | "append" )*
field_path ( ":" value field_body? )? ;
field_path = identifier ("." identifier)* ;
(* `required aspect <key>` — a mandatory aspect blank on a type body.
`cascade`/`append` are rejected on aspects; aspects always cascade with
override semantics (Spec §4.4). *)
type_aspect_blank = "required" "aspect" aspect_key ( ":" aspect_value_list )? ;
type_subdecl = "required"? ( surface_decl | interface_decl | module_decl ) ;
value = identifier | quoted_string | number | "true" | "false" ;
module_decl = type_name stable_id? identifier? in_clause? module_body? ;
(* v0.7: the name (identifier) may be OMITTED for a CUSTOM-typed nested module —
it auto-takes its type name, snake_case→UpperCamelCase (type `database` →
`Database`). Validator constraints not in the EBNF: base `module` does not
qualify; the applied type must be used exactly ONCE in the parent; no
stable_id until promoted; it is NOT a TODO (fully resolved). *)
type_name = identifier ; (* registered module type *)
in_clause = "in" qualified_name ;
module_body = "{" module_member* "}" ;
module_member = field_assignment
| description
| aspect_decl (* replaces labels_block *)
| surface_decl
| interface_decl
| module_decl (* nested submodules *)
| process_decl (* module-scoped process *)
| subprocess_decl (* reusable helper *)
| process_step (* v0.7: bare step — anonymous single-step
process; implicit caller = this module *)
| drop_stmt
| override_decl
;
description = quoted_string ;
field_assignment = field_path ":" value ;
(* ASPECT — the keyword takes no dot after it (`aspect deployment.zone`, not
`aspect.deployment.zone`). An inline entry's value is OPTIONAL (a bare
`aspect domain` is a valueless blank). A bare STRING value is a
classification (a shared group identity); a bare MODULE-REF value is a
membership — the ref must resolve to a real module, which becomes a
*place* on that key's plane (Spec §4.4). Block entries are separated by
`;` or a newline — NEVER a comma; `aspect { a: "x", b: "y" }` is a parse
error, write `aspect { a: "x"; b: "y" }` or one entry per line. *)
aspect_decl = "aspect" ( aspect_inline | aspect_block ) ;
aspect_inline = aspect_key ( ":" aspect_value_list )? ;
aspect_key = identifier ("." identifier)* ;
aspect_block = "{" aspect_entry* "}" ; (* SINGULAR keyword — one block, many keys;
entries separated by `;` or a newline *)
aspect_entry = "drop" aspect_key
| "required" aspect_key ( ":" aspect_value_list )?
| aspect_key ":" aspect_value_list
;
(* Aspect VALUES accept a scalar, a bracketed multi-value list (multi-
membership — `tag: ["edge", "public"]` joins both), OR a module reference
(dotted — places nest, e.g. `Euro.DC1`) with an optional `as <instance>`
naming the member's instance at that place. `as` is legal ONLY in an
aspect value — in a general field value it is a parse error. Multiple
bare values on one line are comma-separated (`Euro as active, Euro as
standby`); trailing `,` is a parse error. *)
aspect_value_list = aspect_value ( "," aspect_value )* ;
aspect_value = quoted_string | number | "true" | "false" | bracket_array
| qualified_name ( "as" identifier )? ;
bracket_array = "[" ( aspect_value ( ","? aspect_value )* ","? )? "]" ;
drop_stmt = "drop" qualified_name ;
override_decl = "override" ( surface_decl | interface_decl | module_decl ) ;
surface_decl = surface_type identifier surface_body? ;
surface_type = "surface" | identifier ; (* "surface" or user-defined *)
surface_body = "{" surface_member* "}" ;
surface_member = field_assignment
| description
| aspect_decl (* classification/required/drop forms only —
a surface confers no membership presence *)
| surface_decl (* nested surfaces *)
| interface_decl
| drop_stmt
| override_decl
;
interface_decl = "export"? type_name identifier interface_body? ;
type_name = identifier ; (* registered interface type *)
interface_body = "{" interface_member* "}" ;
interface_member = field_assignment
| description
| aspect_decl (* classification/required/drop forms only —
an interface confers no membership presence *)
| drop_stmt
;
(* v0.7 removed the `subscribes:` member. An event is an async-kind interface
(e.g. `kafka`) reached by an ordinary `>` process edge — no subscription
wiring on the interface. *)
process_decl = "process" stable_id? identifier in_clause? "{" process_body "}" ;
process_body = ( description | process_step )* ;
process_step = ( simple_step step_name? )
| note_step
| each_step
| if_step
| select_step
| try_step
| parallel_step
| reversible_step
| await_step
| do_step
| go_step
| owner_block
| fail_step
| finish_step
;
(* OWNER — who performs the node. A qualified name (module / actor / earlier-bound
name), `|` (fan = the previous sibling's head), or `dist` (distributed /
emergent — no coordinator). Omitted = inherit from the nearest enclosing owner,
else a TODO. `dist` is control-only (a call always has a caller) and does NOT
propagate. Resolution: explicit → `|` → nearest non-dist enclosing → TODO. *)
owner = qualified_name | "|" | "dist" ;
caller = qualified_name | "|" ; (* note / `do` / unwind-call owner:
never `dist`, never `\` *)
call_caller = caller | "\\" ; (* `\\` (snake) is call-only *)
(* OWNER BLOCK — set the owner (or `dist`) for a run of steps. *)
owner_block = ( qualified_name | "|" | "dist" ) block ;
(* CALL — caller (= owner) left of `>`, may be omitted (anonymous head / bare) or
`|` (fan) / `\` (snake, call-only). Hops CHAIN: `A > B > C` desugars to `A > B ; B > C`, each hop's
caller being the top module of the previous callee; binding captures the final
hop; linear only. A call_target resolving to a MODULE synthesizes an anonymous
interface (a TODO); a trailing "." marks an unspecified interface (a TODO). *)
simple_step = ( identifier "=" )? call_caller? ( ">" call_target arg_list? )+ ;
step_name = "as" identifier ; (* name a step for `go` / diff *)
(* A path. A single "." is the sole separator. A TRAILING "." marks the terminal
interface as unspecified/anonymous (a TODO). A reserved keyword is legal in
member position after "." (`Worker.process`) — never as the first segment. *)
call_target = identifier ( "." member_seg )* "."? ;
member_seg = identifier | keyword ;
arg_list = "(" ( arg ("," arg)* )? ")" ;
arg = identifier ; (* bare intent names; not type-checked *)
(* NOTE — owner-anchored free-form string, no edge. The owner is REQUIRED
(`Orders "…"`); a bare string is a description, not a note. *)
note_step = caller quoted_string ;
(* All heads / conditions / labels / await targets below are FREE-FORM: rendered,
not resolved or validated. A body is a brace block or a single `:`-step. *)
(* BRANCH — exclusive choice. A condition with no body = a draft branch. *)
if_step = owner? "if" condition? body?
( "else" "if" condition? body? )*
( "else" body? )? ;
condition = identifier | quoted_string ;
(* SELECT — multi-way, INCLUSIVE (every matching case runs). `one` = first match,
N = first N matching in order, bare = all matching. *)
select_step = owner? "select" select_count? select_head? "{" select_case+ "}" ;
select_count = "one" | number ;
select_head = identifier | quoted_string ;
select_case = case_label body ;
case_label = identifier | quoted_string ;
(* LOOP / RETRY. `each` iterates. `each <bound> try { } catch { } else { }` = the
retry form (catch = restoration + retry; success exits; bound exhausted → else).
A `try` *nested* in a plain `each` body is a per-iteration guard. *)
each_step = owner? "each" each_head? ( retry_tail | body ) ;
each_head = ( ( identifier | quoted_string ) ( "in" ( qualified_name | quoted_string ) )? )
| number ;
retry_tail = "try" block ( "catch" catch_label? body )* ( "else" body )? ;
catch_label = identifier | quoted_string ;
(* GUARD — plain try/catch: handle the failure, continue forward (no retry). *)
try_step = owner? "try" body ( "catch" catch_label? body )* ;
(* PARALLEL — concurrent branches with a PREFIX merge clause. `out` = detach (no
wait); `join N` = proceed at N, keep losers (N omitted = all); `race N` =
proceed at N, cancel losers (N omitted = 1). The merge may be a boolean over
branch names. `select` = concurrent inclusive; `each` = fan-out. *)
parallel_step = owner? "parallel" "out"? merge_clause? parallel_body ;
parallel_body = ( "each" each_head block ) (* fan-out: a loop body *)
| ( "select" select_count? "{" branch+ "}" ) (* concurrent inclusive *)
| ( "{" branch+ "}" ) ; (* plain branches *)
merge_clause = ( "join" | "race" ) merge_spec? ;
merge_spec = number | bool_join ;
bool_join = bool_and ( "or" bool_and )* ; (* `or`/`and` case-insensitive; no `not` *)
bool_and = bool_atom ( "and" bool_atom )* ;
bool_atom = identifier | "(" bool_join ")" ; (* branch names only *)
(* BRANCH — a parallel branch: optionally NAMED (the name a boolean merge refers
to), then a one-liner, a block, or a nested control node. *)
branch = "branch" branch_name? ( ( ":" process_step ) | block | control_step ) ;
branch_name = identifier ;
control_step = if_step | select_step | each_step | try_step | parallel_step
| reversible_step | await_step ;
(* AWAIT — timed / event wait, owned by the waiter; target free-form. *)
await_step = owner? "await" await_target ;
await_target = quoted_string | ( identifier | number )+ ;
(* REVERSIBLE — saga span; trigger defaults to `on error`. Each forward step or
block may carry an `unwind` (inline catch-and-rethrow). `unwind` may NOT nest
inside control — wrap the control in a block and put `unwind` on the block.
A line may be reversal-only (`unwind` with no forward action). *)
reversible_step = owner? "reversible" trigger? "{" reversible_item* "}" ;
trigger = "on" ( identifier | quoted_string ) ;
reversible_item = description
| ( ( simple_step | block ) unwind_clause? step_name? )
| ( unwind_clause step_name? ) (* reversal-only line *)
| note_step | do_step | go_step | fail_step | finish_step
| control_step ;
unwind_clause = "unwind" unwind_action ;
unwind_call = ( identifier "=" )? caller? ( ">" call_target arg_list? )+ ;
unwind_action = unwind_call | fail_step | finish_step | do_step | block ;
(* RE-ENTRY / REUSE — structural ops, OWNERLESS. `go` re-enters a step named with
`as` and resumes forward. `do` splices a subprocess; an optional caller prefix
is the TEMPLATE default-caller for the subprocess's owner-omitted steps (bound
to `subprocess X on Caller`), NOT an owner of the `do`. *)
go_step = "go" identifier ;
do_step = caller? "do" qualified_name arg_list? ;
fail_step = "fail" quoted_string? ; (* terminate: failure *)
finish_step = "finish" quoted_string? ; (* terminate: success *)
(* Steps in a block / body are separated by newline OR `;` (inline). *)
block = "{" ( process_step ( ";" process_step )* )? "}" ;
body = block | ( ":" process_step ( ";" process_step )* ) ;
subprocess_decl = "export"? "subprocess" stable_id? identifier on_caller? param_list? "{" process_body "}" ;
on_caller = "on" identifier ; (* template default-caller param, bound at the `do` site *)
param_list = "(" ( identifier ("," identifier)* )? ")" ; (* intent-only, not bound *)

A view body is an ORDERED-CLAUSE body (like process/policy): a header zone (description / field / knob) followed by a run of ordered clauses. A view carries at most one representation clause (table / matrix / flow) — the board is the absence of one.

view_decl = "view" stable_id? view_head "{" view_body "}" ;
(* views are never `export`ed. Declares at file top level or
in a module body; `in <Module> view … { }` is the placed
twin of the body form. A module-scoped view resolves
selector names from its module context first. *)
view_head = identifier identifier? ; (* <Name> | <ParentView> <Name> —
the two-identifier form is a VIEW INSTANCE: its clauses
append to the referenced (parent) view. *)
view_body = view_header_member* view_clause* ;
view_header_member
= description
| knob_decl (* a view parameter, four shapes below *)
| field_assignment (* base view only *)
| knob_binding ; (* instance only — binds a parent-view knob *)
knob_binding = identifier ":" signed_value ;
view_clause = show_clause | hide_clause | focus_clause | group_by_clause
| on_clause | style_clause | flow_clause
| table_clause | matrix_clause | grid_clause | lens_clause
| story_clause ;
show_clause = "show" selector ; (* union matching nodes/edges into the view *)
hide_clause = "hide" selector ; (* subtract *)
focus_clause = "focus" selector ; (* emphasis; never changes the node selection *)
group_by_clause = "group" "by" ( getter | "in" selector ) ;
(* nest by repetition; the getter is sigiled (`@@team`) and groups
by VALUE, the `in` form groups by CONTAINMENT — its selector
names the containers, one frame each *)
on_clause = "on" plane_name ("," plane_name)* ; (* one = plane board, many
= weave; an `on` in a `flow` view diagnoses — a process
carries its own plane *)
style_clause = "style" selector "{" style_member* "}" ; (* the universal
rewrite rule; the subject may be `violating <Policy>` *)
flow_clause = "flow" ( qualified_name | knob_ref ) [ "sequence" | "bpmn" ] flow_body? ;
(* the process representation; plain `flow X` is the swimlane
walkthrough, `sequence` a UML lifeline diagram, `bpmn` a
lane/pool BPMN. `flow $knob` parameterizes the subject.
A `flow_body` is `bpmn`-only. *)
flow_body = "{" ( lane_entry | pool_entry )* "}" ;
lane_entry = "lane" "by" getter (* one lane per getter value *)
| "lane" quoted_string ":" selector (* named lane merging a query *)
| "lane" selector ; (* expand one callee's steps *)
pool_entry = "pool" "by" getter (* one pool per getter value *)
| "pool" quoted_string ":" selector (* named pool merging a query *)
| "pool" ( qualified_name | knob_ref ) ; (* one pool for an element ref *)
table_clause = "table" "{" ( table_column | table_sort )* "}" ;
table_column = "column" getter quoted_string? (* getter form — editable *)
| "column" quoted_string ":" expression ; (* calc form — read-only *)
table_sort = "sort" "by" getter ;
matrix_clause = "matrix" ( "{" matrix_entry* "}" )? ;
matrix_entry = "axis" getter | "rows" getter | "cols" getter
| "order" "by" matrix_order | "cluster" ;
matrix_order = "cluster" | "layer" | getter ;
(* DSM: `axis` groups the shared SQUARE axis; `rows`/`cols` diverge only
for a rectangular matrix (no `order`/`cluster` there). `order by
cluster`/`layer` runs SCC + topological analysis; `cluster` boxes
cycles on the diagonal and implies `order by cluster`. *)
grid_clause = "grid" ( "{" ( "rows" getter | "cols" getter | "color" getter )* "}" )? ;
(* cross-tab: cells LIST the modules at each (row, col) label
intersection; `color` tints members by a third field. *)
lens_clause = "lens" "{" lens_entry* "}" ; (* computed inspector values *)
lens_entry = identifier quoted_string? ":" expression ; (* name ["Title"]: <expr> *)
story_clause = "story" "{" story_chapter+ "}" ; (* an ordered walk; at most 5 chapters *)
story_chapter = "chapter" quoted_string "{" quoted_string? story_node_list "}" ;
(* the LEADING string is the chapter's note, the same law
module and view descriptions obey; the nodes follow, and
their WRITTEN ORDER is the narration's order *)
story_node_list = selector ( ("," | logical_line_break) selector )* ;
plane_name = qualified_name ; (* dotted, open-world; no `plane` keyword *)

In story_node_list, logical_line_break is the newline separating two selectors — the one place in a view body where a line break carries meaning, so that a chapter’s nodes may be written one per line instead of comma-separated.

Style body. A style clause body (shared with the top-level style bundle declaration) is a set of field rewrites, colon-less nested field GROUPS, the rename / pin directives, and use <Bundle>.

style_member = style_rewrite | style_group | rename_directive | pin_directive | style_use ;
style_rewrite = qualified_name ":" expression ; (* RHS is the one expression grammar *)
style_group = qualified_name "{" style_member* "}" ; (* colon-less nested field group *)
rename_directive = "rename" quoted_string ; (* a display label *)
pin_directive = "pin" signed_number "," signed_number ; (* WYSIWYG position; drag writes it *)
style_use = "use" identifier ; (* apply a named style bundle *)
signed_number = "-"? number ;

Knobs. A knob is a view parameter in one of FOUR shapes. The head disambiguates: knob { opens a block of enum entries; otherwise, after the knob name, : → a literal default (number/slider), from → a value dropdown, and a second identifier → a kind-led element picker.

knob_decl = "knob" ( knob_block | knob_number | knob_from | knob_element ) ;
knob_block = "{" knob_enum_entry* "}" ;
knob_enum_entry = identifier ( "one" | "any" ) "of" ":" atomic_value ("," atomic_value)* ;
(* single | multi; each element is ATOMIC — string / identifier /
number ONLY (no boolean, no array) *)
atomic_value = identifier | quoted_string | number ;
knob_number = identifier ":" signed_value knob_restrictions? ; (* literal default ⇒ slider *)
knob_restrictions = "{" ( ("min" | "max" | "step") ":" signed_number )* "}" ;
knob_from = identifier "from" getter ; (* a dropdown over a plane/aspect's values *)
knob_element = identifier identifier ; (* `knob <type> <name>` — a kind-led element picker *)
(* A kind-determined value slot (knob default / binding) is a `scalar` —
string, identifier, number, or boolean — with an optional leading `-` on a
numeric literal (drag writes negatives). It is NOT an array. *)
signed_value = "-" number | scalar ;

A selector picks a set of model elements OR a set of edges — its sort is decidable from the text (an arrow makes it edge-sort, otherwise node-sort). Selectors are the shared core beneath view (show/hide/ focus/style), policy, and violating. Precedence (loosest→tightest): or < and < not < arrow/term.

selector = sel_or ;
sel_or = sel_and ("or" sel_and)* ;
sel_and = sel_not ("and" sel_not)* ;
sel_not = "not" sel_not | sel_term ;
sel_term = sel_arrow | sel_primary ;
(* Edge patterns. A chain (A > B > C) is a single edge-sort term — the union of
its hops. `>` = direct adjacency, `>>` = directed reachability cone,
`<>` = undirected (symmetrized). `within N` is a suffix after a hop's right
side (`A <> B within 2`); on a chain it binds — and distributes — per hop. *)
sel_arrow = sel_primary ( arrow_op sel_primary within_bound? )+ ;
arrow_op = ">" | ">>" | "<>" ;
within_bound = "within" ( NUMBER | knob_ref ) ;
sel_primary = "*" (* universal — every element *)
| "this" (* the bound subject (predicates/policies) *)
| "outside" (* elements not selected in this view *)
| knob_ref (* $name — a view knob *)
| aspect_atom (* @@key[:"value" | :$knob] *)
| "violating" qualified_name (* a policy's active findings; sort follows it *)
| extraction
| predicate
| "(" selector ")"
| qualified_name ; (* a built-in kind, a user type (incl. subtypes),
or an element reference *)
extraction = ("sources" | "targets" | "nodes" | "owners") "of" "(" selector ")" ;
predicate = "in" sel_primary (* structural containment / process participation *)
| "on" plane_name (* plane membership *)
| "where" "(" expression ")" ; (* a boolean expression per candidate *)
aspect_atom = "@@" qualified_name (":" (value | knob_ref))? ;
(* glued: @@team, @@zone:"pci", @@zone:$zone *)
knob_ref = "$" identifier ;

The expression grammar is the value language: sigiled getters over an element, operators, aggregates, and functions. Selectors and expressions meet ONLY at parenthesised keyword portals (where (…), exists (…), count (…), <agg> <getter> of (…)).

expression = expr_or ;
expr_or = expr_and ("or" expr_and)* ;
expr_and = expr_not ("and" expr_not)* ;
expr_not = "not" expr_not | expr_compare ;
expr_compare = expr_add ( compare_op expr_add )? ;
compare_op = "=" | "!=" | "<" | "<=" | ">" | ">=" ;
expr_add = expr_mul ( ("+" | "-") expr_mul )* ;
expr_mul = expr_unary ( ("*" | "/") expr_unary )* ;
expr_unary = "-" expr_unary | expr_primary ;
expr_primary = literal
| "this"
| getter
| aggregate
| exists
| function_call
| "(" expression ")" ;
(* Getters. `@field` reads a field, `@@aspect` reads an aspect; builtins include
@name @type @kind @connections @todos @dist @reversible @compensated. A `(…)`
suffix is a capability getter with raw argument text (e.g. @ver.changes(...)). *)
getter = ("@" | "@@") getter_path ( "(" raw_args ")" )? ;
getter_path = word ("." word)* ; (* word may be a reserved keyword after a sigil *)
aggregate = "count" "(" selector ")"
| "count" "distinct" getter "of" "(" selector ")"
| ("sum" | "min" | "max") getter "of" "(" selector ")" ;
exists = "exists" "(" selector ")" ;
function_call = identifier "(" ( expression ("," expression)* )? ")" ; (* colorize / heat / css *)
literal = quoted_string | number | "true" | "false" | identifier ;
(* a bare identifier is an identifier-VALUE (a bareword, like an
unquoted string). The reserved expression words `this` /
`exists` / `count` / `sum` / `min` / `max`, and an identifier
glued to `(` (a function_call), are matched by earlier
expr_primary alternatives — none fall through to this
identifier-as-literal case. *)

A policy is a query over the model that yields findings (a node or an edge that violates a rule), enforced by archlang check, surfaced as LSP diagnostics, and selectable via violating <Policy>. A policy declares at file top level or in a module body; in <Module> policy … { } is the placed twin of the body form. A module-scoped policy’s scope is that module’s subtree — node findings by subtree membership, edge findings by their source endpoint.

policy_decl = "export"? "policy" stable_id? identifier "{" policy_body "}" ;
policy_body = ( header_field | policy_rule )* ;
header_field = qualified_name ":" value ; (* e.g. severity: error | warning | advisory *)
policy_rule = forbid_rule | require_rule | except_rule | escalate_rule | when_gate ;
forbid_rule = "forbid" selector ; (* a finding per matching node/edge *)
require_rule = "require" selector ":" selector ; (* subject : obligation — a finding when unmet *)
except_rule = "except" selector quoted_string field_body? ; (* a waiver; reason mandatory *)
escalate_rule = "escalate" qualified_name "to" identifier ; (* raise-only severity override *)
(* Change gate — evaluated over a base→head diff (the accept-gate). The
`changed(<getter>)` portal narrows to elements whose getter value differs; it
is legal on `changed` ONLY — `added`/`removed` take no portal. An edge subject
(arrow / `violating`) takes `added`/`removed` only. *)
when_gate = "when" selector change_verb "{" review_req* "}" ;
change_verb = "added" | "removed" | "changed" ( "(" getter ")" )? ;
review_req = "require" "review" "from" ( NUMBER "of" )? ( getter | identifier ) ;

Query, style bundle & lens declarations (v0.10)

Section titled “Query, style bundle & lens declarations (v0.10)”

Four governance/vocabulary declarations that reuse the selector/expression core. All are member-head dispatched: a head followed by : / . is a field, not one of these declarations. A stable id, when present, sits right after the keyword. view, policy, query, and lens declare at file top level or inside a module body; view and policy additionally place via in <Module> …. style bundle and use policy stay top-level-only.

query_decl = "export"? "query" stable_id? identifier ":" selector ;
(* a named, reusable selector; `export` publishes it across
the package boundary *)
style_bundle_decl = "style" "bundle" stable_id? identifier "{" style_member* "}" ;
(* a reusable style body (style_member, above), applied
elsewhere via `use <Bundle>`. Not `export`able. *)
lens_decl = "lens" "{" lens_entry* "}" ;
(* a computed-inspector block at top level or in a module
body; reuses lens_entry (above). Unnamed — no name, no
stable_id (dispatched on `lens` immediately followed by `{`). *)
use_policy_stmt = "use" "policy" identifier "from" dotted_name policy_use_body? ;
policy_use_body = "{" ( except_rule | escalate_rule )* "}" ;
(* governance INSTANTIATION, not a type import: pulls a
published `policy` from a package, then may locally
waive (`except`) or raise (`escalate`) it. *)

The view/policy/selector heads below are contextual identifiers — recognised by position (a clause head / member head / a slot after a sigil), not hard reserved words; each remains a legal identifier everywhere else. focus, group, by, on, and one are the exception: those ARE hard lexer keywords (§Reserved keywords), reused in view/selector position. Note the one / any asymmetry — one is a hard keyword, any is contextual.

show, hide, style, flow, table, matrix, lens, story, chapter, knob, column,
sort, rename, pin, rows, cols, any, of, bundle, sequence, bpmn, lane, pool,
policy, query, forbid, require, except, escalate, when, review, severity,
violating, sources, targets, nodes, owners, this, outside, where, exists,
count, sum, min, max, distinct, and, or, not, within

A manifest declares exactly one of package: (a package root — an opaque resolution unit) or name: (a space within the enclosing package). See PACKAGE-ARCHSPACE for the full visibility model.

manifest = manifest_field* ;
manifest_field = package_field
| space_field
| version_field
| widgets_field
| description
| evidence_pin
| dependencies_block
| policy_packs_block
| use_decl
;
package_field = "package" ":" dotted_name ; (* package root — opaque unit *)
space_field = "name" ":" dotted_name ; (* a space within a package *)
version_field = "version" ":" quoted_string ;
widgets_field = "widgets" ":" quoted_string ;
description = quoted_string ;
evidence_pin = ( "repo" ":" quoted_string ) | ( "commit" ":" quoted_string ) ;
(* `package.archspace` ONLY: the code repository this
package's `sources:` bindings are checked against, and
the revision they are true of. `commit` is required
whenever `repo` is set — an unpinned path attests that a
file existed once, not that the model is true of a
commit. The bindings themselves are ordinary fields
(`sources: "src/a.ts:12-88", "src/b.ts"`), so they need
no production of their own. *)
dependencies_block = "dependencies" "{" dep_entry* "}" ;
dep_entry = dotted_name ":" quoted_string ","? ;
policy_packs_block = "policies" "{" ( dotted_name ":" quoted_string )* "}" ;
(* `package.archspace` ONLY (peer of `dependencies { }`): the
governance PACKS this package adopts, each pinned to the
version the pack declares. Adoption is what makes a pack
un-droppable — the gate fails if a pinned pack stops
resolving, ships a different version, or has one of its
exported policies no longer imported. *)
(* `use_decl` as defined under Top-level; package-scoped at the package root,
space-scoped in a space manifest. *)
dotted_name = identifier ("." identifier)* ;

.arch for source files. package.archspace for the manifest.

UTF-8.

Not syntactically significant outside quoted strings and comments. Indentation is recommended for readability; stdlib uses 4 spaces.

A quoted string may span multiple lines. The lexer auto-dedents: a leading whitespace-only line and the minimum shared indent on remaining lines are stripped. Recognized escapes: \" and \\.

Triple-quoted strings ("""…""") are raw — no escapes, no interpolation.