{
  "sha": "f0eaa940a004893a4a814ed8810e84885e64bcd6",
  "message": "fix: order news by displayed source publication time (#234)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "web-flow",
    "name": "",
    "email": ""
  },
  "created_at": "2026-10-01T06:14:17Z",
  "files": [
    {
      "filename": ".github/deploy-trigger",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 6,
      "deletions": 4,
      "changes": 10
    },
    {
      "filename": "tests/test_news_chronology.py",
      "previous_filename": "",
      "status": "added",
      "additions": 54,
      "deletions": 0,
      "changes": 54
    }
  ],
  "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\")"
}
