@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,257 @@
1
+ import { memoHelper } from '@owlmeans/context'
2
+ import { PlanningSchemaKind, SchemaOrigin, WorkcardKind } from '../consts.js'
3
+ import { PlanningUnsupported, SchemaInvalid, SchemaSealed } from '../errors.js'
4
+ import { makeSchemaRegistry } from '../registry.js'
5
+ import { StatusFlowSchemaSchema, WorkcardTypeSchemaSchema } from '../schemas.js'
6
+ import type {
7
+ AnyTypeSchema, PlanningSchemaBundle, PlanningSchemaRegistry, SchemaScope, ScopedSchemaBundle,
8
+ ScopedSchemaRecord, ScopedSchemaRegistry, StatusFlowSchema, WorkcardTypeSchema,
9
+ } from '../types.js'
10
+ import type { ScopedSchemaHelper } from './scoped/types.js'
11
+ import type { Candidate, Resolved } from './types.local.js'
12
+ import { makeAjv, validateHelper } from './validate.js'
13
+
14
+ const messageOf = (error: unknown): string => error instanceof Error ? error.message : `${error}`
15
+
16
+ export const createScopedSchemaHelper = (): ScopedSchemaHelper => {
17
+ const checkers = memoHelper.once(() => {
18
+ const ajv = makeAjv()
19
+ return { flow: ajv.compile(StatusFlowSchemaSchema), type: ajv.compile(WorkcardTypeSchemaSchema) }
20
+ })
21
+
22
+ const schemaRecordKey = (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>): string =>
23
+ `${record.entityId}\u0000${record.project ?? ''}\u0000${record.kind}\u0000${record.key}`
24
+
25
+ const schemaKeyOf = (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema): string =>
26
+ kind === PlanningSchemaKind.Type ? (definition as WorkcardTypeSchema).type : (definition as StatusFlowSchema).id
27
+
28
+ const openType = (type: AnyTypeSchema): boolean => type.kind === WorkcardKind.Card && type.overridable === true
29
+
30
+ const assertOverridable = (
31
+ code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows'>, kind: PlanningSchemaKind, key: string
32
+ ): void => {
33
+ if (kind === PlanningSchemaKind.Type) {
34
+ if (code.has(key) && !openType(code.type(key))) {
35
+ throw new SchemaSealed(`type:${key}`)
36
+ }
37
+ return
38
+ }
39
+ const flow = code.flows().find(entry => entry.id === key)
40
+ if (flow != null && flow.overridable !== true) {
41
+ throw new SchemaSealed(`flow:${key}`)
42
+ }
43
+ }
44
+
45
+ const assertFlowSchema = (flow: StatusFlowSchema): void => {
46
+ const validate = checkers().flow
47
+ if (!validate(flow)) {
48
+ throw new SchemaInvalid(`flow:${(flow as Partial<StatusFlowSchema>)?.id ?? ''}:${validateHelper.ajvErrorText(validate.errors)}`)
49
+ }
50
+ const keys = new Set<string>()
51
+ for (const status of flow.statuses) {
52
+ if (keys.has(status.key)) {
53
+ throw new SchemaInvalid(`flow:${flow.id}:status-repeated:${status.key}`)
54
+ }
55
+ keys.add(status.key)
56
+ }
57
+ for (const rule of flow.transitions) {
58
+ if (!keys.has(rule.to)) {
59
+ throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:to:${rule.to}`)
60
+ }
61
+ if (typeof rule.from === 'string') {
62
+ if (rule.from !== '*') {
63
+ throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:from:${rule.from}`)
64
+ }
65
+ continue
66
+ }
67
+ const unknown = rule.from.find(status => !keys.has(status))
68
+ if (unknown != null) {
69
+ throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:from:${unknown}`)
70
+ }
71
+ }
72
+ }
73
+
74
+ const assertTypeSchema = (
75
+ type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>
76
+ ): void => {
77
+ const validate = checkers().type
78
+ if (!validate(type)) {
79
+ throw new SchemaInvalid(`type:${(type as Partial<WorkcardTypeSchema>)?.type ?? ''}:${validateHelper.ajvErrorText(validate.errors)}`)
80
+ }
81
+ if (type.kind !== WorkcardKind.Card) {
82
+ throw new SchemaInvalid(`type:${type.type}:kind:${type.kind}`)
83
+ }
84
+ try {
85
+ makeAjv().compile(type.fields)
86
+ } catch (error) {
87
+ throw new SchemaInvalid(`type:${type.type}:fields:${messageOf(error)}`)
88
+ }
89
+ const seen = new Set<string>()
90
+ for (const id of type.flows) {
91
+ if (seen.has(id)) {
92
+ throw new SchemaInvalid(`type:${type.type}:flow-repeated:${id}`)
93
+ }
94
+ seen.add(id)
95
+ try {
96
+ flows.flow(id)
97
+ } catch {
98
+ throw new SchemaInvalid(`type:${type.type}:flow:${id}`)
99
+ }
100
+ }
101
+ }
102
+
103
+ const resolveKind = <T extends WorkcardTypeSchema | StatusFlowSchema | AnyTypeSchema>(
104
+ kind: PlanningSchemaKind,
105
+ code: readonly T[],
106
+ keyOf: (definition: T) => string,
107
+ open: (definition: T) => boolean,
108
+ valid: (definition: T) => boolean,
109
+ layers: { entity: readonly ScopedSchemaRecord[], project: readonly ScopedSchemaRecord[] },
110
+ ): Resolved<T> => {
111
+ const coded = new Map(code.map(definition => [keyOf(definition), definition]))
112
+ const byKey = (records: readonly ScopedSchemaRecord[]) => new Map(records
113
+ .filter(record => record.kind === kind)
114
+ .map(record => [record.key, record] as const)
115
+ .filter(([key, record]) => {
116
+ const definition = record.definition as T
117
+ return valid(definition) && keyOf(definition) === key
118
+ }))
119
+ const entity = byKey(layers.entity)
120
+ const project = byKey(layers.project)
121
+ const added = [...new Set([...entity.keys(), ...project.keys()])].filter(key => !coded.has(key)).sort()
122
+
123
+ const stamped = (record: ScopedSchemaRecord): T => ({ ...(record.definition as T), version: record.version })
124
+ const result: Resolved<T> = { definitions: [], origins: {}, retired: [] }
125
+ for (const key of [...coded.keys(), ...added]) {
126
+ const declared = coded.get(key)
127
+ const candidates: Candidate<T>[] = []
128
+ if (declared == null || open(declared)) {
129
+ const own = project.get(key)
130
+ if (own != null) {
131
+ candidates.push({ origin: SchemaOrigin.Project, definition: stamped(own), retired: own.retired === true })
132
+ }
133
+ const shared = entity.get(key)
134
+ if (shared != null) {
135
+ candidates.push({ origin: SchemaOrigin.Entity, definition: stamped(shared), retired: shared.retired === true })
136
+ }
137
+ }
138
+ if (declared != null) {
139
+ candidates.push({ origin: SchemaOrigin.Code, definition: declared, retired: false })
140
+ }
141
+ // A retired record gives way to a live declaration below it; only when every layer holding the
142
+ // key retired it does the key resolve retired — still there for the cards that use it.
143
+ const chosen = candidates.find(candidate => !candidate.retired) ?? candidates[0]
144
+ if (chosen == null) {
145
+ continue
146
+ }
147
+ result.definitions.push(chosen.definition)
148
+ result.origins[key] = chosen.origin
149
+ if (chosen.retired) {
150
+ result.retired.push(key)
151
+ }
152
+ }
153
+
154
+ return result
155
+ }
156
+
157
+ const resolveScopedBundle = (
158
+ code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
159
+ ): ScopedSchemaBundle => {
160
+ const mine = records.filter(record => record.entityId === scope.entityId)
161
+ const layers = {
162
+ entity: mine.filter(record => record.project == null),
163
+ project: scope.project == null ? [] : mine.filter(record => record.project === scope.project),
164
+ }
165
+ const types = resolveKind<AnyTypeSchema>(
166
+ PlanningSchemaKind.Type, code.types, type => type.type, openType,
167
+ type => type != null && typeof type.type === 'string' && type.kind === WorkcardKind.Card, layers,
168
+ )
169
+ const flows = resolveKind<StatusFlowSchema>(
170
+ PlanningSchemaKind.Flow, code.flows, flow => flow.id, flow => flow.overridable === true,
171
+ flow => flow != null && typeof flow.id === 'string', layers,
172
+ )
173
+
174
+ return structuredClone({
175
+ version: code.version,
176
+ types: types.definitions,
177
+ flows: flows.definitions,
178
+ scope: scope.project == null ? { entityId: scope.entityId } : { entityId: scope.entityId, project: scope.project },
179
+ revision: scope.revision ?? 0,
180
+ origins: { types: types.origins, flows: flows.origins },
181
+ retired: { types: types.retired, flows: flows.retired },
182
+ })
183
+ }
184
+
185
+ const scopedRegistryOf = (bundle: ScopedSchemaBundle): ScopedSchemaRegistry => {
186
+ const base = makeSchemaRegistry(bundle)
187
+ const kept = structuredClone(bundle)
188
+ const refuse = (): never => {
189
+ throw new PlanningUnsupported('schemas:read-only')
190
+ }
191
+ const originsOf = (kind: PlanningSchemaKind) =>
192
+ kind === PlanningSchemaKind.Type ? kept.origins?.types : kept.origins?.flows
193
+ const retiredOf = (kind: PlanningSchemaKind) =>
194
+ kind === PlanningSchemaKind.Type ? kept.retired?.types : kept.retired?.flows
195
+
196
+ return {
197
+ ...base,
198
+ registerType: refuse,
199
+ registerFlow: refuse,
200
+ load: refuse,
201
+ scope: kept.scope ?? { entityId: '' },
202
+ revision: kept.revision ?? 0,
203
+ originOf: (kind, key) => originsOf(kind)?.[key]
204
+ ?? ((kind === PlanningSchemaKind.Type ? base.has(key) : base.flows().some(flow => flow.id === key))
205
+ ? SchemaOrigin.Code
206
+ : undefined),
207
+ isRetired: (kind, key) => retiredOf(kind)?.includes(key) === true,
208
+ bundle: () => structuredClone(kept),
209
+ }
210
+ }
211
+
212
+ const flowInUse = (bundle: ScopedSchemaBundle, flow: string): string | undefined => {
213
+ if (bundle.retired?.flows.includes(flow) !== true) {
214
+ return undefined
215
+ }
216
+
217
+ return bundle.types.find(type => bundle.retired?.types.includes(type.type) !== true && type.flows.includes(flow))?.type
218
+ }
219
+
220
+ return {
221
+ schemaRecordKey, schemaKeyOf, assertOverridable, assertFlowSchema, assertTypeSchema, resolveScopedBundle,
222
+ scopedRegistryOf, flowInUse,
223
+ }
224
+ }
225
+
226
+ export const scopedSchemaHelper = createScopedSchemaHelper()
227
+
228
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.schemaRecordKey(…)` */
229
+ export const schemaRecordKey = (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>): string =>
230
+ scopedSchemaHelper.schemaRecordKey(record)
231
+
232
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.schemaKeyOf(…)` */
233
+ export const schemaKeyOf = (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema): string =>
234
+ scopedSchemaHelper.schemaKeyOf(kind, definition)
235
+
236
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertOverridable(…)` */
237
+ export const assertOverridable = (
238
+ code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows'>, kind: PlanningSchemaKind, key: string
239
+ ): void => scopedSchemaHelper.assertOverridable(code, kind, key)
240
+
241
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertFlowSchema(…)` */
242
+ export const assertFlowSchema = (flow: StatusFlowSchema): void => scopedSchemaHelper.assertFlowSchema(flow)
243
+
244
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertTypeSchema(…)` */
245
+ export const assertTypeSchema = (type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>): void =>
246
+ scopedSchemaHelper.assertTypeSchema(type, flows)
247
+
248
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.resolveScopedBundle(…)` */
249
+ export const resolveScopedBundle = (
250
+ code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
251
+ ): ScopedSchemaBundle => scopedSchemaHelper.resolveScopedBundle(code, records, scope)
252
+
253
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.scopedRegistryOf(…)` */
254
+ export const scopedRegistryOf = (bundle: ScopedSchemaBundle): ScopedSchemaRegistry => scopedSchemaHelper.scopedRegistryOf(bundle)
255
+
256
+ /** @deprecated compat:factory-refactor — use `scopedSchemaHelper.flowInUse(…)` */
257
+ export const flowInUse = (bundle: ScopedSchemaBundle, flow: string): string | undefined => scopedSchemaHelper.flowInUse(bundle, flow)
@@ -0,0 +1,26 @@
1
+ import type { AnyTypeSchema, PlanningSchemaRegistry, Specification, SpecificationSlot, Workcard } from '../../types.js'
2
+
3
+ /** The slot and revision rules of the documents a parent card carries. */
4
+ export interface SpecificationHelper {
5
+ /** The slot a parent type declares for a category. */
6
+ slotOf: (type: AnyTypeSchema, category: string) => SpecificationSlot | undefined
7
+ isRevisioned: (slot?: SpecificationSlot | null) => boolean
8
+ bodyCharsOf: (body?: string | null) => number | undefined
9
+ /** The revision a write lands as: 1 for a new document, current + 1 after, none when unrevisioned. */
10
+ nextRevision: (current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null) => number | undefined
11
+ /**
12
+ * The current document of a category among a parent's specifications: the highest revision, then
13
+ * the most recently updated. `null` when the category has none.
14
+ */
15
+ currentSpecification: (specs: Workcard[], category: string) => Specification | null
16
+ /**
17
+ * The specification type a new document of a slot is created with: the slot's own `type`, else
18
+ * the only registered specification type, else the one sharing the parent type's prefix
19
+ * (`viable:project` → `viable:spec`).
20
+ *
21
+ * @throws {UnknownWorkcardType} when none or several candidates remain
22
+ */
23
+ specificationTypeOf: (
24
+ registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
25
+ ) => string
26
+ }
@@ -3,63 +3,81 @@ import { UnknownWorkcardType } from '../errors.js'
3
3
  import type {
4
4
  AnyTypeSchema, PlanningSchemaRegistry, Specification, SpecificationSlot, Workcard,
5
5
  } from '../types.js'
6
+ import type { SpecificationHelper } from './specification/types.js'
6
7
 
7
- /** The slot a parent type declares for a category. */
8
- export const slotOf = (type: AnyTypeSchema, category: string): SpecificationSlot | undefined =>
9
- type.specifications.find(slot => slot.category === category)
8
+ export const createSpecificationHelper = (): SpecificationHelper => {
9
+ const slotOf = (type: AnyTypeSchema, category: string): SpecificationSlot | undefined =>
10
+ type.specifications.find(slot => slot.category === category)
10
11
 
11
- export const isRevisioned = (slot?: SpecificationSlot | null): boolean => slot?.revisioned === true
12
+ const isRevisioned = (slot?: SpecificationSlot | null): boolean => slot?.revisioned === true
12
13
 
13
- export const bodyCharsOf = (body?: string | null): number | undefined => body == null ? undefined : body.length
14
+ const bodyCharsOf = (body?: string | null): number | undefined => body == null ? undefined : body.length
14
15
 
15
- /** The revision a write lands as: 1 for a new document, current + 1 after, none when unrevisioned. */
16
- export const nextRevision = (
17
- current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null
18
- ): number | undefined => isRevisioned(slot) ? (current?.revision ?? 0) + 1 : undefined
19
-
20
- /**
21
- * The current document of a category among a parent's specifications: the highest revision, then
22
- * the most recently updated. `null` when the category has none.
23
- */
24
- export const currentSpecification = (
25
- specs: Workcard[], category: string
26
- ): Specification | null => specs
27
- .filter((spec): spec is Specification => spec.kind === WorkcardKind.Specification
28
- && (spec as Specification).category === category)
29
- .reduce<Specification | null>((best, spec) => {
30
- if (best == null) {
31
- return spec
16
+ const nextRevision = (
17
+ current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null
18
+ ): number | undefined => isRevisioned(slot) ? (current?.revision ?? 0) + 1 : undefined
19
+
20
+ const currentSpecification = (
21
+ specs: Workcard[], category: string
22
+ ): Specification | null => specs
23
+ .filter((spec): spec is Specification => spec.kind === WorkcardKind.Specification
24
+ && (spec as Specification).category === category)
25
+ .reduce<Specification | null>((best, spec) => {
26
+ if (best == null) {
27
+ return spec
28
+ }
29
+ const byRevision = (spec.revision ?? 0) - (best.revision ?? 0)
30
+ if (byRevision !== 0) {
31
+ return byRevision > 0 ? spec : best
32
+ }
33
+ return (spec.updatedAt ?? spec.createdAt) > (best.updatedAt ?? best.createdAt) ? spec : best
34
+ }, null)
35
+
36
+ const prefixOf = (type: string): string => type.includes(':') ? type.slice(0, type.lastIndexOf(':')) : ''
37
+
38
+ const specificationTypeOf = (
39
+ registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
40
+ ): string => {
41
+ if (slot?.type != null) {
42
+ return slot.type
32
43
  }
33
- const byRevision = (spec.revision ?? 0) - (best.revision ?? 0)
34
- if (byRevision !== 0) {
35
- return byRevision > 0 ? spec : best
44
+ const candidates = registry.types().filter(type => type.kind === WorkcardKind.Specification)
45
+ if (candidates.length === 1) {
46
+ return candidates[0].type
36
47
  }
37
- return (spec.updatedAt ?? spec.createdAt) > (best.updatedAt ?? best.createdAt) ? spec : best
38
- }, null)
39
-
40
- const prefixOf = (type: string): string => type.includes(':') ? type.slice(0, type.lastIndexOf(':')) : ''
41
-
42
- /**
43
- * The specification type a new document of a slot is created with: the slot's own `type`, else
44
- * the only registered specification type, else the one sharing the parent type's prefix
45
- * (`viable:project` → `viable:spec`).
46
- *
47
- * @throws {UnknownWorkcardType} when none or several candidates remain
48
- */
49
- export const specificationTypeOf = (
50
- registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
51
- ): string => {
52
- if (slot?.type != null) {
53
- return slot.type
54
- }
55
- const candidates = registry.types().filter(type => type.kind === WorkcardKind.Specification)
56
- if (candidates.length === 1) {
57
- return candidates[0].type
58
- }
59
- const prefixed = candidates.filter(type => prefixOf(type.type) === prefixOf(parentType))
60
- if (prefixed.length === 1) {
61
- return prefixed[0].type
48
+ const prefixed = candidates.filter(type => prefixOf(type.type) === prefixOf(parentType))
49
+ if (prefixed.length === 1) {
50
+ return prefixed[0].type
51
+ }
52
+
53
+ throw new UnknownWorkcardType(`specification:${parentType}:${slot?.category ?? ''}`)
62
54
  }
63
55
 
64
- throw new UnknownWorkcardType(`specification:${parentType}:${slot?.category ?? ''}`)
56
+ return { slotOf, isRevisioned, bodyCharsOf, nextRevision, currentSpecification, specificationTypeOf }
65
57
  }
58
+
59
+ export const specificationHelper = createSpecificationHelper()
60
+
61
+ /** @deprecated compat:factory-refactor — use `specificationHelper.slotOf(…)` */
62
+ export const slotOf = (type: AnyTypeSchema, category: string): SpecificationSlot | undefined =>
63
+ specificationHelper.slotOf(type, category)
64
+
65
+ /** @deprecated compat:factory-refactor — use `specificationHelper.isRevisioned(…)` */
66
+ export const isRevisioned = (slot?: SpecificationSlot | null): boolean => specificationHelper.isRevisioned(slot)
67
+
68
+ /** @deprecated compat:factory-refactor — use `specificationHelper.bodyCharsOf(…)` */
69
+ export const bodyCharsOf = (body?: string | null): number | undefined => specificationHelper.bodyCharsOf(body)
70
+
71
+ /** @deprecated compat:factory-refactor — use `specificationHelper.nextRevision(…)` */
72
+ export const nextRevision = (
73
+ current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null
74
+ ): number | undefined => specificationHelper.nextRevision(current, slot)
75
+
76
+ /** @deprecated compat:factory-refactor — use `specificationHelper.currentSpecification(…)` */
77
+ export const currentSpecification = (specs: Workcard[], category: string): Specification | null =>
78
+ specificationHelper.currentSpecification(specs, category)
79
+
80
+ /** @deprecated compat:factory-refactor — use `specificationHelper.specificationTypeOf(…)` */
81
+ export const specificationTypeOf = (
82
+ registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
83
+ ): string => specificationHelper.specificationTypeOf(registry, parentType, slot)
@@ -0,0 +1,46 @@
1
+ import type { IntrinsicStatus } from '../../consts.js'
2
+ import type { AnyTypeSchema, StatusDefinition, StatusFlowSchema, StatusTransitionRule } from '../../types.js'
3
+ import type { FlowLookup } from '../types.local.js'
4
+
5
+ /** The status-flow rules: statuses, the transitions a flow offers, and the intrinsic state. */
6
+ export interface StatusHelper {
7
+ /** The primary flow id of a type — `flows[0]`. @throws {UnknownStatusFlow} when it declares none */
8
+ primaryFlowOf: (type: AnyTypeSchema) => string
9
+ flowIdsOf: (type: AnyTypeSchema) => string[]
10
+ statusDefinitionOf: (flow: StatusFlowSchema, status: string) => StatusDefinition | undefined
11
+ /** The intrinsic state a status maps onto, or `undefined` for a status the flow does not declare. */
12
+ intrinsicOf: (flow: StatusFlowSchema, status: string) => IntrinsicStatus | undefined
13
+ /** The status marked `initial`, else the first one. @throws {PlanningError} for a flow with none */
14
+ initialStatusOf: (flow: StatusFlowSchema) => string
15
+ isTerminal: (flow: StatusFlowSchema, status: string) => boolean
16
+ /**
17
+ * The rule a named transition follows from a status.
18
+ *
19
+ * A name may be declared several times with different `from` sets; the rule naming the status
20
+ * explicitly wins, and a `'*'` rule of that name answers only when none does. A status the flow
21
+ * does NOT declare — a card whose flow changed under it — matches every rule of the name, the first
22
+ * declared answering: such a card can always move back onto the flow it now runs.
23
+ */
24
+ ruleOf: (flow: StatusFlowSchema, transition: string, from: string) => StatusTransitionRule | undefined
25
+ canTransit: (flow: StatusFlowSchema, transition: string, from: string) => boolean
26
+ /**
27
+ * Every transition available from a status, one rule per name (the one `ruleOf` would pick), in
28
+ * declaration order. `explicit: true` keeps only the rules offered to a person.
29
+ */
30
+ transitionsFrom: (flow: StatusFlowSchema, status: string, opts?: { explicit?: boolean }) => StatusTransitionRule[]
31
+ /**
32
+ * A new card's status per flow: every flow's initial status, with the primary one overridden by
33
+ * `status` when the draft names one.
34
+ *
35
+ * @throws {UnknownStatusFlow}
36
+ */
37
+ initialFlowsOf: (type: AnyTypeSchema, registry: FlowLookup, status?: string) => Record<string, string>
38
+ /**
39
+ * The card's intrinsic state over its flows. {@link IntrinsicPolicy.Primary} reads the primary
40
+ * flow; {@link IntrinsicPolicy.All} takes the least advanced flow. A status the flow does not
41
+ * declare reads as planned.
42
+ *
43
+ * @throws {UnknownStatusFlow}
44
+ */
45
+ resolveIntrinsic: (type: AnyTypeSchema, flows: Record<string, string>, registry: FlowLookup) => IntrinsicStatus
46
+ }