{"record":{"id":"37e26c06aa8c47bd","repo":"mvanhorn/last30days-skill","slug":"competitorsplan-top-level-must-be-a-dict-of-ent","errorCode":null,"errorMessage":"[CompetitorsPlan] Top-level must be a dict of {entity: {targeting}}, got {type(parsed).__name__}\\n","messagePattern":"\\[CompetitorsPlan\\] Top-level must be a dict of (.+?)\\}, got (.+?)\\\\n","errorType":"console","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/last30days.py","lineNumber":814,"sourceCode":"    plan_str = raw\n    if os.path.isfile(plan_str):\n        try:\n            with open(plan_str, encoding=\"utf-8\") as f:\n                plan_str = f.read()\n        except (OSError, UnicodeDecodeError) 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\", \"trustpilot_domain\", \"context\",\n    }\n    normalized: dict[str, dict] = {}\n    for entity, entry in parsed.items():\n        if not isinstance(entry, dict):\n            sys.stderr.write(\n                f\"[CompetitorsPlan] Entry for {entity!r} must be a dict, \"\n                f\"got {type(entry).__name__}; skipping.\\n\"\n            )\n            continue\n        unknown = set(entry.keys()) - known_fields\n        if unknown:\n            sys.stderr.write(\n                f\"[CompetitorsPlan] Unknown fields in {entity!r}: \"\n                f\"{sorted(unknown)}; ignoring.\\n\"\n            )","sourceCodeStart":796,"sourceCodeEnd":832,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/last30days.py#L796-L832","documentation":"A structural guard in parse_competitors_plan(): valid JSON that parses to a list, string, number, or null is rejected because the plan must be a dict mapping entity names to targeting dicts. The message names the actual top-level type received. Inner entries that are not dicts are only warned-and-skipped; only the top level is fatal.","triggerScenarios":"--competitors-plan '[\"Acme\", \"Beta\"]' or a file containing a bare array of entities; a top-level {'entities': [...]} wrapper instead of the flat {entity: {targeting}} shape.","commonSituations":"Plan schemas from other tools (list-of-objects) fed in unchanged; LLM-generated plans wrapping entities under a key; confusion with --competitors-list which does take a comma string.","solutions":["Reshape the plan to a flat dict: {\"Acme\": {\"x_handle\": \"acme\"}, \"Beta\": {\"subreddits\": [\"beta\"]}}.","Allowed targeting keys are enumerated just below the guard: x_handle, x_related, subreddits, github_user, github_repos, trustpilot_domain, context — use only these.","If you just want entity names without targeting, use --competitors-list 'Acme,Beta' instead."],"exampleFix":"# before\n[{\"entity\": \"Acme\", \"x_handle\": \"acme\"}]\n\n# after\n{\"Acme\": {\"x_handle\": \"acme\"}}","handlingStrategy":"type-guard","validationCode":"parsed = json.loads(plan_str)\nif not isinstance(parsed, dict) or not all(isinstance(v, dict) for v in parsed.values()):\n    raise ValueError('plan must be {entity: {targeting...}}, got a different shape')","typeGuard":"def is_competitors_plan(value: Any) -> TypeGuard[dict[str, dict]]:\n    return isinstance(value, dict) and all(\n        isinstance(k, str) and isinstance(v, dict) for k, v in value.items()\n    )","tryCatchPattern":null,"preventionTips":["Use only the known targeting keys (x_handle, x_related, subreddits, github_user, github_repos, trustpilot_domain, context).","For name-only lists, prefer --competitors-list over a plan file."],"tags":["cli","json","schema","competitors"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}