@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 +2 -2
- package/agent-meta/manifest.json +62 -4
- package/agent-meta/skills/client-planning/SKILL.md +10 -149
- package/agent-meta/skills/client-planning/parts/aux-mirrors.md +61 -0
- package/agent-meta/skills/client-planning/parts/mirror.md +67 -0
- package/agent-meta/skills/client-planning/parts/validation.md +54 -0
- package/package.json +15 -15
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.
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/client-planning",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
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.
|
|
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
|
-
##
|
|
182
|
+
## Parts
|
|
183
|
+
|
|
184
|
+
Load a part on top of this core when its job is in play.
|
|
325
185
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
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.
|
|
28
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
33
|
-
"@owlmeans/planning": "^0.1.18-rc.
|
|
34
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/log": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/socket": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/state": "^0.1.18-rc.
|
|
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.
|
|
47
|
-
"@owlmeans/test": "^0.1.18-rc.
|
|
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",
|