@niscorp/nisc 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/AGENTS.md +4 -3
  2. package/CHANGELOG.md +106 -0
  3. package/package.json +10 -10
package/AGENTS.md CHANGED
@@ -88,15 +88,16 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
88
88
 
89
89
  5. All data in and out of an action flows through its declared `endpoints`. Shaping is declared on the endpoint: a static request body is plain JSON; a body derived from the action's data is a Prism config, kept in a sibling `.prism.ts`. Prefer plain JSON when no mapping is required. Components never shape data.
90
90
  6. Nova doesn't know Prism: the shell's injected `transform` is the socket that makes Prism endpoint configs work. Wire it once in setup, and fold ambient app context (the app's "today", the session's principal) into the source there — per session on the server under moss, in the shell factory in a client-degrade app — never into per-action data. The socket takes Prism's `evaluate` as it is (`transform: evaluate`), or a wrapper that adds to the source: `(config, source) => evaluate(config, { ...source, today })`. Hand `evaluate` the config object itself, never a copy of it and never one parsed again: it checks a config the first time it sees the object and only evaluates it after that. A test or a tool that must check everything on every call passes `{ check: 'always' }`. A config that is stored is compiled once and its IR executed (`compile`, `execute`).
91
- 7. An endpoint is a contract, not an implementation (D1, D2). In-browser, server, or mixed — actions can't tell the difference, and must not be able to. Server functions are endpoints too: the manifest's `functions(session)` seam runs them in-process under the caller's policy. A client-side `fn` registry is the escape hatch for client-degrade apps, and discouraged elsewhere.
91
+ 7. An endpoint is a contract, not an implementation (D1, D2). In-browser, server, or mixed — actions can't tell the difference, and must not be able to. Server functions are endpoints too: the manifest's `functions(session)` seam runs them in-process under the caller's policy. The seam returns a list of `{ id, description, execute }`: the id is what the charter and an `fn:` endpoint name, and the description says what the function does and what to send, for the model that writes a call to it; a map of handlers is deprecated. A client-side `fn` registry is the escape hatch for client-degrade apps, and discouraged elsewhere.
92
92
  7a. The manifest's three code seams are distinct, and app code goes in the one that describes it. `functions(session)` is for **endpoints** — things an action calls. `onSession(session)` is for per-shell code that is **not** an endpoint: an observer, a roster, an agent watching the screen. `runs` is the **sink** for model runs, fed by `session.recordRun`. Registering a handler nothing calls, to get code to run per session, is a lie about what that code is — use `onSession`. Both it and `functions` run before the shell finishes building, so neither may touch `session.shell` synchronously. The members of that shell that change what it is made of (`registerAction`, `setLayout`, `addCanvas` and their kin) throw there; the whole shell is `unverifiedShell(session)` from `@niscorp/moss/unverified`.
93
+ 7b. A layout generated while the app runs is saved through `session.artifacts`, not set on the shell. `session.artifacts.save([{ kind: 'layout', body: { ref: 'action/<the action’s id>', layout, use: true } }])` keeps it for the session's principal: it is theirs alone, it is drawn on their screens at once with their data kept, and every save is a version (`list`, `history`, `restore`, `reset`). Without `use: true` it is kept and not drawn. The layout of the app's own shell is saved the same way under `ref: 'frame'`, and a canvas's under `ref: 'canvas/<the canvas’s id>'`: a page keeps the layouts it declares, a shell manifest with no `layout` (or a canvas with no `actionLayout`) has none to take the place of, and a frame that leaves a canvas out is saved as it is, so check that yourself where it matters. A canvas their shell does not declare, that a layout in force for a person has a canvas slot for, is taken out of their shell, with what is mounted on it, once no layout has the slot: declare a canvas that is to stay. No call can name anybody else: moss reads and writes those rows as the caller, and vex keeps them to the caller's own. **Who may save is the charter's**: put the call in a function, and grant the action that calls it to the roles that may. A body moss refuses (a ref no action of theirs keeps a layout under, a component no shell has, a binding to data the action lacks, a control the action listens on left out) rejects with a sentence: hand it back to whoever wrote the layout. A layout for everybody is published from the app's own server code and never from a session: `server.defaults(publisher).save([...])`, with the same body and the same five calls. It is drawn for everybody who holds the action, under a person's own layout in use and their roles' layout variant, and over the build's. `publisher` is who published, kept with the save: hand it the session's principal. **Who may publish is the charter's** too: put the call in a function and grant the action that calls it to the owners' role. No `userId` is `*`: that is whose the published rows are. An action or a fragment generated while the app runs is saved the same way: `{ kind: 'action', body }` or `{ kind: 'fragment', body }`, the body without an `id`, because moss gives it (`generated.` and a uuid, which `save` answers). It is its owner's alone, and what it defines replays reads and calls the functions its owner's roles grant: each endpoint is a `POST` to the app's vex route naming one fingerprint of a read the app ships and its owner may replay, or of a read vex wrote that its owner saved, or an `fn:` endpoint to a function its owner's roles grant (`session.functions()` lists them with their descriptions: hand that list to the model that writes the action). A read vex wrote for `session.answerIntent` is kept the same way: `session.artifacts.save([{ kind: 'read', id: '<the fingerprint vex answered>' }])`, with no body. It is answered to that person alone, it survives a schema change that does not touch what it reads, and their generated actions may replay it. A function its owner's roles do not grant, a write and any other address are refused with a sentence, and so is a generated action that opens an action its owner does not hold. Once it is in the shell it is an action like any other: it emits and listens on channels, and a fragment's steps run inside the action it is wrapped around. What an assistant may generate is the app's to decide, in its prompt and in the function that calls `save`. It is in its owner's shells a moment after `save` answers, not before. What is still being generated, and is to be seen at once, goes into the draft of the one shell it is worked on in: `session.draft.put([...])` takes what `save` takes and answers when that shell has it, so the function opens it on its next line; `session.draft.save()` then keeps all of it in one save, and `discard()` drops it. A draft is saved nowhere and is gone when its shell is. Never give an action or a fragment the app ships an id beginning with `generated.`. A layout is the one kind that is published.
93
94
  8. Writes are endpoints fired by triggers, never inline mutations (D4). Identity and tenancy are stamped server-side from the session, never client-supplied: a form never carries `owner_id`. What a principal *is* beyond its id — a tenant, an org, a region — is the manifest's `scope(principal)` seam: moss always injects `{ userId }`, `scope` contributes the rest, and the merged set is what a behavior's `to:` resolves against at execute. The mapping is application knowledge, so the app supplies it; the values are injected server-side and unreferenceable by a request.
94
95
  9. Formatting and derivation live in Prism transforms, never in components and never in action code.
95
96
  9a. File pickers live in the kit. A file never goes over the socket; the app saves it wherever it wants.
96
97
 
97
98
  **Principals**
98
99
 
99
- 10. The charter is the app's policy document: role names → glob selections over the app's universes — `actions` (which action ids exist for a principal) and `data` (`table.verb` capabilities, compiled into the Vex `ScopePolicy`). The charter compiles; it never enforces and never shapes. Moss resolves it per principal and refuses to boot incoherent. Assignments (principal → roles) are app data beside it; row-level semantics the charter can't express are `behaviors`, compiled into the policy. One role name is fixed: a request with no session, and a signed-in principal with no assignment, resolve to `public`, so every charter defines it — `public: []` where it grants nothing. Boot does not check for it; without it those requests fail (`Unknown role "public"`).
100
+ 10. The charter is the app's policy document: role names → glob selections over the app's universes — `actions` (which action ids exist for a principal), `data` (`table.verb` capabilities, compiled into the Vex `ScopePolicy`) and `functions` (which of the app's functions a principal's shell holds: a role with none may call none). Grant each role the functions its actions call, deliberately: moss refuses to boot and lists every call whose grants miss, for each role on its own, each combination people wear and `public`, and says where a role allows every function (`'*'`). The charter compiles; it never enforces and never shapes. Moss resolves it per principal and refuses to boot incoherent. Assignments (principal → roles) are app data beside it; row-level semantics the charter can't express are `behaviors`, compiled into the policy. One role name is fixed: a request with no session, and a signed-in principal with no assignment, resolve to `public`, so every charter defines it — `public: []` where it grants nothing. Boot does not check for it; without it those requests fail (`Unknown role "public"`).
100
101
  10a. The `data` universe is the INTROSPECTED database, not your schema file: it holds the engine's own tables too — moss's `integrations` (integration key hashes), `integration_actions`, `moss_generation`, `sessions` (session token hashes, under moss's credential), the `strata_ledger`, the vex cache. A role granted a data wildcard (`*.read`) denies them explicitly (`data: { allow: ['*.read'], deny: [...] }`; denies do not inherit, so every role that grants the wildcard denies). A charter check resolves against the universe boot resolves against — the app's migrations and moss's run on a scratch database, then introspected — or it proves nothing about wildcards.
101
102
  11. Per-principal UI is existence or a served variant, never a conditional. An action a principal lacks does not exist in their shell (ring 1); a different shape of the same action is a served variant (ring 2). Layouts and components never branch on roles or capability data. Ring 1 does the deriving: a canvas's `initial` takes a **candidate list** and the first id the principal actually holds mounts — members boot their home, anonymous boots the login, and nobody configures which. An ungranted candidate simply isn't there.
102
103
  11a. Per-principal boot has two hooks, and they are twins. `inputs` derives boot **data** — merged over each canvas's static seed, read from action data downstream. `seeds` derives boot **instances** — which actions to push onto which canvases, computed from the session and its own reads over the session's wire, ring-1-filtered like every other mount. A composed surface (a home, an agent's column) is `seeds` plus a `list` canvas; hand-authoring the same arrangement names it twice. Branching chrome on `inputs` is a stopgap where a served variant doesn't exist yet.
@@ -242,7 +243,7 @@ export const todosOpen: SeedEntry = {
242
243
  - **Every list states its `limit`.** A seeded entry reads at most the `limit` it states, and the engine does not lower that number unless the deployment tells it to. A list that states none gets the engine's default of 100 and no more — when exactly that many come back, the reply's `meta.warnings` and the server console say the list may have been cut. That line means the entry is missing a `limit`; write one.
243
244
  - **Writes are mutation entries in the same store.** A `SeedMutation`: a statement in the closed mutation grammar in place of a query DSL, the same wire shape (`{ fingerprint, context }`). Mutations are replay-only forever — never generated; `lintMutation` runs at seed and throws on a failing statement, and context signatures are derived for discovery.
244
245
  - **A parent and its children are one batch.** An array of statements runs in one transaction, and a later statement reads the row an earlier one wrote: `{ $returned: 'orders.id' }` is the id the database just generated for the `orders` insert earlier in the same batch, so a generated id reaches the rows that point at it without leaving the server, and the write is all or nothing. The statement it names must write exactly one row, or the whole batch is refused. The reply is every statement's rows in order — and exactly one row comes back as that row, not an array of one.
245
- - **Under moss (the D1 default), hand the entries to `defineApp({ entries })`.** The server derives the data layer: boots the engine, prewarms protected, serves locked replay-only endpoints, and compiles each principal's `ScopePolicy` from the charter's `data` section. The app never touches an engine.
246
+ - **Under moss (the D1 default), hand the entries to `defineApp({ entries })`.** The server derives the data layer: boots the engine, holds the entries and compiles them, serves locked replay-only endpoints, and compiles each principal's `ScopePolicy` from the charter's `data` section. The app never touches an engine, and never writes an entry into vex's cache table itself: entries that arrive while the app runs are handed over with `server.replaceEntries(source, entries)`.
246
247
  - **A client-degrade app boots its own engine** — database adapter + cache backend + `ScopePolicy` — `introspect()` once at startup, memoized behind a single accessor. Prewarm at boot through the cache backend's own `set()`: key = the entry's `fingerprint`, `prismIr` = `await compile(mapping ?? { $ref: '$.result' })` (`@niscorp/prism`), plus the shape, the schema fingerprint, and `protected: true` — a seeded entry can never be replaced by a stray request. Seed the identity IR for mapping-less entries explicitly — a NULL IR falls through to the LLM mapper. Throw on duplicate names while seeding.
247
248
  - **The fingerprint is the cache key.** Every entry replays as `{ fingerprint, context }` — no shape, no intent on the wire. Id fields still follow `<entity>_id` (self-describing rows); never a shared `{ value, label }`.
248
249
  - **The mapping owns the result shape.** Vex evaluates it over `{ result, context, scope }` and returns the output verbatim — array, object, or scalar. The entry's stored shape picks what `$.result` is: an **array** shape → `$.result` is the rows (map with `$map`, or `{ $ref: '$.result' }` for identity); an **object** shape → `$.result` is the single (first) row, `null` when none came back (read fields as `$.result.monthly`, never `[0, …]`). Formatting (money, dates) lives here, in shared helpers.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,111 @@
1
1
  # @niscorp/nisc
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e9b987e: Moss holds an app's entries itself, in memory, and copies none into `vex_cache`. `server.replaceEntries(source, entries)` hands a set of entries over while a server runs. **Breaking.**
8
+
9
+ BREAKING — approved by moccadroid, 2026-10-12: an app that runs a vex engine of its own over `runtime.cache` gives that engine the entries it replays (`seedCache(cache, entries)`), and an app that wrote an entry into that table for moss to serve hands it over with `server.replaceEntries(source, entries)`.
10
+ - **Where a fingerprint resolves.** The entries an app ships are read from the build at every boot and held by the running manifest, compiled as vex replays them. The engine asks it first, and the cache table (`vex_cache`, or what `runtime.cache` hands over) only for what vex wrote itself while the app runs, such as a query `session.answerIntent` wrote. A boot still refuses an entry vex cannot run.
11
+ - **Nothing is copied into the cache table, and a row in it that no request wrote is not replayed.** Until now every entry was written there at each boot, and a replay resolved in the table. Moss now answers from the table only for a row vex wrote for a request (it carries that request's hash). The rows an earlier release wrote stay where they are and are answered from no longer: an entry the app stopped shipping is refused (`cache_miss`), where it went on answering before. Moss deletes none of them.
12
+ - **`server.replaceEntries(source, entries)`** is new, and is to entries what `server.replaceActions` is to actions: a named set, replaced whole, checked before it is taken (the schema the manifest's entries are read by, vex's authoring lint for a write, the reach it declares), frozen, and this process's alone. An entry of a set is replayed in place of one the app shipped under the same fingerprint.
13
+ - **The list of fingerprints** a feature or a discovery is told of holds the entries in force and the queries vex wrote. An app whose `runtime.cache` could not list its rows (vex's tiered cache) was told of none; it is told of the entries in force now.
14
+ - **`nisc migrate`** is as it was. Its pass for an entry that already did not fit before a run asks the cache table for a copy of that entry. Moss writes no such copy from this release on, so in a database only moss writes to, an entry first shipped from now on gets no pass. A copy an earlier release wrote, or one the app writes itself for an engine of its own, still gives one.
15
+ - **What a discovery says of an entry of the app.** `createdAt` is when this process compiled it, and `lastUsedAt` is not said: a replay of an entry moss holds stamps nothing. Both came from the row in the table before.
16
+ - **The refusal for a set that is not the caller's** says `the set "…"` where it said `the set of actions "…"`: a set holds entries too.
17
+ - The CLI and `@niscorp/nisc` change in the moss they bring; `AGENTS.md` says where entries are held.
18
+
19
+ - fc96c47: `functions(session)` returns a list of `FunctionDefinition`, `{ id, description, execute }`, and `session.functions()` lists each function a session may call with its description.
20
+
21
+ ```ts
22
+ functions: (session) => [
23
+ {
24
+ id: 'notes.archive',
25
+ description: 'Archives one of the person’s notes. Send { id }.',
26
+ execute: async ({ id }, signal) => archive(session, id, signal),
27
+ },
28
+ ],
29
+ ```
30
+
31
+ - **`id`** is what the charter's `functions` and every `fn:` endpoint name. **`execute`** is what an `fn:` endpoint calls, `(data, signal) => Promise<unknown>`, as a handler in the map was. **`description`** says what it does and what to send, for the model that writes a call to it: `session.functions()` answers `[{ id: 'notes.archive', description: '…' }]`.
32
+ - **Two functions under one id are refused** when a shell is built: `moss: functions(session) returned "auth.leave" twice.`
33
+ - **A map of handlers by id still works, and is deprecated.** The first shell a process builds says once what to change, read off what that session was given: `[moss] functions(session) returned a map of handlers, which is deprecated: return a list of { id, description, execute }.`, or, for a list, `[moss] functions with no description: auth.leave. Give each a description.` An empty description counts as none; such a function, and one given by a map, is listed by its id alone.
34
+ - `FunctionDefinition` is exported, for a helper that returns some of an app's functions.
35
+
36
+ For whoever upgrades an app, person or agent: turn the map into a list, one `{ id, description, execute }` for each handler, the id being its key; a helper that returns part of the map returns part of the list, and the parts are joined with `[...a, ...b]`. Write each description as what the function does and what to send.
37
+
38
+ - 9e6a7c3: The charter decides which of an app's functions a principal may call. **Breaking.**
39
+
40
+ BREAKING — approved by moccadroid, 2026-10-14: give every role of the charter a `functions` part, the functions its actions call, or `functions: ['*']` to keep calling all of them; a role written as a list (`member: ['home', 'settings']`) becomes `member: { actions: ['home', 'settings'], functions: [...] }`.
41
+ - **A shell holds only the functions its principal's roles grant** (`functions`, beside `actions`, `data` and `layouts`, with the same globs, `extends` and `deny`). A role with no `functions` may call none. Before, every shell held every function. A living shell follows the roles an adopt answers, as its actions do.
42
+ - **The boot is refused, with the list of what to grant**, where a role on its own, a combination people wear, or `public` holds an action (or the shells ship a fragment) that calls a function its grants miss: `"member" hold "chrome.topbar", which calls "auth.leave": their functions do not grant it`. A name with a template in it is decided when it is called.
43
+ - **`'*'` is said at boot**: `role "member" allows every function ('*'): the generated actions of the people who hold it, or a role that extends it, may call any it does not deny`.
44
+ - **A grant that names no function** (a typo) is said at the first shell a process builds, with a deny that matches nothing and a function no role grants: the names of the app's functions are known once `functions(session)` has run, and are read as that first session's.
45
+ - **A generated action or fragment may call a function its owner's roles grant**, named written out; one they do not grant is refused when it is saved and left out when a shell is built. `session.functions()` lists the functions a session may call, `{ id, description? }`, for the model that writes one.
46
+ - `@niscorp/charter` has the `functions` section, and `verifyCharter` checks it where a `functions` universe is handed in.
47
+
48
+ For whoever upgrades an app, person or agent: boot it. The refusal lists, per role combination, each function an action calls that the charter does not grant. Add those names to the `functions` of the role that holds the action, or `['*']` to the roles whose people may call everything. This is the app's permissions table: write it deliberately, and do not generate it.
49
+
50
+ - b7fabdb: Moss keeps every action's layout in the shell's layout store, under `action/<id>`. A layout that changes now reaches a screen that is already open. **Breaking.**
51
+
52
+ BREAKING — approved by moccadroid, 2026-10-12 (`action/`) and 2026-10-13 (`frame`, `canvas/`): a manifest's `layoutStore` declares no name beginning with `action/` or `canvas/` and not the name `frame`, no action's layout and no layout variant names one of those by ref, and code that takes the whole shell (`unverifiedShell(session)`) to read an action's layout off a definition reads it from the layout store (`shell.layoutStore.get('action/<id>')`).
53
+ - **An open screen follows a layout that changed, with its data kept.** A set of actions handed over with `server.replaceActions` used to reach only a screen opened afterwards: an instance that was open kept the layout it was opened with. What an open instance still keeps is the rest of its definition (its data's shape, its endpoints, its triggers).
54
+ - **Where the layout is kept does not show to an app.** An app writes an action's layout in its definition as before. `server.actions()` answers it as written, and so does a definition read off a session's shell (`session.shell.getRuntime(id).definition`), with the layout the screen is drawn with now in place. Inside nova's own shell, which `unverifiedShell(session)` hands out, the definition names its layout by `{ ref: 'action/<id>' }` and the store holds the layout, or the layout variant the person's roles hold. An action with no layout, or one whose `layout` is the name of a stored layout, is handed over as it is.
55
+ - **A session's shell hands out its layout store to read.** `set` and `delete` on `session.shell.layoutStore` throw, as `setLayout` does. They went through before.
56
+ - **`action/` is moss's in a layout store.** A manifest whose `layoutStore` declares a name beginning with it, in the app's shell or in a page, refuses the boot and says where. So does an action's layout or a layout variant that names one, at boot and when a set of actions is handed over: a layout cannot be drawn inside itself.
57
+ - **The layout of an action that leaves a shell is taken out of its store.**
58
+ - **The shell's own layout and a canvas's are kept in the store too**, under `frame` and `canvas/<id>`, and the shell draws each by that name. An app writes `layout` and a canvas's `actionLayout` as before, and a screen is drawn as before; one that is the name of a stored layout is handed over as it is. A manifest whose `layoutStore` declares `frame`, or a name beginning with `canvas/`, in the app's shell or in a page, refuses the boot and says where. So does an action's layout or a layout variant that names one of them, at boot and when a set of actions is handed over. A layout is named by a node that is `{ ref }` and nothing else: a `ref` a component carries, on its node or in its props, names none.
59
+ - **Whether a page can still do something is read through every ref.** A layout a screen names by `{ ref }` is read where the store holds it, so a two-way bound field in a stored layout the app names itself makes its page live. It was missed before, and such a page was reported as a file.
60
+ - The CLI and `@niscorp/nisc` change in the moss they bring.
61
+
62
+ ### Patch Changes
63
+
64
+ - 4dc982a: Moss keeps what was generated while the app runs for the person who saved it (a layout, an action, a fragment), and a layout an app publishes for everybody.
65
+ - **`session.artifacts`, on the session an app's functions are handed.** `save`, `list`, `history`, `restore` and `reset`. A layout is saved for the action it is for (`{ kind: 'layout', body: { ref: 'action/<id>', layout, use? } }`). It is the session's principal's alone, gets its id from moss, and is drawn on their screens while it is marked `use: true`; their open screens follow at once, with their data kept. Who may save is the charter's: the function that calls it is the app's, and the charter says who holds the action that calls that function. A session nobody is signed in to saves nothing.
66
+ - **A row is its principal's, and vex keeps it so.** Moss reads and writes the two tables as the caller, under one row rule: a caller reads and writes only the rows whose `principal` is their `userId`, and a row they insert or update carries it. No call names whose rows are meant. The same rule is part of the policy an app's charter compiles to, so a role that is granted the tables, by name or by a wildcard, reaches its own rows there and nobody else's. A row of `artifact_saves` is moss's alone to make: an app's entry that inserts or updates one is refused. An app's `behaviors` that name `artifacts` or `artifact_saves` refuse the boot.
67
+ - **`server.defaults(publisher)`, on the server: what is published for everybody.** The same five calls and the same body. A published layout is drawn for everybody who holds its action; every living shell adopts it at once, and every other process follows the generation pointer, which a publish moves. It is kept as the rows of one principal no caller is (`*`), under the same row rule, with who published in `created_by`. No session can publish: only the app's own server code reaches the call, and the charter says who holds the action that makes it. One read of what is published answers every shell a process builds: a boot makes it, and the first shell after a publish there or a move of the pointer makes it again.
68
+ - **`session.artifacts` also keeps an action and a fragment** (`kind: 'action'`, `kind: 'fragment'`). Moss gives the id, `generated.` and a uuid; a body that names another is refused. It is its owner's alone: it is in the shells built for them and in nobody else's, and in no catalog. What it defines replays reads and calls the functions its owner's roles grant: each endpoint is a `POST` to the app's vex route naming one fingerprint of a read the app ships or was handed and its owner may replay, or of a read vex wrote that its owner saved, or an `fn:` endpoint to a function its owner's roles grant (see the charter's `functions`); a write, a query a model wrote that nobody saved and every other address are refused, and so is a generated action that opens an action its owner does not hold. Moss checks the artifact; once it is in a shell it is an action or a fragment like any other, on nova's channels and in nova's merge of a fragment into the action it is wrapped around. It is checked at the save and at every shell, and one that no longer passes is left out with a line. `session.actions`, and the `actions` the `inputs` and `seeds` hooks are handed, are the ids the shell was built with, so they hold a principal's generated actions too.
69
+ - **`session.draft`, on the same session: what is being generated in one shell, before it is saved.** `put` takes what `save` takes, runs the same check with the draft laid over what is in force and against what that one shell can draw, and answers when that shell has it, so a function puts an action and opens it on its next line. `list`, `remove` and `discard`; each put or removal is a numbered step, and `show(step)` has the shell show the draft as it was after it; `save` is one save of all of it under the ids it had. A draft is in no other shell, is saved nowhere, and is gone when its shell is.
70
+ - **An id beginning with `generated.` is moss's to give.** A boot and `replaceActions` refuse an action under one, and the manifest's parse refuses a shell's fragment under one.
71
+ - **No caller's `userId` is `*`, and no caller's scope carries `artifactSavedBy`.** For one whose sign-in or `scope` says either, no shell is built and `/api/vex` answers 500.
72
+ - **Under an action's ref a shell draws**, the first of these there is: the person's own layout marked in use, the layout variant their roles hold, the published layout marked in use, the build's.
73
+ - **Every save is a version, and moss changes and deletes none.** Going back to a version is a new save that holds a copy of it; a reset is a new save that says none is in force, and the layout under it is drawn again.
74
+ - **A layout is checked when it is saved and when a shell is built**: its shape, that the action it is for has a layout of its own and is held by whom it is saved for, its components against what the app's shells have, that it names no ref moss keeps a layout under (`action/…`, `canvas/…`, `frame`), and what nova's audit finds of its action with the layout in place and not of the action as it is built. A save is refused with a sentence. A saved layout that no longer passes is left out with a line in the log, and what is under it is drawn.
75
+ - **Two tables, in migration `nisc.moss/5`: `artifact_saves` and `artifacts`.** A deployment that boots with `migrations: 'verify'` runs `nisc migrate` first, as for every migration. An app whose database already has a table by either name renames its own before it takes this release.
76
+ - **The fabric carries one more message, `adopt`**: it names a principal and tells a process to bring the shells it holds for them level. A process that runs an earlier release drops it as a message it does not know, as it drops every such message.
77
+ - **The layouts of the app's own shell are saved the same way: its own under `ref: 'frame'`, a canvas's under `ref: 'canvas/<id>'`**, by a session for its principal and by `server.defaults` for everybody. The app's own shell draws the person's own in use, else the published one, else the one its manifest declares; their open screen follows at once. A page draws the layout it declares. A shell manifest with no `layout`, or a canvas with no `actionLayout` of its own, has none to take the place of, and a save for it is refused. It is checked as a layout for an action is, against the components of the app's own shell and without nova's audit. A frame that leaves a canvas of the shell out is saved as it is: that canvas is drawn nowhere.
78
+ - **A canvas that only a layout's slot draws goes when the slot goes.** Nova gives a shell a canvas when something is first pushed onto it. Where a layout in force for a principal holds a canvas slot with a written-out `canvasId` for a canvas their shell's manifest does not declare, and then no layout does (the saved layout was reset, the action left the shell), an open shell loses that canvas with what is mounted on it. It stayed before, drawn nowhere. The canvases a manifest declares are never taken out, and moss gives a shell none.
79
+ - **A read vex wrote is kept the same way** (`{ kind: 'read', id: 'fp_…' }`, no body): moss takes the query and the Prism config of its mapping from vex's table. It is answered to the person who saved it and to nobody else (never to a caller nobody is signed in for), held to vex's limits as a query vex wrote is, it survives a schema change that does not touch what it reads, and their generated actions may replay it. One whose query no longer runs is left out with a line, with the generated actions that replay it. Moss's data layer hands vex's table only what vex wrote for a request: a saved read vex replays is not copied into it.
80
+ - Not built yet: publishing an action, a fragment or a read; a layout of a page's shell.
81
+ - The CLI and `@niscorp/nisc` change in the moss they bring. `AGENTS.md` gains rule 7b: how a layout, an action or a fragment generated while the app runs is saved or kept in a draft, how a layout is published for everybody, and that the charter decides who may do either.
82
+
83
+ - c7c4f9b: A nova shell keeps actions and fragments in one registry. **Breaking.**
84
+
85
+ BREAKING — approved by moccadroid, 2026-10-13: `shell.registerFragment` is gone, and an action and a fragment no longer share an id.
86
+ - **`shell.registerAction(definition)` takes an action or a fragment**, told apart by the fragment's `kind: 'fragment'`, and **`shell.removeAction(id)` takes either out**. There was no call to take a fragment out. Taken out, a fragment is unknown to a later `with: [...]`; an instance that was opened with it keeps the definition it was composed into, and `back` skips a position that was opened with it, as it skips one it cannot put back.
87
+ - **A fragment handed to `createShell` is validated**, as an action is: one that does not parse throws `DefinitionValidationError` at construction. Only `registerFragment` validated one before.
88
+ - **An id names an action or a fragment, never both.** `createShell` and `registerAction` throw `DefinitionValidationError` for an id that is already the other kind's.
89
+ - Opening a fragment by itself still throws `UnknownActionError`, and a `with: [...]` that names an action still throws `UnknownFragmentError`. `ShellConfig` keeps `actions` and `fragments`.
90
+ - **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.
91
+
92
+ **What to change:** `shell.registerFragment(fragment)` becomes `shell.registerAction(fragment)`. Give an action and a fragment that share an id two ids.
93
+
94
+ - Updated dependencies [0a7beae]
95
+ - Updated dependencies [e9b987e]
96
+ - Updated dependencies [fc96c47]
97
+ - Updated dependencies [9e6a7c3]
98
+ - Updated dependencies [b7fabdb]
99
+ - Updated dependencies [4dc982a]
100
+ - Updated dependencies [c7c4f9b]
101
+ - Updated dependencies [4068f98]
102
+ - @niscorp/moss@0.7.0
103
+ - @niscorp/cli@0.7.0
104
+ - @niscorp/charter@0.1.4
105
+ - @niscorp/nova@0.4.0
106
+ - @niscorp/loom@0.4.0
107
+ - @niscorp/vex@0.4.0
108
+
3
109
  ## 0.6.1
4
110
 
5
111
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niscorp/nisc",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "The nisc platform as one known-compatible set: every @niscorp package, each pinned to the exact version it was released with.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -19,18 +19,18 @@
19
19
  "node": ">=22.12"
20
20
  },
21
21
  "dependencies": {
22
- "@niscorp/charter": "0.1.3",
23
- "@niscorp/cli": "0.6.0",
22
+ "@niscorp/cli": "0.7.0",
23
+ "@niscorp/loom": "0.4.0",
24
24
  "@niscorp/cortex": "0.1.4",
25
- "@niscorp/loom": "0.3.1",
26
- "@niscorp/moss": "0.6.0",
27
- "@niscorp/nova": "0.3.1",
25
+ "@niscorp/moss": "0.7.0",
26
+ "@niscorp/charter": "0.1.4",
27
+ "@niscorp/nova": "0.4.0",
28
+ "@niscorp/prism": "0.3.1",
28
29
  "@niscorp/signal": "0.1.4",
29
- "@niscorp/strata": "0.1.7",
30
- "@niscorp/tide": "0.1.7",
31
- "@niscorp/vex": "0.3.1",
32
30
  "@niscorp/solid": "0.1.2",
33
- "@niscorp/prism": "0.3.1"
31
+ "@niscorp/strata": "0.1.7",
32
+ "@niscorp/vex": "0.4.0",
33
+ "@niscorp/tide": "0.1.7"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "zod": "^4.2.0"