---
name: topic-radar
description: "Use when the user asks what people actually said about a topic in the last week or two, whether a hype is real or media-driven, how Chinese and English communities differ, or wants content angles backed by citable evidence. Triggers — find out what people have been talking about over the past week, is this hype genuinely hot or media-inflated, give me a few topics with sources, how do the Chinese and English communities see this, is this topic worth making."
---

> Original skill · commissioned by the user · English edition, 2026-11-12

# topic-radar (hot-topic evidence gathering)

One line: **gather evidence first, then talk about topics.** Any judgement without evidence is labelled "inference".

## When to use

- "What has the X circle been discussing in the past week / half month"
- "Is this hype genuinely hot, or has the media blown it up"
- "Give me a few topics I can start on, with sources"
- "Where do Chinese and English views on the same thing differ"

## When not to use

- The topic is already decided and only the body copy is missing → `hot-take-writing`
- You need to assemble a WeChat Official Account article straight from RSS / search results → `ai-trend-publish`
- You need historical knowledge or conclusions from past projects → `mnemon_document_search` / `kb-builder`

## Four hard rules

1. **Free sources only by default**: HN, GitHub, Bilibili, Sogou WeChat, plus cookies from the local login state (`<BROWSER_PROFILE>`, `<COOKIE_FILE>`). Any paid API (Brave / ScrapeCreators / official X) requires a budget report first, then explicit user permission.
2. **Evidence is graded L1–L4; L1/L2/L3 can carry a topic, L4 cannot.** L3 (media paraphrase, platform rankings / heat) can carry topics such as "how hot is this, how does the ranking move", but cannot carry claims about "real user attitudes / opinions" — those require an L2 first-person post. L4 second-hand aggregation is background only.
3. **Coverage must be honest**: sources that fail to run (Weibo login state expired, Douyin needs signing, the Reddit domain is blocked, X has no key) must be written into the brief's "not covered this run"; silent skipping and pretending you checked are forbidden.
4. **Every concluding sentence must point back to an evidence id**; a sentence that cannot is written as "inference", and must say explicitly that it is an inference.

## Workflow

### Step 1 · Health check

```bash
node <skill>/scripts/probe_sources.mjs
```

Two output classes: **6 sources that can be collected by script** (HN / GitHub / Bilibili / Weibo / Zhihu / Sogou WeChat) + **3 sources that need another channel** (Douyin / Reddit / X). If the former fails, note it down first; for the latter, follow the order "public-page fallback → browser channel → model channel", and do not abandon the whole research just because sources are incomplete.

### Step 2 · Script-channel collection

```bash
node <skill>/scripts/collect_sources.mjs --topic "AI coding" \
  --q-zh "<topic in the Chinese source's own wording>" --q-en "AI coding agent" \
  --window 7 --slug "ai-coding" --out <WORKSPACE>/work/raw-<slug>.json
```

- `--window` defaults to 7, and can be changed to 14 / 30.
- **The dual-language, dual-track form must be given separate query terms**: Chinese sources use `--q-zh`, English sources use `--q-en`. With only `--topic`, both sides share it, and a Chinese topic recalls very badly on English sources.
- `--out` must always be an **absolute path**, with the file name `raw-<slug>.json`. A relative path lands in the node process's cwd (not the directory you assume); the script now resolves it to an absolute path and prints it, so stop immediately if the path looks wrong.
- Results are cached for 24 hours (`.topic-radar-cache/`, location changeable with `TOPIC_RADAR_CACHE`). On a cache hit the coverage note says "cache hit (fetched X hours ago)" — a brief produced from cache is not a fresh check, do not treat it as real time.
- Cookie directory: `TOPIC_RADAR_COOKIE_DIR` first, otherwise search upward from cwd for the `<COOKIE_FILE>` directory (so it is also found when running inside a workspace subdirectory), at most 5 levels. Each platform has its own `<COOKIE_FILE>`: Weibo (**`.weibo.cn` domain only** — mixing in a single `.weibo.com` entry yields `ok:-100`), Bilibili, Zhihu.

### Step 2.2 · Public-page fallback (default strategy, before writing "not covered")

**An expired login state — or no login at all — does not mean the source is unusable.** A large number of public pages can be read without logging in: platform rankings (Weibo trending, Zhihu hot list, Bilibili rankings, GitHub Trending), topic detail pages, question pages, video pages, official announcement pages.

Fixed order: **script channel → public-page search (`web_search` to find an entry point + `read_url` to read the body) → only then write "not covered"**. When marking a source not covered, state exactly which step blocked it (403 / anti-scraping page / needs a QR scan / only a summary available).

- Ranking-type evidence is recorded as **L3**: it is the platform's own heat signal, not a person's statement. It **can serve as the basis of a topic** (especially "how hot is this / how does the ranking move"), but when the claim lands on "how users evaluate it, why they buy in", an L2 first-person post must be added.
- First-person posts, original answers and original videos read from public pages are recorded as **L2**, provided the publish time and at least one interaction count are visible.
- If only a search summary is available and the body cannot be opened, record **L4** and write "summary only" in `note`.
- Anything you did read must be written to a file in the `extra-<slug>.json` format; do not just keep it in your head.

### Step 2.5 · Browser-channel collection (X, and the fallback when Weibo / Zhihu cookies expire)

```bash
node <skill>/scripts/collect_browser.mjs --topic "DeepSeek" --q-en "DeepSeek" --sources x \
  --out <WORKSPACE>/work/raw-browser.json
```

- Prerequisite: a local Chrome started with `--remote-debugging-port=9222`, and **that profile** is already logged in to X (or Weibo). Startup instructions are in `<BROWSER_PROFILE>/README-gemini.md`.
- It reads the rendered page (X search page, Weibo search page) and does not depend on whether cookies can be exported.
- Weibo over the browser channel accepts only entries whose time can be converted; when the search page collapses interaction counts it marks `fetched:false` — do not read that as 0 heat.

### Step 3 · Model-channel supplementary sourcing (**this step may not be skipped**)

For sources the script channel cannot cover, use `web_search` / `read_url` to fill them in one by one, and write the results to `extra-<slug>.json` in the same schema (an array; fields in `references/output-spec.md`):

- Weibo: search "topic past week", or read the public topic page
- Douyin / Zhihu: search for in-site result page summaries
- Reddit / X: search site title + summary; the Reddit domain is blocked on the workstation, so only search summaries are possible — **if you cannot extract it, do not invent it**
- Official announcements: if the official site is directly reachable, use `read_url` to get the original text (these count as L1)
- WeChat Official Account: Sogou returns redirect links; if the body cannot be read, keep title + source only and mark L4

Hold three lines while filling in: downgrade when the body is unavailable; `published_at` must be visible on the page — **if it is not visible, do not take the entry** (writing `null` gets it filtered out by the window anyway, which equals not taking it; do not write a null to comfort yourself); `excerpt` quotes the original words only, no rewriting.

### Step 4 · Merge and score

```bash
node <skill>/scripts/merge_score.mjs --raw raw-<slug>.json --extra extra-<slug>.json \
  --window 7 --out evidence-<slug>.json
```

Dedupe (same URL / same id) → merge the same event (title similarity ≥ 0.62) → score → output the evidence table. Rules in `references/scoring.md`.

**Then review the evidence table once**, but **do not hand-edit `evidence-<slug>.json`** (it is a generated artifact; a rerun wipes it). Write a `review-<slug>.json` and rerun merge:

```json
{
  "drop": ["en-github-xxxx"],
  "adjust": [
    { "id": "zh-bilibili-yyyy", "score_factor": 0.35, "grade": "L3", "relevance": 0.1, "note": "off-topic: actually a card-game video" }
  ]
}
```

- Off-topic but somewhat useful → downweight with `score_factor`; completely irrelevant → put it in `drop`.
- Grade upgrade: **only reading the official body itself on an official domain (gov.cn, a company newsroom, an official account) counts as L1**; a third party transcribing an official document (for example "a transcript of a command" on HN) still counts as L3, unless the same text can be found on the official site.
- Every review must state the reason in `note`; an adjustment without a reason counts as not reviewed.

### Step 5 · Write topics

Write angles on the basis of the evidence table and save them as `angles.json`. **3–5 by default**; if the user explicitly asks for only 1–2 (or is just spot-checking the process), follow the user — do not pad to 5.

```json
{ "topic": "…", "angles": [ { "angle": "…", "platform": "…", "claim": "…",
  "evidence_ids": ["zh-bilibili-xxxx"], "risk": "…", "assets": "…" } ] }
```

Rules: fill `evidence_ids` with L1/L2/L3 ids (L4 is not allowed); each topic needs at least 2 pieces of evidence, otherwise write "insufficient evidence, on hold"; when a claim involves "real user attitudes / opinions", at least 1 L2 is required; `risk` must state where this topic is most likely to fail (data unverified, single source, the party involved has not responded).

### Step 6 · Render and deliver

```bash
node <skill>/scripts/render_brief.mjs --evidence evidence-<slug>.json --angles angles.json --outdir <outputs directory>
```

Produces two md files (naming in `references/output-spec.md`). The timeline and the evidence table carry their own source links; no manual filling. Run the self-check before delivery:

- [ ] No source in the coverage matrix is "queried, reported success, actually empty"
- [ ] For sources marked "not supported by the script channel", if the matrix shows N supplemented entries, those N entries really exist in the body
- [ ] Every topic's evidence ids can be found in the evidence table
- [ ] No L3/L4 is used as the basis of a claim
- [ ] No "inference" is written as fact
- [ ] The coverage note says whether it was "freshly fetched this run" or a "cache hit (X hours ago)"
- [ ] File names follow `<YYYYMMDD>_<topic>_<purpose>.md`

## Known limits on the workstation (measured in a past run)

| Source | Status | Handling |
|---|---|---|
| Hacker News / GitHub / Bilibili / Weibo / Zhihu / Sogou WeChat | script-collectable | run normally (Weibo, Bilibili, Zhihu need cookies) |
| X / Twitter | needs the browser channel | `collect_browser.mjs` + a logged-in profile on port 9222; without login state, model-channel summaries only |
| Douyin | needs signing parameters | use `web_search` on public topic pages / rankings first; mark not covered only if unreadable |
| Reddit | domain blocked by the workstation's network policy | model channel only; if nothing can be obtained, write nothing |

**What to do when the login state expires**: first read public pages per step 2.2 (rankings / topics / detail pages are mostly readable), and write "cookie expired" into the coverage note at the same time; do not write off an entire source because of one 403.

**Why not use the default Chrome profile's login state**: from Chrome ≥127 cookies use App-Bound encryption (`app_bound_encrypted_key` in `Local State`), bound to "installation + profile path", so a copied profile cannot decrypt them; from Chrome 136 the default user-data-dir also ignores `--remote-debugging-port`. The login state therefore has to live in "a non-default profile opened specifically with a debugging port", and `yt-dlp --cookies-from-browser chrome` cannot get it either (measured: DB lock + ABE).

## Common pitfalls

- **Assuming the English track has results without setting `--q-en`**: HN/GitHub recall with Chinese words is near zero, so "no hot topics in the English track" usually means the query terms were wrong.
- **Taking GitHub's broad matching at face value**: repository search hits README/description, anything below 0.34 relevance is dropped automatically, and the remainder still needs model review.
- **Reading Bilibili view counts of all 0 as "nobody watches it"**: when cookies expire everything shows 0 and coverage carries a ⚠; when that happens, change cookies and rerun.
- **Bilibili HTTP 412**: running two rounds within a minute from the same IP always triggers it; it is an anti-scraping barrier, not a ban. The script already backs off 2s→8s; if that still fails, wait 1–2 minutes, do not retry a third time in a row.
- **Thinking the cache is broken after `--no-cache`**: no-cache neither reads nor writes the cache; run once normally afterwards to generate it.
- **Failures being cached**: only successful responses are written to the cache; if one source keeps failing for 24h straight, delete the corresponding file in `.topic-radar-cache/` first.
- **Stripping the query from URLs to dedupe**: Sogou/Weibo article ids live in the query, so stripping it collapses a whole page of results into 1 entry (measured: the Chinese track went 30 → 2). Strip only `#hash`.
- **Padding topics when in-window evidence is insufficient**: `merge_score` exits with code 1 when there are 0 entries. At that point answer "no valid evidence for this topic in the window" instead of inventing.
