{"record":{"id":"a6adc83d7dd516d5","repo":"mvanhorn/last30days-skill","slug":"field-source-weights-must-be-an-object-when-prov","errorCode":null,"errorMessage":"field 'source_weights' must be an object when provided","messagePattern":"field 'source_weights' must be an object when provided","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/planner.py","lineNumber":180,"sourceCode":"\ndef validate_external_plan(raw: dict) -> None:\n    \"\"\"Validate explicit-plan structure before permissive sanitization.\n\n    Enum-like metadata stays permissive because direct pipeline callers rely on\n    the sanitizer to canonicalize those values.\n    \"\"\"\n    if not isinstance(raw, dict):\n        raise ValueError(\"top-level plan must be an object\")\n    for field in (\"intent\", \"freshness_mode\", \"cluster_mode\", \"subqueries\"):\n        if field not in raw:\n            raise ValueError(f\"missing required field '{field}'\")\n    for field in (\"intent\", \"freshness_mode\", \"cluster_mode\"):\n        if not isinstance(raw[field], str) or not raw[field].strip():\n            raise ValueError(f\"field '{field}' must be a non-empty string\")\n\n    source_weights = raw.get(\"source_weights\")\n    if source_weights is not None and not isinstance(source_weights, dict):\n        raise ValueError(\"field 'source_weights' must be an object when provided\")\n    for source, weight in (source_weights or {}).items():\n        if (\n            not isinstance(source, str)\n            or not source.strip()\n            or isinstance(weight, bool)\n            or not isinstance(weight, (int, float))\n        ):\n            raise ValueError(\"field 'source_weights' must map source names to numbers\")\n    subqueries = raw[\"subqueries\"]\n    if not isinstance(subqueries, list) or not subqueries:\n        raise ValueError(\"field 'subqueries' must be a non-empty array\")\n    for index, subquery in enumerate(subqueries):\n        if not isinstance(subquery, dict):\n            raise ValueError(f\"subqueries[{index}] must be an object\")\n        for field in (\"search_query\", \"ranking_query\"):\n            if not isinstance(subquery.get(field), str) or not subquery[field].strip():\n                raise ValueError(f\"subqueries[{index}].{field} must be a non-empty string\")\n        sources = subquery.get(\"sources\")","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/planner.py#L162-L198","documentation":"Optional-field shape check in validate_external_plan: source_weights may be omitted (None), but if present it must be a dict mapping source name → numeric weight. Passing a list of pairs, an array, or a string fails here before individual entries are examined.","triggerScenarios":"external_plan includes \"source_weights\": [[\"reddit\", 2], [\"x\", 1]] (array of pairs) or \"source_weights\": \"reddit:2\" — anything that is not None and not a dict.","commonSituations":"JSON authors modeling the map as an array of {source, weight} objects; YAML-to-JSON conversions changing the shape; LLM plans inventing a list format.","solutions":["Use a JSON object: \"source_weights\": {\"reddit\": 2, \"x\": 1}.","Convert array-of-pairs formats before submission: dict(pairs).","Or omit source_weights entirely — it is optional."],"exampleFix":"# before\n\"source_weights\": [{\"source\": \"reddit\", \"weight\": 2}]\n\n# after\n\"source_weights\": {\"reddit\": 2}","handlingStrategy":"type-guard","validationCode":"sw = plan.get(\"source_weights\")\nif sw is not None and not isinstance(sw, dict):\n    plan[\"source_weights\"] = dict(sw)  # only if it's an iterable of pairs; else drop","typeGuard":"def is_source_weights_map(v) -> bool:\n    return v is None or (isinstance(v, dict) and all(isinstance(k, str) and k.strip() for k in v))","tryCatchPattern":null,"preventionTips":["Model source_weights as a JSON object keyed by source name, never an array of pairs.","If the field is unused, omit it entirely — it is optional."],"tags":["planner","external-plan","validation","json"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}