@niscorp/nisc 0.1.0 → 0.2.1
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.
- package/AGENTS.md +455 -0
- package/CHANGELOG.md +25 -0
- package/README.md +9 -0
- package/STYLE_GUIDE.md +230 -0
- package/package.json +7 -7
package/AGENTS.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# Building an application on nisc
|
|
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.
|
|
4
|
+
|
|
5
|
+
For agents generating or reviewing a nisc application. This file states the application-level rules, and it presumes no app that came before it — but it is not the whole specification. The **grammars** live in the package docs, and they are required reading, not background: a layout node, an `ActionDefinition`, a Prism config, a charter document and a Vex DSL are each defined in their own package's `README.md` and `DESIGN.md`. Read the doc for a package before authoring that package's artifact. This file governs how they compose into an application.
|
|
6
|
+
|
|
7
|
+
## The thesis
|
|
8
|
+
|
|
9
|
+
Everything the app does is data with a schema. An action is JSON. A layout is JSON. A transform is JSON. A query is JSON. A mutation is JSON. A charter is JSON. A plan is JSON. Each artifact is validated by a Zod schema at the boundary and executed by a runtime; nothing executable is a code string. Imperative TypeScript exists only at the edges, and only as:
|
|
10
|
+
|
|
11
|
+
1. a **renderer primitive** — a registry component: props in, events out;
|
|
12
|
+
2. an **endpoint** — real data access behind a URL or registered function;
|
|
13
|
+
3. **setup** — manifest assembly, registry assembly, boot;
|
|
14
|
+
4. **authored data** — an `ActionDefinition`, a Prism config, a Zod schema: data written in TS for type-checking, serializable as-is;
|
|
15
|
+
5. a **check** — a `dev/` script driving the real shell to assert behavior.
|
|
16
|
+
|
|
17
|
+
Code that is none of these — a React feature component, a fetch-and-massage helper, an inline formatter — is a discipline break.
|
|
18
|
+
|
|
19
|
+
The mental model for the UI: **shells host canvases, canvases host action instances, actions render JSON layouts.** There are no screens. Chrome, lists, forms, dialogs, dashboards — all of them are actions loaded onto canvases. How actions are arranged across canvases is a design choice.
|
|
20
|
+
|
|
21
|
+
A canvas holds its instances in one of two modes. `stack` (the default) is a card deck: the top instance is active, the rest suspended, and the canvas renders the top alone — menu → list → detail → form, with Back real. `list` is a tray: every instance stays live and the canvas renders them all through an `actionLayout` that loops `$.instances` into `ActionSlot`s. A list canvas is how a surface gets *composed* rather than drawn — nobody hand-authors a launcher; the actions render themselves.
|
|
22
|
+
|
|
23
|
+
By default the shell runs on the server: the app is a `defineApp` manifest handed to moss, which builds one durable shell per principal and streams rendered trees to a thin canvas terminal. That shell is **the app** — one per person, the thing that rearranges itself around them. A path can also lead to a **page** (`pages` on the manifest): the same actions on a manifest of its own, drawn for whoever asks and kept by nothing — a welcome, the docs. And the first screen of either can arrive *with* the page request, drawn on the server by the app's own kit and adopted by the terminal (moss DESIGN.md, "The document" and "Pages"). Client execution — the shell in the browser, built by the app's own factory — is an explicit degrade for offline and zero-backend apps (D1), not a mode menu.
|
|
24
|
+
|
|
25
|
+
## Decisions are the user's
|
|
26
|
+
|
|
27
|
+
This document has **rules** and **decision points**. Rules you follow without asking. Decision points are choices nisc deliberately leaves open — **never resolve one silently**. Present the options, the tradeoffs, and a recommendation, then wait for the answer. Decide alone only when you hold all the information an informed decision needs — for architecture questions you almost never do.
|
|
28
|
+
|
|
29
|
+
Collect the answers **before implementation starts**. The build is meant to run as an implement/check loop, and a loop that stops mid-flight to ask questions isn't running — front-loading the decisions is what lets it run to done. The known decision points:
|
|
30
|
+
|
|
31
|
+
- **D1 — Posture.** A moss server app (default: the shell runs server-side, the client is a canvas terminal) or a client-degrade app (the shell runs in the browser — offline, zero-backend, portable). The degrade is explicit, per app or per canvas.
|
|
32
|
+
- **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.
|
|
33
|
+
- **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").
|
|
34
|
+
- **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.
|
|
35
|
+
- **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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
Conflicts get the same treatment too: when this document, a package README, and the code disagree, one of them has rotted — report the discrepancy, don't guess which one is right.
|
|
40
|
+
|
|
41
|
+
## Before any code: the interview
|
|
42
|
+
|
|
43
|
+
Step one of every build is a conversation, not a scaffold. Ask the user one batched round of questions and record every answer in `PLAN.md` before writing anything else:
|
|
44
|
+
|
|
45
|
+
- **The decision points D1–D5**, each with options, a recommendation, and the consequences spelled out. Consequences are the point: "a browser database" is not an answer a user can evaluate — "your data lives in one browser profile: no sync, no other devices, gone if site data is cleared" is.
|
|
46
|
+
- **The dial-in:** which entities and relationships; expected scale; who uses it (a single principal, or roles — a multi-principal app sketches its roles and charter in the interview, not after); AI features (now, later, never — this shapes D3); look and feel; deployment target; what happens to the data long-term.
|
|
47
|
+
|
|
48
|
+
Rules of the interview:
|
|
49
|
+
|
|
50
|
+
- **Front-load everything.** Sweep the brief against D1–D5, the rules, and the order of work, and ask it all in one round. The goal is zero questions after the build starts; a fork that surfaces mid-build still gets asked, but treat it as a defect of your interview, not as routine.
|
|
51
|
+
- **Every choice lands in `PLAN.md` with its tier**: **answered** by the user, **delegated** by name ("D4: delegated"), or **derived** — it plainly follows from answers already given and is recorded for review, not asked. A blanket "go" or "you decide" delegates nothing. A choice that fits none of the three tiers blocks the build.
|
|
52
|
+
- If an answer implies a constraint the user didn't name (multi-user later → roles and scope rules now; AI later → keep reads Vex-shaped now), say so during the interview, not in a commit message.
|
|
53
|
+
|
|
54
|
+
## The toolbox
|
|
55
|
+
|
|
56
|
+
Pick per need; every piece works standalone. Nova is the only mandatory one for an app with a UI; moss is the default host for one with a server (D1).
|
|
57
|
+
|
|
58
|
+
| Package | Use it for | Don't use it for |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `nova` | the UI: shells, canvases, actions, layouts, fragments. Renderers are adapters (`/adapters/react`, `/dom`, `/tty`, `/ink`) — the core is surface-blind. `/reflect` inspects a live tree (checks, screen diffs), `/agent` is the layout-authoring surface, `/devtools` the inspector | — |
|
|
61
|
+
| `moss` | the app server: `defineApp` manifest + runtime → data layer, per-principal policy and catalogs, server shells, pages, the socket, and the canvas terminal (`/terminal` plus a per-surface entry: `/terminal/react`, `/vue`, `/dom`, `/tty`, `/ink`). Draws a page request's first screen (`renderDocument`, with `/terminal/react/server` and its siblings) and writes pages as files (`exportDocuments`) | client-degrade apps — they wire their own shell. Routing inside the app: there is none, shell state is the truth |
|
|
62
|
+
| `charter` | the policy document: roles → glob selections over the app's universes, resolved per principal, verified at boot | enforcement — the governed target enforces |
|
|
63
|
+
| `prism` | every transform: shaping, formatting, branching | anything a schema or layout expresses directly |
|
|
64
|
+
| `vex` | query and mutation endpoints: `{ fingerprint, context }` → rows or effects (see "Using Vex") | writes outside its closed mutation grammar |
|
|
65
|
+
| `loom` | compiling a Zod schema into an editing form — prefer it over hand-laying a form the schema already describes | layouts that aren't schema-backed documents |
|
|
66
|
+
| `signal` | LLM calls | — |
|
|
67
|
+
| `cortex` | agents, tools, orchestration | — |
|
|
68
|
+
| `solid` | streaming structured LLM output into UI | — |
|
|
69
|
+
| `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 |
|
|
70
|
+
| `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 |
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
**Actions**
|
|
75
|
+
|
|
76
|
+
1. Everything visible is an action (`.action.ts`) + JSON layout (`.layout.ts`) loaded onto a canvas. No React views, no app logic in components.
|
|
77
|
+
2. One component registry, assembled once. Primitives are domain-blind: a component name containing a domain noun (`InvoiceRow`, `DealCard`) is a feature component in disguise — wrong. Repeated structure is a data-driven spec prop on a generic primitive (a `Table`'s `columns`), not a new component. A component never imports the shell, actions, or data code; if it needs domain knowledge to render, compute the field upstream in a transform. **Components are configured, never styled:** a prop says what a thing is (an area, an ink, a level) from a closed set of names; no `style`, `className`, colour or size is accepted from a layout, and an unknown value falls back to the default. The look lives only in the kit's CSS and tokens — a different look is a different stylesheet over the same layouts.
|
|
78
|
+
2a. A kit whose pages are drawn on the server draws the same thing on its first pass in the browser. Nothing that differs between the two — the window's width, a media query, storage, a portal target — may decide a component's first render: read it through `useSyncExternalStore` with a server answer, or in an effect. A `typeof window` guard around a state initialiser is not a fix, it is the bug: it makes the two sides disagree about the same render, and the terminal throws the page's elements away. What a component writes onto `<html>` from an effect (a palette, a scheme) is not in a drawn page until its script runs; the app reads it off the snapshot and hands it to the document (`renderDocument`'s `htmlAttributes`).
|
|
79
|
+
3. Reusable chrome (dialog frame, panel, drawer) is a fragment composed when an action is loaded (`with: [...]`), not a wrapper component. Modality is arrangement — an action loaded onto an overlay canvas with fragment chrome — never a `$.modalOpen` flag inside an action.
|
|
80
|
+
4. Shell state is the truth. Routing (D5), if wired, is a data table mapping paths ↔ actions, synced by an adapter; Nova stays URL-agnostic. A **page** is not routing inside the app: it is a separate manifest at a path (`pages`), drawn and let go, and a path's parameters seed the canvas the page names — nothing else about a URL reaches an action.
|
|
81
|
+
|
|
82
|
+
**Data**
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
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.
|
|
86
|
+
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.
|
|
87
|
+
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.
|
|
88
|
+
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.
|
|
89
|
+
9. Formatting and derivation live in Prism transforms, never in components and never in action code.
|
|
90
|
+
|
|
91
|
+
**Principals**
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
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.
|
|
95
|
+
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.
|
|
96
|
+
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.
|
|
97
|
+
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.
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
**Schemas**
|
|
101
|
+
|
|
102
|
+
13. Everything that crosses a boundary is parsed by a Zod schema at that boundary. `.strict()` on external input.
|
|
103
|
+
14. An action's openable inputs live in its definition's optional `input` field: a JSON Schema — authored in Zod with `.describe()` on every field, converted with `z.toJSONSchema` — of the `data` keys an opener may seed when loading the action. The fields are a subset of `data`; no seedable keys, no `input`. That schema is the action's public contract, for a nav item, a URL, a command palette, and an agent alike. Catalogs curate ids and descriptions and are resolved per principal (moss serves the resolved catalog); they read `input` off the definition, never restate it.
|
|
104
|
+
15. For any LLM contract, the Zod schema is the single source of truth: constraints in `.describe()`, JSON Schema injected into the prompt at runtime, always minified. No prose "how the DSL works" sections in prompts.
|
|
105
|
+
|
|
106
|
+
**Code**
|
|
107
|
+
|
|
108
|
+
16. `/STYLE_GUIDE.md` applies in full, with two app-level overrides: it is written for the packages, so its "custom error classes for domain errors" does **not** apply here — an app declares no classes at all and throws plain `Error`; and "Package prompts serve every app" binds packages, not apps — an app's prompts may be about the app. What an app measures against a package's agent is fixed in that package only in general form (see that section). Zero-tolerance: `any`, type assertions (`as` — `as const` excepted), non-null `!`, `enum`, classes, default exports, `function` declarations. Deps via `pnpm add` only. Files: `kebab-case.role.ts`. Declared exception: a file that must match an external module's shape (a shim) may break these rules — it says why in a header comment, and reviewers honor it.
|
|
109
|
+
|
|
110
|
+
**Versions** (`@niscorp/strata`; the why is `packages/strata/DESIGN.md`)
|
|
111
|
+
|
|
112
|
+
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).
|
|
113
|
+
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.
|
|
114
|
+
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 is refused (`TOO_NEW`), never guessed at: the reader upgrades first — host before add-on, server before terminal.
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
## A worked trio
|
|
118
|
+
|
|
119
|
+
One feature — a list you can act on — as the three files it actually is. It replays the `todos/open` entry from "Using Vex" below; nothing else about it is special. Read the rules through this, not around it.
|
|
120
|
+
|
|
121
|
+
**`todos.action.ts`** — the definition. Every `data` key carries a default; endpoints declare their shaping; triggers catch refs.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { z } from 'zod';
|
|
125
|
+
import type { ActionDefinition } from '@niscorp/nova';
|
|
126
|
+
import { todosLayout } from './todos.layout';
|
|
127
|
+
import { todosPrism, setDonePrism } from './todos.prism';
|
|
128
|
+
|
|
129
|
+
export const todosAction: ActionDefinition = {
|
|
130
|
+
id: 'todos.list',
|
|
131
|
+
title: 'Todos',
|
|
132
|
+
// endpoint-fed slots + UI state — every key present, every key defaulted
|
|
133
|
+
data: { ownerId: '', rows: [], toggleTodoId: '', toggleDone: false, loading: true },
|
|
134
|
+
layout: todosLayout,
|
|
135
|
+
// a contract, not an implementation (rule 7): the same action runs
|
|
136
|
+
// unchanged against moss or against a degrade app's own endpoints
|
|
137
|
+
endpoints: {
|
|
138
|
+
load: { url: '/api/todos/vex', method: 'POST', request: todosPrism, target: 'rows' },
|
|
139
|
+
setDone: { url: '/api/todos/vex', method: 'POST', request: setDonePrism },
|
|
140
|
+
},
|
|
141
|
+
// mount loads; loading is explicit data, never Suspense
|
|
142
|
+
lifecycle: { mount: [{ call: 'load', onSuccess: [{ set: 'loading', value: false }] }] },
|
|
143
|
+
triggers: [
|
|
144
|
+
{
|
|
145
|
+
event: 'ui:click',
|
|
146
|
+
ref: 'toggle',
|
|
147
|
+
do: [
|
|
148
|
+
{ set: 'toggleTodoId', value: '@event.payload.todo_id' },
|
|
149
|
+
{ set: 'toggleDone', value: '@event.payload.next' },
|
|
150
|
+
// writers announce…
|
|
151
|
+
{ call: 'setDone', onSuccess: [{ call: 'load' }, { emit: { channel: 'todos-changed' } }] },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
// …viewers react — this action displays todos too, so it listens to its own
|
|
155
|
+
{ message: 'todos-changed', do: [{ call: 'load' }] },
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Rule 14: what an opener may seed, as a JSON Schema. Every field described;
|
|
160
|
+
// every field a key of `data` above.
|
|
161
|
+
export const todosInputSchema = z.toJSONSchema(
|
|
162
|
+
z.object({
|
|
163
|
+
ownerId: z.string().optional().describe('Seeded from the session by the chrome; never client-authored.'),
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**`todos.layout.ts`** — JSON. Domain-blind primitives driven by spec props (rule 2); `ref` is what the trigger catches. Component names resolve against the app's registry in `ui/`. Nova ships a headless base set per adapter (`Stack`, `Box`, `Text`, `Input`, `Button`, `Panel`, `JsonTree`, plus the `CanvasSlot` / `ActionSlot` markers) via `registerNovaReactComponents` — but both exemplars take only the two slot markers and build the rest themselves, because the kit is where the app's look lives (order of work, step 3). Assume every other name here is one you define.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import type { LayoutNode } from '@niscorp/nova';
|
|
172
|
+
|
|
173
|
+
export const todosLayout: LayoutNode = {
|
|
174
|
+
component: 'Stack',
|
|
175
|
+
props: { gap: 20, maxWidth: 900 },
|
|
176
|
+
children: [
|
|
177
|
+
{
|
|
178
|
+
component: 'Rows', // generic, not TodoRow
|
|
179
|
+
props: {
|
|
180
|
+
rows: '$.rows',
|
|
181
|
+
loading: '$.loading',
|
|
182
|
+
rowKey: 'todo_id',
|
|
183
|
+
empty: 'Nothing open.',
|
|
184
|
+
columns: [ // repeated structure is DATA
|
|
185
|
+
{ label: 'Todo', w: 1, cell: { kind: 'primary', key: 'title', subKey: 'due_display' } },
|
|
186
|
+
{ label: 'Done', w: 'auto', cell: { kind: 'switch', key: 'done', ref: 'toggle', label: '' } },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**`todos.prism.ts`** — the endpoint bodies. A Vex replay is `{ fingerprint, context }`; `$ref` pulls from the action's own data (rule 5).
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { todosOpen, todoSetDone } from '../../vex/todo.entries';
|
|
198
|
+
|
|
199
|
+
export const todosPrism = {
|
|
200
|
+
fingerprint: todosOpen.fingerprint,
|
|
201
|
+
context: { ownerId: { $ref: '$.ownerId' } },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const setDonePrism = {
|
|
205
|
+
fingerprint: todoSetDone.fingerprint,
|
|
206
|
+
context: { todoId: { $ref: '$.toggleTodoId' }, done: { $ref: '$.toggleDone' } },
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Note what is absent: no React, no `fetch`, no date formatting, no `if (role === …)`, no `owner_id` on the wire from the client. `due_display` arrives already formatted — that happened in the entry's mapping, upstream (rule 9).
|
|
211
|
+
|
|
212
|
+
## Using Vex
|
|
213
|
+
|
|
214
|
+
Whether to use Vex is D3 (reads) and D4 (writes). How to use it is not a choice:
|
|
215
|
+
|
|
216
|
+
- **Reads the app depends on are deterministic.** Author each one as a seed entry — Vex's own cache row minus the machine-filled bits. `SeedEntry` and `SeedMutation` are exported for exactly this; don't hand-roll a `Pick<>` over the cache types:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import type { SeedEntry } from '@niscorp/vex';
|
|
220
|
+
|
|
221
|
+
export const todosOpen: SeedEntry = {
|
|
222
|
+
fingerprint: 'todos/open', // THE cache key — the name every read replays
|
|
223
|
+
intent: 'List open todos ordered by due date', // one factual sentence — mandatory
|
|
224
|
+
shape: [{ todo_id: '', title: '', due_display: '' }], // drives the array-vs-single envelope on replay
|
|
225
|
+
dsl: { // hand-authored; { $context: 'key' } for parameters
|
|
226
|
+
from: ['todos'],
|
|
227
|
+
fields: [{ field: 'todos.id', as: 'todo_id' }, 'todos.title', 'todos.due_date'],
|
|
228
|
+
filter: { eq: ['todos.done', false] },
|
|
229
|
+
sort: [{ field: 'todos.due_date', dir: 'asc' }],
|
|
230
|
+
},
|
|
231
|
+
mapping: { /* Prism over { result }: the rows (array shape) or the one row (object shape); omit when the DSL already aliases to the shape */ },
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- **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.
|
|
236
|
+
- **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.
|
|
237
|
+
- **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.
|
|
238
|
+
- **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 }`.
|
|
239
|
+
- **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.
|
|
240
|
+
- **App reads are replay-only.** Moss serves its endpoints locked; a client-degrade app passes `{ locked: true }` itself. An unknown fingerprint (a missed prewarm, a discipline break) is a 500, never a silent LLM call. With no AI features, also wire no hooks: warm-only, enforced twice.
|
|
241
|
+
- **Live queries are the opt-in LLM path.** Wire `createQueryDsl` / `createShapeMapper` (`@niscorp/vex/agent`) as the engine's `generateDsl` / `mapToShape` hooks; a request without a fingerprint generates, caches, and mints one (`meta.cache.fingerprint`) — embed that to replay the proven query. App reads stay locked and never depend on this path; agents and ad-hoc features do.
|
|
242
|
+
- **Freshness is the entry's.** `refresh: 'snapshot'` (the default) answers once; `refresh: 'reactive'` answers again when a write lands on a table the query reads. Declare it on reads that are on screen while others write — a roster, a count, a board. A reactive entry sorts, and reads time from `$scope`, never context.
|
|
243
|
+
- **Staleness is handled through the slot.** When an entry must change, send its fingerprint with the changed request — the slot regenerates and replaces (protected entries 409 until unprotected). Don't edit cache rows by hand.
|
|
244
|
+
- **Scope is engine-side.** Access policy lives in the engine's `ScopePolicy` — compiled from the charter under moss — invisible to and unforgeable by query authors, human or LLM.
|
|
245
|
+
|
|
246
|
+
## Working patterns
|
|
247
|
+
|
|
248
|
+
Stack-independent conventions:
|
|
249
|
+
|
|
250
|
+
- **Action data = endpoint-fed slots + UI state**, every key with a default. Loading is explicit data (`loading: true`), not Suspense.
|
|
251
|
+
- **Mount loads; `onSuccess` chains dependent loads.** Each independent section of a detail action is its own endpoint into its own slot.
|
|
252
|
+
- **Reactive reads keep screens current; channels are for what is not data.** A vex read whose entry declares `refresh: 'reactive'` answers again, on every screen that has it open, whenever a write lands on a table it reads — nothing announces the change and nothing listens (vex DESIGN.md, "Reactive reads"). Under moss, that is how a screen follows its data. Where a read stays a snapshot, or a client-degrade app has no follower, the older pattern holds: writers announce, viewers react — a successful write emits a channel (`<entity>s-changed`) and every action displaying that entity listens and re-reads. Channels also carry signals that are not data (the deck moving on). No shared stores. A message can carry a payload (`{ emit: { channel, payload } }`, or moss's `shells.deliver(principal, channel, payload)` from the server); a listener reads it as `@event.payload`. A payload says what happened, never data a screen should read: data still reaches a screen through its reads.
|
|
253
|
+
- **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.
|
|
254
|
+
- **Interaction = `ref` + trigger.** Layout nodes carry `ref`; triggers catch `{ event, ref }` and run steps. Event payloads flow via `@event.payload`; no callbacks in props.
|
|
255
|
+
- **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.
|
|
256
|
+
- **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.
|
|
257
|
+
- **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.
|
|
258
|
+
|
|
259
|
+
## Failure modes worth naming
|
|
260
|
+
|
|
261
|
+
Four mistakes that have each cost more than the thing they were avoiding. The
|
|
262
|
+
first three share one shape — **concluding the system cannot do something without
|
|
263
|
+
testing whether it can** — and each produced a workaround larger than the check
|
|
264
|
+
would have been. The fourth is what happens on the way back out.
|
|
265
|
+
|
|
266
|
+
### 1. Do not rationalise a limitation into a principle
|
|
267
|
+
|
|
268
|
+
While adding per-role reach (`scoping`), the resolver collapsed a principal's
|
|
269
|
+
roles into ONE profile and threw when two roles named different ones. That throw
|
|
270
|
+
was written up as *"incoherence, not a precedence puzzle — refused, in the same
|
|
271
|
+
spirit as two granted layout variants for one action."*
|
|
272
|
+
|
|
273
|
+
It is not the same. Two layout variants for one action genuinely conflict — a
|
|
274
|
+
screen renders one. Two roles naming different reaches is an instructor who also
|
|
275
|
+
trains: staff on the roster, a member in the class. The rule existed to make the
|
|
276
|
+
design self-consistent, and the domain broke it within the hour.
|
|
277
|
+
|
|
278
|
+
**The tell:** a rule justified by analogy to a different rule, rather than by a
|
|
279
|
+
case it would prevent. If you cannot name the bad thing it stops, it is a
|
|
280
|
+
limitation wearing a principle's clothes.
|
|
281
|
+
|
|
282
|
+
### 2. Test the mechanism before you blame it
|
|
283
|
+
|
|
284
|
+
A charter `deny` was used to stop a grant riding up the role ladder. It appeared
|
|
285
|
+
not to survive `extends`, so a table that should have been deleted was kept and
|
|
286
|
+
the "limitation" was written into permanent comments as its justification.
|
|
287
|
+
|
|
288
|
+
`deny` works exactly as documented — a child re-adds a denied grant, verified in
|
|
289
|
+
five lines. What had actually happened: a patch script targeted the wrong role
|
|
290
|
+
because line numbers shifted after an earlier edit. The fault was in the edit,
|
|
291
|
+
not the engine.
|
|
292
|
+
|
|
293
|
+
**The rule:** before documenting a limitation in someone else's package, write
|
|
294
|
+
the smallest program that demonstrates it. If that program is hard to write, the
|
|
295
|
+
limitation probably is not real.
|
|
296
|
+
|
|
297
|
+
### 3. Believe the schema over the resolver
|
|
298
|
+
|
|
299
|
+
The schema splits `staff` and `memberships` into two tables, with a comment
|
|
300
|
+
saying why: *a person can be staff at a studio and hold a membership there too.*
|
|
301
|
+
The directory above it then flattened every person to one word
|
|
302
|
+
(`audienceOf(staffRole) ?? 'member'`), and `assignments` gave each person one
|
|
303
|
+
role. The thing the data model deliberately kept apart, the policy layer
|
|
304
|
+
collapsed — and three workarounds grew on top of the collapse: an extra scoping
|
|
305
|
+
profile, a projection table, and a deny that was never needed.
|
|
306
|
+
|
|
307
|
+
**The rule:** when a resolver contradicts a comment in the schema, the schema is
|
|
308
|
+
usually right. It was written when somebody was thinking about the domain; the
|
|
309
|
+
resolver was written when somebody was thinking about the code.
|
|
310
|
+
|
|
311
|
+
### 4. Undoing a workaround moves the boundary — say where it went
|
|
312
|
+
|
|
313
|
+
Deleting the three workarounds was not free, and the free-looking version would
|
|
314
|
+
have been the dangerous one. `member_cards` existed so a member could see their
|
|
315
|
+
plan and price without holding `subscriptions.read`; deleting it means the member
|
|
316
|
+
rung holds that verb, at personal reach. Replaying the revenue fingerprint as a
|
|
317
|
+
member therefore returns a *number* rather than a refusal — their own bill, not
|
|
318
|
+
the studio's — where before it bounced at the verb.
|
|
319
|
+
|
|
320
|
+
That is a real weakening, and it is the right trade: the same verb at two reaches
|
|
321
|
+
is the whole design. But it was found by a check going red, not by the reasoning
|
|
322
|
+
that authored the change. Two existing checks asserted "refused" and had to be
|
|
323
|
+
rewritten to assert "not the studio's figure" — and rewriting a red assertion is
|
|
324
|
+
exactly where a leak gets waved through.
|
|
325
|
+
|
|
326
|
+
**The rule:** when a check fails because the design moved, do not adjust it to
|
|
327
|
+
match the new behaviour. Work out what the old assertion was protecting, then
|
|
328
|
+
write the assertion that protects the same thing under the new shape. Here that
|
|
329
|
+
meant comparing the member's figure against the owner's rather than testing for a
|
|
330
|
+
status code.
|
|
331
|
+
|
|
332
|
+
**And check the ones that stayed green.** A check that asserts a *mechanism* can
|
|
333
|
+
go on passing while the *consequence* it was standing in for has inverted.
|
|
334
|
+
`scoping-check` asserted "an instructor who also trains reads the whole studio" —
|
|
335
|
+
green, correct, and describing a screen that was showing him all 93 bookings at
|
|
336
|
+
the studio under the heading "what you have booked". Widening reach turned a
|
|
337
|
+
true statement about the engine into a false one about the product. After a
|
|
338
|
+
change to how far somebody reaches, re-read every assertion that mentions them,
|
|
339
|
+
not only the red ones.
|
|
340
|
+
|
|
341
|
+
### The habit that catches all four
|
|
342
|
+
|
|
343
|
+
Before building around a constraint, spend five minutes proving it exists. Every
|
|
344
|
+
workaround in this list was bigger than its proof would have been, and two of
|
|
345
|
+
them were built around constraints that were never there.
|
|
346
|
+
|
|
347
|
+
And when you take one out, name what it was holding up. A workaround that was
|
|
348
|
+
never needed still moved a boundary while it stood there.
|
|
349
|
+
|
|
350
|
+
## Layout of an app
|
|
351
|
+
|
|
352
|
+
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/`.
|
|
353
|
+
|
|
354
|
+
Three naming rules make the tree legible, and they're the same rules the artifact library will type its rows by:
|
|
355
|
+
|
|
356
|
+
- **A folder names the governor** (or, for nova's many kinds, the domain): `charter/` and `vex/` group a library's artifacts; `actions/domains/<x>/` groups a domain's.
|
|
357
|
+
- **A suffix names the kind** — the specific artifact type, which is also its library row type: `.action.ts`, `.layout.ts`, `.fragment.ts`, `.prism.ts`, `.entries.ts`. Multi-member kinds (many files, one artifact each) carry it. A file holding several artifacts of one kind pluralizes — `issue.actions.ts`, `arrival.layouts.ts` — which is a legitimate grouping when the artifacts are one feature's set, not a dumping ground.
|
|
358
|
+
- **A single-document field is named by the field, no suffix**: `charter.ts`, `assignments.ts`, `behaviors.ts`, `resources.ts`, `app.ts`. There's only one — nothing to disambiguate. Index files that assemble a kind are named for it: `action-catalog.ts`, `layout-variants.ts`.
|
|
359
|
+
|
|
360
|
+
There is deliberately **no library suffix** (`.vex.ts`, `.charter.ts`): the suffix names the kind, not the governor — the governor is the folder. `.action.ts` is not `.nova.ts`.
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
src/
|
|
364
|
+
app/ ARTIFACTS ONLY — pure JSON, each parses its schema
|
|
365
|
+
app.ts the defineApp manifest — one import per field
|
|
366
|
+
pages.ts pages: what is drawn at a path and kept by nothing (single doc) —
|
|
367
|
+
only if the app has any
|
|
368
|
+
action-catalog.ts ring 1: the action index (actions)
|
|
369
|
+
layout-variants.ts ring 2: the minted layout-variant index (layouts) — only if
|
|
370
|
+
the app mints variants; ring-1 existence covers most difference
|
|
371
|
+
charter/ charter.ts, assignments.ts
|
|
372
|
+
actions/ the trios — chrome/, domains/<x>/, surfaces/, shared/ if earned
|
|
373
|
+
(a variant layout sits beside its base: topbar.layout.ts + topbar.full.layout.ts)
|
|
374
|
+
shell/ the shell manifest's chrome — frame.layout, stack layouts, fragments/
|
|
375
|
+
prisms/ shared authored transforms (money, dates)
|
|
376
|
+
vex/ the vex data surface (governor folder, mirrors charter/)
|
|
377
|
+
<entity>.entries.ts read + mutation entries, one file per entity
|
|
378
|
+
behaviors.ts row-level scope semantics (single doc)
|
|
379
|
+
resources.ts entity subgraphs → /api/<name>/vex (single doc)
|
|
380
|
+
grammars/ only if the app owns a grammar: its strata sequences
|
|
381
|
+
(`<app>.kit` — its components' props), manifest field `grammars`
|
|
382
|
+
db/ the environment (D2): schema DDL, seed — NOT artifacts, not in the library
|
|
383
|
+
lib/ code helpers (date, etc.) — only what has nowhere better to live
|
|
384
|
+
server/ moss glue (boot + listener), server fns, and the manifest's
|
|
385
|
+
non-endpoint session code (onSession observers, the run sink).
|
|
386
|
+
When pages are drawn on the server: document.ts (how a screen is
|
|
387
|
+
drawn — the kit handed to moss, and what the kit would put on
|
|
388
|
+
<html>). No production route: `nisc start` serves the built terminal.
|
|
389
|
+
No dev glue either: `nisc dev` runs the app server inside vite
|
|
390
|
+
(moss's dev plugin, fed from nisc.config.ts) — vite.config.ts
|
|
391
|
+
holds the framework plugin and nothing else
|
|
392
|
+
ui/ the component kit + registry — the only renderer code in the app
|
|
393
|
+
dev/ headless checks; strata.ts lists the artifacts for `pnpm strata`
|
|
394
|
+
main.tsx the terminal entry
|
|
395
|
+
nisc.config.ts what the app tells the `nisc` command: how it boots, how a screen is
|
|
396
|
+
drawn — only if its pages are drawn on the server or built to files
|
|
397
|
+
strata.lock.json which grammar version the source is written in (rule 20) — committed,
|
|
398
|
+
moved only by `pnpm strata verify`
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
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.
|
|
402
|
+
|
|
403
|
+
`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.
|
|
404
|
+
|
|
405
|
+
An app may stand a **sibling service** beside the main one — a connector host, a back-office tool — as its own process with its own port, manifest and charter. It is a separate app under the same roof, not a section of this one: it does not share the main app's shell, and what crosses between them crosses over the wire through a gate the main app owns. Treat it as a second application built by these same rules.
|
|
406
|
+
|
|
407
|
+
## The client-degrade path (D1)
|
|
408
|
+
|
|
409
|
+
The degrade is for offline, zero-backend and portable apps — a demo that must run from a static host, a tool with no server to deploy. It is a posture, not a lesser mode: the artifacts are identical, and an action cannot tell which one it is running under.
|
|
410
|
+
|
|
411
|
+
What changes is only who assembles and serves:
|
|
412
|
+
|
|
413
|
+
- **The shell is a factory, not a manifest.** `createAppShell(deps)` builds it in the browser with everything environmental injected — the fetch seam, the app's `today`, the transform socket (rule 6). Nothing environmental is read from module scope, so a check constructs the real app with real deps and nothing leaks between runs.
|
|
414
|
+
- **`server/` is replaced by whatever serves the endpoints** (D2): in-memory fixtures, PGlite in the browser, a real backend, or a mix. Endpoints keep their URLs and shapes — that is what makes the posture invisible upstream. The client-side `fn` registry is the escape hatch here and only here (rule 7).
|
|
415
|
+
- **The app boots its own Vex engine** — database adapter, cache backend, `ScopePolicy` — `introspect()` once at startup behind a single memoized accessor, prewarming and locking replay itself. See "Using Vex" for the exact seeding contract; under moss the server does all of this, and here the app does.
|
|
416
|
+
- **`inputs`, `seeds`, `scope`, `onSession` and `runs` have no host.** Whatever they derived, the factory derives at construction. There is no durable per-principal shell and no server-side resolution.
|
|
417
|
+
- **The first screen still arrives as markup.** A degrade app is not a blank page until its script runs: `nisc export` runs the app's own boot at build, draws the settled screen to markup, and writes one file per path; the browser entry boots the same shell and adopts it (`@niscorp/cli` README, "An app with its own shell"). The `nisc.config.ts` hands over `shell` (the boot — unmemoized, handed a path and nothing else), `draw` and `adopt`; the build fails unless every path is drawn, whole, the same on a second boot, and adopted clean. That makes the boot's determinism a rule here, not a nicety: no random id or wall-clock read may reach the markup, and a read that answers differently in the page than it did at build is drawn again there — the build names every such read ("opened with …").
|
|
418
|
+
|
|
419
|
+
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.
|
|
420
|
+
|
|
421
|
+
`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.
|
|
422
|
+
|
|
423
|
+
## Order of work
|
|
424
|
+
|
|
425
|
+
1. **The interview** (see "Before any code") — every decision answered or delegated by name, recorded in `PLAN.md` with scope, data model, and roles.
|
|
426
|
+
2. **Scaffold** — manifest + runtime + terminal (moss), or boot + shell factory (degrade); canvases + empty registry; one placeholder action renders. `npm create nisc` makes exactly this — from templates that are themselves checked apps — and writes `PLAN.md` with D1 and the renderer answered and the rest open; the interview (step 1) is then about what it left open.
|
|
427
|
+
3. **Kit** — primitives against a kitchen-sink action; lock the look before any real feature.
|
|
428
|
+
4. **Data layer** — per D2/D3: schema, seed, entries, behaviors; prove one read end-to-end.
|
|
429
|
+
5. **Actions** — domain by domain: list → detail → form; chrome and routes as they're needed; new action ids granted in the charter as they're minted.
|
|
430
|
+
6. **Writes** — per D4, with change channels; prove one round-trip: create → announce → re-read.
|
|
431
|
+
7. **Checks and polish** — a dev check per feature; empty states, skeletons, transitions last.
|
|
432
|
+
|
|
433
|
+
## Verification
|
|
434
|
+
|
|
435
|
+
Every feature ships a headless check: a standalone script under `dev/` that boots the real app — for a moss app, the manifest through `createServer` with a dev runtime; for a degrade app, the shell factory — dispatches events against the shell, asserts on runtime data and the render tree (`@niscorp/nova/reflect` reads a live tree), prints one `[pass]`/`[fail]` line per assertion, and exits non-zero on any failure. No browser, no mocks.
|
|
436
|
+
|
|
437
|
+
A `dev/all-checks.ts` runs the suite and is the app's `check` script. It spawns **each check as its own process over its own fresh database** — checks ship the app different histories, and a shared database would make the order of the suite part of its meaning.
|
|
438
|
+
|
|
439
|
+
A review pass checks, in order:
|
|
440
|
+
|
|
441
|
+
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.
|
|
442
|
+
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.
|
|
443
|
+
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.
|
|
444
|
+
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.
|
|
445
|
+
5. No component name contains a domain noun; no component imports shell, action, or data code.
|
|
446
|
+
6. No layout or component branches on roles or capability data (rule 11) — per-principal difference is existence or a served variant.
|
|
447
|
+
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.
|
|
448
|
+
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.
|
|
449
|
+
6c. No role holding a data wildcard reaches the engine's tables, and the charter check resolves against the universe boot does (rule 10a).
|
|
450
|
+
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).
|
|
451
|
+
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.
|
|
452
|
+
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.
|
|
453
|
+
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.
|
|
454
|
+
|
|
455
|
+
Report violations by rule number. A change that makes the app less declarative, less validated, or less observable is wrong even if it works.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @niscorp/nisc
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [2548e43]
|
|
8
|
+
- @niscorp/moss@0.2.1
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 3a9866e: `@niscorp/moss/vite` — `mossDev({ app })`, the app server inside vite's dev process: loaded through vite so an edit re-boots it (the old server answers until the new one is up, then is let go with its timers), the socket attached once and delegating to whichever server is current, `/` and every page's path answered with its first screen drawn into vite's transformed index.html, moss's paths answered by the app server, and a dev-only `/dev/as/<who>` when the app can mint a token. `vite` is an optional peer. `MOSS_PATHS` is exported from `@niscorp/moss/node`.
|
|
15
|
+
- a768f3b: `@niscorp/nisc` carries the rulebook: `AGENTS.md` and `STYLE_GUIDE.md` ship in the package, so an app reads the rules for the nisc version it has installed (`node_modules/@niscorp/nisc/AGENTS.md`). The repository's root files are links to them.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [3a9866e]
|
|
20
|
+
- Updated dependencies [3a9866e]
|
|
21
|
+
- Updated dependencies [534eb4b]
|
|
22
|
+
- @niscorp/cli@0.2.0
|
|
23
|
+
- @niscorp/moss@0.2.0
|
|
24
|
+
- @niscorp/nova@0.1.1
|
|
25
|
+
- @niscorp/prism@0.1.1
|
package/README.md
CHANGED
|
@@ -17,6 +17,15 @@ was tested as nisc 0.4.0. Import from the packages themselves
|
|
|
17
17
|
Its version moves whenever any member's does, and it breaks whenever any member
|
|
18
18
|
breaks — `pnpm check:changesets` refuses a release plan that forgets.
|
|
19
19
|
|
|
20
|
+
## The rulebook
|
|
21
|
+
|
|
22
|
+
It also carries the rules a nisc application is built by, for the versions it
|
|
23
|
+
pins: [`AGENTS.md`](AGENTS.md) — the decision points, the rules, the order of
|
|
24
|
+
work, what a review checks — and [`STYLE_GUIDE.md`](STYLE_GUIDE.md). They are
|
|
25
|
+
written for the coding agent building the app as much as for a person. An app
|
|
26
|
+
made by `npm create nisc` has a short `AGENTS.md` of its own that points here,
|
|
27
|
+
so the rules it follows are always the ones for what it has installed.
|
|
28
|
+
|
|
20
29
|
## License
|
|
21
30
|
|
|
22
31
|
Apache-2.0
|
package/STYLE_GUIDE.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Code Style Guide
|
|
2
|
+
|
|
3
|
+
Rules for writing code in this codebase. Short, focused on what matters.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## TypeScript Strictness
|
|
8
|
+
|
|
9
|
+
- `strict: true` in tsconfig. Always.
|
|
10
|
+
- No `any`. Use `unknown` when the type is genuinely unknown, then narrow it.
|
|
11
|
+
- No type assertions (`as`, `as unknown as`). If you need to cast, the types are wrong. Fix them.
|
|
12
|
+
- No non-null assertions (`!`). Never. There is always a way to restructure the code so TypeScript can narrow without `!`. If you think you need it, you're wrong.
|
|
13
|
+
- No `enum`. Use `z.enum()` or `as const` objects.
|
|
14
|
+
- No classes unless you genuinely need identity + mutable state (almost never in these projects).
|
|
15
|
+
- No decorators, no `reflect-metadata`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Functions
|
|
20
|
+
|
|
21
|
+
- Arrow functions with `const`. No `function` declarations.
|
|
22
|
+
- Exported functions must have explicit return types.
|
|
23
|
+
- Internal functions can rely on inference.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// yes
|
|
27
|
+
export const evaluate = (config: Config, source: JsonObject): JsonValue => { ... };
|
|
28
|
+
|
|
29
|
+
// no
|
|
30
|
+
export function evaluate(config: Config, source: JsonObject) { ... }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Types
|
|
36
|
+
|
|
37
|
+
Types are written where they make sense. Not everything comes from Zod.
|
|
38
|
+
|
|
39
|
+
- **Zod-inferred types** for schemas that need runtime validation (DSL nodes, configs, API contracts, anything that crosses a boundary).
|
|
40
|
+
- **Hand-written types** for everything else: function parameters, return types, internal interfaces, options objects, dependency bags, runtime state, callbacks, etc.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Zod-inferred: this is a DSL node, needs runtime validation
|
|
44
|
+
export const RefNodeSchema = z.object({ $ref: z.string() }).strict();
|
|
45
|
+
export type RefNode = z.infer<typeof RefNodeSchema>;
|
|
46
|
+
|
|
47
|
+
// Hand-written: this is a function parameter bag, no validation needed
|
|
48
|
+
export type RendererConfig = {
|
|
49
|
+
registry: ComponentRegistry;
|
|
50
|
+
eventBus?: EventBus;
|
|
51
|
+
layoutStore?: LayoutStore;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Hand-written: this is a return type contract
|
|
55
|
+
export type LayoutRenderer = {
|
|
56
|
+
render: (layout: LayoutNode, data: Record<string, unknown>) => React.ReactNode;
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Zod
|
|
63
|
+
|
|
64
|
+
- `.strict()` on object schemas that represent external input.
|
|
65
|
+
- `.describe()` on schemas and fields that will be consumed by LLMs or generate JSON Schema. Not on every internal helper schema.
|
|
66
|
+
- `safeParse` at boundaries (user input, API responses, LLM output). `parse` inside trusted code where failure means a bug.
|
|
67
|
+
|
|
68
|
+
### Never pretty-print JSON inside prompts
|
|
69
|
+
|
|
70
|
+
When embedding JSON (a schema, an example, a tool descriptor) into an LLM prompt, **always minify**. `JSON.stringify(value)` — no `null, 2`. Pretty-printing roughly doubles the token count for no benefit: the model parses minified JSON exactly as well, and the cost is paid on every call.
|
|
71
|
+
|
|
72
|
+
Pretty-printing is fine when the destination is a human (debug output, logs, the showroom inspector). For prompts, minify. If you genuinely need both — say, a `debug` flag that pretty-prints — name it explicitly and default to minified.
|
|
73
|
+
|
|
74
|
+
### Schemas are the single source of truth for LLM agents
|
|
75
|
+
|
|
76
|
+
When an LLM agent produces or consumes a typed shape, **the Zod schema is the one source of truth**. Do not duplicate schema knowledge into system prompts.
|
|
77
|
+
|
|
78
|
+
- **Every constraint goes on the Zod field via `.describe()`**: format rules, syntactic gotchas, examples, what each variant means. If a `$ref` field requires JSONPath syntax starting with `$.`, the `.describe()` says so. If a template uses `{{double}}` braces, the `.describe()` says so. The schema teaches the model how to fill it in.
|
|
79
|
+
- **Inject the JSON Schema at runtime**: agent system prompts use `z.toJSONSchema(schema, { target: 'draft-7' })` and embed the result. The system prompt itself stays minimal — agent role, output envelope, a placeholder where the schema lands, and one short worked example at most.
|
|
80
|
+
- **Never write a "here is how the DSL works" prose section in the system prompt.** Never maintain a hand-curated operations list alongside the schema. Both will drift from the schema and silently produce broken output. The author has been burned by this.
|
|
81
|
+
- **Apply this to all packages.** Cortex agents, Signal agents, future Vex agents, anything that talks to a model with a typed contract. The schema is the contract; the prompt is the convener.
|
|
82
|
+
|
|
83
|
+
The reason for the schemas in the first place is that the system becomes self-explanatory and we have ONE source of truth. Code that violates this is wrong — fix the schema's `.describe()` calls, not the prompt.
|
|
84
|
+
|
|
85
|
+
### What a model decides on has a `description`
|
|
86
|
+
|
|
87
|
+
Whatever is offered to a model to decide on — an effect, a tool, an action, a shape to answer in — carries a `description`: one to three sentences saying what it does and what it is for. The model decides by that, never by a name or an id, which make it guess. The field is called `description`; not `does`, not `means`, not anything else. This binds apps as well as packages.
|
|
88
|
+
|
|
89
|
+
### Package prompts serve every app
|
|
90
|
+
|
|
91
|
+
A package's agent prompt, its schemas' `.describe()`s and its worked example say what the grammar is and what it can express — never what some app asked for. Nothing in them comes from an app: not its names, its phrasings, its domain, its data, its fixed clocks, nor an example taken from a case that failed. Doc comments beside a prompt follow the same rule; they are where the next edit copies from.
|
|
92
|
+
|
|
93
|
+
When an agent gets a case wrong, the fix is in the grammar (what it can express, how its fields are described) or in the tool's contract — stated as it would have been written had that case never happened. If the only sentence that fixes it names the case, the grammar is missing something; add that instead. Apps measure; packages generalise.
|
|
94
|
+
|
|
95
|
+
**Every edit to a package's prompt or descriptions is reviewed by a subagent before it lands**, given this section, the diff, and read access to the apps' probes. It names any line that reads as fitted to a use case. A flagged line is rewritten or removed, not argued with.
|
|
96
|
+
|
|
97
|
+
This binds packages only. An app's prompt may be about its app — being fitted to its own case can be the point.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Unused Parameters
|
|
102
|
+
|
|
103
|
+
Underscore prefix. Simple.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const handler = (_event: Event, _from: Address) => { ... };
|
|
107
|
+
template.replace(TEMPLATE_REGEX, (_match: string, path: string) => { ... });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## File Naming
|
|
113
|
+
|
|
114
|
+
Kebab-case with dot notation to denote the file's role:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
action-plan.schema.ts # Zod schema definitions
|
|
118
|
+
postgres.adapter.ts # Database adapter implementation
|
|
119
|
+
internal.types.ts # Type definitions
|
|
120
|
+
event-bus.ts # Implementation (no role suffix needed for primary files)
|
|
121
|
+
query.agent.ts # Agent implementation
|
|
122
|
+
scope.apply.ts # Domain logic
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The part before the last dot is the name, the part after is the role: `.schema.ts`, `.types.ts`, `.adapter.ts`, `.agent.ts`, `.test.ts`.
|
|
126
|
+
|
|
127
|
+
Plain `name.ts` is fine when the file's role is obvious from context or when it's the primary implementation file.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Exports
|
|
132
|
+
|
|
133
|
+
- Named exports only. No default exports.
|
|
134
|
+
- Barrel `index.ts` files for each module directory.
|
|
135
|
+
- Root `index.ts` is the public API - be explicit about what's exported, don't `export *` from internal modules.
|
|
136
|
+
- Use `import type` for type-only imports.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Naming
|
|
141
|
+
|
|
142
|
+
| Thing | Convention | Example |
|
|
143
|
+
|-------|-----------|---------|
|
|
144
|
+
| Files | `kebab-case.role.ts` | `action-plan.schema.ts` |
|
|
145
|
+
| Functions | `camelCase` | `createShell`, `evaluateNode` |
|
|
146
|
+
| Types/Interfaces | `PascalCase` | `LayoutNode`, `ShellConfig` |
|
|
147
|
+
| Constants | `UPPER_SNAKE_CASE` | `TEMPLATE_REGEX`, `DEFAULT_TIMEOUT` |
|
|
148
|
+
| Variables | `camelCase` | `currentValue`, `pendingRequests` |
|
|
149
|
+
| Booleans | `is`/`has`/`should` prefix | `isOptional`, `hasChildren` |
|
|
150
|
+
| Factories | `create` prefix | `createShell`, `createMessageBus` |
|
|
151
|
+
| Type guards | `is` prefix | `isComponentNode`, `isBinding` |
|
|
152
|
+
|
|
153
|
+
Names carry their SUBJECT — the reader must never have to open the body to learn what a thing operates on or emits:
|
|
154
|
+
|
|
155
|
+
- **A producer is named for the block it emits** — `componentPalette`, `actionCatalog`, `transformDsl`, `editingGuide`. If the name and the emitted block's heading disagree, one of them is wrong. Annotate definitions `satisfies Producer`.
|
|
156
|
+
- **A function that transforms X carries X in its name** — `namespaceActionId(def)`, never `namespaced(def)`. Same for locals: `buildResult`, not `built`; `builtAction`, not `def`.
|
|
157
|
+
- **Agent files are `<name>.agent.ts` exporting `<name>Agent`.** Producers live in `producers.ts` (agent-domain) or `knowledge.ts` (app-domain) — never inside agent files. A lone producer in an agent file is a filing error, not a convention.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Patterns
|
|
162
|
+
|
|
163
|
+
**Factory functions** for stateful objects. Return an explicit interface, not the whole closure:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export const createMessageBus = (): MessageBus => {
|
|
167
|
+
const subscribers: Subscriber[] = [];
|
|
168
|
+
// ... private state
|
|
169
|
+
const send = (msg: Message): void => { ... };
|
|
170
|
+
const receive = (addr: Address, handler: Handler): Unsubscribe => { ... };
|
|
171
|
+
return { send, receive };
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Subscriptions return cleanup functions:**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const unsubscribe = bus.on('topic', handler);
|
|
179
|
+
// later
|
|
180
|
+
unsubscribe();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Single config object** for function dependencies, not positional args:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// yes
|
|
187
|
+
export const createShell = (config: ShellConfig, deps: ShellDeps): Shell => { ... };
|
|
188
|
+
|
|
189
|
+
// no
|
|
190
|
+
export const createShell = (config: ShellConfig, bus: MessageBus, reg: Registry, eb?: EventBus): Shell => { ... };
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Model-boundary schemas** (tool inputs, envelope fields, anything a model fills in):
|
|
194
|
+
|
|
195
|
+
- Models say `null` for "absent". Every optional field is `.nullish()`, never bare `.optional()`; code normalizes `?? undefined`. A field that is genuinely REQUIRED stays strict — `null` there is a real failure.
|
|
196
|
+
- No `.transform()` in tool input schemas — they serialize to JSON Schema for the wire and `z.toJSONSchema` throws on transforms. Normalize inside `execute`.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Immutability
|
|
201
|
+
|
|
202
|
+
- Don't mutate function inputs. Spread to create new objects.
|
|
203
|
+
- `let` only when reassignment is genuinely necessary.
|
|
204
|
+
- `push` is fine in local accumulators inside pure functions. Not fine on external state.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Error Handling
|
|
209
|
+
|
|
210
|
+
- Custom error classes with error codes and context for domain errors.
|
|
211
|
+
- `Result<T>` (`{ ok: true, data } | { ok: false, error }`) for functions where callers are expected to handle failure.
|
|
212
|
+
- Event/message handlers catch their own errors and log them - never crash the bus.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Comments
|
|
217
|
+
|
|
218
|
+
- Explain **why**, not **what**. If the code needs a "what" comment, make the code clearer.
|
|
219
|
+
- Schema `.describe()` replaces JSDoc for anything that generates documentation.
|
|
220
|
+
- No JSDoc on functions unless the signature is genuinely ambiguous.
|
|
221
|
+
- No commented-out code. Git has history.
|
|
222
|
+
- Section headers (`// ═══` major, `// ───` minor) to organize large files.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Build
|
|
227
|
+
|
|
228
|
+
- ESM source, dual ESM/CJS output via tsup.
|
|
229
|
+
- Target ES2022, Node >= 18.18.
|
|
230
|
+
- Minimal dependencies. Every dep must earn its place.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niscorp/nisc",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@niscorp/charter": "0.1.0",
|
|
23
|
+
"@niscorp/cli": "0.2.0",
|
|
23
24
|
"@niscorp/loom": "0.1.0",
|
|
24
|
-
"@niscorp/moss": "0.1
|
|
25
|
-
"@niscorp/cli": "0.1.0",
|
|
25
|
+
"@niscorp/moss": "0.2.1",
|
|
26
26
|
"@niscorp/cortex": "0.1.0",
|
|
27
|
-
"@niscorp/
|
|
27
|
+
"@niscorp/nova": "0.1.1",
|
|
28
|
+
"@niscorp/prism": "0.1.1",
|
|
28
29
|
"@niscorp/solid": "0.1.0",
|
|
29
|
-
"@niscorp/
|
|
30
|
+
"@niscorp/strata": "0.1.0",
|
|
30
31
|
"@niscorp/tide": "0.1.0",
|
|
31
32
|
"@niscorp/vex": "0.1.0",
|
|
32
|
-
"@niscorp/signal": "0.1.0"
|
|
33
|
-
"@niscorp/strata": "0.1.0"
|
|
33
|
+
"@niscorp/signal": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"zod": "^4.2.0"
|