@propertysimple/client 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 (54) hide show
  1. package/API.md +79 -0
  2. package/README.md +348 -0
  3. package/client/src/directory-sync.d.ts +123 -0
  4. package/client/src/directory-sync.js +404 -0
  5. package/client/src/index.d.ts +5123 -0
  6. package/client/src/index.js +285 -0
  7. package/client/src/managed-sync.d.ts +129 -0
  8. package/client/src/managed-sync.js +410 -0
  9. package/contracts/src/catalog-derived.d.ts +7 -0
  10. package/contracts/src/catalog-derived.js +78 -0
  11. package/contracts/src/catalog-fields.d.ts +231 -0
  12. package/contracts/src/catalog-fields.js +194 -0
  13. package/contracts/src/catalog-inventory.d.ts +14 -0
  14. package/contracts/src/catalog-inventory.js +10 -0
  15. package/contracts/src/directory-execution.d.ts +320 -0
  16. package/contracts/src/directory-execution.js +122 -0
  17. package/contracts/src/directory-identity.d.ts +1255 -0
  18. package/contracts/src/directory-identity.js +129 -0
  19. package/contracts/src/directory-sales.d.ts +333 -0
  20. package/contracts/src/directory-sales.js +102 -0
  21. package/contracts/src/directory.d.ts +1088 -0
  22. package/contracts/src/directory.js +179 -0
  23. package/contracts/src/index.d.ts +18 -0
  24. package/contracts/src/index.js +18 -0
  25. package/contracts/src/intelligence.d.ts +366 -0
  26. package/contracts/src/intelligence.js +157 -0
  27. package/contracts/src/location-serving.d.ts +13 -0
  28. package/contracts/src/location-serving.js +89 -0
  29. package/contracts/src/location.d.ts +73 -0
  30. package/contracts/src/location.js +75 -0
  31. package/contracts/src/openapi.d.ts +22 -0
  32. package/contracts/src/openapi.js +109 -0
  33. package/contracts/src/operations.d.ts +8316 -0
  34. package/contracts/src/operations.js +56 -0
  35. package/contracts/src/opportunity-events.d.ts +38 -0
  36. package/contracts/src/opportunity-events.js +69 -0
  37. package/contracts/src/platform-api.d.ts +12 -0
  38. package/contracts/src/platform-api.js +1 -0
  39. package/contracts/src/professional-intelligence.d.ts +256 -0
  40. package/contracts/src/professional-intelligence.js +90 -0
  41. package/contracts/src/property-features.d.ts +94 -0
  42. package/contracts/src/property-features.js +398 -0
  43. package/contracts/src/publication.d.ts +44 -0
  44. package/contracts/src/publication.js +42 -0
  45. package/contracts/src/routes.d.ts +437 -0
  46. package/contracts/src/routes.js +496 -0
  47. package/contracts/src/schemas.d.ts +3198 -0
  48. package/contracts/src/schemas.js +674 -0
  49. package/contracts/src/search.d.ts +2630 -0
  50. package/contracts/src/search.js +223 -0
  51. package/contracts/src/smart-feed.d.ts +3731 -0
  52. package/contracts/src/smart-feed.js +109 -0
  53. package/openapi.json +43542 -0
  54. package/package.json +60 -0
@@ -0,0 +1,674 @@
1
+ import { z } from 'zod';
2
+ import { CatalogInventoryStatusSchema } from './catalog-inventory.js';
3
+ import { LocationSchema } from './location.js';
4
+ import { redactListingAddress } from './location-serving.js';
5
+ import { SharedListingFields, SourceFieldsSchema, ProfessionalContactsSchema, } from './catalog-fields.js';
6
+ export * from './catalog-fields.js';
7
+ export * from './intelligence.js';
8
+ /** Wire types are intentionally independent of database rows and provider payloads. */
9
+ export const Id = z.string().min(1).max(256);
10
+ export const Timestamp = z.iso.datetime({ offset: true });
11
+ export const DateOnly = z.iso.date();
12
+ export const Json = z.json();
13
+ export const Empty = z.strictObject({});
14
+ export const PrincipalSchema = z.strictObject({
15
+ subject_id: Id,
16
+ tenant_id: Id,
17
+ product_id: Id,
18
+ environment: z.enum(['development', 'test', 'production']),
19
+ scopes: z.array(z.string()),
20
+ dataset_ids: z.array(Id),
21
+ profile_ids: z.array(Id),
22
+ audience_ids: z.array(Id),
23
+ account_ids: z.array(Id),
24
+ producer_ids: z.array(Id),
25
+ });
26
+ export const ProblemSchema = z.strictObject({
27
+ type: z.string(),
28
+ title: z.string(),
29
+ status: z.number().int(),
30
+ detail: z.string(),
31
+ code: z.string(),
32
+ request_id: z.string(),
33
+ retryable: z.boolean(),
34
+ details: Json.optional(),
35
+ });
36
+ export const PageQuery = z.strictObject({
37
+ cursor: z.string().min(1).max(4096).optional(),
38
+ limit: z.coerce.number().int().min(1).max(200).default(100),
39
+ });
40
+ export const ResourceQuery = z.strictObject({ id: Id });
41
+ export const ResourcePageQuery = PageQuery.extend({ id: Id });
42
+ export const DatasetPageQuery = PageQuery.extend({ dataset_id: Id.optional() });
43
+ export const MarketStatus = z.enum([
44
+ 'active',
45
+ 'active_under_contract',
46
+ 'pending',
47
+ 'sold',
48
+ 'withdrawn',
49
+ 'expired',
50
+ 'unknown',
51
+ ]);
52
+ export const Money = z.strictObject({
53
+ amount: z.string().regex(/^\d+(?:\.\d{1,2})?$/),
54
+ currency: z.string().regex(/^[A-Z]{3}$/),
55
+ period: z.string().nullable(),
56
+ });
57
+ export const Address = z.strictObject({
58
+ street: z.string().nullable(),
59
+ unit: z.string().nullable(),
60
+ city: z.string().nullable(),
61
+ region: z.string().nullable(),
62
+ postal_code: z.string().nullable(),
63
+ country: z.string().nullable(),
64
+ });
65
+ export const AttributeAssessment = z.strictObject({
66
+ attribute: Id,
67
+ value: z.union([z.string(), z.boolean(), z.number(), z.null()]),
68
+ evidence_state: z.enum(['supported', 'explicitly_absent', 'not_stated', 'conflicting']),
69
+ origin: z.enum(['source_structured', 'source_text_interpreted']),
70
+ evidence_refs: z.array(z.string()),
71
+ input_fingerprint: z.string(),
72
+ taxonomy_version: z.string(),
73
+ evaluation_id: Id,
74
+ freshness: z.enum(['current', 'invalidated', 'pending', 'failed']),
75
+ });
76
+ /** Public attribution excludes email and private lead-routing destinations. */
77
+ export const ImageRenditionSchema = z.strictObject({
78
+ url: z.url(),
79
+ // Bounding dimensions, not the actual dimensions: the CDN preserves aspect
80
+ // ratio and never upscales. Consumers must not treat these as srcset widths.
81
+ max_width: z.number().int().positive(),
82
+ max_height: z.number().int().positive(),
83
+ });
84
+ export const ListingMediaSchema = z.strictObject({
85
+ url: z.url(),
86
+ order: z.number().int().nonnegative(),
87
+ caption: z.string().nullable(),
88
+ width: z.number().int().positive().nullable(),
89
+ height: z.number().int().positive().nullable(),
90
+ modified_at: Timestamp.nullable(),
91
+ delivery: z.enum(['source_url', 'hosted']),
92
+ renditions: z
93
+ .strictObject({
94
+ thumbnail: ImageRenditionSchema,
95
+ map: ImageRenditionSchema,
96
+ card: ImageRenditionSchema,
97
+ gallery: ImageRenditionSchema,
98
+ })
99
+ .optional(),
100
+ });
101
+ export const ListingAttributionSchema = z.strictObject({
102
+ agent_name: z.string().nullable(),
103
+ agent_license: z.string().nullable().optional(),
104
+ office_name: z.string().nullable(),
105
+ office_key: z.string().nullable(),
106
+ office_phone: z.string().nullable(),
107
+ office_website: z.url().nullable(),
108
+ broker_name: z.string().nullable(),
109
+ broker_phone: z.string().nullable().optional(),
110
+ originating_system: z.string().nullable(),
111
+ });
112
+ export const PropertyDetailsSchema = z.strictObject({
113
+ category: z.string().nullable(),
114
+ sub_type: z.string().nullable(),
115
+ bathrooms_full: z.number().nonnegative().nullable(),
116
+ bathrooms_half: z.number().nonnegative().nullable(),
117
+ bathrooms_one_quarter: z.number().nonnegative().nullable().optional(),
118
+ bathrooms_three_quarter: z.number().nonnegative().nullable().optional(),
119
+ stories: z.number().nonnegative().nullable(),
120
+ garage_spaces: z.number().nonnegative().nullable(),
121
+ parking_total: z.number().nonnegative().nullable(),
122
+ county: z.string().nullable(),
123
+ subdivision: z.string().nullable(),
124
+ features: z.record(z.string(), z.array(z.string())),
125
+ // Additive source detail fields: old snapshots remain readable. False is a
126
+ // supplied assertion; a missing key is unknown, never an inferred negative.
127
+ source_flags: z
128
+ .partialRecord(z.enum([
129
+ 'AssociationYN',
130
+ 'AttachedGarageYN',
131
+ 'BasementYN',
132
+ 'CarportYN',
133
+ 'CoolingYN',
134
+ 'ElectricOnPropertyYN',
135
+ 'FireplaceYN',
136
+ 'GarageYN',
137
+ 'HeatingYN',
138
+ 'NewConstructionYN',
139
+ 'PoolPrivateYN',
140
+ 'SpaYN',
141
+ 'WaterfrontYN',
142
+ 'AccessibleElevatorInstalled',
143
+ 'Dock',
144
+ ]), z.boolean())
145
+ .optional(),
146
+ association_fees: z
147
+ .array(z.strictObject({
148
+ kind: z.enum(['association', 'association_secondary']),
149
+ amount: z
150
+ .string()
151
+ .regex(/^\d+(?:\.\d{1,2})?$/)
152
+ .nullable(),
153
+ amount_state: z.enum(['supported', 'not_stated', 'unknown']),
154
+ currency: z
155
+ .string()
156
+ .regex(/^[A-Z]{3}$/)
157
+ .nullable(),
158
+ currency_source: z.enum(['field', 'source_default']).nullable(),
159
+ currency_state: z.enum(['supported', 'not_stated', 'unknown']),
160
+ period: z
161
+ .enum(['day', 'week', 'month', 'quarter', 'half_year', 'year', 'one_time'])
162
+ .nullable(),
163
+ period_state: z.enum(['supported', 'not_stated', 'unknown']),
164
+ source_paths: z.array(z.string()),
165
+ }))
166
+ .optional(),
167
+ rooms: z
168
+ .array(z.strictObject({
169
+ type: z.string().nullable(),
170
+ level: z.string().nullable(),
171
+ features: z.array(z.string()),
172
+ dimensions: z.string().nullable(),
173
+ length: z.number().nonnegative().nullable(),
174
+ width: z.number().nonnegative().nullable(),
175
+ unit: z.string().nullable(),
176
+ source_paths: z.array(z.string()),
177
+ }))
178
+ .optional(),
179
+ year_built_effective: z.number().int().nonnegative().nullable().optional(),
180
+ units_total: z.number().int().nonnegative().nullable().optional(),
181
+ entry_level: z.number().nonnegative().nullable().optional(),
182
+ });
183
+ export const CatalogProfile = z.enum(['catalog', 'portal_active']);
184
+ export const ListingDetailQuery = ResourceQuery.extend({
185
+ profile: CatalogProfile.optional(),
186
+ dataset_id: Id.optional(),
187
+ });
188
+ const ListingWireSchema = z.strictObject({
189
+ ...SharedListingFields,
190
+ id: Id,
191
+ episode_id: Id,
192
+ revision: z.string(),
193
+ property_id: Id.nullable().optional(),
194
+ source_identities: z.array(z.strictObject({ source: Id, key: Id })),
195
+ market_status: MarketStatus,
196
+ source_availability: z.enum(['present', 'removed_unknown']),
197
+ display_eligibility: z.boolean(),
198
+ price: Money.nullable(),
199
+ // Optional so historical snapshots remain readable by the coordinated SDK.
200
+ // Unknown/conflicting source evidence never becomes an inferred sale or rental.
201
+ transaction_type: z.enum(['for_sale', 'for_rent', 'unknown', 'conflicting']).optional(),
202
+ price_period_state: z.enum(['supported', 'not_stated', 'unknown', 'conflicting']).optional(),
203
+ address: Address,
204
+ listing_agent_ids: z.array(Id),
205
+ description: z.string().nullable(),
206
+ photos: z.array(z.string()),
207
+ /** Total gallery size. Search hits contain only a cover; detail/feed contain all photos. */
208
+ photo_count: z.number().int().nonnegative().optional(),
209
+ media: z.array(ListingMediaSchema).optional(),
210
+ attribution: ListingAttributionSchema.optional(),
211
+ property_details: PropertyDetailsSchema.optional(),
212
+ raw_status: z.string().nullable().optional(),
213
+ property_type: z.string().nullable(),
214
+ bedrooms: z.number().nullable(),
215
+ bathrooms: z.number().nullable(),
216
+ living_area: z.number().nullable(),
217
+ living_area_unit: z.string().nullable().optional(),
218
+ lot_size: z.number().nullable().optional(),
219
+ lot_size_unit: z.string().nullable().optional(),
220
+ year_built: z.number().int().nullable().optional(),
221
+ latitude: z.number().min(-90).max(90).nullable().optional(),
222
+ longitude: z.number().min(-180).max(180).nullable().optional(),
223
+ location: LocationSchema.optional(),
224
+ mls_number: z.string().nullable().optional(),
225
+ source_url: z.string().nullable(),
226
+ listed_date: z.string().nullable(),
227
+ listed_precision: z.enum(['date', 'instant', 'unknown']),
228
+ open_houses: z.array(z.strictObject({
229
+ id: Id,
230
+ start_at: z.string(),
231
+ end_at: z.string().nullable(),
232
+ timezone: z.string().nullable(),
233
+ status: z.enum(['scheduled', 'cancelled']),
234
+ })),
235
+ classifications: z.array(AttributeAssessment),
236
+ display: z.strictObject({ address_allowed: z.boolean(), disclaimer: z.string().nullable() }),
237
+ freshness: z.strictObject({
238
+ last_seen_at: Timestamp,
239
+ published_at: Timestamp,
240
+ source_modified_at: Timestamp.nullable(),
241
+ }),
242
+ });
243
+ // Strict validation runs first. Redaction also protects older stored payloads at
244
+ // the API/SDK boundary without changing identities, revisions or listing status.
245
+ export const ListingSchema = ListingWireSchema.overwrite(redactListingAddress);
246
+ /** Contact routing is server-only and never embedded in public listing projections. */
247
+ export const ListingContactsSchema = z.strictObject({
248
+ professionals: ProfessionalContactsSchema.optional(),
249
+ source_fields: SourceFieldsSchema.optional(),
250
+ listing_id: Id,
251
+ agent_name: z.string().nullable(),
252
+ agent_email: z.email().nullable(),
253
+ agent_phone: z.string().nullable(),
254
+ agent_license: z.string().nullable(),
255
+ agent_website: z.url().nullable(),
256
+ lead_routing_email: z.email().nullable(),
257
+ preferred_email: z.email().nullable(),
258
+ });
259
+ export const AgentSchema = z.strictObject({
260
+ id: Id,
261
+ source_identities: z.array(z.strictObject({ source: Id, key: Id })),
262
+ name: z.string().nullable(),
263
+ first_name: z.string().nullable().optional(),
264
+ last_name: z.string().nullable().optional(),
265
+ email: z.email().nullable().optional(),
266
+ office_phone: z.string().nullable().optional(),
267
+ primary_contact_phone: z.string().nullable().optional(),
268
+ website_url: z.string().nullable().optional(),
269
+ listings_count_total: z.number().int().nullable().optional(),
270
+ listings_count_active: z.number().int().nullable().optional(),
271
+ mls_id: z.string().nullable().optional(),
272
+ license: z.string().nullable().optional(),
273
+ fax: z.string().nullable().optional(),
274
+ profile_observation: z
275
+ .strictObject({
276
+ observed_at: Timestamp,
277
+ source_modified_at: Timestamp.nullable(),
278
+ basis: z.literal('retained_source_profile'),
279
+ })
280
+ .optional(),
281
+ inventory: z
282
+ .strictObject({
283
+ as_of: Timestamp,
284
+ source: Id,
285
+ basis: z.literal('current_visible_catalog'),
286
+ coverage: z.literal('imported_inventory_only'),
287
+ })
288
+ .optional(),
289
+ });
290
+ const ListingQueryFields = DatasetPageQuery.extend({
291
+ transaction_type: z.enum(['for_sale', 'for_rent']).optional(),
292
+ price_period: z.string().min(1).max(40).optional(),
293
+ profile: CatalogProfile.optional(),
294
+ sort: z
295
+ .enum(['id', 'price_asc', 'price_desc', 'newest', 'bedrooms_desc', 'source_modified_desc'])
296
+ .optional(),
297
+ include_total: z.union([z.boolean(), z.enum(['true', 'false'])]).optional(),
298
+ city: z.string().min(1).max(200).optional(),
299
+ region: z.string().min(1).max(100).optional(),
300
+ postal_code: z.string().min(1).max(30).optional(),
301
+ property_type: z.string().min(1).max(100).optional(),
302
+ currency: z
303
+ .string()
304
+ .regex(/^[A-Z]{3}$/)
305
+ .optional(),
306
+ max_bedrooms: z.coerce.number().min(0).max(100).optional(),
307
+ min_bathrooms: z.coerce.number().min(0).max(100).optional(),
308
+ max_bathrooms: z.coerce.number().min(0).max(100).optional(),
309
+ min_living_area: z.coerce.number().min(0).max(100000000).optional(),
310
+ max_living_area: z.coerce.number().min(0).max(100000000).optional(),
311
+ living_area_unit: z.enum(['sqft', 'sqm']).optional(),
312
+ q: z.string().min(1).max(500).optional(),
313
+ address_q: z.string().trim().min(2).max(500).optional(),
314
+ market_status: MarketStatus.optional(),
315
+ agent_id: Id.optional(),
316
+ party_id: Id.optional(),
317
+ inventory_basis: z.literal('listing_representation').optional(),
318
+ mls_number: z.string().max(128).optional(),
319
+ modified_since: Timestamp.optional(),
320
+ source_modified_since: Timestamp.optional(),
321
+ min_price: z
322
+ .string()
323
+ .regex(/^\d+(?:\.\d{1,2})?$/)
324
+ .optional(),
325
+ max_price: z
326
+ .string()
327
+ .regex(/^\d+(?:\.\d{1,2})?$/)
328
+ .optional(),
329
+ min_bedrooms: z.coerce.number().min(0).max(100).optional(),
330
+ features: z.array(z.string().min(1).max(80)).max(20).optional(),
331
+ bbox: z
332
+ .string()
333
+ .max(128)
334
+ .refine((value) => {
335
+ if (!/^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/.test(value))
336
+ return false;
337
+ const parts = value.split(',').map(Number);
338
+ return (parts.length === 4 &&
339
+ parts.every(Number.isFinite) &&
340
+ parts[0] >= -180 &&
341
+ parts[2] <= 180 &&
342
+ parts[1] >= -90 &&
343
+ parts[3] <= 90 &&
344
+ parts[0] <= parts[2] &&
345
+ parts[1] <= parts[3]);
346
+ }, 'Use west,south,east,north within geographic bounds, without crossing the antimeridian.')
347
+ .optional(),
348
+ });
349
+ const explicitPartyInventory = (query) => Boolean(query.party_id) === Boolean(query.inventory_basis);
350
+ const partyInventoryMessage = {
351
+ message: 'party_id and inventory_basis=listing_representation must be supplied together.',
352
+ };
353
+ export const ListingQuery = ListingQueryFields.refine(explicitPartyInventory, partyInventoryMessage);
354
+ export const IdentityQuery = z.strictObject({ source: Id, key: Id });
355
+ export const IdentitySchema = z.strictObject({
356
+ source: Id,
357
+ key: Id,
358
+ listing_id: Id,
359
+ episode_id: Id,
360
+ });
361
+ export const AgentIdentitySchema = z.strictObject({ source: Id, key: Id, agent_id: Id });
362
+ export function page(item) {
363
+ return z.strictObject({
364
+ data: z.array(item),
365
+ next_cursor: z.string().nullable(),
366
+ has_more: z.boolean(),
367
+ meta: z.record(z.string(), Json),
368
+ });
369
+ }
370
+ /** Operational source health is live metadata; immutable listing snapshots keep their original observation times. */
371
+ export const CatalogSourceFreshnessSchema = z.strictObject({
372
+ source: Id,
373
+ last_completed_enumeration_at: Timestamp.nullable(),
374
+ last_full_enumeration_at: Timestamp.nullable(),
375
+ state: z.enum(['fresh', 'stale', 'never_completed']),
376
+ freshness_window_seconds: z.number().int().positive(),
377
+ /** Continuous sources separate change-scan freshness from withdrawal coverage. */
378
+ full_enumeration_window_seconds: z.number().int().positive().optional(),
379
+ /** A provider response is not proof that enumeration or withdrawals completed. */
380
+ last_contact_at: Timestamp.nullable().optional(),
381
+ last_contact_status: z.number().int().min(100).max(599).nullable().optional(),
382
+ refresh: z
383
+ .strictObject({
384
+ state: z.enum(['on_schedule', 'delayed', 'blocked']),
385
+ cadence_seconds: z.number().int().positive(),
386
+ next_due_at: Timestamp,
387
+ phase: z.string().nullable(),
388
+ last_attempt_at: Timestamp.nullable(),
389
+ last_progress_at: Timestamp.nullable(),
390
+ last_error_at: Timestamp.nullable(),
391
+ error_code: z.string().nullable(),
392
+ consecutive_failures: z.number().int().nonnegative(),
393
+ })
394
+ .optional(),
395
+ });
396
+ export const CatalogHealthQuery = z.strictObject({ dataset_id: Id });
397
+ export const CatalogHealthSchema = z.strictObject({
398
+ dataset_id: Id,
399
+ source_freshness: z.array(CatalogSourceFreshnessSchema),
400
+ inventory: CatalogInventoryStatusSchema.optional(),
401
+ checked_at: Timestamp,
402
+ });
403
+ export const ListingPage = page(ListingSchema);
404
+ export const ListingMapQuery = ListingQueryFields.omit({
405
+ cursor: true,
406
+ limit: true,
407
+ sort: true,
408
+ include_total: true,
409
+ })
410
+ .extend({
411
+ bbox: ListingQueryFields.shape.bbox.unwrap(),
412
+ limit: z.coerce.number().int().min(1).max(1000).default(500),
413
+ })
414
+ .refine(explicitPartyInventory, partyInventoryMessage);
415
+ export const ListingMapSchema = z.strictObject({
416
+ data: z.array(z.strictObject({
417
+ id: Id,
418
+ latitude: z.number(),
419
+ longitude: z.number(),
420
+ price: Money.nullable(),
421
+ property_type: z.string().nullable(),
422
+ market_status: MarketStatus,
423
+ })),
424
+ total: z.number().int().nonnegative(),
425
+ truncated: z.boolean(),
426
+ meta: z.record(z.string(), Json),
427
+ });
428
+ export const SimilarListingQuery = z.strictObject({
429
+ id: Id,
430
+ dataset_id: Id.optional(),
431
+ profile: CatalogProfile.optional(),
432
+ limit: z.coerce.number().int().min(1).max(20).default(6),
433
+ });
434
+ export const SimilarListingsSchema = z.strictObject({
435
+ data: z.array(z.strictObject({
436
+ listing: ListingSchema,
437
+ score: z.number().min(0).max(100),
438
+ reasons: z.array(z.string()),
439
+ })),
440
+ method: z.literal('facts_similarity_v1'),
441
+ candidate_count: z.number().int().nonnegative(),
442
+ candidates_truncated: z.boolean(),
443
+ });
444
+ export const SnapshotRequest = z.strictObject({ dataset_id: Id });
445
+ export const SnapshotSchema = z.strictObject({
446
+ id: Id,
447
+ dataset_id: Id,
448
+ status: z.enum(['building', 'ready', 'failed', 'expired']),
449
+ schema_version: z.string(),
450
+ snapshot_sequence: z.string(),
451
+ replay_cursor: z.string(),
452
+ created_at: Timestamp,
453
+ expires_at: Timestamp,
454
+ count: z.number().int().nonnegative().optional(),
455
+ });
456
+ export const CatalogChangeSchema = z.strictObject({
457
+ id: Id,
458
+ sequence: z.string(),
459
+ listing_id: Id,
460
+ revision: z.string(),
461
+ operation: z.enum(['upsert', 'remove']),
462
+ listing: ListingSchema.nullable(),
463
+ published_at: Timestamp,
464
+ });
465
+ export const DomainEventSchema = z.strictObject({
466
+ id: Id,
467
+ sequence: z.string(),
468
+ listing_id: Id,
469
+ episode_id: Id,
470
+ type: z.string(),
471
+ occurred_at: Timestamp,
472
+ observed_at: Timestamp,
473
+ evidence: z.record(z.string(), Json),
474
+ });
475
+ export const ClassificationDefinitionSchema = z.strictObject({
476
+ taxonomy_version: z.string(),
477
+ attributes: z.array(z.strictObject({
478
+ id: Id,
479
+ description: z.string(),
480
+ type: z.enum(['boolean', 'string', 'number']),
481
+ values: z.array(z.string()).optional(),
482
+ })),
483
+ });
484
+ export const ActionProfileSchema = z.strictObject({
485
+ id: Id,
486
+ family: Id,
487
+ purpose: z.string(),
488
+ owner_product: Id,
489
+ channel: z.enum(['email', 'social', 'ads']),
490
+ target_unit: z.enum(['contact_actions', 'content_actions', 'ad_candidates']),
491
+ timezone: z.string(),
492
+ policy_revision: z.string(),
493
+ required_context: z.array(z.string()),
494
+ enabled: z.boolean(),
495
+ });
496
+ export const OpportunityQuery = PageQuery.extend({
497
+ profile_id: Id,
498
+ account_id: Id.optional(),
499
+ agent_id: Id.optional(),
500
+ });
501
+ export const OpportunitySchema = z.strictObject({
502
+ id: Id,
503
+ profile_id: Id,
504
+ listing_id: Id,
505
+ episode_id: Id,
506
+ recipient_id: Id,
507
+ intent_id: Id,
508
+ primary_angle: z.string(),
509
+ trigger_event_ids: z.array(Id),
510
+ supporting_evidence_refs: z.array(z.string()),
511
+ reason_codes: z.array(z.string()),
512
+ priority_index: z.number(),
513
+ valid_until: Timestamp,
514
+ decision_revision: z.string(),
515
+ });
516
+ export const PlanRequest = z.strictObject({
517
+ profile_id: Id,
518
+ business_date: DateOnly,
519
+ audience_id: Id,
520
+ target: z.strictObject({
521
+ count: z.number().int().min(1).max(100000),
522
+ unit: z.enum(['contact_actions', 'content_actions', 'ad_candidates']),
523
+ }),
524
+ run_slot: z
525
+ .string()
526
+ .min(1)
527
+ .max(64)
528
+ .regex(/^[a-z0-9_-]+$/),
529
+ });
530
+ export const PlanSchema = z.strictObject({
531
+ id: Id,
532
+ profile_id: Id,
533
+ business_date: DateOnly,
534
+ timezone: z.string(),
535
+ status: z.enum(['queued', 'building', 'ready', 'failed', 'cancelled', 'expired']),
536
+ requested_count: z.number().int().nonnegative(),
537
+ selected_count: z.number().int().nonnegative(),
538
+ unique_recipients: z.number().int().nonnegative(),
539
+ reserve_count: z.number().int().nonnegative(),
540
+ shortfall: z.number().int().nonnegative(),
541
+ completeness: z.enum(['pending', 'complete', 'partial']),
542
+ decision_mode: z.enum(['jev', 'cached_jev', 'fixture']),
543
+ created_at: Timestamp,
544
+ expires_at: Timestamp,
545
+ policy_revision: z.string(),
546
+ exclusions: z.record(z.string(), z.number().int().nonnegative()),
547
+ cutoffs: z.record(z.string(), z.string()),
548
+ execution_counts: z.record(z.string(), z.number().int().nonnegative()).optional(),
549
+ });
550
+ export const ActionState = z.enum([
551
+ 'unclaimed',
552
+ 'reserved',
553
+ 'dispatching',
554
+ 'accepted',
555
+ 'delivered',
556
+ 'rejected',
557
+ 'invalidated',
558
+ 'unknown',
559
+ ]);
560
+ export const PlanItemReplacementSchema = z.strictObject({
561
+ item_id: Id,
562
+ opportunity_id: Id,
563
+ listing_id: Id,
564
+ recipient_id: Id,
565
+ reason: z.string(),
566
+ replaced_at: Timestamp,
567
+ });
568
+ export const PlanItemSchema = z.strictObject({
569
+ id: Id,
570
+ slot: z.number().int(),
571
+ rank: z.number().int(),
572
+ opportunity_id: Id,
573
+ listing_id: Id,
574
+ episode_id: Id,
575
+ recipient_id: Id,
576
+ primary_angle: z.string(),
577
+ trigger_event_ids: z.array(Id),
578
+ supporting_evidence_refs: z.array(z.string()),
579
+ reason_codes: z.array(z.string()),
580
+ valid_until: Timestamp,
581
+ execution_state: ActionState,
582
+ action_id: Id.nullable(),
583
+ // Selection facts remain the original snapshot; this names the reserve executing its logical slot.
584
+ replacement: PlanItemReplacementSchema.optional(),
585
+ });
586
+ export const DispatchRequest = z.strictObject({
587
+ plan_id: Id,
588
+ limit: z.number().int().min(1).max(100),
589
+ worker_id: Id,
590
+ });
591
+ export const DispatchActionSchema = z.strictObject({
592
+ id: Id,
593
+ plan_item_id: Id,
594
+ listing_id: Id,
595
+ recipient_id: Id,
596
+ action_kind: z.string(),
597
+ lease_token: z.string(),
598
+ lease_expires_at: Timestamp,
599
+ endpoint: z.string(),
600
+ payload: z.record(z.string(), Json),
601
+ });
602
+ export const DispatchBatchSchema = z.strictObject({
603
+ id: Id,
604
+ plan_id: Id,
605
+ actions: z.array(DispatchActionSchema),
606
+ exhausted: z.boolean(),
607
+ });
608
+ export const AttemptRequest = z.strictObject({ id: Id, lease_token: z.string().min(1).max(1024) });
609
+ export const AttemptSchema = z.strictObject({
610
+ id: Id,
611
+ action_id: Id,
612
+ provider_idempotency_key: z.string(),
613
+ state: z.literal('dispatching'),
614
+ started_at: Timestamp,
615
+ replayed: z.boolean(),
616
+ });
617
+ export const ActionOutcomeSchema = z.strictObject({
618
+ event_id: Id,
619
+ action_id: Id,
620
+ attempt_id: Id,
621
+ provider: Id,
622
+ provider_event_id: Id,
623
+ outcome: z.enum(['accepted', 'delivered', 'rejected', 'unknown', 'bounced', 'complained']),
624
+ occurred_at: Timestamp,
625
+ details: z.record(z.string(), Json).optional(),
626
+ });
627
+ export const OutcomesRequest = z.strictObject({
628
+ outcomes: z.array(ActionOutcomeSchema).min(1).max(100),
629
+ });
630
+ export const ProductContextEventSchema = z.strictObject({
631
+ event_id: Id,
632
+ producer_id: Id,
633
+ stream_epoch: Id,
634
+ coverage_id: Id,
635
+ subject_id: Id,
636
+ subject_type: Id,
637
+ subject_version: z.number().int().nonnegative(),
638
+ stream_sequence: z.number().int().nonnegative(),
639
+ type: z.string().min(1).max(128),
640
+ occurred_at: Timestamp,
641
+ payload: z.record(z.string(), Json),
642
+ });
643
+ export const ContextEventsRequest = z.strictObject({
644
+ events: z.array(ProductContextEventSchema).min(1).max(100),
645
+ });
646
+ export const BatchResultSchema = z.strictObject({
647
+ results: z.array(z.strictObject({
648
+ event_id: Id,
649
+ status: z.enum(['accepted', 'duplicate', 'rejected']),
650
+ code: z.string().nullable(),
651
+ })),
652
+ });
653
+ export const WebhookRequest = z.strictObject({
654
+ url: z.url(),
655
+ dataset_id: Id,
656
+ event_types: z.array(z.string()).min(1).max(20),
657
+ });
658
+ export const WebhookSchema = z.strictObject({
659
+ id: Id,
660
+ url: z.string(),
661
+ status: z.enum(['pending_verification', 'active', 'disabled']),
662
+ });
663
+ export const CapabilitiesSchema = z.strictObject({
664
+ api_version: z.literal('v1'),
665
+ dataset_ids: z.array(Id),
666
+ profile_ids: z.array(Id),
667
+ features: z.array(z.string()),
668
+ limits: z.record(z.string(), z.number()),
669
+ });
670
+ export const ReadinessSchema = z.strictObject({
671
+ ready: z.boolean(),
672
+ mode: z.string(),
673
+ checks: z.array(z.strictObject({ name: z.string(), ready: z.boolean(), detail: z.string() })),
674
+ });