@niscorp/nisc 0.3.2 → 0.4.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 +17 -10
  2. package/CHANGELOG.md +122 -0
  3. package/package.json +10 -10
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Building an application on nisc
2
2
 
3
- > **Reading this from `node_modules/@niscorp/nisc/`?** It is the rulebook for the nisc version installed beside it, shipped with it. Where it names a path in the nisc repository, look here instead: `packages/<name>/README.md` and `DESIGN.md` are `node_modules/@niscorp/<name>/`; `/STYLE_GUIDE.md` is beside this file; the lab apps it holds up as examples (`apps/lab/atrium`, `mythos`, …) and `docs/` are at [github.com/moccadroid/niscorp](https://github.com/moccadroid/niscorp). An app made by `npm create nisc` points its own `AGENTS.md` here.
3
+ > **Reading this from `node_modules/@niscorp/nisc/`?** It is the rulebook for the nisc version installed beside it, shipped with it. Where it names a path in the nisc repository, look here instead: `packages/<name>/README.md` and `DESIGN.md` are `node_modules/@niscorp/<name>/`; `/STYLE_GUIDE.md` is beside this file; the lab apps it holds up as examples (`apps/lab/lyceum`, …) and `docs/` are at [github.com/moccadroid/niscorp](https://github.com/moccadroid/niscorp). An app made by `npm create nisc` points its own `AGENTS.md` here.
4
4
 
5
5
  > **Changing nisc itself — a package, not an app built on it?** The `@niscorp` packages are live on npm and may be in use by third parties. No change may break an app that works today, and a breaking change is never yours to decide: it is a last resort that needs the maintainer's explicit approval *before* it is written. Read `/STYLE_GUIDE.md`, "The packages are live", first.
6
6
 
@@ -34,7 +34,7 @@ Collect the answers **before implementation starts**. The build is meant to run
34
34
  - **D2 — Environment.** What the runtime is handed: the database (PGlite for dev and demos, Postgres for real), a cache backend, a session verifier. For a client-degrade app: what serves the endpoints — in-memory fixtures, PGlite, a real backend, mixed.
35
35
  - **D3 — Reads.** Vex query endpoints, or plain endpoints with hand-written handlers. *Whether* to use Vex is the decision; *how* is fixed (see "Using Vex").
36
36
  - **D4 — Writes.** Vex mutation entries — the closed grammar, authored like reads and replayed as `{ fingerprint, context }` — or plain endpoint handlers. Either way a write is an endpoint; nothing writes inline.
37
- - **D5 — Routing.** What a path leads to. Three separate questions: whether the first screen is **drawn on the server** (it then needs the cookie copy of the session token, rule 12 — and a page drawn for a signed-in person may be kept by nothing between the server and their browser); which paths are **pages** rather than the app, and whether those are served live or written out as files (a file is only ever the page as nobody sees it; the build reports which pages can still do something and so want a server beside them); and whether the app's own shell state syncs to the address bar. Consequences to say out loud: a page keeps no shell, so anything on it that can be pressed gets a shell per connection, not per person; and the app's shell is one per person across every tab they have open, so a URL cannot hold a position inside it that two tabs disagree about.
37
+ - **D5 — Routing.** What a path leads to. Three separate questions: whether the first screen is **drawn on the server** (it is drawn for whoever the browser's session cookie says, rule 12 — and a page drawn for a signed-in person may be kept by nothing between the server and their browser); which paths are **pages** rather than the app, and whether those are served live or written out as files (a file is only ever the page as nobody sees it; the build reports which pages can still do something and so want a server beside them); and whether the app's own shell state syncs to the address bar. Consequences to say out loud: a page keeps no shell, so anything on it that can be pressed gets a shell per connection, not per person; and the app's shell is one per person across every tab they have open, so a URL cannot hold a position inside it that two tabs disagree about.
38
38
 
39
39
  Any other fork not covered by a rule gets one of two treatments. A genuine open choice is surfaced like a decision point. A choice that plainly follows from answers already given — the canvas arrangement implied by the views, one form per entity, delete-with-confirm — is **derived**: recorded in `PLAN.md` for review, not asked. When in doubt, ask.
40
40
 
@@ -68,7 +68,7 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
68
68
  | `signal` | LLM calls | — |
69
69
  | `cortex` | agents, tools, orchestration | — |
70
70
  | `solid` | streaming structured LLM output into UI | — |
71
- | `cli` | the `nisc` command: `dev`, `build` (bundle, then say how each path is served — a file, or a server, and why), `export` (the site as a folder; only ever the page as nobody sees it), `start` (the built terminal, pages drawn), `check`. One file, `nisc.config.ts`, says how the app boots and how a screen is drawn | configuration of anything derivable — which paths exist and what each needs is read off the app |
71
+ | `cli` | the `nisc` command: `dev`, `build` (bundle, then say how each path is served — a file, or a server, and why), `export` (the site as a folder; only ever the page as nobody sees it), `start` (the built terminal, pages drawn), `check`, `migrate` (the tables' pending migrations, applied only if the app's entries fit the result). One file, `nisc.config.ts`, says how the app boots and how a screen is drawn | configuration of anything derivable — which paths exist and what each needs is read off the app |
72
72
  | `strata` | versions: an app's tables as ledgered sequences; stored and submitted documents stamped and upgraded through grammar sequences; `strata upgrade` for artifacts in source (see Versions) | editing a migration that has run — append one |
73
73
 
74
74
  ## Rules
@@ -86,11 +86,12 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
86
86
  **Data**
87
87
 
88
88
  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.
89
- 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.
89
+ 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`).
90
90
  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
91
  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.
92
92
  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.
93
93
  9. Formatting and derivation live in Prism transforms, never in components and never in action code.
94
+ 9a. File pickers live in the kit. A file never goes over the socket; the app saves it wherever it wants.
94
95
 
95
96
  **Principals**
96
97
 
@@ -99,7 +100,7 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
99
100
  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.
100
101
  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.
101
102
  11b. A page is drawn for whoever asks, and what it has for them is ring 1 like everything else: a strip naming the signed-in person is an action a member is granted, on a canvas whose candidates a stranger is granted none of. A page never knows less about a signed-in person than the app does, and never keeps a shell for them. **Who may keep a drawn page follows from who asked, and from nothing else** — drawn for nobody it is the same for everybody and may be cached or written to a file; drawn for somebody it is theirs. No page, route or app declares which; a static build only ever asks for the page as nobody.
102
- 12. Auth is a session token, nothing else — magic link is the default strategy; there are no username/password pairs in a nisc app if it can be avoided. Login is the anonymous principal's application. Session lifecycle is a **capability, not a channel**: login and sign-out are ordinary `fn:` endpoints calling `session.grant(token)` and `session.revoke()`, and the terminal reconnects as the new principal. The server consumes sessions and never mints identity. A page request can carry one credential, a cookie: when pages are drawn on the server the wire keeps a **copy** of the token there (`browserEnv({ cookie: true })`), written and cleared with the stored one, and it is read for one thing only — choosing whose screen to draw. Every other surface still wants the token itself. A sign-in handoff that stores the token stores the copy too, or the page it redirects to is drawn for nobody.
103
+ 12. Auth is a session token, nothing else — magic link is the default strategy; there are no username/password pairs in a nisc app if it can be avoided. Login is the anonymous principal's application. Session lifecycle is a **capability, not a channel**: login and sign-out are ordinary `fn:` endpoints calling `session.grant(token)` and `session.revoke()`, and the terminal reconnects as the new principal. The server consumes sessions and never mints identity. A page served by its app does not hold its session: the **browser** does, in a cookie no script can read, which moss writes in its answer to the socket's upgrade and reads for two things only — the upgrade from the app's own page, and choosing whose screen to draw. No route answers to it. Nothing in an app stores a token: not `localStorage`, not a cookie script can read. A sign-in the app answers over HTTP (a redeemed link, a provider's callback) sets that cookie on its response with `sessionCookies(request, token)` and redirects; it never hands the token to the page. A terminal that is not a page of its app (a process, another origin) holds its own token and offers it.
103
104
 
104
105
  **Schemas**
105
106
 
@@ -113,7 +114,7 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
113
114
 
114
115
  **Versions** (`@niscorp/strata`; the why is `packages/strata/DESIGN.md`)
115
116
 
116
- 17. No package — and no app, where it can help it — runs DDL outside a strata sequence. Tables change by APPENDING a migration to their owner's sequence; a migration that has run is history, checksummed in the ledger, and editing it makes every database that ran it refuse to boot. The first migration of a sequence may be the old `IF NOT EXISTS` DDL verbatim — split with `sqlSteps(ddl)`, never on every `;` — and that is how an existing database adopts. An app's own tables are its `<app>.app` sequence, migrated by its runtime before the seed (lyceum is the example).
117
+ 17. No package — and no app, where it can help it — runs DDL outside a strata sequence. Tables change by APPENDING a migration to their owner's sequence; a migration that has run is history, checksummed in the ledger, and editing it makes every database that ran it refuse to boot. The first migration of a sequence may be the old `IF NOT EXISTS` DDL verbatim — split with `sqlSteps(ddl)`, never on every `;` — and that is how an existing database adopts. An app's own tables are its `<app>.app` sequence, migrated by its runtime before the seed (lyceum is the example). An app deployed with `nisc migrate` hands that sequence to moss instead — `runtime.tables`, with `TIDE_SEQUENCE` when it keeps tide's tables — and the function that opens its runtime migrates and seeds nothing: what it changes itself is changed before the step can check it. Rows an app needs in order to start are written by a migration step, in the same transaction as its tables; a seed that runs at start is for development and demos.
117
118
  18. A grammar change is a migration. Changing a schema behind a document kind — nova's actions and layouts, Prism's config, an app kit's component props (`NiscApp.grammars`) — appends to that grammar's sequence: an empty marker for an addition (strict readers must still refuse the newer documents), document steps (a Prism config over ONE node; the grammar's embeddings find every node) for anything else. `pnpm check:grammars` refuses the change until it does, and checks every captured document still upgrades and parses. A grammar version is history — its migrations, and every corpus file captured at an earlier stamp. Its snapshot is its fingerprint: when the grammar did not change but what records it did (a validator describing the same schema differently, strata reading it differently), the current version is re-recorded with `pnpm strata:snapshot --rebaseline` (an app kit: its own snapshot script with `--rebaseline`) in a commit that changes nothing else, and only while the corpus passes — never edited by hand, never answered with a marker.
118
119
  19. Stored and submitted documents carry the stamp they were written at and are upgraded where they are read. Ahead of the reader on any grammar the reader was given is refused (`TOO_NEW`), never guessed at: the reader upgrades first — host before add-on, server before terminal.
119
120
  20. An app's source records its grammars in `strata.lock.json`. When a grammar moves: `pnpm strata upgrade` writes the exact expected JSON and a report; edit what it lists; `pnpm strata verify` holds the edit to that JSON and only then moves the lock. Never edit the lock by hand, and never "fix" an edit toward a green verify by any route but the expected JSON. `pnpm check:sources` fails an app whose source is behind.
@@ -231,12 +232,15 @@ export const todosOpen: SeedEntry = {
231
232
  fields: [{ field: 'todos.id', as: 'todo_id' }, 'todos.title', 'todos.due_date'],
232
233
  filter: { eq: ['todos.done', false] },
233
234
  sort: [{ field: 'todos.due_date', dir: 'asc' }],
235
+ limit: 200, // how many rows this list is for — state it on every list
234
236
  },
235
237
  mapping: { /* Prism over { result }: the rows (array shape) or the one row (object shape); omit when the DSL already aliases to the shape */ },
236
238
  };
237
239
  ```
238
240
 
241
+ - **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.
239
242
  - **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.
243
+ - **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.
240
244
  - **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.
241
245
  - **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.
242
246
  - **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 }`.
@@ -257,6 +261,7 @@ Stack-independent conventions:
257
261
  - **One form action per entity, create and edit.** Loaded bare it creates; loaded with the record's raw fields seeded it edits. Keep raw values (numbers, ISO dates, ids) alongside `*_display` strings so forms round-trip.
258
262
  - **Interaction = `ref` + trigger.** Layout nodes carry `ref`; triggers catch `{ event, ref }` and run steps. Event payloads flow via `@event.payload`; no callbacks in props.
259
263
  - **A composed surface is rows, not a layout.** Where several things coexist — a home, a launcher strip, an agent's column — use a `list` canvas seeded by `seeds` from resolved rows, and let each action render itself. Which surfaces land where is the *row's* business (its canvas), not a second authored list. Whether an action can appear that way is read off its own contract — an action that declares a collapse key in its `input` can render small, so a surface that ships one joins the composition the moment its rows land, with no registry to update.
264
+ - **What should still be there is a row, not a screen.** A server shell is memory: a deploy, a restart or an idle spell replaces it with one built from the manifest and whatever the app reads from rows as it builds — `inputs`, `seeds`, an action's mount hook. So what a person should find again is a row, and an action that shows nothing keeps a canvas on it: its mount reads the row and `replace`s or `reconcile`s the canvas (lyceum's `stage.deck` keeps the stage on the slide its `deck` row names). A screen a person opened that no row describes — a sheet, a record pushed over a list — is gone after a rebuild, with whatever was typed into it. Shell state is the truth of what is on screen now (rule 4); it is not kept.
260
265
  - **Seeds choose their clock.** A seed pins a fixed reference date or shifts with the wall clock — per what the seed is for: a regression fixture pins, a demo that must look alive today shifts. Either way the app compares against injected context (`$.today`, per request under moss) — never a wall-clock read inside a transform, query, or layout.
261
266
  - **The app is a manifest; only degrade shells are factories.** Under moss the app is `defineApp({...})` and the server builds one durable shell per principal — dev checks boot the same manifest with a dev runtime. A client-degrade app builds its shell as `createAppShell(deps)` with everything environmental injected, so checks construct the real app with real deps and nothing leaks between runs.
262
267
 
@@ -353,7 +358,7 @@ never needed still moved a boundary while it stood there.
353
358
 
354
359
  ## Layout of an app
355
360
 
356
- One arrangement — atrium's, the exemplar (`apps/lab/atrium`; relay is the older sibling and still worth reading for the surfaces it targets). `app/` holds **artifacts only**: every file there is pure, schema-valid JSON authored in TS (a check enforces it). Code lives outside `app/` — the environment in `db/`, helpers in `lib/`, the server glue and fns in `server/`, the component kit in `ui/`.
361
+ One arrangement. `app/` holds **artifacts only**: every file there is pure, schema-valid JSON authored in TS (a check enforces it). Code lives outside `app/` — the environment in `db/`, helpers in `lib/`, the server glue and fns in `server/`, the component kit in `ui/`.
357
362
 
358
363
  Three naming rules make the tree legible, and they're the same rules the artifact library will type its rows by:
359
364
 
@@ -402,6 +407,8 @@ strata.lock.json which grammar version the source is written in (rule
402
407
  moved only by `pnpm strata verify`
403
408
  ```
404
409
 
410
+ Apps to read: lyceum (`apps/lab/lyceum`) for a moss server app; nisc-website and moccadroid-website for an app with its own shell. atrium, lyra and relay are older: read one for a surface it targets, not as a model.
411
+
405
412
  Why the split: the manifest's data fields are on their way to becoming library rows (deploy = a write, not a rebuild), so keeping them pure and schema-valid now — with `db/` and `lib/` code held firmly outside `app/` — is what makes the tree row-ready. A client-degrade app replaces `server/` with whatever serves its endpoints (per D2) and boots its shell factory from the entry point.
406
413
 
407
414
  `ui/` is the app's renderer kit, and which renderer is a choice: a React or DOM terminal in a browser, a tty or ink one in a console. The core is surface-blind, so the kit is the only place that isn't — an app targeting two surfaces ships two kits against one registry contract, and nothing above `ui/` changes.
@@ -422,7 +429,7 @@ What changes is only who assembles and serves:
422
429
 
423
430
  Say this out loud in the interview, because it is the part a user cannot infer: **in a degrade app the charter is not enforcement.** Ring 1 still decides what exists in the shell, and that is honest UI, but the data is in the browser and the policy is in the browser with it. Nothing here is a security boundary. An app that needs one needs a server (D1) — offer that rather than a scope rule that looks like a lock.
424
431
 
425
- `fable` and `mythos` are the reference degrade apps. Both predate the `app/`-is-artifacts-only tree above and are arranged differently (`nova/`, `api/`, `vex/`, `boot.ts`) — read them for the boot and engine wiring, not for the layout.
432
+ `fable` and `mythos` predate the `app/`-is-artifacts-only tree above and are arranged differently (`nova/`, `api/`, `vex/`, `boot.ts`) — read them for the boot and engine wiring, not for the layout.
426
433
 
427
434
  ## Order of work
428
435
 
@@ -443,7 +450,7 @@ A `dev/all-checks.ts` runs the suite and is the app's `check` script. It spawns
443
450
  A review pass checks, in order:
444
451
 
445
452
  1. Typecheck passes; every dev check passes; the manifest boots — moss refuses an incoherent charter, and a boot refusal is a finding, not an environment problem.
446
- 2. No renderer code (JSX, a tty kit) outside `ui/` and the entry point. `server/document.ts` hands the kit's registry to moss to draw a page with; it imports the kit and contains no renderer code of its own. No `fetch` outside the endpoint layer. No formatting (`Intl`, `toLocaleString`, date libs) outside Prism transforms.
453
+ 2. No renderer code (JSX, a tty kit) outside `ui/` and the entry point. `server/document.ts` hands the kit's registry to moss to draw a page with; it imports the kit and contains no renderer code of its own. No `fetch` outside the endpoint layer, except to send a file (rule 9a). No formatting (`Intl`, `toLocaleString`, date libs) outside Prism transforms.
447
454
  3. Every action an opener loads with input declares it in the definition's `input`, and the declared fields are a subset of the action's `data` keys.
448
455
  4. Every displayer of an entity stays current after a write: its read is `refresh: 'reactive'`, or the write's success path emits a change channel the displayer listens to.
449
456
  5. No component name contains a domain noun; no component imports shell, action, or data code.
@@ -451,7 +458,7 @@ A review pass checks, in order:
451
458
  6a. Every artifact the manifest carries is pure JSON and parses its schema — a function, a `Date`, an `undefined`, or a class instance in an artifact is a code file masquerading as data. The manifest's declared code fields (`scope`, `functions`, `onSession`, `runs`, and the shell's `inputs` and `seeds` hooks) are the only exceptions; they live outside the artifact tree and are validated by running, not by this check.
452
459
  6b. Session code sits in the seam that describes it (rule 7a): no handler registered on `functions` that nothing calls. Tenancy resolves through `scope` and engine-side behaviors, never from a request field.
453
460
  6c. No role holding a data wildcard reaches the engine's tables, and the charter check resolves against the universe boot does (rule 10a).
454
- 6d. Where pages are drawn on the server: a check opens the drawn page with the real wire and target and the terminal adopts it with nothing said (rule 2a), at every window width the kit distinguishes; a page drawn for somebody goes out `private, no-store`; and every sign-in handoff sets the cookie copy with the token (rule 12).
461
+ 6d. Where pages are drawn on the server: a check opens the drawn page with the real wire and target and the terminal adopts it with nothing said (rule 2a), at every window width the kit distinguishes; a page drawn for somebody goes out `private, no-store`; and every sign-in the app answers over HTTP sets the session cookie on its response and hands the page nothing (rule 12).
455
462
  7. Style guide bans (rule 16): `any`, `enum`, classes, default exports, `function` declarations by grep; type assertions and non-null `!` at the lint/typecheck level — grep can't tell negation from assertion. Declared shim exceptions are honored.
456
463
  8. Decision points: each of D1–D5 is recorded in the app's `PLAN.md` with its tier — answered, delegated by name, or derived — never assumed silently.
457
464
  9. Versions (rules 17–20): `pnpm check:grammars` and `pnpm check:sources` pass; no DDL outside a sequence; no edited migration, snapshot, early-stamp corpus file or hand-moved lock.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,127 @@
1
1
  # @niscorp/nisc
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f834f3f: `evaluate` checks a config object once and only evaluates it after that; `prismTransform` is gone. **Breaking.**
8
+
9
+ BREAKING — approved by moccadroid, 2026-10-09: replace `prismTransform` with `evaluate` (`transform: evaluate`), and pass limits as `{ limits: … }`.
10
+
11
+ 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 wrapped `evaluate`, which checked the config on every call. There are two now, with one job each.
12
+
13
+ **`evaluate(config, source, options?)`** — for a config a program holds.
14
+ - The first time it is handed a config object it checks it and keeps its tree; the second time it prepares the tree as `compile` would; 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.
15
+ - It is kept **by the object**: a config changed in place after its first call is not read again. Hand over a new object.
16
+ - Both arguments are `unknown`, so it is what a host's transform seam takes: `createTide({ transform: evaluate })`, `createUpgrader(grammars, { transform: evaluate })`.
17
+ - The third argument is `{ limits?, check? }`. Limits were the third argument themselves: `evaluate(config, source, { maxSteps: 100 })` is now `evaluate(config, source, { limits: { maxSteps: 100 } })`.
18
+ - `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 holding `undefined`, a function, a `Date` or a non-finite number is refused with `E_TYPE`).
19
+ - An invalid config is a `PrismError` (`E_SCHEMA`) naming the part that is wrong, however deep the config. `prismTransform` threw zod's own error, and a `RangeError` for a config nested far past the limit.
20
+
21
+ `evaluateSafe` takes the same arguments.
22
+
23
+ **`compile` and `execute`** — for a config that is stored. Unchanged.
24
+
25
+ **`prismTransform` is removed**, from `@niscorp/prism` and from `@niscorp/prism/migrations`. What it added over `evaluate` was a check that the source is plain JSON; that check is now `check: 'always'`, and a seam's source is taken as it is.
26
+
27
+ **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.
28
+
29
+ moss's shell and nova's `$prism` binding called `evaluate` on every use and so paid the check each time: an endpoint's request and response under moss, and a `$prism` value 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 name `evaluate` where they named `prismTransform`.
30
+
31
+ **What to change**
32
+ - `import { prismTransform } from '@niscorp/prism'` or `'@niscorp/prism/migrations'` → `import { evaluate } from '@niscorp/prism'`, and `transform: prismTransform` → `transform: evaluate`. Every app made by `create-nisc` has this in `src/dev/strata.ts`; the app's typecheck and `npm run strata` both stop on that line until it is changed.
33
+ - A third argument of limits becomes `{ limits: … }`.
34
+ - 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: hand `evaluate` the config itself.
35
+ - Code that changes a config object in place and evaluates it again must hand over a new object, or pass `{ check: 'always' }`.
36
+ - A host that relied on `prismTransform` refusing a source that is not plain JSON checks it itself, or passes `{ check: 'always' }`.
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies [023bcf9]
41
+ - Updated dependencies [1db6df4]
42
+ - Updated dependencies [6d098e4]
43
+ - Updated dependencies [8b4d97e]
44
+ - Updated dependencies [f834f3f]
45
+ - Updated dependencies [b084e30]
46
+ - Updated dependencies [663d724]
47
+ - Updated dependencies [6512d91]
48
+ - Updated dependencies [7d682b9]
49
+ - Updated dependencies [d2e9d22]
50
+ - Updated dependencies [e0e8abc]
51
+ - Updated dependencies [7fe1135]
52
+ - @niscorp/prism@0.3.0
53
+ - @niscorp/moss@0.4.0
54
+ - @niscorp/nova@0.3.0
55
+ - @niscorp/vex@0.3.0
56
+ - @niscorp/loom@0.3.0
57
+ - @niscorp/cli@0.4.0
58
+ - @niscorp/tide@0.1.5
59
+ - @niscorp/strata@0.1.5
60
+
61
+ ## 0.3.3
62
+
63
+ ### Patch Changes
64
+
65
+ - 78a563f: A new app's terminal entry is `browserEnv()`, and AGENTS.md rule 12 says where a session is kept: in the browser's own cookie, which no script can read — not in `localStorage`, and not in a cookie copy.
66
+
67
+ The rule used to tell an app that draws its pages on the server to keep a script-readable copy of the session token in a cookie (`browserEnv({ cookie: true })`) and to have every sign-in handoff store that copy too. moss now keeps the session itself, where the page cannot reach it, so the rule says the opposite: nothing in an app stores a token, and a sign-in the app answers over HTTP sets the session cookie on its response (`sessionCookies(request, token)`) and hands the page nothing. D5 and check 6d follow.
68
+
69
+ **What to change:** nothing in an app that exists — `browserEnv({ cookie: true })` still compiles, and the option does nothing now.
70
+
71
+ - 9a30cfc: `AGENTS.md` no longer calls one lab app the exemplar. "Layout of an app" describes one arrangement and then says which apps to read: lyceum for a moss server app, nisc-website and moccadroid-website for an app with its own shell; atrium, lyra and relay are older, and are read for a surface they target, not as a model. `fable` and `mythos` are no longer called the reference degrade apps. And a working pattern says what a rebuilt server shell holds: what the app reads from rows as it builds — `inputs`, `seeds`, an action's mount hook — and nothing else, so a screen a person opened that no row describes is gone after a deploy, a restart or an idle spell.
72
+
73
+ **What to change:** nothing.
74
+
75
+ - fc50d20: `AGENTS.md` says where files go. Rule 9a: file pickers live in the kit; a file never goes over the socket; the app saves it wherever it wants. The review pass's "no `fetch` outside the endpoint layer" gains its one exception, to send a file. No code changed.
76
+
77
+ A picker that puts a file in its model value sends it over the socket as base64, and the tree brings it back on every render of that canvas. Measured against a moss server: one 15 MB file was 20 MB up and 100 MB down across five renders. Sent by the picker to a route the app mounts on its server, or to a system the app already has, the same file put nothing on the socket.
78
+
79
+ moss gains a test and nothing else: a route an app adds to the built server reads who is asking, as moss's own surfaces do.
80
+
81
+ **What to change:** nothing for the rule itself. But this release also limits a message a terminal sends to 256 KB (the moss entry on the socket's message limit), so a picker that emits a file's bytes as its model value now works only for a file under about 190 KB: send the file from the picker, as rule 9a says, or raise the limit where the app attaches the socket.
82
+
83
+ - 64d3765: AGENTS.md: `nisc migrate`, where an app deployed with it puts its tables, and where the rows it needs to start are written.
84
+
85
+ The toolbox row for `cli` names the command. Rule 17 gains two sentences. An app deployed with `nisc migrate` hands its `<app>.app` sequence to moss (`runtime.tables`, with `TIDE_SEQUENCE` when it keeps tide's tables), and the function that opens its runtime migrates and seeds nothing: what it changes itself is changed before the step can check it. And rows an app needs in order to start are written by a migration step, in the same transaction as its tables; a seed that runs at start is for development and demos.
86
+
87
+ **What to change:** nothing.
88
+
89
+ - 9311312: AGENTS.md: every list states its `limit`, and a parent and its children are one batch.
90
+
91
+ The worked `todos/open` entry stated no `limit`, so the guide's own example was a list the engine would stop at 100 rows. It states one now, and "Using Vex" says why: a seeded entry reads at most the `limit` it states, and a list without one gets the default.
92
+
93
+ "Using Vex" also gains the batch form for a write that creates a parent and its children: `{ $returned: 'table.column' }` reads the row an earlier statement of the same batch wrote, and the bullet says what the reply of such a batch looks like.
94
+
95
+ **What to change:** nothing.
96
+
97
+ - Updated dependencies [7f8d202]
98
+ - Updated dependencies [8abbb96]
99
+ - Updated dependencies [78a563f]
100
+ - Updated dependencies [51dc5ce]
101
+ - Updated dependencies [da42472]
102
+ - Updated dependencies [a91b5db]
103
+ - Updated dependencies [812c464]
104
+ - Updated dependencies [770f754]
105
+ - Updated dependencies [61a4060]
106
+ - Updated dependencies [8012cd8]
107
+ - Updated dependencies [10875eb]
108
+ - Updated dependencies [fc50d20]
109
+ - Updated dependencies [2b5fd83]
110
+ - Updated dependencies [1e5ad2d]
111
+ - Updated dependencies [2011951]
112
+ - Updated dependencies [2c57a38]
113
+ - Updated dependencies [2d20daf]
114
+ - Updated dependencies [b711c38]
115
+ - Updated dependencies [3d8518c]
116
+ - Updated dependencies [b711c38]
117
+ - Updated dependencies [1903342]
118
+ - @niscorp/nova@0.2.2
119
+ - @niscorp/prism@0.2.2
120
+ - @niscorp/strata@0.1.4
121
+ - @niscorp/cli@0.3.2
122
+ - @niscorp/moss@0.3.3
123
+ - @niscorp/vex@0.2.3
124
+
3
125
  ## 0.3.2
4
126
 
5
127
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niscorp/nisc",
3
- "version": "0.3.2",
3
+ "version": "0.4.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/cli": "0.3.1",
23
- "@niscorp/cortex": "0.1.3",
24
- "@niscorp/loom": "0.2.2",
25
22
  "@niscorp/charter": "0.1.2",
26
- "@niscorp/moss": "0.3.2",
27
- "@niscorp/prism": "0.2.1",
28
- "@niscorp/nova": "0.2.1",
23
+ "@niscorp/cli": "0.4.0",
24
+ "@niscorp/loom": "0.3.0",
25
+ "@niscorp/moss": "0.4.0",
26
+ "@niscorp/cortex": "0.1.3",
27
+ "@niscorp/nova": "0.3.0",
29
28
  "@niscorp/signal": "0.1.3",
30
29
  "@niscorp/solid": "0.1.2",
31
- "@niscorp/strata": "0.1.3",
32
- "@niscorp/tide": "0.1.4",
33
- "@niscorp/vex": "0.2.2"
30
+ "@niscorp/strata": "0.1.5",
31
+ "@niscorp/prism": "0.3.0",
32
+ "@niscorp/tide": "0.1.5",
33
+ "@niscorp/vex": "0.3.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "zod": "^4.2.0"