@kernhq/module-tracker 0.1.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 (158) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +265 -0
  3. package/dist/contract/events.d.ts +140 -0
  4. package/dist/contract/events.d.ts.map +1 -0
  5. package/dist/contract/events.js +51 -0
  6. package/dist/contract/events.js.map +1 -0
  7. package/dist/contract/index.d.ts +6 -0
  8. package/dist/contract/index.d.ts.map +1 -0
  9. package/dist/contract/index.js +6 -0
  10. package/dist/contract/index.js.map +1 -0
  11. package/dist/contract/models.d.ts +2547 -0
  12. package/dist/contract/models.d.ts.map +1 -0
  13. package/dist/contract/models.js +1214 -0
  14. package/dist/contract/models.js.map +1 -0
  15. package/dist/contract/notifications.d.ts +4 -0
  16. package/dist/contract/notifications.d.ts.map +1 -0
  17. package/dist/contract/notifications.js +46 -0
  18. package/dist/contract/notifications.js.map +1 -0
  19. package/dist/contract/permissions.d.ts +161 -0
  20. package/dist/contract/permissions.d.ts.map +1 -0
  21. package/dist/contract/permissions.js +192 -0
  22. package/dist/contract/permissions.js.map +1 -0
  23. package/dist/contract/router.d.ts +9659 -0
  24. package/dist/contract/router.d.ts.map +1 -0
  25. package/dist/contract/router.js +732 -0
  26. package/dist/contract/router.js.map +1 -0
  27. package/dist/kql/ast.d.ts +90 -0
  28. package/dist/kql/ast.d.ts.map +1 -0
  29. package/dist/kql/ast.js +80 -0
  30. package/dist/kql/ast.js.map +1 -0
  31. package/dist/kql/dates.d.ts +16 -0
  32. package/dist/kql/dates.d.ts.map +1 -0
  33. package/dist/kql/dates.js +54 -0
  34. package/dist/kql/dates.js.map +1 -0
  35. package/dist/kql/fields.d.ts +61 -0
  36. package/dist/kql/fields.d.ts.map +1 -0
  37. package/dist/kql/fields.js +106 -0
  38. package/dist/kql/fields.js.map +1 -0
  39. package/dist/kql/index.d.ts +16 -0
  40. package/dist/kql/index.d.ts.map +1 -0
  41. package/dist/kql/index.js +16 -0
  42. package/dist/kql/index.js.map +1 -0
  43. package/dist/kql/lexer.d.ts +28 -0
  44. package/dist/kql/lexer.d.ts.map +1 -0
  45. package/dist/kql/lexer.js +127 -0
  46. package/dist/kql/lexer.js.map +1 -0
  47. package/dist/kql/parser.d.ts +14 -0
  48. package/dist/kql/parser.d.ts.map +1 -0
  49. package/dist/kql/parser.js +290 -0
  50. package/dist/kql/parser.js.map +1 -0
  51. package/dist/kql/suggest.d.ts +8 -0
  52. package/dist/kql/suggest.d.ts.map +1 -0
  53. package/dist/kql/suggest.js +96 -0
  54. package/dist/kql/suggest.js.map +1 -0
  55. package/dist/kql/validate.d.ts +10 -0
  56. package/dist/kql/validate.d.ts.map +1 -0
  57. package/dist/kql/validate.js +84 -0
  58. package/dist/kql/validate.js.map +1 -0
  59. package/dist/server/index.d.ts +17 -0
  60. package/dist/server/index.d.ts.map +1 -0
  61. package/dist/server/index.js +583 -0
  62. package/dist/server/index.js.map +1 -0
  63. package/dist/server/kql/compile.d.ts +49 -0
  64. package/dist/server/kql/compile.d.ts.map +1 -0
  65. package/dist/server/kql/compile.js +372 -0
  66. package/dist/server/kql/compile.js.map +1 -0
  67. package/dist/server/rank.d.ts +23 -0
  68. package/dist/server/rank.d.ts.map +1 -0
  69. package/dist/server/rank.js +66 -0
  70. package/dist/server/rank.js.map +1 -0
  71. package/dist/server/rich.d.ts +30 -0
  72. package/dist/server/rich.d.ts.map +1 -0
  73. package/dist/server/rich.js +97 -0
  74. package/dist/server/rich.js.map +1 -0
  75. package/dist/server/router.d.ts +10589 -0
  76. package/dist/server/router.d.ts.map +1 -0
  77. package/dist/server/router.js +431 -0
  78. package/dist/server/router.js.map +1 -0
  79. package/dist/server/schema.d.ts +6622 -0
  80. package/dist/server/schema.d.ts.map +1 -0
  81. package/dist/server/schema.js +581 -0
  82. package/dist/server/schema.js.map +1 -0
  83. package/dist/server/services/access.d.ts +53 -0
  84. package/dist/server/services/access.d.ts.map +1 -0
  85. package/dist/server/services/access.js +146 -0
  86. package/dist/server/services/access.js.map +1 -0
  87. package/dist/server/services/comments.d.ts +36 -0
  88. package/dist/server/services/comments.d.ts.map +1 -0
  89. package/dist/server/services/comments.js +231 -0
  90. package/dist/server/services/comments.js.map +1 -0
  91. package/dist/server/services/config.d.ts +130 -0
  92. package/dist/server/services/config.d.ts.map +1 -0
  93. package/dist/server/services/config.js +797 -0
  94. package/dist/server/services/config.js.map +1 -0
  95. package/dist/server/services/db.d.ts +93 -0
  96. package/dist/server/services/db.d.ts.map +1 -0
  97. package/dist/server/services/db.js +519 -0
  98. package/dist/server/services/db.js.map +1 -0
  99. package/dist/server/services/imports.d.ts +37 -0
  100. package/dist/server/services/imports.d.ts.map +1 -0
  101. package/dist/server/services/imports.js +324 -0
  102. package/dist/server/services/imports.js.map +1 -0
  103. package/dist/server/services/index.d.ts +36 -0
  104. package/dist/server/services/index.d.ts.map +1 -0
  105. package/dist/server/services/index.js +57 -0
  106. package/dist/server/services/index.js.map +1 -0
  107. package/dist/server/services/intake.d.ts +47 -0
  108. package/dist/server/services/intake.d.ts.map +1 -0
  109. package/dist/server/services/intake.js +268 -0
  110. package/dist/server/services/intake.js.map +1 -0
  111. package/dist/server/services/issues.d.ts +763 -0
  112. package/dist/server/services/issues.d.ts.map +1 -0
  113. package/dist/server/services/issues.js +1381 -0
  114. package/dist/server/services/issues.js.map +1 -0
  115. package/dist/server/services/notify.d.ts +51 -0
  116. package/dist/server/services/notify.d.ts.map +1 -0
  117. package/dist/server/services/notify.js +90 -0
  118. package/dist/server/services/notify.js.map +1 -0
  119. package/dist/server/services/planning.d.ts +62 -0
  120. package/dist/server/services/planning.d.ts.map +1 -0
  121. package/dist/server/services/planning.js +614 -0
  122. package/dist/server/services/planning.js.map +1 -0
  123. package/dist/server/services/projects.d.ts +43 -0
  124. package/dist/server/services/projects.d.ts.map +1 -0
  125. package/dist/server/services/projects.js +375 -0
  126. package/dist/server/services/projects.js.map +1 -0
  127. package/dist/server/services/query.d.ts +24 -0
  128. package/dist/server/services/query.d.ts.map +1 -0
  129. package/dist/server/services/query.js +312 -0
  130. package/dist/server/services/query.js.map +1 -0
  131. package/dist/server/services/reports.d.ts +30 -0
  132. package/dist/server/services/reports.d.ts.map +1 -0
  133. package/dist/server/services/reports.js +295 -0
  134. package/dist/server/services/reports.js.map +1 -0
  135. package/dist/server/services/time.d.ts +53 -0
  136. package/dist/server/services/time.d.ts.map +1 -0
  137. package/dist/server/services/time.js +192 -0
  138. package/dist/server/services/time.js.map +1 -0
  139. package/dist/server/services/transitions.d.ts +61 -0
  140. package/dist/server/services/transitions.d.ts.map +1 -0
  141. package/dist/server/services/transitions.js +493 -0
  142. package/dist/server/services/transitions.js.map +1 -0
  143. package/dist/server/services/views.d.ts +33 -0
  144. package/dist/server/services/views.d.ts.map +1 -0
  145. package/dist/server/services/views.js +195 -0
  146. package/dist/server/services/views.js.map +1 -0
  147. package/migrations/0000_init.sql +529 -0
  148. package/migrations/0001_rls.sql +259 -0
  149. package/migrations/meta/0000_snapshot.json +4140 -0
  150. package/migrations/meta/_journal.json +20 -0
  151. package/package.json +82 -0
  152. package/src/client/api.ts +15 -0
  153. package/src/client/format.ts +164 -0
  154. package/src/client/group.ts +150 -0
  155. package/src/client/index.ts +60 -0
  156. package/src/client/kql.ts +492 -0
  157. package/src/client/rank.ts +87 -0
  158. package/src/client/types.ts +57 -0
@@ -0,0 +1,1214 @@
1
+ import { Color, Id, Timestamp, UserId, WorkspaceId } from '@kernhq/contracts';
2
+ import { ApprovalState, StatusCategory, WorkflowDefinition } from '@kernhq/workflow';
3
+ import { z } from 'zod';
4
+ export const MODULE_ID = 'tracker';
5
+ // =====================================================================================
6
+ // shared scalars
7
+ // =====================================================================================
8
+ /** Project key: 2–10 uppercase letters/digits, starts with a letter (`KRN`, `OPS2`). */
9
+ export const ProjectKey = z
10
+ .string()
11
+ .trim()
12
+ .toUpperCase()
13
+ .regex(/^[A-Z][A-Z0-9]{1,9}$/, '2–10 uppercase letters/digits, starting with a letter');
14
+ /** Issue key `KRN-123`. */
15
+ export const IssueKey = z.string().regex(/^[A-Z][A-Z0-9]{1,9}-\d+$/, 'issue key like KRN-123');
16
+ /** Machine key for types/fields/labels: lowercase snake, starts with a letter. */
17
+ export const MachineKey = z
18
+ .string()
19
+ .trim()
20
+ .min(1)
21
+ .max(48)
22
+ .regex(/^[a-z][a-z0-9_]*$/, 'lowercase letters, digits and underscores');
23
+ /** Tiptap / ProseMirror JSON document. */
24
+ export const RichDoc = z
25
+ .object({ type: z.literal('doc'), content: z.array(z.record(z.string(), z.unknown())).optional() })
26
+ .catchall(z.unknown());
27
+ /** `YYYY-MM-DD` calendar date (no time zone). */
28
+ export const DateOnly = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD');
29
+ export const Priority = z.enum(['none', 'low', 'medium', 'high', 'urgent']);
30
+ export const PRIORITY_ORDER = { none: 0, low: 1, medium: 2, high: 3, urgent: 4 };
31
+ export const EstimateUnit = z.enum(['points', 'hours', 'none']);
32
+ export { ApprovalState, StatusCategory, WorkflowDefinition };
33
+ const ok = z.object({ ok: z.literal(true) });
34
+ export const Ok = ok;
35
+ // =====================================================================================
36
+ // projects
37
+ // =====================================================================================
38
+ export const ProjectVisibility = z.enum(['workspace', 'private']);
39
+ export const CycleSettings = z.object({
40
+ enabled: z.boolean().default(false),
41
+ lengthWeeks: z.number().int().min(1).max(8).default(2),
42
+ /** days between cycles */
43
+ cooldownDays: z.number().int().min(0).max(14).default(0),
44
+ /** move unfinished issues to the next cycle when one completes */
45
+ autoRoll: z.boolean().default(true),
46
+ /** 0 = Sunday … 6 = Saturday */
47
+ startDay: z.number().int().min(0).max(6).default(1),
48
+ /** auto-create the next upcoming cycle so there is always one planned */
49
+ autoCreateUpcoming: z.number().int().min(0).max(6).default(1),
50
+ });
51
+ export const ProjectSettings = z.object({
52
+ estimation: EstimateUnit.default('points'),
53
+ /** allowed point values for the estimate picker (points only) */
54
+ estimateScale: z.array(z.number()).default([0, 1, 2, 3, 5, 8, 13, 21]),
55
+ cycles: CycleSettings.default({
56
+ enabled: false,
57
+ lengthWeeks: 2,
58
+ cooldownDays: 0,
59
+ autoRoll: true,
60
+ startDay: 1,
61
+ autoCreateUpcoming: 1,
62
+ }),
63
+ triage: z.object({ enabled: z.boolean().default(false) }).default({ enabled: false }),
64
+ sla: z
65
+ .object({
66
+ enabled: z.boolean().default(false),
67
+ /** hours to first response / resolution per priority */
68
+ goals: z
69
+ .partialRecord(Priority, z.object({
70
+ firstResponseHours: z.number().positive().optional(),
71
+ resolveHours: z.number().positive().optional(),
72
+ }))
73
+ .default({}),
74
+ /** status categories during which the SLA clock is paused */
75
+ pauseInCategories: z.array(StatusCategory).default(['backlog']),
76
+ })
77
+ .default({ enabled: false, goals: {}, pauseInCategories: ['backlog'] }),
78
+ /** require a resolution when moving to a done-category status */
79
+ requireResolution: z.boolean().default(false),
80
+ /** per-issue chat channel created automatically on issue creation */
81
+ autoCreateIssueChannel: z.boolean().default(false),
82
+ /** timezone used for cycle boundaries / due dates */
83
+ timezone: z.string().default('UTC'),
84
+ });
85
+ export const Project = z.object({
86
+ id: Id,
87
+ workspaceId: WorkspaceId,
88
+ key: ProjectKey,
89
+ name: z.string().min(1).max(120),
90
+ description: z.string().max(4000).nullable(),
91
+ icon: z.string().max(64).nullable(),
92
+ color: Color.nullable(),
93
+ leadId: UserId.nullable(),
94
+ visibility: ProjectVisibility,
95
+ defaultAssignee: z.enum(['unassigned', 'lead']),
96
+ workflowSchemeId: Id.nullable(),
97
+ typeSchemeId: Id.nullable(),
98
+ fieldSchemeId: Id.nullable(),
99
+ settings: ProjectSettings,
100
+ /** public intake form token (null = intake disabled) */
101
+ intakeToken: z.string().nullable(),
102
+ /** last allocated issue number */
103
+ issueCounter: z.number().int().nonnegative(),
104
+ /** last allocated cycle number */
105
+ cycleCounter: z.number().int().nonnegative(),
106
+ memberCount: z.number().int().nonnegative(),
107
+ openIssueCount: z.number().int().nonnegative(),
108
+ archivedAt: Timestamp.nullable(),
109
+ createdBy: UserId.nullable(),
110
+ createdAt: Timestamp,
111
+ updatedAt: Timestamp,
112
+ });
113
+ export const CreateProject = z.object({
114
+ key: ProjectKey,
115
+ name: z.string().min(1).max(120),
116
+ description: z.string().max(4000).optional(),
117
+ icon: z.string().max(64).optional(),
118
+ color: Color.optional(),
119
+ leadId: UserId.optional(),
120
+ visibility: ProjectVisibility.default('workspace'),
121
+ defaultAssignee: z.enum(['unassigned', 'lead']).default('unassigned'),
122
+ /** seed types/workflow from a built-in template */
123
+ template: z.enum(['software', 'kanban', 'simple', 'blank']).default('software'),
124
+ /** or from a saved project template (overrides `template`) */
125
+ templateId: Id.optional(),
126
+ settings: ProjectSettings.partial().optional(),
127
+ memberIds: z.array(UserId).max(500).default([]),
128
+ });
129
+ export const UpdateProject = z.object({
130
+ name: z.string().min(1).max(120).optional(),
131
+ description: z.string().max(4000).nullable().optional(),
132
+ icon: z.string().max(64).nullable().optional(),
133
+ color: Color.nullable().optional(),
134
+ leadId: UserId.nullable().optional(),
135
+ visibility: ProjectVisibility.optional(),
136
+ defaultAssignee: z.enum(['unassigned', 'lead']).optional(),
137
+ workflowSchemeId: Id.nullable().optional(),
138
+ typeSchemeId: Id.nullable().optional(),
139
+ fieldSchemeId: Id.nullable().optional(),
140
+ settings: ProjectSettings.partial().optional(),
141
+ });
142
+ /** Project member: a user with a project role (mirrored as kernel authz binding at scope `project`). */
143
+ export const ProjectRole = z.enum(['admin', 'member', 'viewer']);
144
+ export const ProjectMember = z.object({
145
+ projectId: Id,
146
+ userId: UserId,
147
+ role: ProjectRole,
148
+ addedBy: UserId.nullable(),
149
+ addedAt: Timestamp,
150
+ });
151
+ /** Reusable project blueprint (types, workflow, fields, labels, sample views). */
152
+ export const ProjectTemplate = z.object({
153
+ id: Id,
154
+ workspaceId: WorkspaceId.nullable(),
155
+ key: MachineKey,
156
+ name: z.string().min(1).max(120),
157
+ description: z.string().max(1000).nullable(),
158
+ icon: z.string().max(64).nullable(),
159
+ /** a full ProjectTemplateBody JSON */
160
+ body: z.record(z.string(), z.unknown()),
161
+ builtin: z.boolean(),
162
+ createdAt: Timestamp,
163
+ });
164
+ // =====================================================================================
165
+ // work item types & hierarchy
166
+ // =====================================================================================
167
+ /** -1 sub-item · 0 standard (task/bug/story) · 1 epic · 2 initiative. */
168
+ export const HierarchyLevel = z.number().int().min(-1).max(2);
169
+ export const FieldLayoutItem = z.object({
170
+ /** system field name (`priority`, `dueDate`…) or custom field id */
171
+ fieldId: z.string().min(1),
172
+ section: z.enum(['main', 'sidebar', 'hidden']).default('sidebar'),
173
+ required: z.boolean().default(false),
174
+ hidden: z.boolean().default(false),
175
+ order: z.number().int().default(0),
176
+ });
177
+ export const WorkItemType = z.object({
178
+ id: Id,
179
+ workspaceId: WorkspaceId,
180
+ /** null = workspace-level type shared by all projects */
181
+ projectId: Id.nullable(),
182
+ key: MachineKey,
183
+ name: z.string().min(1).max(60),
184
+ description: z.string().max(500).nullable(),
185
+ icon: z.string().max(64).nullable(),
186
+ color: Color.nullable(),
187
+ level: HierarchyLevel,
188
+ isDefault: z.boolean(),
189
+ /** explicit workflow; null → project's workflow scheme / default workflow */
190
+ workflowId: Id.nullable(),
191
+ fieldLayout: z.array(FieldLayoutItem),
192
+ /** default description for new items of this type */
193
+ templateBody: RichDoc.nullable(),
194
+ order: z.number().int(),
195
+ archivedAt: Timestamp.nullable(),
196
+ createdAt: Timestamp,
197
+ updatedAt: Timestamp,
198
+ });
199
+ export const UpsertWorkItemType = WorkItemType.pick({
200
+ key: true,
201
+ name: true,
202
+ level: true,
203
+ }).extend({
204
+ projectId: Id.nullable().optional(),
205
+ description: z.string().max(500).nullable().optional(),
206
+ icon: z.string().max(64).nullable().optional(),
207
+ color: Color.nullable().optional(),
208
+ isDefault: z.boolean().optional(),
209
+ workflowId: Id.nullable().optional(),
210
+ fieldLayout: z.array(FieldLayoutItem).optional(),
211
+ templateBody: RichDoc.nullable().optional(),
212
+ order: z.number().int().optional(),
213
+ });
214
+ /** Which levels may parent which: stored per workspace; default: 2→1→0→-1 (strict) with `allowSkip`. */
215
+ export const HierarchyRules = z.object({
216
+ /** allow e.g. an initiative (2) to directly parent a task (0) */
217
+ allowSkipLevels: z.boolean().default(true),
218
+ /** allow same-level parenting (task under task) */
219
+ allowSameLevel: z.boolean().default(false),
220
+ /** max depth of nested sub-items */
221
+ maxSubItemDepth: z.number().int().min(1).max(5).default(1),
222
+ });
223
+ /** Type scheme: which types a project uses (+ default) – null projectTypeIds = all workspace types. */
224
+ export const TypeScheme = z.object({
225
+ id: Id,
226
+ workspaceId: WorkspaceId,
227
+ name: z.string().min(1).max(120),
228
+ typeIds: z.array(Id),
229
+ defaultTypeId: Id.nullable(),
230
+ createdAt: Timestamp,
231
+ });
232
+ // =====================================================================================
233
+ // custom fields
234
+ // =====================================================================================
235
+ export const FieldType = z.enum([
236
+ 'text',
237
+ 'textarea',
238
+ 'number',
239
+ 'date',
240
+ 'datetime',
241
+ 'select',
242
+ 'multiselect',
243
+ 'user',
244
+ 'multiuser',
245
+ 'label',
246
+ 'url',
247
+ 'checkbox',
248
+ 'relation',
249
+ 'formula',
250
+ ]);
251
+ export const FieldOption = z.object({
252
+ id: z.string().min(1).max(64),
253
+ label: z.string().min(1).max(120),
254
+ color: Color.nullable().default(null),
255
+ order: z.number().int().default(0),
256
+ archived: z.boolean().default(false),
257
+ });
258
+ /** Type-specific configuration. */
259
+ export const FieldConfig = z
260
+ .object({
261
+ /** number */
262
+ min: z.number().optional(),
263
+ max: z.number().optional(),
264
+ precision: z.number().int().min(0).max(6).optional(),
265
+ unit: z.string().max(16).optional(),
266
+ /** text */
267
+ maxLength: z.number().int().positive().optional(),
268
+ pattern: z.string().max(200).optional(),
269
+ /** relation: restrict to item types/projects */
270
+ relationTypeIds: z.array(Id).optional(),
271
+ relationProjectIds: z.array(Id).optional(),
272
+ relationMultiple: z.boolean().optional(),
273
+ /** formula: expression over other fields, e.g. `{estimate} * 2` or `daysBetween({startDate},{dueDate})` */
274
+ formula: z.string().max(500).optional(),
275
+ formulaResult: z.enum(['number', 'text', 'date', 'boolean']).optional(),
276
+ /** datetime/date: include time-of-day */
277
+ includeTime: z.boolean().optional(),
278
+ })
279
+ .catchall(z.unknown());
280
+ export const FieldDef = z.object({
281
+ id: Id,
282
+ workspaceId: WorkspaceId,
283
+ /** null = workspace-level (available to all projects) */
284
+ projectId: Id.nullable(),
285
+ /** machine key, referenced in KQL as `cf.<key>` and in `issue.custom[key]` */
286
+ key: MachineKey,
287
+ name: z.string().min(1).max(80),
288
+ description: z.string().max(500).nullable(),
289
+ type: FieldType,
290
+ options: z.array(FieldOption),
291
+ defaultValue: z.unknown().nullable(),
292
+ config: FieldConfig,
293
+ searchable: z.boolean(),
294
+ required: z.boolean(),
295
+ /** show in list/board cards by default */
296
+ showInCards: z.boolean(),
297
+ order: z.number().int(),
298
+ archivedAt: Timestamp.nullable(),
299
+ createdAt: Timestamp,
300
+ updatedAt: Timestamp,
301
+ });
302
+ export const UpsertFieldDef = z.object({
303
+ projectId: Id.nullable().optional(),
304
+ key: MachineKey,
305
+ name: z.string().min(1).max(80),
306
+ description: z.string().max(500).nullable().optional(),
307
+ type: FieldType,
308
+ options: z.array(FieldOption).optional(),
309
+ defaultValue: z.unknown().nullable().optional(),
310
+ config: FieldConfig.optional(),
311
+ searchable: z.boolean().optional(),
312
+ required: z.boolean().optional(),
313
+ showInCards: z.boolean().optional(),
314
+ order: z.number().int().optional(),
315
+ });
316
+ /** Field scheme: which custom fields a project exposes (null = all applicable). */
317
+ export const FieldScheme = z.object({
318
+ id: Id,
319
+ workspaceId: WorkspaceId,
320
+ name: z.string().min(1).max(120),
321
+ fieldIds: z.array(Id),
322
+ createdAt: Timestamp,
323
+ });
324
+ // =====================================================================================
325
+ // workflows
326
+ // =====================================================================================
327
+ export const Workflow = z.object({
328
+ id: Id,
329
+ workspaceId: WorkspaceId,
330
+ /** null = workspace-level workflow available to all projects */
331
+ projectId: Id.nullable(),
332
+ name: z.string().min(1).max(120),
333
+ description: z.string().max(1000).nullable(),
334
+ definition: WorkflowDefinition,
335
+ isDefault: z.boolean(),
336
+ /** number of issues currently using it (computed) */
337
+ usageCount: z.number().int().nonnegative().optional(),
338
+ archivedAt: Timestamp.nullable(),
339
+ createdAt: Timestamp,
340
+ updatedAt: Timestamp,
341
+ });
342
+ export const UpsertWorkflow = z.object({
343
+ projectId: Id.nullable().optional(),
344
+ name: z.string().min(1).max(120),
345
+ description: z.string().max(1000).nullable().optional(),
346
+ definition: WorkflowDefinition,
347
+ isDefault: z.boolean().optional(),
348
+ });
349
+ /** Maps item types to workflows for a project (typeId → workflowId; `defaultWorkflowId` otherwise). */
350
+ export const WorkflowScheme = z.object({
351
+ id: Id,
352
+ workspaceId: WorkspaceId,
353
+ name: z.string().min(1).max(120),
354
+ defaultWorkflowId: Id,
355
+ mappings: z.array(z.object({ typeId: Id, workflowId: Id })),
356
+ createdAt: Timestamp,
357
+ updatedAt: Timestamp,
358
+ });
359
+ export const UpsertWorkflowScheme = WorkflowScheme.pick({
360
+ name: true,
361
+ defaultWorkflowId: true,
362
+ mappings: true,
363
+ });
364
+ /** One status as exposed to the UI together with the workflow it belongs to. */
365
+ export const StatusInfo = z.object({
366
+ id: z.string(),
367
+ name: z.string(),
368
+ category: StatusCategory,
369
+ color: z.string().nullable(),
370
+ order: z.number().int(),
371
+ workflowId: Id,
372
+ });
373
+ /** Available transition for an issue, as seen by the UI. */
374
+ export const AvailableTransition = z.object({
375
+ id: z.string(),
376
+ name: z.string(),
377
+ toStatusId: z.string(),
378
+ toStatus: StatusInfo,
379
+ allowed: z.boolean(),
380
+ reasons: z.array(z.object({ kind: z.string(), message: z.string(), field: z.string().optional() })),
381
+ requiresApproval: z.boolean(),
382
+ screen: z.object({ fields: z.array(z.string()), comment: z.boolean() }).nullable(),
383
+ hidden: z.boolean(),
384
+ });
385
+ // =====================================================================================
386
+ // issues
387
+ // =====================================================================================
388
+ export const RelationType = z.enum([
389
+ 'blocks',
390
+ 'blocked_by',
391
+ 'relates',
392
+ 'duplicates',
393
+ 'duplicated_by',
394
+ 'clones',
395
+ 'cloned_by',
396
+ ]);
397
+ /** inverse relation for the other side */
398
+ export const RELATION_INVERSE = {
399
+ blocks: 'blocked_by',
400
+ blocked_by: 'blocks',
401
+ relates: 'relates',
402
+ duplicates: 'duplicated_by',
403
+ duplicated_by: 'duplicates',
404
+ clones: 'cloned_by',
405
+ cloned_by: 'clones',
406
+ };
407
+ export const RelationSummary = z.object({
408
+ blocks: z.number().int().nonnegative(),
409
+ blockedBy: z.number().int().nonnegative(),
410
+ /** blockers that are still open */
411
+ openBlockers: z.number().int().nonnegative(),
412
+ relates: z.number().int().nonnegative(),
413
+ duplicates: z.number().int().nonnegative(),
414
+ subItems: z.number().int().nonnegative(),
415
+ subItemsDone: z.number().int().nonnegative(),
416
+ });
417
+ export const SlaState = z.object({
418
+ firstResponseDueAt: Timestamp.nullable(),
419
+ firstRespondedAt: Timestamp.nullable(),
420
+ resolveDueAt: Timestamp.nullable(),
421
+ pausedAt: Timestamp.nullable(),
422
+ /** total paused seconds */
423
+ pausedSec: z.number().int().nonnegative(),
424
+ breached: z.boolean(),
425
+ });
426
+ export const Issue = z.object({
427
+ id: Id,
428
+ workspaceId: WorkspaceId,
429
+ projectId: Id,
430
+ key: IssueKey,
431
+ number: z.number().int().positive(),
432
+ typeId: Id,
433
+ title: z.string().min(1).max(500),
434
+ description: RichDoc.nullable(),
435
+ /** plain-text rendering of description (search, previews) */
436
+ descriptionText: z.string(),
437
+ statusId: z.string(),
438
+ statusCategory: StatusCategory,
439
+ priority: Priority,
440
+ assigneeIds: z.array(UserId),
441
+ reporterId: UserId.nullable(),
442
+ creatorId: UserId.nullable(),
443
+ /** label ids */
444
+ labelIds: z.array(Id),
445
+ componentIds: z.array(Id),
446
+ /** fix versions */
447
+ versionIds: z.array(Id),
448
+ affectsVersionIds: z.array(Id),
449
+ cycleId: Id.nullable(),
450
+ milestoneId: Id.nullable(),
451
+ parentId: Id.nullable(),
452
+ /** fractional index for manual ordering (backlog / board) */
453
+ rank: z.string(),
454
+ estimate: z.number().nullable(),
455
+ estimateUnit: EstimateUnit,
456
+ startDate: DateOnly.nullable(),
457
+ dueDate: DateOnly.nullable(),
458
+ completedAt: Timestamp.nullable(),
459
+ cancelledAt: Timestamp.nullable(),
460
+ resolution: z.string().max(64).nullable(),
461
+ /** custom field values keyed by field key */
462
+ custom: z.record(z.string(), z.unknown()),
463
+ watcherIds: z.array(UserId),
464
+ subscriberCount: z.number().int().nonnegative(),
465
+ commentCount: z.number().int().nonnegative(),
466
+ attachmentCount: z.number().int().nonnegative(),
467
+ relationSummary: RelationSummary,
468
+ timeSpentSec: z.number().int().nonnegative(),
469
+ remainingSec: z.number().int().nonnegative().nullable(),
470
+ originalEstimateSec: z.number().int().nonnegative().nullable(),
471
+ sla: SlaState.nullable(),
472
+ triage: z.boolean(),
473
+ /** hidden from the triage queue until this time */
474
+ snoozedUntil: Timestamp.nullable(),
475
+ /** external origin: email/intake/import */
476
+ source: z.enum(['app', 'email', 'intake', 'import', 'api', 'automation', 'recurring']),
477
+ /** external reference (email message id, jira key…) */
478
+ externalRef: z.string().nullable(),
479
+ /** per-issue chat channel id once opened */
480
+ chatChannelId: Id.nullable(),
481
+ archivedAt: Timestamp.nullable(),
482
+ createdAt: Timestamp,
483
+ updatedAt: Timestamp,
484
+ lastActivityAt: Timestamp,
485
+ });
486
+ /** Lightweight issue for lists/boards/pickers. */
487
+ export const IssueSummary = Issue.pick({
488
+ id: true,
489
+ workspaceId: true,
490
+ projectId: true,
491
+ key: true,
492
+ number: true,
493
+ typeId: true,
494
+ title: true,
495
+ statusId: true,
496
+ statusCategory: true,
497
+ priority: true,
498
+ assigneeIds: true,
499
+ labelIds: true,
500
+ cycleId: true,
501
+ parentId: true,
502
+ rank: true,
503
+ estimate: true,
504
+ dueDate: true,
505
+ startDate: true,
506
+ triage: true,
507
+ archivedAt: true,
508
+ updatedAt: true,
509
+ });
510
+ /** Fields an issue may be created with. */
511
+ export const CreateIssue = z.object({
512
+ projectId: Id,
513
+ typeId: Id.optional(),
514
+ title: z.string().min(1).max(500),
515
+ description: RichDoc.nullable().optional(),
516
+ statusId: z.string().optional(),
517
+ priority: Priority.optional(),
518
+ assigneeIds: z.array(UserId).max(20).optional(),
519
+ reporterId: UserId.optional(),
520
+ labelIds: z.array(Id).max(50).optional(),
521
+ componentIds: z.array(Id).max(20).optional(),
522
+ versionIds: z.array(Id).max(20).optional(),
523
+ affectsVersionIds: z.array(Id).max(20).optional(),
524
+ cycleId: Id.nullable().optional(),
525
+ milestoneId: Id.nullable().optional(),
526
+ parentId: Id.nullable().optional(),
527
+ estimate: z.number().nullable().optional(),
528
+ startDate: DateOnly.nullable().optional(),
529
+ dueDate: DateOnly.nullable().optional(),
530
+ originalEstimateSec: z.number().int().nonnegative().nullable().optional(),
531
+ custom: z.record(z.string(), z.unknown()).optional(),
532
+ watcherIds: z.array(UserId).optional(),
533
+ triage: z.boolean().optional(),
534
+ /** issue template to apply first */
535
+ templateId: Id.optional(),
536
+ /** place relative to another issue in rank order */
537
+ rankAfterId: Id.optional(),
538
+ rankBeforeId: Id.optional(),
539
+ /** core file ids to attach */
540
+ attachmentIds: z.array(Id).max(50).optional(),
541
+ });
542
+ /** Patch for update/bulk-update. `null` clears nullable fields. Arrays replace unless `*Add/*Remove` used. */
543
+ export const UpdateIssue = z.object({
544
+ typeId: Id.optional(),
545
+ title: z.string().min(1).max(500).optional(),
546
+ description: RichDoc.nullable().optional(),
547
+ priority: Priority.optional(),
548
+ assigneeIds: z.array(UserId).max(20).optional(),
549
+ assigneeAdd: z.array(UserId).optional(),
550
+ assigneeRemove: z.array(UserId).optional(),
551
+ reporterId: UserId.nullable().optional(),
552
+ labelIds: z.array(Id).max(50).optional(),
553
+ labelAdd: z.array(Id).optional(),
554
+ labelRemove: z.array(Id).optional(),
555
+ componentIds: z.array(Id).max(20).optional(),
556
+ versionIds: z.array(Id).max(20).optional(),
557
+ affectsVersionIds: z.array(Id).max(20).optional(),
558
+ cycleId: Id.nullable().optional(),
559
+ milestoneId: Id.nullable().optional(),
560
+ parentId: Id.nullable().optional(),
561
+ estimate: z.number().nullable().optional(),
562
+ startDate: DateOnly.nullable().optional(),
563
+ dueDate: DateOnly.nullable().optional(),
564
+ resolution: z.string().max(64).nullable().optional(),
565
+ originalEstimateSec: z.number().int().nonnegative().nullable().optional(),
566
+ remainingSec: z.number().int().nonnegative().nullable().optional(),
567
+ /** merged into existing custom values; `null` value removes a key */
568
+ custom: z.record(z.string(), z.unknown()).optional(),
569
+ triage: z.boolean().optional(),
570
+ });
571
+ export const BulkResult = z.object({
572
+ results: z.array(z.object({
573
+ id: Id,
574
+ ok: z.boolean(),
575
+ error: z.object({ code: z.string(), message: z.string() }).optional(),
576
+ })),
577
+ succeeded: z.number().int().nonnegative(),
578
+ failed: z.number().int().nonnegative(),
579
+ });
580
+ /** Activity/history entry for an issue (projection of core activity + status history). */
581
+ export const IssueHistoryEntry = z.object({
582
+ id: Id,
583
+ issueId: Id,
584
+ actorId: UserId.nullable(),
585
+ action: z.string(),
586
+ changes: z.array(z.object({ field: z.string(), from: z.unknown(), to: z.unknown() })),
587
+ data: z.record(z.string(), z.unknown()),
588
+ occurredAt: Timestamp,
589
+ });
590
+ export const StatusHistoryEntry = z.object({
591
+ id: Id,
592
+ issueId: Id,
593
+ fromStatusId: z.string().nullable(),
594
+ toStatusId: z.string(),
595
+ fromCategory: StatusCategory.nullable(),
596
+ toCategory: StatusCategory,
597
+ actorId: UserId.nullable(),
598
+ transitionId: z.string().nullable(),
599
+ /** seconds spent in the previous status */
600
+ durationSec: z.number().int().nonnegative().nullable(),
601
+ occurredAt: Timestamp,
602
+ });
603
+ // ---------- comments ----------
604
+ export const ReactionSummary = z.object({
605
+ emoji: z.string(),
606
+ count: z.number().int(),
607
+ userIds: z.array(UserId),
608
+ });
609
+ export const Comment = z.object({
610
+ id: Id,
611
+ workspaceId: WorkspaceId,
612
+ issueId: Id,
613
+ /** threaded reply */
614
+ parentId: Id.nullable(),
615
+ authorId: UserId.nullable(),
616
+ body: RichDoc,
617
+ bodyText: z.string(),
618
+ mentionIds: z.array(UserId),
619
+ reactions: z.array(ReactionSummary),
620
+ /** internal comments are hidden from portal/email requesters */
621
+ internal: z.boolean(),
622
+ /** comment created from an inbound email */
623
+ source: z.enum(['app', 'email', 'automation', 'system']),
624
+ replyCount: z.number().int().nonnegative(),
625
+ editedAt: Timestamp.nullable(),
626
+ deletedAt: Timestamp.nullable(),
627
+ createdAt: Timestamp,
628
+ updatedAt: Timestamp,
629
+ });
630
+ // ---------- relations / attachments / links ----------
631
+ export const Relation = z.object({
632
+ id: Id,
633
+ workspaceId: WorkspaceId,
634
+ type: RelationType,
635
+ fromIssueId: Id,
636
+ toIssueId: Id,
637
+ createdBy: UserId.nullable(),
638
+ createdAt: Timestamp,
639
+ });
640
+ /** Relation as seen from one issue's perspective. */
641
+ export const RelationView = z.object({
642
+ id: Id,
643
+ type: RelationType,
644
+ issue: IssueSummary,
645
+ createdAt: Timestamp,
646
+ });
647
+ export const Attachment = z.object({
648
+ id: Id,
649
+ workspaceId: WorkspaceId,
650
+ issueId: Id,
651
+ /** core file id */
652
+ fileId: Id,
653
+ name: z.string(),
654
+ mimeType: z.string(),
655
+ size: z.number().int().nonnegative(),
656
+ uploadedBy: UserId.nullable(),
657
+ createdAt: Timestamp,
658
+ });
659
+ export const Link = z.object({
660
+ id: Id,
661
+ workspaceId: WorkspaceId,
662
+ issueId: Id,
663
+ url: z.string().url(),
664
+ title: z.string().max(300).nullable(),
665
+ /** e.g. `github_pr`, `doc`, `generic` */
666
+ kind: z.string().max(32),
667
+ createdBy: UserId.nullable(),
668
+ createdAt: Timestamp,
669
+ });
670
+ // ---------- time tracking ----------
671
+ export const Worklog = z.object({
672
+ id: Id,
673
+ workspaceId: WorkspaceId,
674
+ projectId: Id,
675
+ issueId: Id,
676
+ userId: UserId,
677
+ startedAt: Timestamp,
678
+ durationSec: z.number().int().positive(),
679
+ note: z.string().max(2000).nullable(),
680
+ billable: z.boolean(),
681
+ createdAt: Timestamp,
682
+ updatedAt: Timestamp,
683
+ });
684
+ export const UpsertWorklog = z.object({
685
+ startedAt: Timestamp.optional(),
686
+ durationSec: z.number().int().positive(),
687
+ note: z.string().max(2000).nullable().optional(),
688
+ billable: z.boolean().optional(),
689
+ /** reduce remaining estimate by: auto (duration) | leave | set */
690
+ adjustRemaining: z.enum(['auto', 'leave', 'set']).default('auto'),
691
+ remainingSec: z.number().int().nonnegative().optional(),
692
+ });
693
+ export const Timer = z.object({
694
+ id: Id,
695
+ workspaceId: WorkspaceId,
696
+ issueId: Id,
697
+ userId: UserId,
698
+ startedAt: Timestamp,
699
+ note: z.string().max(2000).nullable(),
700
+ });
701
+ /** Timesheet cell: seconds per (user, issue/project, day). */
702
+ export const TimesheetRow = z.object({
703
+ userId: UserId,
704
+ projectId: Id,
705
+ issueId: Id.nullable(),
706
+ issueKey: z.string().nullable(),
707
+ date: DateOnly,
708
+ durationSec: z.number().int().nonnegative(),
709
+ billableSec: z.number().int().nonnegative(),
710
+ });
711
+ // ---------- templates / recurring ----------
712
+ export const IssueTemplate = z.object({
713
+ id: Id,
714
+ workspaceId: WorkspaceId,
715
+ projectId: Id.nullable(),
716
+ name: z.string().min(1).max(120),
717
+ description: z.string().max(500).nullable(),
718
+ typeId: Id.nullable(),
719
+ /** CreateIssue-shaped defaults (without projectId) */
720
+ defaults: CreateIssue.omit({ projectId: true, templateId: true }).partial(),
721
+ /** sub-items to create with the issue */
722
+ subItems: z.array(z.object({ title: z.string().min(1).max(500), typeId: Id.optional() })),
723
+ createdBy: UserId.nullable(),
724
+ createdAt: Timestamp,
725
+ updatedAt: Timestamp,
726
+ });
727
+ export const UpsertIssueTemplate = IssueTemplate.pick({ name: true }).extend({
728
+ projectId: Id.nullable().optional(),
729
+ description: z.string().max(500).nullable().optional(),
730
+ typeId: Id.nullable().optional(),
731
+ defaults: IssueTemplate.shape.defaults.optional(),
732
+ subItems: IssueTemplate.shape.subItems.optional(),
733
+ });
734
+ /** Recurrence rule (simplified RRULE). */
735
+ export const RecurrenceRule = z.object({
736
+ freq: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
737
+ interval: z.number().int().min(1).max(365).default(1),
738
+ /** weekly: 0–6; monthly: day of month 1–31 */
739
+ byWeekday: z.array(z.number().int().min(0).max(6)).optional(),
740
+ byMonthDay: z.number().int().min(1).max(31).optional(),
741
+ /** HH:mm in project timezone */
742
+ at: z
743
+ .string()
744
+ .regex(/^\d{2}:\d{2}$/)
745
+ .default('09:00'),
746
+ until: Timestamp.nullable().optional(),
747
+ count: z.number().int().positive().nullable().optional(),
748
+ });
749
+ export const RecurringIssue = z.object({
750
+ id: Id,
751
+ workspaceId: WorkspaceId,
752
+ projectId: Id,
753
+ name: z.string().min(1).max(120),
754
+ rule: RecurrenceRule,
755
+ /** template to instantiate (CreateIssue defaults) */
756
+ defaults: IssueTemplate.shape.defaults,
757
+ enabled: z.boolean(),
758
+ nextRunAt: Timestamp.nullable(),
759
+ lastRunAt: Timestamp.nullable(),
760
+ lastIssueId: Id.nullable(),
761
+ runCount: z.number().int().nonnegative(),
762
+ createdBy: UserId.nullable(),
763
+ createdAt: Timestamp,
764
+ updatedAt: Timestamp,
765
+ });
766
+ export const UpsertRecurringIssue = RecurringIssue.pick({ name: true, rule: true, defaults: true }).extend({
767
+ enabled: z.boolean().optional(),
768
+ });
769
+ // =====================================================================================
770
+ // planning: cycles, milestones, versions, components, labels
771
+ // =====================================================================================
772
+ export const CycleStatus = z.enum(['upcoming', 'active', 'completed']);
773
+ export const Cycle = z.object({
774
+ id: Id,
775
+ workspaceId: WorkspaceId,
776
+ projectId: Id,
777
+ number: z.number().int().positive(),
778
+ name: z.string().min(1).max(120),
779
+ goal: z.string().max(2000).nullable(),
780
+ startAt: Timestamp,
781
+ endAt: Timestamp,
782
+ status: CycleStatus,
783
+ startedAt: Timestamp.nullable(),
784
+ completedAt: Timestamp.nullable(),
785
+ /** issues carried over from the previous cycle when this one started */
786
+ carryOverCount: z.number().int().nonnegative(),
787
+ /** snapshot of scope (estimate sum) at start / issue counts */
788
+ stats: z.object({
789
+ total: z.number().int().nonnegative(),
790
+ done: z.number().int().nonnegative(),
791
+ estimateTotal: z.number().nonnegative(),
792
+ estimateDone: z.number().nonnegative(),
793
+ }),
794
+ createdAt: Timestamp,
795
+ updatedAt: Timestamp,
796
+ });
797
+ export const UpsertCycle = z.object({
798
+ name: z.string().min(1).max(120).optional(),
799
+ goal: z.string().max(2000).nullable().optional(),
800
+ startAt: Timestamp,
801
+ endAt: Timestamp,
802
+ });
803
+ export const Milestone = z.object({
804
+ id: Id,
805
+ workspaceId: WorkspaceId,
806
+ projectId: Id,
807
+ name: z.string().min(1).max(120),
808
+ description: z.string().max(2000).nullable(),
809
+ targetDate: DateOnly.nullable(),
810
+ status: z.enum(['open', 'completed', 'cancelled']),
811
+ stats: z.object({ total: z.number().int().nonnegative(), done: z.number().int().nonnegative() }),
812
+ completedAt: Timestamp.nullable(),
813
+ createdAt: Timestamp,
814
+ updatedAt: Timestamp,
815
+ });
816
+ export const UpsertMilestone = z.object({
817
+ name: z.string().min(1).max(120),
818
+ description: z.string().max(2000).nullable().optional(),
819
+ targetDate: DateOnly.nullable().optional(),
820
+ status: z.enum(['open', 'completed', 'cancelled']).optional(),
821
+ });
822
+ export const VersionStatus = z.enum(['unreleased', 'released', 'archived']);
823
+ export const Version = z.object({
824
+ id: Id,
825
+ workspaceId: WorkspaceId,
826
+ projectId: Id,
827
+ name: z.string().min(1).max(120),
828
+ description: z.string().max(2000).nullable(),
829
+ status: VersionStatus,
830
+ startDate: DateOnly.nullable(),
831
+ releaseDate: DateOnly.nullable(),
832
+ releasedAt: Timestamp.nullable(),
833
+ stats: z.object({ total: z.number().int().nonnegative(), done: z.number().int().nonnegative() }),
834
+ order: z.number().int(),
835
+ createdAt: Timestamp,
836
+ updatedAt: Timestamp,
837
+ });
838
+ export const UpsertVersion = z.object({
839
+ name: z.string().min(1).max(120),
840
+ description: z.string().max(2000).nullable().optional(),
841
+ startDate: DateOnly.nullable().optional(),
842
+ releaseDate: DateOnly.nullable().optional(),
843
+ order: z.number().int().optional(),
844
+ });
845
+ export const Component = z.object({
846
+ id: Id,
847
+ workspaceId: WorkspaceId,
848
+ projectId: Id,
849
+ name: z.string().min(1).max(120),
850
+ description: z.string().max(2000).nullable(),
851
+ leadId: UserId.nullable(),
852
+ /** issues with this component default-assign to: none | lead | project default */
853
+ defaultAssignee: z.enum(['none', 'lead', 'project']),
854
+ issueCount: z.number().int().nonnegative(),
855
+ createdAt: Timestamp,
856
+ updatedAt: Timestamp,
857
+ });
858
+ export const UpsertComponent = z.object({
859
+ name: z.string().min(1).max(120),
860
+ description: z.string().max(2000).nullable().optional(),
861
+ leadId: UserId.nullable().optional(),
862
+ defaultAssignee: z.enum(['none', 'lead', 'project']).optional(),
863
+ });
864
+ export const Label = z.object({
865
+ id: Id,
866
+ workspaceId: WorkspaceId,
867
+ /** null = workspace-level label */
868
+ projectId: Id.nullable(),
869
+ name: z.string().min(1).max(60),
870
+ color: Color.nullable(),
871
+ description: z.string().max(300).nullable(),
872
+ /** label groups (Linear-style); labels in the same group are mutually exclusive */
873
+ groupName: z.string().max(60).nullable(),
874
+ issueCount: z.number().int().nonnegative(),
875
+ archivedAt: Timestamp.nullable(),
876
+ createdAt: Timestamp,
877
+ });
878
+ export const UpsertLabel = z.object({
879
+ projectId: Id.nullable().optional(),
880
+ name: z.string().min(1).max(60),
881
+ color: Color.nullable().optional(),
882
+ description: z.string().max(300).nullable().optional(),
883
+ groupName: z.string().max(60).nullable().optional(),
884
+ });
885
+ // =====================================================================================
886
+ // views & boards
887
+ // =====================================================================================
888
+ export const ViewLayout = z.enum(['list', 'board', 'calendar', 'timeline', 'spreadsheet']);
889
+ export const ViewVisibility = z.enum(['private', 'project', 'workspace']);
890
+ /** Group/sort keys accepted by views and `issues.query`. */
891
+ export const GroupBy = z.enum([
892
+ 'none',
893
+ 'status',
894
+ 'statusCategory',
895
+ 'assignee',
896
+ 'priority',
897
+ 'type',
898
+ 'label',
899
+ 'cycle',
900
+ 'milestone',
901
+ 'project',
902
+ 'parent',
903
+ 'component',
904
+ 'version',
905
+ 'dueDate',
906
+ 'createdAt',
907
+ ]);
908
+ export const OrderBy = z.object({
909
+ /** KQL field name (`priority`, `updated`, `rank`, `cf.severity`…) */
910
+ field: z.string().min(1),
911
+ dir: z.enum(['asc', 'desc']).default('asc'),
912
+ });
913
+ export const BoardColumn = z.object({
914
+ id: z.string().min(1),
915
+ name: z.string().min(1).max(80),
916
+ /** statuses shown in this column */
917
+ statusIds: z.array(z.string()),
918
+ wipLimit: z.number().int().positive().nullable().default(null),
919
+ collapsed: z.boolean().default(false),
920
+ });
921
+ export const ViewDisplay = z.object({
922
+ groupBy: GroupBy.default('none'),
923
+ subGroupBy: GroupBy.optional(),
924
+ orderBy: z.array(OrderBy).default([{ field: 'rank', dir: 'asc' }]),
925
+ /** visible columns (list/spreadsheet): system field names or `cf.<key>` */
926
+ columns: z.array(z.string()).default(['key', 'title', 'status', 'assignee', 'priority', 'updated']),
927
+ /** board: swimlane grouping */
928
+ swimlanes: GroupBy.optional(),
929
+ showSubItems: z.boolean().default(true),
930
+ showEmptyGroups: z.boolean().default(true),
931
+ showCompleted: z.enum(['all', 'none', '1d', '7d', '30d']).default('7d'),
932
+ /** board columns (null → one column per status of the project's workflows) */
933
+ boardColumns: z.array(BoardColumn).nullable().default(null),
934
+ wipLimits: z.record(z.string(), z.number().int().positive()).default({}),
935
+ /** calendar: which date field positions issues */
936
+ calendarField: z.enum(['dueDate', 'startDate', 'createdAt']).default('dueDate'),
937
+ /** timeline: show dependency arrows */
938
+ showDependencies: z.boolean().default(true),
939
+ density: z.enum(['compact', 'comfortable']).default('comfortable'),
940
+ /** card properties to display */
941
+ cardProps: z.array(z.string()).default(['key', 'priority', 'assignee', 'labels', 'estimate', 'dueDate']),
942
+ });
943
+ /** Visual filter builder state (kept verbatim for the UI; the server only uses `kql`). */
944
+ export const ViewFilters = z.record(z.string(), z.unknown());
945
+ export const View = z.object({
946
+ id: Id,
947
+ workspaceId: WorkspaceId,
948
+ /** null = cross-project (workspace) view */
949
+ projectId: Id.nullable(),
950
+ name: z.string().min(1).max(120),
951
+ description: z.string().max(500).nullable(),
952
+ icon: z.string().max(64).nullable(),
953
+ kql: z.string().max(4000),
954
+ layout: ViewLayout,
955
+ display: ViewDisplay,
956
+ filters: ViewFilters,
957
+ visibility: ViewVisibility,
958
+ ownerId: UserId.nullable(),
959
+ /** pinned to the sidebar for the caller */
960
+ pinned: z.boolean(),
961
+ /** built-in views (My issues, Triage, Backlog…) cannot be deleted */
962
+ builtin: z.boolean(),
963
+ order: z.number().int(),
964
+ createdAt: Timestamp,
965
+ updatedAt: Timestamp,
966
+ });
967
+ export const UpsertView = z.object({
968
+ projectId: Id.nullable().optional(),
969
+ name: z.string().min(1).max(120),
970
+ description: z.string().max(500).nullable().optional(),
971
+ icon: z.string().max(64).nullable().optional(),
972
+ kql: z.string().max(4000).default(''),
973
+ layout: ViewLayout.default('list'),
974
+ display: ViewDisplay.partial().optional(),
975
+ filters: ViewFilters.optional(),
976
+ visibility: ViewVisibility.default('private'),
977
+ order: z.number().int().optional(),
978
+ });
979
+ // =====================================================================================
980
+ // KQL
981
+ // =====================================================================================
982
+ export const KqlError = z.object({
983
+ message: z.string(),
984
+ start: z.number().int().nonnegative(),
985
+ end: z.number().int().nonnegative(),
986
+ });
987
+ export const KqlSuggestion = z.object({
988
+ kind: z.enum(['field', 'operator', 'value', 'keyword', 'function']),
989
+ label: z.string(),
990
+ insertText: z.string(),
991
+ detail: z.string().optional(),
992
+ });
993
+ export const KqlFieldInfo = z.object({
994
+ name: z.string(),
995
+ type: z.enum(['text', 'enum', 'user', 'number', 'date', 'datetime', 'boolean', 'ref', 'id', 'key']),
996
+ label: z.string(),
997
+ operators: z.array(z.string()),
998
+ /** enum/ref fields: known values for autocomplete */
999
+ values: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
1000
+ custom: z.boolean(),
1001
+ sortable: z.boolean(),
1002
+ });
1003
+ export const KqlParseResult = z.object({
1004
+ ok: z.boolean(),
1005
+ /** normalized AST (JSON) – see `@kernhq/module-tracker/kql` for the typed shape */
1006
+ ast: z.unknown().nullable(),
1007
+ errors: z.array(KqlError),
1008
+ /** normalised, pretty-printed query */
1009
+ normalized: z.string().nullable(),
1010
+ suggestions: z.array(KqlSuggestion),
1011
+ });
1012
+ export const IssueQueryInput = z.object({
1013
+ workspaceId: WorkspaceId,
1014
+ kql: z.string().max(4000).default(''),
1015
+ /** restrict to these projects (ACL is applied regardless) */
1016
+ projectIds: z.array(Id).optional(),
1017
+ orderBy: z.array(OrderBy).optional(),
1018
+ groupBy: GroupBy.optional(),
1019
+ cursor: z.string().optional(),
1020
+ limit: z.number().int().min(1).max(500).default(100),
1021
+ includeArchived: z.boolean().default(false),
1022
+ include: z
1023
+ .object({
1024
+ /** total count (extra query) */
1025
+ total: z.boolean().default(false),
1026
+ /** per-group counts when groupBy set */
1027
+ groupCounts: z.boolean().default(false),
1028
+ /** full Issue instead of IssueSummary */
1029
+ full: z.boolean().default(false),
1030
+ })
1031
+ .default({ total: false, groupCounts: false, full: false }),
1032
+ });
1033
+ export const IssueQueryResult = z.object({
1034
+ items: z.array(Issue),
1035
+ nextCursor: z.string().nullable(),
1036
+ total: z.number().int().nonnegative().optional(),
1037
+ groups: z
1038
+ .array(z.object({
1039
+ key: z.string().nullable(),
1040
+ count: z.number().int().nonnegative(),
1041
+ estimate: z.number().nullable(),
1042
+ }))
1043
+ .optional(),
1044
+ /** fields used in the query (for the UI to highlight) */
1045
+ fields: z.array(z.string()),
1046
+ });
1047
+ // =====================================================================================
1048
+ // intake / triage / email
1049
+ // =====================================================================================
1050
+ export const IntakeForm = z.object({
1051
+ projectId: Id,
1052
+ projectName: z.string(),
1053
+ token: z.string(),
1054
+ title: z.string(),
1055
+ description: z.string().nullable(),
1056
+ fields: z.array(z.object({
1057
+ key: z.string(),
1058
+ label: z.string(),
1059
+ type: z.enum(['text', 'textarea', 'email', 'select']),
1060
+ required: z.boolean(),
1061
+ options: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
1062
+ })),
1063
+ /** allow file uploads on the public form */
1064
+ allowAttachments: z.boolean(),
1065
+ });
1066
+ export const IntakeSubmission = z.object({
1067
+ token: z.string().min(8),
1068
+ title: z.string().min(1).max(500),
1069
+ description: z.string().max(20000).optional(),
1070
+ email: z.string().email().optional(),
1071
+ name: z.string().max(120).optional(),
1072
+ fields: z.record(z.string(), z.unknown()).optional(),
1073
+ /** honeypot – must be empty */
1074
+ website: z.string().max(0).optional(),
1075
+ });
1076
+ export const EmailAddress = z.object({ address: z.string(), name: z.string().nullable().optional() });
1077
+ /** Inbound email as handed over by the mail module. */
1078
+ export const InboundEmail = z.object({
1079
+ /** project intake token (from `intake+<token>@…`) – or projectId for trusted callers */
1080
+ projectToken: z.string().optional(),
1081
+ projectId: Id.optional(),
1082
+ workspaceId: WorkspaceId.optional(),
1083
+ messageId: z.string().min(1),
1084
+ inReplyTo: z.string().nullable().optional(),
1085
+ references: z.array(z.string()).default([]),
1086
+ from: EmailAddress,
1087
+ to: z.array(EmailAddress).default([]),
1088
+ subject: z.string().max(1000),
1089
+ text: z.string().max(200000).nullable().optional(),
1090
+ html: z.string().max(500000).nullable().optional(),
1091
+ receivedAt: Timestamp.optional(),
1092
+ /** core file ids already stored by the mail module */
1093
+ attachments: z
1094
+ .array(z.object({ fileId: Id, name: z.string(), mimeType: z.string(), size: z.number().int() }))
1095
+ .default([]),
1096
+ });
1097
+ export const EmailIngestResult = z.object({
1098
+ action: z.enum(['created', 'commented', 'ignored']),
1099
+ issueId: Id.nullable(),
1100
+ issueKey: z.string().nullable(),
1101
+ commentId: Id.nullable(),
1102
+ reason: z.string().optional(),
1103
+ });
1104
+ // =====================================================================================
1105
+ // reports
1106
+ // =====================================================================================
1107
+ export const BurndownPoint = z.object({
1108
+ date: DateOnly,
1109
+ remaining: z.number(),
1110
+ ideal: z.number(),
1111
+ completed: z.number(),
1112
+ scope: z.number(),
1113
+ /** scope added/removed that day */
1114
+ scopeChange: z.number(),
1115
+ });
1116
+ export const BurndownReport = z.object({
1117
+ cycle: Cycle,
1118
+ unit: EstimateUnit,
1119
+ points: z.array(BurndownPoint),
1120
+ });
1121
+ export const VelocityReport = z.object({
1122
+ unit: EstimateUnit,
1123
+ cycles: z.array(z.object({
1124
+ cycle: Cycle.pick({ id: true, number: true, name: true, startAt: true, endAt: true, status: true }),
1125
+ committed: z.number(),
1126
+ completed: z.number(),
1127
+ committedCount: z.number().int(),
1128
+ completedCount: z.number().int(),
1129
+ })),
1130
+ average: z.number(),
1131
+ });
1132
+ export const CfdReport = z.object({
1133
+ statuses: z.array(StatusInfo.pick({ id: true, name: true, category: true, color: true })),
1134
+ points: z.array(z.object({ date: DateOnly, counts: z.record(z.string(), z.number().int()) })),
1135
+ });
1136
+ export const CreatedVsResolvedReport = z.object({
1137
+ points: z.array(z.object({
1138
+ date: DateOnly,
1139
+ created: z.number().int(),
1140
+ resolved: z.number().int(),
1141
+ openTotal: z.number().int(),
1142
+ })),
1143
+ });
1144
+ export const TimeReport = z.object({
1145
+ from: DateOnly,
1146
+ to: DateOnly,
1147
+ totalSec: z.number().int().nonnegative(),
1148
+ billableSec: z.number().int().nonnegative(),
1149
+ rows: z.array(TimesheetRow),
1150
+ byUser: z.array(z.object({ userId: UserId, durationSec: z.number().int(), billableSec: z.number().int() })),
1151
+ byIssue: z.array(z.object({
1152
+ issueId: Id,
1153
+ issueKey: z.string(),
1154
+ title: z.string(),
1155
+ durationSec: z.number().int(),
1156
+ originalEstimateSec: z.number().int().nullable(),
1157
+ remainingSec: z.number().int().nullable(),
1158
+ })),
1159
+ });
1160
+ // =====================================================================================
1161
+ // imports
1162
+ // =====================================================================================
1163
+ export const ImportSource = z.enum(['jira', 'linear', 'csv']);
1164
+ export const CsvMapping = z.object({
1165
+ /** CSV column → issue field (system name or `cf.<key>`) */
1166
+ columns: z.record(z.string(), z.string()),
1167
+ /** parse options */
1168
+ delimiter: z.string().max(1).default(','),
1169
+ hasHeader: z.boolean().default(true),
1170
+ dateFormat: z.string().optional(),
1171
+ /** map status names → status ids (unmapped → initial status) */
1172
+ statusMap: z.record(z.string(), z.string()).default({}),
1173
+ /** map user emails/names → user ids */
1174
+ userMap: z.record(z.string(), UserId).default({}),
1175
+ /** map type names → type ids */
1176
+ typeMap: z.record(z.string(), Id).default({}),
1177
+ priorityMap: z.record(z.string(), Priority).default({}),
1178
+ createMissingLabels: z.boolean().default(true),
1179
+ });
1180
+ export const ImportJob = z.object({
1181
+ id: Id,
1182
+ workspaceId: WorkspaceId,
1183
+ projectId: Id,
1184
+ source: ImportSource,
1185
+ fileId: Id,
1186
+ mapping: z.record(z.string(), z.unknown()),
1187
+ status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']),
1188
+ progress: z.object({
1189
+ total: z.number().int().nonnegative(),
1190
+ processed: z.number().int().nonnegative(),
1191
+ created: z.number().int().nonnegative(),
1192
+ skipped: z.number().int().nonnegative(),
1193
+ failed: z.number().int().nonnegative(),
1194
+ }),
1195
+ errors: z.array(z.object({ row: z.number().int().nullable(), message: z.string() })),
1196
+ /** external id → issue key mapping (for relations / idempotency) */
1197
+ createdBy: UserId.nullable(),
1198
+ startedAt: Timestamp.nullable(),
1199
+ finishedAt: Timestamp.nullable(),
1200
+ createdAt: Timestamp,
1201
+ });
1202
+ // =====================================================================================
1203
+ // approvals (transition approvals persisted per issue)
1204
+ // =====================================================================================
1205
+ export const IssueApproval = z.object({
1206
+ id: Id,
1207
+ workspaceId: WorkspaceId,
1208
+ issueId: Id,
1209
+ transitionId: z.string(),
1210
+ state: ApprovalState,
1211
+ createdAt: Timestamp,
1212
+ updatedAt: Timestamp,
1213
+ });
1214
+ //# sourceMappingURL=models.js.map