14. Board ⇄ AST ⇄ Text
Two editable views create a harder problem than two read-only views.
If FlowScript were only an export, Flow-Like could print a Board and stop. If the Board were only a diagram generated from code, Flow-Like could throw the old graph away after every text edit and draw a new one. Neither shortcut would satisfy the promise of equal authoring surfaces. A builder must be able to move one node without replacing the program, while a developer must be able to change one literal without erasing the builder’s layout.
Flow-Like solves that problem with a typed intermediate representation called BoardAst. The AST
captures what the program means without pretending that source text should own every visual and
runtime detail of the Board.
readBoard ── lower ──▶ BoardAst ── render ──▶ canonical FlowScript ▲ ▲ │ │ └──────── parse ─────────────┘ └── atomic Board commands ◀──── reconcileThe persisted Board remains the executable model. The AST is the temporary semantic contract. FlowScript and the visual editor are equal ways to author that model.
Release check: The current FlowScript editor treats text changes as a draft until Apply. Studio/Desktop can also run the authoritative reconciler to show a semantic command preview; web currently has client-side structural feedback and the authoritative server Apply response, but no equivalent pre-Apply command preview. Parsing validates syntax; reconciliation resolves catalog calls and validates the proposed graph change. Any diagnostic blocks Apply. A valid edit becomes a minimal batch of Board commands, applied with rollback on failure, and the editor reloads canonical source from the resulting Board. Formatting choices are deliberately standardized rather than preserved. Hidden secret-variable values do not enter rendered source and survive unrelated edits. Existing anchored Function signatures cannot yet be changed safely through FlowScript; the edit is rejected instead of partially rebuilding the boundary.
14.1 One program, three representations
Section titled “14.1 One program, three representations”The three forms overlap, but they do not own identical information.
| Representation | Its job | Information it owns |
|---|---|---|
| Board | Persist and execute the Flow | Node and pin identity, connections, defaults, layers, coordinates, viewport, presentation, package and runtime metadata |
BoardAst | Express the typed meaning shared by both editors | Imports, interfaces, variables, Functions, Events, modules, detached chains, ordered statements and expressions, plus identity anchors used during editing |
| FlowScript | Give people and tools a compact authoring surface | A canonical textual projection of the AST’s program structure |
This is why “both views are the source of truth” is close but imprecise. It sounds as if Flow-Like stores two independent programs and later tries to synchronize them. It does not. The sharper contract is:
Studio and FlowScript are equal authoring surfaces over one underlying Flow model.
An edit on the Board changes that model and therefore changes its next textual projection. An applied text edit becomes Board commands and therefore changes the next visual projection. The views agree on executable meaning, not on every character or pixel.
That distinction matters in practice. A Board node needs an opaque identity so wires, logs, undo,
and versions can keep referring to the same operation. A programmer needs a readable name such as
productionStopped. Both describe one operation, but the readable name is not a sufficient
database key and the database key is not a good programming language.
14.2 Why Board JSON is not the language
Section titled “14.2 Why Board JSON is not the language”Try this small experiment in Studio:
- Select two or three connected nodes on a Board.
- Copy them.
- Paste the clipboard into a text editor instead of another Board.
What appears is useful JSON. It contains serialized nodes, comments, cursor position, layers, variables, schema references, pin IDs, defaults, and connection sets. Flow-Like can paste that data back because it is an excellent transport format. It can mint new node, pin, and layer IDs, then reconnect the copied selection.
Now imagine reviewing a five-hundred-node application in that form. The business decision is buried among storage identities, adjacency lists, coordinates, compatibility data, and UI metadata. Moving a node changes the document even when the program means exactly the same thing. An AI editing that JSON must manipulate graph bookkeeping correctly before it can express a simple domain change.
This is where the AST became necessary.
FlowScript was already in mind while Boards were being built, but the architectural need crystallized later: once text became an authoritative editing surface, particularly a safe surface for FlowPilot, “print this graph readably” was no longer enough. Direct text-to-Board conversion would couple language syntax to persistence and encourage whole-graph reconstruction. A typed semantic pivot instead lets both directions meet on the program’s meaning.
The repository history supports that interpretation. The first FlowScript implementation landed with the FlowPilot update and introduced the AST, parser, renderer, Board lowering, and reconciliation together. That is an architectural clue, not a claim that every design decision was made in one moment.
The practical outcome is more important than the chronology:
- Board JSON remains optimized for persistence, execution, transport, and the visual editor.
- FlowScript remains optimized for reading, reasoning, completion, review, and focused edits.
BoardAstprevents either representation from dictating the other’s accidental details.
14.3 Lowering a Board extracts the program
Section titled “14.3 Lowering a Board extracts the program”Lowering begins with the live Board and its catalog. It does not simply iterate through a node map and print one line per entry. A graph has connections; a program has scopes, expressions, statements, and order. The lowerer must recover those language structures.
It performs several related jobs:
- Board variables become typed top-level declarations.
- schema-backed shapes become FlowScript interfaces;
- Function layers become typed
functiondeclarations; - trigger nodes and their reachable execution paths become Event bodies;
- every execution chain no trigger reaches becomes its own
detachedblock; - impure nodes on execution wires become ordered statements;
- pure data dependencies are pulled backward and nested as expressions;
- named execution outputs become branches such as
onSuccess,execError, or anifbody; and - node, variable, and layer identities can become editing anchors.
That detached block exists because the chain has to go somewhere. FlowScript has no top-level
statement position, and the chain’s root is an ordinary node, so spelling it as an entry would
misrepresent it and hide its inputs. Nothing inside such a block runs; it reports what the Board
already contains rather than offering a way to author new work.
Consider the Incident Desk rule:
const productionStopped = report.contains({ substring: "production is on hold", ignoreCase: true})if (productionStopped || customerFacing) { severity = "SEV-1"}The Board does not store that exact syntax. It stores a String Contains operation, variable reads, a boolean operation, a Branch, a Set Variable operation, pins, and the relevant data and execution connections. Lowering recognizes that shape and chooses the compact expression and control-flow forms.
Some visual helpers disappear from the semantic projection. A reroute helps a wire travel cleanly across a canvas but performs no domain operation, so lowering follows through it. Collapsed visual boundaries may flatten where their contents remain the real computation. Coordinates can provide a stable ordering hint for otherwise independent entries without becoming source-code syntax.
This is semantic compression, not data loss from the Board. The live Board still exists while the text is being edited.
14.4 Rendering chooses one canonical spelling
Section titled “14.4 Rendering chooses one canonical spelling”Once lowering has produced BoardAst, rendering is deterministic. The renderer chooses a stable
document order, four-space indentation, double-quoted strings, decorator order, spacing,
parentheses, and declaration layout.
Canonical source gives humans and machines a smaller comparison surface. Two authors cannot spend an afternoon debating semicolons or quote style because those choices are not part of the contract. After Apply, the system reloads source from the Board and standardizes it as much as possible.
For example, an author may type:
if (customerFacing && impact == 'production-stopped') { severity = 'SEV-1';}Pure parse-and-format produces the canonical form:
if (customerFacing && (impact == "production-stopped")) { severity = "SEV-1"}The added parentheses make precedence explicit. Quote choice and semicolons disappear. This is a semantic AST, not a concrete syntax tree intended to reproduce the author’s keystrokes.
Board lowering also chooses graph-derived sugar. Pure node outputs may become nested expressions. Catalog metadata can turn a static call into a method call. Imports are derived only when they make several calls clearer without creating ambiguity.
Suppose two namespaces both expose contains. Opening both names would make the call unclear, so
the lowerer keeps the conflicting use inline:
const inReport = string::contains({ string: report, substring: "production is on hold", ignoreCase: true})const inSystems = array::contains({ array: affectedSystems, value: systemId })Flow-Like does not guess which contains the author meant. It uses the qualified spelling until an
import is safe.
Secrets do not need a readable value
Section titled “Secrets do not need a readable value”A secret variable can have a configured value on the Board while its textual declaration has no initializer:
@secretconst incidentApiKey: stringThe secret value is deliberately omitted when the Board becomes an AST. Because an unchanged declaration carries no replacement value, applying an unrelated edit preserves the configured secret rather than clearing it. FlowScript also refuses to populate a new secret from a nonempty source initializer.
This protection is specific, not magical. It does not make arbitrary downstream values safe to log or return, and it should not be generalized into a claim that every sensitive pin on every local and remote rendering path has been audited. Secret handling still depends on the node, execution boundary, and storage path involved.
14.5 Parsing turns a draft back into typed meaning
Section titled “14.5 Parsing turns a draft back into typed meaning”Typing in the editor does not mutate the Board on every keystroke. The text buffer is a draft. Flow-Like parses it for feedback, and changes the Board only when a valid revision is applied. Studio/Desktop can additionally reconcile the draft against a cloned Board for an authoritative command preview without persistence or undo mutation. Web currently performs that authoritative step in the server Apply path.
Parsing answers questions such as:
- Are delimiters balanced?
- Is this declaration allowed in this scope?
- Does the expression have a valid syntactic shape?
- Is a decorator written correctly?
- Is the document nested within the defensive parser limit?
It deliberately does not answer every program question. The parser can recognize the shape of
madeUp::operation({ value: 1 }) without knowing whether the installed catalog contains that
node. It can recognize a named argument without knowing whether the node owns a compatible pin.
Those checks require the live catalog and Board, so they belong to reconciliation.
The editor therefore has two useful feedback layers:
- Parse feedback identifies the first syntactic problem with a line and column.
- Reconcile feedback, when the client requests that authoritative check or Apply reaches the server, reports unresolved calls, ambiguous names, missing pins, incompatible types or shapes, invalid boundaries, policy limits, and unsafe edits.
Only a revision that passes both layers can be applied. Invalid source remains a harmless draft, and the author gets a concrete problem to fix.
For experts: the small parser behind the editor
Section titled “For experts: the small parser behind the editor”The current parser is hand-written and intentionally compact:
characters → context-free tokens → recursive-descent declarations and statements → Pratt-parsed expressions → typed BoardAstThe lexer records line, column, and byte position. Recursive descent handles document structure; a Pratt parser handles operator precedence and associativity. A shared nesting budget of 128 protects blocks, modules, expressions, and nested interface types from adversarial editor or API input.
The diagnostic model is still modest. Parsing stops at its first error and reports a line and column rather than a rich multi-error span set. Template-expression errors receive rebased source coordinates, while later semantic diagnostics locate relevant tokens on a best-effort basis. This is enough for a useful guarded editor, but it should not be described as a finished compiler diagnostics system.
The formatter’s invariant is straightforward:
canonical = render(parse(authored))render(parse(canonical)) == canonicalThat stable second rendering is more important to Flow-Like than preserving arbitrary first-pass style.
14.6 Reconciliation plans the smallest valid change
Section titled “14.6 Reconciliation plans the smallest valid change”After parsing, the reconciler compares the edited AST with the current Board. It uses the catalog to resolve calls, derives their expected pins, checks types and shapes, accounts for dynamic pins, and matches existing entities through stable identity information.
Its result is not a replacement Board. It is a command plan using the same vocabulary as the visual editor, including operations such as:
- update one node pin;
- add, update, or remove a node;
- connect or disconnect two pins;
- add or update a variable;
- add, update, or remove a layer; and
- move a node into another layer.
Canvas movement also uses the shared command vocabulary, but FlowScript reconciliation currently does not reposition existing nodes. New text-created structures receive automatic placement while untouched coordinates remain untouched.
That command boundary is the central preservation mechanism. If one literal changes, the plan can update one pin while leaving every other node ID, coordinate, comment, connection, package field, and visual choice alone. If a new expression requires three nodes and four wires, the preview can say so before any command runs.
The Studio/Desktop Apply preview groups the semantic consequences so a reviewer can distinguish an ordinary configuration update from a structural or destructive edit. Web currently returns the same class of authoritative diagnostics and command outcome during Apply rather than showing that command plan beforehand. Deletions need separate approval, which Chapter 15 examines in detail.
Apply then follows a fail-closed sequence:
- Reconcile the complete draft against the current Board and catalog.
- Stop with no executed commands if any diagnostic exists.
- Stop if a destructive plan has not received explicit approval.
- Validate and execute the command batch against a staged Board.
- Roll back earlier commands if a later command fails.
- Persist the Board only after the complete application succeeds.
- Lower and render the result back into canonical FlowScript.
The last step matters. Successful Apply does not preserve the author’s preferred spelling; it preserves the accepted program and the live Board information outside that program.
14.7 Follow one Incident Desk change through the loop
Section titled “14.7 Follow one Incident Desk change through the loop”Start with the anchored line that recognizes the report from the 3 A.M. call. The identity below is shortened for the book:
const productionStopped = report.contains({ substring: "production is on hold", ignoreCase: true}) //@n:contains-reportChange only the literal:
const productionStopped = report.contains({ substring: "customer-facing outage", ignoreCase: true}) //@n:contains-reportThe parser produces the same statement shape with a different string. The anchor identifies the
existing Contains node. Reconciliation sees that its substring input is not connected to another
node and that only its stored value changed. The resulting plan contains exactly one
UpdateNodePin; it does not remove and recreate the node.
That is the smallest useful demonstration of the contract:
one source literal → one AST literal → one changed input pin → one Board command → the same node in the same placeA structural edit makes the same principle more visible. Derive one more signal from the existing report without changing the Function boundary:
const customerFacing = report.contains({ substring: "customer-facing", ignoreCase: true})Then revise the rule:
if (productionStopped) {if (productionStopped || customerFacing) { severity = "SEV-1"}For the current standard catalog, the semantic delta is a new String Contains operation, a Boolean OR operation, their literal settings, and the data wires into the existing Branch. Unchanged nodes retain their identity and placement. The exact preview count still depends on the installed catalog and live graph shape, which is why the product shows the real plan rather than promising a fixed expansion in prose.
The example intentionally avoids adding customerFacing as a parameter to the existing anchored
decideIncident Function. The current reconciler rejects signature changes to an established
Function boundary because it cannot yet migrate that layer and all callers safely. A new Function
can be created with the desired signature; changing an existing one requires a future safe
migration path. Failing closed is better than silently detaching caller pins.
14.8 The text does not own the canvas
Section titled “14.8 The text does not own the canvas”Suppose a domain expert places the severity branch beside the input it explains, colors its layer, adds a Board comment, and routes a long connection around an unrelated cluster. None of those choices needs a FlowScript keyword.
Because Apply patches the live Board instead of reconstructing it, an unrelated text edit can preserve:
- node and pin IDs;
- coordinates and viewport;
- layer colors, icons, and presentation settings;
- package, score, version, and runtime metadata;
- decorative Board comments; and
- reroutes that make wires readable.
Preservation does not mean every visual construct has an equivalent line of source. A reroute may vanish while reading FlowScript because it is semantically transparent, then remain on the live Board because no command touched it. Board comments are not currently lowered into FlowScript comments, and source comments are not a reliable way to create or replace decorative Board comments. A complete text-only graph rebuild therefore cannot promise pixel-identical recovery.
This leads to the right definition of round-trip fidelity:
FlowScript aims to preserve supported program meaning and untouched Board identity, not arbitrary author formatting and not a byte-for-byte or pixel-for-pixel serialization.
There are still unsupported edges. Existing Function signature migration is one. A whole-Board unchanged round trip also has known difficult fixtures around duplicated handler names and cross-handler references. Report such cases with the Flow-Like version, the smallest reproducing Board, and the text before and after Apply. “Equal authoring surfaces” is a contract strengthened by tests, not a reason to hide unfinished cases.
14.9 The mental model to keep
Section titled “14.9 The mental model to keep”When you edit the Board, Flow-Like owns graph identity and projects readable source. When you edit FlowScript, you are proposing a semantic patch to that graph. The AST is the meeting point that makes both operations precise.
Remember the six verbs:
- Lower the Board into typed meaning.
- Render that meaning as canonical FlowScript.
- Parse an edited draft back into typed meaning.
- Reconcile its semantic difference against the live Board and catalog.
- Apply the minimal valid command batch atomically.
- Reload the result so both editors show the same accepted program.
The next chapter looks at the identity anchors, correction proposals, deletion guards, and scoped editing rules that make step four safe on large Flows.