A database has no compiler.
In Nisc your screens and queries are data, saved in a database and run from there. Rename a prop, and the compiler flags every file that uses it. The screens saved in the database still say the old name, and nothing flags them. Strata upgrades saved data with a migration, at the moment it is read.
pnpm add @niscorp/strata zod · 0.1.7 · Apache-2.0
The kit renamed a button’s label to text
In your code: the compiler stops you
// the kit, after the release
type ButtonProps = {
text: string;
variant?: 'filled' | 'plain';
};
// your file, written in March
const book: ButtonProps = {
label: 'Book seats',
tone: 'primary',
};
In your database: a screen saved in March
{
"component": "Stack",
"children": [
{
"component": "Card",
"children": [
{
"component": "Text",
"children": "The Tempest, 19:30"
},
{
"component": "Button",
"props": {
"label": "Book seats",
"tone": "primary"
}
}
]
},
{
"component": "Button",
"props": {
"label": "Other shows",
"tone": "quiet"
}
}
]
}
The Tempest, 19:30
Nothing stopped this. The yellow lines are the ones the new kit no longer understands. It looks for text and finds label, so both buttons are drawn with no words. A member finds out when they open the screen.
The fix is data too.
Strata has no language of its own. A migration is a Prism config, the same kind of transform Nisc uses everywhere else. A screen is a tree, so Strata walks it and gives the config one node at a time. The node comes in as $.document, and what the config returns replaces it. This one renames label to text and returns every other node unchanged.
{
"$case": {
"branches": [
{
"when": {
"$has": {
"from": { "$ref": "$.document" },
"path": ["props", "label"]
}
},
"then": {
"$update": {
"from": { "$ref": "$.document" },
"path": ["props"],
"value": {
"$renameKeys": {
"from": { "$var": "current" },
"map": { "label": "text" }
}
}
}
}
}
],
"else": { "$ref": "$.document" }
}
}Every node it was given, and what it returned
Stack unchanged
Card unchanged
Text unchanged
Button label is now text
Button label is now text
Each screen knows how old it is.
The theatre shipped four releases this year, and three of them came with a migration. A saved screen carries a stamp: how many migrations existed when it was saved. Strata runs the ones it missed when the screen is read, and one call can rewrite a whole table the same way. A screen saved by a newer release than the code is refused.
Its stamp
{
"nisc.nova": 2,
"nisc.prism": 3,
"theatre.kit": 0
}
The screen, as saved
{
"component": "Stack",
"children": [
{
"component": "Card",
"children": [
{
"component": "Text",
"children": "The Tempest, 19:30"
},
{
"component": "Button",
"props": {
"label": "Book seats",
"tone": "primary"
}
}
]
},
{
"component": "Button",
"props": {
"label": "Other shows",
"tone": "quiet"
}
}
]
}
Read by December’s code
The screen, after reading
{
"component": "Stack",
"children": [
{
"component": "Tile",
"children": [
{
"component": "Text",
"children": "The Tempest, 19:30"
},
{
"component": "Button",
"props": {
"text": "Book seats",
"variant": "filled"
}
}
]
},
{
"component": "Button",
"props": {
"text": "Other shows",
"variant": "plain"
}
}
]
}
Saved before any of the three changes. All three migrations run. Yellow on the left is what goes, yellow on the right is what replaced it.
A release leaves a note for whoever fixes your code.
Some changes are in code, and no migration can rewrite code. The kit’s sell() used to take a show’s title. Now it takes the show’s id. Both are strings, so the compiler accepts the old call. The release ships a migration with no steps, only a description of the change. That still moves the version, so your CI fails until someone has been handed the note.
src/boxoffice.ts, your code
import { sell } from './kit';
export const sellTonight = (seats: string[]): string => {
return sell('The Tempest', seats);
};
{
"grammar": {
"nisc.nova": 2,
"nisc.prism": 3,
"theatre.kit": 0
}
}# strata upgrade The source is written at **nisc.nova 2, nisc.prism 3, theatre.kit 0**; the installed grammars are at **nisc.nova 2, nisc.prism 3, theatre.kit 1**. ## Migrations - `theatre.kit/1` — sell() takes the show’s id, where it took its title ## Nothing to edit No artifact changes (1 checked). Run `verify` to move the lock.
The lock file is one behind, so pnpm strata status --check fails. pnpm strata upgrade writes the report on the right. The line under Migrations is the note. "Nothing to edit" means none of your screens changes: the edit is in your code, on the yellow line.
nisc.nova/1 The `reconcile` step: make a canvas hold exactly the actions a list in the action's data names nisc.nova/2 The endpoint `timeoutMs`: how long to wait for the reply before the call fails nisc.prism/1 Transform ops ($has, $renameKeys, $update, $assert, $walk); $ref "$" for the whole source; $join parts may be any node that evaluates to an array; an op name is not a template key nisc.prism/2 A template key never starts with "$": a `$` name that is not an op is refused by the schema, where it was refused only once evaluated nisc.prism/3 $mod and $toNumber; $round takes a mode (nearest, floor, ceil); $replace takes all; a $ref path is keys and indexes only: any other JSONPath form is refused, where it was read as "$"
Give saved data a compiler error.
Strata has a check for CI. It fails when a schema changed and no migration was added. It also upgrades screens that were really saved and fails when one no longer fits the schema.
- properties.tone + properties.variant
The schema changed and nobody wrote a migration. Three of the four saved buttons still say tone, so they fail. The build stops here, before anything reaches production.
SQL migrations, by the same rules.
The idea comes from SQL migrations, and Strata runs those too. It runs the ones a database has not run yet, in one transaction, and records each in a ledger table with a checksum. When the app starts, it compares the ledger with the code.
The ledger and the code match. Nothing runs.
It is why the packages can change.
Without it, a release of Nisc could never change the format of anything you had saved. With it, a release ships its migrations and your saved screens follow.