@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.
- package/README.md +4 -1
- package/contracts/README.md +30 -0
- package/contracts/coach-availability/README.md +355 -0
- package/contracts/coach-availability/README.v2.md +263 -0
- package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
- package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
- package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
- package/contracts/coaching-confirmation/README.md +96 -0
- package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
- package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
- package/contracts/credit-reservation-funding-state/README.md +147 -0
- package/contracts/credit-reservation-lock/README.md +433 -0
- package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
- package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
- package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
- package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
- package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
- package/contracts/event-envelope/README.md +205 -0
- package/contracts/event-envelope/schema/envelope-v1.json +2 -2
- package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
- package/contracts/event-types-registry.json +337 -24
- package/contracts/external-actions/README.md +338 -0
- package/contracts/finance-mart/README.md +238 -0
- package/contracts/finance-mart/cac-payback.md +113 -0
- package/contracts/finance-mart/cash-position.md +98 -0
- package/contracts/finance-mart/cohort-summary.md +72 -0
- package/contracts/finance-mart/customer-journey-audit.md +92 -0
- package/contracts/finance-mart/ltv.md +92 -0
- package/contracts/finance-mart/margin.md +87 -0
- package/contracts/finance-mart/pnl.md +83 -0
- package/contracts/finance-mart/reconciliation.md +98 -0
- package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
- package/contracts/finance-mart/unit-economics.md +94 -0
- package/contracts/growth-warehouse-api/README.md +162 -0
- package/contracts/identity/README.md +184 -0
- package/contracts/identity/person-canonical-fields.md +120 -0
- package/contracts/identity/person-externals.md +267 -0
- package/contracts/identity/person-resolution-semantics.md +144 -0
- package/contracts/identity/person-role-taxonomy.md +120 -0
- package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
- package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
- package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
- package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
- package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
- package/contracts/identity/validation/client-table.md +131 -0
- package/contracts/identity/validation/coach-handling.md +100 -0
- package/contracts/identity/validation/person-graph.md +140 -0
- package/contracts/lead-lifecycle/README.md +187 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
- package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
- package/contracts/lesson-lifecycle/README.md +118 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
- package/contracts/lesson-lifecycle/validation/README.md +5 -0
- package/contracts/mart-consumer-api/README.md +108 -0
- package/contracts/order-flow/README.md +106 -0
- package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
- package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
- package/contracts/payment-flow/README.md +157 -0
- package/contracts/platform-comms/README.md +84 -0
- package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
- package/contracts/platform-geography-snapshot/README.md +205 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
- package/contracts/portfolio-mart/README.md +133 -0
- package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
- package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
- package/contracts/portfolio-mart/cross-market-performance.md +84 -0
- package/contracts/portfolio-mart/health-composites.md +88 -0
- package/contracts/portfolio-mart/org-topology.md +70 -0
- package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
- package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
- package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
- package/contracts/refund-flow/README.md +136 -0
- package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
- package/contracts/sales-scheduling-surface/README.md +532 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
- package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
- package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
- package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
- package/contracts/warehouse-silver/README.md +118 -0
- package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
- package/dist/events.d.ts +63 -0
- package/dist/events.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/postgres-consumer.js +2 -1
- package/dist/validator.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Platform Comms
|
|
2
|
+
|
|
3
|
+
**Status:** v1.0.0
|
|
4
|
+
**Date:** 2026-05-17
|
|
5
|
+
**Owner:** platform (comms ingress)
|
|
6
|
+
**Consumers:** sales (cadence reset), platform-warehouse (analytics ingestion)
|
|
7
|
+
**Related ADRs:** ADR-0001 (tenant_id), ADR-0002 (canonical entity ID template), ADR-0003 (Person canonical and Guardian routing), ADR-0005 (event envelope), ADR-0009 (dispatcher transport, producer transactional guarantee)
|
|
8
|
+
**Related contracts:** Event Envelope Contract (`../event-envelope/README.md`), Identity Contract (`../identity/README.md`), Lead Lifecycle Contract (`../lead-lifecycle/README.md`)
|
|
9
|
+
**Sub-specs (authoritative):** `schema/payloads/platform.comms.inbound-v1.json`
|
|
10
|
+
**Validations:** none yet
|
|
11
|
+
|
|
12
|
+
## 1. Purpose and scope
|
|
13
|
+
|
|
14
|
+
This contract specifies the Platform-owned inbound communications signal used by Sales to reset cadence when a Person sends an inbound SMS or places an inbound voice call. Platform owns provider ingress, Person resolution, tenancy scoping, consent and opt-out checks, Guardian-aware routing checks, and normalized dispatcher emission. Sales owns the cadence reaction after it consumes the dispatcher event.
|
|
15
|
+
|
|
16
|
+
In scope: the event name, payload shape, producer responsibilities, consumer responsibilities, privacy constraints, and versioning rules for `platform.comms.inbound`.
|
|
17
|
+
|
|
18
|
+
Out of scope: provider webhook payload formats, raw SMS body text, call audio, call transcripts, outbound messaging APIs, Sales cadence state transitions, and the operator read surface for full communication details. Raw provider payloads and full message content stay in Platform-owned storage and do not ride on the dispatcher event.
|
|
19
|
+
|
|
20
|
+
## 2. Normative language
|
|
21
|
+
|
|
22
|
+
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted per RFC 2119.
|
|
23
|
+
|
|
24
|
+
## 3. Terminology
|
|
25
|
+
|
|
26
|
+
**Inbound communication.** A customer-originated SMS or voice call received through a communications provider and resolved by Platform to a canonical Person.
|
|
27
|
+
|
|
28
|
+
**Cadence reset signal.** The dispatcher event Sales consumes to stop or reset outbound cadence timing because the customer contacted Sguild first.
|
|
29
|
+
|
|
30
|
+
**Communication ID.** A Platform-owned identifier for the stored inbound communication. Consumers treat it as opaque and use Platform read APIs, not provider payloads, when they need operator-safe detail.
|
|
31
|
+
|
|
32
|
+
## 4. Event types
|
|
33
|
+
|
|
34
|
+
### 4.1 `platform.comms.inbound`
|
|
35
|
+
|
|
36
|
+
Emitted by Platform after an inbound SMS or inbound voice call has passed provider ingress, tenant resolution, Person resolution, consent checks, and Guardian-aware routing checks, and has been routed to Sales as a cadence-reset signal.
|
|
37
|
+
|
|
38
|
+
The event envelope subject SHALL be the canonical `person_id`. The actor SHALL be `system:platform`. The payload carries the same `person_id` for warehouse convenience and consumer-side validation. Payload v1 is authoritative at `schema/payloads/platform.comms.inbound-v1.json`.
|
|
39
|
+
|
|
40
|
+
Known `channel` values at v1 are `sms` and `voice_call`. New channel values are additive only when consumers can safely ignore them.
|
|
41
|
+
|
|
42
|
+
The payload SHALL NOT carry full SMS body text, call transcript text, provider raw payloads, or contact details. `body_preview` MAY carry an operator-safe preview or summary and MAY be null.
|
|
43
|
+
|
|
44
|
+
## 5. Producer responsibilities
|
|
45
|
+
|
|
46
|
+
Platform SHALL emit `platform.comms.inbound` only after resolving the communication to a canonical Person and determining that Sales should receive the cadence-reset signal.
|
|
47
|
+
|
|
48
|
+
Platform SHALL keep raw provider payloads, full body text, transcripts, and contact details in Platform-owned storage, not on the event payload.
|
|
49
|
+
|
|
50
|
+
Platform SHALL set the event subject to `person_id`, set actor to `system:platform`, and publish with `schema_version=1`.
|
|
51
|
+
|
|
52
|
+
Platform SHALL set payload `tenant_id` from dispatcher runtime context, not from untrusted provider input.
|
|
53
|
+
|
|
54
|
+
Platform SHOULD publish inside the same transaction that records the normalized communication when such storage exists at the call site. If the ingress path has no domain write in the current implementation phase, the publish may run as its own dispatcher insert while preserving the same payload validation and idempotency checks available to the caller.
|
|
55
|
+
|
|
56
|
+
## 6. Consumer responsibilities
|
|
57
|
+
|
|
58
|
+
Sales consumes `platform.comms.inbound` as a cadence-reset signal. Sales SHALL NOT treat the event as the canonical source of full message content, call transcript, phone number, or communication-provider details beyond the safe metadata explicitly present in the payload.
|
|
59
|
+
|
|
60
|
+
Consumers SHALL deduplicate by `(consumer, event_id)` through the dispatcher SDK.
|
|
61
|
+
|
|
62
|
+
Consumers SHALL treat `communication_id` and `provider_message_id` as opaque identifiers.
|
|
63
|
+
|
|
64
|
+
Platform warehouse ingests the event for cross-domain communication and cadence analytics.
|
|
65
|
+
|
|
66
|
+
## 7. Security and privacy
|
|
67
|
+
|
|
68
|
+
This event deliberately minimizes PII. It carries Person and organization identifiers, channel, timing, provider name, provider message identifier, routing metadata, and an optional safe preview. It MUST NOT carry raw provider payloads, phone numbers, SMS bodies, call audio, or transcripts.
|
|
69
|
+
|
|
70
|
+
Consumers MAY log `event_id`, `person_id`, and `communication_id`. Consumers SHOULD avoid long-lived logging of `body_preview` unless there is a clear operator need.
|
|
71
|
+
|
|
72
|
+
## 8. Versioning
|
|
73
|
+
|
|
74
|
+
This contract follows the additive-discipline pattern from the Event Envelope Contract. New optional fields and new safe-to-ignore enum-like values may land as v1.x changes. Removing fields, renaming fields, narrowing accepted values, or adding content-bearing PII requires a major version bump and a deprecation window.
|
|
75
|
+
|
|
76
|
+
Per-event `schema_version` is scoped to `platform.comms.inbound`.
|
|
77
|
+
|
|
78
|
+
## 9. Trigger to revisit
|
|
79
|
+
|
|
80
|
+
Revisit this contract if Sales needs content-bearing message detail on the event, if another domain needs to consume inbound comms for load-bearing workflows, if provider ingress cannot reliably resolve to Person before event emission, or if a future channel cannot be represented additively by the v1 payload.
|
|
81
|
+
|
|
82
|
+
## 10. Change log
|
|
83
|
+
|
|
84
|
+
- **v1.0.0** (2026-05-17): Initial release. Adds `platform.comms.inbound` for inbound SMS and voice-call cadence reset signals per `2026-05-17-platform-cadence-inbound-comms-position`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/platform-comms/schema/payloads/platform.comms.inbound-v1.json",
|
|
4
|
+
"title": "platform.comms.inbound payload v1",
|
|
5
|
+
"description": "Payload for the platform.comms.inbound event_type at schema_version 1. Platform emits this after inbound SMS or inbound voice-call provider ingress resolves to a canonical Person, passes tenant, consent, opt-out, and Guardian-aware routing checks, and routes to Sales as a cadence-reset signal. The envelope subject is the person_id and actor is system:platform. The payload intentionally excludes full SMS body text, call transcript text, raw provider payloads, and contact details; raw communications stay in Platform-owned storage.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"communication_id",
|
|
10
|
+
"person_id",
|
|
11
|
+
"tenant_id",
|
|
12
|
+
"organization_id",
|
|
13
|
+
"channel",
|
|
14
|
+
"received_at",
|
|
15
|
+
"provider",
|
|
16
|
+
"provider_message_id",
|
|
17
|
+
"routing_result",
|
|
18
|
+
"guardian_routed",
|
|
19
|
+
"body_preview"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"communication_id": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Platform-owned identifier for the normalized inbound communication. Consumers treat it as opaque and use Platform read APIs for any operator-safe detail.",
|
|
25
|
+
"minLength": 5,
|
|
26
|
+
"maxLength": 128,
|
|
27
|
+
"pattern": "^com_[A-Za-z0-9._:-]+$"
|
|
28
|
+
},
|
|
29
|
+
"person_id": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Canonical Person that the inbound communication resolved to. Mirrors the event envelope subject. per_<UUID v7 canonical> per ADR-0002.",
|
|
32
|
+
"pattern": "^per_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
33
|
+
},
|
|
34
|
+
"tenant_id": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Tenant discriminator. Mirrors the event envelope tenant_id and is set from dispatcher runtime context, not provider input.",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"maxLength": 64
|
|
39
|
+
},
|
|
40
|
+
"organization_id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Customer organization scope resolved by Platform before emission. Opaque to this contract because organization identifier shape is Platform-owned.",
|
|
43
|
+
"minLength": 1,
|
|
44
|
+
"maxLength": 128
|
|
45
|
+
},
|
|
46
|
+
"channel": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": ["sms", "voice_call"],
|
|
49
|
+
"description": "Inbound communications channel. v1 covers SMS and voice calls; future channel values require an additive contract update and consumer-safe rollout."
|
|
50
|
+
},
|
|
51
|
+
"received_at": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"format": "date-time",
|
|
54
|
+
"description": "When Platform received or accepted the provider signal. UTC ISO 8601."
|
|
55
|
+
},
|
|
56
|
+
"provider": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Provider identifier, for example twilio. Human-readable and safe for logs.",
|
|
59
|
+
"minLength": 1,
|
|
60
|
+
"maxLength": 100
|
|
61
|
+
},
|
|
62
|
+
"provider_message_id": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Opaque provider-side message or call identifier. Consumers do not parse it.",
|
|
65
|
+
"minLength": 1,
|
|
66
|
+
"maxLength": 200
|
|
67
|
+
},
|
|
68
|
+
"routing_result": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"enum": ["sales"],
|
|
71
|
+
"description": "The Platform routing decision that made this a Sales cadence-reset signal. Non-Sales inbound communications do not emit this event at v1."
|
|
72
|
+
},
|
|
73
|
+
"guardian_routed": {
|
|
74
|
+
"type": "boolean",
|
|
75
|
+
"description": "True when the inbound communication was resolved through a Guardian-aware route for a minor Person."
|
|
76
|
+
},
|
|
77
|
+
"body_preview": {
|
|
78
|
+
"type": ["string", "null"],
|
|
79
|
+
"description": "Optional operator-safe preview or summary. Must not contain the full SMS body, full call transcript, raw provider payload, or contact details.",
|
|
80
|
+
"maxLength": 280
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Platform Geography Snapshot Contract
|
|
2
|
+
|
|
3
|
+
**Status:** v0.1.0
|
|
4
|
+
**Date:** 2026-05-16
|
|
5
|
+
**Owner:** Platform (identity service, envelope steward)
|
|
6
|
+
**Consumers:** Sales, Delivery (reference-holder and optional consumer)
|
|
7
|
+
**Related ADRs:** ADR-0001 (tenant_id), ADR-0003 (canonical entity discipline), ADR-0005 (event envelope, pending), ADR-0013 (Platform-owned canonical geography)
|
|
8
|
+
**Sub-specs (authoritative):** n/a
|
|
9
|
+
**Validations:** n/a
|
|
10
|
+
|
|
11
|
+
## 1. Purpose and scope
|
|
12
|
+
|
|
13
|
+
This contract defines the geography-snapshot event stream that lets consuming domains keep a local mirror of the cross-domain geography catalog (Markets and Service Areas). Today the canonical reads happen against Platform's synchronous `GET /api/geography/catalog`, which works well in steady state but offers no graceful degradation when Platform is unreachable. The snapshot stream lets a consumer build a local read replica from event-sourced upserts, fall back to that replica when the live API errors, and replay history on demand.
|
|
14
|
+
|
|
15
|
+
The first consumer is Sales, which caches the snapshot in its `platform_location_snapshot` Postgres table (per ADR-0001 phase 1 slice 1.4) and uses the cache as a fallback in its geographies resolver. Other domains (Delivery, Revenue) may register as consumers in later minor versions if a similar resilience need emerges; the contract is shaped for many consumers, not Sales-specifically.
|
|
16
|
+
|
|
17
|
+
In scope: event names and payload shapes for Markets and Service Areas, producer/consumer responsibilities, versioning policy. Out of scope at v0.1: Lesson Sites (Delivery-owned, not consumed by Sales today; add in v0.2 when a consumer requests them), Markets-to-Service-Area cross-reference event sequencing (the event payloads carry the parent reference inline, sequencing is best-effort), the synchronous catalog API itself (this contract is the eventually-consistent companion to that API, not a replacement).
|
|
18
|
+
|
|
19
|
+
The geography ownership boundary is settled per ADR-0013: Platform owns canonical Market identity and canonical Service Area identity. Delivery owns lesson sites, lesson-site config, coverage, assignment, scheduling, and other delivery facts that reference `service_area_id`. This contract codifies the producer split that boundary implies: Platform produces all v0.1 `geography.market.*` and `geography.service-area.*` events, and Delivery is a reference-holder and optional consumer rather than a co-producer.
|
|
20
|
+
|
|
21
|
+
## 2. Normative language
|
|
22
|
+
|
|
23
|
+
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted per RFC 2119.
|
|
24
|
+
|
|
25
|
+
## 3. Terminology
|
|
26
|
+
|
|
27
|
+
- **Market.** A canonical geography record at the Operations base level (Oahu and Dallas today). Platform-owned canonical entity. Identifier prefix reserved per ADR-0002.
|
|
28
|
+
- **Service Area.** A Platform-owned routing region within a Market. Service Areas point at Markets via `market_id`; Delivery-owned operational rows may reference `service_area_id`.
|
|
29
|
+
- **Lesson Site.** A Delivery-owned physical or virtual location within a Service Area. Not in scope at v0.1.
|
|
30
|
+
- **Snapshot.** A consumer's local mirror of the geography catalog populated from events; not the canonical store.
|
|
31
|
+
- **Producer.** The domain authoritative for a given record type and responsible for emitting that record's lifecycle events.
|
|
32
|
+
- **Envelope.** The Platform-stewarded event envelope (ADR-0005, pending) that wraps every payload defined here.
|
|
33
|
+
|
|
34
|
+
## 4. Surface
|
|
35
|
+
|
|
36
|
+
### 4.1 Event types
|
|
37
|
+
|
|
38
|
+
Four event types compose v0.1, two per entity:
|
|
39
|
+
|
|
40
|
+
- `geography.market.upserted` (Platform produces): emitted on Market mint or any subsequent change to a canonical field. Payload §4.2.
|
|
41
|
+
- `geography.market.archived` (Platform produces): emitted when a Market transitions to `archived` (soft delete). Payload §4.3.
|
|
42
|
+
- `geography.service-area.upserted` (Platform produces): emitted on Service Area mint or any subsequent change to a canonical field. Payload §4.4.
|
|
43
|
+
- `geography.service-area.archived` (Platform produces): emitted when a Service Area transitions to `archived`. Payload §4.5.
|
|
44
|
+
|
|
45
|
+
Consumers SHALL treat unknown event types as safe-to-ignore.
|
|
46
|
+
|
|
47
|
+
### 4.2 `geography.market.upserted` payload
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"market_id": "mkt_018f4d5a-1234-7abc-8def-0123456789ab",
|
|
52
|
+
"tenant_id": "tnt_default",
|
|
53
|
+
"slug": "oahu",
|
|
54
|
+
"name": "Oahu",
|
|
55
|
+
"status": "active",
|
|
56
|
+
"metadata": {},
|
|
57
|
+
"created_at": "2026-05-16T15:00:00Z",
|
|
58
|
+
"updated_at": "2026-05-16T15:00:00Z"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Field semantics:
|
|
63
|
+
|
|
64
|
+
| Field | Type | Required | Notes |
|
|
65
|
+
|--------------|------------|----------|------------------------------------------------------------------------------------------------|
|
|
66
|
+
| `market_id` | string | yes | Platform-minted, `mkt_<UUID v7>` per ADR-0002. Immutable. |
|
|
67
|
+
| `tenant_id` | string | yes | Tenant scope per ADR-0001. Singleton in v0.1; reserved for multi-tenant without a major bump. |
|
|
68
|
+
| `slug` | string | yes | Stable lookup key (kebab-case). Unique per tenant. |
|
|
69
|
+
| `name` | string | yes | Operator-facing label. |
|
|
70
|
+
| `status` | string | yes | One of `active`, `archived`. New values SHALL be appended in minor versions. |
|
|
71
|
+
| `metadata` | object | optional | Free-form Platform-internal metadata. Non-contractual; consumers MUST NOT build business behavior on its keys. |
|
|
72
|
+
| `created_at` | RFC 3339 | yes | UTC. Immutable after mint. |
|
|
73
|
+
| `updated_at` | RFC 3339 | yes | UTC. Bumped on every emit. |
|
|
74
|
+
|
|
75
|
+
### 4.3 `geography.market.archived` payload
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"market_id": "mkt_018f4d5a-1234-7abc-8def-0123456789ab",
|
|
80
|
+
"tenant_id": "tnt_default",
|
|
81
|
+
"archived_at": "2026-05-16T15:00:00Z",
|
|
82
|
+
"reason": "operator_action"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Field | Type | Required | Notes |
|
|
87
|
+
|---------------|----------|----------|--------------------------------------------------------------------------------------|
|
|
88
|
+
| `market_id` | string | yes | Identifies the Market being archived. The `upserted` row stays present. |
|
|
89
|
+
| `tenant_id` | string | yes | Tenant scope. |
|
|
90
|
+
| `archived_at` | RFC 3339 | yes | UTC. |
|
|
91
|
+
| `reason` | string | optional | Free-form short reason code. Consumer behavior SHALL NOT depend on a specific value. |
|
|
92
|
+
|
|
93
|
+
### 4.4 `geography.service-area.upserted` payload
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"service_area_id": "sva_018f4d5a-1234-7abc-8def-0123456789ab",
|
|
98
|
+
"tenant_id": "tnt_default",
|
|
99
|
+
"market_id": "mkt_018f4d5a-1234-7abc-8def-0123456789ab",
|
|
100
|
+
"slug": "honolulu",
|
|
101
|
+
"name": "Honolulu",
|
|
102
|
+
"status": "active",
|
|
103
|
+
"metadata": {},
|
|
104
|
+
"created_at": "2026-05-16T15:00:00Z",
|
|
105
|
+
"updated_at": "2026-05-16T15:00:00Z"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Notes |
|
|
110
|
+
|-------------------|----------|----------|--------------------------------------------------------------------------------------------|
|
|
111
|
+
| `service_area_id` | string | yes | Platform-minted, `sva_<UUID v7>` per ADR-0002 (reserved; promote on first use). |
|
|
112
|
+
| `tenant_id` | string | yes | Tenant scope. |
|
|
113
|
+
| `market_id` | string | yes | Parent Market reference. Forward reference; Platform's `market.upserted` for this id MAY arrive after this event under non-FIFO transports. |
|
|
114
|
+
| `slug` | string | yes | Stable lookup key. |
|
|
115
|
+
| `name` | string | yes | Operator-facing label. |
|
|
116
|
+
| `status` | string | yes | One of `active`, `archived`. |
|
|
117
|
+
| `metadata` | object | optional | Free-form Platform-internal metadata. Non-contractual; consumers MUST NOT build business behavior on its keys. |
|
|
118
|
+
| `created_at` | RFC 3339 | yes | UTC. Immutable after mint. |
|
|
119
|
+
| `updated_at` | RFC 3339 | yes | UTC. Bumped on every emit. |
|
|
120
|
+
|
|
121
|
+
### 4.5 `geography.service-area.archived` payload
|
|
122
|
+
|
|
123
|
+
Same shape as §4.3 with `service_area_id` in place of `market_id`.
|
|
124
|
+
|
|
125
|
+
### 4.6 Envelope
|
|
126
|
+
|
|
127
|
+
Every event payload rides the Platform-stewarded event envelope (ADR-0005, pending). The envelope carries `event_type`, `tenant_id`, `correlation_id`, `actor`, `subject`, `schema_version`, and the payload. This contract's payloads occupy the envelope's payload slot; envelope rules (idempotency keys, redelivery, ordering) live in the envelope contract.
|
|
128
|
+
|
|
129
|
+
`schema_version` SHALL be an integer payload schema version registered in `contracts/event-types-registry.json`. The four v0.1 event payloads use `schema_version: 1`. Contract README versions use semantic versions independently from payload schema versions. Consumers SHALL treat unknown future integer schema versions as incompatible until their payload schemas are registered and mirrored locally.
|
|
130
|
+
|
|
131
|
+
## 5. Versioning policy
|
|
132
|
+
|
|
133
|
+
### 5.1 Semantic versioning
|
|
134
|
+
|
|
135
|
+
- **Patch** (0.1.0 → 0.1.1): editorial clarifications, typo fixes. No behavioral change.
|
|
136
|
+
- **Minor** (0.1.x → 0.2.0): additive changes. New optional fields, new event types (e.g., Lesson Site events when needed), appended `status` enum values. Consumers on older minors continue to work.
|
|
137
|
+
- **Major** (0.x.y → 1.0.0): breaking changes. Removal, type change, narrowing enum values, breaking payload changes. v1.0.0 is the milestone where Lesson Sites are likely to join.
|
|
138
|
+
|
|
139
|
+
### 5.2 Deprecation policy
|
|
140
|
+
|
|
141
|
+
On major-version publication, the previous major enters a two-week deprecation window per the org-standard contract-currency rule. Consumers running against deprecated versions after the window contribute to drift.
|
|
142
|
+
|
|
143
|
+
### 5.3 Additive discipline within a major
|
|
144
|
+
|
|
145
|
+
New fields MUST default to null or a backwards-compatible value. New enum values MUST be appended; consumers MUST treat unknown enum values as safe-to-ignore.
|
|
146
|
+
|
|
147
|
+
## 6. Consumer responsibilities
|
|
148
|
+
|
|
149
|
+
### 6.1 Subscribe at-least-once
|
|
150
|
+
|
|
151
|
+
Consumers SHALL subscribe via the standard envelope rail with at-least-once delivery semantics. Idempotency keys on the envelope let the consumer detect redelivery; the snapshot upsert SHALL be idempotent on `(tenant_id, market_id)` or `(tenant_id, service_area_id)`.
|
|
152
|
+
|
|
153
|
+
### 6.2 Forward references tolerated
|
|
154
|
+
|
|
155
|
+
A `service-area.upserted` event MAY arrive before the `market.upserted` for its parent. Consumers SHALL tolerate the forward reference (e.g., by storing the parent id and surfacing label-only rendering until the parent arrives), not by failing the upsert. A reconciliation pass (§6.4) is the recovery path for permanently-missing parents.
|
|
156
|
+
|
|
157
|
+
### 6.3 Resolver fallback
|
|
158
|
+
|
|
159
|
+
Consumers that maintain a snapshot SHOULD fall back to snapshot reads when the live geography catalog is unreachable. The fallback is best-effort and SHALL be labeled in the consumer's UI or audit log as "from cache, last synced <timestamp>" so operators can distinguish live data from stale.
|
|
160
|
+
|
|
161
|
+
### 6.4 Periodic reconciliation
|
|
162
|
+
|
|
163
|
+
Consumers SHOULD run a daily reconciliation pass against the live catalog API: any local snapshot row not present in the live catalog is a candidate to mark `archived` locally; any live row not in the snapshot is a candidate for resync. Reconciliation is a backstop for missed events, not a substitute for the event stream.
|
|
164
|
+
|
|
165
|
+
### 6.5 Tenant scoping
|
|
166
|
+
|
|
167
|
+
Every snapshot read and write SHALL carry `tenant_id` from auth context. Cross-tenant reads are prohibited.
|
|
168
|
+
|
|
169
|
+
### 6.6 Reference by ID
|
|
170
|
+
|
|
171
|
+
Consumers MUST store geography references as `market_id` / `service_area_id` values, not as denormalized copies of names. Display-layer caching via the snapshot table is permitted; long-term references to slugs or names are not.
|
|
172
|
+
|
|
173
|
+
## 7. Producer responsibilities
|
|
174
|
+
|
|
175
|
+
### 7.1 Event fidelity
|
|
176
|
+
|
|
177
|
+
Producers SHALL emit the appropriate event (`upserted` or `archived`) for every mutation of a canonical field before the corresponding API mutation response returns. Consumers MAY assume that a successful API response implies the corresponding event has been published with at-least-once delivery.
|
|
178
|
+
|
|
179
|
+
### 7.2 Uniqueness
|
|
180
|
+
|
|
181
|
+
Producers SHALL ensure Platform-minted `market_id` and `service_area_id` uniqueness within a tenant. Collisions are a critical integrity bug.
|
|
182
|
+
|
|
183
|
+
### 7.3 Parent existence at archive time
|
|
184
|
+
|
|
185
|
+
Producers SHOULD NOT archive a Market while live Service Areas reference it; archive child Service Areas first, then the Market. The contract does not enforce this; it surfaces in operator workflows.
|
|
186
|
+
|
|
187
|
+
### 7.4 Initial bootstrap
|
|
188
|
+
|
|
189
|
+
When a new consumer subscribes, producers SHALL offer a one-shot replay of all currently `active` records via the envelope contract's replay surface (or an out-of-band initial fetch against the synchronous catalog API). v0.1 leaves the replay mechanism to the envelope contract; a consumer integrating without replay support uses the catalog API for a one-time initial fill.
|
|
190
|
+
|
|
191
|
+
## 8. Security and privacy
|
|
192
|
+
|
|
193
|
+
Geography records carry no PII. Market and Service Area names are operator-facing labels (Oahu, Honolulu) and are not user-derived. Consumers MAY log `market_id`, `service_area_id`, `slug`, and `name` freely. `metadata` payloads MAY contain producer-internal operational state; consumers SHALL NOT depend on metadata field shapes across version bumps.
|
|
194
|
+
|
|
195
|
+
Access to the event stream is gated by tenant auth context per the envelope contract. Cross-tenant reads are prohibited.
|
|
196
|
+
|
|
197
|
+
## 9. Future work
|
|
198
|
+
|
|
199
|
+
- **v0.2 Lesson Site events.** Add `geography.lesson-site.upserted` and `geography.lesson-site.archived`, produced by Delivery, when a consumer requests them. Sales does not consume Lesson Sites today; Delivery uses them internally.
|
|
200
|
+
- **v1.0.0.** Promote to v1 when the contract has shipped with at least one production consumer for two stable weeks and the Markets canonical-entity migration is past its operational checkpoint.
|
|
201
|
+
- **Markets-internal canonical-entity surface.** Platform's `mkt_` prefix is reserved here; final shape (display fields, lifecycle states beyond `active`/`archived`, Org parentage) lands as Platform's Markets migration concretizes. Contract additive-discipline applies.
|
|
202
|
+
|
|
203
|
+
## 10. Change log
|
|
204
|
+
|
|
205
|
+
- **v0.1.0** (2026-05-16): Initial draft. Proposed by Sales in `2026-05-16-sales-platform-geography-snapshot-contract-proposal`, then amended after Platform and Delivery responses on the same thread. Platform owns all four Market and Service Area v0.1 emitters per ADR-0013. Delivery is a reference-holder and optional consumer, not a Service Area lifecycle producer. Lesson Sites deferred to v0.2.
|
package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json",
|
|
4
|
+
"title": "geography.market.archived payload v1",
|
|
5
|
+
"description": "Payload for Platform-produced geography.market.archived at schema_version 1. Emitted when a canonical Market transitions to archived. The envelope subject is market_id and actor is system:platform.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"market_id",
|
|
10
|
+
"tenant_id",
|
|
11
|
+
"archived_at"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"market_id": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Platform-minted Market id. mkt_<UUID v7 canonical> per ADR-0002.",
|
|
17
|
+
"pattern": "^mkt_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
18
|
+
},
|
|
19
|
+
"tenant_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Tenant discriminator. Mirrors the event envelope tenant_id.",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 64
|
|
24
|
+
},
|
|
25
|
+
"archived_at": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"format": "date-time"
|
|
28
|
+
},
|
|
29
|
+
"reason": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Optional short reason code. Consumer behavior SHALL NOT depend on a specific value.",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"maxLength": 100
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json",
|
|
4
|
+
"title": "geography.market.upserted payload v1",
|
|
5
|
+
"description": "Payload for Platform-produced geography.market.upserted at schema_version 1. Emitted when a canonical Market is minted or when a canonical Market field changes without transitioning to archived. The envelope subject is market_id and actor is system:platform.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"market_id",
|
|
10
|
+
"tenant_id",
|
|
11
|
+
"slug",
|
|
12
|
+
"name",
|
|
13
|
+
"status",
|
|
14
|
+
"created_at",
|
|
15
|
+
"updated_at"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"market_id": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Platform-minted Market id. mkt_<UUID v7 canonical> per ADR-0002.",
|
|
21
|
+
"pattern": "^mkt_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
22
|
+
},
|
|
23
|
+
"tenant_id": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Tenant discriminator. Mirrors the event envelope tenant_id.",
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"maxLength": 64
|
|
28
|
+
},
|
|
29
|
+
"slug": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Stable kebab-case lookup key.",
|
|
32
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
33
|
+
"maxLength": 100
|
|
34
|
+
},
|
|
35
|
+
"name": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Operator-facing Market label.",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"maxLength": 200
|
|
40
|
+
},
|
|
41
|
+
"status": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["active", "archived"]
|
|
44
|
+
},
|
|
45
|
+
"metadata": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"description": "Optional, non-contractual Platform metadata. Consumers MUST NOT build business behavior on these keys.",
|
|
48
|
+
"additionalProperties": true
|
|
49
|
+
},
|
|
50
|
+
"created_at": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"format": "date-time"
|
|
53
|
+
},
|
|
54
|
+
"updated_at": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"format": "date-time"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json",
|
|
4
|
+
"title": "geography.service-area.archived payload v1",
|
|
5
|
+
"description": "Payload for Platform-produced geography.service-area.archived at schema_version 1. Emitted when a canonical Service Area transitions to archived. The envelope subject is service_area_id and actor is system:platform.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"service_area_id",
|
|
10
|
+
"tenant_id",
|
|
11
|
+
"archived_at"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"service_area_id": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Platform-minted Service Area id. sva_<UUID v7 canonical> per ADR-0002.",
|
|
17
|
+
"pattern": "^sva_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
18
|
+
},
|
|
19
|
+
"tenant_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Tenant discriminator. Mirrors the event envelope tenant_id.",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 64
|
|
24
|
+
},
|
|
25
|
+
"archived_at": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"format": "date-time"
|
|
28
|
+
},
|
|
29
|
+
"reason": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Optional short reason code. Consumer behavior SHALL NOT depend on a specific value.",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"maxLength": 100
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json",
|
|
4
|
+
"title": "geography.service-area.upserted payload v1",
|
|
5
|
+
"description": "Payload for Platform-produced geography.service-area.upserted at schema_version 1. Emitted when a canonical Service Area is minted or when a canonical Service Area field changes without transitioning to archived. The envelope subject is service_area_id and actor is system:platform.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"service_area_id",
|
|
10
|
+
"tenant_id",
|
|
11
|
+
"market_id",
|
|
12
|
+
"slug",
|
|
13
|
+
"name",
|
|
14
|
+
"status",
|
|
15
|
+
"created_at",
|
|
16
|
+
"updated_at"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"service_area_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Platform-minted Service Area id. sva_<UUID v7 canonical> per ADR-0002.",
|
|
22
|
+
"pattern": "^sva_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
23
|
+
},
|
|
24
|
+
"tenant_id": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Tenant discriminator. Mirrors the event envelope tenant_id.",
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"maxLength": 64
|
|
29
|
+
},
|
|
30
|
+
"market_id": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Parent Market id. mkt_<UUID v7 canonical> per ADR-0002.",
|
|
33
|
+
"pattern": "^mkt_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
34
|
+
},
|
|
35
|
+
"slug": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Stable kebab-case lookup key within the parent Market.",
|
|
38
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
39
|
+
"maxLength": 100
|
|
40
|
+
},
|
|
41
|
+
"name": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Operator-facing Service Area label.",
|
|
44
|
+
"minLength": 1,
|
|
45
|
+
"maxLength": 200
|
|
46
|
+
},
|
|
47
|
+
"status": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["active", "archived"]
|
|
50
|
+
},
|
|
51
|
+
"metadata": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"description": "Optional, non-contractual Platform metadata. Consumers MUST NOT build business behavior on these keys.",
|
|
54
|
+
"additionalProperties": true
|
|
55
|
+
},
|
|
56
|
+
"created_at": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"format": "date-time"
|
|
59
|
+
},
|
|
60
|
+
"updated_at": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"format": "date-time"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|