@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,183 +1,166 @@
1
1
  import { IntrinsicStatus, TransitionAction } from '../consts.js'
2
2
  import { PlanningError } from '../errors.js'
3
3
  import type { Relationship, RelationshipDraft, Transition, Workcard } from '../types.js'
4
-
5
- /** Never written by a transition's `changes`: the fold takes them from the transition itself. */
6
- const IDENTITY_KEYS = new Set(['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt'])
7
-
8
- /** Cannot be cleared: a record without them is not a record. */
9
- const REQUIRED_KEYS = new Set([
10
- ...IDENTITY_KEYS, 'title', 'parents', 'status', 'intrinsic', 'flows', 'labels', 'fields',
11
- ])
12
-
13
- /** Merged shallowly into the card's own map rather than replacing it. */
14
- const MERGED_KEYS = new Set(['fields', 'flows'])
4
+ import type { ApplyHelper } from './apply/types.js'
5
+ import { IDENTITY_KEYS, MERGED_KEYS, REQUIRED_KEYS } from './apply/consts.local.js'
15
6
 
16
7
  const clean = <T extends object>(record: T): T =>
17
8
  Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
18
9
 
19
- const outOfOrder = (card: Workcard | null | undefined, transition: Transition): PlanningError =>
20
- new PlanningError(`fold:out-of-order:${transition.card}:${card?.seq ?? 0}:${transition.seq}`)
21
-
22
- const unsetOn = (record: Record<string, unknown>, paths: string[] | undefined): void => {
23
- for (const path of paths ?? []) {
24
- const dot = path.indexOf('.')
25
- if (dot > 0) {
26
- const head = path.slice(0, dot)
27
- const map = record[head]
28
- if (MERGED_KEYS.has(head) && map != null && typeof map === 'object') {
29
- delete (map as Record<string, unknown>)[path.slice(dot + 1)]
10
+ export const createApplyHelper = (): ApplyHelper => {
11
+ const outOfOrder = (card: Workcard | null | undefined, transition: Transition): PlanningError =>
12
+ new PlanningError(`fold:out-of-order:${transition.card}:${card?.seq ?? 0}:${transition.seq}`)
13
+
14
+ const unsetOn = (record: Record<string, unknown>, paths: string[] | undefined): void => {
15
+ for (const path of paths ?? []) {
16
+ const dot = path.indexOf('.')
17
+ if (dot > 0) {
18
+ const head = path.slice(0, dot)
19
+ const map = record[head]
20
+ if (MERGED_KEYS.has(head) && map != null && typeof map === 'object') {
21
+ delete (map as Record<string, unknown>)[path.slice(dot + 1)]
22
+ }
23
+ continue
24
+ }
25
+ if (!REQUIRED_KEYS.has(path)) {
26
+ delete record[path]
30
27
  }
31
- continue
32
- }
33
- if (!REQUIRED_KEYS.has(path)) {
34
- delete record[path]
35
28
  }
36
29
  }
37
- }
38
30
 
39
- const writeChanges = (record: Record<string, unknown>, changes: Record<string, unknown>): void => {
40
- for (const [key, value] of Object.entries(changes)) {
41
- if (value === undefined || IDENTITY_KEYS.has(key)) {
42
- continue
43
- }
44
- if (MERGED_KEYS.has(key)) {
45
- const merged = { ...(record[key] as Record<string, unknown> | undefined ?? {}) }
46
- for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
47
- if (entryValue === null) {
48
- delete merged[entry]
49
- } else if (entryValue !== undefined) {
50
- merged[entry] = entryValue
31
+ const writeChanges = (record: Record<string, unknown>, changes: Record<string, unknown>): void => {
32
+ for (const [key, value] of Object.entries(changes)) {
33
+ if (value === undefined || IDENTITY_KEYS.has(key)) {
34
+ continue
35
+ }
36
+ if (MERGED_KEYS.has(key)) {
37
+ const merged = { ...(record[key] as Record<string, unknown> | undefined ?? {}) }
38
+ for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
39
+ if (entryValue === null) {
40
+ delete merged[entry]
41
+ } else if (entryValue !== undefined) {
42
+ merged[entry] = entryValue
43
+ }
51
44
  }
45
+ record[key] = merged
46
+ continue
52
47
  }
53
- record[key] = merged
54
- continue
55
- }
56
- if (value === null) {
57
- if (!REQUIRED_KEYS.has(key)) {
58
- delete record[key]
48
+ if (value === null) {
49
+ if (!REQUIRED_KEYS.has(key)) {
50
+ delete record[key]
51
+ }
52
+ continue
59
53
  }
60
- continue
54
+ record[key] = value
61
55
  }
62
- record[key] = value
63
56
  }
64
- }
65
57
 
66
- /**
67
- * THE fold: the card after one transition. Pure, total, no I/O — every store (memory, Mongo, a
68
- * client mirror) folds with this and nothing else, so one log always means one card.
69
- *
70
- * - An already-applied transition (`seq <= card.seq`) returns the card unchanged: re-folding is
71
- * safe.
72
- * - Anything but the next seq (`card.seq + 1`, or 1 for a create with no card) is refused with
73
- * `PlanningError('fold:out-of-order')`.
74
- * - `create` builds the whole record from `changes` plus the transition's identity; `delete`
75
- * answers `null`; `link`/`unlink` only move `seq`/`head`/`updatedAt` (the edges are
76
- * {@link applyRelationship}'s); `update`/`transit` write `changes` — `fields` and `flows` merge,
77
- * everything else replaces — and clear `unset`.
78
- * - Every result has `seq = transition.seq`, `head = max(head, seq)`, `updatedAt = transition.at`.
79
- *
80
- * @throws {PlanningError} `planning:fold:out-of-order:…`
81
- */
82
- export const applyTransition = <T extends Workcard = Workcard>(
83
- card: T | null | undefined, transition: Transition
84
- ): T | null => {
85
- if (card != null && transition.seq <= card.seq) {
86
- return card
87
- }
88
- if (card == null) {
89
- if (transition.action === TransitionAction.Delete) {
90
- return null
58
+ const applyTransition = <T extends Workcard = Workcard>(
59
+ card: T | null | undefined, transition: Transition
60
+ ): T | null => {
61
+ if (card != null && transition.seq <= card.seq) {
62
+ return card
91
63
  }
92
- if (transition.action !== TransitionAction.Create || transition.seq !== 1) {
64
+ if (card == null) {
65
+ if (transition.action === TransitionAction.Delete) {
66
+ return null
67
+ }
68
+ if (transition.action !== TransitionAction.Create || transition.seq !== 1) {
69
+ throw outOfOrder(card, transition)
70
+ }
71
+ } else if (transition.seq !== card.seq + 1) {
93
72
  throw outOfOrder(card, transition)
94
73
  }
95
- } else if (transition.seq !== card.seq + 1) {
96
- throw outOfOrder(card, transition)
97
- }
98
74
 
99
- if (transition.action === TransitionAction.Delete) {
100
- return null
101
- }
75
+ if (transition.action === TransitionAction.Delete) {
76
+ return null
77
+ }
78
+
79
+ const head = Math.max(card?.head ?? card?.seq ?? 0, transition.seq)
80
+ const changes = structuredClone(transition.changes ?? {}) as Record<string, unknown>
102
81
 
103
- const head = Math.max(card?.head ?? card?.seq ?? 0, transition.seq)
104
- const changes = structuredClone(transition.changes ?? {}) as Record<string, unknown>
82
+ if (transition.action === TransitionAction.Create) {
83
+ const record: Record<string, unknown> = {
84
+ parents: [], labels: [], fields: {}, flows: {}, status: '', intrinsic: IntrinsicStatus.Planned,
85
+ }
86
+ writeChanges(record, changes)
87
+ unsetOn(record, transition.unset)
88
+
89
+ return clean({
90
+ ...record,
91
+ id: transition.card,
92
+ kind: transition.kind,
93
+ type: transition.type,
94
+ entityId: transition.entityId,
95
+ seq: transition.seq,
96
+ head,
97
+ createdAt: transition.at,
98
+ updatedAt: transition.at,
99
+ }) as unknown as T
100
+ }
105
101
 
106
- if (transition.action === TransitionAction.Create) {
107
- const record: Record<string, unknown> = {
108
- parents: [], labels: [], fields: {}, flows: {}, status: '', intrinsic: IntrinsicStatus.Planned,
102
+ const record = structuredClone(card) as unknown as Record<string, unknown>
103
+ if (transition.action === TransitionAction.Update || transition.action === TransitionAction.Transit) {
104
+ writeChanges(record, changes)
105
+ unsetOn(record, transition.unset)
109
106
  }
110
- writeChanges(record, changes)
111
- unsetOn(record, transition.unset)
112
-
113
- return clean({
114
- ...record,
115
- id: transition.card,
116
- kind: transition.kind,
117
- type: transition.type,
118
- entityId: transition.entityId,
119
- seq: transition.seq,
120
- head,
121
- createdAt: transition.at,
122
- updatedAt: transition.at,
123
- }) as unknown as T
124
- }
107
+ record.seq = transition.seq
108
+ record.head = head
109
+ record.updatedAt = transition.at
125
110
 
126
- const record = structuredClone(card) as unknown as Record<string, unknown>
127
- if (transition.action === TransitionAction.Update || transition.action === TransitionAction.Transit) {
128
- writeChanges(record, changes)
129
- unsetOn(record, transition.unset)
111
+ return clean(record) as unknown as T
130
112
  }
131
- record.seq = transition.seq
132
- record.head = head
133
- record.updatedAt = transition.at
134
-
135
- return clean(record) as unknown as T
136
- }
137
113
 
138
- const edgeOf = (transition: Transition, draft: RelationshipDraft): Relationship => clean({
139
- entityId: transition.entityId,
140
- type: draft.type,
141
- from: draft.from ?? transition.card,
142
- to: draft.to,
143
- project: transition.project,
144
- fields: draft.fields == null ? undefined : structuredClone(draft.fields),
145
- createdAt: transition.at,
146
- transition: transition.id,
147
- })
148
-
149
- const sameEdge = (left: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>, right: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>): boolean =>
150
- left.entityId === right.entityId && left.from === right.from && left.to === right.to && left.type === right.type
151
-
152
- /**
153
- * The relationships after one transition. Pure and total, like {@link applyTransition}.
154
- *
155
- * `create` adds its `links`, `link` adds its `link`, `unlink` removes the matching edge, and a
156
- * `delete` removes every edge touching the card. An edge that already exists is kept as it is, so
157
- * re-folding adds nothing. Every other action answers the list unchanged. Always a new array.
158
- */
159
- export const applyRelationship = (links: Relationship[], transition: Transition): Relationship[] => {
160
- const add = (drafts: RelationshipDraft[]): Relationship[] => drafts.reduce((result, draft) => {
161
- const edge = edgeOf(transition, draft)
162
- return result.some(existing => sameEdge(existing, edge)) ? result : [...result, edge]
163
- }, [...links])
164
-
165
- switch (transition.action) {
166
- case TransitionAction.Create:
167
- return add(transition.links ?? [])
168
- case TransitionAction.Link:
169
- return add(transition.link == null ? [] : [transition.link])
170
- case TransitionAction.Unlink: {
171
- if (transition.link == null) {
172
- return [...links]
114
+ const edgeOf = (transition: Transition, draft: RelationshipDraft): Relationship => clean({
115
+ entityId: transition.entityId,
116
+ type: draft.type,
117
+ from: draft.from ?? transition.card,
118
+ to: draft.to,
119
+ project: transition.project,
120
+ fields: draft.fields == null ? undefined : structuredClone(draft.fields),
121
+ createdAt: transition.at,
122
+ transition: transition.id,
123
+ })
124
+
125
+ const sameEdge = (left: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>, right: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>): boolean =>
126
+ left.entityId === right.entityId && left.from === right.from && left.to === right.to && left.type === right.type
127
+
128
+ const applyRelationship = (links: Relationship[], transition: Transition): Relationship[] => {
129
+ const add = (drafts: RelationshipDraft[]): Relationship[] => drafts.reduce((result, draft) => {
130
+ const edge = edgeOf(transition, draft)
131
+ return result.some(existing => sameEdge(existing, edge)) ? result : [...result, edge]
132
+ }, [...links])
133
+
134
+ switch (transition.action) {
135
+ case TransitionAction.Create:
136
+ return add(transition.links ?? [])
137
+ case TransitionAction.Link:
138
+ return add(transition.link == null ? [] : [transition.link])
139
+ case TransitionAction.Unlink: {
140
+ if (transition.link == null) {
141
+ return [...links]
142
+ }
143
+ const edge = edgeOf(transition, transition.link)
144
+ return links.filter(existing => !sameEdge(existing, edge))
173
145
  }
174
- const edge = edgeOf(transition, transition.link)
175
- return links.filter(existing => !sameEdge(existing, edge))
146
+ case TransitionAction.Delete:
147
+ return links.filter(existing => existing.entityId !== transition.entityId
148
+ || (existing.from !== transition.card && existing.to !== transition.card))
149
+ default:
150
+ return [...links]
176
151
  }
177
- case TransitionAction.Delete:
178
- return links.filter(existing => existing.entityId !== transition.entityId
179
- || (existing.from !== transition.card && existing.to !== transition.card))
180
- default:
181
- return [...links]
182
152
  }
153
+
154
+ return { applyTransition, applyRelationship }
183
155
  }
156
+
157
+ export const applyHelper = createApplyHelper()
158
+
159
+ /** @deprecated compat:factory-refactor — use `applyHelper.applyTransition(…)` */
160
+ export const applyTransition = <T extends Workcard = Workcard>(
161
+ card: T | null | undefined, transition: Transition
162
+ ): T | null => applyHelper.applyTransition<T>(card, transition)
163
+
164
+ /** @deprecated compat:factory-refactor — use `applyHelper.applyRelationship(…)` */
165
+ export const applyRelationship = (links: Relationship[], transition: Transition): Relationship[] =>
166
+ applyHelper.applyRelationship(links, transition)
@@ -0,0 +1,20 @@
1
+ import type { Card, Project, Specification, Workcard, WorkcardDraft } from '../../types.js'
2
+
3
+ /** The kind guards and the parent rules every record keeps. */
4
+ export interface CardHelper {
5
+ isProject: (card?: Workcard | null) => card is Project
6
+ isCard: (card?: Workcard | null) => card is Card
7
+ isSpecification: (card?: Workcard | null) => card is Specification
8
+ /** `parents` with `parent` first and no repeats — the invariant every record keeps. */
9
+ normalizeParents: (parent?: string | null, parents?: string[] | null) => string[]
10
+ /** A draft with the collections every record carries filled in. */
11
+ cardDefaults: (draft: WorkcardDraft) => WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>>
12
+ /** A transition was allocated for the card and has not been folded yet. */
13
+ isPending: (card: Pick<Workcard, 'seq' | 'head'>) => boolean
14
+ parentOf: (card: Workcard) => string | undefined
15
+ /**
16
+ * The project a card belongs to: a project is its own; a card under a project is that project's;
17
+ * anything deeper is its parent's project, which needs the parent to answer.
18
+ */
19
+ projectOf: (card: Workcard, parent?: Workcard | null) => string | undefined
20
+ }
@@ -1,42 +1,68 @@
1
1
  import { WorkcardKind } from '../consts.js'
2
2
  import type { Card, Project, Specification, Workcard, WorkcardDraft } from '../types.js'
3
+ import type { CardHelper } from './card/types.js'
3
4
 
4
- export const isProject = (card?: Workcard | null): card is Project => card?.kind === WorkcardKind.Project
5
+ export const createCardHelper = (): CardHelper => {
6
+ const isProject = (card?: Workcard | null): card is Project => card?.kind === WorkcardKind.Project
5
7
 
6
- export const isCard = (card?: Workcard | null): card is Card => card?.kind === WorkcardKind.Card
8
+ const isCard = (card?: Workcard | null): card is Card => card?.kind === WorkcardKind.Card
7
9
 
8
- export const isSpecification = (card?: Workcard | null): card is Specification =>
9
- card?.kind === WorkcardKind.Specification
10
+ const isSpecification = (card?: Workcard | null): card is Specification =>
11
+ card?.kind === WorkcardKind.Specification
10
12
 
11
- /** `parents` with `parent` first and no repeats — the invariant every record keeps. */
12
- export const normalizeParents = (parent?: string | null, parents?: string[] | null): string[] =>
13
- [...new Set([...(parent != null && parent !== '' ? [parent] : []), ...(parents ?? [])])]
14
- .filter(id => id !== '')
15
-
16
- /** A draft with the collections every record carries filled in. */
17
- export const cardDefaults = (draft: WorkcardDraft): WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>> => ({
18
- ...draft,
19
- parents: normalizeParents(draft.parent, draft.parents),
20
- labels: [...new Set(draft.labels ?? [])],
21
- fields: draft.fields ?? {},
22
- })
23
-
24
- /** A transition was allocated for the card and has not been folded yet. */
25
- export const isPending = (card: Pick<Workcard, 'seq' | 'head'>): boolean => (card.head ?? card.seq) > card.seq
26
-
27
- export const parentOf = (card: Workcard): string | undefined => card.parent ?? card.parents[0]
28
-
29
- /**
30
- * The project a card belongs to: a project is its own; a card under a project is that project's;
31
- * anything deeper is its parent's project, which needs the parent to answer.
32
- */
33
- export const projectOf = (card: Workcard, parent?: Workcard | null): string | undefined => {
34
- if (isProject(card)) {
35
- return card.id
36
- }
37
- if (parent != null) {
38
- return isProject(parent) ? parent.id : parent.parent
13
+ const normalizeParents = (parent?: string | null, parents?: string[] | null): string[] =>
14
+ [...new Set([...(parent != null && parent !== '' ? [parent] : []), ...(parents ?? [])])]
15
+ .filter(id => id !== '')
16
+
17
+ const cardDefaults = (draft: WorkcardDraft): WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>> => ({
18
+ ...draft,
19
+ parents: normalizeParents(draft.parent, draft.parents),
20
+ labels: [...new Set(draft.labels ?? [])],
21
+ fields: draft.fields ?? {},
22
+ })
23
+
24
+ const isPending = (card: Pick<Workcard, 'seq' | 'head'>): boolean => (card.head ?? card.seq) > card.seq
25
+
26
+ const parentOf = (card: Workcard): string | undefined => card.parent ?? card.parents[0]
27
+
28
+ const projectOf = (card: Workcard, parent?: Workcard | null): string | undefined => {
29
+ if (isProject(card)) {
30
+ return card.id
31
+ }
32
+ if (parent != null) {
33
+ return isProject(parent) ? parent.id : parent.parent
34
+ }
35
+
36
+ return card.parent
39
37
  }
40
38
 
41
- return card.parent
39
+ return { isProject, isCard, isSpecification, normalizeParents, cardDefaults, isPending, parentOf, projectOf }
42
40
  }
41
+
42
+ export const cardHelper = createCardHelper()
43
+
44
+ /** @deprecated compat:factory-refactor — use `cardHelper.isProject(…)` */
45
+ export const isProject = (card?: Workcard | null): card is Project => cardHelper.isProject(card)
46
+
47
+ /** @deprecated compat:factory-refactor — use `cardHelper.isCard(…)` */
48
+ export const isCard = (card?: Workcard | null): card is Card => cardHelper.isCard(card)
49
+
50
+ /** @deprecated compat:factory-refactor — use `cardHelper.isSpecification(…)` */
51
+ export const isSpecification = (card?: Workcard | null): card is Specification => cardHelper.isSpecification(card)
52
+
53
+ /** @deprecated compat:factory-refactor — use `cardHelper.normalizeParents(…)` */
54
+ export const normalizeParents = (parent?: string | null, parents?: string[] | null): string[] =>
55
+ cardHelper.normalizeParents(parent, parents)
56
+
57
+ /** @deprecated compat:factory-refactor — use `cardHelper.cardDefaults(…)` */
58
+ export const cardDefaults = (draft: WorkcardDraft): WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>> =>
59
+ cardHelper.cardDefaults(draft)
60
+
61
+ /** @deprecated compat:factory-refactor — use `cardHelper.isPending(…)` */
62
+ export const isPending = (card: Pick<Workcard, 'seq' | 'head'>): boolean => cardHelper.isPending(card)
63
+
64
+ /** @deprecated compat:factory-refactor — use `cardHelper.parentOf(…)` */
65
+ export const parentOf = (card: Workcard): string | undefined => cardHelper.parentOf(card)
66
+
67
+ /** @deprecated compat:factory-refactor — use `cardHelper.projectOf(…)` */
68
+ export const projectOf = (card: Workcard, parent?: Workcard | null): string | undefined => cardHelper.projectOf(card, parent)
@@ -0,0 +1,17 @@
1
+ /** Never in a caller's changes: the transition itself carries them. */
2
+ export const IDENTITY_KEYS = ['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt']
3
+
4
+ /** Written once, from a create's draft; never by `changes` or `unset`, whatever the action. */
5
+ export const PROVENANCE_KEYS = ['createdBy']
6
+
7
+ /** Move only through `transit` (or a create's draft). */
8
+ export const FLOW_KEYS = ['status', 'intrinsic', 'flows', 'closedAt']
9
+
10
+ /** Computed by the executor, never supplied. */
11
+ export const DERIVED_KEYS = ['revision', 'bodyChars']
12
+
13
+ /** Cannot be cleared. */
14
+ export const REQUIRED_KEYS = ['title', 'parents', 'labels', 'fields', 'category', 'format']
15
+
16
+ /** Changing one of these produces a new revision of a revisioned document. */
17
+ export const CONTENT_KEYS = ['body', 'ref', 'format', 'version']
@@ -0,0 +1,66 @@
1
+ import type {
2
+ AnyTypeSchema, SpecificationSlot, TransitionExecution, Workcard, WorkcardChanges,
3
+ } from '../../types.js'
4
+ import type { FlowLookup } from '../types.local.js'
5
+
6
+ export interface ChangeSet {
7
+ changes: WorkcardChanges
8
+ unset: string[]
9
+ /** Transit only: the flow that moved, and the move. */
10
+ flow?: string
11
+ from?: string
12
+ to?: string
13
+ }
14
+
15
+ /** What a transition records — computed from an execution, and the guards it must pass first. */
16
+ export interface ChangesHelper {
17
+ /** Structural equality over JSON values. */
18
+ sameValue: (left: unknown, right: unknown) => boolean
19
+ /** A shallow merge that drops keys patched to `null`. */
20
+ mergeFields: (base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null) => Record<string, unknown>
21
+ /** A copy of the record with the paths (`key`, `fields.key`, `flows.id`) removed. */
22
+ applyUnset: <T extends object>(record: T, unset?: string[]) => T
23
+ /**
24
+ * Refuse an execution that writes what only the transition, the flow or the executor may write.
25
+ *
26
+ * `status`/`intrinsic`/`flows`/`closedAt` move only through `transit` (a create's draft may name
27
+ * the initial status); `code` is fixed once minted unless the type's policy says `mutable`;
28
+ * `category` is fixed; `revision`/`bodyChars` are computed; `createdBy` is the create DRAFT's
29
+ * alone — named in `changes` or `unset` it is refused on every action, so an owner check
30
+ * (`card.createdBy === profileId`) can trust it.
31
+ *
32
+ * @throws {PlanningError} `planning:immutable:<field>`
33
+ */
34
+ assertMutable: (exec: TransitionExecution, type: AnyTypeSchema) => void
35
+ /**
36
+ * The `changes`/`unset` a transition records (plan step 9).
37
+ *
38
+ * - `create`: the whole initial record from the draft (overlaid by `exec.changes`) — `parents`
39
+ * normalized, every flow at its initial status (the draft's `status` for the primary one),
40
+ * `status`/`intrinsic` mirrored, `closedAt` when it starts closed; a specification gets its
41
+ * format, `bodyChars` and revision 1 when the slot is revisioned.
42
+ * - `update`: only what differs; `null` or `exec.unset` clears a field that has a value; a moved
43
+ * `parent` replaces the old one in `parents`; a revisioned document whose content changed gets
44
+ * `revision + 1`. An update that changes nothing is empty ({@link ChangesHelper.isEmptyChange}).
45
+ * - `transit`: `flows[flow] = rule.to`, `status` when the flow is primary, `intrinsic` when it
46
+ * moves, `closedAt` set on entering closed and cleared on leaving it — plus the caller's own
47
+ * changes, diffed as in an update.
48
+ * - `link`, `unlink`, `delete`: nothing.
49
+ *
50
+ * `createdBy` comes from a create's draft only: `changes` and `unset` never carry it, on any action
51
+ * ({@link ChangesHelper.assertMutable} refuses an execution that tries).
52
+ *
53
+ * @throws {IllegalTransition} a transit the flow does not offer from the current status
54
+ * @throws {PlanningError} `malformed:*` for an execution missing what its action needs
55
+ */
56
+ computeChanges: (
57
+ card: Workcard | null | undefined,
58
+ exec: TransitionExecution,
59
+ type: AnyTypeSchema,
60
+ registry: FlowLookup,
61
+ at: string,
62
+ opts?: { slot?: SpecificationSlot | null }
63
+ ) => ChangeSet
64
+ /** Nothing to write: no changed value and nothing to clear. */
65
+ isEmptyChange: (set: Pick<ChangeSet, 'changes' | 'unset'>) => boolean
66
+ }