@v587d/capital-generation 2.12.0 → 2.13.1

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.
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Strict storage and wire schemas for the automation domain. Every object is a
3
+ * `strictObject`: an unknown field is a write error, not something JSON will silently drop.
4
+ * Run/notification records are disposable; task records are authoritative.
5
+ */
6
+ import { z } from 'zod'
7
+ import {
8
+ ACTIVE_RUN_STATUSES,
9
+ MAX_DESCRIPTION_CHARS,
10
+ MAX_ERROR_MESSAGE_CHARS,
11
+ MAX_ERRORS_PER_RUN,
12
+ MAX_INSTRUCTION_CHARS,
13
+ MAX_MAX_STEPS,
14
+ MAX_MAX_TOOL_CALLS,
15
+ MAX_RUN_TIMEOUT_MINUTES,
16
+ MAX_TITLE_CHARS,
17
+ MIN_MAX_STEPS,
18
+ MIN_MAX_TOOL_CALLS,
19
+ MIN_RUN_TIMEOUT_MINUTES,
20
+ RUN_STATUSES,
21
+ } from './defaults.js'
22
+
23
+ const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
24
+
25
+ /** Canonical UTC ISO instant; `Date.parse` alone accepts offsets and seconds variants. */
26
+ export const isoInstant = z.string().refine((value) => ISO_UTC.test(value) && new Date(value).toISOString() === value, {
27
+ message: 'must be a canonical UTC ISO instant',
28
+ })
29
+
30
+ const TIME_OF_DAY = /^(?:[01]\d|2[0-3]):[0-5]\d$/
31
+
32
+ export function isValidTimeZone(value) {
33
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64) return false
34
+ try {
35
+ new Intl.DateTimeFormat('en-US', { timeZone: value }).format(0)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ export const timeZone = z.string().min(1).max(64).refine(isValidTimeZone, { message: 'must be a valid IANA time zone' })
43
+
44
+ /** Registered Workspace id; empty/absent means the profile's dedicated automation workspace. */
45
+ export const workspaceId = z.string().max(120)
46
+
47
+ export const TriggerSchema = z.strictObject({
48
+ kind: z.literal('schedule'),
49
+ cadence: z.enum(['daily', 'weekly', 'monthly']),
50
+ time: z.string().regex(TIME_OF_DAY, { message: 'must be HH:mm' }),
51
+ timezone: timeZone,
52
+ weekdays: z.array(z.number().int().min(1).max(7)).min(1).max(7).optional(),
53
+ dayOfMonth: z.number().int().min(1).max(28).optional(),
54
+ tradingDaysOnly: z.boolean(),
55
+ }).superRefine((trigger, ctx) => {
56
+ if (trigger.cadence === 'weekly') {
57
+ if (trigger.weekdays === undefined) ctx.addIssue({ code: 'custom', path: ['weekdays'], message: 'weekly requires weekdays' })
58
+ else if (new Set(trigger.weekdays).size !== trigger.weekdays.length) ctx.addIssue({ code: 'custom', path: ['weekdays'], message: 'weekdays must be unique' })
59
+ if (trigger.dayOfMonth !== undefined) ctx.addIssue({ code: 'custom', path: ['dayOfMonth'], message: 'weekly must not carry dayOfMonth' })
60
+ }
61
+ if (trigger.cadence === 'monthly') {
62
+ if (trigger.dayOfMonth === undefined) ctx.addIssue({ code: 'custom', path: ['dayOfMonth'], message: 'monthly requires dayOfMonth' })
63
+ if (trigger.weekdays !== undefined) ctx.addIssue({ code: 'custom', path: ['weekdays'], message: 'monthly must not carry weekdays' })
64
+ }
65
+ if (trigger.cadence === 'daily') {
66
+ if (trigger.weekdays !== undefined) ctx.addIssue({ code: 'custom', path: ['weekdays'], message: 'daily must not carry weekdays' })
67
+ if (trigger.dayOfMonth !== undefined) ctx.addIssue({ code: 'custom', path: ['dayOfMonth'], message: 'daily must not carry dayOfMonth' })
68
+ }
69
+ })
70
+
71
+ export const LimitsSchema = z.strictObject({
72
+ runTimeoutMinutes: z.number().int().min(MIN_RUN_TIMEOUT_MINUTES).max(MAX_RUN_TIMEOUT_MINUTES),
73
+ idleTimeoutMinutes: z.number().int().min(1).max(30),
74
+ maxSteps: z.number().int().min(MIN_MAX_STEPS).max(MAX_MAX_STEPS),
75
+ maxToolCalls: z.number().int().min(MIN_MAX_TOOL_CALLS).max(MAX_MAX_TOOL_CALLS),
76
+ })
77
+
78
+ export const NotificationsSchema = z.strictObject({
79
+ inApp: z.boolean(),
80
+ onFailure: z.boolean(),
81
+ onSuccess: z.boolean(),
82
+ })
83
+
84
+ export const ExecutionModeSchema = z.enum(['news', 'market', 'review', 'research'])
85
+
86
+ /** User-writable configuration. Server-owned fields are deliberately absent. */
87
+ export const TaskConfigSchema = z.strictObject({
88
+ category: z.enum(['news', 'market', 'review', 'research']),
89
+ title: z.string().min(1).max(MAX_TITLE_CHARS),
90
+ description: z.string().max(MAX_DESCRIPTION_CHARS),
91
+ trigger: TriggerSchema,
92
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS),
93
+ mode: ExecutionModeSchema,
94
+ cwdKind: z.literal('profile'),
95
+ workspaceId: workspaceId.optional(),
96
+ limits: LimitsSchema,
97
+ notifications: NotificationsSchema,
98
+ })
99
+
100
+ export const TaskOverridesSchema = z.strictObject({
101
+ category: z.enum(['news', 'market', 'review', 'research']).optional(),
102
+ title: z.string().min(1).max(MAX_TITLE_CHARS).optional(),
103
+ description: z.string().max(MAX_DESCRIPTION_CHARS).optional(),
104
+ trigger: TriggerSchema.optional(),
105
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS).optional(),
106
+ mode: ExecutionModeSchema.optional(),
107
+ workspaceId: workspaceId.optional(),
108
+ limits: LimitsSchema.optional(),
109
+ notifications: NotificationsSchema.optional(),
110
+ })
111
+
112
+ export const TaskStateSchema = z.enum(['dormant', 'active', 'paused'])
113
+
114
+ export const AutomationTaskSchema = z.strictObject({
115
+ id: z.string().min(1).max(80),
116
+ schemaVersion: z.literal(1),
117
+ definitionVersion: z.number().int().min(0),
118
+ origin: z.enum(['builtin', 'custom']),
119
+ sourceBuiltinId: z.string().min(1).max(80).optional(),
120
+ profileKey: z.string().min(1).max(80),
121
+ profileName: z.string().min(1).max(200),
122
+ state: TaskStateSchema,
123
+ category: z.enum(['news', 'market', 'review', 'research']),
124
+ title: z.string().min(1).max(MAX_TITLE_CHARS),
125
+ description: z.string().max(MAX_DESCRIPTION_CHARS),
126
+ trigger: TriggerSchema,
127
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS),
128
+ mode: ExecutionModeSchema,
129
+ cwdKind: z.literal('profile'),
130
+ workspaceId: workspaceId.optional(),
131
+ limits: LimitsSchema,
132
+ notifications: NotificationsSchema,
133
+ overrides: TaskOverridesSchema.optional(),
134
+ scheduleAnchorAt: isoInstant,
135
+ lastHandledOccurrenceKey: z.string().max(300).optional(),
136
+ lastScheduledFor: isoInstant.optional(),
137
+ createdAt: isoInstant,
138
+ updatedAt: isoInstant,
139
+ })
140
+
141
+ /** Durable builtin override row. Runtime fields (state, anchors) always sit on the record. */
142
+ export const TaskRecordSchema = z.strictObject({
143
+ schemaVersion: z.literal(1),
144
+ id: z.string().min(1).max(80),
145
+ origin: z.enum(['builtin', 'custom']),
146
+ sourceBuiltinId: z.string().min(1).max(80).optional(),
147
+ profileKey: z.string().min(1).max(80),
148
+ profileName: z.string().min(1).max(200),
149
+ state: TaskStateSchema,
150
+ definitionVersion: z.number().int().min(0),
151
+ custom: TaskConfigSchema.optional(),
152
+ overrides: TaskOverridesSchema.optional(),
153
+ scheduleAnchorAt: isoInstant,
154
+ lastHandledOccurrenceKey: z.string().max(300).optional(),
155
+ lastScheduledFor: isoInstant.optional(),
156
+ createdAt: isoInstant,
157
+ updatedAt: isoInstant,
158
+ })
159
+
160
+ export const RunErrorSchema = z.strictObject({
161
+ at: isoInstant,
162
+ phase: z.enum(['schedule', 'session', 'turn', 'tool', 'llm', 'timeout', 'storage']),
163
+ severity: z.enum(['error', 'warning']),
164
+ code: z.string().min(1).max(80),
165
+ message: z.string().min(1).max(MAX_ERROR_MESSAGE_CHARS),
166
+ tool: z.string().max(80).optional(),
167
+ callId: z.string().max(120).optional(),
168
+ })
169
+
170
+ /** Durable turn boundary: recovery must distinguish an interrupted turn from a finished one. */
171
+ export const TurnStateSchema = z.strictObject({
172
+ phase: z.enum(['running', 'ended']),
173
+ turn: z.number().int().min(0),
174
+ status: z.enum(RUN_STATUSES).optional(),
175
+ at: isoInstant,
176
+ }).superRefine((state, ctx) => {
177
+ if (state.phase === 'ended' && state.status === undefined) {
178
+ ctx.addIssue({ code: 'custom', message: 'ended turn state requires status' })
179
+ }
180
+ if (state.phase === 'running' && state.status !== undefined) {
181
+ ctx.addIssue({ code: 'custom', message: 'running turn state must not carry status' })
182
+ }
183
+ })
184
+
185
+ export const RunRecordSchema = z.strictObject({
186
+ schemaVersion: z.literal(1),
187
+ id: z.string().min(1).max(80),
188
+ profileKey: z.string().min(1).max(80),
189
+ taskId: z.string().min(1).max(80),
190
+ taskTitle: z.string().min(1).max(MAX_TITLE_CHARS),
191
+ trigger: z.enum(['scheduled', 'manual']),
192
+ occurrenceKey: z.string().min(1).max(300),
193
+ scheduleVersion: z.string().min(1).max(80).optional(),
194
+ scheduledFor: isoInstant.optional(),
195
+ status: z.enum(RUN_STATUSES),
196
+ sessionId: z.string().min(1).max(120).optional(),
197
+ sessionTitle: z.string().min(1).max(200).optional(),
198
+ turnState: TurnStateSchema.optional(),
199
+ queuedAt: isoInstant,
200
+ startedAt: isoInstant.optional(),
201
+ finishedAt: isoInstant.optional(),
202
+ durationMs: z.number().int().min(0).optional(),
203
+ steps: z.number().int().min(0),
204
+ toolCalls: z.number().int().min(0),
205
+ blockedRequiresUser: z.boolean(),
206
+ errors: z.array(RunErrorSchema).max(MAX_ERRORS_PER_RUN),
207
+ summary: z.string().max(2_000).optional(),
208
+ })
209
+
210
+ export const AttemptRecordSchema = z.strictObject({
211
+ schemaVersion: z.literal(1),
212
+ id: z.string().min(1).max(300),
213
+ profileKey: z.string().min(1).max(80),
214
+ taskId: z.string().min(1).max(80),
215
+ runId: z.string().min(1).max(80),
216
+ occurrenceKey: z.string().min(1).max(300),
217
+ state: z.enum(['claimed', 'session_created', 'prompt_submitted', 'accepted', 'failed', 'unknown']),
218
+ createdAt: isoInstant,
219
+ updatedAt: isoInstant,
220
+ })
221
+
222
+ export const NotificationSchema = z.strictObject({
223
+ schemaVersion: z.literal(1),
224
+ id: z.string().min(1).max(80),
225
+ profileKey: z.string().min(1).max(80),
226
+ kind: z.enum(['run_failed', 'run_timeout', 'run_succeeded', 'scheduler_standby', 'calendar_unavailable', 'skipped']),
227
+ title: z.string().min(1).max(120),
228
+ message: z.string().max(MAX_ERROR_MESSAGE_CHARS),
229
+ runId: z.string().min(1).max(80).optional(),
230
+ taskId: z.string().min(1).max(80).optional(),
231
+ createdAt: isoInstant,
232
+ readAt: isoInstant.optional(),
233
+ })
234
+
235
+ export const OwnerRecordSchema = z.strictObject({
236
+ schemaVersion: z.literal(1),
237
+ profileKey: z.string().min(1).max(80),
238
+ ownerId: z.string().min(1).max(120),
239
+ fencingToken: z.string().min(1).max(120),
240
+ leaseExpiresAt: isoInstant,
241
+ heartbeatAt: isoInstant,
242
+ schedulerState: z.enum(['active', 'standby']),
243
+ updatedAt: isoInstant,
244
+ })
245
+
246
+ /** Create/patch wire schemas: no profile, owner, session, preset or cwd field exists at all. */
247
+ export const TaskCreateSchema = z.strictObject({
248
+ title: z.string().min(1).max(MAX_TITLE_CHARS),
249
+ description: z.string().max(MAX_DESCRIPTION_CHARS).optional(),
250
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS),
251
+ category: z.enum(['news', 'market', 'review', 'research']),
252
+ trigger: TriggerSchema,
253
+ mode: ExecutionModeSchema,
254
+ workspaceId: workspaceId.optional(),
255
+ limits: LimitsSchema.optional(),
256
+ notifications: NotificationsSchema.optional(),
257
+ })
258
+
259
+ export const TaskPatchSchema = z.strictObject({
260
+ title: z.string().min(1).max(MAX_TITLE_CHARS).optional(),
261
+ description: z.string().max(MAX_DESCRIPTION_CHARS).optional(),
262
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS).optional(),
263
+ category: z.enum(['news', 'market', 'review', 'research']).optional(),
264
+ trigger: TriggerSchema.optional(),
265
+ mode: ExecutionModeSchema.optional(),
266
+ workspaceId: workspaceId.optional(),
267
+ limits: LimitsSchema.optional(),
268
+ notifications: NotificationsSchema.optional(),
269
+ })
270
+
271
+ export const NotificationsReadSchema = z.strictObject({
272
+ ids: z.array(z.string().min(1).max(80)).max(200).optional(),
273
+ all: z.boolean().optional(),
274
+ }).refine((value) => value.all === true || (value.ids !== undefined && value.ids.length > 0), {
275
+ message: 'either all=true or a non-empty ids array is required',
276
+ })
277
+
278
+ export const BuiltinDefaultsSchema = z.strictObject({
279
+ state: z.literal('dormant'),
280
+ category: z.enum(['news', 'market', 'review', 'research']),
281
+ title: z.string().min(1).max(MAX_TITLE_CHARS),
282
+ description: z.string().max(MAX_DESCRIPTION_CHARS),
283
+ trigger: TriggerSchema,
284
+ instruction: z.string().min(1).max(MAX_INSTRUCTION_CHARS),
285
+ mode: ExecutionModeSchema,
286
+ cwdKind: z.literal('profile'),
287
+ limits: LimitsSchema,
288
+ notifications: NotificationsSchema,
289
+ })
290
+
291
+ export const BuiltinDefinitionSchema = z.strictObject({
292
+ id: z.string().min(1).max(80),
293
+ version: z.number().int().min(1),
294
+ category: z.enum(['news', 'market', 'review', 'research']),
295
+ defaults: BuiltinDefaultsSchema,
296
+ })
297
+
298
+ /** Reject values JSON would silently change: undefined, NaN/Infinity, holes, non-object members. */
299
+ export function assertCleanJson(value, path = '$') {
300
+ if (value === undefined) throw new TypeError(`undefined is not allowed at ${path}`)
301
+ if (typeof value === 'number' && !Number.isFinite(value)) throw new TypeError(`non-finite number at ${path}`)
302
+ if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint') {
303
+ throw new TypeError(`unsupported value at ${path}`)
304
+ }
305
+ if (Array.isArray(value)) {
306
+ for (let index = 0; index < value.length; index += 1) {
307
+ if (!(index in value)) throw new TypeError(`sparse array hole at ${path}[${index}]`)
308
+ assertCleanJson(value[index], `${path}[${index}]`)
309
+ }
310
+ return
311
+ }
312
+ if (value !== null && typeof value === 'object') {
313
+ const proto = Object.getPrototypeOf(value)
314
+ if (proto !== Object.prototype && proto !== null) throw new TypeError(`non-plain object at ${path}`)
315
+ for (const [key, item] of Object.entries(value)) assertCleanJson(item, `${path}.${key}`)
316
+ }
317
+ }
318
+
319
+ export function parseStrict(schema, value, label) {
320
+ try {
321
+ assertCleanJson(value)
322
+ } catch (error) {
323
+ throw new SchemaError('invalid_record', `${label}: ${error instanceof Error ? error.message : String(error)}`)
324
+ }
325
+ const result = schema.safeParse(value)
326
+ if (!result.success) {
327
+ const issue = result.error.issues[0]
328
+ const where = issue?.path?.length ? ` at ${issue.path.join('.')}` : ''
329
+ throw new SchemaError('invalid_record', `${label}${where}: ${issue?.message ?? 'invalid value'}`)
330
+ }
331
+ return result.data
332
+ }
333
+
334
+ export class SchemaError extends Error {
335
+ constructor(code, message) {
336
+ super(message)
337
+ this.name = 'SchemaError'
338
+ this.code = code
339
+ }
340
+ }
341
+
342
+ export function isActiveRun(status) {
343
+ return ACTIVE_RUN_STATUSES.includes(status)
344
+ }
345
+
346
+ /** Merge order: code definition -> legal user override -> server runtime fields. */
347
+ export function resolveTaskRecord(record, definition) {
348
+ const base = record.origin === 'builtin'
349
+ ? definition?.defaults
350
+ : record.custom
351
+ if (base === undefined) throw new SchemaError('invalid_record', `task ${record.id} has no resolvable base configuration`)
352
+ const overrides = record.overrides ?? {}
353
+ const merged = {
354
+ ...base,
355
+ ...overrides,
356
+ state: record.state,
357
+ }
358
+ const task = {
359
+ id: record.id,
360
+ schemaVersion: 1,
361
+ definitionVersion: record.definitionVersion,
362
+ origin: record.origin,
363
+ ...(record.sourceBuiltinId === undefined ? {} : { sourceBuiltinId: record.sourceBuiltinId }),
364
+ profileKey: record.profileKey,
365
+ profileName: record.profileName,
366
+ ...merged,
367
+ ...(record.overrides === undefined ? {} : { overrides: record.overrides }),
368
+ scheduleAnchorAt: record.scheduleAnchorAt,
369
+ ...(record.lastHandledOccurrenceKey === undefined ? {} : { lastHandledOccurrenceKey: record.lastHandledOccurrenceKey }),
370
+ ...(record.lastScheduledFor === undefined ? {} : { lastScheduledFor: record.lastScheduledFor }),
371
+ createdAt: record.createdAt,
372
+ updatedAt: record.updatedAt,
373
+ }
374
+ return parseStrict(AutomationTaskSchema, task, 'automation task')
375
+ }