@niscorp/nisc 0.4.0 → 0.6.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.
- package/AGENTS.md +50 -5
- package/CHANGELOG.md +510 -0
- package/STYLE_GUIDE.md +26 -3
- package/package.json +9 -9
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
|
-
> **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.
|
|
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, and then the last part of this file, "Changing a package": where a new thing goes, and the builds that fail when it goes somewhere else.
|
|
6
6
|
|
|
7
7
|
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.
|
|
8
8
|
|
|
@@ -60,7 +60,8 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
|
|
|
60
60
|
| Package | Use it for | Don't use it for |
|
|
61
61
|
|---|---|---|
|
|
62
62
|
| `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 | — |
|
|
63
|
-
| `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 |
|
|
63
|
+
| `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`). What is not its core is a feature, created by the app and named in the manifest's `features`: `/integrations` (separate services that add screens) and `/tide` (`createTideFacts`: each committed write, minted into tide as a fact) | client-degrade apps — they wire their own shell. Routing inside the app: there is none, shell state is the truth |
|
|
64
|
+
| `tide` | automations, as data: a reflex turns the clock, a write or a signal into one named effect, through a durable ledger. `createTideDriver` paces it (wake on a fact, sleep until the next due instant). `/postgres` is its store over Postgres (`createTideStore(pool)`; `TIDE_SEQUENCE` is its tables, for `runtime.tables`). Under moss a write reaches it through moss's `/tide` feature, and its ledger is read through vex like any table | a step language — a flow of several steps is a chain of reflexes, each effect producing the fact the next one fires on |
|
|
64
65
|
| `charter` | the policy document: roles → glob selections over the app's universes, resolved per principal, verified at boot | enforcement — the governed target enforces |
|
|
65
66
|
| `prism` | every transform: shaping, formatting, branching | anything a schema or layout expresses directly |
|
|
66
67
|
| `vex` | query and mutation endpoints: `{ fingerprint, context }` → rows or effects (see "Using Vex") | writes outside its closed mutation grammar |
|
|
@@ -88,7 +89,7 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
|
|
|
88
89
|
5. All data in and out of an action flows through its declared `endpoints`. Shaping is declared on the endpoint: a static request body is plain JSON; a body derived from the action's data is a Prism config, kept in a sibling `.prism.ts`. Prefer plain JSON when no mapping is required. Components never shape data.
|
|
89
90
|
6. Nova doesn't know Prism: the shell's injected `transform` is the socket that makes Prism endpoint configs work. Wire it once in setup, and fold ambient app context (the app's "today", the session's principal) into the source there — per session on the server under moss, in the shell factory in a client-degrade app — never into per-action data. The socket takes Prism's `evaluate` as it is (`transform: evaluate`), or a wrapper that adds to the source: `(config, source) => evaluate(config, { ...source, today })`. Hand `evaluate` the config object itself, never a copy of it and never one parsed again: it checks a config the first time it sees the object and only evaluates it after that. A test or a tool that must check everything on every call passes `{ check: 'always' }`. A config that is stored is compiled once and its IR executed (`compile`, `execute`).
|
|
90
91
|
7. An endpoint is a contract, not an implementation (D1, D2). In-browser, server, or mixed — actions can't tell the difference, and must not be able to. Server functions are endpoints too: the manifest's `functions(session)` seam runs them in-process under the caller's policy. A client-side `fn` registry is the escape hatch for client-degrade apps, and discouraged elsewhere.
|
|
91
|
-
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
|
+
7a. The manifest's three code seams are distinct, and app code goes in the one that describes it. `functions(session)` is for **endpoints** — things an action calls. `onSession(session)` is for per-shell code that is **not** an endpoint: an observer, a roster, an agent watching the screen. `runs` is the **sink** for model runs, fed by `session.recordRun`. Registering a handler nothing calls, to get code to run per session, is a lie about what that code is — use `onSession`. Both it and `functions` run before the shell finishes building, so neither may touch `session.shell` synchronously. The members of that shell that change what it is made of (`registerAction`, `setLayout`, `addCanvas` and their kin) throw there; the whole shell is `unverifiedShell(session)` from `@niscorp/moss/unverified`.
|
|
92
93
|
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
94
|
9. Formatting and derivation live in Prism transforms, never in components and never in action code.
|
|
94
95
|
9a. File pickers live in the kit. A file never goes over the socket; the app saves it wherever it wants.
|
|
@@ -114,7 +115,7 @@ Pick per need; every piece works standalone. Nova is the only mandatory one for
|
|
|
114
115
|
|
|
115
116
|
**Versions** (`@niscorp/strata`; the why is `packages/strata/DESIGN.md`)
|
|
116
117
|
|
|
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.
|
|
118
|
+
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` (from `@niscorp/tide/postgres`) 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.
|
|
118
119
|
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.
|
|
119
120
|
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.
|
|
120
121
|
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.
|
|
@@ -262,6 +263,7 @@ Stack-independent conventions:
|
|
|
262
263
|
- **Interaction = `ref` + trigger.** Layout nodes carry `ref`; triggers catch `{ event, ref }` and run steps. Event payloads flow via `@event.payload`; no callbacks in props.
|
|
263
264
|
- **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
265
|
- **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.
|
|
266
|
+
- **What a lost connection looks like is the kit's.** When the socket drops, the last screen stays up while the terminal reconnects, and looks exactly like a live one. Every render target is told the connection's status: a browser target says it on its root element (`data-wire-status`: `connecting`, `open`, `closed`, `incompatible`, `static`) and draws nothing, so the kit's stylesheet decides what `closed` looks like. No action, layout or component prop carries it: the connection is the terminal's, not the app's data.
|
|
265
267
|
- **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.
|
|
266
268
|
- **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.
|
|
267
269
|
|
|
@@ -455,7 +457,7 @@ A review pass checks, in order:
|
|
|
455
457
|
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.
|
|
456
458
|
5. No component name contains a domain noun; no component imports shell, action, or data code.
|
|
457
459
|
6. No layout or component branches on roles or capability data (rule 11) — per-principal difference is existence or a served variant.
|
|
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.
|
|
460
|
+
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`, `identity`, `phrases`, `functions`, `onSession`, `runs`, a reaction's `run`, `features`, 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. Moss reads the manifest by a schema when it is handed to `createServer` and refuses to boot on one that does not parse, a code field that is not a function included; this check finds it before a boot does.
|
|
459
461
|
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.
|
|
460
462
|
6c. No role holding a data wildcard reaches the engine's tables, and the charter check resolves against the universe boot does (rule 10a).
|
|
461
463
|
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).
|
|
@@ -464,3 +466,46 @@ A review pass checks, in order:
|
|
|
464
466
|
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.
|
|
465
467
|
|
|
466
468
|
Report violations by rule number. A change that makes the app less declarative, less validated, or less observable is wrong even if it works.
|
|
469
|
+
|
|
470
|
+
## Changing a package
|
|
471
|
+
|
|
472
|
+
**This part is for whoever changes a `@niscorp` package. Everything above it is for whoever builds an app on them.** Read `/STYLE_GUIDE.md`, "The packages are live", before it: what a change may do to an app that works today comes first.
|
|
473
|
+
|
|
474
|
+
**A package is held to the rules nisc sets for apps.** It runs no SQL of its own: its reads and writes are vex entries, as an app's are ("Using Vex"). What crosses a boundary is parsed by a schema (rule 13), rule 16 applies in full, and its tables are strata sequences (rule 17). Moss broke each of them for as long as they were only written down: forty statements sent straight to the pool, one boundary with a schema, 135 exported names of which 71 were imported by nothing, one function of 1,345 lines (the audit is in the history of `docs/plans/moss-refactor.md`). The rules below are what was missing, and each has a build that fails.
|
|
475
|
+
|
|
476
|
+
**A package is made of modules.** A module is a directory under `src/` with an `index.ts`.
|
|
477
|
+
|
|
478
|
+
1. **One door.** Code outside a module imports `../<module>`, never a file inside it.
|
|
479
|
+
2. **Declared dependencies.** Which module may import which is one table, in `scripts/check-modules.mts`, and the table has no cycle. A new import between two modules is a line in that table, in the same change.
|
|
480
|
+
3. **One owner.** Each table and each piece of state that outlives a request belongs to one module. Nobody else writes it.
|
|
481
|
+
4. **Narrow inputs.** A factory takes a typed object of the functions and values it needs. Never the whole manifest, never the whole runtime, never an object that stands in for either.
|
|
482
|
+
5. **Parse where it enters.** A body, a socket message, a row, a fetched payload, a message from another process, a token: parsed by a schema in the module where it enters. No type assertion.
|
|
483
|
+
6. **Nothing flat.** Only `index.ts` sits at the top of `src/`.
|
|
484
|
+
7. **Limits.** A function of up to 200 lines is fine; 300 is the ceiling, for the rare one that reads better whole than cut up. A file is at most 400 lines of code, a line at most 200 wide. Do not split a function or a file to satisfy a number: split it when it has stopped being one thing. The other way round too: a file of a few lines joins the file that uses it, unless it is a published entry point.
|
|
485
|
+
8. **The code speaks.** A comment is there only where the reasoning, a decision or the workings would be misread without it (`/STYLE_GUIDE.md`, "Comments"). No file headers, and no prose on the fields of a type: what an app author needs to know of an exported type is in the package's `DOCS.md`.
|
|
486
|
+
|
|
487
|
+
**How something new gets in.** In this order, and before any of it is written:
|
|
488
|
+
|
|
489
|
+
1. Name the question it answers. It goes in the module that owns that question (a package's `DESIGN.md` says which module owns what; moss's is "How the code is laid out").
|
|
490
|
+
2. If it answers none of them, it is a **feature**: a directory of its own, an entry point of its own, and it reaches the core only through a seam. The core never imports it, reads or writes no row of its tables, and holds no setting of its own for it. The tables that are a feature's it names on itself, and its seams reach those and no other.
|
|
491
|
+
3. If the seam it needs is not there, the seam is the first change, on its own, added to the module that owns the answer.
|
|
492
|
+
4. A new export is a decision, not a side effect of a file needing a name: it is a line in the package's `api.txt` in the same change, and it is documented under its entry point.
|
|
493
|
+
5. A statement outside vex is an entry in the ledger of `scripts/check-raw-sql.mts`, with its reason, or it does not exist. The reasons that hold are a credential read before anybody is signed in, an engine's own store, and a migration.
|
|
494
|
+
6. If it fits nowhere, the architecture is wrong. Say so, and propose the refactor before writing the feature. A refactor is considered every time; adding on top of what is there is how the one function got to 1,345 lines.
|
|
495
|
+
|
|
496
|
+
A defect found on the way gets a test that fails first. A commit moves code or changes behaviour, never both.
|
|
497
|
+
|
|
498
|
+
**The gates.** All four run in `pnpm verify` and in CI.
|
|
499
|
+
|
|
500
|
+
| Run | It fails when |
|
|
501
|
+
|---|---|
|
|
502
|
+
| `pnpm check:modules` | a file other than `index.ts` sits at the top of `src/`; a module imports a file inside another module instead of its door; a module imports one the table does not give it; the table has a cycle; the core imports a feature |
|
|
503
|
+
| `pnpm check:api` | an entry point exports a name that is not in the package's `api.txt`, or `api.txt` names one that is no longer exported; an entry writes `export *` |
|
|
504
|
+
| `pnpm check:raw-sql` | a file outside the script's ledger holds a statement or a call on a database pool, or a file in the ledger holds more or fewer than its line says |
|
|
505
|
+
| `pnpm lint` | in moss's `src/`, or tide's Postgres store or driver: a type assertion other than `as const`; an `any` that becomes a typed value; a written type predicate; a non-null `!`; a `function` declaration; a function over 300 lines, a file over 400 lines of code, a line over 200 wide; an inline directive that switches a rule off |
|
|
506
|
+
|
|
507
|
+
The lint reads types, and a type from another package of the repository is read from its `dist`: `pnpm build` first, as `pnpm verify` and CI do.
|
|
508
|
+
|
|
509
|
+
Rules 3, 4 and 8 have no gate. They are what a review reads for. What two processes do to each other is in no package's tests either: those run on PGlite, which has one connection.
|
|
510
|
+
|
|
511
|
+
`check:raw-sql` sweeps every package but vex and strata, where SQL lives. `check:modules` and `check:api` hold moss. A package joins `check:modules` by being made of modules and getting an entry in `PACKAGES`; it joins `check:api` by an entry in `ENTRIES` and `pnpm check:api --write`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,515 @@
|
|
|
1
1
|
# @niscorp/nisc
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `server.invalidateTenant(tag)` is `server.invalidateTag(tag)`. **Breaking.**
|
|
8
|
+
|
|
9
|
+
BREAKING — approved by moccadroid, 2026-10-12: call `server.invalidateTag(tag)` where the app called `server.invalidateTenant(tag)`.
|
|
10
|
+
- It forgets every identity record that carries one cache tag (`IdentityRecord.tag`). The field named the mechanism and the method named one use of it, a tenant, which moss does not know. The method is named for what it takes. It does what it did.
|
|
11
|
+
- The signal it sends the other processes of a deployment is `invalidate-tag`, where it was `invalidate-tenant`. Processes on the old and the new release do not hear each other's during a rolling deploy: a record forgotten in one is forgotten in the other when it is next resolved again (`sessionRevalidateMs`).
|
|
12
|
+
- Nothing else of moss changes, and the CLI and `@niscorp/nisc` change only in the moss they bring.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @niscorp/moss@0.6.0
|
|
18
|
+
- @niscorp/cli@0.6.0
|
|
19
|
+
|
|
20
|
+
## 0.5.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 0b25acc: Moss is rebuilt as modules, and its public API is what an app is meant to call. **Breaking.**
|
|
25
|
+
|
|
26
|
+
BREAKING — approved by moccadroid, 2026-10-09: import only what `@niscorp/moss` still exports (the list is `packages/moss/api.txt`); the changes an app has to make are listed below, one per part.
|
|
27
|
+
|
|
28
|
+
Moss had grown to twenty-eight files flat in `src/`, one of them a single function of 1,345 lines, and it exported 135 names of which 71 were imported by nothing outside the package. `docs/plans/moss-refactor.md` says how moss is built now; the audit is in that file's history.
|
|
29
|
+
|
|
30
|
+
**How to read this.** The parts are in eight groups, in the order an app meets them: (1) what is imported from where, (2) the manifest, (3) the session and who is calling, (4) integrations, (5) a write and tide, (6) the socket and the shells, (7) pages and the Node entry, (8) the terminal. Each part says what changed and what an app writes instead, and a part that asks nothing of an app says so. The last part, "What an app changes", is the list to work down.
|
|
31
|
+
|
|
32
|
+
**(1) WHAT IS IMPORTED FROM WHERE**
|
|
33
|
+
|
|
34
|
+
**The main entry exports 30 names, not 135. All eighteen entries together export 90, where fourteen exported 185.**
|
|
35
|
+
|
|
36
|
+
106 names left the main entry and one arrived (`verifyApp`). Every name that left is in this table, with what to write instead. A name stays where it is the one way to do something an app is meant to do, or the type of something an app writes; a second way to what the server answers, a part of something exported whole, and a type that only names what an exported function takes or answers and that nothing imports, left.
|
|
37
|
+
|
|
38
|
+
| Was imported from `@niscorp/moss` | Write instead |
|
|
39
|
+
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `createAssertionSigner` | the same name, from `@niscorp/moss/integrations` |
|
|
41
|
+
| the type `StorePress` | nothing to import. A host copies a listing's images itself, in `extension.prepare` ("What a host with a store takes over", below) |
|
|
42
|
+
| `integrationOfAction(actionId)` | `integrations.integrationOfAction(actionId)`, on the object `createIntegrations` answers |
|
|
43
|
+
| the type `CallIntegration` | `Integrations['callIntegration']`, with `Integrations` from `@niscorp/moss/integrations` |
|
|
44
|
+
| `createTideStore`, `TIDE_SEQUENCE`, `TIDE_TABLES`, `TIDE_DDL`, the type `TideStoreOptions` | the same name, from `@niscorp/tide/postgres` |
|
|
45
|
+
| `createTideDriver`, the types `TideDriver`, `TideDriverConfig` | the same name, from `@niscorp/tide` |
|
|
46
|
+
| the type `FactIntake` | `Pick<Tide, 'ingest'>`, with `Tide` from `@niscorp/tide`: what `tide` in `createTideFacts` answers |
|
|
47
|
+
| `integrationByKey(pool, key)` | `integrations.integrationByKey(key)`, on the object `createIntegrations` answers ("Integrations are a feature", below) |
|
|
48
|
+
| `listAttachments(pool, hostAction)`, `listPlacements(pool)` | `integrations.listIntegrations()`: every registered integration with its stored `extension`, where the host's own vocabulary keeps what is attached and placed ("A bundle is nisc's own fields and `extension`", below) |
|
|
49
|
+
| `callIntegrationWith` | `integrations.callIntegration(id, path, init)` |
|
|
50
|
+
| `listIntegrations(pool)`, the type `IntegrationRow` | `GET /operator/integrations` with the operator key answers `{ integrations }` |
|
|
51
|
+
| `describePlacements` | nothing to import. The sentence is the host's to write, from its own vocabulary: in `extension.prepare`, kept with the rest of the extension |
|
|
52
|
+
| `buildContract`, `contractAsMarkdown`, the type `Contract` | `GET /api/integrations/contract`, and the same with `?format=md` |
|
|
53
|
+
| `runIntake`, `copyPress`, `initIntegrations`, the types `Bundle`, `IntakeContext`, `IntakeResult`, `PressResult` | nothing to call. Registering an integration (`POST /operator/integrations`) runs intake, and `createServer` and `migrateTables` create the tables |
|
|
54
|
+
| `mintIntegrationKey`, `hashIntegrationKey` | nothing to call. A key is minted when an integration is registered and answered once; `integrations.integrationByKey(key)` says whose a key is |
|
|
55
|
+
| `verifyAssertion`, the type `Assertion` | an integration's service verifies an assertion itself. The token is `<body>.<signature>`, both base64url; the signature is ed25519 over the body's bytes, under the key `GET /api/integrations/verify-key` answers (SPKI DER, base64); the body is JSON, `{ integration, principal, scope, iat, exp }` |
|
|
56
|
+
| the type `AssertionSigner` | `ReturnType<typeof createAssertionSigner>` |
|
|
57
|
+
| `resolveCatalog(app, principal)` | `(await server.catalogOf(principal)).ids` |
|
|
58
|
+
| `resolveCatalogForRoles(app, roles, installed)`, `filterInstalled` | `(await server.catalogOf(principal)).ids`, for a principal who holds those roles: the server resolves their roles and their installs the way a request does. There is no asking by roles and a list of installs the caller wrote down |
|
|
59
|
+
| the type `Catalog` | `Awaited<ReturnType<MossServer['catalogOf']>>` |
|
|
60
|
+
| `resolvePolicyForRoles(app, grants, roles)` | `server.policyForRoles(roles)` |
|
|
61
|
+
| `resolvePolicyAtReach` | nothing to call. The routes and `server.executeAs` serve an entry at the reach it declares: replay the entry |
|
|
62
|
+
| `resolveRoles(app, principal)` | `(await server.identity(principal)).roles` |
|
|
63
|
+
| `resolvePolicy(app, grants, principal)` | `server.policyForRoles((await server.identity(principal)).roles)` |
|
|
64
|
+
| `resolveVariants(app, principal)`, `resolveVariantsForRoles(app, roles)` | nothing to call. The shell a server builds for somebody has the variant they hold in place of each action's layout, and `server.charterReport()` and `verifyApp` say whether the variants are coherent |
|
|
65
|
+
| `auditClosure`, `wearableOf`, `verifyVariants` | `server.charterReport()` for what a server is serving, `verifyApp(app, grants)` where there is no server |
|
|
66
|
+
| `loadIntegrationActions` | `server.actions()` |
|
|
67
|
+
| `memoKeyOf(app, principal)` | nothing to call: it was the key of memos nothing outside the server can address. To learn whether two principals are served the same application, compare `(await server.catalogOf(principal)).hash` |
|
|
68
|
+
| `createShellHost`, the types `ShellSession`, `ShellOpening`, `ShellReport` | `server.shells`, and `server.page(path)?.host` for a page. The types are `Awaited<ReturnType<ShellHost['session']>>`, `Parameters<ShellHost['session']>[2]` and `ReturnType<ShellHost['list']>[number]` |
|
|
69
|
+
| `shellNeedOf`, the type `ShellNeed` | `live` and `why` on what `shells.snapshot(token, principal)` answers; for a build, on each result of `exportDocuments` |
|
|
70
|
+
| `createSocket`, the types `SocketAccept`, `SocketContext` | `server.socket`, whose type is `MossServer['socket']` |
|
|
71
|
+
| the type `ClientMessage` | not exported. The six messages a terminal sends are listed under "A message on the socket", below |
|
|
72
|
+
| `PROTOCOL`, `CLOSE_INVALID_TOKEN`, `CLOSE_SIGNED_OUT`, `offerToken`, the types `ServerMessage`, `Connection` | the same name, from `@niscorp/moss/protocol`: `import { PROTOCOL, offerToken } from '@niscorp/moss/protocol'`. It is for code that speaks the wire itself (a client written by hand, a check that opens a socket, a transport other than `@niscorp/moss/node`); a terminal on `@niscorp/moss/client` and an app served by `nisc` or `serve` need none of it |
|
|
73
|
+
| `CLOSE_SHELL_FAILED` | the number, `4500` |
|
|
74
|
+
| `createDataLayer`, the type `DataLayer` | nothing to call: `createServer` stands the data layer up. `server.executeAs` replays an entry as a charter role, and `session.answerIntent` runs a query written for an intent |
|
|
75
|
+
| `createGeneration`, `GENERATION_DDL`, `DEFAULT_GENERATION_POLL_MS`, the type `Generation` | nothing to call: the counter is the server's, and `server.generation()` is gone. `server.refresh()` and `server.replaceActions` move it. Its table is part of `MOSS_SEQUENCE`, and it is polled every `runtime.sessionRevalidateMs` (60 seconds when unset) |
|
|
76
|
+
| `createIdentityCache`, `DEFAULT_IDENTITY_MAX`, `DEFAULT_IDENTITY_IDLE_MS`, the types `IdentityCache`, `IdentityCacheContext` | declare `identity` on the manifest. The limits are `runtime.identityMax` (10,000 when unset) and `runtime.identityIdleMs` (30 minutes); `server.invalidateIdentity`, `server.invalidateTenant` and `server.identities` are the rest |
|
|
77
|
+
| the type `IdentityReport` | `ReturnType<NonNullable<MossServer['identities']>['list']>[number]` |
|
|
78
|
+
| `DEFAULT_IDLE_MS`, `DEFAULT_REVALIDATE_MS` | the settings they were the defaults of: `runtime.shellIdleMs` (30 minutes when unset), `runtime.sessionRevalidateMs` (60 seconds) |
|
|
79
|
+
| `devSession` | `session: 'dev-open'` ("The dev verifier is not exported", below) |
|
|
80
|
+
| `sessionVerifierOf(runtime)` | `server.principalOf(token)`, from outside the verifier: it asks whatever `runtime.session` is |
|
|
81
|
+
| the type `SessionVerifier` | `Exclude<NiscRuntime['session'], string>` |
|
|
82
|
+
| `revokeSession(pool, token)` | `revokeAllFor(pool, principal)`, or `session.revoke()` in a function: both sign the principal out everywhere. No exported call deletes one token and leaves the principal's others |
|
|
83
|
+
| `SESSIONS_SEQUENCE`, `initSessions(pool)` | nothing to call. With `session: 'sessions'`, `createServer` and `migrateTables` make the table, in the one ledgered run |
|
|
84
|
+
| `sessionOf(pool, token)` | with `session: 'sessions'`, `server.principalOf(token)`: that deployment's verifier is this lookup. Nothing to call from inside a verifier of the app's own: see "Moss's stored sessions are what `session: 'sessions'` verifies with", in group 3 |
|
|
85
|
+
| `wireFabric`, the types `Fabric`, `FabricSignal`, `FabricMessage`, `FabricApply` | set `runtime.fabric`. Its type is `NonNullable<NiscRuntime['fabric']>`, and a message is what its `publish` is handed |
|
|
86
|
+
| `emitterOf`, `spanClock`, the types `Emit`, `SpanClock` | nothing to call. A sink goes in `runtime.telemetry` (`Telemetry`), and moss catches what it throws |
|
|
87
|
+
| `mintWrites` | name `createTideFacts(...)` in `features` ("Tide's hosting is tide's", below) |
|
|
88
|
+
| `SNAPSHOT_ELEMENT_ID`, `embedSnapshot(snapshot, principal, path?)` | `readDocumentSnapshot()` reads the element and `renderDocument` writes it, in the page it draws. Its id is `nisc-snapshot` |
|
|
89
|
+
| `documentHeaders(principal)` | `(await renderDocument(config)).headers` |
|
|
90
|
+
| `tokenFromCookie(header, key?)` | nothing to call. No route of an app answers to the session cookie: `renderDocument` reads it to choose whose screen to draw, the socket's upgrade reads it, and a sign-in the app answers sets it with `sessionCookies` |
|
|
91
|
+
| `createPageRouter(pages)`, the types `PageRouter`, `PageMatch` | `server.page(path)`: the page a path leads to, its host and the input its path carries |
|
|
92
|
+
| the type `TablesReport` | `Awaited<ReturnType<typeof migrateTables>>` |
|
|
93
|
+
|
|
94
|
+
Where the right column says there is nothing to call and an app did call it, say which and why in an issue: the answer is a call that is meant to be public, not the name coming back.
|
|
95
|
+
|
|
96
|
+
**Integrations are a feature: `@niscorp/moss/integrations`, named in `features`.**
|
|
97
|
+
- Integrations (services that add screens: registration and approval, the proxy, webhooks, the integration key) are not part of moss's core any more. They ship from an entry point of their own, and an app that uses them creates the feature and names it in its manifest:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createIntegrations } from '@niscorp/moss/integrations';
|
|
101
|
+
|
|
102
|
+
const integrations = createIntegrations({ integrationActor, extension });
|
|
103
|
+
const app = defineApp({ charter, actions, features: [integrations] });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- **Nine fields leave `NiscApp`.** One is configuration of `createIntegrations`, under the name it had: `integrationActor`. Seven named one product's vocabulary and are no option of anything: `attachable`, `menuSlots`, `assistantTools`, `publishChecks`, `editorRegions`, `placementNames`, `storePress`. What a host says with them it says in its own `extension` ("What a host with a store takes over", below). The ninth, `installedIntegrations`, was deprecated and never called, and is gone. A manifest that still names one of the nine is refused at boot, as any field the type does not have is: `(the manifest): Unrecognized key: "menuSlots"`.
|
|
107
|
+
- `NiscApp.features` is new. The server starts each feature, in order, once its core stands and before anything is served. `Feature` and `FeatureSeams` are not exported: the two features are moss's own, and an app cannot write one.
|
|
108
|
+
- **An app that does not name the feature has no integrations.** None of `/api/integrations/*`, `/integrations/:id/*` and `/operator/integrations` is served, an `ik_` key is read as a session token and refused as one (`invalid_token`), and no integration's actions are loaded. `integrations` and `integration_actions` are still created, because `nisc.moss` made them and still does; no row of them is read, written or served. The stored actions used to be brought to the running code's grammars at every boot, whatever the app named: the feature has that done when it starts, and a row written by newer code refuses the boot of an app that names the feature, as it did of every app.
|
|
109
|
+
- **`installed` narrows what somebody holds only where the feature is named.** The install list an app's `identity.resolve` answers (`installed`) took every `ext.<audience>.<integration>.<name>` action out of what a principal holds where that integration is not in their list, in every app. The rule is the integrations feature's, and it is laid when the feature starts. An app that ships actions under `ext.…` ids of its own, answers `installed`, and names no feature serves those actions to everybody the charter grants them, whatever `installed` says. Such an app names the feature, `features: [createIntegrations()]`, which brings the rule back with the rest. No lab app ships such an id without the feature.
|
|
110
|
+
- **Each call that moved is on the object `createIntegrations` returned**, and takes no pool: `integrations.callIntegration(id, path, init)` (`MossServer.callIntegration` is gone), `integrations.integrationByKey(key)`, `integrations.integrationOfAction(id)`; and one is new, `integrations.listIntegrations()`. They work once `createServer` has answered. Before that the three that read rows reject with a sentence that says so.
|
|
111
|
+
- New from the entry: `createIntegrations` and the types `Integrations`, `IntegrationsConfig`, `ExtensionConfig`, `ExtensionContext` and `RegisteredIntegration`.
|
|
112
|
+
- `app.actions` is what the app shipped, and stays it. Moss wrote the integrations' actions into that record; it no longer does. Code that read them there reads `server.actions()`.
|
|
113
|
+
|
|
114
|
+
**Tide's hosting is tide's: the store and the driver ship from tide, and the write-fact bridge is a feature, `@niscorp/moss/tide`.**
|
|
115
|
+
- Moss held a Postgres store that implements tide's store contract, and a driver that paces tide. They are tide's storage and tide's pacing, and every moss app installed `@niscorp/tide` because of them. Both are in tide now (the tide changeset), under the names they had, and each is called as it was: `createTideStore(runtime.pool)` takes the pool it took.
|
|
116
|
+
- **The tables are the ones the database has.** `TIDE_SEQUENCE` is the same sequence under the same id, `nisc.moss.tide`, with the same migrations in the same order. A deployment that ran it finds nothing pending.
|
|
117
|
+
- **`@niscorp/tide` is an optional peer of moss.** An app that runs no tide does not install it, and nothing in moss's main entry imports it. An app that uses `@niscorp/tide/postgres` has `@niscorp/strata` installed, which every moss app does.
|
|
118
|
+
- **`NiscApp.facts` is gone: minting a write into tide is a feature.** An app that did it creates the feature and names it in its manifest:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { createTideFacts } from '@niscorp/moss/tide';
|
|
122
|
+
|
|
123
|
+
const app = defineApp({
|
|
124
|
+
charter,
|
|
125
|
+
actions,
|
|
126
|
+
features: [createTideFacts({ tide: () => driver, identity, chain })],
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`tide`, `identity` and `chain` are the three functions `facts` held, under the names they had, and each is asked what it was asked. A manifest that still names `facts` is refused at boot: `(the manifest): Unrecognized key: "facts"`.
|
|
131
|
+
|
|
132
|
+
- **An app that does not name the feature mints nothing.** No write becomes a fact, and a reflex with a `write` trigger is never woken by one.
|
|
133
|
+
- A write is told to the features that hear writes, in the order the app named them, and then to `reactions`. That is the order it was told in while the bridge was part of the server.
|
|
134
|
+
- New from `@niscorp/moss/tide`: `createTideFacts`, and the type `TideFactsConfig`.
|
|
135
|
+
|
|
136
|
+
**The client and terminal entries export what an app calls: six names leave.**
|
|
137
|
+
- `@niscorp/moss/client` loses `SNAPSHOT_ELEMENT_ID` and the type `WireTokenStore`. The id is read by `readDocumentSnapshot()` and written by `renderDocument` (main entry): call those. The type is `WireEnv['tokens']`.
|
|
138
|
+
- `@niscorp/moss/terminal/react` loses `registerWireSlots` and the type `WireSlotOptions`. `reactTarget`, `renderSnapshot` (`/terminal/react/server`) and `inkTarget` register the wire slots on the registry they are handed: call one of those. A kit that wants its own element around a canvas wraps the slot moss registered, read back with `registry.get('CanvasSlot')` once the target is made, as lyceum's does.
|
|
139
|
+
- `@niscorp/moss/terminal/vue` loses `registerWireSlots` and `terminalFrame`: `vueTarget` and `renderSnapshot` (`/terminal/vue/server`) are the two things they were for.
|
|
140
|
+
- Nothing in this repository imported any of the six.
|
|
141
|
+
|
|
142
|
+
**(2) THE MANIFEST**
|
|
143
|
+
|
|
144
|
+
**The manifest is parsed and frozen when it is handed over.**
|
|
145
|
+
- `createServer(app, runtime)` parses the manifest before anything else uses it, and a manifest that does not parse refuses the boot with `The manifest does not parse — refusing to serve:` and one line for each place that does not fit (`actions.home: Unrecognized key: "tittle"`, `shell.inputs: expected a function`). It used to be used as it stood: a definition nova refuses was found when a shell that holds it was built, for whoever connected first, and a seam that was not a function when it was first called.
|
|
146
|
+
- An action, a fragment and a layout are read by nova's schemas, an entry's `dsl` and `mutation` by vex's, its `mapping` by Prism's, `grammars` by strata's. Every other data field is held to the shape `NiscApp` gives it, and a code seam to being a function.
|
|
147
|
+
- A field the type does not have is refused, on the manifest itself, on the shell or a page, on a canvas, a layout variant, an entry, a reaction, `identity` and `phraseKeys`. The compiler already refuses one written in place; one that arrived through a spread or a cast used to be carried and ignored. An app that kept something of its own there keeps it beside the manifest. A component's contract (`shell.components`) and a resource written as an object may still carry more than moss names.
|
|
148
|
+
- An app whose manifest is typed as `NiscApp`, with no cast and no extra field on the way, has nothing to change for the parse.
|
|
149
|
+
- Then `createServer` freezes the manifest, every level of it, in place. Once it has returned, an edit throws a `TypeError` where it is written: `app.actions.x = …`, `delete app.actions.x`, `app.actions = …`, `app.charter.role = …`, `app.entries.push(…)`, a write to a definition, a layout node, a canvas or a variant. A function and an instance of a class (a zod schema a kit handed over as a component's props) are not frozen and not looked inside. Before `createServer` is called nothing is frozen: `defineApp` returns its argument, and an app may go on assembling it.
|
|
150
|
+
- An edit to the charter, the behaviors or the layout variants used to be half served: it reached whoever was resolved next and nobody already resolved, and no check had seen it until a `refresh`. An entry added to `app.entries` after boot was never seeded.
|
|
151
|
+
- **What an app does instead.** Actions that arrive while it runs are handed over as a set, with `server.replaceActions` (next part). A charter, a layout variant, an entry, a behavior and the shell do not change while a server runs: they change in the manifest the next server is created with.
|
|
152
|
+
- An object the app shares between its manifest and its own code is frozen for both: code that wrote to one writes to a copy of its own. The checks of the five lab apps (lyceum, lyra, atrium, relay, encore) ran against the frozen manifest and none of them hit one, in the app, in moss or in nova.
|
|
153
|
+
|
|
154
|
+
**`server.replaceActions(source, actions)`: an app hands over the actions it loads from rows.**
|
|
155
|
+
- New. An app that loads actions from its own tables hands them over: `server.replaceActions('bundles', actions)`. The name is the app's; handing the same name over again puts the new set in place of the old, so an action it no longer names leaves. The actions in force are what the app shipped, then each set. **A set has one writer.** Each feature the app names keeps a set of its own under its own name (`'integrations'` is the integrations feature's, read from its tables), and only that feature replaces it: `server.replaceActions` throws for a feature's name, saying whose the set is; and two features of one name refuse the boot. An app names its own sets anything else.
|
|
156
|
+
- What the app would then consist of is verified first, and every definition of the set is read by nova's schema before that. Refused, the call throws and nothing changed: not the actions being served, not what any role holds, not the record that was handed in. The sentence is the one `refresh` throws, or `Refresh refused — actions nova does not read:` with the action and what is wrong with it. Accepted, it does what `refresh` does in the process it was called in: memos are dropped and every living shell adopts; and the set is frozen, so it is changed by handing over another of the same name.
|
|
157
|
+
- **The set is handed to one process, and no other process is handed it.** The call moves the generation pointer, and every other process on the same database follows the pointer within one poll (`sessionRevalidateMs`, 60 seconds when unset): it reads again each set of actions it has a loader for, runs the same check over what it would then serve, drops what it derived, and has its living shells adopt. That is all the pointer carries. A set that reached a process through `replaceActions` alone is in that process's memory: every other process reaches the same generation and goes on serving the set of that name it was last handed itself, or none.
|
|
158
|
+
- **An app that runs several processes hands the set to each of them.** A set handed to `server.replaceActions` is that process's. The app calls it in each of its processes itself, on a signal of its own; each call moves the pointer once more, and the others drop what they derived and adopt again. The integrations feature's set, `'integrations'`, is the one that crosses processes: it is kept in rows and read through a loader within one poll.
|
|
159
|
+
- Editing `app.actions` and calling `refresh()` is gone. The edit throws, and `refresh` ships nothing: it verifies what is in force again, drops what was derived from it and has living shells adopt, which is what a changed role or install needs.
|
|
160
|
+
- An app whose sets are `ActionDefinition`s nova reads has nothing more to change. One that handed over a definition with a field nova does not have corrects the definition: it used to be served, and was found when a shell that holds it was built.
|
|
161
|
+
|
|
162
|
+
**A server answers what somebody holds, which actions are in force, and what a set of roles may read.**
|
|
163
|
+
- New on `MossServer`: `catalogOf(principal)` answers `{ ids, hash }` from the actions in force; `actions()` is the actions in force, by id; `policyForRoles(roles)` is the vex policy a request by somebody holding exactly those roles is served under. There is no dry run of `replaceActions` (`server.verifyActions` is not on the server): `server.replaceActions` throws with the reason when a set is refused and leaves what is in force unchanged, and `verifyApp` verifies a manifest without a server.
|
|
164
|
+
- New in the main entry: `verifyApp(app, grants)`, the check boot runs for an app that is not booted. Its report is `{ errors, said, charter }` (its type is `ReturnType<typeof verifyApp>`). `errors[].check` is one of `'actions'`, `'charter'`, `'reach'`, `'variants'`. `report.charter` is what `verifyCharter` answered, closure issues included, and `report.errors.filter((error) => error.check === 'variants')` is what `verifyVariants` returned.
|
|
165
|
+
- These are what the functions they replace should have been. `resolveCatalog` read `assignments` and nothing else, so in an app that resolves people through `identity` it answered as `public` for everybody the assignments did not name; the server resolves who somebody is the way a request does. `resolvePolicyForRoles` compiled from whatever manifest and whatever list of grants it was handed, so a check that wrote the grants down itself read a policy no request was served under; the server compiles from the charter and the `behaviors` in force, over every table the database has. What `policyForRoles` answers is compiled for the asking and is the caller's own: nothing done to it reaches a request.
|
|
166
|
+
- **`resolveRoles`, `resolvePolicy` and `resolveVariants` are gone too.** Each read `assignments` and nothing else, so for an app that resolves people through `identity` each answered as `public` for everybody: a wrong answer, handed to whoever picked that way beside `server.identity` and `server.policyForRoles`. Who somebody is, is asked of the server, which resolves them the way a request does; a check that has no server boots one, as the checks of the lab apps do.
|
|
167
|
+
- Code running in a session that wants what its own caller holds asks `GET /catalog` over the session's wire, as before.
|
|
168
|
+
|
|
169
|
+
**`refresh` runs the check boot runs.**
|
|
170
|
+
- `refresh()` ran a shorter copy of the boot check: the charter and the layout variants. It is now the same verification as boot's, entries' `reach` included. It verifies what is in force, and what is in force was verified when it was taken, so an app sees no difference.
|
|
171
|
+
- A boot that is refused prints the chains of steps that never end before it throws. It printed them only when the charter had passed.
|
|
172
|
+
- **Where an app's entries are held, as before.** In two places while a server runs. The running manifest holds the list the app shipped and is their source: the boot check reads it, and a shell is told of the reads on its screens from it. The list is copied once, at boot, into the cache vex replays from (`vex_cache`, or the cache `runtime.cache` hands over), and the cache is what runs them: a request's fingerprint resolves there, and the manifest is not asked. The cache also holds what the list does not: a query `session.answerIntent` wrote while the app runs, and any row it held before this boot, because seeding writes each entry of the list and takes none out. None of that moved in this release, and an app has nothing to change.
|
|
173
|
+
- **A boot the check refuses has seeded no entry.** The app's entries were written into `vex_cache` before the check ran, so a manifest that was refused had already written its rows. They are seeded once the check has passed, by the same seeding, into the same table. A manifest that fails both the check and vex's lint of a write (`Mutation seed "…" fails the authoring lint`) is refused with the check's sentence, where it was refused with the lint's; corrected, the next boot says the other. An app that boots has nothing to change.
|
|
174
|
+
|
|
175
|
+
**The generation counter is read and moved through vex, and a change another process made is followed, or tried again.**
|
|
176
|
+
- Every process polls one row, `moss_generation`, to learn that another changed what the app consists of. It was read and moved by statements sent to the pool: the last SQL in moss with no reason to be outside vex. It is two entries of moss's own now, replayed in process under a policy that admits that one table. No route serves them, and no charter has to grant them. A role granted a data wildcard still denies `moss_generation.*` (AGENTS.md, rule 10a).
|
|
177
|
+
- A move is a compare and a set. Vex's write grammar sets a column to a value and reads none, so it cannot add one: the counter is read, and the next number is written where the counter is still what was read. Where it is not, another process moved it in that instant, and it is read again. A counter that could not be moved after five tries in a row is logged (`[moss:generation] could not move the pointer`). A row somebody deleted used to be moved without a word, and nothing was moved.
|
|
178
|
+
- The counter is moved through `runtime.db`, the client every write is replayed on. It was `runtime.pool`. An app whose `db` and `pool` reach one database, which is every app there is, sees no difference. No span is emitted for the counter's reads and moves, as none was.
|
|
179
|
+
- When the pointer moves, a process reads again the sets of actions it has a loader for (the integrations feature's, kept in rows). A set it was handed with no loader stays what it was handed ("`server.replaceActions`", above). When that reading failed (the database out of reach for a moment, a set this process refuses), the move was counted as seen all the same: the process went on serving what it had until the pointer moved again, and its log said the pointer `could not be read`, which it had been. A move is counted as seen once it has been followed. One that could not be is logged as that (`the generation pointer moved (3 → 4) and this process could not follow it: …`), and is tried again at the next poll (`sessionRevalidateMs`), and the one after, until it is. It is logged when it is news, not at every poll: when it first fails, again when the pointer has moved on or the reload fails another way, and once when it is followed (`followed the generation pointer to 4, after 12 looks that failed`).
|
|
180
|
+
- A process that moves the pointer itself counted the number its own move landed on as seen. When another process had moved the pointer first and no poll had come between, what the other one changed was never read here. Its own move is counted as seen only when nothing else is waiting to be followed; otherwise the next poll follows both.- An app has nothing to change.
|
|
181
|
+
|
|
182
|
+
**A page whose path names one parameter twice refuses the boot.**
|
|
183
|
+
- `path: '/docs/:id/versions/:id'` was accepted, and the canvas its parameters go to was handed one `id`: the later segment's. The first was matched and lost. `createServer` refuses it now: `moss: page "…" names the parameter ":id" twice in "…" — give each parameter of a path its own name.`
|
|
184
|
+
- An app with such a path names the second parameter something else. No lab app has one.
|
|
185
|
+
|
|
186
|
+
**(3) THE SESSION, AND WHO IS CALLING**
|
|
187
|
+
|
|
188
|
+
**`FunctionSession` carries no `runtime` and no `policy`.**
|
|
189
|
+
- The session an app's `functions` and `onSession` are handed loses two fields. `runtime` was the whole environment: the database pool, the operator key, the signing seed. `policy` was the object the routes enforce for everybody holding those roles. Code that runs for one person was handed what reaches every person's rows, and the one object whose edit would change what the routes allow. A session now says who is calling (`principal`, `roles`, `identity`, `actions`), lends `wire`, `grant`, `revoke`, `recordRun`, and one call that is new.
|
|
190
|
+
- **New: `session.answerIntent(writers, asked)`**, for an app that lets a model write a query. `writers` is vex's two seams, `{ generateDsl, mapToShape }`; `asked` is `{ intent, shape, context? }`. The query is written, compiled and run under the policy and the scope values the routes serve the session's principal under, resolved when it is asked, and kept in the cache the routes replay from: `answer.meta.cache.fingerprint` replays over `session.wire` afterwards. It rejects with what vex throws (`code: 'unsatisfiable'`, `'scope_denied'`). Moss keeps one engine for each `writers` object, so hand over the same object each time.
|
|
191
|
+
- **What an app calls instead.**
|
|
192
|
+
- It built a second vex engine from `session.runtime.pool` and `session.runtime.cache` and executed with `scopePolicy: session.policy` → `await session.answerIntent({ generateDsl, mapToShape }, { intent, shape })`. Moss builds that engine over its own adapter and cache, with the app's `behaviors` and `runtime.vexConfig`, and hands neither out.
|
|
193
|
+
- It read `session.policy` to know what the caller may read (which feeds, which stored queries to offer) → `server.policyForRoles(session.roles)`.
|
|
194
|
+
- It read `session.runtime` for its own database (minting a session for somebody else, a sync, a raw statement of the deployment's own) → the runtime the app created and handed to `createServer`, closed over where the app builds its manifest. Moss does not hand it back.
|
|
195
|
+
- A read or a write as the caller is `session.wire`, as before.
|
|
196
|
+
|
|
197
|
+
**The shell a session hands out refuses the nine members that change what a shell is made of.**
|
|
198
|
+
- `registerAction`, `removeAction`, `registerFragment`, `addCanvas`, `removeCanvas`, `setCanvasLayout`, `setLayout`, `setPhrases` and `dispose` throw on `session.shell` and on the `shell` of what `server.shells.session(…)` answers. An action, a fragment, a canvas, a layout and the phrases reach a shell through moss, which verifies them and decides who holds them; added on the shell directly they are not verified, are in nobody's catalog and are gone when the shell is rebuilt. Reads, navigation, `dispatch`, `publish` and the observers work as before, and the type is still nova's `Shell`.
|
|
199
|
+
- **What an app writes instead.** Code that does it anyway takes the whole shell, and says so in its imports: `import { unverifiedShell } from '@niscorp/moss/unverified'`, then `unverifiedShell(session).registerAction(definition)`.
|
|
200
|
+
|
|
201
|
+
**Moss's stored sessions are what `session: 'sessions'` verifies with, and `mintSession` and `revokeAllFor` are for that deployment.**
|
|
202
|
+
- Of the stored credential the main entry keeps two calls, `mintSession(pool, principal, ttlMs)` and `revokeAllFor(pool, principal)`. `sessionOf`, `revokeSession`, `initSessions` and `SESSIONS_SEQUENCE` left. The table is made by `createServer` and by `migrateTables` under `session: 'sessions'`, and only then; the lookup is that deployment's verifier.
|
|
203
|
+
- **An app whose `session` is a function of its own that read moss's stored sessions** (`session: (token) => sessionOf(pool, token)` with a check of its own around it, and `initSessions(pool)` to make the table) has no call left to do that with, and `server.principalOf(token)` is not one: it asks the app's own verifier, which would be asking itself. It chooses one of the two. With `session: 'sessions'`, moss verifies, and the app's own check moves to where it belongs: `revokeAllFor(pool, principal)` at the moment somebody is to be signed out everywhere, or `identity.resolve` answering `{ roles: ['public'], scope: {} }` for somebody who may hold nothing. Or it keeps its verifier and its own credential with it: its own table or signed token, minted and revoked by the app, and neither `mintSession` nor `revokeAllFor`. No app in this repository did this.
|
|
204
|
+
|
|
205
|
+
**The dev verifier is not exported, and a verifier of the app's own is named at boot.**
|
|
206
|
+
- `devSession` is gone from the main entry. It trusts any token that carries a `sub`, and passed as `session: devSession` it booted without the line `session: 'dev-open'` prints. An app that imported it sets `session: 'dev-open'`, which is the same verifier and says so at boot. A check that wrapped it to make a token expire keeps a map of the tokens it issued instead. `mintDevToken` stays.
|
|
207
|
+
- A `session` that is a function prints one line at boot: `[moss] session: a function the app supplied (name) decides who a token is.` Nothing about the verifier changes, and an app has nothing to do.
|
|
208
|
+
|
|
209
|
+
**The record `identity.resolve` answers is read by a schema.**
|
|
210
|
+
- `roles` is a list of strings, `scope` an object, `installed` (when there) a list of strings, `tag` (when there) a string. A record that does not fit is refused with a sentence that names the field, and nothing is held for that principal: a request whose principal resolves to such a record is answered `500` `identity_failed` (next part). It used to be carried on as it was, and failed later, somewhere else, or not at all.
|
|
211
|
+
- What is inside `scope` is not looked at, and a field moss does not name stays on the record the `scope` hook is handed.
|
|
212
|
+
- An app whose `resolve` returns an `IdentityRecord` has nothing to change.
|
|
213
|
+
|
|
214
|
+
**A failure to find out who is calling is `500 identity_failed`, and is logged. It was `401`, and silent.**
|
|
215
|
+
- Every HTTP request is identified once, at the door. A token the verifier says is nobody's is a failed sign-in, and is answered `401` `invalid_token`, as before.
|
|
216
|
+
- Anything that _threw_ while the server found out who is calling was answered `401` `no_identity` too, with nothing in the log: the database being down, the app's `identity.resolve` throwing, an identity record that does not parse. A client that drops its session on a `401` dropped it over a fault of the server's. It is answered `500` `{ error: 'identity_failed', message: 'The server could not find out who is calling. Why is in its log, not here.' }` now, and the error is logged. None of its text is sent.
|
|
217
|
+
- The same answer where the throw used to reach Hono's own handler and came back as the text `Internal Server Error`: a `session` verifier that throws, and a feature's credential (an integration key) that cannot be read or whose principal cannot be resolved.
|
|
218
|
+
- **`no_identity` is gone.** An app whose `identity.resolve` throws for a principal it does not know answers a record for nobody instead (`{ roles: ['public'], scope: {} }`), as lyra and lyceum do, or has its `session` verifier answer `null` for the token. Code that matched on `401` `no_identity` matches on `500` `identity_failed`.
|
|
219
|
+
- The socket is as it was: a session that cannot be opened is `session_failed` and a `4500` close.
|
|
220
|
+
|
|
221
|
+
**`runtime.identityMax` of zero or less refuses the boot.**
|
|
222
|
+
- With an `identity` seam declared, `identityMax: 0` (or less) used to boot and then hang the process on the first sign-in: making room in a cache that has none never ended. `createServer` now refuses it with a sentence that names the setting. Give it 1 or more, or leave it unset for the default of 10,000.
|
|
223
|
+
|
|
224
|
+
**An identity that is forgotten stays forgotten.**
|
|
225
|
+
- `invalidateIdentity`, `invalidateTenant` and `refresh` forget what moss holds of a principal. A record that was being resolved while one of them ran used to be held when it landed, so a role change or an install made at that moment was not seen until the record expired by itself (`sessionRevalidateMs`). It is now handed to whoever was waiting for it and not held. An app has nothing to change.
|
|
226
|
+
|
|
227
|
+
**What a fabric's transport hands over is read by a schema.**
|
|
228
|
+
- `runtime.fabric` carries three signals between the processes of one deployment. What its `subscribe` handed back was used as the type said it was: a message from another process, off a wire that is the host's, was switched on unread, and a `principal` that was not a string reached the identity cache as it came. It is parsed now: the object moss published, with exactly its fields. What does not parse is dropped, and the first one is logged with what was wrong with it and nothing it carried (`[moss:fabric] the transport handed over what is not a message…`).
|
|
229
|
+
- The applier `subscribe` is handed takes `unknown` where it took a `FabricMessage`: a transport passes on what `JSON.parse` answered and casts nothing. A transport typed against the old applier still compiles.
|
|
230
|
+
- **A transport that hands the message over as it was published has nothing to change.** One that handed over the text it received parses it first; one that added a field of its own to moss's message carries that field beside the message, not in it.
|
|
231
|
+
|
|
232
|
+
**(4) INTEGRATIONS**
|
|
233
|
+
|
|
234
|
+
**Integrations read and write through vex, and a feature is handed no pool.**
|
|
235
|
+
- Every statement integrations ran on `integrations` and `integration_actions` is a vex entry now, replayed in process. The audit counted 28 statements in moss with no reason to be outside vex, 24 of them on those two tables; none is left (the generation counter's four went last), and `pnpm check:raw-sql` holds the count at zero.
|
|
236
|
+
- The entries are the feature's own. They are seeded into a cache no route serves, so `/api/vex` answers `cache_miss` for one of them whatever the charter grants whoever asks, and they run under a policy that admits the feature's tables and nothing else. A data grant on `integrations` opens no approval and no registration: those stay behind the operator key.
|
|
237
|
+
- A feature is handed no pool: it hands its reads and writes over as vex entries and replays them in process.
|
|
238
|
+
- A feature names the tables that are its own and the strata sequences that make them. The server migrates the sequences in the one ledgered run, after moss's own and before `runtime.tables`, and `migrateTables` (`nisc migrate`) runs them too.
|
|
239
|
+
- **A feature's own entries reach the tables it named on itself, and no other.** They run outside the charter, under a policy that admits those tables alone, so which tables they are is read off the feature before it runs and is not something it says when it hands entries over. An entry that reads or writes any other table refuses the hand-over, and with it the boot: `moss: the feature "…" handed over an entry that reads or writes a table that is not its own: "…"`. What an app's table holds, a feature reads as the app does: through the app's entries, under the charter.
|
|
240
|
+
- `runtime.vexConfig.entities`, a list of the tables the app's engine is to know, says nothing about these entries: a feature's own, and the generation counter's (group 2), read the tables that are theirs off the database whatever the list names, and know no other. An app that sets the list has nothing to change.
|
|
241
|
+
- No two features of an app share a name. `integrations` and `integration_actions` are made by `nisc.moss`, where every deployment's ledger has them, and are the integrations feature's: it names both.
|
|
242
|
+
- Each feature is started with seams made for it, and what they reach is that feature's. Every feature was handed one and the same object.
|
|
243
|
+
- **A telemetry sink is told of the registry's reads and writes.** Each replay of one of the feature's own entries emits a `vex.execute` span, as a request's replay does and one `executeAs` makes: its `fingerprint` is the entry's (`integrations/list`, `integrations/by-key`, …) and `hasPrincipal` is `false`. The statements they replaced emitted none. Counted in one process, by the entries each path replays: an operator's list of integrations is two spans, a request that carries an integration key is one, and the boot of an app that names the feature is one. A sink that counts `vex.execute` spans, or alerts on their rate, tells these apart by a `fingerprint` that starts with `integrations/`. The generation counter's reads and moves emit none.
|
|
244
|
+
- What a caller sees: `last_import_at` is the server's clock where it was the database's. Nothing to change.
|
|
245
|
+
|
|
246
|
+
**A bundle is nisc's own fields and `extension`.**
|
|
247
|
+
- A bundle had sixteen top-level fields. Five are nisc's own artifacts, and moss reads them as it did: `integration`, `grants`, `grammar`, `actions`, `phrasebook`. Eleven were one product's vocabulary (`meta`, `capabilities`, `offers`, `needs`, `attachments`, `placements`, `settings`, `configuration`, `documents`, `assistants`) or served it (`frames`, next part), and are no field of a bundle. A package does not own a closed vocabulary for what is open: the app that has a store, documents and assistants defines them.
|
|
248
|
+
- **One field is new: `extension`.** Whatever the host app's own vocabulary is; moss does not read inside it. The app says what it may be when it creates the feature:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
createIntegrations({
|
|
252
|
+
integrationActor,
|
|
253
|
+
extension: {
|
|
254
|
+
schema, // a Zod schema: what a bundle's `extension` may be
|
|
255
|
+
check, // (extension, bundle) => string[]: the reasons the app refuses it
|
|
256
|
+
prepare, // (extension, bundle) => extension: once at import, before the row is written
|
|
257
|
+
contract, // a plain value the app tells vendors about its vocabulary
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Only `schema` is required. `check` and `prepare` are handed what moss knows of the bundle: `{ integration, url, actions, fingerprints, layoutMisfits }` (its id, where its service lives, its action definitions, the fingerprints the app serves, and the host's answer about a layout). `createIntegrations` is generic in what the schema answers, so what the app reads back is typed by its own schema.
|
|
263
|
+
|
|
264
|
+
- **A top-level key that is not a field of a bundle refuses it**, with a sentence that names the key: `"meta" is not a field of a bundle. What belongs to the host app’s own vocabulary goes under `extension`.` **Every vendor's bundle changes shape**: what it said in the ten fields it says under `extension`, in the shape its host's schema reads. An app that supplies no `extension` takes no bundle that carries one.
|
|
265
|
+
- Intake holds a bundle to what it did for the fields that stay (its actions' namespace, endpoints, layouts and chains of steps), then reads `extension` with the app's schema and runs the app's `check`. Reasons from all of them are reported together, as before.
|
|
266
|
+
- **`integrations.listIntegrations()` is the one read**, in place of `listAttachments` and `listPlacements`: every registered integration as `{ id, status, url, extension }`, the extension read by the app's schema. A host builds its placements, its listing and anything else from that. An app's own vex entry reads `integrations.extension` as any column.
|
|
267
|
+
- **The operator's list** (`GET /operator/integrations`) answers `id`, `url`, `status`, `requestedActions`, `requestedData`, `approvedData`, `extension`, `lastImportAt`, `lastError`, `actionCount`. `title`, `tagline`, `adds`, `settingsAction`, `offers`, `needs`, `capabilities`, `configuration`, `documents` and `assistants` are gone from it: a pane that showed them reads them off `extension`.
|
|
268
|
+
- **The contract** (`GET /api/integrations/contract`) no longer carries `attachable` and `menuSlots`, and its Markdown has no section on where screens appear. It carries the app's `extension.contract` under `extension`, as the app handed it over.
|
|
269
|
+
- **A preview is no longer a path the proxy forwards.** The reach of an integration is what its actions' endpoints declare. An endpoint a host's own vocabulary names (the preview of an attachment) the host calls itself, with `integrations.callIntegration(id, path, { principal, body })`, which signs the call as the person looking and is not held to reach.
|
|
270
|
+
|
|
271
|
+
**Frames are gone.**
|
|
272
|
+
- A frame was a vendor's own page shown in an iframe through moss: the `frames` field of a bundle, `POST /api/integrations/frame`, `GET /integrations/:id/frame/:token` and `POST /integrations/:id/frame/:token/:call`. All four are gone, and `/integrations/<id>/frame/` is a path of the integration's like any other.
|
|
273
|
+
- An app that wants a vendor's page inside its UI mounts a route of its own (`routes(server)` in `nisc.config.ts`), decides there who may open it, and fetches the page with `integrations.callIntegration`. Moss does not need to know.
|
|
274
|
+
|
|
275
|
+
**What a host with a store takes over.**
|
|
276
|
+
|
|
277
|
+
Everything below left moss. The last commit that still has all of it is `41fc8942` (`packages/moss/src/integrations/`: `bundle.ts`, `intake.ts`, `press.ts`, `placements.ts`, `reach.ts`, `registry.ts`, `entries.ts`, `contract.ts`), for a host that lifts it into its own `extension`.
|
|
278
|
+
- **Bundle fields**, with their schemas (`bundle.ts`): `meta` (`title`, `tagline`, `description`, `story`, `highlights`, `press`); `capabilities` (`{ id, title }`, the id rule); `offers`; `needs`; `attachments` (action id → host action, or `{ to, preview }`); `placements` (action id → menu hub); `settings`; `configuration` (the four closed kinds `toggle`, `number`, `pick`, `line`, each with a default, and their coherence rules); `documents` (the field vocabulary, section types with fragments, the fixed and open section models, regions, checks, the publish path); `assistants` (instructions, grounding, tools, blocks with fragments, starters, what it applies to).
|
|
279
|
+
- **Options of `createIntegrations`**: `attachable`, `menuSlots`, `assistantTools`, `publishChecks`, `editorRegions`, `placementNames`, `storePress`, `pressMaxBytes`. They are the host's own values now, read by its own `check` and `prepare`.
|
|
280
|
+
- **On what `createIntegrations` returns**: `listAttachments(hostAction)` and `listPlacements()`. A host derives both from `listIntegrations()`.
|
|
281
|
+
- **Checks** (`intake.ts`, the host's `check` now): an attachment names an action of the bundle and a host action that offers a seat, and its preview is under the integration's own prefix; a placement names an action of the bundle and a hub the host offers; a press path is under the integration's own prefix and not under `hook/`; no two capabilities share an id; `settings` is an action of the bundle; a document's id is its own, its capability is one the bundle declares, its publish path is under the integration's own prefix, its regions are actions of the bundle or regions the host offers, its checks are ones the host offers, its section types and field keys are not repeated, a `pick` has two options, a `list` is in no section, its fragments fit the kit, a fixed section's type is in the registry and what it extends is a sibling; an assistant's id is its own, it applies to a document or a screen of the bundle, its grounding names fingerprints the app serves, its tools are ones the host offers, and its blocks hold the same rules as a section's fields and fragments. `bundle.actions`, `bundle.fingerprints` and `bundle.layoutMisfits` are what a `check` needs of moss for them.
|
|
282
|
+
- **Helpers**: `copyPress` (`press.ts`: each press image fetched once from the vendor's service with a timeout and a bound on its bytes, refused unless it is an image, handed to `storePress`, the bundle refused whole when one fails), which is a host's `prepare` now; and `describePlacements` (`placements.ts`: the sentence that says what appears where, in the host's words).
|
|
283
|
+
- **The proxy's reach for a preview** (`reach.ts`): a preview was forwarded for whoever held the attached action. A host makes the call itself, with `callIntegration`.
|
|
284
|
+
- **Columns**: of `integrations`, `title`, `tagline`, `description`, `story`, `highlights`, `press`, `adds`, `settings_action`, `offers`, `needs`, `capabilities`, `configuration`, `documents`, `assistants` and `frames`; of `integration_actions`, `attach_to`, `preview` and `place_in`. One column holds what replaced them, `integrations.extension`.
|
|
285
|
+
- **The contract's** `attachable` and `menuSlots`, and the Markdown that explained them: a host says them in `extension.contract`.
|
|
286
|
+
|
|
287
|
+
**Registering an integration is one transaction, and an approval is verified before it is written.**
|
|
288
|
+
- A bundle's row, its actions and the hash of its key landed as separate statements. One that failed part-way left a row that said one thing over actions that said another, or over none. They are one transaction now: a bundle lands whole, or the import before it is still there, whole.
|
|
289
|
+
- **`runtime.db` has to have `transaction`.** Landing a bundle is a vex batch, and a batch needs a client that can transact, as any batch an app writes does. A `db` that cannot answers `500` (`Batch mutations require a transactional client.`) to a registration. `createPglitePool` has one; a pool an app wraps itself gives `transaction` as it gives `query`.
|
|
290
|
+
- Approving wrote the approval, handed the actions to the running manifest, and wrote the row back when that was refused. A process that died between the two writes left an approved row the next boot refused. The route now asks first whether the app could be served with what the integration shipped, and writes only then. A refused approval answers the same `422` with the same reason.
|
|
291
|
+
- An approval that is refused when it is asked about no longer leaves its reason in `last_error` (`lastError` in the operator's list): nothing is written. A pane that read why an approval was refused off the list reads it off the answer.
|
|
292
|
+
- Asking first makes a refusal after the write rare, and something else can still change what the app consists of between the question and the hand-over. An approval that was written and that the app then does not take is taken back, as it always was: the row is pending again with the reason in `last_error`, and the answer is the same `422`.
|
|
293
|
+
- Importing a bundle again for an approved integration asks the same question, and was answered by putting the bundle's actions into the running manifest and taking them out again when the check threw: a refusal dropped every identity record and every compiled policy twice and moved the generation pointer twice, in every process. The bundle is verified as a candidate now, and the running app is not touched. The answer is the same `422`, and an import that is accepted moves the pointer once.
|
|
294
|
+
|
|
295
|
+
**Two registrations of one new integration at once are answered one key, and it is the key that works.**
|
|
296
|
+
- Registering asks how the integration stands, mints a key when it holds none, and lands the bundle with the key's hash. Two registrations of one new integration in the same moment (an operator's tool that retries while the first call is still out, two operators) were both told it holds no key, both minted one, and the later landing wrote its hash over the earlier one's. Both were answered `200` with a `key`, and one of the two keys was nobody's from the start. Found by running the routes from two connections of a real Postgres: 200 rounds, 200 dead keys.
|
|
297
|
+
- The key's hash is written where the row holds none, in the same transaction as the bundle, and the registration reads off what was written whether its key is the one that was kept. The one whose key was kept is answered it. The other is answered as any import after the first is: `200`, its bundle landed, and no `key`.
|
|
298
|
+
- **A tool that registers reads `key` from the answer that carries one**, as it did, and does not expect one from every answer to a first registration it sent twice. Nothing else to change.
|
|
299
|
+
|
|
300
|
+
**No call out to an integration waits for ever.**
|
|
301
|
+
- New on `createIntegrations`: `outboundTimeoutMs`, default `30000` (nova's default wait for an endpoint's reply). It is a whole number of milliseconds from 1 to 2^31 − 1; anything else (`Infinity`, `0`, a fraction) throws where `createIntegrations` is called.
|
|
302
|
+
- **The sentence put under a call that failed with none names no product.** It was `The <id> add-on is not answering right now. The rest of the studio is unaffected.` It is `The <id> integration is not answering right now. The rest of the app is unaffected.` A check that reads the old sentence off a screen reads the new one.
|
|
303
|
+
- **The `integration.call` span says which integration under `integration`.** The attribute was `addon`. A sink or a dashboard that reads `attributes.addon` reads `attributes.integration`.
|
|
304
|
+
- Every call out is given up on when it has taken `outboundTimeoutMs`, its answer and its body together: a screen's call through the proxy, a vendor's webhook, `callIntegration`, the bundle fetched at registration, and the operator's probe. It reads as a service that is unreachable: `502` from the proxy, the webhook and a registration whose bundle did not arrive; `callIntegration` rejects; the probe answers `status: 0`. None had a timeout, and a service that never answered held the call for as long as the process lived.
|
|
305
|
+
- **An integration that takes longer than thirty seconds to answer, or whose answer streams for longer, is cut.** Set `outboundTimeoutMs` to what the slowest one needs. A longer `timeoutMs` on the integration's own action is not enough by itself: the shorter of the two ends the call.
|
|
306
|
+
|
|
307
|
+
**Two migrations of `nisc.moss` arrive with this release, and one takes columns out.**
|
|
308
|
+
- `nisc.moss` gains its third migration (next part) and its fourth. The fourth adds `integrations.extension` and drops the columns that held one product's vocabulary: `title`, `tagline`, `description`, `story`, `highlights`, `press`, `adds`, `settings_action`, `offers`, `needs`, `capabilities`, `configuration`, `documents`, `assistants` and `frames` of `integrations`; `attach_to`, `preview` and `place_in` of `integration_actions`.
|
|
309
|
+
- A deployment that boots with `migrations: 'verify'` runs `nisc migrate` before it starts on this release, as for any release that adds a migration. With the default, `'apply'`, the server applies both when it boots.
|
|
310
|
+
- **An integration registered before keeps its row, its key, its grants, its reach, its phrases and its actions, and loses what the dropped columns held** until it is imported again in the new shape (`POST /operator/integrations` with the same `{ id, url }`): its listing, where its screens were placed, its settings screen. Its `extension` is null until then, and `listIntegrations()` answers it `undefined`.
|
|
311
|
+
- **An app's own vex entry that selects one of the dropped columns** (a store that read `integrations.title`) selects `integrations.extension` and reads the value out of it.
|
|
312
|
+
|
|
313
|
+
**An integration's id is a name, and the hash of its key is its own.**
|
|
314
|
+
- An id is lower-case letters, digits and hyphens, and starts with a letter: what the paths (`/integrations/<id>/…`) and the action names (`ext.<audience>.<id>.<name>`) already assumed. `POST /operator/integrations` with any other id answers `400` and fetches nothing; a bundle that calls itself anything else is refused at intake.
|
|
315
|
+
- An integration that was registered earlier under an id that does not fit goes on being served. It cannot be imported again under that id: remove it and register it under one that fits.
|
|
316
|
+
- The third migration of `nisc.moss` is a unique index on `integrations.key_hash` where it is not null. No two integrations hold the hash of one key. Moss mints a key from 32 random bytes and never wrote one hash to two rows; two rows somebody gave the same hash by hand refuse the migration, and one of them is cleared first.
|
|
317
|
+
|
|
318
|
+
**A signing seed that cannot be a key refuses the boot.**
|
|
319
|
+
- **The signing seed is the integrations feature's own setting: `createIntegrations({ signingSeed })`.** It was `runtime.signingSeed`, the deployment's, and reached the feature as a seam, so every feature an app named was handed the private half of the key assertions are signed with. `NiscRuntime` has no `signingSeed` any more, and nothing a feature is started with carries one. An app that set it moves the value from the runtime to where it creates the feature; a runtime that still names it is refused by the compiler where it is written in place, and is otherwise carried and ignored, which signs with a key made at boot. Lyra is changed here: `LYRA_SIGNING_SEED` is read where the feature is created.
|
|
320
|
+
- The seed makes the keypair assertions are signed with the same in every process and across restarts. It is 32 bytes, as base64. One that decoded to fewer was dropped without a word and a random key was generated in its place: two processes given the same seed signed with different keys, each refused what the other had signed, and an integration holding the public half of one refused every call from the other. Nothing said why.
|
|
321
|
+
- A seed that is given and decodes to fewer than 32 bytes throws now, when the signer is made: for a server that names the integrations feature, while it boots; for `createAssertionSigner(seed)` called directly, at the call. `moss: the signing seed decodes to 13 bytes, and a signing key is 32. Give 32 random bytes as base64 (\`signingSeed\` of \`createIntegrations\`), or give none: …` The sentence says the seed's length and never the seed.
|
|
322
|
+
- No seed is as it was: absent, or the empty string, generates a key that is kept nowhere. A seed longer than 32 bytes is its first 32, as it was.
|
|
323
|
+
- **An app whose seed is shorter than 32 bytes gives one that is 32** (`node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"`), or removes it. Such an app was already signing with a random key: every integration that holds its public key reads the new one from `GET /api/integrations/verify-key`, once, and it stays put from then on.
|
|
324
|
+
|
|
325
|
+
**The contract lists the fingerprints its caller may replay.**
|
|
326
|
+
- `GET /api/integrations/contract` listed every fingerprint the app serves under `fingerprints`, whoever asked: it compiled the caller's policy and then kept every entry. It lists the reads and writes the caller's own policy can touch now, as vex's discovery does for that policy.
|
|
327
|
+
- **Asked with no credential, it lists what `public` may replay.** An integration's author asks with the integration's key and `x-nisc-acts-for`, and reads what the integration's actor reaches.
|
|
328
|
+
- Intake is as it was: it admits an endpoint that names any fingerprint the app serves. Whether a call is served was, and is, decided by the caller's policy when it is made.
|
|
329
|
+
|
|
330
|
+
**Three answers of the integration routes differ at the edges.**
|
|
331
|
+
- A stored action definition that nova's schema refuses is left out of what is served, with a line in the log. It used to be served as it stood.
|
|
332
|
+
- A route body that is JSON and not an object (`null`) reads as an empty body. The route used to answer `500`.
|
|
333
|
+
- A component whose `propsSchema` cannot convert itself to JSON Schema is listed in the contract by its name and description. The contract route used to answer `500`.
|
|
334
|
+
|
|
335
|
+
**(5) A WRITE, AND TIDE**
|
|
336
|
+
|
|
337
|
+
**A lane that throws on a write no longer keeps the other from hearing of it.**
|
|
338
|
+
- A committed write is told to the features that hear writes (`createTideFacts`) and then to the reactions (`app.reactions`). When the fact lane threw (its `tide()`, `identity(scope)` or `chain` throwing on a scope the app did not expect) no reaction heard of that write, and the throw went to vex. Each is told on its own now: the throw is logged (`[moss:writes] …`) and the reactions run.
|
|
339
|
+
- A reaction that throws was already kept from the next reaction, and still is. An app has nothing to change.
|
|
340
|
+
|
|
341
|
+
**`x-tide-depth` is read as digits, not with `Number()`.**
|
|
342
|
+
- A write through vex may name its place in a chain of effects in two headers, `x-tide-cause` and `x-tide-depth`, which the `chain` gate of `createTideFacts` then decides whether to believe. The feature reads them itself; the core's routes used to. The depth was read with `Number()`: an empty header was depth `0`, the start of a chain, and so `0x2` was 2, `1e1` was 10 and thirty nines a depth no whole number holds. A depth is digits and nothing else now, and a number that is exact. Anything else is no position: `chain` is not asked and the write is heard with none.
|
|
343
|
+
- A caller that writes the header as `String(depth)` has nothing to change; lyra's does.
|
|
344
|
+
|
|
345
|
+
**(6) THE SOCKET AND THE SHELLS**
|
|
346
|
+
|
|
347
|
+
**A message on the socket is read whole, by a schema, or not at all.**
|
|
348
|
+
- The server parses what a terminal sends. A message with a field the protocol does not name used to be routed with that field ignored; it is now answered `invalid_message` and nothing of it is acted on.
|
|
349
|
+
- A terminal built on `@niscorp/moss/client` sends no such message, and has nothing to change. A terminal written by hand sends exactly one of `{ type: 'event', canvas, event }`, `{ type: 'publish', channel, payload? }`, `{ type: 'popTo', canvas, instance }`, `{ type: 'back' }`, `{ type: 'resync' }`, `{ type: 'reset' }`, and nothing beside those fields.
|
|
350
|
+
- `invalid_message` is one sentence for every message that cannot be read. An event whose `event` is not an object used to get a sentence of its own, with its `canvas` on the reply; it gets the same answer as the rest.
|
|
351
|
+
- The terminal (`@niscorp/moss/client`) parses what the server sends, and the screen a drawn page carries. A message it cannot read is named by its type in the console and dropped: a tree holding something that is not one of nova's four nodes, or a node with a field nova's node does not have, used to be handed to the kit and is now not shown. An app has nothing to change.
|
|
352
|
+
- `readDocumentSnapshot` answers `undefined` for a snapshot that does not parse whole. It used to drop a `seed`, `path` or `live` of the wrong type and start from the rest.
|
|
353
|
+
- The terminal now runs zod in the page. Zod was already a peer every app installs; a page whose content security policy forbids `eval` may see zod's one probe for it reported, and sets `z.config({ jitless: true })` in its entry to skip the probe.
|
|
354
|
+
|
|
355
|
+
**The address of an upgrade is read by a schema, and a protocol is digits.**
|
|
356
|
+
- What a terminal writes in the address it connects to (`protocol`, `key`, `seed`, `path`, `delta`, `sealed`, `leave`) was read where each was used, each with a test of its own. One schema reads them now, where the handshake begins. What each accepts is what it accepted, with one difference.
|
|
357
|
+
- `protocol` was read with `Number()`: `0x2`, `2.0`, `2e0` and ` 2` were all protocol 2, and a terminal that wrote one was served. A protocol is digits and nothing else. Anything else is answered `client_too_old` and closed `4426`, as `banana` always was.
|
|
358
|
+
- A terminal built on `@niscorp/moss/client` writes the number, and has nothing to change.
|
|
359
|
+
|
|
360
|
+
**An event a terminal sends is read by nova's schema before a shell is handed it.**
|
|
361
|
+
- The socket holds `event` to being an object and no more; the shell it is dispatched into took whatever arrived. It is now parsed by nova's `NovaEventSchema` first: one of `ui:click`, `ui:submit`, `ui:input`, `ui:focus`, `ui:blur`, `ui:model`, `ui:key`, `ui:drop`, with `ref`, `payload`, `origin` (and `key` on `ui:key`) and no other field. One that does not parse is dropped, as an event naming an instance that is not on its canvas is.
|
|
362
|
+
- A terminal built on `@niscorp/moss/client` and one of nova's adapters sends nothing else, and has nothing to change. A terminal written by hand that added a field of its own to an event, or sent a type nova does not define, used to have it dispatched; it sends nova's event.
|
|
363
|
+
- **An event tagged with a canvas the shell does not have is dropped.** A terminal tags an event with the canvas it drew it in, and the shell holds what the event says of itself to that canvas. For a canvas id the shell does not have there was nothing to hold it to, so the event was dispatched with no origin, which nova delivers to every instance of the shell: one message naming a canvas that is not there pressed a button on every screen that had one of that `ref`. The same holds for `session.dispatch(canvas, event)` called in process. A terminal built on `@niscorp/moss/client` has nothing to change. One written by hand that named a canvas of its own invention, to reach every screen at once, tags an event with the canvas it came from; and to tell every screen something, it publishes on a channel (`{ type: 'publish', channel, payload? }`), which every action that listens hears.
|
|
364
|
+
|
|
365
|
+
**Shells for nobody signed in have a limit: `runtime.anonymousShellMax`, default 1,000.**
|
|
366
|
+
- Somebody signed in has one shell, however many terminals they open. Nobody signed in has one for each connection, and opening a connection takes no account and no token: nothing limited how many were alive, so the memory of the process was anybody's to fill.
|
|
367
|
+
- New: `runtime.anonymousShellMax`, how many shells for nobody may be alive at once. A connection over it is sent `{ type: 'error', code: 'at_capacity', message: 'This server is serving as many terminals with nobody signed in as it is set to. Try again in a moment.' }` and closed with the new close code `4503`. No shell is built for it. Somebody signed in is served whatever the count is.
|
|
368
|
+
- **The default is 1,000, from a measurement.** Lyceum's real boot, 2,000 connections for nobody opened through `server.socket` and held, the heap read after a forced collection before and after: 52.5 KB of heap for each (about 110 KB resident); 26.7 KB for an app of one canvas and one action. The Node transport holds about 260 KB resident more for each connection while compression is on. A thousand fit a process given 512 MB.
|
|
369
|
+
- **An app with a public screen that more than a thousand visitors have open at once sets `anonymousShellMax`** to what its memory carries (`Infinity` is no limit, as before). A page of the manifest that is not live opens no socket and takes no place.
|
|
370
|
+
- **It is one count for everybody with nobody signed in, in one process, and it bounds memory.** It is not a limit for each visitor: one client that holds that many connections has every place, and every other visitor with nobody signed in is refused until one closes. Moss does not know a visitor's address (behind a proxy it is the proxy's), so a deployment that faces the internet limits the connections one address may hold at its proxy or load balancer, on `/socket`. `DOCS.md` says this under the setting.
|
|
371
|
+
- A terminal built on `@niscorp/moss/client` has nothing to change: it logs the `error`, and reconnects with its usual backoff, as after any close that is not `4401` or `4403`. The backoff grows for as long as it is refused ("A connection the server refuses is tried again more slowly each time", in group 8). A terminal written by hand treats `4503` as a close to retry later, and later each time, and keeps its token.
|
|
372
|
+
|
|
373
|
+
**A failure on the socket stays one terminal's, and its text stays on the server.**
|
|
374
|
+
- A message the server reads and then throws on used to let the throw out into the transport's listener, where Node ends the process and every session with it. It is now logged, and the terminal is sent an `error` with the code `message_failed`; the connection stays open.
|
|
375
|
+
- A session the server could not open used to send the terminal the error's own text, which can name a host, a table or a database user. The `session_failed` error now says only that the session was refused, and the error is in the server's log. It still names the definitions that failed validation, when that is the cause.
|
|
376
|
+
- A handshake that failed (the verifier threw, what somebody holds could not be asked, their shell could not be built) emitted no span: with the verifier down, upgrades did not turn into errors on whatever reads `runtime.telemetry`, they stopped being counted. It emits one `socket.upgrade` span now, `status: 'error'`, with `hasPrincipal` as far as the handshake got. A sink that counts `socket.upgrade` spans by status gains a value.
|
|
377
|
+
- An app has nothing to change.
|
|
378
|
+
|
|
379
|
+
**A living shell holds what its principal holds now, and nothing that left.**
|
|
380
|
+
- When what the app consists of changes (`replaceActions`, `refresh`, an integration approved or removed, a change another process made), every living shell adopts. It used to register what was held and never take anything away: an action that was removed, or that its principal was no longer granted, stayed mounted and pushable in every shell that had it, until that shell was rebuilt. It is now removed from the shell: its instances are unmounted, it leaves the history `back` walks, and a push of it fails as for any action the principal does not hold.
|
|
381
|
+
- **Every living shell is every one the server built that has not ended**: the shell the app keeps for each signed-in person, the shell built for each connection of nobody signed in, and the shell a page builds for each connection, signed in or not. The last two used to keep what they were built with for as long as they lived, so a page somebody had open, and a visitor's screen, went on offering an action that had left. A change reaches every living shell of every host: the app's own, and each page's. A shell built to draw one document (`snapshot`) is read and ended, and adopts nothing.
|
|
382
|
+
- **A host holds a shell that is not kept until it ends**, which is how a change reaches it. It ends when its last terminal leaves, and the host holds nothing of it from then on. The socket does that for every connection it opens. Code that opens such a session in process and attaches nothing (`server.shells.session(null, null)`, `server.page(path).host.session(…)` in a check or a tool) ends it with `session.detach(connection)`, as the socket does for a connection that closed early; left alone, it used to be collected once nothing referenced it, and is now held for as long as the server is. A check that runs and exits has nothing to change.
|
|
383
|
+
- **`server.invalidateIdentity(principal)` resets every living shell of that principal**: the one the app keeps for them, and the shell a page built for each connection of theirs. It reset the kept one alone, so a page they had open went on as who they had been. `reset(principal)` on a page's host answered `false` and did nothing; it rebuilds each of their shells on that host, carrying its terminal across, and answers `true`.
|
|
384
|
+
- A shell whose principal could not be resolved during an adopt used to end the walk, and every shell after it kept the old application. It is logged and keeps what it held; the rest adopt.
|
|
385
|
+
- A shell that was being built while the app changed adopts when its build lands, and so does one a reset was replacing: the replacement is built from what its principal held when the reset began, and used to take the shell's place holding that. After two changes a shell holds what the later one left its principal with, whichever of the two is answered first. A change waits for no other change and for no build: a principal whose shell never finishes building (an `inputs` that never answers) keeps nobody else from adopting, as before.
|
|
386
|
+
- **Each living shell adopts on its own.** After a change the living shells were asked one after another what their principal holds now, each after the one before it had been answered, as in 0.4.0: a principal for whom that is never answered (an app's `identity` that hangs for them) held every shell after theirs for that change. No shell waits for another now. What an app notices: a change asks about every living shell in the same moment, so an app's `identity`, and the read of its assignments, run for every signed-in principal whose record is not held at once, where they ran one after another. Who a principal is, is asked once for all of their shells and remembered, as before.
|
|
387
|
+
- An action a living shell gains may mention a component no layout mentioned before: it is registered when the shell adopts. The component names a shell is built with are read off the actions in force each time they change, and were read once, when the server started.
|
|
388
|
+
- An app has nothing to change. One that relied on a revoked screen staying open until the next sign-in no longer sees it.
|
|
389
|
+
|
|
390
|
+
**A shell host has no `adopt` and no `stop`: both are the server's.**
|
|
391
|
+
- `ShellHost` (`server.shells`, and `server.page(path).host`) loses two members. `adopt()` brought the living shells of that one host level with what their principals hold; `server.refresh()` does it for every host, the app's and each page's, so the member was a second way that left the other hosts on the old application. `stop()` stopped one host's idle sweep and was kept "for hosts that outlive their server"; a host is made by a server and by nothing else now, and `server.close()` stops every host's sweep with everything else the server started.
|
|
392
|
+
- **What an app writes instead.** `server.shells.adopt()` → `server.refresh()`, which also verifies what is in force again, drops what was derived from it, and tells the other processes. `server.shells.stop()` → `server.close()`, which also stops the socket's revalidation, the identity cache's sweep and the generation poll: a check that called `server.socket.stop()` and `server.shells?.stop()` so that its process could end calls `server.close()` once (encore's boot and relay's expiry check are changed here).
|
|
393
|
+
|
|
394
|
+
**A nudge reaches every living shell of a principal, and so does what a reaction delivers.**
|
|
395
|
+
- `server.nudge(principal, channel)` and a reaction's `deliver(principal, channel, payload?)` published onto the shell the app keeps for that principal, and onto no other: an action on a page they had open, listening on that channel, heard nothing. Both reach every living shell of theirs now: the kept one, and the shell a page built for each connection they have to it. Between processes it is as it was: every other process is told the channel, with no payload, and tells the shells it holds.
|
|
396
|
+
- **What each is for.** `server.nudge` tells somebody's screens that something changed, in whichever host and whichever process they live; it carries no payload and answers nothing. `server.shells.deliver(principal, channel, payload?)` puts a message with a payload into the shell the app's own host keeps for somebody, in this process alone, and answers whether there was one; it is as it was. Where code called `server.shells.deliver(principal, channel)` with no payload to say "this changed", `server.nudge(principal, channel)` says it to every screen they have, and to every process.
|
|
397
|
+
- `deliver` on a page's host (`server.page(path).host.deliver(…)`) answered `false` and did nothing, because a page's host keeps no shell. It publishes into each shell that host built for the principal, and answers whether there was one.
|
|
398
|
+
- An app has nothing to change. A page's action that listens on a channel the app nudges, or a reaction delivers on, now hears it.
|
|
399
|
+
|
|
400
|
+
**One shell for a principal, one reset of it at a time, and the canvases it has are its own.**
|
|
401
|
+
- Two first arrivals for one principal in the same moment (a drawn page's read and its socket, two tabs) each built a shell. The second took the first's place and the first was never disposed: its `functions` and `onSession` code ran for as long as the process did. One is built, and both are handed it. `functions` and `onSession` are called once for it.
|
|
402
|
+
- **A build that takes longer than `runtime.endpointTimeoutMs` fails** (30 seconds when unset). The setting bounded a screen's endpoint call, and bounds the build of a shell too. A build runs the app's code: `identity`, the shell's `inputs`, `phrases`. Where one of those never answered for one call, the build never ended. Now it fails when the time is up: a terminal that waited on it is told and its socket closes `4500`, a page that was being drawn for them is served undrawn, and the next connection builds afresh. If the app's code answers after that, the build stops there: none of `functions`, `onSession` and `seeds` is called for it, and no shell is made. **What an app notices:** an app whose `identity`, `inputs` or `phrases` takes longer than the timeout for somebody gets no shell for them, where it got one late. It raises `runtime.endpointTimeoutMs`, which is also how long a screen's endpoint call waits.
|
|
403
|
+
- Resets that overlapped (`server.invalidateIdentity` twice, or with a terminal's own `reset`) could leave every terminal attached to a shell that was no longer the principal's: nothing they pressed arrived. A shell's resets run in turn. A reset whose build throws or runs out of time is logged and leaves the shell that stood, and the reset after it goes ahead.
|
|
404
|
+
- A page's shell that was reset lost the parameters of its path and the instance ids its page was drawn with. It is rebuilt with what it was opened with.
|
|
405
|
+
- A canvas a shell gained while it ran (`shell.addCanvas`, or a push to a canvas its manifest does not list) was never sent to a terminal: the host walked the manifest's list. It is rendered, sent, attached to and read by a snapshot like the ones the shell was built with. A canvas that was removed is sent empty once, and no longer handed to a terminal that attaches later.
|
|
406
|
+
- An app has nothing to change.
|
|
407
|
+
|
|
408
|
+
**A shell is attached to what it uses of a connection.**
|
|
409
|
+
- `server.shells.session(…)` answers a session whose `attach`, `detach` and `resync` took the transport's `Connection`. They take what a shell uses of one: `{ send, close, seal? }`. A `Connection` is one of those, so a transport and a check that pass a connection have nothing to change. An object literal written in place with `onMessage` or `onClose` beside them is refused by the compiler, as any literal with a member its type does not have: the shell never called either, and they are dropped (encore's check is changed here).
|
|
410
|
+
|
|
411
|
+
**A connection that closed before its session was ready leaves nothing attached, and a live connection's credential is asked about by one pass at a time.**
|
|
412
|
+
- Opening a session waits twice: for what somebody holds, then for their shell to be built. A terminal that left in between (a tab closed while its shell was built, a reconnect that gave up) was attached to the shell all the same, and on Node nothing detached it afterwards: `ws` tells a close to whoever is listening when it happens, and the socket started listening after the second wait. A signed-in person's shell then counted a terminal that was not there and was never swept as idle (`runtime.shellIdleMs`); a shell built for nobody was never disposed. The close is heard before either wait now. A connection that closed while its catalog was asked is not handed to a shell host; one that closed while its shell was built is not attached, and the shell idles, or is disposed, at once.
|
|
413
|
+
- A transport's `onClose` is called once for a connection, where it was called up to three times.
|
|
414
|
+
- Every `sessionRevalidateMs` the socket asks `session` about each signed-in connection, one after another. The timer started the next pass whether or not that one had finished, so a verifier that took longer than the interval was asked by two passes at once, then three. A pass that is due while one is still asking is skipped now.
|
|
415
|
+
- A verifier that throws leaves the connection open, as before. That it threw was dropped without a word; it is logged, once for a pass, with how many connections could not be asked about and the first error.
|
|
416
|
+
- A connection whose transport throws when it is told its session expired stayed in the list, so every later pass threw at the same place: no connection after it was asked about again, and the throw was an unhandled rejection. It is forgotten first, closed even if it cannot be told, and what it threw is logged.
|
|
417
|
+
- An app and a transport have nothing to change.
|
|
418
|
+
|
|
419
|
+
**(7) PAGES, AND THE NODE ENTRY**
|
|
420
|
+
|
|
421
|
+
**A document's `root` is any empty element, and one with no place for a screen is not reported as drawn.**
|
|
422
|
+
- `renderDocument` put the screen into the root by looking for `></div>` in it. A `root` that was another element (`<main id="app"></main>`) was written back as it was, with the snapshot beside it and no screen in it, and the page was reported `drawn: true`: the terminal started from a screen that was not in the page. The screen goes inside any root that is an empty element now, an opening tag and at once its closing tag.
|
|
423
|
+
- A `root` with no inside (an opening tag alone, an element that holds something, one closed in its own tag) is answered as a template that does not hold the root always was: the template as it is, `drawn: false`, and one line in the log that names the root.
|
|
424
|
+
- An app that passes no `root`, or one that is an empty `div`, sees no difference.
|
|
425
|
+
|
|
426
|
+
**`DocumentConfig.server` is the three things a document asks.**
|
|
427
|
+
- `renderDocument` and `exportDocuments` took a `MossServer` and called `principalOf`, `page` and `shells` on it. `server` is typed as those three now, and of each shell host as `snapshot` alone. A `MossServer` is one of them: a caller that passes its server has nothing to change, and a check that stood a server up to draw a page can hand over the three.
|
|
428
|
+
|
|
429
|
+
**`serve` stops what it started.**
|
|
430
|
+
- `serve(app, runtime)` (`@niscorp/moss/node`) makes an HTTP listener and a websocket transport and returns the server and neither of them. `server.close()` stopped moss's timers and left the port bound and every terminal connected, and nothing else could reach either. `close()` on the server `serve` returns now stops all three: moss's own timers, then every connection (closed `1001`), then the listener.
|
|
431
|
+
- `attachSocket(…)` answers `{ close }` where it answered nothing: `close()` ends every connection the transport opened (`1001`) and takes no more. A host that ignores what it answers has nothing to change.
|
|
432
|
+
- An app that called `server.close()` on a server from `serve` and went on using the port has to stop doing one of the two. `createServer`'s own `close` is as it was.
|
|
433
|
+
|
|
434
|
+
**A boot that fails stops what it started.**
|
|
435
|
+
- `createServer` starts timers before it has a server to return: the identity cache's sweep, and one idle sweep for the app's shell host and each page's. When a step after them refused the boot (a feature that throws when it is started, a page whose path the router refuses), they went on running, and nothing was returned to stop them by. Every timer is `unref`'d, so a process that exits on a failed boot never saw it; a process that boots again in place (the dev plugin on every edit, a host that retries) kept two more intervals, and what they hold, for each failed boot.
|
|
436
|
+
- A boot that throws stops everything it had started, and then throws what it threw. `close()` on a server that did boot runs the same list.
|
|
437
|
+
- An app has nothing to change.
|
|
438
|
+
|
|
439
|
+
**An interval Node cannot hold is off, and an operator gate that returns nothing goes on to the key check.**
|
|
440
|
+
- `runtime.sessionRevalidateMs` is handed to a timer in two places: the poll of the generation pointer, and the pass that asks about each live connection's session. Node holds a timer's delay in 31 bits and cuts a longer one to 1 ms. So `Infinity` made the pointer be read without pause, and an interval above 2^31 − 1 ms (24.8 days) did that to both. Either is off now, as `0` is. **A deployment that set `Infinity` to mean "never" gets never.**
|
|
441
|
+
- `runtime.operatorGate` may return nothing: its type says so, and it was documented as falling through to moss's key check. A gate that returned nothing without calling `next()` was answered `500`. It goes on to the key check now. A gate that returns a `Response` refuses, and one that awaits `next()` sees the outcome, as before.
|
|
442
|
+
- An app has nothing to change.
|
|
443
|
+
|
|
444
|
+
**The two settings of the transport hold wherever a server's socket is attached: `runtime.socketMaxMessageBytes`, `runtime.socketCompression`.**
|
|
445
|
+
- New: `runtime.socketMaxMessageBytes`, the largest message a connection may send. It could be said to `attachSocket` (`maxMessageBytes`) and not to the runtime. Unset, the limit is the transport's 256 KiB, as before.
|
|
446
|
+
- `runtime.socketCompression` was read by `serve` and by nothing else: `nisc start` and the dev plugin attach the socket themselves and are not handed the runtime, so under those two a deployment that turned compression off was served compressed. The server reads both settings now, once, and the socket it hands out (`server.socket`) carries them to whichever transport is handed it. They hold under `serve`, `nisc start`, the dev plugin, and a host that calls `attachSocket(httpServer, server.socket)`.
|
|
447
|
+
- **A deployment that set `socketCompression` and runs under `nisc start` or `nisc dev` gets what it set**, where it got compression on. Nothing to change, unless the setting was left there by mistake.
|
|
448
|
+
- `attachSocket`'s `options` are for a host that means otherwise, and for an accept of its own that says nothing: a setting named there wins, and one that is not named stays what the socket says. A host that passed `{ compression: runtime.socketCompression }` itself deletes the argument.
|
|
449
|
+
- **In the dev plugin, the connections of an app that an edit replaced are closed (`1001`).** The plugin kept one transport for as long as vite ran, and a terminal that was connected when an edit re-booted the app stayed attached to the server that had been let go. There is one transport for each app that is booted now, set as that app's runtime says and retired with it: a page is reloaded as before, and a terminal that is not a page (a console) connects again, to the app that is booted. An upgrade that meets an app that did not boot is answered `503`, where it was opened and closed with `1011`.
|
|
450
|
+
|
|
451
|
+
**The dev sign-in answers this machine only.**
|
|
452
|
+
- `/dev/as/<who>` (the dev plugin's `signIn`, `dev.signIn` in `nisc.config.ts`) puts a session for whoever the address names in the browser that asks. It asked nothing about where the request came from: with vite listening on a network (`vite --host`), every device on that network could sign in as anybody.
|
|
453
|
+
- It answers a request whose socket ends on this machine: `127.0.0.0/8`, `::1`, or either written as an IPv4-mapped IPv6 address. Any other is answered `403` with a sentence that says why, and the app's `signIn` is not called. What a header says of where a request came from (`X-Forwarded-For`) is not read.
|
|
454
|
+
- New on `mossDev`: `signInFromNetwork: true` answers it to every device that can reach the dev server, for trying a signed-in screen on a phone on a network you trust.
|
|
455
|
+
- **A developer who opened `/dev/as/<who>` from a second device** sets `signInFromNetwork: true` where the app adds `mossDev` itself, or `dev.signInFromNetwork: true` in `nisc.config.ts` under `nisc dev`.
|
|
456
|
+
- **New in `nisc.config.ts`, for `nisc dev`: `dev.signInFromNetwork`, `dev.watch`, `dev.owned`.** The command made the dev plugin with none of its options, so an app under `nisc dev` could not say which changed files re-boot it, or that a route of its own outside moss's prefixes goes to the app server and not to vite. All three are handed to the plugin as they are, and read when the command starts.
|
|
457
|
+
- A dev server behind a proxy on the same machine sees every request as the proxy's, which is this machine: the check cannot tell those apart, and a proxy that listens on a network is its owner's to close.
|
|
458
|
+
|
|
459
|
+
**(8) THE TERMINAL**
|
|
460
|
+
|
|
461
|
+
**Every render target is told the connection's status, the same way.**
|
|
462
|
+
- After a connection dropped, the last screen stayed up and nothing on it said so. The wire has always known (`wire.status()`), and told whoever subscribed; but what a target renders against had no status on it, so the React, Vue and DOM targets were never told, and the tty and ink targets were told only where the host remembered to pass `status: wire.status`.
|
|
463
|
+
- `TerminalApi`, what a target renders against, gains `status(): WireStatus` beside `frame()` and `canvasTree()`. The conductor (`createTerminal`, `mountTerminal`) fills it from its wire, and a change of status is an `update` like a change of a tree, as it already was.
|
|
464
|
+
- **`ttyTarget` and `inkTarget` take no `status`.** They read it from what they render against. An app deletes the line: `ttyTarget({ input, output, status: wire.status })` → `ttyTarget({ input, output })`, and the same for `inkTarget`. What they print is what they printed. (relay's `tty.ts`, `ink.ts` and ink check, and lyceum's ssh door, are changed here.)
|
|
465
|
+
- **The React, Vue and DOM targets say the status on their root element**, as `data-wire-status="connecting | open | closed | incompatible | static"`, from the moment they mount until they are destroyed, and draw nothing for it. What a lost connection looks like is the app's: its stylesheet (`[data-wire-status='closed'] …`), or a component of its kit that reads the status where the target provides what it renders against, `useContext(TerminalApiContext)?.status()` in React and `inject(TerminalApiKey)?.status()` in Vue, and is drawn again when it changes.
|
|
466
|
+
- A page drawn on the server (`renderSnapshot` of each target) is drawn with the status its terminal starts in, so a kit component that draws the status draws the same thing on both sides and the page is adopted: `static` for a snapshot that says `live: false` (no socket is opened for it), `connecting` for every other.
|
|
467
|
+
- **A target of an app's own** is handed the status and may read it; one that hands what it renders against on to a moss target has nothing to change (lyceum's does). **Code that builds a `TerminalApi` by hand** to drive a target directly (a test) adds `status: () => 'open'`.
|
|
468
|
+
|
|
469
|
+
**The wire holds one socket at a time.**
|
|
470
|
+
- `wire.reset()` on a socket that is not open connects again. Pressed while a connect was still in flight (the reset key held down on a slow network, a reset straight after the page loads), the socket it was waiting on was left as it was, with the wire still listening to it. Both opened: two sockets wrote one snapshot, a sign-out or a drop heard on either was taken for the connection's, and the first to close scheduled a third. The wire lets go of the socket it had before it makes another: it is no longer listened to, and it is closed.
|
|
471
|
+
- A wire whose status is `incompatible` stops retrying, and `reset()` is the way to try again once the server has been updated. A connection made that way kept the mark of the one before it: when it later dropped for any reason, the status read `incompatible` again and nothing retried. Whether the two ends can speak is found out for each connection now.
|
|
472
|
+
- An app has nothing to change.
|
|
473
|
+
|
|
474
|
+
**A connection the server refuses is tried again more slowly each time.**
|
|
475
|
+
- The wire waits before it connects again: half a second to a second at first, twice as long after each failure, up to thirty seconds. It forgot that count whenever a socket opened. A server opens a connection before it refuses one, because a refusal is a message and a close code: `4503` when there is no place left for a shell for nobody, `4500` when it could not open the session. So a terminal that was refused each time connected again every half second to a second, for as long as it was refused, and each of those connects had the server do the refusing again.
|
|
476
|
+
- The count is forgotten when a connection is served its first frame, which is the first thing a shell sends whatever attaches to it. A connection that drops after that is tried again after the first, short wait, as before.
|
|
477
|
+
- An app has nothing to change. A terminal that connects where the server serves it no shell (a path that is no page, in an app that declares no shell) is never served a frame: after each drop it waits longer, up to the thirty seconds.
|
|
478
|
+
|
|
479
|
+
**In the full-screen terminal, digits that arrive together are the number they spell, and `onQuit` is the terminal ending.**
|
|
480
|
+
- `inkTarget` acts on a typed number: `12` clicks what is marked `[12]`. It read one digit for each key it was handed, and two keys pressed in turn can be handed over as one (a slow link, an ssh session, a paste): those were dropped, and nothing happened. They are read as the number they spell.
|
|
481
|
+
- `onQuit` was called whenever the ink app unmounted, so a host that destroyed the terminal itself (`terminal.destroy()`, a swap to another target) was told that somebody quit. A handler that ends the process ended it on a swap. `onQuit` is not called for a destroy now, as `ttyTarget`'s never was. A host that relied on `onQuit` to clean up after its own `destroy()` does that cleanup where it calls `destroy()`. relay and lyceum did not rely on it.
|
|
482
|
+
- A component of the kit that throws while ink draws it ends the ink app, and ink says so by rejecting. Nothing handled that: the rejection reached the process, and Node ends a process on a rejection nobody handles, with every other terminal in it (a host that serves terminals over ssh has one for each person). It is logged (`[moss/ink] the terminal ended on an error:`) and reported through `onQuit` now, like a quit: that terminal ends, and the process and the others go on.
|
|
483
|
+
|
|
484
|
+
**WHAT AN APP CHANGES**
|
|
485
|
+
|
|
486
|
+
In this order. An app that uses no integrations and no tide skips steps 3 and 4, and one whose terminal is built on `@niscorp/moss/client` and moss's targets skips step 10.
|
|
487
|
+
1. **Move the packages together.** `@niscorp/moss`, `@niscorp/nisc`, `@niscorp/cli` and `create-nisc` are one breaking release; `@niscorp/tide` moves with them for an app that runs tide, because the store and the driver are in it now.
|
|
488
|
+
2. **Let the compiler list the imports.** Every name that left `@niscorp/moss` is in the table at the top, with what to write instead. A hand-written client or a check that opens a socket takes `PROTOCOL`, `CLOSE_INVALID_TOKEN`, `CLOSE_SIGNED_OUT`, `offerToken`, `ServerMessage` and `Connection` from `@niscorp/moss/protocol`.
|
|
489
|
+
3. **Integrations.** Create the feature (`createIntegrations({...})`), move `integrationActor` off the manifest into it, name it in `features`, and call `integrationByKey` and `callIntegration` on it. Write the app's own vocabulary as `extension` (a schema, and the checks and helpers lifted from "What a host with a store takes over"), have each vendor's bundle say it under `extension`, and read placements, attachments and listings off `listIntegrations()` or off `integrations.extension` in an entry of the app's own. Where a screen framed a vendor's page, mount a route of the app's own for it. Import each integration again after the first start, so its row holds an extension. Check that `runtime.db` has `transaction`. Set `outboundTimeoutMs` if an integration answers in more than thirty seconds. Move `runtime.signingSeed` to `createIntegrations({ signingSeed })`, and give it 32 bytes as base64, or none: a shorter one refuses the boot.
|
|
490
|
+
4. **Tide.** Import the store from `@niscorp/tide/postgres` and the driver from `@niscorp/tide`. Replace `facts: { tide, identity, chain }` with `createTideFacts({ tide, identity, chain })` in `features`.
|
|
491
|
+
5. **The manifest.** Remove any field `NiscApp` does not have. Replace `server.shells.adopt()` with `server.refresh()`, and `server.shells.stop()` with `server.close()`. Where code edited the manifest after `createServer` (most often `app.actions`, then `refresh()`), hand the actions over with `server.replaceActions(name, actions)`, and read the actions in force from `server.actions()`. Where the app runs several processes, hand the set to each of them: a set handed to `server.replaceActions` is that process's. Hand no set over under a feature's name: it is that feature's.
|
|
492
|
+
6. **Functions.** Replace `session.runtime` with the runtime the app created, `session.policy` with `server.policyForRoles(session.roles)`, and an engine built from the two with `session.answerIntent(writers, asked)`. Where a function calls `registerAction`, `removeAction`, `registerFragment`, `addCanvas`, `removeCanvas`, `setCanvasLayout`, `setLayout`, `setPhrases` or `dispose` on `session.shell`, take the whole shell: `unverifiedShell(session)`, from `@niscorp/moss/unverified`.
|
|
493
|
+
7. **Who is calling.** Replace `session: devSession` with `session: 'dev-open'`. Call `mintSession` and `revokeAllFor` only where `session` is `'sessions'`; a verifier of the app's own that read moss's stored sessions becomes `session: 'sessions'`, or keeps a credential of its own. An app that names no integrations feature, ships `ext.…` actions of its own and relied on `installed` to narrow them, names the feature. Have `identity.resolve` answer `{ roles: ['public'], scope: {} }` for a principal it does not know, where it threw. Where code matched `401` `no_identity`, match `500` `identity_failed`. Give `runtime.identityMax` 1 or more, or leave it unset. Where a deployment has a `runtime.fabric`, have its transport hand `apply` the object that was published, parsed, with nothing added to it.
|
|
494
|
+
8. **Checks.** Replace `resolveCatalog` and `resolveCatalogForRoles` with `server.catalogOf(principal)`, `resolvePolicyForRoles` and `resolvePolicy` with `server.policyForRoles(roles)`, and `resolveRoles` with `(await server.identity(principal)).roles`; replace a boot check put together by hand with `server.charterReport()` or `verifyApp(app, grants)`. `server.verifyActions` and `server.generation()` are gone: `server.replaceActions` throws with the reason when a set is refused and changes nothing, and the counter is the server's.
|
|
495
|
+
9. **The terminal.** Delete `status: wire.status` from `ttyTarget` and `inkTarget`. Add `status: () => 'open'` where a test builds a `TerminalApi` by hand. Style `[data-wire-status='closed']` if a lost connection should show.
|
|
496
|
+
10. **A terminal written by hand.** Send the six messages with exactly their fields, and nova's events with exactly theirs, each tagged with a canvas the shell has; treat a `4503` close as one to retry later, and later each time.
|
|
497
|
+
11. **Telemetry.** A sink that counts `vex.execute` spans leaves out, or counts apart, the ones whose `fingerprint` starts with `integrations/`: the registry's reads and writes are spans now.
|
|
498
|
+
12. **Before the first start.** A deployment that boots with `migrations: 'verify'` runs `nisc migrate`: `nisc.moss` has a third and a fourth migration, and the fourth drops columns. A page that more than a thousand visitors have open at once, none signed in, sets `runtime.anonymousShellMax`. A host that calls `server.close()` on what `serve` answered expects the port to be let go. A host that passed `runtime.socketCompression` to `attachSocket` itself deletes the argument: the server's socket carries it.
|
|
499
|
+
13. **In development.** `/dev/as/<who>` answers this machine only: set `signInFromNetwork: true` on `mossDev` to open it to other devices. A terminal that is not a page is closed (`1001`) when an edit re-boots the app, and connects again.
|
|
500
|
+
|
|
501
|
+
### Patch Changes
|
|
502
|
+
|
|
503
|
+
- Updated dependencies [de2a757]
|
|
504
|
+
- Updated dependencies [0b25acc]
|
|
505
|
+
- Updated dependencies [de2a757]
|
|
506
|
+
- Updated dependencies [7fb8322]
|
|
507
|
+
- @niscorp/charter@0.1.3
|
|
508
|
+
- @niscorp/moss@0.5.0
|
|
509
|
+
- @niscorp/cli@0.5.0
|
|
510
|
+
- @niscorp/strata@0.1.6
|
|
511
|
+
- @niscorp/tide@0.1.6
|
|
512
|
+
|
|
3
513
|
## 0.4.0
|
|
4
514
|
|
|
5
515
|
### Minor Changes
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -164,6 +164,27 @@ Plain `name.ts` is fine when the file's role is obvious from context or when it'
|
|
|
164
164
|
|
|
165
165
|
---
|
|
166
166
|
|
|
167
|
+
## Modules
|
|
168
|
+
|
|
169
|
+
A package that holds more than one concern is made of modules. The rules, and how something new gets in, are in AGENTS.md, "Changing a package". What is longer than a rule is here.
|
|
170
|
+
|
|
171
|
+
**A module has one door.** A module is a directory under `src/` with an `index.ts`. Code in another module imports the door (`../identity`), never a file behind it (`../identity/cache`). An entry point of the package is a door (`node/index.ts` is `@niscorp/moss/node`), or one file beside it that exports only what is published (`shells/unverified.ts` is `@niscorp/moss/unverified`). A helper is not merged into an entry file where that would export it. Which question a module answers is in the package's `DESIGN.md`, not in a comment on its door.
|
|
172
|
+
|
|
173
|
+
**What a factory is handed is a type in its own module.** Each member is one function or one value, named for what it answers (`ShellHostConfig`, `SocketConfig`). Not the manifest, not the runtime, not an object carrying either. When the type needs a shape another module owns and may not import that module, the shape is written out where it is needed, and the wiring holds the two to each other where it hands one over. Its name ends in one of two words, by what it means and not by which module it is in. `…Config` is the argument of something another module or an app calls: what the caller chooses (`ShellHostConfig`, `DataConfig`, `IntegrationsConfig`). `…Deps` is what a module's own code hands one of its own parts: what already stands (`ServerDeps` and `RouteDeps` in the server's wiring, `OperatorDeps` in the integrations feature, `ExecuteAsDeps` inside `data`). What a thing holds while it runs is neither: it is its state (`HostState`).
|
|
174
|
+
|
|
175
|
+
**A seam belongs to the module that answers the question.** A feature reaches the core through one object it is handed when it starts. Each member is one function or one narrow value, and each is there because a feature uses it; one that stops being used is taken out, and one is not added for a feature that might want it. The object is made for the feature it is handed to. A member that reaches something owned (a table, a set of actions, the documents in a row) is bound to that feature and takes no name of one, so a feature cannot ask for what is another's; what a feature owns it says on itself, where it can be read before it runs (`Feature.tables`). No member carries a secret: a setting only one feature needs is that feature's own configuration.
|
|
176
|
+
|
|
177
|
+
**What each gate reads.** What each fails on is the table in AGENTS.md, "Changing a package".
|
|
178
|
+
|
|
179
|
+
- `scripts/check-modules.mts`. `PACKAGES` holds, for each package it knows, which module may import which and which modules are features. It reads the relative imports and re-exports of every `.ts` and `.tsx` file under `src/`.
|
|
180
|
+
- `scripts/check-api.mts`. `ENTRIES` maps each entry point of a package to its source file, and the record is `packages/<name>/api.txt`, one line for each exported name. `pnpm check:api --write` records what is exported now; the diff of `api.txt` is what a reviewer reads.
|
|
181
|
+
- `scripts/check-raw-sql.mts`. It sweeps `src/` of every package but vex and strata. Its ledger names each file that holds a statement or a call on a database pool, with its exact numbers and a reason: `credential` (it runs before a principal exists, so there is no policy to run under), `store` (an engine's own storage, in that engine's package), `tables` (a strata sequence). A number that is off fails in either direction, so a statement that is taken out comes out of the ledger in the same change.
|
|
182
|
+
- `eslint.config.mjs`. The block for a package's modules holds rule 16 in full and the three sizes: 300 lines to a function (200 is fine; 300 is the ceiling), 400 lines of code to a file, 200 characters to a line. It lints with the compiler's types, so an `any` cannot become a typed value by being assigned, returned or handed over: `const row: Row = JSON.parse(text)` is the cast it is. Inline configuration is off, so no comment silences a rule. A package joins by having its `src/` added to the block's `files` and to the `lint` script.
|
|
183
|
+
|
|
184
|
+
The 200 is not the formatter's width: `.prettierrc` names 100 and nothing enforces prettier. It keeps a line from holding a paragraph; it does not make the code one width.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
167
188
|
## Naming
|
|
168
189
|
|
|
169
190
|
| Thing | Convention | Example |
|
|
@@ -242,11 +263,13 @@ export const createShell = (config: ShellConfig, bus: MessageBus, reg: Registry,
|
|
|
242
263
|
|
|
243
264
|
## Comments
|
|
244
265
|
|
|
245
|
-
-
|
|
266
|
+
- The code speaks. A comment is there only where the reasoning, a decision or the workings would be misread without it, and that is rare. Every other comment is text a reader has to take in and nothing checks: it goes.
|
|
267
|
+
- Not a comment: what the next line does, what a name already says, what a file or a module contains, what a setting is for, what `DOCS.md` or `DESIGN.md` already says.
|
|
268
|
+
- If a name is only understood with its comment, change the name.
|
|
269
|
+
- No history: what the code used to do, what a review found, a date, a count. That is the commit message.
|
|
270
|
+
- No file headers and no section banners. A file that needs sections has stopped being one thing.
|
|
246
271
|
- Schema `.describe()` replaces JSDoc for anything that generates documentation.
|
|
247
|
-
- No JSDoc on functions unless the signature is genuinely ambiguous.
|
|
248
272
|
- No commented-out code. Git has history.
|
|
249
|
-
- Section headers (`// ═══` major, `// ───` minor) to organize large files.
|
|
250
273
|
|
|
251
274
|
---
|
|
252
275
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niscorp/nisc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The nisc platform as one known-compatible set: every @niscorp package, each pinned to the exact version it was released with.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"node": ">=22.12"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@niscorp/charter": "0.1.
|
|
23
|
-
"@niscorp/cli": "0.
|
|
22
|
+
"@niscorp/charter": "0.1.3",
|
|
23
|
+
"@niscorp/cli": "0.6.0",
|
|
24
24
|
"@niscorp/loom": "0.3.0",
|
|
25
|
-
"@niscorp/moss": "0.
|
|
25
|
+
"@niscorp/moss": "0.6.0",
|
|
26
26
|
"@niscorp/cortex": "0.1.3",
|
|
27
|
-
"@niscorp/
|
|
27
|
+
"@niscorp/prism": "0.3.0",
|
|
28
28
|
"@niscorp/signal": "0.1.3",
|
|
29
29
|
"@niscorp/solid": "0.1.2",
|
|
30
|
-
"@niscorp/
|
|
31
|
-
"@niscorp/
|
|
32
|
-
"@niscorp/
|
|
33
|
-
"@niscorp/
|
|
30
|
+
"@niscorp/tide": "0.1.6",
|
|
31
|
+
"@niscorp/strata": "0.1.6",
|
|
32
|
+
"@niscorp/vex": "0.3.0",
|
|
33
|
+
"@niscorp/nova": "0.3.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"zod": "^4.2.0"
|