feat: vs mode N full passes + --competitors auto-discovery + (/Last30Days) title (#312)
* feat: vs mode runs N full passes; --competitors wraps vs with auto-discovery
Unifies vs-mode and --competitors onto one fanout architecture. A topic
containing "vs" / "versus" now runs N full pipeline.run() calls in parallel
(reverting the one-pass latency optimization that removed per-entity
depth); --competitors becomes a SKILL.md-level shortcut where the hosting
reasoning model (Claude Code, Codex, Hermes, Gemini) discovers N peers via
its own WebSearch, runs Step 0.55 per entity, and invokes the engine with
a vs-topic + --competitors-plan JSON.
Changed:
- vs-mode: N full passes in parallel via fanout (was 1 merged pass).
- --competitors: SKILL.md shortcut for vs-mode-with-discovery. Engine flag
kept for headless/cron use. LAW 7-style stderr reframed to lead with the
hosting-model path (use WebSearch + --competitors-plan) instead of
BRAVE_API_KEY. Footer BRAVE/SERPER nudge suppressed when --plan or
--competitors-plan present (hosting model already has WebSearch).
Added:
- --competitors-plan JSON flag: per-entity {x_handle, x_related, subreddits,
github_user, github_repos, context}. Accepts inline JSON or file path.
subrun_kwargs_for helper is the single source of truth for per-entity
kwargs — no closure-default fallthrough from main scope.
- Per-entity save files: each entity's sub-run produces its own
{slug}-raw.md with a single-row Resolved Entities block.
- --polymarket-keywords filter for ambiguous single-token topics.
Fixed:
- test_competitor_subrun_isolation regression suite locks in 3.0.12's
no-leak invariant (main flags do not inherit into peer sub-runs).
- Updates test_regression.py for the new comparison-mode payload shape.
Bumps plugin.json to 3.0.13. 1,219 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: comparison title attribution — (Last 30 Days) → (/Last30Days)
User feedback on 3.0.13 dogfood runs (Kanye vs Drake, Mercer Island,
Figma): the comparison-mode synthesis title should attribute to the
slash command rather than restate the date range.
Three SKILL.md occurrences updated. Pure documentation change. Bumps to
3.0.14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -443,7 +443,7 @@ def run(
|
||||
if bundle.items_by_source.get(source):
|
||||
del bundle.errors_by_source[source]
|
||||
|
||||
items_by_source = _finalize_items_by_source(bundle.items_by_source, topic=topic)
|
||||
items_by_source = _finalize_items_by_source(bundle.items_by_source, topic=topic, config=config)
|
||||
candidates = weighted_rrf(bundle.items_by_source_and_query, plan, pool_limit=settings["pool_limit"])
|
||||
ranked_candidates = rerank.rerank_candidates(
|
||||
topic=topic,
|
||||
@@ -511,6 +511,7 @@ def _normalize_score_dedupe(
|
||||
def _finalize_items_by_source(
|
||||
items_by_source_raw: dict[str, list[schema.SourceItem]],
|
||||
topic: str = "",
|
||||
config: dict | None = None,
|
||||
) -> dict[str, list[schema.SourceItem]]:
|
||||
finalized = {}
|
||||
for source, items in items_by_source_raw.items():
|
||||
@@ -523,6 +524,11 @@ def _finalize_items_by_source(
|
||||
# (e.g., WTI crude oil, Elon tweet counts) before footer emission.
|
||||
if source == "polymarket" and topic:
|
||||
items = polymarket.filter_items_against_topic(topic, items)
|
||||
# --polymarket-keywords (via config): additional keyword filter
|
||||
# for ambiguous single-token topics (e.g., "Warriors" → nba,gsw).
|
||||
keywords = config.get("_polymarket_keywords") if isinstance(config, dict) else None
|
||||
if keywords:
|
||||
items = polymarket.filter_items_against_keywords(items, keywords)
|
||||
finalized[source] = items
|
||||
return finalized
|
||||
|
||||
|
||||
@@ -232,6 +232,39 @@ def filter_items_against_topic(topic: str, items: List[Any]) -> List[Any]:
|
||||
return filtered
|
||||
|
||||
|
||||
def filter_items_against_keywords(items: List[Any], keywords: List[str]) -> List[Any]:
|
||||
"""Keep only items whose title contains at least one keyword (case-insensitive).
|
||||
|
||||
Intended for disambiguating ambiguous single-token topics like 'Warriors'
|
||||
via --polymarket-keywords (e.g., 'nba,gsw,golden-state') to filter out
|
||||
Glasgow Warriors rugby, Honor of Kings Rogue Warriors markets that share
|
||||
the 'Warriors' token but are not the target entity.
|
||||
"""
|
||||
if not keywords:
|
||||
return items
|
||||
normalized_keywords = [kw.strip().lower() for kw in keywords if kw and kw.strip()]
|
||||
if not normalized_keywords:
|
||||
return items
|
||||
|
||||
filtered = []
|
||||
for item in items:
|
||||
title = getattr(item, "title", None)
|
||||
if title is None and isinstance(item, dict):
|
||||
title = item.get("title", "")
|
||||
title = (title or "").lower()
|
||||
if any(kw in title for kw in normalized_keywords):
|
||||
filtered.append(item)
|
||||
|
||||
dropped = len(items) - len(filtered)
|
||||
if dropped:
|
||||
_log(
|
||||
f"Keyword filter dropped {dropped} Polymarket items; "
|
||||
f"kept {len(filtered)} matching {normalized_keywords}"
|
||||
)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def _extract_domain_queries(topic: str, events: List[Dict]) -> List[str]:
|
||||
"""Extract domain-indicator search terms from first-pass event tags.
|
||||
|
||||
|
||||
@@ -624,6 +624,17 @@ def render_full(report: schema.Report) -> str:
|
||||
lines.extend(f"- {warning}" for warning in report.warnings)
|
||||
lines.append("")
|
||||
|
||||
# When this Report is a per-entity sub-run from vs-mode / --competitors,
|
||||
# include the single-row Resolved Entities block so the saved file is
|
||||
# self-describing. The artifact is populated by last30days.py's
|
||||
# _competitor_runner and _main_runner closures.
|
||||
resolved = report.artifacts.get("resolved")
|
||||
if isinstance(resolved, dict) and resolved.get("entity"):
|
||||
single_row = _render_resolved_entities_block([(resolved["entity"], report)])
|
||||
if single_row:
|
||||
lines.extend(single_row)
|
||||
lines.append("")
|
||||
|
||||
# ALL clusters (no limit)
|
||||
lines.append("## Ranked Evidence Clusters")
|
||||
lines.append("")
|
||||
|
||||
Reference in New Issue
Block a user