{
  "schema_version": "1.0",
  "baseline": "A-runtime-cross-integration-demo-v1",
  "phase": "Cross-runtime demo · local/dev only",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "honest_note": "Browser-side orchestration demo that chains TWO already-running A-owned local services (Admin Policy Engine port 8090, Feature Flags Service port 8080) with a policy-first · eval-second rule. This is a DEMO page · there is no new backend service · no JWT · no audit sink · no mutation · no persistence. If either service is not running locally, the demo honestly surfaces a connection error. The chain adds NO capability beyond what the two services already offer individually — its value is making the combined flow legible and testable for operators.",

  "contract_refs": [
    "docs/runtime/feature-flags-service/http_contract.json (4 endpoints · port 8080 · evaluator_version 2a_plus-py-port)",
    "docs/runtime/admin-control-plane-service/policy_contract.json (7 endpoints · port 8090 · engine_version 2a-py-policy-engine)",
    "docs/runtime/feature-flags/registry.json (16 flags · 7 sensitive · 14 requires_approval · 15 draft + 1 internal)",
    "docs/runtime/admin-control-plane/access_policy.json (masked-by-default · 8 precedence rules)",
    "docs/runtime/admin-control-plane/role_registry.json (12 roles · 4 categories)",
    "docs/kb/data/approval_matrix.json (B-owned · read-only · row-sensitive-override source)"
  ],

  "purpose_th": "สัญญาของ demo ที่เรียก 2 service ของจริง: ตรวจ policy ก่อน แล้วค่อย eval flag · รวมผลเป็นมุมมองเดียว",
  "purpose_en": "Contract for a demo that calls 2 real services sequentially: policy first, then flag eval · unified view",

  "transport": {
    "protocol": "HTTP/1.1 · JSON · UTF-8",
    "execution_model": "browser-side fetch chain · NO new server · NO proxy",
    "default_targets": {
      "policy_service":   "http://127.0.0.1:8090",
      "flags_service":    "http://127.0.0.1:8080"
    },
    "bases_configurable_in_ui": true,
    "cors_dev": "both services serve Access-Control-Allow-Origin: * in dev · OK to fetch from file:// or any localhost"
  },

  "request_context_shape": {
    "description_en": "Single form captures actor + target + flag_key · splits into the two services' request shapes deterministically.",
    "fields": {
      "actor_role":       { "type": "string", "required": true,  "source": "role_registry.roles[].key" },
      "actor_user_id":    { "type": "string", "required": true },
      "actor_tenant_id":  { "type": "string", "required": false },
      "target_tenant_id": { "type": "string", "required": false },
      "target_user_id":   { "type": "string", "required": false },
      "flag_key":         { "type": "string", "required": true,  "source": "registry.flags[].key" },
      "requested_action": { "type": "enum",   "values": ["read","write","export"], "default": "read" },
      "approval_refs":    { "type": "array<string>", "required": false },
      "assist_ctx":       { "type": "object", "required": false },
      "view_as_ctx":      { "type": "object", "required": false },
      "env":              { "type": "enum",   "values": ["prod","staging","dev"], "default": "prod" },
      "tier":             { "type": "enum",   "values": ["anonymous","member","gold","platinum","staff","admin"], "default": "staff" },
      "now_iso":          { "type": "string", "required": false, "description": "pin for deterministic TTL math in demos" }
    }
  },

  "orchestration_steps": [
    {
      "step": 1,
      "name": "derive_sensitivity",
      "action": "Look up the flag in the browser-loaded registry.json to determine flag.sensitive_flag and flag.requires_approval. This is an IN-PAGE lookup · no service call yet.",
      "determines": "policy_precheck_required (true if flag.sensitive_flag OR flag.requires_approval)"
    },
    {
      "step": 2,
      "name": "policy_precheck",
      "endpoint": {
        "method": "POST",
        "path": "/api/policy/sensitive/check",
        "service": "policy_service",
        "body_shape": {
          "context": "request_context_shape subset (actor · target · approval_refs · now_iso)",
          "field_category": "flag_key treated as the field category indicator · demo maps to 'admin_flag_access'",
          "requested_action": "from request_context_shape"
        },
        "fallback_endpoint": {
          "note": "When flag.sensitive_flag is false but flag.requires_approval is true, the demo calls /api/policy/access/check instead of /api/policy/sensitive/check",
          "method": "GET", "path": "/api/policy/access/check"
        }
      },
      "on_ok":   "policy decision = allow → proceed to step 3",
      "on_deny": "policy decision = deny → SHORT-CIRCUIT · skip step 3 · surface reasons + required_approvers + approval_matrix_row"
    },
    {
      "step": 3,
      "name": "flag_eval",
      "runs_when": "step 2 decision == allow OR policy_precheck_required == false",
      "endpoint": {
        "method": "GET",
        "path": "/api/flags/eval",
        "service": "flags_service",
        "query_from_context": ["flag_key as key", "user_id", "tenant_id", "env", "tier"]
      },
      "on_ok": "flag evaluator returns {value, source, stage, bucket, trace, evaluator_version}",
      "on_fail": "surface the error envelope · do NOT swallow"
    },
    {
      "step": 4,
      "name": "combine",
      "action": "Build combined_response from the two real payloads. No fabrication · every field maps 1:1 to an actual service response field."
    }
  ],

  "policy_precheck_shape": {
    "description_en": "Subset of Admin Policy Service response surfaced in the demo.",
    "fields": {
      "decision":        "enum · allow | mask | deny (from /access/check) OR allow | deny (from /sensitive/check)",
      "reasons":         "array<{id, text, rule_ref?}>",
      "required_approvers":      "array<string> · role keys",
      "approval_matrix_row":     "string · typically row-sensitive-override · nullable",
      "current_approvals_satisfy": "boolean · from /sensitive/check",
      "tenant_relation": "enum · own | cross | unknown | platform-global (from /access/check)",
      "self_view":       "boolean · from /access/check",
      "trace":           "array<string> · ordered step log",
      "service_version": "string",
      "engine_version":  "string"
    }
  },

  "flag_eval_shape": {
    "description_en": "Subset of Feature Flags Service response surfaced in the demo.",
    "fields": {
      "flag_key":          "string",
      "value":             "any · boolean | string | number",
      "source":            "enum · default | rollout_pct | tenant_override | user_override | stage-internal | stage-pilot | stage-full | paused | rolled_back | dep_unsatisfied | approval_missing | unknown_flag | error",
      "stage":             "string · flag.rollout_stage at eval time",
      "bucket":            "integer | null · 0..99 when source=rollout_pct",
      "cached":            "boolean · always false in this phase",
      "deps_evaluated":    "array · recursive dependency trace",
      "trace":             "array<string>",
      "evaluated_at":      "ISO timestamp",
      "evaluator_version": "string · 2a_plus-py-port"
    }
  },

  "combined_response_shape": {
    "description_en": "What the demo panel renders after steps 1..4.",
    "fields": {
      "flag_key":              "echo from request",
      "sensitivity_profile":   { "sensitive_flag": "bool", "requires_approval": "bool", "rollout_stage": "string", "policy_precheck_required": "bool" },
      "policy_gate_status":    "enum · allow | deny | skipped (when policy_precheck_required=false) | error",
      "can_proceed_to_eval":   "boolean",
      "flag_eval_status":      "enum · evaluated | short_circuited_by_policy | error · not_run",
      "policy_result":         "policy_precheck_shape | null",
      "flag_result":           "flag_eval_shape | null",
      "combined_trace":        "array<string> · interleaved trace with service prefix (POLICY/ FLAGS/)",
      "honest_limits":         "array<string> · echoes non_goals + deferred verbatim so operator cannot confuse demo with production"
    }
  },

  "error_modes": {
    "policy_service_unreachable": {
      "detection": "fetch to policy base URL throws TypeError or timeout",
      "ui_surface": "red banner · 'Policy service not reachable at {base}' · no silent success · can_proceed_to_eval = false",
      "action": "operator should run `bash docs/runtime/admin-control-plane-service/run.sh`"
    },
    "flags_service_unreachable": {
      "detection": "fetch to flags base URL throws",
      "ui_surface": "red banner · 'Flags service not reachable at {base}' · flag_eval_status = error",
      "action": "operator should run `bash docs/runtime/feature-flags-service/run.sh`"
    },
    "invalid_request_context": {
      "detection": "missing required field in request_context_shape",
      "ui_surface": "inline validation · submit disabled"
    },
    "unknown_flag_key": {
      "detection": "flag_key not in loaded registry.flags[]",
      "ui_surface": "warning banner · policy precheck still runs with field_category='admin_flag_access' · flag_eval will return source='unknown_flag'",
      "action": "operator picks a key from the dropdown"
    },
    "registry_load_failure": {
      "detection": "fetch of /runtime/feature-flags/registry.json fails",
      "ui_surface": "cannot determine sensitivity profile · policy_precheck_required defaults to TRUE (fail-safe)",
      "action": "operator should verify registry.json path"
    }
  },

  "observability_note": "Every request/response pair is visible in the browser devtools Network tab. Both services emit stdout JSON logs (see each service's README). No telemetry backend in this phase.",

  "determinism_note": "Policy engine decisions are deterministic given (models, context, now_iso). Flag evaluations are deterministic given (registry, request). If the operator supplies now_iso in the form, TTL-sensitive paths (assist/view-as) give identical outputs across runs. Without now_iso the policy engine uses the server clock at request time.",

  "non_goals": [
    "No new backend service · the demo is a browser page calling two existing services",
    "No auth · dev-mode X-PTT-* headers and query params only",
    "No writes · neither policy nor flag state is mutated",
    "No audit sink · no Kafka · no WORM",
    "No approval CRUD · approval_refs are taken at face value",
    "No cross-tenant escalation paths beyond what assist_model + access_policy already define",
    "No production claim · explicitly labeled local/dev demo"
  ],

  "deferred": [
    {"item": "JWT / IdP",                    "why": "depends on IdP integration · Phase 2b",      "next_phase": "Admin Phase 2b + FF Phase 2b"},
    {"item": "approval store backing",       "why": "requires Postgres approval_queue persistence", "next_phase": "Admin Phase 2b"},
    {"item": "flag transition / flip API",   "why": "mutation path not scaffolded",                "next_phase": "FF Phase 2c"},
    {"item": "WORM audit sink",              "why": "Kafka ptt.audit.trail topic + producer not wired", "next_phase": "Admin Phase 2b+"},
    {"item": "Redis invalidation / pubsub",  "why": "cache layer not built",                       "next_phase": "FF Phase 2a+ infra"},
    {"item": "admin mutation UI / Kanban",   "why": "out of scope for decision-service demo",      "next_phase": "Admin Phase 3"},
    {"item": "server-side orchestration service", "why": "browser chain sufficient · no Gateway authority required for a demo", "next_phase": "when middleware layer is consolidated"},
    {"item": "real rollout persistence",     "why": "registry.json is file-backed only",           "next_phase": "FF Phase 2c"}
  ],

  "surface_inventory_expected": [
    "docs/runtime/cross-runtime-integration/demo_contract.json (this file)",
    "docs/runtime/cross-runtime-integration/index.html (demo surface · composer + 2 panels + combined view)",
    "docs/runtime/cross-runtime-integration/README.md (operator usage · deny/allow explanation)",
    "docs/planning/cross-runtime-integration.html (IA rationale · DoD · what-this-is-not)"
  ]
}
