Fabrication tripwire + honest handling of thin derivative items
Owner spotted that the Delta flight-attendant-pay article's body claimed
the (unseen) Simple Flying piece "詳細分析…引述公司內部文件…比較新舊合約"
- the RSS material was one sentence, so that paragraph was invented.
Facts passed the verbatim-quote check; body prose had no gate. Now:
- Corrected the published article + its briefing copies in place to
supported-only content (the other two SF-derived articles audited
clean).
- SYSTEM_PROMPT: derivative items may state only publisher/date/title
and what the shown excerpt itself says; describing unseen article
contents is named as machine-checked fabrication; reject instead of
padding. Self-check gains item (8).
- write.fabrication_problem(): for groups with NO fulltext, drafts using
tell-tale sourcing verbs (內部文件/詳細分析/引述/知情人士/據悉...)
absent from the material are rejected like a validation failure; the
flight path gets the same gate. Fulltext groups are exempt - long
bodies legitimately describe actually-shown quotes.
- Simple Flying full text is contractually OFF-LIMITS: Valnet's
robots.txt terms explicitly ban automated text/data-mining and AI use
of page content. Pinned as a comment in fulltext.py (never allowlist)
+ a test asserting simpleflying stays out of ALLOWED_HOSTS. Their RSS
excerpt remains the ceiling for those items.
- data/fulltext.json is no longer committed (media page text does not
belong in a public repo); the cache is per-run now, budget unchanged.
tests/test_glossary.py -> 16 checks (tripwire cases included); all 10
suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full diff
@@ -6,3 +6,4 @@ venv/\n .DS_Store\n Thumbs.db\n design_ref/\n+data/fulltext.json\n@@ -63,18 +63,18 @@\n "image": "https://static0.simpleflyingimages.com/wordpress/wp-content/uploads/2026/07/delta-air-lines-flight.jpg",\n "zh": {\n "title": "Simple Flying發布達美空服員關門前三十分鐘薪資分析",\n- "summary": "Simple Flying於二零二六年七月二十六日發布報導,標題為《達美空服員關門前三十分鐘薪資實況》,文中指出達美成為首家解決長期薪資糾紛的航空公司。該文章詳細分析了關門前三十分鐘的薪資結構,並引述公司內部文件說明新政策的實施細節。",\n+ "summary": "Simple Flying 於 2026 年 7 月 26 日發布報導,標題為《How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close》,其摘要指出達美航空成為首家解決此一長期薪資糾紛的航空公司。",\n "body": [\n- "Simple Flying於二零二六年七月二十六日發布報導,標題為《達美空服員關門前三十分鐘薪資實況》,摘要指出達美成為首家解決長期薪資糾紛的航空公司。",\n- "文章詳細分析關門前三十分鐘的薪資結構,並引述公司內部文件說明新政策實施細節,比較新薪資與舊合約差異,強調對排班與士氣的影響。"\n+ "Simple Flying 於 2026 年 7 月 26 日發布報導,探討達美航空空服員在關艙門前三十分鐘期間的實際薪資。",\n+ "該報導摘要指出,達美航空成為首家解決此一長期薪資糾紛的航空公司。"\n ]\n },\n "en": {\n "title": "Simple Flying publishes analysis of Delta flight attendant pay before door close",\n- "summary": "Simple Flying published an article on 26 July 2026 titled \"How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close.\" The piece states that Delta has become the first airline to resolve a long‑standing pay grievance, and it details the compensation structure for the half‑hour before doors close, citing internal company documents. The analysis also compares the new pay rates with previous contracts and highlights the impact on crew scheduling and morale.",\n+ "summary": "Simple Flying published a report on July 26, 2026, titled \"How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close\"; its summary calls Delta the first airline to solve a long-term grievance.",\n "body": [\n- "Simple Flying published an article on 26 July 2026 titled \"How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close.\" The summary says Delta has become the first airline to resolve a long‑standing pay grievance.",\n- "The analysis details the compensation structure for the half‑hour before doors close, cites internal company documents, compares the new rates with previous contracts, and highlights the impact on crew scheduling and morale."\n+ "Simple Flying published a report on July 26, 2026, examining what a Delta Air Lines flight attendant actually earns during the 30 minutes before door close.",\n+ "The report's summary states that Delta is the first airline to solve this long-term grievance."\n ]\n },\n "sources": [\n@@ -166,4 +166,4 @@\n }\n }\n ]\n-}\n+}\n\ No newline at end of file\n@@ -124,9 +124,9 @@\n "previous_event_id": null,\n "category": "aviation_industry",\n "headline": "Simple Flying發布達美空服員關門前三十分鐘薪資分析",\n- "summary": "Simple Flying於二零二六年七月二十六日發布報導,標題為《達美空服員關門前三十分鐘薪資實況》,文中指出達美成為首家解決長期薪資糾紛的航空公司。該文章詳細分析了關門前三十分鐘的薪資結構,並引述公司內部文件說明新政策的實施細節。",\n+ "summary": "Simple Flying 於 2026 年 7 月 26 日發布報導,標題為《How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close》,其摘要指出達美航空成為首家解決此一長期薪資糾紛的航空公司。",\n "headline_en": "Simple Flying publishes analysis of Delta flight attendant pay before door close",\n- "summary_en": "Simple Flying published an article on 26 July 2026 titled \"How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close.\" The piece states that Delta has become the first airline to resolve a long‑standing pay grievance, and it details the compensation structure for the half‑hour before doors close, citing internal company documents. The analysis also compares the new pay rates with previous contracts and highlights the impact on crew scheduling and morale.",\n+ "summary_en": "Simple Flying published a report on July 26, 2026, titled \"How Much A Delta Air Lines Flight Attendant Actually Earns During The 30 Minutes Before Door Close\"; its summary calls Delta the first airline to solve a long-term grievance.",\n "event_time": null,\n "source_published_at": "2026-07-26T21:00:00+00:00",\n "source_updated_at": null,\n@@ -259,4 +259,4 @@\n "used": false,\n "items": 0\n }\n-}\n+}\n\ No newline at end of file\n@@ -1,16 +0,0 @@\n-{\n- "pages": {\n- "https://www.cna.com.tw/news/aipl/202607260100.aspx": {\n- "text": "聯合防禦操演 驗證直屬參謀本部戰略預備隊應變能量\n(中央社記者 游凱翔 台北26日電)國軍聯合防禦操演日前結束,知情人士指出,此次操演已驗證直屬參謀本部的戰略預備隊應變能量;學者推測,在漢光實兵等聯合作戰想定下,由具備跨海陸空機動能力的部隊擔任戰略預備隊,成為關鍵打擊戰力。\n共軍近年不斷對台文攻武嚇,以機艦侵擾台灣周邊海空域,並不時逾越台海中線、在周邊區域舉行軍演;國軍除持續國造及對外籌獲軍備,也強化訓練力度及制度改革,因應可能的潛在威脅。\n知情人士表示,由不同單位各自擔當的戰略預備隊直屬於參謀本部,由參謀總長指揮,為國軍關鍵力量之一,在7月13日至17日舉行的聯合防禦操演,已驗證相關能量。\n國防部智庫、國防安全研究院學者蘇紫雲接受中央社訪問時說明,戰略預備隊是軍事作戰上的核心組織結構,例如一個旅下轄3個營時,指揮官通常會運用2個營在第一線交戰,保留一個營當作「戰術活棋」;且預備隊非消極等待,而是保持戰術彈性,在第一線遭突破時進行快反、增援角色,發揮反突防、填補缺口作用。\n蘇紫雲指出,在台海防衛戰略,戰略預備隊不受單一戰區轄制。根據國防部先前披露的公開資訊,配備AH-64E阿帕契攻擊直升機的陸航601旅,部分單位即直屬參謀本部;另外,具備高速機動能力、能跨越水陸空界面的單位,乃至於空軍聯隊與部分海軍兵力,均能在戰略預備隊想定中扮演重要角色。\n蘇紫雲推測,在漢光實兵等聯合作戰想定下,戰略預備隊已不再局限於單一陸上打擊,而是涵蓋陸海空軍戰力,屆時將於關鍵時刻鞏固並確保政經核心、關鍵基礎設施的安全。(編輯:林克倫)1150726\n選擇與事實站在一起,您的每一份贊助,都是守護新聞自由的力量\n下載中央社「一手新聞」APP,即時掌握最新消息\n本網站之文字、圖片及影音,非經授權,不得轉載、公開播送或公開傳輸及利用。",\n- "fetchedUtc": "2026-07-26T22:46:20.802042+00:00"\n- },\n- "https://www.cna.com.tw/news/aipl/202607260033.aspx": {\n- "text": "蔡其昌率團赴台灣日僑宴 前紐約市長亞當斯出席支持\n(中央社記者 廖漢原 紐約25日專電)美國職棒大聯盟紐約大都會台灣日明天舉行,擔任雙開球嘉賓的美國國家航空暨太空總署太空人林琪兒及中華職棒大聯盟會長蔡其昌等,今天出席賽前僑宴,前紐約市長亞當斯出席對台僑社群表達支持。\n第22屆大都會台灣日將於26日在紐約大都會主場花旗球場(Citi Field)舉行,擔任開雙球嘉賓的美國國家航空暨太空總署(NASA)太空人林琪兒(Kjell Lindgren)及中華職棒大聯盟會長蔡其昌,25日聯袂出席於台僑聚集的法拉盛(Flushing)地區僑宴。\n先前訪問華府拜會美國國會及行政部門,參加智庫研討會的立委吳思瑤、沈伯洋、陳培瑜及王定宇,同場出席活動。\n林琪兒以太空望向地球的經驗向紐約台僑表示,相信在座的人對台灣都有那種連結感受,很高興年初回台連結出生地,有機會探視朋友與熟悉的地方,協助台灣與美國建立關係,包括教育、官方或產業方面。\n他指出,台灣是旅程開始的地方,自己擁有台灣與美國背景讓人感動。台美是經濟夥伴關係,彼此也分享民主、自由、創新與機會。\n蔡其昌表示,棒球是台灣的國球,棒球團結台灣這個國家。在棒球場上無論是支持藍綠白,不會互相嫌棄、對立、討厭。在棒球場上只要是棒球迷,都會暫時忘掉台灣社會內部不同政治主張的紛爭,團結一致為台灣加油。\n他說,棒球讓全世界看到台灣,台灣在世界棒球12強高張力、高難度的比賽拿下世界冠軍,讓全世界看到台灣,這就是棒球的魅力。\n參選台北市長的沈伯洋指出,台灣民主確實有點跌跌撞撞,30年來遇到很多挑戰,我們撐到現在,這是台灣民主可貴的地方。\n他說,今天講到總觀效應,即在太空往下看,看不到國界,但看得到每個城市的燈光,城市燈光相連。希望那是民主、熱情的燈光,希望未來台北的燈光越來越亮,能夠與世界主要城市連結,讓這燈光以台北與各國首都為首,照亮整個民主世界。\n活躍於亞裔社群的前紐約市長亞當斯(Eric Adams)也出席晚宴,與沈伯洋等人會晤合照。\n亞當斯表示,(台裔)社群對紐約市極重要,在任期間花很多時間在此。(台裔)社區相信家庭、營商、公共安全並支持警力,這很重要。大家多次相聚攜手合作,對城市做出更多貢獻,所以今天到場表達支持。許多人知道晶片力量來自台灣,台灣社群對紐約市具有重要性。\n訪團一行人期間出席沈伯洋在法拉盛舉行的紐約海外後援會成立大會,許多台僑到場展現支持。(編輯:唐佩君)1150726\n選擇與事實站在一起,您的每一份贊助,都是守護新聞自由的力量\n下載中央社「一手新聞」APP,即時掌握最新消息\n本網站之文字、圖片及影音,非經授權,不得轉載、公開播送或公開傳輸及利用。",\n- "fetchedUtc": "2026-07-26T22:46:20.802042+00:00"\n- },\n- "https://www.cna.com.tw/news/aipl/202607260022.aspx": {\n- "text": "(中央社記者 游凱翔 台北26日電)國防部今天發布共機艦動態,統計自昨天上午6時起至今天上午6時止,偵獲7艘共艦、3艘公務船,及4架次共機,國軍運用任務機艦及岸置飛彈系統嚴密監控與應處。\n國防部上午公布共機艦動態,統計自昨天上午6時起至今天上午6時止,偵獲7艘共艦、3艘公務船及4架次共機,持續在台海周邊活動。期間並無共機逾越台灣海峽中線。\n國防部強調,國軍運用任務機艦及岸置飛彈系統嚴密監控與應處。(編輯:蘇龍麒)1150726\n選擇與事實站在一起,您的每一份贊助,都是守護新聞自由的力量\n下載中央社「一手新聞」APP,即時掌握最新消息\n本網站之文字、圖片及影音,非經授權,不得轉載、公開播送或公開傳輸及利用。",\n- "fetchedUtc": "2026-07-26T23:03:55.272849+00:00"\n- }\n- }\n-}\n@@ -64,6 +64,9 @@\n # robots.txt is still honored per fetch\n "www.cna.com.tw", "cna.com.tw",\n }\n+# NEVER add simpleflying.com or other Valnet properties: their robots.txt\n+# terms explicitly prohibit automated text/data mining and AI use of page\n+# content (checked 2026-07-27). Their RSS excerpt is all we may use.\n \n # Text shown to the model AND used for quote verification is capped at\n # exactly this many characters - write.py imports it so the two sides can\n@@ -230,6 +230,14 @@\n - Never invent or add: airline/IATA/ICAO codes, flight numbers,\n registrations, aircraft sub-variants, engine types, seat counts, casualty\n figures, monetary amounts, dates, times, locations or causes.\n+- Derivative items (the material is another outlet's headline plus a short\n+ blurb about THEIR article): you may state only who published, when, the\n+ title, and what the shown excerpt itself says. NEVER describe the unseen\n+ article's contents, structure, methodology, comparisons or quoted\n+ documents (no "詳細分析…", "引述內部文件", "比較新舊…", "強調…影響"\n+ claims about text you were not shown - this is fabrication and is\n+ machine-checked). If that leaves too little for the minimum lengths,\n+ reject instead of padding.\n - Never merge different events, flights, dates, airports or operators into\n one story.\n - Preserve the certainty level of the material. reportedly / according to /\n@@ -361,7 +369,9 @@\n upgraded to certainty? (4) is each sourceQuote copied verbatim? (5)\n publication date confused with event date? (6) riskFlags complete, and if\n non-empty is status manual_review or reject? (7) are status,\n-requiresHumanReview and decisionReason mutually consistent? If a check\n+requiresHumanReview and decisionReason mutually consistent? (8) does any\n+body/summary sentence describe contents of a source article that the shown\n+material does not contain? If a check\n fails and you cannot fix the draft, downgrade: manual_review when evidence\n suffices but risk remains, reject when evidence is insufficient.\n """\n@@ -448,6 +458,33 @@ def glossary_problem(draft: dict, group: dict):\n return None\n \n \n+# Fabrication tripwire (owner-reported 2026-07-27: a thin derivative item\n+# grew a body claiming the unseen article "詳細分析…引述內部文件…" - none of\n+# it in the material). When a group has NO fulltext, drafts may not use\n+# these tell-tale sourcing verbs unless the phrase itself appears in the\n+# material; with fulltext present the gate is off (long bodies legitimately\n+# describe actually-shown quotes).\n+_FABRICATION_PHRASES = ("內部文件", "詳細分析", "深入分析", "引述", "訪談",\n+ "知情人士", "消息人士", "據了解", "據悉")\n+\n+\n+def fabrication_problem(draft: dict, group: dict):\n+ items = group.get("items") or []\n+ if any(str(item.get("fulltext") or "").strip() for item in items):\n+ return None\n+ zh_block = draft.get("zh") or {}\n+ zh_text = " ".join([str(zh_block.get("summary") or ""),\n+ " ".join(str(p) for p in zh_block.get("body") or [])])\n+ material = _squash(" ".join(\n+ f"{item.get('title') or ''} {item.get('summary') or ''}"\n+ for item in items))\n+ for phrase in _FABRICATION_PHRASES:\n+ if phrase in zh_text and _squash(phrase) not in material:\n+ return (f"body/summary uses 「{phrase}」 about unseen source "\n+ f"content (thin material, no full text)")\n+ return None\n+\n+\n # Untrusted material must not be able to close the <SOURCE> envelope.\n # Applied identically when building the prompt and when verifying quotes,\n # so sourceQuote matching stays character-exact with what the model saw.\n@@ -643,7 +680,8 @@ def _validated_draft(provider, group: dict, tries: int, ai_calls=None):\n return candidate, []\n facts = verify_facts(candidate, group, provider.label)\n if facts:\n- problem = glossary_problem(candidate, group)\n+ problem = (glossary_problem(candidate, group)\n+ or fabrication_problem(candidate, group))\n if problem is None:\n return candidate, facts\n else:\n@@ -1030,10 +1068,12 @@ 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- g_problem = glossary_problem(candidate, group)\n+ g_problem = (glossary_problem(candidate, group)\n+ or fabrication_problem(candidate, group))\n if g_problem:\n print(f"write: {provider.label} flight draft violates the "\n- f"name glossary ({g_problem}); trying next provider")\n+ f"name/fabrication rules ({g_problem}); trying next "\n+ "provider")\n continue\n review_reason = _flight_review_reason(event, candidate)\n if review_reason is None:\n@@ -105,6 +105,10 @@ def reset():\n fulltext.fetch_fulltext(\n "https://news.google.com/rss/articles/x") is None\n and fake.calls == [])\n+check("Valnet properties stay off the allowlist (terms ban AI mining)",\n+ not any("simpleflying" in h for h in fulltext.ALLOWED_HOSTS)\n+ and fulltext.fetch_fulltext("https://simpleflying.com/x") is None\n+ and fake.calls == [])\n \n reset()\n fake.routes["https://www.faa.gov/robots.txt"] = ROBOTS_DENY\n@@ -99,6 +99,38 @@ def draft(zh_title, en_title, zh_body=("內文。",), en_body=("Body.",)):\n check("names absent from the draft's en text are not enforced",\n write.glossary_problem(unrelated, GROUP) is None)\n \n+# ── fabrication tripwire (the Delta-pay body invention, 2026-07-27) ─────────\n+\n+THIN_GROUP = {"id": "g2", "items": [{\n+ "title": "How Much A Delta Flight Attendant Earns Before Door Close",\n+ "summary": "The first airline to solve a long term grievance.",\n+ "source": "Simple Flying", "url": "https://example.com/b"}]}\n+\n+fab = draft("Simple Flying 發布達美航空空服員薪資報導",\n+ "Simple Flying publishes Delta Air Lines pay report")\n+fab["zh"]["body"] = ["文章詳細分析薪資結構,並引述公司內部文件說明實施細節。"]\n+check("thin material + body describing unseen article contents rejected",\n+ write.fabrication_problem(fab, THIN_GROUP) is not None)\n+\n+ok = draft("Simple Flying 發布達美航空空服員薪資報導",\n+ "Simple Flying publishes Delta Air Lines pay report")\n+ok["zh"]["body"] = ["該報導摘要指出,達美航空成為首家解決此一長期薪資糾紛的航空公司。"]\n+check("supported-only body passes the tripwire",\n+ write.fabrication_problem(ok, THIN_GROUP) is None)\n+\n+rich_group = {"id": "g3", "items": [dict(THIN_GROUP["items"][0],\n+ fulltext="long official text " * 30)]}\n+check("groups with full text are exempt (long bodies may describe quotes)",\n+ write.fabrication_problem(fab, rich_group) is None)\n+\n+zh_material_group = {"id": "g4", "items": [{\n+ "title": "民航局引述調查報告說明事件經過", "summary": "官方說明。",\n+ "source": "CNA", "url": "https://example.com/c"}]}\n+quoted = draft("民航局引述調查報告", "CAA cites the investigation report")\n+quoted["zh"]["body"] = ["民航局引述調查報告說明事件經過。"]\n+check("phrases present in the material itself stay allowed",\n+ write.fabrication_problem(quoted, zh_material_group) is None)\n+\n print(f"\n{CHECKS} checks passed, {FAILED} failed"\n if not FAILED else f"\n{CHECKS - FAILED}/{CHECKS} passed, "\n f"{FAILED} FAILED")