{
  "description": "S4: canonical exposure-event SHAPE for trackExposure(). Each case gives a decision's resolvedLayers (some flagged attributionOnly), the assignments, and the in-session dedup state (alreadyExposed = (unit, layer, allocation) rows already exposed for this unit). expectedEvents is the exact set of exposure events trackExposure() MUST emit: EXACTLY ONE event per call carrying ONLY newly-exposed, non-attributionOnly layers, or ZERO events when nothing survives filtering. IMPORTANT: only layers[] is filtered — the event's assignments carry the FULL decision assignment map and are NOT narrowed to the surviving layers' parameters, matching the Node reference (trackExposure emits `assignments: decision.assignments` unchanged, and only `layers` is the filtered exposedLayers set). Session dedup is ON by default. Every event in expectedEvents also validates against events.schema.json. HAND-AUTHORED to the decided 0.7.0 shape (Node's current shape); js-client/iOS emit one event per deduped layer with the full unfiltered layers array, and php/python emit a single event with no dedup — all aligned in Phase 2.",
  "schema": "events.schema.json",
  "dedupKey": "(unitKey, layerId, allocationName)",
  "testCases": [
    {
      "name": "mixed_layers_single_event",
      "comment": "Three layers resolved: layer_hero (contextual, requested), layer_pricing (attributionOnly — params not requested by this decision), layer_checkout (adaptive, requested). Nothing previously exposed. trackExposure emits ONE event whose layers[] drops the attributionOnly layer_pricing, keeping layer_hero + layer_checkout with their propensity rows.",
      "unitKey": "user-mix-1",
      "configVersion": "2024-06-01T00:00:00.000Z",
      "assignments": {
        "ui.heroVariant": "hero_bold",
        "checkout.buttonColor": "green"
      },
      "resolvedLayers": [
        {
          "layerId": "layer_hero",
          "bucket": 893,
          "policyId": "policy_contextual",
          "policyKey": "hero-contextual",
          "allocationName": "treatment_a",
          "allocationKey": "treatment-a",
          "probability": 0.422379,
          "modelVersion": "2024-05-30T12:00:00.000Z"
        },
        {
          "layerId": "layer_pricing",
          "bucket": 430,
          "policyId": "policy_static",
          "policyKey": "pricing-rollout",
          "allocationName": "control",
          "allocationKey": "control",
          "attributionOnly": true
        },
        {
          "layerId": "layer_checkout",
          "bucket": 493,
          "policyId": "policy_thompson",
          "policyKey": "checkout-bandit",
          "allocationName": "treatment",
          "allocationKey": "treatment",
          "probability": 0.6
        }
      ],
      "alreadyExposed": [],
      "expectedEvents": [
        {
          "type": "exposure",
          "orgId": "org_test",
          "projectId": "proj_test",
          "env": "production",
          "unitKey": "user-mix-1",
          "timestamp": "2024-06-02T12:00:00.000Z",
          "sdkName": "node",
          "sdkVersion": "1.0.0",
          "configVersion": "2024-06-01T00:00:00.000Z",
          "assignments": {
            "ui.heroVariant": "hero_bold",
            "checkout.buttonColor": "green"
          },
          "layers": [
            {
              "layerId": "layer_hero",
              "bucket": 893,
              "policyId": "policy_contextual",
              "policyKey": "hero-contextual",
              "allocationName": "treatment_a",
              "allocationKey": "treatment-a",
              "probability": 0.422379,
              "modelVersion": "2024-05-30T12:00:00.000Z"
            },
            {
              "layerId": "layer_checkout",
              "bucket": 493,
              "policyId": "policy_thompson",
              "policyKey": "checkout-bandit",
              "allocationName": "treatment",
              "allocationKey": "treatment",
              "probability": 0.6
            }
          ]
        }
      ]
    },
    {
      "name": "session_dedup_excludes_already_exposed",
      "comment": "Same decision as above, but (user-dedup-2, layer_hero, treatment_a) was already exposed earlier this session. layer_hero is suppressed by dedup and layer_pricing by attributionOnly, so the single event's layers[] carries only layer_checkout. assignments are NOT narrowed: the event still carries the FULL decision assignment map (ui.heroVariant + checkout.buttonColor), matching the Node reference which emits `assignments: decision.assignments` unchanged and only filters layers[].",
      "unitKey": "user-dedup-2",
      "configVersion": "2024-06-01T00:00:00.000Z",
      "assignments": {
        "ui.heroVariant": "hero_bold",
        "checkout.buttonColor": "green"
      },
      "resolvedLayers": [
        {
          "layerId": "layer_hero",
          "bucket": 893,
          "policyId": "policy_contextual",
          "policyKey": "hero-contextual",
          "allocationName": "treatment_a",
          "allocationKey": "treatment-a",
          "probability": 0.422379,
          "modelVersion": "2024-05-30T12:00:00.000Z"
        },
        {
          "layerId": "layer_pricing",
          "bucket": 430,
          "policyId": "policy_static",
          "policyKey": "pricing-rollout",
          "allocationName": "control",
          "allocationKey": "control",
          "attributionOnly": true
        },
        {
          "layerId": "layer_checkout",
          "bucket": 493,
          "policyId": "policy_thompson",
          "policyKey": "checkout-bandit",
          "allocationName": "treatment",
          "allocationKey": "treatment",
          "probability": 0.6
        }
      ],
      "alreadyExposed": [
        { "layerId": "layer_hero", "allocationName": "treatment_a" }
      ],
      "expectedEvents": [
        {
          "type": "exposure",
          "orgId": "org_test",
          "projectId": "proj_test",
          "env": "production",
          "unitKey": "user-dedup-2",
          "timestamp": "2024-06-02T12:05:00.000Z",
          "sdkName": "node",
          "sdkVersion": "1.0.0",
          "configVersion": "2024-06-01T00:00:00.000Z",
          "assignments": {
            "ui.heroVariant": "hero_bold",
            "checkout.buttonColor": "green"
          },
          "layers": [
            {
              "layerId": "layer_checkout",
              "bucket": 493,
              "policyId": "policy_thompson",
              "policyKey": "checkout-bandit",
              "allocationName": "treatment",
              "allocationKey": "treatment",
              "probability": 0.6
            }
          ]
        }
      ]
    },
    {
      "name": "only_attribution_layers_no_event",
      "comment": "Every resolved layer is attributionOnly (the decision requested no parameters owned by these layers). After filtering, no layer remains -> trackExposure MUST emit NO event.",
      "unitKey": "user-attr-3",
      "configVersion": "2024-06-01T00:00:00.000Z",
      "assignments": {},
      "resolvedLayers": [
        {
          "layerId": "layer_hero",
          "bucket": 893,
          "policyId": "policy_contextual",
          "policyKey": "hero-contextual",
          "allocationName": "treatment_a",
          "allocationKey": "treatment-a",
          "probability": 0.422379,
          "modelVersion": "2024-05-30T12:00:00.000Z",
          "attributionOnly": true
        },
        {
          "layerId": "layer_pricing",
          "bucket": 430,
          "policyId": "policy_static",
          "policyKey": "pricing-rollout",
          "allocationName": "control",
          "allocationKey": "control",
          "attributionOnly": true
        }
      ],
      "alreadyExposed": [],
      "expectedEvents": []
    },
    {
      "name": "all_already_exposed_no_event",
      "comment": "Both non-attributionOnly layers were already exposed this session for this unit+allocation. Dedup removes both; nothing remains -> trackExposure MUST emit NO event (never an empty-layers event).",
      "unitKey": "user-seen-4",
      "configVersion": "2024-06-01T00:00:00.000Z",
      "assignments": {
        "ui.heroVariant": "hero_bold",
        "checkout.buttonColor": "green"
      },
      "resolvedLayers": [
        {
          "layerId": "layer_hero",
          "bucket": 893,
          "policyId": "policy_contextual",
          "policyKey": "hero-contextual",
          "allocationName": "treatment_a",
          "allocationKey": "treatment-a",
          "probability": 0.422379,
          "modelVersion": "2024-05-30T12:00:00.000Z"
        },
        {
          "layerId": "layer_checkout",
          "bucket": 493,
          "policyId": "policy_thompson",
          "policyKey": "checkout-bandit",
          "allocationName": "treatment",
          "allocationKey": "treatment",
          "probability": 0.6
        }
      ],
      "alreadyExposed": [
        { "layerId": "layer_hero", "allocationName": "treatment_a" },
        { "layerId": "layer_checkout", "allocationName": "treatment" }
      ],
      "expectedEvents": []
    }
  ]
}
