Briefing = pure bulletin: formal site articles no longer mirrored
Owner direction: the briefing page duplicates stories already on 最新;
keep only the Perplexity-style bulleted bulletin. run_edition now fills
sections from the grounded sweep only; the repo var
BRIEFING_INCLUDE_ARTICLES=true restores the merged mode (machinery and
its tests kept intact). Grounded-failure warnings are now reader-facing
zh (quota-limited / key missing / generic) instead of raw errors, and
the index subtitle explains the bulletin scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full 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()