{"record":{"id":"68f2649f21c85de5","repo":"mvanhorn/last30days-skill","slug":"field-field-must-be-a-non-empty-string","errorCode":null,"errorMessage":"field '{field}' must be a non-empty string","messagePattern":"field '(.+?)' must be a non-empty string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/planner.py","lineNumber":176,"sourceCode":"    \"jobs\": {\"jobs\", \"company_signal\", \"link\"},\n    \"corpus\": {\"reference\", \"analysis\"},\n}\n\n\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\")","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/planner.py#L158-L194","documentation":"Type/format check in validate_external_plan for the three string metadata fields (intent, freshness_mode, cluster_mode): each must be a str AND non-empty after strip(). Whitespace-only strings also fail. Values are not yet checked against valid enums here — that canonicalization is the sanitizer's job — but they must at least be real, non-empty strings.","triggerScenarios":"Passing intent: 123 or intent: null; passing \" \" (whitespace-only); passing an empty string \"\" for any of the three metadata fields.","commonSituations":"LLM-generated plans emitting null for optional-looking fields; templates with placeholder spaces; numbers or booleans where a string mode was expected (e.g. freshness_mode: 7 instead of \"recent\").","solutions":["Set each of intent/freshness_mode/cluster_mode to a real non-empty string (e.g. \"factual\", \"recent\", \"thematic\").","If a value came back as null from a generator, default it before submission.","Strip values and assert truthiness in your plan builder."],"exampleFix":"# before\n{\"intent\": \" \", \"freshness_mode\": None, \"cluster_mode\": \"thematic\", \"subqueries\": [...]}\n\n# after\n{\"intent\": \"factual\", \"freshness_mode\": \"recent\", \"cluster_mode\": \"thematic\", \"subqueries\": [...]}","handlingStrategy":"type-guard","validationCode":"for f in (\"intent\", \"freshness_mode\", \"cluster_mode\"):\n    v = plan.get(f)\n    if not isinstance(v, str) or not v.strip():\n        plan[f] = DEFAULTS[f]  # or raise before the pipeline does","typeGuard":"def is_nonempty_str(v) -> bool:\n    return isinstance(v, str) and bool(v.strip())","tryCatchPattern":"try:\n    planner.validate_external_plan(plan)\nexcept ValueError as exc:\n    if \"must be a non-empty string\" in str(exc):\n        raise PlanInputError(f\"metadata fields must be strings: {exc}\") from exc\n    raise","preventionTips":["Coerce/null-check the three metadata fields in your plan emitter.","Never pass null for a mode — pick a concrete value like \"factual\"/\"recent\"/\"thematic\"."],"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"}