# Comparison mode

> --competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template.

- Repository: mvanhorn/last30days-skill
- GitHub: https://github.com/mvanhorn/last30days-skill
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/llms-full.txt

## Source Files

- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/scripts/lib/competitors.py`
- `skills/last30days/scripts/lib/fanout.py`
- `tests/test_cli_competitors.py`
- `tests/test_competitor_fanout.py`

---

---
title: "Comparison mode"
description: "--competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template."
---

Comparison mode runs one full `pipeline.run()` per entity in parallel, merges evidence for synthesis, and emits a Head-to-Head scaffold. Entry paths are a `vs`/`versus` topic string (automatic vs-mode), `--competitors` with optional engine-side peer discovery, or `--competitors-list` with explicit peer names. Per-entity Step 0.55 targeting is supplied through `--competitors-plan` (preferred) or engine `auto_resolve` when a web backend is configured.

## When comparison mode activates

| Trigger | Behavior |
| --- | --- |
| Topic contains ` vs `, ` versus `, ` vs.`, or `/` between entities | `planner._comparison_entities()` splits the topic; first entity is main, rest become peers; stderr logs `[Competitors] vs-mode: routing to N-pass fanout` |
| `--competitors` or `--competitors-list` | Enables fanout even without a vs-string; peers from discovery or the list |
| Neither of the above | Single-entity pipeline only |

Vs-mode detection uses a richer splitter than render (supports `difference between X and Y`, `compared to`, slash-separated lists, trailing context stripping, dedup). Entity count from the topic is capped at four subqueries for comparison intent. Render’s `_parse_comparison_entities()` caps table columns at four for readability.

`--competitors-plan` does not enable fanout by itself. Pass it together with a vs-topic or `--competitors` / `--competitors-list` so `parse_competitors_plan()` can thread targeting into peer sub-runs.

## End-to-end flow

```mermaid
sequenceDiagram
    participant Harness as Hosting model / CLI
    participant CLI as last30days.py
    participant Disc as competitors.discover_competitors
    participant Fan as fanout.run_competitor_fanout
    participant Pipe as pipeline.run
    participant Rend as render.render_comparison_multi

    Harness->>CLI: topic + flags (--competitors-plan, outer --x-handle, ...)
    CLI->>CLI: resolve_competitors_args / vs_entities
    alt explicit --competitors-list
        CLI->>CLI: peers = list
    else --competitors N and web backend
        CLI->>Disc: 3 parallel SERP queries
        Disc-->>CLI: ranked peer names
    else no backend (hosted path)
        CLI-->>Harness: exit 2 + SKILL.md recovery text
    end
    CLI->>Fan: main_runner + competitor_runner × N
    par MAX_PARALLEL_SUBRUNS=6
        Fan->>Pipe: main topic
        Fan->>Pipe: each peer (internal_subrun=True)
    end
    Fan-->>CLI: ordered (entity, Report)[]
    CLI->>Rend: merged compact/md/json/context/html
    Rend-->>Harness: evidence envelope + Head-to-Head scaffold
```

## CLI flags

<ParamField body="--competitors" type="integer (optional)" default="2 when flag present without value">
Auto-discover N peer entities and fan out. Bare `--competitors` defaults to N=2 (main + two peers → three-way). Range 1–6; values below 1 exit 2; values above 6 clamp with stderr warning. Ignored when `--competitors-list` is set (list length wins).
</ParamField>

<ParamField body="--competitors-list" type="comma-separated string">
Explicit peer names; whitespace-trimmed. Enables comparison mode without `--competitors`. Empty list exits 2. More than six entries clamp to `COMPETITORS_MAX` (6).
</ParamField>

<ParamField body="--competitors-plan" type="inline JSON or file path">
Per-entity Step 0.55 targeting for peer sub-runs. Parsed by `parse_competitors_plan()`; keys normalized to lowercase entity names. Preferred over list-only mode when the hosting model has already resolved handles and subreddits.
</ParamField>

<ParamField body="--polymarket-keywords" type="comma-separated string">
Optional disambiguation for ambiguous single-token topics during comparison runs (documented alongside competitor mode in SKILL.md).
</ParamField>

### Resolution precedence (`resolve_competitors_args`)

1. No flags → comparison disabled.
2. `--competitors-list` present → enabled; count = list length; numeric `--competitors` only warns if mismatched.
3. `--competitors=N` only → enabled; discovery count = N (after clamp).

## Engine-side discovery (`--competitors` without a list)

`lib/competitors.discover_competitors()` runs when auto-discovery is needed and a web backend exists (`BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, etc., via `resolve._has_backend`).

<Steps>
<Step title="Fan out three SERP queries in parallel">
`{topic} competitors`, `{topic} alternatives`, `{topic} vs` — each through `grounding.web_search()` with the run’s lookback window.
</Step>
<Step title="Extract brand-shaped phrases">
Regex mining on titles and snippets; stopword filtering; reject candidates overlapping topic tokens.
</Step>
<Step title="Rank and return">
Frequency across SERP items; deterministic tie-break; return up to `count` names.
</Step>
</Steps>

If no backend is configured and `--mock` is not set, the engine exits 2 and prints recovery steps: hosting models should WebSearch peers, run Step 0.55 per entity, then re-invoke with a vs-topic and `--competitors-plan`; headless runs should configure API keys or pass `--competitors-list`.

Empty discovery also exits 2 (`Pass --competitors-list to override`).

## Parallel fan-out (`lib/fanout.py`)

`run_competitor_fanout()` schedules the main topic plus each peer in a `ThreadPoolExecutor` with `max_workers = min(len(competitors) + 1, MAX_PARALLEL_SUBRUNS)` where `MAX_PARALLEL_SUBRUNS = 6`.

- Per-entity failures log to stderr and are dropped; order preserved from submission order (main first, then peers in list order).
- Surviving reports must be at least two total; otherwise exit 1 (`Fewer than 2 sub-runs survived`).
- Empty competitor list degenerates to a single main run.

Each peer sub-run sets `internal_subrun=True`, deep-copies config, and records `report.artifacts["resolved"]` for the Resolved Entities block.

## `--competitors-plan` JSON schema

Top-level object: entity name → targeting object. Entity keys are case-insensitive after parse (stored lowercased).

| Field | Type | Role |
| --- | --- | --- |
| `x_handle` | string | Primary X handle (`@` stripped) |
| `x_related` | string[] | Related handles (`@` stripped) |
| `subreddits` | string[] | Subreddit names (`r/` prefix stripped) |
| `github_user` | string | GitHub username (lowercased, `@` stripped) |
| `github_repos` | string[] | `owner/repo` pairs; entries without `/` filtered out |
| `context` | string | News/events context for planner subqueries |

Unknown fields log a stderr warning and are dropped. Malformed JSON or a non-object top level exits 2. Non-dict entries for an entity are skipped with a warning.

`subrun_kwargs_for()` merges plan entries over `auto_resolve` results. Plan values win when present.

### Engine `auto_resolve` skip

When a plan entry includes both `x_handle` and `subreddits`, the engine skips `resolve.auto_resolve()` for that peer (hosting-model-driven Step 0.55). Otherwise, if a web backend exists, `auto_resolve(entity)` runs before `pipeline.run()`.

### File path vs inline JSON

`parse_competitors_plan()` accepts a filesystem path (same pattern as `--plan`). SKILL.md recommends a quoted heredoc tmpfile to avoid shell apostrophe breaks in `context` strings.

<RequestExample>

```bash
COMPETITORS_PLAN_FILE=$(mktemp "${TMPDIR:-/tmp}/last30days-competitors.XXXXXX")
trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT
cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF'
{
  "Anthropic": {"x_handle":"AnthropicAI","subreddits":["ClaudeAI","MachineLearning"],"github_user":"anthropics","context":"Claude 4 rollout"},
  "xAI": {"x_handle":"xai","subreddits":["singularity"],"github_user":"xai-org"}
}
PLAN_EOF

python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic vs xAI" \
  --emit=compact \
  --x-handle=OpenAI \
  --subreddits=OpenAI,MachineLearning \
  --competitors-plan "$COMPETITORS_PLAN_FILE"
```

</RequestExample>

Main topic (first entity in the vs-string) uses outer CLI flags (`--x-handle`, `--subreddits`, `--github-user`, `--github-repo`, TikTok/IG flags). Peers use `--competitors-plan` entries keyed by entity name.

## Per-entity Step 0.55 (hosting model)

On platforms with WebSearch (Claude Code, Codex, Hermes, Gemini, etc.), SKILL.md requires Step 0.55 once per entity before engine invocation: X handles, subreddits (including category-peer expansion for product topics), GitHub user/repos, and news context.

The four-step competitor protocol when the user asks for comparison without naming peers:

1. WebSearch `"{topic} competitors"` / `"{topic} alternatives"` (default N=2 peers unless `--competitors=N`).
2. Step 0.55 for main topic and each peer.
3. Build vs-topic: `"{main} vs {peer1} vs {peer2}"`.
4. Invoke engine with vs-topic, outer flags for main, and `--competitors-plan` for peers.

<Warning>
List-only mode (`--competitors-list` without plan) leaves peers on planner defaults — visibly thinner evidence. Dashes in `## Resolved Entities` for a peer mean Step 0.55 was skipped for that entity.
</Warning>

Platforms without WebSearch should use `--auto-resolve` on the engine command so `resolve.auto_resolve()` backs each sub-run when keys exist.

## Engine output shape

`render_comparison_multi()` produces:

- Badge and comparison header (`# last30days v{VERSION}: {entity1} vs {entity2} ...`)
- Optional aggregated `## Warnings`
- `<!-- EVIDENCE FOR SYNTHESIS -->` with per-entity `## {label}` evidence subsections (lower `cluster_limit` than single-entity runs)
- `## Resolved Entities` — per-entity X / Subs / GitHub / Context summary from artifacts
- `## Head-to-Head` empty table scaffold (nine axes from the April 9 exemplar: What it is, GitHub stars, Philosophy, Skills, Memory, Models, Security, Best for, Install)
- Pass-through emoji footer from the main report

`--emit=json` returns `{"comparison": true, "entities": [...], "reports": [...]}`. With `--save-dir`, the merged comparison saves once; each peer also gets an individual `{slug}-raw.md` (historical N-pass behavior).

Passing `--competitors-plan` or `--plan` suppresses the runtime web-promo nudge in the UI layer.

## Hosting model synthesis template

Comparison queries use a dedicated template (not `What I learned:` / KEY PATTERNS). LAW 2 and LAW 4 comparison exceptions apply.

Required structure after the badge:

| Section | Purpose |
| --- | --- |
| `# {A} vs {B} [vs {C}]: What the Community Says (/Last30Days)` | Title line |
| `## Quick Verdict` | Thesis, scale stats, one quotable community framing |
| `## {Entity}` (one per entity) | Community Sentiment; Strengths; Weaknesses bullets with `per <source>` |
| `## Head-to-Head` | Fill engine scaffold cells (5–15 words; ` - ` not em-dashes) |
| `## The Bottom Line` | `**Choose {Entity} if**` per entity |
| `## The emerging stack` | Combination pattern or explicit “no pattern yet” |

Forbidden for comparisons: `What I learned:`, bold-lead-in general voice, `KEY PATTERNS` lists, extra `##` headers beyond the six above, fabricated `## Notable Stats` (footer is LAW 5 pass-through).

Supplement after engine run: WebSearch `{A} vs {B} comparison {YEAR}` and `{A} vs {B} which is better` for rivalry coverage.

## Hosting model vs headless CLI

| Concern | Hosting model (slash command) | Direct CLI / cron |
| --- | --- | --- |
| Peer discovery | Model WebSearch + vs-topic + `--competitors-plan` | `--competitors` + web API keys, or `--competitors-list` |
| Per-entity targeting | `--competitors-plan` from Step 0.55 | Plan file or `auto_resolve` per peer |
| Synthesis | Model follows SKILL.md comparison template | Engine emits evidence + scaffold; synthesis still model-driven in skill hosts |
| Thin peer data | Avoid `--competitors-list` alone | Same; check Resolved Entities block |

`--competitors` on the engine signals intent in skill hosts; discovery and Step 0.55 remain the hosting model’s job before the vs-topic invocation.

## Failure modes and exit codes

| Condition | Exit / signal |
| --- | --- |
| `--competitors` below 1 | Exit 2 |
| Empty `--competitors-list` | Exit 2 |
| Auto-discovery with no web backend (non-mock) | Exit 2 + recovery stderr |
| Zero peers discovered | Exit 2 |
| Fewer than two sub-runs survived | Exit 1 |
| Invalid `--competitors-plan` JSON | Exit 2 |

Partial peer failure is graceful: remaining entities still render if at least two reports survive.

## Related pages

<CardGroup>
<Card title="Query types" href="/query-types">
COMPARISON intent detection, vs-string parsing, and pre-flight refusal paths.
</Card>
<Card title="Comparison recipes" href="/comparison-recipes">
Copy-paste workflows for vs-topic phrasing, fan-out, and Head-to-Head expectations.
</Card>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, LAW exceptions for comparison voice, and engine invocation rules.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full flag inventory including `--competitors*` and emit modes.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, evidence envelope boundaries, and footer pass-through (LAW 5).
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Recurring `--competitors-plan` templates and project-scoped env isolation.
</Card>
</CardGroup>
