@ultdai/pulse 0.2.0 → 0.3.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.
@@ -177,11 +177,37 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
177
177
  version: z.ZodNumber;
178
178
  }, z.core.$strict>;
179
179
  export declare const SignalReducerSchema: z.ZodEnum<{
180
+ "additive-window": "additive-window";
180
181
  "distinct-per-window": "distinct-per-window";
181
182
  lifecycle: "lifecycle";
182
183
  presence: "presence";
183
184
  }>;
184
185
  export declare const SignalTypeDefinitionSchema: z.ZodObject<{
186
+ additive: z.ZodOptional<z.ZodObject<{
187
+ baseScopeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
188
+ cohortAtField: z.ZodString;
189
+ dedupeFields: z.ZodArray<z.ZodString>;
190
+ dimensionSets: z.ZodArray<z.ZodObject<{
191
+ kind: z.ZodString;
192
+ fields: z.ZodArray<z.ZodString>;
193
+ }, z.core.$strict>>;
194
+ latenessMs: z.ZodNumber;
195
+ metricRules: z.ZodArray<z.ZodObject<{
196
+ name: z.ZodString;
197
+ op: z.ZodEnum<{
198
+ count: "count";
199
+ sum: "sum";
200
+ }>;
201
+ unitField: z.ZodOptional<z.ZodString>;
202
+ valueField: z.ZodOptional<z.ZodString>;
203
+ when: z.ZodOptional<z.ZodObject<{
204
+ field: z.ZodString;
205
+ equals: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
206
+ }, z.core.$strict>>;
207
+ }, z.core.$strict>>;
208
+ primaryMetric: z.ZodString;
209
+ stableScopeField: z.ZodOptional<z.ZodString>;
210
+ }, z.core.$strict>>;
185
211
  id: z.ZodString;
186
212
  inputPulseTypes: z.ZodArray<z.ZodObject<{
187
213
  id: z.ZodString;
@@ -203,6 +229,7 @@ export declare const SignalTypeDefinitionSchema: z.ZodObject<{
203
229
  timeoutMs: z.ZodNumber;
204
230
  }, z.core.$strict>>;
205
231
  reducer: z.ZodEnum<{
232
+ "additive-window": "additive-window";
206
233
  "distinct-per-window": "distinct-per-window";
207
234
  lifecycle: "lifecycle";
208
235
  presence: "presence";
@@ -229,6 +256,7 @@ export declare const MetricPulseTargetSchema: z.ZodObject<{
229
256
  refs: z.ZodArray<z.ZodObject<{
230
257
  signalId: z.ZodString;
231
258
  dimension: z.ZodString;
259
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
232
260
  }, z.core.$strict>>;
233
261
  sessionStartedAt: z.ZodString;
234
262
  shardCount: z.ZodNumber;
@@ -289,6 +317,7 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
289
317
  refs: z.ZodArray<z.ZodObject<{
290
318
  signalId: z.ZodString;
291
319
  dimension: z.ZodString;
320
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
292
321
  }, z.core.$strict>>;
293
322
  sessionStartedAt: z.ZodString;
294
323
  shardCount: z.ZodNumber;
@@ -299,6 +328,86 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
299
328
  windowStartAt: z.ZodOptional<z.ZodString>;
300
329
  }, z.core.$strict>;
301
330
  }, z.core.$strict>;
331
+ /** A pulse carries a bounded vector of exact, non-sampled metric additions. */
332
+ export declare const AdditivePulseTargetSchema: z.ZodObject<{
333
+ dedupeKey: z.ZodString;
334
+ generation: z.ZodNumber;
335
+ latenessMs: z.ZodNumber;
336
+ metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
337
+ organizationId: z.ZodString;
338
+ ownerId: z.ZodString;
339
+ primaryMetric: z.ZodString;
340
+ refs: z.ZodArray<z.ZodObject<{
341
+ signalId: z.ZodString;
342
+ dimension: z.ZodString;
343
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
344
+ }, z.core.$strict>>;
345
+ shardCount: z.ZodNumber;
346
+ shardIndex: z.ZodNumber;
347
+ signalTypeId: z.ZodString;
348
+ stableScopeKey: z.ZodOptional<z.ZodString>;
349
+ windowEndAt: z.ZodString;
350
+ windowStartAt: z.ZodString;
351
+ }, z.core.$strict>;
352
+ export declare const RoutedAdditivePulseSchema: z.ZodObject<{
353
+ pulse: z.ZodObject<{
354
+ data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
355
+ eventId: z.ZodOptional<z.ZodString>;
356
+ id: z.ZodString;
357
+ organizationId: z.ZodString;
358
+ sampling: z.ZodDefault<z.ZodObject<{
359
+ applied: z.ZodBoolean;
360
+ appliedKeepProbability: z.ZodNumber;
361
+ effectiveKeepProbability: z.ZodNumber;
362
+ ingressApplied: z.ZodBoolean;
363
+ ingressKeepProbability: z.ZodNumber;
364
+ ingressMethod: z.ZodEnum<{
365
+ none: "none";
366
+ "pulse-hash-v1": "pulse-hash-v1";
367
+ }>;
368
+ knownKeepProbability: z.ZodNullable<z.ZodNumber>;
369
+ method: z.ZodEnum<{
370
+ none: "none";
371
+ "pulse-hash-v1": "pulse-hash-v1";
372
+ }>;
373
+ policyId: z.ZodNullable<z.ZodString>;
374
+ policyRevision: z.ZodNullable<z.ZodNumber>;
375
+ unit: z.ZodEnum<{
376
+ member: "member";
377
+ pulse: "pulse";
378
+ }>;
379
+ }, z.core.$strict>>;
380
+ source: z.ZodString;
381
+ subject: z.ZodOptional<z.ZodObject<{
382
+ id: z.ZodString;
383
+ type: z.ZodString;
384
+ }, z.core.$strip>>;
385
+ type: z.ZodString;
386
+ typeVersion: z.ZodNumber;
387
+ occurredAt: z.ZodString;
388
+ receivedAt: z.ZodString;
389
+ }, z.core.$strict>;
390
+ target: z.ZodObject<{
391
+ dedupeKey: z.ZodString;
392
+ generation: z.ZodNumber;
393
+ latenessMs: z.ZodNumber;
394
+ metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
395
+ organizationId: z.ZodString;
396
+ ownerId: z.ZodString;
397
+ primaryMetric: z.ZodString;
398
+ refs: z.ZodArray<z.ZodObject<{
399
+ signalId: z.ZodString;
400
+ dimension: z.ZodString;
401
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
402
+ }, z.core.$strict>>;
403
+ shardCount: z.ZodNumber;
404
+ shardIndex: z.ZodNumber;
405
+ signalTypeId: z.ZodString;
406
+ stableScopeKey: z.ZodOptional<z.ZodString>;
407
+ windowEndAt: z.ZodString;
408
+ windowStartAt: z.ZodString;
409
+ }, z.core.$strict>;
410
+ }, z.core.$strict>;
302
411
  export declare const LifecyclePulseTargetSchema: z.ZodObject<{
303
412
  generation: z.ZodNumber;
304
413
  groupId: z.ZodString;
@@ -312,6 +421,7 @@ export declare const LifecyclePulseTargetSchema: z.ZodObject<{
312
421
  refs: z.ZodArray<z.ZodObject<{
313
422
  signalId: z.ZodString;
314
423
  dimension: z.ZodString;
424
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
315
425
  }, z.core.$strict>>;
316
426
  scopeId: z.ZodString;
317
427
  sessionStartedAt: z.ZodString;
@@ -372,6 +482,7 @@ export declare const RoutedLifecyclePulseSchema: z.ZodObject<{
372
482
  refs: z.ZodArray<z.ZodObject<{
373
483
  signalId: z.ZodString;
374
484
  dimension: z.ZodString;
485
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
375
486
  }, z.core.$strict>>;
376
487
  scopeId: z.ZodString;
377
488
  sessionStartedAt: z.ZodString;
@@ -393,15 +504,20 @@ export declare const MetricSnapshotSchema: z.ZodObject<{
393
504
  varianceEstimate: z.ZodOptional<z.ZodNumber>;
394
505
  generation: z.ZodNumber;
395
506
  kind: z.ZodEnum<{
507
+ "additive-window": "additive-window";
396
508
  "distinct-per-window": "distinct-per-window";
397
509
  presence: "presence";
398
510
  }>;
511
+ latenessMs: z.ZodOptional<z.ZodNumber>;
512
+ metrics: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
399
513
  observedAt: z.ZodString;
400
514
  organizationId: z.ZodString;
401
515
  originId: z.ZodString;
402
516
  recovering: z.ZodBoolean;
403
517
  revision: z.ZodNumber;
404
518
  sampledCount: z.ZodNumber;
519
+ scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
520
+ scopeKind: z.ZodOptional<z.ZodString>;
405
521
  shardCount: z.ZodNumber;
406
522
  signalId: z.ZodString;
407
523
  signalTypeId: z.ZodString;
@@ -517,6 +633,7 @@ export declare const LivePulseTargetSchema: z.ZodObject<{
517
633
  memberId: z.ZodString;
518
634
  organizationId: z.ZodString;
519
635
  reducer: z.ZodEnum<{
636
+ "additive-window": "additive-window";
520
637
  "distinct-per-window": "distinct-per-window";
521
638
  lifecycle: "lifecycle";
522
639
  presence: "presence";
@@ -570,6 +687,7 @@ export declare const RoutedLivePulseSchema: z.ZodObject<{
570
687
  memberId: z.ZodString;
571
688
  organizationId: z.ZodString;
572
689
  reducer: z.ZodEnum<{
690
+ "additive-window": "additive-window";
573
691
  "distinct-per-window": "distinct-per-window";
574
692
  lifecycle: "lifecycle";
575
693
  presence: "presence";
@@ -667,6 +785,8 @@ export type LiveShardSnapshot = z.infer<typeof LiveShardSnapshotSchema>;
667
785
  export type MetricPulseTarget = z.infer<typeof MetricPulseTargetSchema>;
668
786
  export type MetricSnapshot = z.infer<typeof MetricSnapshotSchema>;
669
787
  export type RoutedMetricPulse = z.infer<typeof RoutedMetricPulseSchema>;
788
+ export type RoutedAdditivePulse = z.infer<typeof RoutedAdditivePulseSchema>;
789
+ export type AdditivePulseTarget = z.infer<typeof AdditivePulseTargetSchema>;
670
790
  export type RoutedLifecyclePulse = z.infer<typeof RoutedLifecyclePulseSchema>;
671
791
  export type LifecyclePulseTarget = z.infer<typeof LifecyclePulseTargetSchema>;
672
792
  export type LifecycleDecision = z.infer<typeof LifecycleDecisionSchema>;
package/dist/index.js CHANGED
@@ -11,22 +11,29 @@ var compilePayloadSchema = (value) => {
11
11
  const required = new Set(schema.required);
12
12
  const fields = {};
13
13
  for (const [name, field] of Object.entries(schema.properties)) {
14
- let validator;
15
- if (field.enum) {
16
- if (field.enum.length === 0 || field.enum.some((item) => typeof item !== "string")) {
17
- throw new Error(`Invalid enum for ${name}.`);
18
- }
19
- validator = z.enum(field.enum);
20
- } else if (field.type === "string") {
14
+ let validator2;
15
+ if (field.pattern !== void 0 && field.type !== "string") {
16
+ throw new Error(`Pattern is only supported for string field ${name}.`);
17
+ }
18
+ if (field.type === "string" || field.type === void 0 && field.enum?.every((item) => typeof item === "string")) {
21
19
  let string = z.string();
22
20
  if (field.minLength !== void 0)
23
21
  string = string.min(field.minLength);
24
22
  if (field.maxLength !== void 0)
25
23
  string = string.max(field.maxLength);
24
+ if (field.pattern !== void 0) {
25
+ let pattern;
26
+ try {
27
+ pattern = new RegExp(field.pattern);
28
+ } catch {
29
+ throw new Error(`Invalid pattern for ${name}.`);
30
+ }
31
+ string = string.regex(pattern);
32
+ }
26
33
  if (field.format === "date-time") {
27
- validator = string.refine((item) => Number.isFinite(Date.parse(item)), "Invalid date-time.");
34
+ validator2 = string.refine((item) => Number.isFinite(Date.parse(item)), "Invalid date-time.");
28
35
  } else if (field.format === void 0) {
29
- validator = string;
36
+ validator2 = string;
30
37
  } else {
31
38
  throw new Error(`Unsupported string format for ${name}.`);
32
39
  }
@@ -36,20 +43,46 @@ var compilePayloadSchema = (value) => {
36
43
  number = number.min(field.minimum);
37
44
  if (field.maximum !== void 0)
38
45
  number = number.max(field.maximum);
39
- validator = number;
46
+ validator2 = number;
40
47
  } else if (field.type === "boolean") {
41
- validator = z.boolean();
48
+ validator2 = z.boolean();
42
49
  } else {
43
50
  throw new Error(`Unsupported payload field ${name}.`);
44
51
  }
45
- fields[name] = required.has(name) ? validator : validator.optional();
52
+ if (field.enum !== void 0) {
53
+ const enumType = field.type === "number" || field.type === "integer" ? "number" : "string";
54
+ if (field.enum.length === 0 || field.enum.some((item) => typeof item !== enumType)) {
55
+ throw new Error(`Invalid enum for ${name}.`);
56
+ }
57
+ const allowed = new Set(field.enum);
58
+ validator2 = validator2.refine((item) => allowed.has(item), "Invalid enum value.");
59
+ }
60
+ fields[name] = required.has(name) ? validator2 : validator2.optional();
46
61
  }
47
62
  for (const name of required) {
48
63
  if (!fields[name])
49
64
  throw new Error(`Required payload field ${name} is undefined.`);
50
65
  }
51
66
  const object = z.object(fields);
52
- return schema.additionalProperties === false ? object.strict() : object.passthrough();
67
+ const validator = schema.additionalProperties === false ? object.strict() : object.passthrough();
68
+ if (!schema.dependentRequired)
69
+ return validator;
70
+ for (const [field, dependents] of Object.entries(schema.dependentRequired)) {
71
+ if (!fields[field] || !Array.isArray(dependents) || dependents.some((dependent) => !fields[dependent])) {
72
+ throw new Error(`Invalid dependentRequired for ${field}.`);
73
+ }
74
+ }
75
+ return validator.superRefine((payload, context) => {
76
+ for (const [field, dependents] of Object.entries(schema.dependentRequired ?? {})) {
77
+ if (payload[field] === void 0)
78
+ continue;
79
+ for (const dependent of dependents) {
80
+ if (payload[dependent] === void 0) {
81
+ context.addIssue({ code: "custom", path: [dependent], message: `${dependent} is required with ${field}.` });
82
+ }
83
+ }
84
+ }
85
+ });
53
86
  };
54
87
 
55
88
  // ../../libs/contracts/dist/index.js
@@ -205,8 +238,36 @@ var PulseTypeDefinitionSchema = z2.object({
205
238
  context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle pulses need explicit IDs, live delivery, and an interval only for observations." });
206
239
  }
207
240
  });
208
- var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle"]);
241
+ var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle", "additive-window"]);
242
+ var MetricNameSchema = z2.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/).max(64);
243
+ var AdditiveMetricRuleSchema = z2.object({
244
+ name: MetricNameSchema,
245
+ op: z2.enum(["count", "sum"]),
246
+ unitField: z2.string().trim().min(1).max(200).optional(),
247
+ valueField: z2.string().trim().min(1).max(200).optional(),
248
+ when: z2.object({
249
+ field: z2.string().trim().min(1).max(200),
250
+ equals: z2.union([z2.string(), z2.number().finite(), z2.boolean()])
251
+ }).strict().optional()
252
+ }).strict().superRefine((rule, context) => {
253
+ if (rule.op === "sum" !== Boolean(rule.valueField)) {
254
+ context.addIssue({ code: "custom", path: ["valueField"], message: "Sum needs valueField; count must not have one." });
255
+ }
256
+ });
209
257
  var SignalTypeDefinitionSchema = z2.object({
258
+ additive: z2.object({
259
+ baseScopeFields: z2.array(z2.string().trim().min(1).max(100)).max(4).optional(),
260
+ cohortAtField: z2.string().trim().min(1).max(200),
261
+ dedupeFields: z2.array(z2.string().trim().min(1).max(200)).min(1).max(4),
262
+ dimensionSets: z2.array(z2.object({
263
+ kind: z2.string().trim().min(1).max(100),
264
+ fields: z2.array(z2.string().trim().min(1).max(100)).max(4)
265
+ }).strict()).min(1).max(16),
266
+ latenessMs: z2.number().int().min(0).max(36e5),
267
+ metricRules: z2.array(AdditiveMetricRuleSchema).min(1).max(32),
268
+ primaryMetric: MetricNameSchema,
269
+ stableScopeField: z2.string().trim().min(1).max(200).optional()
270
+ }).strict().optional(),
210
271
  id: z2.string().trim().min(1).max(100),
211
272
  inputPulseTypes: z2.array(z2.object({
212
273
  id: z2.string().trim().min(1).max(100),
@@ -246,8 +307,26 @@ var SignalTypeDefinitionSchema = z2.object({
246
307
  if (definition.reducer === "lifecycle" && (!definition.lifecycle || !definition.partitionKey || !definition.dimensions?.length)) {
247
308
  context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle needs a configuration, partition key and dimensions." });
248
309
  }
310
+ if (definition.reducer === "additive-window") {
311
+ const additive = definition.additive;
312
+ if (!additive || !definition.windowMs || !definition.partitionKey) {
313
+ context.addIssue({ code: "custom", path: ["additive"], message: "Additive window needs metrics, windowMs and partitionKey." });
314
+ } else {
315
+ const names = additive.metricRules.map((rule) => rule.name);
316
+ if (new Set(names).size !== names.length || !names.includes(additive.primaryMetric)) {
317
+ context.addIssue({ code: "custom", path: ["additive", "metricRules"], message: "Metric names must be unique and include the primary metric." });
318
+ }
319
+ const kinds = additive.dimensionSets.map((set) => set.kind);
320
+ const sets = additive.dimensionSets.map((set) => JSON.stringify(set.fields));
321
+ if (new Set(sets).size !== sets.length || new Set(kinds).size !== kinds.length) {
322
+ context.addIssue({ code: "custom", path: ["additive", "dimensionSets"], message: "Dimension sets and kinds must be unique." });
323
+ }
324
+ }
325
+ } else if (definition.additive) {
326
+ context.addIssue({ code: "custom", path: ["additive"], message: "Only additive-window signals may define additive metrics." });
327
+ }
249
328
  const schedule = definition.shardSchedule ?? [];
250
- if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length)) {
329
+ if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length && !definition.additive?.dimensionSets.length)) {
251
330
  context.addIssue({ code: "custom", path: ["partitionKey"], message: "Scheduled routing needs partitionKey and dimensions." });
252
331
  }
253
332
  if (schedule.length > 0 && definition.reducer === "presence" && !definition.presence?.startedAtField) {
@@ -269,7 +348,8 @@ var SignalTypeDefinitionSchema = z2.object({
269
348
  });
270
349
  var MetricSignalRefSchema = z2.object({
271
350
  signalId: z2.string().trim().min(1).max(500),
272
- dimension: z2.string().trim().min(1).max(100)
351
+ dimension: z2.string().trim().min(1).max(100),
352
+ scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional()
273
353
  }).strict();
274
354
  var MetricPulseTargetSchema = z2.object({
275
355
  generation: z2.number().int().positive(),
@@ -301,6 +381,33 @@ var RoutedMetricPulseSchema = z2.object({
301
381
  pulse: RawPulseSchema,
302
382
  target: MetricPulseTargetSchema
303
383
  }).strict();
384
+ var AdditivePulseTargetSchema = z2.object({
385
+ dedupeKey: z2.string().trim().min(1).max(500),
386
+ generation: z2.number().int().positive(),
387
+ latenessMs: z2.number().int().min(0).max(36e5),
388
+ metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()),
389
+ organizationId: z2.string().trim().min(1).max(100),
390
+ ownerId: z2.string().trim().min(1).max(500),
391
+ primaryMetric: MetricNameSchema,
392
+ refs: z2.array(MetricSignalRefSchema).min(1).max(16),
393
+ shardCount: z2.number().int().min(1).max(65536),
394
+ shardIndex: z2.number().int().nonnegative(),
395
+ signalTypeId: z2.string().trim().min(1).max(100),
396
+ stableScopeKey: z2.string().trim().min(1).max(500).optional(),
397
+ windowEndAt: isoDateTime,
398
+ windowStartAt: isoDateTime
399
+ }).strict().superRefine((target, context) => {
400
+ if (target.shardIndex >= target.shardCount || Date.parse(target.windowStartAt) >= Date.parse(target.windowEndAt)) {
401
+ context.addIssue({ code: "custom", path: ["shardIndex"], message: "Invalid additive shard or window." });
402
+ }
403
+ if (!Object.hasOwn(target.metrics, target.primaryMetric)) {
404
+ context.addIssue({ code: "custom", path: ["metrics"], message: "Additive metrics must include the primary metric." });
405
+ }
406
+ if (new Set(target.refs.map((ref) => ref.signalId)).size !== target.refs.length) {
407
+ context.addIssue({ code: "custom", path: ["refs"], message: "Additive signal references must be unique." });
408
+ }
409
+ });
410
+ var RoutedAdditivePulseSchema = z2.object({ pulse: RawPulseSchema, target: AdditivePulseTargetSchema }).strict();
304
411
  var LifecyclePulseTargetSchema = z2.object({
305
412
  generation: z2.number().int().positive(),
306
413
  groupId: z2.string().trim().min(1).max(100),
@@ -335,22 +442,38 @@ var MetricSnapshotSchema = z2.object({
335
442
  estimatedCount: z2.number().finite().nonnegative(),
336
443
  varianceEstimate: z2.number().finite().nonnegative().optional(),
337
444
  generation: z2.number().int().positive(),
338
- kind: z2.enum(["presence", "distinct-per-window"]),
445
+ kind: z2.enum(["presence", "distinct-per-window", "additive-window"]),
446
+ latenessMs: z2.number().int().min(0).max(36e5).optional(),
447
+ metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()).optional(),
339
448
  observedAt: isoDateTime,
340
449
  organizationId: z2.string().trim().min(1).max(100),
341
450
  originId: z2.string().trim().min(1).max(200),
342
451
  recovering: z2.boolean(),
343
452
  revision: z2.number().int().nonnegative(),
344
453
  sampledCount: z2.number().int().nonnegative(),
454
+ scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional(),
455
+ scopeKind: z2.string().trim().min(1).max(100).optional(),
345
456
  shardCount: z2.number().int().min(1).max(65536),
346
457
  signalId: z2.string().trim().min(1).max(500),
347
458
  signalTypeId: z2.string().trim().min(1).max(100),
348
459
  windowEndAt: isoDateTime.optional(),
349
460
  windowStartAt: isoDateTime.optional()
350
461
  }).strict().superRefine((snapshot, context) => {
351
- if (snapshot.kind === "distinct-per-window" && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
462
+ if ((snapshot.kind === "distinct-per-window" || snapshot.kind === "additive-window") && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
352
463
  context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Distinct snapshot needs a window." });
353
464
  }
465
+ if (snapshot.kind === "additive-window" && !snapshot.metrics) {
466
+ context.addIssue({ code: "custom", path: ["metrics"], message: "Additive snapshot needs metrics." });
467
+ }
468
+ if (snapshot.kind === "additive-window" && snapshot.latenessMs === void 0) {
469
+ context.addIssue({ code: "custom", path: ["latenessMs"], message: "Additive snapshot needs its lateness bound." });
470
+ }
471
+ if (snapshot.kind === "additive-window" && (!snapshot.scope || !snapshot.scopeKind)) {
472
+ context.addIssue({ code: "custom", path: ["scope"], message: "Additive snapshot needs a structured scope." });
473
+ }
474
+ if (snapshot.kind !== "additive-window" && snapshot.metrics) {
475
+ context.addIssue({ code: "custom", path: ["metrics"], message: "Only additive snapshots carry metrics." });
476
+ }
354
477
  if (snapshot.kind === "presence" && (snapshot.windowStartAt || snapshot.windowEndAt)) {
355
478
  context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Presence snapshot must not have a window." });
356
479
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultdai/pulse",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "files": ["dist", "README.md"],
6
6
  "publishConfig": { "access": "public" },
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json --emitDeclarationOnly && node scripts/bundle.mjs",
17
- "prepack": "pnpm --dir ../../libs/contracts run build && pnpm run build",
17
+ "prepack": "corepack pnpm --dir ../../libs/contracts run build && corepack pnpm run build",
18
18
  "test": "vitest run test --exclude 'dist/**'",
19
19
  "typecheck": "tsc -p tsconfig.json --noEmit"
20
20
  },