# Combinations synthesis rules

`/combinations/` is a picker page: a reader chooses any MaaS from
`research/maas/<id>/` and any gateway from `research/gateway/<id>/` (either
half optionally `none`) and the page renders one column per pair with each
capability row's cells synthesized from the two halves.

Every capability in [`research/capabilities.json`](capabilities.json) carries a
`combineRule` that tells the synthesizer which half is authoritative when both
are selected. The default is `"either"` and is omitted from the file. The
active values are `"gateway_wins"`, `"maas_wins"`, `"either"`, and
`"both_required"`.

This file is the single explanation of what each rule does, how `n/a` is
treated, how the delivery attribution badge reads, and which rule each of the
47 capabilities carries and why. Change the rule on a capability only after
re-reading its requirement text; log the flip in the PR body the same way a
score change is logged in a gateway's `decision-history.md`.

Not covered here: the `resolveCell` mode toggle on `/matrix/` (that lens
belongs to the composite view; see the AGENTS.md convention on
`gatewayNative`), and how the split-index datasets are minted (see the
convention on `research/maas/<id>/` and `research/gateway/<id>/`).

## Rule values

Each rule below is described by what the combo cell reads when both halves are
selected. The one-sided cases (MaaS `none` or gateway `none`) always resolve
to the selected half's own cell, regardless of the rule, because there is
nothing to combine.

### `gateway_wins` — the gateway half is authoritative

- Combo status equals the gateway's status.
- A MaaS `yes` does not rescue a gateway `no`.
- A MaaS `no` does not override a gateway `yes` (the point of the rule).
- Gateway `n/a` is the one escape hatch: it says the axis does not apply to
  the gateway half at all, so the MaaS half decides on its own.
- Both `yes`/`paid` earns an `M+G` badge — both halves actually deliver.
  Otherwise the badge is `G` (gateway native) or `Gi` (gateway integration).

Use when the capability is one the gateway product owns end to end: the
identity path developers hit, the harness endpoints, the policy plane,
gateway-specific security controls, and observability the gateway emits
itself (like OTLP). A MaaS-side claim of `yes` on these rows is usually about
an unrelated cloud-plumbing capability and does not answer the client's
question.

### `maas_wins` — the MaaS half is authoritative

Mirror image of `gateway_wins`.

- Combo status equals the MaaS's status.
- A gateway `yes` does not rescue a MaaS `no`.
- MaaS `n/a` hands the read to the gateway alone.
- Both `yes`/`paid` earns `M+G`. Otherwise `M` or `Mi`.

Use for the model-hosting contract questions: the model provider's data
retention terms, the regions the model actually runs in, whether using a
model needs a contract beyond the CSP agreement, whose data-processing terms
apply. A portable gateway cannot fabricate a residency zone or a ZDR
guarantee the underlying MaaS does not provide.

### `both_required` — both halves must deliver

- Combo is `yes`/`paid` only when both halves are `yes`/`paid`.
- Any `no` on either half forces the combo to `no`.
- Both `unknown` (or one `unknown` with no explicit `no`) keeps the combo
  `unknown`: we lack proof both deliver, but we also lack proof either fails.
- `n/a` on one half is not a `no` — it means the axis does not apply to that
  half, so the other decides alone (there is no requirement to combine with
  something that does not apply).
- A combined-delivery `yes` carries `M+G`. Any other status has no badge.

Use when the combined system can only satisfy the requirement if both halves
independently satisfy it: tenant isolation (a leak on either half is a leak),
end-to-end added latency (both slow-downs stack), and sensitive-data
non-persistence in stored logs and caches (a leak on either half violates
it).

### `either` — either half's yes carries the combo

- Any `yes`/`paid` on either half makes the combo `yes`/`paid`. `paid` is
  preferred over `yes` when both hold, so the combo says the more accurate
  thing.
- Both `yes`/`paid` earns `M+G`. Otherwise the single winning half's badge.
- `n/a` on one half hands the read to the other; both `n/a` is `n/a`.
- Neither yes/paid, neither `n/a` → pick the more informative of the two.
  `no` beats `unknown`, so a scored gap is preserved rather than downgraded
  into "unknown".

The default. Use for capabilities either layer can deliver independently and
where one delivery is enough for the buyer's requirement: log lineage
(whichever layer's log carries it), OpenAI-shape unified API (some MaaS
expose it, so does most gateways), semantic caching, per-team cost
attribution, and so on.

## Delivery attribution

The small badge under each `yes` cell:

| Badge | Meaning                                                       |
| ----- | ------------------------------------------------------------- |
| `M`   | MaaS half delivers this natively.                             |
| `Mi`  | MaaS half delivers via a required integration (yes~).         |
| `G`   | Gateway half delivers this natively.                          |
| `Gi`  | Gateway half delivers via a required integration.             |
| `M+G` | Both halves actually deliver (both scored yes/paid).          |

`M+G` is a factual attribution, not a "yes counts more". A pill still shows
its `~` modifier when either half of an `M+G` is integration-backed, and the
per-cell tooltip spells out the delivery mode per half so a reader knows
which side needs the extra product. `paid` is treated as native for badge
purposes: the delivery mode is about "is a separate product wired in",
not "is this a paid tier".

No badge on `no`, `unknown`, or `n/a` combos: there is nothing to attribute.

## `n/a` semantics

`n/a` means the axis does not apply to this half. It is not "no" and it is
not "unknown".

- Under `gateway_wins`, `maas_wins`: `n/a` on the winning half hands the
  read to the other half alone; `n/a` on the losing half is ignored.
- Under `both_required`: `n/a` on one half means the other decides alone;
  `n/a` on both is `n/a`.
- Under `either`: `n/a` on one half hands the read to the other; both `n/a`
  is `n/a`; a plain `n/a` never masks a real `yes` or `no` on the other side.

Under no rule does `n/a` ever count as evidence against a requirement. The
POC's original tiebreak rank (`no > unknown > n/a`) treated `n/a` as
informative-vs-unknown; this write-up drops `n/a` from that rank so it can
never be picked over a scored `no` or `unknown` finding.

## Per-cell tooltip

Every synthesized cell carries a `class="mark-tip" data-tooltip="…"` (bound
by `bindTooltips` in [`assets/rows.js`](../assets/rows.js) — the site's one
hover system; never the native `title` attribute) explaining why it reads the
way it does under the chosen rule. Reason text follows one shape:

> {rule lead} ({rule name}). {primary} = {status} ({delivery}), so combo =
> {status}. {secondary} = {status} {does not rescue|is not counted here|
> also delivers but is not authoritative here}.

If the underlying cell has a `note`, it is appended after the reason as
`Note: …` so both the mechanical explanation and the substantive note are
available on hover.

## Per-capability assignments

Ordered by tier (matches `capabilities.json`). One-line rationale per row.

### Tier 1 · Non-negotiable

| Capability                          | Rule           | Rationale                                                                                       |
| ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| identity_existing_idp               | gateway_wins   | The gateway is the surface developers authenticate to; a MaaS IdP behind a shared key does not help. |
| identity_team_attribution           | either         | Either layer's log with team attribution answers the client's question about who called what.   |
| per_user_api_keys                   | gateway_wins   | Keys are minted at the gateway the developer hits; a MaaS key store behind a shared gateway key is moot. |
| builder_self_serve_keys             | gateway_wins   | Self-serve minting lives on the gateway's admin surface, not in the MaaS IAM.                   |
| identity_oidc_sso                   | gateway_wins   | OIDC/SSO friction is the sign-in the developer sees at the gateway.                             |
| harness_endpoints                   | gateway_wins   | The gateway is what a harness points at; MaaS endpoints are irrelevant if the gateway does not carry them. |
| harness_ux_delta                    | gateway_wins   | The delta a developer feels is the gateway hop, not the MaaS behind it.                         |
| observability_lineage               | either         | Either layer's log with caller/model/timestamp lineage satisfies the requirement.               |
| observability_team_export           | either         | Whichever layer stores the audit trail can export it.                                           |
| observability_otel                  | gateway_wins   | Requirement text: "The gateway emits" — this is a gateway-emitted capability by definition.     |
| policy_definition                   | gateway_wins   | Requirement text: "enforced at the gateway, not in client code."                                |
| provider_unified_api                | either         | A MaaS with an OpenAI-compatible surface satisfies this as much as a gateway that translates.   |
| provider_model_alias                | either         | Either layer can offer a stable alias that repoints without a client redeploy.                  |
| model_availability                  | either         | A MaaS-only shape (Bedrock) or a gateway-only shape (OpenRouter) can independently carry the frontier catalog. |
| security_sensitive_data             | gateway_wins   | Redaction must happen before the request leaves the client-side trust boundary — the gateway hop. |
| security_fallback_enforcement       | gateway_wins   | Fallback routing is gateway policy; the MaaS does not know it is a fallback destination.        |
| zdr_provider_terms                  | maas_wins      | The model provider owns the data-processing terms; a gateway ZDR promise cannot rewrite MaaS terms. |
| no_gateway_body_persistence         | gateway_wins   | Requirement text: "The gateway itself does not persist prompt or completion bodies."            |
| zdr_compatibility_discovery         | gateway_wins   | Requirement text: "The gateway supplies a filter or catalog annotation."                        |
| semantic_caching                    | either         | Cache can live at either layer; either one that serves similar prompts without a new model call satisfies. |

### Tier 2 · Needed once real traffic arrives

| Capability                          | Rule           | Rationale                                                                                       |
| ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| audit_log_retention                 | either         | Whichever layer holds the audit trail sets retention; either configurable retention window satisfies. |
| siem_audit_export                   | either         | Either layer's log can stream to an external SIEM.                                              |
| policy_violation_reporting          | either         | Blocks reported from either layer's enforcement satisfy the "distinct category" requirement.    |
| sensitive_data_not_persisted        | both_required  | A leak on either half violates the requirement; both halves' logs, caches, and metrics have to stay clean. |
| finops_attribution                  | either         | Team-level spend attribution can come from cloud IAM tags or from gateway virtual keys.         |
| finops_budget_cutoff                | either         | An enforced hard stop at either layer stops the next request.                                   |
| finops_model_scoping                | either         | Per-user/per-model access grants can be enforced at either layer.                               |
| usage_cost_export_api               | either         | Either layer's cost API or export satisfies "pull without a screenshot."                        |
| reliability_failover                | either         | Policy-based retry/failover at either layer satisfies. A MaaS-native model router with a fallback chain counts. |
| reliability_latency                 | both_required  | Added latency is combined; if either layer blows the budget the combo blows the budget.         |
| upstream_provider_health            | either         | A health signal from either layer is readable by the operator.                                  |
| classification_aware_routing        | gateway_wins   | Classification-tier routing with an approved-model allowlist is a gateway routing decision.     |
| complexity_cost_routing             | either         | A gateway model router or a MaaS-native model router (Foundry) each satisfies independently.    |
| deploy_portable_cloud               | either         | Portability of the routing/policy/logging design lives with whichever layer carries them.       |
| region_residency_routing            | maas_wins      | The MaaS defines which regions actually exist; a gateway cannot pin traffic to a region the MaaS does not host in. |
| standalone_first_hop                | gateway_wins   | About the gateway product being usable in a greenfield estate with no pre-existing gateway.     |

### Tier 3 · Defer

| Capability                          | Rule           | Rationale                                                                                       |
| ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| routing_fallback_chain              | either         | An approved-fallback chain at either layer satisfies (Foundry's model router carries one; so do most gateways). |
| routing_load_balancing              | either         | Load balancing across deployments can happen at either layer.                                   |
| agentic_attribution                 | either         | MCP/A2A traffic logged at whichever layer sees it answers the attribution requirement.          |
| agentic_tool_allowlist              | gateway_wins   | Requirement text: "refused at the gateway."                                                     |

### Beyond the 90% · Adversarial tests

| Capability                          | Rule           | Rationale                                                                                       |
| ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| tenant_isolation_revocation         | both_required  | A cross-tenant leak on either half is a leak; both halves have to isolate.                      |
| fail_closed_outage                  | gateway_wins   | Requirement text: "the gateway fails closed."                                                   |

### Commercial & business-model questions

| Capability                          | Rule           | Rationale                                                                                       |
| ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| licensing                           | maas_wins      | Whether an extra contract is needed is a model-provider fact, not a gateway feature.            |
| where_models_run                    | maas_wins      | The MaaS is where inference runs; the gateway does not host models.                             |
| oss_vs_paid                         | either         | Commercial commentary from either half is fair; the combo shows whichever layer's tier applies. |
| billing_unit                        | gateway_wins   | About the gateway product's billing unit (display=mark row); the MaaS's per-token billing is a separate axis. |
| layering                            | gateway_wins   | About the gateway fronting a CSP; the MaaS does not front itself.                               |

## Deliberations worth flagging

A few rows sat between two rules until a specific test decided it. Recording
them here so a future flip has a starting point.

- **`no_gateway_body_persistence`** could read as `both_required` — if the
  MaaS persists bodies, prompts are still in a durable store. Landed on
  `gateway_wins` because the requirement text is specifically "The gateway
  itself does not persist", and MaaS-side body persistence is captured by
  the ZDR row and per-MaaS residency rather than by this one. A future
  ticket may add a paired `no_maas_body_persistence` row if that split is
  worth making explicit.
- **`security_sensitive_data`** could read as `either` — Azure Foundry's
  content filters redact sensitive data before the underlying model sees
  it, which arguably satisfies "before a provider sees it". Landed on
  `gateway_wins` because keeping sensitive data inside the client-side
  trust boundary means the redaction must happen before the request leaves
  the gateway hop; MaaS-side redaction still means the data traveled.
- **`deploy_portable_cloud`** could read as `gateway_wins` — cloud
  portability is fundamentally a gateway trait since MaaS entries in this
  library are all cloud-locked. Landed on `either` so a future entry (a
  self-hosted vLLM catalog on the MaaS side, say) is not pre-emptively
  scored `no` on portability.
- **`region_residency_routing`** could read as `both_required` — a gateway
  that routes by region but is paired with a single-region MaaS still leaks
  the residency zone. Landed on `maas_wins` because the MaaS is the actual
  ceiling: a gateway cannot fabricate a region. If the client wants "the
  gateway must also route by region", they can additionally require a
  gateway-yes on `classification_aware_routing`, which is `gateway_wins`.
- **`billing_unit`** is a `display: "mark"` row that prints a phrase
  ("per seat", "request capacity") rather than a yes/no. It reads as
  `gateway_wins` because the phrase describes the gateway product's
  billing; bare Bedrock's "tokens only" is the MaaS's model-token billing,
  a different axis.

## Missing cells on a selected half

A picker choice has three states per half, not two:

1. **Not selected** — the reader picked *None* for that half. The other
   half is the whole combo.
2. **Selected with a scored cell** — read the cell verbatim.
3. **Selected but the cell is missing** — the half is in play, but the
   capability has no scored answer on this half. **This is an implicit
   `unknown`, not "not selected".**

Case (3) matters because it is common in a real picker. Family 2 halves
split cells by delivery: `research/gateway/apim/data.json` scores the
capabilities APIM delivers; `research/maas/foundry/data.json` scores the
capabilities Foundry delivers. If a reader picks Foundry + LiteLLM (a
cross-vendor overlay), Foundry alone has no cell for `tenant_isolation_revocation`
(it lives on APIM's half in the composite). Under `both_required`, treating
that gap as "MaaS not selected" would let LiteLLM's `yes` publish as the
whole combo — silently promoting a scored `yes` on one half into a combo
`yes` on a row where the other half hasn't been scored at all. The rule
engines all read `unknown` correctly (`either` refuses to treat unknown
as a yes carrier, `both_required` refuses to combine yes with unknown,
the *_wins rules keep their authoritative-half semantics), so lifting the
missing cell to `unknown` is the correct fix rather than special-casing
each rule.

## Related open work

- **Split data placement**: some `maas_wins` capabilities (`licensing`,
  `zdr_provider_terms` on the Azure and Databricks composites) currently
  live on the gateway half of the split because the composite marked them
  `gatewayNative: true`. A follow-up may move those cells to the MaaS half
  so a `maas_wins` capability's authoritative answer sits in the
  authoritative file. Same shape as `#297`'s matrix migration decision.
- **Extending both_required**: this pass identified three rows. A future
  audit against real-use signals from `/combinations/` may find more —
  candidates include `security_fallback_enforcement` (both layers must
  hold protection under a fallback) and `no_gateway_body_persistence`
  when paired with a paired `no_maas_body_persistence` row.
