@owlmeans/planning 0.1.18-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +169 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/planning/SKILL.md +248 -0
  4. package/build/consts.d.ts +126 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +129 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/entrypoints.d.ts +14 -0
  9. package/build/entrypoints.d.ts.map +1 -0
  10. package/build/entrypoints.js +69 -0
  11. package/build/entrypoints.js.map +1 -0
  12. package/build/errors.d.ts +94 -0
  13. package/build/errors.d.ts.map +1 -0
  14. package/build/errors.js +169 -0
  15. package/build/errors.js.map +1 -0
  16. package/build/helpers/apply.d.ts +27 -0
  17. package/build/helpers/apply.d.ts.map +1 -0
  18. package/build/helpers/apply.js +162 -0
  19. package/build/helpers/apply.js.map +1 -0
  20. package/build/helpers/card.d.ts +17 -0
  21. package/build/helpers/card.d.ts.map +1 -0
  22. package/build/helpers/card.js +31 -0
  23. package/build/helpers/card.js.map +1 -0
  24. package/build/helpers/changes.d.ts +51 -0
  25. package/build/helpers/changes.d.ts.map +1 -0
  26. package/build/helpers/changes.js +277 -0
  27. package/build/helpers/changes.js.map +1 -0
  28. package/build/helpers/code.d.ts +24 -0
  29. package/build/helpers/code.d.ts.map +1 -0
  30. package/build/helpers/code.js +47 -0
  31. package/build/helpers/code.js.map +1 -0
  32. package/build/helpers/index.d.ts +9 -0
  33. package/build/helpers/index.d.ts.map +1 -0
  34. package/build/helpers/index.js +9 -0
  35. package/build/helpers/index.js.map +1 -0
  36. package/build/helpers/query.d.ts +39 -0
  37. package/build/helpers/query.d.ts.map +1 -0
  38. package/build/helpers/query.js +293 -0
  39. package/build/helpers/query.js.map +1 -0
  40. package/build/helpers/specification.d.ts +21 -0
  41. package/build/helpers/specification.d.ts.map +1 -0
  42. package/build/helpers/specification.js +48 -0
  43. package/build/helpers/specification.js.map +1 -0
  44. package/build/helpers/status.d.ts +44 -0
  45. package/build/helpers/status.d.ts.map +1 -0
  46. package/build/helpers/status.js +78 -0
  47. package/build/helpers/status.js.map +1 -0
  48. package/build/helpers/validate.d.ts +30 -0
  49. package/build/helpers/validate.d.ts.map +1 -0
  50. package/build/helpers/validate.js +93 -0
  51. package/build/helpers/validate.js.map +1 -0
  52. package/build/i18n/be.json +51 -0
  53. package/build/i18n/de.json +51 -0
  54. package/build/i18n/en.json +51 -0
  55. package/build/i18n/es.json +51 -0
  56. package/build/i18n/pl.json +51 -0
  57. package/build/i18n/ru.json +51 -0
  58. package/build/i18n/uk.json +51 -0
  59. package/build/i18n.d.ts +2 -0
  60. package/build/i18n.d.ts.map +1 -0
  61. package/build/i18n.js +19 -0
  62. package/build/i18n.js.map +1 -0
  63. package/build/index.d.ts +10 -0
  64. package/build/index.d.ts.map +1 -0
  65. package/build/index.js +9 -0
  66. package/build/index.js.map +1 -0
  67. package/build/models/index.d.ts +7 -0
  68. package/build/models/index.d.ts.map +1 -0
  69. package/build/models/index.js +19 -0
  70. package/build/models/index.js.map +1 -0
  71. package/build/models/project.d.ts +4 -0
  72. package/build/models/project.d.ts.map +1 -0
  73. package/build/models/project.js +17 -0
  74. package/build/models/project.js.map +1 -0
  75. package/build/models/specification.d.ts +4 -0
  76. package/build/models/specification.d.ts.map +1 -0
  77. package/build/models/specification.js +15 -0
  78. package/build/models/specification.js.map +1 -0
  79. package/build/models/workcard.d.ts +14 -0
  80. package/build/models/workcard.d.ts.map +1 -0
  81. package/build/models/workcard.js +103 -0
  82. package/build/models/workcard.js.map +1 -0
  83. package/build/queue/index.d.ts +25 -0
  84. package/build/queue/index.d.ts.map +1 -0
  85. package/build/queue/index.js +14 -0
  86. package/build/queue/index.js.map +1 -0
  87. package/build/registry.d.ts +10 -0
  88. package/build/registry.d.ts.map +1 -0
  89. package/build/registry.js +75 -0
  90. package/build/registry.js.map +1 -0
  91. package/build/schemas.d.ts +56 -0
  92. package/build/schemas.d.ts.map +1 -0
  93. package/build/schemas.js +578 -0
  94. package/build/schemas.js.map +1 -0
  95. package/build/types.d.ts +785 -0
  96. package/build/types.d.ts.map +1 -0
  97. package/build/types.js +2 -0
  98. package/build/types.js.map +1 -0
  99. package/package.json +58 -0
  100. package/src/consts.ts +136 -0
  101. package/src/entrypoints.ts +127 -0
  102. package/src/errors.ts +207 -0
  103. package/src/helpers/apply.ts +183 -0
  104. package/src/helpers/card.ts +42 -0
  105. package/src/helpers/changes.ts +333 -0
  106. package/src/helpers/code.ts +66 -0
  107. package/src/helpers/index.ts +8 -0
  108. package/src/helpers/query.ts +345 -0
  109. package/src/helpers/specification.ts +65 -0
  110. package/src/helpers/status.ts +117 -0
  111. package/src/helpers/validate.ts +107 -0
  112. package/src/i18n/be.json +51 -0
  113. package/src/i18n/de.json +51 -0
  114. package/src/i18n/en.json +51 -0
  115. package/src/i18n/es.json +51 -0
  116. package/src/i18n/pl.json +51 -0
  117. package/src/i18n/ru.json +51 -0
  118. package/src/i18n/uk.json +51 -0
  119. package/src/i18n.ts +21 -0
  120. package/src/index.ts +9 -0
  121. package/src/models/index.ts +21 -0
  122. package/src/models/project.ts +24 -0
  123. package/src/models/specification.ts +21 -0
  124. package/src/models/workcard.ts +138 -0
  125. package/src/queue/index.ts +31 -0
  126. package/src/registry.ts +90 -0
  127. package/src/schemas.ts +654 -0
  128. package/src/types.ts +837 -0
  129. package/tests/apply.spec.ts +176 -0
  130. package/tests/code.spec.ts +40 -0
  131. package/tests/entrypoints.spec.ts +57 -0
  132. package/tests/fixtures.ts +124 -0
  133. package/tests/i18n.spec.ts +30 -0
  134. package/tests/models.spec.ts +133 -0
  135. package/tests/query.spec.ts +90 -0
  136. package/tests/schemas.spec.ts +74 -0
  137. package/tests/status.spec.ts +75 -0
  138. package/tests/tsconfig.json +17 -0
  139. package/tsconfig.json +15 -0
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @owlmeans/planning
2
+
3
+ Runtime-free contracts for project planning: workcards (cards, projects, specifications), typed
4
+ relationships, shareable status flows over three intrinsic states, the transition event and its
5
+ pure fold, the schema registry, the protocol tree and the models that behave identically on a
6
+ server and in a browser. An application depends on it from its shared contract package; it wires
7
+ the executor with `@owlmeans/server-planning` and reads remotely with `@owlmeans/client-planning`.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ bun add @owlmeans/planning@^0.1.18-rc.0 ajv ajv-formats
13
+ ```
14
+
15
+ ## Concepts
16
+
17
+ - **Transition** — the only way a record changes: an append-only event; a card is the fold of its
18
+ transitions in `seq` order (`applyTransition`).
19
+ - **Workcard** — base fields at the top level (`title`, `status`, `parent`, `parents`, `labels`,
20
+ `order`); a type's own fields under `fields`, validated by the type's schema.
21
+ - **Status flow** — shareable data declaring statuses (each mapped to `planned`, `in-progress` or
22
+ `closed`) and named transitions; a type may run several flows.
23
+ - **Specification** — a document workcard held in a slot the parent type declares; a revisioned
24
+ slot revises one record in place.
25
+ - **Wire query** — the scalar form a query travels in over HTTP (`encode*Query` / `decode*Query`).
26
+
27
+ ## Usage
28
+
29
+ Declare a flow and a type, and register them:
30
+
31
+ ```ts
32
+ import { CodeScope, CodeStyle, IntrinsicStatus, WorkcardKind, makeSchemaRegistry } from '@owlmeans/planning'
33
+
34
+ const taskFlow = {
35
+ id: 'app:task', version: 1,
36
+ statuses: [
37
+ { key: 'todo', intrinsic: IntrinsicStatus.Planned, initial: true },
38
+ { key: 'doing', intrinsic: IntrinsicStatus.InProgress },
39
+ { key: 'done', intrinsic: IntrinsicStatus.Closed, terminal: true },
40
+ ],
41
+ transitions: [
42
+ { name: 'start', from: ['todo'], to: 'doing', explicit: true },
43
+ { name: 'finish', from: ['doing'], to: 'done', explicit: true },
44
+ { name: 'reset', from: '*' as const, to: 'todo' },
45
+ ],
46
+ }
47
+
48
+ const registry = makeSchemaRegistry({
49
+ flows: [taskFlow],
50
+ types: [{
51
+ type: 'app:task', kind: WorkcardKind.Card, version: 1, flows: ['app:task'], specifications: [],
52
+ fields: { type: 'object', properties: { estimate: { type: 'number' } }, additionalProperties: false },
53
+ code: { prefix: 'T-', style: CodeStyle.Random, length: 5, uppercase: true, uniqueWithin: CodeScope.Parent },
54
+ }],
55
+ })
56
+ ```
57
+
58
+ Declare the protocol tree once in the shared package:
59
+
60
+ ```ts
61
+ import { makePlanningProtocols } from '@owlmeans/planning'
62
+
63
+ export const planningProtocols = makePlanningProtocols({
64
+ base: { alias: 'app:api:planning', path: '/planning' },
65
+ guards: 'guard:default',
66
+ })
67
+ ```
68
+
69
+ Encode a query on the client, decode it in a handler:
70
+
71
+ ```ts
72
+ import { criteriaOf, decodeWorkcardQuery, encodeWorkcardQuery } from '@owlmeans/planning'
73
+
74
+ const query = encodeWorkcardQuery({ parent: projectId, status: ['todo', 'doing'], sort: ['order'] })
75
+ const page = await context.entrypoint(planningProtocols.card.list).call({ query })
76
+
77
+ // server side
78
+ const where = criteriaOf(decodeWorkcardQuery(req.query), { entityId })
79
+ ```
80
+
81
+ Work through a model — the same code on either side of the wire:
82
+
83
+ ```ts
84
+ import { makeWorkcardModel } from '@owlmeans/planning'
85
+
86
+ const task = makeWorkcardModel(card, facade)
87
+ if (task.can('start')) {
88
+ const receipt = await task.transit('start', undefined, { wait: true })
89
+ }
90
+ await task.update({ title: 'Renamed' }) // expectSeq defaults to the record's head
91
+ ```
92
+
93
+ Fold transitions yourself (a mirror, a test, a store):
94
+
95
+ ```ts
96
+ import { applyRelationship, applyTransition } from '@owlmeans/planning'
97
+
98
+ const card = transitions.reduce((current, transition) => applyTransition(current, transition), undefined)
99
+ const links = transitions.reduce(applyRelationship, [])
100
+ ```
101
+
102
+ ## API
103
+
104
+ - Records and declarations: `Workcard`, `Card`, `Project`, `Specification`, `Relationship`,
105
+ `Transition`, `StatusFlowSchema`, `WorkcardTypeSchema`, `ProjectTypeSchema`, `SpecificationSlot`,
106
+ `CodePolicy`, `RelationshipType`, `PlanningSchemaBundle`.
107
+ - Write path and feed: `TransitionExecution`, `WorkcardDraft`, `ExecuteRequest`,
108
+ `TransitionReceipt`, `TransitionReceiptView`, `CommitEvent`, `CommitStatus`, `CommitSource`.
109
+ - Ports and seam: `TransitionStore`, `ProjectionStore`, `SpecificationStore`, `RelationshipStore`,
110
+ `PlanningStore`, `PlanningPlugin`, `PlanningScope`, `PlanningFacade`, `PlanningService`.
111
+ - Enums and constants: `WorkcardKind`, `IntrinsicStatus`, `IntrinsicPolicy`, `TransitionAction`,
112
+ `CommitState`, `SpecificationFormat`, `CodeStyle`, `CodeScope`, `PLANNING_SERVICE`,
113
+ `PLANNING_PATH`, `PLANNING_COMMIT_EVENT`, `planningAliases`, limits (`TITLE_MAX`, …).
114
+ - Fold and changes: `applyTransition`, `applyRelationship`, `computeChanges`, `isEmptyChange`,
115
+ `assertMutable`, `mergeFields`, `applyUnset`.
116
+ - Status: `intrinsicOf`, `initialStatusOf`, `ruleOf`, `canTransit`, `transitionsFrom`,
117
+ `initialFlowsOf`, `resolveIntrinsic`, `primaryFlowOf`, `isTerminal`.
118
+ - Query: `criteriaOf`, `listOptionsOf`, `specCriteriaOf`, `linkWhereOf`, `transitionWhereOf`,
119
+ `summaryOf`, `encode/decodeWorkcardQuery`, `encode/decodeSummaryQuery`,
120
+ `encode/decodeTransitionQuery`, `encode/decodeSpecificationQuery`,
121
+ `encode/decodeRelationshipQuery`.
122
+ - Codes, specifications, validation: `mintCode`, `codeScopeOf`, `slotOf`, `currentSpecification`,
123
+ `nextRevision`, `specificationTypeOf`, `makeAjv`, `validateFields`, `validateCard`,
124
+ `validateSpecificationBody`.
125
+ - Registry, protocols, models: `makeSchemaRegistry`, `makePlanningProtocols`, `makeWorkcardModel`,
126
+ `makeProjectModel`, `makeSpecificationModel`, `modelOf`.
127
+ - Errors: `PlanningError`, `WorkcardNotFound`, `UnknownWorkcardType`, `UnknownStatusFlow`,
128
+ `ParentNotFound`, `CardTypeNotAllowed`, `PlanningRefused`, `IllegalTransition`, `FieldsInvalid`,
129
+ `LabelNotAllowed`, `SpecificationSlotUnknown`, `SpecificationRevisionConflict`,
130
+ `RelationshipRefused`, `CodeTaken`, `WorkcardConflict`, `CommitTimeout`, `CommitFailed`,
131
+ `PlanningScopeMismatch`, `PlanningUnsupported`.
132
+ - Schemas: `WorkcardSchema`, `SpecificationSchema`, `TransitionSchema`, `ExecuteRequestSchema`,
133
+ `WorkcardQuerySchema`, … (every record, declaration, request and view).
134
+ - Subpath `@owlmeans/planning/queue`: `PLANNING_PROJECTION_QUEUE`, `PLANNING_PROJECT_JOB`,
135
+ `declarePlanningQueue`, `ProjectionRequest`.
136
+
137
+ ## Common pitfalls
138
+
139
+ - Pass a query through `encode*Query` before an HTTP call — arrays and objects do not survive a
140
+ query string as they are.
141
+ - `status`, `intrinsic` and `flows` move only through `transit`; an `update` naming them is refused.
142
+ - `fields` and `flows` in `changes` merge; every other key replaces. Clear with `null` or `unset`.
143
+ - A model's `expectSeq` is its record's head: a stale model refuses with `WorkcardConflict`.
144
+ - `WorkcardNotFound` is also the answer for another entity's card — never read it as "deleted".
145
+ - `CommitTimeout` leaves the transition pending; it will still commit.
146
+ - Keep schemas `$jsonSchema`-safe: no `integer`, every optional property nullable.
147
+
148
+ ## Related packages
149
+
150
+ - `@owlmeans/server-planning` — executor, plugin registry, memory store, handlers
151
+ - `@owlmeans/client-planning` — remote facade, state mirror, commit waiting
152
+ - `@owlmeans/resource` — the criteria language
153
+ - `@owlmeans/queue` — the projection queue declared by the `./queue` subpath
154
+
155
+ <!-- owlmeans:agent-guidance:start -->
156
+ ## Agent guidance
157
+
158
+ This package ships embedded agent skills under `agent-meta/`. After installing your
159
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
160
+ your project's skill store (`.agents/skills/`):
161
+
162
+ ```sh
163
+ npx @owlmeans/agent-skills@^0.1.18-rc.27
164
+ ```
165
+
166
+ The embedded files are version-matched to this package release. Do not edit them
167
+ directly — they are regenerated on each publish. To contribute guidance edits,
168
+ open a PR against the source monorepo.
169
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "package": "@owlmeans/planning",
4
+ "version": "0.1.18-rc.0",
5
+ "generatedAt": "2026-09-16T15:11:05.003Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "planning",
11
+ "category": "package-specific",
12
+ "file": "skills/planning/SKILL.md",
13
+ "canonicalPath": ".agents/skills/planning/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: planning
3
+ description: How to use @owlmeans/planning — the runtime-free workcard model (cards, projects, specifications), typed relationships, shareable status flows with three intrinsic states, the Transition event and its pure fold, the schema registry, the query language and its wire encoding, the protocol tree, and the models that work identically on a server and in a browser. Auto-invoked when importing a Workcard type, a status flow, applyTransition, computeChanges, criteriaOf, encodeWorkcardQuery, makePlanningProtocols or a planning model.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/planning
9
+
10
+ **Layer:** Cross-cutting domain
11
+ **Install:** `"@owlmeans/planning": "^0.1.18-rc.0"` in `dependencies` (`ajv` and `ajv-formats` are peers)
12
+
13
+ The contracts of project planning: record shapes, schemas, refusals, the protocol tree, the pure
14
+ fold and the models. No database, no fastify, no React. The executor, the plugin registry, the
15
+ memory store and the handlers are `@owlmeans/server-planning`; the remote facade and the state
16
+ mirror are `@owlmeans/client-planning`; a durable store implements the ports declared here.
17
+
18
+ ## The one idea
19
+
20
+ Every change is a **transition** — an append-only event — and a card IS the fold of its
21
+ transitions in `seq` order. There is one write path (`facade.execute(exec)`), one fold
22
+ (`applyTransition`) and one query translation (`criteriaOf`), shared by every store and both sides
23
+ of the wire, so one log always means one card.
24
+
25
+ ## Key exports
26
+
27
+ | Export | What it is |
28
+ |---|---|
29
+ | `Workcard`, `Card`, `Project`, `Specification`, `Relationship`, `Transition` | The records |
30
+ | `StatusFlowSchema`, `WorkcardTypeSchema`, `ProjectTypeSchema`, `SpecificationSlot`, `CodePolicy`, `RelationshipType`, `PlanningSchemaBundle` | Type declarations — data, not code |
31
+ | `TransitionExecution`, `WorkcardDraft`, `TransitionReceipt`, `CommitEvent`, `CommitSource` | The write path and its commit feed |
32
+ | `TransitionStore`, `ProjectionStore`, `SpecificationStore`, `RelationshipStore`, `PlanningStore` | Ports a store implements (only `cards` is required) |
33
+ | `PlanningPlugin`, `PlanningScope`, `PlanningFacade`, `PlanningService` | The plugin seam and the facade, typed here so a plugin and a client can name them |
34
+ | `WorkcardKind`, `IntrinsicStatus`, `IntrinsicPolicy`, `TransitionAction`, `CommitState`, `SpecificationFormat`, `CodeStyle`, `CodeScope` | Enums |
35
+ | `PLANNING_SERVICE` (`'planning'`), `PLANNING_PATH`, `PLANNING_COMMIT_EVENT` (`'planning-commit'`), `planningAliases(base)` | Names both sides share |
36
+ | `TITLE_MAX` 2048, `DESCRIPTION_MAX` 16384, `CODE_MAX` 64, `BODY_MAX` 1048576, `MAX_LABELS` 32, `MAX_PARENTS` 32, `DEFAULT_COMMIT_TIMEOUT` 30 s, `DEFAULT_COMMIT_POLL` 20 s, `MAX_COMMIT_POLL` 55 s, `CODE_MINT_ATTEMPTS` 8 | Limits |
37
+ | `applyTransition`, `applyRelationship` | The fold |
38
+ | `computeChanges`, `isEmptyChange`, `assertMutable`, `mergeFields`, `applyUnset`, `sameValue` | What a transition records |
39
+ | `intrinsicOf`, `initialStatusOf`, `ruleOf`, `canTransit`, `transitionsFrom`, `initialFlowsOf`, `resolveIntrinsic`, `primaryFlowOf`, `isTerminal` | Status helpers |
40
+ | `criteriaOf`, `listOptionsOf`, `specCriteriaOf`, `linkWhereOf`, `transitionWhereOf`, `summaryOf` | Querying |
41
+ | `encode*Query` / `decode*Query` (Workcard, Summary, Transition, Specification, Relationship), `encodeSort`, `decodeSort`, `encodeList`, `decodeList` | The wire form of a query |
42
+ | `mintCode`, `codeScopeOf`, `slugOf` | Codes |
43
+ | `slotOf`, `currentSpecification`, `nextRevision`, `specificationTypeOf`, `bodyCharsOf` | Specifications |
44
+ | `makeAjv`, `validateFields`, `validateCard`, `validateSpecificationBody`, `invalidFieldKeys`, `ajvErrorText` | Validation |
45
+ | `makeSchemaRegistry(bundle?)` | Types, flows, cached field validators; `bundle()` / `load()` |
46
+ | `makePlanningProtocols(opts)` | The protocol tree |
47
+ | `makeWorkcardModel`, `makeProjectModel`, `makeSpecificationModel`, `modelOf`, `executeFor` | Models |
48
+ | `*Schema` | AJV schemas of every record, declaration, request and view |
49
+ | `@owlmeans/planning/queue` | `PLANNING_PROJECTION_QUEUE`, `PLANNING_PROJECT_JOB`, `declarePlanningQueue(cfg, opts?)`, `ProjectionRequest` |
50
+
51
+ ## The record model
52
+
53
+ - **Base fields at the top level, provider fields under `fields`.** `title`, `description`, `code`,
54
+ `status`, `labels`, `order`, `parent` are generic and indexable; everything a type adds lives in
55
+ `fields` and is validated by the type's `fields` JSON schema. Generic code reads only the top.
56
+ A `fields` key never holds `.` or `$` at any depth (`invalidFieldKeys`).
57
+ - **`entityId` is the organization's stable id**, never a slug. Every read is scoped by it.
58
+ - **Membership is `parent` + `parents`, never a relationship.** `parents` always contains `parent`
59
+ (first), so many-to-many membership needs no junction row. `within` queries `parents`.
60
+ - **Specification is a workcard** (`kind: specification`) with `category`, `format`, `body`, `ref`,
61
+ `revision`, `version`, `bodyChars`. The SLOT — category, format, `multiple`, `revisioned`,
62
+ `keepRevisions`, a JSON body `schema` — is declared on the PARENT's type. One record per
63
+ `(parent, category)` unless the slot is `multiple`; a revisioned slot increments `revision` on the
64
+ same record, and earlier bodies come back from the transition log.
65
+ - **`seq` is the last folded transition, `head` the highest allocated.** `head > seq` means a write
66
+ is in flight (`isPending`); optimistic concurrency (`expectSeq`) compares against the head.
67
+ - `order` is a double: a card may sit between two others (`3.5`).
68
+ - Timestamps are ISO-8601 UTC strings (`toISOString()`), so lexicographic order is chronological.
69
+
70
+ ## Status flows
71
+
72
+ - Every status maps onto one of three intrinsic states: `planned`, `in-progress`, `closed`.
73
+ Summaries, gates and boards read the intrinsic state; the status key is the provider's vocabulary.
74
+ - A flow is shareable across types, and a type may run several (`flows[0]` is primary).
75
+ `flows` on the record is authoritative for every flow; `status`/`intrinsic` mirror the primary.
76
+ - A rule name may repeat with different `from` sets (`start` from `planned`, `start` from
77
+ `failed`). `ruleOf` picks the rule naming the current status; a `'*'` rule of that name answers
78
+ only when none does, wherever it is declared. `'*'` includes the target status itself, so
79
+ `reset` from `planned` is legal.
80
+ - `transitionsFrom(flow, status)` answers one rule per name in declaration order;
81
+ `{ explicit: true }` keeps the ones offered to a person.
82
+ - `IntrinsicPolicy.Primary` (default) reads the primary flow; `IntrinsicPolicy.All` takes the least
83
+ advanced flow — closed only when every flow is closed. A status a flow does not declare reads as
84
+ planned.
85
+
86
+ ## Declaring a type
87
+
88
+ A `WorkcardTypeSchema` / `ProjectTypeSchema` is plain data registered through a plugin's `schemas`
89
+ or `makeSchemaRegistry({ types, flows })`:
90
+
91
+ - `fields` — the AJV schema of `fields`; the registry compiles and caches it (`validator(type)`).
92
+ - `specifications` — the slots this type's cards carry as children. A slot may name its
93
+ specification `type`; otherwise `specificationTypeOf` takes the only registered specification
94
+ type, then the one sharing the parent type's prefix (`viable:project` → `viable:spec`).
95
+ - `code` — `CodePolicy { prefix?, style: random|sequential|slug, length?, uppercase?, uniqueWithin:
96
+ parent|entity, mutable? }`. A code is fixed once minted unless `mutable`.
97
+ - `relationships` — the edge types a card may start (`single` = at most one per card).
98
+ - `labels` — the allowed labels (any when omitted).
99
+ - A project type lists `cardTypes` (and `projectTypes` for nesting) its children may have.
100
+
101
+ ## The fold
102
+
103
+ `applyTransition(card | undefined, transition)` is pure and total:
104
+
105
+ - an already-applied seq (`seq <= card.seq`) returns the same card — re-folding is safe;
106
+ - anything but `card.seq + 1` (or seq 1 for a create with no card) throws
107
+ `PlanningError('fold:out-of-order')`;
108
+ - `create` builds the whole record from `changes` + `id`/`kind`/`type`/`entityId`/`createdAt`/`seq`;
109
+ - `update`/`transit` write `changes` — **`fields` and `flows` merge shallowly, every other key
110
+ replaces** — and clear `unset` (`key`, `fields.key`, `flows.id`); identity and required keys are
111
+ never written or cleared;
112
+ - `link`/`unlink` move only `seq`/`head`/`updatedAt`; `delete` answers `null`;
113
+ - every result carries `seq = transition.seq`, `head = max(head, seq)`, `updatedAt = transition.at`.
114
+
115
+ `applyRelationship(links, transition)` folds the edges: a create's `links`, `link`, `unlink`, and a
116
+ `delete` dropping every edge touching the card. An existing edge is kept, so re-folding adds nothing.
117
+
118
+ `computeChanges(card, exec, type, registry, at, { slot? })` is what the executor records:
119
+
120
+ - create — the whole initial record: normalized `parents`, every flow at its initial status (the
121
+ draft's `status` for the primary), mirrored `status`/`intrinsic`, `closedAt` when it starts
122
+ closed; a specification gets its format, `bodyChars` and revision 1 when the slot is revisioned.
123
+ The code must already be on the draft (the executor mints first);
124
+ - update — only differing values; `null` or `exec.unset` clears a field that has one; a moved
125
+ `parent` replaces the old one in `parents`; changed content of a revisioned document sets
126
+ `revision + 1`. An update that changes nothing is `isEmptyChange` and must append nothing;
127
+ - transit — `flows[flow] = rule.to`, `status` on the primary flow, `intrinsic` when it moves,
128
+ `closedAt` set on entering closed and cleared on leaving; throws `IllegalTransition`;
129
+ - link/unlink/delete — nothing.
130
+
131
+ `assertMutable(exec, type)` refuses (`planning:immutable:<field>`) identity keys, `revision` /
132
+ `bodyChars`, `status`/`intrinsic`/`flows`/`closedAt` outside a create (they move only through
133
+ `transit`), `category`, a fixed `code`, and clearing a required field.
134
+
135
+ ## Querying
136
+
137
+ `criteriaOf(query, scope)` is the ONE translation from `WorkcardQuery` to `Criteria<Workcard>`:
138
+ bare values and arrays pass through (`kind`, `type`, `status`, `intrinsic`, `code`, `category`),
139
+ `parent` is direct children, `within` → `parents: { $contains }`, `labels` → `$overlaps`, `ids` →
140
+ `id: { $in }`, `flow` → `flows.<id>`, `fields` → `fields.<key>` equality, `q` → `$or` of title /
141
+ description `$ilike` (wildcards escaped) and code `$startsWith`, `updatedSince` → `updatedAt: {
142
+ $gte }`. `entityId` always comes from the scope; `undefined` is omitted. Paging and sort are
143
+ `listOptionsOf` (`size: 0` = no limit). `summaryOf(cards, parents)` counts DIRECT children by
144
+ intrinsic state and gives no key to a parent with none.
145
+
146
+ **A query crosses HTTP in its wire form.** The OwlMeans transport cannot carry arrays or nested
147
+ objects in a query string (the client writes `key[]=`, the server reads that key literally), so
148
+ the list, summary, transitions, specifications and links protocols take `*QueryWire` shapes: lists
149
+ comma-joined (a JSON array when an entry holds a comma), objects as JSON, sort as `field,-field`.
150
+ A client calls `encodeWorkcardQuery(query)` before `call({ query })`; a handler calls
151
+ `decodeWorkcardQuery(req.query)` — tolerant of arrays and of the rich form — and then
152
+ `criteriaOf`. A value that does not decode throws `PlanningError('malformed:query:<key>')`.
153
+
154
+ ## The protocol tree and mounting
155
+
156
+ `makePlanningProtocols({ base: { alias, path?, parent?, service? }, guards, gate?, socketBase? })`:
157
+
158
+ | Leaf | Route | Request → reply |
159
+ |---|---|---|
160
+ | `schema.list` | GET `/schemas` | → `PlanningSchemaBundle` |
161
+ | `card.list` | GET `/cards` | query `WorkcardQueryWire` → `ListResult<Workcard>` |
162
+ | `card.summary` | GET `/cards/summary` | query `SummaryQueryWire` → `SummaryView` |
163
+ | `card.get` | GET `/cards/:id` | → `Workcard` |
164
+ | `card.transitions` | GET `/cards/:id/transitions` | query `TransitionQueryWire` |
165
+ | `card.specifications` | GET `/cards/:id/specifications` | query `SpecificationQueryWire` |
166
+ | `spec.get` / `spec.revisions` | GET `/specifications/:id` / `…/revisions` | query `RevisionsQuery` |
167
+ | `link.list` | GET `/links` | query `RelationshipQueryWire` |
168
+ | `transition.get` | GET `/transitions/:transition` | → `Transition` |
169
+ | `execute` | POST `/execute` | body `ExecuteRequest` → `TransitionReceiptView` |
170
+ | `commit.get` | GET `/commits/:transition` | query `CommitQuery { wait? }` → `CommitStatus` |
171
+ | `commit.events` | SOCKET `/commits` | query `CommitFeedQuery` → `CommitEvent` frames |
172
+
173
+ - Aliases derive from the base alias (`planningAliases(base)` → `<base>:card:list`, …); two mounts
174
+ are two base aliases.
175
+ - The guards and the gate sit on the base alone; every HTTP leaf inherits them. `path` defaults to
176
+ `/planning`.
177
+ - Static `/cards/summary` is declared before parametric `/cards/:id`.
178
+ - The commit socket hangs under `socketBase` when given and then inherits THAT base's guards —
179
+ pass an update/socket base that authenticates; otherwise it hangs under the planning base.
180
+ - `actor` in an execute body is ignored on the wire: the server fills it from the request.
181
+ - Responses carry no runtime schema (typed only), so a serializer never strips `fields`.
182
+
183
+ ## Models
184
+
185
+ `makeWorkcardModel(record, facade)` (and `makeProjectModel`, `makeSpecificationModel`, `modelOf`
186
+ by kind) is the same object on a server and in a browser. `schema`, `flow`, `statusOf`,
187
+ `intrinsicOf`, `can`, `available`, `pending` answer from the registry with no I/O; `transit`,
188
+ `update`, `remove`, `link`, `unlink`, `write` build a `TransitionExecution` and call
189
+ `facade.execute`.
190
+
191
+ - `expectSeq` defaults to `record.head ?? record.seq`; pass `null` to opt out, a number to pin.
192
+ A stale model therefore refuses with `WorkcardConflict` — `reload()` and retry.
193
+ - `write(category, body)` creates the slot's document when the slot is empty and updates it when
194
+ filled (expecting the DOCUMENT's head, not the parent's). The model never computes a revision;
195
+ the executor does.
196
+ - `ProjectModel` adds `cards`, `projects` (by `within`), `summary` (zero counts when empty) and
197
+ `purge` (a delete of the project — the store removes everything under it).
198
+ - `SpecificationModel` adds `body`, `revise`, `history` (from the log).
199
+
200
+ ## Errors
201
+
202
+ Every refusal is a registered `ResilientError` whose message is `planning:<marker>:<detail>` —
203
+ the marker survives a hop where the class is unknown, so match on it there:
204
+
205
+ | Class | Marker |
206
+ |---|---|
207
+ | `PlanningError` | `planning:` (`fold:out-of-order`, `immutable:<field>`, `malformed:<what>`) |
208
+ | `WorkcardNotFound` | `workcard-not-found:` — also the answer for another entity's card |
209
+ | `UnknownWorkcardType` / `UnknownStatusFlow` | `unknown-type:` / `unknown-flow:` |
210
+ | `ParentNotFound` / `CardTypeNotAllowed` | `parent-not-found:` / `card-type-not-allowed:` |
211
+ | `PlanningRefused` | `refused:` — a plugin middleware said no |
212
+ | `IllegalTransition` | `illegal-transition:<flow>:<transition>:<from>` |
213
+ | `FieldsInvalid` / `LabelNotAllowed` | `fields-invalid:` / `label-not-allowed:` |
214
+ | `SpecificationSlotUnknown` / `SpecificationRevisionConflict` | `specification-slot-unknown:` / `specification-revision-conflict:` |
215
+ | `RelationshipRefused` / `CodeTaken` | `relationship-refused:` / `code-taken:` |
216
+ | `WorkcardConflict` | `workcard-conflict:` — stale `expectSeq` |
217
+ | `CommitTimeout` / `CommitFailed` | `commit-timeout:` (still pending, nothing undone) / `commit-failed:` |
218
+ | `PlanningScopeMismatch` / `PlanningUnsupported` | `scope-mismatch:` / `unsupported:` |
219
+
220
+ Type names are `PlanningError<Suffix>` (`PlanningErrorWorkcardNotFound`).
221
+
222
+ ## i18n
223
+
224
+ Importing the package registers, in all seven languages, `lib:planning.*` labels (`kind.*`,
225
+ `intrinsic.*`, `action.*`, `commit.*`, `format.*`) and one text per refusal type under the shared
226
+ `errors` resource (`errors.<TypeName>`), where a panel's error lookup finds it.
227
+
228
+ ## Schema rules
229
+
230
+ A record schema may become a Mongo `$jsonSchema`, so every exported schema keeps two rules: **no
231
+ `integer`** (`number` only) and **every optional property `nullable: true`** (an enum then admits
232
+ `null` too). No `oneOf`/`anyOf` inside a record schema. Build type arrays per schema, never share
233
+ one object between schemas — ajv may extend a `type` array while compiling. The walk that pins this
234
+ is part of the package's own tests.
235
+
236
+ ## Depends On
237
+
238
+ - `@owlmeans/resource` — `Criteria`, `ListOptions`, `ListResult`, `createListSchema`
239
+ - `@owlmeans/entrypoint`, `@owlmeans/route` — the protocol tree
240
+ - `@owlmeans/error`, `@owlmeans/i18n`, `@owlmeans/auth` (`IdValueSchema`), `@owlmeans/basic-ids`, `@owlmeans/context`
241
+ - `@owlmeans/queue` — only through the `./queue` subpath
242
+
243
+ ## Related
244
+
245
+ - [[server-planning]] — the executor, the plugin registry, the memory store, the handlers
246
+ - [[client-planning]] — the remote facade, the state mirror, waiting for a commit
247
+ - [[resource]] — the criteria language `criteriaOf` targets
248
+ - [[queue]] — the projection queue the `./queue` subpath declares
@@ -0,0 +1,126 @@
1
+ /** What a workcard IS. Cards, projects and specifications share one id space and one fold. */
2
+ export declare enum WorkcardKind {
3
+ Card = "card",
4
+ Project = "project",
5
+ Specification = "specification"
6
+ }
7
+ /**
8
+ * The three states every status of every flow maps onto.
9
+ *
10
+ * A provider's statuses are its own vocabulary; these three are the only ones generic code — a
11
+ * summary, a board header, a gate — ever reads.
12
+ */
13
+ export declare enum IntrinsicStatus {
14
+ Planned = "planned",
15
+ InProgress = "in-progress",
16
+ Closed = "closed"
17
+ }
18
+ /** Advancement order — `resolveIntrinsic` under {@link IntrinsicPolicy.All} takes the minimum. */
19
+ export declare const INTRINSIC_ORDER: Readonly<Record<IntrinsicStatus, number>>;
20
+ /** How a type running several flows answers its one intrinsic status. */
21
+ export declare enum IntrinsicPolicy {
22
+ /** The primary flow's status decides. */
23
+ Primary = "primary",
24
+ /** The least advanced flow decides — closed only when every flow is closed. */
25
+ All = "all"
26
+ }
27
+ export declare enum TransitionAction {
28
+ Create = "create",
29
+ Update = "update",
30
+ Transit = "transit",
31
+ Link = "link",
32
+ Unlink = "unlink",
33
+ Delete = "delete"
34
+ }
35
+ export declare enum CommitState {
36
+ Pending = "pending",
37
+ Committed = "committed",
38
+ Failed = "failed"
39
+ }
40
+ export declare enum SpecificationFormat {
41
+ Markdown = "markdown",
42
+ Json = "json",
43
+ Text = "text"
44
+ }
45
+ export declare enum CodeStyle {
46
+ Random = "random",
47
+ Sequential = "sequential",
48
+ Slug = "slug"
49
+ }
50
+ /** Where a minted code must be unique. */
51
+ export declare enum CodeScope {
52
+ Parent = "parent",
53
+ Entity = "entity"
54
+ }
55
+ /** Service alias of the planning facade host. Identical on the server and in a client. */
56
+ export declare const PLANNING_SERVICE = "planning";
57
+ /** The path segment the protocol tree answers under when the mount names none. */
58
+ export declare const PLANNING_PATH = "/planning";
59
+ /** The socket frame name the commit feed pushes under. */
60
+ export declare const PLANNING_COMMIT_EVENT = "planning-commit";
61
+ /** The i18n resource the package's labels are registered under (`lib:planning.*`). */
62
+ export declare const PLANNING_I18N = "planning";
63
+ /** A `from` that matches every status of the flow. */
64
+ export declare const ANY_STATUS = "*";
65
+ /** How long `committed()` waits by default, in milliseconds. */
66
+ export declare const DEFAULT_COMMIT_TIMEOUT = 30000;
67
+ /** Seconds a single long poll of `commit.get` holds by default. */
68
+ export declare const DEFAULT_COMMIT_POLL = 20;
69
+ /** The longest single long poll a server grants, in seconds. */
70
+ export declare const MAX_COMMIT_POLL = 55;
71
+ /** Re-rolls a code policy gets before it refuses with `CodeTaken`. */
72
+ export declare const CODE_MINT_ATTEMPTS = 8;
73
+ /** Random part length of a code policy that names none. */
74
+ export declare const DEFAULT_CODE_LENGTH = 6;
75
+ export declare const DEFAULT_PLUGIN_ORDER = 50;
76
+ export declare const DEFAULT_PAGE_SIZE = 100;
77
+ export declare const PLANNING_SCHEMA_VERSION = 1;
78
+ export declare const TITLE_MAX = 2048;
79
+ export declare const DESCRIPTION_MAX = 16384;
80
+ export declare const CODE_MAX = 64;
81
+ export declare const TYPE_MAX = 128;
82
+ export declare const LABEL_MAX = 64;
83
+ export declare const MAX_LABELS = 32;
84
+ export declare const MAX_PARENTS = 32;
85
+ export declare const BODY_MAX = 1048576;
86
+ export declare const CAUSE_MAX = 256;
87
+ export declare const KEY_MAX = 256;
88
+ export declare const REF_MAX = 1024;
89
+ export declare const QUERY_TEXT_MAX = 256;
90
+ /** The longest list a wire query carries in one value (ids, parents). */
91
+ export declare const MAX_QUERY_LIST = 500;
92
+ /**
93
+ * The aliases one planning tree answers under, derived from the mount's base alias.
94
+ *
95
+ * Two mounts of the tree in one deployment are two base aliases, never two copies of this table —
96
+ * which is why nothing here is a global constant.
97
+ */
98
+ export declare const planningAliases: (base: string) => Readonly<{
99
+ base: string;
100
+ schema: Readonly<{
101
+ list: `${string}:schema:list`;
102
+ }>;
103
+ card: Readonly<{
104
+ list: `${string}:card:list`;
105
+ summary: `${string}:card:summary`;
106
+ get: `${string}:card:get`;
107
+ transitions: `${string}:card:transitions`;
108
+ specifications: `${string}:card:specifications`;
109
+ }>;
110
+ spec: Readonly<{
111
+ get: `${string}:specification:get`;
112
+ revisions: `${string}:specification:revisions`;
113
+ }>;
114
+ link: Readonly<{
115
+ list: `${string}:link:list`;
116
+ }>;
117
+ transition: Readonly<{
118
+ get: `${string}:transition:get`;
119
+ }>;
120
+ execute: `${string}:execute`;
121
+ commit: Readonly<{
122
+ get: `${string}:commit:get`;
123
+ events: `${string}:commit:events`;
124
+ }>;
125
+ }>;
126
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,oBAAY,YAAY;IACtB,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,aAAa,kBAAkB;CAChC;AAED;;;;;GAKG;AACH,oBAAY,eAAe;IACzB,OAAO,YAAY;IACnB,UAAU,gBAAgB;IAC1B,MAAM,WAAW;CAClB;AAED,kGAAkG;AAClG,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAIpE,CAAA;AAEF,yEAAyE;AACzE,oBAAY,eAAe;IACzB,yCAAyC;IACzC,OAAO,YAAY;IACnB,+EAA+E;IAC/E,GAAG,QAAQ;CACZ;AAED,oBAAY,gBAAgB;IAC1B,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,MAAM,WAAW;CAClB;AAED,oBAAY,WAAW;IACrB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,MAAM,WAAW;CAClB;AAED,oBAAY,mBAAmB;IAC7B,QAAQ,aAAa;IACrB,IAAI,SAAS;IACb,IAAI,SAAS;CACd;AAED,oBAAY,SAAS;IACnB,MAAM,WAAW;IACjB,UAAU,eAAe;IACzB,IAAI,SAAS;CACd;AAED,0CAA0C;AAC1C,oBAAY,SAAS;IACnB,MAAM,WAAW;IACjB,MAAM,WAAW;CAClB;AAED,0FAA0F;AAC1F,eAAO,MAAM,gBAAgB,aAAa,CAAA;AAE1C,kFAAkF;AAClF,eAAO,MAAM,aAAa,cAAc,CAAA;AAExC,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,oBAAoB,CAAA;AAEtD,sFAAsF;AACtF,eAAO,MAAM,aAAa,aAAa,CAAA;AAEvC,sDAAsD;AACtD,eAAO,MAAM,UAAU,MAAM,CAAA;AAE7B,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAC5C,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,gEAAgE;AAChE,eAAO,MAAM,eAAe,KAAK,CAAA;AACjC,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB,IAAI,CAAA;AACpC,eAAO,MAAM,oBAAoB,KAAK,CAAA;AACtC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,eAAO,MAAM,SAAS,OAAO,CAAA;AAC7B,eAAO,MAAM,eAAe,QAAQ,CAAA;AACpC,eAAO,MAAM,QAAQ,KAAK,CAAA;AAC1B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAC3B,eAAO,MAAM,SAAS,KAAK,CAAA;AAC3B,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,WAAW,KAAK,CAAA;AAC7B,eAAO,MAAM,QAAQ,UAAY,CAAA;AACjC,eAAO,MAAM,SAAS,MAAM,CAAA;AAC5B,eAAO,MAAM,OAAO,MAAM,CAAA;AAC1B,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,eAAO,MAAM,cAAc,MAAM,CAAA;AACjC,yEAAyE;AACzE,eAAO,MAAM,cAAc,MAAM,CAAA;AAEjC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,SAAU,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB1C,CAAA"}