Full diff
@@ -3,5 +3,5 @@\n This file intentionally triggers the manual-article-deploy workflow.\n \n source_main_commit=370d81ed1fc0465c7d39f2c2842f4105680db802\n-reason=publish manual Tigerair Taiwan B-50031 delivery article\n+reason=sort news feeds and search by displayed source publication time\n triggered_at=2026-10-01\n@@ -1201,8 +1201,8 @@ def side(own, other):\n image = normalize_image(selected_image, titles=[\n (raw.get(lang) or {}).get("title") for lang in ("zh", "en")])\n # Display the SOURCE's newest publication time when the write stage\n- # recorded one; the generation time (dt) is used for ordering only, so\n- # a days-old official release is never presented as breaking news.\n+ # recorded one. Chronological feeds use this same timestamp so a late\n+ # generated article cannot place an older source above newer news.\n display_dt = publication_dt\n if raw.get("sourcePublishedUtc"):\n try:\n@@ -1227,6 +1227,7 @@ def side(own, other):\n return {\n "id": art_id,\n "dt": dt,\n+ "news_dt": display_dt,\n "published_dt": publication_dt,\n "modified_dt": modified_dt,\n "published_iso": iso_timestamp(publication_dt),\n@@ -1281,7 +1282,8 @@ def collect_articles():\n prev = by_id.get(a["id"])\n if prev is None or a["dt"] > prev["dt"]:\n by_id[a["id"]] = a\n- return sorted(by_id.values(), key=lambda a: a["dt"], reverse=True)\n+ return sorted(by_id.values(),\n+ key=lambda a: (a["news_dt"], a["id"]), reverse=True)\n \n \n @lru_cache(maxsize=1)\n@@ -1920,7 +1922,7 @@ def search_index_item(article, alias_groups, prompt_aliases=(),\n },\n "source": article["source"],\n "date": article["meta_ts"],\n- "published": article["dt"].isoformat(),\n+ "published": article["news_dt"].isoformat(),\n "articleFormat": article["article_format"],\n "availableLanguages": article["available_languages"],\n "airlines": article_airline_keys(article, airline_catalog),\n@@ -0,0 +1,54 @@\n+"""Chronological feeds and search must agree with the displayed news time."""\n+import json\n+import sys\n+import tempfile\n+from pathlib import Path\n+from unittest.mock import patch\n+\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "pipeline"))\n+import build\n+\n+\n+def test_news_chronology():\n+ def article(id, published, source=None, sort=None):\n+ row = {\n+ "id": id, "publishedUtc": published, "cat": "ops",\n+ "zh": {"title": "台灣虎航接收A320neo客機", "body": ["交機新聞。"]},\n+ "sources": [{"name": "Test", "url": "https://example.com/news"}],\n+ }\n+ if source is not None:\n+ row["sourcePublishedUtc"] = source\n+ if sort is not None:\n+ row["sortUtc"] = sort\n+ return row\n+\n+ rows = [\n+ article("older-late-generated", "2026-10-01T02:00:00Z",\n+ "2026-09-28T08:00:00Z"),\n+ article("newer-manual", "2026-09-30T08:50:00Z",\n+ "2026-09-30T08:50:00Z", "2026-09-20T00:00:00Z"),\n+ article("no-source-time", "2026-09-29T08:00:00Z"),\n+ article("invalid-source-time", "2026-09-27T08:00:00Z", "invalid"),\n+ article("date-only-source", "2026-10-01T03:00:00Z",\n+ "2026-09-26T00:00:00Z"),\n+ ]\n+ with tempfile.TemporaryDirectory() as tmp:\n+ folder = Path(tmp)\n+ (folder / "fixture.json").write_text(json.dumps({"articles": rows}))\n+ with patch.object(build, "ARTICLES_DIR", folder):\n+ collected = build.collect_articles()\n+ assert [a["id"] for a in collected] == [\n+ "newer-manual", "no-source-time", "older-late-generated",\n+ "invalid-source-time", "date-only-source",\n+ ]\n+ assert collected[0]["meta_ts"] == "2026-09-30 4:50 PM UTC+8"\n+ assert collected[-1]["meta_ts"] == "2026-09-26"\n+ for prepared in collected:\n+ indexed = build.search_index_item(prepared, [])\n+ assert build.parse_iso(indexed["published"]) == prepared["news_dt"]\n+ assert collected[0]["published_iso"] == "2026-09-30T08:50:00Z"\n+\n+\n+if __name__ == "__main__":\n+ test_news_chronology()\n+ print("PASS news chronology, source-time fallback and search timestamps")