2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM Auto-updates hourly 中文 EN
LIVE WIRE
← Back to the site changelog Site Changelog

Original commit record

Restrict news to aviation and withdraw Arctic shipping article (#304)

SHA
a8bbd76a65adb780e6f6cd9c8c28474d17b597a6
Commit date
Author
nnneeeooo-nnneeeooo
Committer
web-flow

Changed files (7)

  • .github/workflows/hourly.yml modified · +1 / −0
  • data/articles/2026-10-13T13.json modified · +3 / −1
  • data/flashes.json modified · +3 / −1
  • pipeline/build.py modified · +2 / −2
  • pipeline/common.py modified · +42 / −51
  • pipeline/write.py modified · +7 / −4
  • tests/test_scope.py modified · +33 / −3

Full diff

@@ -46,6 +46,7 @@ jobs:\n       - run: pip install -r requirements.txt\n       - name: Run pipeline regression tests\n         run: |\n+          python tests/test_scope.py\n           python tests/test_companion.py\n           python tests/test_flightwatch.py\n           python tests/test_airport_codes.py\n@@ -362,7 +362,9 @@\n           "sourceUrl": "https://www.cna.com.tw/news/aopl/202610130286.aspx"\n         }\n       ],\n-      "evidenceVersion": 2\n+      "evidenceVersion": 2,\n+      "archived": true,\n+      "archiveReason": "非航空新聞:主題為北極海運貨櫃輪,依航空限定編輯範圍撤下。"\n     },\n     {\n       "id": "a-20261013-1326-delta-flight-234-inspected-at-ben-gurion",\n@@ -61,7 +61,9 @@\n     "pinned": true,\n     "zh": "李在明歡迎北極航線貨櫃輪返國 韓國目標2030年開通定期服務",\n     "en": "Lee welcomes Arctic-route container ship home; South Korea targets 2030 regular service",\n-    "articleId": "a-20261013-1326-south-korea-s-lee-welcomes-return-of-arc"\n+    "articleId": "a-20261013-1326-south-korea-s-lee-welcomes-return-of-arc",\n+    "archived": true,\n+    "archiveReason": "非航空新聞:主題為北極海運貨櫃輪,依航空限定編輯範圍撤下。"\n   },\n   {\n     "timeUtc": "2026-10-13T13:26Z",\n@@ -177,7 +177,7 @@ def static_asset_version() -> str:\n         "cats": ["全部", "事故", "法規", "商業", "營運", "軍事"],\n         "searchKicker": "News Search", "searchTitle": "搜尋新聞",\n         "newsKicker": "News Index", "newsTitle": "全部新聞",\n-        "newsSub": "依發布時間排列的本站航空與運輸新聞索引。",\n+        "newsSub": "依發布時間排列的本站航空新聞索引。",\n         "paginationLabel": "新聞分頁", "previousPage": "上一頁",\n         "nextPage": "下一頁", "pageLabel": "第 {page} / {pages} 頁",\n         "headerSearchPlaceholder": "搜尋班號,例如 CI100",\n@@ -312,7 +312,7 @@ def static_asset_version() -> str:\n                  "Military"],\n         "searchKicker": "News Search", "searchTitle": "Search news",\n         "newsKicker": "News Index", "newsTitle": "All news",\n-        "newsSub": "An archive of SKYTICAL aviation and transport news, ordered by publication time.",\n+        "newsSub": "An archive of SKYTICAL aviation news, ordered by publication time.",\n         "paginationLabel": "News pagination", "previousPage": "Previous",\n         "nextPage": "Next", "pageLabel": "Page {page} of {pages}",\n         "headerSearchPlaceholder": "Search a flight number, e.g. CI100",\n@@ -139,15 +139,15 @@ def group_has_material(group: dict) -> bool:\n \n \n # Broad Taiwan feeds use one shared allowlist so a new category cannot drift\n-# away from the established aviation/transport coverage gate.\n+# away from the aviation-only coverage gate. The legacy constant name is\n+# retained for callers; maritime, rail and road transport are out of scope.\n TAIWAN_TRANSPORT_KEYWORDS = (\n     "航空", "民航", "飛安", "航班", "航機", "飛機", "客機",\n     "貨機", "小型機", "機場", "航線", "空域", "空管", "跑道",\n     "墜機", "迫降", "轉降", "起飛", "降落", "華航", "中華航空",\n     "長榮航", "星宇", "台灣虎航", "臺灣虎航", "立榮", "華信",\n-    "空軍", "軍機", "共機", "中共機艦", "共艦", "軍艦", "軍港",\n-    "無人機", "直升機", "戰機", "轟炸機", "台鐵", "臺鐵",\n-    "高鐵", "捷運", "航港", "港務", "小三通", "船班",\n+    "空軍", "軍機", "共機", "中共機艦",\n+    "無人機", "直升機", "戰機", "轟炸機",\n )\n \n TAIWAN_AIRLINE_KEYWORDS = (\n@@ -466,15 +466,15 @@ def group_has_material(group: dict) -> bool:\n         "fmt": "RSS",\n         "url": "https://www.cna.com.tw",\n         # official CNA feed (broad politics wire); the keyword filter below\n-        # keeps only aviation / military-aviation / transport items so the\n+        # keeps only civil / military aviation items so the\n         # editorial stage never burns model calls on unrelated politics\n         "endpoint": "https://feeds.feedburner.com/rsscna/politics",\n         "type": "rss",\n         "keywords": list(TAIWAN_TRANSPORT_KEYWORDS),\n         "scope_filter": True,\n         "cover": {\n-            "zh": "中央社 — 航空、軍航與交通相關報導",\n-            "en": "CNA Taiwan — aviation, military air & transport",\n+            "zh": "中央社 — 民航與軍事航空報導",\n+            "en": "CNA Taiwan — civil and military aviation",\n         },\n     },\n     "cnataiwanfinance": {\n@@ -487,8 +487,8 @@ def group_has_material(group: dict) -> bool:\n         "keywords": list(TAIWAN_TRANSPORT_KEYWORDS),\n         "scope_filter": True,\n         "cover": {\n-            "zh": "中央社產經證券 — 國籍航空、航太與交通產業",\n-            "en": "CNA business — Taiwan airlines, aerospace & transport",\n+            "zh": "中央社產經證券 — 國籍航空與航空產業",\n+            "en": "CNA business — Taiwan airlines and aviation industry",\n         },\n     },\n     "cnataiwanairlines": {\n@@ -527,25 +527,8 @@ def group_has_material(group: dict) -> bool:\n         "type": "rss",\n         "scope_filter": True,\n         "cover": {\n-            "zh": "國防部/軍聞社 — 軍事航空、機艦與運輸動態",\n-            "en": "Taiwan MND — military aviation, vessels & transport",\n-        },\n-    },\n-    "twtransport": {\n-        "name": "臺灣交通要聞",\n-        "kind": "media",\n-        "fmt": "RSS",\n-        "url": "https://news.google.com",\n-        "endpoint": (\n-            "https://news.google.com/rss/search?"\n-            "q=(台鐵%20OR%20高鐵%20OR%20捷運%20OR%20航港局%20OR%20小三通)"\n-            "%20(事故%20OR%20停駛%20OR%20停航%20OR%20出軌%20OR%20恢復)"\n-            "%20when:1d&hl=zh-TW&gl=TW&ceid=TW:zh-Hant"\n-        ),\n-        "type": "rss",\n-        "cover": {\n-            "zh": "台鐵/高鐵/捷運/海運重大營運與事故報導",\n-            "en": "Taiwan rail / metro / maritime major events",\n+            "zh": "國防部/軍聞社 — 軍事航空動態",\n+            "en": "Taiwan MND — military aviation",\n         },\n     },\n }\n@@ -599,27 +582,19 @@ def _load_manufacturer_source_catalog() -> dict:\n CATEGORIES = ("safety", "reg", "biz", "ops", "mil")\n \n # Site-wide subject gate.  A military label does not by itself make a story\n-# relevant to an aviation and transport publication: the material must still\n-# contain a concrete aviation, airspace, maritime, rail or road-transport\n+# relevant to an aviation publication: the material must still\n+# contain a concrete civil/military aviation or airspace\n # subject.  Keep these terms specific enough that generic politics/defence\n # stories (budgets, speeches, presidential inspections, land exercises) fail.\n _TRANSPORT_ZH_TERMS = (\n     # civil and military aviation\n     "航空", "民航", "飛安", "航班", "航機", "飛機", "小型機", "客機",\n-    "貨機", "機場", "航線", "飛航", "空域", "空管", "航管", "塔台",\n+    "貨機", "機場", "飛航", "空域", "空管", "航管", "塔台",\n     "跑道", "起飛", "降落", "墜機", "空難", "迫降", "轉降", "客艙",\n     "空服員", "機師", "飛行員", "直升機", "無人機", "戰機", "軍機",\n     "共機", "轟炸機",\n     "機艦", "航太", "波音", "空中巴士", "華航", "中華航空", "長榮航",\n     "星宇", "台灣虎航", "臺灣虎航", "立榮", "華信",\n-    # maritime transport and military vessels\n-    "共艦", "軍艦", "軍港", "航空母艦", "航母", "艦艇", "船班", "船舶",\n-    "客輪", "貨輪", "渡輪", "郵輪", "海運", "航運", "航港", "港務",\n-    "港口", "碼頭", "小三通", "停航", "擱淺", "沉船",\n-    # rail and road transport\n-    "台鐵", "臺鐵", "高鐵", "捷運", "鐵路", "列車", "火車", "軌道",\n-    "出軌", "停駛", "客運", "公車", "巴士", "高速公路", "道路運輸",\n-    "交通事故",\n )\n _TRANSPORT_EN_RE = re.compile(\n     r"\b(?:"\n@@ -628,23 +603,21 @@ def _load_manufacturer_source_catalog() -> dict:\n     r"air\s+cargo|airspace|air\s+traffic|flights?|runways?|pilots?|"\n     r"cabin\s+crew|flight\s+attendants?|boeing|airbus|e-?vtol|ads-?b|"\n     r"drones?|helicopters?|fighter\s+(?:jets?|aircraft)|bombers?|"\n-    r"warships?|naval\s+(?:vessels?|base|port)|aircraft\s+carriers?|"\n-    r"shipping|maritime|ports?|harbou?rs?|ferr(?:y|ies)|cruise\s+ships?|"\n-    r"cargo\s+ships?|passenger\s+ships?|vessels?|"\n-    r"railways?|railroads?|trains?|metros?|subways?|high-?speed\s+rail|"\n-    r"buses?|coaches|highways?|road\s+transport|transportation"\n+    r"air\s+force|air\s+base|airbase"\n     r")\b",\n     re.IGNORECASE,\n )\n _TRANSPORT_HEADLINE_ZH_TERMS = (\n-    "豪華經濟艙", "商務艙", "機組員", "紅箭", "火箭", "探空", "地滑翔機",\n-    "航艦", "無人載具", "教練機", "開航", "航展", "登機門", "候機廳",\n-    "指揮控制", "發動機", "原型機", "史基浦", "達美", "墨航",\n+    "豪華經濟艙", "商務艙", "機組員", "紅箭", "地滑翔機",\n+    "教練機", "航展", "登機門", "候機廳",\n+    "史基浦", "達美", "墨航", "電磁彈射", "蒸汽彈射",\n )\n _TRANSPORT_HEADLINE_EN_RE = re.compile(\n     r"\b(?:air\s+force|airshow|air\s+show|aircrew|red\s+arrows?|"\n     r"seaglider|schiphol|farnborough|gulfstream|cessna|lockheed|"\n-    r"recaro|collins|delta|rafale|fighter|prototype|rocket|missile|"\n+    r"recaro|collins|delta|rafale|fighter|"\n+    r"atr|wizz\s+air|westjet|airadvisor|rolls[-\s]+royce|trent\s+7000|"\n+    r"emals|steam\s+catapults?|"\n     r"air\s+base|airbase)\b",\n     re.IGNORECASE,\n )\n@@ -682,7 +655,16 @@ def _string_values(value):\n \n \n def is_transport_story(*records) -> bool:\n-    """True only for concrete aviation or transport subject matter."""\n+    """Legacy API: accept only aviation, with a title-level subject gate.\n+\n+    Source names, URLs and incidental body mentions cannot qualify a story.\n+    """\n+    items = [item for record in records if isinstance(record, dict)\n+             for item in (record.get("items") or []) if isinstance(item, dict)]\n+    headlines = items or [record for record in records\n+                         if list(_headline_values(record))]\n+    if headlines:\n+        return is_transport_headline(*headlines)\n     text = " ".join(\n         part for record in records for part in _string_values(record)\n     )\n@@ -743,7 +725,7 @@ def _headline_names_airline(headline: str, records) -> bool:\n \n \n def is_transport_headline(*records) -> bool:\n-    """True when a story's headline itself names a transport subject.\n+    """Legacy API: true when the headline itself names an aviation subject.\n \n     Summaries and body paragraphs often mention an airline only as a partner,\n     advertiser or piece of background context.  Broad feeds therefore need a\n@@ -757,7 +739,16 @@ def is_transport_headline(*records) -> bool:\n     if (_MILITARY_REPLICA_ZH_RE.search(headline)\n             or _MILITARY_REPLICA_EN_RE.search(headline)):\n         return False\n-    return (is_transport_story({"title": headline})\n+    # A ship's "航線"/"航班" and an aircraft carrier's name are not\n+    # aviation evidence. Retain genuine aircraft activity on carriers.\n+    headline = re.sub(r"航空母艦|aircraft\s+carriers?", "", headline,\n+                      flags=re.IGNORECASE)\n+    if re.search(r"貨櫃輪|貨輪|客輪|船舶|船班|海運|航運|渡輪|郵輪|"\n+                 r"container\s+ship|shipping|maritime|ferr(?:y|ies)|vessels?",\n+                 headline, re.IGNORECASE):\n+        headline = re.sub(r"航班|起飛|降落", "", headline)\n+    return (any(term in headline for term in _TRANSPORT_ZH_TERMS)\n+            or _TRANSPORT_EN_RE.search(headline) is not None\n             or any(term in headline for term in _TRANSPORT_HEADLINE_ZH_TERMS)\n             or _TRANSPORT_HEADLINE_EN_RE.search(headline) is not None\n             or _AIRCRAFT_MODEL_RE.search(headline) is not None\n@@ -414,18 +414,21 @@ def simplified_chinese_error(draft: dict) -> str | None:\n \n EDITORIAL SCOPE GATE:\n - SKYTICAL publishes only civil/military aircraft, airlines, airports, flights,\n-  airspace/ATC, aviation industry and safety, maritime transport, vessels,\n-  ports, rail, metro or road transport.\n+  airspace/ATC, aviation industry and safety. The core event must directly\n+  concern civil or military aviation. Maritime shipping, vessels, ports,\n+  rail, metro and road transport are OUT-OF-SCOPE unless the core event\n+  directly affects aircraft, flights or airport operations. 航線 and 航班\n+  can describe ships and are not sufficient evidence of aviation relevance.\n - A defence ministry, armed forces, exercise, president, budget, weapon or\n   military label alone is insufficient. Military stories qualify only when\n   the evidenced core event directly concerns aircraft, airspace, aviation,\n-  vessels/ports or a concrete transport operation.\n+  or a concrete flight operation.\n - General politics, rebuttals, speeches, elections, defence-policy positions,\n   land exercises and presidential inspection protocol are OUT-OF-SCOPE and\n   must be rejected.\n - Military replicas, scale models and mock-ups are OUT-OF-SCOPE when the core\n   event is the replica or political/budget statement. Incidental warships,\n-  jets, bases or ports do not make an item aviation or transport news.\n+  jets, bases or ports do not make an item aviation news.\n \n AVIATION SAFETY RULES (strictest standard):\n - For accidents, serious incidents, emergency/forced landings, diversions,\n@@ -22,6 +22,8 @@\n         "2026-07-27T04.json",\n     "a-20260727-0443-defense-minister-clarifies-no-live-fire":\n         "2026-07-27T04.json",\n+    "a-20261013-1326-south-korea-s-lee-welcomes-return-of-arc":\n+        "2026-10-13T13.json",\n }\n \n \n@@ -95,12 +97,36 @@ def main() -> None:\n     assert common.is_transport_story(story(\n         "國防部公布共機艦動態",\n         "偵獲共機4架次、共艦7艘於台海周邊活動。"))\n-    assert common.is_transport_story(story(\n+    assert not common.is_transport_story(story(\n         "台鐵列車故障部分區間停駛"))\n-    assert common.is_transport_story(story(\n+    assert not common.is_transport_story(story(\n         "港務公司公告客輪停航"))\n-    assert common.is_transport_story(story(\n+    assert not common.is_transport_story(story(\n         "高速公路客運事故造成回堵"))\n+    for title in (\n+        "李在明歡迎北極航線貨櫃輪返國 拚2030年前開通定期服務",\n+        "South Korea's Lee welcomes return of Arctic route container ship",\n+        "海運公司宣布貨輪定期航班服務", "航空母艦返港",\n+        "Aircraft carrier returns to naval port", "Railway resumes service",\n+        "Metro train derails", "Highway bus crash", "共艦通過台灣海峽",\n+    ):\n+        item = {"title": title, "summary": "航空公司為背景合作夥伴。"}\n+        assert not common.is_transport_headline(item), title\n+        assert not common.is_transport_story(story(title, item["summary"])), title\n+        assert not fetch._matches_keywords(item, common.TAIWAN_TRANSPORT_KEYWORDS)\n+        assert write.build_article(\n+            draft(), story(title), datetime.now(timezone.utc), set()) is None\n+    for title in (\n+        "貨輪事故影響機場聯外道路 航班延誤", "航母艦載戰機起飛訓練",\n+        "Aircraft carrier launches fighter jets", "FAA updates safety rules",\n+        "空軍接收新型教練機", "華航新增北極航線",\n+        "ATR identifies potential new routes in Indonesia",\n+        "Wizz Air postpones six Abu Dhabi routes",\n+        "Rolls-Royce triples 787 engine overhaul interval",\n+        "Aircraft carrier switches to steam catapults",\n+    ):\n+        assert common.is_transport_headline({"title": title}), title\n+    assert "twtransport" not in common.SOURCES\n     assert not common.is_transport_story(story(\n         "國防預算攻防延燒 朝野公布最新主張"))\n     assert not common.is_transport_story(story(\n@@ -153,5 +179,9 @@ def main() -> None:\n     print("test_scope: OK")\n \n \n+def test_scope_regressions() -> None:\n+    main()\n+\n+\n if __name__ == "__main__":\n     main()

You found a secret theme!

8-bit mode is unlocked. Refresh to add it to your theme selector.