nexu-io/open-design · error · SystemExit
[CompetitorsPlan] Top-level must be a dict of {entity: {targ
Error message
[CompetitorsPlan] Top-level must be a dict of {entity: {targeting}}, got {type(parsed).__name__} What it means
After successfully parsing JSON, the sanitizer enforces the top-level shape: it must be a dict (object) whose keys are entity names and whose values are themselves dicts of targeting fields. If `parsed` is a list, string, number, bool, or null, it writes `[CompetitorsPlan] Top-level must be a dict of {entity: {targeting}}, got <type>` and raises SystemExit(2). This is a structural guard before any per-entry validation runs.
Source
Thrown at design-templates/last30days/scripts/last30days.py:330
return {}
plan_str = raw
if os.path.isfile(plan_str):
try:
plan_str = open(plan_str).read()
except OSError as exc:
sys.stderr.write(f"[CompetitorsPlan] Cannot read plan file: {exc}\n")
raise SystemExit(2)
try:
parsed = json.loads(plan_str)
except json.JSONDecodeError as exc:
sys.stderr.write(f"[CompetitorsPlan] Invalid JSON: {exc}\n")
raise SystemExit(2)
if not isinstance(parsed, dict):
sys.stderr.write(
f"[CompetitorsPlan] Top-level must be a dict of "
f"{{entity: {{targeting}}}}, got {type(parsed).__name__}\n"
)
raise SystemExit(2)
known_fields = {
"x_handle", "x_related", "subreddits",
"github_user", "github_repos", "context",
}
normalized: dict[str, dict] = {}
for entity, entry in parsed.items():
if not isinstance(entry, dict):
sys.stderr.write(
f"[CompetitorsPlan] Entry for {entity!r} must be a dict, "
f"got {type(entry).__name__}; skipping.\n"
)
continue
unknown = set(entry.keys()) - known_fields
if unknown:
sys.stderr.write(
f"[CompetitorsPlan] Unknown fields in {entity!r}: "
f"{sorted(unknown)}; ignoring.\n"
)View on GitHub (pinned to 5be4028344)
Solutions
- Wrap the data as an object keyed by entity name: `{"acme": {...}, "beta": {...}}`.
- Move any per-entry fields (x_handle, subreddits, github_user, github_repos, context, x_related) inside each entity's value dict.
Example fix
# before
--plan '[{"entity":"acme","x_handle":"acme"}]'
# after
--plan '{"acme": {"x_handle": "acme"}}' Defensive patterns
Strategy: type-guard
Validate before calling
if not isinstance(parsed, dict):
raise TypeError(f'plan top-level must be object, got {type(parsed).__name__}') Type guard
def is_plan_dict(parsed: object) -> bool:
return isinstance(parsed, dict) and all(isinstance(v, dict) for v in parsed.values()) Prevention
- Key the plan by entity name; never use a top-level array.
- Validate structure with the type guard before invoking the run.
When it happens
Trigger: `--plan '[{"acme": {}}]'` (a list of one dict), `--plan '"acme"'` (a bare string), or `--plan 'null'`.
Common situations: Authoring the plan as a JSON array because that felt natural for multiple competitors; export from a tool that produces a list; confusion between the comparison list (`--competitors-list`) shape and the plan shape.
Related errors
- [CompetitorsPlan] Invalid JSON: {exc}
- [CompetitorsPlan] Cannot read plan file: {exc}
- [Competitors] --competitors-list is empty.\n
- [Competitors] --competitors must be >= {COMPETITORS_MIN} (go
- Unknown search source: {source}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/8291f3c7240922f9.
Report an issue: GitHub.