{
  "sha": "b309c745abd04e031cc4db5e5698242152d9cc60",
  "message": "Briefing = pure bulletin: formal site articles no longer mirrored\n\nOwner direction: the briefing page duplicates stories already on 最新;\nkeep only the Perplexity-style bulleted bulletin. run_edition now fills\nsections from the grounded sweep only; the repo var\nBRIEFING_INCLUDE_ARTICLES=true restores the merged mode (machinery and\nits tests kept intact). Grounded-failure warnings are now reader-facing\nzh (quota-limited / key missing / generic) instead of raw errors, and\nthe index subtitle explains the bulletin scope.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
  "author": {
    "login": "avwire",
    "name": "avwire",
    "email": ""
  },
  "committer": {
    "login": "avwire",
    "name": "avwire",
    "email": ""
  },
  "created_at": "2026-07-27T00:00:09Z",
  "files": [
    {
      "filename": ".github/workflows/briefing.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 2,
      "deletions": 0,
      "changes": 2
    },
    {
      "filename": "pipeline/briefing.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 25,
      "deletions": 7,
      "changes": 32
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 2,
      "deletions": 2,
      "changes": 4
    },
    {
      "filename": "tests/test_briefing.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 17,
      "deletions": 0,
      "changes": 17
    }
  ],
  "diff": "@@ -86,6 +86,8 @@ jobs:\\n           # BETA grounded search sweep (owner-approved; off unless var set)\\n           BRIEFING_GROUNDED: ${{ vars.BRIEFING_GROUNDED }}\\n           BRIEFING_GROUNDED_MODEL: ${{ vars.BRIEFING_GROUNDED_MODEL }}\\n+          # bulletin-only by default; true restores merged site articles\\n+          BRIEFING_INCLUDE_ARTICLES: ${{ vars.BRIEFING_INCLUDE_ARTICLES }}\\n         run: |\\n           if [ -n \"${{ steps.ed.outputs.date }}\" ]; then\\n             python pipeline/briefing.py --edition \"${{ steps.ed.outputs.edition }}\" --date \"${{ steps.ed.outputs.date }}\"\\n@@ -105,6 +105,15 @@ def _llm_intro_enabled() -> bool:\\n             in (\"1\", \"true\", \"yes\"))\\n \\n \\n+def _include_articles() -> bool:\\n+    \"\"\"Owner decision 2026-07-27: the briefing page is a pure bulletin\\n+    (grounded sweep items only) - formal site articles live on 最新 and\\n+    are NOT mirrored here. Repo var BRIEFING_INCLUDE_ARTICLES=true\\n+    restores the old merged behaviour.\"\"\"\\n+    return (os.environ.get(\"BRIEFING_INCLUDE_ARTICLES\", \"\").strip().lower()\\n+            in (\"1\", \"true\", \"yes\"))\\n+\\n+\\n def load_editions() -> dict:\\n     editions = load_json(EDITIONS_PATH, None)\\n     if not isinstance(editions, dict) or set(editions) != {\\n@@ -725,15 +734,20 @@ def run_edition(edition: str, taipei_date: date) -> int:\\n     window = get_briefing_window(edition, taipei_date)\\n     warnings: list[str] = []\\n \\n-    articles, load_warnings = load_published_articles()\\n-    warnings.extend(load_warnings)\\n     checked_sources, source_warnings = checked_sources_snapshot()\\n     warnings.extend(source_warnings)\\n \\n     event_index = load_event_index()\\n     prune_event_index(event_index, now)\\n-    picked = select_items(articles, window, event_index, now, warnings)\\n-    sections = arrange_sections(picked, warnings)\\n+    load_warnings: list[str] = []\\n+    if _include_articles():\\n+        articles, load_warnings = load_published_articles()\\n+        warnings.extend(load_warnings)\\n+        picked = select_items(articles, window, event_index, now, warnings)\\n+        sections = arrange_sections(picked, warnings)\\n+    else:\\n+        # pure bulletin mode: sections are filled by the grounded sweep\\n+        sections = {name: [] for name in SECTIONS}\\n \\n     # BETA: owner-approved model-assisted search sweep (grounded.py).\\n     # Only Google-retrieved sources can be cited; any failure degrades to\\n@@ -749,7 +763,7 @@ def run_edition(edition: str, taipei_date: date) -> int:\\n                                for sec in sections.values() for it in sec]\\n             data, shim = grounded.call_grounded(window)\\n             if data is None:\\n-                warnings.append(\"grounded sweep skipped: no GEMINI_API_KEY\")\\n+                warnings.append(\"AI 搜尋所需的模型金鑰未設定，本期未產生條列內容\")\\n             else:\\n                 g_items, g_warnings = grounded.sanitize_items(\\n                     data, existing_titles, g_seen, now)\\n@@ -771,8 +785,12 @@ def run_edition(edition: str, taipei_date: date) -> int:\\n                     except Exception:\\n                         pass\\n     except Exception as exc:\\n-        warnings.append(f\"grounded sweep failed: \"\\n-                        f\"{type(exc).__name__}: {exc}\")\\n+        if \"429\" in str(exc):\\n+            warnings.append(\"AI 搜尋暫時受免費額度限制（配額不足），本期未能\"\\n+                            \"產生條列內容；下一期將自動重試\")\\n+        else:\\n+            warnings.append(f\"AI 搜尋本期失敗（{type(exc).__name__}: \"\\n+                            f\"{str(exc)[:120]}），已略過\")\\n \\n     # partial = our own inputs were degraded, never \"no events\".\\n     partial = bool(load_warnings or source_warnings)\\n@@ -88,7 +88,7 @@\\n         \"cats\": [\"全部\", \"事故\", \"法規\", \"商業\", \"營運\"],\\n         # daily briefings\\n         \"brKicker\": \"Daily Briefing\", \"brIndexTitle\": \"快報\",\\n-        \"brIndexSub\": \"每日三次（臺北時間 07:15／15:15／23:15）自動彙整固定八小時窗口內、已通過驗證發布的航空與交通事件。\",\\n+        \"brIndexSub\": \"每日三次（臺北時間 07:15／15:15／23:15）的條列式全球航空與交通要聞快報，由具搜尋能力的模型彙整（Beta）；本站正式新聞請見「最新」。\",\\n         \"brLatest\": \"最新快報\", \"brWindow\": \"資料範圍\", \"brCutoff\": \"資料截止\",\\n         \"brGenerated\": \"系統整理完成\", \"brTo\": \"至\",\\n         \"brEmpty\": \"截至本期資料截止時間，系統在本次查核的指定來源中，未發現符合收錄門檻的新事件。\",\\n@@ -149,7 +149,7 @@\\n         \"cats\": [\"All\", \"Safety\", \"Regulation\", \"Business\", \"Operations\"],\\n         # daily briefings\\n         \"brKicker\": \"Daily Briefing\", \"brIndexTitle\": \"Briefings\",\\n-        \"brIndexSub\": \"Three times daily (07:15 / 15:15 / 23:15 Taipei time): verified aviation and transport events from a fixed eight-hour window.\",\\n+        \"brIndexSub\": \"A bulleted global air & transport bulletin three times daily (07:15 / 15:15 / 23:15 Taipei time), compiled by a search-capable model (Beta); the site's formal articles live under Latest.\",\\n         \"brLatest\": \"Latest briefing\", \"brWindow\": \"Data window\",\\n         \"brCutoff\": \"Data cutoff\", \"brGenerated\": \"Compiled\", \"brTo\": \"to\",\\n         \"brEmpty\": \"As of this edition's data cutoff, no new events meeting the inclusion bar were found in the sources checked for this edition.\",\\n@@ -23,6 +23,9 @@\\n REPO = Path(__file__).resolve().parent.parent\\n TMP = Path(tempfile.mkdtemp(prefix=\"avwire-briefing-\"))\\n os.environ[\"AVWIRE_DATA_DIR\"] = str(TMP)\\n+# The selection/dedup/update machinery below is exercised with the merged\\n+# mode on; production default is the pure bulletin (owner 2026-07-27).\\n+os.environ[\"BRIEFING_INCLUDE_ARTICLES\"] = \"true\"\\n \\n sys.path.insert(0, str(REPO / \"pipeline\"))\\n import briefing  # noqa: E402\\n@@ -530,6 +533,20 @@ def _art_text(text):\\n check(\"fatal items render warning+red marks\",\\n       \"⚠\" in _marks_fatal and \"\\U0001f534\" in _marks_fatal)\\n \\n+# ── bulletin-only production default ─────────────────────────────────────────\\n+\\n+reset()\\n+write_sources(ok=True)\\n+write_articles([make_article(\"a-bul\", \"正式新聞不入快報\",\\n+                             tpe(2026, 7, 27, 6))])\\n+del os.environ[\"BRIEFING_INCLUDE_ARTICLES\"]\\n+briefing.main([\"--edition\", \"morning\", \"--date\", \"2026-07-27\"])\\n+os.environ[\"BRIEFING_INCLUDE_ARTICLES\"] = \"true\"\\n+bm = load_brief(\"2026-07-27-morning\")\\n+check(\"default mode keeps formal site articles OUT of the bulletin\",\\n+      bm[\"item_count\"] == 0\\n+      and all(v == [] for v in bm[\"sections\"].values()))\\n+\\n # ── caps ─────────────────────────────────────────────────────────────────────\\n \\n reset()"
}
