{"record":{"id":"d2d403e65e068d71","repo":"mvanhorn/last30days-skill","slug":"nominations-bundle-path-must-carry-a-top-level","errorCode":null,"errorMessage":"Nominations bundle {path} must carry a top-level \"nominations\" list, got {type(rows_raw).__name__}. {_RESWEEP_REMEDY}","messagePattern":"Nominations bundle (.+?) must carry a top-level \"nominations\" list, got (.+?)\\. (.+?)","errorType":"exception","errorClass":"HandoffContractError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/discovery_handoff.py","lineNumber":462,"sourceCode":"        kind=schema.DISCOVERY_NOMINATIONS_KIND,\n        schema_version=schema.DISCOVERY_NOMINATIONS_SCHEMA_VERSION,\n        remedy=_RESWEEP_REMEDY,\n        missing_id_context=\"judgments cannot bind to it\",\n        stale_context=\"the momentum window it captured has moved on\",\n    )\n    version = payload.get(\"schema_version\")\n\n    context = payload.get(\"context\") or {}\n    boundary = context.get(\"enrichment_source_boundary\")\n    requested = context.get(\"requested_sources\")\n    try:\n        lookback_days = int(context.get(\"lookback_days\") or 30)\n    except (TypeError, ValueError):\n        lookback_days = 30\n\n    rows_raw = payload.get(\"nominations\")\n    if not isinstance(rows_raw, list):\n        raise HandoffContractError(\n            f\"Nominations bundle {path} must carry a top-level \"\n            f\"\\\"nominations\\\" list, got {type(rows_raw).__name__}. \"\n            f\"{_RESWEEP_REMEDY}\"\n        )\n\n    nominations: list[BundleNomination] = []\n    for position, row in enumerate(rows_raw, start=1):\n        # Lenient per row: the bundle is engine-written, but one corrupted\n        # row must not discard the rest of the pool.\n        if not isinstance(row, dict):\n            _warn(\n                f\"skipping malformed nomination row {position} in \"\n                f\"{path.name} (not an object)\"\n            )\n            continue\n        try:\n            nomination = pipeline.Nomination(\n                **schema.nomination_kwargs_from_dict(row.get(\"nomination\") or {})","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/discovery_handoff.py#L444-L480","documentation":"`_parse_bundle_file` raises HandoffContractError when the bundle's top-level `nominations` value is not a JSON array. After the shared envelope checks pass, the reader requires a list of nomination rows; anything else (dict keyed by nomination id, null, string) is a corrupted pool. Per-row handling is deliberately lenient after this point, but the top-level shape is strict. Message includes the actual type name and the re-sweep remedy.","triggerScenarios":"`nominations` hand-converted to an object keyed by nomination_id; set to null or missing via a jq rewrite (`jq 'del(.nominations)'`); a partial rewrite that replaced the list with a placeholder string.","commonSituations":"Editing bundles to remove unwanted nominations by restructuring instead of filtering the array in place; model-assisted 'cleanup' of the file; sync/merge damage to that key.","solutions":["Verify with `jq '.nominations | type' <path>` — must be 'array'.","If you must filter, keep the array shape: `jq '.nominations |= map(select(.heuristic_junk != true))' file > tmp && mv tmp file` — but regeneration is safer.","When in doubt, re-run `--discover --nominate-only`; the engine never writes a non-list pool, so this shape always means the file was altered."],"exampleFix":"# before\njq '.nominations |= { (.nomination_id): . }' discover-nominations.json > tmp && mv tmp discover-nominations.json  # now an object\n# after (keep array, or regenerate)\njq '.nominations |= map(select(.heuristic_junk != true))' discover-nominations.json > tmp && mv tmp discover-nominations.json","handlingStrategy":"type-guard","validationCode":"import json\nfrom pathlib import Path\n\ndef nominations_is_list(path: Path) -> bool:\n    try:\n        return isinstance(json.loads(path.read_text(encoding=\"utf-8\")).get(\"nominations\"), list)\n    except (OSError, json.JSONDecodeError):\n        return False","typeGuard":"def is_nomination_pool(value) -> bool:\n    return isinstance(value, list) and all(isinstance(row, dict) for row in value)","tryCatchPattern":"from lib import discovery_handoff\ntry:\n    bundle = discovery_handoff.load_nominations_bundle(save_dir=save_dir, config_dir=config_dir)\nexcept discovery_handoff.HandoffContractError as exc:\n    if '\"nominations\" list' in exc.message:\n        # reshaped pool: re-sweep, do not repair\n        ...","preventionTips":["When filtering a bundle with jq, keep the array shape (|= map(select(...))) and never convert to an object.","Prefer encoding junk/pruning decisions in the judgments file instead of editing the bundle."],"tags":["discovery","handoff","json","schema","exit-code-2"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}