2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM 每小時自動更新 中文 EN
LIVE 快訊
← 返回網站更新紀錄 Site Changelog

提交原始紀錄

Rare-aircraft observations auto-publish through a machine gate Owner policy change (2026-07-27): routine rare-aircraft observation items no longer wait for a human. write.py now publishes a drafted flight event directly when ALL of these hold: - ADS-B sources are not in conflict (crossCheck != conflicting_sources) - the draft is not safety-flavoured (cat != safety, incident == null, no accident/casualty/investigation/conflict risk flags) - validation and verbatim-quote verification passed as before Anything else still goes to data/review.json exactly as promised on the public methodology page (safety events remain human-gated). Repo var FLIGHT_AUTO_PUBLISH=false restores review-everything behaviour; the flight prompt no longer asks the model to mention any review workflow inside article text (publication policy is code's job, not the model's). tests: test_flightwatch.py reworked - auto-publish happy path, conflict gate, safety-flag gate, kill switch; all 8 suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

SHA
7f4234edf5a42e6e85b6180a29cb007c4075b698
提交時間
作者
avwire
提交者
avwire

變更檔案 (4)

  • .github/workflows/hourly.yml modified · +2 / −0
  • pipeline/write.py modified · +67 / −13
  • prompts/rare-aircraft-news-zh.txt modified · +6 / −3
  • tests/test_flightwatch.py modified · +72 / −20

完整差異

@@ -51,6 +51,8 @@ jobs:\n           NEWS_MAX_AGE_HOURS: ${{ vars.NEWS_MAX_AGE_HOURS }}\n           # private usage-dashboard URL token (page skipped when unset)\n           AVWIRE_USAGE_TOKEN: ${{ secrets.AVWIRE_USAGE_TOKEN }}\n+          # rare-aircraft auto-publish kill switch (code default: on)\n+          FLIGHT_AUTO_PUBLISH: ${{ vars.FLIGHT_AUTO_PUBLISH }}\n         run: |\n           python pipeline/fetch.py\n           python pipeline/dedupe.py\n@@ -27,6 +27,7 @@\n """\n from __future__ import annotations\n \n+import os\n import re\n from datetime import timedelta\n \n@@ -839,13 +840,47 @@ def _review_entry(candidate: dict, group: dict, writer: str, facts: list,\n     }\n \n \n+# Machine gate for rare-aircraft auto-publish (owner's 2026-07-27 policy:\n+# routine observation items publish without a human). Anything that smells\n+# of a safety event, a source conflict or an incident still goes to the\n+# human queue - the public methodology page promises that.\n+_FLIGHT_BLOCKING_FLAGS = {\n+    "accident_or_serious_incident", "casualties_or_injuries",\n+    "investigation", "conflicting_information",\n+}\n+\n+\n+def _flight_auto_publish_enabled() -> bool:\n+    # default ON per the owner; repo var FLIGHT_AUTO_PUBLISH=false restores\n+    # the review-everything behaviour\n+    return (os.environ.get("FLIGHT_AUTO_PUBLISH", "true").strip().lower()\n+            not in ("0", "false", "no"))\n+\n+\n+def _flight_review_reason(event, candidate):\n+    """None when the observation may auto-publish, else the zh reason."""\n+    if not _flight_auto_publish_enabled():\n+        return "自動發布已停用(FLIGHT_AUTO_PUBLISH=false),須人工確認後發布"\n+    if event.get("crossCheck") == "conflicting_sources":\n+        return "兩個 ADS-B 來源觀測結果不一致,須人工確認後發布"\n+    if candidate.get("cat") == "safety" or candidate.get("incident"):\n+        return "涉及飛安事件,依編輯規範一律人工覆核"\n+    flags = candidate.get("riskFlags")\n+    flags = set(flags) if isinstance(flags, list) else set()\n+    if flags & _FLIGHT_BLOCKING_FLAGS:\n+        return "模型標記高風險旗標,須人工確認後發布"\n+    return None\n+\n+\n def _process_flight_events(queue, providers, dead_platforms, dead_auth,\n-                           ai_calls, queued_entries, now):\n+                           ai_calls, queued_entries, now,\n+                           new_articles, new_flashes, used_ids):\n     """Draft queued flight-observation events with the dedicated prompt.\n \n-    Outcomes per event: queued into the human-review file (the ONLY path to\n-    publication), rejected by the model (dropped for good), or left in the\n-    queue when providers fail (retried next hour). Never auto-published.\n+    Outcomes per event: auto-published (routine observation that clears\n+    the machine gate above), queued into the human-review file (gate\n+    failed), rejected by the model (dropped for good), or left in the\n+    queue when providers fail (retried next hour).\n     """\n     remaining = []\n     prompt = flightnews.flight_prompt()\n@@ -911,8 +946,28 @@ def _process_flight_events(queue, providers, dead_platforms, dead_auth,\n                 print(f"write: {provider.label} flight draft has no "\n                       "machine-verifiable sourceQuote; trying next provider")\n                 continue\n-            # v1 hard rule: rare-aircraft items are ALWAYS human-gated,\n-            # regardless of what the model claimed.\n+            review_reason = _flight_review_reason(event, candidate)\n+            if review_reason is None:\n+                # Machine gate cleared: routine, non-safety observation with\n+                # consistent sources - publish without a human.\n+                candidate["status"] = "publish"\n+                candidate["requiresHumanReview"] = False\n+                candidate["riskFlags"] = []\n+                candidate["decisionReason"] = ""\n+                article = build_article(candidate, group, now, used_ids,\n+                                        provider.label, facts)\n+                if article is None:\n+                    print(f"write: flight event {event.get('eventId')} has "\n+                          "no usable sources; dropping")\n+                    outcome = "rejected"\n+                    break\n+                new_articles.append(article)\n+                new_flashes.append(build_flash(candidate, article["id"], now))\n+                print(f"write: flight event {event.get('eventId')} "\n+                      f"auto-published as {article['id']} "\n+                      f"(crossCheck={event.get('crossCheck')})")\n+                outcome = "published"\n+                break\n             candidate["status"] = "manual_review"\n             candidate["requiresHumanReview"] = True\n             flags = candidate.get("riskFlags")\n@@ -921,9 +976,7 @@ def _process_flight_events(queue, providers, dead_platforms, dead_auth,\n             if "unconfirmed_or_developing" not in flags:\n                 flags.append("unconfirmed_or_developing")\n             candidate["riskFlags"] = flags\n-            if not candidate.get("decisionReason"):\n-                candidate["decisionReason"] = \\n-                    "自動 ADS-B 觀測事件,須人工確認後發布"\n+            candidate["decisionReason"] = review_reason\n             entry = _review_entry(candidate, group, provider.label, facts,\n                                   now)\n             entry["flight"] = {\n@@ -935,7 +988,7 @@ def _process_flight_events(queue, providers, dead_platforms, dead_auth,\n             }\n             queued_entries.append(entry)\n             print(f"write: flight event {event.get('eventId')} queued for "\n-                  "human review")\n+                  f"human review ({review_reason})")\n             outcome = "queued"\n             break\n         if outcome is None:\n@@ -1117,13 +1170,14 @@ def main() -> None:\n \n     # --- 2b. rare-aircraft flight-observation candidates -------------------\n     # (queued by pipeline/flightwatch.py; zero AI calls when the queue is\n-    # empty, and EVERY drafted event is forced into manual review - v1\n-    # rare-aircraft items never auto-publish.)\n+    # empty. Routine observations that clear the machine gate publish\n+    # automatically; safety-flavoured or conflicting events still go to\n+    # human review.)\n     flight_queue = flightnews.load_queue()\n     if flight_queue and providers:\n         remaining_flight = _process_flight_events(\n             flight_queue, providers, dead_platforms, dead_auth, ai_calls,\n-            queued_entries, now)\n+            queued_entries, now, new_articles, new_flashes, used_ids)\n         if remaining_flight != flight_queue:\n             flightnews.save_queue(remaining_flight)\n \n@@ -30,9 +30,12 @@ OUTPUT (same schema and language rules as the main pipeline):\n   body 2-4 short factual paragraphs built only from quoted facts.\n - cat MUST be "ops". incident MUST be null (an arrival is not an ICAO\n   Annex 13 occurrence). flash: one line per language, hot=false.\n-- status MUST be "manual_review" with requiresHumanReview=true and\n-  decisionReason explaining this is an automated ADS-B observation awaiting\n-  editor confirmation. riskFlags MUST include "unconfirmed_or_developing".\n+- status: "manual_review" with requiresHumanReview=true (publication policy\n+  is decided by pipeline code, never by you; it may release routine\n+  observation items automatically). Never mention any review process,\n+  pending confirmation or publication workflow inside titles, summaries or\n+  body text - write the observation itself only. riskFlags MUST include\n+  "unconfirmed_or_developing".\n - facts: 1-6 atomic claims, each quoting one <SOURCE> line verbatim.\n - entities: only values literally present in <SOURCE>.\n - eventStatus: "confirmed" refers ONLY to the observed arrival itself.\n@@ -334,7 +334,17 @@ def _run_write_with(providers_list):\n         write.build_providers = original\n \n \n-def test_flight_events_into_review_queue():\n+def _published_articles():\n+    articles_dir = Path(os.environ["AVWIRE_DATA_DIR"]) / "articles"\n+    out = []\n+    for f in articles_dir.glob("*.json"):\n+        out.extend(json.loads(f.read_text(encoding="utf-8"))["articles"])\n+    return out\n+\n+\n+def test_flight_events_auto_publish():\n+    """Owner policy 2026-07-27: routine observations clear the machine\n+    gate and publish without a human."""\n     _reset_data()\n     flightnews.save_queue([json.loads(json.dumps(FLIGHT_EVENT))])\n     solo = FakeProvider("gemini", [_flight_draft(status="publish")])\n@@ -343,25 +353,66 @@ def test_flight_events_into_review_queue():\n     check(solo.calls == 1, f"one AI call for one event, got {solo.calls}")\n     review = common.load_json(Path(os.environ["AVWIRE_DATA_DIR"])\n                               / "review.json", [])\n-    check(len(review) == 1, "flight event landed in the review queue")\n-    entry = review[0]\n-    check(entry["draft"]["status"] == "manual_review"\n-          and entry["draft"]["requiresHumanReview"] is True,\n-          "publish output was FORCED down to manual_review")\n-    check("unconfirmed_or_developing" in entry["draft"]["riskFlags"],\n-          "risk flag enforced on flight events")\n-    check(entry["flight"]["eventId"] == FLIGHT_EVENT["eventId"]\n-          and entry["flight"]["crossCheck"] == "confirmed_by_two_sources",\n-          "flight metadata stored with the review entry")\n-    check(entry["group"]["items"][0]["url"] == "https://airplanes.live/"\n-          and entry["group"]["items"][1]["url"] == "https://www.adsb.lol/",\n+    check(review == [], "clean observation skips the review queue")\n+    arts = _published_articles()\n+    check(len(arts) == 1, "observation auto-published as one article")\n+    art = arts[0]\n+    check(art["riskFlags"] == [] and art["cat"] == "ops",\n+          "published observation carries no risk flags, cat=ops")\n+    check({s["url"] for s in art["sources"]}\n+          == {"https://airplanes.live/", "https://www.adsb.lol/"},\n           "attribution links become the article's sources")\n-    check(flightnews.load_queue() == [], "queue emptied after queuing")\n-    # No articles were published (manual_review only).\n-    articles_dir = Path(os.environ["AVWIRE_DATA_DIR"]) / "articles"\n-    check(not list(articles_dir.glob("*.json")),\n-          "nothing auto-published for a rare-aircraft event")\n-    print("test_flight_events_into_review_queue: done")\n+    check(flightnews.load_queue() == [], "queue emptied after publishing")\n+    print("test_flight_events_auto_publish: done")\n+\n+\n+def test_flight_gate_blocks_conflicts_safety_and_kill_switch():\n+    # conflicting ADS-B sources -> human review, never published\n+    _reset_data()\n+    event = json.loads(json.dumps(FLIGHT_EVENT))\n+    event["crossCheck"] = "conflicting_sources"\n+    flightnews.save_queue([event])\n+    solo = FakeProvider("gemini", [_flight_draft(status="publish")])\n+    _run_write_with([solo])\n+    review = common.load_json(Path(os.environ["AVWIRE_DATA_DIR"])\n+                              / "review.json", [])\n+    check(len(review) == 1 and not _published_articles(),\n+          "conflicting sources -> review queue, nothing published")\n+    check(review[0]["draft"]["status"] == "manual_review"\n+          and "unconfirmed_or_developing" in review[0]["draft"]["riskFlags"]\n+          and "不一致" in review[0]["draft"]["decisionReason"],\n+          "conflict entry forced to manual_review with reason")\n+    check(review[0]["flight"]["eventId"] == FLIGHT_EVENT["eventId"],\n+          "flight metadata stored with the review entry")\n+\n+    # a safety-flavoured draft -> human review even with clean sources\n+    _reset_data()\n+    flightnews.save_queue([json.loads(json.dumps(FLIGHT_EVENT))])\n+    safety_draft = _flight_draft(status="manual_review")\n+    safety_draft["riskFlags"] = ["investigation"]\n+    safety_draft["requiresHumanReview"] = True\n+    safety_draft["decisionReason"] = "involves an investigation"\n+    solo = FakeProvider("gemini", [safety_draft])\n+    _run_write_with([solo])\n+    review = common.load_json(Path(os.environ["AVWIRE_DATA_DIR"])\n+                              / "review.json", [])\n+    check(len(review) == 1 and not _published_articles(),\n+          "safety risk flag -> review queue, nothing published")\n+\n+    # kill switch restores review-everything behaviour\n+    _reset_data()\n+    flightnews.save_queue([json.loads(json.dumps(FLIGHT_EVENT))])\n+    os.environ["FLIGHT_AUTO_PUBLISH"] = "false"\n+    try:\n+        solo = FakeProvider("gemini", [_flight_draft(status="publish")])\n+        _run_write_with([solo])\n+    finally:\n+        del os.environ["FLIGHT_AUTO_PUBLISH"]\n+    review = common.load_json(Path(os.environ["AVWIRE_DATA_DIR"])\n+                              / "review.json", [])\n+    check(len(review) == 1 and not _published_articles(),\n+          "FLIGHT_AUTO_PUBLISH=false -> review queue, nothing published")\n+    print("test_flight_gate_blocks_conflicts_safety_and_kill_switch: done")\n \n \n def test_flight_no_candidates_no_ai():\n@@ -411,7 +462,8 @@ def main():\n         test_rarity_and_bootstrap,\n         test_event_identity_and_config,\n         test_source_assembly_privacy,\n-        test_flight_events_into_review_queue,\n+        test_flight_events_auto_publish,\n+        test_flight_gate_blocks_conflicts_safety_and_kill_switch,\n         test_flight_no_candidates_no_ai,\n         test_flight_bad_quote_and_reject,\n     ]

你發現了隱藏主題!

8-bit 主題已解鎖。重新整理後,就能在主題選單選擇 8-bit。