{
  "schema_version": "1.0",
  "baseline": "A-feature-flags-phase-2b-plus",
  "phase": "2b+",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "Cache invalidation contract สำหรับ FF Service · ระบุ channel · key prefix · TTL · trigger · current-impl-note · deferred matrix",
  "purpose_en": "Cache invalidation contract for FF Service. Declares channel, key prefixes, TTL defaults, invalidation triggers, current implementation state, and deferred items.",
  "honest_note": "No cache is wired in 2b+ runtime. This document is the SHAPE that Phase 2c Redis integration must conform to. Consumers that call /v1/flags/evaluate today get a fresh file read each time — no stale-cache class exists yet.",
  "channel": {
    "name": "ptt.ff.invalidate",
    "type": "pub/sub (Redis channel · planned)",
    "payload_shape": {
      "scope": "enum[flag, tenant_override, user_override, evaluated]",
      "key": "affected key pattern · string",
      "reason": "enum[mutation, registry_reload, manual]",
      "ts": "iso_8601_utc"
    },
    "current_state": "channel name declared · no publisher · no subscriber"
  },
  "key_model": {
    "prefix": "ptt:ff:",
    "namespaces": [
      { "namespace": "flag", "pattern": "ptt:ff:flag:<flag_id>", "value": "registry entry JSON", "ttl_seconds": 300, "notes": "Reloaded on registry.json change or explicit manual invalidate" },
      { "namespace": "override", "pattern": "ptt:ff:override:<tenant|user>:<id>:<flag_id>", "value": "override entry JSON", "ttl_seconds": 60, "notes": "Short TTL · overrides change more frequently than flags" },
      { "namespace": "evaluated", "pattern": "ptt:ff:eval:<user_id>:<tenant_id>:<flag_id>", "value": "evaluation response JSON", "ttl_seconds": 30, "notes": "Very short TTL · pre-computed result for hot paths" },
      { "namespace": "jwt", "pattern": "ptt:ff:jwt:<kid>", "value": "JWKS entry", "ttl_seconds": 3600, "notes": "JWKS cache · used once verified-mode wires in Phase 2c" }
    ]
  },
  "invalidation_triggers": [
    {
      "event": "override_mutation",
      "paths": ["PUT /v1/flags/override/tenant/:tid/:flag", "PUT /v1/flags/override/user/:uid/:flag", "DELETE *"],
      "invalidate_keys": [
        "ptt:ff:override:<scope>:<id>:<flag_id>",
        "ptt:ff:eval:*:<tenant_id>:<flag_id>",
        "ptt:ff:eval:<user_id>:*:<flag_id>"
      ],
      "broadcast": true
    },
    {
      "event": "registry_reload",
      "paths": ["filesystem watch on registry.json · or manual POST /v1/flags/_reload"],
      "invalidate_keys": [
        "ptt:ff:flag:*",
        "ptt:ff:eval:*"
      ],
      "broadcast": true
    },
    {
      "event": "ttl_expiry",
      "paths": ["passive · key read after expiry"],
      "invalidate_keys": ["passive-per-key"],
      "broadcast": false
    },
    {
      "event": "manual_op",
      "paths": ["admin-run tool · POST /v1/flags/_cache/invalidate {pattern}"],
      "invalidate_keys": ["per pattern"],
      "broadcast": true,
      "current_state": "endpoint not implemented · reserved in contract"
    }
  ],
  "stampede_protection": {
    "strategy": "soft-ttl + refresh-ahead",
    "description_th": "เมื่อ TTL ใกล้หมด · request แรกที่ hit จะ refresh background · ทุก request ระหว่างนั้นได้ค่าเดิม · กันไม่ให้หลาย worker ยิง evaluate ซ้ำกัน",
    "description_en": "Soft-TTL + refresh-ahead: first request after soft-TTL crosses triggers background refresh while other requests still receive cached value. Prevents thundering-herd.",
    "current_state": "Documented only · no implementation"
  },
  "consistency_model": {
    "level": "eventual · bounded by TTL",
    "read_after_write_promise": "only via invalidation broadcast (30s worst case) · clients needing strong consistency must pass ?fresh=true (endpoint does not exist yet · reserved)",
    "honest_note": "Phase 2b+ does not claim read-after-write consistency. Any consumer assuming it MUST refuse to cache evaluate responses locally."
  },
  "current_state_summary": {
    "runtime_cache": "NONE · every /v1/flags/evaluate reads file-backed JSON",
    "jwks_cache": "NONE · verified mode not wired",
    "invalidation_publisher": "NONE",
    "invalidation_subscriber": "NONE",
    "operator_tooling": "NONE · planned admin endpoint reserved"
  },
  "deferred_to_2c": [
    "Redis client + connection pool",
    "Publisher in app.py emit on mutation endpoints",
    "Subscriber worker to keep local cache warm",
    "Soft-TTL + refresh-ahead logic",
    "Manual invalidate endpoint + auth gate",
    "JWKS cache in verified mode"
  ]
}
