{"record":{"id":"3c671192f00b2e5c","repo":"mvanhorn/last30days-skill","slug":"field-source-weights-must-map-source-names-to-nu","errorCode":null,"errorMessage":"field 'source_weights' must map source names to numbers","messagePattern":"field 'source_weights' must map source names to numbers","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/planner.py","lineNumber":188,"sourceCode":"        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\")\n        if not isinstance(sources, list) or not sources or not all(\n            isinstance(source, str) and source.strip() for source in sources\n        ):\n            raise ValueError(f\"subqueries[{index}].sources must be a non-empty string array\")\n        weight = subquery.get(\"weight\")\n        if weight is not None and (\n            isinstance(weight, bool) or not isinstance(weight, (int, float))\n        ):","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/planner.py#L170-L206","documentation":"Per-entry check in validate_external_plan: every key in source_weights must be a non-empty string (after strip) and every value must be an int or float — and explicitly NOT a bool, since isinstance(True, int) is True in Python and booleans would otherwise sneak through as 1/0.","triggerScenarios":"source_weights containing a boolean value ({\"reddit\": true}), a string number ({\"reddit\": \"2\"}), null, a list, or an empty/whitespace key ({\" \": 1}).","commonSituations":"JSON true/false leaking in from LLM plans meaning yes/no; weights quoted as strings; a null weight for a disabled source.","solutions":["Use plain numbers: {\"reddit\": 2, \"hackernews\": 1.5}.","Convert string numbers with float(); drop null/boolean entries.","Never use true/false as weights — remove the key entirely to disable a source."],"exampleFix":"# before\n\"source_weights\": {\"reddit\": true, \"x\": \"3\"}\n\n# after\n\"source_weights\": {\"reddit\": 2, \"x\": 3}","handlingStrategy":"validation","validationCode":"sw = plan.get(\"source_weights\") or {}\nplan[\"source_weights\"] = {\n    k: float(v) for k, v in sw.items()\n    if isinstance(k, str) and k.strip() and isinstance(v, (int, float)) and not isinstance(v, bool)\n}","typeGuard":"def is_numeric_weight(v) -> bool:\n    return isinstance(v, (int, float)) and not isinstance(v, bool)","tryCatchPattern":null,"preventionTips":["Remember bool is a subclass of int in Python — JSON true/false will pass naive isinstance checks.","Reject or cast string numbers with float() at the boundary."],"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"}