Method
How the desk decides what you see
Every step is deterministic and runs locally from JSON files: the same inputs always produce the same site. Nothing here calls a model, a feed or a database.
Demo data
Every organisation, product, source and report in this build is fictional and written for the demo. Reports have no links, because a made-up link would look like a real citation. The desk clock is frozen at 2026-03-12T09:00:00Z; all ages, heat values, trends and freshness states are computed against it, so nothing here can look live. The build refuses any report or source not marked as demo.
Pipeline
- Validate every source, topic, entity, report and override; stop on any problem.
- Deduplicate: drop re-filed items and mark syndicated copies.
- Group reports into events and log every decision.
- Score heat, trend, status and freshness at the demo clock.
- Compile daily editions by rule.
- Render and audit the static site; refuse unsafe output and broken links.
Run python3 tools/explain.py to print every decision, or read the desk log.
Duplicates and syndicated copies
A duplicate is the same source filing the same title again, usually a re-fetch. It is dropped and never counts. A syndicated copy is another source carrying identical title and summary text within 24 hours, or a report that declares its origin with via. It stays on the event's timeline for provenance but is not an independent source, so it adds nothing to heat or the source count. In this build: 1 duplicate dropped, 1 syndicated copy marked.
Grouping reports into events
Reports are visited oldest first. Each is compared with every report in every event whose latest report is less than 14 days old:
similarity = 0.6 × J(entities) + 0.4 × J(words)
J is the Jaccard index: shared items divided by all items. Words are those of the title and summary, without stop words. Two reports that share no named entity score zero, so similar wording alone never merges stories. The best event joins at 0.35 or more; otherwise the report starts a new event. A report arriving more than 24 hours after an event's first report is a follow-up; earlier ones are coverage.
The threshold of 0.35 was set on this demo data. In this build the weakest join scores 0.362 and the strongest rejected candidate 0.283; the intended stories are pinned in tests/expected_events.json and checked by the test suite. Real data needs a labelled set of report pairs to set it. Editor overrides in data/overrides.json are applied after the algorithm and are never overwritten by it; this build has one, recorded on the event page and in the log.
Entities are hand-labelled on demo reports. In production they would come from an extraction step, and candidate search would use embeddings rather than comparing every pair.
Heat
heat = 10 × Σ 0.5age / 24 h
The sum runs over the event's independent sources. For each source only its newest report within the last 48 hours counts, so one outlet filing ten stories counts once, and a story nobody has touched for two days falls to zero. A source that reported just now adds 10; a day later it adds 5. The bars on this site are full at 50.
Worked example, the top event at the demo clock (Introducing Orchard 3: open-weight models in three sizes):
- Orchard Labs Blog: 19 h old, 10 × 0.519.00/24 = 5.78
- Signal Wire: 18.3 h old, 10 × 0.518.33/24 = 5.89
- Circuit Review: 16.5 h old, 10 × 0.516.50/24 = 6.21
- Builders Forum top threads: 15 h old, 10 × 0.515.00/24 = 6.48
- Gradient Gazette: 1.5 h old, 10 × 0.51.50/24 = 9.58
Total 33.9. These are demo values computed against a frozen clock; they are not live.
Status and trend
- New
- First seen within the last 24 hours before the demo clock.
- Follow-up
- A follow-up report arrived within the last 24 hours.
- Continuing
- More coverage within the last 24 hours, but no follow-up.
- Earlier
- Nothing new within the last 24 hours.
Trend compares heat now with heat 6 hours earlier. Rising needs a gain of at least 3 and of 25 percent; cooling a loss of at least 3. Events first seen within those 6 hours are new instead. When a source that contributed is stale, the trend is marked incomplete.
Editions
The edition for a day is compiled at 08:00 UTC from reports in the previous 24 hours. Events need 2 independent sources or an official first-hand source to lead; the rest are listed as also noted. One entry per event, ranked by heat at compile time, at most 6. An event already featured returns only with a follow-up in the window. Reports after compile time wait for the next edition; the briefing shows them under "Since this edition".
Freshness
Each source declares how often it publishes. When its last check is more than 1 interval old it is late; beyond 3 intervals it is stale. The briefing and the sources page say so plainly instead of implying the desk is complete.
Exports
Generated locally with the site, from the same data, and marked as demo in every payload:
- feed.xml: RSS 2.0, one item per edition. Item links use the placeholder base
https://news.tongjiai.icu/zh(a reserved, non-resolving domain) until a real HTTPS domain exists. - api/v1/index.json: lists the JSON files below.
- api/v1/events.json, api/v1/topics.json and one file per edition under
api/v1/editions/.
These are static files shaped like an API, not a running service: no query parameters, no pagination, no authentication.
Parameters in this build
heat.window_hours | 48 |
|---|---|
heat.half_life_hours | 24 |
heat.scale | 10 |
heat.trend_hours | 6 |
heat.rising_min_delta | 3.0 |
heat.rising_min_ratio | 0.25 |
heat.series_hours | 24 |
grouping.window_days | 14 |
grouping.threshold | 0.35 |
grouping.entity_weight | 0.6 |
grouping.token_weight | 0.4 |
grouping.followup_after_hours | 24 |
grouping.syndication_window_hours | 24 |
edition.compile_hour_utc | 8 |
edition.max_items | 6 |
edition.min_independent_sources | 2 |
freshness.late_after_intervals | 1 |
freshness.stale_after_intervals | 3 |
What works now, and what needs more
Works in this build
- Validation, deduplication, syndication detection and event grouping with a full decision log.
- Reproducible heat, trend, status and freshness at a fixed clock.
- Daily editions by rule, topic pages, a timeline per event, search and filters.
- Local read marks, RSS and JSON exports, all generated offline.
Needs real infrastructure
- Real feeds and a scheduler to fetch them, with per-source rate limits.
- A database for history beyond what fits in files.
- Entity extraction and embeddings for candidate search, and a model or editor to judge follow-ups.
- Licensing review of every real source before anything is republished.