@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
package/src/registry.ts CHANGED
@@ -1,23 +1,47 @@
1
+ import type { AssigneeTypeSchema } from './resources/types.js'
1
2
  import type { ValidateFunction } from 'ajv'
2
3
  import { PLANNING_SCHEMA_VERSION } from './consts.js'
3
4
  import { UnknownStatusFlow, UnknownWorkcardType } from './errors.js'
5
+ import { appSchemaHelper } from './helpers/app.js'
4
6
  import { makeAjv } from './helpers/validate.js'
5
- import type { AnyTypeSchema, PlanningSchemaBundle, PlanningSchemaRegistry, StatusFlowSchema } from './types.js'
7
+ import type { AnyTypeSchema, PlanningAppDeclarations, PlanningSchemaBundle, PlanningSchemaRegistry, StatusFlowSchema } from './types.js'
6
8
 
7
9
  /**
8
- * Types, flows and the compiled validator of each type's `fields`.
10
+ * Types, flows, the application's declarations and the compiled validator of each type's `fields`
11
+ * (its own schema, with the application's base card fields for a `card` type).
9
12
  *
10
13
  * It lives in the contract package so a client answers `can()` / `available()` from a loaded bundle
11
14
  * with no round trip. A later registration of the same type or flow id replaces the earlier one;
12
- * `load(bundle)` replaces everything.
15
+ * `registerApp` replaces the declarations; `load(bundle)` replaces everything.
13
16
  */
14
17
  export const makeSchemaRegistry = (bundle?: Partial<PlanningSchemaBundle>): PlanningSchemaRegistry => {
15
18
  const types = new Map<string, AnyTypeSchema>()
19
+ const assigneeTypes = new Map<string, AssigneeTypeSchema>()
20
+ const assigneeValidators = new Map<string, ValidateFunction>()
16
21
  const flows = new Map<string, StatusFlowSchema>()
17
22
  const validators = new Map<string, ValidateFunction>()
23
+ let app: PlanningAppDeclarations | undefined
18
24
  let ajv = makeAjv()
19
25
 
20
26
  const registry: PlanningSchemaRegistry = {
27
+ registerAssigneeType: schema => {
28
+ assigneeTypes.set(schema.type, schema)
29
+ assigneeValidators.delete(schema.type)
30
+ },
31
+ assigneeTypes: () => [...assigneeTypes.values()],
32
+ assigneeType: type => {
33
+ const schema = assigneeTypes.get(type)
34
+ if (schema == null) throw new UnknownWorkcardType(`assignee:${type}`)
35
+ return schema
36
+ },
37
+ assigneeValidator: type => {
38
+ let validator = assigneeValidators.get(type)
39
+ if (validator == null) {
40
+ validator = ajv.compile(registry.assigneeType(type).fields)
41
+ assigneeValidators.set(type, validator)
42
+ }
43
+ return validator
44
+ },
21
45
  registerType: schema => {
22
46
  types.set(schema.type, schema)
23
47
  validators.delete(schema.type)
@@ -27,6 +51,22 @@ export const makeSchemaRegistry = (bundle?: Partial<PlanningSchemaBundle>): Plan
27
51
  flows.set(flow.id, flow)
28
52
  },
29
53
 
54
+ registerApp: next => {
55
+ if (next != null) {
56
+ appSchemaHelper.assertDeclarations(next)
57
+ }
58
+ if (next == null && app == null) {
59
+ return
60
+ }
61
+ app = next == null ? undefined : structuredClone(next)
62
+ validators.clear()
63
+ assigneeValidators.clear()
64
+ // A fresh ajv: a replaced declaration must not collide with a schema `$id` the old one compiled.
65
+ ajv = makeAjv()
66
+ },
67
+
68
+ app: () => app,
69
+
30
70
  types: () => [...types.values()],
31
71
 
32
72
  type: type => {
@@ -57,10 +97,12 @@ export const makeSchemaRegistry = (bundle?: Partial<PlanningSchemaBundle>): Plan
57
97
  return registry.flow(primary)
58
98
  },
59
99
 
100
+ fieldsSchema: type => appSchemaHelper.fieldsSchemaOf(registry.type(type), app),
101
+
60
102
  validator: type => {
61
103
  let validate = validators.get(type)
62
104
  if (validate == null) {
63
- validate = ajv.compile(registry.type(type).fields)
105
+ validate = ajv.compile(registry.fieldsSchema(type))
64
106
  validators.set(type, validate)
65
107
  }
66
108
  return validate
@@ -70,19 +112,28 @@ export const makeSchemaRegistry = (bundle?: Partial<PlanningSchemaBundle>): Plan
70
112
  version: PLANNING_SCHEMA_VERSION,
71
113
  types: [...types.values()],
72
114
  flows: [...flows.values()],
115
+ assigneeTypes: [...assigneeTypes.values()],
116
+ ...(app != null ? { app } : {}),
73
117
  }),
74
118
 
75
119
  load: next => {
120
+ assigneeTypes.clear()
121
+ assigneeValidators.clear()
76
122
  types.clear()
77
123
  flows.clear()
78
124
  validators.clear()
125
+ app = undefined
79
126
  // A fresh ajv: a replaced type must not collide with a schema `$id` the old one compiled.
80
127
  ajv = makeAjv()
128
+ registry.registerApp(next.app ?? undefined)
129
+ next.assigneeTypes?.forEach(registry.registerAssigneeType)
81
130
  next.flows.forEach(registry.registerFlow)
82
131
  next.types.forEach(registry.registerType)
83
132
  },
84
133
  }
85
134
 
135
+ registry.registerApp(bundle?.app ?? undefined)
136
+ bundle?.assigneeTypes?.forEach(registry.registerAssigneeType)
86
137
  bundle?.flows?.forEach(registry.registerFlow)
87
138
  bundle?.types?.forEach(registry.registerType)
88
139
 
@@ -0,0 +1,11 @@
1
+ import type { MentionHelper } from './types.local.js'
2
+
3
+ /** Stable-id Markdown mentions. Escaped display text cannot change the referenced identity. */
4
+ export const mentionHelper: MentionHelper = {
5
+ encode: (id, nickname) => `[@${nickname.replace(/[\\\[\]]/g, '\\$&')}](assignee:${encodeURIComponent(id)})`,
6
+ parse: body => [...new Set([...body.matchAll(/\[@(?:\\.|[^\]\\])*\]\(assignee:([^\s)]+)\)/g)]
7
+ .flatMap(match => {
8
+ try { return [decodeURIComponent(match[1])] } catch { return [] }
9
+ }))],
10
+ nicknameKey: nickname => nickname.normalize('NFKC').trim().toLocaleLowerCase('en-US'),
11
+ }
@@ -0,0 +1,21 @@
1
+ import type { PlanningReply } from '../../helpers/reply/types.js'
2
+ import type { EntrypointProtocol } from '@owlmeans/entrypoint'
3
+ import type { ListResult } from '@owlmeans/resource'
4
+ import type { AssigneeDraft, TeamDraft, CommentDraft, PlanningRecord } from '../types.js'
5
+
6
+ export interface PlanningResourceQueryWire { query?: string }
7
+ export interface PlanningResourceCommand {
8
+ action: 'create' | 'update' | 'retire' | 'remove' | 'members' | 'projects' | 'assignees' | 'addMember' | 'removeMember' | 'attach' | 'detach' | 'rebuild'
9
+ id?: string
10
+ draft?: Partial<AssigneeDraft & TeamDraft & CommentDraft>
11
+ version?: number
12
+ assignee?: string
13
+ project?: string
14
+ }
15
+ export type PlanningResourceReply = PlanningRecord | PlanningRecord[] | string[] | null
16
+
17
+ export interface PlanningResourceProtocols<R extends PlanningRecord = PlanningRecord> {
18
+ get: EntrypointProtocol<{ params: { id: string } }, PlanningReply<R>>
19
+ list: EntrypointProtocol<{ query: PlanningResourceQueryWire }, PlanningReply<ListResult<R>>>
20
+ write: EntrypointProtocol<{ body: PlanningResourceCommand }, PlanningReply<PlanningResourceReply>>
21
+ }
@@ -0,0 +1,36 @@
1
+ import type { PlanningReply } from '../helpers/reply/types.js'
2
+ import { planningAliasHelper } from '../aliases.js'
3
+ import { contract, protocol, typed } from '@owlmeans/entrypoint'
4
+ import type { EntrypointProtocol } from '@owlmeans/entrypoint'
5
+ import { backend, route, RouteMethod } from '@owlmeans/route'
6
+ import { AssigneeKind } from '../consts.js'
7
+ import { WorkcardParamsSchema } from '../schemas.js'
8
+ import type { ListResult } from '@owlmeans/resource'
9
+ import type { PlanningRecord } from './types.js'
10
+ import type { PlanningResourceCommand, PlanningResourceProtocols, PlanningResourceQueryWire, PlanningResourceReply } from './protocol/types.js'
11
+
12
+ /** Opt-in auxiliary branches share the planning base's authentication guards. */
13
+ export const makePlanningResourceProtocols = <R extends PlanningRecord>(base: EntrypointProtocol<any, any>, alias: string, name: 'assignees' | 'teams' | 'comments' | 'mentions'): PlanningResourceProtocols<R> => {
14
+ const aliases = planningAliasHelper.planningResourceAliases(alias)[name]
15
+ const commandSchema = {
16
+ type: 'object', properties: {
17
+ action: { type: 'string', enum: ['create', 'update', 'retire', 'remove', 'members', 'projects', 'assignees', 'addMember', 'removeMember', 'attach', 'detach', 'rebuild'] },
18
+ id: { type: 'string', minLength: 1, maxLength: 128, nullable: true },
19
+ version: { type: 'number', minimum: 1, nullable: true },
20
+ assignee: { type: 'string', minLength: 1, maxLength: 128, nullable: true },
21
+ project: { type: 'string', minLength: 1, maxLength: 128, nullable: true },
22
+ draft: { type: 'object', properties: {
23
+ nickname: { type: 'string', nullable: true }, kind: { type: 'string', enum: [...Object.values(AssigneeKind), null], nullable: true },
24
+ type: { type: 'string', nullable: true }, name: { type: 'string', nullable: true }, externalId: { type: 'string', nullable: true },
25
+ card: { type: 'string', nullable: true }, body: { type: 'string', nullable: true },
26
+ fields: { type: 'object', additionalProperties: true, required: [], nullable: true },
27
+ authentication: { type: 'object', properties: { provider: { type: 'string' }, externalId: { type: 'string' } }, required: ['provider', 'externalId'], additionalProperties: false, nullable: true },
28
+ }, required: [], additionalProperties: false, nullable: true },
29
+ }, required: ['action'], additionalProperties: false,
30
+ }
31
+ return Object.freeze({
32
+ get: protocol(route(aliases.get, `/${name}/:id`, backend({ parent: base }, RouteMethod.GET)), contract.request({ params: typed<{ id: string }>(WorkcardParamsSchema) }, typed<PlanningReply<R>>())),
33
+ list: protocol(route(aliases.list, `/${name}`, backend({ parent: base }, RouteMethod.GET)), contract.request({ query: typed<PlanningResourceQueryWire>({ type: 'object', properties: { query: { type: 'string', maxLength: 16384, nullable: true } }, required: [], additionalProperties: false }) }, typed<PlanningReply<ListResult<R>>>())),
34
+ write: protocol(route(aliases.write, `/${name}`, backend({ parent: base }, RouteMethod.POST)), contract.request({ body: typed<PlanningResourceCommand>(commandSchema as any) }, typed<PlanningReply<PlanningResourceReply>>())),
35
+ })
36
+ }
@@ -0,0 +1,5 @@
1
+ export interface MentionHelper {
2
+ encode: (id: string, nickname: string) => string
3
+ parse: (body: string) => string[]
4
+ nicknameKey: (nickname: string) => string
5
+ }
@@ -0,0 +1,148 @@
1
+ import type { AnySchema, ValidateFunction } from 'ajv'
2
+ import type { ListOptions, ListResult, ResourceRecord, Criteria } from '@owlmeans/resource'
3
+ import type { AssigneeKind } from '../consts.js'
4
+
5
+ /** Identity in an external authentication domain; neither email nor nickname is its key. */
6
+ export interface AssigneeAuthentication {
7
+ provider: string
8
+ externalId: string
9
+ }
10
+
11
+ export interface AssigneeTypeSchema {
12
+ type: string
13
+ version: number
14
+ kind: AssigneeKind
15
+ fields: AnySchema
16
+ authentication?: 'required' | 'optional'
17
+ label?: string
18
+ overridable?: boolean
19
+ }
20
+
21
+ export interface PlanningRecord extends ResourceRecord {
22
+ id?: string
23
+ entityId: string
24
+ version: number
25
+ createdAt: string
26
+ updatedAt?: string
27
+ }
28
+
29
+ export interface Assignee extends PlanningRecord {
30
+ nickname: string
31
+ /** Organization-scoped, Unicode-normalized, case-insensitive identity key. */
32
+ nicknameKey: string
33
+ kind: AssigneeKind
34
+ type: string
35
+ fields: Record<string, unknown>
36
+ authentication?: AssigneeAuthentication
37
+ retired?: boolean
38
+ }
39
+
40
+ export interface Team extends PlanningRecord {
41
+ name: string
42
+ externalId?: string
43
+ fields: Record<string, unknown>
44
+ }
45
+
46
+ export interface Comment extends PlanningRecord {
47
+ card: string
48
+ /** Immutable author, resolved by the host from a trusted planning scope. */
49
+ author: string
50
+ body: string
51
+ }
52
+
53
+ /** A derived mention of a stable assignee id; revision is the source comment's version. */
54
+ export interface CommentMention extends PlanningRecord {
55
+ comment: string
56
+ card: string
57
+ assignee: string
58
+ revision: number
59
+ }
60
+
61
+ export interface AssigneeDraft {
62
+ nickname: string
63
+ kind: AssigneeKind
64
+ type: string
65
+ fields?: Record<string, unknown>
66
+ authentication?: AssigneeAuthentication
67
+ }
68
+
69
+ export interface TeamDraft {
70
+ name: string
71
+ externalId?: string
72
+ fields?: Record<string, unknown>
73
+ }
74
+
75
+ export interface CommentDraft {
76
+ card: string
77
+ body: string
78
+ }
79
+
80
+ /** Compare-and-set version is mandatory for updates/removal, optional for retiring an assignee. */
81
+ export interface PlanningWriteOptions { version: number }
82
+
83
+ export interface AssigneeQuery extends ListOptions<Assignee> {
84
+ ids?: string[]
85
+ nickname?: string
86
+ kind?: AssigneeKind
87
+ type?: string
88
+ authentication?: AssigneeAuthentication
89
+ retired?: boolean
90
+ }
91
+ export interface TeamQuery extends ListOptions<Team> { ids?: string[], externalId?: string }
92
+ export interface CommentQuery extends ListOptions<Comment> { card?: string, author?: string }
93
+ export interface MentionQuery extends ListOptions<CommentMention> { comment?: string, card?: string, assignee?: string }
94
+
95
+ /** Durable versioned resource port. put expects stored version + 1, with 1 for creation. */
96
+ export interface PlanningRecordStore<R extends PlanningRecord> {
97
+ get: (id: string, entityId: string) => Promise<R | null>
98
+ list: (where: Criteria<R>, opts?: ListOptions<R>) => Promise<ListResult<R>>
99
+ put: (record: R) => Promise<R>
100
+ drop: (id: string, entityId: string, version: number) => Promise<void>
101
+ }
102
+
103
+ export interface PlanningResourceFacade {
104
+ assignees: {
105
+ get: (id: string) => Promise<Assignee>
106
+ load: (id: string) => Promise<Assignee | null>
107
+ list: (query?: AssigneeQuery) => Promise<ListResult<Assignee>>
108
+ create: (draft: AssigneeDraft) => Promise<Assignee>
109
+ update: (id: string, changes: Partial<AssigneeDraft>, opts: PlanningWriteOptions) => Promise<Assignee>
110
+ retire: (id: string, opts: PlanningWriteOptions) => Promise<Assignee>
111
+ }
112
+ teams: {
113
+ get: (id: string) => Promise<Team>
114
+ load: (id: string) => Promise<Team | null>
115
+ list: (query?: TeamQuery) => Promise<ListResult<Team>>
116
+ create: (draft: TeamDraft) => Promise<Team>
117
+ update: (id: string, changes: Partial<TeamDraft>, opts: PlanningWriteOptions) => Promise<Team>
118
+ remove: (id: string, opts: PlanningWriteOptions) => Promise<void>
119
+ members: (team: string) => Promise<Assignee[]>
120
+ addMember: (team: string, assignee: string) => Promise<void>
121
+ removeMember: (team: string, assignee: string) => Promise<void>
122
+ projects: (team: string) => Promise<string[]>
123
+ attach: (team: string, project: string) => Promise<void>
124
+ detach: (team: string, project: string) => Promise<void>
125
+ assignees: (project: string) => Promise<Assignee[]>
126
+ }
127
+ comments: {
128
+ get: (id: string) => Promise<Comment>
129
+ load: (id: string) => Promise<Comment | null>
130
+ list: (query?: CommentQuery) => Promise<ListResult<Comment>>
131
+ create: (draft: CommentDraft) => Promise<Comment>
132
+ update: (id: string, changes: Pick<CommentDraft, 'body'>, opts: PlanningWriteOptions) => Promise<Comment>
133
+ remove: (id: string, opts: PlanningWriteOptions) => Promise<void>
134
+ }
135
+ mentions: {
136
+ get: (id: string) => Promise<CommentMention>
137
+ load: (id: string) => Promise<CommentMention | null>
138
+ list: (query?: MentionQuery) => Promise<ListResult<CommentMention>>
139
+ rebuild: (comment: string) => Promise<CommentMention[]>
140
+ }
141
+ }
142
+
143
+ export interface AssigneeSchemaRegistry {
144
+ registerAssigneeType: (schema: AssigneeTypeSchema) => void
145
+ assigneeType: (type: string) => AssigneeTypeSchema
146
+ assigneeTypes: () => AssigneeTypeSchema[]
147
+ assigneeValidator: (type: string) => ValidateFunction
148
+ }
package/src/schemas.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { AssigneeTypeSchema, Assignee, Team, Comment, CommentMention } from './resources/types.js'
2
+ import { AssigneeKind, PlanningResourceKind, ProjectMode } from './consts.js'
1
3
  import type { JSONSchemaType } from 'ajv'
2
4
  import { IdValueSchema } from '@owlmeans/auth'
3
5
  import { createListSchema } from '@owlmeans/resource'
@@ -5,12 +7,14 @@ import type { ListResult } from '@owlmeans/resource'
5
7
  import {
6
8
  BODY_MAX, CAUSE_MAX, CODE_MAX, CodeScope, CodeStyle, CommitState, DESCRIPTION_MAX, IntrinsicPolicy,
7
9
  IntrinsicStatus, KEY_MAX, LABEL_MAX, MAX_COMMIT_POLL, MAX_LABELS, MAX_PARENTS, MAX_QUERY_LIST,
8
- QUERY_TEXT_MAX, REF_MAX, SpecificationFormat, TITLE_MAX, TransitionAction, TYPE_MAX, WorkcardKind,
10
+ PlanningSchemaKind, QUERY_TEXT_MAX, REF_MAX, SchemaOrigin, SchemaWriteMode, SpecificationFormat,
11
+ TITLE_MAX, TransitionAction, TYPE_MAX, WorkcardKind,
9
12
  } from './consts.js'
10
13
  import type {
11
14
  AnyTypeSchema, CodePolicy, CommitEvent, CommitFeedQuery, CommitQuery, CommitStatus, ExecuteRequest,
12
- PlanningSchemaBundle, ProjectTypeSchema, Relationship, RelationshipDraft, RelationshipQueryWire,
13
- RelationshipType, RevisionsQuery, Specification, SpecificationQueryWire, SpecificationRevision,
15
+ PlanningAppDeclarations, PlanningSchemaBundle, ProjectTypeSchema, Relationship, RelationshipDraft, RelationshipQueryWire,
16
+ RelationshipType, RevisionsQuery, SchemaDefineRequest, SchemaKey, SchemaListQuery, ScopedSchemaBundle,
17
+ ScopedSchemaRecord, Specification, SpecificationQueryWire, SpecificationRevision,
14
18
  SpecificationRevisionList, SpecificationSlot, StatusDefinition, StatusFlowSchema,
15
19
  StatusTransitionRule, SummaryQueryWire, SummaryView, Transition, TransitionActor, TransitionCommit,
16
20
  TransitionExecution, TransitionParams, TransitionQueryWire, TransitionReceiptView, Workcard,
@@ -66,6 +70,10 @@ const workcardProperties = {
66
70
  title: { type: 'string', minLength: 1, maxLength: TITLE_MAX },
67
71
  description: { type: 'string', maxLength: DESCRIPTION_MAX, nullable: true },
68
72
  parent: OptionalId,
73
+ parentType: { ...TypeKey, nullable: true },
74
+ reporter: OptionalId,
75
+ assignee: OptionalId,
76
+ mode: { type: 'string', enum: [...Object.values(ProjectMode), null], nullable: true },
69
77
  parents: { type: 'array', maxItems: MAX_PARENTS, items: Id },
70
78
  status: Label,
71
79
  intrinsic: IntrinsicStatusSchema,
@@ -99,7 +107,7 @@ const specificationProperties = {
99
107
  /** The optional form of a schema: `nullable`, and `null` admitted by an enum too. */
100
108
  const nullable = (schema: object): object => {
101
109
  const enumerated = (schema as { enum?: unknown[] }).enum
102
- return { ...schema, nullable: true, ...(enumerated != null ? { enum: [...enumerated, null] } : {}) }
110
+ return { ...schema, nullable: true, ...(enumerated != null ? { enum: [...new Set([...enumerated, null])] } : {}) }
103
111
  }
104
112
 
105
113
  const nullableOf = (properties: Record<string, object>): Record<string, object> =>
@@ -133,6 +141,8 @@ export const RelationshipDraftSchema = cast<RelationshipDraft>({
133
141
  type: Label,
134
142
  from: OptionalId,
135
143
  to: Id,
144
+ fromKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
145
+ toKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
136
146
  fields: OptionalOpenObject,
137
147
  },
138
148
  required: ['type', 'to'],
@@ -147,6 +157,8 @@ export const RelationshipSchema = cast<Relationship>({
147
157
  type: Label,
148
158
  from: Id,
149
159
  to: Id,
160
+ fromKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
161
+ toKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
150
162
  project: OptionalId,
151
163
  fields: OptionalOpenObject,
152
164
  createdAt: IsoDate,
@@ -164,6 +176,10 @@ export const WorkcardChangesSchema = cast<WorkcardChanges>({
164
176
  title: workcardProperties.title,
165
177
  description: workcardProperties.description,
166
178
  parent: workcardProperties.parent,
179
+ parentType: workcardProperties.parentType,
180
+ reporter: workcardProperties.reporter,
181
+ assignee: workcardProperties.assignee,
182
+ mode: workcardProperties.mode,
167
183
  parents: workcardProperties.parents,
168
184
  status: workcardProperties.status,
169
185
  intrinsic: workcardProperties.intrinsic,
@@ -260,6 +276,11 @@ export const StatusFlowSchemaSchema = cast<StatusFlowSchema>({
260
276
  statuses: { type: 'array', minItems: 1, items: StatusDefinitionSchema },
261
277
  transitions: { type: 'array', items: StatusTransitionRuleSchema },
262
278
  label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
279
+ tone: { type: 'string', maxLength: LABEL_MAX, nullable: true },
280
+ icon: { type: 'string', maxLength: LABEL_MAX, nullable: true },
281
+ overridable: { type: 'boolean', nullable: true },
282
+ /** The application's own properties — their own schema is applied by `assertFlowSchema`. */
283
+ app: OptionalOpenObject,
263
284
  },
264
285
  required: ['id', 'version', 'statuses', 'transitions'],
265
286
  additionalProperties: false,
@@ -303,6 +324,10 @@ export const RelationshipTypeSchema = cast<RelationshipType>({
303
324
  name: Label,
304
325
  from: { type: 'array', items: TypeKey, nullable: true },
305
326
  to: { type: 'array', items: TypeKey, nullable: true },
327
+ fromKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
328
+ toKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
329
+ field: { ...TypeKey, nullable: true },
330
+ multiple: { type: 'boolean', nullable: true },
306
331
  inverse: { ...Label, nullable: true },
307
332
  single: { type: 'boolean', nullable: true },
308
333
  label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
@@ -318,10 +343,17 @@ const typeProperties = {
318
343
  flows: { type: 'array', minItems: 1, items: TypeKey },
319
344
  intrinsic: nullable(IntrinsicPolicySchema),
320
345
  specifications: { type: 'array', items: SpecificationSlotSchema },
346
+ parents: { type: 'object', properties: { types: { type: 'array', items: TypeKey, nullable: true }, required: { type: 'boolean', nullable: true } }, required: [], additionalProperties: false, nullable: true },
347
+ children: { type: 'object', properties: { types: { type: 'array', items: TypeKey } }, required: ['types'], additionalProperties: false, nullable: true },
321
348
  relationships: { type: 'array', items: RelationshipTypeSchema, nullable: true },
322
349
  labels: { type: 'array', items: Label, nullable: true },
323
350
  code: { ...CodePolicySchema, nullable: true },
324
351
  label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
352
+ tone: { type: 'string', maxLength: LABEL_MAX, nullable: true },
353
+ icon: { type: 'string', maxLength: LABEL_MAX, nullable: true },
354
+ overridable: { type: 'boolean', nullable: true },
355
+ /** The application's own properties — their own schema is applied by `assertTypeSchema`. */
356
+ app: OptionalOpenObject,
325
357
  }
326
358
 
327
359
  const typeRequired = ['type', 'kind', 'version', 'fields', 'flows', 'specifications']
@@ -343,6 +375,7 @@ export const ProjectTypeSchemaSchema = cast<ProjectTypeSchema>({
343
375
  kind: { type: 'string', enum: [WorkcardKind.Project] },
344
376
  cardTypes: { type: 'array', items: TypeKey },
345
377
  projectTypes: { type: 'array', items: TypeKey, nullable: true },
378
+ scopedCardTypes: { type: 'boolean', nullable: true },
346
379
  },
347
380
  required: [...typeRequired, 'cardTypes'],
348
381
  additionalProperties: false,
@@ -356,22 +389,159 @@ export const AnyTypeSchemaSchema = cast<AnyTypeSchema>({
356
389
  kind: WorkcardKindSchema,
357
390
  cardTypes: { type: 'array', items: TypeKey, nullable: true },
358
391
  projectTypes: { type: 'array', items: TypeKey, nullable: true },
392
+ scopedCardTypes: { type: 'boolean', nullable: true },
359
393
  },
360
394
  required: typeRequired,
361
395
  additionalProperties: false,
362
396
  })
363
397
 
398
+ export const AssigneeTypeSchemaSchema = cast<AssigneeTypeSchema>({
399
+ type: 'object', properties: {
400
+ type: TypeKey, version: { type: 'number', minimum: 1 }, kind: { type: 'string', enum: Object.values(AssigneeKind) },
401
+ fields: OpenObject, authentication: { type: 'string', enum: ['required', 'optional', null], nullable: true },
402
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true }, overridable: { type: 'boolean', nullable: true },
403
+ }, required: ['type', 'version', 'kind', 'fields'], additionalProperties: false,
404
+ })
405
+ const planningRecordProperties = { id: OptionalId, entityId: Id, version: { type: 'number', minimum: 1 }, createdAt: IsoDate, updatedAt: OptionalIsoDate }
406
+ const planningRecordRequired = ['entityId', 'version', 'createdAt']
407
+ export const AssigneeSchema = cast<Assignee>({
408
+ type: 'object', properties: { ...planningRecordProperties,
409
+ nickname: { type: 'string', minLength: 1, maxLength: TYPE_MAX }, nicknameKey: TypeKey,
410
+ kind: { type: 'string', enum: Object.values(AssigneeKind) }, type: TypeKey, fields: OpenObject,
411
+ authentication: { type: 'object', properties: { provider: TypeKey, externalId: { type: 'string', minLength: 1, maxLength: REF_MAX } }, required: ['provider', 'externalId'], additionalProperties: false, nullable: true },
412
+ retired: { type: 'boolean', nullable: true },
413
+ }, required: [...planningRecordRequired, 'nickname', 'nicknameKey', 'kind', 'type', 'fields'], additionalProperties: false,
414
+ })
415
+ export const TeamSchema = cast<Team>({
416
+ type: 'object', properties: { ...planningRecordProperties, name: { type: 'string', minLength: 1, maxLength: TITLE_MAX }, externalId: { type: 'string', minLength: 1, maxLength: REF_MAX, nullable: true }, fields: OpenObject },
417
+ required: [...planningRecordRequired, 'name', 'fields'], additionalProperties: false,
418
+ })
419
+ export const CommentSchema = cast<Comment>({
420
+ type: 'object', properties: { ...planningRecordProperties, card: Id, author: Id, body: { type: 'string', minLength: 1, maxLength: BODY_MAX } },
421
+ required: [...planningRecordRequired, 'card', 'author', 'body'], additionalProperties: false,
422
+ })
423
+ export const CommentMentionSchema = cast<CommentMention>({
424
+ type: 'object', properties: { ...planningRecordProperties, comment: Id, card: Id, assignee: Id, revision: { type: 'number', minimum: 1 } },
425
+ required: [...planningRecordRequired, 'comment', 'card', 'assignee', 'revision'], additionalProperties: false,
426
+ })
427
+
428
+ /**
429
+ * The application's declarations as a bundle carries them. Each kind is an object schema whose
430
+ * closed form (`type`, `properties`, `required` only) `appSchemaHelper.assertDeclarations` checks.
431
+ */
432
+ export const PlanningAppDeclarationsSchema = cast<PlanningAppDeclarations>({
433
+ type: 'object',
434
+ properties: { type: OptionalOpenObject, flow: OptionalOpenObject, card: OptionalOpenObject },
435
+ required: [],
436
+ additionalProperties: false,
437
+ })
438
+
364
439
  export const PlanningSchemaBundleSchema = cast<PlanningSchemaBundle>({
365
440
  type: 'object',
366
441
  properties: {
367
442
  version: { type: 'number', minimum: 1 },
368
443
  types: { type: 'array', items: AnyTypeSchemaSchema },
369
444
  flows: { type: 'array', items: StatusFlowSchemaSchema },
445
+ assigneeTypes: { type: 'array', items: AssigneeTypeSchemaSchema, nullable: true },
446
+ app: { ...PlanningAppDeclarationsSchema, nullable: true },
447
+ },
448
+ required: ['version', 'types', 'flows'],
449
+ additionalProperties: false,
450
+ })
451
+
452
+ // ─── Data-defined (scoped) schemas ───────────────────────────────────────────────────────────────
453
+
454
+ export const PlanningSchemaKindSchema = cast<PlanningSchemaKind>({ type: 'string', enum: Object.values(PlanningSchemaKind) })
455
+ export const SchemaOriginSchema = cast<SchemaOrigin>({ type: 'string', enum: Object.values(SchemaOrigin) })
456
+ export const SchemaWriteModeSchema = cast<SchemaWriteMode>({ type: 'string', enum: Object.values(SchemaWriteMode) })
457
+
458
+ export const ScopedSchemaRecordSchema = cast<ScopedSchemaRecord>({
459
+ type: 'object',
460
+ properties: {
461
+ id: OptionalId,
462
+ entityId: Id,
463
+ project: OptionalId,
464
+ kind: PlanningSchemaKindSchema,
465
+ key: TypeKey,
466
+ version: { type: 'number', minimum: 1 },
467
+ /** A card type or a flow — its own schema is applied by `assertTypeSchema` / `assertFlowSchema`. */
468
+ definition: OpenObject,
469
+ retired: { type: 'boolean', nullable: true },
470
+ rev: { type: 'number', minimum: 0, nullable: true },
471
+ createdAt: IsoDate,
472
+ updatedAt: OptionalIsoDate,
473
+ by: { ...TransitionActorSchema, nullable: true },
474
+ },
475
+ required: ['entityId', 'kind', 'key', 'version', 'definition', 'createdAt'],
476
+ additionalProperties: false,
477
+ })
478
+
479
+ export const SchemaKeySchema = cast<SchemaKey>({
480
+ type: 'object',
481
+ properties: { kind: PlanningSchemaKindSchema, key: TypeKey },
482
+ required: ['kind', 'key'],
483
+ additionalProperties: false,
484
+ })
485
+
486
+ const originMap = () => ({ type: 'object', additionalProperties: { type: 'string', enum: Object.values(SchemaOrigin) }, required: [] })
487
+ const keyList = () => ({ type: 'array', items: TypeKey })
488
+
489
+ export const ScopedSchemaBundleSchema = cast<ScopedSchemaBundle>({
490
+ type: 'object',
491
+ properties: {
492
+ version: { type: 'number', minimum: 1 },
493
+ types: { type: 'array', items: AnyTypeSchemaSchema },
494
+ flows: { type: 'array', items: StatusFlowSchemaSchema },
495
+ assigneeTypes: { type: 'array', items: AssigneeTypeSchemaSchema, nullable: true },
496
+ app: { ...PlanningAppDeclarationsSchema, nullable: true },
497
+ scope: {
498
+ type: 'object',
499
+ properties: { entityId: Id, project: OptionalId },
500
+ required: ['entityId'],
501
+ additionalProperties: false,
502
+ nullable: true,
503
+ },
504
+ revision: { type: 'number', minimum: 0, nullable: true },
505
+ origins: {
506
+ type: 'object',
507
+ properties: { types: originMap(), flows: originMap(), assigneeTypes: { ...originMap(), nullable: true } },
508
+ required: ['types', 'flows'],
509
+ additionalProperties: false,
510
+ nullable: true,
511
+ },
512
+ retired: {
513
+ type: 'object',
514
+ properties: { types: keyList(), flows: keyList(), assigneeTypes: { ...keyList(), nullable: true } },
515
+ required: ['types', 'flows'],
516
+ additionalProperties: false,
517
+ nullable: true,
518
+ },
370
519
  },
371
520
  required: ['version', 'types', 'flows'],
372
521
  additionalProperties: false,
373
522
  })
374
523
 
524
+ export const SchemaListQuerySchema = cast<SchemaListQuery>({
525
+ type: 'object',
526
+ properties: { project: OptionalId },
527
+ required: [],
528
+ additionalProperties: false,
529
+ })
530
+
531
+ export const SchemaDefineRequestSchema = cast<SchemaDefineRequest>({
532
+ type: 'object',
533
+ properties: {
534
+ project: OptionalId,
535
+ mode: nullable(SchemaWriteModeSchema),
536
+ types: { type: 'array', maxItems: MAX_QUERY_LIST, items: WorkcardTypeSchemaSchema, nullable: true },
537
+ flows: { type: 'array', maxItems: MAX_QUERY_LIST, items: StatusFlowSchemaSchema, nullable: true },
538
+ assigneeTypes: { type: 'array', maxItems: MAX_QUERY_LIST, items: AssigneeTypeSchemaSchema, nullable: true },
539
+ retire: { type: 'array', maxItems: MAX_QUERY_LIST, items: SchemaKeySchema, nullable: true },
540
+ },
541
+ required: [],
542
+ additionalProperties: false,
543
+ })
544
+
375
545
  // ─── Execution ───────────────────────────────────────────────────────────────────────────────────
376
546
 
377
547
  export const WorkcardDraftSchema = cast<WorkcardDraft>({
@@ -380,6 +550,9 @@ export const WorkcardDraftSchema = cast<WorkcardDraft>({
380
550
  kind: WorkcardKindSchema,
381
551
  type: TypeKey,
382
552
  parent: OptionalId,
553
+ reporter: OptionalId,
554
+ assignee: OptionalId,
555
+ mode: workcardProperties.mode,
383
556
  parents: { ...workcardProperties.parents, nullable: true },
384
557
  title: workcardProperties.title,
385
558
  description: workcardProperties.description,
@@ -577,6 +750,8 @@ export const SpecificationQuerySchema = cast<SpecificationQueryWire>({
577
750
  export const RelationshipQuerySchema = cast<RelationshipQueryWire>({
578
751
  type: 'object',
579
752
  properties: {
753
+ fromKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
754
+ toKind: { type: 'string', enum: [...Object.values(PlanningResourceKind), null], nullable: true },
580
755
  ...listWireProperties(),
581
756
  from: listValue(),
582
757
  to: listValue(),