@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,345 +1,175 @@
1
1
  import type { Criteria, ListOptions, ResourceRecord, Sort } from '@owlmeans/resource'
2
2
  import { IntrinsicStatus, WorkcardKind } from '../consts.js'
3
- import { PlanningError } from '../errors.js'
4
- import type {
5
- IntrinsicCounts, ListWire, PlanningScope, RelationshipQuery, RelationshipQueryWire, RelationshipWhere,
6
- Specification, SpecificationQuery, SpecificationQueryWire, SummaryQuery, SummaryQueryWire, SummaryView,
7
- TransitionQuery, TransitionQueryWire, TransitionWhere, Workcard, WorkcardQuery, WorkcardQueryWire,
8
- } from '../types.js'
9
-
10
- type Scope = Pick<PlanningScope, 'entityId'>
3
+ import type { IntrinsicCounts, RelationshipQuery, RelationshipQueryWire, RelationshipWhere, Specification, SpecificationQuery, SpecificationQueryWire, SummaryQuery, SummaryQueryWire, SummaryView, TransitionQuery, TransitionQueryWire, TransitionWhere, Workcard, WorkcardQuery, WorkcardQueryWire } from '../types.js'
4
+ import type { QueryHelper } from './query/types.js'
5
+ import type { Scope } from './types.local.js'
6
+ import { wireHelper } from './wire.js'
11
7
 
12
8
  const clean = <T extends object>(record: T): T =>
13
9
  Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
14
10
 
15
- /** `%`, `_` and `\` are wildcards/escapes to `$ilike`; a search text means them literally. */
16
- const escapeLike = (text: string): string => text.replace(/[\\%_]/g, char => `\\${char}`)
17
-
18
- /**
19
- * `WorkcardQuery` → `Criteria<Workcard>` — the ONE translation every store answers, so a query
20
- * means the same thing in memory, in Mongo and in a client mirror.
21
- *
22
- * `entityId` always comes from the scope (a query that forgot it would be a cross-tenant read);
23
- * `undefined` values are omitted. Paging and sorting are {@link listOptionsOf}'s.
24
- */
25
- export const criteriaOf = (query?: WorkcardQuery | null, scope?: Scope): Criteria<Workcard> => {
26
- const where: Record<string, unknown> = {
27
- entityId: scope?.entityId,
28
- kind: query?.kind,
29
- type: query?.type,
30
- status: query?.status,
31
- intrinsic: query?.intrinsic,
32
- code: query?.code,
33
- parent: query?.parent,
34
- parents: query?.within != null ? { $contains: query.within } : undefined,
35
- labels: query?.labels != null ? { $overlaps: query.labels } : undefined,
36
- id: query?.ids != null ? { $in: query.ids } : undefined,
37
- category: query?.category,
38
- updatedAt: query?.updatedSince != null ? { $gte: query.updatedSince } : undefined,
39
- }
40
- if (query?.flow != null) {
41
- where[`flows.${query.flow.id}`] = query.flow.status
42
- }
43
- for (const [key, value] of Object.entries(query?.fields ?? {})) {
44
- if (value !== undefined) {
45
- where[`fields.${key}`] = value
11
+ export const createQueryHelper = (): QueryHelper => {
12
+ /** `%`, `_` and `\` are wildcards/escapes to `$ilike`; a search text means them literally. */
13
+ const escapeLike = (text: string): string => text.replace(/[\\%_]/g, char => `\\${char}`)
14
+
15
+ const criteriaOf = (query?: WorkcardQuery | null, scope?: Scope): Criteria<Workcard> => {
16
+ const where: Record<string, unknown> = {
17
+ entityId: scope?.entityId,
18
+ kind: query?.kind,
19
+ type: query?.type,
20
+ status: query?.status,
21
+ intrinsic: query?.intrinsic,
22
+ code: query?.code,
23
+ parent: query?.parent,
24
+ parents: query?.within != null ? { $contains: query.within } : undefined,
25
+ labels: query?.labels != null ? { $overlaps: query.labels } : undefined,
26
+ id: query?.ids != null ? { $in: query.ids } : undefined,
27
+ category: query?.category,
28
+ updatedAt: query?.updatedSince != null ? { $gte: query.updatedSince } : undefined,
46
29
  }
47
- }
48
- if (query?.q != null && query.q !== '') {
49
- const like = `%${escapeLike(query.q)}%`
50
- where.$or = [
51
- { title: { $ilike: like } },
52
- { description: { $ilike: like } },
53
- { code: { $startsWith: query.q } },
54
- ]
30
+ if (query?.flow != null) {
31
+ where[`flows.${query.flow.id}`] = query.flow.status
32
+ }
33
+ for (const [key, value] of Object.entries(query?.fields ?? {})) {
34
+ if (value !== undefined) {
35
+ where[`fields.${key}`] = value
36
+ }
37
+ }
38
+ if (query?.q != null && query.q !== '') {
39
+ const like = `%${escapeLike(query.q)}%`
40
+ where.$or = [
41
+ { title: { $ilike: like } },
42
+ { description: { $ilike: like } },
43
+ { code: { $startsWith: query.q } },
44
+ ]
45
+ }
46
+
47
+ return clean(where) as Criteria<Workcard>
55
48
  }
56
49
 
57
- return clean(where) as Criteria<Workcard>
58
- }
50
+ const listOptionsOf = <T extends ResourceRecord>(query?: ListOptions<T> | null): ListOptions<T> => clean({
51
+ page: query?.page,
52
+ size: query?.size,
53
+ sort: query?.sort,
54
+ })
55
+
56
+ const specCriteriaOf = (
57
+ parent: string, query?: SpecificationQuery | null, scope?: Scope
58
+ ): Criteria<Specification> => clean({
59
+ entityId: scope?.entityId,
60
+ kind: WorkcardKind.Specification,
61
+ parent,
62
+ category: query?.category,
63
+ }) as Criteria<Specification>
59
64
 
60
- /** Paging and sorting out of a query. `size: 0` (no limit) survives. */
61
- export const listOptionsOf = <T extends ResourceRecord>(query?: ListOptions<T> | null): ListOptions<T> => clean({
62
- page: query?.page,
63
- size: query?.size,
64
- sort: query?.sort,
65
- })
65
+ const linkWhereOf = (query: RelationshipQuery | null | undefined, scope: Scope): RelationshipWhere => clean({
66
+ entityId: scope.entityId,
67
+ from: query?.from,
68
+ to: query?.to,
69
+ type: query?.type,
70
+ })
66
71
 
67
- /** The criteria over a parent's specifications. */
68
- export const specCriteriaOf = (
69
- parent: string, query?: SpecificationQuery | null, scope?: Scope
70
- ): Criteria<Specification> => clean({
71
- entityId: scope?.entityId,
72
- kind: WorkcardKind.Specification,
73
- parent,
74
- category: query?.category,
75
- }) as Criteria<Specification>
76
-
77
- export const linkWhereOf = (query: RelationshipQuery | null | undefined, scope: Scope): RelationshipWhere => clean({
78
- entityId: scope.entityId,
79
- from: query?.from,
80
- to: query?.to,
81
- type: query?.type,
82
- })
83
-
84
- export const transitionWhereOf = (query: TransitionQuery | null | undefined, scope: Scope): TransitionWhere => clean({
85
- entityId: scope.entityId,
86
- card: query?.card,
87
- project: query?.project,
88
- sinceSeq: query?.sinceSeq,
89
- action: query?.action,
90
- state: query?.state,
91
- })
92
-
93
- const emptyCounts = (): IntrinsicCounts => ({
94
- total: 0, [IntrinsicStatus.Planned]: 0, [IntrinsicStatus.InProgress]: 0, [IntrinsicStatus.Closed]: 0,
95
- })
96
-
97
- /** Counts of DIRECT children (`card.parent`) per parent, by intrinsic state. No key for a parent with none. */
98
- export const summaryOf = (cards: Workcard[], parents: string[]): SummaryView => cards.reduce<SummaryView>(
99
- (view, card) => {
100
- if (card.parent == null || !parents.includes(card.parent)) {
101
- return view
102
- }
103
- const counts = view[card.parent] ?? emptyCounts()
104
- counts.total += 1
105
- counts[card.intrinsic] = (counts[card.intrinsic] ?? 0) + 1
106
- view[card.parent] = counts
107
- return view
108
- }, {}
109
- )
110
-
111
- // ─── Wire encoding ───────────────────────────────────────────────────────────────────────────────
112
-
113
- /** A list as one query value: comma-joined, or JSON when an element itself holds a comma. */
114
- export const encodeList = (value?: string | readonly string[] | null): string | undefined => {
115
- if (value == null) {
116
- return undefined
117
- }
118
- if (typeof value === 'string') {
119
- return encodeList([value])
120
- }
121
- if (value.length === 0) {
122
- return undefined
123
- }
72
+ const transitionWhereOf = (query: TransitionQuery | null | undefined, scope: Scope): TransitionWhere => clean({
73
+ entityId: scope.entityId,
74
+ card: query?.card,
75
+ project: query?.project,
76
+ sinceSeq: query?.sinceSeq,
77
+ action: query?.action,
78
+ state: query?.state,
79
+ })
124
80
 
125
- return value.some(entry => entry.includes(',') || entry.startsWith('['))
126
- ? JSON.stringify(value)
127
- : value.join(',')
128
- }
81
+ const emptyCounts = (): IntrinsicCounts => ({
82
+ total: 0, [IntrinsicStatus.Planned]: 0, [IntrinsicStatus.InProgress]: 0, [IntrinsicStatus.Closed]: 0,
83
+ })
129
84
 
130
- /** Every entry of a list query value, whatever shape the transport left it in. */
131
- export const decodeList = (value: unknown, key: string = 'list'): string[] | undefined => {
132
- if (value == null || value === '') {
133
- return undefined
134
- }
135
- if (Array.isArray(value)) {
136
- const entries = value.filter(entry => entry != null && entry !== '').map(entry => `${entry}`)
137
- return entries.length > 0 ? entries : undefined
138
- }
139
- if (typeof value !== 'string') {
140
- return [`${value}`]
141
- }
142
- if (value.startsWith('[')) {
143
- const parsed: unknown = (() => {
144
- try {
145
- return JSON.parse(value)
146
- } catch {
147
- throw new PlanningError(`malformed:query:${key}`)
85
+ const summaryOf = (cards: Workcard[], parents: string[]): SummaryView => cards.reduce<SummaryView>(
86
+ (view, card) => {
87
+ if (card.parent == null || !parents.includes(card.parent)) {
88
+ return view
148
89
  }
149
- })()
150
- return Array.isArray(parsed) ? parsed.map(entry => `${entry}`) : undefined
151
- }
152
- const entries = value.split(',').map(entry => entry.trim()).filter(entry => entry !== '')
90
+ const counts = view[card.parent] ?? emptyCounts()
91
+ counts.total += 1
92
+ counts[card.intrinsic] = (counts[card.intrinsic] ?? 0) + 1
93
+ view[card.parent] = counts
94
+ return view
95
+ }, {}
96
+ )
153
97
 
154
- return entries.length > 0 ? entries : undefined
98
+ return { criteriaOf, listOptionsOf, specCriteriaOf, linkWhereOf, transitionWhereOf, summaryOf }
155
99
  }
156
100
 
157
- /** A list decoded to a bare value when it holds one — the same criteria either way. */
158
- const decodeOneOrMany = <T extends string>(value: unknown, key: string): T | T[] | undefined => {
159
- const entries = decodeList(value, key) as T[] | undefined
160
- return entries == null ? undefined : entries.length === 1 ? entries[0] : entries
161
- }
101
+ export const queryHelper = createQueryHelper()
162
102
 
163
- const encodeJson = (value: unknown): string | undefined => value == null ? undefined : JSON.stringify(value)
103
+ /** @deprecated compat:factory-refactor — use `queryHelper.criteriaOf(…)` */
104
+ export const criteriaOf = (query?: WorkcardQuery | null, scope?: Scope): Criteria<Workcard> =>
105
+ queryHelper.criteriaOf(query, scope)
164
106
 
165
- const decodeJson = <T>(value: unknown, key: string): T | undefined => {
166
- if (value == null || value === '') {
167
- return undefined
168
- }
169
- if (typeof value !== 'string') {
170
- return value as T
171
- }
172
- try {
173
- return JSON.parse(value) as T
174
- } catch {
175
- throw new PlanningError(`malformed:query:${key}`)
176
- }
177
- }
107
+ /** @deprecated compat:factory-refactor — use `queryHelper.listOptionsOf(…)` */
108
+ export const listOptionsOf = <T extends ResourceRecord>(query?: ListOptions<T> | null): ListOptions<T> =>
109
+ queryHelper.listOptionsOf<T>(query)
178
110
 
179
- const decodeNumber = (value: unknown, key: string): number | undefined => {
180
- if (value == null || value === '') {
181
- return undefined
182
- }
183
- const number = typeof value === 'number' ? value : Number(value)
184
- if (Number.isNaN(number)) {
185
- throw new PlanningError(`malformed:query:${key}`)
186
- }
111
+ /** @deprecated compat:factory-refactor — use `queryHelper.specCriteriaOf(…)` */
112
+ export const specCriteriaOf = (
113
+ parent: string, query?: SpecificationQuery | null, scope?: Scope
114
+ ): Criteria<Specification> => queryHelper.specCriteriaOf(parent, query, scope)
187
115
 
188
- return number
189
- }
116
+ /** @deprecated compat:factory-refactor — use `queryHelper.linkWhereOf(…)` */
117
+ export const linkWhereOf = (query: RelationshipQuery | null | undefined, scope: Scope): RelationshipWhere =>
118
+ queryHelper.linkWhereOf(query, scope)
190
119
 
191
- const decodeBoolean = (value: unknown): boolean | undefined =>
192
- value == null || value === '' ? undefined : value === true || value === 'true' || value === '1'
120
+ /** @deprecated compat:factory-refactor — use `queryHelper.transitionWhereOf(…)` */
121
+ export const transitionWhereOf = (query: TransitionQuery | null | undefined, scope: Scope): TransitionWhere =>
122
+ queryHelper.transitionWhereOf(query, scope)
193
123
 
194
- const decodeText = (value: unknown): string | undefined =>
195
- value == null || value === '' ? undefined : `${Array.isArray(value) ? value[0] : value}`
124
+ /** @deprecated compat:factory-refactor — use `queryHelper.summaryOf(…)` */
125
+ export const summaryOf = (cards: Workcard[], parents: string[]): SummaryView => queryHelper.summaryOf(cards, parents)
196
126
 
197
- /** `[{ field: 'order' }, { field: 'updatedAt', order: 'desc' }]` ⇄ `order,-updatedAt`. */
198
- export const encodeSort = <T>(sort?: Sort<T>[] | null): string | undefined =>
199
- sort == null || sort.length === 0 ? undefined : sort.map(entry => typeof entry === 'string'
200
- ? entry
201
- : `${entry.order === 'desc' ? '-' : ''}${entry.field}`).join(',')
127
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeList(…)` */
128
+ export const encodeList = (value?: string | readonly string[] | null): string | undefined => wireHelper.encodeList(value)
202
129
 
203
- export const decodeSort = <T>(value: unknown): Sort<T>[] | undefined => {
204
- if (value == null || value === '') {
205
- return undefined
206
- }
207
- const entries: unknown[] = Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : [value]
208
- const sort = entries.flatMap((entry): Sort<T>[] => {
209
- if (typeof entry !== 'string') {
210
- return entry != null && typeof entry === 'object' && 'field' in entry ? [entry as Sort<T>] : []
211
- }
212
- const field = entry.trim()
213
- if (field === '' || field === '-') {
214
- return []
215
- }
216
- return field.startsWith('-')
217
- ? [{ field: field.slice(1) as Sort<T> & string, order: 'desc' } as Sort<T>]
218
- : [field as Sort<T>]
219
- })
130
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeList(…)` */
131
+ export const decodeList = (value: unknown, key: string = 'list'): string[] | undefined => wireHelper.decodeList(value, key)
220
132
 
221
- return sort.length > 0 ? sort : undefined
222
- }
133
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeSort(…)` */
134
+ export const encodeSort = <T>(sort?: Sort<T>[] | null): string | undefined => wireHelper.encodeSort<T>(sort)
223
135
 
224
- const encodeListOptions = (query?: ListOptions<any> | null): ListWire => clean({
225
- page: query?.page,
226
- size: query?.size,
227
- sort: encodeSort(query?.sort),
228
- })
229
-
230
- const decodeListOptions = <T>(wire: Record<string, unknown>): ListOptions<T> => clean({
231
- page: decodeNumber(wire.page, 'page'),
232
- size: decodeNumber(wire.size, 'size'),
233
- sort: decodeSort<T>(wire.sort),
234
- })
235
-
236
- const asRecord = (wire: unknown): Record<string, unknown> =>
237
- wire != null && typeof wire === 'object' ? wire as Record<string, unknown> : {}
238
-
239
- export const encodeWorkcardQuery = (query?: WorkcardQuery | null): WorkcardQueryWire => clean({
240
- ...encodeListOptions(query),
241
- kind: encodeList(query?.kind),
242
- type: encodeList(query?.type),
243
- parent: query?.parent,
244
- within: query?.within,
245
- status: encodeList(query?.status),
246
- intrinsic: encodeList(query?.intrinsic),
247
- flow: encodeJson(query?.flow),
248
- labels: encodeList(query?.labels),
249
- code: encodeList(query?.code),
250
- ids: encodeList(query?.ids),
251
- fields: query?.fields != null && Object.keys(query.fields).length > 0 ? encodeJson(query.fields) : undefined,
252
- q: query?.q,
253
- category: encodeList(query?.category),
254
- updatedSince: query?.updatedSince,
255
- })
256
-
257
- /** @throws {PlanningError} `malformed:query:<key>` for a value that does not decode */
258
- export const decodeWorkcardQuery = (wire?: WorkcardQueryWire | WorkcardQuery | null): WorkcardQuery => {
259
- const value = asRecord(wire)
260
- return clean({
261
- ...decodeListOptions<Workcard>(value),
262
- kind: decodeOneOrMany<WorkcardKind>(value.kind, 'kind'),
263
- type: decodeOneOrMany(value.type, 'type'),
264
- parent: decodeText(value.parent),
265
- within: decodeText(value.within),
266
- status: decodeOneOrMany(value.status, 'status'),
267
- intrinsic: decodeOneOrMany<IntrinsicStatus>(value.intrinsic, 'intrinsic'),
268
- flow: decodeJson<WorkcardQuery['flow']>(value.flow, 'flow'),
269
- labels: decodeList(value.labels, 'labels'),
270
- code: decodeOneOrMany(value.code, 'code'),
271
- ids: decodeList(value.ids, 'ids'),
272
- fields: decodeJson<Record<string, unknown>>(value.fields, 'fields'),
273
- q: decodeText(value.q),
274
- category: decodeOneOrMany(value.category, 'category'),
275
- updatedSince: decodeText(value.updatedSince),
276
- })
277
- }
136
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeSort(…)` */
137
+ export const decodeSort = <T>(value: unknown): Sort<T>[] | undefined => wireHelper.decodeSort<T>(value)
278
138
 
279
- export const encodeSummaryQuery = (query: SummaryQuery): SummaryQueryWire => clean({
280
- parents: encodeList(query.parents) ?? '',
281
- kind: query.kind,
282
- type: encodeList(query.type),
283
- })
284
-
285
- export const decodeSummaryQuery = (wire?: SummaryQueryWire | SummaryQuery | null): SummaryQuery => {
286
- const value = asRecord(wire)
287
- return clean({
288
- parents: decodeList(value.parents, 'parents') ?? [],
289
- kind: decodeText(value.kind) as WorkcardKind | undefined,
290
- type: decodeOneOrMany(value.type, 'type'),
291
- })
292
- }
139
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeWorkcardQuery(…)` */
140
+ export const encodeWorkcardQuery = (query?: WorkcardQuery | null): WorkcardQueryWire => wireHelper.encodeWorkcardQuery(query)
293
141
 
294
- export const encodeTransitionQuery = (query?: TransitionQuery | null): TransitionQueryWire => clean({
295
- ...encodeListOptions(query),
296
- card: query?.card,
297
- project: query?.project,
298
- sinceSeq: query?.sinceSeq,
299
- action: encodeList(query?.action),
300
- state: query?.state,
301
- })
302
-
303
- export const decodeTransitionQuery = (wire?: TransitionQueryWire | TransitionQuery | null): TransitionQuery => {
304
- const value = asRecord(wire)
305
- return clean({
306
- ...decodeListOptions(value),
307
- card: decodeText(value.card),
308
- project: decodeText(value.project),
309
- sinceSeq: decodeNumber(value.sinceSeq, 'sinceSeq'),
310
- action: decodeOneOrMany(value.action, 'action'),
311
- state: decodeText(value.state),
312
- }) as TransitionQuery
313
- }
142
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeWorkcardQuery(…)` */
143
+ export const decodeWorkcardQuery = (wire?: WorkcardQueryWire | WorkcardQuery | null): WorkcardQuery =>
144
+ wireHelper.decodeWorkcardQuery(wire)
314
145
 
315
- export const encodeSpecificationQuery = (query?: SpecificationQuery | null): SpecificationQueryWire => clean({
316
- ...encodeListOptions(query),
317
- category: encodeList(query?.category),
318
- all: query?.all,
319
- })
320
-
321
- export const decodeSpecificationQuery = (wire?: SpecificationQueryWire | SpecificationQuery | null): SpecificationQuery => {
322
- const value = asRecord(wire)
323
- return clean({
324
- ...decodeListOptions<Specification>(value),
325
- category: decodeOneOrMany(value.category, 'category'),
326
- all: decodeBoolean(value.all),
327
- })
328
- }
146
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeSummaryQuery(…)` */
147
+ export const encodeSummaryQuery = (query: SummaryQuery): SummaryQueryWire => wireHelper.encodeSummaryQuery(query)
329
148
 
330
- export const encodeRelationshipQuery = (query?: RelationshipQuery | null): RelationshipQueryWire => clean({
331
- ...encodeListOptions(query),
332
- from: encodeList(query?.from),
333
- to: encodeList(query?.to),
334
- type: encodeList(query?.type),
335
- })
336
-
337
- export const decodeRelationshipQuery = (wire?: RelationshipQueryWire | RelationshipQuery | null): RelationshipQuery => {
338
- const value = asRecord(wire)
339
- return clean({
340
- ...decodeListOptions(value),
341
- from: decodeOneOrMany(value.from, 'from'),
342
- to: decodeOneOrMany(value.to, 'to'),
343
- type: decodeOneOrMany(value.type, 'type'),
344
- }) as RelationshipQuery
345
- }
149
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeSummaryQuery(…)` */
150
+ export const decodeSummaryQuery = (wire?: SummaryQueryWire | SummaryQuery | null): SummaryQuery =>
151
+ wireHelper.decodeSummaryQuery(wire)
152
+
153
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeTransitionQuery(…)` */
154
+ export const encodeTransitionQuery = (query?: TransitionQuery | null): TransitionQueryWire =>
155
+ wireHelper.encodeTransitionQuery(query)
156
+
157
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeTransitionQuery(…)` */
158
+ export const decodeTransitionQuery = (wire?: TransitionQueryWire | TransitionQuery | null): TransitionQuery =>
159
+ wireHelper.decodeTransitionQuery(wire)
160
+
161
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeSpecificationQuery(…)` */
162
+ export const encodeSpecificationQuery = (query?: SpecificationQuery | null): SpecificationQueryWire =>
163
+ wireHelper.encodeSpecificationQuery(query)
164
+
165
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeSpecificationQuery(…)` */
166
+ export const decodeSpecificationQuery = (wire?: SpecificationQueryWire | SpecificationQuery | null): SpecificationQuery =>
167
+ wireHelper.decodeSpecificationQuery(wire)
168
+
169
+ /** @deprecated compat:factory-refactor — use `wireHelper.encodeRelationshipQuery(…)` */
170
+ export const encodeRelationshipQuery = (query?: RelationshipQuery | null): RelationshipQueryWire =>
171
+ wireHelper.encodeRelationshipQuery(query)
172
+
173
+ /** @deprecated compat:factory-refactor — use `wireHelper.decodeRelationshipQuery(…)` */
174
+ export const decodeRelationshipQuery = (wire?: RelationshipQueryWire | RelationshipQuery | null): RelationshipQuery =>
175
+ wireHelper.decodeRelationshipQuery(wire)
@@ -0,0 +1,64 @@
1
+ import type { PlanningSchemaKind } from '../../consts.js'
2
+ import type {
3
+ PlanningSchemaBundle, PlanningSchemaRegistry, SchemaScope, ScopedSchemaBundle, ScopedSchemaRecord,
4
+ ScopedSchemaRegistry, StatusFlowSchema, WorkcardTypeSchema,
5
+ } from '../../types.js'
6
+
7
+ /** Data-defined (scoped) types and flows: their keys, the checks a write passes, and the layer they resolve to. */
8
+ export interface ScopedSchemaHelper {
9
+ /** One layer's identity of a record: organization, project (or none), kind, key. */
10
+ schemaRecordKey: (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>) => string
11
+ /** The key a declaration is addressed by: a type's `type`, a flow's `id`. */
12
+ schemaKeyOf: (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema) => string
13
+ /**
14
+ * Refuse a data-defined declaration of a key the code registry seals: a code type or flow that does
15
+ * not say `overridable: true`, and any project or specification type.
16
+ *
17
+ * @throws {SchemaSealed}
18
+ */
19
+ assertOverridable: (
20
+ code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows'>, kind: PlanningSchemaKind, key: string
21
+ ) => void
22
+ /**
23
+ * The closed-form checks of a data-defined flow: the declaration's own schema, at least one status,
24
+ * unique status keys, every rule's `to` declared, and a `from` that is `'*'` or a list of declared
25
+ * statuses.
26
+ *
27
+ * @throws {SchemaInvalid}
28
+ */
29
+ assertFlowSchema: (flow: StatusFlowSchema) => void
30
+ /**
31
+ * The closed-form checks of a data-defined type: the declaration's own schema, the `card` kind,
32
+ * a `fields` schema that compiles, and unique flow ids every one of which resolves.
33
+ *
34
+ * @throws {SchemaInvalid}
35
+ */
36
+ assertTypeSchema: (type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>) => void
37
+ /**
38
+ * One layer resolved: the code bundle, overlaid by the organization-wide records, then by the
39
+ * project's (when `scope.project` names one). Pure — every store and both sides of the wire resolve
40
+ * the same records to the same bundle.
41
+ *
42
+ * - A code key is sealed unless it says `overridable: true`; project and specification types are
43
+ * always sealed, so only card types and flows are data-defined. A record under a sealed key is
44
+ * ignored here (the write path refuses it with `SchemaSealed`).
45
+ * - A project record overrides an organization-wide one of the same key.
46
+ * - A retired record gives way to the nearest live declaration below it; a key retired in every
47
+ * layer resolves retired — present in `types`/`flows`, listed in `retired`.
48
+ * - A data-defined declaration carries its record's `version`.
49
+ */
50
+ resolveScopedBundle: (
51
+ code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
52
+ ) => ScopedSchemaBundle
53
+ /**
54
+ * A read-only registry over a resolved layer. It answers exactly what `makeSchemaRegistry` answers
55
+ * for the same types and flows, plus where each key came from and whether it is retired; its
56
+ * `registerType`, `registerFlow` and `load` refuse with `PlanningUnsupported`.
57
+ */
58
+ scopedRegistryOf: (bundle: ScopedSchemaBundle) => ScopedSchemaRegistry
59
+ /**
60
+ * The live type that keeps a flow from being retired in a resolved layer: the flow resolves retired
61
+ * (no live declaration below the retired record) while a type that is not retired still runs it.
62
+ */
63
+ flowInUse: (bundle: ScopedSchemaBundle, flow: string) => string | undefined
64
+ }