Design decisions
Why Synod is the way it is. Every decision holds on its own scope; none overrides another. Add an entry whenever an implementation has to decide something the spec did not.
| # | Decision | Why |
|---|---|---|
| 1 | The design principles hold together; a design that cannot satisfy all of them is changed | No principle is traded against another, so rules never contradict |
| 2 | One spelling per meaning; the formatter enforces it | Repos stay uniform; LLM output needs no style review |
| 3 | The language is a fixed set of words and forms; everything else, the standard library included, is phrases | A tiny kernel; the language grows by packages, not by keywords |
| 4 | An inline hole takes exactly one item and a block hole one block; nested sentences are parenthesized | Exact matching, no precedence, no ambiguity from free-form sentences |
| 5 | Names from another package's namespace (its phrases, record types, Node and node fields) are always qualified; the current package's never are (Go) |
You can always see where a name comes from |
| 6 | Capitalization means exported, nothing else | One meaning per signal |
| 7 | use … as only on a last-segment collision: two imports of a file share one, or an import's last segment is the importing package's own name |
No second spelling of a package; a package raft can still import another raft |
| 8 | Node code names its node with a receiver, a node-typed hole before the head: define [n: Node] Version, when [n: Node] (…), called n.Version; only node types are receivers |
No implicit current node and no hidden state; every access shows whose state it is (Go receivers) |
| 9 | result gives a phrase's value |
Packages can define value helpers, not just natives |
| 10 | Collections iterate like Go's range; unused locals and each names are errors |
Familiar, and only one loop form compiles |
| 11 | One number type, plus time with units |
Fewer primitives; units stop after 5 being ambiguous |
| 12 | Node state, messages and events are declared with has; messages and events are immutable records |
One declaration form for all three |
| 13 | {} is the one empty literal for lists and maps; its type comes from its use, and a {} whose type cannot be found is an ambiguous-empty error |
One empty literal, never a guess |
| 14 | emit lets packages add trace facts |
Package concepts (leader elected) can animate richly |
| 15 | Behaviour is simulated, never narrated by an LLM | Faithful learning; Break it and Compare need real models |
| 16 | Two clocks: simulated and presentation | A 1ms hop can still animate over 400ms |
| 17 | Observers are pure | A look can never change what happens |
| 18 | Node code reads and writes only its receiver's state; it reaches other nodes with net.Send |
Realism: nodes only learn through messages |
| 19 | Node effects name their node: net.Send m from n to p, disk.Write v at k on n, emit e by n, spelled like the trace facts they record |
Nothing acts for a node it does not name |
| 20 | One motion curve, no easing parameter | Every package moves alike |
| 21 | Packages from Git URLs only, no registry | Go's model; nothing central to run |
| 22 | A folder holds one library package and at most one main file (# package main, with define main); importing the folder never includes the main file |
A library ships its demo next to it; libraries and programs never mix in one package |
| 23 | Behaviour runs on nodes by "running" a package; running is transitive over use |
Roles (client, cache, db) and composition (Cassandra = gossip + ring + …) with no extra keyword |
| 24 | deliver is main-only; it injects outside requests |
Clean boundary between the world and the cluster |
| 25 | is not, at least etc. exist, and the formatter rewrites not (…) into them |
Readable operators without two spellings |
| 26 | Compiler, simulator and scene builder are written in Go and built natively and to WebAssembly; one JavaScript canvas library, player, draws scenes in the Mac app and on the site |
One implementation of each stage, everywhere; the app stays a thin host |
| 27 | Node fields, phrases, record types and Node share one namespace per package; record fields belong to their record type, so they are never package-qualified |
n.Known and n.Version never collide; several messages can all have a term field; a record's type already names its package |
| 28 | sim.Nobody is the only "no value" |
No general null; the one case that needs it (nodes) is explicit |
| 29 | A zero-hole phrase is one item, written bare: time.Now, theme.Accent, n.Version |
They read like names |
| 30 | when bindings and hole parameters, receivers included, may go unused |
Like Go parameters; no _ placeholder needed |
| 31 | Importing a package that has observers or invariants uses it | Looks are added by import, like Go's side-effect imports, without a _ form |
| 32 | Node state is initialized when the node is created; start runs as the node's first event and again on every restart |
main can adjust state (a server in LISTEN) before the node starts |
| 33 | Entry points are definitions: define main and define [n: Node] start; main and start are reserved for them; a file root holds only declarations |
One definition form; nothing runs implicitly at file root |
| 34 | . reaches named members only: package members, record fields, node fields, receiver phrases; elements are item … of, attached shapes name of v |
One meaning for ., one element form |
| 35 | Collection types name their element types, list of T and map of K to V, and are checked at compile time |
Holes say exactly what they take; no run-time element errors |
| 36 | node is any node, identity only; every library package has a node type Node (p.Node outside) with its fields; a p.Node is accepted as q.Node when running p runs q, and as node |
Fields are reachable only where the type proves they exist |
| 37 | A when event phrase may bind a node with a type, [n: raft.Node], and then matches only such nodes |
Observers read the fields of the nodes they draw |
| 38 | lists.Append adds one item; lists.Extend joins two lists |
Two meanings, two phrases; no overloading |
| 39 | Commas stand only between list items, between message … with settings and between the two each names; a comma anywhere else is a syntax error |
Every comma has one job, so a stray comma is caught at parse time |
| 40 | A continuation line (one word with a block, at its statement's indentation, directly after the statement's block) continues that statement; else is one |
One rule builds if … else and every multi-block phrase |
| 41 | net.DropNext names the record type as text: net.DropNext "Accept" from a to b |
One spelling in the catalog, the spec and Break it |
| 42 | The organization site is synodlang.github.io: static, with examples edited and re-run in the browser by synod.wasm |
GitHub Pages for the organization; no server; interactive examples replace recordings |
| 43 | The header is # package <word> on the first non-blank line; with imports, one blank line follows the last import; without, the first declaration follows the header directly |
One layout per file; the header and imports read as the file's preamble |
| 44 | Comments attach to lines (the next line, or the line they trail) and never form blocks or statements; a comment line's indentation is not checked | Comments cannot change structure; the formatter re-indents them |
| 45 | +1, .5, x-1 and spaces around . are syntax errors, not formatter fixes |
Each has no single obvious meaning to rewrite to, so the formatter does not guess |
| 46 | CR LF line endings and a byte order mark are non-canonical; synod fmt fixes them |
They are harmless to read, so they are repaired, not rejected |
| 47 | Two atoms (words, members, numbers, durations, texts, colors) are always separated by whitespace | x-1 is neither x minus 1 nor the number -1 after x; no silent guess |
| 48 | Imports are sorted by the byte order of the full path text, @version included; a use below a declaration is moved up |
A total order that needs no locale or parsing; one place for imports |
| 49 | Blank and comment lines may sit between a block and its continuation line; the formatter drops the blanks and keeps the comments before the continuation | A comment never breaks if … else |
| 50 | The formatter rewrites not (c) into the direct operator both ways, is not into is included; not (a and b) stays; a pattern whose head is directly followed by a comparison operator is a bad-pattern error |
One spelling per meaning in both directions; such a pattern would read as a comparison |
| 51 | The formatter removes parentheses but never inserts them; a missing pair is an unknown-phrase error |
Wrapping would guess where an argument ends |
| 52 | A sentence's phrase is chosen by its shape (item count, literal words, block structure); types never select between phrases, and a type failure is type-mismatch |
One reading per sentence; no overloading by type, and errors name the phrase you meant |
| 53 | result stands only where the phrase is still running: not in an after block or a block argument |
Those blocks run after the phrase has returned, so there is nothing to return to |
| 54 | A local from outside an after block or block argument cannot be set inside it |
The block holds a copy, so the set would silently do nothing |
| 55 | A local created in a block is visible only to the end of that block | Go's scoping; no value depends on which branch ran |
| 56 | Look context reads only exported state, even of its own package | Looks see what other packages see, and the trace records only that |
| 57 | The Context column of the std catalog is enforced at every call of a std phrase, written in Synod or native | time.Every cannot leak into look context just because its body would allow it |
| 58 | draw … as w declares a shape local; drawing w again in the same scope rebinds it; a draw name may go unused |
Drawing is itself the effect, like decision 30; redrawing is not shadowing |
| 59 | A place's type is the join of its initial value and every assignment; unrelated node types do not join | One type per place, decided without guessing node |
| 60 | The compiler reads downloaded modules only from vendor/<module path>@<version>/ at the root of the main module, ignored by git; synod get fills it from a shared download store ~/.synod/cache/<module>@<version>/ (verified against synod.sum, never edited), downloading only what the store lacks; no override |
Dependencies live with the project and have one place to read; the store only saves repeated downloads, and synod.sum keeps fresh downloads reproducible |
| 61 | Every Synod repository shares MAJOR.MINOR with the language version and numbers its own PATCH: a dsl patch is a clarification or spec bug fix; a patch elsewhere is a bug fix or a feature implementing more of the same language version; a new language feature raises MINOR, spec first |
One number says which language a release speaks; patches never change the language |
| 62 | The run's seed is --seed, else sim.Seed, else 1 |
A command-line seed can replay any program, including demos that call sim.Seed |
| 63 | A message flight is the native look.Flight … for m; it spans the presentation slots of net.Sent and its landing fact; net.Id gives the id carried by a message bound by a net fact; equality ignores the id |
Flights readable without look-ahead or animating in the past (rule 1 of spec/09 §9.6) |
| 64 | Deleting a shape is the unexported look.delete; look.Remove is the one public spelling; a deleted shape stays attached; every look phrase on it does nothing |
One spelling (decision 2); a late card of n never fails because a node left |
| 65 | For "x" and "y", look.Tween takes a number or a shape; looks never read coordinates |
Travel in Synod without exposing positions |
| 66 | Presentation time is whole ms and pace is in thousandths; every look duration (after, Tween, builder constants) is scaled by pace |
Integer arithmetic, byte-identical scenes everywhere |
| 67 | A tween on a property supersedes the part of a running tween on that property after its start | One timeline per property, no queues |
| 68 | A run-time error in look context stops the scene, never the simulation; it is not recorded in the trace | Observers are pure (decision 17) |
| 69 | look.Place is a constraint: the shape follows its anchor until the shape itself is moved |
Rows and tags follow cards when layouts re-flow |
| 70 | synod.wasm exposes a synchronous global synod (check, run, format, version); results are the CLI's bytes as strings and it never throws (errors are {code, message}, exit code 2) |
Byte-identical to the CLI; exceptions cannot cross Go frames; a Web Worker keeps pages responsive |
| 71 | synod.wasm embeds only std; other modules come in files, under vendor/ as synod get lays them out; the site copies synod.wasm and its matching wasm_exec.js from a pinned release at build time |
One loader for disk and memory; examples stay editable; no CORS on release assets |
| 72 | In 0.2 every label is drawn in the player's sans at 16 px, weight 500, centered; size, weight and mono are not yet choosable and will be additive label props | The format has no text size or weight; adding props later breaks no reader (format rule: keys only added) |
| 73 | Readers draw scene.json by the fixed rules of implementation/04-formats.md (Drawing a scene); a frame test compares display lists, pixels are a secondary check |
The same scene looks the same on every engine, and determinism can be tested exactly |
| 74 | The Mac app runs a program with synod check --json --tokens <program> and, when it reports no error, synod run <program> --trace <file> --scene <file> --style short --color never, from the module root; the two files go to the app's cache folder, not to out/; a change re-runs after 300 ms and the newest run wins; the viewer keeps the current fact (same seq), else the current time; the editor's other checks (decisions 78 to 81) run no program |
Workspaces stay clean and are never clobbered; every run brings the tokens of its files (decision 78); it uses only the flags 05-cli.md documents, and the short form is the one the app parses |
| 75 | A video export of duration D ms at fps and rate r has N = ceil(D·fps/(1000·r)) + 1 frames; frame k shows at_k = min(k·r·1000/fps, D), has presentation time k/fps, and is the player's renderFrame(at_k) from an offscreen web view, moved as a lossless PNG; 4K is the 1080p layout at pixel ratio 2, letterboxed on Paper; video is BT.709; the rate defaults to the timeline speed |
One render path, so an exported frame equals the stage frame (decision 73); integer-exact timing with no dropped or doubled frames; the speed is the player rate, not the scene's pace (decision 66) |
| 76 | The Mac app bundles a pinned synod (darwin-universal release), a pinned player release and the examples at a pinned commit, listed in deps.env and verified by SHA256SUMS and pinned hashes; its releases are v0.2.x (decision 61), named Synod-<tag>-macos.zip with a SHA256SUMS, ad-hoc signed, not notarized |
Reproducible builds with known engines; notarization needs a Developer ID the project does not have yet (as for the binaries in 05-cli.md), so the README gives the Gatekeeper steps |
| 77 | Predict answers are stored outside the workspace, in ~/Library/Application Support/Synod/answers, keyed by the module root, the program and the question text |
Programs stay pure Synod source under Git (06-mac-app.md); the same question in another program or module has its own answer |
| 78 | The Mac app's editor gets highlighting, go-to-definition and diagnostics only from synod check --json --tokens; between two checks the token ranges move with the edits; the app has no lexer of its own |
One implementation of the lexer and the resolver (decision 26); the editor and the CLI never disagree about a word |
| 79 | --tokens lists every user file of the checked packages; a file that fails to lex or parse, and every other file of its package, gets all its tokens, or those before its first lexical error, with lexical roles only (language, value, comment, and name for any other word, member part or path) and no def |
Highlighting survives a typing mistake, and no role is guessed |
| 80 | synod check --overlay <file> (read by synod doc too, decision 92) reads {"format":"synod-overlay/1","replace":{path: path}} and checks each key's file as if it held its value's content; diagnostics and tokens keep the key's path and positions; only check and doc take it; a key that is not an existing .synod file or synod.mod, or an unreadable overlay, is exit 2 |
An editor checks unsaved text without writing into the workspace (decision 74), as Go's -overlay does |
| 81 | The editor checks unsaved buffers with --overlay 300 ms after the last edit, the overlay files living in the app's cache folder, and never runs them; saving writes the file atomically and then runs synod fmt <file>, so every run reads saved, canonical files; a help edit is applied only when the user picks that fix, to its own file's buffer, as one undoable change |
Runs stay reproducible from disk and Git (decision 74); a fix never surprises the user, and fmt is the only formatter (decision 2) |
| 82 | lesson.Ask [q: text] choosing [c: list of text] asks with 2 to 6 distinct, non-empty choices (else bad-argument); lesson.Asked gains the exported field Choices, {} for lesson.Ask [q: text]; a pause marker carries choices when they are not empty; the app offers exactly those, stores the pick with the answer (decision 77) and grades nothing |
The choices are part of the lesson, written next to the question, and the run shows what happens (decision 15); choosing {} is an error, so plain Ask stays the one spelling of an open question (decision 2); a gap of 0.2 fixed in a patch like G14 (decision 61) |
| 83 | A shape attached to a node (draw … as w for n) carries node, the node's name, and a shape flown by look.Flight … for m carries msg, the message id, each from the first such attachment or flight; the scene builder writes them, so any look gets them; key order is id, kind, node, msg, props, keys, end; drawing ignores both |
Cards and dots of every look become clickable with no new phrase (G15, G17); looks stay pure (decision 17); keys are only added |
| 84 | hitTest(x, y) returns the topmost drawn circle or rect within 4 CSS px of the point whose own or nearest enclosing group's node or msg is set; filled shapes hit inside, unfilled stroked ones near their outline; labels, lines and shapes below world opacity 0.05 never hit |
The same answer on every engine, since it needs no font metrics (decision 73); decorative shapes never steal a click |
| 85 | Break it appends one after <T> block with one statement to define main of the main file, naming nodes as (item i of sim.Nodes) with a trailing # <name>: Crash, Restart, Partition and Heal at the first whole millisecond after the current fact's time, and Drop this message as net.DropNext "<Type>" from … to … at that message's send time, offered only when no earlier message of that type on that link was sent at that instant; missing use lines are added; then the file is saved (formatted) and the program re-runs |
One readable line of ordinary Synod per click and no LLM (decision 15); sim.Nodes names any node without a guessed local; net.DropNext acts at send time (decision 41), so the send time is the one that singles the message out |
| 86 | After a Break it run the app checks that the action's fact is in the new trace and otherwise reports that it had no effect, with Undo; a rule broken in the new run but not in the previous one is shown at its simulated time and source line | The user sees what the change did, and a no-op is never silent |
| 87 | Compare shows two runs (program, seed and mode each) side by side on one timeline in simulated time; each side is at the presentation time of the piecewise-linear map through (0, 0), (t, the last slot of the facts at t) for every fact time t, and (end, duration); the left side drives; nodes are matched by name |
Runs with different seeds share simulated time, not presentation time; names, not draw-site ids, are what a viewer compares |
| 88 | A GIF export uses the schedule of decision 75 over a range [a, b] (the whole scene by default), N = ceil((b−a)·fps/(1000·r)) + 1 frames at a + k·r·1000/fps clamped to b; fps is 10, 20, 25 or 50, each frame's delay 100/fps hundredths; the width is 480, 640, 800 or 960 px at pixel ratio 1 with the scene's aspect, there are at most 6,000 frames, and the file loops forever; one global palette derived from the scene theme, each pixel its nearest entry in OKLab, no dithering |
Exact delays and no palette flicker; the frames come from renderFrame as in decision 75, so the render path is one (decision 73) |
| 89 | A run-time error raised while std code runs is reported at the position in user code of the call that entered std (the outermost std frame's caller); the std position is kept as a note of the rich and --json forms, never in the trace |
The user can act only on their own code and std is not theirs to edit, so the report gives the exact problem at the exact place they can change; the trace stays one position, as in spec/11 §11.3 |
| 90 | synod doc documents a package from its source as the compiler loads and checks it: each declaration's pattern as the compiler prints it, qualified as written outside the package, its result, kind, contexts (std only) and doc comment, which is the comment lines directly above the declaration, with no blank line between; records and Node fields with their types; --json is synod-doc/1; a package is named std/<p> (the std/ prefix is always std), by its folder, or by its import path |
The code is the one source of documentation; one printer of patterns; one spelling of a package name (decision 2) |
| 91 | Every std define has a doc comment equal to its catalog Meaning, every Meaning is non-empty, and Gives uses one spelling per rule (—, a type, same as `x`, item type of `x`, list of the key type of `x`); synod doc --catalog generates the agent catalog from std's source |
The generated catalog equals the normative one, checked by a test, never by hand |
| 92 | synod doc --json --scope <file> lists the names a file can write, spelled as that file must write them (own package bare, imports by qualifier or alias), and takes --overlay; the Mac app's phrase completion gets its items only from it, finds the word being typed as the run of letters, digits and . before the caret (a run of characters, not a token), and inserts a phrase's items with its holes as placeholders |
One implementation of names and spelling (decisions 5, 7, 26, 78); completion never guesses a qualifier |
| 93 | synod explain texts live in the compiler, one per spec/11 code, each with a minimal example and a fix that the compiler checks or runs at explain time; the diagnostic shown is produced then, never stored; the site's Errors pages and agent-skills errors.md are generated from synod explain |
One source; an example can never drift from the compiler |
| 94 | synod new <dir> refuses a non-empty folder and writes synod.mod (module path from --module or the folder's base name), .gitignore (vendor/, out/) and a package named after the path's last segment (a lower-case word that is not main and not the last element of a std package the template imports) with a library file and, unless --lib, a demo.synod; all pass fmt --check, check and run |
A new module starts canonical and runnable, and nothing is ever overwritten |
| 95 | synod get downloads a module with git from https:// + the first three path segments at tag <version> (<subfolder>/<version> for a module in a subfolder), writes each store entry once, atomically and read-only, records <module> <version> <commit> h1:<dirhash> in synod.sum on first download and verifies every store entry against it before copying; vendor/ holds copies of exactly the build list and synod.sum lists exactly it; a mismatch stops with exit 2; it needs no network when the store has every module |
Git URLs only (21); reproducible and verified (60); an edit in vendor/ can never reach the store |
| 96 | The Mac app's prompt bar runs the user's own Claude Code CLI only after the user turns it on in Settings; the app never asks for, stores or passes a credential of the user (its own editor tokens, decision 98, are not one): it gives the CLI an allowlisted environment, the module root as working folder, the bundled synod first on PATH, the bundled skill as system prompt and the tool list of decision 100, which is Read, Glob, Grep, the editor tools, the synod tools doc, explain, check and run_summary; dirty buffers are saved before sending, Claude's changes are undoable editor edits that it saves itself, and the result is run and revertible by the user |
Claude only ever writes Synod source (decision 15); credentials stay with Claude Code; everything else works offline; the app stays a thin host |
| 97 | agent-skills is a Claude Code plugin whose catalog, errors, canonical form, trace events, examples and AGENTS.md are generated from pinned dsl, synod and examples releases; its CI fails on any difference after regeneration and when a newer dsl, compiler or examples release exists; each dsl release is followed by an agent-skills release |
Regeneration is enforced, not remembered; any agent reads the same files |
| 98 | The Mac app's editor MCP server is reached only through the bundled stdio bridge synod-editor-mcp over a Unix socket in a 0700 folder of the user's temporary directory, after a handshake with a 64-hex token; it listens only while a prompt-bar request runs or while "Let Claude Code in Terminal use the editor" is on, a setting that is off at every launch; prompt-bar tokens live for one request and the external token for one app run, in memory only, rotated by Disconnect; logs hold tool, path and sizes, never text or tokens |
No network listener and no exposure to browsers or other users; nothing secret outlives the session; the user always knows when Claude can reach the editor |
| 99 | The editor tools (list_files, read_buffer, get_diagnostics, get_selection, apply_edits, replace_buffer, create_file, save_files, open_file) work on module-relative paths inside the module; reads see unsaved buffers; an edit needs the buffer's current version and lands as one undoable, unsaved change; of the tools only two write to disk, save_files (the editor's Save) for a buffer and create_file for a new, formatted file, which must exist on disk to be checked (decision 80); a run reads saved files (decision 81), so Claude saves before it runs; bundled examples are read-only |
Claude works in the same buffers as the user, every change can be undone, and disk writes go through the one Save path and the one creation path (decisions 81 and 2) |
| 100 | A prompt-bar request gives Claude the editor server and synod mcp through a 0600 config file deleted when the request ends; the prompt bar's built-in tools are restricted to Read, Glob and Grep (--tools), the allowed MCP tools are every editor tool and the synod tools doc, explain, check and run_summary, and Write, Edit, MultiEdit, NotebookEdit, WebFetch, WebSearch, Bash, Agent, Task and the synod tool fmt are disallowed; there is no shell, so synod get is the user's step |
Every edit is visible and undoable, formatting happens in Save, Claude needs no shell and downloads stay the user's action (decision 96 holds the opt-in and credential rule) |
| 101 | synod mcp serves the tools doc, explain, check, run_summary and fmt over stdio, each one exactly the CLI command it names, run in-process from the root; run_summary writes its trace and scene to a temporary folder it deletes; it has no network code |
Agents without a shell get the same answers as the CLI, from one implementation (decision 26) |
| 102 | Both MCP servers speak JSON-RPC 2.0, one message per line, with only initialize, notifications/initialized, ping, tools/list, tools/call and notifications/cancelled; they negotiate the protocol version from a fixed supported list |
A small, testable surface that any MCP client can use |
| 103 | The tutorial is dsl/tutorial/. A chapter NN-slug.md holds prose and <!-- step K program=<folder> --> directives, never step code. Step K is the complete folder code/NN-slug/stepK/: from chapter 4 a module with the reader's synod.mod, before it one folder holding a main file. Its golden is code/NN-slug/stepK.summary. A "Why it looks like this" program is code/NN-slug/why-<slug>/ with its expected diagnostic code in expect, and the diagnostic itself is produced at site build time |
What the reader types is what CI checks and what the site runs; a shown diagnostic can never drift (decision 93) |
| 104 | dsl/tutorial/coverage.md names one chapter for every item of the compiler's form catalog (the forms of spec/02, the types of spec/04 §4.1 and §4.2, the contexts of spec/05 §5.4 and the std packages) and may name chapters for std phrases. A row is named <kind>:<id> with the kind form, type, context, package or phrase, because ids repeat across kinds, and a package row's reference is std/README.md § std/<p>. A step uses only catalog items and std phrases named for its chapter or an earlier one. The catalog marks each item as shown or as taught in prose (form:define-native and type:value are taught in prose, because a reader's program cannot hold them), and every shown item whose row names chapter N is used in at least one step of chapter N. A compiler test checks the catalog against the spec text |
Bottom-up is enforced, not remembered; a new form fails CI until a chapter teaches it, and a chapter teaches only what it shows |
| 105 | A Library example is a folder dsl/std/examples/<package>/<name>/ with a main file; a folder without a main file is a helper package that some example imports. Every exported std phrase, events included, is used by at least one example of its package. An example folder that holds a file expect with one diagnostic code fails its run with exactly that code (as sim/failing shows sim.Failed), and an example folder without expect runs to its end. The generated index dsl/std/examples/README.md maps phrases to examples and the Library page shows them |
The Library promises a runnable example per phrase, and an example of a failure says which failure; checking use keeps that true |
| 106 | The site is built by a Go standard-library program in synodlang.github.io from deps.json, which pins for each docs version the dsl, std and examples tags with their commits, and the compiler and player tags with the SHA-256 of each asset used. Assets are verified against the release SHA256SUMS and against the pin; repositories are checked out byte-exact at the pinned commit with the git settings of decision 95; GitHub Actions deploys the result to Pages |
One place says what the site is made of; a rebuild gives the same bytes; the build needs no package manager |
| 107 | Docs live at /docs/v<MAJOR>.<MINOR>/, built from that version's pins; /docs/ leads to the latest version, and each version serves its own synod.wasm and player |
Follows decision 61; old docs run with the compiler they describe |
| 108 | The site highlights Synod code only from synod-check/1 tokens: at build time from the native pinned synod check --json --tokens, while editing from synod.wasm check with tokens; a fragment is shown unhighlighted |
One lexer (decisions 26 and 78) |
| 109 | A Run on the site calls synod.wasm run in one Web Worker per page and the newest request wins; the result plays in player with stopAtPause on, a pause marker shows its question and choices, and nothing is graded or stored |
The same engine as the CLI (decision 70); Predict as in the app (decision 82) |
| 110 | The home page loops a scene that the site build makes with the native pinned synod run of examples/raft, played by player with loop: true |
The first page loads no 8 MB module; the loop is the real run (decision 15), drawn live as a scene (decision 42) |
| 111 | synod-explain/1 example.files holds every file the example is checked or run with, its synod.mod included; an Errors page runs exactly those files, and the site build checks natively that they give the entry's diagnostic |
Every code has a page that runs, from one source (decision 93), with no copy of a compiler constant |