@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
@@ -0,0 +1,154 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ IntrinsicStatus, PlanningSchemaKind, SchemaOrigin, WorkcardKind,
4
+ } from '../src/consts.js'
5
+ import { PlanningUnsupported, SchemaInvalid, SchemaSealed } from '../src/errors.js'
6
+ import { makeSchemaRegistry } from '../src/registry.js'
7
+ import type {
8
+ ProjectTypeSchema, ScopedSchemaRecord, StatusFlowSchema, WorkcardTypeSchema,
9
+ } from '../src/types.js'
10
+ import { scopedSchemaHelper } from '../src/helpers/scoped.js'
11
+
12
+ /** A community garden: plots are tended through a season flow; a shared tool shed is sealed. */
13
+ const SEASON: StatusFlowSchema = {
14
+ id: 'garden:season',
15
+ version: 1,
16
+ overridable: true,
17
+ statuses: [
18
+ { key: 'fallow', intrinsic: IntrinsicStatus.Planned, initial: true },
19
+ { key: 'growing', intrinsic: IntrinsicStatus.InProgress },
20
+ { key: 'harvested', intrinsic: IntrinsicStatus.Closed },
21
+ ],
22
+ transitions: [
23
+ { name: 'sow', from: ['fallow'], to: 'growing' },
24
+ { name: 'harvest', from: ['growing'], to: 'harvested' },
25
+ ],
26
+ }
27
+
28
+ const ROTA: StatusFlowSchema = {
29
+ id: 'garden:rota',
30
+ version: 1,
31
+ statuses: [{ key: 'open', intrinsic: IntrinsicStatus.Planned, initial: true }],
32
+ transitions: [],
33
+ }
34
+
35
+ const PLOT: WorkcardTypeSchema = {
36
+ type: 'garden:plot', kind: WorkcardKind.Card, version: 1, overridable: true,
37
+ fields: { type: 'object' }, flows: [SEASON.id], specifications: [],
38
+ }
39
+
40
+ const SHED: WorkcardTypeSchema = {
41
+ type: 'garden:shed', kind: WorkcardKind.Card, version: 1,
42
+ fields: { type: 'object' }, flows: [ROTA.id], specifications: [],
43
+ }
44
+
45
+ const GARDEN: ProjectTypeSchema = {
46
+ type: 'garden:site', kind: WorkcardKind.Project, version: 1, fields: { type: 'object' },
47
+ flows: [ROTA.id], specifications: [], cardTypes: [PLOT.type], scopedCardTypes: true,
48
+ }
49
+
50
+ const code = makeSchemaRegistry({ flows: [SEASON, ROTA], types: [PLOT, SHED, GARDEN] })
51
+
52
+ const record = (patch: Partial<ScopedSchemaRecord> & Pick<ScopedSchemaRecord, 'kind' | 'key' | 'definition'>): ScopedSchemaRecord => ({
53
+ entityId: 'org-1', version: 1, createdAt: '2026-09-01T00:00:00.000Z', ...patch,
54
+ })
55
+
56
+ const BED: WorkcardTypeSchema = {
57
+ type: 'garden:bed', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: [SEASON.id], specifications: [],
58
+ }
59
+
60
+ describe('scoped schemas — layering', () => {
61
+ test('an organization record adds a type, a project record overrides it for that project only', () => {
62
+ const records = [
63
+ record({ kind: PlanningSchemaKind.Type, key: BED.type, definition: BED }),
64
+ record({ kind: PlanningSchemaKind.Type, key: BED.type, project: 'site-1', version: 3, definition: { ...BED, label: 'Raised bed' } }),
65
+ ]
66
+
67
+ const shared = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1', revision: 2 }))
68
+ const site = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1', project: 'site-1' }))
69
+ const other = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-2' }))
70
+
71
+ expect(shared.type(BED.type).label).toBeUndefined()
72
+ expect(shared.originOf(PlanningSchemaKind.Type, BED.type)).toBe(SchemaOrigin.Entity)
73
+ expect(shared.revision).toBe(2)
74
+ expect(site.type(BED.type)).toMatchObject({ label: 'Raised bed', version: 3 })
75
+ expect(site.originOf(PlanningSchemaKind.Type, BED.type)).toBe(SchemaOrigin.Project)
76
+ expect(site.originOf(PlanningSchemaKind.Type, PLOT.type)).toBe(SchemaOrigin.Code)
77
+ expect(other.has(BED.type)).toBe(false)
78
+ })
79
+
80
+ test('a sealed code key ignores a record; an overridable one takes it', () => {
81
+ const records = [
82
+ record({ kind: PlanningSchemaKind.Type, key: SHED.type, definition: { ...SHED, label: 'Hijacked' } }),
83
+ record({ kind: PlanningSchemaKind.Type, key: PLOT.type, definition: { ...PLOT, label: 'Allotment' } }),
84
+ record({ kind: PlanningSchemaKind.Type, key: GARDEN.type, definition: { ...BED, type: GARDEN.type } }),
85
+ ]
86
+ const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1' }))
87
+
88
+ expect(view.type(SHED.type).label).toBeUndefined()
89
+ expect(view.type(PLOT.type).label).toBe('Allotment')
90
+ expect(view.type(GARDEN.type).kind).toBe(WorkcardKind.Project)
91
+ expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Type, SHED.type)).toThrow(SchemaSealed)
92
+ expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Type, GARDEN.type)).toThrow(SchemaSealed)
93
+ expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Flow, ROTA.id)).toThrow(SchemaSealed)
94
+ expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Flow, SEASON.id)).not.toThrow()
95
+ })
96
+
97
+ test('a retired record gives way to a live one below it, and resolves retired when none is left', () => {
98
+ const records = [
99
+ record({ kind: PlanningSchemaKind.Type, key: PLOT.type, retired: true, definition: { ...PLOT, label: 'Old' } }),
100
+ record({ kind: PlanningSchemaKind.Type, key: BED.type, retired: true, definition: BED }),
101
+ ]
102
+ const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1' }))
103
+
104
+ expect(view.type(PLOT.type).label).toBeUndefined()
105
+ expect(view.isRetired(PlanningSchemaKind.Type, PLOT.type)).toBe(false)
106
+ expect(view.type(BED.type).type).toBe(BED.type)
107
+ expect(view.isRetired(PlanningSchemaKind.Type, BED.type)).toBe(true)
108
+ expect(view.bundle().retired).toEqual({ types: [BED.type], flows: [] })
109
+ })
110
+
111
+ test('the view is read-only', () => {
112
+ const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), [], { entityId: 'org-1' }))
113
+
114
+ expect(() => view.registerType(BED)).toThrow(PlanningUnsupported)
115
+ expect(() => view.load(code.bundle())).toThrow(PlanningUnsupported)
116
+ expect(view.primaryFlow(PLOT.type).id).toBe(SEASON.id)
117
+ })
118
+
119
+ test('a flow retired with no live fallback is in use while a live type runs it', () => {
120
+ const flow: StatusFlowSchema = { ...ROTA, id: 'garden:watering' }
121
+ const using: WorkcardTypeSchema = { ...BED, type: 'garden:hose', flows: [flow.id] }
122
+ const retiredFlow = record({ kind: PlanningSchemaKind.Flow, key: flow.id, retired: true, definition: flow })
123
+ const live = scopedSchemaHelper.resolveScopedBundle(code.bundle(), [
124
+ retiredFlow, record({ kind: PlanningSchemaKind.Type, key: using.type, definition: using }),
125
+ ], { entityId: 'org-1' })
126
+ const idle = scopedSchemaHelper.resolveScopedBundle(code.bundle(), [
127
+ retiredFlow, record({ kind: PlanningSchemaKind.Type, key: using.type, retired: true, definition: using }),
128
+ ], { entityId: 'org-1' })
129
+
130
+ expect(scopedSchemaHelper.flowInUse(live, flow.id)).toBe(using.type)
131
+ expect(scopedSchemaHelper.flowInUse(idle, flow.id)).toBeUndefined()
132
+ })
133
+ })
134
+
135
+ describe('scoped schemas — closed-form checks', () => {
136
+ test('a flow needs unique statuses, declared targets and a from that is * or declared', () => {
137
+ expect(() => scopedSchemaHelper.assertFlowSchema(SEASON)).not.toThrow()
138
+ expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, statuses: [...SEASON.statuses, SEASON.statuses[0]] }))
139
+ .toThrow('status-repeated:fallow')
140
+ expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, transitions: [{ name: 'compost', from: '*', to: 'compost' }] }))
141
+ .toThrow('compost:to:compost')
142
+ expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, transitions: [{ name: 'sow', from: ['frozen'], to: 'growing' }] }))
143
+ .toThrow('sow:from:frozen')
144
+ expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, statuses: [] })).toThrow(SchemaInvalid)
145
+ })
146
+
147
+ test('a type must be a card with a compiling fields schema and unique flows that resolve', () => {
148
+ expect(() => scopedSchemaHelper.assertTypeSchema(BED, code)).not.toThrow()
149
+ expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, kind: WorkcardKind.Specification }, code)).toThrow('kind:specification')
150
+ expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, fields: { type: 'nonsense' } }, code)).toThrow('fields:')
151
+ expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, flows: [SEASON.id, SEASON.id] }, code)).toThrow('flow-repeated')
152
+ expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, flows: ['garden:frost'] }, code)).toThrow('flow:garden:frost')
153
+ })
154
+ })
@@ -1,55 +1,64 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { IntrinsicStatus } from '../src/consts.js'
3
3
  import { UnknownStatusFlow, UnknownWorkcardType } from '../src/errors.js'
4
- import {
5
- canTransit, initialFlowsOf, initialStatusOf, intrinsicOf, resolveIntrinsic, ruleOf, transitionsFrom,
6
- } from '../src/helpers/status.js'
7
4
  import { PROJECT_TYPE, REVIEW_FLOW, STORY_FLOW, STORY_TYPE, TASK_TYPE, makeRegistry } from './fixtures.js'
5
+ import { statusHelper } from '../src/helpers/status.js'
8
6
 
9
7
  const registry = makeRegistry()
10
8
 
11
9
  describe('status flows', () => {
12
10
  test('every status maps onto its intrinsic state; the initial one is the marked one', () => {
13
- expect(intrinsicOf(STORY_FLOW, 'failed')).toBe(IntrinsicStatus.Planned)
14
- expect(intrinsicOf(STORY_FLOW, 'completed')).toBe(IntrinsicStatus.Closed)
15
- expect(intrinsicOf(STORY_FLOW, 'unknown')).toBeUndefined()
16
- expect(initialStatusOf(REVIEW_FLOW)).toBe('pending')
11
+ expect(statusHelper.intrinsicOf(STORY_FLOW, 'failed')).toBe(IntrinsicStatus.Planned)
12
+ expect(statusHelper.intrinsicOf(STORY_FLOW, 'completed')).toBe(IntrinsicStatus.Closed)
13
+ expect(statusHelper.intrinsicOf(STORY_FLOW, 'unknown')).toBeUndefined()
14
+ expect(statusHelper.initialStatusOf(REVIEW_FLOW)).toBe('pending')
17
15
  })
18
16
 
19
17
  test('a repeated name picks the rule whose from matches, and a wildcard answers last', () => {
20
- expect(ruleOf(STORY_FLOW, 'start', 'failed')?.to).toBe('in-progress')
21
- expect(ruleOf(STORY_FLOW, 'start', 'completed')).toBeUndefined()
22
- expect(ruleOf(REVIEW_FLOW, 'reopen', 'approved')?.to).toBe('reviewing')
23
- expect(ruleOf(REVIEW_FLOW, 'reopen', 'reviewing')?.to).toBe('pending')
18
+ expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'failed')?.to).toBe('in-progress')
19
+ expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'completed')).toBeUndefined()
20
+ expect(statusHelper.ruleOf(REVIEW_FLOW, 'reopen', 'approved')?.to).toBe('reviewing')
21
+ expect(statusHelper.ruleOf(REVIEW_FLOW, 'reopen', 'reviewing')?.to).toBe('pending')
22
+ })
23
+
24
+ test('a status the flow does not declare matches every rule of the name, the first declared answering', () => {
25
+ // A card whose flow changed under it: `abandoned` is no longer a status of the flow.
26
+ expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'abandoned')?.to).toBe('in-progress')
27
+ expect(statusHelper.ruleOf(STORY_FLOW, 'complete', 'abandoned')?.to).toBe('completed')
28
+ expect(statusHelper.ruleOf(STORY_FLOW, 'reset', 'abandoned')?.to).toBe('planned')
29
+ expect(statusHelper.ruleOf(STORY_FLOW, 'nope', 'abandoned')).toBeUndefined()
30
+ // A declared status keeps its own rules only.
31
+ expect(statusHelper.ruleOf(STORY_FLOW, 'complete', 'planned')).toBeUndefined()
32
+ expect(statusHelper.transitionsFrom(STORY_FLOW, 'abandoned').map(rule => rule.name)).toEqual(['start', 'complete', 'fail', 'reset'])
24
33
  })
25
34
 
26
35
  test('canTransit and transitionsFrom read the same rules, one per name', () => {
27
- expect(canTransit(STORY_FLOW, 'reset', 'planned')).toBe(true)
28
- expect(canTransit(STORY_FLOW, 'complete', 'planned')).toBe(false)
29
- expect(transitionsFrom(STORY_FLOW, 'in-progress').map(rule => rule.name)).toEqual(['complete', 'fail', 'reset'])
30
- expect(transitionsFrom(STORY_FLOW, 'failed', { explicit: true }).map(rule => `${rule.name}→${rule.to}`))
36
+ expect(statusHelper.canTransit(STORY_FLOW, 'reset', 'planned')).toBe(true)
37
+ expect(statusHelper.canTransit(STORY_FLOW, 'complete', 'planned')).toBe(false)
38
+ expect(statusHelper.transitionsFrom(STORY_FLOW, 'in-progress').map(rule => rule.name)).toEqual(['complete', 'fail', 'reset'])
39
+ expect(statusHelper.transitionsFrom(STORY_FLOW, 'failed', { explicit: true }).map(rule => `${rule.name}→${rule.to}`))
31
40
  .toEqual(['start→in-progress', 'reset→planned'])
32
41
  })
33
42
  })
34
43
 
35
44
  describe('intrinsic resolution', () => {
36
45
  test('primary policy reads the primary flow only', () => {
37
- expect(resolveIntrinsic(STORY_TYPE, { 'test:story': 'in-progress' }, registry)).toBe(IntrinsicStatus.InProgress)
38
- expect(resolveIntrinsic(PROJECT_TYPE, { 'test:story': 'completed' }, registry)).toBe(IntrinsicStatus.Closed)
46
+ expect(statusHelper.resolveIntrinsic(STORY_TYPE, { 'test:story': 'in-progress' }, registry)).toBe(IntrinsicStatus.InProgress)
47
+ expect(statusHelper.resolveIntrinsic(PROJECT_TYPE, { 'test:story': 'completed' }, registry)).toBe(IntrinsicStatus.Closed)
39
48
  })
40
49
 
41
50
  test('IntrinsicPolicy.All takes the least advanced flow', () => {
42
- expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'reviewing' }, registry))
51
+ expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'reviewing' }, registry))
43
52
  .toBe(IntrinsicStatus.InProgress)
44
- expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'approved' }, registry))
53
+ expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'approved' }, registry))
45
54
  .toBe(IntrinsicStatus.Closed)
46
- expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'failed', 'test:review': 'approved' }, registry))
55
+ expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'failed', 'test:review': 'approved' }, registry))
47
56
  .toBe(IntrinsicStatus.Planned)
48
57
  })
49
58
 
50
59
  test('initialFlowsOf starts every flow at its initial status, the primary at the draft status', () => {
51
- expect(initialFlowsOf(TASK_TYPE, registry)).toEqual({ 'test:story': 'planned', 'test:review': 'pending' })
52
- expect(initialFlowsOf(TASK_TYPE, registry, 'failed')).toEqual({ 'test:story': 'failed', 'test:review': 'pending' })
60
+ expect(statusHelper.initialFlowsOf(TASK_TYPE, registry)).toEqual({ 'test:story': 'planned', 'test:review': 'pending' })
61
+ expect(statusHelper.initialFlowsOf(TASK_TYPE, registry, 'failed')).toEqual({ 'test:story': 'failed', 'test:review': 'pending' })
53
62
  })
54
63
  })
55
64