@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,133 @@
1
+ # Portfolio Mart Contract
2
+
3
+ **Status:** v1.2.0
4
+ **Date:** 2026-05-19
5
+ **Owner:** portfolio (derived portfolio store and the portfolio gold section)
6
+ **Consumers:** strategy reporting consumers (leadership and operator dashboards); no operational domain consumes this contract
7
+ **Related ADRs:** ADR-0015, ADR-0016, ADR-0003, ADR-0013, ADR-0014
8
+ **Sub-specs (authoritative):** cross-market-performance.md, cross-discipline-performance.md, portfolio-level-funnel-health.md, org-topology.md, health-composites.md, cohort-funnel-panel.md
9
+ **Validations:** consumer-isolation.md, decoupling-discipline.md
10
+
11
+ ## 1. Purpose and scope
12
+
13
+ The Portfolio Mart Contract specifies the read surface of the `portfolio` section of the gold mart (ADR-0016): the cross-domain strategy metrics that leadership reads to see how the business performs across Markets, across disciplines, and across the funnel from acquisition through delivery. Portfolio composes these metrics over the warehouse silver tier and the conformed identity and geography spine, materializes them into its derived portfolio store, and serves them through this contracted section. The contract is what lets strategy reporting consumers build on a stable surface rather than on Portfolio's internal store shape, and it is the artifact that defines the firewall bound for the portfolio consumer tag per ADR-0016.
14
+
15
+ Portfolio is a cross-domain reporting domain per ADR-0015. It owns no source-of-record data, mints no role records, and produces no domain events. Every fact in this contract originates in another domain and reaches Portfolio through silver. The contract therefore specifies a read surface and the guarantees Portfolio makes about it; it specifies no write path, no event, and no operational behavior.
16
+
17
+ In scope: the served surface and how it is read, the grain and dimensions the mart is keyed by, the metric families the section exposes, the identity and dedup rules the composition follows, the versioning and deprecation policy, and the consumer and producer responsibilities. Out of scope: the silver tier shape (the `warehouse-silver` contract, owned by Platform), the gold mart shell and the consumer-firewall mechanism (Platform, per ADR-0016), financial reporting metrics (the `finance-mart` contract, owned by Finance per ADR-0015), any operational domain's per-domain gold section, canonical geography and Organization records (Platform, per ADR-0013 and ADR-0014), and Person identity (Platform). If a metric belongs to one operating domain, or is financial, or is a source-of-record fact, it does not belong in this contract.
18
+
19
+ ## 2. Normative language
20
+
21
+ The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted per RFC 2119.
22
+
23
+ ## 3. Terminology
24
+
25
+ - **Portfolio section**: the `portfolio` section of the single gold mart (ADR-0016), built across silver faces and the spine. The materialized surface this contract governs.
26
+ - **Derived portfolio store**: Portfolio's own Prisma deployment, where composed metrics are materialized before they are served. An implementation detail of the producer, not part of the contracted surface.
27
+ - **Silver face**: one conforming dbt view per warehouse, per the `warehouse-silver` contract. Portfolio reads across the five faces (growth, sales, delivery, coaching, revenue).
28
+ - **Identity and geography spine**: the conformed reference dimension carrying the Person dimension and the Organization to Market analytics geography hierarchy, per the `warehouse-silver` contract. Service Area and Lesson Site are Platform-owned operational reference data and appear in this contract only as source-backed passthrough keys where a family manifest names them. Portfolio leans hardest on the spine.
29
+ - **Discipline**: an operating unit modeled as its own Organization per ADR-0014. Cross-discipline metrics aggregate across these Organizations.
30
+ - **Metric family**: a coherent group of strategy metrics sharing a grain and a dimensional key (for example cross-Market performance). Each family is the unit of the per-family column manifests named under future work.
31
+ - **Strategy reporting consumer**: a leadership-facing or operator-facing dashboard or report that reads the portfolio section. Not a domain in the closed set; no operational domain consumes this contract.
32
+
33
+ ## 4. Surface
34
+
35
+ ### 4.1 The served surface
36
+
37
+ The portfolio section is materialized, not computed per query, per the gold tier model in ADR-0016. Consumers read the section through the gold mart's consumer-firewall mechanism: the portfolio consumer tag is bounded by this contract, so a strategy reporting consumer sees the cross-silver composition this contract defines and nothing outside it. The firewall bound is contract-shaped rather than warehouse-shaped, which is the distinction ADR-0016 draws between a reporting section and an operational section.
38
+
39
+ Every row the section serves carries an `as_of` timestamp (UTC ISO 8601) recording the refresh point of the underlying materialization. Consumers SHALL treat `as_of` as the freshness signal and SHALL NOT assume the section is real-time; it is materialized on the gold refresh cadence, which Platform owns and documents.
40
+
41
+ ### 4.2 Grain and dimensions
42
+
43
+ Portfolio metrics are aggregate. The section exposes no Person-grain rows. Every metric family is keyed by some subset of the following dimensions. Market and Organization come from the conformed identity and geography spine; Service Area is a source-backed passthrough dimension only when a family manifest names it.
44
+
45
+ - **Market**: the Platform-owned canonical geography dimension per ADR-0013. Portfolio reports across Markets; it does not mint or edit them.
46
+ - **Service Area**: optional source-backed passthrough key on a silver face. Portfolio may slice by Service Area only where the relevant family manifest names the dependency and the contributing face carries it.
47
+ - **Organization (discipline)**: each discipline as its own Organization per ADR-0014.
48
+ - **Time period**: a conformed calendar grain (day, week, month, quarter). The smallest grain a family supports is named in that family's manifest.
49
+ - **Funnel stage**: a conformed acquisition-through-delivery stage label, used by the funnel-health family.
50
+
51
+ A metric family MUST declare its dimensional key in its manifest. A consumer reading a family SHALL key on the declared dimensions and SHALL NOT infer a finer grain than the family exposes.
52
+
53
+ ### 4.3 Metric families
54
+
55
+ v1.2.0 contracts six metric families and names each family's exact column list in authoritative sibling manifests. This README contracts the family boundaries, the grain, and the cross-cutting rules.
56
+
57
+ **Cross-Market performance.** How the business performs across Markets and, where source-backed, Service Areas: the steering metrics that compare one Market to another, keyed by Market or optional Service Area and time period. The family answers "which Markets are growing, which are not, and on what signal." The column manifest is `cross-market-performance.md`.
58
+
59
+ **Cross-discipline performance.** How the business performs across disciplines, keyed by Organization and time period, with cross-discipline rollups that follow the dedup rule in §4.5. The family answers "how does each discipline perform, and what does the portfolio look like aggregated across all of them." The column manifest is `cross-discipline-performance.md`.
60
+
61
+ **Portfolio-level funnel health.** The health of the funnel from acquisition through delivery at the portfolio grain, keyed by funnel stage and time period and optionally by Market or Organization. The family answers "where is the cross-domain funnel converting and where is it leaking," composed from the growth, sales, revenue, delivery, and coaching silver faces. The column manifest is `portfolio-level-funnel-health.md`.
62
+
63
+ **Org topology.** The structure of the portfolio: the organizations (disciplines) and org-markets (Market × discipline operating units) that constitute the portfolio, their active status, and their age distribution. Keyed by Organization or org-market and snapshot-day. Sourced from the conformed identity and geography spine exclusively; this family reads no silver face. The column manifest is `org-topology.md`.
64
+
65
+ **Health composites.** Composite health indicators at the org-market, org, and portfolio level, each derived by applying a declared formula over constituent metrics from the cross-Market performance and cross-discipline performance families. The family answers "what is the overall health signal for each operating unit, for each discipline, and for the portfolio as a whole." The column manifest is `health-composites.md`.
66
+
67
+ **Cohort funnel panel.** A cross-domain cohort view of the customer funnel: for each intake cohort, the non-financial panel of counts from acquisition through delivery (intake, first credit reservation, completed lessons, status as of now). Keyed by org-market and intake-cohort week with current-week as a second axis. Composed from growth, revenue (reservation state only, not financial amounts), and delivery silver faces. Financial columns (revenue earned and paid at cohort grain) are served by the `finance-mart` `cohort_summary` sub-section; callers assemble the full §9 cohort panel by joining the two sections on the cohort key. The column manifest is `cohort-funnel-panel.md`.
68
+
69
+ ### 4.4 What the portfolio section does not expose
70
+
71
+ - **No source-of-record data.** The section exposes composed metrics, not the underlying domain records. A consumer needing a domain's operational records reads that domain's surface, not this one.
72
+ - **No financial metrics.** Revenue, margin, cash position, and reconciliation are the `finance-mart` contract's surface per ADR-0015. The boundary is grain and authority: if the question is financial, it is Finance's.
73
+ - **No per-domain operational metrics.** A metric that belongs to one operating domain is that domain's gold section, not this one. The decoupling discipline in `validation/decoupling-discipline.md` is the producer-side guarantee that such metrics are not absorbed here.
74
+ - **No Person-grain rows and no events.** The section is aggregate-only, and Portfolio produces nothing on the event envelope.
75
+
76
+ ### 4.5 Identity and dedup rule
77
+
78
+ Portfolio does not mint Person and does not own Person identity. Cross-domain and cross-Organization joins go through `person_id` per ADR-0003; Person, geography, and Organization facts come from the conformed identity and geography spine, never from a shadow copy and never by reaching into another domain's storage. Cross-discipline counts follow ADR-0014's Person-level dedup rule: a person active in two disciplines is counted once in a portfolio-level rollup and once per discipline in the per-discipline cut, and a family's manifest names which of the two it reports. Consumers SHALL NOT sum per-discipline cuts to reconstruct a portfolio total; the deduped portfolio rollup is the authoritative cross-discipline figure.
79
+
80
+ ## 5. Versioning policy
81
+
82
+ ### 5.1 Semantic versioning
83
+
84
+ - **Patch** (1.0.0 to 1.0.1): editorial clarifications, typo fixes. No change to the served surface.
85
+ - **Minor** (1.x.y to 1.x+1.0): additive changes. A new metric family, a new column on an existing family, a new optional dimension. Consumers on older minors continue to work.
86
+ - **Major** (1.x.y to 2.0.0): breaking changes. Removing or renaming a family or a column, changing a column type, changing a family's grain, narrowing a dimensional key.
87
+
88
+ ### 5.2 Deprecation policy
89
+
90
+ On major-version publication, the previous major enters a two-week deprecation window per the org contract-currency standard. No strategy reporting consumer SHALL run against a deprecated version for more than two weeks after a new major publishes. The deprecation window is planned before the new major ships, not after, per `contracts/README.md`.
91
+
92
+ ### 5.3 Additive discipline within a major
93
+
94
+ New columns MUST default to null or a backwards-compatible value. A new metric family is additive and does not change existing families. Consumers MUST treat unknown columns and unknown families, added in a later minor, as safe to ignore.
95
+
96
+ ### 5.4 Silver churn is not a contract change
97
+
98
+ A change to a silver face or the spine does not by itself bump this contract. Portfolio absorbs silver churn in its composition logic. This contract bumps only when the served surface that strategy consumers see changes. That insulation is the point of the reporting-domain tier: silver insulates Portfolio from bronze, and this contract insulates strategy consumers from Portfolio's composition.
99
+
100
+ ## 6. Consumer responsibilities
101
+
102
+ - A strategy reporting consumer SHALL read the portfolio section through the consumer-firewall tag and SHALL NOT read silver, bronze, another gold section, or Portfolio's derived store directly.
103
+ - A consumer SHALL key on a family's declared dimensions and SHALL NOT infer a finer grain than the family exposes.
104
+ - A consumer SHALL treat `as_of` as the freshness signal and degrade gracefully when the section is materialized behind real time.
105
+ - A consumer SHALL NOT sum per-discipline cuts to reconstruct a portfolio total; it SHALL read the deduped portfolio rollup per §4.5.
106
+ - A consumer SHALL treat unknown columns and unknown families as safe to ignore.
107
+ - An operational domain SHALL NOT consume this contract as an input to operational behavior. The portfolio section is a reporting surface; an operational consumer of it is a trigger to revisit ADR-0015.
108
+
109
+ ## 7. Producer responsibilities
110
+
111
+ - Portfolio SHALL compose every metric over the silver tier and the spine, never over bronze and never by reaching into another domain's storage.
112
+ - Portfolio SHALL keep cross-domain strategy metrics in the portfolio section and SHALL NOT push a strategy metric back into any per-domain warehouse. This is Portfolio's core decoupling discipline; see `validation/decoupling-discipline.md`.
113
+ - Portfolio SHALL resolve all cross-domain and cross-Organization joins through `person_id` per ADR-0003 and SHALL source Person, geography, and Organization facts from the spine, never from a shadow copy.
114
+ - Portfolio SHALL apply the ADR-0014 Person-level dedup rule to every cross-discipline rollup and SHALL name, in each family's manifest, whether the family reports the deduped portfolio figure or the per-discipline figure.
115
+ - Portfolio SHALL stamp every served row with an `as_of` timestamp reflecting the underlying materialization refresh.
116
+ - Portfolio SHALL plan a two-week deprecation window before publishing any major version, per §5.2.
117
+ - Portfolio SHALL decline, in writing through a memo, any request that belongs to an operational domain, to Finance, or to Platform's geography surface, rather than absorbing it into this contract.
118
+
119
+ ## 8. Security and privacy
120
+
121
+ The portfolio section is aggregate-only: it exposes no Person-grain rows. The composition reads PII from the spine and the silver faces (`person_id` and conformed Person attributes) to compute deduped cross-discipline counts and funnel metrics, but the served surface carries aggregates, not Person records. Portfolio SHALL apply the same log-hygiene discipline to `person_id` and any Person attribute touched during composition that any consumer of the spine applies. The consumer firewall, a gold-tier mechanism Platform owns, is what scopes the portfolio consumer tag to this contract's surface; access to the section is Platform-controlled through that mechanism. Naming the fact for the next reader: there is no PII on the served surface in v1.0.0, and a future family that would change that is a major-version question, not a minor.
122
+
123
+ ## 9. Future work
124
+
125
+ - **Per-family validation notes** under `validation/`, proving each family composes only over silver and the spine and holds the decoupling discipline against live traffic.
126
+ - **Refresh-cadence alignment** with Platform's gold materialization decision (ADR-0016 action item 10). When Platform documents the gold refresh cadence and staleness monitoring, this contract gains a stated freshness expectation for the portfolio section.
127
+ - **Consumer registry.** v1.0.0 names strategy reporting consumers as a class. If the set of consumers grows enough that per-consumer scoping within the portfolio tag becomes useful, a consumer registry lands as a minor.
128
+
129
+ ## 10. Change log
130
+
131
+ - **v1.2.0** (2026-05-19): Adds three new metric families — `org_topology` (portfolio structure: orgs, org-markets, active counts, age distributions; spine-only reads), `health_composites` (composite health indicators at org-market, org, and portfolio grain derived from the cross-Market and cross-discipline families), and `cohort_funnel_panel` (cross-domain non-financial cohort view: intake, first credit reservation, completed lessons, status as of now, keyed by org-market × intake-cohort). The financial cohort columns (revenue earned and paid) are explicitly scoped out of this contract per §4.4 and are served by `finance-mart` v2.0.0 `cohort_summary`; callers assemble the full Them OS §9 cohort panel by joining the two sections. Per-family column manifests land as authoritative sibling files. Uses `created_at` on Organization and OrgMarket rows as the de-facto `active_from_at` source for age-distribution metrics, with a documented caveat in the `org-topology.md` manifest.
132
+ - **v1.1.0** (2026-05-18): Adds authoritative per-family column manifests for cross-Market performance, cross-discipline performance, and portfolio-level funnel health. Clarifies that Market and Organization are spine dimensions while Service Area is an optional source-backed passthrough key where a family manifest names it. Names the served columns, silver input columns, grains, and ADR-0014 person-dedup rules Platform needs to implement the derived portfolio store models.
133
+ - **v1.0.0** (2026-05-14): Initial publication. Establishes the portfolio gold section's served surface, the grain and dimensions, the three v1.0.0 metric families (cross-Market performance, cross-discipline performance, portfolio-level funnel health), the identity and dedup rules, the consumer and producer responsibilities, and the versioning and deprecation policy. Published per ADR-0015 action item 6 and ADR-0016 action item 8.
@@ -0,0 +1,76 @@
1
+ # Cohort Funnel Panel Manifest
2
+
3
+ **Status:** v1.2.0
4
+ **Date:** 2026-05-19
5
+ **Owner:** portfolio
6
+ **Parent contract:** `portfolio-mart` v1.2.0
7
+ **Family key:** `cohort_funnel_panel`
8
+ **Related:** `2026-05-19-portfolio-mart-v1-2-published`, `2026-05-19-portfolio-cohort-panel-finance-close`
9
+
10
+ ## Purpose
11
+
12
+ The cohort funnel panel answers how each intake cohort progresses through the non-financial funnel from acquisition through delivery. For each org-market and intake-cohort week, it tracks where the cohort stands today: how many people were captured at intake, how many reserved a credit, how many completed lessons, and what their current status is.
13
+
14
+ This family is the portfolio section of the cross-domain cohort view the Them OS §9 `cohort_cross_domain_panel` spec defines. It covers the non-financial columns only. The financial columns (`revenue_earned`, `revenue_paid`) are served by `finance-mart` v2.0.0 `cohort_summary` at the same grain; callers assemble the full §9 panel by joining the two sections on the cohort key (`org_market_id` + `intake_cohort_week`).
15
+
16
+ ## Grain
17
+
18
+ One row per:
19
+
20
+ - `org_market_id`
21
+ - `intake_cohort_week` (the ISO week of the cohort's first intake touchpoint)
22
+ - `snapshot_week` (the ISO week of the current materialization; the "current-week" axis)
23
+
24
+ The panel is two-dimensional: `intake_cohort_week` and `snapshot_week`. For each cohort, new rows accumulate each week as the cohort ages. A single cohort (one `intake_cohort_week` value for one org-market) produces one row per snapshot week for as long as the portfolio mart is materialized.
25
+
26
+ Org-level rollup rows (one row per `organization_id` × `intake_cohort_week` × `snapshot_week`) are served where `org_market_id` is null and `organization_id` is non-null; they sum the non-financial counts across the Organization's active org-markets and use the `distinct_person_id_within_organization` dedup rule for person counts. Portfolio does not serve a cross-org deduped rollup for this family because a person in two disciplines belongs to two separate cohort journeys.
27
+
28
+ ## Served columns
29
+
30
+ | Column | Type | Required | Definition |
31
+ | --- | --- | --- | --- |
32
+ | `family_key` | string | yes | Constant `cohort_funnel_panel`. |
33
+ | `org_market_id` | string or null | yes | OrgMarket slug from the spine. Null on org-level rollup rows. |
34
+ | `organization_id` | string or null | yes | Organization identifier from the spine. Null on org-market rows. |
35
+ | `organization_name` | string or null | yes | Organization display name from the spine. Null on org-market rows. |
36
+ | `market_id` | string or null | yes | Market identifier from the spine. Null on org-level rollup rows. |
37
+ | `intake_cohort_week` | date | yes | The Monday of the ISO week in which the cohort's first intake touchpoint occurred. The cohort key. |
38
+ | `snapshot_week` | date | yes | The Monday of the ISO week of the current materialization snapshot. |
39
+ | `cohort_age_weeks` | integer | yes | `(snapshot_week − intake_cohort_week) / 7` in complete weeks. |
40
+ | `intake_person_count` | integer | yes | Distinct `person_id` count from `growth.touchpoint` where the touchpoint is an intake-stage touchpoint with the org-market and `intake_cohort_week` slice. Fixed at the cohort's birth week; does not change across snapshot weeks for the same cohort. |
41
+ | `first_reservation_person_count` | integer | yes | Distinct `person_id` count from `revenue.credit_reservation` where the person has a first portfolio-relevant reservation with `reservation_state` indicating locked, and the person belongs to this cohort and org-market slice. Cumulative to `snapshot_week`. |
42
+ | `completed_lesson_person_count` | integer | yes | Distinct `person_id` count from `delivery.attendance` where the person has at least one attended or completed lesson, belongs to this cohort and org-market slice, and the lesson occurred on or before `snapshot_week`. Cumulative to `snapshot_week`. |
43
+ | `intake_to_first_reservation_rate` | decimal or null | yes | `first_reservation_person_count / intake_person_count`; null when denominator is zero or required source columns are unavailable. Increases monotonically as the cohort ages. |
44
+ | `first_reservation_to_completed_lesson_rate` | decimal or null | yes | `completed_lesson_person_count / first_reservation_person_count`; null when denominator is zero or required source columns are unavailable. |
45
+ | `intake_to_completed_lesson_rate` | decimal or null | yes | `completed_lesson_person_count / intake_person_count`; null when denominator is zero. The end-to-end funnel rate for this cohort as of the snapshot week. |
46
+ | `cohort_status_summary` | string | yes | A derived status label for the cohort as of `snapshot_week`. Values: `early` (cohort age < 4 weeks), `converting` (age ≥ 4 weeks, at least one reservation, not all delivered), `delivered` (intake_to_completed_lesson_rate ≥ 0.5), `stalled` (age ≥ 8 weeks, first_reservation_person_count = 0), `partial` (age ≥ 8 weeks, first_reservation_person_count > 0, completed_lesson_person_count = 0). |
47
+ | `dedup_rule` | string | yes | `distinct_person_id_within_org_market_cohort_slice` for org-market rows; `distinct_person_id_within_organization_cohort_slice` for org-level rollup rows. |
48
+ | `as_of` | timestamp | yes | UTC ISO 8601 materialization timestamp. |
49
+
50
+ ## Silver inputs
51
+
52
+ | Source | Required columns | Use |
53
+ | --- | --- | --- |
54
+ | `growth.touchpoint` | `person_id`, `occurred_at`, `intake_stage`, `org_market_id`, `organization_id`, `market_id` | Cohort assignment (intake week) and intake person count. |
55
+ | `revenue.credit_reservation` | `person_id`, `reserved_at`, `reservation_state`, `org_market_id`, `organization_id` | First credit reservation counts. Reservation state only — no money amounts; this is a funnel-stage signal, not financial reporting. |
56
+ | `delivery.attendance` | `person_id`, `lesson_id`, `attended_at`, `attendance_state`, `org_market_id`, `organization_id` | Completed lesson person counts cumulative to snapshot week. |
57
+ | spine | `org_market_id` (slug), `organization_id`, `organization_name`, `market_id`, `market_name` | Entity identity for row keys and display columns. |
58
+
59
+ ## Composition rules
60
+
61
+ - Cohort membership is determined at intake: a person belongs to a cohort based on the `intake_cohort_week` of their first intake-stage touchpoint for the org-market slice. If a person has multiple intake touchpoints (re-intakes), the first is canonical. Portfolio does not model re-intake separately in v1.2.
62
+ - `intake_person_count` is immutable after the cohort birth week: it counts people whose first intake touchpoint falls in `intake_cohort_week`. It does not change as `snapshot_week` advances.
63
+ - `first_reservation_person_count` and `completed_lesson_person_count` are cumulative: they count people who have reached the milestone at any point on or before `snapshot_week`, not within a specific calendar window.
64
+ - Revenue inputs are used only for reservation state (whether a credit was reserved). Money amounts are not read. `revenue.credit_reservation.reservation_state` is the funnel-stage signal; `revenue.credit_reservation` financial columns are not read.
65
+ - If a source field named above is absent from the live silver face, the affected column returns `null:upstream_unavailable` and the gap is filed back to Platform.
66
+ - The org-market slug (`org_market_id`) is the cohort key Platform's registry uses for the `org_market_id` grain per ADR-0019. Portfolio uses the slug format, not the UUID.
67
+
68
+ ## Join pattern for the full §9 panel
69
+
70
+ Callers assembling the Them OS §9 `cohort_cross_domain_panel` join this family's rows to `finance-mart` `cohort_summary` on (`org_market_id`, `intake_cohort_week`, `snapshot_week`). The financial columns `revenue_earned` and `revenue_paid` come from `finance-mart`; the non-financial columns above come from this family. Portfolio does not duplicate or proxy the financial columns.
71
+
72
+ ## Known coverage gaps for Platform assessment
73
+
74
+ - `growth.touchpoint.org_market_id` is required to key cohort membership by org-market. If the growth face carries only `market_id` and `organization_id` separately (not the compound slug), Platform should derive the slug via the spine join before Portfolio reads it.
75
+ - `revenue.credit_reservation.org_market_id` is required for the reservation count at org-market grain. If the revenue face carries only `market_id` and `organization_id` separately, same pattern applies.
76
+ - `delivery.attendance.org_market_id` is required for the completed lesson count at org-market grain. Same pattern if not in the face.
@@ -0,0 +1,91 @@
1
+ # Cross-Discipline Performance Manifest
2
+
3
+ **Status:** v1.1.0
4
+ **Date:** 2026-05-18
5
+ **Owner:** portfolio
6
+ **Parent contract:** `portfolio-mart` v1.1.0
7
+ **Family key:** `cross_discipline_performance`
8
+ **Related:** `2026-05-18-platform-portfolio-mart-silver-surface-and-firewall-confirmation`
9
+
10
+ ## Purpose
11
+
12
+ The cross-discipline performance family compares performance by discipline, where each discipline is an Organization per ADR-0014, and serves a portfolio rollup that deduplicates people across disciplines. It answers how each discipline performs and what the portfolio looks like when people active in more than one discipline are counted once.
13
+
14
+ This family is aggregate-only and reads silver plus the spine. It does not ask an operational domain to compute a cross-domain metric on Portfolio's behalf.
15
+
16
+ ## Grain
17
+
18
+ One row per:
19
+
20
+ - `period_grain`
21
+ - `period_start`
22
+ - `period_end`
23
+ - `rollup_scope`
24
+ - `organization_id` when `rollup_scope = discipline`
25
+
26
+ `rollup_scope` values:
27
+
28
+ - `discipline`: one row per Organization.
29
+ - `portfolio`: one deduped rollup row across Organizations. `organization_id`, `organization_name`, and `discipline_key` are null on this row.
30
+
31
+ The smallest supported period grain is `week`.
32
+
33
+ ## Served columns
34
+
35
+ | Column | Type | Required | Definition |
36
+ | --- | --- | --- | --- |
37
+ | `family_key` | string | yes | Constant `cross_discipline_performance`. |
38
+ | `period_grain` | enum | yes | `week`, `month`, or `quarter`. |
39
+ | `period_start` | date | yes | Inclusive UTC date boundary. |
40
+ | `period_end` | date | yes | Exclusive UTC date boundary. |
41
+ | `rollup_scope` | enum | yes | `discipline` or `portfolio`. |
42
+ | `organization_id` | string or null | yes | Organization identifier from the spine for discipline rows, null for portfolio rollup rows. |
43
+ | `organization_name` | string or null | yes | Organization display name from the spine for discipline rows, null for portfolio rollup rows. |
44
+ | `discipline_key` | string or null | yes | Stable discipline key from the spine for discipline rows, null for portfolio rollup rows. |
45
+ | `intake_person_count` | integer | yes | Distinct `person_id` count from `growth.touchpoint`. |
46
+ | `qualified_lead_person_count` | integer | yes | Distinct `person_id` count from `sales.lead` where the lead reaches qualified state. |
47
+ | `first_reservation_person_count` | integer | yes | Distinct `person_id` count from `revenue.credit_reservation` where the first portfolio-relevant reservation is locked. |
48
+ | `active_participant_person_count` | integer | yes | Distinct `person_id` count from `delivery.attendance` where the person attended or completed a lesson. This is the authoritative active customer denominator. |
49
+ | `scheduled_lesson_count` | integer | yes | Distinct `lesson_id` count from `delivery.lesson` where a lesson is scheduled. |
50
+ | `completed_lesson_count` | integer | yes | Distinct `lesson_id` count from `delivery.attendance` where attendance marks the lesson completed or attended. |
51
+ | `active_coach_person_count` | integer | yes | Distinct coach `person_id` count from `coaching.coach` where the coach is active. |
52
+ | `confirmed_booking_count` | integer | yes | Distinct booking count from `coaching.lesson_booking` where the booking is confirmed. |
53
+ | `intake_to_active_participant_rate` | decimal or null | yes | `active_participant_person_count / intake_person_count`; null when denominator is zero or required source columns are unavailable. |
54
+ | `coach_to_completed_lesson_ratio` | decimal or null | yes | `completed_lesson_count / active_coach_person_count`; null when denominator is zero or required source columns are unavailable. |
55
+ | `dedup_rule` | string | yes | `distinct_person_id_within_organization` for discipline rows; `distinct_person_id_across_organizations` for portfolio rows. |
56
+ | `as_of` | timestamp | yes | UTC ISO 8601 materialization timestamp. |
57
+
58
+ ## Silver inputs
59
+
60
+ | Source | Required columns | Use |
61
+ | --- | --- | --- |
62
+ | spine | `organization_id`, `organization_name`, `discipline_key` | Discipline identity and Organization attributes. |
63
+ | `growth.touchpoint` | `person_id`, `organization_id`, `occurred_at`, `intake_stage` | Intake counts by Organization and portfolio rollup. |
64
+ | `sales.lead` | `person_id`, `organization_id`, `created_at`, `qualified_at`, `lead_stage`, `lead_substage` | Qualified lead counts by Organization and portfolio rollup. |
65
+ | `revenue.credit_reservation` | `person_id`, `organization_id`, `reserved_at`, `reservation_state` | First reservation counts by Organization and portfolio rollup. |
66
+ | `delivery.lesson` | `person_id`, `organization_id`, `lesson_id`, `scheduled_start_at`, `lesson_state` | Scheduled lesson counts by Organization and portfolio rollup. |
67
+ | `delivery.attendance` | `person_id`, `organization_id`, `lesson_id`, `attended_at`, `attendance_state` | Active participant denominator and completed lesson counts. |
68
+ | `coaching.coach` | `person_id`, `organization_id`, `coach_state`, `active_from_at`, `active_until_at` | Active coach denominator. |
69
+ | `coaching.lesson_booking` | `person_id`, `organization_id`, `booking_id`, `booking_start_at`, `booking_state` | Confirmed booking counts. |
70
+
71
+ ## Dedup rule
72
+
73
+ Discipline rows count a person once within an Organization and period. Portfolio rollup rows count a person once across all Organizations and period, even when the person appears in two disciplines. Consumers must read the `portfolio` rollup row for portfolio totals and must not sum `discipline` rows to reconstruct a portfolio total.
74
+
75
+ The authoritative person-count denominator for active customers is `delivery.attendance.person_id` where `attendance_state` indicates attended or completed. `delivery.lesson.person_id` is not the active customer denominator because scheduled lessons can exist without attendance.
76
+
77
+ The authoritative person-count denominator for active coaches is `coaching.coach.person_id` where `coach_state` indicates active during the period. `coaching.lesson_booking.person_id` is not the active coach denominator because a coach can be active without a confirmed booking in the period.
78
+
79
+ ## Composition rules
80
+
81
+ - Each person metric uses `COUNT(DISTINCT person_id)` at the declared row key.
82
+ - Each lesson metric uses `COUNT(DISTINCT lesson_id)` at the declared row key.
83
+ - Each booking metric uses `COUNT(DISTINCT booking_id)` at the declared row key.
84
+ - Revenue inputs are used only as non-financial funnel signals. Money amounts, margin, cash, and reconciliation remain Finance-owned.
85
+ - If a source field named above is absent from the live silver face, the affected metric returns `null:upstream_unavailable` and the gap is filed back to Platform.
86
+
87
+ ## Known coverage gaps for Platform assessment
88
+
89
+ - `discipline_key` is required for a stable discipline label. If the spine exposes Organization but not a separate discipline key, Organization is sufficient for v1.1.0 and `discipline_key` may mirror `organization_id` until Platform names a stable display-safe key.
90
+ - `sales.lead.qualified_at` is required for period-correct qualified counts. If Sales silver carries only current lead stage, qualified lead counts by historical period should stay null until a source-backed qualification timestamp exists.
91
+ - `coaching.lesson_booking.booking_id` is required for confirmed booking counts. If the live face names the booking identifier differently, Platform should map it through silver naming conformance rather than requiring Portfolio to depend on a source-local name.
@@ -0,0 +1,84 @@
1
+ # Cross-Market Performance Manifest
2
+
3
+ **Status:** v1.1.0
4
+ **Date:** 2026-05-18
5
+ **Owner:** portfolio
6
+ **Parent contract:** `portfolio-mart` v1.1.0
7
+ **Family key:** `cross_market_performance`
8
+ **Related:** `2026-05-18-platform-portfolio-mart-silver-surface-and-firewall-confirmation`
9
+
10
+ ## Purpose
11
+
12
+ The cross-Market performance family compares operating performance by Market and, where a source face carries it, by Service Area. It answers which Markets are growing, which are not, and on which source-backed signal.
13
+
14
+ This family is aggregate-only. It reads silver faces and the conformed spine. It does not read bronze, another gold section, or a domain source database.
15
+
16
+ ## Grain
17
+
18
+ One row per:
19
+
20
+ - `period_grain`
21
+ - `period_start`
22
+ - `period_end`
23
+ - `market_id`
24
+ - optional `service_area_id`
25
+
26
+ `market_id` and `market_name` come from the conformed spine. `service_area_id` and `service_area_name` are optional source-backed passthrough keys when a contributing silver face carries them. If a face does not carry Service Area, Service Area columns are `null` and the row is Market-grain.
27
+
28
+ The smallest supported period grain is `week`. Daily rows are out of scope for v1.1.0.
29
+
30
+ ## Served columns
31
+
32
+ | Column | Type | Required | Definition |
33
+ | --- | --- | --- | --- |
34
+ | `family_key` | string | yes | Constant `cross_market_performance`. |
35
+ | `period_grain` | enum | yes | `week`, `month`, or `quarter`. |
36
+ | `period_start` | date | yes | Inclusive UTC date boundary. |
37
+ | `period_end` | date | yes | Exclusive UTC date boundary. |
38
+ | `market_id` | string | yes | Platform-owned Market identifier from the spine. |
39
+ | `market_name` | string | yes | Market display name from the spine. |
40
+ | `service_area_id` | string or null | no | Source-backed passthrough Service Area identifier, where present. |
41
+ | `service_area_name` | string or null | no | Source-backed passthrough Service Area display name, where present. |
42
+ | `intake_person_count` | integer | yes | Distinct `person_id` count from `growth.touchpoint` where the touchpoint is an intake-stage touchpoint in the period and slice. |
43
+ | `qualified_lead_person_count` | integer | yes | Distinct `person_id` count from `sales.lead` where the lead reaches a qualified stage in the period and slice. |
44
+ | `first_reservation_person_count` | integer | yes | Distinct `person_id` count from `revenue.credit_reservation` where the first portfolio-relevant reservation is locked in the period and slice. |
45
+ | `scheduled_lesson_count` | integer | yes | Distinct `lesson_id` count from `delivery.lesson` where a lesson is scheduled in the period and slice. |
46
+ | `completed_lesson_count` | integer | yes | Distinct `lesson_id` count from `delivery.attendance` where attendance marks the lesson completed or attended in the period and slice. |
47
+ | `active_participant_person_count` | integer | yes | Distinct `person_id` count from `delivery.attendance` where the person attended or completed at least one lesson in the period and slice. |
48
+ | `active_coach_person_count` | integer | yes | Distinct coach `person_id` count from `coaching.coach` where the coach is active in the period and slice. |
49
+ | `intake_to_qualified_rate` | decimal or null | yes | `qualified_lead_person_count / intake_person_count`; null when denominator is zero or required source columns are unavailable. |
50
+ | `qualified_to_first_reservation_rate` | decimal or null | yes | `first_reservation_person_count / qualified_lead_person_count`; null when denominator is zero or required source columns are unavailable. |
51
+ | `first_reservation_to_completed_lesson_rate` | decimal or null | yes | Distinct people with completed attendance after first reservation divided by `first_reservation_person_count`; null when denominator is zero or required source columns are unavailable. |
52
+ | `person_dedup_basis` | string | yes | Constant `distinct_person_id_within_market_slice`. |
53
+ | `as_of` | timestamp | yes | UTC ISO 8601 materialization timestamp. |
54
+
55
+ ## Silver inputs
56
+
57
+ | Source | Required columns | Use |
58
+ | --- | --- | --- |
59
+ | `growth.touchpoint` | `person_id`, `occurred_at`, `market_id`, `intake_stage` | Intake person counts. |
60
+ | `growth.touchpoint` | `service_area_id` | Optional Service Area slice when source-backed. |
61
+ | `sales.lead` | `person_id`, `created_at`, `qualified_at`, `market_id`, `lead_stage`, `lead_substage` | Qualified lead counts and intake-to-qualified conversion. |
62
+ | `sales.lead` | `service_area_id` | Optional Service Area slice when source-backed. |
63
+ | `revenue.credit_reservation` | `person_id`, `reserved_at`, `reservation_state`, `market_id` | First reservation counts and qualified-to-reservation conversion. |
64
+ | `revenue.credit_reservation` | `service_area_id` | Optional Service Area slice when source-backed. |
65
+ | `delivery.lesson` | `person_id`, `lesson_id`, `scheduled_start_at`, `lesson_state`, `market_id` | Scheduled lesson counts. |
66
+ | `delivery.lesson` | `service_area_id` | Optional Service Area slice when source-backed. |
67
+ | `delivery.attendance` | `person_id`, `lesson_id`, `attended_at`, `attendance_state`, `market_id` | Completed lesson and active participant counts. |
68
+ | `delivery.attendance` | `service_area_id` | Optional Service Area slice when source-backed. |
69
+ | `coaching.coach` | `person_id`, `coach_state`, `active_from_at`, `active_until_at`, `market_id` | Active coach counts. |
70
+ | `coaching.coach` | `service_area_id` | Optional Service Area slice when source-backed. |
71
+
72
+ ## Composition rules
73
+
74
+ - Counts that name people use `COUNT(DISTINCT person_id)` within the row key.
75
+ - Counts that name lessons use `COUNT(DISTINCT lesson_id)` within the row key.
76
+ - Market attributes are joined from the spine. Portfolio does not persist shadow Market attributes in the derived store beyond the materialized aggregate row.
77
+ - Revenue inputs are used only as non-financial funnel signals. Money amounts, margin, cash, and reconciliation remain Finance-owned.
78
+ - If a source field named above is absent from the live silver face, the affected metric returns `null:upstream_unavailable` and the gap is filed back to Platform.
79
+
80
+ ## Known coverage gaps for Platform assessment
81
+
82
+ - `service_area_id` is optional in this family because `warehouse-silver` v1.1.0 contracts Org and Market as analytics spine dimensions. Service Area may be used only where the live face carries it as source-backed passthrough.
83
+ - `sales.lead.qualified_at` is required for period-correct qualified counts. If Sales silver carries only current lead stage, Platform should treat qualified conversion timing as a gap rather than deriving it from current state.
84
+ - `coaching.coach.market_id` is required for active coach counts by Market. If coach Market assignment lives only on booking rows, active coach counts should stay null until the source-backed coach Market assignment is available.
@@ -0,0 +1,88 @@
1
+ # Health Composites Manifest
2
+
3
+ **Status:** v1.2.0
4
+ **Date:** 2026-05-19
5
+ **Owner:** portfolio
6
+ **Parent contract:** `portfolio-mart` v1.2.0
7
+ **Family key:** `health_composites`
8
+ **Related:** `2026-05-19-portfolio-mart-v1-2-published`
9
+
10
+ ## Purpose
11
+
12
+ The health composites family provides a single composite health indicator at three altitudes — org-market, org, and portfolio — giving leadership one signal per operating unit per week rather than requiring them to synthesize across the performance families themselves. It answers "is this market healthy this week?", "is this discipline healthy?", and "what is the portfolio's overall health signal?"
13
+
14
+ This family is a derived family: every constituent metric is computed by the cross-Market performance and cross-discipline performance families. The health composites cannot be materialized until both of those families have live data for the same period and slice. Portfolio declares the formula; Platform implements the aggregation once the constituent families are populated.
15
+
16
+ ## Grain
17
+
18
+ One row per entity × per-week:
19
+
20
+ - **`org_market` rows**: one row per OrgMarket × week.
21
+ - **`org` rows**: one row per Organization × week, aggregated across that Organization's active org-markets.
22
+ - **`portfolio` rows**: one row per portfolio × week, aggregated across all Organizations.
23
+
24
+ The smallest supported period grain is `week`.
25
+
26
+ ## Formula declaration
27
+
28
+ The composite is a weighted mean of a declared constituent metric set drawn from the v1.1.0 families. Portfolio declares the constituent set and their weights here; Platform encodes these weights in the registry's `_composite_formula` field.
29
+
30
+ ### Constituent metrics and weights (org-market grain)
31
+
32
+ The `org_market_health` score is a weighted mean of the following rate columns from `cross_market_performance`, all read at the same org-market × week slice:
33
+
34
+ | Constituent | Weight | Rationale |
35
+ | --- | --- | --- |
36
+ | `intake_to_qualified_rate` | 0.25 | Top-of-funnel health signal. |
37
+ | `qualified_to_first_reservation_rate` | 0.35 | Close-rate signal; most directly operator-controllable. |
38
+ | `first_reservation_to_completed_lesson_rate` | 0.40 | Delivery follow-through; the outcome signal. |
39
+
40
+ Each rate column is already bounded [0, 1] or null. The composite is computed only when all three constituent columns are non-null for the slice; when any constituent is null, `org_market_health` is null with `allowed_null_reasons: [upstream_unavailable]`.
41
+
42
+ The composite is a linear weighted mean, not a geometric mean or log-scaled blend: `health = 0.25 × r1 + 0.35 × r2 + 0.40 × r3`.
43
+
44
+ ### Org-level rollup
45
+
46
+ `org_health` is the weighted mean of the constituent `org_market`s' `org_market_health` scores, weighted by each org-market's `active_participant_person_count` from `cross_discipline_performance` for the same period. This is the `weighted_mean_by_active_participant_count` rollup rule. An org-market with no active participants in the period contributes a weight of 0 (does not distort the rollup) but is still counted in the org-market availability check: if any org-market in the Org has non-null `org_market_health`, the org health is computed; otherwise null.
47
+
48
+ ### Portfolio-level rollup
49
+
50
+ `portfolio_health` is the weighted mean of all Organizations' `org_health` scores, weighted by each Organization's total `active_participant_person_count` from `cross_discipline_performance` using the `distinct_person_id_across_organizations` dedup rule for the same period. This prevents double-counting people active in two disciplines when weighting the portfolio composite.
51
+
52
+ ## Served columns
53
+
54
+ | Column | Type | Required | Definition |
55
+ | --- | --- | --- | --- |
56
+ | `family_key` | string | yes | Constant `health_composites`. |
57
+ | `period_grain` | enum | yes | `week` (the only supported grain in v1.2.0). |
58
+ | `period_start` | date | yes | Inclusive UTC date boundary. |
59
+ | `period_end` | date | yes | Exclusive UTC date boundary. |
60
+ | `entity_type` | enum | yes | `org_market`, `org`, or `portfolio`. |
61
+ | `organization_id` | string or null | yes | Organization identifier from the spine. Null on `portfolio` rows. |
62
+ | `organization_name` | string or null | yes | Organization display name from the spine. Null on `portfolio` rows. |
63
+ | `org_market_id` | string or null | yes | OrgMarket slug from the spine. Null on `org` and `portfolio` rows. |
64
+ | `market_id` | string or null | yes | Market identifier. Null on `org` and `portfolio` rows. |
65
+ | `health_score` | decimal or null | yes | The composite health indicator in [0, 1]. Null when any required constituent is unavailable (`allowed_null_reasons: [upstream_unavailable]`). |
66
+ | `constituent_count` | integer | yes | Number of constituent org-markets that contributed to the composite (for `org` and `portfolio` rows). Always 1 on `org_market` rows. |
67
+ | `rollup_weight_basis` | string | yes | `weighted_mean_by_rate_formula` for `org_market` rows; `weighted_mean_by_active_participant_count` for `org` rows; `weighted_mean_by_active_participant_count_cross_org_dedup` for `portfolio` rows. |
68
+ | `composite_formula_version` | string | yes | A stable version tag for the declared formula. Current: `v1`. Formula changes are MAJOR version bumps on this family manifest. |
69
+ | `as_of` | timestamp | yes | UTC ISO 8601 materialization timestamp. |
70
+
71
+ ## Constituent family dependencies
72
+
73
+ | Constituent family | Required columns |
74
+ | --- | --- |
75
+ | `cross_market_performance` | `intake_to_qualified_rate`, `qualified_to_first_reservation_rate`, `first_reservation_to_completed_lesson_rate` at `org_market × week` grain |
76
+ | `cross_discipline_performance` | `active_participant_person_count` at `discipline × week` grain (for org-level weighting) and `distinct_person_id_across_organizations` deduped rollup (for portfolio weighting) |
77
+ | `org_topology` | `org_market_active_count_in_org` (to know how many org-markets are in each org in the period) |
78
+
79
+ ## Composition rules
80
+
81
+ - A formula change (any weight change, any constituent swap) is a MAJOR version bump on this manifest. Consumers downstream of `health_score` that build on this family need to know when the number changes because the formula changed, not because the underlying operational reality changed. The `composite_formula_version` field surfaces this on every row.
82
+ - All constituent metrics must be read at the same period grain and the same entity slice. Portfolio does not mix grains within a composite row.
83
+ - The health score is bounded [0, 1] by construction (all constituent rates are bounded [0, 1]). Platform should validate this invariant at materialization time and flag any out-of-range values as a computation error, not surface them.
84
+ - The `portfolio` rollup uses the cross-discipline dedup rule from `cross_discipline_performance`; Platform MUST use the `distinct_person_id_across_organizations` deduped `active_participant_person_count` for the portfolio-level weight, not the sum of per-discipline counts. Summing per-discipline counts would double-count people active in two disciplines and distort the weight toward larger multi-discipline operating units.
85
+
86
+ ## Known implementation sequence
87
+
88
+ Platform should implement this family last among the v1.2 families. The constituent families must have live data before composite materialization is meaningful. Implement in order: `org_topology` → `cross_market_performance` and `cross_discipline_performance` (already contracted in v1.1.0) → `cohort_funnel_panel` → `health_composites`.
@@ -0,0 +1,70 @@
1
+ # Org Topology Manifest
2
+
3
+ **Status:** v1.2.0
4
+ **Date:** 2026-05-19
5
+ **Owner:** portfolio
6
+ **Parent contract:** `portfolio-mart` v1.2.0
7
+ **Family key:** `org_topology`
8
+ **Related:** `2026-05-19-portfolio-mart-v1-2-published`
9
+
10
+ ## Purpose
11
+
12
+ The org topology family exposes the structure of the portfolio: which Organizations (disciplines) and org-markets (Market × discipline operating units) exist, which are active, and how old each is. It answers the structural context questions that make the performance families interpretable to leadership — "how many markets does Swim operate in?", "which org-markets are newest?", "how many active operating units are in the portfolio today?"
13
+
14
+ This family reads the conformed identity and geography spine exclusively. It reads no silver face. Canonical geography and Organization records are Platform-owned per ADR-0013 and ADR-0014; Portfolio reads them, it does not mint or edit them.
15
+
16
+ ## Grain
17
+
18
+ Two distinct row kinds, declared by `entity_type`:
19
+
20
+ - **`org` rows**: one row per Organization × snapshot-day.
21
+ - **`org_market` rows**: one row per OrgMarket × snapshot-day.
22
+
23
+ Rollup rows (one row per portfolio × snapshot-day) are covered by the `org_active_count` and `org_market_active_count` columns on a sentinel row where both `organization_id` and `org_market_id` are null and `entity_type = portfolio_rollup`.
24
+
25
+ The smallest supported snapshot grain is `day`.
26
+
27
+ ## Served columns
28
+
29
+ | Column | Type | Required | Definition |
30
+ | --- | --- | --- | --- |
31
+ | `family_key` | string | yes | Constant `org_topology`. |
32
+ | `snapshot_date` | date | yes | UTC date of the materialization snapshot. |
33
+ | `entity_type` | enum | yes | `org`, `org_market`, or `portfolio_rollup`. |
34
+ | `organization_id` | string or null | yes | Organization identifier from the spine. Null on `portfolio_rollup` rows. |
35
+ | `organization_name` | string or null | yes | Organization display name from the spine. Null on `portfolio_rollup` rows. |
36
+ | `discipline_key` | string or null | yes | Stable discipline key from the spine. Null on `portfolio_rollup` rows. |
37
+ | `org_market_id` | string or null | yes | OrgMarket slug from the spine (`{org_slug}-{market_slug}` format per Platform's ADR-0019 slug column). Null on `org` and `portfolio_rollup` rows. |
38
+ | `market_id` | string or null | yes | Market identifier from the spine. Null on `org` and `portfolio_rollup` rows. |
39
+ | `market_name` | string or null | yes | Market display name from the spine. Null on `org` and `portfolio_rollup` rows. |
40
+ | `status` | string | yes | Current status from the spine: `active` or `inactive`. For `portfolio_rollup` rows, always `active` (the rollup is always materialized). |
41
+ | `created_at` | timestamp | yes | UTC ISO 8601 row-creation timestamp from the spine. Used as the de-facto activation timestamp (see caveat below). |
42
+ | `tenure_weeks` | integer | yes | `floor((snapshot_date − date(created_at)) / 7)`. The number of complete weeks since the entity was created. |
43
+ | `org_active_count` | integer or null | yes | Count of active `org` rows in the portfolio on the snapshot date. Populated on `portfolio_rollup` rows; null on `org` and `org_market` rows. |
44
+ | `org_market_active_count` | integer or null | yes | Count of active `org_market` rows in the portfolio on the snapshot date. Populated on `portfolio_rollup` rows; null on `org` and `org_market` rows. |
45
+ | `org_market_active_count_in_org` | integer or null | yes | Count of active `org_market` rows within the Organization on the snapshot date. Populated on `org` rows; null on `org_market` and `portfolio_rollup` rows. |
46
+ | `age_bucket` | string | yes | Tenure bucket: `0-12w`, `13-26w`, `27-52w`, `53-104w`, `105w+`. Derived from `tenure_weeks`. |
47
+ | `person_dedup_basis` | string | yes | Constant `spine_read_no_dedup` — this family reads entity records, not person-level facts; no person dedup applies. |
48
+ | `as_of` | timestamp | yes | UTC ISO 8601 materialization timestamp. |
49
+
50
+ ## Spine inputs
51
+
52
+ | Source | Required columns | Use |
53
+ | --- | --- | --- |
54
+ | spine (Organization) | `organization_id`, `organization_name`, `discipline_key`, `created_at`, `status` | Org rows and portfolio rollup org counts. |
55
+ | spine (OrgMarket) | `org_market_id` (slug), `organization_id`, `market_id`, `market_name`, `created_at`, `status` | Org-market rows and org-level org-market counts. |
56
+
57
+ No silver face is read by this family.
58
+
59
+ ## active_from_at caveat
60
+
61
+ The spine carries `created_at` on both Organization and OrgMarket rows (row-creation timestamp) but does not carry a separate `active_from_at` or status-transition-history surface. Platform confirmed (`2026-05-19-platform-mart-registry-spine-active-from-at-gap`) that Organization and OrgMarket rows are created at activation time in current practice, making `created_at` a near-equivalent to `active_from_at`. An entity that was activated, deactivated, and reactivated would require a status-history surface to compute strict active-from semantics; that surface does not exist today. Reactivation events are operationally rare; `created_at` is acceptable for leadership-grain age-distribution at this scale.
62
+
63
+ If Platform adds a status-history surface in a future release, Portfolio will update the manifest to use the authoritative first-activation timestamp in a patch or minor. Until then, `created_at` is the declared source field and consumers should treat `tenure_weeks` as "weeks since row creation."
64
+
65
+ ## Composition rules
66
+
67
+ - `org_active_count` and `org_market_active_count` on `portfolio_rollup` rows count rows where `status = active` on the snapshot date.
68
+ - `org_market_active_count_in_org` on `org` rows counts active OrgMarket rows with matching `organization_id` on the snapshot date.
69
+ - `tenure_weeks` uses UTC dates throughout; the snapshot date boundary is midnight UTC.
70
+ - If a spine entity is absent from the live spine surface, the affected row is omitted and the gap is filed back to Platform.