{"record":{"id":"4035a71370a7c54e","repo":"ZhuLinsen/daily_stock_analysis","slug":"field-name-must-be-one-of-allowed-text-4035a7","errorCode":null,"errorMessage":"{field_name} must be one of {allowed_text}","messagePattern":"(.+?) must be one of (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_service.py","lineNumber":1164,"sourceCode":"    @staticmethod\n    def _normalize_action(value: Any) -> str:\n        action = str(value or \"\").strip().lower()\n        if not action or action not in DECISION_ACTIONS:\n            raise ValueError(\"action must be one of buy/add/hold/reduce/sell/watch/avoid/alert\")\n        return action\n\n    @classmethod\n    def _normalize_optional_action(cls, value: Any) -> Optional[str]:\n        if value in (None, \"\"):\n            return None\n        return cls._normalize_action(value)\n\n    @staticmethod\n    def _normalize_enum(value: Any, allowed: frozenset[str], field_name: str) -> str:\n        text = str(value or \"\").strip()\n        if text not in allowed:\n            allowed_text = \", \".join(sorted(allowed))\n            raise ValueError(f\"{field_name} must be one of {allowed_text}\")\n        return text\n\n    @classmethod\n    def _normalize_optional_enum(\n        cls,\n        value: Any,\n        allowed: frozenset[str],\n        field_name: str,\n    ) -> Optional[str]:\n        if value in (None, \"\"):\n            return None\n        return cls._normalize_enum(value, allowed, field_name)\n\n    @staticmethod\n    def _normalize_trigger_source(value: Any) -> str:\n        text = DecisionSignalService._public_text(value, \"trigger_source\", max_length=64, required=True)\n        if not text:\n            raise ValueError(\"trigger_source is required\")","sourceCodeStart":1146,"sourceCodeEnd":1182,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_service.py#L1146-L1182","documentation":"Generic enum validator ValueError from DecisionSignalService._normalize_enum (src/services/decision_signal_service.py:1164): used for fields like status, market_phase, horizon, decision_profile. The raw value is str()-ed, stripped (NOT lowercased), and must be an exact member of the allowed frozenset; otherwise '{field_name} must be one of {sorted list}' is raised. Case errors survive because only whitespace is trimmed.","triggerScenarios":"Payloads with market_phase: 'Bull' (must be the exact token, e.g. 'bull'... check MARKET_PHASES spelling), status: 'Active' (capital A fails — only stripped, not lowercased), horizon: 'short term' vs 'short_term', decision_profile: 'aggressive ' (ok) vs 'AGGRESSIVE' (fails). Which enum is hit depends on the field being normalized.","commonSituations":"API consumers assuming case-insensitive enums; frontend select components exporting labels instead of values; prompt templates listing display names ('Short-term') that get persisted verbatim; enum extension without notifying clients.","solutions":["Read the exact allowed tokens from the error message (it lists them sorted) and resend with an exact match including case.","Lowercase/normalize the value client-side before the call where the enum is lowercase.","Bind frontend dropdowns to the enum values, not localized labels.","For LLM-produced fields, validate against the enum in the extractor and re-ask/fallback rather than persisting raw text."],"exampleFix":"# before\nservice.update_signal_status(signal_id, status=\"Active\")  # not lowercased → ValueError: status must be one of ...\n\n# after\nstatus = \"Active\".strip().lower()\nservice.update_signal_status(signal_id, status=status)","handlingStrategy":"type-guard","validationCode":"def check_enum(value, allowed: frozenset, field: str):\n    text = str(value or '').strip()\n    if text not in allowed:\n        raise ValueError(f'{field} must be one of {sorted(allowed)}')\ncheck_enum(payload.get('status'), SIGNAL_STATUSES, 'status')","typeGuard":"def make_enum_guard(allowed: frozenset):\n    return lambda v: str(v or '').strip() in allowed\nis_valid_horizon = make_enum_guard(HORIZONS)","tryCatchPattern":null,"preventionTips":["Enums are exact-match after strip only — match case exactly; lowercase client-side for lowercase enums.","Bind UI dropdowns to enum values, not display labels.","Validate LLM-produced enum fields in the extractor and retry/fallback on mismatch."],"tags":["decision-signal","validation","enum","case-sensitivity"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}