{
  "schema_version": "1.0",
  "baseline": "A-runtime-ff-phase-2b",
  "phase": "Phase 2b",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "honest_note": "Phase 2b adds three things on top of Phase 2a+ (HTTP boundary): (1) a JWT-claim expectation + dev-mode Bearer resolution, (2) a FILE-backed override store (JSON on disk) with documented precedence over stage/default, and (3) a sensitive-flag precheck helper that surfaces the sensitivity profile + required approvers from the registry. NONE of the following are live: real JWT verification via JWKS/IdP, Postgres-backed persistence, Redis cache, pubsub invalidation, flag flip / transition write API, WORM audit sink, rate limiting, or admin UI. All writes remain deferred to Phase 2c. The evaluator semantics are unchanged; Phase 2b only changes HOW overrides reach the evaluator and HOW context is resolved.",

  "contract_refs": [
    "docs/runtime/feature-flags-service/http_contract.json (Phase 2a+ · envelope + 4 base endpoints)",
    "docs/runtime/feature-flags-service/evaluator_contract.json is Phase 2a · unchanged",
    "docs/runtime/feature-flags-service/override_store_schema.json (Phase 2b · this commit)",
    "docs/runtime/feature-flags-service/override_examples.json (Phase 2b · seed file · 8 rows)",
    "docs/runtime/admin-control-plane-service/policy_contract.json (referenced for sensitive_dual requirements)",
    "docs/runtime/feature-flags/registry.json (source of truth · flag sensitivity + approval gates)",
    "docs/runtime/cross-runtime-integration/demo_contract.json (policy-first/eval-second orchestration)"
  ],

  "purpose_th": "ยกระดับ FF service จาก dev shim สู่ scaffold ที่พร้อมรับ auth จริงและ override store จริง · โดยยังไม่ overclaim",
  "purpose_en": "Level up FF service from dev shim to a scaffold that is ready for real auth + real override store without overclaiming the current state",

  "delta_from_phase_2a_plus": {
    "auth_context": {
      "before": "X-PTT-User-Id / X-PTT-Tenant-Id / X-PTT-Tier / X-PTT-Env / X-PTT-Role-Key / X-PTT-Env (headers) OR query params",
      "after":  "Authorization: Bearer <jwt> (preferred · resolved BEFORE headers when PyJWT is installed AND POLICY_JWT_VERIFY=true AND public key provided) · dev X-PTT-* headers remain as fallback · every response now includes service.auth_source enum ['jwt','dev_headers','query','mixed','none']"
    },
    "override_store": {
      "before": "overrides only accepted inline via request.overrides.{user,tenant}[key]",
      "after":  "file-backed store loaded at startup · per-request merge happens server-side · inline request.overrides still supported and documented to WIN over the file store"
    },
    "sensitive_gate": {
      "before": "the evaluator itself applies requires_approval + audit.last_approval_ref gate (step 4 · prec-4)",
      "after":  "NEW endpoint POST /api/flags/sensitive-eval returns {can_proceed_to_eval, policy_hint, sensitivity_profile, flag_result} · policy_hint is computed IN-PROCESS from registry fields · cross-service call to admin policy stays in the cross-runtime-integration demo · NOT inside this service"
    },
    "error_vocabulary": {
      "added": ["auth_not_verified (warn · when PyJWT missing or POLICY_JWT_VERIFY=false · request still served)", "override_store_unavailable (warn · store file failed to parse · falls back to no-store evaluation)"]
    }
  },

  "auth_context_resolution": {
    "resolution_order": [
      "1. Authorization: Bearer <token> — parsed (decode-only unless verification conditions all satisfied)",
      "2. X-PTT-* headers — used to fill any claim the Bearer did not provide",
      "3. Query params (single-eval GET /api/flags/eval) — lowest priority",
      "4. Defaults: env='dev', tier='anonymous', tenant_id=null"
    ],
    "verification_conditions_all_required_for_real_verification": [
      "PyJWT library installed (optional dependency · see requirements.txt)",
      "Env POLICY_JWT_VERIFY=true",
      "Env POLICY_JWT_PUBLIC_KEY or POLICY_JWKS_URL set",
      "Token has valid signature + exp + not yet expired"
    ],
    "honest_statement_when_not_verified": "service.auth_source = 'jwt_unverified' + response.service.warnings += 'auth_not_verified' · decode still works for claim extraction in dev",
    "missing_user_id_after_resolution": "HTTP 400 invalid_request (unchanged from Phase 2a+)",
    "auth_source_values": {
      "jwt": "claims verified AND used",
      "jwt_unverified": "bearer provided · decoded but not verified · dev-only",
      "dev_headers": "context from X-PTT-* headers",
      "query": "context from query params (eval endpoint only)",
      "mixed": "claims from bearer + fills from headers/query",
      "none": "no identity context provided (request is likely invalid)"
    }
  },

  "jwt_claim_expectation": {
    "algorithm_planned": "RS256 (HS256 acceptable for local tests)",
    "claims": {
      "sub":       {"required": true,  "maps_to": "user_id",    "description": "stable subject identifier"},
      "tenant_id": {"required": false, "maps_to": "tenant_id",  "description": "null for platform roles"},
      "tier":      {"required": true,  "enum": ["anonymous","member","gold","platinum","staff","admin"]},
      "role":      {"required": false, "maps_to": "role_key",   "description": "one of role_registry.roles[].key"},
      "tenant_roles": {"required": false, "type": "array<string>"},
      "assist_ctx":    {"required": false, "description": "object with target_tenant_id, consent_record_id, granted_at, expires_at, scope[], case_ref"},
      "view_as_ctx":   {"required": false, "description": "object with target_tenant_id, granted_at, expires_at"},
      "exp":       {"required": true,  "description": "expiry · short TTL recommended when assist/view-as is active"},
      "iat":       {"required": true},
      "iss":       {"required": false, "description": "issuer (IdP)"},
      "aud":       {"required": false, "description": "audience · defaults to 'pty-feature-flags' when validated"}
    },
    "jwks_url_env":    "POLICY_JWKS_URL",
    "public_key_env":  "POLICY_JWT_PUBLIC_KEY",
    "rotation_plan_deferred": "JWKS cache + rotation handling is Phase 2b+ infra · this contract reserves the env vars so the scaffold does not change shape when it lands"
  },

  "override_store_shape": {
    "file_location": "docs/runtime/feature-flags-service/override_examples.json (default · overridable via env FF_OVERRIDES_PATH)",
    "schema_ref":    "docs/runtime/feature-flags-service/override_store_schema.json",
    "load_policy":   "read once at service startup · no hot reload in this phase (deferred to Phase 2b+ alongside Redis)",
    "row_shape": {
      "id":          {"type": "string", "required": true, "example": "ovr-tenant-pty-zeroth-cases-runtime-v1"},
      "scope":       {"type": "enum",   "required": true, "values": ["tenant","user"]},
      "flag_key":    {"type": "string", "required": true},
      "tenant_id":   {"type": "string|null", "required_when": "scope == 'tenant' OR scope == 'user'"},
      "user_id":     {"type": "string|null", "required_when": "scope == 'user'"},
      "value":       {"type": "any",    "required": true, "must_match_flag_type": true},
      "expires_at":  {"type": "string|null", "format": "ISO-8601"},
      "approval_ref":{"type": "string|null", "description": "id from approval_queue · null for non-gated overrides"},
      "created_at":  {"type": "string", "format": "ISO-8601"},
      "created_by":  {"type": "string", "description": "actor user_id"},
      "source":      {"type": "enum",   "values": ["manual","api","migration","demo-seed"]},
      "rationale":   {"type": "string", "required": true, "description": "non-empty free-text · human-readable"}
    },
    "precedence_model": {
      "description_en": "When eval is requested, overrides from inline request.overrides still win (kept for backward-compat with Phase 2a examples). Store-backed overrides layer below that.",
      "order_top_to_bottom": [
        "1. inline request.overrides.user[flag_key] (Phase 2a semantics · wins over everything)",
        "2. inline request.overrides.tenant[flag_key]",
        "3. store user override matching (flag_key, user_id) with (expires_at null OR expires_at > now)",
        "4. store tenant override matching (flag_key, tenant_id) with (expires_at null OR expires_at > now)",
        "5. evaluator proceeds to staged bucketing / rollout_stage_map / default"
      ],
      "expired_rows_behavior": "treated as if absent · NOT auto-deleted in this phase · deletion/garbage-collection is Phase 2b+",
      "approval_ref_enforcement": "store rows MAY carry approval_ref · this scaffold does NOT verify the ref against an approval store (that is Phase 2b+ store-to-store integration) · UI should display the ref so humans can cross-check"
    }
  },

  "sensitive_gate_shape": {
    "endpoint": "POST /api/flags/sensitive-eval",
    "purpose": "Return a combined view: (a) sensitivity_profile from registry, (b) honest policy_hint WITHOUT calling the admin service, (c) flag_result if the gate is open. The cross-runtime demo remains the place where a REAL policy call happens.",
    "request_shape": {
      "context": "same as Phase 2a+ request context · user_id required",
      "flag_key": "string · must exist in registry.flags[]",
      "approval_refs": "array<string> · optional · consulted only for reporting (not verified)"
    },
    "response_data_shape": {
      "sensitivity_profile": {
        "sensitive_flag":          "bool (from flag.sensitive_flag)",
        "requires_approval":       "bool (from flag.requires_approval)",
        "rollout_stage":           "string",
        "policy_precheck_required": "bool (either of the above)"
      },
      "policy_hint": {
        "decision":           "enum · allow | deny-needs-approval | skipped",
        "required_approvers": "array<string> · role keys (from registry.approval_matrix_row lookup)",
        "approval_matrix_row":"string | null",
        "approvals_present":  "bool · true if request.approval_refs non-empty (NOT verified in this phase)",
        "honest_note":        "POLICY_HINT_ONLY · this does NOT call admin policy · use /runtime/cross-runtime-integration/ for a real policy-first chain"
      },
      "can_proceed_to_eval": "bool · (NOT policy_precheck_required) OR approvals_present",
      "flag_result":         "full eval result when can_proceed_to_eval=true · null otherwise",
      "warnings":            "array<string>"
    }
  },

  "read_paths": [
    {"method":"GET",  "path":"/api/flags/eval",              "status":"unchanged from 2a+", "note":"accepts Authorization Bearer · auth_source field added"},
    {"method":"POST", "path":"/api/flags/eval/batch",        "status":"unchanged from 2a+", "note":"same · Bearer-aware"},
    {"method":"GET",  "path":"/api/flags/registry",          "status":"unchanged from 2a+"},
    {"method":"GET",  "path":"/api/flags/health",            "status":"extended", "note":"health.data gains override_store_loaded bool + override_count int + auth_verification_live bool"},
    {"method":"GET",  "path":"/api/flags/overrides/by-flag/{flag_key}", "status":"NEW (Phase 2b)", "note":"read-only · returns [{store row}] matching the flag_key · expired rows flagged as such"},
    {"method":"POST", "path":"/api/flags/sensitive-eval",    "status":"NEW (Phase 2b)", "note":"see sensitive_gate_shape · side-effect-free"}
  ],

  "write_paths": {
    "status": "NONE live in Phase 2b",
    "reason": "Mutation (flag transitions, override CRUD, approval approvals) is deferred to Phase 2c. This phase stays READ-only to preserve the decision-service safety profile.",
    "planned_for_2c": [
      "POST /api/flags/{key}/transition",
      "POST /api/flags/overrides (create)",
      "DELETE /api/flags/overrides/{id}",
      "POST /api/flags/rollback"
    ]
  },

  "error_modes": {
    "auth_not_verified": {
      "http": 200,
      "severity": "warn",
      "how_surfaced": "service.auth_source='jwt_unverified' + service.warnings includes 'auth_not_verified'",
      "when": "Bearer provided but verification preconditions not all met (PyJWT missing OR POLICY_JWT_VERIFY!=true OR key missing OR signature invalid)",
      "operator_action": "treat as dev · do not rely on identity claims for prod decisions"
    },
    "override_store_unavailable": {
      "http": 200,
      "severity": "warn",
      "how_surfaced": "service.warnings includes 'override_store_unavailable' · health.data.override_store_loaded=false",
      "when": "FF_OVERRIDES_PATH file missing or JSON parse fails at startup",
      "operator_action": "check file path · restart service after fix"
    },
    "override_row_invalid": {
      "http": 200,
      "severity": "warn",
      "how_surfaced": "service.warnings includes 'override_row_invalid:<id>'",
      "when": "row fails shape validation (missing required field / value type mismatch)",
      "operator_action": "fix row in override_examples.json"
    },
    "unknown_flag_in_sensitive_eval": {
      "http": 200,
      "how_surfaced": "data.flag_result.source='unknown_flag' · sensitivity_profile nullish · can_proceed_to_eval=false",
      "when": "flag_key not in registry · same semantics as existing evaluator",
      "operator_action": "fix request flag_key"
    },
    "invalid_request": {"http": 400, "status": "unchanged from 2a+"},
    "models_unavailable": {"http": 503, "note": "not applicable to FF service · registry is its source of truth"},
    "registry_unavailable": {"http": 503, "status": "unchanged from 2a+"}
  },

  "observability": {
    "phase_2b_reality": "stdout JSON logs per request (unchanged shape from 2a+) · event enum gains 'overrides_read' and 'sensitive_eval'",
    "fields_added_to_log": ["auth_source", "override_hit (bool)", "override_row_id (string|null)"],
    "metrics_deferred": ["prometheus export · OTel tracing · remain Phase 2b+ infra"]
  },

  "non_goals_phase_2b": [
    "Real JWT verification via JWKS / IdP at runtime (contract reserves env vars · implementation depends on PyJWT install + infra)",
    "Postgres-backed override persistence (scaffold is file-backed)",
    "Redis cache / pubsub invalidation",
    "Flag flip / transition write API (Phase 2c)",
    "Override CRUD API (Phase 2c)",
    "WORM audit sink (Phase 2b+ infra)",
    "Rate limiting / quota enforcement",
    "Admin Kanban UI",
    "Hot reload of registry or override store on SIGHUP (deferred)",
    "Cross-service policy call from inside this service (belongs to cross-runtime-integration demo)"
  ],

  "deferred_matrix": [
    {"item": "JWT verification (JWKS/IdP)",       "deferred": true,  "current": "decode-only in dev · verification gated by 3 env/install preconditions", "why": "depends on IdP integration + key rotation infra", "next_phase": "Phase 2b+ (infra)"},
    {"item": "fallback dev X-PTT-* headers",      "deferred": false, "current": "LIVE and intentional", "why": "local-dev ergonomics", "next_phase": "retire when real JWT verification is on in prod"},
    {"item": "override persistence backend",      "deferred": true,  "current": "file-backed JSON loaded at startup · no writes", "why": "Postgres not yet provisioned for this surface", "next_phase": "Phase 2c"},
    {"item": "Redis / cache invalidation",        "deferred": true,  "current": "no cache layer", "why": "infra not wired", "next_phase": "Phase 2b+ infra"},
    {"item": "approval store backing",            "deferred": true,  "current": "approval_ref on override rows is display-only (not verified)", "why": "belongs to Admin Phase 2b", "next_phase": "Admin Phase 2b"},
    {"item": "sensitive WRITE operations",        "deferred": true,  "current": "scaffold is READ-only", "why": "deliberate decision-service profile", "next_phase": "Phase 2c"},
    {"item": "real flag transition API",          "deferred": true,  "current": "not implemented", "why": "requires rollout_model state machine implementation + audit chain", "next_phase": "Phase 2c"},
    {"item": "audit write / WORM sink",           "deferred": true,  "current": "stdout logs only", "why": "Kafka ptt.audit.trail producer not wired", "next_phase": "Phase 2b+ infra"},
    {"item": "rate limiting",                     "deferred": true,  "current": "none", "why": "dev-mode binding to 127.0.0.1", "next_phase": "Phase 2b+ infra"},
    {"item": "production deployment hardening",   "deferred": true,  "current": "service runs on localhost only", "why": "dev-mode by design", "next_phase": "deployment phase"}
  ],

  "phase_2b_deliverables": [
    "phase_2b_contract.json (this file)",
    "override_store_schema.json (JSON-Schema draft-07 for override rows)",
    "override_examples.json (8 seed rows covering tenant · user · TTL · approval_ref precedence)",
    "app.py extended append-first (Bearer resolver + file-backed store loader + 2 new endpoints)",
    "phase-2b.html (runtime debug surface · JWT section · store section · sensitive-gate section · 4 use cases)",
    "docs/planning/feature-flags-phase-2b.html (planning rationale + delta + DoD)",
    "Catalog entries + cross-links + SYNC append"
  ]
}
