Skip to content

Persistence

A saved Dendrite program is the thing its author wrote, not the thing the analyser made of it.

Three forms can be stored, and they are the three ways a program can be authored:

FormHoldsWritten by
codethe source textthe code editor
retean opaque graph blobthe graph editor, when it lands
astthe raw program as plain recordscode that builds a program directly

The one that is not on that list is the core program, and that is the decision. An AST loses the comments, the whitespace and the symbol surface: reload $score >= 60 from a tree and you get Not(LessThan($score, 60)) back, which is the same program and not the same document. So the authoring artefact is what is kept, and the analysed form is rebuilt.

load never trusts what it is given. It parses and analyses from scratch, against the descriptor as it is now.

That is the point. A host that adds an op, renames an input, or tightens a type does not have to migrate anything: the next load of every stored program checks it against the new world and reports what no longer fits. A program cannot silently drift away from the language it was written in, because nothing about its analysis was stored to drift.

The cost is that loading is not free. It is a parse and an analysis, which is why a host holds a compiled program while it is in use rather than reloading per evaluation.

A saved program has an optional ports key: the inputs, outputs and types the document declares for itself, as opposed to the ones its host declares. That is the persisted layer from ports and layers, travelling with the program it belongs to.

One thing does not survive the trip. A type’s schema, its runtime validator, is a function, and a function does not go through JSON: JSON.stringify turns a zod schema into an object that revives as nothing, dropping any .refine predicate silently. So a layer that is persisted may not declare a schema at all, and an instance refuses one rather than saving something that will come back broken. A layer type that extends a language type inherits the validator without carrying it, which is the way round.

Two different things can change shape, so there are two versions and they are independent.

The format version is on the saved program, and it belongs to core. When the shape of a SavedProgram changes, the number goes up and migrate brings older blobs forward. A blob from a newer version than the running build fails with unsupported_version rather than being guessed at.

The envelope version belongs to whoever wraps a program in their own document. The editor has one, on EditorDocument, over the same generic migration chain, because what an editor saves is a program plus its input values plus whatever else that host needs. Core knows nothing about it.

Keeping them apart means a host can change its own document shape without touching the language, and core can change the program format without knowing what anyone wrapped it in. trust.