{"record":{"id":"6927ce8c974d9e5e","repo":"mvanhorn/last30days-skill","slug":"subqueries-index-sources-must-be-a-non-empty-st","errorCode":null,"errorMessage":"subqueries[{index}].sources must be a non-empty string array","messagePattern":"subqueries\\[(.+?)\\]\\.sources must be a non-empty string array","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/planner.py","lineNumber":202,"sourceCode":"            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        ):\n            raise ValueError(f\"subqueries[{index}].weight must be a number when provided\")\n\n\nDEFAULT_INTENT_CAPABILITIES = {\n    \"comparison\": {\"discussion\", \"video\", \"web\", \"reference\", \"social\", \"link\", \"market\"},\n    \"how_to\": {\"discussion\", \"video\", \"web\", \"reference\", \"link\"},\n}\n\n\nclass DrillTargetError(ValueError):\n    \"\"\"Raised when a follow-up target cannot be resolved to a report cluster.\"\"\"\n\n    def __init__(self, target: str, clusters: list[schema.Cluster]) -> None:\n        candidates = \", \".join(","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/planner.py#L184-L220","documentation":"Per-subquery check that 'sources' is a non-empty list of non-blank strings. An empty array, a missing sources key, a comma-separated string like \"reddit, x\", or a list containing null/whitespace entries all fail. The sources list is what the sanitizer intersects with availability, so it must be well-formed up front.","triggerScenarios":"\"sources\": [] , \"sources\": \"reddit\", \"sources\": [\"reddit\", \" \"] , or the key omitted entirely from a subquery object.","commonSituations":"LLM emitting a comma-separated string instead of an array; generators dropping the key when 'all sources' was intended; empty list meaning 'use defaults' — not supported, sources must be explicit.","solutions":["Always provide an explicit non-empty array: \"sources\": [\"reddit\", \"hackernews\"].","Split string forms: [s.strip() for s in val.split(',') if s.strip()].","If 'all sources' is intended, list them explicitly."],"exampleFix":"# before\n{\"search_query\": \"q\", \"ranking_query\": \"q\", \"sources\": \"reddit, x\"}\n\n# after\n{\"search_query\": \"q\", \"ranking_query\": \"q\", \"sources\": [\"reddit\", \"x\"]}","handlingStrategy":"type-guard","validationCode":"srcs = q.get(\"sources\")\nif isinstance(srcs, str):\n    srcs = [s.strip() for s in srcs.split(\",\") if s.strip()]\nq[\"sources\"] = srcs or [\"reddit\"]\nassert q[\"sources\"] and all(isinstance(s, str) and s.strip() for s in q[\"sources\"])","typeGuard":"def is_source_string_array(v) -> bool:\n    return isinstance(v, list) and len(v) > 0 and all(isinstance(s, str) and s.strip() for s in v)","tryCatchPattern":null,"preventionTips":["Always emit sources as a JSON array of names, never a comma-separated string.","There is no 'all sources' sentinel — list the sources you want explicitly."],"tags":["planner","external-plan","validation","json","sources"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}