{
  "schema_version": "1.0",
  "baseline": "A-feature-flags-batch-4-readiness",
  "phase": "2b+ → 2c readiness",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "ตัวอย่าง pub/sub payload สำหรับ channel ptt.ff.invalidate · ครอบคลุม tenant / user / flag-registry / global invalidate · JWKS rotation notify · ใช้เป็น fixture สำหรับ subscriber test",
  "purpose_en": "Concrete pub/sub payload examples for channel ptt.ff.invalidate. Each example is a JSON message. Subscribers parse and act. Publishers must emit exactly this shape. Use as fixtures for subscriber unit tests once Redis is wired.",
  "honest_note": "No publisher is wired in 2b+. No subscriber is wired in 2b+. These are fixture payloads for the Phase 2c wiring pass.",
  "channel_name": "ptt.ff.invalidate",
  "key_prefix": "ptt:ff:",
  "envelope_shape": {
    "required_fields": ["kind", "ts"],
    "optional_fields": ["tenant_id", "user_id", "flag_id", "reason", "message_id", "actor"],
    "ts_format": "ISO-8601 with timezone · must be UTC",
    "message_id_format": "UUID v4 · optional but recommended for duplicate detection"
  },
  "kinds": [
    "tenant_override",
    "user_override",
    "flag_registry",
    "global",
    "jwks_rotation",
    "approval_ttl_refresh"
  ],
  "examples": [
    {
      "id": "PUB-01",
      "kind": "tenant_override",
      "payload": {
        "kind": "tenant_override",
        "tenant_id": "T-pty-pilot-01",
        "flag_id": "ff.wizard.interactive_draft",
        "ts": "2026-04-19T08:00:00Z",
        "message_id": "11111111-1111-4111-8111-111111111111",
        "actor": "U-wizard-pm",
        "reason": "tenant scope update"
      },
      "subscriber_action": "DEL ptt:ff:override:tenant:T-pty-pilot-01:ff.wizard.interactive_draft · SCAN+DEL ptt:ff:eval:*:T-pty-pilot-01:ff.wizard.interactive_draft"
    },
    {
      "id": "PUB-02",
      "kind": "user_override",
      "payload": {
        "kind": "user_override",
        "user_id": "U1001",
        "flag_id": "ff.generated_assets.local_notes",
        "ts": "2026-04-19T08:01:00Z",
        "message_id": "22222222-2222-4222-8222-222222222222",
        "actor": "U-admin-1",
        "reason": "user-level rollback"
      },
      "subscriber_action": "DEL ptt:ff:override:user:U1001:ff.generated_assets.local_notes · SCAN+DEL ptt:ff:eval:U1001:*:ff.generated_assets.local_notes"
    },
    {
      "id": "PUB-03",
      "kind": "flag_registry",
      "payload": {
        "kind": "flag_registry",
        "flag_id": "ff.sensitive_preview",
        "ts": "2026-04-19T09:15:00Z",
        "message_id": "33333333-3333-4333-8333-333333333333",
        "actor": "U-platform-lead",
        "reason": "approval_matrix_row updated · requires cache refresh"
      },
      "subscriber_action": "DEL ptt:ff:flag:ff.sensitive_preview · SCAN+DEL ptt:ff:eval:*:*:ff.sensitive_preview (broad)"
    },
    {
      "id": "PUB-04",
      "kind": "global",
      "payload": {
        "kind": "global",
        "ts": "2026-04-19T12:00:00Z",
        "message_id": "44444444-4444-4444-8444-444444444444",
        "actor": "U-oncall",
        "reason": "post-migration cache drain"
      },
      "subscriber_action": "SCAN+DEL ptt:ff:* (scoped to FF prefix only · never touch ptt:admin: or other prefixes)",
      "guardrails": "Rate-limited to 1/minute/actor · requires admin role · emits audit.boundary event type='cache.global_invalidate'"
    },
    {
      "id": "PUB-05",
      "kind": "jwks_rotation",
      "payload": {
        "kind": "jwks_rotation",
        "ts": "2026-04-19T13:00:00Z",
        "message_id": "55555555-5555-4555-8555-555555555555",
        "actor": "system:jwks_refresher",
        "new_kids": ["ptt-auth-2026-02"],
        "retired_kids": ["ptt-auth-2025-11"]
      },
      "subscriber_action": "DEL ptt:ff:jwks:current · next request triggers fresh fetch (single-flight lock applies)"
    },
    {
      "id": "PUB-06",
      "kind": "approval_ttl_refresh",
      "payload": {
        "kind": "approval_ttl_refresh",
        "approval_id": "APP-100001-abcd",
        "ts": "2026-04-19T14:00:00Z",
        "message_id": "66666666-6666-4666-8666-666666666666",
        "actor": "admin-control-plane-service",
        "new_ttl_state": "near-expiry"
      },
      "subscriber_action": "DEL ptt:ff:approval:APP-100001-abcd · next sensitive.check.2b path refetches from Admin"
    }
  ],
  "subscriber_pseudo": "async for msg in pubsub.listen():\n  if msg['type'] != 'message': continue\n  data = json.loads(msg['data'])\n  seen = await dedupe(data.get('message_id'))\n  if seen: continue\n  kind = data.get('kind')\n  if kind == 'tenant_override': ...\n  elif kind == 'user_override': ...\n  elif kind == 'flag_registry': ...\n  elif kind == 'global': await audit_boundary('cache.global_invalidate', data) ; await scan_del('ptt:ff:*')\n  elif kind == 'jwks_rotation': await redis.delete('ptt:ff:jwks:current')\n  elif kind == 'approval_ttl_refresh': await redis.delete(f\"ptt:ff:approval:{data['approval_id']}\")\n  else: log.warning('unknown kind', kind)",
  "publisher_pseudo": "def publish(kind, **fields):\n  msg = {'kind': kind, 'ts': datetime.now(UTC).isoformat(), 'message_id': uuid4().hex, **fields}\n  await redis.publish('ptt.ff.invalidate', json.dumps(msg))",
  "test_scenarios": [
    "T-01: publish PUB-01 · subscriber deletes exactly the expected keys · no others",
    "T-02: publish PUB-04 · subscriber respects prefix scope · no ptt:admin:* deletion",
    "T-03: replay same message_id twice · second is silently dropped",
    "T-04: subscriber offline during PUB-03 · on reconnect, drops local cache · next eval fetches fresh",
    "T-05: publish invalid payload (kind missing) · subscriber logs + skips · no crash"
  ],
  "deferred_items": [
    { "item": "Publisher wiring in app.py", "state": "not_wired" },
    { "item": "Subscriber task lifecycle in app.on_startup", "state": "not_wired" },
    { "item": "Dedupe set (in-memory LRU)", "state": "not_wired" },
    { "item": "Cross-region replication", "state": "out_of_scope" }
  ],
  "pair_with": {
    "cache_runtime_notes": "docs/runtime/feature-flags-service/cache_runtime_notes.json",
    "cache_invalidation_contract": "docs/runtime/feature-flags-service/cache_invalidation_contract.json",
    "jwks_notes": "docs/runtime/feature-flags-service/jwks_integration_notes.json"
  }
}
