@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
package/README.md
CHANGED
|
@@ -190,6 +190,7 @@ and the first docs set landed in this slice:
|
|
|
190
190
|
- `docs/dispatcher/phase-3-observability.md`
|
|
191
191
|
- `docs/dispatcher/coaching-cutover-guide.md`
|
|
192
192
|
- `docs/dispatcher/reference-implementations.md`
|
|
193
|
+
- `docs/dispatcher/event-vs-external-actions.md`
|
|
193
194
|
|
|
194
195
|
## Consumer fallback
|
|
195
196
|
|
|
@@ -213,7 +214,9 @@ on the same rail.
|
|
|
213
214
|
publish, consume, dedup hit, dead-letter, end-to-end latency, and
|
|
214
215
|
handler latency.
|
|
215
216
|
- Enablement docs: Phase 3 metrics wiring, Coaching cutover guide,
|
|
216
|
-
Revenue emit reference,
|
|
217
|
+
Revenue emit reference, Coaching projection subscriber reference,
|
|
218
|
+
dedup escalation for cutover windows, and dispatcher-vs-external-actions
|
|
219
|
+
guidance.
|
|
217
220
|
|
|
218
221
|
Per-event-type payload schemas still land with the owning contract as
|
|
219
222
|
each event_type acquires a binding consumer.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Contracts
|
|
2
|
+
|
|
3
|
+
Cross-domain contracts that bind Platform's rails to the four user-facing domains. Identity, the event envelope, the credit reservation lock. Each contract lives in its own directory with a `README.md` (the contract proper), validation notes, and any sub-specs.
|
|
4
|
+
|
|
5
|
+
## What lives here
|
|
6
|
+
|
|
7
|
+
`identity/` — Person identifiers, tenancy, canonical Person record shape, resolution and merge semantics. Owned by Platform's identity service. Consumed by all four user-facing domains.
|
|
8
|
+
|
|
9
|
+
`event-envelope/` — The shared envelope every cross-domain event rides on. Seven required fields plus three optional. JSON Schema validation in CI.
|
|
10
|
+
|
|
11
|
+
`credit-reservation-lock/` — The five-state lock state machine that binds Delivery's execution side to Revenue's funding side.
|
|
12
|
+
|
|
13
|
+
`platform-comms/` — Platform-owned inbound communications event surface for Sales cadence-reset signals and warehouse ingestion.
|
|
14
|
+
|
|
15
|
+
`coaching-confirmation/` - Sales and Coaching confirmation-handshake event surface for Sales cadence Stage 3b.
|
|
16
|
+
|
|
17
|
+
## How to propose a change
|
|
18
|
+
|
|
19
|
+
A contract change starts as a memo in `memos/<year>/`, not as a contracts PR. The memo argues for the change, names the consumers affected, and proposes a deprecation window. Once consensus is reached, the change is implemented in this directory in a single PR that also touches the relevant ADR and any affected memos. That keeps the rationale, the decision, and the contract change atomically reviewable.
|
|
20
|
+
|
|
21
|
+
Org standard: no consumer running against a deprecated contract version for more than two weeks after a new version is published. Plan the deprecation window before the new version ships.
|
|
22
|
+
|
|
23
|
+
## Versioning
|
|
24
|
+
|
|
25
|
+
Each contract carries its own version in its `README.md` header (`**Status:** v1.0.0`). Bump the version when the contract changes. Major version bumps require a memo. Minor version bumps require an ADR if the change has cross-domain implications, otherwise a contracts PR is sufficient.
|
|
26
|
+
|
|
27
|
+
## Related
|
|
28
|
+
|
|
29
|
+
ADRs: `../adrs/`. Each contract's `README.md` lists the ADRs that govern it.
|
|
30
|
+
Memos: `../memos/<year>/`. Memos that proposed or amended a contract should be tagged `[contracts, <contract-name>]` so the indexer can cross-reference.
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Coach Availability Contract
|
|
2
|
+
|
|
3
|
+
**Status:** v1.1.0
|
|
4
|
+
**Date:** 2026-05-16
|
|
5
|
+
**Owner:** Coaching (availability service; drafted by Delivery pre-stand-up per ADR-0008 action item 4, ownership transfers to Coaching on stand-up)
|
|
6
|
+
**Consumers:** Sales (offer construction at the close), Delivery (coach-assignment at scheduling and lesson time)
|
|
7
|
+
**Related ADRs:** ADR-0001 (tenant ID shape), ADR-0003 (Person canonical with role records, amended 2026-05-01 to relocate Coach to Coaching), ADR-0005 (event envelope), ADR-0006 (credit reservation lock state machine), ADR-0008 (Coaching as sixth domain)
|
|
8
|
+
**Sub-specs (authoritative):** n/a for v1.0.0; the README carries the full surface
|
|
9
|
+
**Validations:** `validation/sales-offer-construction.md`, `validation/delivery-assignment.md`
|
|
10
|
+
|
|
11
|
+
## 1. Purpose and scope
|
|
12
|
+
|
|
13
|
+
Coach availability is the shared concern Coaching exists to own. Sales reads availability and eligibility at offer-construction time to know whether a slot can be promised before a credit reservation is requested. Delivery reads the same surface at assignment time to attach a coach to a confirmed lesson. Before this contract, both consumers reached into Delivery's coach surface; per ADR-0008, that ownership moved to Coaching and the two consumers now hit a single producer.
|
|
14
|
+
|
|
15
|
+
This contract specifies the read surface, the lock-aware availability projection that backs the read, the subscriber relationship to Revenue's credit-reservation-lock event surface, and the consumer and producer responsibilities the two sides take on. The read surface is read-only in v1.0.0; capacity adjustments, certification updates, and any other write paths into Coaching are deliberately out of scope and named in §9.
|
|
16
|
+
|
|
17
|
+
Out of scope for v1.0.0: Coach assignment to a specific lesson (Delivery owns assignment per ADR-0008; `coach.assigned` is a Delivery event), Person identity (Platform owns Person per ADR-0003), the credit reservation lock state machine (Delivery and Revenue jointly per ADR-0006; Coaching is a subscriber, not a writer), pricing and program rules (out of Coaching's scope), payroll-adjacent signals (out of Coaching's scope). Capacity adjustments, certification publication, and any v1.x extensions are scoped in §9.
|
|
18
|
+
|
|
19
|
+
This document is the contract. Two validation files document each consumer's specific use of the surface so the producer can verify both reads are first-class. Sub-specs are not required for v1.0.0; the surface is small enough to fit here.
|
|
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
|
+
- **Coach.** A Person with a `coa_` role record at Coaching. Per the amended ADR-0003. One Coach per Person per Organization.
|
|
28
|
+
- **Window.** A half-open time interval `[start, end)`, expressed in ISO 8601 with timezone, scoped to a single Organization and Service Area. Windows are the unit of subtraction and unsubtraction in the lock-aware projection.
|
|
29
|
+
- **Service Area.** A geographic boundary owned by Delivery. Coaches are eligible for windows within service areas they cover; the coverage relationship is a Coaching-side fact (a multi-link on Coach Profile per ADR-0007) and the Service Area record itself remains Delivery's.
|
|
30
|
+
- **Slot.** The pair `(coach_id, window)`. The atomic unit the projection tracks.
|
|
31
|
+
- **Capacity.** The maximum number of concurrent active assignments a Coach may carry, scoped to a coverage type if applicable. A Coaching-side fact.
|
|
32
|
+
- **Eligibility.** The boolean predicate "may this Coach take this lesson," computed over capacity, service area, certification, and conflict-window inputs against Coach state at the read instant.
|
|
33
|
+
- **Availability.** The boolean predicate "is this Coach's slot free for the requested window," computed over the lock-aware projection at the read instant. Availability is necessary but not sufficient for eligibility; eligibility composes availability with the other predicates.
|
|
34
|
+
- **Lock-aware projection.** Coaching's per-window state derived from the credit-reservation-lock event stream. Each `(coach_id, window)` slot has a state in `{free, reserved, locked}`. Reserved and locked subtract from availability; free does not.
|
|
35
|
+
- **Authoritative state.** Revenue's lock-state-machine record per ADR-0006 and the credit-reservation-lock contract. The projection is eventually-consistent against authoritative state.
|
|
36
|
+
- **Freshness SLO.** The maximum lag between Revenue emitting a `credit.*` event and Coaching's projection reflecting it. Bounded by the event envelope SLO; Coaching does not invent a stricter bound in v1.0.0.
|
|
37
|
+
|
|
38
|
+
## 4. Surface
|
|
39
|
+
|
|
40
|
+
The contract defines two read endpoints, the lock-aware projection that backs them, and the event subscription that maintains the projection.
|
|
41
|
+
|
|
42
|
+
### 4.1 Availability read
|
|
43
|
+
|
|
44
|
+
`GET /coaching/v1/availability`
|
|
45
|
+
|
|
46
|
+
Query parameters:
|
|
47
|
+
|
|
48
|
+
- `organization_id` (required, string). Tenancy scope.
|
|
49
|
+
- `service_area_id` (required, string). The Delivery-owned Service Area to scope the read to. Coaches outside the service area's coverage SHALL NOT appear in the response.
|
|
50
|
+
- `window_start` (required, ISO 8601 with timezone). Start of the read window.
|
|
51
|
+
- `window_end` (required, ISO 8601 with timezone). End of the read window. MUST be later than `window_start`. Maximum span 90 days; longer reads SHALL return HTTP 400.
|
|
52
|
+
- `coach_id` (optional, string). If present, scope to a single Coach. If omitted, return all coaches in the service area's coverage.
|
|
53
|
+
|
|
54
|
+
Response (HTTP 200):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{
|
|
58
|
+
"as_of": "2026-05-01T18:42:11.123Z",
|
|
59
|
+
"organization_id": "org_...",
|
|
60
|
+
"service_area_id": "sva_...",
|
|
61
|
+
"window": { "start": "...", "end": "..." },
|
|
62
|
+
"slots": [
|
|
63
|
+
{
|
|
64
|
+
"coach_id": "coa_...",
|
|
65
|
+
"window": { "start": "...", "end": "..." },
|
|
66
|
+
"state": "free" | "reserved" | "locked",
|
|
67
|
+
"reservation_id": "crr_..." | null,
|
|
68
|
+
"lock_id": "lck_..." | null
|
|
69
|
+
},
|
|
70
|
+
...
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`as_of` is the projection's read instant per the producer's clock. Consumers SHOULD log the value when surfacing availability to operators so a stale-read root-cause is traceable.
|
|
76
|
+
|
|
77
|
+
`reservation_id` is populated when `state` is `reserved` and references the originating `crr_` from the credit-reservation-lock contract. `lock_id` is populated when `state` is `locked` and references the corresponding lock record. Both are null when `state` is `free`. Consumers MUST NOT attempt to read or write into Revenue's surface using these IDs without going through the lock contract; the IDs are surfaced for traceability and operator-tooling only.
|
|
78
|
+
|
|
79
|
+
The endpoint returns slots at the granularity Coaching maintains internally (typically 30-minute increments aligned to organization scheduling); the contract does not pin the granularity in v1.0.0 and consumers MUST NOT depend on a specific cadence. Consumers that need a different cadence SHALL aggregate or split client-side.
|
|
80
|
+
|
|
81
|
+
### 4.2 Eligibility projection
|
|
82
|
+
|
|
83
|
+
The eligibility projection composes availability with capacity, service area coverage, certification, and conflict-window predicates. Three endpoint shapes are exposed because the consumers call at different points in the lifecycle.
|
|
84
|
+
|
|
85
|
+
#### 4.2.1 Eligibility by lesson ID
|
|
86
|
+
|
|
87
|
+
`GET /coaching/v1/eligibility/by-lesson`
|
|
88
|
+
|
|
89
|
+
Query parameters:
|
|
90
|
+
|
|
91
|
+
- `organization_id` (required).
|
|
92
|
+
- `lesson_id` (required, string). The Delivery-owned `les_` identifier. Coaching reads the lesson's window, service area, lesson type, and program-rule references from Delivery's lesson surface (the read is producer-internal; consumers do not pass these inputs).
|
|
93
|
+
|
|
94
|
+
Response (HTTP 200):
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
{
|
|
98
|
+
"as_of": "...",
|
|
99
|
+
"projection_as_of": "...",
|
|
100
|
+
"organization_id": "org_...",
|
|
101
|
+
"lesson_id": "les_...",
|
|
102
|
+
"lesson_window": { "start": "...", "end": "..." },
|
|
103
|
+
"eligible_coaches": [
|
|
104
|
+
{
|
|
105
|
+
"coach_id": "coa_...",
|
|
106
|
+
"availability_state": "free" | "reserved" | "locked",
|
|
107
|
+
"capacity_remaining": 0..N,
|
|
108
|
+
"matches_certification": true | false,
|
|
109
|
+
"covers_service_area": true | false,
|
|
110
|
+
"no_conflict_window": true | false,
|
|
111
|
+
"is_eligible": true | false
|
|
112
|
+
},
|
|
113
|
+
...
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`is_eligible` is the AND of the four predicates plus `availability_state == "free"`. The decomposed predicates are returned alongside so consumers can render explanations for ineligibility without re-implementing the rules.
|
|
119
|
+
|
|
120
|
+
This endpoint is what Delivery calls at assignment time, when a `les_` already exists.
|
|
121
|
+
|
|
122
|
+
`as_of` is the response serialization instant per the producer's clock. `projection_as_of` is Coaching's producer projection clock for the predicate inputs used to compute the eligibility answer. Consumers SHOULD persist both values with decision-time audit state. Sales and Delivery remain responsible for their own consumer-decision evidence; this producer field only proves which Coaching projection snapshot produced the answer.
|
|
123
|
+
|
|
124
|
+
#### 4.2.2 Eligibility by lesson description
|
|
125
|
+
|
|
126
|
+
`POST /coaching/v1/eligibility/by-description`
|
|
127
|
+
|
|
128
|
+
Body:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
{
|
|
132
|
+
"organization_id": "org_...",
|
|
133
|
+
"service_area_id": "sva_...",
|
|
134
|
+
"window": { "start": "...", "end": "..." },
|
|
135
|
+
"lesson_type_id": "lty_...",
|
|
136
|
+
"required_certifications": ["..."]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Response: same shape as 4.2.1 but with `lesson_id` absent and `lesson_window` set to the requested window. The response includes `projection_as_of`.
|
|
141
|
+
|
|
142
|
+
This endpoint is what Sales calls at offer construction, before any `les_` exists. It exists because the close orchestration shape (Sales constructing an offer that becomes a lesson when the operator commits) does not yet have a lesson record to query against.
|
|
143
|
+
|
|
144
|
+
#### 4.2.3 Eligibility by coach and window
|
|
145
|
+
|
|
146
|
+
`GET /coaching/v1/eligibility/by-coach-and-window`
|
|
147
|
+
|
|
148
|
+
Query parameters:
|
|
149
|
+
|
|
150
|
+
- `organization_id` (required).
|
|
151
|
+
- `coach_id` (required, string). The Coaching-owned `coa_` identifier the producer is re-checking.
|
|
152
|
+
- `service_area_id` (required, string). Same scope rule as §4.2.2.
|
|
153
|
+
- `window_start` (required, ISO 8601). Lower bound on the lesson window.
|
|
154
|
+
- `window_end` (required, ISO 8601). Upper bound on the lesson window.
|
|
155
|
+
- `lesson_type_id` (required, string).
|
|
156
|
+
- `required_certifications` (optional, repeated). Same semantics as §4.2.2's body field.
|
|
157
|
+
|
|
158
|
+
Response (HTTP 200):
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
{
|
|
162
|
+
"as_of": "...",
|
|
163
|
+
"projection_as_of": "...",
|
|
164
|
+
"organization_id": "org_...",
|
|
165
|
+
"coach_id": "coa_...",
|
|
166
|
+
"lesson_window": { "start": "...", "end": "..." },
|
|
167
|
+
"availability_state": "free" | "reserved" | "locked",
|
|
168
|
+
"capacity_remaining": 0..N,
|
|
169
|
+
"matches_certification": true | false,
|
|
170
|
+
"covers_service_area": true | false,
|
|
171
|
+
"no_conflict_window": true | false,
|
|
172
|
+
"is_eligible": true | false
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
This endpoint is what Delivery calls for the producer-side write-time re-check named in sales-scheduling-surface §7.7 when Delivery already has the selected `coach_id` and does not need the full service-area coach list. A 404 is the right response when `coach_id` does not resolve to a Coach in the named organization. A 200 with `is_eligible: false` is the right response when the Coach exists but fails the predicate set; Delivery maps that case to its own hold-create conflict path with `conflict_reason: coach_no_longer_eligible`.
|
|
177
|
+
|
|
178
|
+
The three endpoints SHALL produce the same `is_eligible` answer and the same decomposed predicates for the same effective inputs; the only difference is whether the consumer supplies a lesson id, a lesson description, or a single coach plus window. Consumers MUST NOT assume the endpoints diverge.
|
|
179
|
+
|
|
180
|
+
### 4.3 Lock-aware projection mechanics
|
|
181
|
+
|
|
182
|
+
The projection is maintained by Coaching's subscriber on the credit-reservation-lock event surface. The subscription set, subtraction rule, and freshness model are all part of this contract; the lock-state-machine itself is governed by ADR-0006 and the credit-reservation-lock contract, not here.
|
|
183
|
+
|
|
184
|
+
**Interim implementation note (v1.0.1, 2026-05-01).** The dispatcher SDK named in the event-envelope contract (`contracts/event-envelope/README.md` §3, §6, §9) does not yet exist. Until the SDK ships, Coaching satisfies the projection's observable behavior described in this section by synchronously querying Revenue's authoritative lock-state API at request time, computing the equivalent of the subtraction rule against live data per request. The projection state model in §4.3.1 through §4.3.4 below describes the SDK-readiness target shape; the interim implementation realizes the same external behavior without a maintained projection. The SDK-readiness consumer-migration window from interim sync-query to projection-based reads is a v1.x or v2 contract event, decided when the SDK consumer-readiness milestone lands per Platform's planning thread. Background and rationale: see `memos/2026/2026-05-01-platform-dispatcher-sdk-gap-and-interim-shape` and `memos/2026/2026-05-01-delivery-dispatcher-sdk-interim-confirmation`.
|
|
185
|
+
|
|
186
|
+
#### 4.3.1 Subscription set
|
|
187
|
+
|
|
188
|
+
Coaching subscribes to:
|
|
189
|
+
|
|
190
|
+
- `credit.reserved`
|
|
191
|
+
- `credit.locked`
|
|
192
|
+
- `credit.released`
|
|
193
|
+
- `credit.forfeited`
|
|
194
|
+
|
|
195
|
+
`credit.consumed` is not subscribed. Consumed transitions are terminal-past and do not bear on future-window availability; the projection ages out past windows on a clock rather than on event signal. `credit.funded` is not subscribed either; the funding sub-state is Revenue-internal and does not change whether the slot is claimed.
|
|
196
|
+
|
|
197
|
+
#### 4.3.2 Subtraction rule
|
|
198
|
+
|
|
199
|
+
`credit.reserved` SHALL subtract the slot. The slot is claimed at the moment the reservation is recorded, regardless of funding state. State transitions to `reserved` for the slot.
|
|
200
|
+
|
|
201
|
+
`credit.locked` SHALL keep the slot subtracted. State transitions to `locked` for the slot. No projection delta.
|
|
202
|
+
|
|
203
|
+
`credit.released` SHALL unsubtract the slot. State transitions to `free` for the slot. The reason code on the payload is logged but does not change the rule.
|
|
204
|
+
|
|
205
|
+
`credit.forfeited` SHALL unsubtract the slot. State transitions to `free` for the slot. Customer-side cancellation post-lock and no-show both land here per §9.7 of the credit-reservation-lock contract.
|
|
206
|
+
|
|
207
|
+
#### 4.3.3 Freshness
|
|
208
|
+
|
|
209
|
+
The projection is eventually-consistent against Revenue's lock state machine per §12.3 of the credit-reservation-lock contract. The lag between Revenue emitting a `credit.*` event and the projection reflecting it is bounded by the event envelope SLO. Consumers MUST NOT treat the projection as the authoritative lock; Revenue's API on the reservation request itself is the gate when a consumer commits.
|
|
210
|
+
|
|
211
|
+
Coaching SHALL NOT introduce a stricter freshness bound in v1.0.0. If operational evidence shows the envelope SLO is too loose for offer construction in a specific cadence (Sales' high-traffic windows), the trigger to revisit on ADR-0008 reopens that conversation.
|
|
212
|
+
|
|
213
|
+
#### 4.3.4 Idempotency and ordering
|
|
214
|
+
|
|
215
|
+
Coaching's subscriber MUST be idempotent over `event_id` per §12.1 of the credit-reservation-lock contract. Redelivery is expected; the projection's per-slot state SHALL converge to the latest authoritative state regardless of replay or out-of-order delivery within a per-slot stream.
|
|
216
|
+
|
|
217
|
+
Out-of-order events within the same `(organization_id, coach_id, window)` slot are not expected (the credit-reservation-lock contract preserves intra-slot order), but the projection SHALL be robust to it. The implementation pattern is "compute slot state from the latest set of events for that slot, not from the most recent event alone."
|
|
218
|
+
|
|
219
|
+
## 5. Versioning policy
|
|
220
|
+
|
|
221
|
+
### 5.1 Semantic versioning
|
|
222
|
+
|
|
223
|
+
- **Patch** (v1.0.0 → v1.0.1): editorial clarifications, typo fixes, validation file additions. No surface change.
|
|
224
|
+
- **Minor** (v1.x.y → v1.x+1.0): additive changes. New optional query parameters, new optional response fields, new endpoints, appended enum values. Consumers on older minors continue to work.
|
|
225
|
+
- **Major** (v1.x.y → v2.0.0): breaking changes. Removed endpoints, changed response shapes, narrowed enums, required parameters added.
|
|
226
|
+
|
|
227
|
+
### 5.2 Deprecation policy
|
|
228
|
+
|
|
229
|
+
On major-version publication, the previous major enters a two-week deprecation window per the org standard. Consumers running against deprecated versions after the window contribute to the contract-currency drift metric. The deprecation window is written in from v1.0.0 even though there is no v0.x to deprecate; the discipline applies forward.
|
|
230
|
+
|
|
231
|
+
### 5.3 Additive discipline within a major
|
|
232
|
+
|
|
233
|
+
New response fields MUST default to null or a backwards-compatible value. New enum values for `state`, `availability_state`, and any future enum on the contract surface MUST be appended; consumers MUST treat unknown enum values as safe-to-ignore for branching logic and surface them as opaque strings to operators.
|
|
234
|
+
|
|
235
|
+
New query parameters or body fields MUST be optional within a minor version. Required-parameter additions are major-version changes.
|
|
236
|
+
|
|
237
|
+
## 6. Consumer responsibilities
|
|
238
|
+
|
|
239
|
+
### 6.1 Tenancy scoping
|
|
240
|
+
|
|
241
|
+
Every read SHALL include `organization_id`. The producer rejects reads without one with HTTP 400. A read for one organization SHALL NOT return slots, coaches, or eligibility data for another, even if the calling consumer has access to multiple organizations on the producer side.
|
|
242
|
+
|
|
243
|
+
### 6.2 Stale-read handling
|
|
244
|
+
|
|
245
|
+
Consumers SHALL treat the projection as eventually-consistent. If a consumer commits to an offer or an assignment and the underlying reservation request fails because Revenue's authoritative state is different, the consumer SHALL retry against a fresh read rather than retry against the same projection snapshot. Consumers MUST NOT cache projection responses across operator commits.
|
|
246
|
+
|
|
247
|
+
### 6.3 No direct Revenue surface access through projection IDs
|
|
248
|
+
|
|
249
|
+
`reservation_id` and `lock_id` returned in the availability response are surfaced for traceability. Consumers SHALL NOT use them to read or write into Revenue's surface directly outside the credit-reservation-lock contract's published consumer endpoints.
|
|
250
|
+
|
|
251
|
+
### 6.4 No direct Coaching storage access
|
|
252
|
+
|
|
253
|
+
Consumers SHALL NOT reach into Coaching's storage layer for Coach records, capacity records, or projection state. All access goes through the contract surface. Coach record reads needed for operator-tooling display SHALL go through Platform's Person facts API for identity fields and through this contract for Coaching-owned fields.
|
|
254
|
+
|
|
255
|
+
### 6.5 Sales-specific (offer construction)
|
|
256
|
+
|
|
257
|
+
Sales calls 4.2.2 (eligibility by description) at offer construction. Sales has no `lesson_id` at this point; the operator constructs an offer by selecting a coach and a window from the eligibility response, then submits a reservation request to Revenue's credit-reservation-lock surface. Sales' assignment-at-close orchestration calls Delivery's assignment surface (not Coaching's; assignment is a Delivery write) once the reservation lands.
|
|
258
|
+
|
|
259
|
+
If the projection is stale at the moment Sales submits the reservation request and the slot has been taken, Revenue's concurrency control on the reservation surface returns a conflict; Sales SHALL retry against a fresh 4.2.2 read.
|
|
260
|
+
|
|
261
|
+
See `validation/sales-offer-construction.md` for the consumer-shape verification.
|
|
262
|
+
|
|
263
|
+
### 6.6 Delivery-specific (assignment)
|
|
264
|
+
|
|
265
|
+
Delivery calls 4.2.1 (eligibility by lesson) at assignment time, when a `les_` exists. Delivery's coach-day planner module composes the eligibility response with its own scheduling-side facts (lesson types, lesson-site constraints, intra-day gaps) to surface a recommended coach and offer the operator a confirm or override. The `coach.assigned` event remains a Delivery emission; this contract does not produce assignment events.
|
|
266
|
+
|
|
267
|
+
Delivery's pre-existing `delivery/modules/coach-day/` planner is rewritten to consume this contract per ADR-0008 action item 7. The coach-day result type stays in Delivery as a scheduling artifact.
|
|
268
|
+
|
|
269
|
+
See `validation/delivery-assignment.md` for the consumer-shape verification.
|
|
270
|
+
|
|
271
|
+
## 7. Producer responsibilities
|
|
272
|
+
|
|
273
|
+
### 7.1 Subscriber discipline
|
|
274
|
+
|
|
275
|
+
Coaching's lock-event subscriber SHALL satisfy §12 of the credit-reservation-lock contract in full: idempotent over `event_id` (§12.1), no independent computation of "first lock" (§12.2), Revenue's API as source of truth for lock state (§12.3), no direct emission of `credit.released` or `credit.forfeited` (§12.4).
|
|
276
|
+
|
|
277
|
+
**Interim implementation note (v1.0.1, 2026-05-01).** During the dispatcher SDK gap (see §4.3 interim note), this subscriber does not run; Coaching's reads instead synchronously query Revenue's authoritative lock-state API per request. Of the four §12 sub-rules: §12.2 (no independent computation of first lock) and §12.4 (no direct emission of `credit.released` or `credit.forfeited`) remain binding under both shapes; §12.1 (idempotency over `event_id`) is moot under the interim shape because no envelopes are received; §12.3 (Revenue's API as source of truth) is satisfied even more directly under the interim shape since reads go straight to Revenue's API. When the SDK lands and the subscriber stands up, all four §12 sub-rules apply in full per the v1.0.0 wording.
|
|
278
|
+
|
|
279
|
+
### 7.2 No event emission on the credit-reservation-lock surface
|
|
280
|
+
|
|
281
|
+
Coaching SHALL NOT emit any `credit.*` event under any circumstance. If a Coaching-internal workflow ever cancels a coach window after a lock has fired (a coach pulls availability), the workflow SHALL submit a cancellation request to Revenue with `initiator=coach` per §6 and §13 of the credit-reservation-lock contract, and Revenue runs the state machine.
|
|
282
|
+
|
|
283
|
+
### 7.3 No emission of `coach.assigned`
|
|
284
|
+
|
|
285
|
+
`coach.assigned` is a Delivery event per ADR-0008. Coaching SHALL NOT emit it.
|
|
286
|
+
|
|
287
|
+
### 7.4 Read consistency across §4.2 endpoints
|
|
288
|
+
|
|
289
|
+
For the same effective inputs, the three eligibility endpoints SHALL produce the same `is_eligible` answer and the same decomposed predicates. The producer implementation SHOULD share the predicate computation across all three; consumers SHALL NOT depend on divergence.
|
|
290
|
+
|
|
291
|
+
Every eligibility response SHALL include `projection_as_of`. For v1.1.0 this value is a timestamp. The field is required for producer-side stale-read tracing, but it does not move consumer-decision audit ownership out of Sales or Delivery.
|
|
292
|
+
|
|
293
|
+
### 7.5 Org-isolation invariant
|
|
294
|
+
|
|
295
|
+
A response for `organization_id=A` SHALL NOT contain coaches, slots, or eligibility data scoped to `organization_id=B`. The producer SHALL enforce this at the storage-access layer, not solely at the response-construction layer. Test coverage for org isolation is part of v1.0.0's CI gate.
|
|
296
|
+
|
|
297
|
+
### 7.6 Projection backfill on stand-up
|
|
298
|
+
|
|
299
|
+
When Coaching stands up the lock-event subscriber, the producer SHALL backfill the projection against the current authoritative state of every active reservation and lock the producer is aware of in the organizations Coaching serves. Backfill happens once at stand-up (ADR-0008 action item 8) and is not part of the runtime contract; subsequent state changes flow exclusively through the event subscription.
|
|
300
|
+
|
|
301
|
+
## 8. Security and privacy
|
|
302
|
+
|
|
303
|
+
### 8.1 PII footprint
|
|
304
|
+
|
|
305
|
+
The contract surface returns `coach_id` (a `coa_` role-record reference), state predicates, and timestamps. No Person-level PII (name, contact information, demographics) flows through this contract. Consumers that need Person fields SHALL resolve via Platform's Person facts API per the identity contract.
|
|
306
|
+
|
|
307
|
+
### 8.2 Org isolation as a security invariant
|
|
308
|
+
|
|
309
|
+
Cross-organization data leakage through this contract surface is treated as a P1 security event. The org-isolation rule in §7.5 is enforced at storage-access; producer logs SHALL include `organization_id` on every read so leakage detection is auditable.
|
|
310
|
+
|
|
311
|
+
### 8.3 Log hygiene
|
|
312
|
+
|
|
313
|
+
Producer logs SHALL NOT include `coach_id` to PII linkages (no log line correlates `coa_` to a Person's name or contact). Consumer log discipline is governed by the consumer's domain logging standard, not this contract.
|
|
314
|
+
|
|
315
|
+
### 8.4 Rate limiting
|
|
316
|
+
|
|
317
|
+
The producer SHALL rate-limit the read endpoints per organization. Limits are operational, not contract-defined; consumers SHOULD handle HTTP 429 with exponential backoff. The numeric limits are tuned in production and are not part of the contract surface.
|
|
318
|
+
|
|
319
|
+
## 9. Future work
|
|
320
|
+
|
|
321
|
+
### 9.1 Softly-held projection field
|
|
322
|
+
|
|
323
|
+
Sales named a future use case in `2026-05-01-sales-coaching-availability-ack` where pre-`credit.reserved` operator-tooling reservations would be exposed as a "softly held" indicator on the slot. The forward-looking framing (a separate field on the response, additive, no semantic change to the subtraction rule) is preserved for v1.x. Trigger to add: Sales operator-tooling needs the indicator for multi-operator coordination on overlapping territory, or Sales reports false-negative offer construction caused by other-operator races.
|
|
324
|
+
|
|
325
|
+
### 9.2 Capacity write surface
|
|
326
|
+
|
|
327
|
+
Today the contract is read-only. Coaching-internal capacity adjustments (a coach reduces hours, takes vacation, increases coverage) do not have a published write endpoint. The first consumer that needs to write capacity through the contract (an HR-shaped tool, a coach self-service portal) triggers the v1.x or v2.0 expansion. Trigger to add: a consumer that writes capacity exists outside Coaching's repo.
|
|
328
|
+
|
|
329
|
+
### 9.3 Certification publication
|
|
330
|
+
|
|
331
|
+
Coach certifications are a Coaching-side fact today (an attribute on Coach Profile). The contract uses certifications in the eligibility predicate but does not expose certification publication, expiry, or a write surface. Trigger to add: a consumer needs to surface or update certifications outside of Coaching's tooling.
|
|
332
|
+
|
|
333
|
+
### 9.4 Coaching event producers
|
|
334
|
+
|
|
335
|
+
If Coaching grows event producers (capacity-changed, certification-issued, certification-expired), they ride the existing event envelope per ADR-0005 and are added to this contract or to a sibling contract. Trigger to add: a consumer subscribes to a Coaching-emitted event for the first time.
|
|
336
|
+
|
|
337
|
+
### 9.5 Sub-spec extraction
|
|
338
|
+
|
|
339
|
+
The lock-aware projection mechanics (§4.3) and the eligibility predicate decomposition (§4.2.1, §4.2.2, §4.2.3) are candidates for sub-spec extraction if they grow. Trigger to add: either section reaches a length where the README's index role is muddied, or a non-coordination consumer (a future Coaching-internal architecture doc, a runbook) needs to reference the section without referencing the rest of the README.
|
|
340
|
+
|
|
341
|
+
## 10. Change log
|
|
342
|
+
|
|
343
|
+
### v1.1.0 (2026-05-12)
|
|
344
|
+
|
|
345
|
+
Additive minor adding `GET /coaching/v1/eligibility/by-coach-and-window` as §4.2.3. The endpoint returns the single-coach subset of the existing eligibility predicate set for Delivery's sales-scheduling-surface §7.7 write-time re-check. Sales' offer-construction read remains §4.2.2, Delivery's assignment-time read remains §4.2.1, and the shared predicate consistency invariant in §7.4 now applies across all three endpoint shapes.
|
|
346
|
+
|
|
347
|
+
Amended 2026-05-16 to add required `projection_as_of` to all §4.2 eligibility responses. This is an additive producer evidence field for stale-read tracing and does not change the eligibility predicates, assignment authority, reservation authority, or consumer-decision audit ownership.
|
|
348
|
+
|
|
349
|
+
### v1.0.1 (2026-05-01)
|
|
350
|
+
|
|
351
|
+
Editorial patch documenting the interim implementation of §4.3 (lock-aware projection mechanics) and §7.1 (subscriber discipline) during the dispatcher SDK gap. No surface change; consumers on v1.0.0 continue to work unchanged. The patch acknowledges that the dispatcher SDK described in `contracts/event-envelope/README.md` §3, §6, and §9 does not yet exist (Platform-attributed gap, see `memos/2026/2026-05-01-platform-dispatcher-sdk-gap-and-interim-shape`), and that Coaching satisfies the projection's observable behavior via synchronous queries to Revenue's authoritative lock-state API at request time until the SDK lands. The SDK-readiness consumer-migration window from interim sync-query to projection-based reads is tracked as a future v1.x or v2 contract event on Platform's SDK-build planning thread.
|
|
352
|
+
|
|
353
|
+
### v1.0.0 (2026-05-01)
|
|
354
|
+
|
|
355
|
+
Initial publication per ADR-0008 (Coaching as sixth domain). Drafted by Delivery pre-stand-up; ownership transfers to Coaching on stand-up. Producer Coaching, consumers Sales and Delivery. Read surface covers availability (§4.1) and eligibility (§4.2) keyed by lesson ID or by lesson description for offer construction. Lock-aware availability projection (§4.3) maintained by a subscription on `credit.reserved`, `credit.locked`, `credit.released`, `credit.forfeited`; `credit.consumed` is not subscribed. Subtraction rule per §4.3.2: `credit.reserved` subtracts, `credit.locked` keeps subtracted, `credit.released` and `credit.forfeited` unsubtract. Freshness inherits the event envelope SLO. Two validation files in `validation/` document Sales' offer-construction shape and Delivery's assignment shape.
|