@owlmeans/planning 0.1.18-rc.8 → 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/errors.ts CHANGED
@@ -206,6 +206,72 @@ export class PlanningScopeMismatch extends PlanningError {
206
206
  }
207
207
  }
208
208
 
209
+ /**
210
+ * A data-defined schema write lost its compare-and-set: the layer's record is not at the version
211
+ * the write replaces. Re-read the layer and retry.
212
+ */
213
+ export class SchemaConflict extends PlanningError {
214
+ public static override typeName: string = `${PlanningError.typeName}SchemaConflict`
215
+ /** The stored record moved past the expected version: answered 409. */
216
+ public static httpStatus = 409
217
+
218
+ constructor(message: string = 'error') {
219
+ super(`schema-conflict:${message}`)
220
+ this.type = SchemaConflict.typeName
221
+ }
222
+ }
223
+
224
+ /** A flow cannot be retired while a live type of an affected layer still resolves it. */
225
+ export class SchemaInUse extends PlanningError {
226
+ public static override typeName: string = `${PlanningError.typeName}SchemaInUse`
227
+ /** Other declarations still depend on the key: answered 409. */
228
+ public static httpStatus = 409
229
+
230
+ constructor(message: string = 'error') {
231
+ super(`schema-in-use:${message}`)
232
+ this.type = SchemaInUse.typeName
233
+ }
234
+ }
235
+
236
+ /**
237
+ * The key is registered in code and does not declare itself `overridable` — or it is a project
238
+ * or specification type, which only code declares.
239
+ */
240
+ export class SchemaSealed extends PlanningError {
241
+ public static override typeName: string = `${PlanningError.typeName}SchemaSealed`
242
+ /** The key cannot be declared as data: answered 422. */
243
+ public static httpStatus = 422
244
+
245
+ constructor(message: string = 'error') {
246
+ super(`schema-sealed:${message}`)
247
+ this.type = SchemaSealed.typeName
248
+ }
249
+ }
250
+
251
+ /** A data-defined declaration fails its closed-form checks. */
252
+ export class SchemaInvalid extends PlanningError {
253
+ public static override typeName: string = `${PlanningError.typeName}SchemaInvalid`
254
+ /** The declaration's content is refused: answered 422. */
255
+ public static httpStatus = 422
256
+
257
+ constructor(message: string = 'error') {
258
+ super(`schema-invalid:${message}`)
259
+ this.type = SchemaInvalid.typeName
260
+ }
261
+ }
262
+
263
+ /** The request's access does not grant the write (creating or deleting a project, defining schemas). */
264
+ export class PlanningForbidden extends PlanningError {
265
+ public static override typeName: string = `${PlanningError.typeName}Forbidden`
266
+ /** The caller lacks the grant the write needs: answered 403. */
267
+ public static httpStatus = 403
268
+
269
+ constructor(message: string = 'error') {
270
+ super(`forbidden:${message}`)
271
+ this.type = PlanningForbidden.typeName
272
+ }
273
+ }
274
+
209
275
  /** The store behind this type cannot do what was asked (a foreign provider without a log). */
210
276
  export class PlanningUnsupported extends PlanningError {
211
277
  public static override typeName: string = `${PlanningError.typeName}Unsupported`
@@ -234,4 +300,9 @@ ResilientError.registerErrorClass(WorkcardConflict)
234
300
  ResilientError.registerErrorClass(CommitTimeout)
235
301
  ResilientError.registerErrorClass(CommitFailed)
236
302
  ResilientError.registerErrorClass(PlanningScopeMismatch)
303
+ ResilientError.registerErrorClass(SchemaConflict)
304
+ ResilientError.registerErrorClass(SchemaInUse)
305
+ ResilientError.registerErrorClass(SchemaSealed)
306
+ ResilientError.registerErrorClass(SchemaInvalid)
307
+ ResilientError.registerErrorClass(PlanningForbidden)
237
308
  ResilientError.registerErrorClass(PlanningUnsupported)
@@ -0,0 +1,5 @@
1
+ /** The kinds an application declares properties for, in merge order. */
2
+ export const APP_KINDS = ['type', 'flow', 'card'] as const
3
+
4
+ /** The only keywords a kind's declaration may carry — a merge by name honours nothing else. */
5
+ export const APP_SCHEMA_KEYS = new Set(['type', 'properties', 'required'])
@@ -0,0 +1,54 @@
1
+ import type { AnySchema } from 'ajv'
2
+ import type { PlanningSchemaKind } from '../../consts.js'
3
+ import type { AnyTypeSchema, PlanningAppDeclarations } from '../../types.js'
4
+
5
+ /** The declarations whose properties a definition carries in its `app` bag. */
6
+ export type PlanningAppBagKind = PlanningSchemaKind.Type | PlanningSchemaKind.Flow
7
+
8
+ /**
9
+ * The application's pregenerated planning properties: the `app` bag of every type and flow
10
+ * declaration, and the base `fields` of every card — declared in code, beside the custom fields a
11
+ * data-defined type gives its cards.
12
+ */
13
+ export interface AppSchemaHelper {
14
+ /**
15
+ * Merge declarations in the order given — a planning service passes its own `schemas.app` first,
16
+ * then every plugin's in plugin order (`order` ascending, registration order among equals). Per
17
+ * kind (`type`, `flow`, `card`): `properties` are united, a later declaration of a name replacing
18
+ * the earlier one whole; `required` is the union. `undefined` when no source declares anything.
19
+ *
20
+ * @throws {SchemaInvalid} a malformed source
21
+ */
22
+ merge: (sources: ReadonlyArray<PlanningAppDeclarations | undefined>) => PlanningAppDeclarations | undefined
23
+ /**
24
+ * The closed form of declarations: per kind only `type` (`'object'`), `properties` (names without
25
+ * `.` or `$`) and `required` (each name declared), and — outside a browser document — a schema that
26
+ * compiles. A page never compiles: a strict `script-src` refuses code generation, and what a page
27
+ * registers its server compiled when it registered it.
28
+ *
29
+ * @throws {SchemaInvalid} `app:<kind>:…`
30
+ */
31
+ assertDeclarations: (app: PlanningAppDeclarations) => void
32
+ /**
33
+ * The schema a definition's `app` bag must match: the declared kind closed over its properties
34
+ * (`additionalProperties: false`). `undefined` when the kind is not declared.
35
+ */
36
+ bagSchemaOf: (app: PlanningAppDeclarations | undefined, kind: PlanningAppBagKind) => AnySchema | undefined
37
+ /**
38
+ * Check a declaration's `app` bag: refused when the kind is undeclared and the bag is present;
39
+ * matched against {@link bagSchemaOf} otherwise (an absent bag is `{}`, so a required property
40
+ * makes the bag mandatory).
41
+ *
42
+ * @throws {SchemaInvalid} `<kind>:<key>:app:…`
43
+ */
44
+ assertBag: (kind: PlanningAppBagKind, key: string, bag: unknown, app?: PlanningAppDeclarations) => void
45
+ /**
46
+ * The schema a type's `fields` validate against. Without base card fields, or for a project or
47
+ * specification type, it is the type's own `fields` object itself. Otherwise the type's schema
48
+ * stays the root (so its `#/…` references still resolve) and gains: each base name in its
49
+ * top-level `properties` as `{}` when it does not declare it (its `additionalProperties` never
50
+ * refuses a base field; its own declaration of the name still applies and may narrow it), and the
51
+ * base object schema appended to its `allOf`.
52
+ */
53
+ fieldsSchemaOf: (type: AnyTypeSchema, app?: PlanningAppDeclarations) => AnySchema
54
+ }
@@ -0,0 +1,158 @@
1
+ import type { AnySchema, ValidateFunction } from 'ajv'
2
+ import { PlanningSchemaKind, WorkcardKind } from '../consts.js'
3
+ import { SchemaInvalid } from '../errors.js'
4
+ import type { PlanningAppDeclarations, PlanningAppSchema } from '../types.js'
5
+ import type { AppSchemaHelper, PlanningAppBagKind } from './app/types.js'
6
+ import { APP_KINDS, APP_SCHEMA_KEYS } from './app/consts.local.js'
7
+ import { makeAjv, validateHelper } from './validate.js'
8
+
9
+ const messageOf = (error: unknown): string => error instanceof Error ? error.message : `${error}`
10
+
11
+ const isObject = (value: unknown): value is Record<string, unknown> =>
12
+ value != null && typeof value === 'object' && !Array.isArray(value)
13
+
14
+ export const createAppSchemaHelper = (): AppSchemaHelper => {
15
+ // Keyed by the declared kind object a registry keeps — a fresh ajv per entry, so a replaced
16
+ // declaration leaves nothing compiled behind.
17
+ const bags = new WeakMap<PlanningAppSchema, ValidateFunction>()
18
+
19
+ // A page never compiles: under a strict `script-src` Ajv's code generation is refused, and the
20
+ // declarations a page registers are its server's, compiled there when they were registered.
21
+ const compiles = (): boolean => typeof (globalThis as { document?: unknown }).document === 'undefined'
22
+
23
+ const objectSchemaOf = (declared: PlanningAppSchema): Record<string, unknown> => ({
24
+ type: 'object',
25
+ properties: declared.properties,
26
+ ...((declared.required?.length ?? 0) > 0 ? { required: [...declared.required!] } : {}),
27
+ })
28
+
29
+ const declaredOf = (app: PlanningAppDeclarations | undefined, kind: PlanningAppBagKind): PlanningAppSchema | undefined =>
30
+ kind === PlanningSchemaKind.Type ? app?.type : app?.flow
31
+
32
+ const assertDeclarations: AppSchemaHelper['assertDeclarations'] = app => {
33
+ if (!isObject(app)) {
34
+ throw new SchemaInvalid('app:declarations')
35
+ }
36
+ const kinds: readonly string[] = APP_KINDS
37
+ const stranger = Object.keys(app).find(key => !kinds.includes(key))
38
+ if (stranger != null) {
39
+ throw new SchemaInvalid(`app:kind:${stranger}`)
40
+ }
41
+ for (const kind of APP_KINDS) {
42
+ const declared: unknown = app[kind]
43
+ if (declared == null) {
44
+ continue
45
+ }
46
+ if (!isObject(declared)) {
47
+ throw new SchemaInvalid(`app:${kind}:schema`)
48
+ }
49
+ const keyword = Object.keys(declared).find(key => !APP_SCHEMA_KEYS.has(key))
50
+ if (keyword != null) {
51
+ throw new SchemaInvalid(`app:${kind}:keyword:${keyword}`)
52
+ }
53
+ if (declared.type != null && declared.type !== 'object') {
54
+ throw new SchemaInvalid(`app:${kind}:type`)
55
+ }
56
+ if (!isObject(declared.properties)) {
57
+ throw new SchemaInvalid(`app:${kind}:properties`)
58
+ }
59
+ const names = Object.keys(declared.properties)
60
+ const badName = names.find(name => name === '' || name.includes('.') || name.includes('$'))
61
+ if (badName != null) {
62
+ throw new SchemaInvalid(`app:${kind}:name:${badName}`)
63
+ }
64
+ const required: unknown = declared.required
65
+ if (required != null && (!Array.isArray(required) || required.some(name => typeof name !== 'string'))) {
66
+ throw new SchemaInvalid(`app:${kind}:required`)
67
+ }
68
+ const missing = (required as string[] | undefined)?.find(name => !names.includes(name))
69
+ if (missing != null) {
70
+ throw new SchemaInvalid(`app:${kind}:required:${missing}`)
71
+ }
72
+ if (compiles()) {
73
+ try {
74
+ makeAjv().compile(objectSchemaOf(declared as unknown as PlanningAppSchema))
75
+ } catch (error) {
76
+ throw new SchemaInvalid(`app:${kind}:${messageOf(error)}`)
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ const merge: AppSchemaHelper['merge'] = sources => {
83
+ const merged: PlanningAppDeclarations = {}
84
+ for (const source of sources) {
85
+ if (source == null) {
86
+ continue
87
+ }
88
+ assertDeclarations(source)
89
+ for (const kind of APP_KINDS) {
90
+ const next = source[kind]
91
+ if (next == null) {
92
+ continue
93
+ }
94
+ const current = merged[kind]
95
+ const required = [...new Set([...(current?.required ?? []), ...(next.required ?? [])])]
96
+ merged[kind] = {
97
+ type: 'object',
98
+ properties: { ...current?.properties, ...structuredClone(next.properties) },
99
+ ...(required.length > 0 ? { required } : {}),
100
+ }
101
+ }
102
+ }
103
+
104
+ return Object.keys(merged).length > 0 ? merged : undefined
105
+ }
106
+
107
+ const bagSchemaOf: AppSchemaHelper['bagSchemaOf'] = (app, kind) => {
108
+ const declared = declaredOf(app, kind)
109
+
110
+ return declared == null ? undefined : { ...objectSchemaOf(declared), additionalProperties: false }
111
+ }
112
+
113
+ const assertBag: AppSchemaHelper['assertBag'] = (kind, key, bag, app) => {
114
+ const declared = declaredOf(app, kind)
115
+ if (declared == null) {
116
+ if (bag != null) {
117
+ throw new SchemaInvalid(`${kind}:${key}:app:undeclared`)
118
+ }
119
+ return
120
+ }
121
+ const keys = validateHelper.invalidFieldKeys(bag)
122
+ if (keys.length > 0) {
123
+ throw new SchemaInvalid(`${kind}:${key}:app:keys:${keys.join(',')}`)
124
+ }
125
+ let validate = bags.get(declared)
126
+ if (validate == null) {
127
+ validate = makeAjv().compile(bagSchemaOf(app, kind) as object)
128
+ bags.set(declared, validate)
129
+ }
130
+ if (!validate(bag ?? {})) {
131
+ throw new SchemaInvalid(`${kind}:${key}:app:${validateHelper.ajvErrorText(validate.errors)}`)
132
+ }
133
+ }
134
+
135
+ const fieldsSchemaOf: AppSchemaHelper['fieldsSchemaOf'] = (type, app) => {
136
+ const base = app?.card
137
+ const own: AnySchema = type.fields
138
+ if (base == null || type.kind !== WorkcardKind.Card) {
139
+ return own
140
+ }
141
+ const declared = objectSchemaOf(base)
142
+ if (!isObject(own)) {
143
+ return own === false ? false : declared
144
+ }
145
+ // The type's schema stays the root, so its own `#/…` references keep resolving.
146
+ const admitted = Object.fromEntries(Object.keys(base.properties).map(name => [name, {}]))
147
+
148
+ return {
149
+ ...own,
150
+ properties: { ...admitted, ...(isObject(own.properties) ? own.properties : {}) },
151
+ allOf: [...(Array.isArray(own.allOf) ? own.allOf : []), declared],
152
+ }
153
+ }
154
+
155
+ return { merge, assertDeclarations, bagSchemaOf, assertBag, fieldsSchemaOf }
156
+ }
157
+
158
+ export const appSchemaHelper = createAppSchemaHelper()
@@ -0,0 +1,10 @@
1
+ /** Never written by a transition's `changes`: the fold takes them from the transition itself. */
2
+ export const IDENTITY_KEYS = new Set(['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt'])
3
+
4
+ /** Cannot be cleared: a record without them is not a record. */
5
+ export const REQUIRED_KEYS = new Set([
6
+ ...IDENTITY_KEYS, 'title', 'parents', 'status', 'intrinsic', 'flows', 'labels', 'fields',
7
+ ])
8
+
9
+ /** Merged shallowly into the card's own map rather than replacing it. */
10
+ export const MERGED_KEYS = new Set(['fields', 'flows'])
@@ -0,0 +1,32 @@
1
+ import type { Relationship, Transition, Workcard } from '../../types.js'
2
+
3
+ /** The pure fold — the card and its edges after one transition. */
4
+ export interface ApplyHelper {
5
+ /**
6
+ * THE fold: the card after one transition. Pure, total, no I/O — every store (memory, Mongo, a
7
+ * client mirror) folds with this and nothing else, so one log always means one card.
8
+ *
9
+ * - An already-applied transition (`seq <= card.seq`) returns the card unchanged: re-folding is
10
+ * safe.
11
+ * - Anything but the next seq (`card.seq + 1`, or 1 for a create with no card) is refused with
12
+ * `PlanningError('fold:out-of-order')`.
13
+ * - The log is replayed as written: a transition the executor would refuse today (an update naming
14
+ * `createdBy`, appended before that refusal existed) still folds. Guards live at admission.
15
+ * - `create` builds the whole record from `changes` plus the transition's identity; `delete`
16
+ * answers `null`; `link`/`unlink` only move `seq`/`head`/`updatedAt` (the edges are
17
+ * {@link ApplyHelper.applyRelationship}'s); `update`/`transit` write `changes` — `fields` and
18
+ * `flows` merge, everything else replaces — and clear `unset`.
19
+ * - Every result has `seq = transition.seq`, `head = max(head, seq)`, `updatedAt = transition.at`.
20
+ *
21
+ * @throws {PlanningError} `planning:fold:out-of-order:…`
22
+ */
23
+ applyTransition: <T extends Workcard = Workcard>(card: T | null | undefined, transition: Transition) => T | null
24
+ /**
25
+ * The relationships after one transition. Pure and total, like {@link ApplyHelper.applyTransition}.
26
+ *
27
+ * `create` adds its `links`, `link` adds its `link`, `unlink` removes the matching edge, and a
28
+ * `delete` removes every edge touching the card. An edge that already exists is kept as it is, so
29
+ * re-folding adds nothing. Every other action answers the list unchanged. Always a new array.
30
+ */
31
+ applyRelationship: (links: Relationship[], transition: Transition) => Relationship[]
32
+ }