@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,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
+ })
59
55
 
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
- })
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>
66
64
 
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
- }
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, fromKind: query?.fromKind, toKind: query?.toKind,
70
+ })
124
71
 
125
- return value.some(entry => entry.includes(',') || entry.startsWith('['))
126
- ? JSON.stringify(value)
127
- : value.join(',')
128
- }
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
+ })
129
80
 
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}`)
81
+ const emptyCounts = (): IntrinsicCounts => ({
82
+ total: 0, [IntrinsicStatus.Planned]: 0, [IntrinsicStatus.InProgress]: 0, [IntrinsicStatus.Closed]: 0,
83
+ })
84
+
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 @@
1
+ export const REPLY_CONTAINERS = new Set(['items', 'records', 'card', 'record', 'transition', 'bundle', 'scope'])
@@ -0,0 +1,13 @@
1
+ /** Storage organization metadata is absent from planning HTTP and socket replies. */
2
+ export type PlanningReply<T> = T extends readonly (infer R)[] ? PlanningReply<R>[]
3
+ : T extends object ? {
4
+ [K in keyof T as K extends 'entityId' ? never : K]: K extends
5
+ 'items' | 'records' | 'card' | 'record' | 'transition' | 'bundle' | 'scope'
6
+ ? PlanningReply<T[K]> : T[K]
7
+ } : T
8
+
9
+ export interface PlanningReplyHelper {
10
+ project: <T>(value: T) => PlanningReply<T>
11
+ /** Restore the shared facade shape with empty, non-authoritative organization metadata. */
12
+ hydrate: <T>(value: PlanningReply<T>) => T
13
+ }
@@ -0,0 +1,27 @@
1
+ import { REPLY_CONTAINERS } from './reply/consts.js'
2
+ import type { PlanningReply, PlanningReplyHelper } from './reply/types.js'
3
+
4
+ /** Project known planning envelopes; custom fields, changes and schema definitions remain opaque. */
5
+ export const createPlanningReplyHelper = (): PlanningReplyHelper => {
6
+ const visit = (value: unknown, hydrate: boolean, scope = false): unknown => {
7
+ if (Array.isArray(value)) return value.map(item => visit(item, hydrate))
8
+ if (value == null || typeof value !== 'object') return value
9
+ const source = value as Record<string, unknown>
10
+ const result: Record<string, unknown> = {}
11
+ for (const [key, item] of Object.entries(source)) {
12
+ if (key === 'entityId') continue
13
+ result[key] = REPLY_CONTAINERS.has(key) ? visit(item, hydrate, key === 'scope') : item
14
+ }
15
+ const record = typeof source.createdAt === 'string'
16
+ || (typeof source.card === 'string' && typeof source.seq === 'number'
17
+ && (source.commit != null || typeof source.state === 'string'))
18
+ if (hydrate && (scope || record)) result.entityId = ''
19
+ return result
20
+ }
21
+ return {
22
+ project: <T>(value: T): PlanningReply<T> => visit(value, false) as PlanningReply<T>,
23
+ hydrate: <T>(value: PlanningReply<T>): T => visit(value, true) as T,
24
+ }
25
+ }
26
+
27
+ export const planningReplyHelper = createPlanningReplyHelper()
@@ -0,0 +1,78 @@
1
+ import type { AssigneeTypeSchema } from '../../resources/types.js'
2
+ import type { PlanningSchemaKind } from '../../consts.js'
3
+ import type {
4
+ PlanningAppDeclarations, PlanningSchemaBundle, PlanningSchemaRegistry, SchemaScope, ScopedSchemaBundle, ScopedSchemaRecord,
5
+ ScopedSchemaRegistry, StatusFlowSchema, WorkcardTypeSchema,
6
+ } from '../../types.js'
7
+
8
+ /** Data-defined (scoped) types and flows: their keys, the checks a write passes, and the layer they resolve to. */
9
+ export interface ScopedSchemaHelper {
10
+ /** One layer's identity of a record: organization, project (or none), kind, key. */
11
+ schemaRecordKey: (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>) => string
12
+ /** The key a declaration is addressed by: a type's `type`, a flow's `id`. */
13
+ schemaKeyOf: (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema | AssigneeTypeSchema) => string
14
+ /**
15
+ * Refuse a data-defined declaration of a key the code registry seals: a code type or flow that does
16
+ * not say `overridable: true`, and any project or specification type.
17
+ *
18
+ * @throws {SchemaSealed}
19
+ */
20
+ assertOverridable: (
21
+ code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows' | 'assigneeTypes'>, kind: PlanningSchemaKind, key: string
22
+ ) => void
23
+ /**
24
+ * The closed-form checks of a data-defined assignee type: the declaration's own schema and a
25
+ * `fields` schema that compiles.
26
+ *
27
+ * @throws {SchemaInvalid}
28
+ */
29
+ assertAssigneeTypeSchema: (schema: AssigneeTypeSchema) => void
30
+ /**
31
+ * The closed-form checks of a data-defined flow: the declaration's own schema, at least one status,
32
+ * unique status keys, every rule's `to` declared, a `from` that is `'*'` or a list of declared
33
+ * statuses, and its `app` bag against the application's `flow` declaration
34
+ * (`appSchemaHelper.assertBag` — refused when `app` declares none).
35
+ *
36
+ * @throws {SchemaInvalid}
37
+ */
38
+ assertFlowSchema: (flow: StatusFlowSchema, app?: PlanningAppDeclarations) => void
39
+ /**
40
+ * The closed-form checks of a data-defined type: the declaration's own schema, the `card` kind,
41
+ * its `app` bag against the application's `type` declaration (refused when `app` declares none),
42
+ * a `fields` schema that compiles together with the application's base card fields, and unique
43
+ * flow ids every one of which resolves.
44
+ *
45
+ * @throws {SchemaInvalid}
46
+ */
47
+ assertTypeSchema: (type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>, app?: PlanningAppDeclarations) => void
48
+ /**
49
+ * One layer resolved: the code bundle, overlaid by the organization-wide records, then by the
50
+ * project's (when `scope.project` names one). Pure — every store and both sides of the wire resolve
51
+ * the same records to the same bundle.
52
+ *
53
+ * - A code key is sealed unless it says `overridable: true`; project and specification types are
54
+ * always sealed, so only card types and flows are data-defined. A record under a sealed key is
55
+ * ignored here (the write path refuses it with `SchemaSealed`).
56
+ * - A project record overrides an organization-wide one of the same key.
57
+ * - A retired record gives way to the nearest live declaration below it; a key retired in every
58
+ * layer resolves retired — present in `types`/`flows`, listed in `retired`.
59
+ * - A data-defined declaration carries its record's `version`.
60
+ * - The code bundle's `app` declarations pass through as they are; a record's `app` bag is not
61
+ * re-checked here (the write path checks it), so a record written under earlier declarations
62
+ * still resolves.
63
+ */
64
+ resolveScopedBundle: (
65
+ code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
66
+ ) => ScopedSchemaBundle
67
+ /**
68
+ * A read-only registry over a resolved layer. It answers exactly what `makeSchemaRegistry` answers
69
+ * for the same types and flows, plus where each key came from and whether it is retired; its
70
+ * `registerType`, `registerFlow` and `load` refuse with `PlanningUnsupported`.
71
+ */
72
+ scopedRegistryOf: (bundle: ScopedSchemaBundle) => ScopedSchemaRegistry
73
+ /**
74
+ * The live type that keeps a flow from being retired in a resolved layer: the flow resolves retired
75
+ * (no live declaration below the retired record) while a type that is not retired still runs it.
76
+ */
77
+ flowInUse: (bundle: ScopedSchemaBundle, flow: string) => string | undefined
78
+ }