@owlmeans/planning 0.1.18-rc.0

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 (139) hide show
  1. package/README.md +169 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/planning/SKILL.md +248 -0
  4. package/build/consts.d.ts +126 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +129 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/entrypoints.d.ts +14 -0
  9. package/build/entrypoints.d.ts.map +1 -0
  10. package/build/entrypoints.js +69 -0
  11. package/build/entrypoints.js.map +1 -0
  12. package/build/errors.d.ts +94 -0
  13. package/build/errors.d.ts.map +1 -0
  14. package/build/errors.js +169 -0
  15. package/build/errors.js.map +1 -0
  16. package/build/helpers/apply.d.ts +27 -0
  17. package/build/helpers/apply.d.ts.map +1 -0
  18. package/build/helpers/apply.js +162 -0
  19. package/build/helpers/apply.js.map +1 -0
  20. package/build/helpers/card.d.ts +17 -0
  21. package/build/helpers/card.d.ts.map +1 -0
  22. package/build/helpers/card.js +31 -0
  23. package/build/helpers/card.js.map +1 -0
  24. package/build/helpers/changes.d.ts +51 -0
  25. package/build/helpers/changes.d.ts.map +1 -0
  26. package/build/helpers/changes.js +277 -0
  27. package/build/helpers/changes.js.map +1 -0
  28. package/build/helpers/code.d.ts +24 -0
  29. package/build/helpers/code.d.ts.map +1 -0
  30. package/build/helpers/code.js +47 -0
  31. package/build/helpers/code.js.map +1 -0
  32. package/build/helpers/index.d.ts +9 -0
  33. package/build/helpers/index.d.ts.map +1 -0
  34. package/build/helpers/index.js +9 -0
  35. package/build/helpers/index.js.map +1 -0
  36. package/build/helpers/query.d.ts +39 -0
  37. package/build/helpers/query.d.ts.map +1 -0
  38. package/build/helpers/query.js +293 -0
  39. package/build/helpers/query.js.map +1 -0
  40. package/build/helpers/specification.d.ts +21 -0
  41. package/build/helpers/specification.d.ts.map +1 -0
  42. package/build/helpers/specification.js +48 -0
  43. package/build/helpers/specification.js.map +1 -0
  44. package/build/helpers/status.d.ts +44 -0
  45. package/build/helpers/status.d.ts.map +1 -0
  46. package/build/helpers/status.js +78 -0
  47. package/build/helpers/status.js.map +1 -0
  48. package/build/helpers/validate.d.ts +30 -0
  49. package/build/helpers/validate.d.ts.map +1 -0
  50. package/build/helpers/validate.js +93 -0
  51. package/build/helpers/validate.js.map +1 -0
  52. package/build/i18n/be.json +51 -0
  53. package/build/i18n/de.json +51 -0
  54. package/build/i18n/en.json +51 -0
  55. package/build/i18n/es.json +51 -0
  56. package/build/i18n/pl.json +51 -0
  57. package/build/i18n/ru.json +51 -0
  58. package/build/i18n/uk.json +51 -0
  59. package/build/i18n.d.ts +2 -0
  60. package/build/i18n.d.ts.map +1 -0
  61. package/build/i18n.js +19 -0
  62. package/build/i18n.js.map +1 -0
  63. package/build/index.d.ts +10 -0
  64. package/build/index.d.ts.map +1 -0
  65. package/build/index.js +9 -0
  66. package/build/index.js.map +1 -0
  67. package/build/models/index.d.ts +7 -0
  68. package/build/models/index.d.ts.map +1 -0
  69. package/build/models/index.js +19 -0
  70. package/build/models/index.js.map +1 -0
  71. package/build/models/project.d.ts +4 -0
  72. package/build/models/project.d.ts.map +1 -0
  73. package/build/models/project.js +17 -0
  74. package/build/models/project.js.map +1 -0
  75. package/build/models/specification.d.ts +4 -0
  76. package/build/models/specification.d.ts.map +1 -0
  77. package/build/models/specification.js +15 -0
  78. package/build/models/specification.js.map +1 -0
  79. package/build/models/workcard.d.ts +14 -0
  80. package/build/models/workcard.d.ts.map +1 -0
  81. package/build/models/workcard.js +103 -0
  82. package/build/models/workcard.js.map +1 -0
  83. package/build/queue/index.d.ts +25 -0
  84. package/build/queue/index.d.ts.map +1 -0
  85. package/build/queue/index.js +14 -0
  86. package/build/queue/index.js.map +1 -0
  87. package/build/registry.d.ts +10 -0
  88. package/build/registry.d.ts.map +1 -0
  89. package/build/registry.js +75 -0
  90. package/build/registry.js.map +1 -0
  91. package/build/schemas.d.ts +56 -0
  92. package/build/schemas.d.ts.map +1 -0
  93. package/build/schemas.js +578 -0
  94. package/build/schemas.js.map +1 -0
  95. package/build/types.d.ts +785 -0
  96. package/build/types.d.ts.map +1 -0
  97. package/build/types.js +2 -0
  98. package/build/types.js.map +1 -0
  99. package/package.json +58 -0
  100. package/src/consts.ts +136 -0
  101. package/src/entrypoints.ts +127 -0
  102. package/src/errors.ts +207 -0
  103. package/src/helpers/apply.ts +183 -0
  104. package/src/helpers/card.ts +42 -0
  105. package/src/helpers/changes.ts +333 -0
  106. package/src/helpers/code.ts +66 -0
  107. package/src/helpers/index.ts +8 -0
  108. package/src/helpers/query.ts +345 -0
  109. package/src/helpers/specification.ts +65 -0
  110. package/src/helpers/status.ts +117 -0
  111. package/src/helpers/validate.ts +107 -0
  112. package/src/i18n/be.json +51 -0
  113. package/src/i18n/de.json +51 -0
  114. package/src/i18n/en.json +51 -0
  115. package/src/i18n/es.json +51 -0
  116. package/src/i18n/pl.json +51 -0
  117. package/src/i18n/ru.json +51 -0
  118. package/src/i18n/uk.json +51 -0
  119. package/src/i18n.ts +21 -0
  120. package/src/index.ts +9 -0
  121. package/src/models/index.ts +21 -0
  122. package/src/models/project.ts +24 -0
  123. package/src/models/specification.ts +21 -0
  124. package/src/models/workcard.ts +138 -0
  125. package/src/queue/index.ts +31 -0
  126. package/src/registry.ts +90 -0
  127. package/src/schemas.ts +654 -0
  128. package/src/types.ts +837 -0
  129. package/tests/apply.spec.ts +176 -0
  130. package/tests/code.spec.ts +40 -0
  131. package/tests/entrypoints.spec.ts +57 -0
  132. package/tests/fixtures.ts +124 -0
  133. package/tests/i18n.spec.ts +30 -0
  134. package/tests/models.spec.ts +133 -0
  135. package/tests/query.spec.ts +90 -0
  136. package/tests/schemas.spec.ts +74 -0
  137. package/tests/status.spec.ts +75 -0
  138. package/tests/tsconfig.json +17 -0
  139. package/tsconfig.json +15 -0
package/src/schemas.ts ADDED
@@ -0,0 +1,654 @@
1
+ import type { JSONSchemaType } from 'ajv'
2
+ import { IdValueSchema } from '@owlmeans/auth'
3
+ import { createListSchema } from '@owlmeans/resource'
4
+ import type { ListResult } from '@owlmeans/resource'
5
+ import {
6
+ BODY_MAX, CAUSE_MAX, CODE_MAX, CodeScope, CodeStyle, CommitState, DESCRIPTION_MAX, IntrinsicPolicy,
7
+ 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,
9
+ } from './consts.js'
10
+ import type {
11
+ AnyTypeSchema, CodePolicy, CommitEvent, CommitFeedQuery, CommitQuery, CommitStatus, ExecuteRequest,
12
+ PlanningSchemaBundle, ProjectTypeSchema, Relationship, RelationshipDraft, RelationshipQueryWire,
13
+ RelationshipType, RevisionsQuery, Specification, SpecificationQueryWire, SpecificationRevision,
14
+ SpecificationRevisionList, SpecificationSlot, StatusDefinition, StatusFlowSchema,
15
+ StatusTransitionRule, SummaryQueryWire, SummaryView, Transition, TransitionActor, TransitionCommit,
16
+ TransitionExecution, TransitionParams, TransitionQueryWire, TransitionReceiptView, Workcard,
17
+ WorkcardChanges, WorkcardDraft, WorkcardParams, WorkcardQueryWire, WorkcardTypeSchema,
18
+ } from './types.js'
19
+
20
+ /**
21
+ * Two rules every schema here keeps, because a record schema may be handed to a Mongo collection
22
+ * as its `$jsonSchema` and Mongo refuses both at boot: no `integer` anywhere (`number` only), and
23
+ * every optional property `nullable: true`. `schemas.spec.ts` walks every export and pins both.
24
+ */
25
+
26
+ const cast = <T>(schema: object): JSONSchemaType<T> => schema as unknown as JSONSchemaType<T>
27
+
28
+ const IsoDate = { type: 'string', minLength: 20, maxLength: 32, format: 'date-time' } as const
29
+ const OptionalIsoDate = { ...IsoDate, nullable: true } as const
30
+ const Id = IdValueSchema
31
+ const OptionalId = { ...IdValueSchema, nullable: true }
32
+ const OpenObject = { type: 'object', additionalProperties: true, required: [] } as const
33
+ const OptionalOpenObject = { ...OpenObject, nullable: true } as const
34
+ const Label = { type: 'string', minLength: 1, maxLength: LABEL_MAX } as const
35
+ const TypeKey = { type: 'string', minLength: 1, maxLength: TYPE_MAX } as const
36
+
37
+ export const WorkcardKindSchema = cast<WorkcardKind>({ type: 'string', enum: Object.values(WorkcardKind) })
38
+ export const IntrinsicStatusSchema = cast<IntrinsicStatus>({ type: 'string', enum: Object.values(IntrinsicStatus) })
39
+ export const IntrinsicPolicySchema = cast<IntrinsicPolicy>({ type: 'string', enum: Object.values(IntrinsicPolicy) })
40
+ export const TransitionActionSchema = cast<TransitionAction>({ type: 'string', enum: Object.values(TransitionAction) })
41
+ export const CommitStateSchema = cast<CommitState>({ type: 'string', enum: Object.values(CommitState) })
42
+ export const SpecificationFormatSchema = cast<SpecificationFormat>({ type: 'string', enum: Object.values(SpecificationFormat) })
43
+
44
+ // ─── Records ─────────────────────────────────────────────────────────────────────────────────────
45
+
46
+ export const TransitionActorSchema = cast<TransitionActor>({
47
+ type: 'object',
48
+ properties: {
49
+ profileId: OptionalId,
50
+ userId: OptionalId,
51
+ service: { type: 'string', maxLength: TYPE_MAX, nullable: true },
52
+ agent: { type: 'string', maxLength: TYPE_MAX, nullable: true },
53
+ runId: { type: 'string', maxLength: KEY_MAX, nullable: true },
54
+ channel: { type: 'string', maxLength: LABEL_MAX, nullable: true },
55
+ },
56
+ required: [],
57
+ additionalProperties: false,
58
+ })
59
+
60
+ const workcardProperties = {
61
+ id: OptionalId,
62
+ kind: WorkcardKindSchema,
63
+ type: TypeKey,
64
+ entityId: Id,
65
+ code: { type: 'string', minLength: 1, maxLength: CODE_MAX, nullable: true },
66
+ title: { type: 'string', minLength: 1, maxLength: TITLE_MAX },
67
+ description: { type: 'string', maxLength: DESCRIPTION_MAX, nullable: true },
68
+ parent: OptionalId,
69
+ parents: { type: 'array', maxItems: MAX_PARENTS, items: Id },
70
+ status: Label,
71
+ intrinsic: IntrinsicStatusSchema,
72
+ flows: { type: 'object', additionalProperties: Label, required: [] },
73
+ labels: { type: 'array', maxItems: MAX_LABELS, items: Label },
74
+ order: { type: 'number', nullable: true },
75
+ fields: OpenObject,
76
+ seq: { type: 'number', minimum: 0 },
77
+ head: { type: 'number', minimum: 0, nullable: true },
78
+ createdBy: OptionalId,
79
+ createdAt: IsoDate,
80
+ updatedAt: OptionalIsoDate,
81
+ closedAt: OptionalIsoDate,
82
+ }
83
+
84
+ const workcardRequired = [
85
+ 'kind', 'type', 'entityId', 'title', 'parents', 'status', 'intrinsic', 'flows', 'labels', 'fields',
86
+ 'seq', 'createdAt',
87
+ ]
88
+
89
+ const specificationProperties = {
90
+ category: Label,
91
+ format: SpecificationFormatSchema,
92
+ body: { type: 'string', maxLength: BODY_MAX, nullable: true },
93
+ ref: { type: 'string', maxLength: REF_MAX, nullable: true },
94
+ revision: { type: 'number', minimum: 1, nullable: true },
95
+ version: { type: 'number', nullable: true },
96
+ bodyChars: { type: 'number', minimum: 0, nullable: true },
97
+ }
98
+
99
+ /** The optional form of a schema: `nullable`, and `null` admitted by an enum too. */
100
+ const nullable = (schema: object): object => {
101
+ const enumerated = (schema as { enum?: unknown[] }).enum
102
+ return { ...schema, nullable: true, ...(enumerated != null ? { enum: [...enumerated, null] } : {}) }
103
+ }
104
+
105
+ const nullableOf = (properties: Record<string, object>): Record<string, object> =>
106
+ Object.fromEntries(Object.entries(properties).map(([key, value]) => [key, nullable(value)]))
107
+
108
+ export const WorkcardSchema = cast<Workcard>({
109
+ type: 'object',
110
+ properties: workcardProperties,
111
+ required: workcardRequired,
112
+ additionalProperties: false,
113
+ })
114
+
115
+ export const SpecificationSchema = cast<Specification>({
116
+ type: 'object',
117
+ properties: { ...workcardProperties, ...specificationProperties },
118
+ required: [...workcardRequired, 'category', 'format'],
119
+ additionalProperties: false,
120
+ })
121
+
122
+ /** Any kind of workcard — what a receipt, a commit or a view carries. */
123
+ export const AnyWorkcardSchema = cast<Workcard>({
124
+ type: 'object',
125
+ properties: { ...workcardProperties, ...nullableOf(specificationProperties) },
126
+ required: workcardRequired,
127
+ additionalProperties: false,
128
+ })
129
+
130
+ export const RelationshipDraftSchema = cast<RelationshipDraft>({
131
+ type: 'object',
132
+ properties: {
133
+ type: Label,
134
+ from: OptionalId,
135
+ to: Id,
136
+ fields: OptionalOpenObject,
137
+ },
138
+ required: ['type', 'to'],
139
+ additionalProperties: false,
140
+ })
141
+
142
+ export const RelationshipSchema = cast<Relationship>({
143
+ type: 'object',
144
+ properties: {
145
+ id: OptionalId,
146
+ entityId: Id,
147
+ type: Label,
148
+ from: Id,
149
+ to: Id,
150
+ project: OptionalId,
151
+ fields: OptionalOpenObject,
152
+ createdAt: IsoDate,
153
+ transition: OptionalId,
154
+ },
155
+ required: ['entityId', 'type', 'from', 'to', 'createdAt'],
156
+ additionalProperties: false,
157
+ })
158
+
159
+ /** Every changeable field, each nullable: `null` in an update asks for the field to be cleared. */
160
+ export const WorkcardChangesSchema = cast<WorkcardChanges>({
161
+ type: 'object',
162
+ properties: nullableOf({
163
+ code: workcardProperties.code,
164
+ title: workcardProperties.title,
165
+ description: workcardProperties.description,
166
+ parent: workcardProperties.parent,
167
+ parents: workcardProperties.parents,
168
+ status: workcardProperties.status,
169
+ intrinsic: workcardProperties.intrinsic,
170
+ flows: workcardProperties.flows,
171
+ labels: workcardProperties.labels,
172
+ order: workcardProperties.order,
173
+ fields: workcardProperties.fields,
174
+ createdBy: workcardProperties.createdBy,
175
+ updatedAt: workcardProperties.updatedAt,
176
+ closedAt: workcardProperties.closedAt,
177
+ ...specificationProperties,
178
+ }),
179
+ required: [],
180
+ additionalProperties: false,
181
+ })
182
+
183
+ export const TransitionCommitSchema = cast<TransitionCommit>({
184
+ type: 'object',
185
+ properties: {
186
+ state: CommitStateSchema,
187
+ at: OptionalIsoDate,
188
+ error: { type: 'string', maxLength: DESCRIPTION_MAX, nullable: true },
189
+ },
190
+ required: ['state'],
191
+ additionalProperties: false,
192
+ })
193
+
194
+ const UnsetSchema = { type: 'array', maxItems: 64, items: { type: 'string', minLength: 1, maxLength: TYPE_MAX } }
195
+
196
+ export const TransitionSchema = cast<Transition>({
197
+ type: 'object',
198
+ properties: {
199
+ id: OptionalId,
200
+ entityId: Id,
201
+ card: Id,
202
+ kind: WorkcardKindSchema,
203
+ type: TypeKey,
204
+ project: OptionalId,
205
+ seq: { type: 'number', minimum: 1 },
206
+ action: TransitionActionSchema,
207
+ flow: { ...Label, nullable: true },
208
+ transition: { ...Label, nullable: true },
209
+ from: { ...Label, nullable: true },
210
+ to: { ...Label, nullable: true },
211
+ changes: WorkcardChangesSchema,
212
+ unset: { ...UnsetSchema, nullable: true },
213
+ link: { ...RelationshipDraftSchema, nullable: true },
214
+ links: { type: 'array', maxItems: MAX_PARENTS, items: RelationshipDraftSchema, nullable: true },
215
+ actor: TransitionActorSchema,
216
+ cause: { type: 'string', maxLength: CAUSE_MAX, nullable: true },
217
+ key: { type: 'string', minLength: 1, maxLength: KEY_MAX, nullable: true },
218
+ at: IsoDate,
219
+ commit: TransitionCommitSchema,
220
+ },
221
+ required: ['entityId', 'card', 'kind', 'type', 'seq', 'action', 'changes', 'actor', 'at', 'commit'],
222
+ additionalProperties: false,
223
+ })
224
+
225
+ // ─── Type and flow schemas ───────────────────────────────────────────────────────────────────────
226
+
227
+ export const StatusDefinitionSchema = cast<StatusDefinition>({
228
+ type: 'object',
229
+ properties: {
230
+ key: Label,
231
+ intrinsic: IntrinsicStatusSchema,
232
+ initial: { type: 'boolean', nullable: true },
233
+ terminal: { type: 'boolean', nullable: true },
234
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
235
+ tone: { type: 'string', maxLength: LABEL_MAX, nullable: true },
236
+ },
237
+ required: ['key', 'intrinsic'],
238
+ additionalProperties: false,
239
+ })
240
+
241
+ export const StatusTransitionRuleSchema = cast<StatusTransitionRule>({
242
+ type: 'object',
243
+ properties: {
244
+ name: Label,
245
+ /** An array of status keys, or `'*'`. */
246
+ from: { type: ['array', 'string'], maxLength: 1, items: Label },
247
+ to: Label,
248
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
249
+ explicit: { type: 'boolean', nullable: true },
250
+ },
251
+ required: ['name', 'from', 'to'],
252
+ additionalProperties: false,
253
+ })
254
+
255
+ export const StatusFlowSchemaSchema = cast<StatusFlowSchema>({
256
+ type: 'object',
257
+ properties: {
258
+ id: TypeKey,
259
+ version: { type: 'number', minimum: 1 },
260
+ statuses: { type: 'array', minItems: 1, items: StatusDefinitionSchema },
261
+ transitions: { type: 'array', items: StatusTransitionRuleSchema },
262
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
263
+ },
264
+ required: ['id', 'version', 'statuses', 'transitions'],
265
+ additionalProperties: false,
266
+ })
267
+
268
+ export const SpecificationSlotSchema = cast<SpecificationSlot>({
269
+ type: 'object',
270
+ properties: {
271
+ category: Label,
272
+ format: SpecificationFormatSchema,
273
+ required: { type: 'boolean', nullable: true },
274
+ multiple: { type: 'boolean', nullable: true },
275
+ revisioned: { type: 'boolean', nullable: true },
276
+ keepRevisions: { type: 'number', minimum: 0, nullable: true },
277
+ schema: OptionalOpenObject,
278
+ version: { type: 'number', nullable: true },
279
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
280
+ type: { ...TypeKey, nullable: true },
281
+ },
282
+ required: ['category', 'format'],
283
+ additionalProperties: false,
284
+ })
285
+
286
+ export const CodePolicySchema = cast<CodePolicy>({
287
+ type: 'object',
288
+ properties: {
289
+ prefix: { type: 'string', maxLength: 16, nullable: true },
290
+ style: { type: 'string', enum: Object.values(CodeStyle) },
291
+ length: { type: 'number', minimum: 1, maximum: CODE_MAX, nullable: true },
292
+ uppercase: { type: 'boolean', nullable: true },
293
+ uniqueWithin: { type: 'string', enum: Object.values(CodeScope) },
294
+ mutable: { type: 'boolean', nullable: true },
295
+ },
296
+ required: ['style', 'uniqueWithin'],
297
+ additionalProperties: false,
298
+ })
299
+
300
+ export const RelationshipTypeSchema = cast<RelationshipType>({
301
+ type: 'object',
302
+ properties: {
303
+ name: Label,
304
+ from: { type: 'array', items: TypeKey, nullable: true },
305
+ to: { type: 'array', items: TypeKey, nullable: true },
306
+ inverse: { ...Label, nullable: true },
307
+ single: { type: 'boolean', nullable: true },
308
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
309
+ },
310
+ required: ['name'],
311
+ additionalProperties: false,
312
+ })
313
+
314
+ const typeProperties = {
315
+ type: TypeKey,
316
+ version: { type: 'number', minimum: 1 },
317
+ fields: OpenObject,
318
+ flows: { type: 'array', minItems: 1, items: TypeKey },
319
+ intrinsic: nullable(IntrinsicPolicySchema),
320
+ specifications: { type: 'array', items: SpecificationSlotSchema },
321
+ relationships: { type: 'array', items: RelationshipTypeSchema, nullable: true },
322
+ labels: { type: 'array', items: Label, nullable: true },
323
+ code: { ...CodePolicySchema, nullable: true },
324
+ label: { type: 'string', maxLength: TITLE_MAX, nullable: true },
325
+ }
326
+
327
+ const typeRequired = ['type', 'kind', 'version', 'fields', 'flows', 'specifications']
328
+
329
+ export const WorkcardTypeSchemaSchema = cast<WorkcardTypeSchema>({
330
+ type: 'object',
331
+ properties: {
332
+ ...typeProperties,
333
+ kind: { type: 'string', enum: [WorkcardKind.Card, WorkcardKind.Specification] },
334
+ },
335
+ required: typeRequired,
336
+ additionalProperties: false,
337
+ })
338
+
339
+ export const ProjectTypeSchemaSchema = cast<ProjectTypeSchema>({
340
+ type: 'object',
341
+ properties: {
342
+ ...typeProperties,
343
+ kind: { type: 'string', enum: [WorkcardKind.Project] },
344
+ cardTypes: { type: 'array', items: TypeKey },
345
+ projectTypes: { type: 'array', items: TypeKey, nullable: true },
346
+ },
347
+ required: [...typeRequired, 'cardTypes'],
348
+ additionalProperties: false,
349
+ })
350
+
351
+ /** Either type schema — `cardTypes` is required by the project kind only. */
352
+ export const AnyTypeSchemaSchema = cast<AnyTypeSchema>({
353
+ type: 'object',
354
+ properties: {
355
+ ...typeProperties,
356
+ kind: WorkcardKindSchema,
357
+ cardTypes: { type: 'array', items: TypeKey, nullable: true },
358
+ projectTypes: { type: 'array', items: TypeKey, nullable: true },
359
+ },
360
+ required: typeRequired,
361
+ additionalProperties: false,
362
+ })
363
+
364
+ export const PlanningSchemaBundleSchema = cast<PlanningSchemaBundle>({
365
+ type: 'object',
366
+ properties: {
367
+ version: { type: 'number', minimum: 1 },
368
+ types: { type: 'array', items: AnyTypeSchemaSchema },
369
+ flows: { type: 'array', items: StatusFlowSchemaSchema },
370
+ },
371
+ required: ['version', 'types', 'flows'],
372
+ additionalProperties: false,
373
+ })
374
+
375
+ // ─── Execution ───────────────────────────────────────────────────────────────────────────────────
376
+
377
+ export const WorkcardDraftSchema = cast<WorkcardDraft>({
378
+ type: 'object',
379
+ properties: {
380
+ kind: WorkcardKindSchema,
381
+ type: TypeKey,
382
+ parent: OptionalId,
383
+ parents: { ...workcardProperties.parents, nullable: true },
384
+ title: workcardProperties.title,
385
+ description: workcardProperties.description,
386
+ code: workcardProperties.code,
387
+ labels: { ...workcardProperties.labels, nullable: true },
388
+ order: workcardProperties.order,
389
+ fields: OptionalOpenObject,
390
+ status: { ...Label, nullable: true },
391
+ createdBy: OptionalId,
392
+ ...nullableOf(specificationProperties),
393
+ },
394
+ required: ['kind', 'type', 'title'],
395
+ additionalProperties: false,
396
+ })
397
+
398
+ const executionProperties = {
399
+ /** A card id, or the draft of a create. */
400
+ card: { anyOf: [Id, WorkcardDraftSchema] },
401
+ action: TransitionActionSchema,
402
+ transition: { ...Label, nullable: true },
403
+ flow: { ...TypeKey, nullable: true },
404
+ changes: { ...WorkcardChangesSchema, nullable: true },
405
+ unset: { ...UnsetSchema, nullable: true },
406
+ link: { ...RelationshipDraftSchema, nullable: true },
407
+ links: { type: 'array', maxItems: MAX_PARENTS, items: RelationshipDraftSchema, nullable: true },
408
+ actor: { ...TransitionActorSchema, nullable: true },
409
+ cause: { type: 'string', maxLength: CAUSE_MAX, nullable: true },
410
+ key: { type: 'string', minLength: 1, maxLength: KEY_MAX, nullable: true },
411
+ expectSeq: { type: 'number', minimum: 0, nullable: true },
412
+ }
413
+
414
+ export const TransitionExecutionSchema = cast<TransitionExecution>({
415
+ type: 'object',
416
+ properties: executionProperties,
417
+ required: ['card', 'action'],
418
+ additionalProperties: false,
419
+ })
420
+
421
+ export const ExecuteRequestSchema = cast<ExecuteRequest>({
422
+ type: 'object',
423
+ properties: {
424
+ ...executionProperties,
425
+ wait: { type: 'boolean', nullable: true },
426
+ timeout: { type: 'number', minimum: 0, nullable: true },
427
+ },
428
+ required: ['card', 'action'],
429
+ additionalProperties: false,
430
+ })
431
+
432
+ export const TransitionReceiptViewSchema = cast<TransitionReceiptView>({
433
+ type: 'object',
434
+ properties: {
435
+ transition: TransitionSchema,
436
+ card: { ...AnyWorkcardSchema, nullable: true },
437
+ },
438
+ required: ['transition'],
439
+ additionalProperties: false,
440
+ })
441
+
442
+ // ─── Commits ─────────────────────────────────────────────────────────────────────────────────────
443
+
444
+ export const CommitStatusSchema = cast<CommitStatus>({
445
+ type: 'object',
446
+ properties: {
447
+ transition: Id,
448
+ state: CommitStateSchema,
449
+ at: OptionalIsoDate,
450
+ error: { type: 'string', maxLength: DESCRIPTION_MAX, nullable: true },
451
+ card: { ...AnyWorkcardSchema, nullable: true },
452
+ },
453
+ required: ['transition', 'state'],
454
+ additionalProperties: false,
455
+ })
456
+
457
+ export const CommitEventSchema = cast<CommitEvent>({
458
+ type: 'object',
459
+ properties: {
460
+ transition: Id,
461
+ card: Id,
462
+ entityId: Id,
463
+ project: OptionalId,
464
+ kind: WorkcardKindSchema,
465
+ type: TypeKey,
466
+ seq: { type: 'number', minimum: 1 },
467
+ action: TransitionActionSchema,
468
+ state: CommitStateSchema,
469
+ at: IsoDate,
470
+ error: { type: 'string', maxLength: DESCRIPTION_MAX, nullable: true },
471
+ record: { ...AnyWorkcardSchema, nullable: true },
472
+ },
473
+ required: ['transition', 'card', 'entityId', 'kind', 'type', 'seq', 'action', 'state', 'at'],
474
+ additionalProperties: false,
475
+ })
476
+
477
+ // ─── Views ───────────────────────────────────────────────────────────────────────────────────────
478
+
479
+ const Count = { type: 'number', minimum: 0 } as const
480
+
481
+ export const SummaryViewSchema = cast<SummaryView>({
482
+ type: 'object',
483
+ additionalProperties: {
484
+ type: 'object',
485
+ properties: {
486
+ total: Count,
487
+ [IntrinsicStatus.Planned]: Count,
488
+ [IntrinsicStatus.InProgress]: Count,
489
+ [IntrinsicStatus.Closed]: Count,
490
+ },
491
+ required: ['total', IntrinsicStatus.Planned, IntrinsicStatus.InProgress, IntrinsicStatus.Closed],
492
+ additionalProperties: false,
493
+ },
494
+ required: [],
495
+ })
496
+
497
+ export const SpecificationRevisionSchema = cast<SpecificationRevision>({
498
+ type: 'object',
499
+ properties: {
500
+ revision: { type: 'number', minimum: 1 },
501
+ body: specificationProperties.body,
502
+ ref: specificationProperties.ref,
503
+ bodyChars: specificationProperties.bodyChars,
504
+ version: specificationProperties.version,
505
+ at: IsoDate,
506
+ by: { ...TransitionActorSchema, nullable: true },
507
+ transition: Id,
508
+ },
509
+ required: ['revision', 'at', 'transition'],
510
+ additionalProperties: false,
511
+ })
512
+
513
+ export const SpecificationRevisionListSchema = cast<SpecificationRevisionList>({
514
+ type: 'object',
515
+ properties: { items: { type: 'array', items: SpecificationRevisionSchema } },
516
+ required: ['items'],
517
+ additionalProperties: false,
518
+ })
519
+
520
+ export const WorkcardListSchema: JSONSchemaType<ListResult<Workcard>> = createListSchema(AnyWorkcardSchema)
521
+ export const SpecificationListSchema: JSONSchemaType<ListResult<Specification>> = createListSchema(SpecificationSchema)
522
+ export const RelationshipListSchema: JSONSchemaType<ListResult<Relationship>> = createListSchema(RelationshipSchema)
523
+ export const TransitionListSchema: JSONSchemaType<ListResult<Transition>> = createListSchema(TransitionSchema)
524
+
525
+ // ─── Wire queries and params ─────────────────────────────────────────────────────────────────────
526
+
527
+ /**
528
+ * Query-string schemas accept both what `encode*Query` writes (scalars) and what a tolerant
529
+ * transport may already have split (arrays), and coerce numbers and booleans from strings.
530
+ */
531
+ // Factories, not shared constants: ajv may extend a `type` array in place when it compiles a
532
+ // nullable schema, and a shared array would leak that into every other schema spreading it.
533
+ const listValue = () => ({ type: ['string', 'array'], maxLength: 4096, maxItems: MAX_QUERY_LIST, items: { type: 'string', maxLength: 256 }, nullable: true })
534
+ const jsonValue = () => ({ type: ['string', 'object'], maxLength: 4096, nullable: true })
535
+ const TextValue = { type: 'string', maxLength: QUERY_TEXT_MAX, nullable: true }
536
+
537
+ const listWireProperties = () => ({
538
+ page: { type: 'number', minimum: 0, nullable: true },
539
+ size: { type: 'number', minimum: 0, nullable: true },
540
+ sort: { type: ['string', 'array'], maxLength: 512, nullable: true },
541
+ })
542
+
543
+ export const WorkcardQuerySchema = cast<WorkcardQueryWire>({
544
+ type: 'object',
545
+ properties: {
546
+ ...listWireProperties(),
547
+ kind: listValue(),
548
+ type: listValue(),
549
+ parent: TextValue,
550
+ within: TextValue,
551
+ status: listValue(),
552
+ intrinsic: listValue(),
553
+ flow: jsonValue(),
554
+ labels: listValue(),
555
+ code: listValue(),
556
+ ids: listValue(),
557
+ fields: jsonValue(),
558
+ q: TextValue,
559
+ category: listValue(),
560
+ updatedSince: TextValue,
561
+ },
562
+ required: [],
563
+ additionalProperties: false,
564
+ })
565
+
566
+ export const SpecificationQuerySchema = cast<SpecificationQueryWire>({
567
+ type: 'object',
568
+ properties: {
569
+ ...listWireProperties(),
570
+ category: listValue(),
571
+ all: { type: 'boolean', nullable: true },
572
+ },
573
+ required: [],
574
+ additionalProperties: false,
575
+ })
576
+
577
+ export const RelationshipQuerySchema = cast<RelationshipQueryWire>({
578
+ type: 'object',
579
+ properties: {
580
+ ...listWireProperties(),
581
+ from: listValue(),
582
+ to: listValue(),
583
+ type: listValue(),
584
+ },
585
+ required: [],
586
+ additionalProperties: false,
587
+ })
588
+
589
+ export const TransitionQuerySchema = cast<TransitionQueryWire>({
590
+ type: 'object',
591
+ properties: {
592
+ ...listWireProperties(),
593
+ card: TextValue,
594
+ project: TextValue,
595
+ sinceSeq: { type: 'number', minimum: 0, nullable: true },
596
+ action: listValue(),
597
+ state: TextValue,
598
+ },
599
+ required: [],
600
+ additionalProperties: false,
601
+ })
602
+
603
+ export const SummaryQuerySchema = cast<SummaryQueryWire>({
604
+ type: 'object',
605
+ properties: {
606
+ parents: { type: ['string', 'array'], minLength: 1, maxLength: 4096, minItems: 1, maxItems: MAX_QUERY_LIST, items: { type: 'string', maxLength: 256 } },
607
+ kind: TextValue,
608
+ type: listValue(),
609
+ },
610
+ required: ['parents'],
611
+ additionalProperties: false,
612
+ })
613
+
614
+ export const WorkcardParamsSchema = cast<WorkcardParams>({
615
+ type: 'object',
616
+ properties: { id: Id },
617
+ required: ['id'],
618
+ additionalProperties: false,
619
+ })
620
+
621
+ export const TransitionParamsSchema = cast<TransitionParams>({
622
+ type: 'object',
623
+ properties: { transition: Id },
624
+ required: ['transition'],
625
+ additionalProperties: false,
626
+ })
627
+
628
+ export const CommitQuerySchema = cast<CommitQuery>({
629
+ type: 'object',
630
+ properties: {
631
+ wait: { type: 'number', minimum: 0, maximum: MAX_COMMIT_POLL, nullable: true },
632
+ },
633
+ required: [],
634
+ additionalProperties: false,
635
+ })
636
+
637
+ export const CommitFeedQuerySchema = cast<CommitFeedQuery>({
638
+ type: 'object',
639
+ properties: {
640
+ project: OptionalId,
641
+ card: OptionalId,
642
+ },
643
+ required: [],
644
+ additionalProperties: false,
645
+ })
646
+
647
+ export const RevisionsQuerySchema = cast<RevisionsQuery>({
648
+ type: 'object',
649
+ properties: {
650
+ limit: { type: 'number', minimum: 1, maximum: 1000, nullable: true },
651
+ },
652
+ required: [],
653
+ additionalProperties: false,
654
+ })