@owlmeans/planning 0.1.18-rc.2 → 0.1.18-rc.21

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 (228) hide show
  1. package/README.md +69 -26
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/planning/SKILL.md +157 -45
  4. package/build/aliases/types.d.ts +49 -0
  5. package/build/aliases/types.d.ts.map +1 -0
  6. package/build/aliases/types.js +2 -0
  7. package/build/aliases/types.js.map +1 -0
  8. package/build/aliases.d.ts +4 -0
  9. package/build/aliases.d.ts.map +1 -0
  10. package/build/aliases.js +27 -0
  11. package/build/aliases.js.map +1 -0
  12. package/build/consts.d.ts +23 -34
  13. package/build/consts.d.ts.map +1 -1
  14. package/build/consts.js +26 -25
  15. package/build/consts.js.map +1 -1
  16. package/build/entrypoints/types.d.ts +96 -0
  17. package/build/entrypoints/types.d.ts.map +1 -0
  18. package/build/entrypoints/types.js +2 -0
  19. package/build/entrypoints/types.js.map +1 -0
  20. package/build/entrypoints.d.ts +5 -1
  21. package/build/entrypoints.d.ts.map +1 -1
  22. package/build/entrypoints.js +17 -6
  23. package/build/entrypoints.js.map +1 -1
  24. package/build/errors.d.ts +41 -0
  25. package/build/errors.d.ts.map +1 -1
  26. package/build/errors.js +61 -0
  27. package/build/errors.js.map +1 -1
  28. package/build/helpers/apply/consts.local.d.ts +7 -0
  29. package/build/helpers/apply/consts.local.d.ts.map +1 -0
  30. package/build/helpers/apply/consts.local.js +9 -0
  31. package/build/helpers/apply/consts.local.js.map +1 -0
  32. package/build/helpers/apply/types.d.ts +32 -0
  33. package/build/helpers/apply/types.d.ts.map +1 -0
  34. package/build/helpers/apply/types.js +2 -0
  35. package/build/helpers/apply/types.js.map +1 -0
  36. package/build/helpers/apply.d.ts +5 -23
  37. package/build/helpers/apply.d.ts.map +1 -1
  38. package/build/helpers/apply.js +121 -143
  39. package/build/helpers/apply.js.map +1 -1
  40. package/build/helpers/card/types.d.ts +20 -0
  41. package/build/helpers/card/types.d.ts.map +1 -0
  42. package/build/helpers/card/types.js +2 -0
  43. package/build/helpers/card/types.js.map +1 -0
  44. package/build/helpers/card.d.ts +11 -7
  45. package/build/helpers/card.d.ts.map +1 -1
  46. package/build/helpers/card.js +41 -28
  47. package/build/helpers/card.js.map +1 -1
  48. package/build/helpers/changes/consts.local.d.ts +13 -0
  49. package/build/helpers/changes/consts.local.d.ts.map +1 -0
  50. package/build/helpers/changes/consts.local.js +13 -0
  51. package/build/helpers/changes/consts.local.js.map +1 -0
  52. package/build/helpers/changes/types.d.ts +58 -0
  53. package/build/helpers/changes/types.d.ts.map +1 -0
  54. package/build/helpers/changes/types.js +2 -0
  55. package/build/helpers/changes/types.js.map +1 -0
  56. package/build/helpers/changes.d.ts +11 -42
  57. package/build/helpers/changes.d.ts.map +1 -1
  58. package/build/helpers/changes.js +234 -251
  59. package/build/helpers/changes.js.map +1 -1
  60. package/build/helpers/code/types.d.ts +27 -0
  61. package/build/helpers/code/types.d.ts.map +1 -0
  62. package/build/helpers/code/types.js +2 -0
  63. package/build/helpers/code/types.js.map +1 -0
  64. package/build/helpers/code.d.ts +6 -17
  65. package/build/helpers/code.d.ts.map +1 -1
  66. package/build/helpers/code.js +39 -40
  67. package/build/helpers/code.js.map +1 -1
  68. package/build/helpers/index.d.ts +12 -0
  69. package/build/helpers/index.d.ts.map +1 -1
  70. package/build/helpers/index.js +2 -0
  71. package/build/helpers/index.js.map +1 -1
  72. package/build/helpers/query/types.d.ts +23 -0
  73. package/build/helpers/query/types.d.ts.map +1 -0
  74. package/build/helpers/query/types.js +2 -0
  75. package/build/helpers/query/types.js.map +1 -0
  76. package/build/helpers/query.d.ts +25 -17
  77. package/build/helpers/query.d.ts.map +1 -1
  78. package/build/helpers/query.js +108 -280
  79. package/build/helpers/query.js.map +1 -1
  80. package/build/helpers/scoped/types.d.ts +59 -0
  81. package/build/helpers/scoped/types.d.ts.map +1 -0
  82. package/build/helpers/scoped/types.js +2 -0
  83. package/build/helpers/scoped/types.js.map +1 -0
  84. package/build/helpers/scoped.d.ts +24 -0
  85. package/build/helpers/scoped.d.ts.map +1 -0
  86. package/build/helpers/scoped.js +197 -0
  87. package/build/helpers/scoped.js.map +1 -0
  88. package/build/helpers/specification/types.d.ts +24 -0
  89. package/build/helpers/specification/types.d.ts.map +1 -0
  90. package/build/helpers/specification/types.js +2 -0
  91. package/build/helpers/specification/types.js.map +1 -0
  92. package/build/helpers/specification.d.ts +9 -13
  93. package/build/helpers/specification.d.ts.map +1 -1
  94. package/build/helpers/specification.js +47 -44
  95. package/build/helpers/specification.js.map +1 -1
  96. package/build/helpers/status/types.d.ts +48 -0
  97. package/build/helpers/status/types.d.ts.map +1 -0
  98. package/build/helpers/status/types.js +2 -0
  99. package/build/helpers/status/types.js.map +1 -0
  100. package/build/helpers/status.d.ts +16 -29
  101. package/build/helpers/status.d.ts.map +1 -1
  102. package/build/helpers/status.js +77 -73
  103. package/build/helpers/status.js.map +1 -1
  104. package/build/helpers/types.local.d.ts +17 -0
  105. package/build/helpers/types.local.d.ts.map +1 -0
  106. package/build/helpers/types.local.js +2 -0
  107. package/build/helpers/types.local.js.map +1 -0
  108. package/build/helpers/validate/types.d.ts +30 -0
  109. package/build/helpers/validate/types.d.ts.map +1 -0
  110. package/build/helpers/validate/types.js +2 -0
  111. package/build/helpers/validate/types.js.map +1 -0
  112. package/build/helpers/validate.d.ts +8 -19
  113. package/build/helpers/validate.d.ts.map +1 -1
  114. package/build/helpers/validate.js +74 -79
  115. package/build/helpers/validate.js.map +1 -1
  116. package/build/helpers/wire/types.d.ts +28 -0
  117. package/build/helpers/wire/types.d.ts.map +1 -0
  118. package/build/helpers/wire/types.js +2 -0
  119. package/build/helpers/wire/types.js.map +1 -0
  120. package/build/helpers/wire.d.ts +4 -0
  121. package/build/helpers/wire.d.ts.map +1 -0
  122. package/build/helpers/wire.js +215 -0
  123. package/build/helpers/wire.js.map +1 -0
  124. package/build/i18n/be.json +5 -0
  125. package/build/i18n/de.json +5 -0
  126. package/build/i18n/en.json +5 -0
  127. package/build/i18n/es.json +5 -0
  128. package/build/i18n/fr.json +56 -0
  129. package/build/i18n/pl.json +5 -0
  130. package/build/i18n/ru.json +5 -0
  131. package/build/i18n/uk.json +5 -0
  132. package/build/i18n.js +5 -4
  133. package/build/i18n.js.map +1 -1
  134. package/build/index.d.ts +4 -0
  135. package/build/index.d.ts.map +1 -1
  136. package/build/index.js +2 -0
  137. package/build/index.js.map +1 -1
  138. package/build/models/index.d.ts +2 -3
  139. package/build/models/index.d.ts.map +1 -1
  140. package/build/models/index.js +1 -15
  141. package/build/models/index.js.map +1 -1
  142. package/build/models/model.d.ts +5 -0
  143. package/build/models/model.d.ts.map +1 -0
  144. package/build/models/model.js +16 -0
  145. package/build/models/model.js.map +1 -0
  146. package/build/models/project.d.ts +2 -1
  147. package/build/models/project.d.ts.map +1 -1
  148. package/build/models/project.js.map +1 -1
  149. package/build/models/specification.d.ts +2 -1
  150. package/build/models/specification.d.ts.map +1 -1
  151. package/build/models/specification.js.map +1 -1
  152. package/build/models/types.d.ts +69 -0
  153. package/build/models/types.d.ts.map +1 -0
  154. package/build/models/types.js +2 -0
  155. package/build/models/types.js.map +1 -0
  156. package/build/models/workcard.d.ts +2 -1
  157. package/build/models/workcard.d.ts.map +1 -1
  158. package/build/models/workcard.js +12 -12
  159. package/build/models/workcard.js.map +1 -1
  160. package/build/schemas.d.ts +10 -2
  161. package/build/schemas.d.ts.map +1 -1
  162. package/build/schemas.js +87 -1
  163. package/build/schemas.js.map +1 -1
  164. package/build/types.d.ts +199 -148
  165. package/build/types.d.ts.map +1 -1
  166. package/build/utils.d.ts +6 -0
  167. package/build/utils.d.ts.map +1 -0
  168. package/build/utils.js +6 -0
  169. package/build/utils.js.map +1 -0
  170. package/package.json +11 -11
  171. package/src/aliases/types.ts +38 -0
  172. package/src/aliases.ts +31 -0
  173. package/src/consts.ts +26 -25
  174. package/src/entrypoints/types.ts +68 -0
  175. package/src/entrypoints.ts +34 -14
  176. package/src/errors.ts +71 -0
  177. package/src/helpers/apply/consts.local.ts +10 -0
  178. package/src/helpers/apply/types.ts +32 -0
  179. package/src/helpers/apply.ts +135 -152
  180. package/src/helpers/card/types.ts +20 -0
  181. package/src/helpers/card.ts +59 -33
  182. package/src/helpers/changes/consts.local.ts +17 -0
  183. package/src/helpers/changes/types.ts +66 -0
  184. package/src/helpers/changes.ts +267 -280
  185. package/src/helpers/code/types.ts +31 -0
  186. package/src/helpers/code.ts +52 -48
  187. package/src/helpers/index.ts +13 -0
  188. package/src/helpers/query/types.ts +26 -0
  189. package/src/helpers/query.ts +144 -314
  190. package/src/helpers/scoped/types.ts +64 -0
  191. package/src/helpers/scoped.ts +257 -0
  192. package/src/helpers/specification/types.ts +26 -0
  193. package/src/helpers/specification.ts +69 -51
  194. package/src/helpers/status/types.ts +46 -0
  195. package/src/helpers/status.ts +117 -93
  196. package/src/helpers/types.local.ts +18 -0
  197. package/src/helpers/validate/types.ts +30 -0
  198. package/src/helpers/validate.ts +84 -77
  199. package/src/helpers/wire/types.ts +31 -0
  200. package/src/helpers/wire.ts +251 -0
  201. package/src/i18n/be.json +5 -0
  202. package/src/i18n/de.json +5 -0
  203. package/src/i18n/en.json +5 -0
  204. package/src/i18n/es.json +5 -0
  205. package/src/i18n/fr.json +56 -0
  206. package/src/i18n/pl.json +5 -0
  207. package/src/i18n/ru.json +5 -0
  208. package/src/i18n/uk.json +5 -0
  209. package/src/i18n.ts +6 -4
  210. package/src/index.ts +4 -0
  211. package/src/models/index.ts +2 -17
  212. package/src/models/model.ts +18 -0
  213. package/src/models/project.ts +2 -1
  214. package/src/models/specification.ts +2 -1
  215. package/src/models/types.ts +70 -0
  216. package/src/models/workcard.ts +14 -16
  217. package/src/schemas.ts +98 -2
  218. package/src/types.ts +207 -126
  219. package/src/utils.ts +9 -0
  220. package/tests/apply.spec.ts +95 -31
  221. package/tests/code.spec.ts +9 -9
  222. package/tests/entrypoints.spec.ts +18 -2
  223. package/tests/error-status.spec.ts +6 -1
  224. package/tests/models.spec.ts +16 -15
  225. package/tests/query.spec.ts +13 -15
  226. package/tests/schemas.spec.ts +8 -8
  227. package/tests/scoped.spec.ts +154 -0
  228. package/tests/status.spec.ts +31 -22
@@ -1,17 +1,17 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { IntrinsicStatus, TransitionAction, WorkcardKind } from '../src/consts.js'
3
3
  import { IllegalTransition, PlanningError } from '../src/errors.js'
4
- import { applyRelationship, applyTransition } from '../src/helpers/apply.js'
5
- import { assertMutable, computeChanges, isEmptyChange } from '../src/helpers/changes.js'
6
- import type { Relationship, Specification, Workcard } from '../src/types.js'
4
+ import type { Relationship, Specification, Workcard, WorkcardChanges } from '../src/types.js'
7
5
  import {
8
6
  AT, ENTITY, LATER, PROJECT_TYPE, SPEC_TYPE, STORY_TYPE, TASK_TYPE, makeRegistry, transitionOf,
9
7
  } from './fixtures.js'
8
+ import { applyHelper } from '../src/helpers/apply.js'
9
+ import { changesHelper } from '../src/helpers/changes.js'
10
10
 
11
11
  const registry = makeRegistry()
12
12
 
13
13
  const created = (): Workcard => {
14
- const { changes } = computeChanges(undefined, {
14
+ const { changes } = changesHelper.computeChanges(undefined, {
15
15
  action: TransitionAction.Create,
16
16
  card: {
17
17
  kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'As a user I sign in',
@@ -19,7 +19,7 @@ const created = (): Workcard => {
19
19
  },
20
20
  }, STORY_TYPE, registry, AT)
21
21
 
22
- return applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
22
+ return applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
23
23
  }
24
24
 
25
25
  describe('applyTransition — the fold', () => {
@@ -38,7 +38,7 @@ describe('applyTransition — the fold', () => {
38
38
  test('update merges fields, replaces the rest, clears unset — and never mutates its input', () => {
39
39
  const card = created()
40
40
  const before = structuredClone(card)
41
- const next = applyTransition(card, transitionOf({
41
+ const next = applyHelper.applyTransition(card, transitionOf({
42
42
  card: 'c1', seq: 2, action: TransitionAction.Update, at: LATER,
43
43
  changes: { title: 'Renamed', fields: { warning: 'x' }, labels: ['ui'] },
44
44
  unset: ['code', 'fields.primary'],
@@ -55,23 +55,23 @@ describe('applyTransition — the fold', () => {
55
55
  test('an applied seq returns the card unchanged; a gap or a missing card is refused', () => {
56
56
  const card = { ...created(), head: 3 }
57
57
 
58
- expect(applyTransition(card, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Update }))).toBe(card)
59
- expect(() => applyTransition(card, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update })))
58
+ expect(applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Update }))).toBe(card)
59
+ expect(() => applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update })))
60
60
  .toThrow(PlanningError)
61
- expect(() => applyTransition(undefined, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update })))
61
+ expect(() => applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update })))
62
62
  .toThrow('fold:out-of-order')
63
- expect(applyTransition({ ...card, seq: 1 }, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Link, at: LATER })))
63
+ expect(applyHelper.applyTransition({ ...card, seq: 1 }, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Link, at: LATER })))
64
64
  .toMatchObject({ seq: 2, head: 3, updatedAt: LATER, title: card.title })
65
65
  })
66
66
 
67
67
  test('delete answers null, and a revised document carries its new revision', () => {
68
68
  const card = created()
69
- expect(applyTransition(card, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Delete }))).toBeNull()
69
+ expect(applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Delete }))).toBeNull()
70
70
 
71
71
  const spec = { ...card, kind: WorkcardKind.Specification, category: 'design', format: 'json', body: '{}', revision: 1 } as Specification
72
- const { changes } = computeChanges(spec, { card: 'c1', action: TransitionAction.Update, changes: { body: '{"a":1}' } },
72
+ const { changes } = changesHelper.computeChanges(spec, { card: 'c1', action: TransitionAction.Update, changes: { body: '{"a":1}' } },
73
73
  SPEC_TYPE, registry, LATER, { slot: STORY_TYPE.specifications[0] })
74
- const revised = applyTransition(spec, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update, changes })) as Specification
74
+ const revised = applyHelper.applyTransition(spec, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update, changes })) as Specification
75
75
 
76
76
  expect([revised.revision, revised.body, revised.bodyChars]).toEqual([2, '{"a":1}', 7])
77
77
  })
@@ -79,7 +79,7 @@ describe('applyTransition — the fold', () => {
79
79
 
80
80
  describe('applyRelationship', () => {
81
81
  test('a create folds its links, from the card unless a draft names another end', () => {
82
- const links = applyRelationship([], transitionOf({
82
+ const links = applyHelper.applyRelationship([], transitionOf({
83
83
  id: 't1', card: 'c2', seq: 1, action: TransitionAction.Create, project: 'p1',
84
84
  links: [{ type: 'follows', to: 'c1' }, { type: 'follows', from: 'c0', to: 'c2' }],
85
85
  }))
@@ -92,12 +92,12 @@ describe('applyRelationship', () => {
92
92
 
93
93
  test('link is idempotent and unlink removes only the matching edge', () => {
94
94
  const link = transitionOf({ card: 'c2', seq: 2, action: TransitionAction.Link, link: { type: 'follows', to: 'c1' } })
95
- const once = applyRelationship([], link)
96
- const twice = applyRelationship(once, link)
97
- const other = applyRelationship(twice, { ...link, link: { type: 'blocks', to: 'c1' } })
95
+ const once = applyHelper.applyRelationship([], link)
96
+ const twice = applyHelper.applyRelationship(once, link)
97
+ const other = applyHelper.applyRelationship(twice, { ...link, link: { type: 'blocks', to: 'c1' } })
98
98
 
99
99
  expect(twice).toHaveLength(1)
100
- expect(applyRelationship(other, { ...link, action: TransitionAction.Unlink }).map(edge => edge.type))
100
+ expect(applyHelper.applyRelationship(other, { ...link, action: TransitionAction.Unlink }).map(edge => edge.type))
101
101
  .toEqual(['blocks'])
102
102
  })
103
103
 
@@ -105,7 +105,7 @@ describe('applyRelationship', () => {
105
105
  const edge = (from: string, to: string): Relationship => ({ entityId: ENTITY, type: 'follows', from, to, createdAt: AT })
106
106
  const links = [edge('c1', 'c2'), edge('c3', 'c1'), edge('c3', 'c4')]
107
107
 
108
- expect(applyRelationship(links, transitionOf({ card: 'c1', seq: 5, action: TransitionAction.Delete })))
108
+ expect(applyHelper.applyRelationship(links, transitionOf({ card: 'c1', seq: 5, action: TransitionAction.Delete })))
109
109
  .toEqual([edge('c3', 'c4')])
110
110
  })
111
111
  })
@@ -113,23 +113,23 @@ describe('applyRelationship', () => {
113
113
  describe('computeChanges', () => {
114
114
  test('an update records only what differs, and one that changes nothing is empty', () => {
115
115
  const card = created()
116
- const set = computeChanges(card, {
116
+ const set = changesHelper.computeChanges(card, {
117
117
  card: 'c1', action: TransitionAction.Update,
118
118
  changes: { title: card.title, description: 'New', fields: { area: 'user', primary: true } },
119
119
  }, STORY_TYPE, registry, LATER)
120
120
 
121
121
  expect(set).toEqual({ changes: { description: 'New', fields: { primary: true } }, unset: [] })
122
- expect(isEmptyChange(computeChanges(card, {
122
+ expect(changesHelper.isEmptyChange(changesHelper.computeChanges(card, {
123
123
  card: 'c1', action: TransitionAction.Update, changes: { title: card.title, code: null as never },
124
124
  }, STORY_TYPE, registry, LATER))).toBe(false)
125
- expect(isEmptyChange(computeChanges(card, {
125
+ expect(changesHelper.isEmptyChange(changesHelper.computeChanges(card, {
126
126
  card: 'c1', action: TransitionAction.Update, changes: { title: card.title, description: null as never },
127
127
  }, STORY_TYPE, registry, LATER))).toBe(true)
128
128
  })
129
129
 
130
130
  test('a transit moves the flow, mirrors status and intrinsic, and sets or clears closedAt', () => {
131
131
  const card = { ...created(), status: 'in-progress', flows: { 'test:story': 'in-progress' }, intrinsic: IntrinsicStatus.InProgress }
132
- const completed = computeChanges(card, { card: 'c1', action: TransitionAction.Transit, transition: 'complete' }, STORY_TYPE, registry, LATER)
132
+ const completed = changesHelper.computeChanges(card, { card: 'c1', action: TransitionAction.Transit, transition: 'complete' }, STORY_TYPE, registry, LATER)
133
133
 
134
134
  expect(completed).toEqual({
135
135
  changes: { flows: { 'test:story': 'completed' }, status: 'completed', intrinsic: IntrinsicStatus.Closed, closedAt: LATER },
@@ -137,23 +137,23 @@ describe('computeChanges', () => {
137
137
  })
138
138
 
139
139
  const closed = { ...card, status: 'completed', flows: { 'test:story': 'completed' }, intrinsic: IntrinsicStatus.Closed, closedAt: LATER }
140
- expect(computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'reset' }, STORY_TYPE, registry, LATER).unset)
140
+ expect(changesHelper.computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'reset' }, STORY_TYPE, registry, LATER).unset)
141
141
  .toEqual(['closedAt'])
142
- expect(() => computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'start' }, STORY_TYPE, registry, LATER))
142
+ expect(() => changesHelper.computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'start' }, STORY_TYPE, registry, LATER))
143
143
  .toThrow(IllegalTransition)
144
144
  })
145
145
 
146
146
  test('a non-primary transit under IntrinsicPolicy.All keeps the card open until every flow closes', () => {
147
147
  const task = { ...created(), type: TASK_TYPE.type, status: 'completed', intrinsic: IntrinsicStatus.Planned,
148
148
  flows: { 'test:story': 'completed', 'test:review': 'reviewing' } }
149
- const set = computeChanges(task, { card: 'c1', action: TransitionAction.Transit, transition: 'approve', flow: 'test:review' },
149
+ const set = changesHelper.computeChanges(task, { card: 'c1', action: TransitionAction.Transit, transition: 'approve', flow: 'test:review' },
150
150
  TASK_TYPE, registry, LATER)
151
151
 
152
152
  expect(set.changes).toEqual({ flows: { 'test:review': 'approved' }, intrinsic: IntrinsicStatus.Closed, closedAt: LATER })
153
153
  })
154
154
 
155
155
  test('a create of a revisioned document starts at revision 1 with its body length', () => {
156
- const { changes } = computeChanges(undefined, {
156
+ const { changes } = changesHelper.computeChanges(undefined, {
157
157
  action: TransitionAction.Create,
158
158
  card: { kind: WorkcardKind.Specification, type: SPEC_TYPE.type, parent: 'p1', title: 'scaffold', category: 'scaffold', body: '{}' },
159
159
  }, SPEC_TYPE, registry, AT, { slot: PROJECT_TYPE.specifications[1] })
@@ -164,13 +164,77 @@ describe('computeChanges', () => {
164
164
 
165
165
  describe('assertMutable', () => {
166
166
  test('status moves only through transit, a fixed code stays fixed, derived fields are never supplied', () => {
167
- expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { status: 'completed' } }, STORY_TYPE))
167
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { status: 'completed' } }, STORY_TYPE))
168
168
  .toThrow('immutable:status')
169
- expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'US-X' } }, STORY_TYPE))
169
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'US-X' } }, STORY_TYPE))
170
170
  .toThrow('immutable:code')
171
- expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['title'] }, STORY_TYPE))
171
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['title'] }, STORY_TYPE))
172
172
  .toThrow('immutable:title')
173
- expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'free' } }, TASK_TYPE))
173
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'free' } }, TASK_TYPE))
174
174
  .not.toThrow()
175
175
  })
176
+
177
+ test('provenance is never written after the create: createdBy and createdAt in changes or unset, on any action', () => {
178
+ const draft = { kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'Owned', createdBy: 'owner-1' }
179
+ const refused = [
180
+ { card: 'c1', action: TransitionAction.Update, changes: { createdBy: 'intruder' } },
181
+ { card: 'c1', action: TransitionAction.Update, changes: { createdBy: null } },
182
+ { card: 'c1', action: TransitionAction.Update, unset: ['createdBy'] },
183
+ { card: 'c1', action: TransitionAction.Transit, transition: 'start', changes: { createdBy: 'intruder' } },
184
+ { card: 'c1', action: TransitionAction.Link, link: { type: 'follows', to: 'c2' }, unset: ['createdBy'] },
185
+ { card: draft, action: TransitionAction.Create, changes: { createdBy: 'intruder' } },
186
+ ] as Parameters<typeof changesHelper.assertMutable>[0][]
187
+
188
+ for (const exec of refused) {
189
+ expect(() => changesHelper.assertMutable(exec, STORY_TYPE)).toThrow('planning:immutable:createdBy')
190
+ }
191
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { createdAt: LATER } as WorkcardChanges }, STORY_TYPE))
192
+ .toThrow('planning:immutable:createdAt')
193
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['createdAt'] }, STORY_TYPE))
194
+ .toThrow('planning:immutable:createdAt')
195
+ expect(() => changesHelper.assertMutable({ card: draft, action: TransitionAction.Create }, STORY_TYPE)).not.toThrow()
196
+ expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { createdBy: undefined, title: 'T' } }, STORY_TYPE))
197
+ .not.toThrow()
198
+ })
199
+ })
200
+
201
+ describe('createdBy — written by the create alone', () => {
202
+ const owned = (): Workcard => ({ ...created(), createdBy: 'owner-1' })
203
+
204
+ test('computeChanges takes it from the draft and never from changes or unset', () => {
205
+ const create = changesHelper.computeChanges(undefined, {
206
+ action: TransitionAction.Create,
207
+ card: { kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'Owned', createdBy: 'owner-1' },
208
+ changes: { createdBy: 'intruder' },
209
+ }, STORY_TYPE, registry, AT)
210
+ expect(create.changes.createdBy).toBe('owner-1')
211
+
212
+ const update = changesHelper.computeChanges(owned(), {
213
+ card: 'c1', action: TransitionAction.Update, changes: { createdBy: 'intruder', title: 'Renamed' }, unset: ['createdBy'],
214
+ }, STORY_TYPE, registry, LATER)
215
+ expect(update).toEqual({ changes: { title: 'Renamed' }, unset: [] })
216
+
217
+ const cleared = changesHelper.computeChanges(owned(), { card: 'c1', action: TransitionAction.Update, changes: { createdBy: null } as unknown as WorkcardChanges },
218
+ STORY_TYPE, registry, LATER)
219
+ expect(changesHelper.isEmptyChange(cleared)).toBe(true)
220
+
221
+ const transit = changesHelper.computeChanges(owned(), {
222
+ card: 'c1', action: TransitionAction.Transit, transition: 'start', changes: { createdBy: 'intruder' }, unset: ['createdBy'],
223
+ }, STORY_TYPE, registry, LATER)
224
+ expect(transit.changes.createdBy).toBeUndefined()
225
+ expect(transit.unset).not.toContain('createdBy')
226
+ })
227
+
228
+ test('the fold replays a legacy transition that named it, as written — the refusal is at admission', () => {
229
+ const card = owned()
230
+ const renamed = applyHelper.applyTransition(card, transitionOf({
231
+ card: 'c1', seq: 2, action: TransitionAction.Update, at: LATER, changes: { title: 'Legacy', createdBy: 'intruder' },
232
+ }))!
233
+ const cleared = applyHelper.applyTransition(renamed, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update, unset: ['createdBy'] }))!
234
+
235
+ expect([renamed.seq, renamed.title, renamed.createdBy, renamed.createdAt]).toEqual([2, 'Legacy', 'intruder', AT])
236
+ expect([cleared.seq, cleared.title, cleared.createdBy]).toEqual([3, 'Legacy', undefined])
237
+ expect(applyHelper.applyTransition(cleared, transitionOf({ card: 'c1', seq: 4, action: TransitionAction.Update, changes: { title: 'After' } }))!
238
+ .title).toBe('After')
239
+ })
176
240
  })
@@ -1,14 +1,14 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { CodeScope, CodeStyle } from '../src/consts.js'
3
3
  import { CodeTaken } from '../src/errors.js'
4
- import { codeScopeOf, mintCode } from '../src/helpers/code.js'
5
4
  import { STORY_TYPE } from './fixtures.js'
5
+ import { codeHelper } from '../src/helpers/code.js'
6
6
 
7
7
  const free = () => false
8
8
 
9
9
  describe('mintCode', () => {
10
10
  test('random: prefix plus an upper-cased Base58 part of the policy length', async () => {
11
- const code = await mintCode(STORY_TYPE.code!, free)
11
+ const code = await codeHelper.mintCode(STORY_TYPE.code!, free)
12
12
 
13
13
  expect(code).toMatch(/^US-[1-9A-Z]{5}$/)
14
14
  })
@@ -17,24 +17,24 @@ describe('mintCode', () => {
17
17
  const policy = { style: CodeStyle.Slug, uniqueWithin: CodeScope.Entity }
18
18
  const taken = new Set(['shop-dashboard', 'shop-dashboard-2'])
19
19
 
20
- expect(await mintCode(policy, free, 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard')
21
- expect(await mintCode(policy, code => taken.has(code), 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard-3')
22
- expect(await mintCode(policy, free)).toMatch(/^[a-z]+-[a-z]+$/)
20
+ expect(await codeHelper.mintCode(policy, free, 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard')
21
+ expect(await codeHelper.mintCode(policy, code => taken.has(code), 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard-3')
22
+ expect(await codeHelper.mintCode(policy, free)).toMatch(/^[a-z]+-[a-z]+$/)
23
23
  })
24
24
 
25
25
  test('sequential: counts on from the scope, zero-padded to the length', async () => {
26
26
  const policy = { prefix: 'REQ-', style: CodeStyle.Sequential, length: 3, uniqueWithin: CodeScope.Parent }
27
27
 
28
- expect(await mintCode(policy, free, 8, { count: 41 })).toBe('REQ-042')
29
- expect(await mintCode(policy, async code => code === 'REQ-042', 8, { count: 41 })).toBe('REQ-043')
28
+ expect(await codeHelper.mintCode(policy, free, 8, { count: 41 })).toBe('REQ-042')
29
+ expect(await codeHelper.mintCode(policy, async code => code === 'REQ-042', 8, { count: 41 })).toBe('REQ-043')
30
30
  })
31
31
 
32
32
  test('refuses with CodeTaken after the attempt cap, asking exactly that many times', async () => {
33
33
  let asked = 0
34
34
  const always = async () => { asked++; return true }
35
35
 
36
- await expect(mintCode(STORY_TYPE.code!, always, 3)).rejects.toThrow(CodeTaken)
36
+ await expect(codeHelper.mintCode(STORY_TYPE.code!, always, 3)).rejects.toThrow(CodeTaken)
37
37
  expect(asked).toBe(3)
38
- expect(codeScopeOf(STORY_TYPE.code!, { parent: 'p1' })).toEqual({ parent: 'p1' })
38
+ expect(codeHelper.codeScopeOf(STORY_TYPE.code!, { parent: 'p1' })).toEqual({ parent: 'p1' })
39
39
  })
40
40
  })
@@ -2,14 +2,15 @@ import { describe, expect, test } from 'bun:test'
2
2
  import { protocols } from '@owlmeans/entrypoint'
3
3
  import { RouteMethod, RouteProtocols } from '@owlmeans/route'
4
4
  import { makePlanningProtocols } from '../src/entrypoints.js'
5
- import { PLANNING_PATH, planningAliases } from '../src/consts.js'
5
+ import { PLANNING_PATH } from '../src/consts.js'
6
+ import { planningAliasHelper } from '../src/aliases.js'
6
7
 
7
8
  const BASE = 'app:api:planning'
8
9
 
9
10
  describe('makePlanningProtocols', () => {
10
11
  test('declares every leaf once, each alias derived from the base alias', () => {
11
12
  const tree = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
12
- const aliases = planningAliases(BASE)
13
+ const aliases = planningAliasHelper.planningAliases(BASE)
13
14
  const declared = protocols(tree as never).map(protocol => protocol.alias)
14
15
 
15
16
  expect(new Set(declared).size).toBe(declared.length)
@@ -46,6 +47,21 @@ describe('makePlanningProtocols', () => {
46
47
  expect(tree.card.list.contract?.requestSchemas.query).toBeDefined()
47
48
  })
48
49
 
50
+ test('definitions add the scoped schema list and the schema write; without them neither exists', () => {
51
+ const plain = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
52
+ const scoped = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default', definitions: true })
53
+ const define = planningAliasHelper.planningDefinitionAliases(BASE).define
54
+
55
+ expect(plain.schema.define).toBeUndefined()
56
+ expect(plain.schema.list.contract?.requestSchemas.query).toBeUndefined()
57
+ expect(protocols(plain as never).some(protocol => protocol.alias === define)).toBe(false)
58
+
59
+ expect(scoped.schema.define?.alias).toBe(define)
60
+ expect(scoped.schema.define?.route.route).toMatchObject({ path: '/schemas', method: RouteMethod.POST, parent: BASE })
61
+ expect(scoped.schema.list.contract?.requestSchemas.query).toBeDefined()
62
+ expect(protocols(scoped as never).filter(protocol => protocol.alias === define)).toHaveLength(1)
63
+ })
64
+
49
65
  test('the commit feed is a socket under socketBase when given, under the planning base otherwise', () => {
50
66
  const own = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
51
67
  const shared = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default', socketBase: 'app:update' })
@@ -26,6 +26,11 @@ const DECLARED: Record<string, number | undefined> = {
26
26
  LabelNotAllowed: 422,
27
27
  SpecificationSlotUnknown: 422,
28
28
  RelationshipRefused: 422,
29
+ SchemaConflict: 409,
30
+ SchemaInUse: 409,
31
+ SchemaSealed: 422,
32
+ SchemaInvalid: 422,
33
+ PlanningForbidden: 403,
29
34
  CommitTimeout: undefined,
30
35
  CommitFailed: undefined,
31
36
  PlanningUnsupported: undefined,
@@ -40,7 +45,7 @@ describe('planning refusals — declared HTTP statuses', () => {
40
45
  expect(classes.map(([name]) => name).sort()).toEqual(Object.keys(DECLARED).sort())
41
46
  })
42
47
 
43
- test('an absent card is 404, a card in another state 409, refused content 422; faults declare nothing', () => {
48
+ test('an absent card is 404, a missing grant 403, a card in another state 409, refused content 422; faults declare nothing', () => {
44
49
  for (const [name, Class] of classes) {
45
50
  expect([name, statusOf(new Class('x'))]).toEqual([name, DECLARED[name]])
46
51
  }
@@ -1,16 +1,17 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { applyQuery } from '@owlmeans/resource'
3
2
  import { CommitState, TransitionAction, WorkcardKind } from '../src/consts.js'
4
3
  import { WorkcardNotFound } from '../src/errors.js'
5
- import { applyTransition } from '../src/helpers/apply.js'
6
- import { computeChanges, isEmptyChange } from '../src/helpers/changes.js'
7
- import { criteriaOf, summaryOf } from '../src/helpers/query.js'
8
- import { currentSpecification, slotOf } from '../src/helpers/specification.js'
9
4
  import { makeProjectModel, makeWorkcardModel, modelOf } from '../src/models/index.js'
5
+ import type { ProjectModel } from '../src/models/types.js'
10
6
  import type {
11
7
  PlanningFacade, Project, Specification, TransitionExecution, Workcard, WorkcardDraft,
12
8
  } from '../src/types.js'
13
9
  import { AT, ENTITY, PROJECT_TYPE, STORY_TYPE, makeRegistry } from './fixtures.js'
10
+ import { applyHelper } from '../src/helpers/apply.js'
11
+ import { changesHelper } from '../src/helpers/changes.js'
12
+ import { queryHelper } from '../src/helpers/query.js'
13
+ import { specificationHelper } from '../src/helpers/specification.js'
14
+ import { recordQueryHelper } from '@owlmeans/resource'
14
15
 
15
16
  /**
16
17
  * The smallest facade the models can run against: maps, folded synchronously with the package's
@@ -28,14 +29,14 @@ const makeFacade = () => {
28
29
  cards: {
29
30
  get: async id => cards.get(id) ?? Promise.reject(new WorkcardNotFound(id)),
30
31
  load: async id => cards.get(id) ?? null,
31
- list: async query => applyQuery([...cards.values()], criteriaOf(query, facade.scope)),
32
- count: async query => applyQuery([...cards.values()], criteriaOf(query, facade.scope)).total,
32
+ list: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)),
33
+ count: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).total,
33
34
  summary: async (parents, query) =>
34
- summaryOf(applyQuery([...cards.values()], criteriaOf(query, facade.scope)).items, parents),
35
+ queryHelper.summaryOf(recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).items, parents),
35
36
  },
36
37
  specifications: {
37
- current: async (parent, category) => currentSpecification([...cards.values()].filter(card => card.parent === parent), category),
38
- list: async parent => applyQuery([...cards.values()], { parent, kind: WorkcardKind.Specification }) as never,
38
+ current: async (parent, category) => specificationHelper.currentSpecification([...cards.values()].filter(card => card.parent === parent), category),
39
+ list: async parent => recordQueryHelper.applyQuery([...cards.values()], { parent, kind: WorkcardKind.Specification }) as never,
39
40
  get: async id => cards.get(id) as Specification,
40
41
  revisions: async () => [],
41
42
  },
@@ -53,18 +54,18 @@ const makeFacade = () => {
53
54
  const type = schemas.type(card?.type ?? draft!.type)
54
55
  const parent = cards.get(card?.parent ?? draft?.parent ?? '')
55
56
  const category = (card as Specification | undefined)?.category ?? draft?.category
56
- const slot = parent != null && category != null ? slotOf(schemas.type(parent.type), category) : undefined
57
- const set = computeChanges(card, exec, type, schemas, AT, { slot })
57
+ const slot = parent != null && category != null ? specificationHelper.slotOf(schemas.type(parent.type), category) : undefined
58
+ const set = changesHelper.computeChanges(card, exec, type, schemas, AT, { slot })
58
59
  const id = card?.id ?? `card-${++next}`
59
60
  const transition = {
60
61
  id: `t-${++next}`, entityId: ENTITY, card: id, kind: card?.kind ?? draft!.kind, type: type.type,
61
62
  seq: (card?.head ?? card?.seq ?? 0) + 1, action: exec.action, changes: set.changes, unset: set.unset,
62
63
  actor: {}, at: AT, commit: { state: CommitState.Committed },
63
64
  }
64
- if (exec.action === TransitionAction.Update && isEmptyChange(set)) {
65
+ if (exec.action === TransitionAction.Update && changesHelper.isEmptyChange(set)) {
65
66
  return { transition, card, committed: async () => card ?? null }
66
67
  }
67
- const folded = applyTransition(card, transition)
68
+ const folded = applyHelper.applyTransition(card, transition)
68
69
  folded == null ? cards.delete(id) : cards.set(id, folded)
69
70
  return { transition, card: folded, committed: async () => folded }
70
71
  },
@@ -125,7 +126,7 @@ describe('write', () => {
125
126
  const project = await seed({ kind: WorkcardKind.Project, type: PROJECT_TYPE.type, title: 'Shop' })
126
127
  await seed({ kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: project.id, title: 'Story', fields: { area: 'user', primary: false } })
127
128
 
128
- const model = await facade.model(project) as unknown as ReturnType<typeof makeProjectModel>
129
+ const model = await facade.model(project) as unknown as ProjectModel
129
130
 
130
131
  expect(await model.summary()).toEqual({ total: 1, planned: 1, 'in-progress': 0, closed: 0 })
131
132
  expect((await model.cards()).total).toBe(1)
@@ -1,13 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { applyQuery } from '@owlmeans/resource'
3
2
  import { IntrinsicStatus, WorkcardKind } from '../src/consts.js'
4
3
  import { PlanningError } from '../src/errors.js'
5
- import {
6
- criteriaOf, decodeSummaryQuery, decodeWorkcardQuery, encodeSummaryQuery, encodeWorkcardQuery, listOptionsOf,
7
- summaryOf,
8
- } from '../src/helpers/query.js'
9
4
  import type { Workcard, WorkcardQuery } from '../src/types.js'
10
5
  import { AT, ENTITY } from './fixtures.js'
6
+ import { queryHelper } from '../src/helpers/query.js'
7
+ import { wireHelper } from '../src/helpers/wire.js'
8
+ import { recordQueryHelper } from '@owlmeans/resource'
11
9
 
12
10
  const card = (id: string, extra: Partial<Workcard>): Workcard => ({
13
11
  id, kind: WorkcardKind.Card, type: 'test:story', entityId: ENTITY, title: `Card ${id}`, parent: 'p1',
@@ -23,11 +21,11 @@ const CARDS = [
23
21
  ]
24
22
 
25
23
  const ids = (query: WorkcardQuery): string[] =>
26
- applyQuery(CARDS, criteriaOf(query, { entityId: ENTITY }), listOptionsOf(query)).items.map(item => item.id!)
24
+ recordQueryHelper.applyQuery(CARDS, queryHelper.criteriaOf(query, { entityId: ENTITY }), queryHelper.listOptionsOf(query)).items.map(item => item.id!)
27
25
 
28
26
  describe('criteriaOf', () => {
29
27
  test('translates the table, always scoped to the entity and omitting what is undefined', () => {
30
- expect(criteriaOf({
28
+ expect(queryHelper.criteriaOf({
31
29
  kind: WorkcardKind.Card, type: ['test:story', 'test:task'], parent: 'p1', within: 'p2', status: undefined,
32
30
  labels: ['ui'], ids: ['a'], flow: { id: 'test:story', status: ['planned'] }, fields: { area: 'user' },
33
31
  code: 'US-AAA', updatedSince: AT,
@@ -60,30 +58,30 @@ describe('wire queries', () => {
60
58
  flow: { id: 'test:story', status: ['planned', 'failed'] }, fields: { area: 'user', primary: true },
61
59
  q: 'sign in', page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }],
62
60
  }
63
- const wire = encodeWorkcardQuery(query)
61
+ const wire = wireHelper.encodeWorkcardQuery(query)
64
62
 
65
63
  expect(Object.values(wire).every(value => typeof value === 'string' || typeof value === 'number')).toBe(true)
66
64
  expect(wire.sort).toBe('order,-updatedAt')
67
- expect(criteriaOf(decodeWorkcardQuery(wire), { entityId: ENTITY })).toEqual(criteriaOf(query, { entityId: ENTITY }))
68
- expect(listOptionsOf(decodeWorkcardQuery(wire))).toEqual({ page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }] })
65
+ expect(queryHelper.criteriaOf(wireHelper.decodeWorkcardQuery(wire), { entityId: ENTITY })).toEqual(queryHelper.criteriaOf(query, { entityId: ENTITY }))
66
+ expect(queryHelper.listOptionsOf(wireHelper.decodeWorkcardQuery(wire))).toEqual({ page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }] })
69
67
  })
70
68
 
71
69
  test('decode accepts what a transport may have split already, and coerces numbers', () => {
72
- expect(decodeWorkcardQuery({ status: ['planned', 'failed'], size: '10', labels: 'ui' } as never))
70
+ expect(wireHelper.decodeWorkcardQuery({ status: ['planned', 'failed'], size: '10', labels: 'ui' } as never))
73
71
  .toEqual({ status: ['planned', 'failed'], size: 10, labels: ['ui'] })
74
- expect(decodeSummaryQuery(encodeSummaryQuery({ parents: ['p1', 'p2'], type: 'test:story' })))
72
+ expect(wireHelper.decodeSummaryQuery(wireHelper.encodeSummaryQuery({ parents: ['p1', 'p2'], type: 'test:story' })))
75
73
  .toEqual({ parents: ['p1', 'p2'], type: 'test:story' })
76
74
  })
77
75
 
78
76
  test('a value that does not decode is a malformed query', () => {
79
- expect(() => decodeWorkcardQuery({ fields: '{broken' })).toThrow(PlanningError)
80
- expect(() => decodeWorkcardQuery({ size: 'many' } as never)).toThrow('malformed:query:size')
77
+ expect(() => wireHelper.decodeWorkcardQuery({ fields: '{broken' })).toThrow(PlanningError)
78
+ expect(() => wireHelper.decodeWorkcardQuery({ size: 'many' } as never)).toThrow('malformed:query:size')
81
79
  })
82
80
  })
83
81
 
84
82
  describe('summaryOf', () => {
85
83
  test('counts direct children per parent by intrinsic state, with no key for a parent without any', () => {
86
- expect(summaryOf(CARDS.filter(item => item.entityId === ENTITY), ['p1', 'p3'])).toEqual({
84
+ expect(queryHelper.summaryOf(CARDS.filter(item => item.entityId === ENTITY), ['p1', 'p3'])).toEqual({
87
85
  p1: { total: 2, planned: 1, 'in-progress': 0, closed: 1 },
88
86
  })
89
87
  })
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import * as schemas from '../src/schemas.js'
3
3
  import { TransitionAction, WorkcardKind } from '../src/consts.js'
4
- import { makeAjv, validateCard } from '../src/helpers/validate.js'
5
- import { applyTransition } from '../src/helpers/apply.js'
6
- import { computeChanges } from '../src/helpers/changes.js'
4
+ import { makeAjv, validateHelper } from '../src/helpers/validate.js'
7
5
  import { FieldsInvalid } from '../src/errors.js'
8
6
  import { AT, STORY_TYPE, makeRegistry, transitionOf } from './fixtures.js'
7
+ import { applyHelper } from '../src/helpers/apply.js'
8
+ import { changesHelper } from '../src/helpers/changes.js'
9
9
 
10
10
  type Node = Record<string, unknown>
11
11
 
@@ -53,15 +53,15 @@ describe('schemas', () => {
53
53
  exported.forEach(([, schema]) => ajv.compile(schema as object))
54
54
 
55
55
  const registry = makeRegistry()
56
- const { changes } = computeChanges(undefined, {
56
+ const { changes } = changesHelper.computeChanges(undefined, {
57
57
  action: TransitionAction.Create,
58
58
  card: { kind: WorkcardKind.Card, type: STORY_TYPE.type, title: 'Story', fields: { area: 'user', primary: true } },
59
59
  }, STORY_TYPE, registry, AT)
60
- const card = applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
60
+ const card = applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
61
61
 
62
- expect(() => validateCard(card, registry)).not.toThrow()
63
- expect(() => validateCard({ ...card, fields: { area: 'moon', primary: true } }, registry)).toThrow(FieldsInvalid)
64
- expect(() => validateCard({ ...card, fields: { 'a.b': 1 } }, registry)).toThrow('keys:a.b')
62
+ expect(() => validateHelper.validateCard(card, registry)).not.toThrow()
63
+ expect(() => validateHelper.validateCard({ ...card, fields: { area: 'moon', primary: true } }, registry)).toThrow(FieldsInvalid)
64
+ expect(() => validateHelper.validateCard({ ...card, fields: { 'a.b': 1 } }, registry)).toThrow('keys:a.b')
65
65
  })
66
66
 
67
67
  test('an execute request accepts a card id or a create draft, and refuses an unknown action', () => {