@sguild/dispatcher 2.0.0 → 2.0.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.
Files changed (96) hide show
  1. package/README.md +4 -1
  2. package/contracts/README.md +30 -0
  3. package/contracts/coach-availability/README.md +355 -0
  4. package/contracts/coach-availability/README.v2.md +263 -0
  5. package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
  6. package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
  7. package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
  8. package/contracts/coaching-confirmation/README.md +96 -0
  9. package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
  10. package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
  11. package/contracts/credit-reservation-funding-state/README.md +147 -0
  12. package/contracts/credit-reservation-lock/README.md +433 -0
  13. package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
  14. package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
  15. package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
  16. package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
  17. package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
  18. package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
  19. package/contracts/event-envelope/README.md +205 -0
  20. package/contracts/event-envelope/schema/envelope-v1.json +2 -2
  21. package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
  22. package/contracts/event-types-registry.json +337 -24
  23. package/contracts/external-actions/README.md +338 -0
  24. package/contracts/finance-mart/README.md +238 -0
  25. package/contracts/finance-mart/cac-payback.md +113 -0
  26. package/contracts/finance-mart/cash-position.md +98 -0
  27. package/contracts/finance-mart/cohort-summary.md +72 -0
  28. package/contracts/finance-mart/customer-journey-audit.md +92 -0
  29. package/contracts/finance-mart/ltv.md +92 -0
  30. package/contracts/finance-mart/margin.md +87 -0
  31. package/contracts/finance-mart/pnl.md +83 -0
  32. package/contracts/finance-mart/reconciliation.md +98 -0
  33. package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
  34. package/contracts/finance-mart/unit-economics.md +94 -0
  35. package/contracts/growth-warehouse-api/README.md +162 -0
  36. package/contracts/identity/README.md +184 -0
  37. package/contracts/identity/person-canonical-fields.md +120 -0
  38. package/contracts/identity/person-externals.md +267 -0
  39. package/contracts/identity/person-resolution-semantics.md +144 -0
  40. package/contracts/identity/person-role-taxonomy.md +120 -0
  41. package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
  42. package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
  43. package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
  44. package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
  45. package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
  46. package/contracts/identity/validation/client-table.md +131 -0
  47. package/contracts/identity/validation/coach-handling.md +100 -0
  48. package/contracts/identity/validation/person-graph.md +140 -0
  49. package/contracts/lead-lifecycle/README.md +187 -0
  50. package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
  51. package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
  52. package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
  53. package/contracts/lesson-lifecycle/README.md +118 -0
  54. package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
  55. package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
  56. package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
  57. package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
  58. package/contracts/lesson-lifecycle/validation/README.md +5 -0
  59. package/contracts/mart-consumer-api/README.md +108 -0
  60. package/contracts/order-flow/README.md +106 -0
  61. package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
  62. package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
  63. package/contracts/payment-flow/README.md +157 -0
  64. package/contracts/platform-comms/README.md +84 -0
  65. package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
  66. package/contracts/platform-geography-snapshot/README.md +205 -0
  67. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
  68. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
  69. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
  70. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
  71. package/contracts/portfolio-mart/README.md +133 -0
  72. package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
  73. package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
  74. package/contracts/portfolio-mart/cross-market-performance.md +84 -0
  75. package/contracts/portfolio-mart/health-composites.md +88 -0
  76. package/contracts/portfolio-mart/org-topology.md +70 -0
  77. package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
  78. package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
  79. package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
  80. package/contracts/refund-flow/README.md +136 -0
  81. package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
  82. package/contracts/sales-scheduling-surface/README.md +532 -0
  83. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
  84. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
  85. package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
  86. package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
  87. package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
  88. package/contracts/warehouse-silver/README.md +118 -0
  89. package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
  90. package/dist/events.d.ts +63 -0
  91. package/dist/events.js +293 -0
  92. package/dist/index.d.ts +2 -0
  93. package/dist/index.js +7 -1
  94. package/dist/postgres-consumer.js +2 -1
  95. package/dist/validator.js +1 -0
  96. package/package.json +1 -1
@@ -0,0 +1,270 @@
1
+ # Validation: Event envelope against the existing event-name vocabulary
2
+
3
+ **Status:** Passed with one design clarification recommended (intake.captured subject)
4
+ **Date:** 2026-04-25
5
+ **Owner:** Platform
6
+ **Validates:** `platform/contracts/event-envelope/README.md` v1.0.0
7
+
8
+ ## Purpose
9
+
10
+ Confirm that every event_type already named in the Sguild architecture fits the v1.0.0 event envelope shape cleanly. Walk each event, identify producer, subject, actor, payload, and surface any case where the envelope is awkward or insufficient.
11
+
12
+ ## Vocabulary walked
13
+
14
+ Twenty event types across four producers (Platform, Growth, Sales, Delivery, Revenue). Grouped by producer.
15
+
16
+ ## Platform identity service events (producer = `platform`)
17
+
18
+ ### `person.created`
19
+
20
+ - Subject: `person_id` (the new Person)
21
+ - Actor: `system:platform` for automated mint via `intake.captured`; `<person_id>` of an admin for manual entry
22
+ - Payload: full nine-field canonical Person shape
23
+ - Schema version: 1
24
+
25
+ Fits envelope cleanly. The producer is unambiguously Platform's identity service, the subject is the canonical entity, and the payload matches the Identity Contract v1.0.0 person shape.
26
+
27
+ ### `person.updated`
28
+
29
+ - Subject: `person_id`
30
+ - Actor: `system:platform` for automated invariants (the daily `is_minor` job); `<person_id>` for admin or customer-driven updates
31
+ - Payload: full nine-field current Person shape
32
+ - Schema version: 1
33
+
34
+ Fits cleanly. Same shape as `person.created`. Bumps `updated_at` on the Person record as part of producing the event.
35
+
36
+ ### `person.merged`
37
+
38
+ - Subject: `canonical_person_id` (the surviving Person)
39
+ - Actor: `system:platform` for auto-merges; `<person_id>` of the operator for manual merges
40
+ - Payload: `old_person_id`, `canonical_person_id`, `reason_code` (enum), `promoted_fields[]`, `role_record_changes[]`
41
+ - Schema version: 1
42
+
43
+ Fits cleanly. Subject correctly points at the canonical Person rather than the merged-away one; consumers resolving stale references via `alias_of` use the subject as the new reference.
44
+
45
+ ## Growth events (producer = `growth`)
46
+
47
+ ### `intake.captured`
48
+
49
+ - Subject: **see clarification below**
50
+ - Actor: `system:growth` for automated form submissions; `<person_id>` of an operator for manual entry
51
+ - Payload: `form_submission_id`, `source`, `campaign`, `utm_*`, `raw_email`, `raw_phone`, `raw_name`, `submitted_at`, `browser_metadata` (optional)
52
+ - Schema version: 1
53
+
54
+ **Design clarification recommended.** At the moment Growth captures an intake, the canonical `person_id` does not yet exist; Platform's identity service mints or matches the Person in response to this event. So when Growth emits `intake.captured`, the `subject` field has no canonical Person to point at yet. Three options:
55
+
56
+ 1. **Subject = `form_submission_id` (recommended).** Reserve a `fsm_<UUID v7 canonical>` prefix (extends ADR-0002's reserved prefix table) for form submissions. Growth emits `intake.captured` with `subject=fsm_*`. Platform receives, mints or matches the Person, and emits a separate `intake.matched` event whose subject is the new `person_id` and whose payload links back to the original `fsm_*` ID. Consumers that want to track an intake from capture to match join on `fsm_*` until the match event provides the `person_id`.
57
+
58
+ 2. **Subject = empty (omit).** Allowed by the envelope (subject is optional). Consumers that need to track the intake parse the payload's `form_submission_id`. Less self-describing but works.
59
+
60
+ 3. **Subject populated by Platform after match.** Violates envelope immutability (events should not mutate after emission). Reject.
61
+
62
+ Recommendation is option 1 because it keeps the envelope subject self-describing throughout the intake lifecycle and avoids the subjectless event pattern. This requires a small additive change to the Identity Contract (new reserved prefix `fsm_`) and a new Platform-emitted event (`intake.matched`). Both are v1.x additive and do not require event-envelope-v1.0.0 contract changes.
63
+
64
+ ## Sales events (producer = `sales`)
65
+
66
+ ### `lead.qualified`
67
+
68
+ - Subject: `person_id`
69
+ - Actor: `<person_id>` of the sales rep who closed qualification
70
+ - Payload: `lead_id`, `qualification_signal` (enum: `connected`, `replied`, `inbound_request`), `connected_at`
71
+ - Schema version: 1
72
+
73
+ Fits cleanly. Subject points at the human being qualified.
74
+
75
+ ### `lead.unreachable`
76
+
77
+ - Subject: `person_id`
78
+ - Actor: `system:sales` (automated determination after four attempts)
79
+ - Payload: `lead_id`, `attempt_count` (always 4 at this point), `last_attempt_at`
80
+ - Schema version: 1
81
+
82
+ Fits cleanly.
83
+
84
+ ### `lead.disqualified`
85
+
86
+ - Subject: `person_id`
87
+ - Actor: `<person_id>` of the sales rep who decided
88
+ - Payload: `lead_id`, `disqualification_reason` (free text or enum), `disqualified_at`
89
+ - Schema version: 1
90
+
91
+ Fits cleanly.
92
+
93
+ ## Delivery events (producer = `delivery`)
94
+
95
+ ### `lesson.scheduled`
96
+
97
+ - Subject: `lesson_id`
98
+ - Actor: `<person_id>` of the scheduler (sales or ops staff), or `<person_id>` of the customer for self-service scheduling
99
+ - Payload: `lesson_id`, `participant_person_ids[]`, `coach_person_id`, `organization_id`, `lesson_starts_at`, `lesson_duration_minutes`, `offering_id`, `credit_account_id` (the buyer's account, used for the reservation), `credits_required` (N)
100
+ - Schema version: 1
101
+
102
+ Fits cleanly. **Worth noting:** the subject is `lesson_id` (the lesson is the entity the event is about), not a participant `person_id`. For group lessons with multiple participants, all participant IDs ride in the payload. Subscribers who care about a specific person's lessons filter on `participant_person_ids[]` rather than on `subject`. This is correct: events are about entities, and a multi-participant lesson is one entity.
103
+
104
+ ### `lesson.delivered`
105
+
106
+ - Subject: `lesson_id`
107
+ - Actor: `<person_id>` of the coach who marked delivery
108
+ - Payload: `lesson_id`, `attendance[]` (one entry per participant with status `present`/`absent`), `delivered_at`
109
+ - Schema version: 1
110
+
111
+ Fits cleanly. Triggers Revenue's Lesson Completion Job for any reservation tied to this lesson.
112
+
113
+ ### `lesson.cancelled`
114
+
115
+ - Subject: `lesson_id`
116
+ - Actor: depends on initiator (`<person_id>` for human; `system:delivery` for automated)
117
+ - Payload: `lesson_id`, `initiator` (enum: `customer`, `admin`, `coach`, `system_weather`, `system_logistics`, `system_unpaid`, `system_other`), `reason_code`, `reason_note` (optional), `cancelled_at`
118
+ - Schema version: 1
119
+
120
+ Fits cleanly. Triggers Revenue's cancellation policy lookup; result drives the credit reservation lock state machine to `released` or `forfeited`.
121
+
122
+ ## Revenue events (producer = `revenue`)
123
+
124
+ ### `credit.purchased`
125
+
126
+ - Subject: `person_id`
127
+ - Actor: `<person_id>` of the customer (who initiated the purchase)
128
+ - Payload: `credit_account_id`, `person_id`, `purchased_credits` (N), `order_id`, `purchased_at`
129
+ - Schema version: 1
130
+
131
+ Fits cleanly. Subscribers (Sales, Growth, Delivery) reference `person_id` for their own role-record updates.
132
+
133
+ ### `credit.reserved`
134
+
135
+ - Subject: `person_id`
136
+ - Actor: `<person_id>` of the scheduler or `system:revenue` for auto-rebooks after Sguild-side cancel
137
+ - Payload: `credit_reservation_id`, `credit_account_id`, `person_id`, `lesson_id`, `organization_id`, `reserved_credits` (N), `reserved_at`, `lesson_starts_at`
138
+ - Schema version: 1
139
+
140
+ Fits cleanly. Multiple useful IDs in payload (`credit_reservation_id`, `credit_account_id`, `lesson_id`) for downstream joins; subject stays the canonical Person.
141
+
142
+ ### `credit.funded`
143
+
144
+ - Subject: `person_id`
145
+ - Actor: `<person_id>` of the customer for invoice-paid funding; `system:revenue` for credits-available auto-funding at scheduling
146
+ - Payload: `credit_reservation_id`, `funding_source` (enum: `credits_available`, `invoice_paid`), `funded_at`
147
+ - Schema version: 1
148
+
149
+ Fits cleanly. Sales subscribes to mark Lead pipeline state to "funded" without needing to wait for lock.
150
+
151
+ ### `credit.locked`
152
+
153
+ - Subject: `person_id`
154
+ - Actor: `system:revenue` (Reservation Job at T-24h)
155
+ - Payload: `credit_reservation_id`, `lesson_id`, `person_id`, `locked_credits` (N), `locked_at`
156
+ - Schema version: 1
157
+
158
+ Fits cleanly. The first emission per Person fires `customer.handoff` as a sidecar.
159
+
160
+ ### `credit.consumed`
161
+
162
+ - Subject: `person_id`
163
+ - Actor: `system:revenue` (Lesson Completion Job)
164
+ - Payload: `credit_reservation_id`, `lesson_id`, `consumed_credits` (N), `consumed_at`
165
+ - Schema version: 1
166
+
167
+ Fits cleanly.
168
+
169
+ ### `credit.released`
170
+
171
+ - Subject: `person_id`
172
+ - Actor: `<person_id>` of the cancelling human or `system:revenue` for system_unpaid auto-cancel
173
+ - Payload: `credit_reservation_id`, `credit_account_id`, `released_credits` (N), `initiator` (enum), `reversal_reason` (enum: `Credits Released`, `Administrative Void`), `reason_note` (optional), `cancelled_at`
174
+ - Schema version: 1
175
+
176
+ Fits cleanly. Initiator and reversal_reason enums give consumers enough categorical detail to drive their own logic without re-deriving from policy.
177
+
178
+ ### `credit.forfeited`
179
+
180
+ - Subject: `person_id`
181
+ - Actor: `system:revenue` (Forfeit Job)
182
+ - Payload: `credit_reservation_id`, `lesson_id`, `forfeited_credits` (N), `forfeiture_reason` (enum: `late_cancel`, `no_show`), `forfeited_at`
183
+ - Schema version: 1
184
+
185
+ Fits cleanly.
186
+
187
+ ### `customer.handoff`
188
+
189
+ - Subject: `person_id`
190
+ - Actor: `system:revenue` (sidecar emission at first lock)
191
+ - Payload: `person_id`, `first_lesson_id`, `credit_reservation_id`, `handoff_at`
192
+ - Schema version: 1
193
+
194
+ Fits cleanly. Subject points at the human being handed off; payload carries the lesson and reservation context.
195
+
196
+ ### `invoice.sent`
197
+
198
+ - Subject: `person_id`
199
+ - Actor: `system:revenue` (Revenue's invoicing layer, often triggered by a scheduled job at T-7 days)
200
+ - Payload: `invoice_id`, `person_id`, `amount`, `currency`, `line_items[]`, `due_at`, `sent_at`
201
+ - Schema version: 1
202
+
203
+ Fits cleanly.
204
+
205
+ ### `payment.captured`
206
+
207
+ - Subject: `person_id`
208
+ - Actor: `<person_id>` of the customer for self-service; `system:revenue` for auto-capture (e.g., scheduled charge of saved card)
209
+ - Payload: `payment_id`, `invoice_id` (optional, if tied to an invoice), `amount`, `currency`, `captured_at`, `payment_method_id`
210
+ - Schema version: 1
211
+
212
+ Fits cleanly.
213
+
214
+ ## Cross-cutting validation
215
+
216
+ ### Producer enum coverage
217
+
218
+ The five `producer` enum values (`platform`, `growth`, `sales`, `operations`, `revenue`) cover every event type in the vocabulary. No event needs a sub-producer field at the envelope level; sub-job attribution (which Revenue job emitted which event, etc.) lives in payloads or in the ledger's `Created Via` field where applicable. Producer enum is sufficient at v1.0.0.
219
+
220
+ ### Schema version coverage
221
+
222
+ Every event_type starts at `schema_version=1` and registers its initial payload schema in the event-type registry. No event needs a higher version at v1.0.0. Future additive payload changes (new optional fields) can land at v2 of an event_type without envelope contract changes.
223
+
224
+ ### Subject pattern coverage
225
+
226
+ - Person-centric events (`person.*`, `lead.*`, `credit.*`, `customer.handoff`, `invoice.*`, `payment.*`, `intake.captured` post-match) carry `person_id` as subject.
227
+ - Lesson-centric events (`lesson.*`) carry `lesson_id` as subject.
228
+ - Pre-match `intake.captured` is the only event whose subject is not yet a canonical Person ID at emission time; the recommended `fsm_` prefix solves this without breaking the envelope contract.
229
+
230
+ ### Actor pattern coverage
231
+
232
+ Every event's actor falls into one of two patterns: `<person_id>` for human-initiated actions (sales rep, coach, ops staff, customer self-service, admin) or `system:<domain>` for automated actions. The dispatcher SDK auto-populates `system:<producer>` when no human actor is specified and the producer is identifiable. Actor is universally meaningful for audit.
233
+
234
+ ### Correlation chains
235
+
236
+ The most important cross-domain chain in the architecture is intake-to-handoff:
237
+
238
+ ```
239
+ intake.captured (Growth)
240
+ → person.created (Platform)
241
+ → intake.matched (Platform) [proposed, see intake.captured clarification]
242
+ → lead.qualified (Sales)
243
+ → credit.purchased (Revenue)
244
+ → credit.reserved (Revenue, at Sales' scheduling action)
245
+ → credit.funded (Revenue)
246
+ → credit.locked (Revenue)
247
+ → customer.handoff (Revenue, sidecar)
248
+ ```
249
+
250
+ The dispatcher SDK SHOULD auto-propagate `correlation_id` through this chain by stamping each downstream event with the originating event's `event_id` (or with the upstream event's `correlation_id` if that upstream event was itself part of a chain). Without auto-propagation, consumers lose the cross-domain trace. This is an implementation note for the dispatcher SDK, not a contract change.
251
+
252
+ ## Findings
253
+
254
+ 1. Twenty event types validated. Every one fits the envelope cleanly with the seven required fields plus optional subject, actor, and correlation_id.
255
+ 2. The producer enum (`platform`, `growth`, `sales`, `operations`, `revenue`) is sufficient. No envelope changes needed.
256
+ 3. The actor pattern (`<person_id>` for humans, `system:<domain>` for automation) is universally applicable.
257
+ 4. The subject pattern is consistent: person-centric events carry `person_id`, lesson-centric events carry `lesson_id`. Multi-participant events keep `lesson_id` as subject and list participants in payload, which is the correct treatment.
258
+ 5. **One design clarification.** `intake.captured` at emission time precedes the canonical Person mint, so its subject cannot be `person_id`. Recommended fix: reserve a `fsm_` prefix for form submissions and use that as the subject of `intake.captured`. Platform emits a separate `intake.matched` event with the canonical `person_id` once Platform's identity service resolves the Person. This is additive (a new prefix in ADR-0002 and a new Platform event) and does not require changes to ADR-0005 or the event envelope contract.
259
+ 6. The `correlation_id` field exists in the envelope but its propagation behavior (the dispatcher SDK should auto-stamp from upstream event_id) is an implementation note for the dispatcher SDK, not a contract field semantics change.
260
+
261
+ ## Recommendation
262
+
263
+ No changes to Event Envelope Contract v1.0.0 required from this validation. Two follow-up additive items for v1.x:
264
+
265
+ 1. Reserve `fsm_<UUID v7 canonical>` prefix for form submissions in ADR-0002's reserved-prefix table. Update the Identity Contract README's §4.3 prefix table.
266
+ 2. Define a new `intake.matched` event_type emitted by Platform's identity service when a `fsm_*` is matched or minted to a `person_id`. Payload: `form_submission_id`, `person_id`, `match_type` (enum: `auto_matched`, `auto_minted`, `manual_resolved`), `matched_at`. This is an additive event, registered in the event-type registry at `schema_version=1`.
267
+
268
+ Both items are minor and can be addressed when the intake-capture flow is implemented in code. Neither blocks tagging `event-envelope-v1.0.0`.
269
+
270
+ Ready to feed into T3 (tag event-envelope-v1.0.0) and T4 (register in Notion Contracts Registry).