{"record":{"id":"c00b44330968cd2e","repo":"mvanhorn/last30days-skill","slug":"no-listing-sources-are-available-for-global-trendi","errorCode":null,"errorMessage":"No listing sources are available for global trending","messagePattern":"No listing sources are available for global trending","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/planner.py","lineNumber":48,"sourceCode":"\n    An empty domain is global trending: sweep every river feed's own hot list\n    (r/all, HN front page, Digg) with no category scoping. Keyword-driven\n    sources (X, Techmeme, arXiv - none of which expose a river/front-page\n    lane) sit out of the global nominate stage and join per-topic at the\n    enrichment pass, where every nomination gets a full research run.\n    \"\"\"\n    normalized_domain = \" \".join(domain.split())\n    if not normalized_domain:\n        resolved = [\n            subreddit.removeprefix(\"r/\").strip()\n            for subreddit in (subreddits or [\"all\"])\n            if subreddit.strip()\n        ]\n        allowed = set(DISCOVERY_SOURCE_ORDER if available_sources is None else available_sources)\n        allowed.discard(\"x\")\n        sources = [source for source in DISCOVERY_SOURCE_ORDER if source in allowed]\n        if not sources:\n            raise ValueError(\"No listing sources are available for global trending\")\n        return schema.DiscoveryPlan(\n            domain=\"\",\n            category=None,\n            subreddits=resolved or [\"all\"],\n            sources=sources,\n        )\n\n    category = categories.detect_category(normalized_domain)\n    candidate_subreddits = list(subreddits or categories.peer_subs_for(category))\n    seen_subreddits: set[str] = set()\n    resolved_subreddits: list[str] = []\n    for subreddit in candidate_subreddits:\n        normalized_subreddit = subreddit.removeprefix(\"r/\").strip()\n        key = normalized_subreddit.lower()\n        if not normalized_subreddit or key in seen_subreddits:\n            continue\n        seen_subreddits.add(key)\n        resolved_subreddits.append(normalized_subreddit)","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/planner.py#L30-L66","documentation":"ValueError from planner.build_discovery_plan in the GLOBAL trending branch (empty/whitespace domain). After discarding 'x' from the allowed set, it intersects DISCOVERY_SOURCE_ORDER with the allowed sources; if nothing survives, there is no listing source to sweep and discovery cannot proceed. Note this branch explicitly drops 'x' even if the caller allowed it.","triggerScenarios":"build_discovery_plan called with domain='' (or whitespace) and available_sources set to values that exclude all of reddit/hackernews/digg — e.g. available_sources=['x'] (x is discarded) or available_sources=['youtube']. In the real pipeline this is pre-filtered (pipeline.py intersects with DISCOVERY_SOURCES), so hitting it usually means calling the planner directly.","commonSituations":"Direct planner unit-tests or custom orchestrators passing arbitrary available_sources; passing discovery's available list derived from a keyed-only config where none of the three listing sources are usable; mock runs with restricted sources.","solutions":["Include at least one of reddit/hackernews/digg in available_sources when calling build_discovery_plan for global trending.","Prefer the pipeline entry points (run_discover) which pre-filter available sources and raise the earlier, clearer error instead.","If you truly have no listing sources, fix availability first (all three are keyless-capable)."],"exampleFix":"# before\nplan = planner.build_discovery_plan(\"\", available_sources=[\"x\"])\n\n# after\nplan = planner.build_discovery_plan(\"\", available_sources=[\"reddit\", \"hackernews\"])","handlingStrategy":"validation","validationCode":"LISTING = {\"reddit\", \"hackernews\", \"digg\"}\nallowed = set(available_sources or []) & LISTING  # 'x' is always discarded here\nif domain.strip() == \"\" and not allowed:\n    raise SystemExit(\"global trending needs >=1 of reddit/hackernews/digg\")","typeGuard":null,"tryCatchPattern":"try:\n    plan = planner.build_discovery_plan(\"\", available_sources=srcs)\nexcept ValueError as exc:\n    if \"global trending\" in str(exc):\n        srcs = [\"reddit\"]\n        plan = planner.build_discovery_plan(\"\", available_sources=srcs)\n    else:\n        raise","preventionTips":["When calling the planner directly, always intersect available_sources with the listing-source set first.","Remember the global branch silently drops 'x' from allowed sources."],"tags":["planner","discovery","sources","validation"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}