@owlmeans/server-planning 0.1.18-rc.8 → 0.1.18

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 (438) hide show
  1. package/README.md +92 -19
  2. package/agent-meta/manifest.json +105 -4
  3. package/agent-meta/skills/server-planning/SKILL.md +89 -138
  4. package/agent-meta/skills/server-planning/parts/auxiliary.md +54 -0
  5. package/agent-meta/skills/server-planning/parts/executor.md +82 -0
  6. package/agent-meta/skills/server-planning/parts/schemas.md +26 -0
  7. package/agent-meta/skills/server-planning/parts/serving.md +88 -0
  8. package/agent-meta/skills/server-planning/parts/stores.md +91 -0
  9. package/build/actions/apply-schema-request.d.ts +8 -0
  10. package/build/actions/apply-schema-request.d.ts.map +1 -0
  11. package/build/actions/apply-schema-request.js +33 -0
  12. package/build/actions/apply-schema-request.js.map +1 -0
  13. package/build/actions/commit.d.ts +3 -3
  14. package/build/actions/commit.d.ts.map +1 -1
  15. package/build/actions/commit.js +5 -4
  16. package/build/actions/commit.js.map +1 -1
  17. package/build/actions/define-schemas.d.ts +13 -0
  18. package/build/actions/define-schemas.d.ts.map +1 -0
  19. package/build/actions/define-schemas.js +28 -0
  20. package/build/actions/define-schemas.js.map +1 -0
  21. package/build/actions/execute.d.ts +11 -10
  22. package/build/actions/execute.d.ts.map +1 -1
  23. package/build/actions/execute.js +17 -27
  24. package/build/actions/execute.js.map +1 -1
  25. package/build/actions/execution/types.d.ts +34 -0
  26. package/build/actions/execution/types.d.ts.map +1 -0
  27. package/build/actions/execution/types.js +2 -0
  28. package/build/actions/execution/types.js.map +1 -0
  29. package/build/actions/execution.d.ts +4 -0
  30. package/build/actions/execution.d.ts.map +1 -0
  31. package/build/actions/execution.js +88 -0
  32. package/build/actions/execution.js.map +1 -0
  33. package/build/actions/get-card.d.ts +6 -0
  34. package/build/actions/get-card.d.ts.map +1 -0
  35. package/build/actions/get-card.js +9 -0
  36. package/build/actions/get-card.js.map +1 -0
  37. package/build/actions/get-specification.d.ts +6 -0
  38. package/build/actions/get-specification.d.ts.map +1 -0
  39. package/build/actions/get-specification.js +9 -0
  40. package/build/actions/get-specification.js.map +1 -0
  41. package/build/actions/index.d.ts +12 -3
  42. package/build/actions/index.d.ts.map +1 -1
  43. package/build/actions/index.js +11 -3
  44. package/build/actions/index.js.map +1 -1
  45. package/build/actions/links.d.ts +3 -3
  46. package/build/actions/links.d.ts.map +1 -1
  47. package/build/actions/links.js +6 -5
  48. package/build/actions/links.js.map +1 -1
  49. package/build/actions/list-card-specifications.d.ts +6 -0
  50. package/build/actions/list-card-specifications.d.ts.map +1 -0
  51. package/build/actions/list-card-specifications.js +11 -0
  52. package/build/actions/list-card-specifications.js.map +1 -0
  53. package/build/actions/list-card-transitions.d.ts +6 -0
  54. package/build/actions/list-card-transitions.d.ts.map +1 -0
  55. package/build/actions/list-card-transitions.js +11 -0
  56. package/build/actions/list-card-transitions.js.map +1 -0
  57. package/build/actions/list-cards.d.ts +6 -0
  58. package/build/actions/list-cards.d.ts.map +1 -0
  59. package/build/actions/list-cards.js +10 -0
  60. package/build/actions/list-cards.js.map +1 -0
  61. package/build/actions/list-schemas.d.ts +13 -0
  62. package/build/actions/list-schemas.d.ts.map +1 -0
  63. package/build/actions/list-schemas.js +20 -0
  64. package/build/actions/list-schemas.js.map +1 -0
  65. package/build/actions/list-specification-revisions.d.ts +6 -0
  66. package/build/actions/list-specification-revisions.d.ts.map +1 -0
  67. package/build/actions/list-specification-revisions.js +10 -0
  68. package/build/actions/list-specification-revisions.js.map +1 -0
  69. package/build/actions/resources.d.ts +5 -0
  70. package/build/actions/resources.d.ts.map +1 -0
  71. package/build/actions/resources.js +111 -0
  72. package/build/actions/resources.js.map +1 -0
  73. package/build/actions/summarize-cards.d.ts +6 -0
  74. package/build/actions/summarize-cards.d.ts.map +1 -0
  75. package/build/actions/summarize-cards.js +11 -0
  76. package/build/actions/summarize-cards.js.map +1 -0
  77. package/build/actions/transitions.d.ts +3 -3
  78. package/build/actions/transitions.d.ts.map +1 -1
  79. package/build/actions/transitions.js +4 -3
  80. package/build/actions/transitions.js.map +1 -1
  81. package/build/actions/types.d.ts +6 -0
  82. package/build/actions/types.d.ts.map +1 -0
  83. package/build/actions/types.js +2 -0
  84. package/build/actions/types.js.map +1 -0
  85. package/build/actions/watch.d.ts +2 -2
  86. package/build/actions/watch.d.ts.map +1 -1
  87. package/build/actions/watch.js +8 -6
  88. package/build/actions/watch.js.map +1 -1
  89. package/build/conformance/assert/types.d.ts +19 -0
  90. package/build/conformance/assert/types.d.ts.map +1 -0
  91. package/build/conformance/assert/types.js +2 -0
  92. package/build/conformance/assert/types.js.map +1 -0
  93. package/build/conformance/assert.d.ts +15 -0
  94. package/build/conformance/assert.d.ts.map +1 -0
  95. package/build/conformance/assert.js +52 -0
  96. package/build/conformance/assert.js.map +1 -0
  97. package/build/conformance/cases.d.ts +7 -0
  98. package/build/conformance/cases.d.ts.map +1 -0
  99. package/build/conformance/cases.js +544 -0
  100. package/build/conformance/cases.js.map +1 -0
  101. package/build/conformance/clock.d.ts +6 -0
  102. package/build/conformance/clock.d.ts.map +1 -0
  103. package/build/conformance/clock.js +12 -0
  104. package/build/conformance/clock.js.map +1 -0
  105. package/build/conformance/consts.d.ts +30 -0
  106. package/build/conformance/consts.d.ts.map +1 -0
  107. package/build/conformance/consts.js +159 -0
  108. package/build/conformance/consts.js.map +1 -0
  109. package/build/conformance/errors.d.ts +5 -0
  110. package/build/conformance/errors.d.ts.map +1 -0
  111. package/build/conformance/errors.js +8 -0
  112. package/build/conformance/errors.js.map +1 -0
  113. package/build/conformance/fixtures/types.d.ts +13 -0
  114. package/build/conformance/fixtures/types.d.ts.map +1 -0
  115. package/build/conformance/fixtures/types.js +2 -0
  116. package/build/conformance/fixtures/types.js.map +1 -0
  117. package/build/conformance/fixtures.d.ts +14 -0
  118. package/build/conformance/fixtures.d.ts.map +1 -0
  119. package/build/conformance/fixtures.js +26 -0
  120. package/build/conformance/fixtures.js.map +1 -0
  121. package/build/conformance/index.d.ts +15 -0
  122. package/build/conformance/index.d.ts.map +1 -0
  123. package/build/conformance/index.js +12 -0
  124. package/build/conformance/index.js.map +1 -0
  125. package/build/conformance/resources.d.ts +4 -0
  126. package/build/conformance/resources.d.ts.map +1 -0
  127. package/build/conformance/resources.js +228 -0
  128. package/build/conformance/resources.js.map +1 -0
  129. package/build/conformance/types.d.ts +18 -0
  130. package/build/conformance/types.d.ts.map +1 -0
  131. package/build/conformance/types.js +2 -0
  132. package/build/conformance/types.js.map +1 -0
  133. package/build/consts.d.ts +5 -0
  134. package/build/consts.d.ts.map +1 -1
  135. package/build/consts.js +5 -0
  136. package/build/consts.js.map +1 -1
  137. package/build/definitions.d.ts +18 -0
  138. package/build/definitions.d.ts.map +1 -0
  139. package/build/definitions.js +225 -0
  140. package/build/definitions.js.map +1 -0
  141. package/build/executor/changes/types.d.ts +36 -0
  142. package/build/executor/changes/types.d.ts.map +1 -0
  143. package/build/executor/changes/types.js +2 -0
  144. package/build/executor/changes/types.js.map +1 -0
  145. package/build/executor/changes.d.ts +3 -30
  146. package/build/executor/changes.d.ts.map +1 -1
  147. package/build/executor/changes.js +42 -51
  148. package/build/executor/changes.js.map +1 -1
  149. package/build/executor/code/types.d.ts +17 -0
  150. package/build/executor/code/types.d.ts.map +1 -0
  151. package/build/executor/code/types.js +2 -0
  152. package/build/executor/code/types.js.map +1 -0
  153. package/build/executor/code.d.ts +3 -14
  154. package/build/executor/code.d.ts.map +1 -1
  155. package/build/executor/code.js +50 -54
  156. package/build/executor/code.js.map +1 -1
  157. package/build/executor/consts.local.d.ts +5 -0
  158. package/build/executor/consts.local.d.ts.map +1 -0
  159. package/build/executor/consts.local.js +6 -0
  160. package/build/executor/consts.local.js.map +1 -0
  161. package/build/executor/creator/types.d.ts +33 -0
  162. package/build/executor/creator/types.d.ts.map +1 -0
  163. package/build/executor/creator/types.js +2 -0
  164. package/build/executor/creator/types.js.map +1 -0
  165. package/build/executor/creator.d.ts +11 -0
  166. package/build/executor/creator.d.ts.map +1 -0
  167. package/build/executor/creator.js +32 -0
  168. package/build/executor/creator.js.map +1 -0
  169. package/build/executor/resolve/types.d.ts +30 -0
  170. package/build/executor/resolve/types.d.ts.map +1 -0
  171. package/build/executor/resolve/types.js +2 -0
  172. package/build/executor/resolve/types.js.map +1 -0
  173. package/build/executor/resolve.d.ts +5 -34
  174. package/build/executor/resolve.d.ts.map +1 -1
  175. package/build/executor/resolve.js +169 -110
  176. package/build/executor/resolve.js.map +1 -1
  177. package/build/executor/types.d.ts +23 -0
  178. package/build/executor/types.d.ts.map +1 -0
  179. package/build/executor/types.js +2 -0
  180. package/build/executor/types.js.map +1 -0
  181. package/build/executor/validate.d.ts +6 -5
  182. package/build/executor/validate.d.ts.map +1 -1
  183. package/build/executor/validate.js +38 -27
  184. package/build/executor/validate.js.map +1 -1
  185. package/build/executor.d.ts +11 -7
  186. package/build/executor.d.ts.map +1 -1
  187. package/build/executor.js +27 -21
  188. package/build/executor.js.map +1 -1
  189. package/build/facade.d.ts +20 -2
  190. package/build/facade.d.ts.map +1 -1
  191. package/build/facade.js +144 -22
  192. package/build/facade.js.map +1 -1
  193. package/build/helper.d.ts +5 -9
  194. package/build/helper.d.ts.map +1 -1
  195. package/build/helper.js +12 -11
  196. package/build/helper.js.map +1 -1
  197. package/build/hierarchy/types.d.ts +20 -0
  198. package/build/hierarchy/types.d.ts.map +1 -0
  199. package/build/hierarchy/types.js +2 -0
  200. package/build/hierarchy/types.js.map +1 -0
  201. package/build/hierarchy.d.ts +5 -0
  202. package/build/hierarchy.d.ts.map +1 -0
  203. package/build/hierarchy.js +133 -0
  204. package/build/hierarchy.js.map +1 -0
  205. package/build/index.d.ts +1 -0
  206. package/build/index.d.ts.map +1 -1
  207. package/build/index.js +1 -0
  208. package/build/index.js.map +1 -1
  209. package/build/invariants/types.d.ts +6 -0
  210. package/build/invariants/types.d.ts.map +1 -0
  211. package/build/invariants/types.js +2 -0
  212. package/build/invariants/types.js.map +1 -0
  213. package/build/invariants.d.ts +5 -0
  214. package/build/invariants.d.ts.map +1 -0
  215. package/build/invariants.js +126 -0
  216. package/build/invariants.js.map +1 -0
  217. package/build/projection.d.ts +4 -10
  218. package/build/projection.d.ts.map +1 -1
  219. package/build/projection.js +7 -5
  220. package/build/projection.js.map +1 -1
  221. package/build/queue/consts.d.ts +5 -0
  222. package/build/queue/consts.d.ts.map +1 -0
  223. package/build/queue/consts.js +5 -0
  224. package/build/queue/consts.js.map +1 -0
  225. package/build/queue/declare.d.ts +5 -0
  226. package/build/queue/declare.d.ts.map +1 -0
  227. package/build/queue/declare.js +5 -0
  228. package/build/queue/declare.js.map +1 -0
  229. package/build/queue/index.d.ts +3 -18
  230. package/build/queue/index.d.ts.map +1 -1
  231. package/build/queue/index.js +2 -7
  232. package/build/queue/index.js.map +1 -1
  233. package/build/queue/types.d.ts +13 -0
  234. package/build/queue/types.d.ts.map +1 -0
  235. package/build/queue/types.js +2 -0
  236. package/build/queue/types.js.map +1 -0
  237. package/build/registry.d.ts +5 -22
  238. package/build/registry.d.ts.map +1 -1
  239. package/build/registry.js +16 -10
  240. package/build/registry.js.map +1 -1
  241. package/build/resources.d.ts +5 -0
  242. package/build/resources.d.ts.map +1 -0
  243. package/build/resources.js +287 -0
  244. package/build/resources.js.map +1 -0
  245. package/build/service.d.ts +3 -16
  246. package/build/service.d.ts.map +1 -1
  247. package/build/service.js +12 -0
  248. package/build/service.js.map +1 -1
  249. package/build/store/commits.d.ts.map +1 -1
  250. package/build/store/commits.js +3 -1
  251. package/build/store/commits.js.map +1 -1
  252. package/build/store/composite.d.ts +1 -0
  253. package/build/store/composite.d.ts.map +1 -1
  254. package/build/store/composite.js +7 -2
  255. package/build/store/composite.js.map +1 -1
  256. package/build/store/consts.local.d.ts +2 -0
  257. package/build/store/consts.local.d.ts.map +1 -0
  258. package/build/store/consts.local.js +2 -0
  259. package/build/store/consts.local.js.map +1 -0
  260. package/build/store/fold/types.d.ts +43 -0
  261. package/build/store/fold/types.d.ts.map +1 -0
  262. package/build/store/fold/types.js +2 -0
  263. package/build/store/fold/types.js.map +1 -0
  264. package/build/store/fold.d.ts +8 -30
  265. package/build/store/fold.d.ts.map +1 -1
  266. package/build/store/fold.js +190 -198
  267. package/build/store/fold.js.map +1 -1
  268. package/build/store/index.d.ts +1 -0
  269. package/build/store/index.d.ts.map +1 -1
  270. package/build/store/index.js.map +1 -1
  271. package/build/store/memory.d.ts +12 -0
  272. package/build/store/memory.d.ts.map +1 -1
  273. package/build/store/memory.js +0 -0
  274. package/build/store/memory.js.map +1 -1
  275. package/build/store/types.d.ts +15 -1
  276. package/build/store/types.d.ts.map +1 -1
  277. package/build/store/types.local.d.ts +5 -0
  278. package/build/store/types.local.d.ts.map +1 -0
  279. package/build/store/types.local.js +2 -0
  280. package/build/store/types.local.js.map +1 -0
  281. package/build/types.d.ts +126 -2
  282. package/build/types.d.ts.map +1 -1
  283. package/build/types.local.d.ts +7 -0
  284. package/build/types.local.d.ts.map +1 -0
  285. package/build/types.local.js +2 -0
  286. package/build/types.local.js.map +1 -0
  287. package/build/utils/access/types.d.ts +30 -0
  288. package/build/utils/access/types.d.ts.map +1 -0
  289. package/build/utils/access/types.js +2 -0
  290. package/build/utils/access/types.js.map +1 -0
  291. package/build/utils/access.d.ts +4 -0
  292. package/build/utils/access.d.ts.map +1 -0
  293. package/build/utils/access.js +28 -0
  294. package/build/utils/access.js.map +1 -0
  295. package/build/utils/guard/types.d.ts +23 -0
  296. package/build/utils/guard/types.d.ts.map +1 -0
  297. package/build/utils/guard/types.js +2 -0
  298. package/build/utils/guard/types.js.map +1 -0
  299. package/build/utils/guard.d.ts +7 -10
  300. package/build/utils/guard.d.ts.map +1 -1
  301. package/build/utils/guard.js +36 -32
  302. package/build/utils/guard.js.map +1 -1
  303. package/build/utils/handler/types.d.ts +26 -0
  304. package/build/utils/handler/types.d.ts.map +1 -0
  305. package/build/utils/handler/types.js +2 -0
  306. package/build/utils/handler/types.js.map +1 -0
  307. package/build/utils/handler.d.ts +6 -0
  308. package/build/utils/handler.d.ts.map +1 -0
  309. package/build/utils/handler.js +21 -0
  310. package/build/utils/handler.js.map +1 -0
  311. package/build/utils/index.d.ts +8 -0
  312. package/build/utils/index.d.ts.map +1 -1
  313. package/build/utils/index.js +3 -0
  314. package/build/utils/index.js.map +1 -1
  315. package/build/utils/request/types.d.ts +23 -0
  316. package/build/utils/request/types.d.ts.map +1 -0
  317. package/build/utils/request/types.js +2 -0
  318. package/build/utils/request/types.js.map +1 -0
  319. package/build/utils/request.d.ts +4 -0
  320. package/build/utils/request.d.ts.map +1 -0
  321. package/build/utils/request.js +27 -0
  322. package/build/utils/request.js.map +1 -0
  323. package/build/utils/scope.d.ts +17 -13
  324. package/build/utils/scope.d.ts.map +1 -1
  325. package/build/utils/scope.js +21 -35
  326. package/build/utils/scope.js.map +1 -1
  327. package/build/utils/types.d.ts +8 -0
  328. package/build/utils/types.d.ts.map +1 -0
  329. package/build/utils/types.js +2 -0
  330. package/build/utils/types.js.map +1 -0
  331. package/package.json +30 -22
  332. package/src/actions/apply-schema-request.ts +36 -0
  333. package/src/actions/commit.ts +7 -5
  334. package/src/actions/define-schemas.ts +34 -0
  335. package/src/actions/execute.ts +32 -34
  336. package/src/actions/execution/types.ts +34 -0
  337. package/src/actions/execution.ts +100 -0
  338. package/src/actions/get-card.ts +14 -0
  339. package/src/actions/get-specification.ts +14 -0
  340. package/src/actions/index.ts +13 -3
  341. package/src/actions/links.ts +8 -6
  342. package/src/actions/list-card-specifications.ts +15 -0
  343. package/src/actions/list-card-transitions.ts +15 -0
  344. package/src/actions/list-cards.ts +14 -0
  345. package/src/actions/list-schemas.ts +29 -0
  346. package/src/actions/list-specification-revisions.ts +15 -0
  347. package/src/actions/resources.ts +79 -0
  348. package/src/actions/summarize-cards.ts +15 -0
  349. package/src/actions/transitions.ts +6 -4
  350. package/src/actions/types.ts +5 -0
  351. package/src/actions/watch.ts +11 -7
  352. package/src/conformance/assert/types.ts +16 -0
  353. package/src/conformance/assert.ts +67 -0
  354. package/src/conformance/cases.ts +631 -0
  355. package/src/conformance/clock.ts +11 -0
  356. package/src/conformance/consts.ts +165 -0
  357. package/src/conformance/errors.ts +7 -0
  358. package/src/conformance/fixtures/types.ts +11 -0
  359. package/src/conformance/fixtures.ts +46 -0
  360. package/src/conformance/index.ts +14 -0
  361. package/src/conformance/resources.ts +227 -0
  362. package/src/conformance/types.ts +20 -0
  363. package/src/consts.ts +6 -0
  364. package/src/definitions.ts +264 -0
  365. package/src/executor/changes/types.ts +38 -0
  366. package/src/executor/changes.ts +49 -68
  367. package/src/executor/code/types.ts +23 -0
  368. package/src/executor/code.ts +62 -69
  369. package/src/executor/consts.local.ts +8 -0
  370. package/src/executor/creator/types.ts +33 -0
  371. package/src/executor/creator.ts +43 -0
  372. package/src/executor/resolve/types.ts +30 -0
  373. package/src/executor/resolve.ts +181 -139
  374. package/src/executor/types.ts +23 -0
  375. package/src/executor/validate.ts +40 -38
  376. package/src/executor.ts +33 -27
  377. package/src/facade.ts +132 -29
  378. package/src/helper.ts +12 -11
  379. package/src/hierarchy/types.ts +20 -0
  380. package/src/hierarchy.ts +113 -0
  381. package/src/index.ts +1 -0
  382. package/src/invariants/types.ts +6 -0
  383. package/src/invariants.ts +106 -0
  384. package/src/projection.ts +12 -18
  385. package/src/queue/consts.ts +5 -0
  386. package/src/queue/declare.ts +7 -0
  387. package/src/queue/index.ts +3 -25
  388. package/src/queue/types.ts +14 -0
  389. package/src/registry.ts +18 -36
  390. package/src/resources.ts +256 -0
  391. package/src/service.ts +16 -20
  392. package/src/store/commits.ts +5 -4
  393. package/src/store/composite.ts +7 -2
  394. package/src/store/consts.local.ts +1 -0
  395. package/src/store/fold/types.ts +44 -0
  396. package/src/store/fold.ts +214 -212
  397. package/src/store/index.ts +1 -0
  398. package/src/store/memory.ts +0 -0
  399. package/src/store/types.local.ts +3 -0
  400. package/src/store/types.ts +16 -1
  401. package/src/types.local.ts +7 -0
  402. package/src/types.ts +139 -6
  403. package/src/utils/access/types.ts +30 -0
  404. package/src/utils/access.ts +33 -0
  405. package/src/utils/guard/types.ts +21 -0
  406. package/src/utils/guard.ts +44 -31
  407. package/src/utils/handler/types.ts +28 -0
  408. package/src/utils/handler.ts +35 -0
  409. package/src/utils/index.ts +9 -0
  410. package/src/utils/request/types.ts +23 -0
  411. package/src/utils/request.ts +40 -0
  412. package/src/utils/scope.ts +39 -42
  413. package/src/utils/types.ts +8 -0
  414. package/tests/access.spec.ts +160 -0
  415. package/tests/app.spec.ts +112 -0
  416. package/tests/conformance.spec.ts +47 -0
  417. package/tests/create-example.spec.ts +202 -0
  418. package/tests/creator.spec.ts +257 -0
  419. package/tests/entrypoints.spec.ts +5 -0
  420. package/tests/memory-store.spec.ts +52 -0
  421. package/tests/narrowing.spec.ts +94 -0
  422. package/tests/plugins.spec.ts +4 -3
  423. package/tests/writes.spec.ts +140 -0
  424. package/build/actions/cards.d.ts +0 -16
  425. package/build/actions/cards.d.ts.map +0 -1
  426. package/build/actions/cards.js +0 -32
  427. package/build/actions/cards.js.map +0 -1
  428. package/build/actions/schemas.d.ts +0 -6
  429. package/build/actions/schemas.d.ts.map +0 -1
  430. package/build/actions/schemas.js +0 -9
  431. package/build/actions/schemas.js.map +0 -1
  432. package/build/actions/specs.d.ts +0 -10
  433. package/build/actions/specs.d.ts.map +0 -1
  434. package/build/actions/specs.js +0 -14
  435. package/build/actions/specs.js.map +0 -1
  436. package/src/actions/cards.ts +0 -52
  437. package/src/actions/schemas.ts +0 -15
  438. package/src/actions/specs.ts +0 -23
@@ -0,0 +1,631 @@
1
+ import { resourceConformance } from './resources.js'
2
+
3
+ import { applyHelper, CardTypeNotAllowed, CommitState, FieldsInvalid, IntrinsicStatus, PlanningError, PlanningSchemaKind, RelationshipRefused, SchemaConflict, SchemaInUse, SchemaInvalid, SchemaOrigin, SchemaSealed, SpecificationFormat, TransitionAction, UnknownWorkcardType, WorkcardConflict, WorkcardKind, WorkcardNotFound, type CommitEvent, type PlanningFacade, type PlanningStore, type StatusFlowSchema, type Transition, type TransitionExecution, type Unsubscribe, type Workcard, type WorkcardQuery, type WorkcardTypeSchema } from '@owlmeans/planning'
4
+ import { creatorHelper } from '../executor/creator.js'
5
+ import { assertHelper } from './assert.js'
6
+ import { conformanceFixturesOf } from './fixtures.js'
7
+ import { BOOK_TYPE, CONFORMANCE_APP_PLUGIN, LIBRARY, PERIODICAL_TYPE } from './consts.js'
8
+ import type { ConformanceCase } from './types.js'
9
+ import { idHelper } from '@owlmeans/basic-ids'
10
+
11
+ const { check, rejects, same, sameSet } = assertHelper
12
+
13
+ /** A fresh organization per case, so cases never see each other's cards on a shared database. */
14
+ const organization = (): string => `conformance-${idHelper.uuid()}`
15
+
16
+ const ids = async (planning: PlanningFacade, query: WorkcardQuery): Promise<string[]> =>
17
+ (await planning.cards.list({ ...query, size: 0 })).items.map(card => card.id!)
18
+
19
+ const execute = async (
20
+ planning: PlanningFacade, exec: Parameters<PlanningFacade['execute']>[0]
21
+ ): Promise<Workcard | null> => (await planning.execute(exec, { wait: true })).card ?? null
22
+
23
+ const cases: ConformanceCase[] = [
24
+ {
25
+ name: 'folds transitions in seq order and settles each one once',
26
+ run: async ({ facade }) => {
27
+ const planning = facade(organization())
28
+ const fixtures = conformanceFixturesOf(planning)
29
+ const events: CommitEvent[] = []
30
+ const unsubscribe: Unsubscribe = await planning.commits.subscribe(event => { events.push(event) })
31
+ try {
32
+ const branch = await fixtures.createBranch()
33
+ const book = await fixtures.createBook(branch.id!, 'Winter poems')
34
+ await execute(planning, { card: book.id!, action: TransitionAction.Update, changes: { title: 'Winter verses' } })
35
+ await fixtures.transit(book.id!, 'lend')
36
+
37
+ const card = await planning.cards.get(book.id!)
38
+ same([card.seq, card.head, card.title, card.status, card.intrinsic], [3, 3, 'Winter verses', 'lent', IntrinsicStatus.InProgress], 'the folded card')
39
+ same(events.filter(event => event.card === book.id).map(event => [event.seq, event.state]),
40
+ [[1, CommitState.Committed], [2, CommitState.Committed], [3, CommitState.Committed]], 'one committed event per transition')
41
+ } finally {
42
+ unsubscribe()
43
+ }
44
+ },
45
+ },
46
+
47
+ {
48
+ name: 'allocates seq against the head and refuses a stale expectSeq',
49
+ run: async ({ facade, store }) => {
50
+ const planning = facade(organization())
51
+ const fixtures = conformanceFixturesOf(planning)
52
+ const book = await fixtures.createBook((await fixtures.createBranch()).id!)
53
+
54
+ await execute(planning, { card: book.id!, action: TransitionAction.Update, changes: { title: 'First' }, expectSeq: 1 })
55
+ await rejects(planning.execute({ card: book.id!, action: TransitionAction.Update, changes: { title: 'Stale' }, expectSeq: 1 }),
56
+ WorkcardConflict, 'a stale expectSeq')
57
+ same(await store.transitions!.head(book.id!), 2, 'the head after two transitions')
58
+ same(await store.transitions!.nextSeq(`conformance-${idHelper.uuid()}`, null), 1, 'a card with no log allocates 1')
59
+ same((await planning.cards.get(book.id!)).title, 'First', 'the refused write left the card alone')
60
+ },
61
+ },
62
+
63
+ {
64
+ name: 'an idempotency key answers its first receipt',
65
+ run: async ({ facade }) => {
66
+ const planning = facade(organization())
67
+ const fixtures = conformanceFixturesOf(planning)
68
+ const book = await fixtures.createBook((await fixtures.createBranch()).id!)
69
+
70
+ const first = await planning.execute({ card: book.id!, action: TransitionAction.Update, changes: { title: 'Kept' }, key: 'import:1' }, { wait: true })
71
+ const again = await planning.execute({ card: book.id!, action: TransitionAction.Update, changes: { title: 'Lost' }, key: 'import:1' }, { wait: true })
72
+
73
+ same(again.transition.id, first.transition.id, 'the retried key answers the first transition')
74
+ const card = await planning.cards.get(book.id!)
75
+ same([card.title, card.seq], ['Kept', 2], 'one write landed')
76
+ },
77
+ },
78
+
79
+ {
80
+ name: 'keeps a create\'s createdBy — the scope\'s subject, or the one the caller named — through later writes',
81
+ run: async ({ facade }) => {
82
+ const planning = facade(organization())
83
+ const fixtures = conformanceFixturesOf(planning)
84
+ const subject = creatorHelper.creatorOf(planning.scope)
85
+ check(subject != null, 'the conformance facade acts as a signed-in person')
86
+
87
+ const branch = await fixtures.createBranch()
88
+ const stamped = await fixtures.createBook(branch.id!, 'Stamped')
89
+ const named = await fixtures.createBook(branch.id!, 'Named', { createdBy: 'conformance-donor' })
90
+ await execute(planning, { card: stamped.id!, action: TransitionAction.Update, changes: { title: 'Stamped, revised' } })
91
+ await fixtures.transit(stamped.id!, 'lend')
92
+
93
+ same([branch.createdBy, (await planning.cards.get(stamped.id!)).createdBy, (await planning.cards.get(named.id!)).createdBy],
94
+ [subject, subject, 'conformance-donor'], 'createdBy read back after later writes')
95
+ sameSet((await planning.cards.list({ parent: branch.id, size: 0 })).items.map(card => [card.title, card.createdBy]),
96
+ [['Stamped, revised', subject], ['Named', 'conformance-donor']], 'createdBy in a list')
97
+ },
98
+ },
99
+
100
+ {
101
+ name: 'refuses a write that moves createdBy, and folds a legacy row that named it',
102
+ run: async ({ facade, store }) => {
103
+ const org = organization()
104
+ const planning = facade(org)
105
+ const fixtures = conformanceFixturesOf(planning)
106
+ const branch = await fixtures.createBranch()
107
+ const book = await fixtures.createBook(branch.id!, 'Owned')
108
+ const moves: TransitionExecution[] = [
109
+ { card: book.id!, action: TransitionAction.Update, changes: { title: 'Taken', createdBy: 'conformance-intruder' } },
110
+ { card: book.id!, action: TransitionAction.Update, unset: ['createdBy'] },
111
+ ]
112
+
113
+ for (const exec of moves) {
114
+ const error = await rejects(planning.execute(exec, { wait: true }), PlanningError, 'a write naming createdBy')
115
+ check(error.message.includes('immutable:createdBy'), `refused as immutable, not ${error.message}`)
116
+ }
117
+ const kept = await planning.cards.get(book.id!)
118
+ same([kept.createdBy, kept.title, kept.seq, (await planning.transitions.list({ card: book.id! })).total],
119
+ [creatorHelper.creatorOf(planning.scope), 'Owned', 1, 1], 'the refused writes left the card and its log alone')
120
+
121
+ // Rows appended before the refusal existed: the store folds them exactly as the pure fold does.
122
+ const legacy: Transition[] = []
123
+ for (const patch of [{ changes: { title: 'Legacy', createdBy: 'conformance-intruder' } }, { changes: {}, unset: ['createdBy'] }]) {
124
+ const seq = await store.transitions!.nextSeq(book.id!, null)
125
+ legacy.push(await store.transitions!.append({
126
+ entityId: org, card: book.id!, kind: WorkcardKind.Card, type: LIBRARY.book, project: branch.id!, seq,
127
+ action: TransitionAction.Update, actor: {}, at: `2001-01-01T00:00:0${seq}.000Z`,
128
+ commit: { state: CommitState.Pending }, ...patch,
129
+ }))
130
+ }
131
+ await store.cards.project(book.id!)
132
+
133
+ for (const row of legacy) {
134
+ same((await planning.commits.status(row.id!)).state, CommitState.Committed, `legacy row ${row.seq} settles committed`)
135
+ }
136
+ const expected = legacy.reduce<Workcard | null>((card, row) => applyHelper.applyTransition(card, row), kept)!
137
+ const folded = await planning.cards.get(book.id!)
138
+ same([folded.title, folded.seq, folded.createdBy], [expected.title, expected.seq, expected.createdBy], 'the legacy rows folded')
139
+ await execute(planning, { card: book.id!, action: TransitionAction.Update, changes: { title: 'After' } })
140
+ same((await planning.cards.get(book.id!)).title, 'After', 'the next write commits past them')
141
+ },
142
+ },
143
+
144
+ {
145
+ name: 'transit moves the intrinsic state and closedAt, and a second flow leaves the primary alone',
146
+ run: async ({ facade }) => {
147
+ const planning = facade(organization())
148
+ const fixtures = conformanceFixturesOf(planning)
149
+ const book = await fixtures.createBook((await fixtures.createBranch()).id!)
150
+
151
+ await fixtures.transit(book.id!, 'retire')
152
+ const retired = await planning.cards.get(book.id!)
153
+ same([retired.status, retired.intrinsic], ['retired', IntrinsicStatus.Closed], 'retired')
154
+ check(typeof retired.closedAt === 'string', 'closedAt is set on entering closed')
155
+
156
+ await fixtures.transit(book.id!, 'restore')
157
+ const restored = await planning.cards.get(book.id!)
158
+ same([restored.status, restored.intrinsic, restored.closedAt], ['shelved', IntrinsicStatus.Planned, undefined], 'closedAt cleared on leaving closed')
159
+
160
+ await fixtures.transit(book.id!, 'review', { flow: LIBRARY.review })
161
+ const reviewed = await planning.cards.get(book.id!)
162
+ same([reviewed.status, reviewed.flows[LIBRARY.review], reviewed.intrinsic], ['shelved', 'reviewed', IntrinsicStatus.Planned], 'the second flow moved alone')
163
+ },
164
+ },
165
+
166
+ {
167
+ name: 'answers the query operators criteriaOf produces',
168
+ run: async ({ facade }) => {
169
+ const org = organization()
170
+ const planning = facade(org)
171
+ const fixtures = conformanceFixturesOf(planning)
172
+ const east = await fixtures.createBranch('East branch')
173
+ const west = await fixtures.createBranch('West branch')
174
+ const poems = await fixtures.createBook(east.id!, 'Winter poems', { labels: ['rare'], fields: { genre: 'poetry', signed: true } })
175
+ const atlas = await fixtures.createBook(east.id!, 'River atlas', { labels: ['new'], parents: [west.id!], fields: { genre: 'history' } })
176
+ const harbour = await fixtures.createBook(east.id!, 'Quiet harbour', { fields: { genre: 'fiction', signed: false } })
177
+ await fixtures.transit(atlas.id!, 'lend')
178
+ const since = (await planning.cards.get(atlas.id!)).updatedAt!
179
+
180
+ sameSet(await ids(planning, { within: west.id }), [atlas.id], 'within')
181
+ sameSet(await ids(planning, { parent: east.id, flow: { id: LIBRARY.circulation, status: ['lent', 'retired'] } }), [atlas.id], 'a multi-value flow status')
182
+ sameSet(await ids(planning, { parent: east.id, flow: { id: LIBRARY.circulation, status: 'shelved' } }), [poems.id, harbour.id], 'one flow status')
183
+ sameSet(await ids(planning, { parent: east.id, fields: { genre: 'poetry' } }), [poems.id], 'a string field')
184
+ sameSet(await ids(planning, { parent: east.id, fields: { signed: false } }), [harbour.id], 'a boolean field')
185
+ sameSet(await ids(planning, { parent: east.id, labels: ['rare', 'signed'] }), [poems.id], 'labels overlap')
186
+ sameSet(await ids(planning, { parent: east.id, q: 'HARBOUR' }), [harbour.id], 'text search ignores case')
187
+ sameSet(await ids(planning, { parent: east.id, q: poems.code! }), [poems.id], 'text search by code prefix')
188
+ sameSet(await ids(planning, { parent: east.id, q: '100%_' }), [], 'wildcards in a search are literal')
189
+ sameSet(await ids(planning, { parent: null as never, ids: [east.id!, poems.id!] }), [east.id], 'parent: null is a root')
190
+ sameSet(await ids(planning, { kind: [WorkcardKind.Project], ids: [east.id!, west.id!, poems.id!] }), [east.id, west.id], 'kind and ids')
191
+ sameSet(await ids(planning, { parent: east.id, status: ['shelved'] }), [poems.id, harbour.id], 'status')
192
+ sameSet(await ids(planning, { parent: east.id, intrinsic: IntrinsicStatus.InProgress }), [atlas.id], 'intrinsic')
193
+ sameSet(await ids(planning, { parent: east.id, type: [LIBRARY.book, LIBRARY.periodical] }), [poems.id, atlas.id, harbour.id], 'type')
194
+ sameSet(await ids(planning, { parent: east.id, updatedSince: since }), [atlas.id], 'updatedSince')
195
+ same((await facade(organization()).cards.list({ labels: ['rare'] })).total, 0, 'another organization sees nothing')
196
+ },
197
+ },
198
+
199
+ {
200
+ name: 'lists specifications only when the query asks for them',
201
+ run: async ({ facade }) => {
202
+ const planning = facade(organization())
203
+ const fixtures = conformanceFixturesOf(planning)
204
+ const branch = await fixtures.createBranch()
205
+ const book = await fixtures.createBook(branch.id!)
206
+ const model = await planning.model(book)
207
+ const summary = (await model.write('summary', '{"lines":["a"]}', { wait: true })).card!
208
+ const charter = (await (await planning.model(branch)).write('charter', '# Charter', { wait: true })).card!
209
+
210
+ same(await ids(planning, { parent: book.id }), [], 'a card list leaves specifications out')
211
+ sameSet(await ids(planning, { parent: book.id, kind: WorkcardKind.Specification }), [summary.id], 'kind: specification')
212
+ sameSet(await ids(planning, { parent: book.id, category: 'summary' }), [summary.id], 'a category')
213
+ sameSet(await ids(planning, { within: branch.id }), [book.id], 'within leaves the charter out')
214
+ same(await planning.cards.count({ within: branch.id }), 1, 'count routes the same way')
215
+ sameSet(await ids(planning, { within: branch.id, kind: [WorkcardKind.Card, WorkcardKind.Specification] }), [book.id, charter.id], 'both kinds when asked')
216
+ },
217
+ },
218
+
219
+ {
220
+ name: 'pages and sorts a list, counting the whole match',
221
+ run: async ({ facade }) => {
222
+ const planning = facade(organization())
223
+ const fixtures = conformanceFixturesOf(planning)
224
+ const branch = await fixtures.createBranch()
225
+ for (const order of [5, 3, 1, 4, 2]) {
226
+ await fixtures.createBook(branch.id!, `Volume ${order}`, { order })
227
+ }
228
+
229
+ const page = await planning.cards.list({ parent: branch.id, sort: ['order'], size: 2, page: 1 })
230
+ same([page.total, page.page, page.size, page.items.map(card => card.order)], [5, 1, 2, [3, 4]], 'the second page')
231
+ const all = await planning.cards.list({ parent: branch.id, sort: [{ field: 'order', order: 'desc' }], size: 0 })
232
+ same(all.items.map(card => card.order), [5, 4, 3, 2, 1], 'unpaged, descending')
233
+ same(all.total, 5, 'the total')
234
+ },
235
+ },
236
+
237
+ {
238
+ name: 'summarizes direct children by intrinsic state',
239
+ run: async ({ facade }) => {
240
+ const planning = facade(organization())
241
+ const fixtures = conformanceFixturesOf(planning)
242
+ const branch = await fixtures.createBranch()
243
+ const empty = await fixtures.createBranch('Empty branch')
244
+ const lent = await fixtures.createBook(branch.id!, 'Lent')
245
+ const retired = await fixtures.createBook(branch.id!, 'Retired')
246
+ await fixtures.createBook(branch.id!, 'Shelved')
247
+ await fixtures.transit(lent.id!, 'lend')
248
+ await fixtures.transit(retired.id!, 'retire')
249
+ const annex = await fixtures.createBranch('Annex', { parent: branch.id })
250
+ await fixtures.createBook(annex.id!, 'Deeper')
251
+ await (await planning.model(branch)).write('charter', '# Charter', { wait: true })
252
+
253
+ const summary = await planning.cards.summary([branch.id!, empty.id!])
254
+ same(summary, {
255
+ [branch.id!]: { total: 4, [IntrinsicStatus.Planned]: 2, [IntrinsicStatus.InProgress]: 1, [IntrinsicStatus.Closed]: 1 },
256
+ }, 'direct children only, no specifications, no key for an empty parent')
257
+ same((await planning.cards.summary([branch.id!], { kind: WorkcardKind.Card }))[branch.id!]?.total, 3, 'narrowed by kind')
258
+ },
259
+ },
260
+
261
+ {
262
+ name: 'answers a specification current, listed, every document and its revisions',
263
+ run: async ({ facade }) => {
264
+ const planning = facade(organization())
265
+ const fixtures = conformanceFixturesOf(planning)
266
+ const branch = await fixtures.createBranch()
267
+ const book = await fixtures.createBook(branch.id!)
268
+ const model = await planning.model(book)
269
+ await model.write('summary', '{"lines":["first"]}', { wait: true })
270
+ await (await planning.model(book.id!)).write('summary', '{"lines":["second"]}', { wait: true })
271
+
272
+ const current = await planning.specifications.current(book.id!, 'summary')
273
+ same([current?.revision, current?.body], [2, '{"lines":["second"]}'], 'the current document, revised in place')
274
+ const history = await planning.specifications.revisions(current!.id!)
275
+ same(history.map(entry => [entry.revision, entry.body]), [[2, '{"lines":["second"]}'], [1, '{"lines":["first"]}']], 'revisions newest first')
276
+ same((await planning.specifications.list(book.id!)).items.map(spec => spec.id), [current!.id], 'the list')
277
+
278
+ for (const title of ['First note', 'Second note']) {
279
+ await execute(planning, {
280
+ card: { kind: WorkcardKind.Specification, type: LIBRARY.page, parent: branch.id!, title, category: 'notes', format: SpecificationFormat.Markdown, body: title },
281
+ action: TransitionAction.Create,
282
+ })
283
+ }
284
+ same((await planning.specifications.list(branch.id!, { category: 'notes' })).total, 1, 'the current one per category')
285
+ same((await planning.specifications.list(branch.id!, { category: 'notes', all: true })).total, 2, 'every document with all')
286
+ },
287
+ },
288
+
289
+ {
290
+ name: 'keeps links: both ends, the single constraint, unlink and a delete dropping edges',
291
+ run: async ({ facade }) => {
292
+ const planning = facade(organization())
293
+ const fixtures = conformanceFixturesOf(planning)
294
+ const branch = await fixtures.createBranch()
295
+ const first = await fixtures.createBook(branch.id!, 'First')
296
+ const second = await fixtures.createBook(branch.id!, 'Second')
297
+ const third = await fixtures.createBook(branch.id!, 'Third')
298
+
299
+ await execute(planning, { card: second.id!, action: TransitionAction.Link, link: { type: 'sequel-of', to: first.id! } })
300
+ same((await planning.relationships.list({ from: second.id })).total, 1, 'from')
301
+ same((await planning.relationships.list({ to: first.id, type: 'sequel-of' })).items.map(link => link.from), [second.id], 'to and type')
302
+ await rejects(planning.execute({ card: second.id!, action: TransitionAction.Link, link: { type: 'sequel-of', to: third.id! } }),
303
+ RelationshipRefused, 'a second single edge')
304
+
305
+ await execute(planning, { card: third.id!, action: TransitionAction.Link, link: { type: 'shelved-near', to: first.id! } })
306
+ await execute(planning, { card: second.id!, action: TransitionAction.Link, link: { type: 'shelved-near', to: first.id! } })
307
+ await execute(planning, { card: third.id!, action: TransitionAction.Unlink, link: { type: 'shelved-near', to: first.id! } })
308
+ sameSet((await planning.relationships.list({ to: first.id, type: 'shelved-near' })).items.map(link => link.from), [second.id], 'unlink')
309
+
310
+ await execute(planning, { card: first.id!, action: TransitionAction.Delete })
311
+ same((await planning.relationships.list({ to: first.id })).total, 0, 'a delete drops the edges into the card')
312
+ same((await planning.relationships.list({ from: second.id })).total, 0, 'and out of the cards pointing at it')
313
+ },
314
+ },
315
+
316
+ {
317
+ name: 'isolates organizations on every read and write',
318
+ run: async ({ facade }) => {
319
+ const mine = facade(organization())
320
+ const fixtures = conformanceFixturesOf(mine)
321
+ const theirs = facade(organization())
322
+ const branch = await fixtures.createBranch()
323
+ const book = await fixtures.createBook(branch.id!)
324
+ const receipt = await mine.execute({ card: book.id!, action: TransitionAction.Update, changes: { title: 'Mine' } }, { wait: true })
325
+
326
+ same(await theirs.cards.load(book.id!), null, 'load')
327
+ await rejects(theirs.cards.get(book.id!), WorkcardNotFound, 'get')
328
+ same((await theirs.cards.list({ within: branch.id })).total, 0, 'list')
329
+ same(await theirs.cards.summary([branch.id!]), {}, 'summary')
330
+ await rejects(theirs.execute({ card: book.id!, action: TransitionAction.Update, changes: { title: 'Theirs' } }), WorkcardNotFound, 'a write')
331
+ await rejects(theirs.transitions.get(receipt.transition.id!), WorkcardNotFound, 'a transition')
332
+ await rejects(theirs.commits.status(receipt.transition.id!), WorkcardNotFound, 'a commit')
333
+ same((await theirs.relationships.list({ from: book.id })).total, 0, 'links')
334
+ same((await mine.cards.get(book.id!)).title, 'Mine', 'nothing moved')
335
+ },
336
+ },
337
+
338
+ {
339
+ name: 'purges everything under a deleted project, keeping at most its own delete as a tombstone',
340
+ run: async ({ facade }) => {
341
+ const planning = facade(organization())
342
+ const fixtures = conformanceFixturesOf(planning)
343
+ const doomed = await fixtures.createBranch('Doomed branch')
344
+ const kept = await fixtures.createBranch('Kept branch')
345
+ const first = await fixtures.createBook(doomed.id!, 'First')
346
+ const second = await fixtures.createBook(doomed.id!, 'Second')
347
+ await execute(planning, { card: second.id!, action: TransitionAction.Link, link: { type: 'sequel-of', to: first.id! } })
348
+ const summary = (await (await planning.model(first)).write('summary', '{}', { wait: true })).card!
349
+ const annex = await fixtures.createBranch('Annex', { parent: doomed.id })
350
+ const deeper = await fixtures.createBook(annex.id!, 'Deeper')
351
+ const survivor = await fixtures.createBook(kept.id!, 'Survivor')
352
+
353
+ const deleted = await planning.execute({ card: doomed.id!, action: TransitionAction.Delete }, { wait: true })
354
+
355
+ for (const id of [doomed.id!, first.id!, second.id!, summary.id!, annex.id!, deeper.id!]) {
356
+ same(await planning.cards.load(id), null, `purged ${id}`)
357
+ }
358
+ same((await planning.relationships.list({ from: second.id })).total, 0, 'links')
359
+ same((await planning.transitions.list({ card: first.id! })).total, 0, 'a child\'s log')
360
+ const left = (await planning.transitions.list({ project: doomed.id!, size: 0 })).items
361
+ check(left.every(row => row.card === doomed.id && row.action === TransitionAction.Delete), 'only the project\'s own delete may remain')
362
+ same((await planning.commits.status(deleted.transition.id!)).state, CommitState.Committed, 'the delete reads committed')
363
+ check(await planning.cards.load(survivor.id!) != null, 'a sibling project survives')
364
+ same((await planning.cards.list({ within: kept.id })).total, 1, 'with its card')
365
+ },
366
+ },
367
+
368
+ {
369
+ name: 'deletes a card with its own documents and refuses one with a child card',
370
+ run: async ({ facade, store }) => {
371
+ const entityId = organization()
372
+ const planning = facade(entityId)
373
+ const fixtures = conformanceFixturesOf(planning)
374
+ const branch = await fixtures.createBranch()
375
+ const book = await fixtures.createBook(branch.id!, 'Documented')
376
+ const neighbour = await fixtures.createBook(branch.id!, 'Neighbour')
377
+ const summary = (await (await planning.model(book)).write('summary', '{"lines":["first"]}', { wait: true })).card!
378
+ await (await planning.model(book.id!)).write('summary', '{"lines":["second"]}', { wait: true })
379
+ await execute(planning, { card: neighbour.id!, action: TransitionAction.Link, link: { type: 'shelved-near', to: book.id! } })
380
+
381
+ const deleted = await planning.execute({ card: book.id!, action: TransitionAction.Delete }, { wait: true })
382
+
383
+ same((await planning.commits.status(deleted.transition.id!)).state, CommitState.Committed, 'the delete commits')
384
+ same(await planning.cards.load(book.id!), null, 'the card is gone')
385
+ same(await planning.cards.load(summary.id!), null, 'its document is gone')
386
+ same(await store.specs!.current(book.id!, 'summary', entityId), null, 'no current document')
387
+ same((await planning.cards.list({ kind: WorkcardKind.Specification })).total, 0, 'no listed document')
388
+ same((await store.transitions!.list({ entityId, card: summary.id! }, { size: 0 })).total, 0, 'the document\'s log goes with it')
389
+ check((await store.transitions!.list({ entityId, card: book.id! }, { size: 0 })).items.some(row => row.action === TransitionAction.Delete), 'the card keeps its own delete')
390
+ same((await planning.relationships.list({ from: neighbour.id })).total, 0, 'edges into the card dropped')
391
+ check(await planning.cards.load(neighbour.id!) != null, 'a sibling survives')
392
+
393
+ const shelf = (await planning.execute({
394
+ card: { kind: WorkcardKind.Card, type: LIBRARY.section, parent: branch.id!, title: 'Shelf' }, action: TransitionAction.Create,
395
+ }, { wait: true })).card!
396
+ const shelved = await fixtures.createBook(shelf.id!, 'Shelved')
397
+ await (await planning.model(shelved)).write('summary', '{"lines":["kept"]}', { wait: true })
398
+ const refusal = await rejects(planning.execute({ card: shelf.id!, action: TransitionAction.Delete }), PlanningError, 'a card with a child card')
399
+ check(refusal.message.includes('hierarchy:parent-in-use'), `the refusal names the hierarchy: ${refusal.message}`)
400
+ same((await planning.cards.get(shelved.id!)).parent, shelf.id, 'the child card stays')
401
+ same((await planning.specifications.current(shelved.id!, 'summary'))?.body, '{"lines":["kept"]}', 'with its document')
402
+ },
403
+ },
404
+
405
+ {
406
+ name: 'settles a transition that cannot fold in order and folds past it',
407
+ run: async ({ facade, store }) => {
408
+ const org = organization()
409
+ const planning = facade(org)
410
+ const fixtures = conformanceFixturesOf(planning)
411
+ const book = await fixtures.createBook((await fixtures.createBranch()).id!)
412
+ // A row two past the card with nothing between — its allocation lost long ago.
413
+ const rogue = await store.transitions!.append({
414
+ entityId: org, card: book.id!, kind: WorkcardKind.Card, type: LIBRARY.book, seq: 3,
415
+ action: TransitionAction.Update, changes: { title: 'Out of order' }, actor: {},
416
+ at: '2001-01-01T00:00:00.000Z', commit: { state: CommitState.Pending },
417
+ })
418
+ await store.cards.project(book.id!)
419
+
420
+ check((await planning.commits.status(rogue.id!)).state !== CommitState.Pending, 'the out-of-order row is settled, not left pending')
421
+ await execute(planning, { card: book.id!, action: TransitionAction.Update, changes: { title: 'After' } })
422
+ const card = await planning.cards.get(book.id!)
423
+ same([card.title, card.seq, card.head], ['After', 4, 4], 'the next write commits past it')
424
+ },
425
+ },
426
+
427
+ // ─── Data-defined types and flows ─────────────────────────────────────────────────────────────
428
+
429
+ {
430
+ name: 'defines a card type as data and admits it where the project allows data-defined types',
431
+ needs: ['schemas'],
432
+ run: async ({ facade }) => {
433
+ const planning = facade(organization())
434
+ const fixtures = conformanceFixturesOf(planning)
435
+ const pamphlets: StatusFlowSchema = {
436
+ id: 'library:pamphlet-life', version: 1,
437
+ statuses: [{ key: 'stacked', intrinsic: IntrinsicStatus.Planned, initial: true }, { key: 'taken', intrinsic: IntrinsicStatus.Closed }],
438
+ transitions: [{ name: 'take', from: ['stacked'], to: 'taken', explicit: true }],
439
+ }
440
+ const pamphlet: WorkcardTypeSchema = {
441
+ type: 'library:pamphlet', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: [pamphlets.id], specifications: [],
442
+ }
443
+ const written = await planning.definitions!.define({ flows: [pamphlets], types: [pamphlet] })
444
+ same(written.map(record => [record.kind, record.key, record.version]),
445
+ [[PlanningSchemaKind.Flow, pamphlets.id, 1], [PlanningSchemaKind.Type, pamphlet.type, 1]], 'flows first, then types')
446
+
447
+ const branch = await fixtures.createBranch()
448
+ const card = await execute(planning, { card: { kind: WorkcardKind.Card, type: pamphlet.type, parent: branch.id!, title: 'Opening hours' }, action: TransitionAction.Create })
449
+ same([card?.status, card?.intrinsic], ['stacked', IntrinsicStatus.Planned], 'the data-defined flow started it')
450
+ same((await planning.model(card!)).available().map(rule => rule.name), ['take'], 'the model reads the layer')
451
+ await fixtures.transit(card!.id!, 'take')
452
+
453
+ const room = await execute(planning, { card: { kind: WorkcardKind.Project, type: LIBRARY.room, title: 'Reading room' }, action: TransitionAction.Create })
454
+ await rejects(planning.execute({ card: { kind: WorkcardKind.Card, type: pamphlet.type, parent: room!.id!, title: 'Refused' }, action: TransitionAction.Create }),
455
+ CardTypeNotAllowed, 'a project without scopedCardTypes')
456
+
457
+ const bundle = await planning.definitions!.bundle()
458
+ same([bundle.origins?.types[pamphlet.type], bundle.origins?.types[LIBRARY.book]], [SchemaOrigin.Entity, SchemaOrigin.Code], 'origins')
459
+ same((await facade(organization()).definitions!.bundle()).types.some(type => type.type === pamphlet.type), false, 'another organization does not see it')
460
+ },
461
+ },
462
+
463
+ {
464
+ name: 'overrides an overridable code type for the organization, and a project overrides that',
465
+ needs: ['schemas'],
466
+ run: async ({ facade }) => {
467
+ const planning = facade(organization())
468
+ const fixtures = conformanceFixturesOf(planning)
469
+ const branch = await fixtures.createBranch()
470
+ const other = await fixtures.createBranch('Other branch')
471
+ await planning.definitions!.define({ types: [{ ...PERIODICAL_TYPE, label: 'Magazine' }] })
472
+ await planning.definitions!.define({ types: [{ ...PERIODICAL_TYPE, label: 'Zine' }] }, { project: branch.id })
473
+
474
+ const labelOf = async (project?: string) =>
475
+ (await planning.definitions!.registry(project)).type(LIBRARY.periodical).label
476
+ same([await labelOf(), await labelOf(branch.id), await labelOf(other.id)], ['Magazine', 'Zine', 'Magazine'], 'layered labels')
477
+ same((await planning.definitions!.registry(branch.id)).originOf(PlanningSchemaKind.Type, LIBRARY.periodical), SchemaOrigin.Project, 'project origin')
478
+
479
+ const card = await execute(planning, { card: { kind: WorkcardKind.Card, type: LIBRARY.periodical, parent: branch.id!, title: 'Weekly' }, action: TransitionAction.Create })
480
+ same((await planning.model(card!)).schema().label, 'Zine', 'a card reads its own project\'s layer')
481
+ same((await planning.definitions!.records({ project: branch.id })).map(record => record.key), [LIBRARY.periodical], 'the project layer\'s records')
482
+ },
483
+ },
484
+
485
+ {
486
+ name: 'refuses a sealed key, a non-card type and a lost compare-and-set',
487
+ needs: ['schemas'],
488
+ run: async ({ facade }) => {
489
+ const definitions = facade(organization()).definitions!
490
+ await rejects(definitions.putType({ ...BOOK_TYPE, label: 'Renamed' }), SchemaSealed, 'a sealed code type')
491
+ await rejects(definitions.define({ flows: [{ id: LIBRARY.circulation, version: 1, statuses: [{ key: 'x', intrinsic: IntrinsicStatus.Planned }], transitions: [] }] }),
492
+ SchemaSealed, 'a sealed code flow')
493
+ await rejects(definitions.putType({ ...BOOK_TYPE, type: 'library:archive', kind: WorkcardKind.Specification }), SchemaInvalid, 'a specification type as data')
494
+ await rejects(definitions.putType({ ...BOOK_TYPE, type: 'library:atlas', flows: ['library:nowhere'] }), SchemaInvalid, 'a flow that does not resolve')
495
+
496
+ const atlas: WorkcardTypeSchema = { ...BOOK_TYPE, type: 'library:atlas', code: undefined, relationships: undefined }
497
+ await definitions.putType(atlas)
498
+ await rejects(definitions.putType(atlas), SchemaConflict, 'version 1 twice')
499
+ await rejects(definitions.putType({ ...atlas, version: 3 }), SchemaConflict, 'a skipped version')
500
+ same((await definitions.putType({ ...atlas, version: 2, label: 'Atlas' })).version, 2, 'the next version')
501
+ same((await definitions.seed({ types: [{ ...atlas, label: 'Seeded' }] })).length, 0, 'seed adds nothing the layer has')
502
+ same((await definitions.registry()).type('library:atlas').label, 'Atlas', 'seed left it alone')
503
+ },
504
+ },
505
+
506
+ {
507
+ name: 'retires without breaking existing cards and refuses a flow still in use',
508
+ needs: ['schemas'],
509
+ run: async ({ facade }) => {
510
+ const planning = facade(organization())
511
+ const fixtures = conformanceFixturesOf(planning)
512
+ const definitions = planning.definitions!
513
+ const repair: StatusFlowSchema = {
514
+ id: 'library:repair', version: 1,
515
+ statuses: [
516
+ { key: 'queued', intrinsic: IntrinsicStatus.Planned, initial: true },
517
+ { key: 'mending', intrinsic: IntrinsicStatus.InProgress },
518
+ { key: 'done', intrinsic: IntrinsicStatus.Closed },
519
+ ],
520
+ transitions: [{ name: 'mend', from: ['queued'], to: 'mending' }, { name: 'finish', from: ['mending'], to: 'done' }],
521
+ }
522
+ const folio: WorkcardTypeSchema = {
523
+ type: 'library:folio', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: [repair.id], specifications: [],
524
+ }
525
+ await definitions.define({ flows: [repair], types: [folio] })
526
+ const branch = await fixtures.createBranch()
527
+ const card = await execute(planning, { card: { kind: WorkcardKind.Card, type: folio.type, parent: branch.id!, title: 'Old map' }, action: TransitionAction.Create })
528
+ await fixtures.transit(card!.id!, 'mend')
529
+
530
+ await rejects(definitions.retire(PlanningSchemaKind.Flow, repair.id), SchemaInUse, 'a flow a live type runs')
531
+
532
+ // The flow changes under the card: `mending` is no longer declared, and the card still moves.
533
+ await definitions.define({ flows: [{ ...repair, statuses: [repair.statuses[0], repair.statuses[2]], transitions: [{ name: 'finish', from: ['queued'], to: 'done' }] }] })
534
+ await fixtures.transit(card!.id!, 'finish')
535
+ same((await planning.cards.get(card!.id!)).status, 'done', 'an undeclared status follows the rule of the name')
536
+
537
+ await definitions.retire(PlanningSchemaKind.Type, folio.type)
538
+ await rejects(planning.execute({ card: { kind: WorkcardKind.Card, type: folio.type, parent: branch.id!, title: 'New map' }, action: TransitionAction.Create }),
539
+ UnknownWorkcardType, 'nothing new of a retired type')
540
+ await execute(planning, { card: card!.id!, action: TransitionAction.Update, changes: { title: 'Old map, mended' } })
541
+ same((await planning.cards.get(card!.id!)).title, 'Old map, mended', 'an existing card still writes')
542
+
543
+ same((await definitions.retire(PlanningSchemaKind.Flow, repair.id)).retired, true, 'the flow retires once no live type runs it')
544
+ same((await definitions.bundle()).retired, { types: [folio.type], flows: [repair.id] }, 'both listed retired')
545
+ },
546
+ },
547
+
548
+ {
549
+ name: 'stores the application\'s own type and flow properties and validates its base card fields',
550
+ needs: ['schemas'],
551
+ run: async ({ service, facade }) => {
552
+ service.use(CONFORMANCE_APP_PLUGIN)
553
+ try {
554
+ const planning = facade(organization())
555
+ const definitions = planning.definitions!
556
+ const leaflets: StatusFlowSchema = {
557
+ id: 'library:leaflet-life', version: 1, app: { description: 'Leaflets come and go' },
558
+ statuses: [{ key: 'racked', intrinsic: IntrinsicStatus.Planned, initial: true }, { key: 'gone', intrinsic: IntrinsicStatus.Closed }],
559
+ transitions: [{ name: 'take', from: ['racked'], to: 'gone' }],
560
+ }
561
+ const leaflet: WorkcardTypeSchema = {
562
+ type: 'library:leaflet', kind: WorkcardKind.Card, version: 1, app: { description: 'A folded sheet' },
563
+ fields: { type: 'object', properties: { pages: { type: 'number' } }, additionalProperties: false },
564
+ flows: [leaflets.id], specifications: [],
565
+ }
566
+ await definitions.define({ flows: [leaflets], types: [leaflet] })
567
+
568
+ const stored = await definitions.records({ project: null })
569
+ same(stored.map(record => [record.key, (record.definition as { app?: unknown }).app]).sort(),
570
+ [[leaflet.type, leaflet.app], [leaflets.id, leaflets.app]], 'the records keep the app bags')
571
+ const bundle = await definitions.bundle()
572
+ same([bundle.types.find(type => type.type === leaflet.type)?.app, bundle.flows.find(flow => flow.id === leaflets.id)?.app],
573
+ [leaflet.app, leaflets.app], 'the resolved layer keeps them')
574
+ same(Object.keys(bundle.app?.card?.properties ?? {}), ['shelfmark'], 'the bundle carries the declarations')
575
+
576
+ await rejects(definitions.putType({ ...leaflet, version: 2, app: { colour: 'red' } }), SchemaInvalid, 'an undeclared app property')
577
+ await rejects(definitions.putFlow({ ...leaflets, version: 2, app: undefined }), SchemaInvalid, 'a required app property missing')
578
+ same((await definitions.registry()).type(leaflet.type).version, 1, 'a refused write stores nothing')
579
+
580
+ const branch = await conformanceFixturesOf(planning).createBranch()
581
+ const card = await execute(planning, {
582
+ card: { kind: WorkcardKind.Card, type: leaflet.type, parent: branch.id!, title: 'Opening hours', fields: { pages: 2, shelfmark: 'L-1' } },
583
+ action: TransitionAction.Create,
584
+ })
585
+ same(card?.fields, { pages: 2, shelfmark: 'L-1' }, 'a base field beside closed type fields')
586
+ await rejects(planning.execute({
587
+ card: { kind: WorkcardKind.Card, type: leaflet.type, parent: branch.id!, title: 'Refused', fields: { shelfmark: 'nope' } },
588
+ action: TransitionAction.Create,
589
+ }), FieldsInvalid, 'a base field off its schema')
590
+ await rejects(planning.execute({ card: card!.id!, action: TransitionAction.Update, changes: { fields: { shelfmark: 'x' } } }),
591
+ FieldsInvalid, 'an update off the base schema')
592
+ } finally {
593
+ service.use({ name: CONFORMANCE_APP_PLUGIN.name })
594
+ }
595
+ },
596
+ },
597
+
598
+ {
599
+ name: 'moves the revision on every write and resolves the next read from it',
600
+ needs: ['schemas'],
601
+ run: async ({ facade, store }) => {
602
+ const org = organization()
603
+ const definitions = facade(org).definitions!
604
+ const heard: string[] = []
605
+ const unwatch = store.schemas!.watch?.(entityId => { heard.push(entityId) })
606
+ try {
607
+ const before = await store.schemas!.revision(org)
608
+ await definitions.define({ types: [{ ...PERIODICAL_TYPE, label: 'Journal' }] })
609
+ const after = await store.schemas!.revision(org)
610
+ check(after > before, 'a write moves the revision')
611
+ same((await definitions.bundle()).revision, after, 'the bundle names its revision')
612
+ same((await definitions.registry()).type(LIBRARY.periodical).label, 'Journal', 'the first read')
613
+
614
+ await definitions.define({ types: [{ ...PERIODICAL_TYPE, label: 'Gazette' }] })
615
+ same((await definitions.registry()).type(LIBRARY.periodical).label, 'Gazette', 'a cached layer is never served stale')
616
+ if (unwatch != null) {
617
+ check(heard.includes(org), 'a watcher hears the write')
618
+ }
619
+ } finally {
620
+ unwatch?.()
621
+ }
622
+ },
623
+ },
624
+ ]
625
+
626
+ /** Every conformance case, in order. */
627
+ export const planningConformance: readonly ConformanceCase[] = Object.freeze([...cases, ...resourceConformance])
628
+
629
+ /** The cases a store qualifies for — a case needing `schemas` only where the store holds them. */
630
+ export const conformanceCasesFor = (store: PlanningStore): ConformanceCase[] =>
631
+ planningConformance.filter(entry => (entry.needs ?? []).every(need => need === 'schemas' ? store.schemas != null : store.assignees != null && store.teams != null && store.comments != null && store.mentions != null))
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A clock that never repeats an instant — what a conformance service is booted with, so an
3
+ * `updatedSince` or an ordering by `at` is deterministic on a fast store.
4
+ */
5
+ export const conformanceClock = (): (() => string) => {
6
+ let last = 0
7
+ return () => {
8
+ last = Math.max(Date.now(), last + 1)
9
+ return new Date(last).toISOString()
10
+ }
11
+ }