{
  "$comment": [
    "BE↔FE enum-label contract — the single source of truth for 'does every",
    "value this backend enum can emit have a human label in all three",
    "languages?'.",
    "",
    "WHY THIS FILE EXISTS: the frontend renders backend enums through dynamic",
    "lookups — `t(`ns.${value}`)`. Two guards already cover part of that:",
    "`check-i18n-translation-parity.mjs` (was it translated?) and",
    "`check-i18n-dynamic-namespaces.mjs` (does the namespace exist at all?).",
    "Neither can see PER-VALUE coverage, because the value is only known at",
    "runtime. So a backend enum could grow a member and the new value would",
    "silently render as the raw token — or, at the sites that pass no",
    "fallback, as the raw i18n key.",
    "",
    "That is not hypothetical. The 2026-07-30 audit found nine live gaps:",
    "  - `leaver_principal_swept` + `scope_closed` — both actively written to",
    "    `revoke_reason` (the AWS Identity Center leaver sweep and the",
    "    scope-close path) and shown as raw tokens in the grant-history",
    "    「结束原因」column.",
    "  - four of fourteen `myRequests.source` values (`permission`, `sso`,",
    "    `identity_consumer`, `support`) — and two of those render sites pass",
    "    NO fallback, so the employee 'my requests' list and the manager",
    "    'team requests' page printed the literal key `myRequests.source.sso`.",
    "  - `no_response` — written by the certification auto-close UPDATE, also",
    "    a no-fallback site.",
    "  - `manual_provisioned` on the onboarding blocker resolution.",
    "  - `usage` — the base bucket of the cost waterfall.",
    "",
    "TWO TESTS CONSUME THIS FILE, one per language (same shape as",
    "contracts/mobile-deep-links.json):",
    "  - server/tests/test_i18n/test_enum_label_contract.py — imports each",
    "    `module` and asserts the live constants under `const_prefix` are",
    "    EXACTLY `values`. Adding an enum member therefore fails CI until the",
    "    author records it here, which is the moment they also have to decide",
    "    on a label.",
    "  - web/src/i18n/enumLabels.contract.test.ts — asserts every value has a",
    "    key under every listed namespace, in zh-CN, en-US and ja-JP.",
    "",
    "HOW TO ADD AN ENUM: pick a `const_prefix` that selects exactly the",
    "members you mean (`DNS_REQ_ACTION_`, not `DNS_REQ_`, which would also",
    "sweep up the statuses). List every namespace that renders it. If a value",
    "is deliberately never shown to a user, put it in `unlabelled` with a",
    "reason — the frontend test skips those, and the reason is the review",
    "record for why."
  ],
  "enums": [
    {
      "id": "auditor.certification_decision",
      "module": "app.domains.auditor.access_certification_service",
      "const_prefix": "DECISION_",
      "values": [
        "keep",
        "no_response",
        "pending",
        "revoke"
      ],
      "namespaces": [
        "compliance.cert.decisionValue"
      ],
      "notes": "CertificationDetailPage renders this with no fallback, in a Tag and in a filter option list."
    },
    {
      "id": "auditor.signin_anomaly_reason",
      "module": "app.domains.auditor.constants",
      "const_prefix": "ANOMALY_REASON_",
      "values": [
        "headless_ua",
        "impossible_travel",
        "known_bad_ip",
        "login_after_offboard",
        "new_country",
        "new_device",
        "rapid_failed_attempts",
        "unusual_hour"
      ],
      "namespaces": [
        "auditor.signin.reason"
      ],
      "notes": "Rendered as Tags on the sign-in anomaly page. Added 2026-07-31 with ANOMALY_REASON_HEADLESS_UA, which had been mislabelled as new_device (#2174)."
    },
    {
      "id": "cloud.certificate_owner_source",
      "module": "app.domains.cmdb.cert_owner_resolver",
      "const_prefix": "OWNER_SOURCE_",
      "values": [
        "dns_record",
        "domain",
        "manual",
        "network",
        "none"
      ],
      "namespaces": [
        "cloudCertificates.ownerSource"
      ],
      "notes": "CloudCertificatesPage renders this at three sites, all WITHOUT a fallback — a missing value would print the literal i18n key. Verified complete on 2026-07-30; contracted so it stays that way."
    },
    {
      "id": "cloud.dns_request_action",
      "module": "app.domains.cmdb.models",
      "const_prefix": "DNS_REQ_ACTION_",
      "values": [
        "create",
        "delete",
        "disable",
        "enable",
        "update"
      ],
      "namespaces": [
        "networkRequests.dns.action"
      ]
    },
    {
      "id": "cloud.grant_revoke_reason",
      "module": "app.domains.cmdb.models",
      "const_prefix": "GRANT_REVOKE_REASON_",
      "values": [
        "admin_removed",
        "drift_removed",
        "expired",
        "iam_user_disowned",
        "inactive",
        "leaver_principal_swept",
        "offboarding",
        "orphan_principal_swept",
        "owner_removed",
        "scope_closed",
        "user_released"
      ],
      "namespaces": [
        "cloudLifecycle.grants.endedReason"
      ],
      "notes": "Rendered in the grant-history 「结束原因」 column with a raw-value fallback. The namespace also carries `scope_decommissioned` (written as a literal in lifecycle_service) and `unknown` (a frontend-side default when both ended_reason and revoke_reason are null); neither is a constant here, so the frontend test checks coverage in one direction only — every constant must have a label, extra labels are fine."
    },
    {
      "id": "cloud.scope_request_status",
      "module": "app.domains.cmdb.models",
      "const_prefix": "SCOPE_REQUEST_STATUS_",
      "values": [
        "failed",
        "feishu_approved",
        "feishu_sent",
        "pending",
        "provisioned",
        "provisioning",
        "rejected",
        "withdrawn"
      ],
      "namespaces": [
        "cloudLifecycle.scopeRequests.status"
      ]
    },
    {
      "id": "cloud.scope_type",
      "module": "app.domains.cmdb.models",
      "const_prefix": "SCOPE_TYPE_",
      "values": [
        "aliyun_account",
        "aws_account",
        "azure_resource_group",
        "azure_subscription",
        "gcp_billing_account",
        "gcp_project",
        "legacy_billing_unit",
        "standalone_iam"
      ],
      "namespaces": [
        "cloudLifecycle.mySubscriptions.scopeType"
      ],
      "notes": "The admin 「按类型」 chart groups over cmdb_cloud_scopes.scope_type with no tenant-level filter, so every value here reaches the UI — azure_resource_group and gcp_billing_account were missing labels until 2026-07-30."
    },
    {
      "id": "finance.cost_bucket",
      "module": "app.integrations.driver_base",
      "const_prefix": "COST_BUCKET_",
      "values": [
        "adjustment",
        "commitment_discount",
        "commitment_fee",
        "contract_discount",
        "credit",
        "other",
        "support",
        "tax",
        "usage"
      ],
      "namespaces": [
        "finance.waterfall.bucket"
      ],
      "notes": "CloudCostWaterfallPage deliberately excludes `usage` from its columns (a waterfall starts from usage and shows deltas), but CostMethodologyTab's bucketLabel() renders whatever the backend returns — so `usage` needs a label too."
    },
    {
      "id": "me.request_source",
      "module": "app.domains.me_universe.my_requests",
      "const_prefix": "SRC_",
      "values": [
        "ai_tool",
        "announcement",
        "apikey_release",
        "cloud_grant",
        "cloud_scope",
        "dns",
        "domain",
        "email_group_join",
        "employee_request",
        "identity_consumer",
        "permission",
        "sso",
        "support",
        "vpn"
      ],
      "namespaces": [
        "myRequests.source"
      ],
      "notes": "MyRequestsTracker and TeamRequestsPage render this with NO fallback, so a missing key shows the literal i18n key to the user. Highest-severity namespace in this contract."
    },
    {
      "id": "offboarding.process_status",
      "module": "app.domains.offboarding.models",
      "const_prefix": "PROC_",
      "values": [
        "blocked",
        "cancelled",
        "completed",
        "ready",
        "scanning"
      ],
      "namespaces": [
        "offboardingAdmin.status"
      ]
    },
    {
      "id": "offboarding.sla_level",
      "module": "app.domains.offboarding.models",
      "const_prefix": "SLA_",
      "values": [
        "breach",
        "none",
        "urgent",
        "warn"
      ],
      "namespaces": [
        "offboardingCockpit.sla",
        "leaverBlocked.sla"
      ]
    },
    {
      "id": "onboarding.blocker_resolution",
      "module": "app.domains.onboarding.models",
      "const_prefix": "RESO_",
      "values": [
        "identity_sync_managed",
        "manual_provisioned",
        "pending",
        "provisioned",
        "skipped"
      ],
      "namespaces": [
        "onboarding.resolution"
      ]
    },
    {
      "id": "onboarding.process_status",
      "module": "app.domains.onboarding.models",
      "const_prefix": "PROC_",
      "values": [
        "cancelled",
        "completed",
        "live",
        "preparing",
        "ready",
        "scanning"
      ],
      "namespaces": [
        "onboardingDetail.status"
      ]
    },
    {
      "id": "onboarding.sla_level",
      "module": "app.domains.onboarding.models",
      "const_prefix": "SLA_",
      "values": [
        "breach",
        "none",
        "urgent",
        "warn"
      ],
      "namespaces": [
        "onboardingDetail.sla"
      ]
    },
    {
      "id": "support.kb_status",
      "module": "app.domains.support.models",
      "const_prefix": "KB_SOURCE_",
      "values": [
        "ai_ticket",
        "manual"
      ],
      "namespaces": [],
      "unlabelled": {
        "ai_ticket": "Rendered by a hardcoded ternary in SupportKbPage (support.kb.sourceAi / support.kb.sourceManual), not a dynamic namespace lookup — there is no `support.kb.source.*` namespace to cover.",
        "manual": "Same as ai_ticket."
      },
      "notes": "Contracted so that adding a THIRD KB source fails CI: the ternary would silently render it as 'manual'."
    }
  ]
}
