@niscorp/nova
0.5.1
Patch Changes
- a72f2bb: A canvas can be reconciled by a function the app hands the shell.
ShellConfig.reconcilersnames them; a canvas'sreconcilenames the one that reconciles it, whenever areconcilestep aims at it. A canvas that names none is reconciled exactly as before. AReconcileris called asreconcileCanvasis, and its options also carrymoveInstance(canvasId, instanceId, index)— a move that re-mounts nothing and writes nothing to the navigation journal — andkeyOf(instanceId), thekeyan instance was pushed with (PushOptions.key, new). Each entry areconcilestep hands over carries itsrowas written (Desired.row), so a reconciler can match rows by a field of its own choosing — several instances of one action on one canvas, in the list's order. A canvas naming a reconciler the shell was not given is refused bycreateShellandaddCanvas. No grammar change: the step's schema is as it was.
0.5.0
Minor Changes
6a62604:
createDevtoolsFunctionsanswers a list of{ id, description, execute }, andFunctionDefinitionnames that shape. Breaking.BREAKING — approved by moccadroid, 2026-10-18:
createDevtoolsFunctions(config)returned a map of handlers by name; it returnsFunctionDefinition[], each with a description of what it does and what to send. An app that spread the map into its functions spreads the list into the list itsfunctions(session)answers:functions: (session) => [...createDevtoolsFunctions({ shell: () => session.shell, definitions }), ...].
Patch Changes
- 63b9ab5:
auditActionsees through a loop and into a component's props.- A loop variable bound under the data root —
$.row.name,$.index,$.itemsinside afor/as— is no longer reported as a keydatahas no default for: the renderer binds them there. - A ref a component dispatches from inside its props, at any depth (a Table's
menu.items[0].ref), is no longer reported as carried by no layout node.collectInteractivenames such a ref'sviaby the prop's path (menu.items.0.ref). classifyAudit(issue)of@niscorp/nova/reflecttakes the issue alone and keeps one downgrade, a push whose target is a{{…}}template; its loop-variable and prop-ref downgrades had nothing left to downgrade.
- A loop variable bound under the data root —
0.4.0
Minor Changes
c7c4f9b: A nova shell keeps actions and fragments in one registry. Breaking.
BREAKING — approved by moccadroid, 2026-10-13:
shell.registerFragmentis gone, and an action and a fragment no longer share an id.shell.registerAction(definition)takes an action or a fragment, told apart by the fragment'skind: 'fragment', andshell.removeAction(id)takes either out. There was no call to take a fragment out. Taken out, a fragment is unknown to a laterwith: [...]; an instance that was opened with it keeps the definition it was composed into, andbackskips a position that was opened with it, as it skips one it cannot put back.- A fragment handed to
createShellis validated, as an action is: one that does not parse throwsDefinitionValidationErrorat construction. OnlyregisterFragmentvalidated one before. - An id names an action or a fragment, never both.
createShellandregisterActionthrowDefinitionValidationErrorfor an id that is already the other kind's. - Opening a fragment by itself still throws
UnknownActionError, and awith: [...]that names an action still throwsUnknownFragmentError.ShellConfigkeepsactionsandfragments. - Moss: a fragment that is no longer answered for a principal is taken out of their open shell, as an action is. A boot, and a set of actions handed over, refuse an action under the id of a fragment a shell ships. On a session's own shell,
registerAction(refused there as before) is the one call that would take a fragment.
What to change:
shell.registerFragment(fragment)becomesshell.registerAction(fragment). Give an action and a fragment that share an id two ids.
0.3.1
Patch Changes
d6b2c4a: The docs state seventy-three facts about the API that were written only in source comments.
README: what each reflect export returns (
walkNodes,snapshotShell,describeInstance,describeShell,actionGraph,auditCatalog,livenessOf,chainCycles, the*Keyshelpers), the devtools functions and how an app opens the inspector,paletteFromRegistry's options andpaletteEntryOf, whatcollectInteractivereturns, an example'sreplies,fetches,pressesandexpected, whatheadOfreturns, andplaceHead's{ site, path }. ACTION_DOCS.md: a catalog entry'sinput. ADAPTER.md, REACT_DOCS.md and TERMINAL_DOCS.md: what the kits'Tableandfallbackread, what a component'sdispatchis for, what a Vue component and slot wrapper receive, a TTY interactive'slabelandregister, how the terminal kits lay out and focus. I18N_DOCS.md: what counts as a binding and as a phrase,fillPhraseon a plain value, the form ofwhere, and the order of a harvest. SHELL_DOCS.md: a layout given as a store id, whatbackpasses over,removeAction, a reconcile entry'swithand the call'snotes, an unstampedapi.dispatch, and whatcanvasTreecounts as nothing.Two statements are corrected. SHELL_DOCS.md said an
initialseed equals ashell.pushmade aftercreateShellreturns; a seed is pushed withhistory: false, sobackdoes not undo it. TERMINAL_DOCS.md said a model-bound field's marker resolves byoccurrence; the Ink kit passesoccurrencefor aTablerow only, and anInputorCheckboxresolves by its ref.No behaviour changed.
What to change: nothing.
037d26c: Inside the action runtime, the map from a
ui:modelnode's ref to the state path it writes is namedmodelPaths; it wasmodelListenersand holds no listener.teardownModelListeners, which only cleared that map, is gone and its two callers clear the map. No behaviour changed.What to change: nothing.
0.3.0
Minor Changes
f834f3f:
evaluatechecks a config object once and only evaluates it after that;prismTransformis gone. Breaking.BREAKING — approved by moccadroid, 2026-10-09: replace
prismTransformwithevaluate(transform: evaluate), and pass limits as{ limits: … }.Prism had three ways to run a config, and the one a host was told to hand to its seam (
prismTransform) was the one that reported worst, while the hosts on a hot path each wrappedevaluate, which checked the config on every call. There are two now, with one job each.evaluate(config, source, options?)— for a config a program holds.- The first time it is handed a config object it checks it and keeps its tree; the second time it prepares the tree as
compilewould; from then on a call only evaluates. Four fields picked and renamed, the same config object again: 3.5 µs before, 0.4 µs now. A nested choice on two fields: 18 µs before, 0.7 µs now. A config that is a new object on every call costs what it did. - It is kept by the object: a config changed in place after its first call is not read again. Hand over a new object.
- Both arguments are
unknown, so it is what a host's transform seam takes:createTide({ transform: evaluate }),createUpgrader(grammars, { transform: evaluate }). - The third argument is
{ limits?, check? }. Limits were the third argument themselves:evaluate(config, source, { maxSteps: 100 })is nowevaluate(config, source, { limits: { maxSteps: 100 } }). check: 'always'is for writing a config, for tests and for tools: nothing is kept, the config is checked on every call, and the source must be plain JSON (a source holdingundefined, a function, aDateor a non-finite number is refused withE_TYPE).- An invalid config is a
PrismError(E_SCHEMA) naming the part that is wrong, however deep the config.prismTransformthrew zod's own error, and aRangeErrorfor a config nested far past the limit.
evaluateSafetakes the same arguments.compileandexecute— for a config that is stored. Unchanged.prismTransformis removed, from@niscorp/prismand from@niscorp/prism/migrations. What it added overevaluatewas a check that the source is plain JSON; that check is nowcheck: 'always', and a seam's source is taken as it is.moss, nova, vex, loom, nisc, create-nisc and the cli move with it. Each has Prism as a peer or a dependency, or has one of those that do, and a Prism at 0.3 is outside the range their last versions accept: an app takes them together. Nothing in vex, loom or the cli changed beyond that range.
moss's shell and nova's
$prismbinding calledevaluateon every use and so paid the check each time: an endpoint's request and response under moss, and a$prismvalue on each draw and for each row of a list, are 8 to 45 times faster for the configs measured. tide's and strata's documentation nameevaluatewhere they namedprismTransform.What to change
import { prismTransform } from '@niscorp/prism'or'@niscorp/prism/migrations'→import { evaluate } from '@niscorp/prism', andtransform: prismTransform→transform: evaluate. Every app made bycreate-nischas this insrc/dev/strata.ts; the app's typecheck andnpm run strataboth stop on that line until it is changed.- A third argument of limits becomes
{ limits: … }. - A wrapper that parsed the config before calling
evaluate(evaluate(ConfigSchema.parse(config), …)) makes a new object on every call and so is checked on every call: handevaluatethe config itself. - Code that changes a config object in place and evaluates it again must hand over a new object, or pass
{ check: 'always' }. - A host that relied on
prismTransformrefusing a source that is not plain JSON checks it itself, or passes{ check: 'always' }.
- The first time it is handed a config object it checks it and keeps its tree; the second time it prepares the tree as
Patch Changes
- Updated dependencies [023bcf9]
- Updated dependencies [1db6df4]
- Updated dependencies [6d098e4]
- Updated dependencies [8b4d97e]
- Updated dependencies [f834f3f]
- Updated dependencies [b084e30]
- Updated dependencies [663d724]
- Updated dependencies [6512d91]
- Updated dependencies [7d682b9]
- Updated dependencies [d2e9d22]
- Updated dependencies [e0e8abc]
- Updated dependencies [7fe1135]
- @niscorp/[email protected]
0.2.2
Patch Changes
7f8d202: Under
require, the entry points of nova, prism and strata share one copy of their code, as they always have underimport.The CommonJS build was not split, so every entry point carried its own copy of each class and of each React context. The ESM build is byte for byte what it was. Under
require:- an error from
@niscorp/strata/postgres,/check,/upgradeor/nodeis aninstanceoftheStrataErrorthat@niscorp/strataexports; one fromprismTransformof@niscorp/prism/migrationsis aninstanceof PrismError; one from<Nova.Layout>is aninstanceof NovaErrorand of its own class. None of them was. upgradeStorenames the row it refused —integration_actions (integration_id="…", action_id="…"): …, with the upgrader's error as itscause— and so does a moss boot that meets a stored row it cannot read. Both gave the upgrader's sentence alone.- a kit registered from
@niscorp/nova/adapters/react/componentsrenders under<Nova.Shell>. It threwuseShell must be used inside <NovaShellProvider>.
What to change: nothing in an ESM app. A CommonJS app that had built on any of the above — a branch that ran because
instanceoffailed, a match on the whole text of anupgradeStorerefusal — now sees what an ESM app sees. An app that loads both builds of one package in one process still has two copies of it.- an error from
0.2.1
Patch Changes
fe30458: Every package exports its
package.json, so an app can say which version of a package it runs by reading it from the package:import prism from '@niscorp/prism/package.json' with { type: 'json' }; prism.version; // the version that is installed, not the range that asked for itUntil now the
exportsmap hid it, and the only way to the version was a path intonode_modules.check:packagesinstalls the tarballs and reads every package's version this way.What to change: nothing. An app that read a version by path can read it by name.
353b3c3:
@niscorp/nova/examples— the reference as data.NOVA_EXAMPLESis 42 examples, in six groups (NOVA_EXAMPLE_GROUPS): the layout grammar (12), actions at work (7), endpoints (5), composition with fragments (5), shells (8) and i18n (5).An example is a small app, what is done to it, and what it must then say:
{ id, group, title, description, action | shell, fragments?, layouts?, replies?, fetches?, phrases?, phraseKeys?, stage?, presses, expected }.actionis one action alone on one canvas (an example of the layout grammar is an action with no triggers);shellis several actions on several canvases.repliesandfetchessay what its endpoints are answered, so an example comes to the same thing wherever it is run.pressesis what is done: a press, something typed, or the host changing the language.expectedis every piece of text the screen then holds (its text, and what stands in props at a prose key), the one action's data, which actions stand on each canvas, and what was sent to each URL.Every id an example brings begins with its own, so a host can hold all of them in one shell. The examples a host puts on its stage name only the plain components every kit has (
Stack,Text,Button,Input) and nova's two slots, with no prop about looks, so whoever shows them draws them with its own kit. An example withstage: false(the five on i18n) is shown as what it comes to. The package's tests run each one and hold it to itsexpected.Not in it: the adapters' own seams (
slotWrapper), the look of nova's default components, and the harvest functions of@niscorp/nova/i18n, which are called, not mounted.What to change: nothing.
7222c7d: The README's quick example runs as written. It registered no components, so it printed an error node (
COMPONENT_NOT_FOUND: Component not found in registry: Box) where its comment promised the tree — and with components registered,console.logwould still have printed the tree's children as[Object]. It now takes the terminal kit (defaultRegistry()from@niscorp/nova/adapters/tty/components), writes its text as children, the way the kits draw it, sends one press, and prints the whole tree; what it prints is in the README. No code changed.What to change: nothing.
c8982c2:
shellIdle(shell, { waitMs?, stopped? })— a wait for what a press set going.shellSettledanswers once nothing is mounting. That is the first screen being whole, and it says nothing about what happens afterwards: a trigger's steps run detached from the event that fired them, so afterdispatchit can answertruewhile the call the press made is still out. Anything that pressed and then read the screen had to sleep.shellIdleresolvestrueonce nothing is mounting and no chain the shell started is still running: no trigger's steps, noemiton its way to its listeners and what they then call, no hook a navigation started — including the re-read of an action apopreveals, which runs while that action is alreadyactive. It takes the optionsshellSettledtakes, with the same default wait, and resolvesfalsewhen the wait runs out.shell.dispatch({ type: 'ui:click', ref: 'save', origin }); await shellIdle(shell, { waitMs: 5000 }); // the call has answered, and what it chained to has runIt is about the shell's own work. It does not wait for the next body of a read the shell is following (a reactive read answers again when somebody else writes;
shell.onDataChangesays when), nor for anything outside the shell that an endpoint set going. A call that never answers holds it until the call's own timeout fails it.shellSettledis unchanged, and still asks about mounts alone: a page is read whenever it is asked for, also while a long call is out, and that read should not wait on it.ActionRuntimeConfiggains an optionalonChain, which is how a runtime reports what it starts and does not await; a shell made bycreateShellwires it.What to change: nothing. A check that slept after a press can await
shellIdleinstead.
0.2.0
Minor Changes
f1cec45: A Prism config that validates no longer names an op Prism does not have.
validateaccepted{ card: { $fetch: … } }and{ $eval: "…" }: the plain-object branch refused only the names of ops that exist, so any other$key passed as a template key. Such a config could be stored — an endpoint request, a vex mapping, a strata migration — and then failed every time it was evaluated (E_NODE_SHAPE, "Unsupported node shape").A key that starts with
$is now an op's name and nothing else. The template branch refuses every$key — the rule the evaluator already held — sovalidatereports it with the key and where it is (card.$fetch: Not a Prism op. A key that starts with "$" names an op; data with such a key goes in $const.), andevaluateandcompilefail withE_SCHEMAbefore anything runs. An op's name used as a template key keeps its own message.E_NODE_SHAPEis left for a tree that never went through the schema: an IR handed toexecute. Adding an op no longer takes a key away from templates.- prism — the change, and a grammar narrowing:
nisc.prism2, a marker with no document step (a key that never evaluated has nothing to be rewritten to).$keys that are data are untouched: a$constpayload,$withnames,$renameKeysand$fromEntrieskeys. The JSON Schema's template key pattern is^(?!\$)instead of a list of every op. - nova, vex, moss, loom, cli, nisc — no change of their own. They depend on prism and are released with it, so the set installs together; the configs they hand to prism (endpoint
requestandresponse,$prismbindings, vex mappings) are held to the same rule. - create-nisc — the templates' sources are recorded at
nisc.prism2.
What to change: in a config with a
$-prefixed key that is not a Prism op, remove the key — or put the object in$constif it is literal data. An app that keeps astrata.lock.jsonrunspnpm strata upgrade, thenpnpm strata verify; there is nothing to edit unless it has such a config. Everything else: nothing.BREAKING — approved by moccadroid, 2026-10-04: a config with a
$-prefixed key that is not a Prism op no longer validates, compiles or evaluates — also where evaluation never reached the key (an untaken$casebranch, a short-circuited$or, a$mapbody over an empty array), which used to work. A vex seed mapping with one fails when it is seeded, at boot, instead of on each replay. Code that matchedE_NODE_SHAPEfromevaluategetsE_SCHEMA. The@niscorppackages that depend on prism move with it, so an app moves them together (@niscorp/nisc0.3.0).- prism — the change, and a grammar narrowing:
Patch Changes
de6d980: A fix for the last fix:
nova:headis the document's<head>, and holds what a head holds. 0.1.4 gave it five props (title,description,image,kind,structured) and wrote tags from them, which closed a head to whatever nova had been taught: noog:image:alt, noog:typebut two, no whole address inside structured data. That was wrong, and it is replaced, not extended. The maintainer withdrew 0.1.4's props on 2026-10-04; this ships as a patch on that decision.What to change (only an app that used 0.1.4's props): a
nova:headnode no longer takes props. Write its children instead —nova:title,nova:meta,nova:link,nova:script— each with the element's attributes as its props.{ title: X }becomes{ component: 'nova:title', children: X };descriptionbecomesnova:metawithname: 'description';imageandkindbecomenova:metawithproperty: 'og:image'andproperty: 'og:type';structuredbecomesnova:scriptwithtype: 'application/ld+json'anddata.og:titleandog:descriptionare no longer written from the title and description: say them if they are wanted. An address is written as given — a picture's is not made whole fromsite. Code that calledheadOf,placeHeadorcreateTitleKeeper, or readheadoffrenderDocument's result or a route report, takes the new shapes below.- nova — a head's children are the elements a head holds, named as HTML names them, and a child's props are that element's attributes, written as given: nova keeps no list of them, so any
name,propertyorrelcan be said. Nothing in a head is drawn on the screen and no name has to be registered. With more than one head on a screen they are read in the order the screen is drawn, and an element that says the same thing as an earlier one (the title, a<meta>of that name or property, the canonical address) takes its place; anything else stands beside its like. What runs or styles is refused, left out and named: a script a browser would execute (nova:scriptis a data block — a JSONtype, never asrc), a stylesheet,http-equiv, a handler attribute.headOf(api)returns{ elements, actions, refused }. In@niscorp/nova/document,placeHead(html, elements, { site, path })writes them — an element takes the place of the tag that said the same thing, where it stood; what it wrote is markeddata-nova-head, and a tag that gave way is kept inert in a<template data-nova-own>— andcreateHeadKeeper(document)replacescreateTitleKeeper: the whole head follows the screen in the page, and the document's own tags come back when the screen stops saying them. Removed: the five props, theHeadandHeadPropstypes,createTitleKeeper. - cli —
build,exportandstartwrite each path's head from those elements. A head that holds what it may not fails the path, like any other check.sitestill gives every path its own canonical address andog:url; it no longer touches a picture's address. - moss —
renderDocumentandexportDocumentsdo the same from the snapshot (headon the result is{ elements, actions, refused }; a refusal is logged and left out), and the terminal keeps the whole head off the wire. - create-nisc — the two own-shell templates write their welcome screen's head this way.
Where a script goes: in the app's own
index.html, which holds anything and from which every page is written, or in the kit component that needs it. A layout is data and does not carry one.- nova — a head's children are the elements a head holds, named as HTML names them, and a child's props are that element's attributes, written as given: nova keeps no list of them, so any
Updated dependencies [f1cec45]
- @niscorp/[email protected]
0.1.4
Patch Changes
af903c6: A page's head is its screen's own.
nisc exportwrote every path with the builtindex.html's<head>, so a site with more than one page shipped each of them under the front page's title, description, preview card and canonical address.What a screen says about itself is now a node in a layout: nova's
nova:head, withtitle,description,image,kindandstructuredbound to the action's data like any other node's props. It draws nothing — no adapter builds an element for it, no registry has to hold it, and a component is never handed one as a child — and it rides in the render tree, so it reaches a shell in the page, a snapshot a server drew and the trees on a wire alike.headOf(api)reads it off any of them; with more than one on the screen the last one speaks.- nova —
HEAD_NAME,HeadPropsSchema,HEAD_META,headOf,isHeadNode; and@niscorp/nova/document:placeHeadwrites a head into an HTML document (the tag that said the same thing is replaced, a missing one is added before</head>, everything else is left as it was; values are escaped) andcreateTitleKeeperkeeps a tab's title on it. The DOM, React and Vue adapters keep the title on the head as the screen moves, and give the page's own back when the screen has none. - cli —
build,exportandstartwrite each path's document with its screen's head. "Same twice" also fails a boot that says a different head the second time, and the route table says whose head each path goes out with once an app has one. New config fieldsite: the address the site is served at. With it every path says its own canonical address (<link rel="canonical">,og:url) and a head's picture gets a whole address; a build with several paths, a canonical tag inindex.htmland nositesays so. - moss —
renderDocumentandexportDocumentsdo the same from the snapshot (siteon the config,headon the result), and the terminal keeps the tab's title off the wire for every render target. - create-nisc — the two own-shell templates say their welcome screen's head.
What to change: nothing. An app with no
nova:headnode and nositeis built, served and drawn exactly as before. An app that rewrote its exported files to give each page a head can delete that step: put the node in the layout and setsite.- nova —
0.1.3
Patch Changes
- 80e1ee8: Documentation: the packages are live, and releases are compatible.
STYLE_GUIDE.mdgains "The packages are live" — a release does not break an app that works on the one before it; what should go is deprecated and stays; a breaking change is a last resort that needs the maintainer's approval before it is written — andAGENTS.mdpoints every agent changing nisc itself to it. The status lines that said "pre-1.0, breaking changes expected" (nova), "API is pre-1.0 and moves" (moss) and "everything else may move" (tide) now say the same. moss's deprecatedinstalledIntegrationsis no longer described as scheduled for removal: it stays on the type. The style guide's Node floor is corrected to 22.12, what every package'senginesalready says. No code changes.
0.1.2
Patch Changes
633d0d1: The DOM adapter (
@niscorp/nova/adapters/dom) keeps what did not change.createDomViewused to empty its root and build the whole screen again on every render; it now keeps the tree it drew beside the DOM that tree produced and patches the page, so an element whose part of the tree is the same as before is the same DOM node, never taken off the page. Focus, scroll, a selection, an open<details>, a running animation and a timer a component started are kept, and one changed string costs one text node. A change in one instance or on one canvas leaves the others alone; keyed lists move their elements; a different instance in the same place is still all new. The first render still replaces markup drawn ahead of time, and now gives somebody already typing in it their words, caret and focus back by where the field sits rather than by itsref.Fixed with it: typing in the second of two instances that share a
refmoved focus (and the next keystroke) to the first; a bound field with norefof its own lost focus after one keystroke; a pressed button lost focus to<body>;debouncedid not coalesce across a render, and a pending value was not sent on blur.A DOM kit keeps working unchanged if its components are functions of their props and children.
DomComponentContextgains two things:onRemove(cleanup), run once when the element leaves the page for good, anddependsOnChildren(), for a component that marks or counts the children it holds. What a kit should check is in ADAPTER.md, "The DOM adapter keeps what did not change": a component is no longer called because something else on the page changed, children are patched in place only under an element that holds exactly them, and data that changes often should arrive as children rather than as a prop.45d6731: Three fixes.
{ reset: 'path' }run as a step — in a trigger, anonSuccess, a lifecycle hook — now restores the path's initial value; it set the path toundefined, because steps never handed the mutation the initial snapshot. The same omission kept strict mode out of steps: on a strict shell apush/pop/removeAt/move/clearat a missing or wrong-typed path now reports aMutationErrortoonError(exported from the package root) instead of doing nothing.shell.addCanvas({ mode: 'list' })is a list canvas;modewas read only fromcreateShell'scanvases. And an instance covered while itsmounthook is still running stays suspended —mountset it back toactivewhen the hook finished, leaving two active instances on a stack.
0.1.1
Patch Changes
- 534eb4b:
@niscorp/stratais a required peer: nova's shell and layout store and Prism's engine import its document-depth limit at load time, and with strata declared optional an app installing nova or Prism without it crashed on import.check:packagesnow follows every built entry's imports and refuses an undeclared one, or an optional peer loaded by a main entry.