on master help sign in
Document the project and brief milestone 2

docs/ holds what the repo didn't: a snapshot of the design doc's tabs (docs/spec/), where each milestone stands and how to check it (status.md), the questions still open, including eleven that milestone 2 raises and the grammar questions never put to the doc's author (open-questions.md), how to set up a machine and work on the code (development.md), a map of the compiler (compiler.md), and a brief for the developer of milestone 2, types and prelude (prompts/milestone-2.md). README.md links them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRcThHuWjFX8MWUDBeWfyn

commit5cc44b4cb7966874f576a906367ee1437a825010 authorClaude <noreply@anthropic.com> dateFri 30 Oct 2026, 14:21 (1 day ago) parentab797a9
12 files changed+1807−2
README.md+6−2 docs/README.md+37 docs/compiler.md+98 docs/development.md+115 docs/open-questions.md+56 docs/prompts/milestone-2.md+173 docs/spec/core-language.md+175 docs/spec/decisions.md+103 docs/spec/grammar.md+342 docs/spec/implementation-plan.md+104 docs/spec/overview.md+527 docs/status.md+71
README.md+6−2
@@ -1,6 +1,8 @@
11# Jibb
22
3−Jibb is a UI language for Urbit: a Hoon dialect with an indentation layout. Its design doc is the spec, and where this code and the doc disagree, the doc wins.
3+Jibb is a UI language for Urbit: a Hoon dialect with an indentation layout. Its design doc, [Jibb — an Elm-architecture UI language for Urbit](https://claude.ai/artifact/DMBr48qzi5z8hcwn2ccPBv), is the spec, and where this code and the doc disagree, the doc wins.
4+
5+Milestone 1 of the doc's eight, the parser and the `%jibb` mark, is done; milestone 2, types and prelude, is next. [docs/](docs/) holds a copy of the doc's tabs, the status of each milestone, the open questions, a development guide, a map of the compiler, and the brief for milestone 2.
46
57## Layout
68
@@ -10,6 +12,7 @@ Jibb is a UI language for Urbit: a Hoon dialect with an indentation layout. Its
1012- `vendor/`: pinned copies of the base-dev and landscape files the desks need.
1113- `tests/`: golden cases in `golden/<case>/`, and on-ship tests for the `-test` thread in `lib/jibb.hoon`.
1214- `tools/`: dev tooling: `golden`, `hoon` and `doc-blocks`, with their self-test in `testdata/`, and `ship`.
15+- `docs/`: the doc's tabs as Markdown in `spec/`, the status, the open questions, the development guide, the compiler's map, and briefs for milestones in `prompts/`.
1316
1417## Golden tests
1518
@@ -46,7 +49,8 @@ tools/hoon --raw --cord src=tests/golden/foo/source.jibb 'tree:(golden /a/b/jibb
4649Every code block the design doc labels `jibb` is a golden case, `doc-<tab>-<n>-<what>`, where `<n>` counts the tab's code blocks, labelled or not. To check that none is missing or stale, export the doc's tabs as Markdown and pass the files, or a folder of them:
4750
4851```
49−tools/doc-blocks Grammar.md Overview.md Core-language.md
52+tools/doc-blocks docs/spec # the snapshot in docs/spec
53+tools/doc-blocks Grammar.md Overview.md Core-language.md # fresh exports
5054```
5155
5256It matches each labelled block to the case whose `source.jibb` is the block's text plus a newline, and lists the blocks no case holds, each with a diff against the closest unmatched case, and the `doc-*` cases no block holds. Pass every tab with Jibb blocks, or the other tabs' cases count as stale. The exit status is 0 when every block has a case and every case a block, 1 when one doesn't, and 2 when the check can't run, for example because no block is labelled `jibb`.
docs/README.mdnew file+37
@@ -0,0 +1,37 @@
1+# Docs
2+
3+Jibb's spec is a design doc by @iko, [Jibb — an Elm-architecture UI language for Urbit](https://claude.ai/artifact/DMBr48qzi5z8hcwn2ccPBv). Where this repo and the doc disagree, the doc wins. This folder keeps a copy of the doc and what the doc doesn't hold: where the build stands, what is still open, and how to work on the code.
4+
5+| File | What it holds |
6+| --- | --- |
7+| [spec/](spec/) | The doc's tabs, as Markdown |
8+| [status.md](status.md) | The eight milestones, what milestone 1 built, and how to check it |
9+| [open-questions.md](open-questions.md) | Questions still open: the plan's, the grammar's, and those milestone 2 raises |
10+| [development.md](development.md) | Setting up a machine, the ground rules, the checks to run before pushing |
11+| [compiler.md](compiler.md) | A map of `jibb-dev/lib/jibb.hoon`: its API, data, errors and printed formats |
12+| [prompts/milestone-2.md](prompts/milestone-2.md) | A brief for the developer who builds milestone 2, types and prelude |
13+
14+## The spec
15+
16+`spec/` is a snapshot of the doc taken on 2026-10-30, after @iko's answers to the 12 grammar questions that day. Each file is one tab, exported as Markdown and not edited:
17+
18+| File | Tab |
19+| --- | --- |
20+| [spec/overview.md](spec/overview.md) | Jibb — an Elm-architecture UI language for Urbit: the language, the stages, the mark, the host, worked examples |
21+| [spec/grammar.md](spec/grammar.md) | Grammar: lexical rules, literals, runes, imports, modules, molds, tag resolution, patterns, the EBNF |
22+| [spec/core-language.md](spec/core-language.md) | Core language: the untyped language between the checker and both backends |
23+| [spec/decisions.md](spec/decisions.md) | Decisions: every choice, with the date it was made |
24+| [spec/implementation-plan.md](spec/implementation-plan.md) | Implementation plan: the eight milestones, ground rules, architecture, risks, open questions |
25+
26+The doc has a sixth tab, "Loom on Urbit", which is empty, so it has no file.
27+
28+The overview's pipeline diagram, "Jibb pipeline · source to browser in three stages", is embedded content, so the export holds only a placeholder line for it under Architecture. The doc shows it.
29+
30+The snapshot can go stale. Before relying on a detail, check the doc. To refresh it, export each tab as Markdown again (in a Claude session, the Claude Docs connector's `export` gives each tab's Markdown), replace the files, and run:
31+
32+```
33+git diff docs/spec # what changed in the doc
34+tools/doc-blocks docs/spec # every code block labelled jibb has its golden case
35+```
36+
37+A `jibb` block that changed or is new needs its golden case, `tests/golden/doc-<tab>-<n>-<what>/`, where `<n>` counts every code block of the tab, labelled or not ([development.md](development.md)).
docs/compiler.mdnew file+98
@@ -0,0 +1,98 @@
1+# The compiler
2+
3+A map of `jibb-dev/lib/jibb.hoon`, the compiler as milestone 1 left it: the layout pass, the parser, and the printers the golden tests compare. The file explains itself in design notes at its top, numbered 1 to 4; this page says where things are and points there for the reasoning.
4+
5+The plan's Compiler architecture section gives the whole pipeline. Here it is shortened, with the milestone that builds each step; milestone 1 built the first line:
6+
7+```
8+/ui source
9+ → layout pass → parser → a syntax tree per module milestone 1
10+ → module graph: imports resolved, Hoon types read from Clay milestone 2
11+ → type checker, one module at a time, in dependency order milestone 2
12+ → core language milestone 3
13+ → Nock milestone 3
14+ → JavaScript milestone 6
15+```
16+
17+## Constraints
18+
19+- **One core, no Ford runes.** `urbit eval` runs the library as `=> <this file> <expression>`, and a ship builds it as `/lib/jibb.hoon`. So it can't import with `/-` or `/+`, and under `urbit eval` it can't scry. Anything from Clay, such as an imported module's text or a `/sur` file's type, is best passed in by the caller, which keeps every stage testable under eval.
20+- **The standard library is the subject.** Under `urbit eval`, as on a ship, the library is built against `..zuse`: `hoon.hoon`, lull and zuse. It reads literals with Hoon's own parser, `nuck:so`. Hoon's type engine, `ut`, is there for milestone 2, and under eval `ride` compiles Hoon source, such as a `/sur` file without Ford runes, into a type, which tests can use in place of Clay's builds.
21+- **The doc wins.** Where the file and the doc disagree, the doc is right, and the file's notes quote the doc's words for each rule they implement.
22+
23+## API
24+
25+From the header of the file:
26+
27+| Arm | Takes | Gives |
28+| --- | --- | --- |
29+| `+layout` | `[file=path src=@t]` | `(list token)`: never fails; an error is the last token |
30+| `+layout-error` | `[file=path toks=(list token)]` | `(unit error)`: the error token's error, if any |
31+| `+parse` | `[file=path src=@t]` | `(each module error)` |
32+| `+parse-tokens` | `[file=path toks=(list token)]` | `(each module error)` |
33+| `+golden` | `[file=path src=@t]` | `[tokens=@t tree=@t]`: both stages printed, or the rendered error |
34+| `+show-tokens` | `(list token)` | `@t` |
35+| `+show-module` | `module` | `@t` |
36+| `+show-error` | `[src=@t error]` | `@t` |
37+
38+A file is a Clay path whose last segment is the extension, as `/ui/basket/jibb`.
39+
40+## Where things are
41+
42+The core is split into chapters, `+| %name`, in this order:
43+
44+| Chapter | Holds |
45+| --- | --- |
46+| `%positions` | `loc`, `span` and `error` |
47+| `%tokens` | `token`, `lexeme`, `glyph` and `lit`: what the layout pass emits |
48+| `%tree` | The syntax tree: `module`, `import`, `def`, `expr`, `piece`, `qseg`, `item`, `edit`, `arm`, `sample`, `field`, `pattern`, `shape` and `pseg` |
49+| `%api` | The arms above that aren't printers |
50+| `%lexer` | The layout pass's door, `+lexer`, and its helpers |
51+| `%printers` | `+show-tokens`, `+show-module`, `+show-error` |
52+| `%parser-helpers` | Error building (`+fail`, `+fail-in`, `+fail-space`, `+fail-line`, `+moved`, …) and the `+then` combinator |
53+| `%parser` | The parser's door, `+parser`: one arm per EBNF rule, as `+read-module`, `+read-expr`, `+read-mold` |
54+| `%span-check` | `+check-spans`, which `+golden` runs on every tree, and `+check-spans-test` |
55+
56+And the design notes:
57+
58+| Note | Says |
59+| --- | --- |
60+| 1. Positions | Lines and columns count from 1, columns in characters; every token and node has a span, start and end, and the rules a node's span follows |
61+| 2. The layout pass | 2.0 where errors point, then lines, characters, blank lines, levels (INDENT, NEWLINE, DEDENT), the end of the file, tokens and tightness, paths, literals (read with `nuck:so`), strings, and what fails here |
62+| 3. The parser | Why recursive descent (the grammar is LL(2) after the layout pass), the EBNF rule by rule with the node each gives, what the EBNF leaves out and how it's filled in, spacing, and where each error points, with its message |
63+| 4. Printed formats | The token, tree and error formats the golden files hold |
64+
65+## The tree
66+
67+Every node is `[%tag at=span …]`, so `+span-of` finds its span in one place; the few that aren't say so in the `%tree` chapter. The kinds of node:
68+
69+- **Module**: imports (`%sur` for `/-`, `%mark` for `/%`, `%jibb` for `/=`), then definitions (`%mold` for `+$`, `%value` for `++`).
70+- **Expressions** (`expr`): `%lit`, `%str`, `%path`, `%qath`, `%wing`, `%change`, `%pair`, `%unit`, `%group`, `%list`, `%tuple`, `%gate-wide`, `%ctor`, `%call`, `%gate`, `%cast`, `%trace`, `%branch`, `%crash`, and `%body` for an indented body, with its local definitions if it has any.
71+- **Molds as written** (`shape`): `%aura`, `%noun`, `%void`, `%flag`, `%const`, `%ref`, `%tvar`, `%path`, `%apply`, `%cell`, `%union`, `%fn`, `%generic`.
72+- **Samples, fields and patterns**: a sample is `%bind`, `%face`, `%pun`, `%cell` or `%bare`, since a bare name binds anywhere in a sample; a pattern is `%lit`, `%str`, `%any`, `%bind` or `%cell`.
73+- **Literals** (`lit`) keep the form they matched, their aura and value, and their text as written, which the printers show.
74+
75+The tree keeps everything the checker needs to report an error at the code as written: open and closed brackets give the same node, but every node keeps its span.
76+
77+## Positions and errors
78+
79+- A span runs from a node's first character to just past its last. A node ends where its last token ends, never at a layout token, and holds its children's spans in order. `+check-spans` tests both rules on every tree the golden tests print.
80+- An error is `[file=path line=@ud col=@ud msg=tape]`: a start, no end yet. Milestone 2's type errors need more, since the plan asks that "each error names both conflicting sites, with excerpts". Spans keep their ends for that.
81+- Every error points at the first character where the text stops being the start of any valid module (note 2.0). The layout pass never fails: it ends its tokens with an error token, and the parser reports whichever error comes first, its own or that one.
82+- The parser's messages read "expected <what>, found <token>", with an optional "; <hint>". The layout pass's state the rule broken, as "a tab: Jibb uses spaces only" or "`|%` is left out: modules are records". None ends with a period, and all start lowercase unless they open with a name, as "Jibb writes no @c literals". `+show-error` prints `<file>:<line>:<col>: <message>`, the line, and a caret under the column.
83+
84+## Printed formats
85+
86+Note 4 has examples of each.
87+
88+- **Tokens**: one per line, `<line>:<col>`, then `tight` if the token follows the previous one on its line with no space between, then its kind and text.
89+- **Trees**: S-expressions, `(kind line:col fields kids)`, with each node's start; a node whose kids are all leaves prints on one line. Past 64 levels the indent stops growing, so the printed tree stays linear in the text's size.
90+- **Errors**: as above. Control characters print as spaces in the excerpt, so the caret stays under its column.
91+
92+## Tests
93+
94+`+golden` is the only arm the golden tests call: `tools/golden` runs it under `urbit eval` on every case and compares `tokens.txt` and `tree.txt`; `tools/ship gen-tests` writes a `-test` arm per case that does the same on the ship. Adding a stage, such as the checker's output, means adding a field to `+golden`'s product and teaching both runners, the stub libraries in `tools/testdata` and their self-tests about it.
95+
96+## Speed
97+
98+Starting `urbit eval` and building the library takes about 8 s. A 2,000-line module of 400 definitions then parses in about a second. Loops over definitions, arms, lines and items are tail calls, so a long module grows no stack; nesting costs a few stack frames a level, and 100,000 nested brackets still parse (note 3).
docs/development.mdnew file+115
@@ -0,0 +1,115 @@
1+# Development
2+
3+How to work on Jibb: the rules the plan sets, how to set up a machine, and what to run before pushing. The repo's [README](../README.md) describes each tool in full.
4+
5+## Ground rules
6+
7+From the plan's Setup and ground rules, and from @iko:
8+
9+- **The doc wins.** Where code and doc disagree, the doc stands until it changes. [spec/](spec/) is a snapshot; the doc itself is the authority.
10+- **Language changes are agreed first** and recorded in the Decisions tab, before the code changes.
11+- **Questions go to @iko**, as comments on the doc, on the words they're about. While one is open, follow the most likely reading, mark it "(doc question)" in the library's notes, and list it in [open-questions.md](open-questions.md). Where an error points is not a doc question: the library's section 2.0 settles it.
12+- **Every Jibb code block in the doc is a golden test**, and each stage's output is printed and compared. The doc labels its Jibb blocks `jibb`; `tools/doc-blocks` checks that each has its case.
13+- **Each milestone ends with its checks passing on a fake ship**, not only under `urbit eval`.
14+- **Push straight to `master`.** @iko reviews work in the project's chat, not in pull requests, so there are none. Run the checks below first.
15+- **Keep docs/ current**: [status.md](status.md) when a milestone moves, [open-questions.md](open-questions.md) when a question is settled or found.
16+
17+## Setting up a machine
18+
19+The tools need Python 3 and `urbit` (vere) on `PATH`; the ship tools also need a fake `~zod` and click. Everything so far ran with vere 4.6 on Linux.
20+
21+On a usual machine:
22+
23+1. Install vere as [docs.urbit.org](https://docs.urbit.org) says, and get a brass pill.
24+2. Boot a fake ship where `tools/ship` expects it, or point `--pier` or `$JIBB_PIER` elsewhere. Booting takes a few minutes:
25+ ```
26+ mkdir -p /root/urbit && cd /root/urbit
27+ urbit -F zod -B brass.pill -t -x -c zod
28+ ```
29+3. Get click, the command-line client for a ship's `conn.sock`, from [urbit/tools](https://github.com/urbit/tools), at `/root/src/tools/pkg/click/click` or wherever `--click` or `$JIBB_CLICK` says.
30+4. Optionally, clone [urbit/urbit](https://github.com/urbit/urbit) to read Arvo's source. `vendor/SOURCES.txt` names the commit the vendored files come from.
31+
32+In a Claude cloud container, the network blocks urbit.org, bootstrap.urbit.org, nixos.org's installer and GitHub's release downloads. This worked on 2026-10-29, and has to be redone in each new container:
33+
34+```
35+# Nix, single-user
36+curl -sSfL -o install https://releases.nixos.org/nix/nix-2.32.4/install
37+mkdir -p /etc/nix && printf 'build-users-group =\nsandbox = false\n' > /etc/nix/nix.conf
38+sh ./install --no-daemon --no-channel-add --yes
39+. /root/.nix-profile/etc/profile.d/nix.sh
40+
41+# vere, from nixpkgs
42+nix build -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \
43+ --impure --expr 'with import <nixpkgs> {}; urbit'
44+# then put result/bin/urbit on PATH
45+
46+# the brass pill, from Git LFS
47+mkdir -p /root/urbit && cd /root/urbit
48+curl -sSfL -o brass.pill https://media.githubusercontent.com/media/urbit/urbit/master/bin/brass.pill
49+urbit -F zod -B brass.pill -t -x -c zod
50+
51+# click
52+git clone https://github.com/urbit/tools /root/src/tools
53+```
54+
55+`echo '(add 2 2)' | urbit eval` checks that vere works without a ship.
56+
57+## The tools
58+
59+| Tool | What it does |
60+| --- | --- |
61+| `tools/golden` | Runs the golden cases under `urbit eval`: every case, or those matching `--case`; `--update` rewrites the expected outputs; `--show` prints them |
62+| `tools/hoon` | Evaluates Hoon with the library in scope, as `tools/hoon '(golden /a/b/jibb (crip "++ main view"))'`; `--cord name=file` binds a file's text, `-f` reads the expression from a file |
63+| `tools/doc-blocks` | Checks the doc's `jibb` blocks against the `doc-*` golden cases: `tools/doc-blocks docs/spec` |
64+| `tools/ship` | Runs the fake ship: `start`, `stop`, `status`, `log`, `sync <desk>`, `test`, `gen-tests`, `check-commit`, `scry`, `hoon`, `run`, `poke`, `commit`, `mount`, `unmount` |
65+| `tools/testdata/selftest` | Tests `tools/golden` and `tools/hoon` against a stub library, and `tools/doc-blocks` against Markdown it writes |
66+| `tools/testdata/ship-selftest` | Tests `tools/ship`'s test generator and runner against a stub library on the ship |
67+
68+`tools/golden` starts `urbit eval` once and builds the library, which takes about 8 s, then runs every case under `mule`, so a crash fails only its own case.
69+
70+## Before pushing
71+
72+```
73+tools/golden # all cases pass
74+tools/hoon check-spans-test # prints <<>>
75+tools/testdata/selftest # 18 checks, 0 failed
76+tools/doc-blocks docs/spec # every jibb block has its case
77+tools/ship gen-tests --check # tests/lib/jibb.hoon is up to date
78+tools/ship test # the cases pass as -test arms on the ship
79+tools/testdata/ship-selftest # when tools/ship changed
80+tools/ship check-commit # when the mark or the desks changed
81+```
82+
83+`tools/ship test` regenerates `tests/lib/jibb.hoon` and syncs the `jibb-dev` desk before it runs, so commit the regenerated file.
84+
85+## Golden cases
86+
87+A case is a folder `tests/golden/<name>/` holding:
88+
89+- `source.jibb`, the input, ending in one newline;
90+- `about.txt`: a `source:` line with the doc's words or block it comes from, a `tests:` line saying what it exercises, and an `expect:` line, either `parses` or `error at <line>:<col>` and why (one case, `inline-leftout-signed-float-auras`, says `unclear`: see grammar question 13 in [open-questions.md](open-questions.md));
91+- the expected outputs, `tokens.txt` and `tree.txt`, each the output plus one newline.
92+
93+Names say what a case is for:
94+
95+| Name | For |
96+| --- | --- |
97+| `lex-<rule>-ok-<n>`, `lex-<rule>-bad-<n>` | A lexical rule, kept or broken |
98+| `inline-<topic>` | Any other rule, often from one sentence of the Grammar tab |
99+| `doc-<tab>-<n>-<what>` | The doc's `<n>`th code block of the tab, counting every block, labelled or not |
100+
101+Write `source.jibb` and `about.txt` by hand, run `tools/golden --update --case <name>`, and read the outputs against `about.txt` before committing. A changed output on purpose is reviewed with `git diff tests/golden`.
102+
103+## Code conventions
104+
105+`jibb-dev/lib/jibb.hoon` is one core with no Ford runes, so `urbit eval` can run it and Clay can build it. Its design notes open the file in numbered sections; keep them true when the code changes, since they are where the reasoning lives. [compiler.md](compiler.md) maps the file.
106+
107+- An arm's comment is `:: +name: what it is`, then details indented by four spaces.
108+- Types avoid the standard library's names (`mold`, `wing`, `gate`), hence `+shape` for a mold as written. Every type in a recursive cycle has a `$~` default that is a leaf, or Hoon can't build its default.
109+- Two arms do shadow the standard library: `+in`, a bit test, hides the set door `in`, and `+fail` hides Hoon's parser rule `fail`. Inside the library, and under `tools/hoon`, write `~(has in:..zuse s)`, or rename `+in` before code there uses sets.
110+- The parser's errors read "expected <what>, found <token>", with an optional "; <hint>"; the layout pass's state the rule broken, as "a tab: Jibb uses spaces only". No final period, lowercase except for names, and the code the error is about in backticks; example code in a hint, as m(done=%.y), isn't quoted so far. Their wording is fixed by the golden tests once written.
111+- Loops over many items are tail calls, so long inputs grow no stack; check new passes on a long module.
112+
113+## Commits
114+
115+A short subject in the imperative, then a body that says what changed and why, wrapped at 72 columns, as in `git log`. Each commit leaves every check passing.
docs/open-questions.mdnew file+56
@@ -0,0 +1,56 @@
1+# Open questions
2+
3+Questions the doc doesn't settle yet, as of 2026-10-30. Language questions belong to the doc's author, @iko: the plan says they go "as comments on the doc, on the words they're about", and a change to the language is "agreed first and recorded in Decisions". When one is settled, record it there, change the code if needed, and remove it here.
4+
5+## The plan's open questions
6+
7+From [spec/implementation-plan.md](spec/implementation-plan.md), Open questions:
8+
9+| Question | Needed by |
10+| --- | --- |
11+| Are the three proposed core-language rules adopted: gate-free `eq` arguments and map keys, natives that take gates, and no value that depends on itself through a gate? | Milestones 2 and 3 |
12+| What is the typography scale for heading levels and the `role` values? | Milestone 4 |
13+| Does the Eyre channel CSRF test reproduce on a fake ship? @iko has the write-up; if it does, it goes to Urbit privately. | Milestone 4 |
14+| What is the benchmark's bar? Proposed: a keystroke in a 500-row list island renders within one 16 ms frame on a mid-range phone. | Milestone 6 |
15+| Are the proposed loop rules adopted, along with no overlays in v1? | Milestone 6 |
16+| What do controls look like by default: buttons, toggles and fields, with their focus and disabled states? | Milestone 6 |
17+
18+The three core-language rules are in [spec/core-language.md](spec/core-language.md), marked *proposed*: under Values and invariants ("so are `eq`'s arguments and map and set keys, and a gate never tells a union's cases apart"), Programs and linking ("a value that depends on itself, even through a gate, is an error") and Natives ("natives may take gates").
19+
20+## Questions milestone 2 raises
21+
22+Found while writing [prompts/milestone-2.md](prompts/milestone-2.md) and checking it against the doc's examples. The type checker needs an answer to each; none has been put to @iko yet.
23+
24+1. **How the prelude declares natives.** The overview's Prelude section says the prelude is "written in Jibb and vendored as `/lib/jibb/prelude`", and that "Only the prelude declares natives, Jibb's jets, and private helpers". The Core language tab gives each native "a Jibb type, a Hoon gate in `/lib/jibb` … and a JavaScript function in the runtime". The Grammar has no syntax for declaring a native, so how does the prelude say that `add` is a native of type `$-([@ @] @)`?
25+2. **How the prelude marks its private names.** Modules: "Everything is exported and read through the import's face, as `card.card`. Only the prelude has private names." The Grammar has no syntax for privacy either.
26+3. **What the prelude holds, and how much of it milestone 2 writes.** The Prelude table names groups and examples, "such as `(get.by m k)`", and for the data molds says "and the rest", so which names and types make up the prelude? Its list probably belongs in the doc. The plan's milestone 2 builds "the prelude's types", while the Decisions tab's Build order puts "the core prelude gates" in the same step. And which list gates are natives depends on the plan's first open question, "natives that take gates".
27+4. **Aura and type variables in a gate's molds.** Molds: "`add` takes `[@ @]` and returns `@`, all one aura" and "A bare `@` in a `+$` is one aura variable per module, `@ud` if unfixed." Is every bare `@` in one `++`'s molds, its samples and casts, the same aura variable, as in `add`'s type, or is each `@` its own, and is it generalized per `++` as type variables are? With it: must a capital stay general, so that `++ f |= x=A (add x 1)` is an error, or may its body fix it? Is a capital in a local `++` the enclosing `++`'s? And does a number's `@ud` default apply to an aura variable its `++` would generalize, which would make `++ inc |= n=@ (add n 1)` a `$-(@ud @ud)`?
28+5. **Cells of no items or one.** The parser takes `[]` and `[a]` as tuples, as the EBNF's `"[" { item } "]"` allows, and leaves them to the checker. The Literals section may already answer it: "Every literal reads exactly as Hoon reads it", and its table lists Cell. Hoon reads `[a]` as `a` and rejects `[]`. The proposed reading is Hoon's. Whatever the answer, the theme's `dark=[tones=[…]]`, a record of one field, must still check.
29+6. **A change with no items.** `m()` parses as a change with no edits, as `wing "(" { item } ")"` allows. Is it an error?
30+7. **The doc's blocks that aren't whole modules.** The plan's "Done when" for milestone 2 says "the doc's examples typecheck, except those written as errors". Three `jibb` blocks need context to check. The overview's `cmd` and `sub` molds (`doc-overview-01-cmd-sub`) are what the Effects decision calls "the library unions", so in a module of their own they would shadow the prelude's. The Core language tab's `update` (`doc-core-02-update`) is a part of the todo list island. And the Grammar's imports (`doc-grammar-01-imports`) import `/sur/todo.hoon` and `/ui/components/card`, which the doc doesn't show, and the `%todo-action` mark, which the overview shows but which imports `/sur/todo.hoon` itself. The proposed reading is to check each in its context: the first as prelude source, the second inside the island, the third against fixture files.
31+8. **Tuples and records.** Molds: "Faces are part of the type, so `[a=@ b=@]` and `[x=@ y=@]` don't unify", and a cell literal "takes a default when its `++` is checked", a tuple. Can a tuple meet a record? The doc's examples need it to: worked example 3's `init` gives `[ts "" ~]` for the `model` record, and the basket's `init` gives `[[~ %.n] ""]`; `row-of` gives `[id.t …]` where `each` takes `[key=* el=(element M)]`; `|=%typed` builds `[%typed t]` for the case `[%typed text=@t]`, which the Grammar calls "a case `[%typed @t]`"; and `program` and `store` can't know the faces of samples such as `[=msg m=model]` and `[a=cart-action s=state]`. The proposed reading: a position without a face unifies with one that has a face, and takes it; two different faces don't unify.
32+9. **Values where `*` is expected.** Molds: "**Any noun**: `*`", with "no subtyping". `each` and `table` take `key=*`, and worked example 3 passes a `@ud` key. How does a typed value meet `*`? One reading: `*` in a gate's mold is a fresh type variable, so any value fits, and the rule against gates in keys comes from the core-language proposal.
33+10. **What `link`, `image` and `go` take.** Links and URLs: "`link`, `image` and `go` also take two kinds of unchecked cord", besides application paths, and `image`'s `src` is "an application path to a static image or `[url=@t width=@ud height=@ud]`". But the Effects block has `[%go url=@t]`, and a `$?` of a `qath` and a cord breaks the union rule, since "molds that can be either … overlap both" and the Core language tab lowers an application path to "A list of knots". `link` is in worked example 1. Two gates, or a type-directed rule listed in the Grammar ("No built-ins; type-directed rules are welcome, each listed in the Grammar tab")?
34+11. **Stores and derived gates.** "`poke` and `paths` are declarations, not molds" in an API file; is a store's `+$ poke` one too, since as a mold it would shadow the prelude's opaque `poke`? How does the checker know a module is a store before it has checked `main`, whose type decides the kind? What may a derived name clash with, given that the doc's API file imports `/- todo` and has a `%todo` case whose request is `todo`? And since "islands can't watch paths that need a separate scry path", is watching a request whose case has `scry=` a type error?
35+
36+## Grammar questions never put to the doc's author
37+
38+In milestone 1 the parser met about thirty places where the doc could be read more than one way. Twelve went to @iko on 2026-10-30 and are rows of the Decisions tab. These were not asked. For each, the parser follows the reading below, and some are marked "(doc question)" in the library's notes. None changes what a valid program means, except where a reading makes something an error.
39+
40+| # | Question | What the parser does now |
41+| --- | --- | --- |
42+| 1 | `knot` may be empty, so the EBNF admits `//`, `/a//b` and a trailing `/`. Which are paths? | A segment has at least one character, as in Hoon. `/` alone is the empty path, in expressions and molds; `%/` alone is the root application path. `//` fails at the second `/`, and a trailing `/` just past it. `inline-mold-paths` has `/` alone as a mold |
43+| 2 | Is `/` alone a valid path for a `/=` import? | No: `/=` names a module's file, so its path has a segment. `/= card /` fails just past the `/` (`inline-import-root-bad`) |
44+| 3 | The EBNF's `line = [ name "=" ] expr` lets every open bracket's lines take `name=`. What would it mean on a line of an open `(` or `~[`? | An error: `name=` lines belong to tuples and changes. "expected a space between items, found `=`; `name=value` goes in a cell, as [a=1], or a change, as m(a=1)" |
45+| 4 | Does "items named" make every item of a change need `name=`? | Yes: `m(x)` fails just past `x`, "a change's items are named, as in m(done=%.y)" (`lex-tight-change-bad-3` to `-7`; `-8` and `-9` test what follows a name there). `m()` parses; see question 6 above |
46+| 5 | `term` includes `%0`, `%&` and `%\|`, which no mark is called. What does `/%` take? | `%` and a name only: `/% %0` is "expected a mark's term, as `%todo-action`, found `%0`" |
47+| 6 | What does an indented first line mean? | An error: "the first line is indented: top-level lines start at column 1" (`lex-indent-bad-3`) |
48+| 7 | With levels open at columns 1 and 5, what does a line at column 3 mean? | An error naming the open columns: "this line's indentation matches no enclosing line: indent it to column 1 or 5". `lex-indent-bad-2`, with levels at 1, 5 and 7, gives "column 1, 5 or 7" |
49+| 8 | Tabs, carriage returns, other control characters, and non-ASCII text outside strings and comments? | Errors everywhere, strings and comments included, except that UTF-8 is allowed in strings and comments; a tab in a string is written `\09`. `lex-indent-bad-1` and `lex-spacing-bad-2` test tabs |
50+| 9 | Must a file be valid UTF-8? | Not checked yet: any byte from 128 up passes in strings and comments, and columns assume UTF-8. The proposed reading makes a byte that can't continue a UTF-8 sequence an error. An escape such as `\ff` still writes any byte, as in Hoon |
51+| 10 | Inside `{…}`, may `::` start a comment, which would swallow the `}` and the `"`? Is a lone `}` in a string text? May a comment follow code with no space, as in `5::note`? | A `:` inside `{…}` is "a comment can't start inside `{…}`". A lone `}` is text, as in Hoon. `5::note` is `5` and a comment |
52+| 11 | Is a type variable exactly one capital letter, as `typevar = upper` says? | Yes: `Msg` is "a type variable is one capital letter" |
53+| 12 | Hoon has more runes of the left-out kinds than the list names: `?~ ?= ?. ?@ ?^`, `:- :+ :^ :_`, `%- %+ %^ %~`, `=+ =? =.`, `;:` and `~&(…)`. Are they all left out? | Yes, with the list's reasons, as "`:-` is left out: indentation covers it; write a cell as [a b]". Naming them in the doc, or saying "Hoon's other runes", would make the messages the doc's words (`inline-leftout-*`) |
54+| 13 | `@s…` and `@r…` auras are "errors wherever used". Is that the parser's error in Jibb source, or the checker's? | An error before the checker. Where a mold goes, the layout pass's, at the `s` or `r`: "signed and floating-point auras are left out of Jibb". Where an expression goes, the parser's, at the `@`: "expected an expression, found `@`; signed and floating-point auras are left out of Jibb". Imported ones are milestone 2's importer's. The case `inline-leftout-signed-float-auras` still says "expect: unclear" |
55+
56+Where an error points is not a doc question: the doc asks only for "errors with file, line, column and an excerpt". The library puts each error at the first character where the text stops being the start of any valid module, as Hoon's parser does (its section 2.0).
docs/prompts/milestone-2.mdnew file+173
@@ -0,0 +1,173 @@
1+# Milestone 2: types and prelude
2+
3+A brief for the developer who builds milestone 2 of Jibb. Paste it as the first message of a session, or hand it to a person; it assumes a fresh start in the repo `iko-soy/jibb`. Written 2026-10-30, at commit ab797a9, when milestone 1 was done.
4+
5+---
6+
7+You are building milestone 2 of Jibb, a UI language for Urbit: a Hoon dialect with an indentation layout. Its spec is @iko's design doc, [Jibb — an Elm-architecture UI language for Urbit](https://claude.ai/artifact/DMBr48qzi5z8hcwn2ccPBv), copied into `docs/spec/`. Milestone 1 built the layout pass and the parser in Hoon, in `jibb-dev/lib/jibb.hoon`. Milestone 2 adds the type checker, the imports, and the prelude.
8+
9+## Read first
10+
11+1. `docs/README.md`, `docs/status.md` and `docs/development.md`: where things stand, the ground rules, how to set up a machine.
12+2. `docs/spec/implementation-plan.md`: milestone 2, the Compiler architecture section, Risks and Open questions.
13+3. `docs/spec/grammar.md`, all of it, and closely: Literals, Molds, Tag resolution, Literals by context, Patterns, Modules, Imports, Runes, Expressions. This is most of your spec.
14+4. `docs/spec/overview.md`: Importing Hoon types, Prelude, The Elm architecture on Urbit (kinds, islands, effects), Layout and styling with Links and URLs, Agent APIs, Desk layout, the worked examples.
15+5. `docs/spec/decisions.md`: every row, and closely Special rules, Inference, Unions, Branching, Hoon types, Derived gates, Type identity, Kinds, Type variables, Empty mold, Inline gates, Sample names, Binding patterns, Text, Auras, Imports and Data crossing to the browser.
16+6. `docs/spec/core-language.md`, Values and invariants: what the front end guarantees the backends. The checker's share includes "No shadowing" and island data without gates.
17+7. `docs/compiler.md`, then the design notes at the top of `jibb-dev/lib/jibb.hoon` and its `%tree` chapter: the syntax tree you will check.
18+8. `docs/open-questions.md`.
19+
20+Then set up the machine as `docs/development.md` says and run its checks. All 344 golden cases should pass before you change anything.
21+
22+## Ground rules
23+
24+- **The doc wins.** Where code and doc disagree, the doc stands until it changes. `docs/spec/` is a snapshot of 2026-10-30; check the doc itself before relying on a detail.
25+- **Don't decide the language.** A question about what Jibb means goes to @iko: as a comment on the doc, on the words it's about, if you can comment there, or else in the project's chat, where @iko reviews work. List it in `docs/open-questions.md` too. Meanwhile follow the most likely reading, mark it "(doc question)" in the library's notes, and keep going. A language change is agreed first and recorded in the Decisions tab.
26+- **No built-ins.** The Decisions tab's Special rules row: "No built-ins; type-directed rules are welcome, each listed in the Grammar tab". A typing rule the Grammar doesn't list, such as a special case for one gate, is a language change.
27+- **Implementation choices are yours**: data structures, algorithms, error wording within the doc's rules, test layout. Record the reasoning in the library's design notes, as milestone 1 did.
28+- **Everything is a golden test**, and every check passes on the fake ship as well as under `urbit eval`.
29+- **Push straight to `master`**, in commits that each leave every check passing. No pull requests.
30+- **Keep `docs/` current**: `status.md`, `open-questions.md`, `compiler.md` and `development.md` change with the code.
31+
32+## The milestone
33+
34+From the plan, word for word:
35+
36+> **Types and prelude**
37+> - Builds: Hindley–Milner inference with aura variables; tag and literal resolution with deferred constraints; the union disjointness check; exhaustive `?-` with dead-arm errors; `/-`, `/%` and `/=`, with the table for importing Hoon types; API files and their derived gates; the prelude's types.
38+> - Done when: the doc's examples typecheck, except those written as errors; each error the Grammar tab describes has a test with its message, such as "msg has no case %submitt"; a `/sur` file with `$%`, `$@`, maps and a recursive mold imports as the table says.
39+
40+One Grammar error waits: checking an application path "against routes and static files" needs routing, which the plan builds in milestone 4. Say so in your report.
41+
42+And from the plan's Risks:
43+
44+> **Importing Hoon types.** Hoon's types form a graph with lazy `%hold` nodes, so the importer needs Hoon's own type engine to expand them, and it must recognise maps, sets and recursive molds by name. Start it early in milestone 2, on real `/sur` files such as Tlon's.
45+
46+> **Type errors.** Hindley–Milner is known for confusing errors. Each error names both conflicting sites, with excerpts, and the messages are golden tests.
47+
48+## First steps
49+
50+1. **Settle the open questions.** The checker needs answers the doc doesn't give yet. `docs/open-questions.md` lists eleven under "Questions milestone 2 raises", found by walking the doc's examples through its rules:
51+ 1. how the prelude declares natives;
52+ 2. how it marks private names;
53+ 3. what the prelude holds, and whether milestone 2 writes "the prelude's types", as the plan says, or "the core prelude gates", as the Decisions tab's Build order says;
54+ 4. aura and type variables in a gate's molds;
55+ 5. `[]` and `[a]`;
56+ 6. `m()`;
57+ 7. how to check the doc's blocks that aren't whole modules;
58+ 8. whether a tuple can meet a record, which worked example 3 needs;
59+ 9. how a typed value meets `*`, as `each`'s `key=*` does;
60+ 10. what `link`, `image` and `go` take;
61+ 11. stores' `+$ poke`, derived-name clashes, and watching a `scry=` case.
62+
63+ The plan's first open question matters too: whether `eq`'s arguments and map and set keys must be free of gates, and a gate may never tell a union's cases apart, which changes the disjointness check; whether natives may take gates, which decides whether `turn`, `sort` and `skim` are natives; and whether a value that depends on itself through a gate is an error. Put each to @iko in your first days with a proposed answer and the reason for it, and build on the proposal until you hear back.
64+2. **Start the importer early**, as the plan's Risks ask: first on a small `/sur` file, then on Tlon's. "Suggested design" below has a way in.
65+
66+## What the doc asks of the checker
67+
68+The rules are scattered across the tabs. This is a map of them, not a replacement: read each in place, and where this summary and the doc differ, the doc is right.
69+
70+**Types and inference** (Grammar: Molds, Modules, Runes, Expressions; Decisions: Inference, Type variables, Sample names, Inline gates)
71+- Hindley–Milner with aura variables: "every expression has a principal type, and there is no subtyping".
72+- "`@` is an atom whose aura isn't known yet, fixed by what it meets": `add` takes `[@ @]` and returns `@`, all one aura. "Different auras never unify." "A bare `@` in a `+$` is one aura variable per module, `@ud` if unfixed."
73+- Faces are part of record types: `[a=@ b=@]` and `[x=@ y=@]` don't unify. Whether a tuple meets a record is open question 8.
74+- Type variables are capitals, "generalized per `++`". Names "may refer to each other in any order", so infer `++` definitions in dependency order, a group of mutually recursive ones together.
75+- "Gates may call themselves and their siblings; other values can't refer to themselves, so `++ title (trim title)` is an error." Molds may be recursive, and "recursive molds go by name and module".
76+- A sample's mold may be left out: every bare name in a sample binds, at any depth, as in `|=(p title.p)` "when the call fixes the type", `|= [a=@ b]` and `|=(m ~)`.
77+- Generic molds: `+$ result |$ [V E] $?` with cases below, applied as `(result V E)`; the prelude's `cmd` and `sub` are `|$ [M]`.
78+- `!!` is the empty mold, and as an expression it has any type. `*` is any noun; how other types meet it is open question 9.
79+- `poke`, `ask` and `qath` are opaque, built only by derived gates, `pure` and application-path literals.
80+- The Literals table gives every other literal its type: `0x1f` a `@ux`, ships `@p`, dates `@da`, durations `@dr`, `~.knot` a `@ta`, strings `@t`, `` `x `` a `(unit A)`, `~[…]` a `(list A)`, `gap+%s2` the pair `[%gap %s2]`, `/posts/~zod` a `path`, `%/blog` a `qath`.
81+
82+**Tags, `~`, numbers and cells wait for their context** (Grammar: Tag resolution, Literals by context)
83+- A tag or `~` gets a fresh variable and a pending constraint, checked once unification fixes the variable: against a union's cases, by shape and shared tag positions, or as a constant atom for `@tas`, `@ta`, `@` or `*`. A literal that matches nothing is "an error naming the union, such as "msg has no case %submitt"".
84+- `|=%typed` is a case's constructor: "against `$-(@t M)` it is `|=(t [%typed t])`, requiring `M` to have a case `[%typed @t]`. It works for cases tagged at their head; a bare tag is always a value."
85+- "A constrained variable, and any it mentions, doesn't generalize; it stays open until something in the module fixes it. An island's `main` fixes its messages." A constraint still open when the module is checked "is an error asking for a `^-`; the checker never guesses".
86+- "Wing access, changes and `?-` on a type not yet known wait the same way." Patterns don't: they're checked against the subject's type.
87+- Number and cell literals each "takes a default when its `++` is checked": a number `@ud`, a cell a tuple. A cell can also become a record or, where a list is expected, a cons.
88+- In `{…}`, atoms print by aura, a bare `@` as `@ud`; `@t`, `@ta`, `@tas` and tapes splice as text; "any other cell is an error asking for a printer". String literals are always cords.
89+
90+**Unions** (Grammar: Molds, Runes; Decisions: Unions)
91+- Cases "must differ as nouns: at some position of their right-nested cells, one is an atom and the other a cell, or both are different constants. `*`, type variables and molds that can be either, such as `(list A)` or `?`, overlap both. A union whose cases don't all differ is rejected."
92+
93+**Patterns and `?-`** (Grammar: Patterns; Decisions: Branching, Binding patterns)
94+- Exhaustive: "Every case needs an arm or a catch-all, `*` or a bare name." Leaving one out is an error naming it.
95+- "An arm that earlier arms cover, such as a second `%add` or anything after `[%move * *]`, is an error."
96+- `?-` on a loobean switches on `%.y` and `%.n`. A name in a pattern always binds anew; the last position of a cell pattern binds the rest.
97+- The Core language tab lowers `?-` "by the usual pattern-matrix algorithm", so one matrix can serve exhaustiveness, dead arms and, in milestone 3, lowering.
98+
99+**Names and modules** (Grammar: Modules, Imports, Runes, Expressions; Core language: Programs and linking)
100+- "No name may shadow one already in scope, imports and the prelude included": samples, local `++`s and pattern binders too. The backends rely on it (Core language, "No shadowing").
101+- Molds and values have separate namespaces, "told apart by position": `todo-action.todo` is a mold and its constructor.
102+- Local definitions may start any body, "visible to each other and the final expression".
103+- `/- todo` gives the molds of `/sur/todo.hoon` the module uses; `/% %todo-action` gives a mark's type, named after the mark, with no face; `/= card /ui/components/card` gives a Jibb module. API modules re-export their `/%` types, "not `/-` faces or `/=` modules". Import cycles are errors.
104+- Imports and prelude groups are records, read with `.`: `card.card`, `space.theme.theme`, `get.by`. "A module used as a whole value is a cell of its definitions, in name order."
105+- `main`'s type decides a module's kind: `(element !!)` a page, `(page R D)` a page with data, `(program D X M)` an island, `(store S)` a store; no `main`, a library.
106+
107+**Importing Hoon types** (overview: Importing Hoon types; Decisions: Hoon types)
108+- The table: `$%` and `$?` become `$?`; `$@` a `$?` with an atom case; `$^` a `$?` if its cases differ as nouns, else an error where used; `$~` dropped; `map`, `set`, `qeu`, `jar`, `jug` the same, by name; `mop` as `(mop K V)`, read with `on`; other `$|` their base type; `%0`, `%&` and `%|` tags; recursive molds recursive, matched by name; cores, gates, vases, signed and float auras an error where used.
109+- "Unused molds are skipped, so unsupported constructs elsewhere in a `/sur` file are fine."
110+
111+**Islands, pages and stores** (overview: The Elm architecture on Urbit; Grammar: Modules and the store example)
112+- `(page load view)`, `(program init update view subs)` and `(store init update)` build the kinds. The worked examples show their gates' shapes: `update` takes `[msg model]` and gives a model and a list of `cmd`s, `subs` gives a list of `sub`s, a store's `update` takes a poke and the state.
113+- `main.todo-list` "is a record of exactly `init`, `update`, `view` and `subs`"; in `island main.todo-list ts`, `ts` is "the island's data, checked against `init`'s sample"; `island` gives an `(element !!)`, which keeps islands out of islands.
114+- "Island data and models can't hold gates; the checker names the field."
115+
116+**API files, stores and derived gates** (overview: Agent APIs, Effects; Grammar: Modules)
117+- `/ui/api/<agent>.jibb` declares `+$ poke` and `+$ paths`; "`poke` and `paths` are declarations, not molds." "A path case is a request name, a path and a type", and "A case can name another scry path, as in `[%changes /updates (facts todos todo-update) scry=/todos]`, used only by pages." Islands "can't watch paths that need a separate scry path".
118+- Each poke type gives a constructor, as `todo-action.todo`, and each path case a request, as `(todos.todo |=%got)`. A path with typed segments takes them too: the overview's `(post.api slug same)` suggests they come first. A `(facts full diff …)` case's request also takes a fold, as `$-([[%todo-update todo-update] todos] todos)`.
119+- A store exports a constructor per poke type and a `state` request. "Clashing derived names are errors." Open question 11 asks the rest.
120+
121+**The prelude** (overview: Prelude, Layout and styling, Links and URLs, Effects)
122+- Hoon's standard library regrouped, "written in Jibb and vendored as `/lib/jibb/prelude`", plus `eq`, `not`, `page`, `program`, `store`, `pure`, `island`, the layout elements and molds, `attr`, `theme`, and `cmd` and `sub` as the overview's Effects block writes them.
123+- Signatures change where the Prelude table says: maps and sets are plain gates over Hoon's trees, as `(get.by m k)`; folds take a start, as `(roll xs init f)`, `rep.by` and `rep.in`; ordered maps go through lull's `on`, as `(get.on lte m k)`.
124+- Left out: signed and float gates, parsers, and the table's "The rest" (1b, 2p, 3d, 3f, 4c, 4n, 4o, 5a–5f); "`jam`, `cue` and `fein` run inside the runtime".
125+- Write the prelude in Jibb, enough to give every name its type: Jibb-written gates as code the checker checks, natives as declarations, in whatever form open question 1 settles. The Hoon gates behind natives come with the Nock backend in milestone 3.
126+- Element signatures are in the Layout and styling table; what `link`, `image` and `go` take is open question 10. "The same attribute twice in a literal list is an error."
127+
128+**Errors**
129+- Each one the Grammar tab describes needs a golden case with its message. A start on the list: an aura mismatch; mismatched faces; "msg has no case %submitt"; an unresolved tag, as the Grammar's `add-button` ("nothing fixes the union of %add"); an open constraint at the end of a module; a `|=%tag` whose union has no such case tagged at its head; a union whose cases overlap; a missing `?-` case, named; an arm that earlier arms cover; a value that refers to itself; a shadowed name, including a prelude or imported one; a cell in `{…}` with no printer; `~[text "hi"]`, where the checker suggests `~[(text "hi")]`; clashing derived names; an import cycle; from an import, a signed or float aura, a core, gate or vase, or a `$^` whose cases don't differ as nouns, each where used; a `(facts …)` outside an API file's `paths`; a gate in island data or a model, naming the field; a repeated attribute in a literal list. Read the Grammar tab, and the other tabs' mentions of "error" and "the checker", and make the list complete.
130+- "Each error names both conflicting sites, with excerpts." Today's `error` is `[file line col msg]`, a start only; spans keep each node's end so that a type error can underline both sites.
131+- Messages name things in the doc's words and quote the code they're about in backticks, as milestone 1's do. Their wording is fixed by the golden tests once written.
132+
133+## Suggested design
134+
135+Suggestions, not rules; change them where you find better.
136+
137+- **Keep the checker a function.** `urbit eval` can't scry, so let the caller pass in what comes from Clay: the source of each imported module, or its checked interface, each imported Hoon type, and the prelude, which jibb-dev vendors beside the library as `/lib/jibb/prelude` (overview, Desk layout). On a ship the host (milestone 4) reads them; in tests the harness does, loading the prelude once per run. The plan caches "per module, with its interface and body hashed separately", so make a module's interface, its exported names, molds and types, a noun of its own.
138+- **A way into the importer.** Under `urbit eval`, this compiles a `/sur` file and reads a mold's type (checked on 2026-10-30, vere 4.6):
139+ ```hoon
140+ =/ cor=type -:(ride -:!>(..zuse) src) :: src: the /sur file's text, with no Ford runes
141+ =/ t=type (~(play ut cor) (ream '*action')) :: the type of an action
142+ ~(repo ut t) :: expands one %hold
143+ ```
144+ `..zuse` is the subject a ship builds `/sur` files against, so molds from lull and zuse, such as `json` and `mark`, resolve. Each `+$` arm's type comes as a `%hold` whose expansion is a `%hint` with a `%made` note naming the arm, then the mold: a `$%` is a `%fork` of cells headed by `%tas` constants. A `(map id item)` field is named only in its arm's own `%hold`, whose Hoon calls `map` by name. Once the arm is expanded, the field is a `%hold` whose Hoon is the body of `map`, `(tree (pair key value))` with its `apt` check, which expands to a second `%hold`, `tree`'s body, and only then to a treap's `%fork`. So recognising maps, sets and the rest by name means reading an arm's Hoon before expanding it, or comparing a `%hold`'s Hoon with the standard library molds' own bodies. On a ship, `.^(vase %ca /=my-app=/sur/todo/hoon)` builds a `/sur` file, and a mark's type is its door's sample. Most of Tlon's `/sur` files begin with `/-` or `/+` (25 of the 40 in `tloncorp/tlon-apps`, `desk/sur`, on 2026-10-30), so under eval their imports must be resolved by hand; on the ship Clay does it.
145+- **Inference**: union-find over a map of variables, levels for generalization rather than scanning the environment, and a queue of deferred constraints, retried as variables are bound and reported when the module ends. Infer `++`s in strongly connected groups. Measure early: Hoon is slow, and a 2,000-line module should check in seconds, not minutes.
146+- **Exhaustiveness**: Maranget's "Warnings for pattern matching" (Journal of Functional Programming, 2007) fits nouns: constructors are constants and cells.
147+- **A type printer** in Jibb's mold syntax, for golden output and for messages.
148+- **Mind two names.** The library's `+in`, a bit test, hides the standard library's set door `in`, and `+fail` hides Hoon's parser rule `fail`. Write `in:..zuse`, or rename `+in` before the checker uses sets.
149+
150+## Tests
151+
152+- **A third golden output.** Add the checker's output to `+golden`, say `types.txt`: each definition's type in Jibb syntax and the module's kind, or the rendered error. Teach `tools/golden`, `tools/ship gen-tests`, the stubs and self-tests in `tools/testdata`, README.md and `docs/development.md` about it, with an `expect:` form for the checker, then add it to the existing cases and review every one.
153+- **Cases with imports.** Six of the doc's fourteen `jibb` blocks import other modules, three of them `/sur` files or marks, and the Core language tab's `update` needs the todo API without importing it. Give a case a way to carry the files it imports, under eval and on the ship alike. A case also needs its own path under `/ui`: both runners pass the file as `/tests/golden/<case>/source/jibb`, but an API file is known by its path, `/ui/api/<agent>.jibb`, whose name is the agent its constructors poke.
154+- **Hoon files on the ship.** `tools/ship test` runs `-test` on the `jibb-dev` desk's `/tests`, which builds every `.hoon` file there as a test file. A fixture mark that starts `/- todo`, or one of Tlon's files, fails that build, and with it the run. Keep such files where `-test` won't build them, as `.txt` read with `/*`, or in a desk of their own, and keep them out of `jibb-dev/`, which app desks vendor, and `vendor/`, which goes into every desk.
155+- **Fixtures for the doc's examples.** `my-app/ui/` holds nine of the doc's blocks as modules. Add what they import that the doc doesn't show: `/sur/todo.hoon` and the marks `todo-action`, `todos`, `todo-update` and `todo-item` (the overview shows `/mar/todo-action.hoon`; `row-of` reads `id`, `done` and `text` from an item); `/sur/blog.hoon`, a mark for the posts, of type `(list post:blog)`, and `/ui/api/blog.jibb`, whose `posts` case names that type through `/%`; and `/ui/components/card.jibb`, with a `card` gate from a post to an element. Keep them as small as the examples allow, say in a comment at the top of each that it is a fixture and not from the doc, and don't change the doc's blocks.
156+- **Every doc example typechecks**, except the Grammar's buttons example, whose `add-button` is written as an error and must fail with it. The same block marks `labelled` and `wide` as fine, so show them checking too, by printing the definitions that check beside the error or in a case without `add-button`. Open question 7 proposes how to check the three blocks that aren't whole modules.
157+- **The `/sur` import.** A test `/sur` file with `$%`, `$@`, a map and a recursive mold, imported and printed in Jibb syntax, under eval and on the ship. Then Tlon's real files, as the risk asks: they need not import fully, but every mold a test uses must, and any error must be the table's.
158+- **On the ship**: the golden cases as `-test` arms, and the `my-app` desk checked with its Hoon types read through Clay: every module checks except `ui/components/buttons.jibb`, which gives `add-button`'s error.
159+- **Speed**: time the corpus and the `my-app` desk, and record both in `docs/status.md`.
160+
161+## Out of scope
162+
163+- Lowering, the core language, its interpreter and the Nock backend: milestone 3.
164+- Checking application paths against routes and static files, routing, the theme's contrast check and the host: milestone 4. Type `%/blog/(slug)` as a `qath` and its segments as printable atoms, no more.
165+- `load`'s per-request behaviour and binding route parameters: milestone 5. `page`'s type and `load`'s signature are in scope.
166+- The JavaScript backend and the runtime: milestone 6. The formatter and the tree-sitter grammar: milestone 7.
167+
168+## When you're done
169+
170+- The plan's three "Done when" items hold, but for the route check, and every check in `docs/development.md` passes under eval and on the ship.
171+- `docs/status.md` shows milestone 2 done, with counts and timings; `docs/open-questions.md` lists what's still open; `docs/compiler.md`, `docs/development.md` and the library's notes describe the checker.
172+- Everything is pushed to `master`.
173+- Report to @iko in the project's chat, in a few lines: what was built, the test counts and timings, the decisions you needed and got, and what's still open.
docs/spec/core-language.mdnew file+175
@@ -0,0 +1,175 @@
1+# Core language
2+
3+Oct 29, 2026 · @iko
4+
5+After checking, every Jibb module lowers to the core language: Nock's operations on nouns, plus a lambda calculus with names. The Nock and JavaScript backends both compile it, and an interpreter for it, written in Hoon, is the reference both are tested against. Rules marked *proposed* aren't decided yet.
6+
7+## Nodes
8+
9+Fifteen kinds of node: seven that build and test nouns, each one Nock operation; five for control; and natives, crashes and traces.
10+
11+| Node | Value |
12+| --- | --- |
13+| `42`, `%add`, `"milk"` | The atom written, in Jibb's literal syntax |
14+| `x` | The value bound to `x` |
15+| `(cons e f)` | The cell of `e` and `f` |
16+| `(slot n e)` | The value at axis `n` of `e`; crashes if there is none, as Nock 0 does |
17+| `(edit n e v)` | `e` with axis `n` replaced by `v`; crashes if `e` has no axis `n`, as Nock 10 does |
18+| `(cell? e)` | `%.y` if `e` is a cell, else `%.n`, as Nock 3 |
19+| `(eq e f)` | `%.y` if the two nouns are equal, else `%.n`, as Nock 5 |
20+| `(if c t f)` | `t` if `c` is `%.y`, `f` if it's `%.n`; crashes on anything else, as Nock 6 does |
21+| `(let x e b)` | `b`, with `x` bound to `e` |
22+| `(rec ((f l) …) b)` | `b`, with each `f` bound to its `lam`, all visible to each other |
23+| `(lam x b)` | A function of one sample, bound to `x` in `b` |
24+| `(app f e)` | `f` called with `e` as its sample |
25+| `(native n)` | The native function `n` |
26+| `(crash)` | A crash, as `!!` |
27+| `(trace m b)` | Prints the cord `m`, then is `b`, as `~&` |
28+
29+Literals keep the aura they were written with, but only for printing and as a hint to the JavaScript backend. A call with several arguments passes one cell, as Nock's gates take one sample.
30+
31+## Values and invariants
32+
33+A value is a noun or a function. A function can sit inside a cell, but it isn't a noun: each backend represents it its own way, and it never crosses between ship and browser.
34+
35+The front end guarantees the backends:
36+
37+- **No shadowing.** No binding hides another: Jibb forbids it, and generated names start with `_`, which Jibb names can't.
38+- **`rec` binds functions.** Every right side of a `rec` is a `lam`.
39+- **Functions aren't taken apart.** `cell?`, `eq`, `slot` and `edit` never get a function itself, and `eq` never compares nouns that contain one. Island data and models are already first-order. *Proposed*: so are `eq`'s arguments and map and set keys, and a gate never tells a union's cases apart.
40+- **Strict evaluation.** A call's sample is evaluated before the call, and a node's parts in text order; `if` evaluates only the branch it takes. Only `trace` and `crash` can observe the order, and differential tests compare results and whether a program crashes, not traces. So the backends may evaluate a node's parts in another order, as Nock 10 does with edit's v and e.
41+- **Crashes stay crashes.** Where types already guarantee a test, the backends still crash on a violation, as Nock does, instead of carrying on.
42+
43+## Text form
44+
45+S-expressions, so each stage's output can be printed, read back and tested on its own, and backend work can start on hand-written programs.
46+
47+```
48+expr = literal | name
49+ | "(" "cons" expr expr ")"
50+ | "(" "slot" axis expr ")"
51+ | "(" "edit" axis expr expr ")"
52+ | "(" "cell?" expr ")"
53+ | "(" "eq" expr expr ")"
54+ | "(" "if" expr expr expr ")"
55+ | "(" "let" name expr expr ")"
56+ | "(" "rec" "(" { "(" name lam ")" } ")" expr ")"
57+ | lam
58+ | "(" "app" expr expr ")"
59+ | "(" "native" name ")"
60+ | "(" "crash" ")"
61+ | "(" "trace" expr expr ")" ;
62+lam = "(" "lam" name expr ")" ;
63+module = "(" "module" path { "(" "def" name expr ")" } ")" ;
64+name = jname | path ":" jname | "_" ( letter | digit ) { letter | digit } ; (* own or prelude, imported, generated *)
65+axis = digit { digit } ; (* at least 1 *)
66+```
67+
68+- `literal` is Jibb's literal syntax, and the form a literal was written in is the aura the printer writes it back with.
69+- `jname` is a Jibb name; `path` is a module's path under `/ui`, as in `api/todo:todo-action`.
70+- Whitespace separates items, and `::` starts a comment. Source positions aren't written, so golden tests don't change when lines move; the printer can add them as comments.
71+
72+## Lowering from Jibb
73+
74+Types, faces and casts leave nothing behind; everything else lowers by these rules.
75+
76+| Jibb | Core language |
77+| --- | --- |
78+| A number, ship, date, term, loobean, `~`, or a string without `{…}` | An atom literal, keeping its aura |
79+| `"Since {day}"` | `(app (native join) …)` on a list of the pieces, each atom printed first by its aura's native |
80+| `` `x `` | `(cons ~ x)` |
81+| `[a b c]`, `[x=a y=b]`, `~[a b]` | Right-nested `cons` in field order; a list ends in `~` |
82+| `gap+%s2` | `(cons %gap %s2)` |
83+| `/posts/~zod`, `%/blog/(slug)` | A list of knots, typed segments printed by their aura's native; the runtime adds the base path to an application path |
84+| `title.p`, `space.theme.theme` | `slot` at the field's axis, nested for longer wings |
85+| `card.card`, an import | The qualified name `components/card:card` |
86+| `m(draft=t error=~)` | One `edit` per item, in order: `(edit 7 (edit 6 m t) ~)` |
87+| `f a b c` | `(app f (cons a (cons b c)))`: the arguments are the sample |
88+| `\|= [a=@ b=@] body` | `(lam _1 (let a (slot 2 _1) (let b (slot 3 _1) body)))`; a single face, as in `\|= m=model`, names the `lam` itself |
89+| `\|=%got` | `(lam _1 (cons %got _1))` |
90+| `++` definitions, at top level or in a gate | A `rec` per group of gates that call each other; a `let` per other value, in dependency order |
91+| `?- s` | `(let _1 s …)` around a decision tree of `if`, `cell?`, `eq` and `slot`, built by the usual pattern-matrix algorithm; binders become `let`s, and tests the types make redundant are left out |
92+| `?-` on a loobean | `(if s yes no)` |
93+| `^- t e` | `e` |
94+| `~& v body` | `(trace (app _p v) body)`, where `_p` is a printer derived from `v`'s type |
95+| `!!` | `(crash)` |
96+| Derived gates and `pure` | `lam`s that build the cells the runtime reads |
97+| Checks on incoming facts | A validator derived from the path's type, which the runtime runs on each fact |
98+
99+## Example
100+
101+The todo island's `update`, from worked example 3:
102+
103+```jibb
104+++ update |= [=msg m=model]
105+ ?- msg
106+ [%typed t]
107+ ?- (gth (lent (tuba (trip t))) 80)
108+ %.y [m(draft=t error=`"Too long") ~]
109+ %.n [m(draft=t error=~) ~]
110+ %submit [m(draft="") ~[[%poke (todo-action.todo [%add draft.m]) |=%acked]]]
111+ [%toggled id] [m ~[[%poke (todo-action.todo [%toggle id]) |=%acked]]]
112+ [%got ts] [m(todos=ts) ~]
113+ [%acked err] [m(error=err) ~]
114+```
115+
116+lowers to the program below. The model `[todos draft error]` puts `todos` at axis 2, `draft` at 6 and `error` at 7, and `%submit`, the only atom case, needs no test once `cell?` fails.
117+
118+```
119+(def update
120+ (lam _1
121+ (let msg (slot 2 _1)
122+ (let m (slot 3 _1)
123+ (if (cell? msg)
124+ (let _2 (slot 2 msg)
125+ (if (eq _2 %typed)
126+ (let t (slot 3 msg)
127+ (if (app (native gth) (cons (app (native lent) (app (native tuba) (app (native trip) t))) 80))
128+ (cons (edit 7 (edit 6 m t) (cons ~ "Too long")) ~)
129+ (cons (edit 7 (edit 6 m t) ~) ~)))
130+ (if (eq _2 %toggled)
131+ (let id (slot 3 msg)
132+ (cons m (cons (cons %poke (cons (app api/todo:todo-action (cons %toggle id)) (lam _3 (cons %acked _3)))) ~)))
133+ (if (eq _2 %got)
134+ (let ts (slot 3 msg) (cons (edit 2 m ts) ~))
135+ (let err (slot 3 msg) (cons (edit 7 m err) ~))))))
136+ (cons (edit 6 m "")
137+ (cons (cons %poke (cons (app api/todo:todo-action (cons %add (slot 6 m))) (lam _4 (cons %acked _4)))) ~)))))))
138+```
139+
140+## Programs and linking
141+
142+- Each module lowers to `(module <path> (def name e) …)`, in source order. Its own and the prelude's definitions are named plainly, since no name may shadow another; imports are qualified, as in `api/todo:todo-action`.
143+- The linker builds one program per entry: on the ship, each page; in the browser, each island's `update`, `view` and `subs`, and each store. It keeps only the definitions the entry reaches, sorts them by dependency into `let`s and `rec`s, and ends with the entry.
144+- Values that aren't gates are computed once per program, before anything calls them; the Nock backend may compute them at build time. *Proposed*: a value that depends on itself, even through a gate, is an error.
145+- A module used as a whole value is a cell of its definitions, in name order.
146+
147+## Natives
148+
149+- Only the prelude declares natives. Each has a Jibb type, a Hoon gate in `/lib/jibb` that calls the standard library, so its jets fire, and a JavaScript function in the runtime. Differential tests check each pair.
150+- The lowering uses a few private ones: `join` for interpolation, a printer per aura, and the map and set checks in validators.
151+- *Proposed*: natives may take gates. On the ship a Jibb gate is shaped like Hoon's, so Hoon's own `turn`, `sort` and `skim` can call it, with their jets where vere has them. In JavaScript they're loops, which keeps long lists off the stack. This would replace the rule that Jibb-written gates such as `turn` run unjetted on the ship. A native only calls such a gate and never takes it apart, so the invariant above still holds.
152+
153+## Backends
154+
155+Each node becomes a few Nock operations, or a few lines of JavaScript calling the runtime.
156+
157+| Node | Nock | JavaScript |
158+| --- | --- | --- |
159+| Literal | `[1 a]` | A number or a constant atom; a cord also keeps its string |
160+| `x` | `[0 a]`, at `x`'s axis in the subject; a name bound by `rec` is an arm of its core, `[9 a 0 c]` with `c` the core's axis | A variable |
161+| `cons` | A cell of the two formulas | `cell(e, f)` |
162+| `slot` | `[7 e 0 n]` | `slot(n, e)` |
163+| `edit` | `[10 [n v] e]` | `edit(n, e, v)` |
164+| `cell?` | `[3 e]` | `isCell(e)` |
165+| `eq` | `[5 e f]` | `same(e, f)` |
166+| `if` | `[6 c t f]` | A conditional that crashes on anything but 0 or 1 |
167+| `let` | `[8 e b]`, pushing the value onto the subject | `const` |
168+| `rec` | A core with an arm per function, as `\|%` builds | Function declarations |
169+| `lam` | A gate shaped like Hoon's: `$` at axis 2, sample at 6 | A closure of one argument |
170+| `app` | `[9 2 10 [6 e] f]` | `f(e)`; a self call in tail position becomes a loop |
171+| `native` | The native's Hoon gate | The native's runtime function |
172+| `crash` | `[0 0]` | A thrown crash |
173+| `trace` | A `%slog` hint, Nock 11, carrying `[0 leaf+(trip m)]`, since `%slog` takes a priority and a tank, not a cord | `console.log`, then `b` |
174+
175+Gates are shaped like Hoon's, so `app` calls a Jibb gate and a native's Hoon gate the same way, and Hoon code can call Jibb gates.
docs/spec/decisions.mdnew file+103
@@ -0,0 +1,103 @@
1+# Jibb decisions
2+
3+Oct 29, 2026 · @iko
4+
5+The choices behind the Language tab, made on 2026-10-23; a later date marks a revision.
6+
7+## Decisions
8+
9+| Decision | Choice | Status |
10+| --- | --- | --- |
11+| Syntax | Hoon's runes as keywords, and its literals; wide forms only for `\|=`, `$?` and `$-`; changes take record items, as `m(draft="x")`; `.` reads fields, imports and prelude groups; no name may shadow another | Decided 2026-10-29 |
12+| Literals | Exactly Hoon's: `1000` is an error, `~2026.2.31` is `~2026.3.3`, and years BC and sub-second groups are dates; `print-date`'s minute form is not a literal | Decided 2026-10-30 |
13+| Numeric terms | `%` and a decimal as Hoon writes it: `%1.000`; `%1000` and `%007` are errors | Decided 2026-10-30 |
14+| Strings | Hoon's escapes: `\\`, `\"`, `\{`, and `\` with two lowercase hex digits; a string and each `{…}` close on the line they open; `::` in a string is text | Decided 2026-10-30 |
15+| Spacing | Style, kept by the formatter: one or more spaces between items, and spaces allowed after an opener, before a closer and at the end of a line; faces, puns and typed path segments take no spaces; a rune is followed by one | Decided 2026-10-30 |
16+| Path literals | `/posts/~zod` is an expression: `pathlit` is an operand | Decided 2026-10-30 |
17+| Crash | `!!` is an operand, so `f !!` and `~[!!]` need no parentheses | Decided 2026-10-30 |
18+| Sample names | Every bare name in a sample binds, at any depth: `\|= [a=@ b]` binds `a` and `b` | Decided 2026-10-30 |
19+| Local definitions | At the start of any body: a gate's, an arm's, a `^-`'s, a `~&`'s or a `++`'s | Decided 2026-10-30 |
20+| Open molds | A mold's `[` and `(` may be left open at line end, one field or mold per deeper line; a `(` keeps the mold's name on its first line | Decided 2026-10-30 |
21+| Open groups | A `(` alone at line end puts the call's head on the first deeper line | Decided 2026-10-30 |
22+| Several open brackets | The deeper lines go to the last bracket, or the last rune's body, left open on a line, and the brackets before it close with it: `(column ~ ~[` needs no `)` | Decided 2026-10-30 |
23+| Jibb code blocks | Labelled `jibb` in this doc; each is a golden test | Decided 2026-10-30 |
24+| Definitions | `++` for values, `+$` for molds; no `=/` | Decided |
25+| Unions | `$?` only; tags anywhere; cases differ as nouns; a tag literal or `~` waits for its union, and an unresolved one is an error | Decided 2026-10-29 |
26+| Branching | `?-` only, loobeans included; exhaustive, with explicit catch-alls; covered arms are errors; failures say `!!` | Decided 2026-10-29 |
27+| Equality and negation | The gates `eq` and `not` | Decided |
28+| Special rules | No built-ins; type-directed rules are welcome, each listed in the Grammar tab | Decided 2026-10-29 |
29+| Inference | Hindley–Milner with aura variables; a bare `@` in a `+$` is one per module, `@ud` if unfixed; different auras never unify; faces are part of record types; principal types | Decided 2026-10-29 |
30+| Effects | The library unions `cmd` and `sub`; pokes and requests are opaque, from derived gates and `pure`; islands read agents only by watching; replies get full state, and diffs take a fold; timers carry the time; `%who` gives the owner's `@p`; nacks reach replies as text | Decided 2026-10-29 |
31+| Agent APIs | `/ui/api/<agent>.jibb` with `+$ poke` and `+$ paths`, over the agent's mark types; a case is a name, a path and a type, with `(facts full diff …)` for diffs, told apart by mark; an optional scry path, for pages; typed segments by aura | Decided 2026-10-29 |
32+| Mark names | Any name: `/%` names types after marks, and constructors poke with their type's mark; no alias marks | Decided 2026-10-26 |
33+| Derived gates | API files and stores give a constructor per poke type and a checked request per path or store state; clashing names are errors | Decided 2026-10-29 |
34+| Hoon types | A fixed translation table; `map`, `set`, `qeu`, `jar`, `jug` by name; `mop` as `(mop K V)`, read with `on`; unused molds skipped | Decided 2026-10-29 |
35+| Layout | Indentation; juxtaposed calls; a bracket open at line end takes one item per line, with no closer | Decided |
36+| Elements | Ordinary gates; containers take attribute lists; `stack` switches at `wide`; `el` adds attributes and wins; headings 1 to 6; `form` sends on Enter, the innermost when nested; `image` from an application path, sized from the file, or a sized URL; no focus control for now | Decided 2026-10-29 |
37+| Long calls | One argument per line inside parentheses | Decided |
38+| Inline gates | Hoon's wide form; the mold may be inferred; a bare sample name binds; `\|=%got` is a case's constructor, typed by the gate expected; a bare tag is always a value | Decided 2026-10-29 |
39+| Empty mold | `!!`, with no values; the expression has any type; pages are `(element !!)`, which keeps islands out of islands | Decided 2026-10-29 |
40+| Type variables | Capitals, generalized per `++` | Decided 2026-10-29 |
41+| Loobean mold | `?`, switched with `?-` | Decided 2026-10-26 |
42+| Defaults | None | Decided |
43+| Modules | Records, not cores | Decided |
44+| Kinds | The type of `main`: `(element !!)`, `(page R D)`, `(program D X M)` or `(store S)`; other names by file kind, in the Grammar tab | Decided 2026-10-29 |
45+| Type identity | Recursive molds by name and module | Decided 2026-10-29 |
46+| Binding patterns | In `?-` arms; the last position binds the rest | Decided 2026-10-29 |
47+| Text | Strings are written only as `"…"` and are always cords; tapes come from `trip`; `{…}` prints by aura, a bare `@` as `@ud`; `print-date` prints Urbit-style to the minute in the theme's zone, from jibb-dev's zone table; numbers keep Hoon's form | Decided 2026-10-29 |
48+| Auras | Soft, through aura variables | Decided 2026-10-29 |
49+| Crashes | As in Hoon; an island keeps its model and shows the standard notice; a failing page, or one rendering a control outside an island, gets Jibb's error page; no render limit, so an endless render stalls the ship | Decided 2026-10-29 |
50+| Refinement types | None | Decided |
51+| Imports | `/-`, `/%` (a mark's type, with no face) and `/=`; no `/+`; cycles are errors | Decided 2026-10-29 |
52+| Hoon marks for Jibb types | None: wire types are the agent's marks, and Jibb's own types never cross | Decided 2026-10-26 |
53+| Core language | A small untyped core language between the checker and both backends: Nock's operations on nouns plus a lambda calculus with names; one sample per call; s-expressions as its text form; a reference interpreter in Hoon; three rules in its tab still proposed | Decided |
54+| Stages | By kind: pages without data at commit, with data in the host, islands and stores in the browser; never inferred | Decided 2026-10-29 |
55+| Page loading | `load` maps route parameters, `~` for none, to one `ask` or `pure`; pages render per request from a fresh scry of the path, or of its case's scry path; nothing is watched or kept | Decided 2026-10-29 |
56+| Compile targets | Nock on the ship, JavaScript in the browser | Decided |
57+| Optimizer | Basic lowering; holes later | Decided 2026-10-29 |
58+| Island `update` | In the browser | Decided |
59+| Island `init` | Takes the page's data, returns the model | Decided 2026-10-29 |
60+| Island rendering | `view` re-runs on model changes; a keyed diff patches the DOM; holes later | Decided 2026-10-29 |
61+| Data crossing to the browser | No gates in island data or models | Decided 2026-10-29 |
62+| Inputs | A message per input event, none mid-composition; controls reset to the latest view after every event; fields read-only until the island starts | Decided 2026-10-29 |
63+| Transactions | None; one poke per command | Decided |
64+| Reloads | Restart from the model the page is served with | Decided 2026-10-29 |
65+| Shared state | Stores: browser agents written like API files, with `state`, `+$ poke` and `(store init update)`; poked and watched through derived gates; one per page | Decided 2026-10-29 |
66+| Subscriptions | By agent and path, shared per page; the first fact is full state, so no paths that need a scry path | Decided 2026-10-29 |
67+| Apps | One island under a catch-all route, given the path by `pure`; `%url` delivers the path on every change; in-section links act as `go`; focus moves to the first heading | Decided 2026-10-29 |
68+| Layout vocabulary | Jibb's elements and theme tokens; the stylesheet comes from the theme alone; chars and fill are inline; `[%fill n]` below 1 counts as 1 | Decided 2026-10-29 |
69+| Responsive layout | `stack` switches at the theme's `wide` | Decided 2026-10-29 |
70+| Theme | A prelude mold with every token required: spaces, five tones with `normal` and `background`, language, zone, `wide`, and an optional static font; 4.5:1 contrast enforced | Decided 2026-10-29 |
71+| Dark mode | Hand-written dark tones under `dark`, used when the reader prefers dark | Decided 2026-10-27 |
72+| Links | All text escaped; in-app links are application paths, as `%/blog/(slug)`, with no query or fragment, checked at compile time against routes and static files; `link`, `image` and `go` also take unchecked `https`, `mailto` and `tel` URLs and root-relative cords such as `"/apps/groups"`; other cords render as text; a CSP on every page | Decided 2026-10-29 |
73+| Data layer | Hoon's nouns: units, treaps, loobeans | Decided |
74+| Prelude | Hoon's standard library regrouped: arithmetic over aura variables, units, lists, maps, sets, queues, ordered maps, function helpers, text, atom printing, a zoned date printer, time and data molds; folds take a start; bits and hashing as natives; no signed, float or parser gates, and signed or float auras are errors | Decided 2026-10-29 |
75+| Wire format | Noun channels; Jibb's own noun codec in the browser | Decided 2026-10-29 |
76+| Hosting | One thin host agent on `%jibb` binds, caches and serves every Jibb desk; each desk vendors jibb-dev, names its base path in docket's `site+`, and builds and renders with its own copy through fixed entry points; install `%jibb` first; app agents stay plain Hoon; pokes only from the ship | Decided 2026-10-29 |
77+| Private pages | Served by the host per request and never cached; non-owners go to the login page | Decided 2026-10-29 |
78+| Cache | Eyre holds only the hashed assets, all public; the host serves every page; a desk's entries are cleared on suspend or uninstall, and all by `-jibb!clear` | Decided 2026-10-29 |
79+| Static files | Under `/ui/static/`, committed, at hashed immutable URLs; referenced by application path, so a missing file fails to compile, and images are sized from the file; no large media, since Clay keeps every version and the cache ignores `Range` | Decided 2026-10-29 |
80+| Routing | A literal beats a parameter, which beats a catch-all; parameters bind to `load`'s sample by name; `@ta` and `@tas` raw, other auras as Hoon prints them | Decided 2026-10-29 |
81+| Linking | Folder convention; the mark stores text; the host runs each desk's own compiler; modules cached by their own and their imports' hashes | Decided 2026-10-29 |
82+| Access | Every page is private to the owner; public pages come in a later version | Decided 2026-10-29 |
83+| Tests | `test-…` arms under `/ui/tests`, run by `-jibb!test`; the JavaScript side and differential tests under Node from jibb-dev | Decided 2026-10-29 |
84+| Editor support | Tree-sitter grammar | Decided |
85+| Formatter | One canonical layout | Decided |
86+| Name | Jibb | Decided |
87+
88+## Checked against the kernel
89+
90+- **Eyre's cache** is keyed by the exact request target, query included, one entry per URL, and counts against the ship's memory ([eyre.hoon](https://github.com/urbit/urbit/blob/50c7f5239610fc72263df7aa99b4d171cfe9d408/pkg/arvo/sys/vane/eyre.hoon#L922-L927), [vere #626](https://github.com/urbit/vere/issues/626)). An auth-flagged entry gives anyone but the owner a bare 403 before any agent sees the request ([vere http.c](https://github.com/urbit/vere/blob/510d5b7fe4ea81343f2364db081ce61f1d2c72d3/pkg/vere/io/http.c#L1141-L1146)), and entries outlive their agent ([gall.hoon](https://github.com/urbit/urbit/blob/50c7f5239610fc72263df7aa99b4d171cfe9d408/pkg/arvo/sys/vane/gall.hoon#L1382-L1409)).
91+
92+## Build order
93+
94+Each step ends in something usable.
95+
96+1. **Parser and mark**: the layout pass, runes, literals and patterns; the `%jibb` mark.
97+2. **Types and prelude**: inference with aura variables and tag resolution; `/-`, `/%` and `/=`; the Hoon type table; the core prelude gates.
98+3. **Core language**: lowering from the checked tree, the text form, the reference interpreter in Hoon, and the Nock backend.
99+4. **Host and static pages**: the shared host running vendored compilers, routing and application paths, layout and theme, static files, the Eyre asset cache and the owner check. First release.
100+5. **Published pages**: `load`, requests and per-request rendering.
101+6. **Islands**: first a benchmark that runs the same programs as JavaScript and in a Nock interpreter, to pick the browser backend; then that backend, `cmd` and `sub`, keyed diffing, the shared library, stores, the crash notice and URL subscriptions.
102+7. **Tooling**: tests, formatter, tree-sitter grammar and differential tests.
103+8. **Polish**: shared island modules across pages, holes, size.
docs/spec/grammar.mdnew file+342
@@ -0,0 +1,342 @@
1+# Jibb grammar
2+
3+Oct 29, 2026 · @iko
4+
5+The syntax the compiler accepts.
6+
7+## Lexical rules
8+
9+- **Indentation is structure**: a deeper line continues the line above; returning to an earlier indentation closes every block since. Spaces only; blank and comment-only lines don't count.
10+- **Layout tokens**: a deeper line emits INDENT, the same depth NEWLINE, a shallower one a DEDENT per closed level, then NEWLINE.
11+- **Open brackets**: a bracket open at line end takes one item per deeper line and closes at the first line that isn't deeper, with no closer. Brackets opened and closed on one line work as usual. A mold's `[` and `(` open the same way, with one field or mold per deeper line. A line may leave several brackets open: the deeper lines go to the last one, or to the body of the last rune, and the others close with it, so `(column ~ ~[` above its items needs no `)`.
12+- **Tight forms**: `a+b`, `a.b`, `` `x ``, `|=%got` and the openers `~[`, `|=(`, `$?(`, `$-(` take no spaces. A `(` right after a wing is a change: `m(note=text)` changes, `m (note text)` calls. `%/` starts an application path, `%/blog/(slug)`.
13+- **Spacing**: one space between items; more for alignment. That is style, which the formatter keeps: the parser takes one or more spaces between items, and allows spaces after an opener, before a closer and at the end of a line. Faces `name=value`, puns `=name` and typed path segments `[id=@ud]` take no spaces, and a rune is followed by one.
14+- **Comments**: `::` to the end of the line, outside strings; inside a string, `::` is text.
15+- **Names**: Hoon terms; lowercase letters, digits and hyphens, starting with a letter.
16+
17+## Literals
18+
19+| Form | Example | Type |
20+| --- | --- | --- |
21+| Decimal | `1.000` | `@ud`, or any aura by context; groups of three, as in Hoon |
22+| Hexadecimal | `0x1f` | `@ux` |
23+| Base 32 | `0v5.fdf99` | `@uv` |
24+| Ship | `~zod`, `~sampel-palnet` | `@p` |
25+| Date | `~2026.10.23`, `~2026.10.23..14.30.00` | `@da` |
26+| Duration | `~s30`, `~h1.m30` | `@dr` |
27+| Knot | `~.my-post` | `@ta` |
28+| String | `"milk"`, `"Since {day}"` | `@t`, a cord; `{…}` prints by aura; tapes come from `trip`; there is no `'…'` form |
29+| Term | `%add`, `%1.000` | A union's case, or that constant atom where an atom is expected; a numeric term is `%` and a decimal as Hoon writes it, so `%1.000`, never `%1000` |
30+| Loobean | `%.y`, `%.n` | `?` |
31+| Null | `~` | A constant: the `~` case of any union with one, such as `(unit A)`, `(list A)`, `(map K V)` or `(set A)` |
32+| Unit | `` `x `` | `(unit A)` |
33+| Cell | `[a b c]`, `[text="milk" done=%.n]` | A tuple; a record when faced; a cons where a list is expected |
34+| List | `~[1 2 3]` | `(list A)` |
35+| Term-headed pair | `gap+%s2` | `[%gap %s2]`, any operand after the `+` |
36+| Path | `/posts/~zod` | `path` |
37+| Application path | `%/blog`, `%/blog/(slug)` | `qath`, checked against routes and static files; no query or fragment |
38+
39+Every literal reads exactly as Hoon reads it: `1000` is an error (write `1.000`), `~2026.2.31` is `~2026.3.3`, years BC and sub-second groups are dates, and `~2026.10.29..14.30`, the minute form `print-date` writes, is not a literal.
40+
41+## Expressions
42+
43+- **Call**: `f a b`; parentheses only group. A call open in parentheses puts its head and first arguments on the first line, then one argument per indented line; a `(` alone at the end of a line puts the head on the first indented line.
44+- **Lists**: `~[a b c]`, or `~[` at line end with one item per line. On one line, whitespace separates items, so calls need parentheses: `~[(text "hi")]`, which the checker suggests for `~[text "hi"]`. On indented lines, each line is an item.
45+- **Faced cells**: `[text="milk" done=%.n]`, or one `name=value` per line of an open bracket. A change takes the same items, as `m(done=%.y)`.
46+- **Pairs and units**: `gap+%s2` and `` `x `` bind tighter than calls, so `pad+%s3 gap+%s2` is two items.
47+- **Wing** `title.p`, also for imports and prelude groups, which are records: `card.card`, `space.theme.theme`, `get.by`. **Change** `m(draft="x" error=~)`.
48+- **Inline gate**: `|=(p=post.blog title.p)`, or `|=(p title.p)` when the call fixes the type. A bare name in a sample always binds. `|=%got` is case `%got`'s constructor, typed by the gate expected. A bare name binds at any depth of a sample, so `|= [a=@ b]` binds `a` and `b`.
49+- **Interpolation**: `{…}` holds any expression; `\{` writes a brace and `\"` a quote; no `<…>`. Strings take Hoon's escapes: `\\` writes a backslash, and `\` with two lowercase hex digits writes that byte, as `\0a` a line feed; any other `\` is an error. A string and each `{…}` close on the line they open, and `::` inside a string is text.
50+- **No operators**: `eq a b`, `not x`, `gth a b` are gates.
51+
52+## Runes
53+
54+A rune's first children sit on its line, the last taking the rest of it; the others follow, indented.
55+
56+- `++` **luslus**: defines a value, at top level or as local names at the start of any body (a gate's, an arm's, a `^-`'s, a `~&`'s or a `++`'s), visible to each other and the final expression. Gates may call themselves and their siblings; other values can't refer to themselves, so `++ title (trim title)` is an error.
57+- `+$` **lusbuc**: defines a mold, likewise; molds may be recursive.
58+- `|=` **bartis**: a gate with a faced sample, `m=model`, `=msg` (`msg=msg`) or `[a=@ b=@]`; the body on the same line or below.
59+- `$?` **bucwut**: a union, one case per line or `$?(%a %b)`; cases must differ as nouns.
60+- `?-` **wuthep**: the only branch, exhaustive; the subject on its line, then arms of a pattern and an expression or body. Conditions switch on `%.y` and `%.n`.
61+- `^-` **kethep**: a cast; the mold on its line, the expression after it or below.
62+- `~&` **sigpam**: prints the value on its line, then runs the body below; output goes to the ship's log or the browser's console.
63+- `!!` **zapzap**: a crash, an expression of any type; as a mold, the empty mold. It is an operand, so `f !!` and `~[!!]` need no parentheses.
64+- `$-` **buchep**: a gate's mold, `$-(sample product)`.
65+- `|$` **barbuc**: a generic mold, `|$ [A] mold`.
66+
67+Only `|=`, `$?` and `$-` have wide forms: `|=(sample expr)`, `$?(case case)`, `$-(mold mold)`. Their children are operands, so calls inside need parentheses.
68+
69+Left out on purpose:
70+
71+- `|%`, `|_`, `|*`: modules are records.
72+- `$%`: `$?` covers unions.
73+- `?+`, `?:`: `?-` is the branch.
74+- `.=`, `?!`, `=(a b)`, `!x`: `eq` and `not` are gates.
75+- `=/`: `++` defines values.
76+- `|-`, `$(…)`: gates recurse by name.
77+- `~|`: `~&` covers debugging.
78+- `*mold`: no defaults.
79+- `=>`, `=<`, `.*` and `a:b`: names never shadow, and `.` reads imports too.
80+- `!>` and vase runes, `/+`: code also runs in the browser.
81+- Sail's `;` runes: layout is Jibb's elements.
82+- Tall forms such as `:~`, `:*`, `%:` and `%=`, the closers `==` and `--`, and other wide forms such as `^-(…)` and `?-(…)`: indentation covers them.
83+- `@s…` and `@r…` auras: errors wherever used, imports included.
84+
85+## Imports
86+
87+Imports come first:
88+
89+```jibb
90+/- todo :: the molds from /sur/todo.hoon this module uses
91+/% %todo-action :: a mark's type, named todo-action
92+/= card /ui/components/card :: a Jibb module
93+```
94+
95+`/%` names a type after its mark, so it takes no face, and any mark name works; Hoon's `/%` binds the mark's core instead. Derived constructors such as `todo-action.todo` supply the agent and mark, so code never writes one. API modules re-export their `/%` types, not `/-` faces or `/=` modules. Import cycles are errors.
96+
97+## Modules
98+
99+A file is a module: imports, then definitions.
100+
101+- `+$ name mold` defines a mold; `++ name expr` a value.
102+- Names may refer to each other in any order, and no name may shadow one already in scope, imports and the prelude included.
103+- Molds and values have separate namespaces, told apart by position: `todo-action.todo` is a mold and its constructor, `todos.todo` a mold and a request.
104+- `main`'s type decides the kind: `(element !!)` a page; `(page R D)` a page with data, from parameters `R` to data `D`; `(program D X M)` an island, with data, model and messages; `(store S)` a store with state `S`. They're built by `(page load view)`, `(program init update view subs)` and `(store init update)`. A module without `main` is a library.
105+- Everything is exported and read through the import's face, as `card.card`. Only the prelude has private names.
106+- An API module, `/ui/api/<agent>.jibb`, also exports derived gates: a constructor per `poke` type, adding the agent and mark, and a request per `paths` case, taking a reply over full state, plus a fold for `(facts full diff …)`. A store exports a constructor per `poke` type and a `state` request. Clashing derived names are errors.
107+
108+Names the host reads, by file:
109+
110+| File | Names |
111+| --- | --- |
112+| Any module | `main`, the kind |
113+| A store | `state`, `poke` |
114+| `/ui/api/<agent>.jibb` | `poke`, `paths` |
115+| `/ui/theme.jibb` | `theme` |
116+| Under `/ui/tests` | `test-…` arms, one test each |
117+
118+Every other name is free.
119+
120+## Molds
121+
122+A mold is only a type: never called on a noun, and with no default. Types are inferred, Hindley–Milner with aura variables: every expression has a principal type, and there is no subtyping.
123+
124+- **Atoms**: `@` and auras such as `@ud`, `@t`, `@p`, `@da`; `?` is a loobean. `@` is an atom whose aura isn't known yet, fixed by what it meets: `add` takes `[@ @]` and returns `@`, all one aura, so `(add n 1)` with `n=@ud` is a `@ud`. Different auras never unify. A bare `@` in a `+$` is one aura variable per module, `@ud` if unfixed.
125+- **Constants**: `%add`, `~` and the loobeans have one value each.
126+- **Records and tuples**: `[text=@t done=?]`, `[@t ?]`. Faces are part of the type, so `[a=@ b=@]` and `[x=@ y=@]` don't unify. A record too long for one line can leave its `[` open, with one field per deeper line, as an expression's bracket does.
127+- **Unions**: `$?`; tags take their union from context, as Tag resolution describes. Cases must differ as nouns: at some position of their right-nested cells, one is an atom and the other a cell, or both are different constants. `*`, type variables and molds that can be either, such as `(list A)` or `?`, overlap both. A union whose cases don't all differ is rejected.
128+- **Containers**: `(list A)`, `(unit A)`, `(set A)`, `(map K V)`, `(mop K V)`, `(qeu A)`, `(jar K V)`, `(jug K V)`.
129+- **Gates**: `$-(A B)`. Type variables are capitals, generalized per `++`.
130+- **Paths**: `/todos` is constant, `/` empty, and `/todo/[id=@ud]` typed by aura. `%/blog/(slug)` is an `qath`, checked against routes and static files.
131+- **Generic molds**: `+$ result |$ [V E] $?` with cases below.
132+- **Any noun**: `*`. **Empty**: `!!`.
133+- **Opaque**: `poke`, `ask` and `qath`, built only by derived gates, `pure` and application-path literals. `(facts full diff …)` appears only in an API file's `paths`.
134+- **Identity**: recursive molds go by name and module.
135+
136+## Tag resolution
137+
138+A tag literal or `~` gets a fresh type variable and a pending constraint: the variable must become a union with a matching case, or an atom type the term fits. Once unification fixes the variable, each constraint is checked against the union's cases or, for `@tas`, `@ta`, `@` or `*`, makes the tag that constant atom. A literal that matches nothing is an error naming the union, such as "msg has no case %submitt".
139+
140+- A constraint records the literal's shape, such as `[@t %draft]`, and matches cases by shape and shared tag positions; a literal in a field position is checked against that field.
141+- `|=%typed` is a case's constructor: against `$-(@t M)` it is `|=(t [%typed t])`, requiring `M` to have a case `[%typed @t]`. It works for cases tagged at their head; a bare tag is always a value.
142+- A constrained variable, and any it mentions, doesn't generalize; it stays open until something in the module fixes it. An island's `main` fixes its messages, so islands resolve their own tags.
143+- A constraint still open when its module is checked is an error asking for a `^-`; the checker never guesses. Library helpers can cast, as in `^- (list attr)`.
144+- Wing access, changes and `?-` on a type not yet known wait the same way.
145+- Patterns need none of this; they're checked against the subject's type.
146+
147+```jibb
148+:: /ui/components/buttons.jibb, a library with no main
149+++ add-button |= label=@t
150+ button label %add :: error: nothing fixes the union of %add
151+++ labelled |= [label=@t msg=M]
152+ button label msg :: fine: the caller's union
153+++ wide ^- (list attr)
154+ ~[width+[%fill 1] pad+%s2] :: fine: the cast fixes the tags
155+```
156+
157+## Literals by context
158+
159+Number and cell literals resolve like tags: each waits for its context and takes a default when its `++` is checked.
160+
161+| Literal | Can become | Default |
162+| --- | --- | --- |
163+| Tag `%a`, or `~` | A union's case or a constant atom | None: an error asking for a `^-` |
164+| Number `5` | Any aura | `@ud` |
165+| Cell `[x xs]` | A tuple, a record, a cons | A tuple |
166+
167+- In `{…}`, an atom prints by its aura: `@ud` as `1.000`, `@da` as a UTC date with sub-seconds, `@p` as a ship, and a bare `@` as `@ud`. `@t`, `@ta`, `@tas` and tapes splice as text; any other cell is an error asking for a printer. For readers, `print-date` prints in the theme's zone, Urbit-style, to the minute, as `~2026.10.29..14.30`; numbers keep Hoon's form.
168+- String literals are always cords, so an element needs `text`, as `(text "{n} items")`.
169+- Where a list is expected, `[x xs]` is `x` in front of `xs`.
170+- A typed segment prints and parses by aura, whether `[id=@ud]` in an agent's paths, a `_` route parameter typed by `load`'s sample, or a `(…)` segment of an application path; `@ta` and `@tas` are raw text.
171+
172+```jibb
173+++ view |= [day=@da n=@ud]
174+ column ~[gap+%s1] ~[
175+ (text "Updated {day}") :: day printed as a date
176+ (text "{n} items") :: n printed as @ud
177+```
178+
179+## Patterns
180+
181+Arms are tried in order. Every case needs an arm or a catch-all, `*` or a bare name. An arm that earlier arms cover, such as a second `%add` or anything after `[%move * *]`, is an error, and a case that should fail says `!!`. A name in a pattern always binds anew, so compare values with `?- (eq a b)`. Patterns take terms, numbers, strings without `{…}`, ships, loobeans and `~`, not lists or pairs.
182+
183+| Pattern | Matches | Binds |
184+| --- | --- | --- |
185+| `%add` | The term | Nothing |
186+| `%.y`, `%.n` | Yes, no | Nothing |
187+| `[%add t]` | A cell headed by `%add` | `t`, the rest |
188+| `[t %draft]` | A pair ending in `%draft` | `t` |
189+| `[%move %left n]` | Tagged `%move`, then `%left` | `n` |
190+| `~` | Null | Nothing |
191+| `[~ x]` | A unit holding a value | `x` |
192+| `1.000`, `"milk"`, `~zod` | That literal | Nothing |
193+| `[a b]` | Any cell; `b` is the rest | `a`, `b` |
194+| `*` | Anything | Nothing |
195+| `x` | Anything | `x` |
196+
197+Tags can sit anywhere in a case, and a case can carry several:
198+
199+```jibb
200++$ step $?
201+ [%move %left n=@ud]
202+ [%move %right n=@ud]
203+ [text=@t %draft]
204+ [text=@t %sent at=@da]
205+ %stop
206+
207+++ describe |= s=step
208+ ?- s
209+ [%move %left n] "left {n}"
210+ [%move %right n] "right {n}"
211+ [t %draft] t
212+ [t %sent at] t
213+ %stop "stop"
214+```
215+
216+Leaving out `%stop` is an error naming it; to crash instead, write `%stop !!`.
217+
218+## The grammar in EBNF
219+
220+```
221+(* Layout: a deeper line emits INDENT, a line at the same depth NEWLINE, and a
222+ shallower one a DEDENT for each level it closes, then NEWLINE. Blank and
223+ comment-only lines emit nothing. A bracket left open at the end of a line
224+ takes the deeper lines below as its items and needs no closer. When a line leaves
225+ several brackets open, the deeper lines go to the last one, or to the body
226+ of the last rune, and the others close with it. *)
227+
228+module = { import NEWLINE } { definition NEWLINE } ;
229+import = "/-" name | "/%" term | "/=" name pathlit ;
230+definition = "+$" name ( mold | INDENT mold DEDENT )
231+ | "++" name ( expr | body ) ;
232+
233+body = INDENT { definition NEWLINE } expr DEDENT ;
234+expr = rune | call ;
235+call = operand { operand } ;
236+operand = literal | qath | ref | change | pair | unit | group | list | tuple
237+ | gate | ctor | pathlit | "!!" ;
238+ref = wing ;
239+wing = name { "." name } ;
240+change = wing "(" { item } ")" | wing "(" { item } open ; (* no space before "("; items named *)
241+pair = name "+" operand ; (* [%name operand] *)
242+unit = "`" operand ; (* [~ operand] *)
243+group = "(" expr ")" | "(" { operand } open ;
244+list = "~[" { operand } "]" | "~[" { operand } open ;
245+tuple = "[" { item } "]" | "[" { item } open ;
246+item = [ name "=" ] operand ;
247+open = INDENT line { NEWLINE line } DEDENT ;
248+line = [ name "=" ] expr ;
249+qath = "%/" [ asegment { "/" asegment } ] ;
250+asegment = knot | "(" expr ")" ; (* checked against the routes *)
251+gate = "|=(" ( field | name ) operand ")" ; (* the one wide expression *)
252+ctor = "|=" term ; (* a case's constructor *)
253+
254+rune = "|=" field ( expr | body )
255+ | "^-" mold ( expr | body )
256+ | "~&" expr body
257+ | "?-" expr INDENT arm { NEWLINE arm } DEDENT ;
258+arm = pattern ( expr | body ) ; (* every case covered; see Patterns *)
259+
260+mold = aura | "*" | "!!" | "?" | "~" | term | loobean | ref | typevar
261+ | pathmold
262+ | "(" ref mold { mold } ")" | "(" ref { mold } mlines
263+ | "[" field { field } "]" | "[" { field } flines
264+ | "$?" INDENT mold { NEWLINE mold } DEDENT
265+ | "$?(" mold { mold } ")"
266+ | "$-(" mold mold ")"
267+ | "|$" "[" typevar { typevar } "]" mold ;
268+field = [ name "=" ] mold | "=" name ; (* =msg is msg=msg *)
269+mlines = INDENT mold { NEWLINE mold } DEDENT ; (* a mold's ( left open *)
270+flines = INDENT field { NEWLINE field } DEDENT ; (* a mold's [ left open *)
271+pathmold = "/" [ segment { "/" segment } ] ;
272+segment = knot | "[" name "=" aura "]" ;
273+
274+pattern = term | number | string | ship | loobean | "~" | "*" | name
275+ | "[" pattern { pattern } "]" ;
276+
277+literal = number | string | ship | date | duration | knotlit
278+ | term | loobean | "~" ;
279+number = decimal | "0x" hex | "0v" base32 ;
280+pathlit = "/" knot { "/" knot } ;
281+term = "%" name | "%" decimal | "%&" | "%|" ;
282+loobean = "%.y" | "%.n" ;
283+aura = "@" { lower } [ upper ] ;
284+typevar = upper ;
285+name = lower { lower | digit | "-" } ;
286+knot = { lower | digit | "-" | "." | "~" | "_" } ;
287+```
288+
289+## Example: a store and an island
290+
291+```jibb
292+:: /ui/stores/cart.jibb
293++$ state [items=(list @t) open=?]
294++$ cart-action $?
295+ [%add item=@t]
296+ %toggle
297++$ poke cart-action
298+
299+++ main (store init update)
300+++ init ^- state [~ %.n]
301+++ update |= [a=cart-action s=state]
302+ ?- a
303+ [%add item] s(items=[item items.s])
304+ %toggle s(open=(not open.s))
305+```
306+
307+```jibb
308+:: /ui/basket.jibb
309+/= cart /ui/stores/cart
310+
311+++ main (program init update view subs)
312+
313++$ model [cart=state.cart note=@t]
314++$ msg $?
315+ [%noted text=@t]
316+ %add
317+ %toggle
318+ [%got cart=state.cart]
319+ [%acked err=(unit @t)]
320+
321+++ init |= ~
322+ [[~ %.n] ""]
323+
324+++ update |= [=msg m=model]
325+ ?- msg
326+ [%noted t] [m(note=t) ~]
327+ %add [m(note="") ~[[%poke (cart-action.cart [%add note.m]) |=%acked]]]
328+ %toggle [m ~[[%poke (cart-action.cart %toggle) |=%acked]]]
329+ [%got c] [m(cart=c) ~]
330+ [%acked err] [m ~]
331+
332+++ subs |= m=model
333+ ~[[%watch (state.cart |=%got)]]
334+
335+++ view |= m=model
336+ column ~[gap+%s1] ~[
337+ (text "{(lent items.cart.m)} items")
338+ field "Item" note.m |=%noted
339+ button "Add" %add
340+```
341+
342+A store is written like an API file: `state`, `+$ poke` and `(store init update)`. Islands poke it with derived constructors and watch it with `(state.cart |=%got)`, whose first fact is the current state and each later fact the next. It runs in the browser, shared by the page's islands.
docs/spec/implementation-plan.mdnew file+104
@@ -0,0 +1,104 @@
1+# Implementation plan
2+
3+Oct 29, 2026 · @iko
4+
5+Build Jibb in eight milestones, each ending in something that runs on a fake ship. The first release, static pages served by the shared host, comes at milestone 4. The spec is in the other tabs: the language in Jibb — an Elm-architecture UI language for Urbit, its syntax in Grammar, the core language in Core language, and every choice in Decisions.
6+
7+## Milestones
8+
9+Each milestone ends with its checks passing on a fake ship. No dates yet: agree them after milestone 1, once it shows how fast the Hoon work goes.
10+
11+1. **Parser and mark**
12+ - Builds: the `%jibb` mark as the first tab gives it; the layout pass, emitting INDENT, DEDENT and NEWLINE; a parser for the Grammar tab's EBNF that returns a syntax tree with source positions; errors with file, line, column and an excerpt.
13+ - Done when: every Jibb code block in the doc parses; each lexical rule has a test input that breaks it; `|commit` stores `.jibb` files on a fake ship.
14+2. **Types and prelude**
15+ - Builds: Hindley–Milner inference with aura variables; tag and literal resolution with deferred constraints; the union disjointness check; exhaustive `?-` with dead-arm errors; `/-`, `/%` and `/=`, with the table for importing Hoon types; API files and their derived gates; the prelude's types.
16+ - Done when: the doc's examples typecheck, except those written as errors; each error the Grammar tab describes has a test with its message, such as "msg has no case %submitt"; a `/sur` file with `$%`, `$@`, maps and a recursive mold imports as the table says.
17+3. **Core language**
18+ - Builds: lowering from the checked syntax tree; the text form's printer and reader; the reference interpreter, in Hoon; the Nock backend, with gates shaped like Hoon's and natives calling the standard library.
19+ - Done when: the Core language tab's example lowers to the program shown there; the interpreter and the Nock backend agree on a corpus of test programs, crashes included.
20+4. **Host and static pages: the first release**
21+ - Builds: the shared host on the `%jibb` desk, which finds app desks by docket's `site+`, binds their paths, rebuilds on commit one module per Behn timer wake, and serves pages only to the owner; the compiler's `build` and `render` entry points; the renderer written in Jibb, with HTML serialized on the ship; the theme's stylesheet; routing and application paths; static files at hashed URLs in Eyre's cache; the per-module compile cache; `-jibb!clear`.
22+ - Done when: worked example 1 is served at its URL; a logged-out request is sent to the login page; a commit that changes one module recompiles only it and its dependents; a missing static file is a compile error; suspending the desk clears its cache entries.
23+5. **Published pages**
24+ - Builds: `load`, the per-request scry, route parameters parsed by aura, validators on scried data, and the 404 and 500 pages.
25+ - Done when: worked example 2 and a `_slug` route render from a fresh scry on every request; an unparseable parameter gives a 404 and a crashing render a 500, neither cached.
26+6. **Islands**
27+ - Builds, in order: the benchmark, a minimal JavaScript backend against a Nock interpreter in the browser on the same programs, with the result recorded in Decisions; then the chosen backend and the runtime library: the noun codec with `jam`, `cue` and `@uw`, natives, the channel client over `fetch`, the keyed DOM differ, `cmd` and `sub`, stores, the crash notice, URL subscriptions, and the loop rules under Browser runtime.
28+ - Done when: worked example 3 and the Grammar tab's store example run end to end in a browser; the browser code agrees with the reference interpreter on the test corpus; a keystroke in the benchmark's list island meets the bar agreed under Open questions.
29+7. **Tooling**
30+ - Builds: `-jibb!test` for `test-` arms; the Node harness that runs each test module's Nock, with nockjs, beside its JavaScript; the formatter; the tree-sitter grammar.
31+ - Done when: CI runs both test suites on every commit, and the formatter leaves every example in the doc unchanged.
32+8. **Polish**
33+ - Builds: island modules shared across pages, holes, and size work.
34+ - Done when: the shared library stays under 20 KB.
35+
36+## Setup and ground rules
37+
38+- **Code**: one repo with three folders: `jibb-dev`, which app desks vendor (the compiler, prelude, browser library and marks); `jibb`, the desk for the shared host and its threads; and an example app desk built from the doc's worked examples. The Node harness lives beside them.
39+- **Running**: a fake ship, with each folder synced into its desk and `|commit` after changes. App desks also need base-dev's `txt`, `txt-diff`, `js`, `bill` and `kelvin` marks, docket's `docket-0` and a `sys.kelvin`.
40+- **Tests**: from milestone 1 on, every Jibb code block in the doc is a golden test, and each stage's output is printed and compared, the core language in its text form.
41+- **The doc wins**: where code and doc disagree, the doc stands until it changes. A change to the language is agreed first and recorded in Decisions.
42+- **Questions** go as comments on the doc, on the words they're about.
43+
44+## Compiler architecture
45+
46+The compiler is a Hoon library, `/lib/jibb.hoon` in jibb-dev, which Clay builds once per release. The shared host checks it against two entry points, fixed across releases, and calls them: `build` on each commit, which also renders pages without data, and `render` for each request to a page with data. Until a new build links, the last one keeps serving.
47+
48+```
49+/ui source
50+ → layout pass → parser → a syntax tree per module
51+ → module graph: imports resolved, Hoon types read from Clay
52+ → type checker, one module at a time, in dependency order
53+ → core language
54+ → Nock: pages, islands' init and first view, and the libraries they use
55+ → JavaScript: islands' update, view and subs, stores, and the libraries they use
56+```
57+
58+| Decision | Choice | Why |
59+| --- | --- | --- |
60+| Nock code | Emitted directly from the core language, not through generated Hoon | Types are already checked, Hindley–Milner polymorphism has no direct Hoon form, and `mint` is slow on a ship |
61+| Browser code | JavaScript from the core language, unless milestone 6's benchmark favours a Nock interpreter | Both run the same programs, so the benchmark measures only the generated code |
62+| HTML renderer | One, written in Jibb: elements to virtual nodes, then HTML text on the ship and DOM in the browser | The server's HTML and the browser's first view agree by construction, which resuming relies on |
63+| Long compiles | One module per Behn timer wake | An event runs to completion, and moves inside it, self-pokes included, don't yield |
64+| Compile cache | Per module, with its interface and body hashed separately | A body change re-emits the module without re-checking its dependents |
65+
66+Anything that crosses between ship and browser is the same noun on both sides: records are right-nested cells in field order, `%.y` and `~` are 0, cords have their first byte lowest, and maps are treaps ordered by `mug`. Gates never cross, so each backend represents them its own way.
67+
68+## Browser runtime
69+
70+For milestone 6. An island is one loop over one model: events become messages, `update` turns each message into a new model and commands, and only `view` decides what the island shows.
71+
72+- **Start**: run `view` on the jammed model, bind the result to the server's nodes by key, and replace the subtree if they differ. `subs` then opens its watches, and each watch's first fact, full state, arrives as a message.
73+- **Messages** come from controls, command replies, facts, timers and `%url`. *Proposed*: an island handles them one at a time, in arrival order.
74+- **Rendering**: a message changes the model when `update` returns a different noun; `view` then re-runs and a keyed diff patches the DOM. An unchanged model renders nothing. *Proposed*: the patch lands before the next message, so a crash in `view` belongs to one message.
75+- **Controls**: after every event, the control that fired is reset to the latest view, even when the model didn't change. A field's text is rewritten only when it differs from the screen, which keeps the caret and any IME composition.
76+- **Commands**: each reply arrives later as a new message, never inside the `update` that sent it. *Proposed*: commands run in list order, after the patch.
77+
78+| Control | Sends | When |
79+| --- | --- | --- |
80+| `button label msg` | `msg` | On click, or Enter or Space while focused |
81+| `toggle on label msg` | `msg` | When the reader flips it; `msg` carries no new value |
82+| `field label value reply` | `(reply text)` | On every `input` event, and once per IME composition, when it ends |
83+| `form msg kids` | `msg` | On Enter in any field inside it, without reloading the page |
84+
85+*Proposed*: no overlays in v1, so no dialogs, menus, popovers or tooltips.
86+
87+## Risks
88+
89+- **Importing Hoon types.** Hoon's types form a graph with lazy `%hold` nodes, so the importer needs Hoon's own type engine to expand them, and it must recognise maps, sets and recursive molds by name. Start it early in milestone 2, on real `/sur` files such as Tlon's.
90+- **The compiler's speed in Hoon.** A parser and checker written in Hoon run far slower than native code. The cache keeps commits fast, but the first build after an install or upgrade is slow; time the example desk's first build in milestone 4.
91+- **Stack depth in JavaScript.** Most browsers have no tail calls, so self tail calls compile to loops, and list gates need native versions in JavaScript.
92+- **Atoms in JavaScript.** BigInt for every atom is slow. Keep small atoms as numbers and the rest as bytes, with cords caching their decoded string.
93+- **Type errors.** Hindley–Milner is known for confusing errors. Each error names both conflicting sites, with excerpts, and the messages are golden tests.
94+
95+## Open questions
96+
97+| Question | Needed by |
98+| --- | --- |
99+| Are the three proposed core-language rules adopted: gate-free `eq` arguments and map keys, natives that take gates, and no value that depends on itself through a gate? | Milestones 2 and 3 |
100+| What is the typography scale for heading levels and the `role` values? | Milestone 4 |
101+| Does the Eyre channel CSRF test reproduce on a fake ship? @iko has the write-up; if it does, it goes to Urbit privately. | Milestone 4 |
102+| What is the benchmark's bar? Proposed: a keystroke in a 500-row list island renders within one 16 ms frame on a mid-range phone. | Milestone 6 |
103+| Are the proposed loop rules adopted, along with no overlays in v1? | Milestone 6 |
104+| What do controls look like by default: buttons, toggles and fields, with their focus and disabled states? | Milestone 6 |
docs/spec/overview.mdnew file+527
@@ -0,0 +1,527 @@
1+# Jibb — an Elm-architecture UI language for Urbit
2+
3+Oct 29, 2026 · @iko
4+
5+## Summary
6+
7+Jibb is a Hoon dialect for Urbit frontends. `.jibb` files live in the desk under the `%jibb` mark; each desk's vendored Jibb compiles them, run by one shared host, so `|commit` is the only build step. Elm architecture, a small layout vocabulary, and three stages fixed by module kind:
8+
9+- **Build**, per commit: pages without data, as HTML, plus the stylesheet.
10+- **Publish**, per request, in the host: pages with data, from a fresh scry. The host serves every page, to the owner only.
11+- **Client**, in the browser: islands' `update`, `subs` and `view`, and stores, as JavaScript.
12+
13+A page without islands ships no JavaScript.
14+
15+**Goals**: no prebuild; familiar to Hoon programmers; a typed Elm architecture; Hoon's data end to end; no HTML or CSS outside the theme; static by default.
16+
17+**Non-goals**: agent logic, refinement types, transactions, JavaScript interop beyond Jibb's effects.
18+
19+*Name*: four letters like the vanes, after the jib, a boat's front sail.
20+
21+## What Urbit allows
22+
23+Everything needed exists. The one limit: marks can't follow imports, so the host compiles.
24+
25+| Mechanism | Gives Jibb | Limit |
26+| --- | --- | --- |
27+| [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) | `%jibb` stores text, merged by line | A conversion can't scry, so can't follow imports |
28+| Clay | The host reads each desk's `/ui`, static files included, and hears of commits | Every version of every file is kept |
29+| Gall | One host agent serves every Jibb desk | Agent names are ship-wide |
30+| [Eyre `%set-response`](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139) | Responses without an Arvo event | Keyed by the full request target; entries outlive their agent; auth-flagged entries give non-owners a bare 403 |
31+| [Eyre noun channels](https://github.com/urbit/urbit/pull/6472) | Pokes and watches as nouns | Needs `jam`, `cue`, `@uw` and an event stream over `fetch` in the browser |
32+| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | `site+/path`: an agent-served site | One `site` or glob clause per desk |
33+
34+## Prior art
35+
36+| Project | What it is | Jibb takes |
37+| --- | --- | --- |
38+| [Sail](https://docs.urbit.org/hoon/sail) | Hoon's HTML markup | Rendering in the agent |
39+| [Udon](https://docs.urbit.org/hoon/udon) | Markdown-like source under its own `%udon` mark, compiled by `;>` | A source language under its own mark |
40+| [Rudder](https://github.com/Fang-/suite) | Routing for Sail pages | Page routing |
41+| [Mast](https://github.com/R-JG/mast) | Display state in the agent, HTML diffs over a channel | One generic client script; channels as transport |
42+| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | One agent serving every desk's front end | One shared host |
43+| [Elm architecture](https://guide.elm-lang.org/architecture/) | `Model`, `Msg`, `update`, `view`, [effects](https://guide.elm-lang.org/effects/), [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html) | The program model, effects as data, keyed diffing in `each` |
44+| [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) | Layout without HTML or CSS | Rows, columns, `el`, wrapping, paragraphs, padding, fill |
45+| [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/) | `Html Never` content kept out of the bundle | `(element !!)` |
46+| [Astro islands](https://docs.astro.build/en/concepts/islands/) | HTML by default; marked components hydrate | Islands |
47+| [Qwik resumability](https://qwik.dev/docs/concepts/resumable/) | State serialised into HTML; the client resumes | A jammed model; `init` never runs in the browser |
48+| [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js) | Nouns and aura printing in JavaScript; aura-js lacks `@dr` | References for Jibb's own codec |
49+
50+## Architecture
51+
52+Clay stores files; each desk's compiler, run by the shared host, places each module at its kind's stage; only island and store code reaches the browser.
53+
54+&#91;embedded content: Jibb pipeline · source to browser in three stages\]
55+
56+## The language
57+
58+Hoon's runes and literals in an indentation layout, over Hoon's nouns and molds, so pokes, facts and scries need no conversion. The **Grammar** tab defines it.
59+
60+### Importing Hoon types
61+
62+`/-` and `/%` translate each mold a module uses from `/sur` files and marks:
63+
64+| Hoon | Jibb |
65+| --- | --- |
66+| `$%`, `$?` | `$?` |
67+| `$@` | `$?` with an atom case |
68+| `$^` | `$?` if its cases differ as nouns, else an error where used |
69+| `$~` | Dropped |
70+| `map`, `set`, `qeu`, `jar`, `jug` | The same, by name, checked with `apt` on arrival |
71+| `mop` | `(mop K V)`, read with `on` |
72+| Other `$\|` | Its base type |
73+| `%0`, `%&`, `%\|` | Tags |
74+| Recursive molds, such as `json` | Recursive, matched by name |
75+| Cores, gates, vases, signed and float auras | An error where used |
76+
77+Unused molds are skipped, so unsupported constructs elsewhere in a `/sur` file are fine.
78+
79+## Prelude
80+
81+Hoon's standard library regrouped, written in Jibb and vendored as `/lib/jibb/prelude`; an island's bundle carries only what it uses. Only the prelude declares natives, Jibb's jets, and private helpers; Jibb-written gates run unjetted on the ship; *proposed*: list gates such as `turn` become natives that take gates (Core language). It also holds `eq`, `not`, `page`, `program`, `store`, `pure`, `island`, and the layout elements and molds.
82+
83+| Group | Hoon | Jibb |
84+| --- | --- | --- |
85+| Arithmetic | [1a](https://docs.urbit.org/hoon/stdlib/1a), [2g](https://docs.urbit.org/hoon/stdlib/2g) | Natives over aura variables: `add`, `sub`, `div`, `mod`, `gth`, `max`, `pow` |
86+| Units | [2a](https://docs.urbit.org/hoon/stdlib/2a) | `fall`, `need`, `bind`, `biff` |
87+| Lists | [2b](https://docs.urbit.org/hoon/stdlib/2b) | `turn`, `weld`, `snag`, `lent`, `sort`, `skim`; `(roll xs init f)` takes a start |
88+| Maps, sets, queues | [2h](https://docs.urbit.org/hoon/stdlib/2h)–[2m](https://docs.urbit.org/hoon/stdlib/2m), [2o](https://docs.urbit.org/hoon/stdlib/2o) | Plain gates over Hoon's trees, such as `(get.by m k)`; `rep.by` and `rep.in` take a start; ordered maps through lull's `on`, as `(get.on lte m k)` |
89+| Function helpers | [2n](https://docs.urbit.org/hoon/stdlib/2n) | `same`, `head`, `tail`, `cork`, `cury` |
90+| Text | [4b](https://docs.urbit.org/hoon/stdlib/4b) | `crip`, `trip`, `cass`, `cuss`, `trim`, `tufa`, `tuba` |
91+| Atom printing | [4a](https://docs.urbit.org/hoon/stdlib/4a), [4j](https://docs.urbit.org/hoon/stdlib/4j)–[4m](https://docs.urbit.org/hoon/stdlib/4m) | `scot`, `scow`, `slav`, `slaw`. `print-date` prints Urbit-style in the theme's zone, to the minute, as `~2026.10.29..14.30`, from a zone table jibb-dev keeps current. Numbers print as Hoon does, as `1.000` |
92+| Time | [3c](https://docs.urbit.org/hoon/stdlib/3c) | `yore`, `year` |
93+| Data molds | [1c](https://docs.urbit.org/hoon/stdlib/1c), [2q](https://docs.urbit.org/hoon/stdlib/2q), `coin` and `dime` from [3g](https://docs.urbit.org/hoon/stdlib/3g) | `unit`, `list`, `pair`, `tape`, `path` and the rest |
94+| Bits | [2c](https://docs.urbit.org/hoon/stdlib/2c), [2d](https://docs.urbit.org/hoon/stdlib/2d) | Natives; `met`, `cut`, `rap`, `rip` exposed. Jibb's `not` negates a loobean |
95+| Hashing and ordering | [2e](https://docs.urbit.org/hoon/stdlib/2e), [2f](https://docs.urbit.org/hoon/stdlib/2f) | Natives behind maps and sets; `aor` exposed |
96+| Signed and floats | [3a](https://docs.urbit.org/hoon/stdlib/3a), [3b](https://docs.urbit.org/hoon/stdlib/3b) | Left out; `@s…` and `@r…` auras are errors, imported ones included |
97+| Parsers | [4d](https://docs.urbit.org/hoon/stdlib/4d)–[4i](https://docs.urbit.org/hoon/stdlib/4i) | Left out |
98+| The rest | [1b](https://docs.urbit.org/hoon/stdlib/1b), [2p](https://docs.urbit.org/hoon/stdlib/2p), [3d](https://docs.urbit.org/hoon/stdlib/3d), [3f](https://docs.urbit.org/hoon/stdlib/3f), [4c](https://docs.urbit.org/hoon/stdlib/4c), [4n](https://docs.urbit.org/hoon/stdlib/4n), [4o](https://docs.urbit.org/hoon/stdlib/4o), [5a](https://docs.urbit.org/hoon/stdlib/5a)–[5f](https://docs.urbit.org/hoon/stdlib/5f) | Left out; `jam`, `cue` and `fein` run inside the runtime |
99+
100+Maps are Hoon's trees on both sides, so equal maps are equal nouns. Requests check incoming maps and sets with `apt`.
101+
102+## The Elm architecture on Urbit
103+
104+A module's kind is the type of its `main`:
105+
106+| Kind | `main` | Runs | Ships JavaScript |
107+| --- | --- | --- | --- |
108+| Page | `(element !!)` | At commit | Only its islands' |
109+| Page with data | `(page load view)` | In the host, per request | Only its islands' |
110+| Island | `(program init update view subs)` | `init` and the first view with its page; the rest in the browser | Yes |
111+| Store | `(store init update)` | In the browser, shared by a page's islands | With its islands |
112+
113+A module without `main` is a library. The host reads a few other names per file kind, listed in the Grammar tab. Pages embed islands as `island main.todo-list ts`, an `(element !!)`:
114+
115+- `main.todo-list` is a record of exactly `init`, `update`, `view` and `subs`, whose types fix the data, model and messages. `subs` is required, as in Elm's `Browser.element`; `|=(m ~)` means none.
116+- `ts` is the island's data, checked against `init`'s sample.
117+- `(element !!)` means no messages reach the page. It also keeps islands out of islands, since an island's view is an `(element M)`.
118+
119+`init` runs on the ship at the page's stage, and the first view renders into the page with the model jammed in. On start, the browser runs `view` on that model, binds the result to the server's nodes by key, and replaces the subtree if they differ. Island data and models can't hold gates; the checker names the field.
120+
121+**Loading.** A page with data renders in the host per request, from a fresh scry.
122+
123+- `load`, `page`'s first argument, maps route parameters to one `ask`: `++ load |=(~ (todos.todo same))` without parameters.
124+- The host scries the ask's path per request, so the agent must answer a peek there; nothing is watched or kept. A path case can name a separate scry path.
125+- Pages read full state only; folds matter only to islands.
126+- Route parameters are `load`'s sample, bound by name and parsed by aura: `/ui/blog/_slug.jibb` has `++ load |=([slug=@ta] (post.api slug same))`.
127+- `(pure x)` is an ask without an agent; an app's catch-all page passes its path with it.
128+- A rejected path is a 404.
129+
130+Loads and renders run in Nock.
131+
132+**Access.** Every page is private: the host serves it per request, only to the owner, and sends anyone else to `/~/login?redirect=…`. Public pages are left for a later version.
133+
134+**Errors.** A rejected path, an unparseable parameter or a crashing render gets Jibb's error page, 404 or 500; no error is cached. A control rendered outside an island is rejected: at commit for pages without data, per request otherwise. There's no step limit yet, so an endless render stalls the ship, as in Hoon.
135+
136+- A crashing island shows the standard crash notice: in the page's HTML for `init` or the first view. In the browser, a crash in `update`, `view` or `subs` drops the message and keeps the model, DOM and watches.
137+
138+**Effects.** Commands and subscriptions are library values:
139+
140+```jibb
141++$ cmd |$ [M] $?
142+ [%poke p=poke reply=$-((unit @t) M)]
143+ [%go url=@t]
144+ [%wait gap=@dr msg=M]
145+ [%now reply=$-(@da M)]
146+ [%who reply=$-(@p M)]
147++$ sub |$ [M] $?
148+ [%watch a=(ask M)]
149+ [%every gap=@dr reply=$-(@da M)]
150+ [%url reply=$-(path M)]
151+```
152+
153+- `poke` and `ask` are opaque, built only by derived gates and `pure`. A constructor such as `(todo-action.todo [%add draft.m])` checks its value and supplies the agent and mark, so one island can poke several agents. The agent's mark rechecks on the ship; a nack's `tang` reaches the reply as text. Store constructors poke the store in the browser.
154+- A request such as `(todos.todo |=%got)` comes from a declared path or a store's `state`; its reply always gets full state. A path with diffs also takes a fold, `(changes.todo |=%got apply)`, which the runtime applies first. Facts are checked; a bad one crashes the reply, which counts as a crashed `update`.
155+- Islands read agents only by watching. A watch's first fact must be full state, so islands can't watch paths that need a separate scry path. The runtime acks facts, rewatches after a kick or a dropped connection and reseeds from the first fact; a refused watch shows the crash notice.
156+- Watches match by agent and path, timers by gap, and the newest reply is always used. After each `update`, `subs` is diffed to start and stop watches and timers. `%now` and `%every` give the time, `%who` the owner's `@p` from `/~/name`; `%wait` is a one-shot delay.
157+
158+Inputs send a message per `input` event, and one per IME composition, so validation runs as you type; only commands reach the ship. After every event, controls reset to the latest view, so a toggle whose poke fails unchecks. A field's text is rewritten only when it differs, which keeps the caret. Fields are read-only until their island starts.
159+
160+**Island lifecycle.**
161+
162+- `init` runs on the ship with the page's data and returns only the model, jammed into the page; the browser resumes from it at start. A reload starts from the model the page is served with.
163+- A crash in `update` drops the message, keeps the model and shows the standard notice.
164+- Stores share state between a page's islands. A store module, written like an API file, has `state`, `+$ poke` and `++ main (store init update)`, where `update` applies a poke. Islands import it with `/=` and use its derived gates: a constructor per poke type, and `(state.cart |=%got)`, whose first fact is the current state and each later fact the next.
165+- A store runs in the browser from its `init` when its first island starts, and lasts as long as the page. Islands keep what they watch in their own models.
166+- An app is a page whose view is one island under a catch-all route; its `load` passes the path with `pure`. The island's `%url` subscription delivers the path at start and after every change, back and forward included. `go` pushes history, and `go` or `link` within the section changes the island without a reload. Focus then moves to the new view's first heading, which screen readers announce.
167+
168+## Layout and styling
169+
170+Elements are ordinary gates with their own signatures. Containers take attribute lists, and `el` adds attributes to anything. Spaces and colours are theme tokens. Text elements come from `text`, as `(text "hi")`. The attribute union is `attr`, so helpers can declare `^- (list attr)`.
171+
172+| Element | Arguments | Purpose |
173+| --- | --- | --- |
174+| `row`, `column`, `wrap` | `attrs kids` | Containers |
175+| `stack` | `attrs kids` | A column, then a row from the `wide` breakpoint up |
176+| `el` | `attrs kid` | Adds attributes; `el`'s values win |
177+| `para` | `kids` | Flowing text |
178+| `text` | A cord | Text |
179+| `heading` | `level words` | `<h1>` to `<h6>` |
180+| `each` | `(list [key=* el=(element M)])` | Keyed children; repeated keys match in order |
181+| `table` | Columns `(list [header=@t width=length align=align])`, rows `(list [key=* cells=(list (element M))])` | A table with keyed rows |
182+| `image` | `src description`, with `src` an application path to a static image or `[url=@t width=@ud height=@ud]` | Boxed to its aspect ratio before loading, never wider than its container; static images are sized from the file; `""` marks it decorative |
183+| `link` | `url label` | See Links and URLs |
184+| `button` | `label msg` | A control |
185+| `toggle` | `on label msg` | A control |
186+| `field` | `label value reply` | A control; `reply` turns its text into a message, as `\|=%typed` |
187+| `form` | `msg kids` | A control; Enter in a field sends the nearest form's `msg` |
188+| `island` | `program data` | Embeds an island |
189+
190+Controls work only in islands; the host rejects a page that renders one outside an island.
191+
192+| Attribute | Values |
193+| --- | --- |
194+| `pad`, `gap` | `%s0` to `%s3`; `gap` on containers only |
195+| `width` | `[%chars n]`, `[%fill n]`, `%shrink`; `[%fill n]` below 1 counts as 1 |
196+| `align` | `%start`, `%center`, `%end` |
197+| `role` | `%body`, `%caption`, `%label` |
198+| `tone` | `%normal`, `%muted`, `%accent`, `%danger`; danger is also marked `!` and announced |
199+
200+The same attribute twice in a literal list is an error; otherwise the last wins.
201+
202+- Rows lay out left to right, centred vertically; columns top to bottom. Children take their content's width unless they fill.
203+- `[%fill n]` shares leftover space in proportion to `n`, `%shrink` takes the content's width, and `[%chars n]` is `n` widths of `0`.
204+- Empty elements take no room and no gap.
205+- Nothing exceeds the page's width: long words break, and wide content scrolls in its own box.
206+
207+The theme, `++ theme` in `/ui/theme.jibb`, uses the prelude's `theme` mold, with a default for desks without one. It requires `s1` to `s3` (`s0` is zero), five tones, with `normal` the text and `background` the page colour, the page's language, and the time zone dates print in. `wide` is the breakpoint where `stack` becomes a row. Dark tones go under `dark`, used when the reader prefers dark. An optional `font` is an application path to a static font. Tokens become CSS custom properties and `wide` a media query; values must be CSS lengths or colours, and each tone needs 4.5:1 contrast with its background.
208+
209+```jibb
210+++ theme ^- theme
211+ [ space=[s1="4px" s2="8px" s3="16px"]
212+ tones=[normal="#222" background="#fff" accent="#17734f" danger="#b3261e" muted="#5f5f5f"]
213+ wide="60rem"
214+ lang="en"
215+ zone="UTC"
216+ font=`%/static/fonts/inter.woff2
217+ dark=[tones=[normal="#eee" background="#111" accent="#4c9" danger="#f28b82" muted="#aaa"]]
218+```
219+
220+Style attributes compile to atomic classes in one stylesheet built from the theme, so islands can't compute a missing class. `[%chars n]` and `[%fill n]` become inline styles.
221+
222+### Links and URLs
223+
224+Both backends escape all text and attributes. In-app links are application paths, `%/blog` or `%/blog/(slug.p)`, checked at compile time against routes and static files; parameters print by aura, and the base path is implicit. They carry no query or fragment, so apps keep their state in the path.
225+
226+`link`, `image` and `go` also take two kinds of unchecked cord: a root-relative path to another app on the ship, such as `"/apps/groups"`, and an `https`, `mailto` or `tel` URL. A cord starting `//` or `/\` is neither. Any other cord, such as `javascript:`, is drawn as text, and `go` ignores it. External links open in a new tab with `noopener`. Pages send a Content-Security-Policy allowing scripts only from the ship.
227+
228+## Stages
229+
230+A module's kind fixes where it runs; nothing infers stages.
231+
232+| Stage | Runs | Where | Output |
233+| --- | --- | --- | --- |
234+| Build | Pages without data | The host, per commit | HTML and the stylesheet |
235+| Publish | Pages with data | The host, in Nock, per request | HTML |
236+| Client | Islands' `update`, `subs` and `view`; stores | The browser, in JavaScript | DOM patches from a keyed diff |
237+
238+- **Islands follow their page**: `init` and the first view run wherever the page renders, and the model is jammed into the HTML.
239+- **Re-render, then diff**: each message that changes the model re-runs `view`, and a virtual-DOM diff patches the DOM. Children of `each` are keyed, as in Elm's `Html.Keyed`, so focus survives reordering.
240+- **Functions have no stage**: a gate compiles for whichever side calls it.
241+- **No JavaScript needed**: pages render without it, and islands show their server-rendered state, with controls `aria-disabled` and fields read-only until they start.
242+- **Holes later**: compiling views to static HTML plus small functions of the model is an optimisation that changes no types or errors, so it waits.
243+
244+## The mark
245+
246+`%jibb` stores source as text and nothing else. Conversions can't follow imports, so the host compiles, and `|commit` never loads the compiler.
247+
248+### `/mar/jibb.hoon`
249+
250+```hoon
251+|_ src=@t
252+++ grab
253+ |%
254+ ++ mime |=([p=mite q=octs] q.q) :: from Unix on |commit
255+ ++ noun @t
256+ ++ txt of-wain:format
257+ --
258+++ grow
259+ |%
260+ ++ mime [/text/x-jibb (as-octs:mimes:html src)]
261+ ++ txt (to-wain:format src)
262+ ++ noun src
263+ --
264+++ grad %txt :: line diffs and merges, as %hoon does
265+--
266+```
267+
268+- Text, like `%hoon`: `|commit` never fails on a half-written file, and Clay merges by line.
269+- Compiled modules are cached by their hash, their imports' hashes, Hoon files included, and the desk's compiler, so a commit recompiles only what changed. Import cycles are errors.
270+- Relinking prints each error with its file, line, column and an excerpt.
271+- Each app desk vendors `jibb-dev`, as with `base-dev`: compiler, prelude, browser library, and the `%jibb` and static-file marks. It also needs base-dev's `txt`, `txt-diff`, `js`, `bill` and `kelvin` marks, docket's `docket-0`, and a `sys.kelvin`. The `%jibb` desk holds only the shared host and its threads.
272+
273+### Linking
274+
275+On every commit, the host reads each Jibb desk's `/ui`, and that desk's compiler rebuilds what changed and relinks, checking modules in dependency order and resolving `/-` and `/%` through Clay. There's no other build path: marks can't follow imports, and `/~` builds only `.hoon` files.
276+
277+The host is one Gall agent on the `%jibb` desk, serving every Jibb desk, so app agents stay plain Hoon; it never reads their state. A desk opts in by vendoring Jibb and naming its base path in docket's `site+`. The host is thin: it binds paths, watches desks, caches and serves. Each desk's Jibb builds and renders everything else, HTML shell, CSP and error pages included, through a few entry points fixed across releases, so desks upgrade independently. The host clears a desk's cache entries when it's suspended or uninstalled, and accepts pokes only from the ship.
278+
279+### Agent APIs
280+
281+Each agent Jibb talks to has `/ui/api/<agent>.jibb`, declaring its pokes in `+$ poke` and its paths in `+$ paths`, over types from the agent's own marks through `/%`. Importing it gives derived gates: a constructor per poke type, which pokes with that type's mark, and a request per path, which checks what comes back. Clashing derived names are errors.
282+
283+```jibb
284+:: /ui/api/todo.jibb
285+/- todo
286+/% %todo-action
287+/% %todos
288+/% %todo-update
289+/% %todo-item
290+
291++$ item item.todo
292++$ poke todo-action
293++$ paths $?
294+ [%todos /todos todos]
295+ [%changes /changes (facts todos todo-update)]
296+ [%todo /todo/[id=@ud] todo-item]
297+```
298+
299+A path case is a request name, a path and a type. A plain mold is full state, which a watch's first fact must carry. `(facts full diff …)` adds diffs, told apart by mark, and its request takes a fold, here `$-([[%todo-update todo-update] todos] todos)`. Pages scry the path, so the agent must answer a peek there. A case can name another scry path, as in `[%changes /updates (facts todos todo-update) scry=/todos]`, used only by pages.
300+
301+`poke` and `paths` are declarations, not molds. An API module re-exports its `/%` types, not `/-` faces or `/=` modules.
302+
303+Any mark name works: `/% %todo-action-1` gives the type `todo-action-1`, poked with `%todo-action-1`. Helper molds such as `item` come from the `/sur` file through `/-`. The marks are the agent author's:
304+
305+```hoon
306+:: /mar/todo-action.hoon, written with the agent
307+/- todo
308+|_ a=action:todo
309+++ grab |% ++ noun action:todo --
310+++ grow |% ++ noun a --
311+++ grad %noun
312+--
313+```
314+
315+Jibb calls this type `todo-action`, after its mark. Jibb's own types never cross to the ship, so they need no marks.
316+
317+### Routing by file path
318+
319+Pages route by their path under `/ui`; islands, stores and libraries are only imported.
320+
321+| File | URL |
322+| --- | --- |
323+| `/ui/index.jibb` | `/apps/my-app` |
324+| `/ui/blog/index.jibb` | `/apps/my-app/blog` |
325+| `/ui/blog/_slug.jibb` | `/apps/my-app/blog/{slug}`; `_` marks a parameter |
326+| `/ui/settings/__rest.jibb` | `/apps/my-app/settings` and below; `__` marks a catch-all, passed to `load` as a path |
327+
328+- One URL per page, with no trailing slash; other forms and query strings get the same page.
329+- A literal beats a parameter, which beats a catch-all. Two parameters at one level, `blog.jibb` beside `blog/index.jibb`, and a catch-all beside `index.jibb` are errors. Folders can be parameters: `/ui/_user/posts.jibb`.
330+- Parameters bind to `load`'s sample by name, which must have exactly those fields; a catch-all's is a `path`.
331+- `/apps/my-app/~jibb/` and `/apps/my-app/static/` belong to the host.
332+
333+Typed segments share one notation across routes, application paths and agent paths. `@ta` and `@tas` are raw text, checked as knots, as `/blog/my-post`; other auras print and parse as Hoon does, so `@ud` is `1.000`. The host splits the raw URL, so dots stay inside segments; a segment that doesn't parse is a 404. `%/todo/(id)` and `[%todo /todo/[id=@ud] todo-item]` print the same way.
334+
335+### Desk layout
336+
337+```
338+:: an app desk
339+/mar/jibb.hoon /mar/png.hoon … marks from jibb-dev
340+/lib/jibb.hoon /lib/jibb/prelude compiler and prelude from jibb-dev
341+/lib/jibb/runtime/js browser library from jibb-dev
342+/mar/txt.hoon /mar/js.hoon … marks from base-dev
343+/mar/docket-0.hoon docket's mark from landscape
344+/sys.kelvin the desk's kelvin
345+/sur/todo.hoon /mar/todo-*.hoon the %todo agent's types and marks
346+/app/my-app.hoon your agent, unchanged
347+/ui/api/todo.jibb the %todo agent's API
348+/ui/index.jibb /ui/blog/_slug.jibb pages
349+/ui/todo-list.jibb an island
350+/ui/stores/cart.jibb a store
351+/ui/theme.jibb theme
352+/ui/static/img/logo.png static files
353+/ui/tests/todo.jibb tests: each test- arm is one test
354+desk.bill %my-app
355+desk.docket-0 site+/apps/my-app, with docket's other clauses
356+
357+:: the %jibb desk, installed once per ship
358+/app/jibb.hoon the shared host
359+/ted/clear.hoon /ted/test.hoon -jibb!clear, -jibb!test
360+```
361+
362+The `%todo` and `%blog` agents run on their own desks; this desk copies their `/sur` and `/mar` files.
363+
364+## Client runtime and Eyre
365+
366+Islands compile to small JavaScript modules sharing one hand-written library; pages without islands load neither.
367+
368+- **Two backends.** Each module lowers to the core language (Core language), which both backends compile: Nock on the ship for rendering and `init`, with natives calling jets, and JavaScript in the browser for `update`, `subs` and `view`. Differential tests under Node, from jibb-dev, keep them in agreement.
369+- **Shared library**, under 20 KB: its own noun codec (`jam`, `cue`, `@uw`, treaps, aura printing), the channel client, a keyed DOM differ, stores and URL subscriptions. It reads events over `fetch`, since `EventSource` can't send `x-channel-format`, deletes its channel on `pagehide`, and opens a fresh one when a reconnect gets a 404.
370+
371+### What a page with an island looks like
372+
373+```html
374+<!doctype html>
375+<html lang="en">
376+<head>
377+ <meta charset="utf-8">
378+ <meta name="viewport" content="width=device-width, initial-scale=1">
379+ <title>My ship</title>
380+ <link rel="stylesheet" href="/apps/my-app/~jibb/s/0v1a2b3.css">
381+</head>
382+<body>
383+<main>
384+ … server-rendered HTML …
385+ <div data-jibb-island="0v4c5d6">
386+ … initial HTML …
387+ <script type="application/x-jibb-state">… jammed model, @uw-encoded …</script>
388+ </div>
389+</main>
390+<script type="module" src="/apps/my-app/~jibb/lib.0v7e8f9.js" async></script>
391+</body>
392+</html>
393+```
394+
395+The title is the first `heading 1`, and the language comes from the theme. `data-jibb-island` names the module by hash, so a shared island loads once; each instance keeps its state block in its own element. Every island starts as soon as the library loads.
396+
397+### Eyre integration
398+
399+- The host binds each desk's base path, from its `site+` clause, with `%connect`.
400+- The host serves every page itself, per request and only to the owner, sending anyone else to the login page; no page enters Eyre's cache.
401+- CSS, the library, island modules and static files sit at content-hashed, immutable paths, cached with `%set-response` and served without an Arvo event. They're public, since only pages carry data, and each build's files stay a day after replacement so open tabs keep working.
402+- A commit re-renders every page without data, which the host then serves from its copy.
403+- The cache is ship-wide and outlives its agent. The host caches only under each desk's base path and clears a desk's entries on suspend or uninstall; run `-jibb!clear` before uninstalling `%jibb`.
404+- Cache hits skip Eyre's other handling, such as HTTPS redirects and CORS.
405+
406+### Static files
407+
408+Images and fonts live under `/ui/static/`, committed with the desk, so installers get them; jibb-dev's marks store them as they are.
409+
410+- Pages refer to them by application path, as `(image %/static/img/logo.png "The logo")`. A missing file is a compile error, and images are sized from the file.
411+- Each file is served from Eyre's cache at a URL with a short hash of its bytes, such as `/apps/my-app/static/0v1.7rq2a/img/logo.png`, marked immutable; a changed file gets a new URL.
412+- Clay keeps every version, and cache hits ignore `Range`, so audio, video and large image sets stay out for now.
413+
414+## Worked examples
415+
416+One page per stage.
417+
418+### 1. Static: `/ui/about.jibb`
419+
420+```jibb
421+++ main view
422+
423+++ view
424+ column ~[width+[%fill 1] pad+%s3 gap+%s2] ~[
425+ heading 1 "About this ship"
426+ para ~[(text "Hosted on Urbit. This page ships no JavaScript.")]
427+ el ~[tone+%accent] (link %/blog "Read the blog")
428+```
429+
430+An `(element !!)`: build stage, one HTML file plus the stylesheet, rendered on commit, titled "About this ship".
431+
432+### 2. Published: `/ui/blog/index.jibb`
433+
434+```jibb
435+/- blog
436+/= api /ui/api/blog
437+/= card /ui/components/card
438+
439+++ main (page load view)
440+++ load |=(~ (posts.api same))
441+
442+++ view |= ps=(list post.blog)
443+ column ~[gap+%s2] (turn ps card.card)
444+```
445+
446+A `page`: each request scries `%blog` and renders in Nock.
447+
448+### 3. Interactive: `/ui/todo-list.jibb` and `/ui/index.jibb`
449+
450+```jibb
451+/= todo /ui/api/todo
452+
453+++ main (program init update view subs)
454+
455++$ model [todos=(list item.todo) draft=@t error=(unit @t)]
456++$ msg $?
457+ [%typed text=@t]
458+ %submit
459+ [%toggled id=@ud]
460+ [%got todos=(list item.todo)]
461+ [%acked err=(unit @t)]
462+
463+++ init |= ts=(list item.todo)
464+ [ts "" ~]
465+
466+++ update |= [=msg m=model]
467+ ?- msg
468+ [%typed t]
469+ ?- (gth (lent (tuba (trip t))) 80)
470+ %.y [m(draft=t error=`"Too long") ~]
471+ %.n [m(draft=t error=~) ~]
472+ %submit [m(draft="") ~[[%poke (todo-action.todo [%add draft.m]) |=%acked]]]
473+ [%toggled id] [m ~[[%poke (todo-action.todo [%toggle id]) |=%acked]]]
474+ [%got ts] [m(todos=ts) ~]
475+ [%acked err] [m(error=err) ~]
476+
477+++ subs |= m=model
478+ ~[[%watch (todos.todo |=%got)]]
479+
480+++ view |= m=model
481+ column ~[gap+%s2] ~[
482+ heading 2 "Todos"
483+ each (turn todos.m row-of)
484+ row ~[gap+%s1] ~[
485+ el ~[width+[%fill 1]] (field "New todo" draft.m |=%typed)
486+ button "Add" %submit
487+ el ~[tone+%danger] (text (fall error.m ""))
488+
489+++ row-of |= t=item.todo
490+ [id.t (toggle done.t text.t [%toggled id.t])]
491+```
492+
493+```jibb
494+/= todo /ui/api/todo
495+/= todo-list /ui/todo-list
496+
497+++ main (page load view)
498+++ load |=(~ (todos.todo same))
499+
500+++ view |= ts=(list item.todo)
501+ column ~[pad+%s3] ~[
502+ heading 1 "My ship"
503+ island main.todo-list ts
504+```
505+
506+| Part | Stage | Becomes |
507+| --- | --- | --- |
508+| The page's view, "My ship" | Publish | HTML from a fresh scry per request, served only to the owner |
509+| The island's `init` and first view | Publish, with its page | HTML in the page, with the jammed model |
510+| `update`, `subs`, `view` | Client | A JavaScript module, loaded with the page; model changes re-run `view` and patch the DOM |
511+| `[%add …]`, `[%toggle …]` | Ship | One poke each: an Arvo event per command, not per keystroke |
512+
513+### Opting in
514+
515+A desk becomes a Jibb app by vendoring jibb-dev and naming its base path in docket, here `site+/apps/my-app`. The host on the `%jibb` desk binds it, builds `/ui` with the desk's compiler and serves it. Install `%jibb` first; until then the tile opens Eyre's 404. The app's agent, `/app/my-app.hoon`, is unchanged.
516+
517+## Sources
518+
519+- [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) and [fas runes](https://docs.urbit.org/hoon/rune/fas), docs.urbit.org
520+- [Sail](https://docs.urbit.org/hoon/sail) and [Udon](https://docs.urbit.org/hoon/udon), docs.urbit.org
521+- [Docket files](https://docs.urbit.org/build-on-urbit/userspace/dist/docket), docs.urbit.org
522+- [urbit-os v2.139 release: Eyre cache and noun channels](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139); [eyre: add cache, PR #5927](https://github.com/urbit/urbit/pull/5927); [eyre: noun channels, PR #6472](https://github.com/urbit/urbit/pull/6472)
523+- [Publication caches for Eyre](https://gist.github.com/Fang-/1939a5b51fb2f821d960e05b74ad5f9f), an earlier design proposal; Eyre shipped `%set-response` instead
524+- [Mast](https://github.com/R-JG/mast), [Fang-/suite (Rudder)](https://github.com/Fang-/suite)
525+- [Elm architecture](https://guide.elm-lang.org/architecture/), [effects](https://guide.elm-lang.org/effects/) and [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html); [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/)
526+- [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/), [Astro islands](https://docs.astro.build/en/concepts/islands/), [Qwik resumability](https://qwik.dev/docs/concepts/resumable/)
527+- [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js)
docs/status.mdnew file+71
@@ -0,0 +1,71 @@
1+# Status
2+
3+As of 2026-10-30, at commit ab797a9. The plan is [spec/implementation-plan.md](spec/implementation-plan.md); each milestone ends with its checks passing on a fake ship.
4+
5+| # | Milestone | Status |
6+| --- | --- | --- |
7+| 1 | Parser and mark | Done (7c739b7; the doc's grammar decisions of 2026-10-30 in ab797a9) |
8+| 2 | Types and prelude | Next. [prompts/milestone-2.md](prompts/milestone-2.md) briefs the developer |
9+| 3 | Core language | Not started |
10+| 4 | Host and static pages: the first release | Not started |
11+| 5 | Published pages | Not started |
12+| 6 | Islands | Not started |
13+| 7 | Tooling | Not started |
14+| 8 | Polish | Not started |
15+
16+The plan sets no dates: "agree them after milestone 1, once it shows how fast the Hoon work goes". They haven't been agreed yet. Milestone 1's commits run from 2026-10-29 18:13 UTC to 2026-10-30 11:50 UTC.
17+
18+## Milestone 1: parser and mark
19+
20+What the plan asks for, and where it is:
21+
22+| The plan | Where |
23+| --- | --- |
24+| The `%jibb` mark as the first tab gives it | `jibb-dev/mar/jibb.hoon`, as the doc writes it |
25+| The layout pass, emitting INDENT, DEDENT and NEWLINE | `+layout` in `jibb-dev/lib/jibb.hoon`, designed in its section 2 |
26+| A parser for the Grammar tab's EBNF that returns a syntax tree with source positions | `+parse` and `+parse-tokens`, section 3. Every node has a span, its start and end, and `+check-spans` tests the spans of every tree the golden tests print |
27+| Errors with file, line, column and an excerpt | `+show-error`, section 4.3. Each error points at the first character where the text stops being the start of any valid module (section 2.0) |
28+| Done when every Jibb code block in the doc parses | The doc labels its 14 Jibb blocks `jibb`. Each is a golden case, `tests/golden/doc-*`, and `tools/doc-blocks docs/spec` checks that they match. All parse; the one block written as an error, the Grammar's buttons example, has a type error for milestone 2 |
29+| Done when each lexical rule has a test input that breaks it | The `lex-*` golden cases: for each lexical rule, inputs that break it (`-bad-`), each with the error's expected place, and inputs that keep it (`-ok-`), except for the forms Jibb leaves out (the closers, blob and `@c` literals) |
30+| Done when `\|commit` stores `.jibb` files on a fake ship | `tools/ship check-commit`: 7 checks, from reading every file back byte for byte to three-way merges |
31+
32+Besides the plan's list:
33+
34+- 344 golden cases: 152 that parse and 192 that fail, each failure at a stated line and column. Each case's `about.txt` quotes the doc's words it tests.
35+- They pass under `urbit eval` (`tools/golden`, about 9 s for the corpus) and as `-test` arms on the fake ship (`tools/ship test`).
36+- `tools/testdata/selftest` (18 checks) and `tools/testdata/ship-selftest` test the tools themselves.
37+- Two independent audits before 7c739b7, one of which compared the library with a reference parser written in Python from the doc over about 19,000 generated programs. Before ab797a9, a second Python parser, written from the decisions, agreed with the library on 8,000 generated programs and 900 hand-written probes.
38+- Speed: starting `urbit eval` and building the library takes about 8 s. After that, a 2,000-line module of 400 definitions parses in about a second. Loops over definitions, arms, lines and items grow no stack; nesting costs a few stack frames a level, and 100,000 nested brackets still parse (library section 3).
39+
40+### History
41+
42+| Commit | Date | What |
43+| --- | --- | --- |
44+| 1cb51df | 2026-10-29 | Foundation: the parser's design, the tools, the desks, the vendored base-dev and landscape files, the golden corpus |
45+| 15a5545 | 2026-10-29 | The layout pass |
46+| 3c5f50a | 2026-10-29 | The parser |
47+| 7c739b7 | 2026-10-30 | Fixes from two independent audits; milestone 1 done |
48+| 5a4aa73 | 2026-10-30 | `tools/doc-blocks`, after the doc labelled its Jibb blocks |
49+| ab797a9 | 2026-10-30 | The 12 grammar decisions of 2026-10-30: four changed the parser, eight confirmed it |
50+
51+## Checking it
52+
53+On a machine set up as [development.md](development.md) says:
54+
55+```
56+tools/golden # 344 cases under urbit eval
57+tools/hoon check-spans-test # prints <<>> when the span checker works
58+tools/testdata/selftest # the tools themselves
59+tools/doc-blocks docs/spec # the doc's examples are golden cases
60+tools/ship start
61+tools/ship gen-tests --check # tests/lib/jibb.hoon is up to date
62+tools/ship test # the same 344 cases as -test arms on the ship
63+tools/testdata/ship-selftest
64+tools/ship check-commit # |commit stores .jibb files
65+```
66+
67+## What milestone 1 leaves to later milestones
68+
69+- Errors carry a start only. Spans keep each node's end so that milestone 2's type errors can underline both conflicting sites with excerpts (library section 1).
70+- The parser accepts what only the checker can judge: `[]` and `[a]`, `m()`, `!! a`, and the module kinds. The Grammar's buttons example parses, though the doc marks it as a type error.
71+- Grammar questions the doc's author hasn't been asked follow the readings in [open-questions.md](open-questions.md).