{
  "sha": "a8bbd76a65adb780e6f6cd9c8c28474d17b597a6",
  "message": "Restrict news to aviation and withdraw Arctic shipping article (#304)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "web-flow",
    "name": "",
    "email": ""
  },
  "created_at": "2026-10-13T14:08:12Z",
  "files": [
    {
      "filename": ".github/workflows/hourly.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 0,
      "changes": 1
    },
    {
      "filename": "data/articles/2026-10-13T13.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 3,
      "deletions": 1,
      "changes": 4
    },
    {
      "filename": "data/flashes.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 3,
      "deletions": 1,
      "changes": 4
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 2,
      "deletions": 2,
      "changes": 4
    },
    {
      "filename": "pipeline/common.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 42,
      "deletions": 51,
      "changes": 93
    },
    {
      "filename": "pipeline/write.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 7,
      "deletions": 4,
      "changes": 11
    },
    {
      "filename": "tests/test_scope.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 33,
      "deletions": 3,
      "changes": 36
    }
  ],
  "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()"
}
