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

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.37
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.37",
5
+ "generatedAt": "2026-10-20T23:19:58.628Z",
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.37"` 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
@@ -88,42 +88,9 @@ whole cache — an organization-wide write reaches every project's layer — and
88
88
  `model(card)` of a card resolves its type in its project's layer (a project's own id, a card's
89
89
  nearest primary project ancestor); a specification's type is always code's. A tree without `definitions` has none of this.
90
90
 
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
91
  ## Mounting in a target
125
92
 
126
- The web half of the target mount (`planning` → Mounting in a target).
93
+ The web half of the target mount (`planning` → Mounting in a target, in that skill's `mounting` part).
127
94
 
128
95
  - `appendPlanningClient(context, { protocols, bind: false, schemas: false })` — the target binds
129
96
  its whole api tree itself, and a signed-out visitor may read nothing, so the bundle loads on
@@ -157,55 +124,6 @@ drill.available().map(rule => rule.name)
157
124
  project-wide read answers `PlanningUnsupported` on a client.
158
125
  - `size: 0` is "no limit"; a paged server answers 100 rows when no size is asked for.
159
126
 
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
127
  ## Waiting for a commit
210
128
 
211
129
  `execute()` is asynchronous: the receipt names a PENDING transition until the store folds it.
@@ -241,14 +159,6 @@ service; releasing a subscription never closes it — `context.planning().close(
241
159
 
242
160
  A Node client passes no opener and relies on the long poll.
243
161
 
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
162
  ## Gotchas
253
163
 
254
164
  - Mount a feed once per screen; read the store everywhere else.
@@ -262,58 +172,6 @@ clear the marker. A model's `expectSeq` default and its `WorkcardConflict` are `
262
172
  - `@owlmeans/client-entrypoint` — `bindAll`, the typed `call()`
263
173
  - `@owlmeans/state` — the mirror; `@owlmeans/socket` — `Connection`
264
174
 
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
175
  ## Related
318
176
 
319
177
  - `planning` — records, flows, the fold, the models
@@ -321,9 +179,12 @@ A schema write from an old generation cannot invalidate a newer generation's cac
321
179
  - `client-job` — the same seed-then-fold shape for safe application-job views
322
180
  - `state` — `syncHelper.syncCards` is built on its criteria engine and `purge`
323
181
 
324
- ## External docs
182
+ ## Parts
183
+
184
+ Load a part on top of this core when its job is in play.
325
185
 
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.
186
+ | Part | Load it when |
187
+ |---|---|
188
+ | [validation](parts/validation.md) | Validating planning `fields` in a browser form under a strict CSP (`makePlanningFieldValidator`) |
189
+ | [mirror](parts/mirror.md) | Reading lists from the state mirror (`syncHelper`, `planningMirrorOf`), mounting `makePlanningFeed`, or relying on `head > seq` |
190
+ | [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,54 @@
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(type.fields)` to the exact definition returned by the resolved
15
+ organization or project bundle. Its `validateFields(fields)` returns `{ valid, errors }`; errors
16
+ carry decoded `path`, `keyword`, `schemaPath` and a bounded value-free `message`. Construct one JSON
17
+ fields object, validate it and send that same object. No coercion, defaults or property removal
18
+ occurs, and the schema is left unchanged. Preserve nested money objects and calendar date strings.
19
+ The native server validates every write again.
20
+
21
+ The validator interprets schemas with `@cfworker/json-schema`, with no `eval`, `new Function` or
22
+ network reference loading. Do not call `makeSchemaRegistry(...).validator()` or
23
+ `.assigneeValidator()` in a browser with strict `script-src`: those remain native AJV compilers.
24
+ The Draft 7 meta-schema is DATA in `src/validation/consts.ts` (`JSON_SCHEMA_DRAFT_07`, pinned to ajv's
25
+ `refs/json-schema-draft-07.json` by `tests/draft7.spec.ts`), never an import of that file: Node ESM needs
26
+ `with { type: 'json' }` on it, and a bundler that also meets ajv's own attribute-less `require` of the
27
+ same file refuses the pair (Rollup: "import attributes for the same module are always consistent"),
28
+ which fails every target web build that mounts the planning client.
29
+ Static AJV standalone compilation is suitable only when the schema cannot change after building;
30
+ organization and project definitions require the runtime interpreter.
31
+
32
+ The supported contract is Draft 7's object/array/scalar, required, enum/const, ranges, string
33
+ length/pattern, dependencies, boolean, composition and conditional keywords; `$defs`, local/nested
34
+ `$id` references and AJV's `nullable` are also supported. `$ref` sibling constraints are enforced as
35
+ by native planning AJV. String formats use the same `ajv-formats` full-mode predicates, including
36
+ real calendar dates and timezone-aware times. `nullable` adds null to the declared type while
37
+ leaving enum and const constraints intact. All references must resolve inside the supplied field
38
+ schema; productive recursive schemas are supported, same-instance reference cycles are refused.
39
+ Plain JSON values and field names without dot or dollar characters are required.
40
+
41
+ Preparation throws `SchemaInvalid` for malformed schemas, unresolved references, unsupported
42
+ drafts/keywords/formats, numeric formats, `formatMinimum`/`formatMaximum` extensions and
43
+ `multipleOf`. The latter is refused because the interpreter's epsilon and native AJV's quotient
44
+ semantics disagree for fractional and very large numbers. Standard annotation keywords and `x-`
45
+ annotations do not add validation. Never discard a refused constraint, invent a substitute schema,
46
+ enable `unsafe-eval` or proceed with a ready form after preparation fails. Show the error and retain
47
+ disabled save controls. Validation reports at most 64 errors, with paths bounded to 512 characters.
48
+
49
+ ## External docs
50
+
51
+ - https://ajv.js.org/security.html#content-security-policy — runtime AJV requires unsafe-eval; standalone build compilation preserves strict CSP for static schemas.
52
+ - 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).
53
+ - 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).
54
+ - https://ajv.js.org/json-schema.html#nullable-openapi — nullable expands the declared type without adding null to enum or const.
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.37",
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.29",
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.35",
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",