{"record":{"id":"a401c00600f332ce","repo":"ZhuLinsen/daily_stock_analysis","slug":"field-name-must-be-at-most-max-length-characte","errorCode":null,"errorMessage":"{field_name} must be at most {max_length} characters","messagePattern":"(.+?) must be at most (.+?) characters","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/decision_signal_outcome_service.py","lineNumber":669,"sourceCode":"    def _normalize_horizons(self, values: Optional[List[str]]) -> Optional[List[str]]:\n        if not values:\n            return None\n        out: List[str] = []\n        for value in values:\n            horizon = self._normalize_enum(value, HORIZONS, \"horizon\")\n            if horizon not in out:\n                out.append(horizon)\n        return out\n\n    @staticmethod\n    def _optional_public_text(value: Any, field_name: str, *, max_length: int) -> Optional[str]:\n        if value in (None, \"\"):\n            return None\n        text = sanitize_decision_signal_text(value)\n        if not text:\n            return None\n        if len(text) > max_length:\n            raise ValueError(f\"{field_name} must be at most {max_length} characters\")\n        return text\n\n    @staticmethod\n    def _serialize_outcome(row: DecisionSignalOutcomeRecord) -> Dict[str, Any]:\n        return {\n            \"id\": row.id,\n            \"signal_id\": row.signal_id,\n            \"horizon\": row.horizon,\n            \"engine_version\": row.engine_version,\n            \"eval_status\": row.eval_status,\n            \"outcome\": row.outcome,\n            \"direction_expected\": row.direction_expected,\n            \"direction_correct\": row.direction_correct,\n            \"unable_reason\": row.unable_reason,\n            \"anchor_date\": row.anchor_date.isoformat() if row.anchor_date else None,\n            \"eval_window_days\": row.eval_window_days,\n            \"start_price\": row.start_price,\n            \"end_close\": row.end_close,","sourceCodeStart":651,"sourceCodeEnd":687,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/decision_signal_outcome_service.py#L651-L687","documentation":"_optional_public_text sanitizes free-text fields with sanitize_decision_signal_text and enforces a per-field max_length; over-length input raises ValueError '<field> must be at most N characters'. This bounds user-visible text stored on decision-signal outcome records.","triggerScenarios":"Submitting notes/reason/metadata text fields on decision-signal outcome endpoints longer than the field's max_length (e.g. >limit characters after sanitization).","commonSituations":"Pasting long analyst commentary into a notes field; concatenated auto-generated text exceeding the cap; sanitization collapsing whitespace but still leaving the text over limit.","solutions":["Shorten the text to within the field's documented max_length.","Truncate client-side before submit: text[:max_length].","For long content, store it elsewhere (report/attachment) and keep only a short reference here."],"exampleFix":"# before\nservice.update_outcome_note(signal_id=1, note=very_long_text)\n\n# after\nMAX = 200  # field's max_length\nservice.update_outcome_note(signal_id=1, note=very_long_text[:MAX])","handlingStrategy":"validation","validationCode":"def clampText(text: str | None, max_length: int) -> str | None:\n    if not text:\n        return None\n    return text.strip()[:max_length]\n\nnote = clampText(raw_note, max_length=200)  # match field's max_length\nservice.update_outcome_note(signal_id=1, note=note)","typeGuard":"def fitsMaxLength(text: str | None, max_length: int) -> bool:\n    return text is None or len(str(text).strip()) <= max_length","tryCatchPattern":"try:\n    service.update_outcome_note(signal_id=1, note=note)\nexcept ValueError as exc:\n    if \"must be at most\" in str(exc):\n        return JSONResponse(status_code=400, content={\"error\": \"text_too_long\", \"message\": str(exc)})\n    raise","preventionTips":["Set maxlength on text inputs matching each field's max_length.","Trim and measure after sanitization, not before — sanitization can change length.","Store long content elsewhere and keep short references here."],"tags":["decision-signal","text-validation","parameter-validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}