@owlmeans/server-planning 0.1.18-rc.34 → 0.1.18-rc.35

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
@@ -10,7 +10,7 @@ the `@owlmeans/server-planning/store` subpath.
10
10
  ## Installation
11
11
 
12
12
  ```sh
13
- bun add @owlmeans/server-planning@^0.1.18-rc.34 @owlmeans/planning@^0.1.18-rc.28 ajv
13
+ bun add @owlmeans/server-planning@^0.1.18-rc.35 @owlmeans/planning@^0.1.18-rc.29 ajv
14
14
  ```
15
15
 
16
16
  ## Concepts
@@ -214,7 +214,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
214
214
  your project's skill store (`.agents/skills/`):
215
215
 
216
216
  ```sh
217
- npx @owlmeans/agent-skills@^0.1.18-rc.56
217
+ npx @owlmeans/agent-skills@^0.1.18-rc.57
218
218
  ```
219
219
 
220
220
  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/server-planning",
4
- "version": "0.1.18-rc.34",
5
- "generatedAt": "2026-10-17T00:00:32.694Z",
4
+ "version": "0.1.18-rc.35",
5
+ "generatedAt": "2026-10-20T23:19:58.603Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -10,7 +10,108 @@
10
10
  "name": "server-planning",
11
11
  "category": "package-specific",
12
12
  "file": "skills/server-planning/SKILL.md",
13
- "canonicalPath": ".agents/skills/server-planning/SKILL.md"
13
+ "canonicalPath": ".agents/skills/server-planning/SKILL.md",
14
+ "parts": [
15
+ {
16
+ "name": "auxiliary",
17
+ "file": "skills/server-planning/parts/auxiliary.md",
18
+ "description": "Assignees, teams, comments and mentions on native store ports, the reply projection, hierarchy admission and project purge.",
19
+ "purposes": [
20
+ "planning",
21
+ "data"
22
+ ],
23
+ "mentions": [
24
+ "facade.assignees",
25
+ "facade.teams",
26
+ "facade.comments",
27
+ "facade.mentions",
28
+ "manageAssignees",
29
+ "manageTeams",
30
+ "defaultAssigneeId",
31
+ "makePlanningAuth",
32
+ "guardHelper.reply"
33
+ ]
34
+ },
35
+ {
36
+ "name": "executor",
37
+ "file": "skills/server-planning/parts/executor.md",
38
+ "description": "The transition executor step by step (refusal order, actor and createdBy stamping) and the plugin seam (registration, order, store routing, before/after hooks).",
39
+ "purposes": [
40
+ "planning",
41
+ "server-api"
42
+ ],
43
+ "mentions": [
44
+ "executeTransition",
45
+ "creatorHelper",
46
+ "makePluginRegistry",
47
+ "ensurePlanningService",
48
+ "PlanningRefused",
49
+ "mintCode",
50
+ "WorkcardConflict"
51
+ ]
52
+ },
53
+ {
54
+ "name": "schemas",
55
+ "file": "skills/server-planning/parts/schemas.md",
56
+ "description": "Data-defined card types and flows — the resolved layer per organization and project, the facade's definitions, retiring a flow.",
57
+ "purposes": [
58
+ "data",
59
+ "planning",
60
+ "config"
61
+ ],
62
+ "mentions": [
63
+ "makeDefinitions",
64
+ "makeSchemaViews",
65
+ "schemasFor",
66
+ "scopedSchemaHelper",
67
+ "definitions",
68
+ "SchemaInUse"
69
+ ]
70
+ },
71
+ {
72
+ "name": "serving",
73
+ "file": "skills/server-planning/parts/serving.md",
74
+ "description": "The commit feed, request scope and security, the access resolver (projects, writes, grants), and mounting the handlers in a target.",
75
+ "purposes": [
76
+ "server-api",
77
+ "auth",
78
+ "planning"
79
+ ],
80
+ "mentions": [
81
+ "servePlanningEntrypoints",
82
+ "planningHandlerOf",
83
+ "PlanningAccessResolver",
84
+ "makeRequestScope",
85
+ "makePlanningAccessModel",
86
+ "guardHelper",
87
+ "executionHelper",
88
+ "PlanningForbidden"
89
+ ]
90
+ },
91
+ {
92
+ "name": "stores",
93
+ "file": "skills/server-planning/parts/stores.md",
94
+ "description": "The in-memory store, writing a durable or foreign store (ports, foldHelper.foldPending, the commit hub, the conformance suite), and testing against the memory store.",
95
+ "purposes": [
96
+ "data",
97
+ "planning",
98
+ "test"
99
+ ],
100
+ "mentions": [
101
+ "makeMemoryPlanningStore",
102
+ "PlanningStore",
103
+ "foldHelper",
104
+ "makeCommitHub",
105
+ "makeCompositeStore",
106
+ "planningConformance",
107
+ "conformanceCasesFor",
108
+ "planningConformancePlugin",
109
+ "makeProjectionProcessor",
110
+ "@owlmeans/server-planning/store",
111
+ "@owlmeans/server-planning/conformance"
112
+ ]
113
+ }
114
+ ]
14
115
  }
15
116
  ]
16
117
  }
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/server-planning
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.34"` in `dependencies` (`ajv` is a peer)
11
+ **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.35"` in `dependencies` (`ajv` is a peer)
12
12
 
13
13
  The general implementation of `@owlmeans/planning`: the planning service (a plugin host), the
14
14
  scoped facade, the executor every write goes through, the in-memory reference store, the commit
@@ -126,54 +126,6 @@ Every wrong form is a compile error against the typed facade (`PlanningFacade`,
126
126
  typed `any`, so never type one that way. `tests/create-example.spec.ts` runs this example and each
127
127
  wrong form as written.
128
128
 
129
- ## The executor, step by step
130
-
131
- Nothing is appended before step 11 — every refusal leaves the log untouched.
132
-
133
- 1. **Normalize** — deep copy, trimmed text, `parents ∋ parent` (first); a create's draft without
134
- `createdBy` gets `creatorHelper.creatorOf(scope)` (below).
135
- 2. **Scope** — `entityId` must be present.
136
- 3. **Idempotency** — a `key` already in the entity's log answers ITS receipt, before any
137
- validation or middleware.
138
- 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`;
139
- one outside the scope's `projects` is `WorkcardNotFound`), its type and flow, every draft parent
140
- (`ParentNotFound` — loaded through the facade, so an invisible parent is absent) and the parent
141
- project's `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot. Where the
142
- default store holds data-defined schemas, the card's project is found first and everything
143
- after reads that project's resolved layer (`Resolved.schemas`): a data-defined type of a
144
- project with `scopedCardTypes` is admitted, a retired type creates nothing
145
- (`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry.
146
- 5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
147
- 6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
148
- (`planning:immutable:*`, `createdBy` among them — `creatorHelper.assertCreatorFixed`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
149
- parent, the slot (`SpecificationSlotUnknown`, `SpecificationRevisionConflict`, JSON body schema
150
- → `FieldsInvalid`), relationships (`RelationshipRefused`: undeclared type, `from` not the card,
151
- missing target, type constraints, `single`, unlinking an absent edge).
152
- 7. **Card id** — `store.newId?.() ?? options.ids?.() ?? idHelper.uuid()`. Ids are the store's.
153
- 8. **Code** — a supplied code is checked; else the plugins' `mintCode` (first answer wins, checked);
154
- else the type's `CodePolicy` (a slug policy derives from the title) → `CodeTaken`.
155
- 9. **Changes** — `changesHelper.computeChanges`. An `update` that changes nothing answers the card's latest
156
- receipt and appends nothing.
157
- 10. **Seq** — `transitions.nextSeq(card, expectSeq)`, a CAS against `head` (`WorkcardConflict`).
158
- 11. **Append** — `commit: pending`. A lost race on the key answers the winner's receipt.
159
- 12. **Project** — `store.cards.project(card)`: a sync store folds now, a queued one enqueues.
160
- 13. **Receipt** — `{ transition, card? (once committed), committed(opts) }`.
161
- 14. **`wait: true`** — `committed()` before returning (`CommitTimeout`, `CommitFailed`).
162
-
163
- `actor` is the SCOPE's: `profileId`/`userId`/`service`/`channel` come from the scope only; an
164
- in-process caller may add `agent`/`runId` on the execution. A handler never passes a wire `actor`.
165
-
166
- A create's `createdBy` is the scope's subject too — `creatorHelper.creatorOf(scope)`: `profileId`, else `userId`,
167
- else the same two of `scope.actor`. The executor stamps it on every path, so an in-process facade
168
- create and the HTTP `execute` produce the same owner. An in-process draft that names `createdBy`
169
- keeps it (explicit wins — a manager creating on someone's behalf); a scope naming nobody (a service
170
- or system scope) leaves it unset; the wire drops a draft's `createdBy` before the stamp.
171
-
172
- `createdBy` never moves after the create (the rule is `planning` → The record model). The executor
173
- enforces it itself with `creatorHelper.assertCreatorFixed` at step 6, after the `before` chain, so a wire body, an
174
- in-process caller and a plugin are refused alike, whatever `@owlmeans/planning` build the host
175
- resolves. The conformance suite checks a store folds a stored row naming it as the pure fold does.
176
-
177
129
  ## Idempotency and `expectSeq`
178
130
 
179
131
  - Give every retried or replayed write a `key` (`import:<source>:<n>`). The key is unique per entity
@@ -182,231 +134,6 @@ resolves. The conformance suite checks a store folds a stored row naming it as t
182
134
  see the same `seq` but only one wins the head. `null` or omitted opts out; a model fills it from
183
135
  its record.
184
136
 
185
- ## The plugin seam
186
-
187
- | Concern | Rule |
188
- |---|---|
189
- | Registration | `use(plugin)` replaces by `name`; schemas are contributed at `use` time (later type/flow id wins) |
190
- | Order | `order ?? 50` ascending, registration order among equals |
191
- | Store routing | `store(type)` = the first plugin whose `owns(type)` is true AND that supplies a `store`; else the default store. A store factory is resolved once |
192
- | Reads | By id: the default store, then each owned store. A list naming one owned type (or several owned by one store) goes there; any other list — a cross-type query — is the default store's alone |
193
- | `mintCode` | First non-`undefined` answer wins |
194
- | `before` | Every plugin in order; may return a replaced execution; a `ResilientError` passes through, anything else becomes `PlanningRefused('<plugin>:<message>')` |
195
- | `after` | Every plugin in order, once per COMMITTED transition; errors logged, never rethrown |
196
-
197
- **Where `after` runs.** The service's `committed(event)` runs the chain, and it is called by the
198
- process that FOLDS the transition — the memory store inside `project()`, a queued store's projection
199
- job after marking the commit. Never subscribe a process to the commit bus to run hooks: that fires
200
- once per subscribed process. Every process that folds registers the same plugins; each commit is
201
- folded by one of them, so each hook fires once. `hooks: false` makes a process fold without running
202
- them. A hook sees `ctx.transition` (read back, so `actor.channel` is available) and a facade scoped
203
- to the event's entity acting as this process's `cfg.service`.
204
-
205
- The service hands `committed` to a store through `store.bind?.(listener)` the first time it resolves
206
- the store; `bind` replaces the listener, so a store has exactly one.
207
-
208
- ## The memory store
209
-
210
- `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias?, schemas? })`
211
-
212
- - Cards and projects in one map, specifications in their own, relationships in a third; every read
213
- goes through the `@owlmeans/resource` query engine. A card list includes specifications only when
214
- the criteria asks for `kind: specification` or a `category` (`wantsSpecifications`, the rule every
215
- store routes by).
216
- - `schemas: true` adds the data-defined schema port (records per layer, a revision per
217
- organization, synchronous `watch`); off by default — without it every type and flow resolves
218
- from code.
219
- - `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
220
- `sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
221
- commit to stay pending.
222
- - A project `delete` purges the project, everything whose `parents` reach it, their specifications,
223
- links and log; the commit hub remembers the settled event so the waiter still gets its answer.
224
- - **It is wrong for more than one process or a restart** — the log, the cards and the commit feed
225
- are this process's heap. Use it for tests, tools and single-process demos.
226
-
227
- ## Writing a durable store
228
-
229
- - Implement `PlanningStore` (`transitions`, `cards`, `specs`, `links`, `commits`, `newId`). `newId`
230
- answers the id format the host's references need (ObjectId hex for Mongo).
231
- - `transitions.list` treats `sinceSeq` as `seq > sinceSeq`; `nextSeq` is the CAS against `head` and
232
- answers 1 for a card that has no log.
233
- - The projection body is `foldHelper.foldPending(store, cardId, entityId, { onCommitted, publish, touch, limit })`:
234
- it applies pending transitions with `applyHelper.applyTransition` only, marks each committed or failed,
235
- publishes, then runs `onCommitted`. The caller owns single flight per card. Strip `record` from
236
- the event before a cross-process bus.
237
- - **A failed transition never blocks the card.** The fold marks it `failed`, publishes a failed
238
- event, and goes PAST it: the card's `seq` advances to that transition with every other value as
239
- it was, and the next transition folds normally. Without that, every later transition fails as
240
- out of order and the card is stuck for good. Only two cases stop short: a failed `create` leaves
241
- no card to advance, and a store that refuses the advancing write stops the fold with `followUp`,
242
- leaving the rest pending for a retry. `foldHelper.revisionsFromLog` replays past failed transitions the same
243
- way.
244
- - A transactional store passes `FoldOptions.unit` — it wraps each transition's writes (a
245
- savepoint), so a write the database refuses is undone whole and the fold marks that transition
246
- failed and goes past it. `foldHelper.foldPending` folds PAST a failure it produces; a row that is ALREADY
247
- failed at the cursor, or a gap in the seqs, is the store's to step over before calling it
248
- (`planning-postgres` does it in a prelude).
249
- - `foldHelper.failPending` is what a dead projection job must do, or waiters hang to their timeout.
250
- - The data-defined schema port (`schemas`) is optional; `makeCompositeStore` passes the default
251
- store's through. A store that has it must purge a project's layer with the project.
252
- - **Run the conformance suite.** `@owlmeans/server-planning/conformance` exports
253
- `planningConformance` — named cases with no test runner inside (each throws
254
- `ConformanceFailure`) — and `conformanceCasesFor(store)`, which drops the data-defined cases for a
255
- store without the port. Boot a service with `planningConformancePlugin` and `conformanceClock()`
256
- and hand each case `{ service, store, facade }`; every case works in its own organization, so one
257
- boot serves them all.
258
- - `makeCommitHub({ status, remember?, ladder? })` is the `CommitSource`: feed `publish` from the bus,
259
- answer `status` from transition rows. `wait` subscribes first, polls once, then climbs the ladder.
260
- - `foldHelper.revisionsFromLog(transitions, limit?)` answers `specs.revisions`.
261
- - `makeProjectionProcessor(ctx)` + `makePlanningQueueHooks(ctx)` are the store-agnostic job body and
262
- `onJobDead`; a store with an admission CAS wraps its own around them.
263
-
264
- ## A foreign provider
265
-
266
- Only `cards` is required. A plugin that `owns` its types and supplies a store without
267
- `transitions` can be read through the facade; writes to it refuse with `PlanningUnsupported` —
268
- mapping external records (`mappers`) belongs to that plugin's own store adapter.
269
-
270
- ## The commit feed
271
-
272
- - **Poll** — `commit.get` answers the status; with `?wait=<s>` (clamped to `maxPoll`, default 55)
273
- it subscribes, re-polls, and holds until the commit settles or the time is up, answering `pending`
274
- then — never an error.
275
- - **Notify** — `commit.events` pushes `planning-commit` frames (or `opts.event`) filtered by the
276
- authenticated entity and optional `project`/`card` query values; a frame without `record` gets the
277
- card as it reads now. The subscription is released on the socket's `close` frame.
278
- - `execute` with `wait: true` holds at most `maxPoll` seconds whatever `timeout` the body asked for.
279
-
280
- ## Scope and security
281
-
282
- - Without `opts.access`: `entityId` is `makeEntityScope(req).requireEntityKey()`; `opts.scope(req, ctx)` adds a
283
- `channel` or `service` and can never replace the entity. `actor` and a create's `createdBy` come
284
- from the request, never the body (`executionHelper.wireExecution` drops a draft's claim, `creatorHelper.withCreator` stamps the
285
- subject, `creatorHelper.assertCreatorFixed` refuses a body naming `createdBy` — an update never re-owns a card).
286
- - The organization of a scope is the one the request ACTS IN. `makeEntityScope(req).requireEntityKey()` answers
287
- `req.entity.id`, the stable record id (the token's `entitySlug` only where no entity resolver is
288
- registered). Behind an OIDC relying party (`@owlmeans/server-oidc-rp`) the acting organization is
289
- session state: the guard attaches `req.entity` with `id` = the organization claim's frozen
290
- `entityKey`, so the tenant key survives a rename and follows a switch. Take it in the handler and
291
- pass it down; never read it from the context or from a body.
292
- - Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
293
- absent id — on reads and writes alike (`guardHelper.concealed`).
294
- - Queries arrive in their wire form and are decoded with `wireHelper.decode*Query` before `queryHelper.criteriaOf`, which
295
- always adds the scope's `entityId`.
296
-
297
- ### The access resolver
298
-
299
- `opts.access: (req, ctx) => Promise<{ entityId, projects?, writes?, grants? }>` is the hosting app's
300
- decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
301
-
302
- - `entityId` is the resolver's — never the token's, never `opts.scope`'s.
303
- - `projects` becomes `PlanningScope.projects`, which the server facade enforces on reads and
304
- writes. A card is visible when at least one project reached through its recursive parent paths
305
- is allowed; specifications follow their containing cards. Anything else reads as absent
306
- (`WorkcardNotFound`, `null`, an empty list); a create under an invisible parent is
307
- `ParentNotFound`. Links, transitions and commits use current endpoint/card ancestry, with a
308
- cached project fallback only for purged records. Subscriptions and commit waits enforce the
309
- same visibility.
310
- - **Narrowed lists, counts and summaries use the allowed recursive descendant IDs before paging.**
311
- A specification is admitted through the same containing-card hierarchy as a single read.
312
- Do not replace this with a direct-parent query or a filter applied after pagination.
313
- `tests/narrowing.spec.ts` and shared hierarchy conformance pin this behavior.
314
- - A person who may see only SOME projects of their organization (a tool-shed volunteer given one
315
- shed) is expressed here: the resolver answers the project ids the person's grants list as
316
- `projects`, and omits the key when a grant covers every project — never a hand-written filter in
317
- each handler.
318
- - `writes` is the subset of projects the person may WRITE in (`projects` are the ones they VIEW).
319
- `execute` on a card, specification or link needs it to be writable — the recursive ancestor
320
- rule over `writes`: a card admitted directly or through a writable parent path. A create needs
321
- every named parent writable; reparenting checks every supplied destination parent. A create at the root
322
- (no parent, not a project) is refused. `schema.define` of a project layer needs the project in
323
- `writes`. A refusal is `PlanningForbidden` (403, `forbidden:writes:<id>`), checked before the grants
324
- and before the executor. NOT governed by it: a project create (`grants.createProjects` alone) and
325
- the organization layer (`grants.defineSchemas`). A card the scope cannot see is still answered
326
- as absent by the executor. Omitted — every visible project is writable.
327
- `tests/writes.spec.ts` pins viewer, writer and omitted.
328
- - `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
329
- root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
330
- names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
331
- list names project layers). A `grants` object refuses every flag it leaves out with
332
- `PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
333
-
334
- ## Mounting in a target
335
-
336
- The api half of the target mount (the common tree and the kit literals: `planning` → Mounting in a
337
- target).
338
-
339
- - Bind `servePlanningEntrypoints(tree, { access })` WITHOUT the commit socket —
340
- `.filter(entrypoint => entrypoint.protocol !== tree.commit.events)`: nothing authenticates a
341
- target's socket after it opens, so the client long-polls (`commit.get` with `wait`, `execute`
342
- with `wait: true`). Never write card, transition or schema routes beside it.
343
- - The resolver maps container permissions onto the access: `projects` = the containers the person
344
- may view, `writes` = those they may modify, `grants` = `createProjects` / `deleteProjects` /
345
- `defineSchemas` from their create and admin rights. A creator sees what they created — stamp
346
- ownership in a `before` hook and add the card to both lists.
347
-
348
- ## Data-defined types and flows
349
-
350
- Where the default store has the schema port, `runtime.schemasFor(entityId, project?)` answers the
351
- resolved layer (`scopedSchemaHelper.resolveScopedBundle` over the code bundle and the organization's and project's
352
- records), cached per (organization, project) and keyed by the store's `revision()` and the plugin
353
- registry's version — a write anywhere moves the revision the next lookup reads, so a cached layer is
354
- never stale; a store's `watch` evicts early. Without the port it answers the code registry itself.
355
- `makeDefinitions` gives the facade `definitions`: every write checks the project (a project card the
356
- scope can see), the seal, the closed-form checks (a type's flows resolve in its layer, counting flows
357
- written beside it), then the store's compare-and-set. `retire` of a flow refuses with `SchemaInUse`
358
- while a live type of the layer — or of any project layer, for an organization record — still runs
359
- it. `facade.model(card)` builds over the layer of the card's own project, and `schema.list` answers
360
- the scoped bundle (`?project=`) where `definitions` exists, the code bundle otherwise.
361
-
362
- ## Testing
363
-
364
- Category A. Build one real context with `makeBasicContext` + `appendPlanningService({ store:
365
- makeMemoryPlanningStore({ now }), plugins: [fixtures] })`, and run a handler through
366
- `handler.bind({ ref: { ctx } })(req, res)` with a request carrying `auth` and `entity`. A monotonic
367
- `now` keeps log order deterministic; `sync: false` + `flush()` pins pending and hook-count cases.
368
- `tests/conformance.spec.ts` runs the conformance suite against the memory store with and without
369
- `schemas: true`; a durable store's own tests run the same cases against a real database.
370
-
371
- ## Auxiliary resources and hierarchy admission
372
-
373
- `facade.assignees`, `.teams`, `.comments`, `.mentions` use native versioned ports of
374
- `PlanningStore`. The stock memory, PostgreSQL and Mongo stores implement them. Mutations run
375
- inside the store's organization `unit`; CAS, nickname/authentication uniqueness, schema layers,
376
- canonical links and mentions are validated there. Missing ports raise `PlanningUnsupported`. Assignee type changes recheck incoming typed card
377
- references against the proposed type; a change cannot invalidate a persisted assignment.
378
- Do not add a second host-specific planning resource CRUD layer.
379
-
380
- `assignees.list({ retired: false })` means every identity not explicitly retired, including
381
- records with an absent or null retirement flag. The facade translates it to `{ retired: { $ne: true } }`
382
- before the native port reads; `retired: true` selects historical identities and an omitted filter
383
- retains both. Keep this predicate consistent with client mirrors and run its shared conformance
384
- case against memory, PostgreSQL and Mongo; a bare equality to false hides newly created identities.
385
-
386
- The request scope supplies trusted `assigneeId` for comment authors and optional
387
- `defaultAssigneeId` for newly created cards. HTTP handlers stamp actor/organization context;
388
- `makePlanningAuth(...).scopeFor` resolves OwlMeans identities when that integration is enabled.
389
- Access resolvers can grant `manageAssignees` and `manageTeams` separately from card writes.
390
- Comments and project/team attachment require writable containing cards/projects. Auxiliary
391
- organization lists require authenticated organization scope; do not infer ACLs from project mode.
392
-
393
- All stock HTTP replies use `guardHelper.reply`, which conceals scope failures and applies the
394
- typed planning reply projection. Socket commits use the same projection. Organization `entityId`
395
- stays in native records and verified scopes, and never leaves through records, schema scopes or
396
- nested receipts. Keep business fields and schema definitions unchanged when projecting replies.
397
-
398
- Admission and native projection recheck hierarchy, schema fields and endpoint types. Recursive
399
- visibility follows every project ancestor and filters before paging. Definition record reads
400
- use the same recursive visibility as cards. Reparenting validates all
401
- descendants under the prospective schema layer. Ordinary parent deletion refuses existing
402
- children; project purge removes its descendants, comments, mentions, scoped schemas and links,
403
- while retaining organization assignees and reusable teams. Field-backed edges are reconciled in
404
- the persistence unit; named inverse edges are derived read views, never duplicate persisted rows.
405
-
406
- Run shared conformance with `resources` and `schemas` capabilities against each native provider;
407
- cover version races, stable mentions, reusable teams, field references, hierarchy and narrowed ACLs.
408
-
409
-
410
137
  ## Related
411
138
 
412
139
  - `planning` — records, flows, the fold, the query language, the protocol tree, the models
@@ -415,6 +142,12 @@ cover version races, stable mentions, reusable teams, field references, hierarch
415
142
  - `server-job`, `queue` — the declare/serve/feed pattern and the projection queue
416
143
  - `resource` — the criteria engine every memory read goes through
417
144
 
418
- Project purge uses current card ancestry as ownership. A surviving reparented card retains its
419
- complete transition history, documents and edges even when their cached project id names its former
420
- project. Cached project values may select orphaned records, but must never delete surviving records.
145
+ ## Parts
146
+
147
+ | Part | Load it when |
148
+ |---|---|
149
+ | [executor](parts/executor.md) | writing a planning plugin, or diagnosing why a transition was refused or never committed |
150
+ | [stores](parts/stores.md) | choosing or writing a store, or testing planning code |
151
+ | [serving](parts/serving.md) | binding the planning handlers, writing an access resolver, or narrowing what a person sees or writes |
152
+ | [schemas](parts/schemas.md) | defining or retiring types and flows as data |
153
+ | [auxiliary](parts/auxiliary.md) | working with assignees, teams, comments or mentions, or with reparenting and purge |
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: auxiliary
3
+ description: Assignees, teams, comments and mentions on native store ports, the reply projection, hierarchy admission and project purge.
4
+ metadata:
5
+ purposes: planning, data
6
+ mentions: facade.assignees, facade.teams, facade.comments, facade.mentions, manageAssignees, manageTeams, defaultAssigneeId, makePlanningAuth, guardHelper.reply
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/server-planning — Auxiliary resources and hierarchy admission
11
+
12
+ ## Auxiliary resources and hierarchy admission
13
+
14
+ `facade.assignees`, `.teams`, `.comments`, `.mentions` use native versioned ports of
15
+ `PlanningStore`. The stock memory, PostgreSQL and Mongo stores implement them. Mutations run
16
+ inside the store's organization `unit`; CAS, nickname/authentication uniqueness, schema layers,
17
+ canonical links and mentions are validated there. Missing ports raise `PlanningUnsupported`. Assignee type changes recheck incoming typed card
18
+ references against the proposed type; a change cannot invalidate a persisted assignment.
19
+ Do not add a second host-specific planning resource CRUD layer.
20
+
21
+ `assignees.list({ retired: false })` means every identity not explicitly retired, including
22
+ records with an absent or null retirement flag. The facade translates it to `{ retired: { $ne: true } }`
23
+ before the native port reads; `retired: true` selects historical identities and an omitted filter
24
+ retains both. Keep this predicate consistent with client mirrors and run its shared conformance
25
+ case against memory, PostgreSQL and Mongo; a bare equality to false hides newly created identities.
26
+
27
+ The request scope supplies trusted `assigneeId` for comment authors and optional
28
+ `defaultAssigneeId` for newly created cards. HTTP handlers stamp actor/organization context;
29
+ `makePlanningAuth(...).scopeFor` resolves OwlMeans identities when that integration is enabled.
30
+ Access resolvers can grant `manageAssignees` and `manageTeams` separately from card writes.
31
+ Comments and project/team attachment require writable containing cards/projects. Auxiliary
32
+ organization lists require authenticated organization scope; do not infer ACLs from project mode.
33
+
34
+ All stock HTTP replies use `guardHelper.reply`, which conceals scope failures and applies the
35
+ typed planning reply projection. Socket commits use the same projection. Organization `entityId`
36
+ stays in native records and verified scopes, and never leaves through records, schema scopes or
37
+ nested receipts. Keep business fields and schema definitions unchanged when projecting replies.
38
+
39
+ Admission and native projection recheck hierarchy, schema fields and endpoint types. Recursive
40
+ visibility follows every project ancestor and filters before paging. Definition record reads
41
+ use the same recursive visibility as cards. Reparenting validates all
42
+ descendants under the prospective schema layer. Ordinary parent deletion refuses existing
43
+ children; project purge removes its descendants, comments, mentions, scoped schemas and links,
44
+ while retaining organization assignees and reusable teams. Field-backed edges are reconciled in
45
+ the persistence unit; named inverse edges are derived read views, never duplicate persisted rows.
46
+
47
+ Run shared conformance with `resources` and `schemas` capabilities against each native provider;
48
+ cover version races, stable mentions, reusable teams, field references, hierarchy and narrowed ACLs.
49
+
50
+ Project purge uses current card ancestry as ownership. A surviving reparented card retains its
51
+ complete transition history, documents and edges even when their cached project id names its former
52
+ project. Cached project values may select orphaned records, but must never delete surviving records.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: executor
3
+ description: The transition executor step by step (refusal order, actor and createdBy stamping) and the plugin seam (registration, order, store routing, before/after hooks).
4
+ metadata:
5
+ purposes: planning, server-api
6
+ mentions: executeTransition, creatorHelper, makePluginRegistry, ensurePlanningService, PlanningRefused, mintCode, WorkcardConflict
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/server-planning — The executor and the plugin seam
11
+
12
+ ## The executor, step by step
13
+
14
+ Nothing is appended before step 11 — every refusal leaves the log untouched.
15
+
16
+ 1. **Normalize** — deep copy, trimmed text, `parents ∋ parent` (first); a create's draft without
17
+ `createdBy` gets `creatorHelper.creatorOf(scope)` (below).
18
+ 2. **Scope** — `entityId` must be present.
19
+ 3. **Idempotency** — a `key` already in the entity's log answers ITS receipt, before any
20
+ validation or middleware.
21
+ 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`;
22
+ one outside the scope's `projects` is `WorkcardNotFound`), its type and flow, every draft parent
23
+ (`ParentNotFound` — loaded through the facade, so an invisible parent is absent) and the parent
24
+ project's `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot. Where the
25
+ default store holds data-defined schemas, the card's project is found first and everything
26
+ after reads that project's resolved layer (`Resolved.schemas`): a data-defined type of a
27
+ project with `scopedCardTypes` is admitted, a retired type creates nothing
28
+ (`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry.
29
+ 5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
30
+ 6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
31
+ (`planning:immutable:*`, `createdBy` among them — `creatorHelper.assertCreatorFixed`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
32
+ parent, the slot (`SpecificationSlotUnknown`, `SpecificationRevisionConflict`, JSON body schema
33
+ → `FieldsInvalid`), relationships (`RelationshipRefused`: undeclared type, `from` not the card,
34
+ missing target, type constraints, `single`, unlinking an absent edge).
35
+ 7. **Card id** — `store.newId?.() ?? options.ids?.() ?? idHelper.uuid()`. Ids are the store's.
36
+ 8. **Code** — a supplied code is checked; else the plugins' `mintCode` (first answer wins, checked);
37
+ else the type's `CodePolicy` (a slug policy derives from the title) → `CodeTaken`.
38
+ 9. **Changes** — `changesHelper.computeChanges`. An `update` that changes nothing answers the card's latest
39
+ receipt and appends nothing.
40
+ 10. **Seq** — `transitions.nextSeq(card, expectSeq)`, a CAS against `head` (`WorkcardConflict`).
41
+ 11. **Append** — `commit: pending`. A lost race on the key answers the winner's receipt.
42
+ 12. **Project** — `store.cards.project(card)`: a sync store folds now, a queued one enqueues.
43
+ 13. **Receipt** — `{ transition, card? (once committed), committed(opts) }`.
44
+ 14. **`wait: true`** — `committed()` before returning (`CommitTimeout`, `CommitFailed`).
45
+
46
+ `actor` is the SCOPE's: `profileId`/`userId`/`service`/`channel` come from the scope only; an
47
+ in-process caller may add `agent`/`runId` on the execution. A handler never passes a wire `actor`.
48
+
49
+ A create's `createdBy` is the scope's subject too — `creatorHelper.creatorOf(scope)`: `profileId`, else `userId`,
50
+ else the same two of `scope.actor`. The executor stamps it on every path, so an in-process facade
51
+ create and the HTTP `execute` produce the same owner. An in-process draft that names `createdBy`
52
+ keeps it (explicit wins — a manager creating on someone's behalf); a scope naming nobody (a service
53
+ or system scope) leaves it unset; the wire drops a draft's `createdBy` before the stamp.
54
+
55
+ `createdBy` never moves after the create (the rule is `planning` → The record model). The executor
56
+ enforces it itself with `creatorHelper.assertCreatorFixed` at step 6, after the `before` chain, so a wire body, an
57
+ in-process caller and a plugin are refused alike, whatever `@owlmeans/planning` build the host
58
+ resolves. The conformance suite checks a store folds a stored row naming it as the pure fold does.
59
+
60
+ ## The plugin seam
61
+
62
+ | Concern | Rule |
63
+ |---|---|
64
+ | Registration | `use(plugin)` replaces by `name`; schemas are contributed at `use` time (later type/flow id wins) |
65
+ | Order | `order ?? 50` ascending, registration order among equals |
66
+ | Store routing | `store(type)` = the first plugin whose `owns(type)` is true AND that supplies a `store`; else the default store. A store factory is resolved once |
67
+ | Reads | By id: the default store, then each owned store. A list naming one owned type (or several owned by one store) goes there; any other list — a cross-type query — is the default store's alone |
68
+ | `mintCode` | First non-`undefined` answer wins |
69
+ | `before` | Every plugin in order; may return a replaced execution; a `ResilientError` passes through, anything else becomes `PlanningRefused('<plugin>:<message>')` |
70
+ | `after` | Every plugin in order, once per COMMITTED transition; errors logged, never rethrown |
71
+
72
+ **Where `after` runs.** The service's `committed(event)` runs the chain, and it is called by the
73
+ process that FOLDS the transition — the memory store inside `project()`, a queued store's projection
74
+ job after marking the commit. Never subscribe a process to the commit bus to run hooks: that fires
75
+ once per subscribed process. Every process that folds registers the same plugins; each commit is
76
+ folded by one of them, so each hook fires once. `hooks: false` makes a process fold without running
77
+ them. A hook sees `ctx.transition` (read back, so `actor.channel` is available) and a facade scoped
78
+ to the event's entity acting as this process's `cfg.service`.
79
+
80
+ The service hands `committed` to a store through `store.bind?.(listener)` the first time it resolves
81
+ the store; `bind` replaces the listener, so a store has exactly one.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: schemas
3
+ description: Data-defined card types and flows — the resolved layer per organization and project, the facade's definitions, retiring a flow.
4
+ metadata:
5
+ purposes: data, planning, config
6
+ mentions: makeDefinitions, makeSchemaViews, schemasFor, scopedSchemaHelper, definitions, SchemaInUse
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/server-planning — Data-defined types and flows
11
+
12
+ ## Data-defined types and flows
13
+
14
+ Where the default store has the schema port, `runtime.schemasFor(entityId, project?)` answers the
15
+ resolved layer (`scopedSchemaHelper.resolveScopedBundle` over the code bundle and the organization's and project's
16
+ records), cached per (organization, project) and keyed by the store's `revision()` and the plugin
17
+ registry's version — a write anywhere moves the revision the next lookup reads, so a cached layer is
18
+ never stale; a store's `watch` evicts early. Without the port it answers the code registry itself.
19
+ `makeDefinitions` gives the facade `definitions`: every write checks the project (a project card the
20
+ scope can see), the seal, the closed-form checks (a type's flows resolve in its layer, counting flows
21
+ written beside it), then the store's compare-and-set. `retire` of a flow refuses with `SchemaInUse`
22
+ while a live type of the layer — or of any project layer, for an organization record — still runs
23
+ it. `facade.model(card)` builds over the layer of the card's own project, and `schema.list` answers
24
+ the scoped bundle (`?project=`) where `definitions` exists, the code bundle otherwise.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: serving
3
+ description: The commit feed, request scope and security, the access resolver (projects, writes, grants), and mounting the handlers in a target.
4
+ metadata:
5
+ purposes: server-api, auth, planning
6
+ mentions: servePlanningEntrypoints, planningHandlerOf, PlanningAccessResolver, makeRequestScope, makePlanningAccessModel, guardHelper, executionHelper, PlanningForbidden
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/server-planning — Serving the protocol tree
11
+
12
+ ## The commit feed
13
+
14
+ - **Poll** — `commit.get` answers the status; with `?wait=<s>` (clamped to `maxPoll`, default 55)
15
+ it subscribes, re-polls, and holds until the commit settles or the time is up, answering `pending`
16
+ then — never an error.
17
+ - **Notify** — `commit.events` pushes `planning-commit` frames (or `opts.event`) filtered by the
18
+ authenticated entity and optional `project`/`card` query values; a frame without `record` gets the
19
+ card as it reads now. The subscription is released on the socket's `close` frame.
20
+ - `execute` with `wait: true` holds at most `maxPoll` seconds whatever `timeout` the body asked for.
21
+
22
+ ## Scope and security
23
+
24
+ - Without `opts.access`: `entityId` is `makeEntityScope(req).requireEntityKey()`; `opts.scope(req, ctx)` adds a
25
+ `channel` or `service` and can never replace the entity. `actor` and a create's `createdBy` come
26
+ from the request, never the body (`executionHelper.wireExecution` drops a draft's claim, `creatorHelper.withCreator` stamps the
27
+ subject, `creatorHelper.assertCreatorFixed` refuses a body naming `createdBy` — an update never re-owns a card).
28
+ - The organization of a scope is the one the request ACTS IN. `makeEntityScope(req).requireEntityKey()` answers
29
+ `req.entity.id`, the stable record id (the token's `entitySlug` only where no entity resolver is
30
+ registered). Behind an OIDC relying party (`@owlmeans/server-oidc-rp`) the acting organization is
31
+ session state: the guard attaches `req.entity` with `id` = the organization claim's frozen
32
+ `entityKey`, so the tenant key survives a rename and follows a switch. Take it in the handler and
33
+ pass it down; never read it from the context or from a body.
34
+ - Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
35
+ absent id — on reads and writes alike (`guardHelper.concealed`).
36
+ - Queries arrive in their wire form and are decoded with `wireHelper.decode*Query` before `queryHelper.criteriaOf`, which
37
+ always adds the scope's `entityId`.
38
+
39
+ ### The access resolver
40
+
41
+ `opts.access: (req, ctx) => Promise<{ entityId, projects?, writes?, grants? }>` is the hosting app's
42
+ decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
43
+
44
+ - `entityId` is the resolver's — never the token's, never `opts.scope`'s.
45
+ - `projects` becomes `PlanningScope.projects`, which the server facade enforces on reads and
46
+ writes. A card is visible when at least one project reached through its recursive parent paths
47
+ is allowed; specifications follow their containing cards. Anything else reads as absent
48
+ (`WorkcardNotFound`, `null`, an empty list); a create under an invisible parent is
49
+ `ParentNotFound`. Links, transitions and commits use current endpoint/card ancestry, with a
50
+ cached project fallback only for purged records. Subscriptions and commit waits enforce the
51
+ same visibility.
52
+ - **Narrowed lists, counts and summaries use the allowed recursive descendant IDs before paging.**
53
+ A specification is admitted through the same containing-card hierarchy as a single read.
54
+ Do not replace this with a direct-parent query or a filter applied after pagination.
55
+ `tests/narrowing.spec.ts` and shared hierarchy conformance pin this behavior.
56
+ - A person who may see only SOME projects of their organization (a tool-shed volunteer given one
57
+ shed) is expressed here: the resolver answers the project ids the person's grants list as
58
+ `projects`, and omits the key when a grant covers every project — never a hand-written filter in
59
+ each handler.
60
+ - `writes` is the subset of projects the person may WRITE in (`projects` are the ones they VIEW).
61
+ `execute` on a card, specification or link needs it to be writable — the recursive ancestor
62
+ rule over `writes`: a card admitted directly or through a writable parent path. A create needs
63
+ every named parent writable; reparenting checks every supplied destination parent. A create at the root
64
+ (no parent, not a project) is refused. `schema.define` of a project layer needs the project in
65
+ `writes`. A refusal is `PlanningForbidden` (403, `forbidden:writes:<id>`), checked before the grants
66
+ and before the executor. NOT governed by it: a project create (`grants.createProjects` alone) and
67
+ the organization layer (`grants.defineSchemas`). A card the scope cannot see is still answered
68
+ as absent by the executor. Omitted — every visible project is writable.
69
+ `tests/writes.spec.ts` pins viewer, writer and omitted.
70
+ - `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
71
+ root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
72
+ names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
73
+ list names project layers). A `grants` object refuses every flag it leaves out with
74
+ `PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
75
+
76
+ ## Mounting in a target
77
+
78
+ The api half of the target mount (the common tree and the kit literals: `planning` → Mounting in a
79
+ target, its `mounting` part).
80
+
81
+ - Bind `servePlanningEntrypoints(tree, { access })` WITHOUT the commit socket —
82
+ `.filter(entrypoint => entrypoint.protocol !== tree.commit.events)`: nothing authenticates a
83
+ target's socket after it opens, so the client long-polls (`commit.get` with `wait`, `execute`
84
+ with `wait: true`). Never write card, transition or schema routes beside it.
85
+ - The resolver maps container permissions onto the access: `projects` = the containers the person
86
+ may view, `writes` = those they may modify, `grants` = `createProjects` / `deleteProjects` /
87
+ `defineSchemas` from their create and admin rights. A creator sees what they created — stamp
88
+ ownership in a `before` hook and add the card to both lists.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: stores
3
+ description: The in-memory store, writing a durable or foreign store (ports, foldHelper.foldPending, the commit hub, the conformance suite), and testing against the memory store.
4
+ metadata:
5
+ purposes: data, planning, test
6
+ mentions: makeMemoryPlanningStore, PlanningStore, foldHelper, makeCommitHub, makeCompositeStore, planningConformance, conformanceCasesFor, planningConformancePlugin, makeProjectionProcessor, @owlmeans/server-planning/store, @owlmeans/server-planning/conformance
7
+ ---
8
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
9
+
10
+ # @owlmeans/server-planning — Stores
11
+
12
+ ## The memory store
13
+
14
+ `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias?, schemas? })`
15
+
16
+ - Cards and projects in one map, specifications in their own, relationships in a third; every read
17
+ goes through the `@owlmeans/resource` query engine. A card list includes specifications only when
18
+ the criteria asks for `kind: specification` or a `category` (`wantsSpecifications`, the rule every
19
+ store routes by).
20
+ - `schemas: true` adds the data-defined schema port (records per layer, a revision per
21
+ organization, synchronous `watch`); off by default — without it every type and flow resolves
22
+ from code.
23
+ - `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
24
+ `sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
25
+ commit to stay pending.
26
+ - A project `delete` purges the project, everything whose `parents` reach it, their specifications,
27
+ links and log; the commit hub remembers the settled event so the waiter still gets its answer.
28
+ - **It is wrong for more than one process or a restart** — the log, the cards and the commit feed
29
+ are this process's heap. Use it for tests, tools and single-process demos.
30
+
31
+ ## Writing a durable store
32
+
33
+ - Implement `PlanningStore` (`transitions`, `cards`, `specs`, `links`, `commits`, `newId`). `newId`
34
+ answers the id format the host's references need (ObjectId hex for Mongo).
35
+ - `transitions.list` treats `sinceSeq` as `seq > sinceSeq`; `nextSeq` is the CAS against `head` and
36
+ answers 1 for a card that has no log.
37
+ - The projection body is `foldHelper.foldPending(store, cardId, entityId, { onCommitted, publish, touch, limit })`:
38
+ it applies pending transitions with `applyHelper.applyTransition` only, marks each committed or failed,
39
+ publishes, then runs `onCommitted`. The caller owns single flight per card. Strip `record` from
40
+ the event before a cross-process bus.
41
+ - **A failed transition never blocks the card.** The fold marks it `failed`, publishes a failed
42
+ event, and goes PAST it: the card's `seq` advances to that transition with every other value as
43
+ it was, and the next transition folds normally. Without that, every later transition fails as
44
+ out of order and the card is stuck for good. Only two cases stop short: a failed `create` leaves
45
+ no card to advance, and a store that refuses the advancing write stops the fold with `followUp`,
46
+ leaving the rest pending for a retry. `foldHelper.revisionsFromLog` replays past failed transitions the same
47
+ way.
48
+ - A transactional store passes `FoldOptions.unit` — it wraps each transition's writes (a
49
+ savepoint), so a write the database refuses is undone whole and the fold marks that transition
50
+ failed and goes past it. `foldHelper.foldPending` folds PAST a failure it produces; a row that is ALREADY
51
+ failed at the cursor, or a gap in the seqs, is the store's to step over before calling it
52
+ (`planning-postgres` does it in a prelude).
53
+ - `foldHelper.failPending` is what a dead projection job must do, or waiters hang to their timeout.
54
+ - The data-defined schema port (`schemas`) is optional; `makeCompositeStore` passes the default
55
+ store's through. A store that has it must purge a project's layer with the project.
56
+ - **Run the conformance suite.** `@owlmeans/server-planning/conformance` exports
57
+ `planningConformance` — named cases with no test runner inside (each throws
58
+ `ConformanceFailure`) — and `conformanceCasesFor(store)`, which drops the data-defined cases for a
59
+ store without the port. Boot a service with `planningConformancePlugin` and `conformanceClock()`
60
+ and hand each case `{ service, store, facade }`; every case works in its own organization, so one
61
+ boot serves them all.
62
+ - `makeCommitHub({ status, remember?, ladder? })` is the `CommitSource`: feed `publish` from the bus,
63
+ answer `status` from transition rows. `wait` subscribes first, polls once, then climbs the ladder.
64
+ - `foldHelper.revisionsFromLog(transitions, limit?)` answers `specs.revisions`.
65
+ - `makeProjectionProcessor(ctx)` + `makePlanningQueueHooks(ctx)` are the store-agnostic job body and
66
+ `onJobDead`; a store with an admission CAS wraps its own around them.
67
+
68
+ ## A foreign provider
69
+
70
+ Only `cards` is required. A plugin that `owns` its types and supplies a store without
71
+ `transitions` can be read through the facade; writes to it refuse with `PlanningUnsupported` —
72
+ mapping external records (`mappers`) belongs to that plugin's own store adapter.
73
+
74
+ ## Testing
75
+
76
+ Category A. Build one real context with `makeBasicContext` + `appendPlanningService({ store:
77
+ makeMemoryPlanningStore({ now }), plugins: [fixtures] })`, and run a handler through
78
+ `handler.bind({ ref: { ctx } })(req, res)` with a request carrying `auth` and `entity`. A monotonic
79
+ `now` keeps log order deterministic; `sync: false` + `flush()` pins pending and hook-count cases.
80
+ `tests/conformance.spec.ts` runs the conformance suite against the memory store with and without
81
+ `schemas: true`; a durable store's own tests run the same cases against a real database.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-planning",
3
- "version": "0.1.18-rc.34",
3
+ "version": "0.1.18-rc.35",
4
4
  "license": "MIT",
5
5
  "description": "The general planning implementation — the planning service and its plugin/middleware registry, the transition executor, the in-memory store, the protocol handlers and the commit feed.",
6
6
  "type": "module",
@@ -44,22 +44,22 @@
44
44
  }
45
45
  },
46
46
  "dependencies": {
47
- "@owlmeans/auth": "^0.1.18-rc.41",
48
- "@owlmeans/auth-common": "^0.1.18-rc.51",
49
- "@owlmeans/basic-ids": "^0.1.18-rc.40",
50
- "@owlmeans/context": "^0.1.18-rc.34",
51
- "@owlmeans/entrypoint": "^0.1.18-rc.41",
52
- "@owlmeans/error": "^0.1.18-rc.38",
53
- "@owlmeans/planning": "^0.1.18-rc.28",
54
- "@owlmeans/queue": "^0.1.18-rc.46",
55
- "@owlmeans/resource": "^0.1.18-rc.40",
56
- "@owlmeans/log": "^0.1.18-rc.3",
57
- "@owlmeans/route": "^0.1.18-rc.35",
58
- "@owlmeans/server-api": "^0.1.18-rc.55",
59
- "@owlmeans/server-context": "^0.1.18-rc.44",
60
- "@owlmeans/server-entrypoint": "^0.1.18-rc.40",
61
- "@owlmeans/server-socket": "^0.1.18-rc.59",
62
- "@owlmeans/socket": "^0.1.18-rc.44"
47
+ "@owlmeans/auth": "^0.1.18-rc.42",
48
+ "@owlmeans/auth-common": "^0.1.18-rc.52",
49
+ "@owlmeans/basic-ids": "^0.1.18-rc.41",
50
+ "@owlmeans/context": "^0.1.18-rc.35",
51
+ "@owlmeans/entrypoint": "^0.1.18-rc.42",
52
+ "@owlmeans/error": "^0.1.18-rc.39",
53
+ "@owlmeans/planning": "^0.1.18-rc.29",
54
+ "@owlmeans/queue": "^0.1.18-rc.47",
55
+ "@owlmeans/resource": "^0.1.18-rc.41",
56
+ "@owlmeans/log": "^0.1.18-rc.4",
57
+ "@owlmeans/route": "^0.1.18-rc.36",
58
+ "@owlmeans/server-api": "^0.1.18-rc.56",
59
+ "@owlmeans/server-context": "^0.1.18-rc.45",
60
+ "@owlmeans/server-entrypoint": "^0.1.18-rc.41",
61
+ "@owlmeans/server-socket": "^0.1.18-rc.60",
62
+ "@owlmeans/socket": "^0.1.18-rc.45"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "ajv": "*",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@owlmeans/dep-config": "workspace:*",
70
- "@owlmeans/test": "^0.1.18-rc.37",
70
+ "@owlmeans/test": "^0.1.18-rc.38",
71
71
  "@types/bun": "^1.4.2",
72
72
  "@types/node": "^26.6.4",
73
73
  "ajv": "^8.20.0",