<!--
  StemJSON v1.1 — LLM Reference (Condensed)
  Copyright (c) 2026 Vasyl Krychun. Licensed under OWFa 1.0.
  Paired edition of spec/v1.1.md for AI prompt usage.
-->

# StemJSON v1.1 — LLM Reference

*Condensed, LLM-optimised edition of the v1.1 specification. Same normative surface as `spec/v1.1.md`, ~1/3 the size. Load this into an AI prompt when generating StemJSON.*

| Field | Value |
|---|---|
| Normative source | `spec/v1.1.md` |
| Revision | 1.1.0 (same as full spec) |
| Use | AI / LLM authoring |
| License | OWFa 1.0 |

---

## 1. Module shape

Every StemJSON payload has a module at its root.

```json
{
  "id": "mod_example",
  "type": "module",
  "version": "1.0",
  "state": { "count": 0 },
  "context": { "title": "Example" },
  "dependencies": [],
  "children": [ ... ],
  "events": { "onAppear": [ ... ] }
}
```

- `id`, `type` are **REQUIRED** on every component and every action.
- `id` MUST be globally unique across the entire payload (not just within a module).
- `version` SHOULD be `"1.0"` at the module root for forward-compat.
- `state`, `context`, `dependencies`, `children`, `events` are optional.
- Initial `state` values MUST be resolved JSON literals — no `${…}`, no `{{…}}`, no `@{…}`.

---

## 2. Value grammar

Four value forms. Each usable anywhere a value is accepted.

| Form | Scope | Example |
|---|---|---|
| `${path}` | Current module's state only (no cross-module read) | `${email}`, `${user.name}`, `${items[0]}` |
| `@{path}` | Current context chain — **inherited DOWN through every nested component, including across module boundaries**. Author-declared (non-`_`-prefixed) context keys merge from each ancestor into descendants; resolution walks `self → parent → … → root`, nearest binding wins. Internal `_`-prefixed keys (`_text`, `_source`, `_value`, …) stay component-local and do NOT merge. | `@{item.title}` (from a `dynamic` prototype), `@{entries}` (from a root module's custom context), `@{handler.payload}` |
| `{{ expr }}` | Expression evaluated at runtime | `{{ count(${items}) }}`, `{{ ${a} + 1 }}` |
| `#{key}` | Runtime-provided system value | `#{now}`, `#{locale}` |

### Path syntax (inside `${}` / `@{}`)

| Segment | Meaning |
|---|---|
| `.name` | Dict key lookup |
| `[n]` | Array index (`[0]`, `[1]`). Negative = tail (`[-1]` = last) |
| `['key']` | Dict key via string literal |
| `[expr]` | Index/key from an expression (`${items[${i}]}`) |
| `[predicate]` | **Filter predicate (path-only)** — array filter; write the predicate directly inside the brackets (`[done == true]` — no extra delimiters). The brackets contain an expression evaluated against each element with the element's dict as context (so bare identifiers `title`, `hasMood`, etc. resolve to keys on the element). Any operator from §4 works: `~`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `\|\|`, `!`. `val` (right side) may be a literal, `${state}`, `#{system}`, or expression — `@{context}` refs may not resolve. Chained filters `[a][b]` apply in sequence. **`~` is *contains*, not equality** — `hasMood ~ true` resolves to `.none` and filters out everything; use `==` for primitives. |

**Null propagation:** any segment resolving to `.none` or type-incompatible short-circuits the whole path to `.none`. No runtime error.

**Bare refs are valid expressions:** `${key}` and `@{key}` don't need `{{…}}` wrapping unless combined with operators or functions.

**A string value is parsed as exactly one form.** At any value-accepting field, a string is parsed as:

1. **literal** (default — no marker substitution scanning),
2. **single bare reference** (whole value is `"${path}"`, `"@{path}"`, or `"#{key}"` with no surrounding content), or
3. **wrapped expression** `"{{ … }}"`.

Any mix — two refs side by side, ref + literal text, etc. — falls into form (1) and renders verbatim. To compose, wrap in `{{ … }}` and join with `+`.

| Goal | ✅ Right | ❌ Wrong (renders verbatim) |
|---|---|---|
| Single ref alone | `"_text": "@{item.name}"` | — |
| Literal + ref | `"_text": "{{ 'Hi ' + @{item.name} }}"` | `"_text": "Hi @{item.name}"` |
| Two refs joined | `"_text": "{{ @{a} + ' ' + @{b} }}"` | `"_text": "@{a} @{b}"` |
| Ref + system value | `"_text": "{{ @{item.name} + ' · ' + cast(#{now}, 'str') }}"` | `"_text": "@{item.name} · #{now}"` |

**Markers do not combine.** `${...}`, `@{...}`, `#{...}` are FOUR distinct, non-mergeable prefixes. There is no merged form — no `@${...}`, no `${@{...}}`, no `#{${...}}`, no `{${...}}`. A marker prefix is exactly two characters from this set: `${`, `@{`, `#{`, `{{`. The closing brace counts pairs of the matching opener. AI authors familiar with both `${` and `@{` sometimes fabricate `@${...}.field` as if it meant "context-via-state" — it does not; the runtime parses it as literal text and the string renders verbatim, including the curly braces.

```jsonc
"_text": "{{ ${forecast.temperature} }}"      // ✅ state path
"_text": "{{ @{forecast.temperature} }}"      // ✅ context path
"_text": "@{forecast.temperature}"            // ✅ bare context ref
"_text": "${forecast.temperature}"            // ✅ bare state ref

"_text": "{{ @${forecast}.temperature }}"     // ❌ INVENTED — merged @${} prefix; renders as literal text
"_text": "{{ ${@{forecast}}.temperature }}"   // ❌ nested marker — not a valid form
"_text": "@${forecast.temperature}"           // ❌ same — `@$` is not a marker prefix
```

If the data is in module state, use `${...}`. If it's in the inherited context chain (`@{item}`, `@{handler.payload}`, ancestor-declared keys), use `@{...}`. The model picks ONE marker per reference based on where the value lives.

---

## 3. System values (`#{key}`)

| Key | Type | Value |
|---|---|---|
| `#{now}` | date | Current instant |
| `#{today}` | date | Midnight today |
| `#{yesterday}` | date | Midnight yesterday |
| `#{tomorrow}` | date | Midnight tomorrow |
| `#{endofweek}` | date | Start of last day this week |
| `#{startofmonth}` | date | First day this month, midnight |
| `#{endofmonth}` | date | Last second this month |
| `#{user}` | string | Vendor ID (UUID per vendor on device) — **not** a user identity |
| `#{locale}` | string | Locale id, e.g. `"en_US"` (underscore, not hyphen) |
| `#{language}` | string | Language code, e.g. `"en"` |
| `#{currency}` | string | ISO 4217, e.g. `"USD"` |
| `#{timezone}` | string | IANA id, e.g. `"Europe/Kyiv"` |
| `#{orientation}` | string | `"portrait"` \| `"landscape"` |
| `#{device_type}` | string | `"phone"` \| `"pad"` \| `"mac"` \| `"vision"` \| `"other"` |
| `#{device}` | string | Model name, e.g. `"iPhone"` |
| `#{systemname}` | string | OS name |
| `#{systemversion}` | string | OS version string |
| `#{is_split_view}` | bool | True when iPad app is in Split View / Slide Over |
| `#{horizontal_size_class}` | string | `"regular"` \| `"compact"` |

**Date lifecycle.** A `date` value behaves as a date inside expressions and paths — it can be compared with other dates, passed to `cast`, etc. When a `date` value **crosses into a JSON sink** (a repo body, a remote request, a host trigger payload, persisted state), it is flattened to a `double` carrying **Unix epoch seconds since 1970**. It is reconstituted on the way back as a `double`, not as a `date` — once stored, a date is just a number. Schemas downstream of a repo write read these timestamps as numbers (e.g. `@{item.ts}` resolves to a `double`).

**Cast behaviour for dates** (descriptive — see §4 functions table for the canonical row):
- `cast(<date>, 'str')` — returns the locale-formatted display string (e.g. `"5/21/2026, 4:14 PM"` in en_US). Locale-dependent, minute-precision in most locales. Useful for display only.
- `cast(<date>, 'int')` — returns `0`. There is no int conversion for dates today; use the boundary-crossing path above to get the epoch number.
- `cast(<date>, 'date')` — no-op.

---

## 4. Expression language

### 4.1 Operator precedence (high → low)

| Tier | Operators | Meaning |
|---|---|---|
| Postfix | `.name`, `[idx]` | Member access, subscript (tightest) |
| Unary | `!`, `+`, `-` | Not, plus, negate |
| `^` | `^` | Exponentiation |
| `* / %` | `*`, `/`, `%` | Multiplicative |
| `+ -` | `+`, `-` | Additive (see tables below) |
| `< <= > >= == != ~` | Comparison + contains/filter |
| `&&` | Logical AND |
| `\|\|` | Logical OR |
| `??` | Nil-coalesce |
| `? :` | Ternary |
| `\|>` | Pipe (loosest) |

Use `( )` to override. Postfix binds tighter than everything.

### 4.2 `+` operator

| LHS | RHS | Result |
|---|---|---|
| string | any | string concat (`"px" + 5` → `"px5"`) |
| int | string | string concat (`3 + "px"` → `"3px"`) |
| number | number | numeric sum |
| date | number | date shifted forward by that many seconds |
| array | array | concatenated |
| array | any | append |
| any | array | prepend |
| dict | dict | merged (RHS wins on conflict) |
| other | other | `.none` |

**No implicit string→number coercion.** `"3" + 2` → `"32"`, not `5`. Use `cast()`.

### 4.3 `-` operator

| LHS | RHS | Result |
|---|---|---|
| number | number | numeric difference |
| string | string | removes all occurrences (`"hello world" - "world"` → `"hello "`) |
| date | number | date shifted back |
| date | date | double interval in seconds |
| array | array | set difference |
| array | any | filters out all matches |
| dict | string | dict with key removed |
| none | number | negation |

### 4.4 `*` and `/` and `%`

- `*`: numeric product; `string * int` = repeat (`"ab" * 3` → `"ababab"`).
- `/`: always double for numeric; `string / string` = split on separator (`"a,b,c" / ","` → `["a","b","c"]`). Div by zero → `.none`.
- `%`: modulo, numeric only.

### 4.5 Comparison

- `==` / `!=`: strict by type except int/double compare numerically. `none == none` → `true`.
- `<` `<=` `>` `>=`: same type or int/double only.

### 4.6 `~` — contains / filter

| LHS type | Meaning |
|---|---|
| string | case-sensitive substring check |
| array | contains element |
| dict | has key |

Filter-predicate form `${arr[field ~ val]}` is **path-only**, not postfix-on-expression.

### 4.7 Logical + ternary + coalesce + pipe

- `&&`, `\|\|`, `!`: standard; non-bool LHS of `\|\|` returned if truthy.
- `??`: nil-coalesce. `${maybe} ?? 'fallback'`.
- `? :`: ternary. `{{ ${count} > 0 ? @{label} : 'None' }}`. **Chained ternaries take NO parentheses** — right-associative, lowest precedence, so the `else` branch may be a further ternary directly: `{{ notEmpty(${winner}) ? ${winner} + ' wins' : ${moves} == 9 ? 'Tie' : ${turn} + ' to move' }}`. `a ? b : (c ? d : e)` and `a ? b : c ? d : e` are equivalent — but the parenthesized form is the #1 cause of unbalanced-`)` syntax errors (`Expected end of input`). ALWAYS write the flat, paren-free chain; parens only to group a *condition*, never to wrap a nested ternary. 3+ branches → prefer `switch()` (§4.9).
- `\|>`: pipe. `{{ ${name} \|> trim() \|> upper() }}` ≡ `upper(trim(${name}))`. The piped value binds as the **first** argument; call args follow — `{{ ${x} \|> replace('a', 'b') }}` ≡ `replace(${x}, 'a', 'b')`.

### 4.8 Literals

```
42                      // int
3.14                    // double
'hello'                 // string (single quotes)
true, false             // bool
null, nil               // null
[1, 2, 3]               // array (elements are expressions)
{'k': value}            // dict (string keys, expression values)
```

### 4.9 Built-in functions

**Conditional** (spec 1.1.0+; a module using `switch` SHOULD declare `"version": "1.1"`)

| Fn | Signature | Behaviour |
|---|---|---|
| `switch` | `switch(test1, value1, [test2, value2, …], default)` | Flat multi-way conditional. Evaluates tests left-to-right; returns the value paired with the FIRST boolean-true test, else the trailing `default`. **Lazy/short-circuit** like the ternary — only the matched value (or default) is evaluated; unmatched branches never resolve (a branch that would error is harmless). Arg count is always **odd** (N test/value pairs + one mandatory `default`), min 3. Non-boolean test = not matched. Values may differ in type. |

`switch()` is the flat, paren-free alternative to nested ternaries for 3+ branches — it removes the deep parenthesisation that causes unbalanced-`)` errors:

```
{{ switch(notEmpty(${winner}), ${winner} + ' wins', ${moves} == 9, 'Tie game', ${turn} + ' to move') }}
```

**String**

| Fn | Signature | Behaviour |
|---|---|---|
| `upper` | `upper(s, [n])` | Uppercase; n>0 first n chars, n<0 last \|n\| |
| `lower` | `lower(s, [n])` | Lowercase; same n rule |
| `trim` | `trim(s)` | Strip whitespace |
| `replace` | `replace(s, from, to)` | Replace all. String: substring replace. Array: whole-element replace (each element `== from` → `to`) |
| `prefix` | `prefix(s, n)` | First n chars |
| `suffix` | `suffix(s, n)` | Last n chars |

**Inspection**

| Fn | Signature | Returns |
|---|---|---|
| `count` / `length` | `count(v)` | Chars/items/keys, 0 for `.none` |
| `empty` | `empty(v)` | True for empty string/array/dict/none |
| `notEmpty` | `notEmpty(v)` | `!empty(v)` |
| `hasPrefix` | `hasPrefix(s, p)` | Bool |
| `hasSuffix` | `hasSuffix(s, sfx)` | Bool |
| `contains` | `contains(v, needle)` | Substring/element/key |

**Data**

| Fn | Signature | Behaviour |
|---|---|---|
| `strtojson` | `strtojson(s)` | Parse JSON string → value; `.none` on fail |
| `localize` | `localize(code, msg, [args])` | Lookup `l10n://code`, fallback to `msg` |
| `cast` | `cast(v, type)` | `int \| double \| float \| number \| string \| str \| bool \| boolean \| expression \| expr \| date`. Date → `str` returns the locale-formatted display string (e.g. `"5/21/2026, 4:14 PM"`), not an epoch timestamp; date → `int` returns `0` (no epoch conversion). To compare or sort dates, use them as `date` values directly. |
| `format` | `format(v, descriptor)` | Apply `_format` descriptor |

**Collection**

| Fn | Signature | Behaviour |
|---|---|---|
| `sort` | `sort(arr, [key], [direction])` | `"asc"` (default) \| `"desc"`; dicts sorted by key |
| `first` | `first(arr, [key])` | First element, `.none` if empty |
| `last` | `last(arr, [key])` | Last element |
| `min` | `min(arr, [key])` | Smallest (or dict with smallest key) |
| `max` | `max(arr, [key])` | Largest |
| `sum` | `sum(arr, [key])` | Numeric sum, 0 for empty |
| `map` | `map(arr, template)` | Transform each element; `@{item}` / `@{index}` rebind per iteration. Template is **lazy** (not pre-resolved). |
| `range` | `range(n)` \| `range(start, end)` | Integer sequence: `range(n)` → `[0…n-1]`; `range(start, end)` → `[start…end-1]` (end exclusive). Empty/inverted span → `[]`. Pairs with `map` to build structured data (spec 1.1.0+). |
| `setAt` | `setAt(arr, i, v)` | New array with element `i` set to `v`. Negative `i` counts from the tail. Out-of-range `i` (incl. `i == count`) or non-array → subject unchanged. To append use `${arr} + v` (spec 1.1.0+). |
| `removeAt` | `removeAt(arr, i)` | New array without element `i`. Negative `i` from tail; out-of-range / non-array → unchanged (spec 1.1.0+). |
| `insertAt` | `insertAt(arr, i, v)` | New array with `v` inserted before `i`; `i == count` appends. Negative `i` from tail; out-of-range / non-array → unchanged (spec 1.1.0+). |
| `keys` | `keys(dict)` | Array of the dict's keys, **ascending-sorted** (deterministic — dict is unordered). Non-dict → `[]` (spec 1.1.0+). |
| `values` | `values(dict)` | Array of the dict's values, ordered to match `keys(dict)`. Non-dict → `[]` (spec 1.1.0+). |
| `removeKey` | `removeKey(dict, k)` | New dict without key `k`. Absent key / non-dict → unchanged. (Set/add a key with `${dict} + {'k': v}`.) (spec 1.1.0+) |

All six are immutable (return a new value; never mutate). Function names are **case-sensitive**. Index reads/writes share the negative-from-tail convention. `append` / `concat` / `merge` already exist via `+`; `filter` via path predicates `${arr[pred]}`.

**`map` shadowing:** inside nested `map` / `dynamic`, inner `@{item}` / `@{index}` shadow outer — no alias syntax. To reference outer scope, capture the outer value into a state key before entering the inner iteration. A positional array edit (`setAt`) sidesteps this for the common "set cell at tapped index" case — write it in the outer action context, not inside a `map`.

**Randomization** (spec 1.1.0+; a module using `random` / `range` SHOULD declare `"version": "1.1"`)

| Fn | Signature | Behaviour |
|---|---|---|
| `random` | `random()` | Double in `[0, 1)` |
| `random` | `random(min, max)` | Int in `[min, max]` **inclusive** (e.g. `random(1, 6)` die). Reversed bounds tolerated (swapped). Random element: `random(0, count(${arr}) - 1)` |

`random` is **nondeterministic** and **not** seeded/reproducible. **HARD RULE: use `random` only in an action/lifecycle value (e.g. a `state` action fired from `onAppear` or a "New Game" button), NEVER in a render binding.** Expressions re-resolve every render, so `random` in a `text`/style/visibility binding re-rolls each frame and flickers. In a `state` action the value is resolved **once and frozen** into state; the UI reads the stable state. A reset is re-running that action.

Generate a fresh board declaratively — `map` re-resolves its template per element, so each cell is an independent draw:

```json
"events": { "onAppear": [
  { "state": { "id": "act_new_game", "input": {
      "grid": "{{ map(range(25), random(1, 9)) }}"
  } } }
] }
```

---

## 5. Source schemes

| Scheme | Purpose |
|---|---|
| `dp://id` | Dependency registered in current module |
| `sf://name` | System icon (SF Symbol on Apple; fallback on other platforms). Bare name accepted in `_tabImage` |
| `asset://name` | Bundled app asset |
| `file://path` | File inside current StemJSON zip package |
| `rs://name` | Resource in host app binary (not zip) |
| `l10n://key` | Localisation key from packaged `.strings` files |
| `http(s)://…` | Remote URL |

**Source-string scheme prefix is required.** Any value at a source-accepting field MUST carry the `<scheme>://` prefix shown above. The runtime does not infer the scheme — a string without a prefix is treated as a literal. The single exception is `_tabImage` on `tab` children, where a bare SF Symbol name is accepted.

---

## 6. Component reference

Every component has: `id`, `type`, optional `context`, `style`, `children`, `events`.
Context keys with leading `_` are **private** (not inherited). Keys without `_` are **public** (inherited by children; children may override).

### Layout

| Type | Required context | Optional context | Notes |
|---|---|---|---|
| `vstack`/`hstack` | — | `_spacing` (num), `_alignment` (str), `_lazy` (bool) | Stack children along axis |
| `zstack` | — | `_alignment` (default `"center"`) | Z-axis overlay |
| `scroll` | — | `_axis` (default `"vertical"`), `_showIndicators`, `_scrollTrigger` | Scrollable container. Root of any screen whose content can exceed the viewport (see §18) |
| `list` | — | `_isLoading`, `_skeletonRows` (default 5) | Native list styling |
| `grid` | — | `_gridType` (`lazyVGrid`\|`lazyHGrid`\|`grid`), `_spacing`, `_alignment`, `_horizontalSpacing`, `_verticalSpacing` | Grid layout |
| `gridrow` | — | — | Row for `grid` variant |
| `form` | — | `_title` | Grouped-list presentation |
| `section` | — | `_title` | Logical group |
| `spacer` | — | — | Fills axis space |
| `divider` | — | — | Horizontal rule |
| `splitview` | — | `_columnVisibility` (`automatic`\|`all`\|`detailOnly`) | children[0]=sidebar, children[1]=detail, children[2]=3rd column |
| `empty` | — | — | Renders nothing (`EmptyView`) |

### Content

| Type | Required context | Optional context | Notes |
|---|---|---|---|
| `text` | `_text` (string/expr) | `_format` | Display string; may format date/number/duration |
| `image` | `_source` (source) | `_placeholder`, `_contentMode` (`fit`\|`fill`), `_isLoading` | **MUST** declare ≥1 dimension in `style.layout` |
| `video` | `_source` | `_placeholder`, `_autoplay`, `_loop`, `_muted`, `_isLoading` | AVKit on Apple |
| `pdf` | `_source` | `_page` (1-indexed), `_isLoading` | PDFKit |
| `media` | `_source` | `_kind` (`auto`\|`image`\|`video`\|`pdf`), `_placeholder`, `_contentMode`, `_autoplay`, `_loop`, `_muted`, `_page`, `_isLoading` | Universal media viewer |
| `label` | `_title` | `_image`, `_isLoading` | Text + optional icon |
| `progress` | — | `_value`, `_total`, `_isLoading` | Determinate bar when `_value` + `_total` set; indeterminate spinner otherwise |
| `color` | — | `_color` (default `clear`) | Solid color block; needs `style.layout` size |
| `map` | `_position` (object/expr — two-way state binding, shape `{ region: { center: {latitude, longitude}, span: {lat, lon} } }`) | `_region` (Conditional — only when children define annotations), `_name`, `_debounce`, `_isLoading` | Interactive map; children define annotation content |

### Input

| Type | Required context | Optional context | Notes |
|---|---|---|---|
| `textfield` | `_label` (str), `_text` (state ref) | `_focused`, `_debounce`, `_keyboard`, `_isSecured`, `_isLoading` | Events: `onChange`, `onFocus`, `onBlur`, `onSubmit` |
| `texteditor` | `_text` (state ref) | `_placeholder`, `_focused`, `_keyboard`, `_isLoading` | Multi-line |
| `toggle` | `_label`, `_isOn` (state ref, bool) | `_image`, `_isLoading` | Two-way binding |
| `picker` | `_selection` (state ref), `_options` (array) | `_label`, `_isLoading` | `style.picker` selects presentation |
| `slider` | `_value` (state ref, num) | `_isLoading` | Normalised `0..1` continuous input by design — no `_min`/`_max`/`_step`. For integer scales use a segmented `picker` with `_options`. For a continuous-feel slider with integer display, scale at the consumer: `"{{ cast(${v} * 9 + 1, 'int') }}"` maps `0..1` → `1..10`. See §11.1 (authors own end-to-end value semantics). |
| `datepicker` | `_label`, `_date` (state ref, Unix seconds) | `_components`, `_min`, `_max`, `_isLoading` | Default `_components: "date"` |
| `button` | `_label` (label form) OR `children` | `_image`, `_isLoading` | Primary event: `onTap` |

### Navigation

| Type | Required | Optional | Notes |
|---|---|---|---|
| `navigation` | — | `navigationTitle` (public) | Wraps children in nav stack; required ancestor for `navigate` action |
| `link` | `destination` (component of type `"module"`) | — | Tappable row that pushes destination |
| `tab` | children (each a module) | per-child `_tabTitle`, `_tabImage`, `_tabBadge` | Tab-bar container |

**Important:** `link.destination` MUST be `type: "module"`. Other types render but their `events` (incl. `onAppear`) don't fire.

### Non-visual

| Type | Required | Notes |
|---|---|---|
| `module` | `id`, `type: "module"` | Executable boundary. Owns state + dependencies. |
| `template` | `_source` (`dp://` or `file://`) | Proxy for registered component; `_inline` default `true` |
| `dynamic` | `context._data` (array/expr) + **`prototype`** (top-level field, a component — NOT inside `context`, NOT `children`) | Optional: `context._isLoading`, `context._skeletonRows`. Renders prototype per element. Injects `@{item}`, `@{index}` into prototype context |
| `conditional` | `_condition` (bool/expr) | `_trueComponent` required, `_falseComponent` optional (renders nothing if omitted) |

### Shapes

`circle`, `ellipse`, `capsule`, `rectangle`, `roundedrectangle`, `star`, `bubble`.

**Component `type` matching is case-insensitive** — canonical form is lowercase (all spellings above); `datepicker` = `datePicker` = `DATEPICKER`. Convention: component types are lowercase, style domains are camelCase.

Appearance is set via `style.shape` (`fill`, `stroke`, `trim`, `rotation`, `inset`) and sizing via `style.layout`. No `_color` context key on shapes — use `style.shape.fill` or `style.common.foreground`.

Shape-specific context keys:
- `bubble`: `_outgoing` (bool/expr) — `true` = tail points right (outgoing), `false` = tail points left (incoming).
- `star`: `_points` (int) — number of star points, default `5`.

---

## 7. Events

| Event | Schema | Fires on |
|---|---|---|
| `onTap` | array | Single tap |
| `onDoubleTap` | array | Double tap (coexists with `onTap`; runtime disambiguates) |
| `onAppear` | array | Component transitions into the visible UI tree — see "Lifecycle events" below |
| `onDisappear` | array | Component transitions out of the visible UI tree |
| `onRefresh` | array | Pull-to-refresh on scroll/list |
| `onSubmit` | array | Form submit / keyboard submit key |
| `onLongPress` | array | Long-press gesture |
| `onRotate` | array | Device orientation change |
| `onFocus` | array | Input gains focus |
| `onBlur` | array | Input loses focus |
| `onChange` | **object** | Observed state key changes |
| `onCustom` | **object** | Host app dispatches bridge event |

### Lifecycle events — `onAppear` / `onDisappear`

The runtime fires these handlers **only if the component declares them in JSON** — there is no implicit lifecycle behavior. If a tab module needs to refresh its data from the repo when the user activates that tab, it MUST declare `onAppear` with the refresh actions.

`onAppear` fires every time the component **transitions from not-rendered to rendered in the visible UI tree**. Concretely:
- the component's first mount,
- the user switching back to a `tab` whose content was previously visible,
- returning from a pushed module via the navigation back stack,
- any conditional re-mount when its enclosing condition flips to truthy.

It does NOT fire from scroll position changes — the component stays in the tree even when scrolled off-screen.

For tab- and navigation-driven flows, declaring an `onAppear` action that re-reads from the repo (or rebuilds derived state) is the canonical way to keep a view fresh against writes made elsewhere in the app.

`onDisappear` fires correspondingly when the component is removed from the visible UI tree.

### `onChange` — special schema

```json
"onChange": {
  "observed": "query",
  "actions": [ ... ]
}
```
`observed` REQUIRED. Array form `onChange: [...]` is **not supported**.

### `onCustom` — special schema

```json
"onCustom": {
  "name": "eventName",
  "actions": [
    { "state": {
        "id": "handler",
        "input": { "field": "@{handler.payloadField}" }
    } }
  ]
}
```
`name` REQUIRED. Host's dispatched payload is bound into each action's context under the action's `id`. Scalar payload accessible as `@{<action.id>}`.

---

## 8. Actions

All actions share: `id` (REQUIRED, globally unique), `input` (kind-specific), optional `output.success` / `output.failure` (arrays of actions), optional `debounce` (seconds).

Single-key JSON object. Nine kinds: `state`, `repo`, `ai`, `listen`, `service`, `template`, `navigate`, `condition`, `interval`.

### 8.1 `state`

A `state` action applies a partial-merge write. It does exactly **one** of two things, depending on whether `output.success` is present:

- **No `output.success`** → the write **propagates upward by name match**. Every key in `input` is offered to each ancestor module; any ancestor that has declared a key of the same name receives a partial-merge update with that value. This is the canonical pattern for nested modules pushing data to a shared ancestor (e.g. a check-in sheet appending to a root-level `entries` list).
- **`output.success` present** → the write **stays local** (no propagation), and the chain runs in the *dispatching* module's context. This preserves the `${...}` / `@{item}` scope the chain was authored against — essential for follow-up `repo.persist`, `service` calls, or local UI feedback that depend on the dispatching module's bindings.

In both cases the local merge is gated by the dispatching module's declarations: keys not declared locally are silently dropped from the local write.

**Why the chain suppresses propagation.** If a chained write also propagated, the same `output.success` would need to fire from every ancestor that received the propagation — each with a different scope. Path resolution would diverge, `@{item}` may be absent, and the chain's intent (sequencing one piece of async work in one scope) breaks. So the runtime trades propagation for chain-context fidelity.

**When you need both** — propagation *and* sequenced follow-up async work — split into two separate state writes:

```json
// 1. Chain-less write — propagates entries upward to the root tracker.
{ "state": { "id": "push_entry", "input": { "entries": "${draftEntry}" } } }

// 2. Chained write — local-only "Saving…" feedback that sequences the persist call.
{ "state": { "id": "show_saving",
  "input": { "statusMessage": "Saving…" },
  "output": { "success": [
    { "repo": { "id": "persist", "input": { /*…*/ } } }
  ] } } }
```

Both run in parallel from the same event array; each does its own job in the right scope.

**Chain rules (when `output.success` is present).**
- `output.failure` is NOT allowed — state writes never fail.
- Inner actions inside `state.output.success` MUST be non-state. State-to-state chaining is redundant — combine into a single multi-key `input` dict instead.

Parent modules cannot write to child state, siblings cannot write to each other, and no module can read another module's state.

```json
{ "state": { "id": "set_q", "input": { "query": "${q}", "error": "" } } }
// input may also be a reference/expression that RESOLVES to a dictionary — merged like the inline form:
{ "state": { "id": "from_ctx", "input": "@{some_dict_key}" } }    // @{some_dict_key} MUST resolve to a dict
// A non-dictionary input (scalar string/number, or a ref/expr resolving to a scalar) writes nothing.

// Sequencing UI feedback around a blocking async op (canonical state.output use).
// NOTE: because output.success is present, statusMessage/showStatus are written
// LOCALLY only — they do NOT propagate to ancestor modules. The "Saved!" inner
// write is also local. Use this for module-local UI state only.
{ "state": { "id": "show_saving",
  "input": { "statusMessage": "Saving…", "showStatus": true },
  "output": { "success": [
    { "repo": { "id": "persist", "input": { /*…*/ },
      "output": { "success": [
        { "state": { "id": "show_saved",
          "input": { "statusMessage": "Saved!" } } }
      ] } } }
  ] } } }
```

### 8.2 `repo`

| Field | Required | Value |
|---|---|---|
| `input.repositoryId` | ✓ | id of a repository dependency |
| `input.operation` | ✓ | `read` \| `create` \| `update` \| `delete` |
| `input.params` | — | Kind-specific |
| `debounce` | — | Seconds |
| `output.success` | — | Success chain. Access result via `@{actionId}` or `@{actionId.field}` |
| `output.failure` | — | Failure chain. Access error via `@{actionId.code}`, `@{actionId.message}` |

### 8.3 `listen` (firebase realtime)

| Field | Required | Value |
|---|---|---|
| `input.dependencyId` | ✓ | firebase repo id |
| `input.state` | ✓ | state key receiving each update |
| `input.params.collection` | ✓ | Firestore path |
| `input.params.notification` | — | Single action fired on every update |
| `output.success` | — | Fires ONCE when first data arrives |
| `output.failure` | — | Fires if stream errors |

### 8.4 `service`

| Field | Required | Value |
|---|---|---|
| `input.serviceId` | ✓ | service dependency id |
| `input.params` | ✓ | service-specific |

### 8.5 `template`

| Field | Required | Value |
|---|---|---|
| `input` | ✓ | string — the dependency id of the action to invoke |

Runs with caller's context/state.

### 8.6 `navigate`

| Field | Required | Value |
|---|---|---|
| `input.operation` | ✓ | `push` \| `pop` \| `popToRoot` |
| `input.source` | ✓ for push | `dp://…` or `file://…` (source string, NOT inline object) |
| `input.moduleId` | — (pop only) | id of screen to pop back to inclusively |
| `output.success` | — (push only) | Fires after component loaded |
| `debounce` | — | Seconds |

### 8.7 `condition`

| Field | Required | Value |
|---|---|---|
| `input` | ✓ | bool / expr — truthy → `success`, falsy → `failure` |
| `output.success` | — | Actions when truthy |
| `output.failure` | — | Actions when falsy |

### 8.8 `interval`

| Field | Required | Value |
|---|---|---|
| `input.delay` | Conditional (REQUIRED unless `cancel: true`) | Seconds between firings |
| `input.cancel` | — | `true` cancels running interval with this `id` |
| `output.success` | — | Actions fired each tick (resolved fresh every time) |

### 8.9 `ai`

Calls an AI provider; two provider paths, selected by the kind of the repository `input.provider` names — a `remote` repository (module-authored endpoint + interceptor, provider-native `body`) or an **`ai` repository** (host-injected provider; portable `body` convention below). Credentials never appear in JSON on either path.

| Field | Required | Value |
|---|---|---|
| `input.provider` | ✓ | id of the repository fronting the provider: `remote` kind (AI endpoint + auth interceptor) or `ai` kind (host-managed, declare `{ "repository": { "ai": { "id": "my_ai", "config": {} } } }`) |
| `input.body` | ✓ | literal provider request payload (model, prompt/messages, response schema, …); POSTed as the HTTP body |
| `input.subUrl` | — | path appended to the repo's `endPoint` (default `""`) |
| `input.responsePath` | — | structural path into the response (e.g. `output[0].content[0].text`) selecting the answer field; omitted → raw response binds |
| `input.parseJson` | — | when `responsePath` is set, JSON-parse the extracted string (default `true`); `false` keeps it verbatim |
| `output.success` | — | success chain; unwrapped result via `@{actionId}` |
| `output.failure` | — | failure chain; error via `@{actionId.code}`, `@{actionId.message}` |

Switch providers (OpenAI ⇄ Anthropic) by pointing `provider` at a different `remote` repo and authoring the matching `body` + `responsePath` — no runtime change. The key reaches the provider via its interceptor (`setBearerToken`, or `setHeader` `x-api-key`) reading from a `secured` repo.

**Host-managed path (`ai` repository kind):** when `provider` names an `ai`-kind repository, the host injects endpoint/credentials/model; `body` is the portable convention `{ "tier": "strong"|"fast", "system": "…", "prompt": "…", "schema": { … }, "data": { … } }` — `schema` describes the exact reply object (small and flat); the reply binds parsed, read fields via `@{actionId.field}`. `subUrl`/`responsePath`/`parseJson` are ignored. Prefer this path whenever the host offers it (no user API key needed). ALWAYS give an `ai` action an `output.failure` chain that resets any UI-gating flag (e.g. `aiThinking: false`) and surfaces a message — a missing failure chain freezes the gated UI forever on the first provider error. Use `ai` only for moves/decisions that genuinely require reasoning; mechanizable logic (random targeting, dice, pick-any-legal) is computed locally with `random()`/`range()`/filters, and an `ai` failure chain falls back to a locally computed random legal move.

```json
{ "ai": { "id": "opponent", "input": {
    "provider": "openai_repo",
    "body": { "model": "gpt-5.4",
      "input": [ { "role": "user", "content": "{{ 'Board: ' + cast(${board}, 'str') }}" } ],
      "text": { "format": { "type": "json_schema", "name": "move",
        "schema": { "type": "object", "properties": { "from": {"type":"integer"}, "to": {"type":"integer"} },
          "required": ["from","to"], "additionalProperties": false } } } },
    "responsePath": "output[0].content[0].text"
  }, "output": { "success": [
    { "state": { "id": "apply", "input": { "lastMove": "@{opponent}" } } }
  ] } } }
```

### Result envelopes

| Action | `@{id}` on success | Failure branch reachable? |
|---|---|---|
| `repo` | Response body (any JSON) | ✅ |
| `ai` | Provider response, optionally unwrapped via `responsePath` / `parseJson` | ✅ |
| `service` | Service-defined value or null | ✅ |
| `listen` | — (use notification; success fires once) | ✅ |
| `state` | — (not populated) | ❌ never (no failure path; `output.success` allowed, non-state only) |
| `condition` | — | ✅ (when falsy) |
| `navigate` | — | ❌ never |
| `template` | Inherited from invoked action | Inherited |
| `interval` | — | ❌ never (no failure branch) |

### Chaining & scope

- Co-declared actions in one event array fire **in parallel**; completion order not guaranteed.
- `output.success` / `output.failure` chains run **sequentially**, each awaiting the previous. `state` actions accept `output.success` only (and only with non-state inner actions); see §8.1.
- A chain-less `state` write propagates upward by name match to any ancestor that has declared the key (the dispatching module does NOT need to declare it). A chained `state` write (with `output.success`) **does not propagate** — the chain runs in the dispatching module's context and the write stays local. See §8.1.

---

## 9. Dependencies

Declare in `module.dependencies` array. Initialised when module is decoded. Ordering matters — a dependency may only reference earlier-declared ones.

### 9.1 Repositories

```json
{ "repository": { "remote":   { "id": "api",    "config": { "endPoint": "https://..." } } } }
{ "repository": { "local":    { "id": "local",  "config": {} } } }
{ "repository": { "secured":  { "id": "keys",   "config": {} } } }
{ "repository": { "firebase": { "id": "fb",     "config": { "collectionPath": "apps/id" } } } }
{ "repository": { "photos":   { "id": "photos", "config": {} } } }
```

| Kind | Config |
|---|---|
| `remote` | `endPoint` (REQUIRED), `interceptor` (optional) |
| `local` | `{}` |
| `secured` | `{}` |
| `firebase` | `collectionPath` (used by `listen`) |
| `photos` | `{}` |

**Common action shape.** Every `repo` action's `input` has the same three keys:

```json
"input": {
  "repositoryId": "<dep id>",
  "operation":    "read" | "create" | "update" | "delete",
  "params":       { ... kind-specific keys ... }
}
```

The runtime extracts `params` and hands it to the matching repo kind. Keys inside `params` that the kind does not recognise are silently ignored — only the documented keys reach the underlying call.

#### `remote`

HTTP verbs: `create` → POST, `read` → GET, `update` → PUT/PATCH, `delete` → DELETE. `params` recognises:

| Key | Meaning |
|---|---|
| `subUrl` | Path appended to `endPoint` (e.g. `"/v1/search"`). Optional; defaults to `""`. |
| `body` | The request payload. On `GET` it is flattened into query parameters (`?k=v&…`). On `POST` / `PUT` / `DELETE` it is encoded as a JSON request body. |

Headers default to `accept: application/json`, `content-type: application/json; charset=utf-8`, `cache-control: no-cache`. Auth headers come from `config.interceptor`.

```json
// GET https://geocoding-api.open-meteo.com/v1/search?name=Kyiv&count=1
"repo": { "id": "geo_call", "input": {
  "repositoryId": "geo_api",
  "operation":    "read",
  "params":       { "body": { "name": "${city}", "count": 1, "format": "json" } }
} }
```

#### `local`

`params` recognises:

| Key | Operations | Meaning |
|---|---|---|
| `collection` | all | Collection (table) name within the repo. |
| `documentId` | `read` (optional, fetches a single doc), `update` / `delete` (REQUIRED), `create` (optional) | Document identifier. |
| `body` | `create` / `update` | The document's value (dict). |
| `rewrite` | `create` | If `true` and `documentId` already exists, overwrite; otherwise create fails. |

Read returns the whole collection when `documentId` is omitted. Every row exposes its id under the field `documentId`; subsequent `delete` / `update` calls reference rows as `@{item.documentId}`. When `documentId` is omitted on `create`, the runtime assigns `UUID().uuidString`. Identity must be unique within the collection — if you supply `documentId` explicitly, you own its uniqueness.

#### `secured` (keychain)

The shape of `params` depends on the operation:

| Operation | `params` shape | Meaning |
|---|---|---|
| `read` / `delete` | plain string | The keychain key name (e.g. `"params": "openai_token"`). |
| `create` / `update` | `{ key, value }` | `key` (string) — keychain key. `value` (string) — secret to store. |

`read` returns the stored string. Used most often via `interceptor.adapter.setBearerToken` in a remote repo's config — keep secrets out of state, inject them into auth headers at request time.

#### `photos`

`params` recognises:

| Key | Operations | Type | Meaning |
|---|---|---|---|
| `selectionLimit` | `read` | int (default `1`) | Max number of items the picker accepts. |
| `mediaTypes` | `read` | array of string (default `["image"]`) | Filter (e.g. `["image"]`, `["video"]`, `["image", "video"]`). |
| `url` | `create` | string | `file://` URL of an image to save to the library. |
| `assetIdentifier` | `delete` | string | `PHAsset.localIdentifier` returned by a prior `read`. |

`read` **always returns an array** of `file://` URL strings — one element per item picked, `[]` if the user cancels — for any `selectionLimit`. Bind a single pick with `first(@{<read-id>})` (or `@{<read-id>}[0]`); append a multi-pick with `${target} + @{<read-id>}`. Each URL is usable directly as a media / `image` `_source` (no scheme prefix).

#### `firebase`

Driven by the `listen` action, not direct `repo` calls. See §8.x `listen`.

**Remote interceptor** (auth injection):
```json
"interceptor": {
  "adapter": { "operations": [
    { "kind": "setBearerToken", "repositoryId": "keys", "key": "token_name" }
  ] },
  "retrier": { "maxRetries": 3, "retryDelay": 1.0 }
}
```
Operation `kind`: `setHeader` (needs `field`), `setBearerToken`, `setQueryParam` (needs `field`). Token source: either `repositoryId + key` or static `value`.

### 9.2 Services

```json
{ "service": { "audio": { "id": "audio" } } }
{ "service": { "push":  { "id": "push"  } } }
```

| Kind | Params |
|---|---|
| `audio` | `sound` (enum: `newMail`, `mailSent`, `smsReceived`, `sentMessage`, `tweetSent`, `phoneBusy`, `busyTone`, `keyPress`, `lock`, `unlock`, `shutter`, `vibrate`, `none`), `haptic` (enum: `light`, `medium`, `success`, `warning`, `error`, `none`) |
| `push` | `title`, `body`, `when` (`{ now: true }` or scheduled) |

Host apps may register additional service kinds.

### 9.3 Component & action dependencies

```json
{ "component": { "id": "card_tpl", "type": "vstack", ... } }   // reusable via dp://card_tpl
{ "action":    { "repo": { "id": "refresh_action", ... } } }  // invoked via template action
```

---

## 10. Style system

Styles live in `component.style`, grouped into named domains. Apply any combination. Declared style overrides inherited style for the matching key.

### `style.common`
`foreground` (color/gradient), `background` (color/material/gradient/component), `font` (preset or font object), `cornerRadius` (num), `opacity` (num 0–1 or expr), `scale` (num/expr, animatable), `overlay` (component), `tint` (color), `disabled` (bool/expr), `glass` (string — see `style.common.glass`).

### `style.layout`
`width` (num), `height` (num), `minWidth` (num), `minHeight` (num), `maxWidth` (num or `"infinity"`), `maxHeight` (num or `"infinity"`), `alignment` (`leading`/`center`/`trailing`/`top`/`bottom`/`topLeading`/`topTrailing`/`bottomLeading`/`bottomTrailing`; default `center`), `padding` (number for all sides, OR object `{ top, bottom, leading, trailing, horizontal, vertical }` — NO `all` key), `offset` (`{ x, y }`).

### `style.navigation`
`navigationTitle` (string/expr), `navigationBarTitleDisplayMode` (`automatic`/`inline`/`large`; default `automatic`), `toolbarLeading` (component), `toolbarTrailing` (component).

### `style.button`
Two forms. **Standard:** `{ standard: { type: <enum>, shape: <enum> } }` with `type` ∈ `plain`/`borderless`/`automatic`/`bordered`/`borderedProminent` and `shape` ∈ `capsule`/`circle`/`automatic`/`roundedRectangle`. **Custom:** `{ custom: { layout: {…}, common: {…} } }` — embeds `style.layout` and `style.common` for per-button overrides.

### `style.shape`
`fill` (color/gradient), `stroke` (`{ content: <color or gradient — same value forms as fill>, style: { lineWidth } }` — e.g. `content: { "gradient": { "linear": … } }` strokes with a gradient; the gradient is the discriminator itself, never nested inside `color`), `trim` (`{ from: 0.0, to: 1.0 }` — for progress-ring effects), `rotation` (`{ angle, anchor: "center" }` — animatable), `inset` (num).

### `style.animation`
`kind` (`spring`/`easeIn`/`easeOut`/`easeInOut`/`linear`/`none`), `value` (expr or state ref whose change triggers the animation), `duration` (seconds), `bounce` (num 0–1, spring only), `blendDuration` (seconds), `repeater` (`{ count, repeatForever, autoreverses }`).

### `style.modal`
**A `style.modal` block declares one or more modal types**, keyed by type name (each type at most once). Valid keys: `alert`, `popover`, `sheet`, `fullScreenCover`. Every declared modal is attached independently with its own `isPresented` (mirrors SwiftUI's stackable `.alert(…).sheet(…)`) — one component can present, e.g., both an `alert` and a `sheet`. When several are declared they attach in a fixed order: alert → sheet → popover → fullScreenCover.

All types REQUIRE an `isPresented` binding to a state boolean.

- `alert`: REQUIRED `title`, `isPresented`. Optional `message`, `actions` (array of button components).
- `popover`: REQUIRED `isPresented`, `content` (component).
- `sheet`: REQUIRED `isPresented`, `content` (component).
- `fullScreenCover`: REQUIRED `isPresented`, `content` (component).

```json
"style": { "modal": {
  "sheet": { "isPresented": "${showSettings}",
             "content": { "id": "settings_root", "type": "vstack", "children": [...] } }
} }
```

### `style.text`
`multilineTextAlignment` (`leading`/`center`/`trailing`; default `leading`), `lineLimit` (int), `lineSpacing` (num), `truncationMode` (`head`/`middle`/`tail`; default `tail`), `minimumScaleFactor` (num 0–1), `bold` (bool), `italic` (bool), `textCase` (`uppercase` or `upper` / `lowercase` or `lower`).

### `style.textField`
`standard` (`automatic`/`plain`/`roundedBorder`; default `automatic`), `keyboardType` (`default`/`emailAddress`/`numberPad`/`decimalPad`/`phonePad`/`url`/`numbersAndPunctuation`/`twitter`/`webSearch`/`asciiCapable`), `autocapitalization` (`sentences`/`words`/`characters`/`never` or `none`; default `sentences`), `submitLabel` (`done`/`go`/`next`/`return`/`search`/`send`/`continue`/`join`), `autocorrectionDisabled` (bool).

### `style.datePicker`
`preset` (`compact`/`graphical`/`wheel`/`automatic`; default `automatic`).

### `style.shadow`
`color` (color value; default black reduced opacity), `radius` (num — blur radius in points), `x` (num — horizontal offset, positive right), `y` (num — vertical offset, positive down), `outer` (bool — prevents shadow bleeding through transparent areas).

### `style.scroll`
`anchor` (string or object — `top`/`bottom`/`center`/`leading`/`trailing`/`topLeading`/`topTrailing`/`bottomLeading`/`bottomTrailing`; `"bottom"` enables auto-scroll-to-bottom), `scrollDisabled` (bool), `bounce` (bool), `paging` (bool), `dismissesKeyboard` (`automatic`/`interactively`/`immediately`/`never`).

### `style.swipe`
`leading` (SwipeManifest — swipe from left edge), `trailing` (SwipeManifest — swipe from right edge).

**SwipeManifest** shape: `{ content: <component rendered as the action button>, allowsFullSwipe: <bool, default false> }`.

### `style.section`
`listRowBackground` (color value — use `"clear"` to remove default row background), `listRowInsets` (string `"zero"` to remove default insets, OR object `{ top, bottom, leading, trailing }`).

### `style.map`
`kind` (`standard` (default — vector road map) / `hybrid` (satellite + roads/labels) / `imagery` (satellite only)).

### `style.picker`
`picker` (`segmented`/`menu`/`wheel`/`inline`/`automatic`; default `automatic`). Accepts the enum string directly at the value position, e.g. `"style": { "picker": "menu" }` — the enum is the value of the `picker` key.

### `style.video`
`controls` (bool; default `true`), `gravity` (`resizeAspect` (default) / `resizeAspectFill` / `resize`).

### `style.pdf`
`displayMode` (`singlePage` (default) / `singlePageContinuous` / `twoUp` / `twoUpContinuous`), `displayDirection` (`vertical` (default) / `horizontal`), `autoScales` (bool; default `true`), `showPageNumbers` (bool; default `false`).

### `style.grid`
`columns` (array of track descriptors — for `lazyVGrid`), `rows` (array — for `lazyHGrid`), `pinnedViews` (`sectionHeaders`/`sectionFooters`/`all`).

Track descriptor: `{ kind: "fixed"|"flexible"|"adaptive", size: <num, fixed only>, minimum: <num>, maximum: <num>, spacing: <num>, alignment: <string> }`.

### `style.splitView`
`preferred` (`automatic` (default) / `balanced` / `prominentDetail`), `visibility` (`automatic` (default) / `all` / `detailOnly`), `sidebarWidth` (`{ min, ideal, max }` — omit for system defaults).

### `style.accessibility`
`label` (string/expr — VoiceOver label), `hint` (string/expr — action-result description), `value` (string/expr — current value for stateful components), `hidden` (bool/expr — hide from a11y tree for decorative elements), `traits` (comma-separated string of: `button`, `header`, `image`, `link`, `selected`, `searchField`, `startsMediaSession`, `updatesFrequently`, `isModal`), `identifier` (string/expr — stable id for UI testing, not read by VoiceOver).

### `style.tab`
`selectedTint` (color), `unselectedTint` (color), `barBackground` (color).

### `style.common.glass` — iOS 26+
The `glass` key on `style.common`. Values: `regular` (default, medium transparency) / `clear` (high transparency for media-rich backgrounds) / `identity` (no visual effect — for conditional toggling via expressions). Falls back to `.ultraThinMaterial` on iOS ≤ 18. Uses the component's `cornerRadius` for shape (defaults to 16 if unset).

### 10.1 Font values

| Form | Example |
|---|---|
| Preset | `"headline"` (presets: `largeTitle`, `title`, `title2`, `title3`, `headline`, `subheadline`, `body`, `callout`, `footnote`, `caption`, `caption2`) |
| System | `{ "kind": "system", "size": 16 }` |
| Named + weight | `{ "kind": "title", "weight": "bold" }` (weights: `ultraLight`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, `black`) |

### 10.2 Color values

#### Supported color keywords (closed list)

A bare-string color value MUST be one of:

`primary`, `secondary`, `clear`, `accentcolor`, `background`, `surface`,
`surfacevariant`, `onsurface`, `onsurfacevariant`, `outline`, `error`,
`blue`, `white`, `black`, `green`, `red`, `yellow`, `orange`, `purple`,
`gray`, `cyan`, `mint`, `pink`, `indigo`, `teal`, `brown`.

Or a hex literal: `"#RGB"` / `"#RRGGBB"` / `"#RRGGBBAA"`.
Or a state ref: `"${themeColor}"`.

#### Forms

```json
"white"                                    // bare keyword from the list above
"#FF5733"                                  // hex (#RGB / #RRGGBB / #RRGGBBAA)
"${themeColor}"                            // state ref
{ "color": "blue", "opacity": 0.5 }        // with opacity
{ "light": "#FFF", "dark": "#000" }        // adaptive
{ "light": "#FFF", "dark": "#000", "opacity": 0.8 }
```

#### Anti-patterns — NOT supported (will fail validation)

Do NOT use UIKit / SwiftUI semantic color names. They render as literal
strings and validation rejects them:

`label`, `secondaryLabel`, `tertiaryLabel`, `quaternaryLabel`,
`placeholderText`, `separator`, `opaqueSeparator`, `link`, `systemBlue`,
`systemRed`, `systemOrange`, `systemGreen`, `systemYellow`, `systemPink`,
`systemPurple`, `systemTeal`, `systemIndigo`, `systemGray` (and `systemGray2`
… `systemGray6`), `systemBackground`, `secondarySystemBackground`,
`tertiarySystemBackground`, `systemFill`, `secondarySystemFill`,
`tertiarySystemFill`, `lightText`, `darkText`.

For "muted text" use `"secondary"`. For tertiary contrast, opacity it:
`{ "color": "primary", "opacity": 0.6 }`. For brand accent, use the host's
inherited tint — don't hardcode hex.

### 10.3 Gradient values

Gradient itself (the value placed UNDER the `gradient` discriminator — see §10.4):

```json
{ "linear":  { "colors": [...], "startPoint": "top",   "endPoint": "bottom" } }
{ "radial":  { "colors": [...], "center": "center",   "startRadius": 0, "endRadius": 200 } }
{ "angular": { "colors": [...], "center": "center",   "startAngle": 0, "endAngle": 360 } }
```

A raw `{ "linear": {...} }` is NOT a valid `background` or `foreground` value by itself — it must be wrapped with the `gradient` discriminator. See §10.4.

### 10.4 Background / foreground / fill forms

`style.common.background`, `style.common.foreground`, and `style.shape.fill` all accept the same shape-style value: a plain color OR a **dict with a single discriminator key** (`color` / `gradient` / `material`) OR an adaptive-color dict OR a component.

| Form | Example |
|---|---|
| Plain color string | `"background": "blue"`,  `"background": "#4A90D9"`,  `"background": "${dynamicHex}"` |
| Color discriminator | `"background": { "color": "blue" }`,  `"background": { "color": "#4A90D9" }` |
| Color + opacity | `"background": { "color": "blue", "opacity": 0.5 }` |
| Adaptive (light/dark) | `"background": { "light": "white", "dark": "#1C1C1E" }` |
| Adaptive + opacity | `"background": { "light": "white", "dark": "#1C1C1E", "opacity": 0.8 }` |
| Material | `"background": { "material": "ultraThin" }` (values: `ultraThin` / `thin` / `regular` / `thick` / `ultraThick`) |
| Gradient | `"background": { "gradient": { "linear": { "colors": ["#A", "#B"], "startPoint": "top", "endPoint": "bottom" } } }` |
| Color + material | `"background": { "color": "blue", "material": "ultraThin" }` |
| Component (background image / animated layer) | `"background": { "type": "template", "id": "...", "context": { "_source": "dp://..." } }` |

**Critical:** when using a gradient/material/color discriminator, the value MUST be wrapped with that key. Writing `"background": { "linear": {...} }` directly is INVALID — the runtime can't tell whether the dict is meant as a gradient, color object, or background-component, so it rejects the whole value.

The exact same forms apply to `style.common.foreground` and `style.shape.fill` (with the obvious exception that `foreground` does NOT accept materials or background components — color/adaptive/gradient only).

---

## 11. Modals

See `style.modal` above for the full schema. Summary:

- Attached via `style.modal` on any component.
- Modal type is the top-level key inside `style.modal` (not a `kind` property). Four types: `alert`, `popover`, `sheet`, `fullScreenCover`.
- A `style.modal` block may declare **one or more** modal types (each key at most once); every declared modal is attached independently with its own `isPresented`. So a single component can present both an `alert` and a `sheet` — no need to split them onto separate components. When several are declared, attachment order is fixed: alert → sheet → popover → fullScreenCover.
- `isPresented` is a two-way state-boolean binding; auto-updates to `false` when the user dismisses.
- `alert` requires `title` + `isPresented` (optional `message`, `actions` array of button components). `popover` / `sheet` / `fullScreenCover` require `isPresented` + `content` (a component).

---

## 12. Navigation rules

- `navigation` component is REQUIRED as ancestor of any `navigate` action.
- `navigate` operations: `push` (needs `source`), `pop` (optional `moduleId`), `popToRoot`.
- `source` must be a **source string** (`dp://` or `file://`) — NOT an inline component.
- **For single-file inline modules (the default and the typical AI-authoring case), use `dp://<component-id>` ONLY.** The destination module must be declared in `dependencies` as a `component`, then pushed via `dp://`. `file://` resolves to a file in the StemJSON zip package — single-file inline modules have no file bundle, so `file://anything.json` resolves to nothing and the push silently fails (no error, no transition). Use `file://` only when shipping a multi-file `.stemzip` package and you genuinely have that file inside it. See §14 *Package format*.

```jsonc
// ✅ Correct: declare the destination as a component dependency, push via dp://
"dependencies": [
  { "component": { "id": "detail_screen", "type": "module", "children": [ … ] } }
],
"events": {
  "onTap": [
    { "navigate": { "id": "go_detail",
        "input": { "operation": "push", "source": "dp://detail_screen" } } }
  ]
}

// ❌ Wrong in a single-file module: no zip package, no file by that name
"navigate": { "id": "go_detail",
  "input": { "operation": "push", "source": "file://detail_screen.json" } }
```

- `link` component's `destination` MUST be `type: "module"`. Non-module destinations render but their events (`onAppear`) don't fire.
- Pushed destination module has **fresh state** (not shared with parent). It inherits the parent's full **author-declared context** (every non-`_`-prefixed key); inside the destination, `@{key}` resolves against `self → parent → … → root`, nearest binding wins.
- To pass data across screens: (a) put the value in the parent module's context (custom key, e.g. `"selectedId": "${currentId}"`) and read it inside the child via `@{selectedId}`; (b) include it in `@{item}` inherited at push time (for `dynamic.prototype` / `link.destination` flows); or (c) child re-fetches in its `onAppear` using context parameters.

---

## 13. State management

- State is declared at module root as a dict of initial values (literals only).
- **Every key an action writes and every `${key}` you bind must appear in the owning module's `state` block.** The runtime silently drops writes to undeclared keys and resolves undeclared bindings to null — inventing a transient flag (`pending`, `loading`, `aiThinking`) inside an action without declaring it produces a no-op, not an error.
- **Initial values MUST lie inside the domain of any component bound to them.** Each input component declares its domain via its required context — slider range, picker options, toggle bool, datepicker timestamp, and any future component that declares one. The runtime does NOT auto-clamp or coerce out-of-domain initials; they produce broken UI. Where the natural default isn't expressible as a literal (e.g. `#{today}`), declare a placeholder literal and resolve to the proper value in an `onAppear` `state` action.
- **No schema translation across data-flow boundaries.** Whenever a value crosses a boundary — `state` write → later read, `repo.create.body` → later `@{item.<key>}` read, host `trigger` payload → `onCustom` handler, observed-key new value → action chain — the runtime passes it through unchanged. Drift between write schema and read schema is silent (paths resolve to `.none`). Authors maintain consistency at every boundary.
- **Authors own end-to-end value semantics.** The runtime applies declared constraints (`_options` on a picker, `_keyboard` on a textfield, `_format` on a text, `_components` on a datepicker) but does NOT infer intent. Each input has a fixed value contract — e.g. `slider` produces `double` in `0..1`, `toggle` produces `bool`, `textfield` produces `string`. There is no implicit alignment between that contract value and what state, display, or storage actually need. If you want an integer 1-10 from a slider, scale + cast at every consumer (display, repo write, history read). Formatted display (`"5/10"`, `"$12.99"`) requires `_format` or a `{{ … }}` expression. The runtime never rounds, casts, clamps, or relabels — the value flows through unchanged and the author makes all positions agree.
- Each module owns its declared keys **exclusively**. Parent modules cannot write to child-module state; siblings cannot write to each other's state.
- `state` action is the **sole** update mechanism; partial merge of known keys only.
- Unknown keys in `state.input` are silently ignored **for the local merge** (cannot add new keys at runtime). For a chain-less write, every key in `input` is still considered for upward propagation by name match — the dispatching module does not need to declare it. See §8.1.
- `${key}` reads resolve strictly within the current module — no cross-module read resolution of any kind.
- A `state` action updates the current module's own state. If — and only if — it has no `output.success` chain, it then propagates upward by name match: any ancestor module that has declared a state key of the same name is updated with the same partial-merge value. This is the only cross-module state transfer mechanism and is unidirectional (descendant-to-ancestor).
- The `state` action allows `output.success` only for sequencing **non-state** follow-ups (see §8.1); `output.failure` is not allowed. **Adding `output.success` suppresses propagation** — the chain runs in the dispatching module's context and the write stays local. If you need both effects, dispatch two separate state actions (see the two-writes pattern in §8.1).

---

## 14. Package format

### Single file
One root module JSON.

### Zip package

```
package.zip
├── main.json                 ← REQUIRED — root module
├── detail.json               ← optional sub-module (loaded via file://detail.json)
├── localization/
│   ├── en.strings            ← format: "key" = "value";
│   └── uk.strings
└── assets/
    └── logo.png              ← loaded via file://assets/logo.png
```

- `file://` is relative to the zip root.
- Package resources override host-app resources on name conflict.
- Missing `main.json` at archive root = validation failure.

---

## 15. Error codes + severity

### Severity levels (low → high)

`bingo`, `info`, `note`, `warning`, `error`, `critical`

`error` / `critical` block rendering.

### Error codes

| Category | Code |
|---|---|
| General | `unknown`, `cancelled`, `timeout`, `decodingFailed`, `encodingFailed`, `missingData`, `dependencyNotRegistered` |
| Network | `invalidResponse`, `noConnection`, `invalidURL`, `badRequest`, `notAuthenticated`, `notAuthorized`, `notFound`, `conflict`, `internalServer`, `serviceUnavailable` |
| Storage | `documentExists`, `documentNotFound`, `noDocumentId`, `savingFailed`, `deletionFailed` |
| Security | `keychainAccess`, `itemNotFound`, `userCanceledAuth`, `enclaveLocked` |

Access in failure chains via `@{actionId.code}` and `@{actionId.message}`.

---

## 16. `_debug` key

Inspects a specific component at runtime. Development-only; MUST NOT emit in production.

**String form** (entire area):
```json
"_debug": "context"    // entire resolved context dict, every render
"_debug": "events"     // event map + traces on each fire
"_debug": "style"      // resolved style dict, every render
```

**Dict form** (narrow path):
```json
"_debug": { "context": "_title" }         // just _title
"_debug": { "events": "onChange.actions" }
"_debug": { "style": "common.opacity" }
```

---

## 17. Canonical examples

### 17.1 Form with state + repo action

```json
{
  "id": "mod_login", "type": "module", "version": "1.0",
  "state": { "email": "", "password": "", "error": "" },
  "dependencies": [
    { "repository": { "remote": { "id": "api", "config": { "endPoint": "https://example.com/api" } } } }
  ],
  "children": [ { "id": "form_root", "type": "vstack",
    "style": { "layout": { "padding": 16 } },
    "children": [
      { "id": "email_in",    "type": "textfield",
        "context": { "_label": "Email",    "_text": "${email}",    "_keyboard": 7 } },
      { "id": "password_in", "type": "textfield",
        "context": { "_label": "Password", "_text": "${password}", "_isSecured": true } },
      { "id": "err_text",    "type": "text",
        "context": { "_text": "${error}" },
        "style": { "common": { "foreground": { "color": "red" } } } },
      { "id": "submit_btn",  "type": "button",
        "context": { "_label": "Sign In" },
        "events": { "onTap": [
          { "repo": { "id": "auth_call",
            "input": { "repositoryId": "api", "operation": "create",
                       "params": { "body": { "email": "${email}", "password": "${password}" } } },
            "output": {
              "success": [ { "navigate": { "id": "to_home",
                              "input": { "operation": "push", "source": "dp://home_module" } } } ],
              "failure": [ { "state": { "id": "show_err",
                              "input": { "error": "@{auth_call.message}" } } } ]
            }
          } }
        ] }
      }
    ]
  } ]
}
```

### 17.2 Dynamic list from data

```json
{ "id": "list_wrap", "type": "scroll",
  "children": [
    { "id": "rows", "type": "dynamic",
      "context": { "_data": "${items}" },
      "prototype": { "id": "row_tpl", "type": "hstack",
        "context": { "_spacing": 12 },
        "children": [
          { "id": "row_img",  "type": "image",
            "context": { "_source": "@{item.image}" },
            "style": { "layout": { "width": 40, "height": 40 } } },
          { "id": "row_text", "type": "text",
            "context": { "_text": "@{item.title}" } }
        ]
      }
    }
  ]
}
```

### 17.3 Multi-screen navigation (push/pop)

```json
{ "id": "mod_home", "type": "module", "version": "1.0",
  "state": {},
  "dependencies": [
    { "component": { "id": "detail_module", "type": "module", "state": {},
      "children": [ { "id": "detail_root", "type": "vstack", "children": [
        { "id": "back_btn", "type": "button", "context": { "_label": "Back" },
          "events": { "onTap": [ { "navigate": { "id": "back", "input": { "operation": "pop" } } } ] }
        }
      ] } ] } }
  ],
  "children": [ { "id": "home_nav", "type": "navigation",
    "context": { "navigationTitle": "Home" },
    "children": [ { "id": "home_root", "type": "vstack", "children": [
      { "id": "open_btn", "type": "button", "context": { "_label": "Open Detail" },
        "events": { "onTap": [
          { "navigate": { "id": "push_detail",
            "input": { "operation": "push", "source": "dp://detail_module" } } }
        ] } }
    ] } ]
  } ]
}
```

---


## 17.6 Formatting numeric display values

Numeric values render in `_text` at full IEEE 754 precision unless formatted. The runtime never rounds — per §13 (authors own end-to-end value semantics). Three idioms to display a number readably:

| Goal | Form |
|---|---|
| Locale-aware decimal/currency/percent | `"_text": "${v}"` + `"_format": { "number": { "style": "decimal", "maximumFractionDigits": 1 } }` (see §4.3) |
| Integer truncation | `"_text": "{{ cast(${v}, 'int') }}"` |
| Scale 0..1 → 1..N integer | `"_text": "{{ cast(${v} * (N - 1) + 1, 'int') + ' / ' + cast(N, 'str') }}"` |
| Scale 0..1 → percent | `"_text": "{{ cast(${v} * 100, 'int') + '%' }}"` |

Treat raw `"_text": "${doubleKey}"` or `"_text": "@{item.doubleField}"` as a smell. Format at every consumer (display, history row, stat card), not at the writer — keep the stored value in full precision.

---


## 17.7 Formatting date / time display values

Dates stored in repos round-trip as `double` (Unix epoch seconds — see §3
"Date lifecycle"). Rendering one in a `text` requires explicit formatting:
the runtime does NOT auto-detect a timestamp and never substitutes a locale
string for the raw number. Without `_format` the user sees
`1779434773.6742659`, not `"Yesterday"`.

### `_format` shape — single-key wrapper

`_format` on `text` is a **single-key dict** naming the formatter type. The
only valid keys are `date`, `number`, `duration`. Bare-string `_format`
values (e.g. `"_format": "date"`) are silently ignored and the raw value
renders unformatted.

```jsonc
"_format": { "date": "datetime" }                              // ✅ string shorthand inside wrapper
"_format": { "date": "friendly" }                              // ✅ today/yesterday-style
"_format": { "date": { "style": "datetime",
                       "dateStyle": "medium",
                       "timeStyle": "short" } }                // ✅ dict form
"_format": "date"                                              // ❌ silently ignored
"_format": { "style": "date" }                                 // ❌ missing wrapper
"_format": { "date": { "format": "MMM d" } }                   // ❌ no "format" key — use "pattern"
```

### Style values (string shorthand inside `{ "date": ... }`)

| Style | Output (en_US) | Use for |
|---|---|---|
| `"date"` | `"May 22, 2026"` | Day-precision display, no time |
| `"time"` | `"4:14 PM"` | Time-precision display, no date |
| `"datetime"` | `"May 22, 2026 at 4:14 PM"` | Standard timestamp display |
| `"friendly"` | `"Today"` / `"Yesterday"` / falls back to dateStyle for older | History rows where recency matters |
| `"relative"` | `"3 hours ago"` | Activity feeds |
| `"iso8601"` / `"rfc3339"` | `"2026-05-22T16:14:00.000Z"` | Machine-readable / export |

Any other string inside the `date` value is treated as a **custom ICU
pattern** (`"yyyy-MM-dd"`, `"HH:mm"`, `"MMM d, h:mm a"`).

### Dict-form keys

| Key | Values |
|---|---|
| `style` | `"date"` / `"time"` / `"datetime"` / `"friendly"` / `"relative"` / `"iso8601"` / `"rfc3339"` / `"custom"` |
| `pattern` | ICU date-format pattern. Used when `style: "custom"` or when supplied without `style`. E.g. `"yyyy-MM-dd"`, `"HH:mm"`. |
| `dateStyle` | `"short"` / `"medium"` / `"long"` / `"full"` — overrides default for the date portion |
| `timeStyle` | Same values — overrides default for the time portion |
| `locale` | Locale identifier (e.g. `"en_US"`). Defaults to `#{locale}`. |

### Idioms

```jsonc
// History row — recent days get "Today/Yesterday", older fall back to medium date.
{ "id": "row_date", "type": "text",
  "context": { "_text": "@{item.ts}", "_format": { "date": "friendly" } } }

// Stat-card timestamp — full datetime.
{ "id": "stamp", "type": "text",
  "context": { "_text": "@{lastEntry.ts}", "_format": { "date": "datetime" } } }

// Time-only column in a daily log.
{ "id": "row_time", "type": "text",
  "context": { "_text": "@{item.ts}", "_format": { "date": "time" } } }

// Export-friendly ISO string.
{ "id": "iso", "type": "text",
  "context": { "_text": "@{item.ts}", "_format": { "date": "iso8601" } } }

// Custom ICU pattern.
{ "id": "stamp_custom", "type": "text",
  "context": { "_text": "@{item.ts}",
               "_format": { "date": { "style": "custom", "pattern": "MMM d · HH:mm" } } } }
```

Treat raw `"_text": "@{item.ts}"` against a stored timestamp as a bug. The
field is a double; without `_format` the user sees `1779434773.6742659`.

---

## 18. Hard rules (LLM checklist)

Before emitting a StemJSON payload, verify:

- [ ] Root is `{ "id": ..., "type": "module", "version": "1.0", ... }`.
- [ ] Every component AND action has an `id`; all ids globally unique.
- [ ] Initial `state` values are resolved literals — NO `${…}`, `{{…}}`, `@{…}`, `#{…}`.
- [ ] Initial state values lie inside the domain of any input bound to them (slider range, picker options, toggle bool, datepicker timestamp, …).
- [ ] State refs `${key}` reference keys declared in the **current module's** `state` (no cross-module resolution — parent, child, or sibling state is not accessible).
- [ ] Context refs `@{key}` reference keys present in the component's context chain.
- [ ] Expressions `{{ … }}` use the operators/functions defined here — nothing else.
- [ ] No implicit string↔number coercion — use `cast()`.
- [ ] Every key any action writes is declared in the owning module's `state` block (undeclared writes are silently dropped).
- [ ] Every async action (`ai`, `repo`, `service`) that sets a UI-gating flag has an `output.failure` chain resetting that flag and surfacing a message.
- [ ] Every semantic state the request implies — selected, winner, correct/wrong position, own vs. enemy — materializes as explicit conditional styling/content bound to the domain state that carries it; never left to prose or layout implication.
- [ ] No interaction constraints the request didn't ask for — no long-press requirements, forced sequences, or disabled-until gating; default to direct manipulation.
- [ ] No `ai` action for mechanizable logic (random moves, dice, simple picks) — compute locally; when `ai` IS used for judgment moves, its failure chain falls back to a local random legal move.
- [ ] Underscored keys (`_text`, `_source`) are private. Non-underscored keys are public (inherited).
- [ ] `link.destination` is a component with `"type": "module"`.
- [ ] `navigate` action's `input.source` is a source string (`dp://` or `file://`), NOT an inline component.
- [ ] `onChange` / `onCustom` use the **object** schema (`{ "observed"|"name", "actions" }`), not the array form.
- [ ] `navigation` component is ancestor of any `navigate` action.
- [ ] `image` components have at least one dimension in `style.layout`.
- [ ] Every source-string value carries its `<scheme>://` prefix — `_tabImage` is the only exception.
- [ ] Every string value at a value-accepting field is exactly one form: literal, OR a single bare reference, OR `{{ … }}` — no mixed/multi-ref strings without wrapping.
- [ ] Schemas align across every data-flow boundary (state write → state read; `repo.create.body` → `@{item.<key>}`; host trigger payload → `onCustom`; etc.).
- [ ] `dependencies` entries are ordered — each entry may only reference earlier entries.
- [ ] Filter predicates `[field ~ val]` are written inside `${}` / `@{}` paths, NOT as postfix on expressions.
- [ ] Inside `map()` / `dynamic` prototype, `@{item}` / `@{index}` refer to the innermost iteration — capture outer values into state first if needed.
- [ ] `random()` appears only in an action/lifecycle value (`state` action from `onAppear` / a button), NEVER in a render binding — it re-rolls every render otherwise.
- [ ] Cross-module **state** is NOT shared. Cross-module **context** IS — author-declared (non-`_`) context keys propagate down across every nested component including across module boundaries. Thread shared data via context, the repo, or re-fetch in `onAppear`.
- [ ] No component/action type is used that isn't defined above.
- [ ] Root fits the host surface: a module renders into a finite area below host chrome, and overflow is clipped (not revealed). If a screen's content can exceed one viewport (long forms, dynamic lists, anything that grows with state), the root — or the region that overflows — is a `scroll`. A non-scrolling root (`vstack` + `spacer`s) is only for single-screen layouts, and its primary content uses `maxWidth` / `maxHeight: "infinity"` (NOT fixed point `width` / `height`) so it fits small viewports.

---

## 19. Pointer to full specification

For human-readable prose, rationale, worked examples, and authoring guidelines, see [spec/v1.1.md](./v1.1.md). This condensed reference covers the same normative surface.

---

## 20. iOS / SwiftUI priors that fail in StemJSON

StemJSON is declarative and closed. Models trained on iOS Swift code reach
for patterns from UIKit, SwiftUI, and React Native that look right but fail
validation or render verbatim. This appendix names the wrong patterns
explicitly — far more effective than describing the right ones more
thoroughly. Add to this list whenever a new class of mistake recurs.

### Colors
See §10.2 anti-patterns. Do NOT use `label` / `secondaryLabel` /
`tertiaryLabel` / `systemBlue` / `systemBackground` etc. Use the §10.2
closed list of supported keywords.

### Padding
- ❌ `"padding": { "all": 16 }` — no `all` key
- ✅ `"padding": 16` (single number = all sides), or `{ "top": 16, "bottom": 16, ... }`, or `{ "horizontal": 16, "vertical": 12 }`

### Fonts
- ❌ `"font": { "size": 18, "weight": "semibold", "design": "rounded" }` — no `size` outside `{ "kind": "system", ... }`, no `design`, no `tracking`, no `leading`
- ✅ `"font": "headline"` (preset string), or `{ "kind": "title2", "weight": "semibold" }`, or `{ "kind": "system", "size": 18 }`

### Layout dimensions
- ❌ `"layout": { "frame": { "maxWidth": "infinity" } }` — no `frame` wrapper
- ✅ `"layout": { "maxWidth": "infinity" }` directly under `layout`
- ❌ Fixed point `width` / `height` on primary content of a non-scrolling root (e.g. a 300×300 dial filling the screen) — overflows small viewports; size responsively or wrap in `scroll` (see §18)

### Stack spacing / alignment
- ❌ `"style": { "layout": { "spacing": 24, "alignment": "leading" } }` on a `vstack` / `hstack`
- ✅ `"context": { "_spacing": 24, "_alignment": "leading" }`

### Button content
- ❌ `"context": { "_text": "Save" }` on a `button` — `_text` is for `text` only
- ✅ `"context": { "_label": "Save" }`, or use `children` for custom content

### Custom event names
- ❌ `"events": { "onSave": [...], "onIncrement": [...] }` — no developer-named events
- ✅ Wire actions directly to the standard event (`onTap`, `onSubmit`, etc. — see §7)

### Action references
- ❌ `"events": { "onTap": "handleSave" }` — `onTap` is an array of action wrapper objects, not a string handle
- ✅ `"events": { "onTap": [ { "state": { "id": "...", "input": { ... } } } ] }`

### Format descriptors
- ❌ `"_format": "date"` — silently ignored, raw double renders
- ✅ `"_format": { "date": "friendly" }` — see §17.7

### SwiftUI modifiers
- ❌ `.padding()`, `.foregroundColor()`, `.font()` — there are no modifiers
- ✅ Everything is declarative `style.*` blocks on the component

### Programmatic color refs
- ❌ `"Color.blue"`, `"UIColor.systemBlue"`, `".systemBackground"`
- ✅ Bare keyword from §10.2 closed list

### SF Symbol bare names
- ❌ `"sf:plus.circle"`, `"plus.circle"` as an `_image` / `_source` value
- ✅ `"sf://plus.circle"` (full scheme required everywhere except the `_tabImage` exception, see §5)

### Background gradient
- ❌ `"background": { "linear": { ... } }` — bare gradient without the `gradient` discriminator
- ✅ `"background": { "gradient": { "linear": { ... } } }` — see §10.4

### Object-form picker selection
- ❌ `"_options": [{ "label": "...", "value": "..." }, ...]` — pickers take a flat array of strings
- ✅ `"_options": ["small", "medium", "large"]` — see §6 Input table

### Module wrapping
- ❌ `{ "module": { "id": "...", "type": "module", ... } }` — no wrapper
- ❌ `{ { "id": "...", "type": "module", ... } }` — no double brace
- ✅ The outermost `{` IS the module's opening brace — `{ "id": "...", "type": "module", ... }`

### Merged marker prefixes

- ❌ `"_text": "{{ @${forecast}.temperature }}"` — `@$` is not a marker prefix; runtime parses the whole thing as a literal and renders it verbatim with the curly braces visible. Same for `${@{...}}`, `#{${...}}`, etc.
- ✅ Pick ONE marker per reference based on where the value lives:
  - state → `${forecast.temperature}`
  - context → `@{forecast.temperature}`
  - system → `#{now}`
- The four valid marker prefixes (`${`, `@{`, `#{`, `{{`) are mutually exclusive and never combine. See §2 *Value grammar*.

### Navigation source for a sibling screen

- ❌ `"navigate": { "input": { "operation": "push", "source": "file://detail.json" } }` in a single-file inline module — `file://` resolves inside a zip package; in a single-file module there IS no file named `detail.json`, so the push silently fails (no transition, no error).
- ✅ Declare the destination as a `component` dependency, then push via `dp://`:
  ```jsonc
  "dependencies": [
    { "component": { "id": "detail", "type": "module", "children": [ … ] } }
  ],
  "events": { "onTap": [
    { "navigate": { "id": "go", "input": { "operation": "push", "source": "dp://detail" } } }
  ] }
  ```
