{
  "schema_version": "1.0",
  "baseline": "A-runtime-phase-2a",
  "phase": "Phase 2a",
  "updated_at": "2026-04-18",
  "owner": "session_a",
  "honest_note": "Evaluator contract + deterministic JS module are LIVE (code runs · examples reproducible). No HTTP service · no Redis cache · no pubsub · no production traffic. Module is client-side + Node-loadable; backend FastAPI binding is Phase 2a+.",

  "contract_refs": [
    "docs/runtime/feature-flags/registry.json (source of truth)",
    "docs/runtime/feature-flags/evaluation_model.json (9-step order)",
    "docs/runtime/feature-flags/rollout_model.json (7-state machine)",
    "docs/runtime/admin-control-plane/role_registry.json (tier + role source)"
  ],

  "purpose_th": "สัญญาของ evaluator service · นิยาม input/output/trace · fail-safe behavior · precedence",
  "purpose_en": "Evaluator service contract · defines request/response/trace/fail-safe/precedence",

  "transport": {
    "phase_2a_reality": "Pure JavaScript module — runs in browser + Node. Reads registry.json directly. No HTTP boundary yet.",
    "phase_2a_plus_plan": "FastAPI service at /api/flags/eval · same contract · adds Redis cache + pubsub invalidation",
    "module_entrypoint": "evaluator.js → FeatureFlagEvaluator.evaluate(registry, request)",
    "example_node": "const FF = require('./evaluator.js'); const r = FF.evaluate(registry, {flag_key:'cases.runtime_v1', tenant_id:'pty-zeroth', user_id:'U-1', env:'prod', tier:'staff'});",
    "example_browser": "<script src=\"evaluator.js\"></script> → FeatureFlagEvaluator.evaluate(registry, request)"
  },

  "request_shape": {
    "description_en": "Every evaluation takes a registry snapshot + a request context.",
    "fields": {
      "flag_key":   { "type": "string",  "required": true,  "description": "Dotted key · must exist in registry.flags[*].key to return non-default" },
      "tenant_id":  { "type": "string",  "required": false, "description": "Tenant context · null for platform-level evaluations" },
      "user_id":    { "type": "string",  "required": true,  "description": "Stable id · used for deterministic bucketing in staged rollout" },
      "env":        { "type": "enum",    "required": true,  "values": ["prod", "staging", "dev"] },
      "tier":       { "type": "enum",    "required": true,  "values": ["anonymous", "member", "gold", "platinum", "staff", "admin"], "description": "Matches CLAUDE.md RBAC tier · role_registry.jwt_tier_hint" },
      "role_key":   { "type": "string",  "required": false, "description": "Optional role from role_registry · platform vs tenant distinction" },
      "now_iso":    { "type": "string",  "required": false, "description": "ISO timestamp for TTL checks · defaults to evaluator clock" },
      "overrides":  { "type": "object",  "required": false, "description": "Phase 2a+: Redis-backed override lookup · Phase 2a current = must pass inline for simulation" }
    }
  },

  "response_shape": {
    "description_en": "Deterministic result + full trace of which rules fired.",
    "fields": {
      "flag_key":       { "type": "string" },
      "value":          { "type": "any", "description": "boolean | string | number · matches flag.type" },
      "source":         { "type": "enum", "values": ["default", "rollout_pct", "tenant_override", "user_override", "stage-internal", "stage-pilot", "stage-full", "paused", "rolled_back", "dep_unsatisfied", "approval_missing", "unknown_flag", "error"] },
      "stage":          { "type": "string", "description": "Rollout stage at evaluation time" },
      "bucket":         { "type": "integer", "nullable": true, "description": "0..99 · only when source=rollout_pct" },
      "cached":         { "type": "boolean", "description": "Phase 2a: always false · no cache layer yet" },
      "deps_evaluated": { "type": "array",  "description": "Recursively evaluated dependencies · each carries { flag_key, value, satisfied }" },
      "trace":          { "type": "array<string>", "description": "Step-by-step rule firing log · ordered" },
      "evaluated_at":   { "type": "string", "description": "ISO timestamp of evaluation" },
      "evaluator_version": { "type": "string", "const": "2a-js-module" }
    }
  },

  "evaluation_order": [
    { "step": 1, "name": "flag_exists_check",      "on_fail": "return { value:false, source:'unknown_flag' }" },
    { "step": 2, "name": "rollout_stage_short",    "short_circuit_when": "stage in {paused, rolled_back}", "on_short": "return { value:default_value, source:stage }" },
    { "step": 3, "name": "dependency_check",       "cycle_guard": "max depth 8 · visited set", "on_fail": "return { value:default_value, source:'dep_unsatisfied' }" },
    { "step": 4, "name": "approval_gate",          "fires_when": "flag.requires_approval === true && audit.last_approval_ref == null", "on_fire": "return { value:default_value, source:'approval_missing' }" },
    { "step": 5, "name": "user_override_lookup",   "on_hit": "return { value:override, source:'user_override' }" },
    { "step": 6, "name": "tenant_override_lookup", "on_hit": "return { value:override, source:'tenant_override' }" },
    { "step": 7, "name": "cohort_bucketing",       "fires_when": "stage === 'staged'", "logic": "hash(user_id + '|' + flag_key) mod 100 < rollout_pct", "on_fire": "return { value:bucket_result, source:'rollout_pct', bucket }" },
    { "step": 8, "name": "rollout_stage_map",      "draft": "default_value", "internal": "true iff tier in {staff, admin}", "pilot": "true (cohort check deferred to 2b)", "full": "true" },
    { "step": 9, "name": "default_return",         "description": "Fallback to flag.default_value · source='default'" }
  ],

  "precedence_rules": [
    { "id": "pr-1", "rule": "Paused/rolled_back short-circuits BEFORE overrides · overrides cannot bypass safety circuit" },
    { "id": "pr-2", "rule": "User override beats tenant override beats rollout stage beats default" },
    { "id": "pr-3", "rule": "Approval missing returns default regardless of stage or override (no governance bypass)" },
    { "id": "pr-4", "rule": "Dependency unsatisfied returns default (no inconsistent state)" },
    { "id": "pr-5", "rule": "Unknown flag returns false + source='unknown_flag' + alertable metric" }
  ],

  "error_behavior": {
    "registry_invalid":    "return { value:false, source:'error', trace:['registry did not parse'] }",
    "request_invalid":     "return { value:false, source:'error', trace:['request missing required field'] }",
    "dependency_cycle":    "break at depth 8 · log warning · treat as unsatisfied (returns default_value · source='dep_unsatisfied')",
    "registry_unavailable":"Phase 2a+ · at HTTP service layer · return default for all flags + P1 alert",
    "evaluator_exception": "catch-all · return { value:false, source:'error', trace:[error.message] }"
  },

  "determinism_guarantees": [
    "Given same (registry, request) → always same (value, source, bucket)",
    "Hash function for bucketing must be stable across evaluator versions (documented in evaluator.js · tested in evaluator_examples.json)",
    "No Math.random() · no Date.now() except for TTL checks (passed via request.now_iso)",
    "Trace order identical for identical inputs"
  ],

  "http_surface_plan_2a_plus": {
    "description_en": "Planned FastAPI endpoints · NOT live in Phase 2a · documented for contract stability",
    "endpoints": [
      {
        "method": "GET",
        "path":   "/api/flags/eval",
        "query":  "key=<flag_key>&tenant=<id>&user=<id>&env=<env>",
        "auth":   "JWT bearer · extracts tier/role/tenant from claims",
        "response_status": { "200": "successful evaluation", "400": "invalid request", "503": "registry unavailable · returns default with degraded flag" }
      },
      {
        "method": "POST",
        "path":   "/api/flags/eval/batch",
        "body":   "{ flags: [string] }",
        "description": "Evaluate multiple flags in one round-trip · client-side common pattern"
      },
      {
        "method": "GET",
        "path":   "/api/flags/registry",
        "auth":   "admin-tier or staff+ with read scope",
        "description": "Read-only registry snapshot · for tooling"
      }
    ],
    "cache_plan": "Redis 60s TTL · key = ff:<flag_key>:<tenant_id>:<user_id> · pubsub-invalidated on flag flip"
  },

  "non_goals_phase_2a": [
    "HTTP service (defer to 2a+ · same contract)",
    "Redis cache layer (defer to 2a+)",
    "Pubsub invalidation (defer to 2a+)",
    "Override tables (flag_tenant_overrides · flag_user_overrides) — defer to 2b",
    "Transition API (PATCH / rollback) — defer to 2c",
    "Admin flip UI / kanban — defer to Phase 3",
    "IdP / JWT issuance — defer to 2b"
  ],

  "phase_2a_deliverables": [
    "evaluator_contract.json (this file)",
    "evaluator_examples.json (canonical test vectors)",
    "evaluator.js (pure JS module · browser + Node)",
    "evaluator.html (dedicated debug page · contract viewer + request composer + examples runner)"
  ]
}
