@owlmeans/client-planning 0.1.18-rc.36 → 0.1.18-rc.38

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/README.md CHANGED
@@ -9,7 +9,7 @@ planning service runs in-process.
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- bun add @owlmeans/client-planning@^0.1.18-rc.36
12
+ bun add @owlmeans/client-planning@^0.1.18-rc.38
13
13
  ```
14
14
 
15
15
  ## Concepts
@@ -202,7 +202,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
202
202
  your project's skill store (`.agents/skills/`):
203
203
 
204
204
  ```sh
205
- npx @owlmeans/agent-skills@^0.1.18-rc.56
205
+ npx @owlmeans/agent-skills@^0.1.18-rc.57
206
206
  ```
207
207
 
208
208
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
- "schemaVersion": 2,
2
+ "schemaVersion": 3,
3
3
  "package": "@owlmeans/client-planning",
4
- "version": "0.1.18-rc.36",
5
- "generatedAt": "2026-10-20T10:14:53.871Z",
4
+ "version": "0.1.18-rc.38",
5
+ "generatedAt": "2026-10-24T21:07:16.901Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -10,7 +10,65 @@
10
10
  "name": "client-planning",
11
11
  "category": "package-specific",
12
12
  "file": "skills/client-planning/SKILL.md",
13
- "canonicalPath": ".agents/skills/client-planning/SKILL.md"
13
+ "canonicalPath": ".agents/skills/client-planning/SKILL.md",
14
+ "parts": [
15
+ {
16
+ "name": "aux-mirrors",
17
+ "file": "skills/client-planning/parts/aux-mirrors.md",
18
+ "description": "The assignee, team, comment and mention mirrors and resource feeds, and the authenticated scope lifecycle (scopeKey, close, lifecycle.run and lifecycle.mutate).",
19
+ "purposes": [
20
+ "state",
21
+ "auth"
22
+ ],
23
+ "mentions": [
24
+ "makePlanningResourceFeed",
25
+ "scopeKey",
26
+ "lifecycle.run",
27
+ "lifecycle.mutate",
28
+ "registerFeed",
29
+ "client:scope-changed",
30
+ "PlanningReply"
31
+ ]
32
+ },
33
+ {
34
+ "name": "mirror",
35
+ "file": "skills/client-planning/parts/mirror.md",
36
+ "description": "The state mirror's one id space and fold rules, makePlanningFeed, and optimistic writes with head over seq.",
37
+ "purposes": [
38
+ "state",
39
+ "list"
40
+ ],
41
+ "mentions": [
42
+ "appendPlanningStores",
43
+ "planningMirrorOf",
44
+ "applyCommitEvent",
45
+ "applyReceipt",
46
+ "applyCards",
47
+ "syncHelper",
48
+ "syncCards",
49
+ "syncLinks",
50
+ "makePlanningFeed",
51
+ "PlanningFeed",
52
+ "useStoreList"
53
+ ]
54
+ },
55
+ {
56
+ "name": "validation",
57
+ "file": "skills/client-planning/parts/validation.md",
58
+ "description": "CSP-safe browser validation of a served field schema with makePlanningFieldValidator — the supported keywords, refusals and limits, and the external references behind them.",
59
+ "purposes": [
60
+ "form"
61
+ ],
62
+ "mentions": [
63
+ "makePlanningFieldValidator",
64
+ "validateFields",
65
+ "@cfworker/json-schema",
66
+ "JSON_SCHEMA_DRAFT_07",
67
+ "unsafe-eval",
68
+ "SchemaInvalid"
69
+ ]
70
+ }
71
+ ]
14
72
  }
15
73
  ]
16
74
  }
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/client-planning
9
9
 
10
10
  **Layer:** Client
11
- **Install:** `"@owlmeans/client-planning": "^0.1.18-rc.36"` in `dependencies`
11
+ **Install:** `"@owlmeans/client-planning": "^0.1.18-rc.38"` in `dependencies`
12
12
 
13
13
  The client half of OwlMeans planning. It answers the `PlanningFacade` interface of
14
14
  `@owlmeans/planning` over the protocol tree a server mounted with `@owlmeans/server-planning`, keeps
@@ -65,14 +65,18 @@ appendPlanningStores(context) // a browser mirror; a Node client usually
65
65
  from the credential. The execution's own `actor` is never sent.
66
66
  - A client runs no middleware and folds nothing: `store()`, `committed()` and a plugin carrying
67
67
  `before`/`after`/`store`/`mintCode` answer `PlanningUnsupported`. `use()` accepts a schemas-only
68
- plugin, layered over the server's bundle.
68
+ plugin, layered over the server's bundle; its `schemas.app` merges over the served declarations
69
+ (`bundle.app`) in plugin order, and a malformed one is refused (`SchemaInvalid`) before anything
70
+ changes.
69
71
  - `timeout` (ms) bounds every call that is not a long poll; a long poll's deadline is its own hold
70
72
  plus `LONG_POLL_GRACE`.
71
73
 
72
74
  ### The schema bundle
73
75
 
74
76
  `model()` needs the flows and types (`can()`, `available()`, `statusOf()` read the registry with no
75
- round trip). The bundle is fetched in the background once the context is **Ready** and a failure
77
+ round trip). The bundle also carries the application's declarations (`registry.app()`), so
78
+ `registry.fieldsSchema(type)` / `validator(type)` include its base card fields and a definition's
79
+ `app` bag reads back as written. The bundle is fetched in the background once the context is **Ready** and a failure
76
80
  there is swallowed — a browser that is not signed in yet cannot read it, and a context must never
77
81
  fail or wait over it. `model()` and `service.loadSchemas()` load it on first use regardless, and a
78
82
  failed load is retried by the next caller. `schemas: false` skips the background load.
@@ -88,42 +92,9 @@ whole cache — an organization-wide write reaches every project's layer — and
88
92
  `model(card)` of a card resolves its type in its project's layer (a project's own id, a card's
89
93
  nearest primary project ancestor); a specification's type is always code's. A tree without `definitions` has none of this.
90
94
 
91
- ## Browser field validation under CSP
92
-
93
- Bind `makePlanningFieldValidator(type.fields)` to the exact definition returned by the resolved
94
- organization or project bundle. Its `validateFields(fields)` returns `{ valid, errors }`; errors
95
- carry decoded `path`, `keyword`, `schemaPath` and a bounded value-free `message`. Construct one JSON
96
- fields object, validate it and send that same object. No coercion, defaults or property removal
97
- occurs, and the schema is left unchanged. Preserve nested money objects and calendar date strings.
98
- The native server validates every write again.
99
-
100
- The validator interprets schemas with `@cfworker/json-schema`, with no `eval`, `new Function` or
101
- network reference loading. Do not call `makeSchemaRegistry(...).validator()` or
102
- `.assigneeValidator()` in a browser with strict `script-src`: those remain native AJV compilers.
103
- The Draft 7 JSON import carries `with { type: 'json' }` so Node ESM consumers can load the validator.
104
- Static AJV standalone compilation is suitable only when the schema cannot change after building;
105
- organization and project definitions require the runtime interpreter.
106
-
107
- The supported contract is Draft 7's object/array/scalar, required, enum/const, ranges, string
108
- length/pattern, dependencies, boolean, composition and conditional keywords; `$defs`, local/nested
109
- `$id` references and AJV's `nullable` are also supported. `$ref` sibling constraints are enforced as
110
- by native planning AJV. String formats use the same `ajv-formats` full-mode predicates, including
111
- real calendar dates and timezone-aware times. `nullable` adds null to the declared type while
112
- leaving enum and const constraints intact. All references must resolve inside the supplied field
113
- schema; productive recursive schemas are supported, same-instance reference cycles are refused.
114
- Plain JSON values and field names without dot or dollar characters are required.
115
-
116
- Preparation throws `SchemaInvalid` for malformed schemas, unresolved references, unsupported
117
- drafts/keywords/formats, numeric formats, `formatMinimum`/`formatMaximum` extensions and
118
- `multipleOf`. The latter is refused because the interpreter's epsilon and native AJV's quotient
119
- semantics disagree for fractional and very large numbers. Standard annotation keywords and `x-`
120
- annotations do not add validation. Never discard a refused constraint, invent a substitute schema,
121
- enable `unsafe-eval` or proceed with a ready form after preparation fails. Show the error and retain
122
- disabled save controls. Validation reports at most 64 errors, with paths bounded to 512 characters.
123
-
124
95
  ## Mounting in a target
125
96
 
126
- The web half of the target mount (`planning` → Mounting in a target).
97
+ The web half of the target mount (`planning` → Mounting in a target, in that skill's `mounting` part).
127
98
 
128
99
  - `appendPlanningClient(context, { protocols, bind: false, schemas: false })` — the target binds
129
100
  its whole api tree itself, and a signed-out visitor may read nothing, so the bundle loads on
@@ -157,55 +128,6 @@ drill.available().map(rule => rule.name)
157
128
  project-wide read answers `PlanningUnsupported` on a client.
158
129
  - `size: 0` is "no limit"; a paged server answers 100 rows when no size is asked for.
159
130
 
160
- ## The mirror — one id space
161
-
162
- `appendPlanningStores` registers ONE card store for every kind. Projects, cards and specifications
163
- share an id space on the server, so they share one store here: a store per kind lets a card list
164
- that reloads drop the projects beside it.
165
-
166
- That is also why a list reaches the store through **`syncHelper.syncCards(store, items, where)`,
167
- never `replace()`**: every card given is written, every card matching `where` that the list does not
168
- name is dropped, and everything outside `where` is left alone. An unchanged card is not rewritten, so a
169
- periodic re-seed wakes no subscriber. `opts.keep` protects ids a commit wrote while the list was in
170
- flight.
171
-
172
- The fold rules, shared by `syncHelper.syncCards`, the mirror's `applyCards` and `applyCommitEvent`, and
173
- `commits.wait`:
174
-
175
- - an OLDER `seq` never overwrites a newer record — frames and list answers race;
176
- - a committed `delete` removes the row and every link touching it; a project's delete also drops
177
- the rows whose `parents` hold it (the server purged them in the same fold);
178
- - a frame for an id the store never saw still writes a row;
179
- - a `failed` commit leaves the card alone and records the reason in the commit store;
180
- - a frame without `record` (a cross-process bus carries ids only) is re-read through the facade —
181
- without one, only the commit row is written;
182
- - a committed `link`/`unlink` re-lists the card's outgoing links; links written together with a
183
- `create` arrive with the next link seed.
184
-
185
- ## The feed
186
-
187
- ```typescript
188
- const feed = makePlanningFeed(context, {
189
- query: { parent: shedId, type: 'shed:tool' },
190
- filter: { project: shedId },
191
- refresh: 15_000,
192
- onChange: state => setState(state),
193
- })
194
- await feed.ready // never rejects — read feed.error
195
- // …
196
- await feed.stop() // stops folding; the shared socket stays open
197
- ```
198
-
199
- 1. Subscribes to commits FIRST (the socket, when an opener exists).
200
- 2. Seeds: `cards.list` (unpaged unless the query pages) → `syncHelper.syncCards` over `where`
201
- (`queryHelper.criteriaOf(query)` by default). A PAGED seed only writes — a page cannot say what
202
- does not exist.
203
- 3. Folds every frame with `planningMirrorOf(stores).applyCommitEvent`, and re-seeds every `refresh`
204
- ms — the authoritative backstop to a socket that can drop frames.
205
-
206
- `connected` reports whether a socket was open when the feed subscribed; `seeded` tells "nothing
207
- there" from "not loaded yet". A React hook wraps the feed in an effect and stops it on unmount.
208
-
209
131
  ## Waiting for a commit
210
132
 
211
133
  `execute()` is asynchronous: the receipt names a PENDING transition until the store folds it.
@@ -241,14 +163,6 @@ service; releasing a subscription never closes it — `context.planning().close(
241
163
 
242
164
  A Node client passes no opener and relies on the long poll.
243
165
 
244
- ## Optimistic writes and `head > seq`
245
-
246
- `seq` is the last transition folded into a card, `head` the highest allocated. The mirror's
247
- `applyReceipt` (run by `execute` whenever stores are registered) raises the stored card's `head` to
248
- the new transition's `seq`, so `model.pending()` is true before any frame arrives; the commit's fold
249
- brings `seq` up to it. The mirror only ever grows `head`, so a list fetched before the append does not
250
- clear the marker. A model's `expectSeq` default and its `WorkcardConflict` are `planning` → Models.
251
-
252
166
  ## Gotchas
253
167
 
254
168
  - Mount a feed once per screen; read the store everywhere else.
@@ -262,58 +176,6 @@ clear the marker. A model's `expectSeq` default and its `WorkcardConflict` are `
262
176
  - `@owlmeans/client-entrypoint` — `bindAll`, the typed `call()`
263
177
  - `@owlmeans/state` — the mirror; `@owlmeans/socket` — `Connection`
264
178
 
265
- ## Auxiliary mirrors and authenticated scope lifecycle
266
-
267
- With `resources: true` in the shared protocol tree, every remote facade exposes the complete
268
- assignee/team/comment/mention API, including versioned writes, team membership and project
269
- attachment. `appendPlanningStores` registers separate stock typed mirrors for all four resources.
270
- `makePlanningResourceFeed` seeds and polls selected resource queries; use it with the stock stores
271
- and `useStoreList` / `useStoreModel`. Auxiliary writes update the mirror only after success.
272
- An active-assignee query (`retired: false`) includes an absent or null retirement flag. Local
273
- mirror queries and feed reconciliation use `{ retired: { $ne: true } }`, matching the native
274
- facade rather than strict equality to false. A filtered poll removes only its unchanged matching
275
- snapshot rows, preserving unrelated retired identities needed by existing references.
276
- Comments use trusted server authors; mentions are read-only derived cache commands.
277
-
278
- Supply `PlanningClientOptions.scopeKey` from authenticated organization entity slug, profile/user
279
- id, session id and authorization revision; return undefined when signed out. Before switching
280
- organization or signing out, **await** `service.close()` before adopting the credential. It cancels
281
- requests and commit waits, invalidates receipts (including already-settled receipts), stops every
282
- registered feed, removes socket subscriptions, closes the carrier and rejects late socket openers.
283
- A stale operation answers `client:scope-changed`; cancellation never rolls back an accepted server
284
- write. A new subscription opens a fresh carrier after close.
285
-
286
- Close drains admitted **local** mirror writes before clearing every stock store and schema cache.
287
- New calls during an explicit close are refused, including hosts without `scopeKey`. Concurrent
288
- closes share the boundary. If clearing fails, close rejects and fresh work stays blocked; retain
289
- the old credential and retry close explicitly. An observed identity change also cancels old work
290
- and queues clearing before the new identity's mirror writes. Reload schemas and create fresh feeds
291
- after credential adoption. Feed query scope alone is insufficient for this lifecycle.
292
-
293
- The service's `lifecycle` coordinates stock facades, receipts, feeds and schema writes. A custom
294
- mirror integration captures an operation with `lifecycle.run`, performs remote reads through
295
- `operation.wait` with `operation.signal`, then uses `lifecycle.mutate(operation, write)` for local
296
- writes; never hold or nest that queue over a request, socket opener or another queued mutation.
297
- `planningMirrorOf(stores)` accepts `{ lifecycle, operation }` as its optional last argument and
298
- fetches missing frame records/links before entering the queue. Direct mirror/sync helpers without
299
- that option are local primitives: their caller owns isolation and cleanup.
300
-
301
- Planning protocols return `PlanningReply<T>` without storage organization identifiers. Stock
302
- facades hydrate records, receipts, schema scopes and socket frames with `entityId: ''` only to
303
- preserve the shared facade shape. That value is an advisory placeholder, never an organization
304
- identity or a credential. Use `scopeKey` and the authenticated organization entity slug for
305
- identity changes and access isolation; native server records keep their verified stable scope.
306
-
307
- Resource feed reconciliation snapshots mirror versions before requesting a list. Only unchanged
308
- snapshot rows absent from that response are removed, so a new local write or a newer version
309
- survives an older in-flight poll. Stopped feeds cannot publish. Track custom feeds through
310
- `registerFeed` when their lifecycle must end with the planning service. Card feeds use the same
311
- identity fence and stop registration; component cleanup awaits `stop()` when it must observe a
312
- completed boundary. Stop cancels unresolved seeds and drains admitted local changes without waiting
313
- for the network; neither late callbacks nor authoritative deletion passes may publish afterward.
314
- A schema write from an old generation cannot invalidate a newer generation's cached registry.
315
-
316
-
317
179
  ## Related
318
180
 
319
181
  - `planning` — records, flows, the fold, the models
@@ -321,9 +183,12 @@ A schema write from an old generation cannot invalidate a newer generation's cac
321
183
  - `client-job` — the same seed-then-fold shape for safe application-job views
322
184
  - `state` — `syncHelper.syncCards` is built on its criteria engine and `purge`
323
185
 
324
- ## External docs
186
+ ## Parts
187
+
188
+ Load a part on top of this core when its job is in play.
325
189
 
326
- - https://ajv.js.org/security.html#content-security-policy — runtime AJV requires unsafe-eval; standalone build compilation preserves strict CSP for static schemas.
327
- - https://www.npmjs.com/package/@cfworker/json-schema/v/4.1.1 — the interpreter supports dynamic schemas without code generation; select an explicit vocabulary and check its known limitations (4.1.1).
328
- - https://github.com/ajv-validator/ajv-formats#full-and-fast-validation-modes — full-mode predicates validate formats completely and are reusable through the plugin's `get` API (3.0.1).
329
- - https://ajv.js.org/json-schema.html#nullable-openapi — nullable expands the declared type without adding null to enum or const.
190
+ | Part | Load it when |
191
+ |---|---|
192
+ | [validation](parts/validation.md) | Validating planning `fields` in a browser form under a strict CSP (`makePlanningFieldValidator`) |
193
+ | [mirror](parts/mirror.md) | Reading lists from the state mirror (`syncHelper`, `planningMirrorOf`), mounting `makePlanningFeed`, or relying on `head > seq` |
194
+ | [aux-mirrors](parts/aux-mirrors.md) | Assignee, team, comment or mention mirrors and feeds, or switching organization / signing out (`scopeKey`, `close()`) |
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: aux-mirrors
3
+ description: The assignee, team, comment and mention mirrors and resource feeds, and the authenticated scope lifecycle (scopeKey, close, lifecycle.run and lifecycle.mutate).
4
+ metadata:
5
+ purposes: state, auth
6
+ mentions: makePlanningResourceFeed, scopeKey, lifecycle.run, lifecycle.mutate, registerFeed, client:scope-changed, PlanningReply
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/client-planning — Auxiliary mirrors and the scope lifecycle
11
+
12
+ ## Auxiliary mirrors and authenticated scope lifecycle
13
+
14
+ With `resources: true` in the shared protocol tree, every remote facade exposes the complete
15
+ assignee/team/comment/mention API, including versioned writes, team membership and project
16
+ attachment. `appendPlanningStores` registers separate stock typed mirrors for all four resources.
17
+ `makePlanningResourceFeed` seeds and polls selected resource queries; use it with the stock stores
18
+ and `useStoreList` / `useStoreModel`. Auxiliary writes update the mirror only after success.
19
+ An active-assignee query (`retired: false`) includes an absent or null retirement flag. Local
20
+ mirror queries and feed reconciliation use `{ retired: { $ne: true } }`, matching the native
21
+ facade rather than strict equality to false. A filtered poll removes only its unchanged matching
22
+ snapshot rows, preserving unrelated retired identities needed by existing references.
23
+ Comments use trusted server authors; mentions are read-only derived cache commands.
24
+
25
+ Supply `PlanningClientOptions.scopeKey` from authenticated organization entity slug, profile/user
26
+ id, session id and authorization revision; return undefined when signed out. Before switching
27
+ organization or signing out, **await** `service.close()` before adopting the credential. It cancels
28
+ requests and commit waits, invalidates receipts (including already-settled receipts), stops every
29
+ registered feed, removes socket subscriptions, closes the carrier and rejects late socket openers.
30
+ A stale operation answers `client:scope-changed`; cancellation never rolls back an accepted server
31
+ write. A new subscription opens a fresh carrier after close.
32
+
33
+ Close drains admitted **local** mirror writes before clearing every stock store and schema cache.
34
+ New calls during an explicit close are refused, including hosts without `scopeKey`. Concurrent
35
+ closes share the boundary. If clearing fails, close rejects and fresh work stays blocked; retain
36
+ the old credential and retry close explicitly. An observed identity change also cancels old work
37
+ and queues clearing before the new identity's mirror writes. Reload schemas and create fresh feeds
38
+ after credential adoption. Feed query scope alone is insufficient for this lifecycle.
39
+
40
+ The service's `lifecycle` coordinates stock facades, receipts, feeds and schema writes. A custom
41
+ mirror integration captures an operation with `lifecycle.run`, performs remote reads through
42
+ `operation.wait` with `operation.signal`, then uses `lifecycle.mutate(operation, write)` for local
43
+ writes; never hold or nest that queue over a request, socket opener or another queued mutation.
44
+ `planningMirrorOf(stores)` accepts `{ lifecycle, operation }` as its optional last argument and
45
+ fetches missing frame records/links before entering the queue. Direct mirror/sync helpers without
46
+ that option are local primitives: their caller owns isolation and cleanup.
47
+
48
+ Planning protocols return `PlanningReply<T>` without storage organization identifiers. Stock
49
+ facades hydrate records, receipts, schema scopes and socket frames with `entityId: ''` only to
50
+ preserve the shared facade shape. That value is an advisory placeholder, never an organization
51
+ identity or a credential. Use `scopeKey` and the authenticated organization entity slug for
52
+ identity changes and access isolation; native server records keep their verified stable scope.
53
+
54
+ Resource feed reconciliation snapshots mirror versions before requesting a list. Only unchanged
55
+ snapshot rows absent from that response are removed, so a new local write or a newer version
56
+ survives an older in-flight poll. Stopped feeds cannot publish. Track custom feeds through
57
+ `registerFeed` when their lifecycle must end with the planning service. Card feeds use the same
58
+ identity fence and stop registration; component cleanup awaits `stop()` when it must observe a
59
+ completed boundary. Stop cancels unresolved seeds and drains admitted local changes without waiting
60
+ for the network; neither late callbacks nor authoritative deletion passes may publish afterward.
61
+ A schema write from an old generation cannot invalidate a newer generation's cached registry.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: mirror
3
+ description: The state mirror's one id space and fold rules, makePlanningFeed, and optimistic writes with head over seq.
4
+ metadata:
5
+ purposes: state, list
6
+ mentions: appendPlanningStores, planningMirrorOf, applyCommitEvent, applyReceipt, applyCards, syncHelper, syncCards, syncLinks, makePlanningFeed, PlanningFeed, useStoreList
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/client-planning — The mirror and the feed
11
+
12
+ ## The mirror — one id space
13
+
14
+ `appendPlanningStores` registers ONE card store for every kind. Projects, cards and specifications
15
+ share an id space on the server, so they share one store here: a store per kind lets a card list
16
+ that reloads drop the projects beside it.
17
+
18
+ That is also why a list reaches the store through **`syncHelper.syncCards(store, items, where)`,
19
+ never `replace()`**: every card given is written, every card matching `where` that the list does not
20
+ name is dropped, and everything outside `where` is left alone. An unchanged card is not rewritten, so a
21
+ periodic re-seed wakes no subscriber. `opts.keep` protects ids a commit wrote while the list was in
22
+ flight.
23
+
24
+ The fold rules, shared by `syncHelper.syncCards`, the mirror's `applyCards` and `applyCommitEvent`, and
25
+ `commits.wait`:
26
+
27
+ - an OLDER `seq` never overwrites a newer record — frames and list answers race;
28
+ - a committed `delete` removes the row and every link touching it; a project's delete also drops
29
+ the rows whose `parents` hold it (the server purged them in the same fold);
30
+ - a frame for an id the store never saw still writes a row;
31
+ - a `failed` commit leaves the card alone and records the reason in the commit store;
32
+ - a frame without `record` (a cross-process bus carries ids only) is re-read through the facade —
33
+ without one, only the commit row is written;
34
+ - a committed `link`/`unlink` re-lists the card's outgoing links; links written together with a
35
+ `create` arrive with the next link seed.
36
+
37
+ ## The feed
38
+
39
+ ```typescript
40
+ const feed = makePlanningFeed(context, {
41
+ query: { parent: shedId, type: 'shed:tool' },
42
+ filter: { project: shedId },
43
+ refresh: 15_000,
44
+ onChange: state => setState(state),
45
+ })
46
+ await feed.ready // never rejects — read feed.error
47
+ // …
48
+ await feed.stop() // stops folding; the shared socket stays open
49
+ ```
50
+
51
+ 1. Subscribes to commits FIRST (the socket, when an opener exists).
52
+ 2. Seeds: `cards.list` (unpaged unless the query pages) → `syncHelper.syncCards` over `where`
53
+ (`queryHelper.criteriaOf(query)` by default). A PAGED seed only writes — a page cannot say what
54
+ does not exist.
55
+ 3. Folds every frame with `planningMirrorOf(stores).applyCommitEvent`, and re-seeds every `refresh`
56
+ ms — the authoritative backstop to a socket that can drop frames.
57
+
58
+ `connected` reports whether a socket was open when the feed subscribed; `seeded` tells "nothing
59
+ there" from "not loaded yet". A React hook wraps the feed in an effect and stops it on unmount.
60
+
61
+ ## Optimistic writes and `head > seq`
62
+
63
+ `seq` is the last transition folded into a card, `head` the highest allocated. The mirror's
64
+ `applyReceipt` (run by `execute` whenever stores are registered) raises the stored card's `head` to
65
+ the new transition's `seq`, so `model.pending()` is true before any frame arrives; the commit's fold
66
+ brings `seq` up to it. The mirror only ever grows `head`, so a list fetched before the append does not
67
+ clear the marker. A model's `expectSeq` default and its `WorkcardConflict` are `planning` → Models.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: validation
3
+ description: CSP-safe browser validation of a served field schema with makePlanningFieldValidator — the supported keywords, refusals and limits, and the external references behind them.
4
+ metadata:
5
+ purposes: form
6
+ mentions: makePlanningFieldValidator, validateFields, @cfworker/json-schema, JSON_SCHEMA_DRAFT_07, unsafe-eval, SchemaInvalid
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/client-planning — Browser field validation
11
+
12
+ ## Browser field validation under CSP
13
+
14
+ Bind `makePlanningFieldValidator(registry.fieldsSchema(type))` to the registry of the resolved
15
+ organization or project bundle — the type's own `fields` plus the application's base card fields
16
+ (`planning` → types part); with no base fields it IS `type.fields`. Passing `type.fields` alone
17
+ skips the base fields the server still enforces. Its `validateFields(fields)` returns `{ valid, errors }`; errors
18
+ carry decoded `path`, `keyword`, `schemaPath` and a bounded value-free `message`. Construct one JSON
19
+ fields object, validate it and send that same object. No coercion, defaults or property removal
20
+ occurs, and the schema is left unchanged. Preserve nested money objects and calendar date strings.
21
+ The native server validates every write again.
22
+
23
+ The validator interprets schemas with `@cfworker/json-schema`, with no `eval`, `new Function` or
24
+ network reference loading. Do not call `makeSchemaRegistry(...).validator()` or
25
+ `.assigneeValidator()` in a browser with strict `script-src`: those remain native AJV compilers.
26
+ The Draft 7 meta-schema is DATA in `src/validation/consts.ts` (`JSON_SCHEMA_DRAFT_07`, pinned to ajv's
27
+ `refs/json-schema-draft-07.json` by `tests/draft7.spec.ts`), never an import of that file: Node ESM needs
28
+ `with { type: 'json' }` on it, and a bundler that also meets ajv's own attribute-less `require` of the
29
+ same file refuses the pair (Rollup: "import attributes for the same module are always consistent"),
30
+ which fails every target web build that mounts the planning client.
31
+ Static AJV standalone compilation is suitable only when the schema cannot change after building;
32
+ organization and project definitions require the runtime interpreter.
33
+
34
+ The supported contract is Draft 7's object/array/scalar, required, enum/const, ranges, string
35
+ length/pattern, dependencies, boolean, composition and conditional keywords; `$defs`, local/nested
36
+ `$id` references and AJV's `nullable` are also supported. `$ref` sibling constraints are enforced as
37
+ by native planning AJV. String formats use the same `ajv-formats` full-mode predicates, including
38
+ real calendar dates and timezone-aware times. `nullable` adds null to the declared type while
39
+ leaving enum and const constraints intact. All references must resolve inside the supplied field
40
+ schema; productive recursive schemas are supported, same-instance reference cycles are refused.
41
+ Plain JSON values and field names without dot or dollar characters are required.
42
+
43
+ Preparation throws `SchemaInvalid` for malformed schemas, unresolved references, unsupported
44
+ drafts/keywords/formats, numeric formats, `formatMinimum`/`formatMaximum` extensions and
45
+ `multipleOf`. The latter is refused because the interpreter's epsilon and native AJV's quotient
46
+ semantics disagree for fractional and very large numbers. Standard annotation keywords and `x-`
47
+ annotations do not add validation. Never discard a refused constraint, invent a substitute schema,
48
+ enable `unsafe-eval` or proceed with a ready form after preparation fails. Show the error and retain
49
+ disabled save controls. Validation reports at most 64 errors, with paths bounded to 512 characters.
50
+
51
+ ## External docs
52
+
53
+ - https://ajv.js.org/security.html#content-security-policy — runtime AJV requires unsafe-eval; standalone build compilation preserves strict CSP for static schemas.
54
+ - https://www.npmjs.com/package/@cfworker/json-schema/v/4.1.1 — the interpreter supports dynamic schemas without code generation; select an explicit vocabulary and check its known limitations (4.1.1).
55
+ - https://github.com/ajv-validator/ajv-formats#full-and-fast-validation-modes — full-mode predicates validate formats completely and are reusable through the plugin's `get` API (3.0.1).
56
+ - https://ajv.js.org/json-schema.html#nullable-openapi — nullable expands the declared type without adding null to enum or const.
package/build/client.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { Config, Context, PlanningClientOptions, PlanningClientService, Wit
6
6
  * What only a server can do is refused rather than faked: a client runs no middleware and folds
7
7
  * nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
8
8
  * `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
9
- * bundle.
9
+ * bundle (its `schemas.app` merged over the served declarations, in plugin order).
10
10
  */
11
11
  export declare const makePlanningClientService: <C extends Config, T extends Context<C>>(context: T, options: PlanningClientOptions) => PlanningClientService;
12
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAClF,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACrE,CAAC,WAAW,qBAAqB,KACzC,qBAoHF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAChE,CAAC,WAAW,qBAAqB,KACzC,CAAC,GAAG,kBA0BN,CAAA"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAClF,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACrE,CAAC,WAAW,qBAAqB,KACzC,qBA6HF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAChE,CAAC,WAAW,qBAAqB,KACzC,CAAC,GAAG,kBA0BN,CAAA"}
package/build/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { bindAll } from '@owlmeans/client-entrypoint';
2
2
  import { createLazyService, MiddlewareStage, MiddlewareType } from '@owlmeans/context';
3
- import { DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported, } from '@owlmeans/planning';
3
+ import { appSchemaHelper, DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported, } from '@owlmeans/planning';
4
4
  import { makeRemoteCommitSource } from './commits.js';
5
5
  import { makeRemoteDefinitions } from './definitions.js';
6
6
  import { makeRemoteFacade } from './facade.js';
@@ -13,7 +13,7 @@ import { makePlanningClientLifecycle } from './lifecycle.js';
13
13
  * What only a server can do is refused rather than faked: a client runs no middleware and folds
14
14
  * nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
15
15
  * `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
16
- * bundle.
16
+ * bundle (its `schemas.app` merged over the served declarations, in plugin order).
17
17
  */
18
18
  export const makePlanningClientService = (context, options) => {
19
19
  const feeds = new Set();
@@ -33,11 +33,16 @@ export const makePlanningClientService = (context, options) => {
33
33
  const commits = makeRemoteCommitSource(context, options.protocols, {
34
34
  socket: options.socket, poll: options.poll, timeout: options.timeout, stores, lifecycle,
35
35
  });
36
- const layerPlugins = () => plugins.forEach(plugin => {
37
- plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType);
38
- plugin.schemas?.flows?.forEach(registry.registerFlow);
39
- plugin.schemas?.types?.forEach(registry.registerType);
40
- });
36
+ // The application's declarations the server served — what every client plugin's `app` merges over.
37
+ let served;
38
+ const layerPlugins = () => {
39
+ registry.registerApp(appSchemaHelper.merge([served, ...plugins.map(plugin => plugin.schemas?.app)]));
40
+ plugins.forEach(plugin => {
41
+ plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType);
42
+ plugin.schemas?.flows?.forEach(registry.registerFlow);
43
+ plugin.schemas?.types?.forEach(registry.registerType);
44
+ });
45
+ };
41
46
  let loadedKey = scopeKey();
42
47
  let loaded = false;
43
48
  let loading = null;
@@ -57,6 +62,7 @@ export const makePlanningClientService = (context, options) => {
57
62
  const bundle = await operation.wait(context.entrypoint(options.protocols.schema.list).call({ timeout: options.timeout, signal: operation.signal }));
58
63
  operation.check();
59
64
  registry.load(bundle);
65
+ served = registry.app();
60
66
  layerPlugins();
61
67
  loaded = true;
62
68
  return registry;
@@ -87,6 +93,7 @@ export const makePlanningClientService = (context, options) => {
87
93
  loading = null;
88
94
  definitions?.invalidate();
89
95
  registry.load({ version: 1, types: [], flows: [] });
96
+ served = undefined;
90
97
  layerPlugins();
91
98
  await Promise.all(stopping.map(async (feed) => await feed.stop()));
92
99
  });
@@ -102,6 +109,9 @@ export const makePlanningClientService = (context, options) => {
102
109
  if (plugin.before != null || plugin.after != null || plugin.store != null || plugin.mintCode != null) {
103
110
  throw new PlanningUnsupported(`client:plugin:${plugin.name}`);
104
111
  }
112
+ if (plugin.schemas?.app != null) {
113
+ appSchemaHelper.assertDeclarations(plugin.schemas.app);
114
+ }
105
115
  const existing = plugins.findIndex(candidate => candidate.name === plugin.name);
106
116
  if (existing >= 0) {
107
117
  plugins.splice(existing, 1);
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/client-planning",
3
- "version": "0.1.18-rc.36",
3
+ "version": "0.1.18-rc.38",
4
4
  "license": "MIT",
5
5
  "description": "The client half of OwlMeans planning — the remote planning facade, the state mirror and its commit fold, and the subscribe-then-poll commit wait. React-free: it runs in a browser and in a Node client alike.",
6
6
  "type": "module",
@@ -24,18 +24,18 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@cfworker/json-schema": "^4.1.1",
27
- "@owlmeans/auth": "^0.1.18-rc.41",
28
- "@owlmeans/client-context": "^0.1.18-rc.52",
29
- "@owlmeans/client-entrypoint": "^0.1.18-rc.51",
30
- "@owlmeans/context": "^0.1.18-rc.34",
31
- "@owlmeans/entrypoint": "^0.1.18-rc.41",
32
- "@owlmeans/error": "^0.1.18-rc.38",
33
- "@owlmeans/planning": "^0.1.18-rc.28",
34
- "@owlmeans/resource": "^0.1.18-rc.40",
35
- "@owlmeans/log": "^0.1.18-rc.3",
36
- "@owlmeans/route": "^0.1.18-rc.35",
37
- "@owlmeans/socket": "^0.1.18-rc.44",
38
- "@owlmeans/state": "^0.1.18-rc.39"
27
+ "@owlmeans/auth": "^0.1.18-rc.42",
28
+ "@owlmeans/client-context": "^0.1.18-rc.53",
29
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.52",
30
+ "@owlmeans/context": "^0.1.18-rc.35",
31
+ "@owlmeans/entrypoint": "^0.1.18-rc.42",
32
+ "@owlmeans/error": "^0.1.18-rc.39",
33
+ "@owlmeans/planning": "^0.1.18-rc.30",
34
+ "@owlmeans/resource": "^0.1.18-rc.41",
35
+ "@owlmeans/log": "^0.1.18-rc.4",
36
+ "@owlmeans/route": "^0.1.18-rc.36",
37
+ "@owlmeans/socket": "^0.1.18-rc.45",
38
+ "@owlmeans/state": "^0.1.18-rc.40"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "ajv": "*",
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@owlmeans/dep-config": "workspace:*",
46
- "@owlmeans/server-planning": "^0.1.18-rc.34",
47
- "@owlmeans/test": "^0.1.18-rc.37",
46
+ "@owlmeans/server-planning": "^0.1.18-rc.36",
47
+ "@owlmeans/test": "^0.1.18-rc.38",
48
48
  "@types/bun": "^1.4.2",
49
49
  "ajv": "^8.20.0",
50
50
  "ajv-formats": "^3.0.1",
package/src/client.ts CHANGED
@@ -2,9 +2,9 @@ import { bindAll } from '@owlmeans/client-entrypoint'
2
2
  import { createLazyService, MiddlewareStage, MiddlewareType } from '@owlmeans/context'
3
3
  import type { EntrypointTree } from '@owlmeans/entrypoint'
4
4
  import {
5
- DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported,
5
+ appSchemaHelper, DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported,
6
6
  } from '@owlmeans/planning'
7
- import type { PlanningFacade, PlanningPlugin, PlanningSchemaRegistry } from '@owlmeans/planning'
7
+ import type { PlanningAppDeclarations, PlanningFacade, PlanningPlugin, PlanningSchemaRegistry } from '@owlmeans/planning'
8
8
  import { makeRemoteCommitSource } from './commits.js'
9
9
  import { makeRemoteDefinitions } from './definitions.js'
10
10
  import { makeRemoteFacade } from './facade.js'
@@ -21,7 +21,7 @@ import type {
21
21
  * What only a server can do is refused rather than faked: a client runs no middleware and folds
22
22
  * nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
23
23
  * `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
24
- * bundle.
24
+ * bundle (its `schemas.app` merged over the served declarations, in plugin order).
25
25
  */
26
26
  export const makePlanningClientService = <C extends Config, T extends Context<C>>(
27
27
  context: T, options: PlanningClientOptions
@@ -43,11 +43,16 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
43
43
  socket: options.socket, poll: options.poll, timeout: options.timeout, stores, lifecycle,
44
44
  })
45
45
 
46
- const layerPlugins = (): void => plugins.forEach(plugin => {
47
- plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType)
48
- plugin.schemas?.flows?.forEach(registry.registerFlow)
49
- plugin.schemas?.types?.forEach(registry.registerType)
50
- })
46
+ // The application's declarations the server served — what every client plugin's `app` merges over.
47
+ let served: PlanningAppDeclarations | undefined
48
+ const layerPlugins = (): void => {
49
+ registry.registerApp(appSchemaHelper.merge([served, ...plugins.map(plugin => plugin.schemas?.app)]))
50
+ plugins.forEach(plugin => {
51
+ plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType)
52
+ plugin.schemas?.flows?.forEach(registry.registerFlow)
53
+ plugin.schemas?.types?.forEach(registry.registerType)
54
+ })
55
+ }
51
56
 
52
57
  let loadedKey = scopeKey()
53
58
  let loaded = false
@@ -64,6 +69,7 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
64
69
  const bundle = await operation.wait(context.entrypoint(options.protocols.schema.list).call({ timeout: options.timeout, signal: operation.signal }))
65
70
  operation.check()
66
71
  registry.load(bundle)
72
+ served = registry.app()
67
73
  layerPlugins()
68
74
  loaded = true
69
75
 
@@ -97,7 +103,7 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
97
103
  const stopping = [...feeds]
98
104
  feeds.clear()
99
105
  loaded = false; loading = null; definitions?.invalidate()
100
- registry.load({ version: 1, types: [], flows: [] }); layerPlugins()
106
+ registry.load({ version: 1, types: [], flows: [] }); served = undefined; layerPlugins()
101
107
  await Promise.all(stopping.map(async feed => await feed.stop()))
102
108
  })
103
109
 
@@ -116,6 +122,9 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
116
122
  if (plugin.before != null || plugin.after != null || plugin.store != null || plugin.mintCode != null) {
117
123
  throw new PlanningUnsupported(`client:plugin:${plugin.name}`)
118
124
  }
125
+ if (plugin.schemas?.app != null) {
126
+ appSchemaHelper.assertDeclarations(plugin.schemas.app)
127
+ }
119
128
  const existing = plugins.findIndex(candidate => candidate.name === plugin.name)
120
129
  if (existing >= 0) {
121
130
  plugins.splice(existing, 1)
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { appSchemaHelper, FieldsInvalid, SchemaInvalid, TransitionAction, WorkcardKind } from '@owlmeans/planning'
3
+ import type { PlanningAppDeclarations, PlanningService, WorkcardTypeSchema } from '@owlmeans/planning'
4
+ import { makePlanningFieldValidator } from '../src/index.js'
5
+ import { makeSuite, STORY, STORY_FLOW } from './context.js'
6
+ import type { Suite } from './context.js'
7
+
8
+ /** A choir: every type says what it is for, every card may carry a part. */
9
+ const APP: PlanningAppDeclarations = {
10
+ type: { properties: { description: { type: 'string' } } },
11
+ card: { properties: { part: { type: 'string', enum: ['soprano', 'alto', 'tenor', 'bass'] } } },
12
+ }
13
+ /** What a service serves: the declarations merged (normalized with `type: 'object'`). */
14
+ const SERVED = appSchemaHelper.merge([APP])
15
+
16
+ const SECTIONAL: WorkcardTypeSchema = {
17
+ type: 'test:sectional', kind: WorkcardKind.Card, version: 1, flows: [STORY_FLOW], specifications: [],
18
+ fields: { type: 'object', additionalProperties: false },
19
+ }
20
+
21
+ const declared = async (opts: Parameters<typeof makeSuite>[0] = {}): Promise<Suite> => {
22
+ const suite = await makeSuite(opts)
23
+ ;(suite.server as unknown as { planning: () => PlanningService }).planning().use({ name: 'choir', schemas: { app: APP } })
24
+
25
+ return suite
26
+ }
27
+
28
+ describe('@owlmeans/client-planning — application declarations', () => {
29
+ test('the served bundle carries them: the registry validates base fields, a form validator takes the same schema', async () => {
30
+ const suite = await declared()
31
+ const registry = await suite.client.planning().loadSchemas()
32
+
33
+ expect(registry.app()).toEqual(SERVED!)
34
+ expect(registry.validator(STORY)({ area: 'stage', part: 'alto' })).toBe(true)
35
+ expect(registry.validator(STORY)({ area: 'stage', part: 'drums' })).toBe(false)
36
+ const form = makePlanningFieldValidator(registry.fieldsSchema(STORY))
37
+ expect(form.validateFields({ area: 'stage', part: 'alto' }).valid).toBe(true)
38
+ expect(form.validateFields({ area: 'stage', part: 'drums' }).valid).toBe(false)
39
+ expect(form.validateFields({ area: 'stage', colour: 'red' }).valid).toBe(false)
40
+
41
+ const project = await suite.project('Spring concert')
42
+ await expect(suite.planning.execute({
43
+ card: { kind: WorkcardKind.Card, type: STORY, parent: project.id!, title: 'Warm-up', fields: { part: 'drums' } },
44
+ action: TransitionAction.Create,
45
+ }, { wait: true })).rejects.toBeInstanceOf(FieldsInvalid)
46
+ })
47
+
48
+ test('a schemas-only client plugin merges over the served declarations; a malformed one is refused', async () => {
49
+ const suite = await declared()
50
+ const service = suite.client.planning()
51
+ await service.loadSchemas()
52
+
53
+ service.use({ name: 'stage', schemas: { app: { card: { properties: { row: { type: 'number' } } } } } })
54
+ expect(Object.keys(service.schemas.app()?.card?.properties ?? {})).toEqual(['part', 'row'])
55
+ expect(() => service.use({ name: 'broken', schemas: { app: { card: { properties: { 'a.b': {} } } } } })).toThrow(SchemaInvalid)
56
+ expect(service.plugins().map(plugin => plugin.name)).toEqual(['stage'])
57
+ service.use({ name: 'stage' })
58
+ expect(Object.keys(service.schemas.app()?.card?.properties ?? {})).toEqual(['part'])
59
+ })
60
+
61
+ test('definitions carry a type\'s app bag over the wire; a refused bag crosses as SchemaInvalid', async () => {
62
+ const suite = await declared({ definitions: true })
63
+ const definitions = suite.planning.definitions!
64
+
65
+ await definitions.define({ types: [{ ...SECTIONAL, app: { description: 'Small groups' } }] })
66
+ const layer = await definitions.registry()
67
+ expect((layer.type(SECTIONAL.type) as WorkcardTypeSchema).app).toEqual({ description: 'Small groups' })
68
+ expect(layer.app()).toEqual(SERVED!)
69
+ expect(layer.validator(SECTIONAL.type)({ part: 'tenor' })).toBe(true)
70
+ await expect(definitions.putType({ ...SECTIONAL, version: 2, app: { conductor: 'me' } })).rejects.toBeInstanceOf(SchemaInvalid)
71
+ })
72
+ })