@owlmeans/server-planning 0.1.18-rc.21 → 0.1.18-rc.23

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 (313) hide show
  1. package/README.md +24 -17
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-planning/SKILL.md +28 -28
  4. package/build/actions/apply-schema-request.d.ts +8 -0
  5. package/build/actions/apply-schema-request.d.ts.map +1 -0
  6. package/build/actions/apply-schema-request.js +31 -0
  7. package/build/actions/apply-schema-request.js.map +1 -0
  8. package/build/actions/cards.d.ts +5 -3
  9. package/build/actions/cards.d.ts.map +1 -1
  10. package/build/actions/cards.js +1 -1
  11. package/build/actions/cards.js.map +1 -1
  12. package/build/actions/commit.d.ts +3 -3
  13. package/build/actions/commit.d.ts.map +1 -1
  14. package/build/actions/commit.js +5 -4
  15. package/build/actions/commit.js.map +1 -1
  16. package/build/actions/define-schemas.d.ts +13 -0
  17. package/build/actions/define-schemas.d.ts.map +1 -0
  18. package/build/actions/define-schemas.js +28 -0
  19. package/build/actions/define-schemas.js.map +1 -0
  20. package/build/actions/execute.d.ts +8 -28
  21. package/build/actions/execute.d.ts.map +1 -1
  22. package/build/actions/execute.js +17 -101
  23. package/build/actions/execute.js.map +1 -1
  24. package/build/actions/execution/types.d.ts +34 -0
  25. package/build/actions/execution/types.d.ts.map +1 -0
  26. package/build/actions/execution/types.js +2 -0
  27. package/build/actions/execution/types.js.map +1 -0
  28. package/build/actions/execution.d.ts +4 -0
  29. package/build/actions/execution.d.ts.map +1 -0
  30. package/build/actions/execution.js +79 -0
  31. package/build/actions/execution.js.map +1 -0
  32. package/build/actions/get-card.d.ts +6 -0
  33. package/build/actions/get-card.d.ts.map +1 -0
  34. package/build/actions/get-card.js +9 -0
  35. package/build/actions/get-card.js.map +1 -0
  36. package/build/actions/get-specification.d.ts +6 -0
  37. package/build/actions/get-specification.d.ts.map +1 -0
  38. package/build/actions/get-specification.js +9 -0
  39. package/build/actions/get-specification.js.map +1 -0
  40. package/build/actions/index.d.ts +12 -3
  41. package/build/actions/index.d.ts.map +1 -1
  42. package/build/actions/index.js +11 -3
  43. package/build/actions/index.js.map +1 -1
  44. package/build/actions/links.d.ts +3 -3
  45. package/build/actions/links.d.ts.map +1 -1
  46. package/build/actions/links.js +6 -5
  47. package/build/actions/links.js.map +1 -1
  48. package/build/actions/list-card-specifications.d.ts +6 -0
  49. package/build/actions/list-card-specifications.d.ts.map +1 -0
  50. package/build/actions/list-card-specifications.js +11 -0
  51. package/build/actions/list-card-specifications.js.map +1 -0
  52. package/build/actions/list-card-transitions.d.ts +6 -0
  53. package/build/actions/list-card-transitions.d.ts.map +1 -0
  54. package/build/actions/list-card-transitions.js +11 -0
  55. package/build/actions/list-card-transitions.js.map +1 -0
  56. package/build/actions/list-cards.d.ts +6 -0
  57. package/build/actions/list-cards.d.ts.map +1 -0
  58. package/build/actions/list-cards.js +10 -0
  59. package/build/actions/list-cards.js.map +1 -0
  60. package/build/actions/list-schemas.d.ts +13 -0
  61. package/build/actions/list-schemas.d.ts.map +1 -0
  62. package/build/actions/list-schemas.js +20 -0
  63. package/build/actions/list-schemas.js.map +1 -0
  64. package/build/actions/list-specification-revisions.d.ts +6 -0
  65. package/build/actions/list-specification-revisions.d.ts.map +1 -0
  66. package/build/actions/list-specification-revisions.js +10 -0
  67. package/build/actions/list-specification-revisions.js.map +1 -0
  68. package/build/actions/schemas.d.ts +5 -3
  69. package/build/actions/schemas.d.ts.map +1 -1
  70. package/build/actions/schemas.js.map +1 -1
  71. package/build/actions/specs.d.ts +4 -2
  72. package/build/actions/specs.d.ts.map +1 -1
  73. package/build/actions/specs.js.map +1 -1
  74. package/build/actions/summarize-cards.d.ts +6 -0
  75. package/build/actions/summarize-cards.d.ts.map +1 -0
  76. package/build/actions/summarize-cards.js +11 -0
  77. package/build/actions/summarize-cards.js.map +1 -0
  78. package/build/actions/transitions.d.ts +3 -3
  79. package/build/actions/transitions.d.ts.map +1 -1
  80. package/build/actions/transitions.js +4 -3
  81. package/build/actions/transitions.js.map +1 -1
  82. package/build/actions/types.d.ts +5 -3
  83. package/build/actions/types.d.ts.map +1 -1
  84. package/build/actions/watch.d.ts +2 -2
  85. package/build/actions/watch.d.ts.map +1 -1
  86. package/build/actions/watch.js +2 -2
  87. package/build/actions/watch.js.map +1 -1
  88. package/build/conformance/assert/types.d.ts +19 -0
  89. package/build/conformance/assert/types.d.ts.map +1 -0
  90. package/build/conformance/assert/types.js +2 -0
  91. package/build/conformance/assert/types.js.map +1 -0
  92. package/build/conformance/assert.d.ts +8 -13
  93. package/build/conformance/assert.d.ts.map +1 -1
  94. package/build/conformance/assert.js +41 -43
  95. package/build/conformance/assert.js.map +1 -1
  96. package/build/conformance/cases.d.ts.map +1 -1
  97. package/build/conformance/cases.js +86 -67
  98. package/build/conformance/cases.js.map +1 -1
  99. package/build/conformance/clock.d.ts +6 -0
  100. package/build/conformance/clock.d.ts.map +1 -0
  101. package/build/conformance/clock.js +12 -0
  102. package/build/conformance/clock.js.map +1 -0
  103. package/build/conformance/consts.d.ts +3 -1
  104. package/build/conformance/consts.d.ts.map +1 -1
  105. package/build/conformance/consts.js +84 -1
  106. package/build/conformance/consts.js.map +1 -1
  107. package/build/conformance/errors.d.ts +5 -0
  108. package/build/conformance/errors.d.ts.map +1 -0
  109. package/build/conformance/errors.js +8 -0
  110. package/build/conformance/errors.js.map +1 -0
  111. package/build/conformance/fixtures/types.d.ts +13 -0
  112. package/build/conformance/fixtures/types.d.ts.map +1 -0
  113. package/build/conformance/fixtures/types.js +2 -0
  114. package/build/conformance/fixtures/types.js.map +1 -0
  115. package/build/conformance/fixtures.d.ts +8 -8
  116. package/build/conformance/fixtures.d.ts.map +1 -1
  117. package/build/conformance/fixtures.js +24 -107
  118. package/build/conformance/fixtures.js.map +1 -1
  119. package/build/conformance/index.d.ts +3 -0
  120. package/build/conformance/index.d.ts.map +1 -1
  121. package/build/conformance/index.js +1 -0
  122. package/build/conformance/index.js.map +1 -1
  123. package/build/definitions.d.ts.map +1 -1
  124. package/build/definitions.js +9 -9
  125. package/build/definitions.js.map +1 -1
  126. package/build/executor/changes/types.d.ts +36 -0
  127. package/build/executor/changes/types.d.ts.map +1 -0
  128. package/build/executor/changes/types.js +2 -0
  129. package/build/executor/changes/types.js.map +1 -0
  130. package/build/executor/changes.d.ts +3 -30
  131. package/build/executor/changes.d.ts.map +1 -1
  132. package/build/executor/changes.js +42 -51
  133. package/build/executor/changes.js.map +1 -1
  134. package/build/executor/code/types.d.ts +17 -0
  135. package/build/executor/code/types.d.ts.map +1 -0
  136. package/build/executor/code/types.js +2 -0
  137. package/build/executor/code/types.js.map +1 -0
  138. package/build/executor/code.d.ts +3 -14
  139. package/build/executor/code.d.ts.map +1 -1
  140. package/build/executor/code.js +50 -54
  141. package/build/executor/code.js.map +1 -1
  142. package/build/executor/creator/types.d.ts +33 -0
  143. package/build/executor/creator/types.d.ts.map +1 -0
  144. package/build/executor/creator/types.js +2 -0
  145. package/build/executor/creator/types.js.map +1 -0
  146. package/build/executor/creator.d.ts +6 -25
  147. package/build/executor/creator.d.ts.map +1 -1
  148. package/build/executor/creator.js +26 -41
  149. package/build/executor/creator.js.map +1 -1
  150. package/build/executor/resolve/types.d.ts +30 -0
  151. package/build/executor/resolve/types.d.ts.map +1 -0
  152. package/build/executor/resolve/types.js +2 -0
  153. package/build/executor/resolve/types.js.map +1 -0
  154. package/build/executor/resolve.d.ts +4 -41
  155. package/build/executor/resolve.d.ts.map +1 -1
  156. package/build/executor/resolve.js +142 -157
  157. package/build/executor/resolve.js.map +1 -1
  158. package/build/executor/types.d.ts +0 -2
  159. package/build/executor/types.d.ts.map +1 -1
  160. package/build/executor/validate.d.ts +1 -1
  161. package/build/executor/validate.d.ts.map +1 -1
  162. package/build/executor/validate.js +16 -15
  163. package/build/executor/validate.js.map +1 -1
  164. package/build/executor.d.ts +4 -3
  165. package/build/executor.d.ts.map +1 -1
  166. package/build/executor.js +18 -17
  167. package/build/executor.js.map +1 -1
  168. package/build/facade.d.ts.map +1 -1
  169. package/build/facade.js +16 -16
  170. package/build/facade.js.map +1 -1
  171. package/build/helper.d.ts +1 -6
  172. package/build/helper.d.ts.map +1 -1
  173. package/build/helper.js +3 -8
  174. package/build/helper.js.map +1 -1
  175. package/build/projection.d.ts +2 -0
  176. package/build/projection.d.ts.map +1 -1
  177. package/build/projection.js +6 -4
  178. package/build/projection.js.map +1 -1
  179. package/build/queue/declare.d.ts +5 -0
  180. package/build/queue/declare.d.ts.map +1 -0
  181. package/build/queue/declare.js +5 -0
  182. package/build/queue/declare.js.map +1 -0
  183. package/build/queue/index.d.ts +1 -4
  184. package/build/queue/index.d.ts.map +1 -1
  185. package/build/queue/index.js +1 -4
  186. package/build/queue/index.js.map +1 -1
  187. package/build/store/composite.d.ts.map +1 -1
  188. package/build/store/composite.js +2 -2
  189. package/build/store/composite.js.map +1 -1
  190. package/build/store/fold/types.d.ts +43 -0
  191. package/build/store/fold/types.d.ts.map +1 -0
  192. package/build/store/fold/types.js +2 -0
  193. package/build/store/fold/types.js.map +1 -0
  194. package/build/store/fold.d.ts +7 -30
  195. package/build/store/fold.d.ts.map +1 -1
  196. package/build/store/fold.js +185 -203
  197. package/build/store/fold.js.map +1 -1
  198. package/build/store/index.d.ts +1 -0
  199. package/build/store/index.d.ts.map +1 -1
  200. package/build/store/index.js.map +1 -1
  201. package/build/store/memory.d.ts.map +1 -1
  202. package/build/store/memory.js +0 -0
  203. package/build/store/memory.js.map +1 -1
  204. package/build/store/types.local.d.ts +3 -1
  205. package/build/store/types.local.d.ts.map +1 -1
  206. package/build/types.d.ts +2 -1
  207. package/build/types.d.ts.map +1 -1
  208. package/build/utils/access/types.d.ts +30 -0
  209. package/build/utils/access/types.d.ts.map +1 -0
  210. package/build/utils/access/types.js +2 -0
  211. package/build/utils/access/types.js.map +1 -0
  212. package/build/utils/access.d.ts +4 -0
  213. package/build/utils/access.d.ts.map +1 -0
  214. package/build/utils/access.js +28 -0
  215. package/build/utils/access.js.map +1 -0
  216. package/build/utils/guard/types.d.ts +21 -0
  217. package/build/utils/guard/types.d.ts.map +1 -0
  218. package/build/utils/guard/types.js +2 -0
  219. package/build/utils/guard/types.js.map +1 -0
  220. package/build/utils/guard.d.ts +7 -10
  221. package/build/utils/guard.d.ts.map +1 -1
  222. package/build/utils/guard.js +33 -31
  223. package/build/utils/guard.js.map +1 -1
  224. package/build/utils/handler/types.d.ts +26 -0
  225. package/build/utils/handler/types.d.ts.map +1 -0
  226. package/build/utils/handler/types.js +2 -0
  227. package/build/utils/handler/types.js.map +1 -0
  228. package/build/utils/handler.d.ts +6 -0
  229. package/build/utils/handler.d.ts.map +1 -0
  230. package/build/utils/handler.js +21 -0
  231. package/build/utils/handler.js.map +1 -0
  232. package/build/utils/index.d.ts +8 -1
  233. package/build/utils/index.d.ts.map +1 -1
  234. package/build/utils/index.js +3 -0
  235. package/build/utils/index.js.map +1 -1
  236. package/build/utils/request/types.d.ts +23 -0
  237. package/build/utils/request/types.d.ts.map +1 -0
  238. package/build/utils/request/types.js +2 -0
  239. package/build/utils/request/types.js.map +1 -0
  240. package/build/utils/request.d.ts +4 -0
  241. package/build/utils/request.d.ts.map +1 -0
  242. package/build/utils/request.js +27 -0
  243. package/build/utils/request.js.map +1 -0
  244. package/build/utils/scope.d.ts +10 -43
  245. package/build/utils/scope.d.ts.map +1 -1
  246. package/build/utils/scope.js +21 -99
  247. package/build/utils/scope.js.map +1 -1
  248. package/package.json +18 -18
  249. package/src/actions/apply-schema-request.ts +35 -0
  250. package/src/actions/commit.ts +7 -5
  251. package/src/actions/define-schemas.ts +34 -0
  252. package/src/actions/execute.ts +30 -115
  253. package/src/actions/execution/types.ts +34 -0
  254. package/src/actions/execution.ts +96 -0
  255. package/src/actions/get-card.ts +14 -0
  256. package/src/actions/get-specification.ts +14 -0
  257. package/src/actions/index.ts +13 -3
  258. package/src/actions/links.ts +8 -6
  259. package/src/actions/list-card-specifications.ts +15 -0
  260. package/src/actions/list-card-transitions.ts +15 -0
  261. package/src/actions/list-cards.ts +14 -0
  262. package/src/actions/list-schemas.ts +29 -0
  263. package/src/actions/{specs.ts → list-specification-revisions.ts} +4 -11
  264. package/src/actions/summarize-cards.ts +15 -0
  265. package/src/actions/transitions.ts +6 -4
  266. package/src/actions/types.ts +4 -3
  267. package/src/actions/watch.ts +4 -3
  268. package/src/conformance/assert/types.ts +16 -0
  269. package/src/conformance/assert.ts +49 -41
  270. package/src/conformance/cases.ts +88 -67
  271. package/src/conformance/clock.ts +11 -0
  272. package/src/conformance/consts.ts +87 -1
  273. package/src/conformance/errors.ts +7 -0
  274. package/src/conformance/fixtures/types.ts +11 -0
  275. package/src/conformance/fixtures.ts +32 -107
  276. package/src/conformance/index.ts +3 -0
  277. package/src/definitions.ts +9 -9
  278. package/src/executor/changes/types.ts +38 -0
  279. package/src/executor/changes.ts +48 -63
  280. package/src/executor/code/types.ts +23 -0
  281. package/src/executor/code.ts +60 -61
  282. package/src/executor/creator/types.ts +33 -0
  283. package/src/executor/creator.ts +33 -41
  284. package/src/executor/resolve/types.ts +30 -0
  285. package/src/executor/resolve.ts +153 -167
  286. package/src/executor/types.ts +0 -3
  287. package/src/executor/validate.ts +17 -16
  288. package/src/executor.ts +20 -17
  289. package/src/facade.ts +16 -16
  290. package/src/helper.ts +3 -8
  291. package/src/projection.ts +8 -4
  292. package/src/queue/declare.ts +7 -0
  293. package/src/queue/index.ts +1 -8
  294. package/src/store/composite.ts +2 -2
  295. package/src/store/fold/types.ts +44 -0
  296. package/src/store/fold.ts +209 -212
  297. package/src/store/index.ts +1 -0
  298. package/src/store/memory.ts +0 -0
  299. package/src/store/types.local.ts +1 -1
  300. package/src/types.ts +1 -1
  301. package/src/utils/access/types.ts +30 -0
  302. package/src/utils/access.ts +33 -0
  303. package/src/utils/guard/types.ts +19 -0
  304. package/src/utils/guard.ts +41 -30
  305. package/src/utils/handler/types.ts +28 -0
  306. package/src/utils/handler.ts +35 -0
  307. package/src/utils/index.ts +9 -1
  308. package/src/utils/request/types.ts +23 -0
  309. package/src/utils/request.ts +40 -0
  310. package/src/utils/scope.ts +25 -108
  311. package/tests/creator.spec.ts +18 -18
  312. package/src/actions/cards.ts +0 -48
  313. package/src/actions/schemas.ts +0 -87
package/src/store/fold.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { logger } from '@owlmeans/log'
2
- import { applyRelationship, applyTransition, CommitState, isProject, PlanningUnsupported, TransitionAction, type CommitEvent, type PlanningStore, type Relationship, type SpecificationRevision, type Transition, type Workcard } from '@owlmeans/planning'
2
+ import { applyHelper, cardHelper, CommitState, PlanningUnsupported, TransitionAction, type CommitEvent, type PlanningStore, type Relationship, type SpecificationRevision, type Transition, type Workcard } from '@owlmeans/planning'
3
+ import type { FoldHelper } from './fold/types.js'
3
4
  import type { FoldOptions, FoldResult } from './types.js'
4
5
  import { CONTENT_KEYS } from './consts.local.js'
5
6
 
@@ -13,248 +14,244 @@ const clean = <T extends object>(record: T): T =>
13
14
  const errorText = (error: unknown): string =>
14
15
  error instanceof Error ? error.message : `${error}`
15
16
 
16
- const safely = async (label: string, run: () => Promise<void> | void | undefined): Promise<void> => {
17
- try {
18
- await run()
19
- } catch (error) {
20
- log.error('Planning store step failed', { step: label, error })
17
+ export const createFoldHelper = (): FoldHelper => {
18
+ const safely = async (label: string, run: () => Promise<void> | void | undefined): Promise<void> => {
19
+ try {
20
+ await run()
21
+ } catch (error) {
22
+ log.error('Planning store step failed', { step: label, error })
23
+ }
21
24
  }
22
- }
23
25
 
24
- /** The event a settled transition publishes. `record` is in-process only — strip it before a bus. */
25
- export const commitEventOf = (
26
- transition: Transition, state: CommitState, at: string, extra?: { error?: string, record?: Workcard | null }
27
- ): CommitEvent => clean({
28
- transition: transition.id!,
29
- card: transition.card,
30
- entityId: transition.entityId,
31
- project: transition.project,
32
- kind: transition.kind,
33
- type: transition.type,
34
- seq: transition.seq,
35
- action: transition.action,
36
- state,
37
- at,
38
- error: extra?.error,
39
- record: extra?.record,
40
- })
26
+ const commitEventOf = (
27
+ transition: Transition, state: CommitState, at: string, extra?: { error?: string, record?: Workcard | null }
28
+ ): CommitEvent => clean({
29
+ transition: transition.id!,
30
+ card: transition.card,
31
+ entityId: transition.entityId,
32
+ project: transition.project,
33
+ kind: transition.kind,
34
+ type: transition.type,
35
+ seq: transition.seq,
36
+ action: transition.action,
37
+ state,
38
+ at,
39
+ error: extra?.error,
40
+ record: extra?.record,
41
+ })
41
42
 
42
- const bearsRelationships = (transition: Transition): boolean =>
43
- transition.action === TransitionAction.Link
44
- || transition.action === TransitionAction.Unlink
45
- || transition.action === TransitionAction.Delete
46
- || (transition.action === TransitionAction.Create && (transition.links?.length ?? 0) > 0)
43
+ const bearsRelationships = (transition: Transition): boolean =>
44
+ transition.action === TransitionAction.Link
45
+ || transition.action === TransitionAction.Unlink
46
+ || transition.action === TransitionAction.Delete
47
+ || (transition.action === TransitionAction.Create && (transition.links?.length ?? 0) > 0)
47
48
 
48
- const foldRelationships = async (store: PlanningStore, transition: Transition): Promise<void> => {
49
- const links = store.links
50
- if (links == null || !bearsRelationships(transition)) {
51
- return
52
- }
53
- const where = { entityId: transition.entityId }
54
- const [outgoing, incoming] = await Promise.all([
55
- links.list({ ...where, from: transition.card }, { size: 0 }),
56
- links.list({ ...where, to: transition.card }, { size: 0 }),
57
- ])
58
- const before = [...new Map([...outgoing.items, ...incoming.items].map(link => [link.id, link])).values()]
59
- const after = applyRelationship(before, transition)
60
- const kept = new Set(after.filter(link => link.id != null).map(link => link.id))
49
+ const foldRelationships = async (store: PlanningStore, transition: Transition): Promise<void> => {
50
+ const links = store.links
51
+ if (links == null || !bearsRelationships(transition)) {
52
+ return
53
+ }
54
+ const where = { entityId: transition.entityId }
55
+ const [outgoing, incoming] = await Promise.all([
56
+ links.list({ ...where, from: transition.card }, { size: 0 }),
57
+ links.list({ ...where, to: transition.card }, { size: 0 }),
58
+ ])
59
+ const before = [...new Map([...outgoing.items, ...incoming.items].map(link => [link.id, link])).values()]
60
+ const after = applyHelper.applyRelationship(before, transition)
61
+ const kept = new Set(after.filter(link => link.id != null).map(link => link.id))
61
62
 
62
- for (const link of before) {
63
- if (link.id != null && !kept.has(link.id)) {
64
- await links.drop({ entityId: transition.entityId, id: link.id })
63
+ for (const link of before) {
64
+ if (link.id != null && !kept.has(link.id)) {
65
+ await links.drop({ entityId: transition.entityId, id: link.id })
66
+ }
65
67
  }
66
- }
67
- for (const link of after) {
68
- if (link.id == null) {
69
- await links.put(link as Relationship)
68
+ for (const link of after) {
69
+ if (link.id == null) {
70
+ await links.put(link as Relationship)
71
+ }
70
72
  }
71
73
  }
72
- }
73
74
 
74
- const writeFold = async (
75
- store: PlanningStore, before: Workcard | null, after: Workcard | null, transition: Transition
76
- ): Promise<void> => {
77
- if (transition.action === TransitionAction.Delete) {
78
- if (before != null && isProject(before)) {
79
- // A project takes everything under it, its own log included — the hub remembers the commit.
80
- await store.cards.purge(transition.card, transition.entityId)
75
+ const writeFold = async (
76
+ store: PlanningStore, before: Workcard | null, after: Workcard | null, transition: Transition
77
+ ): Promise<void> => {
78
+ if (transition.action === TransitionAction.Delete) {
79
+ if (before != null && cardHelper.isProject(before)) {
80
+ // A project takes everything under it, its own log included — the hub remembers the commit.
81
+ await store.cards.purge(transition.card, transition.entityId)
82
+ return
83
+ }
84
+ await foldRelationships(store, transition)
85
+ await store.cards.drop(transition.card, transition.entityId)
81
86
  return
82
87
  }
88
+ if (after != null) {
89
+ await store.cards.put(after)
90
+ }
83
91
  await foldRelationships(store, transition)
84
- await store.cards.drop(transition.card, transition.entityId)
85
- return
86
92
  }
87
- if (after != null) {
88
- await store.cards.put(after)
89
- }
90
- await foldRelationships(store, transition)
91
- }
92
93
 
93
- /**
94
- * Fold every pending transition of one card — the projection body every store reuses.
95
- *
96
- * Transitions are read in `seq` order past the card's own `seq`, and only `pending` ones are
97
- * applied, each with `applyTransition` and nothing else. Per transition: write the record (a
98
- * `delete` drops it, a project `delete` purges everything under it), fold its relationships, mark
99
- * it `committed`, `publish` the event, then run `onCommitted` — so the `after` chain runs in THIS
100
- * process, exactly once per committed transition. A transition that cannot be folded is marked
101
- * `failed` with the reason and a failed event is published, and the fold goes PAST it: the card's
102
- * `seq` advances to the failed transition with every other value as it was, so the next
103
- * transition applies normally. Nothing is removed from the log. A create that fails leaves no card
104
- * to advance, so what follows it fails too; a store that refuses the advancing write itself stops
105
- * the fold there and reports `followUp`, leaving the rest pending for a retry.
106
- *
107
- * The caller owns single flight: two folds of one card running at once would both publish. A
108
- * transactional store passes `unit`, which wraps each transition's writes (a savepoint), so a write
109
- * the database refuses is undone whole before the fold marks the transition failed.
110
- *
111
- * @throws {PlanningUnsupported} for a store without a transition log
112
- */
113
- export const foldPending = async (
114
- store: PlanningStore, cardId: string, entityId: string, opts: FoldOptions = {}
115
- ): Promise<FoldResult> => {
116
- const transitions = store.transitions
117
- if (transitions == null) {
118
- throw new PlanningUnsupported(`fold:${store.alias ?? 'store'}`)
119
- }
120
- const now = opts.now ?? isoNow
121
-
122
- let card = await store.cards.get(cardId, entityId)
123
- const listed = await transitions.list(
124
- { entityId, card: cardId, sinceSeq: card?.seq ?? 0, state: CommitState.Pending },
125
- { sort: ['seq'], size: opts.limit ?? 0 }
126
- )
127
-
128
- let folded = 0
129
- let failed = 0
130
- for (const transition of listed.items) {
131
- if (transition.commit.state !== CommitState.Pending || transition.id == null) {
132
- continue
94
+ const foldPending = async (
95
+ store: PlanningStore, cardId: string, entityId: string, opts: FoldOptions = {}
96
+ ): Promise<FoldResult> => {
97
+ const transitions = store.transitions
98
+ if (transitions == null) {
99
+ throw new PlanningUnsupported(`fold:${store.alias ?? 'store'}`)
133
100
  }
134
- await opts.touch?.()
135
- const at = now()
101
+ const now = opts.now ?? isoNow
136
102
 
137
- let next: Workcard | null
138
- try {
139
- next = applyTransition(card, transition)
140
- if (card != null && next === card) {
141
- // Already applied by another folder, which owns its commit and its event.
103
+ let card = await store.cards.get(cardId, entityId)
104
+ const listed = await transitions.list(
105
+ { entityId, card: cardId, sinceSeq: card?.seq ?? 0, state: CommitState.Pending },
106
+ { sort: ['seq'], size: opts.limit ?? 0 }
107
+ )
108
+
109
+ let folded = 0
110
+ let failed = 0
111
+ for (const transition of listed.items) {
112
+ if (transition.commit.state !== CommitState.Pending || transition.id == null) {
142
113
  continue
143
114
  }
144
- const before = card
145
- const after = next
146
- await (opts.unit != null
147
- ? opts.unit(async () => { await writeFold(store, before, after, transition) })
148
- : writeFold(store, before, after, transition))
149
- } catch (error) {
150
- failed++
151
- const message = errorText(error)
152
- if (card != null && transition.seq > card.seq) {
153
- // Fold PAST it: the record keeps its previous values and only its cursor moves, so the
154
- // next transition still applies. The card is re-written whole, which also undoes a write
155
- // that got half way before it threw.
156
- const skipped: Workcard = { ...card, seq: transition.seq, head: Math.max(card.head ?? card.seq, transition.seq) }
157
- try {
158
- await store.cards.put(skipped)
159
- card = skipped
160
- } catch (advance) {
161
- // The store itself refuses writes: leave the rest pending for a retry rather than
162
- // failing every later transition as out of order.
163
- log.error('Planning cannot advance a card past a failed transition', { cardId, seq: transition.seq, error: advance })
164
- await safely('commit failure', () => transitions.commit(transition.id!, { state: CommitState.Failed, at, error: message }))
165
- await safely('publish', () => opts.publish?.(commitEventOf(transition, CommitState.Failed, at, { error: message })))
166
- return { card, folded, failed, followUp: true }
115
+ await opts.touch?.()
116
+ const at = now()
117
+
118
+ let next: Workcard | null
119
+ try {
120
+ next = applyHelper.applyTransition(card, transition)
121
+ if (card != null && next === card) {
122
+ // Already applied by another folder, which owns its commit and its event.
123
+ continue
124
+ }
125
+ const before = card
126
+ const after = next
127
+ await (opts.unit != null
128
+ ? opts.unit(async () => { await writeFold(store, before, after, transition) })
129
+ : writeFold(store, before, after, transition))
130
+ } catch (error) {
131
+ failed++
132
+ const message = errorText(error)
133
+ if (card != null && transition.seq > card.seq) {
134
+ // Fold PAST it: the record keeps its previous values and only its cursor moves, so the
135
+ // next transition still applies. The card is re-written whole, which also undoes a write
136
+ // that got half way before it threw.
137
+ const skipped: Workcard = { ...card, seq: transition.seq, head: Math.max(card.head ?? card.seq, transition.seq) }
138
+ try {
139
+ await store.cards.put(skipped)
140
+ card = skipped
141
+ } catch (advance) {
142
+ // The store itself refuses writes: leave the rest pending for a retry rather than
143
+ // failing every later transition as out of order.
144
+ log.error('Planning cannot advance a card past a failed transition', { cardId, seq: transition.seq, error: advance })
145
+ await safely('commit failure', () => transitions.commit(transition.id!, { state: CommitState.Failed, at, error: message }))
146
+ await safely('publish', () => opts.publish?.(commitEventOf(transition, CommitState.Failed, at, { error: message })))
147
+ return { card, folded, failed, followUp: true }
148
+ }
167
149
  }
150
+ await safely('commit failure', () => transitions.commit(transition.id!, { state: CommitState.Failed, at, error: message }))
151
+ const event = commitEventOf(transition, CommitState.Failed, at, { error: message })
152
+ await safely('publish', () => opts.publish?.(event))
153
+ continue
168
154
  }
169
- await safely('commit failure', () => transitions.commit(transition.id!, { state: CommitState.Failed, at, error: message }))
170
- const event = commitEventOf(transition, CommitState.Failed, at, { error: message })
155
+
156
+ card = next
157
+ folded++
158
+ await safely('commit', () => transitions.commit(transition.id!, { state: CommitState.Committed, at }))
159
+ const event = commitEventOf(transition, CommitState.Committed, at, { record: next })
171
160
  await safely('publish', () => opts.publish?.(event))
172
- continue
161
+ await safely('onCommitted', () => opts.onCommitted?.(event))
173
162
  }
174
163
 
175
- card = next
176
- folded++
177
- await safely('commit', () => transitions.commit(transition.id!, { state: CommitState.Committed, at }))
178
- const event = commitEventOf(transition, CommitState.Committed, at, { record: next })
179
- await safely('publish', () => opts.publish?.(event))
180
- await safely('onCommitted', () => opts.onCommitted?.(event))
164
+ return { card, folded, failed, followUp: listed.total > listed.items.length }
181
165
  }
182
166
 
183
- return { card, folded, failed, followUp: listed.total > listed.items.length }
184
- }
185
-
186
- /**
187
- * Mark every pending transition of a card `failed` and publish a failed event for each — what a
188
- * queued store does when its projection job is dead, or waiters hang until their timeout.
189
- */
190
- export const failPending = async (
191
- store: PlanningStore, cardId: string, entityId: string, reason: string,
192
- opts: Pick<FoldOptions, 'publish' | 'now'> = {}
193
- ): Promise<number> => {
194
- const transitions = store.transitions
195
- if (transitions == null) {
196
- return 0
197
- }
198
- const at = (opts.now ?? isoNow)()
199
- const pending = await transitions.list({ entityId, card: cardId, state: CommitState.Pending }, { sort: ['seq'], size: 0 })
200
- for (const transition of pending.items) {
201
- if (transition.id == null) {
202
- continue
167
+ const failPending = async (
168
+ store: PlanningStore, cardId: string, entityId: string, reason: string,
169
+ opts: Pick<FoldOptions, 'publish' | 'now'> = {}
170
+ ): Promise<number> => {
171
+ const transitions = store.transitions
172
+ if (transitions == null) {
173
+ return 0
174
+ }
175
+ const at = (opts.now ?? isoNow)()
176
+ const pending = await transitions.list({ entityId, card: cardId, state: CommitState.Pending }, { sort: ['seq'], size: 0 })
177
+ for (const transition of pending.items) {
178
+ if (transition.id == null) {
179
+ continue
180
+ }
181
+ await transitions.commit(transition.id, { state: CommitState.Failed, at, error: reason })
182
+ const event = commitEventOf(transition, CommitState.Failed, at, { error: reason })
183
+ await safely('publish', () => opts.publish?.(event))
203
184
  }
204
- await transitions.commit(transition.id, { state: CommitState.Failed, at, error: reason })
205
- const event = commitEventOf(transition, CommitState.Failed, at, { error: reason })
206
- await safely('publish', () => opts.publish?.(event))
207
- }
208
185
 
209
- return pending.items.length
210
- }
186
+ return pending.items.length
187
+ }
211
188
 
212
- /**
213
- * A specification's history, replayed from its log: one entry per create and per committed
214
- * content change, newest first. The fold is `applyTransition` itself, so an entry's `body` is the
215
- * body AT that revision even when the transition changed only `ref`.
216
- */
217
- export const revisionsFromLog = (log: Transition[], limit?: number): SpecificationRevision[] => {
218
- const ordered = log.filter(transition => transition.commit.state !== CommitState.Pending)
219
- .sort((left, right) => left.seq - right.seq)
189
+ const revisionsFromLog = (log: Transition[], limit?: number): SpecificationRevision[] => {
190
+ const ordered = log.filter(transition => transition.commit.state !== CommitState.Pending)
191
+ .sort((left, right) => left.seq - right.seq)
220
192
 
221
- const entries: SpecificationRevision[] = []
222
- let state: Workcard | null = null
223
- for (const transition of ordered) {
224
- if (transition.commit.state === CommitState.Failed) {
225
- // The fold went past it; so does the replay.
226
- if (state != null && transition.seq > state.seq) {
227
- state = { ...(state as Workcard), seq: transition.seq }
193
+ const entries: SpecificationRevision[] = []
194
+ let state: Workcard | null = null
195
+ for (const transition of ordered) {
196
+ if (transition.commit.state === CommitState.Failed) {
197
+ // The fold went past it; so does the replay.
198
+ if (state != null && transition.seq > state.seq) {
199
+ state = { ...(state as Workcard), seq: transition.seq }
200
+ }
201
+ continue
228
202
  }
229
- continue
230
- }
231
- try {
232
- state = applyTransition(state, transition)
233
- } catch {
234
- break
235
- }
236
- if (state == null) {
237
- break
238
- }
239
- const changed = transition.action === TransitionAction.Create
240
- || CONTENT_KEYS.some(key => (transition.changes as Record<string, unknown>)[key] !== undefined
241
- || transition.unset?.includes(key) === true)
242
- if (!changed) {
243
- continue
203
+ try {
204
+ state = applyHelper.applyTransition(state, transition)
205
+ } catch {
206
+ break
207
+ }
208
+ if (state == null) {
209
+ break
210
+ }
211
+ const changed = transition.action === TransitionAction.Create
212
+ || CONTENT_KEYS.some(key => (transition.changes as Record<string, unknown>)[key] !== undefined
213
+ || transition.unset?.includes(key) === true)
214
+ if (!changed) {
215
+ continue
216
+ }
217
+ const spec = state as Workcard & { body?: string, ref?: string, bodyChars?: number, version?: number, revision?: number }
218
+ entries.push(clean({
219
+ revision: spec.revision ?? entries.length + 1,
220
+ body: spec.body,
221
+ ref: spec.ref,
222
+ bodyChars: spec.bodyChars,
223
+ version: spec.version,
224
+ at: transition.at,
225
+ by: transition.actor,
226
+ transition: transition.id!,
227
+ }))
244
228
  }
245
- const spec = state as Workcard & { body?: string, ref?: string, bodyChars?: number, version?: number, revision?: number }
246
- entries.push(clean({
247
- revision: spec.revision ?? entries.length + 1,
248
- body: spec.body,
249
- ref: spec.ref,
250
- bodyChars: spec.bodyChars,
251
- version: spec.version,
252
- at: transition.at,
253
- by: transition.actor,
254
- transition: transition.id!,
255
- }))
229
+
230
+ const newest = entries.reverse()
231
+ return limit != null && limit > 0 ? newest.slice(0, limit) : newest
256
232
  }
257
233
 
258
- const newest = entries.reverse()
259
- return limit != null && limit > 0 ? newest.slice(0, limit) : newest
234
+ return { commitEventOf, foldPending, failPending, revisionsFromLog }
260
235
  }
236
+
237
+ export const foldHelper = createFoldHelper()
238
+
239
+ /** @deprecated compat:factory-refactor — use `foldHelper.commitEventOf(…)` */
240
+ export const commitEventOf = (
241
+ transition: Transition, state: CommitState, at: string, extra?: { error?: string, record?: Workcard | null }
242
+ ): CommitEvent => foldHelper.commitEventOf(transition, state, at, extra)
243
+
244
+ /** @deprecated compat:factory-refactor — use `foldHelper.foldPending(…)` */
245
+ export const foldPending = async (
246
+ store: PlanningStore, cardId: string, entityId: string, opts: FoldOptions = {}
247
+ ): Promise<FoldResult> => await foldHelper.foldPending(store, cardId, entityId, opts)
248
+
249
+ /** @deprecated compat:factory-refactor — use `foldHelper.failPending(…)` */
250
+ export const failPending = async (
251
+ store: PlanningStore, cardId: string, entityId: string, reason: string,
252
+ opts: Pick<FoldOptions, 'publish' | 'now'> = {}
253
+ ): Promise<number> => await foldHelper.failPending(store, cardId, entityId, reason, opts)
254
+
255
+ /** @deprecated compat:factory-refactor — use `foldHelper.revisionsFromLog(…)` */
256
+ export const revisionsFromLog = (log: Transition[], limit?: number): SpecificationRevision[] =>
257
+ foldHelper.revisionsFromLog(log, limit)
@@ -1,4 +1,5 @@
1
1
  export type * from './types.js'
2
+ export type * from './fold/types.js'
2
3
 
3
4
  export * from './commits.js'
4
5
  export * from './composite.js'
Binary file
@@ -1,3 +1,3 @@
1
1
  import type { CommitEvent } from '@owlmeans/planning'
2
2
 
3
- export type Listener = (event: CommitEvent) => void | Promise<void>
3
+ export interface Listener { (event: CommitEvent): void | Promise<void> }
package/src/types.ts CHANGED
@@ -146,7 +146,7 @@ export interface PluginRegistry {
146
146
  version: () => number
147
147
  }
148
148
 
149
- export type PlanningServiceApi = Pick<PlanningService, 'use' | 'plugins' | 'schemas' | 'store' | 'for' | 'committed'>
149
+ export interface PlanningServiceApi extends Pick<PlanningService, 'use' | 'plugins' | 'schemas' | 'store' | 'for' | 'committed'> {}
150
150
 
151
151
  /** What the facade and the executor reach the service through. */
152
152
  export interface PlanningRuntime {
@@ -0,0 +1,30 @@
1
+ import type { Workcard } from '@owlmeans/planning'
2
+ import type { PlanningAccess, PlanningAccessGrants } from '../../types.js'
3
+
4
+ /** The write rules of one resolved {@link PlanningAccess} — no access at all gates nothing. */
5
+ export interface PlanningAccessModel {
6
+ readonly record: PlanningAccess | undefined
7
+ /**
8
+ * Refuse a write the access does not grant. No access, or access without `grants`, gates nothing;
9
+ * a `grants` object refuses every flag it leaves out. `true` grants everything; a list grants the
10
+ * project ids it names — `target` is the project the write is about (`undefined` for the
11
+ * organization's root, which only `true` reaches).
12
+ *
13
+ * @throws {PlanningForbidden}
14
+ */
15
+ assertGranted: (grant: keyof PlanningAccessGrants, target?: string) => void
16
+ /**
17
+ * A card the access's `writes` admits — the rule a narrowed read admits, over `writes`: a project
18
+ * it names, or a card whose `parents` name one. A specification is admitted through its parent card
19
+ * by the caller. No `writes` admits everything.
20
+ */
21
+ writableIn: (card: Pick<Workcard, 'id' | 'parents'>) => boolean
22
+ /**
23
+ * Refuse a write into a project the access's `writes` leaves out. No `writes` refuses nothing;
24
+ * `target` is the project the write is in (`undefined` for the organization's root, which a set
25
+ * `writes` never admits).
26
+ *
27
+ * @throws {PlanningForbidden}
28
+ */
29
+ assertWrites: (target?: string) => void
30
+ }
@@ -0,0 +1,33 @@
1
+ import { PlanningForbidden, type Workcard } from '@owlmeans/planning'
2
+ import type { PlanningAccess, PlanningAccessGrants } from '../types.js'
3
+ import type { PlanningAccessModel } from './access/types.js'
4
+
5
+ export const makePlanningAccessModel = (access: PlanningAccess | undefined): PlanningAccessModel => {
6
+ const assertGranted = (grant: keyof PlanningAccessGrants, target?: string): void => {
7
+ if (access?.grants == null) {
8
+ return
9
+ }
10
+ const granted = access.grants[grant]
11
+ if (granted === true || (Array.isArray(granted) && target != null && granted.includes(target))) {
12
+ return
13
+ }
14
+ throw new PlanningForbidden(`${grant}:${target ?? 'root'}`)
15
+ }
16
+
17
+ const writableIn = (card: Pick<Workcard, 'id' | 'parents'>): boolean => {
18
+ const writes = access?.writes
19
+ if (writes == null) {
20
+ return true
21
+ }
22
+ return (card.id != null && writes.includes(card.id)) || card.parents.some(parent => writes.includes(parent))
23
+ }
24
+
25
+ const assertWrites = (target?: string): void => {
26
+ if (access?.writes == null || (target != null && access.writes.includes(target))) {
27
+ return
28
+ }
29
+ throw new PlanningForbidden(`writes:${target ?? 'root'}`)
30
+ }
31
+
32
+ return { record: access, assertGranted, writableIn, assertWrites }
33
+ }
@@ -0,0 +1,19 @@
1
+ import type { WorkcardNotFound, Workcard } from '@owlmeans/planning'
2
+
3
+ /** What every handler answers with when a record is not the caller's: absence, never a refusal. */
4
+ export interface GuardHelper {
5
+ notFoundOf: (id: string) => WorkcardNotFound
6
+ /**
7
+ * A record of another entity is absent, not forbidden.
8
+ *
9
+ * @throws {WorkcardNotFound}
10
+ */
11
+ assertScope: <T extends Pick<Workcard, 'entityId'>>(record: T | null | undefined, scope: { entityId: string }, id: string) => T
12
+ /**
13
+ * Run a handler body so that a scope mismatch leaves as `WorkcardNotFound` — telling "not yours"
14
+ * apart from "does not exist" is what turns an id space into an enumeration oracle.
15
+ */
16
+ concealed: <T>(run: () => Promise<T>) => Promise<T>
17
+ /** Seconds as the wire sends them, clamped to `[0, max]`. */
18
+ clampSeconds: (value: unknown, max: number) => number
19
+ }