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

  1. Wrap the data as an object keyed by entity name: `{"acme": {...}, "beta": {...}}`.
  2. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/8291f3c7240922f9. Report an issue: GitHub.