{"record":{"id":"ff5a40198efd56f4","repo":"nexu-io/open-design","slug":"container-none-does-not-support-deviceframe-iphone","errorCode":null,"errorMessage":"container=none does not support deviceFrame=iphone-dynamic-island; use deviceFrame=none or choose an app container","messagePattern":"container=none does not support deviceFrame=iphone-dynamic-island; use deviceFrame=none or choose an app container","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":133,"sourceCode":"    user = json.loads(Path(path).read_text(encoding=\"utf-8\"))\n    config.update(user)\n    validate_config(config)\n    return config\n\n\ndef validate_config(config: dict) -> None:\n    if config[\"container\"] not in ALLOWED_CONTAINERS:\n        raise ValueError(f\"Unsupported container: {config['container']}\")\n    if config[\"avatarMode\"] not in ALLOWED_AVATAR_MODES:\n        raise ValueError(f\"Unsupported avatarMode: {config['avatarMode']}\")\n    if config[\"deviceFrame\"] not in ALLOWED_DEVICE_FRAMES:\n        raise ValueError(f\"Unsupported deviceFrame: {config['deviceFrame']}\")\n    if config[\"nicknameMode\"] not in ALLOWED_NICKNAME_MODES:\n        raise ValueError(f\"Unsupported nicknameMode: {config['nicknameMode']}\")\n    if config[\"deliveryFormat\"] not in ALLOWED_DELIVERY_FORMATS:\n        raise ValueError(f\"Unsupported deliveryFormat: {config['deliveryFormat']}\")\n    if config[\"container\"] == \"none\" and config[\"deviceFrame\"] == \"iphone-dynamic-island\":\n        raise ValueError(\"container=none does not support deviceFrame=iphone-dynamic-island; use deviceFrame=none or choose an app container\")\n\n    for speaker, participant in config.get(\"participants\", {}).items():\n        side = participant.get(\"side\")\n        if side and side not in {\"left\", \"right\"}:\n            raise ValueError(f\"Unsupported side for participant {speaker}: {side}\")\n        preset_key = participant.get(\"preset\")\n        if preset_key and preset_key not in PRESET_KEYS:\n            raise ValueError(f\"Unsupported preset for participant {speaker}: {preset_key}\")\n        upload_path = participant.get(\"uploadPath\")\n        if participant.get(\"uploadAsset\"):\n            raise ValueError(f\"Participant {speaker} config must use uploadPath, not uploadAsset\")\n        if config[\"avatarMode\"] == \"preset\" and upload_path:\n            raise ValueError(f\"avatarMode=preset does not allow uploadPath for participant {speaker}\")\n        if config[\"avatarMode\"] == \"upload\" and not upload_path:\n            raise ValueError(f\"avatarMode=upload requires uploadPath for participant {speaker}\")\n\n\ndef auto_avatar_for_participant(participant_index: int, used_avatar_keys: set[str]) -> str:","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L115-L151","documentation":"Raised by validate_config as a cross-field rule: when container='none' AND deviceFrame='iphone-dynamic-island', the combination is rejected. The iPhone dynamic-island frame requires an app container to render into; without one, the island has nowhere to sit. Either set deviceFrame='none' or pick a real container (wechat/telegram/messenger).","triggerScenarios":"User wants a bare chat with no app chrome but keeps the default deviceFrame 'iphone-dynamic-island', producing `{container: none, deviceFrame: iphone-dynamic-island}`. This is an easy mistake because both defaults individually seem reasonable.","commonSituations":"Copy-pasting DEFAULT_CONFIG and only flipping container to 'none' while leaving deviceFrame at its default; wanting a 'minimal' look and disabling the container without realizing the frame depends on it; merging two partial config files where each independently is valid but together violate the cross rule.","solutions":["Set deviceFrame to 'none' when container is 'none': `{container: none, deviceFrame: none}`.","Or keep deviceFrame='iphone-dynamic-island' and choose a container: wechat/telegram/messenger.","Review both fields together — they are coupled; do not change one without considering the other.","Pre-validate the pair: `assert not (c['container']=='none' and c['deviceFrame']=='iphone-dynamic-island')`."],"exampleFix":"// before\n{\"container\": \"none\", \"deviceFrame\": \"iphone-dynamic-island\"}\n# -> ValueError: container=none does not support deviceFrame=iphone-dynamic-island ...\n\n// after (option A: no chrome, no frame)\n{\"container\": \"none\", \"deviceFrame\": \"none\"}\n// after (option B: keep the island, add a container)\n{\"container\": \"telegram\", \"deviceFrame\": \"iphone-dynamic-island\"}","handlingStrategy":"validation","validationCode":"if config[\"container\"] == \"none\" and config[\"deviceFrame\"] == \"iphone-dynamic-island\":\n    raise SystemExit(\n        \"container=none cannot use deviceFrame=iphone-dynamic-island. \"\n        \"Set deviceFrame=none, or pick a container (wechat/telegram/messenger).\"\n    )","typeGuard":null,"tryCatchPattern":"try:\n    validate_config(config)\nexcept ValueError as exc:\n    raise SystemExit(f\"Config error: {exc}\") from exc","preventionTips":["Treat container and deviceFrame as a coupled pair when editing config.","When disabling chrome (container=none), audit deviceFrame at the same time.","Encode cross-field rules in jsonschema (if/then) so they fail at load.","Provide a config preset library that bundles known-good combinations."],"tags":["config","validation","cross-field","chat-overlay","device-frame"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}