@owlmeans/planning 0.1.18-rc.9 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +122 -29
  2. package/agent-meta/manifest.json +131 -4
  3. package/agent-meta/skills/planning/SKILL.md +78 -171
  4. package/agent-meta/skills/planning/parts/collaboration.md +67 -0
  5. package/agent-meta/skills/planning/parts/errors.md +51 -0
  6. package/agent-meta/skills/planning/parts/fold.md +69 -0
  7. package/agent-meta/skills/planning/parts/mounting.md +64 -0
  8. package/agent-meta/skills/planning/parts/types.md +141 -0
  9. package/build/aliases/types.d.ts +72 -0
  10. package/build/aliases/types.d.ts.map +1 -0
  11. package/build/aliases/types.js +2 -0
  12. package/build/aliases/types.js.map +1 -0
  13. package/build/aliases.d.ts +4 -0
  14. package/build/aliases.d.ts.map +1 -0
  15. package/build/aliases.js +28 -0
  16. package/build/aliases.js.map +1 -0
  17. package/build/consts.d.ts +43 -34
  18. package/build/consts.d.ts.map +1 -1
  19. package/build/consts.js +49 -25
  20. package/build/consts.js.map +1 -1
  21. package/build/entrypoints/types.d.ts +105 -0
  22. package/build/entrypoints/types.d.ts.map +1 -0
  23. package/build/entrypoints/types.js +2 -0
  24. package/build/entrypoints/types.js.map +1 -0
  25. package/build/entrypoints.d.ts +5 -1
  26. package/build/entrypoints.d.ts.map +1 -1
  27. package/build/entrypoints.js +24 -6
  28. package/build/entrypoints.js.map +1 -1
  29. package/build/errors.d.ts +41 -0
  30. package/build/errors.d.ts.map +1 -1
  31. package/build/errors.js +61 -0
  32. package/build/errors.js.map +1 -1
  33. package/build/helpers/app/consts.local.d.ts +5 -0
  34. package/build/helpers/app/consts.local.d.ts.map +1 -0
  35. package/build/helpers/app/consts.local.js +5 -0
  36. package/build/helpers/app/consts.local.js.map +1 -0
  37. package/build/helpers/app/types.d.ts +53 -0
  38. package/build/helpers/app/types.d.ts.map +1 -0
  39. package/build/helpers/app/types.js +2 -0
  40. package/build/helpers/app/types.js.map +1 -0
  41. package/build/helpers/app.d.ts +4 -0
  42. package/build/helpers/app.d.ts.map +1 -0
  43. package/build/helpers/app.js +139 -0
  44. package/build/helpers/app.js.map +1 -0
  45. package/build/helpers/apply/consts.local.d.ts +7 -0
  46. package/build/helpers/apply/consts.local.d.ts.map +1 -0
  47. package/build/helpers/apply/consts.local.js +9 -0
  48. package/build/helpers/apply/consts.local.js.map +1 -0
  49. package/build/helpers/apply/types.d.ts +32 -0
  50. package/build/helpers/apply/types.d.ts.map +1 -0
  51. package/build/helpers/apply/types.js +2 -0
  52. package/build/helpers/apply/types.js.map +1 -0
  53. package/build/helpers/apply.d.ts +5 -23
  54. package/build/helpers/apply.d.ts.map +1 -1
  55. package/build/helpers/apply.js +127 -144
  56. package/build/helpers/apply.js.map +1 -1
  57. package/build/helpers/card/types.d.ts +20 -0
  58. package/build/helpers/card/types.d.ts.map +1 -0
  59. package/build/helpers/card/types.js +2 -0
  60. package/build/helpers/card/types.js.map +1 -0
  61. package/build/helpers/card.d.ts +11 -7
  62. package/build/helpers/card.d.ts.map +1 -1
  63. package/build/helpers/card.js +41 -28
  64. package/build/helpers/card.js.map +1 -1
  65. package/build/helpers/changes/consts.local.d.ts +13 -0
  66. package/build/helpers/changes/consts.local.d.ts.map +1 -0
  67. package/build/helpers/changes/consts.local.js +13 -0
  68. package/build/helpers/changes/consts.local.js.map +1 -0
  69. package/build/helpers/changes/types.d.ts +58 -0
  70. package/build/helpers/changes/types.d.ts.map +1 -0
  71. package/build/helpers/changes/types.js +2 -0
  72. package/build/helpers/changes/types.js.map +1 -0
  73. package/build/helpers/changes.d.ts +11 -42
  74. package/build/helpers/changes.d.ts.map +1 -1
  75. package/build/helpers/changes.js +237 -251
  76. package/build/helpers/changes.js.map +1 -1
  77. package/build/helpers/code/types.d.ts +27 -0
  78. package/build/helpers/code/types.d.ts.map +1 -0
  79. package/build/helpers/code/types.js +2 -0
  80. package/build/helpers/code/types.js.map +1 -0
  81. package/build/helpers/code.d.ts +6 -17
  82. package/build/helpers/code.d.ts.map +1 -1
  83. package/build/helpers/code.js +39 -40
  84. package/build/helpers/code.js.map +1 -1
  85. package/build/helpers/index.d.ts +16 -0
  86. package/build/helpers/index.d.ts.map +1 -1
  87. package/build/helpers/index.js +4 -0
  88. package/build/helpers/index.js.map +1 -1
  89. package/build/helpers/query/types.d.ts +23 -0
  90. package/build/helpers/query/types.d.ts.map +1 -0
  91. package/build/helpers/query/types.js +2 -0
  92. package/build/helpers/query/types.js.map +1 -0
  93. package/build/helpers/query.d.ts +25 -17
  94. package/build/helpers/query.d.ts.map +1 -1
  95. package/build/helpers/query.js +108 -280
  96. package/build/helpers/query.js.map +1 -1
  97. package/build/helpers/reply/consts.d.ts +2 -0
  98. package/build/helpers/reply/consts.d.ts.map +1 -0
  99. package/build/helpers/reply/consts.js +2 -0
  100. package/build/helpers/reply/consts.js.map +1 -0
  101. package/build/helpers/reply/types.d.ts +10 -0
  102. package/build/helpers/reply/types.d.ts.map +1 -0
  103. package/build/helpers/reply/types.js +2 -0
  104. package/build/helpers/reply/types.js.map +1 -0
  105. package/build/helpers/reply.d.ts +5 -0
  106. package/build/helpers/reply.d.ts.map +1 -0
  107. package/build/helpers/reply.js +29 -0
  108. package/build/helpers/reply.js.map +1 -0
  109. package/build/helpers/scoped/types.d.ts +73 -0
  110. package/build/helpers/scoped/types.d.ts.map +1 -0
  111. package/build/helpers/scoped/types.js +2 -0
  112. package/build/helpers/scoped/types.js.map +1 -0
  113. package/build/helpers/scoped.d.ts +25 -0
  114. package/build/helpers/scoped.d.ts.map +1 -0
  115. package/build/helpers/scoped.js +223 -0
  116. package/build/helpers/scoped.js.map +1 -0
  117. package/build/helpers/specification/types.d.ts +24 -0
  118. package/build/helpers/specification/types.d.ts.map +1 -0
  119. package/build/helpers/specification/types.js +2 -0
  120. package/build/helpers/specification/types.js.map +1 -0
  121. package/build/helpers/specification.d.ts +9 -13
  122. package/build/helpers/specification.d.ts.map +1 -1
  123. package/build/helpers/specification.js +47 -44
  124. package/build/helpers/specification.js.map +1 -1
  125. package/build/helpers/status/types.d.ts +48 -0
  126. package/build/helpers/status/types.d.ts.map +1 -0
  127. package/build/helpers/status/types.js +2 -0
  128. package/build/helpers/status/types.js.map +1 -0
  129. package/build/helpers/status.d.ts +16 -29
  130. package/build/helpers/status.d.ts.map +1 -1
  131. package/build/helpers/status.js +77 -73
  132. package/build/helpers/status.js.map +1 -1
  133. package/build/helpers/types.local.d.ts +17 -0
  134. package/build/helpers/types.local.d.ts.map +1 -0
  135. package/build/helpers/types.local.js +2 -0
  136. package/build/helpers/types.local.js.map +1 -0
  137. package/build/helpers/validate/types.d.ts +31 -0
  138. package/build/helpers/validate/types.d.ts.map +1 -0
  139. package/build/helpers/validate/types.js +2 -0
  140. package/build/helpers/validate/types.js.map +1 -0
  141. package/build/helpers/validate.d.ts +8 -19
  142. package/build/helpers/validate.d.ts.map +1 -1
  143. package/build/helpers/validate.js +79 -79
  144. package/build/helpers/validate.js.map +1 -1
  145. package/build/helpers/wire/types.d.ts +28 -0
  146. package/build/helpers/wire/types.d.ts.map +1 -0
  147. package/build/helpers/wire/types.js +2 -0
  148. package/build/helpers/wire/types.js.map +1 -0
  149. package/build/helpers/wire.d.ts +4 -0
  150. package/build/helpers/wire.d.ts.map +1 -0
  151. package/build/helpers/wire.js +216 -0
  152. package/build/helpers/wire.js.map +1 -0
  153. package/build/i18n/be.json +5 -0
  154. package/build/i18n/de.json +5 -0
  155. package/build/i18n/en.json +5 -0
  156. package/build/i18n/es.json +5 -0
  157. package/build/i18n/fr.json +5 -0
  158. package/build/i18n/pl.json +5 -0
  159. package/build/i18n/ru.json +5 -0
  160. package/build/i18n/uk.json +5 -0
  161. package/build/i18n.js +3 -3
  162. package/build/i18n.js.map +1 -1
  163. package/build/index.d.ts +7 -0
  164. package/build/index.d.ts.map +1 -1
  165. package/build/index.js +3 -0
  166. package/build/index.js.map +1 -1
  167. package/build/models/index.d.ts +2 -3
  168. package/build/models/index.d.ts.map +1 -1
  169. package/build/models/index.js +1 -15
  170. package/build/models/index.js.map +1 -1
  171. package/build/models/model.d.ts +5 -0
  172. package/build/models/model.d.ts.map +1 -0
  173. package/build/models/model.js +16 -0
  174. package/build/models/model.js.map +1 -0
  175. package/build/models/project.d.ts +2 -1
  176. package/build/models/project.d.ts.map +1 -1
  177. package/build/models/project.js.map +1 -1
  178. package/build/models/specification.d.ts +2 -1
  179. package/build/models/specification.d.ts.map +1 -1
  180. package/build/models/specification.js.map +1 -1
  181. package/build/models/types.d.ts +69 -0
  182. package/build/models/types.d.ts.map +1 -0
  183. package/build/models/types.js +2 -0
  184. package/build/models/types.js.map +1 -0
  185. package/build/models/workcard.d.ts +2 -1
  186. package/build/models/workcard.d.ts.map +1 -1
  187. package/build/models/workcard.js +12 -12
  188. package/build/models/workcard.js.map +1 -1
  189. package/build/registry.d.ts +3 -2
  190. package/build/registry.d.ts.map +1 -1
  191. package/build/registry.js +51 -3
  192. package/build/registry.js.map +1 -1
  193. package/build/resources/mentions.d.ts +4 -0
  194. package/build/resources/mentions.d.ts.map +1 -0
  195. package/build/resources/mentions.js +15 -0
  196. package/build/resources/mentions.js.map +1 -0
  197. package/build/resources/protocol/types.d.ts +30 -0
  198. package/build/resources/protocol/types.d.ts.map +1 -0
  199. package/build/resources/protocol/types.js +2 -0
  200. package/build/resources/protocol/types.js.map +1 -0
  201. package/build/resources/protocol.d.ts +6 -0
  202. package/build/resources/protocol.d.ts.map +1 -0
  203. package/build/resources/protocol.js +31 -0
  204. package/build/resources/protocol.js.map +1 -0
  205. package/build/resources/types.d.ts +146 -0
  206. package/build/resources/types.d.ts.map +1 -0
  207. package/build/resources/types.js +2 -0
  208. package/build/resources/types.js.map +1 -0
  209. package/build/resources/types.local.d.ts +6 -0
  210. package/build/resources/types.local.d.ts.map +1 -0
  211. package/build/resources/types.local.js +2 -0
  212. package/build/resources/types.local.js.map +1 -0
  213. package/build/schemas.d.ts +21 -2
  214. package/build/schemas.d.ts.map +1 -1
  215. package/build/schemas.js +164 -2
  216. package/build/schemas.js.map +1 -1
  217. package/build/types.d.ts +330 -151
  218. package/build/types.d.ts.map +1 -1
  219. package/build/utils.d.ts +6 -0
  220. package/build/utils.d.ts.map +1 -0
  221. package/build/utils.js +6 -0
  222. package/build/utils.js.map +1 -0
  223. package/package.json +13 -13
  224. package/src/aliases/types.ts +41 -0
  225. package/src/aliases.ts +34 -0
  226. package/src/consts.ts +50 -25
  227. package/src/entrypoints/types.ts +77 -0
  228. package/src/entrypoints.ts +53 -24
  229. package/src/errors.ts +71 -0
  230. package/src/helpers/app/consts.local.ts +5 -0
  231. package/src/helpers/app/types.ts +54 -0
  232. package/src/helpers/app.ts +158 -0
  233. package/src/helpers/apply/consts.local.ts +10 -0
  234. package/src/helpers/apply/types.ts +32 -0
  235. package/src/helpers/apply.ts +141 -153
  236. package/src/helpers/card/types.ts +20 -0
  237. package/src/helpers/card.ts +59 -33
  238. package/src/helpers/changes/consts.local.ts +17 -0
  239. package/src/helpers/changes/types.ts +66 -0
  240. package/src/helpers/changes.ts +270 -280
  241. package/src/helpers/code/types.ts +31 -0
  242. package/src/helpers/code.ts +52 -48
  243. package/src/helpers/index.ts +17 -0
  244. package/src/helpers/query/types.ts +26 -0
  245. package/src/helpers/query.ts +144 -314
  246. package/src/helpers/reply/consts.ts +1 -0
  247. package/src/helpers/reply/types.ts +13 -0
  248. package/src/helpers/reply.ts +27 -0
  249. package/src/helpers/scoped/types.ts +78 -0
  250. package/src/helpers/scoped.ts +281 -0
  251. package/src/helpers/specification/types.ts +26 -0
  252. package/src/helpers/specification.ts +69 -51
  253. package/src/helpers/status/types.ts +46 -0
  254. package/src/helpers/status.ts +117 -93
  255. package/src/helpers/types.local.ts +18 -0
  256. package/src/helpers/validate/types.ts +31 -0
  257. package/src/helpers/validate.ts +89 -77
  258. package/src/helpers/wire/types.ts +31 -0
  259. package/src/helpers/wire.ts +252 -0
  260. package/src/i18n/be.json +5 -0
  261. package/src/i18n/de.json +5 -0
  262. package/src/i18n/en.json +5 -0
  263. package/src/i18n/es.json +5 -0
  264. package/src/i18n/fr.json +5 -0
  265. package/src/i18n/pl.json +5 -0
  266. package/src/i18n/ru.json +5 -0
  267. package/src/i18n/uk.json +5 -0
  268. package/src/i18n.ts +4 -3
  269. package/src/index.ts +8 -0
  270. package/src/models/index.ts +2 -17
  271. package/src/models/model.ts +18 -0
  272. package/src/models/project.ts +2 -1
  273. package/src/models/specification.ts +2 -1
  274. package/src/models/types.ts +70 -0
  275. package/src/models/workcard.ts +14 -16
  276. package/src/registry.ts +55 -4
  277. package/src/resources/mentions.ts +11 -0
  278. package/src/resources/protocol/types.ts +21 -0
  279. package/src/resources/protocol.ts +36 -0
  280. package/src/resources/types.local.ts +5 -0
  281. package/src/resources/types.ts +148 -0
  282. package/src/schemas.ts +179 -4
  283. package/src/types.ts +334 -130
  284. package/src/utils.ts +9 -0
  285. package/tests/app.spec.ts +188 -0
  286. package/tests/apply.spec.ts +95 -31
  287. package/tests/code.spec.ts +9 -9
  288. package/tests/csp.spec.ts +122 -0
  289. package/tests/entrypoints.spec.ts +18 -2
  290. package/tests/error-status.spec.ts +6 -1
  291. package/tests/models.spec.ts +24 -16
  292. package/tests/query.spec.ts +13 -15
  293. package/tests/schemas.spec.ts +8 -8
  294. package/tests/scoped.spec.ts +161 -0
  295. package/tests/status.spec.ts +31 -22
@@ -1,333 +1,323 @@
1
1
  import { IntrinsicStatus, SpecificationFormat, TransitionAction, WorkcardKind } from '../consts.js'
2
2
  import { IllegalTransition, PlanningError } from '../errors.js'
3
- import type {
4
- AnyTypeSchema, PlanningSchemaRegistry, Specification, SpecificationSlot, TransitionExecution,
5
- Workcard, WorkcardChanges, WorkcardDraft,
6
- } from '../types.js'
7
- import { normalizeParents } from './card.js'
8
- import { bodyCharsOf, nextRevision } from './specification.js'
9
- import { initialFlowsOf, initialStatusOf, primaryFlowOf, resolveIntrinsic, ruleOf } from './status.js'
10
-
11
- type FlowLookup = Pick<PlanningSchemaRegistry, 'flow'>
12
-
13
- export interface ChangeSet {
14
- changes: WorkcardChanges
15
- unset: string[]
16
- /** Transit only: the flow that moved, and the move. */
17
- flow?: string
18
- from?: string
19
- to?: string
20
- }
21
-
22
- /** Never in a caller's changes: the transition itself carries them. */
23
- const IDENTITY_KEYS = ['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt']
24
- /** Move only through `transit` (or a create's draft). */
25
- const FLOW_KEYS = ['status', 'intrinsic', 'flows', 'closedAt']
26
- /** Computed by the executor, never supplied. */
27
- const DERIVED_KEYS = ['revision', 'bodyChars']
28
- /** Cannot be cleared. */
29
- const REQUIRED_KEYS = ['title', 'parents', 'labels', 'fields', 'category', 'format']
30
- /** Changing one of these produces a new revision of a revisioned document. */
31
- const CONTENT_KEYS = ['body', 'ref', 'format', 'version']
3
+ import type { AnyTypeSchema, Specification, SpecificationSlot, TransitionExecution, Workcard, WorkcardChanges, WorkcardDraft } from '../types.js'
4
+ import { cardHelper } from './card.js'
5
+ import type { ChangeSet, ChangesHelper } from './changes/types.js'
6
+ import { CONTENT_KEYS, DERIVED_KEYS, FLOW_KEYS, IDENTITY_KEYS, PROVENANCE_KEYS, REQUIRED_KEYS } from './changes/consts.local.js'
7
+ import { specificationHelper } from './specification.js'
8
+ import { statusHelper } from './status.js'
9
+ import type { FlowLookup } from './types.local.js'
32
10
 
33
11
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
34
12
  value != null && typeof value === 'object' && !Array.isArray(value)
35
13
 
36
- /** Structural equality over JSON values. */
37
- export const sameValue = (left: unknown, right: unknown): boolean => {
38
- if (left === right) {
39
- return true
40
- }
41
- if (Array.isArray(left) && Array.isArray(right)) {
42
- return left.length === right.length && left.every((entry, index) => sameValue(entry, right[index]))
43
- }
44
- if (isPlainObject(left) && isPlainObject(right)) {
45
- const keys = Object.keys(left).filter(key => left[key] !== undefined)
46
- const other = Object.keys(right).filter(key => right[key] !== undefined)
47
- return keys.length === other.length && keys.every(key => sameValue(left[key], right[key]))
48
- }
49
-
50
- return false
51
- }
52
-
53
14
  const clean = <T extends object>(record: T): T =>
54
15
  Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
55
16
 
56
- /** A shallow merge that drops keys patched to `null`. */
57
- export const mergeFields = (
58
- base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null
59
- ): Record<string, unknown> => Object.fromEntries(Object.entries({ ...(base ?? {}), ...(patch ?? {}) })
60
- .filter(([, value]) => value !== null && value !== undefined))
61
-
62
- /** A copy of the record with the paths (`key`, `fields.key`, `flows.id`) removed. */
63
- export const applyUnset = <T extends object>(record: T, unset?: string[]): T => {
64
- const copy = structuredClone(record) as Record<string, unknown>
65
- for (const path of unset ?? []) {
66
- const [head, ...rest] = path.split('.')
67
- if (rest.length > 0) {
68
- const map = copy[head]
69
- if (isPlainObject(map)) {
70
- delete map[rest.join('.')]
71
- }
72
- } else {
73
- delete copy[head]
17
+ export const createChangesHelper = (): ChangesHelper => {
18
+ const sameValue = (left: unknown, right: unknown): boolean => {
19
+ if (left === right) {
20
+ return true
21
+ }
22
+ if (Array.isArray(left) && Array.isArray(right)) {
23
+ return left.length === right.length && left.every((entry, index) => sameValue(entry, right[index]))
24
+ }
25
+ if (isPlainObject(left) && isPlainObject(right)) {
26
+ const keys = Object.keys(left).filter(key => left[key] !== undefined)
27
+ const other = Object.keys(right).filter(key => right[key] !== undefined)
28
+ return keys.length === other.length && keys.every(key => sameValue(left[key], right[key]))
74
29
  }
30
+
31
+ return false
75
32
  }
76
33
 
77
- return copy as T
78
- }
34
+ const mergeFields = (
35
+ base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null
36
+ ): Record<string, unknown> => Object.fromEntries(Object.entries({ ...(base ?? {}), ...(patch ?? {}) })
37
+ .filter(([, value]) => value !== null && value !== undefined))
38
+
39
+ const applyUnset = <T extends object>(record: T, unset?: string[]): T => {
40
+ const copy = structuredClone(record) as Record<string, unknown>
41
+ for (const path of unset ?? []) {
42
+ const [head, ...rest] = path.split('.')
43
+ if (rest.length > 0) {
44
+ const map = copy[head]
45
+ if (isPlainObject(map)) {
46
+ delete map[rest.join('.')]
47
+ }
48
+ } else {
49
+ delete copy[head]
50
+ }
51
+ }
79
52
 
80
- /**
81
- * Refuse an execution that writes what only the transition, the flow or the executor may write.
82
- *
83
- * `status`/`intrinsic`/`flows`/`closedAt` move only through `transit` (a create's draft may name
84
- * the initial status); `code` is fixed once minted unless the type's policy says `mutable`;
85
- * `category` is fixed; `revision`/`bodyChars` are computed.
86
- *
87
- * @throws {PlanningError} `planning:immutable:<field>`
88
- */
89
- export const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => {
90
- const create = exec.action === TransitionAction.Create
91
- const codeFixed = type.code != null && type.code.mutable !== true
92
- const refuse = (key: string): never => {
93
- throw new PlanningError(`immutable:${FLOW_KEYS.includes(key) ? 'status' : key}`)
53
+ return copy as T
94
54
  }
95
55
 
96
- for (const [key, value] of Object.entries(exec.changes ?? {})) {
97
- if (value === undefined) {
98
- continue
99
- }
100
- if (IDENTITY_KEYS.includes(key) || DERIVED_KEYS.includes(key)) {
101
- refuse(key)
56
+ const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => {
57
+ const create = exec.action === TransitionAction.Create
58
+ const codeFixed = type.code != null && type.code.mutable !== true
59
+ const refuse = (key: string): never => {
60
+ throw new PlanningError(`immutable:${FLOW_KEYS.includes(key) ? 'status' : key}`)
102
61
  }
103
- if (!create && (FLOW_KEYS.includes(key) || key === 'category' || (key === 'code' && codeFixed))) {
104
- refuse(key)
62
+
63
+ for (const [key, value] of Object.entries(exec.changes ?? {})) {
64
+ if (value === undefined) {
65
+ continue
66
+ }
67
+ if (IDENTITY_KEYS.includes(key) || PROVENANCE_KEYS.includes(key) || DERIVED_KEYS.includes(key)) {
68
+ refuse(key)
69
+ }
70
+ if (!create && (FLOW_KEYS.includes(key) || key === 'category' || (key === 'code' && codeFixed))) {
71
+ refuse(key)
72
+ }
105
73
  }
106
- }
107
74
 
108
- for (const path of exec.unset ?? []) {
109
- const head = path.split('.')[0]
110
- if (IDENTITY_KEYS.includes(path) || DERIVED_KEYS.includes(path) || REQUIRED_KEYS.includes(path)
111
- || FLOW_KEYS.includes(head) || (path === 'code' && codeFixed)) {
112
- refuse(path)
75
+ for (const path of exec.unset ?? []) {
76
+ const head = path.split('.')[0]
77
+ if (PROVENANCE_KEYS.includes(head)) {
78
+ refuse(head)
79
+ }
80
+ if (IDENTITY_KEYS.includes(path) || DERIVED_KEYS.includes(path) || REQUIRED_KEYS.includes(path)
81
+ || FLOW_KEYS.includes(head) || (path === 'code' && codeFixed)) {
82
+ refuse(path)
83
+ }
113
84
  }
114
85
  }
115
- }
116
86
 
117
- const reach = (record: Record<string, unknown>, path: string): unknown => path.split('.')
118
- .reduce<unknown>((value, step) => isPlainObject(value) ? value[step] : undefined, record)
87
+ const reach = (record: Record<string, unknown>, path: string): unknown => path.split('.')
88
+ .reduce<unknown>((value, step) => isPlainObject(value) ? value[step] : undefined, record)
119
89
 
120
- /** Only what differs from the card; `null` becomes an unset of a field that has a value. */
121
- const diffInto = (card: Workcard, changes: WorkcardChanges | undefined, skip: string[], set: ChangeSet): void => {
122
- const out = set.changes as Record<string, unknown>
123
- const current = card as unknown as Record<string, unknown>
90
+ /** Only what differs from the card; `null` becomes an unset of a field that has a value. */
91
+ const diffInto = (card: Workcard, changes: WorkcardChanges | undefined, skip: string[], set: ChangeSet): void => {
92
+ const out = set.changes as Record<string, unknown>
93
+ const current = card as unknown as Record<string, unknown>
124
94
 
125
- for (const [key, value] of Object.entries(changes ?? {})) {
126
- if (value === undefined || IDENTITY_KEYS.includes(key) || skip.includes(key)) {
127
- continue
128
- }
129
- if (key === 'fields' || key === 'flows') {
130
- const own = (current[key] ?? {}) as Record<string, unknown>
131
- const patch: Record<string, unknown> = {}
132
- for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
133
- if (entryValue === undefined) {
134
- continue
135
- }
136
- if (entryValue === null) {
137
- if (own[entry] !== undefined) {
138
- set.unset.push(`${key}.${entry}`)
95
+ for (const [key, value] of Object.entries(changes ?? {})) {
96
+ if (value === undefined || IDENTITY_KEYS.includes(key) || PROVENANCE_KEYS.includes(key) || skip.includes(key)) {
97
+ continue
98
+ }
99
+ if (key === 'fields' || key === 'flows') {
100
+ const own = (current[key] ?? {}) as Record<string, unknown>
101
+ const patch: Record<string, unknown> = {}
102
+ for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
103
+ if (entryValue === undefined) {
104
+ continue
105
+ }
106
+ if (entryValue === null) {
107
+ if (own[entry] !== undefined) {
108
+ set.unset.push(`${key}.${entry}`)
109
+ }
110
+ } else if (!sameValue(own[entry], entryValue)) {
111
+ patch[entry] = entryValue
139
112
  }
140
- } else if (!sameValue(own[entry], entryValue)) {
141
- patch[entry] = entryValue
142
113
  }
114
+ if (Object.keys(patch).length > 0) {
115
+ out[key] = patch
116
+ }
117
+ continue
143
118
  }
144
- if (Object.keys(patch).length > 0) {
145
- out[key] = patch
119
+ if (value === null) {
120
+ if (current[key] != null) {
121
+ set.unset.push(key)
122
+ }
123
+ continue
146
124
  }
147
- continue
148
- }
149
- if (value === null) {
150
- if (current[key] != null) {
151
- set.unset.push(key)
125
+ if (!sameValue(current[key], value)) {
126
+ out[key] = value
152
127
  }
153
- continue
154
- }
155
- if (!sameValue(current[key], value)) {
156
- out[key] = value
157
128
  }
158
129
  }
159
- }
160
130
 
161
- const unsetInto = (card: Workcard, unset: string[] | undefined, set: ChangeSet): void => {
162
- for (const path of unset ?? []) {
163
- if (reach(card as unknown as Record<string, unknown>, path) !== undefined && !set.unset.includes(path)) {
164
- set.unset.push(path)
131
+ const unsetInto = (card: Workcard, unset: string[] | undefined, set: ChangeSet): void => {
132
+ for (const path of unset ?? []) {
133
+ if (PROVENANCE_KEYS.includes(path.split('.')[0])) {
134
+ continue
135
+ }
136
+ if (reach(card as unknown as Record<string, unknown>, path) !== undefined && !set.unset.includes(path)) {
137
+ set.unset.push(path)
138
+ }
165
139
  }
166
140
  }
167
- }
168
141
 
169
- /** A moved primary parent replaces the old one in `parents`. */
170
- const reparent = (card: Workcard, set: ChangeSet): void => {
171
- const out = set.changes
172
- const moved = out.parent !== undefined || set.unset.includes('parent')
173
- if (!moved || out.parents !== undefined) {
174
- return
175
- }
176
- const parents = normalizeParents(out.parent, card.parents.filter(parent => parent !== card.parent))
177
- if (!sameValue(parents, card.parents)) {
178
- out.parents = parents
142
+ /** A moved primary parent replaces the old one in `parents`. */
143
+ const reparent = (card: Workcard, set: ChangeSet): void => {
144
+ const out = set.changes
145
+ const moved = out.parent !== undefined || set.unset.includes('parent')
146
+ if (!moved || out.parents !== undefined) {
147
+ return
148
+ }
149
+ const parents = cardHelper.normalizeParents(out.parent, card.parents.filter(parent => parent !== card.parent))
150
+ if (!sameValue(parents, card.parents)) {
151
+ out.parents = parents
152
+ }
179
153
  }
180
- }
181
154
 
182
- const reviseInto = (card: Workcard, slot: SpecificationSlot | null | undefined, set: ChangeSet): void => {
183
- if (card.kind !== WorkcardKind.Specification) {
184
- return
185
- }
186
- const out = set.changes
187
- if (out.body !== undefined) {
188
- out.bodyChars = bodyCharsOf(out.body)
189
- } else if (set.unset.includes('body') && !set.unset.includes('bodyChars')) {
190
- set.unset.push('bodyChars')
191
- }
192
- const revised = CONTENT_KEYS.some(key => (out as Record<string, unknown>)[key] !== undefined || set.unset.includes(key))
193
- if (revised) {
194
- const revision = nextRevision(card as Specification, slot)
195
- if (revision != null) {
196
- out.revision = revision
155
+ const reviseInto = (card: Workcard, slot: SpecificationSlot | null | undefined, set: ChangeSet): void => {
156
+ if (card.kind !== WorkcardKind.Specification) {
157
+ return
158
+ }
159
+ const out = set.changes
160
+ if (out.body !== undefined) {
161
+ out.bodyChars = specificationHelper.bodyCharsOf(out.body)
162
+ } else if (set.unset.includes('body') && !set.unset.includes('bodyChars')) {
163
+ set.unset.push('bodyChars')
164
+ }
165
+ const revised = CONTENT_KEYS.some(key => (out as Record<string, unknown>)[key] !== undefined || set.unset.includes(key))
166
+ if (revised) {
167
+ const revision = specificationHelper.nextRevision(card as Specification, slot)
168
+ if (revision != null) {
169
+ out.revision = revision
170
+ }
197
171
  }
198
172
  }
199
- }
200
173
 
201
- const createOf = (
202
- exec: TransitionExecution, type: AnyTypeSchema, registry: FlowLookup, at: string, slot?: SpecificationSlot | null
203
- ): ChangeSet => {
204
- if (typeof exec.card === 'string') {
205
- throw new PlanningError('malformed:create-without-draft')
206
- }
207
- const draft: WorkcardDraft = exec.card
208
- const overlay = Object.fromEntries(Object.entries(exec.changes ?? {})
209
- .filter(([key]) => !FLOW_KEYS.includes(key) && !IDENTITY_KEYS.includes(key) && !DERIVED_KEYS.includes(key))
210
- ) as WorkcardChanges
211
-
212
- const parent = overlay.parent ?? draft.parent
213
- const flows = initialFlowsOf(type, registry, draft.status ?? exec.changes?.status)
214
- const intrinsic = resolveIntrinsic(type, flows, registry)
215
-
216
- const record: Record<string, unknown> = {
217
- title: draft.title,
218
- description: draft.description,
219
- code: draft.code,
220
- order: draft.order,
221
- createdBy: draft.createdBy,
222
- ...overlay,
223
- labels: [...new Set(overlay.labels ?? draft.labels ?? [])],
224
- parent,
225
- parents: normalizeParents(parent, overlay.parents ?? draft.parents),
226
- fields: mergeFields(draft.fields, overlay.fields),
227
- flows,
228
- status: flows[primaryFlowOf(type)],
229
- intrinsic,
230
- closedAt: intrinsic === IntrinsicStatus.Closed ? at : undefined,
231
- }
174
+ const createOf = (
175
+ exec: TransitionExecution, type: AnyTypeSchema, registry: FlowLookup, at: string, slot?: SpecificationSlot | null
176
+ ): ChangeSet => {
177
+ if (typeof exec.card === 'string') {
178
+ throw new PlanningError('malformed:create-without-draft')
179
+ }
180
+ const draft: WorkcardDraft = exec.card
181
+ const overlay = Object.fromEntries(Object.entries(exec.changes ?? {})
182
+ .filter(([key]) => !FLOW_KEYS.includes(key) && !IDENTITY_KEYS.includes(key) && !PROVENANCE_KEYS.includes(key)
183
+ && !DERIVED_KEYS.includes(key))
184
+ ) as WorkcardChanges
185
+
186
+ const parent = overlay.parent ?? draft.parent
187
+ const flows = statusHelper.initialFlowsOf(type, registry, draft.status ?? exec.changes?.status)
188
+ const intrinsic = statusHelper.resolveIntrinsic(type, flows, registry)
189
+
190
+ const record: Record<string, unknown> = {
191
+ title: draft.title,
192
+ description: draft.description,
193
+ code: draft.code,
194
+ order: draft.order,
195
+ createdBy: draft.createdBy,
196
+ reporter: draft.reporter,
197
+ assignee: draft.assignee,
198
+ mode: draft.mode ?? (draft.kind === WorkcardKind.Project ? 'opened' : undefined),
199
+ ...overlay,
200
+ labels: [...new Set(overlay.labels ?? draft.labels ?? [])],
201
+ parent,
202
+ parents: cardHelper.normalizeParents(parent, overlay.parents ?? draft.parents),
203
+ fields: mergeFields(draft.fields, overlay.fields),
204
+ flows,
205
+ status: flows[statusHelper.primaryFlowOf(type)],
206
+ intrinsic,
207
+ closedAt: intrinsic === IntrinsicStatus.Closed ? at : undefined,
208
+ }
232
209
 
233
- if (draft.kind === WorkcardKind.Specification) {
234
- const body = overlay.body ?? draft.body
235
- Object.assign(record, {
236
- category: overlay.category ?? draft.category ?? slot?.category,
237
- format: overlay.format ?? draft.format ?? slot?.format ?? SpecificationFormat.Markdown,
238
- body,
239
- ref: overlay.ref ?? draft.ref,
240
- version: overlay.version ?? draft.version ?? slot?.version,
241
- bodyChars: bodyCharsOf(body),
242
- revision: nextRevision(null, slot),
243
- })
210
+ if (draft.kind === WorkcardKind.Specification) {
211
+ const body = overlay.body ?? draft.body
212
+ Object.assign(record, {
213
+ category: overlay.category ?? draft.category ?? slot?.category,
214
+ format: overlay.format ?? draft.format ?? slot?.format ?? SpecificationFormat.Markdown,
215
+ body,
216
+ ref: overlay.ref ?? draft.ref,
217
+ version: overlay.version ?? draft.version ?? slot?.version,
218
+ bodyChars: specificationHelper.bodyCharsOf(body),
219
+ revision: specificationHelper.nextRevision(null, slot),
220
+ })
221
+ }
222
+
223
+ return { changes: clean(record) as WorkcardChanges, unset: [] }
244
224
  }
245
225
 
246
- return { changes: clean(record) as WorkcardChanges, unset: [] }
247
- }
226
+ const computeChanges = (
227
+ card: Workcard | null | undefined,
228
+ exec: TransitionExecution,
229
+ type: AnyTypeSchema,
230
+ registry: FlowLookup,
231
+ at: string,
232
+ opts?: { slot?: SpecificationSlot | null }
233
+ ): ChangeSet => {
234
+ if (exec.action === TransitionAction.Create) {
235
+ return createOf(exec, type, registry, at, opts?.slot)
236
+ }
237
+ const set: ChangeSet = { changes: {}, unset: [] }
238
+ if (exec.action !== TransitionAction.Update && exec.action !== TransitionAction.Transit) {
239
+ return set
240
+ }
241
+ if (card == null) {
242
+ throw new PlanningError(`malformed:${exec.action}-without-card`)
243
+ }
248
244
 
249
- /**
250
- * The `changes`/`unset` a transition records (plan step 9).
251
- *
252
- * - `create`: the whole initial record from the draft (overlaid by `exec.changes`) — `parents`
253
- * normalized, every flow at its initial status (the draft's `status` for the primary one),
254
- * `status`/`intrinsic` mirrored, `closedAt` when it starts closed; a specification gets its
255
- * format, `bodyChars` and revision 1 when the slot is revisioned.
256
- * - `update`: only what differs; `null` or `exec.unset` clears a field that has a value; a moved
257
- * `parent` replaces the old one in `parents`; a revisioned document whose content changed gets
258
- * `revision + 1`. An update that changes nothing is empty ({@link isEmptyChange}).
259
- * - `transit`: `flows[flow] = rule.to`, `status` when the flow is primary, `intrinsic` when it
260
- * moves, `closedAt` set on entering closed and cleared on leaving it — plus the caller's own
261
- * changes, diffed as in an update.
262
- * - `link`, `unlink`, `delete`: nothing.
263
- *
264
- * @throws {IllegalTransition} a transit the flow does not offer from the current status
265
- * @throws {PlanningError} `malformed:*` for an execution missing what its action needs
266
- */
267
- export const computeChanges = (
268
- card: Workcard | null | undefined,
269
- exec: TransitionExecution,
270
- type: AnyTypeSchema,
271
- registry: FlowLookup,
272
- at: string,
273
- opts?: { slot?: SpecificationSlot | null }
274
- ): ChangeSet => {
275
- if (exec.action === TransitionAction.Create) {
276
- return createOf(exec, type, registry, at, opts?.slot)
277
- }
278
- const set: ChangeSet = { changes: {}, unset: [] }
279
- if (exec.action !== TransitionAction.Update && exec.action !== TransitionAction.Transit) {
280
- return set
281
- }
282
- if (card == null) {
283
- throw new PlanningError(`malformed:${exec.action}-without-card`)
284
- }
245
+ if (exec.action === TransitionAction.Update) {
246
+ diffInto(card, exec.changes, [], set)
247
+ unsetInto(card, exec.unset, set)
248
+ reparent(card, set)
249
+ reviseInto(card, opts?.slot, set)
250
+
251
+ return set
252
+ }
253
+
254
+ if (exec.transition == null) {
255
+ throw new PlanningError('malformed:transit-without-transition')
256
+ }
257
+ const primary = statusHelper.primaryFlowOf(type)
258
+ const flowId = exec.flow ?? primary
259
+ const flow = registry.flow(flowId)
260
+ const from = card.flows[flowId] ?? (flowId === primary ? card.status : statusHelper.initialStatusOf(flow))
261
+ const rule = statusHelper.ruleOf(flow, exec.transition, from)
262
+ if (rule == null) {
263
+ throw new IllegalTransition(`${flowId}:${exec.transition}:${from}`)
264
+ }
285
265
 
286
- if (exec.action === TransitionAction.Update) {
287
- diffInto(card, exec.changes, [], set)
288
- unsetInto(card, exec.unset, set)
266
+ diffInto(card, exec.changes, FLOW_KEYS, set)
267
+ unsetInto(card, exec.unset?.filter(path => !FLOW_KEYS.includes(path.split('.')[0])), set)
289
268
  reparent(card, set)
290
269
  reviseInto(card, opts?.slot, set)
291
270
 
292
- return set
293
- }
271
+ const flows = { ...card.flows, [flowId]: rule.to }
272
+ set.changes.flows = { [flowId]: rule.to }
273
+ if (flowId === primary) {
274
+ set.changes.status = rule.to
275
+ }
276
+ const intrinsic = statusHelper.resolveIntrinsic(type, flows, registry)
277
+ if (intrinsic !== card.intrinsic) {
278
+ set.changes.intrinsic = intrinsic
279
+ }
280
+ if (intrinsic === IntrinsicStatus.Closed && card.intrinsic !== IntrinsicStatus.Closed) {
281
+ set.changes.closedAt = at
282
+ } else if (intrinsic !== IntrinsicStatus.Closed && card.closedAt != null) {
283
+ set.unset.push('closedAt')
284
+ }
294
285
 
295
- if (exec.transition == null) {
296
- throw new PlanningError('malformed:transit-without-transition')
297
- }
298
- const primary = primaryFlowOf(type)
299
- const flowId = exec.flow ?? primary
300
- const flow = registry.flow(flowId)
301
- const from = card.flows[flowId] ?? (flowId === primary ? card.status : initialStatusOf(flow))
302
- const rule = ruleOf(flow, exec.transition, from)
303
- if (rule == null) {
304
- throw new IllegalTransition(`${flowId}:${exec.transition}:${from}`)
286
+ return { ...set, flow: flowId, from, to: rule.to }
305
287
  }
306
288
 
307
- diffInto(card, exec.changes, FLOW_KEYS, set)
308
- unsetInto(card, exec.unset?.filter(path => !FLOW_KEYS.includes(path.split('.')[0])), set)
309
- reparent(card, set)
310
- reviseInto(card, opts?.slot, set)
311
-
312
- const flows = { ...card.flows, [flowId]: rule.to }
313
- set.changes.flows = { [flowId]: rule.to }
314
- if (flowId === primary) {
315
- set.changes.status = rule.to
316
- }
317
- const intrinsic = resolveIntrinsic(type, flows, registry)
318
- if (intrinsic !== card.intrinsic) {
319
- set.changes.intrinsic = intrinsic
320
- }
321
- if (intrinsic === IntrinsicStatus.Closed && card.intrinsic !== IntrinsicStatus.Closed) {
322
- set.changes.closedAt = at
323
- } else if (intrinsic !== IntrinsicStatus.Closed && card.closedAt != null) {
324
- set.unset.push('closedAt')
325
- }
289
+ const isEmptyChange = (set: Pick<ChangeSet, 'changes' | 'unset'>): boolean =>
290
+ set.unset.length === 0 && Object.entries(set.changes).every(([, value]) =>
291
+ value === undefined || (isPlainObject(value) && Object.keys(value).length === 0))
326
292
 
327
- return { ...set, flow: flowId, from, to: rule.to }
293
+ return { sameValue, mergeFields, applyUnset, assertMutable, computeChanges, isEmptyChange }
328
294
  }
329
295
 
330
- /** Nothing to write: no changed value and nothing to clear. */
331
- export const isEmptyChange = (set: Pick<ChangeSet, 'changes' | 'unset'>): boolean =>
332
- set.unset.length === 0 && Object.entries(set.changes).every(([, value]) =>
333
- value === undefined || (isPlainObject(value) && Object.keys(value).length === 0))
296
+ export const changesHelper = createChangesHelper()
297
+
298
+ /** @deprecated compat:factory-refactor — use `changesHelper.sameValue(…)` */
299
+ export const sameValue = (left: unknown, right: unknown): boolean => changesHelper.sameValue(left, right)
300
+
301
+ /** @deprecated compat:factory-refactor — use `changesHelper.mergeFields(…)` */
302
+ export const mergeFields = (
303
+ base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null
304
+ ): Record<string, unknown> => changesHelper.mergeFields(base, patch)
305
+
306
+ /** @deprecated compat:factory-refactor — use `changesHelper.applyUnset(…)` */
307
+ export const applyUnset = <T extends object>(record: T, unset?: string[]): T => changesHelper.applyUnset<T>(record, unset)
308
+
309
+ /** @deprecated compat:factory-refactor — use `changesHelper.assertMutable(…)` */
310
+ export const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => changesHelper.assertMutable(exec, type)
311
+
312
+ /** @deprecated compat:factory-refactor — use `changesHelper.computeChanges(…)` */
313
+ export const computeChanges = (
314
+ card: Workcard | null | undefined,
315
+ exec: TransitionExecution,
316
+ type: AnyTypeSchema,
317
+ registry: FlowLookup,
318
+ at: string,
319
+ opts?: { slot?: SpecificationSlot | null }
320
+ ): ChangeSet => changesHelper.computeChanges(card, exec, type, registry, at, opts)
321
+
322
+ /** @deprecated compat:factory-refactor — use `changesHelper.isEmptyChange(…)` */
323
+ export const isEmptyChange = (set: Pick<ChangeSet, 'changes' | 'unset'>): boolean => changesHelper.isEmptyChange(set)
@@ -0,0 +1,31 @@
1
+ import type { CodePolicy, WorkcardDraft } from '../../types.js'
2
+
3
+ export interface MintCodeOptions {
4
+ /** Slug style: the text the slug is derived from (a title). A word slug when omitted. */
5
+ seed?: string
6
+ /** Sequential style: how many codes the scope already holds. */
7
+ count?: number
8
+ }
9
+
10
+ /** A card's human code: minted under the type's policy, unique within its scope. */
11
+ export interface CodeHelper {
12
+ /** `Some Title!` → `some-title`. */
13
+ slugOf: (text: string, max?: number) => string
14
+ /**
15
+ * Mint a code under a policy, asking `taken` for each candidate.
16
+ *
17
+ * `random` draws `prefix + Base58(length)` (upper-cased when asked) afresh per attempt; `slug`
18
+ * walks `base`, `base-2`, `base-3` from the seed (a word slug without one); `sequential` counts on
19
+ * from `opts.count`, zero-padded to `length`. After `attempts` taken candidates it refuses.
20
+ *
21
+ * @throws {CodeTaken}
22
+ */
23
+ mintCode: (
24
+ policy: CodePolicy,
25
+ taken: (code: string) => boolean | Promise<boolean>,
26
+ attempts?: number,
27
+ opts?: MintCodeOptions
28
+ ) => Promise<string>
29
+ /** Where a draft's code must be unique: its parent, or the whole entity. */
30
+ codeScopeOf: (policy: CodePolicy, draft: Pick<WorkcardDraft, 'parent'>) => { parent?: string }
31
+ }