{"record":{"id":"e01e7e59b6b03598","repo":"nexu-io/open-design","slug":"competitorsplan-cannot-read-plan-file-exc","errorCode":null,"errorMessage":"[CompetitorsPlan] Cannot read plan file: {exc}","messagePattern":"\\[CompetitorsPlan\\] Cannot read plan file: (.+?)","errorType":"console","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"design-templates/last30days/scripts/last30days.py","lineNumber":319,"sourceCode":"\n\ndef parse_competitors_plan(raw: str | None) -> dict[str, dict]:\n    \"\"\"Parse a --competitors-plan argument into a {entity_name_lower: plan_entry} dict.\n\n    Accepts inline JSON or a file path (matches --plan). Returns {} on None/empty.\n    Validation: top-level must be a dict; each value must be a dict. Unknown fields\n    in entry values log a warning but do not abort. Invalid JSON or non-dict shape\n    raises SystemExit(2) with a clear stderr message.\n    \"\"\"\n    if not raw:\n        return {}\n    plan_str = raw\n    if os.path.isfile(plan_str):\n        try:\n            plan_str = open(plan_str).read()\n        except OSError as exc:\n            sys.stderr.write(f\"[CompetitorsPlan] Cannot read plan file: {exc}\\n\")\n            raise SystemExit(2)\n    try:\n        parsed = json.loads(plan_str)\n    except json.JSONDecodeError as exc:\n        sys.stderr.write(f\"[CompetitorsPlan] Invalid JSON: {exc}\\n\")\n        raise SystemExit(2)\n    if not isinstance(parsed, dict):\n        sys.stderr.write(\n            f\"[CompetitorsPlan] Top-level must be a dict of \"\n            f\"{{entity: {{targeting}}}}, got {type(parsed).__name__}\\n\"\n        )\n        raise SystemExit(2)\n    known_fields = {\n        \"x_handle\", \"x_related\", \"subreddits\",\n        \"github_user\", \"github_repos\", \"context\",\n    }\n    normalized: dict[str, dict] = {}\n    for entity, entry in parsed.items():\n        if not isinstance(entry, dict):","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/design-templates/last30days/scripts/last30days.py#L301-L337","documentation":"Inside the CompetitorsPlan sanitizer, when the `--plan` argument is an existing filesystem path (`os.path.isfile(plan_str)`), the loader opens and reads it. On OSError it writes `[CompetitorsPlan] Cannot read plan file: <exc>` to stderr and raises SystemExit(2). This fires only for the file-path branch; inline-JSON plans skip this check and go straight to json.loads (see 528).","triggerScenarios":"`--plan /path/to/plan.json` where the path exists as an entry (isfile returned something truthy enough to attempt) but read() fails — most commonly permission denied, or the path exists but is a broken symlink / special file that isfile fluked.","commonSituations":"Plan file chmod 000 or owned by another user; path is a dangling symlink; NFS/share hiccup; SELinux denying read.","solutions":["Verify the path is a readable regular file: `ls -l <path>` and `cat <path>`.","Fix permissions: `chmod 644 <path>`.","Pass the plan inline as a JSON string instead of a path (the loader detects non-path input and parses it directly)."],"exampleFix":"# before\npython3.12 last30days.py --topic x --plan /secrets/plan.json   # mode 000\n# after\nchmod 644 /secrets/plan.json\npython3.12 last30days.py --topic x --plan /secrets/plan.json","handlingStrategy":"validation","validationCode":"import os\nif os.path.isfile(raw):\n    if not os.access(raw, os.R_OK):\n        raise PermissionError(f'plan file not readable: {raw}')\n    plan_str = open(raw).read()\nelse:\n    plan_str = raw  # treat as inline JSON","typeGuard":null,"tryCatchPattern":null,"preventionTips":["chmod 644 plan files.","Prefer passing inline JSON for ephemeral plans to avoid FS perms."],"tags":["cli","filesystem","python","argument-validation","competitors"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}