بنقرة واحدة
architect
Planning principles, model selection, and scope rules for the Architect agent
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Planning principles, model selection, and scope rules for the Architect agent
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Implementation rules for database migrations, Python scrapers, and Next.js web for the Engineer agent
BaseScraper contract, field rules, and Peatix-specific conventions for the Scraper Expert agent
Scraper test execution rules, output validation criteria, and report format for the Tester agent
BaseScraper contract, field rules, and Peatix-specific conventions for the Scraper Expert agent
Platform rules and field mappings for the 誠品生活日本橋 (eslite spectrum Nihonbashi) scraper
版元ドットコム Playwright scraper for Taiwan-related books (Publication Intel v3.1)
| name | architect |
| description | Planning principles, model selection, and scope rules for the Architect agent |
| applyTo | .github/agents/architect.agent.md |
Read this at the start of every session before producing any plan.
web/ UI 計畫,優先採用本站既有 design system / design token 元件,不得預設用原生 HTML control。若已有 DesignSelect、dialog、button、input、badge 等對應元件,計畫中必須明列使用它;只有在設計系統無法表達互動語意時,才考慮新增原生 fallback。REVOKE authenticated table grants、drop/recreate RLS policy、或改 view security 行為,必須先 grep web/ 是否仍有 browser-client .insert() / .update() / .delete() 使用該表。admin browser-client writes 可保留 table grant,實際權限由 admin-only RLS policy 限制。security_invoker view 也必須同步確認底層表的最小欄位 grants 與 row policy,不能只 grant view。candidate select 必須顯式載入 domain guard 欄位(例:event_form/source_name/image_url),且在實際 write 前必須 re-read 同一 row 再次驗證 guard,避免 TOCTOU 與 helper 直呼繞過查詢 filter。若來源存在共用 placeholder 媒體(例:Hanmoto noimage.jpg/no-cover),需以 canonical helper 精準拒絕 placeholder 變體,不得 blanket 禁用整個來源的真實內容。select(...).execute() 載入「保護判斷資料」(例如 field_corrections、blacklist、mapping)時,必須先驗證是否被預設分頁截斷。最少要做三件事:
first_page_count 與 count='exact'.range(offset, offset+999) 全量掃描一次確認總筆數< 20 個事件 → 一律改為「一次性手動 patch」或「prompt-only 修法」,不做成 daily routine20–100 個事件 → 一次性 backfill 腳本,跑完 archive,不接 CI> 100 個事件且持續累積 → 才考慮做成 daily CI step
違反這條會徒增 CI 時間 + 檔案維護成本,且 Plan Critic 會擋下。Reference: 2026-05-26 enrich_organizers.py 計畫瘦身(5 Phase → 3 Phase)。category/event_form 類型,在設計其查重合流機制(merger)時,應設置專屬的 Pass (例如 Pass 1.1) 優先於 start_date same-day 分組匹配之外執行。在時差 30 日內或年末佔位符階段(如 12-31)強制合併,並提供權威源 metadata 傳遞演算,確保不會受 scraper date extraction 漂移造成重複入庫。source_id 是否為 _subN 位置序號。修法必須先用標題與日期匹配既有子活動並復用原 source_id,新子活動只能 append 到目前最大 _subN 之後,避免 re-annotation 把既有 _sub2〜_subN 覆寫成不同活動。若同日既有子活動是 bundle(開幕イベント、表演+解說等),GPT 拆出的 component(講演、映画、解説)必須命中並跳過 duplicate allocation;sub-event upsert 也必須尊重 field_corrections。web/messages/{en,ja,zh}.json 三包語系檔做 simultaneous update 同步更新。若有漏配,會阻礙 production next build compiler 通過。performing_arts 等混合型活動(現場+線上),設計上必須同時保留實體 location_address 與在 location_name 中並列 オンライン。記得兩個都標,不可因線上屬性而抹除地址資料。rstrip("都道府県")(2026-06-29 教訓):rstrip 是字元集合剝除非後綴剝除,京都府→京、東京都→東京、北海道→北海 皆過度剝離(「都」同時是京都/東京內字)。任何依縣名建查表/分組/排序的計畫,必須要求多段 fallback(精確 label match → rstrip → 原值 direct → 去末字 [:-1]),並列子字串陷阱測試(東京都≠京都、神奈川≠奈良、和歌山≠山形/岡山、福岡≠福島/福井)。Reference: 2026-06-29 週報 geo-sort(commit 3c590e4)。.github/instructions/git.instructions.md 現行政策並比對實務(V-M-D/CI 實際怎麼推),矛盾拆獨立任務對齊,不在子段落暗改全 repo 政策;(b) 新增「供追蹤」的 metadata 欄位(spec frontmatter/DB 欄位)→ 先追其 type/parser/consumer,無 consumer=dead field 會被靜默丟棄(v1 的 worktree: frontmatter 就會被 spec dashboard snapshot parser 丟掉),可推導值優先於新增可過期欄位;(c) 強制/1:1 規則 → 只 prospective 套用並 grandfather 既有資產。詳見 history.md 2026-07-10 spec⟺worktree v1 三 P0。自 2026-07-18 起,資安敏感 Web 發布的計畫與驗收必須分別蒐集四層證據:
main,且 Production deployment 成功綁定同一 SHA。第 1 至 3 層 PASS 不代表第 4 層 PASS。缺少 auth credentials 或 test context 時,未測流程必須明列為 NOT TESTED residual risk/backlog,不得改寫成 PASS。Report-Only CSP 只提供 observability,不是 enforcement;後續切換 enforced CSP 前,必須先完成 violation inventory 與 regression testing。
若 immutable deployment URL 回傳 Deployment Protection 訊號(302、_vercel_sso_nonce、平台 X-Robots-Tag: noindex),不得弱化保護或判為 app regression。改以 GitHub/Vercel exact-SHA provenance 確認部署,再透過 production custom domain 做 app-level smoke。Full-file lint debt 與 changed-hunk regression 必須分開回報;零 changed-hunk overlap 可讓本次 feature 通過,但不會關閉既有 lint backlog。
計畫 gate 中的 DB count assertion 與 git HEAD 假設只在規劃時算一次。派工後若有並行 session(使用者另一視窗、手動腳本、cron)動到 DB 或 push,baseline 會漂移而 gate 仍用舊值放行。審核任何依賴 DB count 或 git HEAD 的計畫 gate 時,必須要求 Engineer 在 Step 0(執行開始) re-confirm 三點,偵測到 drift 即 STOP、先 reconcile 再進 gate:
git fetch origin 後 git rev-parse HEAD == git rev-parse origin/main。report_type 必須 ⊆ 計畫快照(並行 auto_qa.py live run 會注入 auto_simplified_chinese、missing_date 等新型別)。# scraper/ venv,Engineer Step 0
r = sb.table('event_reports').select('report_type', count='exact').eq('status', 'pending').execute()
print('pending total:', r.count) # 必須 == 計畫 baseline
types = sorted({row['report_type'] for row in r.data})
print('report_types:', types) # 必須 ⊆ 計畫快照
配套規則(同次事件學到):
.like():.like('id', prefix+'%') 觸發 Postgres 42883 (uuid ~~ unknown);改用 full-uuid .eq()、對 text 欄位(如 field_corrections.event_id)比對,或 Python 過濾。docs/specs/active/<slug>/ 的長期 feature(跨 session/多檔/新模塊),計畫必須 provision 一個 ttr-<slug>-worktree(branch feat/<slug>),1:1 綁定、僅適用規則上線後新建/重啟的 spec(既有 spec grandfather)。小改動不建 spec、不建 worktree,走主工作目錄 + trunk-based 流程。並行 session 的 git stash/clean 碰不到 linked worktree,避免未 commit WIP 被當「unrelated before deploy」掃走(2026-06-29 v8 投稿精靈 WIP 散落 5 stash 事件)。worktree 命令細節(state matrix、.git/info/exclude、STOP 條件)一律 defer .github/instructions/git.instructions.md § Isolated worktree,SKILL 只管「何時決策」。 runSubagent 同步但 return 常截斷——驗收看 git status/diff 不看回傳。ttr-v8-worktree/)常領先主 repo 數個未 merge 改動;使用者跑 worktree dev server 時,畫面反映的是 worktree 而非主 repo。只讀主 repo 會把「主 repo 落後」誤判成「需要改 worktree」。推論表單/UI 實際 render 的 i18n 值,必須跟著元件 useTranslations(ns) 的 namespace 走到源頭——同名 key(如 fieldVenue 同時存在於 designSystem preview 與 eventIntake 兩個 namespace)只憑 grep 第一個命中行號就斷言會誤判。使用者對自己畫面的觀察優先於先前 summary-derived 結論;被質疑時第一動作是查證兩邊實體(grep 主+worktree、追元件 namespace),不是辯護原判斷(2026-07-01 Phase B2 fieldVenue namespace/codebase 誤判事件,詳見 history.md)。Reference incident: 2026-06-25 — G3.0+G3.1 並行 session 漂移 baseline(pending 350→212→197)+ HEAD(60240df/631efb6),Engineer 中斷帶 live DB 變更但 code 未 commit(fix-forward commit 34bca54,Tester PASS 5/5)。詳見 architect history.md。
任何要在「OCR 上傳海報建活動」流程新增 array 結構欄位(如 co_organizers / sponsors / tags),必須同時改 4 處,缺一即 silent failure:
| 步驟 | 位置 | 說明 |
|---|---|---|
| 1 | web/app/api/admin/extract-from-image/route.ts Vision SYSTEM prompt | 加欄位定義 + 視覺位置提示(如「海報底部 credit block」「主辦資訊欄」)— Vision 模型對位置線索反應好於通用提示 |
| 2 | web/components/AdminEventTable.tsx ARRAY_FIELDS 集合 | 加欄位 key — 否則被 String(val) 強轉成字串污染 form state,TypeScript 不會報錯 |
| 3 | scraper/annotator.py SYSTEM_PROMPT | 加自然語句模式範例(如「○○との共催」「in cooperation with ○○」)— GPT-4o-mini 只認得條列式 label,自然語句要靠 few-shot 觸發 |
| 4 | web/components/AdminEventForm.tsx | 確認 form state 初始值 + 編輯 UI 存在 |
為什麼這是 Architect 必須掌握的 Guard: 該事件 schema 鏈完整(types.ts / DB / i18n / form state 都有 co_organizers),唯獨 Vision prompt + ARRAY_FIELDS + annotator prompt 三路徑漏抽,任何單點修補都無法解決 silent empty array 問題。Architect 在計畫中要明列 4 個 sync points,並標注「ARRAY_FIELDS 是隱性 sync point」。
Reference incident: 2026-05-26 — event 25e27de9 co_organizers / sponsors 三路徑漏(commits 280fdc4 + e54b925)。
每次新增 Category 值,以下 6 個位置必須同一 commit 同步,缺一返工:
| 步驟 | 位置 | 說明 |
|---|---|---|
| 1 | web/lib/types.ts | Category union + CATEGORIES array + CATEGORY_GROUPS |
| 2 | scraper/annotator.py | VALID_CATEGORIES + SYSTEM_PROMPT enum string + definition |
| 3 | web/messages/{zh,en,ja}.json | categories.<key> + categoryDesc.<key>(三語各一) |
| 4 | web/lib/design/organicMotifs.tsx | case '<key>': 5 個 SVG 變體(缺少則 fallback 為預設 blob,無報錯)↳ 同時自動涵蓋 CategoryThumbnail UI + category OG image + event OG image(三者均動態呼叫 getSemanticSymbol(),無硬編碼列表) |
| 5 | web/app/api/admin/extract-from-image/route.ts + web/app/api/admin/annotate-event/route.ts | 兩個 GPT prompt 內的 category: 枚舉清單(靜默漂移風險最高:TS 不檢查 prompt 字串,GPT 缺值時會自創 health 等假分類) |
| 6 | Sync Guard 驗證 | python3 -c "from annotator import VALID_CATEGORIES, _check_category_sync; _check_category_sync()" pass |
步驟 4(縮圖)是最容易被遺漏的:TypeScript 不報錯、build 不失敗、只有在 /debug/motifs 頁才能看到 fallback blob。計畫中必須明確標注 Engineer 要加 case 到 organicMotifs.tsx。
步驟 5(GPT prompt 枚舉)2026-05-20 才被發現是同步位置:兩個 admin API 的 prompt 寫死了分類清單,types.ts 加新值時不會自動同步。GPT 看不到新值就會自創(例:types.ts 有 healthcare 但 prompt 缺 → GPT 回傳 health → 前端顯示 categories.health 生 key)。Architect 計畫必須將兩個 route.ts 明列為同步位置。
Reference incidents:
design_craft、herbal、study_abroad 三分類在 annotator/i18n sync 後,縮圖 case 補加為獨立 commit,違反「同一 commit」原則。教訓:Architect 計畫從此明列步驟 4。healthcare),GPT 自創 health,前端顯示 raw i18n key(commits 997378c)。教訓:新增步驟 5。25e27de9 顯示 categories.photography raw key。GPT 自創 photography 由 admin OCR route 寫入,繞過 annotator 的 _validate_categories()(commit 264afed)。教訓:見下方「Admin API enum 防禦」rule。新增 Category / event_form 計畫起草前必跑:
grep -nE "CATEGORY_GROUPS|group_(arts|culture|sense|lifestyle|society)" web/lib/types.ts | head -20
憑記憶推測 group 名稱會撞「分類值與群組同名」陷阱(如 senses 是分類值、group_arts 是群組名,不存在 group_senses)。Reference: 2026-05-26 photography 計畫誤寫 group_senses 被 Tester 抓出。
問題模式: GPT 在 admin OCR/annotate route 自創 enum 外的值(例:photography 不在 VALID_CATEGORIES 但寫入 DB → 前端 raw i18n key)。三道防線都擋不住:
text[] 陣列元素無效(只能 CHECK 整個陣列)_validate_categories():只有 daily scraper 路徑會跑,admin route 不會規則: 任何由 GPT 輸出 enum 欄位的 admin API route 必須在 server 端 intersect whitelist:
const VALID_CATEGORIES = new Set([...]); // 與 types.ts CATEGORIES 同步
const sanitized = (gpt_response.category ?? []).filter(c => VALID_CATEGORIES.has(c));
Architect 在「新增 enum 值」計畫外,遇到相關 admin route 修改時,應主動建議加白名單過濾器。Reference: 2026-05-26 event 25e27de9 categories.photography raw key(commit 264afed 修分類但未加過濾器,已建 backlog)。
每次新增 event_form 值,以下 6 個位置必須同一 commit 同步:
| 步驟 | 位置 | 說明 |
|---|---|---|
| 1 | supabase/migrations/<NNN>_*.sql | events_event_form_check CHECK constraint(authoritative source) |
| 2 | scraper/annotator.py | VALID_EVENT_FORMS frozenset(L863 附近)+ EVENT FORM RULES prompt 區塊(L686 附近) |
| 3 | scraper/database.py | _VALID_EVENT_FORMS writer whitelist + _event_to_row() regression test;缺值時 scraper 輸出會在入庫前靜默被剝除 |
| 4 | web/lib/types.ts | EVENT_FORMS array / EventForm type,前台與 admin form 共同依賴 |
| 5 | web/app/api/admin/extract-from-image/route.ts + web/app/api/admin/annotate-event/route.ts | 兩個 GPT prompt 內的 event_form: 枚舉清單 |
| 6 | web/messages/{zh,en,ja}.json | eventForm.<key>(三語各一) |
Reference incidents:
broadcast/tasting/study_abroad 等新值並重命名舊值(concert→performance、lecture_seminar→lecture、film_screening→screening、festival→ 移除、sports→ 移除)。annotator.py 跟上了,但兩個 admin API prompt 留在 pre-047 命名 → OCR 回傳舊值 → DB CHECK 拒絕 → 管理画面保存 400 失敗(commit 9ecaae6)。教訓:建立此 checklist。web/lib/types.ts EVENT_FORMS 與 web/messages/*.json 的 eventForm namespace。少同步任一處,會出現 raw key 或前後端 enum 漂移。scraper/database.py::_VALID_EVENT_FORMS 漏列既有 publication;scraper 即使正確輸出該值,_event_to_row() 仍會在 DB writer 層靜默剝除。教訓:writer whitelist 與 regression test 是第 6 個同步點。問題模式:fix commit → V-M-D → docs commit → V-M-D 第二輪。每個邏輯變更跑兩次部署,浪費 token 又阻礙 session。
規則:Architect 設計計畫時,若預判修復會揭露新教訓(silent failure、GPT 污染、新 Guard、paired-handler 模式、第三方 API 行為),必須在計畫的 Verification 段明示:
「Engineer Step 3a 必做:在 commit fix 之前,將教訓寫入對應的
history.md/SKILL.md/agent.md,與 fix code 合併為單一 commit。」
對應稽核點:
不適用情境:cosmetic refactor、dependency bump、typo、純 i18n 字串調整、純 CSS 調整。
order() 語法:.order("col", desc=True) — 不是 .order("col", ascending=False)(pandas 風格在此無效)。計畫中任何排序操作必須使用正確語法。upsert vs update:既存 row 的部分更新必須用 .update().eq(),不可用 upsert(會觸發 INSERT fallback,撞 NOT NULL 約束)。field_corrections.corrected_value は TEXT NOT NULL:qa_auto_fix.unlock_and_write() の lock_empty モードで corrected_value=None を渡すと HTTP 400 (23502) になる。必ず "" 番兵を使用すること(annotator FC guard は行の存在のみチェック)。lock_clean の list/dict 値は json.dumps(v, ensure_ascii=False) で文字列化する。Reference: _fc_value() helper in qa_auto_fix.py。unlock_and_write() で field_corrections へ upsert する際の制約:
| mode | corrected_value に渡す値 | 理由 |
|---|---|---|
lock_clean(新値あり) | _fc_value(new_value) → list/dict は json.dumps()、str は str() | NOT NULL 対応 |
lock_empty(フィールドを NULL クリア) | "" (空文字列・番兵) | None は 23502 違反 |
unlock_only | FC row ごと DELETE | corrected_value 不要 |
なぜ "" 番兵で安全か:annotator.py の FC guard(L1136)は (event_id, field_name) ペアの存在のみチェックし、corrected_value の内容は参照しない。空文字列でも「ロック済み」と正しく判定される。
Partial-write リスク:lock_empty で events.update() 成功後に FC upsert が 400 エラーになると、イベントフィールドは NULL になるが FC ロックが存在しない中途半端な状態になる。_fc_value() helper で事前に型変換してこのリスクを防ぐこと。
Reference incident: 2026-05-25 — qa_heartbeat live run で 23 件が verify_failed(Error 23502)。lock_empty モードが corrected_value=None を渡していた。_fc_value() helper 追加後に解消。
當使用者報「production 後台 / 客戶端寫入突然全部失效」且最近 24h 有 Vercel 部署,在深入結構性分析(migration / RLS / GRANT)之前先要求使用者執行 1 分鐘 DevTools 三點檢查:
authorization: Bearer ... → 客戶端 session 遺失(結構性 bug,深挖 auth callback / cookie 設定)[] 或空白 200 → RLS / expired JWT 過濾為 0 列(可能是暫時性,先請使用者重整再試)42501 GRANT 缺失)sb-<ref>-auth-token HttpOnly 欄:
三點全綠 + 重整後恢復 = 暫時性 Vercel 滾動部署故障,不需動程式碼。 但應提案在 Engineer SKILL guard 加 0-row UPDATE 防護(.select("id") + data.length === 0 視為失敗)。
反面教訓(2026-05-15): 後台 events UPDATE 三項全失,第一輪假設 migration 069 漏 events 表 GRANT(錯誤),第二輪假設 ae9dc77 cookie 寫入問題(錯誤),準備寫新 migration。使用者實測 DevTools 三點全綠後判定暫時性。若一開始就先讓使用者跑三點檢查,可省 20 分鐘誤判時間。
在審核任何包含 async button handler(handleConfirm、handleDismiss、handlePublish 等)的計畫或 PR 前,必須確認:
finally 塊中重置,不論例外或正常結束:
async function handleAction() {
setLoading(true);
try {
const result = await someServerAction();
// handle result
} catch (err) {
console.error(err);
alert("操作に失敗しました。");
} finally {
setLoading(false);
}
}
AbortSignal.timeout(N),防止無限 hang:
const res = await fetch("https://api.github.com/...", {
signal: AbortSignal.timeout(10_000),
});
finally 缺失的雙重故障。appendToHistoryFile、appendPendingRuleToSkill 等),必須在計畫中明確標注 Engineer 需加 AbortSignal.timeout。Reference incident: 2026-05-17 — AdminReportsTable handleConfirm() 無 try/catch,b2e8b92 後累計 4 次 GitHub fetch,GitHub API hang 時按鈕永久卡死(commit 9319f57 修復)。
在審核任何多 session/subagent 平行開發工作流前,必須確認:
[STATE] 開頭:
[WIP] — 草稿,禁止合併[READY] — 已驗證,可立即合併[REVIEW] — 待人工確認[BLOCKED] — 有外部依賴未就緒./scripts/stash-status.sh list 是快速狀態總覽的入口,點 VMD agent 前應先執行。./scripts/stash-status.sh promote <N>):
[READY] stash 並提示促銷,無需手動記得跑 stash-status。[READY] stash 超過 3 天未合併,CLI 標記 ⚠ STALE——可能與 main 衝突或內容過時。architect 設計 multi-session 任務計畫時,必須在「完成條件」中明確指定 stash state 標籤,例如:
完成後執行
git stash push -m "[READY] scraper/starcat: end_date fix"
在審核任何 merger 輸出、或手動設定 merged_into_event_id 前,必須確認不形成循環:
permanentRedirect() 無限觸發,瀏覽器收到 HTTP 308 loop,不停重載。scraper/ 執行):
r = sb.table('events').select('id,merged_into_event_id').not_.is_('merged_into_event_id','null').execute()
merged_map = {e['id']: e['merged_into_event_id'] for e in r.data}
cycles = []
for start in merged_map:
visited = set(); cur = start
while cur in merged_map and cur not in visited:
visited.add(cur); cur = merged_map[cur]
if cur in visited: cycles.append(start)
print(f"{'✅ No cycles' if not cycles else f'⚠ {len(set(cycles))} cycle nodes'}")
merged_into_event_id = NULL。如有多個事件把此節點當 merge target,評估是否改指向真正的 canonical active 事件。A(電影 X)→ B(電影 Y,不同片名)幾乎必然是 merger 錯誤,直接 NULL 斷開。auto_qa 應加入此 cycle 偵測(待實作)。Reference incident: 2026-05-15 — 57642851↔c8e813ae(赤い糸)二節點循環 + 84cb3ff3→2117c91e→a04e7ebb→84cb3ff3(台湾Filmake/めぐる面影)三節點循環,共 4 筆 DB 更新修復。
在審核任何涉及 weekly_line_broadcast.py 的計畫前,必須先確認系統設計的執行時序:
weekly_line_broadcast.py 的 cron 時序設計,不要急於本地補跑草稿,避免製造多餘的重複草稿。/{lang}/events/{id}),不可使用 /r/{id} 短連結(固定重導向至 /zh/)。Reference incident: 2026-05-08 — 多重錯誤同時發生(Supabase order() bug、草稿重複、連結語系錯誤、日期範圍錯誤)。
When a plan includes promoting an auto-generated scraper (auto_scraper_status=success) to status=implemented, the plan must explicitly include all 5 steps:
scraper/sources/<name>.py exists and is registered in scraper/main.py.research_sources row: status = 'implemented'.scraper_source_name = '<scraper key>' — MUST be filled or /admin/sources shows 0 events and cannot trigger Run Scraper (backend JOINs scraper_runs by this key). auto_generate does NOT fill this automatically.python main.py --dry-run --source <key> confirms events are found.YYYY.tiff-jp.net): replace hardcoded year with dynamic year resolution (follow redirect from root domain → fallback datetime.now().year).Missing step 3 has no compile-time or runtime error — it silently appears as a 0-count row in the admin UI.
在審核任何涉及 weekly_line_broadcast.py(或未來任何 LINE push 腳本)的計畫前,必須確認:
_fetch_upcoming_events 必須過濾 annotation_status:只允許 annotated 或 reviewed 的事件進入廣播 pool。
.in_("annotation_status", ["annotated", "reviewed"])
is_active=True 等於翻譯完整:新刮取的事件在 annotator 執行前 name_zh/name_en 為 NULL,annotation_status='pending'。若廣播在每日 pipeline 之前手動觸發,pending 事件會進入 pool,ZH/EN 訂閱者收到日文 fallback。Fetched N upcoming events 中無 pending 事件(annotation_status 過濾後應比無過濾少幾筆)。Reference incident: 2026-05-05 — 赤い糸 輪廻のひみつ 以日文出現在 ZH 週報,缺 annotation_status 過濾(commit 9b33ad3 後修正)。
在審核任何涉及 enrich_person_names() 或人名翻譯邏輯的計畫前,必須確認:
クー・チェンドン → Koo Kuan-Dong),片假名字串不在 desc_en 中,if ja_name in desc_en 永不命中。description_en 必須走 _fix_person_names_gpt_en() GPT 路徑(鏡像 desc_zh 的 _fix_person_names_gpt)。已於 2026-05-05 修復;任何回退到 katakana direct-replace 的 PR 必須拒絕。enrich_movie_titles / enrich_person_names 成功後必須自動鎖:透過 _lock_fields_via_corrections() upsert 進 field_corrections 表,避免下次 re-annotation 覆寫。Reference incident: 2026-05-05 — event f970e4e3(月老)desc_en Koo Kuan-Dong 從 5/4 daily CI 後持續未修正;同事件多次手修又被 AI 覆寫,根因為缺 lock。
_KNOWN_PERSON_MAP 藝名/筆名覆寫機制)在審核任何涉及 performer/director 翻譯、backfill_performer_i18n()、或新增 _KNOWN_PERSON_MAP 條目的計畫前,必須確認:
ギデンズ・コー 與漢字 九把刀 無語音對應關係(pen name),GPT 只能語音推測,必然失敗。所有已知藝名/筆名必須收錄進 _KNOWN_PERSON_MAP。_KNOWN_PERSON_MAP 條目時,ja/zh/en 三個值必須同時提供且驗證。資料來源優先序:eiga.com → 官方網站 → Wikipedia → 可靠第三方。_KNOWN_PERSON_MAP 生效位置:① 主 annotation loop(performer/director GPT 輸出覆寫)② performers[] 陣列逐元素檢查 ③ backfill_performer_i18n() Layer 0(已知名字跳過 GPT)。新增整合點時需三處同步。_KNOWN_PERSON_MAP 或 eiga.com lookup)backfill_performer_i18n() 不可限定 is_active=True:非活躍事件同樣需要翻譯完整性。批次 backfill 腳本的 active 過濾需明確設計。Reference incidents:
ギデンズ・コー → 基登斯·高 (GPT 幻覺);正確 九把刀 / Giddens Ko。14 筆已驗證名人收錄 _KNOWN_PERSON_MAP,11 筆 DB 事件修正。is_active=True 過濾而缺翻譯,需一次性批次 backfill。在審核任何涉及 performer_zh、performer_en、director_zh、director_en 的計畫,或設計多語言表演者顯示邏輯時,必須確認:
performer TEXT:日文原名(供 ja locale)performer_zh / performer_en TEXT:各語言名稱(GPT 填入或人工設定)performers TEXT[]:所有具名表演者/發表者的陣列(支援多人)director / director_zh / director_en:同上,用於導演zh → performer_zh || performer;en → performer_en || performer;ja → performer(不走翻譯欄位)黃以文(AI翻譯))。若來源中有該語言名稱,不加標注。performers[],即使有 5 人以上。field_corrections:performer_zh、performer_en、director_zh、director_en 手動修正必須同時 upsert 進 field_corrections,否則下次 re-annotation 覆寫。_zh/_en 欄位,避免二次 migration。works.work_type 有效值:film | stage | exhibition | concert_tour | tv_drama | tv_variety | other。conference 不在允許清單,學術研討會用 other。Reference incidents:
65a50b9:SYSTEM_PROMPT 追加 AI 翻譯標記規則 + 學術大會 performers[] 填寫規則(Incidents A & B)191d939:migration 053 新增 performers TEXT[](Incident C)3822fb8:migration 054 新增 performer_zh, performer_en, director_zh, director_en(Incident D)Annotator 的 _PERFORMER_INTRO_RE 使用 [\u4e00-\u9fff]{2,5} 純漢字 pattern,會將 カベルナリア 吉田 這類片假名筆名+漢字姓的複合名靜默截斷為 吉田。審核任何新 scraper 計畫,若來源頁面有結構化 instructor / 講師 / 登壇者欄位,必須確認 scraper 在 Event(performer=..., performers=[...]) 直接設定,而非依賴 annotator regex。Reference: wuext_waseda event 1be67e0f, 2026-05-16.
審核任何 multi-session 課程型 scraper(wuext_waseda、asahiculture 等)計畫前,必須確認 business_hours 在 scraper 層直接組裝(曜日 + 時間 + 全N回 + 個別開講日逐項列出),不留給 annotator。Annotator 只能抽取單一時間範圍(19:00〜20:30),無法保留曜日、N 回、跳週日期等資訊。Reference: wuext_waseda event 1be67e0f, 2026-05-16.
在審核任何涉及 organizer_zh、organizer_en 的計畫,或設計主辦方顯示邏輯時,必須確認:
organizer TEXT:日文原名(供 ja locale)organizer_zh / organizer_en TEXT:各語言名稱(_KNOWN_ORGANIZER_MAP 或 GPT 翻譯填入)zh → organizer_zh || organizer;en → organizer_en || organizer;ja → organizer_KNOWN_ORGANIZER_MAP:高頻主辦方(10 筆)hardcoded 在 annotator.py,確保翻譯品質。新增條目時 ja/zh/en 三語同時提供。field_corrections:organizer_zh、organizer_en 手動修正必須同時 upsert 進 field_corrections。i18n 文字欄位新增標準流程(已確立,第三次套用):
field_zh TEXT, field_en TEXTbase.py Event dataclass + database.py _event_to_row() 映射types.ts interface + getEvent<Field>(event, locale) helper + page.tsx 渲染日文漢字 ≠ 簡體中文判斷規則: 使用者反映「簡體字」時,先確認是 GPT 的 SC 輸出還是日文原文被顯示在非 ja 頁面。若為後者,正解是新增多語言欄位,非 SC→TC 轉換。
Reference incident: 2026-05-08 — commit 95c7ad8:migration 059 + annotator + web 全套。273/273 backfill。
在審核任何涉及 _SIMP_TO_TRAD / _to_trad() 擴充的計畫時,必須留意:
annotator.py 的 _SIMP_TO_TRAD_RAW + auto_qa.py 的 SIMP_RE。*_zh fields + translate + FC lock)。_SIMP_TO_TRAD_RAW / SIMP_RE / SC_ONLY 後必跑):偵測集(SIMP_RE、SC_ONLY)的每個字元都必須在 _SIMP_TO_TRAD_RAW 有映射,否則「偵測得到但 _to_trad() 修不掉」→ 無限 dismiss 循環(見下方 Three-Layer 反模式)。在 scraper/ venv 內執行:
python3 -c "
from annotator import _SIMP_TO_TRAD_RAW
from auto_qa import SIMP_RE, SC_ONLY
mapped = set(_SIMP_TO_TRAD_RAW)
simp = set(SIMP_RE.pattern) - {'[', ']'}
print('SIMP_RE 缺映射:', ''.join(sorted(simp - mapped)) or 'OK')
print('SC_ONLY 缺映射:', ''.join(sorted(SC_ONLY - mapped)) or 'OK')
"
理想兩行皆 OK;列出的任何字元 = 偵測得到但無法自動修復,必須補進 _SIMP_TO_TRAD_RAW 或自偵測集移除。
⚠️ 已知 backlog(2026-06-24 實測):
SIMP_RE缺 10 字、SC_ONLY缺 110 字(union 113 唯一字元),偵測集長期領先映射表。新增映射時優先消化此 backlog;長期解仍是點 2 的 OpenCC。此 backlog 代表 commit21b9c97(補当/写/圆三字)只解決冰山一角——只補真實資料出現的字,不代表偵測集與映射表已一致。
SC→TC 防禦分三層,在審核任何涉及 _zh 欄位寫入路徑的計畫時,確認三層全覆蓋:
| Layer | 機制 | 位置 | 職責 |
|---|---|---|---|
| L1 — 預防 | _to_trad() on GPT output | annotator.py 主迴圈 | 捕捉 GPT 輸出的 SC 字元 |
| L2 — Chokepoint guard | _to_trad() on FC write | _lock_fields_via_corrections() | 捕捉所有寫入 FC 的 _zh 值(含 backfill、手動 upsert) |
| L3 — 偵測 + 修復 | SC_ONLY + fix_simplified() | auto_qa.py | 捕捉 L1/L2 遺漏的殘留 SC |
三層一致性規則:
_SIMP_TO_TRAD 字元映射(衍生自 _SIMP_TO_TRAD_RAW)SC_ONLY 字元集必須是 _SIMP_TO_TRAD_RAW.keys() 的子集——不可包含不在映射表中的字元(否則偵測到但無法修復 → 無限 dismiss 循環)fix_simplified() 掃描範圍必須與 _detect_simplified_chinese() 完全一致(目前 6 個 _zh 欄位)反模式:
SC_ONLY 包含 SC/TC 共用字元(如 征/蹈/零/蒙)→ 假陽性_SIMP_TO_TRAD_RAW 缺映射但 SC_ONLY 有該字元 → 偵測到但無法修復fix_simplified() 掃描範圍 < _detect_simplified_chinese() 掃描範圍 → 修復遺漏Reference incidents: 2026-05-11 commits f7790a2, aa24400。
Reference incident: 2026-05-08 — commit 95b79ef:新增 9 字(诗/禅/图/猎/过/员/剧/别/于)。
在調查任何「カテゴリバッジが表示されない」「カテゴリが DB に存在するのに画面に出ない」問題、または name_ja が期待と異なる場合に、必須確認:
name_ja と raw_title の diff を確認する:annotator は raw_title → name_ja 変換時に後置された分類語(「レポート」「告知」「詳細」「ご案内」等)を「タイトルの装飾」と判断して削除することがある。# name_ja を raw_title 通りに復元 + FC ロック
sb.table("events").update({"name_ja": raw_title}).eq("id", eid).execute()
sb.table("field_corrections").upsert({
"event_id": eid, "field_name": "name_ja",
"corrected_value": json.dumps(raw_title, ensure_ascii=False)
}, on_conflict="event_id,field_name").execute()
category: ['report'] が付与されていてもタイトルから「レポート」が消えることがある。category 確認 → ② messages/*.json i18n 確認 → ③ raw_title vs name_ja diff 確認 → ④ UI レンダリングロジック確認。Reference incident: 2026-05-07 — f7ff56ca「台湾文化センター映画...トークイベント レポート」の name_ja から「レポート」が annotator によって削除されており、category: ['movie','lecture','report'] は正常だがタイトルバッジ調査で発覚。
在審核任何直接 SQL UPDATE 翻譯欄位(name_zh / name_en / description_zh / description_en / performer)的計畫前,必須確認:
field_corrections:否則下次 annotation_status 翻回 pending 時,annotator 主迴圈用 GPT 重寫,所有人工修正瞬間蒸發。這是「修了又錯、錯了又修」迴歸鏈的根因。from annotator import _get_supabase, _lock_fields_via_corrections
sb = _get_supabase()
sb.table("events").update({"name_zh": "月老"}).eq("id", eid).execute()
_lock_fields_via_corrections(sb, eid, {"name_zh": "月老"})
enrich_movie_titles 與 enrich_person_names 成功 patch 後已自動 upsert field_corrections(2026-05-05 起)。手動修正不可漏這一步。continue 是反 pattern:lookup 失敗必須 logger.warning,否則 CI log 看不到,錯誤翻譯靜默上線數日。Reference incident: 2026-05-05 — event f970e4e3(月老)多次被修又被 AI 覆寫;今日同步補入 field_corrections 鎖定四個翻譯欄位後免疫。
【レポート】 誤注入防護)在審核任何涉及 report 分類注入、_inject_report_prefix() 邏輯、或手動觸發再 annotation 的計畫前,必須確認:
_inject_report_prefix() 只對兩種情況注入前綴(2026-05-22 修正後):
_HEADLINE_REWRITE_SOURCES(google_news_rss、nhk_rss、prtimes、walkerplus、note_creators):標題是新聞標題非活動名,GPT 分類 report 可信raw_title 本身含有 _REPORT_TRIGGER_RE 關鍵字(レポート|レポ|報告|活動記録|開催記録|鑑賞記録|記録[||]|アーカイブ|recap|行ってきた|観てきた|鑑賞レポ|結果発表)raw_title 是官方活動標題:GPT 可能把「ZINE Fes(ZINE 市集)」或「講演会」的部分面向誤分類為 report,此時 raw_title 不含 report 關鍵字,必須跳過前綴注入。report category 注入後 name_ja 加上 【レポート】;name_zh 加上 【活動報導】;name_en 加上 [Report] ;若無 FC lock,下次 re-annotation 會還原,但已對外顯示污染名稱。_src_is_rewrite = event.get("source_name") in _HEADLINE_REWRITE_SOURCES
_title_is_report = bool(_REPORT_TRIGGER_RE.search(event.get("raw_title") or ""))
if "report" in update_data.get("category", []) and (_src_is_rewrite or _title_is_report):
# inject prefix
Reference incident: 2026-05-22 — event 6850265d(ZINE Fes 誠品生活日本橋)。GPT 分類 ['senses', 'workshop', 'lecture', 'report'],_inject_report_prefix() 對 peatix 爬蟲來源注入 【レポート】,name_ja/name_zh/name_en 三欄全被污染。修正:annotator.py 加入 _src_is_rewrite or _title_is_report 守衛 + FC lock name_ja/name_en/end_date。
Reference incident: 2026-06-13 — 大濛(霧のごとく)/ stranger f3554212。_REPORT_TRIGGER_RE 的寬泛 記録 命中票房文案「興行収入は…現在も記録を更新中」,_inject_keyword_categories() 誤注入 report,stranger name_ja 被污染成 【レポート】霧のごとく,全面 8 筆大濛上映場次卡在 error、首頁不顯示。修正(2026-06-16):記録 → composite terms 活動記録|開催記録|鑑賞記録|記録[||],並同步 SYSTEM_PROMPT report/recap 例外。
在手動觸發再 annotation(設定 annotation_status = 'pending' 並更新 raw_description)的操作前,必須確認:
end_date(設為 None):annotator 的 "end_date": event.get("end_date") or annotation.get("end_date") 使用 Python or 邏輯——若 DB 中已有 end_date(即使是前次 GPT 錯誤推論的值),該值為 truthy,GPT 新推論值永遠不會被採用。start_date 若也可能錯誤,一起清除:同理,若前次 annotation 從稀疏 raw_description 推論出錯誤的 start_date,再 annotation 也無法自動修正。sb.table("events").update({
"raw_description": new_raw,
"description_ja": new_desc,
"end_date": None, # ← MUST clear to allow GPT re-inference
# start_date: None if also suspect
"annotation_status": "pending",
}).eq("id", EID).execute()
Reference incident: 2026-05-22 — event 6850265d(ZINE Fes)end_date 在首次 annotation(從稀疏 raw_description)被設為 2026-05-22;手動更新 description_ja 後設 pending,但未清除 end_date → 再 annotation 時 event.get("end_date") = 2026-05-22(truthy)→ GPT 推論的 2026-05-23 被忽略 → 兩日活動只顯示一天。
在任何包含「新增 prop 到 shared form component」或「後台新增欄位」的計畫前,必須確認:
grep -r "AdminEventForm\|<ComponentName" web/components/ web/app/ 找出所有呼叫點,逐一確認新 prop 是否已傳入。TypeScript 若 prop 有 fallback default 不會報錯,靜默失敗難以發現。AdminEventForm.tsx 增加對應 input,否則管理員無法覆寫 AI 填錯的值。清單:
name_*、description_*、selection_reasonorganizer、organizer_url、event_form、co_organizers、sponsorsprimary_language、has_japanese_support、has_english_support、has_chinese_supportannotation_status → reviewed,AdminEditClient.tsx 的 TRACKED_FIELDS 必須加入。陣列(string[])與布林值欄位需特別確認比對邏輯(不能用 ===,需深比對)。string[] 欄位在 form 用 comma-separated string 表示;save 時轉換:
value.split(',').map(s => s.trim()).filter(Boolean)
讀取時轉換:(arr ?? []).join(', ')Reference incident: 2026-05-05 — AdminEventForm 新增 tEventForm prop 後,AdminEventTable 呼叫時忘記傳入,TypeScript 無錯誤(commit 30999ea 補齊);同次 commit 補齊 organizer、event_form 等 8 個後台隱藏欄位。
在審核任何修改 database.py _build_movie_extend_row()、新增 movie-extend 觸發分支、或擴張「對既存 row 部分更新」邏輯的計畫前,必須確認:
_build_movie_extend_row() 允許更新的欄位限定為 raw_description、business_hours、start_date、end_date、scraped_at、annotation_status(僅在 raw_description 變動時 flip 為 pending)。禁止新增 name_* / description_* / category / location_* / performer / organizer* / is_paid / price_* / event_* 任何欄位。新增白名單欄位的 PR 必須拒絕。existing_movie_state への登録のみ:key in existing_movie_state が唯一の発動条件。existing_movie_state は事前に DB 行の 'movie' ∈ category でフィルタ済み。e.category(incoming scraper event の category)を条件に入れてはいけない ——スクレイパーは annotator 前なので e.category = [] が常であり、and "movie" in (e.category or []) は条件として常に False になる(incident: 2026-05-20)。残りの条件:不在 blocked / reviewed / force_keys——これらは DB 行属性で判定するため問題なし。.update().eq().eq() 而非 upsert:既存 row 確認存在後不可用 client.table("events").upsert(rows, on_conflict=...) ——supabase-py 會嘗試 INSERT fallback,撞 NOT NULL 約束(如 source_url)。詳見 engineer history 2026-05-05 — Partial-payload upsert violates NOT NULL constraints。force_rescrape=true(後者全覆寫會吃掉 movie-extend 的 MIN(start_date) 保留語意)。如需 reviewed 電影更新場次,走 manual SQL + field_corrections 路徑,不可改 movie-extend 條件。_build_movie_extend_row() 加 if-else,必須獨立 helper 並重新評估白名單欄位。end_date = None のままでは _build_movie_extend_row() の MAX ロジックが機能しない(None fallback で old_end そのまま)。映画スクレイパーは「終映日」未取得時に end_date = start_date(当日)をフォールバックとして設定すること——毎日 MAX で端を延ばせる(incident: kyoto_cinema 2026-05-20)。Reference incidents:
8572104 引入 movie-extend;同 commit message 明示「by construction, extend rows touch zero P3.2-protected columns」與 .update() not upsert 的設計理由。end_date 固定在初日:e.category 参照バグ + end_date=None fallback 漏れ(commit a2f5828)。在審核任何修改「事件卡片視覺呈現」的計畫前(包含 location 顯示、徽章、日期格式、分類 chip、save 按鈕、報告按鈕等),必須確認:
web/app/[locale]/page.tsx 使用 inline list-style 渲染,不使用 EventCard.tsx:grep EventCard in page.tsx 會回傳 0 match。修改 EventCard.tsx 對首頁完全無效。EventCard.tsx:這些頁面共享元件。web/components/EventCard.tsx(其他頁面)web/app/[locale]/page.tsx(首頁 inline,行 ~290 附近的 events.map(...) 區塊)web/lib/:純函式(如 getCityLabel、extractCity、日期格式化)抽到 web/lib/<name>.ts,兩處 import;避免「修了一處忘了另一處」的迴歸。React component(如 chip 子元件)若值得共用可抽到 web/components/。curl -s https://tokyotaiwanradar.com/zh | grep -c '<新元素 class 識別字>'
# 期望非 0,若為 0 → 修了 EventCard 但首頁 inline 沒同步
Reference incident: 2026-05-05 — commit 5a29c13 修 EventCard.tsx 城市徽章邏輯,但首頁完全無變化(首頁不用 EventCard)。commit 9f4b468 抽 web/lib/cityLabel.ts 共用 helper 後雙處同步生效。
is_active = False based on end_date < today. Past events must remain is_active = True so users can view event history. Visibility for ended events is controlled by the frontend FilterBar ("顯示已結束活動" toggle), not by is_active.is_active has exactly two legitimate write sources:
merger.py deactivates a duplicate secondary event during merge.is_active must be verified against these two sources before execution. If it does not match either, abort.任何計畫包含 annotator.py --backfill-* 步驟時,必須在步驟清單中明確列出「backfill 後多語言欄位 QA 驗證」子步驟:
selection_reason["ja"]:必須含假名(平假名或片假名);無假名表示語言污染SELECT id, source_name, (selection_reason->>'ja') as ja_text
FROM events
WHERE selection_reason->>'ja' IS NOT NULL
AND selection_reason->>'ja' !~ '[ぁ-んァ-ン]'
LIMIT 20;
Incident: 2026-05-04 --backfill-tier1 導致 49 筆 selection_reason["ja"] 為中文,需人工腳本修正。
/docs 是結構性文件,只在架構改動時需要更新。以下情況屬於架構性改動,計畫中必須明確包含「更新 docs/ARCHITECTURE.md 或 docs/SCRAPER_PIPELINE.md」步驟:
| 改動類型 | 需更新 |
|---|---|
| 新增或移除整個 CI workflow | ARCHITECTURE.md |
| 新增或移除 pipeline layer(auto_scraper、researcher 等) | SCRAPER_PIPELINE.md |
| 新增或移除 Supabase 整合點(LINE bot、新 webhook) | ARCHITECTURE.md |
新增 web/app/api/ 下的 API endpoint | ARCHITECTURE.md |
不屬於架構性改動(無需更新 docs):bug fix、單一 scraper 新增、i18n 修改、CSS 調整、新增個別 Supabase migration。
/docs 記錄的是「系統怎麼運作」,不是「系統現在的狀態」。不要在文件中寫入會每天變動的數字(scraper 數量、事件總數、migration 編號)。
在任何包含「Server Component 中有 badge 或動態計數器」的 feature plan 中,必須明確標示:
⚠️ 此 badge / 計數器需要即時性嗎?若是,plan 必須包含「拆出 Client Component + Supabase Realtime 訂閱」步驟。
分離模式:
ParentComponent (Server Component)
└─ 查詢初始 count(SSR,一次性)
└─ <DynamicBadge initialCount={n} />(Client Component)
└─ Supabase Realtime 訂閱 INSERT + UPDATE 保持即時更新
強制規則:
initialCount prop 作為 SSR 初始值,啟動後改由 Realtime 維護。無操作 Quality Section 應直接移除: Quality check section 如果沒有對應的可執行 action(fix button / batch action),且數值永遠不清空(例如 archive cron 一天只跑一次的 expired-but-active),計畫不應包含此 section,已存在的應移除。只有數值能被操作清零的 check section 才值得顯示。
Reference incident: AdminTabNav badge(2026-05-02)—(commit 4a71258); expired-but-active section(commit cd4cc29).
在任何包含「新增或修改 /admin/quality check 條件」的 feature plan 中,必須確認以下三點:
Quality check 用哪個欄位 IS NULL 做判斷,該欄位必須是詳情頁實際 render 的欄位。
app/[locale]/events/[id]/page.tsx),確認「哪個欄位 null 才真正影響使用者體驗」。location_address IS NULL 做缺地點 check,但詳情頁顯示 location_name(commit b82849d → 80920ce)。設計每個 quality check 時,同步列出「哪些事件類型天生不需要此欄位」,並在 DB query 層排除:
| Check 類型 | 已知排除 | 排除原因 |
|---|---|---|
缺地點(location_name IS NULL) | source_name = 'gguide_tv' | 電視節目 |
缺地點(location_name IS NULL) | category 含 competition | 競賽/補助,全國性活動 |
若未排除 → flag 永遠無法清零 → 無意義的噪音。
排除語法(Supabase RPC):
.not('source_name', 'eq', 'gguide_tv')
.not('category', 'cs', '{"competition"}')
所有 quality check 的排除條件必須推到 DB query(.not()),禁止在 JS 側 .filter() 排除。原因:DB 層過濾減少傳輸量,且排除邏輯集中在 query 中易於審查與維護。
Reference incident: 2026-05-02 quality page — gguide_tv 排除原為 JS client-side filter,後移至 DB query(commit 80920ce);competition 排除直接寫在 DB query(commit 4ca383a).
撰寫任何依賴 DB 欄位的 client-side filter 前,必須確認以下三步驟:
.select("...") 字串中:否則欄位值為 undefined,filter 條件永遠不成立,靜默通過所有資料(不報錯)。location_prefectures?: string[] | null)。反例(commit bf22756 之前):
// ❌ location_prefectures 不在 select 字串 → 欄位 = undefined → 過濾靜默無效
if ((e.location_prefectures?.length ?? 0) > 1) return false;
正確做法:
// Step 1: 確認 select 含欄位
.select("id, location_name, location_prefectures, ...")
// Step 2: interface 宣告型別
interface QualityRow { location_prefectures?: string[] | null; ... }
// Step 3: 才寫 filter 邏輯
if ((e.location_prefectures?.length ?? 0) > 1) return false;
Reference incident: 2026-05-02 — location_prefectures 未加入 select,多城市活動過濾靜默失效。
在審核任何 Server Component(page.tsx) 的 PR 前,若 page.tsx 抓取了資料並用 prop 傳給子 component,必須確認:
worksList 但 <AdminEventTable> 沒有 initialWorks={worksList},資料完全浪費,子 component 會自己做一次 client-side 重複 fetch。// page.tsx(Server Component)
const { data: worksData } = await supabase.from("works").select(...);
const works = (worksData ?? []) as Work[];
return <AdminEventTable initialWorks={works} ... />;
// AdminEventTable(Client Component)
const [works, setWorks] = useState<Work[]>(initialWorks); // 立即可用
useEffect(() => { /* refresh after creation */ }, []); // 僅作刷新用
const { data: ... } = await supabase.from(...) 呼叫,對照子 component 的 Props interface 確認每份資料都有對應 prop。Reference incident: 2026-05-16 — page.tsx 抓取 worksList 但未傳給 AdminEventTable,works state 初始化為 [],用戶開下拉時 fetch 未完成 → 空清單(work選項又不見了)。
在審核任何修改 TAIWAN_VENUE_KEYWORDS(或類似地名比對清單)的 PR 前,必須確認:
'新北' ⊂ '新北島'(大阪市住之江区);'台中' 可能出現在日本地名中。必須使用完整行政單位名:'新北市'、'台中市' 等。grep -r "<keyword>" scraper/ 確認無日本地名誤觸。updated_at ≤ report confirmed_at」時跳過。每次 scraper upsert 更新 updated_at,即使 dismissed,下次 run 仍重新觸發——假陽性關鍵字無法靠 dismiss 解決,必須修正關鍵字本身。Reference incident: 2026-05-05 — '新北' 匹配大阪市 新北島,event 371cf624 (GRAFFYHALL) 連續三次 auto_qa_taiwan_venue (commit 6b7174a)。
在審核任何 auto_qa 偵測器的改動或新增 auto_qa_* 類型前,必須確認:
location_name 為純城市名(東京、大阪、岡山 等)時,新聞彙整類 source(google_news_rss、koryu 等)本就無法提供具體場地。auto_qa_missing_address 不應 flag 此類事件。維護 VAGUE_CITY_NAMES frozenset。OVERSEAS_KEYWORDS tuple。location_address = NULL 的事件,仍應 flag。Reference incident: 2026-05-04 — 13 筆 auto_qa_missing_address pending,其中 5 筆為城市名/海外場地誤報(commit 15c5b4b)。
在任何涉及「SSR 頁面查詢關聯資料(父事件、鏈結實體)」的 feature plan 中,必須確認以下三點:
RLS "Public read events" policy 限制 anon key 只讀 is_active = true 的事件。若查詢目標(如父事件)被下架(is_active = false),anon key 查詢靜默回傳 null,不拋 error,難以察覺。
若查詢的關聯資料可能處於 is_active = false 狀態(例如:父事件下架、存檔紀錄),必須在 Server Component / route handler 中用 service role key:
// Server Component only — 不可傳到 client-side
const adminClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data } = await adminClient
.from("events")
.select("id, name_ja, name_zh, name_en") // 只查需要的欄位
.eq("id", parentId)
.single()
強制限制:service role key 絕對不得暴露到 client-side。只在 Server Components 或 API route handlers 使用。
用 service role key 查詢關聯資料時,只 select 當前頁面真正需要的欄位(如 id, name_ja, name_zh, name_en),不得用 select("*") 避免洩漏敏感欄位。
Reference incident: 2026-05-02 — 父事件(台東祭)被設為 is_active = false 後,子事件詳情頁父事件連結消失;改用 service role key 後恢復(commit f5931e0)。
Before planning any new admin page or dashboard column whose primary output is a count / status / health number, ask:
/admin/..., it will be ignored.Default preference order for monitoring features:
/admin/<topic> page → only when the user explicitly asks for an interactive审查 surface (multi-row triage, manual selection, bulk action).If a plan introduces a new admin page or count column with no associated action, document the rationale explicitly in the plan; otherwise propose the passive push variant first.
Reference incident: 2026-05-01 Tier 1 monitoring — /admin/quality page and /admin/stats SLA columns were planned, implemented, then撤銷 same week; only the LINE budget push (weekly_report.py) survived.
Before approving any change to annotator.py annotation field priority, verify:
start_date / end_datelocation_name / location_addressbusiness_hoursis_paidNone/null).name_zh, name_en, description_*, location_name_zh/en, business_hours_zh/en.name_ja special case: when name_ja_locked=true, the scraper's value is preserved verbatim. The source title may be in Japanese, Chinese, or English — name_ja is a field identifier, not a language constraint.location_url — conditional write: GPT may extract it from raw_description text (schema prompt must say "extract from text only, no hallucination"). Write only when non-null (_loc_url = event.get("location_url") or _str(annotation.get("location_url"))); never write null back to DB — null would overwrite admin-entered values. This is a field shared between scraper/GPT extraction and admin manual entry. (commit fb568c4, 2026-05-02)開催日時: YYYY年MM月DD日 header to raw_description, then set annotation_status='pending' to trigger re-annotation.在審核任何涉及使用者手動建活動、海報 OCR、或是前台標註活動(Annotate Web)功能的計畫或修改時,必須強制執行本三層守護守則:
web/components/AdminCreateClient.tsx 與 web/components/OwnerCreateClient.tsx)的表單欄位映射、成功與錯誤 UI 回饋、狀態機、讀秒動畫、雙區塊同位 notice 頁面以及標註 client action 必須 100% 共用 web/lib/eventIntakeClient.ts 的輔助函式。extract-from-image/route.ts)在所有文字提示字、欄位清單、以及 Web-only glossary(例如特定的 記念講演会 -> 紀念演講/Commemorative Lecture)必須保持字元級同步,防止單邊擷取能力失衡。在 Web 搜尋標註進行欄位合併覆寫(eventFieldMerge.ts)時,為避免「弱相關搜尋」產生的髒資料破壞 OCR 抓取的特徵,或直接洗掉已存在的值,必須套用以下雙信心階梯:
null、""、[]),只要外部搜尋結果具有基本相關性即套用(要求 bestScore >= 3)。bestScore >= 6)。若評分不足,禁止覆蓋現有非空欄位。onChange 時記錄已手動修改的欄位鍵值(lockedFields: string[])。lockedFields 指紋列表中的所有欄位。這確保了「手動修改永久優先於 Annotate Web 自動補全」的安全保證。在審核任何 scraper 的 start_date/end_date 傳入邏輯前,必須確認:
datetime(..., tzinfo=timezone(timedelta(hours=9))) 傳入 Supabase 後以 UTC 儲存,JST+9 偏移導致日期倒退一天(2026-05-08T00:00:00+09:00 → 2026-05-07T15:00:00+00:00)。# CORRECT — 保留日曆日期,強制 UTC tzinfo
start_date = jst_dt.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc)
# WRONG — JST-aware datetime 傳入 Supabase
start_date = jst_dt # tzinfo=JST, Supabase 轉成前一天 UTC
datetime 無 tzinfo 時 Supabase 依伺服器時區解讀(通常 UTC),一般安全,但不如明確設定 UTC midnight。start_date 與來源頁面的日期完全一致。Reference incident: 2026-05-07 — Stranger scraper f3554212 start_date 存為 2026-05-07T15:00:00+00:00(應為 2026-05-08),Vercel 顯示前一天(commit b7dc34f)。
Before acting on any hallucination scan result (address/location not found in raw_description), verify:
MoN Takanawa (inside Takanawa Gateway City) has postal address 港区三田 — not 高輪. Station names, building brands, and postal addresses can differ.港区三田3-16-1 to wrong 港区高輪4-10-30 based on venue name reasoning. Reverted after user confirmation.在審核任何涉及 annotator 日期提取邏輯、或分析任何來源事件年份錯誤時,必須確認:
年份錨點注入必須覆蓋所有來源(非僅 gnews 類):
scraped_at AND "記事配信日:" not in raw_desc(不限 source_name)(記事配信日: YYYY-MM-DD)\n\n 前綴兩類年份幻覺場景均被覆蓋:
5月8日(金)公開)→ GPT 從訓練資料猜年,可能猜出過去年份2025年11月に公開を迎えた台湾国内での興行収入,指另一國的上映年)→ GPT 誤用該年份為日本活動年份SYSTEM_PROMPT DATE Rule 7 必須引用 記事配信日:
(記事配信日: YYYY-MM-DD) → use that year as the reference yearfield_corrections 鎖定已修正的年份:
field_corrections(start_date, end_date),否則 re-annotation 重新覆寫失效症狀:事件 start_date 年份比 scraped_at 年份差超過 1 年(可能是過去也可能是未來)
驗證命令:
grep -n "記事配信日\|scraped_at year anchor" scraper/annotator.py | head -10
# 確認不含 "_gnews_sources_needing_year" 的限制條件
Reference incidents:
0d33b617 (gnews 熊本チップ・オデッセイ) scraped_at=2026-04-28 → annotated start_date=2024-04-01(應為 2026-04-12)dded67a6 (uplink_cinema 霧のごとく大濛) raw_desc 含「2025年11月に公開を迎えた台湾国内での」(台灣內地年份),GPT 幻覺 start_date=2025-05-08(應為 2026-05-08)在審核任何涉及 enrich_movie_titles() 修改、或分析 gnews sub-event 電影標題錯誤的計畫前,必須確認:
gnews sub-event 的 name_ja 不可作為 eiga.com lookup 的標題來源:
source 在 _NEWS_MOVIE_SOURCES 且 bracket 命中來自 name_ja(非 raw_title),且事件有 parent_event_id → 必須 continue(跳過)並記錄 logger.warning。name_ja 是 GPT 從極薄語境(單句描述 + 文章全文)生成,極易幻覺電影名稱。raw_title(scraper 直接捕獲)中的括號標題。enrich_movie_titles select 查詢必須含 parent_event_id:
parent_event_id,若 select 字串缺少此欄位,event.get("parent_event_id") 永遠 None,guard 靜默失效。SYSTEM_PROMPT SUB-EVENT name_ja 規則的 CRITICAL 補丁:
SUB-EVENT name_ja 段落須包含 CRITICAL — DO NOT INFER MOVIE TITLES 文字。根因機制(供調試參考):
驗證命令(執行後確認 _title_from_raw 旗標存在):
grep -n "_title_from_raw\|skipping enrich for news sub-event" scraper/annotator.py
Reference incident: 2026-05-05 — d18339d5 (gnews_f9a2e51bc89a_sub3) raw_desc 只有 1 句話,GPT 幻覺 name_ja = "赤い糸 輪廻のひみつ"(月老),應為チップ・オデッセイ(造山者)場次。無 bracket → enrich_movie_titles 未鎖定,但 GPT 直接寫入 DB(annotation_status=annotated)且人工修正前無法自動偵測。
在審核任何涉及 ks_cinema(或其他系列頁電影場館來源)的 annotator 計畫,或分析同一電影出現多筆事件的問題前,必須確認:
4/25~5/1 10:00、5/2~8 14:40),不建立 sub_events;改用 start_date=首日、end_date=尾日,時段細節放 business_hours」。_cinema_sources = {"ks_cinema"},若 source_name in _cinema_sources AND source_id ends in _{digit} AND parent_event_id=None → sub_events = [](防止首次 race condition)。_0, _1, _2)在 scraper 首次執行時 _get_parent_uuid 查不到 parent(尚未 commit)→ parent_event_id=None。這是已知的架構限制;現有守衛已防止 annotator 誤生成 _sub1。_sub1 不會被 merger 消除:同 source 的事件(ks_cinema → ks_cinema)被 Pass 1 source_name == source_name 跳過。若 DB 已有殘留 _sub1,需人工 deactivated_by_pass=admin_manual。Quick Check(出現重複電影事件時):
# Check if _sub1 events exist for the film
r = sb.table("events").select("id,source_id,is_active,parent_event_id").ilike("source_id", "%_sub1%").eq("source_name","ks_cinema").execute()
# Deactivate if found
sb.table("events").update({"is_active": False, "deactivated_by_pass": "admin_manual"}).in_("source_id", [e["source_id"] for e in r.data if e["is_active"]]).execute()
Reference incident: 2026-05-06 — 車頂上的玄天上帝(ks_cinema taiwan-filmake_2_sub1)因 SYSTEM_PROMPT 多時段規則 + race condition 生成 _sub1,出現 4 筆重複(其中 2 筆已被 merger 停用)。修復:停用 _sub1、SYSTEM_PROMPT 加豁免規則、annotator.py 加程式碼守衛。
在審核任何涉及 organizer 欄位,或評估 category_corrections few-shot 範例設計的計畫前,必須確認:
_gpt_org_raw 在寫入 update_data 前,需確認 _gpt_org_raw in (raw_title + " " + raw_description)。不存在則丟棄(_guarded_organizer = None)並發出 WARNING log。category_corrections 的補正例若含具體主辦方名稱,GPT 可能對缺主辦人的其他活動 hallucinate 相同名稱。這是 named entity 的跨事件遷移效應。category_corrections few-shot → SYSTEM_PROMPT 注入 → GPT hallucinate organizer → P0 保護鏈保存錯值 → 子事件繼承 → 雪球效應。_ai_or_existing():確保 P1(field_corrections)保護也覆蓋 organizer 欄位。驗證指令(改動 annotator.py organizer 邏輯後執行):
# 確認 guard 存在
import ast, pathlib
src = pathlib.Path("scraper/annotator.py").read_text()
assert "_gpt_org_raw" in src and "_source_text" in src and "Organizer hallucination detected" in src
print("Guard: OK")
# DB 掃描:找到 organizer 但 raw_text 不含 organizer 名稱的事件
# (需在 DB 層做,此為人工 SQL)
# SELECT id, name_ja, organizer, LEFT(raw_description, 200)
# FROM events
# WHERE organizer IS NOT NULL
# AND annotation_status = 'annotated'
# AND raw_description NOT ILIKE '%' || organizer || '%'
# AND raw_title NOT ILIKE '%' || organizer || '%'
# LIMIT 20;
Reference incident: 2026-05-06 — category_corrections 含 2 筆 セシリアママのHappy Table... few-shot 範例,導致 31 件 Peatix 活動被 hallucinate organizer = "セシリアママ"(commit fix(annotator): add organizer non-hallucination guard)。
在審核任何設定「配給」→ organizer 的案例,或分析 organizer 字串含「/」的事件前,必須確認:
"JAIHO/Stranger" 是錯誤的)。organizer,其餘 → co_organizers[]。if "/" in (organizer or "") or "/" in (organizer or ""):
# 需要拆分,不可整串儲存
parts = re.split(r"[//]", organizer)
organizer = parts[0].strip()
co_organizers = [p.strip() for p in parts[1:]]
field_corrections:organizer、co_organizers、organizer_type 手動修正後必須同時 upsert。Reference incident: 2026-05-07 — dec5031b organizer = "JAIHO/Stranger" 應為 organizer = "JAIHO", co_organizers = ["Stranger"], organizer_type = ["commercial_brand"]。
在審核任何涉及 work_id 的事件的 name_zh/name_en 前,必須確認:
name_zh/name_en 必須是 name_ja(完整活動標題)的翻譯;不可從 works.title_zh/works.title_en 繼承。電影名、作品名只是活動的一部分,不等於活動標題。len(name_zh) << len(name_ja)(如 name_zh = "中村地平"(4字)而 name_ja = "第78回 日本と台湾を考える集い 紀錄片《中村地平》上映会"(30+字)),屬高可信度異常。# 查所有有 work_id 且 name_zh 長度 < name_ja 長度 50% 的事件
suspect = [e for e in events if e.get("work_id") and e.get("name_zh") and e.get("name_ja")
and len(e["name_zh"]) < len(e["name_ja"]) * 0.5]
field_corrections:name_zh、name_en 均需 upsert,防止 re-annotation 覆寫。Reference incident: 2026-05-07 — 622f51c1 name_zh = "中村地平"(4字)vs name_ja(32字);根因為 annotator 把 works.title_zh 直接用作 name_zh,未翻譯完整活動標題。
在審核任何涉及 note_creators、note.com 等部落格/創作者聚合來源的計畫前,必須確認:
raw_description 通常只有「続きをみる」截斷文字:organizer 在此情況下必然為 null,絕不可從 note 發文者的個人簡介或背景推斷主辦方。is_active=false(非活動事件)。_HEADLINE_REWRITE_SOURCES 必須包含部落格來源:note_creators 的 raw_title 是文章標題,不是活動名稱,必須加入 _HEADLINE_REWRITE_SOURCES 讓 GPT 從 raw_description 重新生成正確的 name_ja。raw_title + raw_description,但文本極短(< 100 字)時 GPT 仍可能從外部知識推斷,guard 無法完全阻止。對此類事件,organizer 應保持 null。快速識別模式(需要 is_active=false 的文章):
NON_EVENT_TITLE_RE = re.compile(
r"(おすすめ|紹介|まとめ|行ってきた|読んでみた|観てきた|鑑賞レポ|映画紹介)",
re.IGNORECASE
)
# raw_title 命中 → 標為非活動文章
驗證指令(note_creators 事件 organizer 掃描):
SELECT id, name_ja, organizer, LEFT(raw_description, 100)
FROM events
WHERE source_name = 'note_creators'
AND organizer IS NOT NULL
AND is_active = true
LIMIT 20;
-- 所有 note_creators 事件的 organizer 應為 NULL
Reference incident: 2026-05-08 — 2cae572a/10a4ee5d organizer 被推斷為 埼玉県日台親善協会(note 發文者);4180ad0f/4ebc8a35 介紹文章/觀影報導入庫(commit b589fbb)。
在審核任何涉及 location_name 抽取邏輯的計畫,或分析展覽類事件 venue 識別錯誤的問題前,必須確認:
〇〇美術館蔵/〇〇博物館蔵 是作品所蔵機關標記,不是活動場地:GPT 容易將「高雄市立美術館蔵」中的「高雄市立美術館」提取為 location_name,這是錯誤的。location_name 應固定為「東京都写真美術館」:無論展品的所蔵機構來自哪個國家或城市,活動場地永遠是東京都写真美術館本身。location_name 有靜態預設值(如 yebizo scraper 應直接設定 location_name="東京都写真美術館")。raw_description 中出現「蔵」字緊接機構名,如 〇〇美術館蔵/〇〇博物館蔵/〇〇文化基金蔵,這些是所蔵標記,不是場地。驗證指令(展覽來源 location 掃描):
-- 確認 yebizo 事件 location 是否正確
SELECT id, name_ja, location_name, location_address
FROM events
WHERE source_name = 'yebizo'
AND location_name != '東京都写真美術館'
AND is_active = true
LIMIT 10;
-- 應為空結果
Reference incident: 2026-05-08 — e37db12e(yebizo)location_name='高雄市立美術館'(作品所蔵元),修正為「東京都写真美術館」(commit 47f8184)。
在審核任何涉及 performer 欄位的計畫,或分析 performer = NULL 案例時,必須確認 annotator.py 是否正確執行三層 fallback:
event.get("performer"))— 已有值時不覆蓋(含 field_corrections 保護)annotation.get("performer"))— SYSTEM_PROMPT 有 PERFORMER EXTRACTION RULES_extract_performer_from_raw(raw_title, raw_description))— GPT 失敗時的最後防線| Pattern | 範例 | Regex |
|---|---|---|
<role>・<name>氏を迎え | 料理研究家・宮武衣充氏を迎え | _PERFORMER_INTRO_RE |
<name>氏を迎え / <name>さんを迎え | 田中花子氏を迎え | _MUKAE_RE |
<name> |<role> | 前田知里|植物民族学研究家 | _PIPE_ROLE_RE |
<role>: <name> | 講師:田中花子 / ゲスト:田中花子 | _PERFORMER_INTRO_RE |
_PIPE_ROLE_RE 使用注意:<name> |<role> 格式常見於 Peatix の「主催者 = 主講人」型活動(例:里山文庫 前田知里 |植物民族学研究家)。Role suffix 限定 家/者/師/士/督 以防假陽性。
搜索範圍:raw_description 前 1500 字元(原為 500,2026-05-06 擴展)。事件 4427f965 的講師資訊在 pos 859,500 字元範圍不夠。
--backfill-performer 後,執行:
SELECT id, raw_title, performer FROM events
WHERE annotation_status='annotated'
AND performer IS NULL
AND (raw_title ILIKE '%氏を迎え%' OR raw_title ILIKE '%さんを迎え%'
OR raw_title ILIKE '%(講師|ゲスト|スピーカー)%');
LIMIT 20;
field_corrections 保護(正確方式;--id 重標注費時且 GPT 可能再次失敗)。"performer" 欄位;PERFORMER EXTRACTION RULES 段落必須在 ORGANIZER 段落前面。料理研究家・宮武衣充氏 — GPT 容易把整個字串當職稱描述,忘記提取人名[\u4e00-\u9fff]{2,5} 純漢字(上限 5),而非排除清單 [^\u3000\u30fb...]
{2,6} → 翻訳者一青窈(6 字)被誤識別為名字(role+name 連串){2,5} → 宇田川幸洋(5 字)仍可匹配,翻訳者一青窈 不匹配_MUKAE_RE 必須有 negative lookbehind (?<![\u4e00-\u9fff]):防止從職稱字串中間開始匹配。例:訳者一青窈 從 訳 開始匹配出 訳者一青窈(訳 前面是漢字 翻,lookbehind 阻擋)。_extract_performer_from_raw,人工確認所有命中をお迎え(帶 お)與 を迎え 是不同 pattern,需同時收錄Reference incidents:
e72b2c15 performer 三層 fallback 缺失;初版 regex 3 件假陽性(commits 562a620, 1ef6953, b2a8806)。4427f965(台湾植物紀行)前田知里|植物民族学研究家 未提取,三重根因:(1) 無 _PIPE_ROLE_RE;(2) 資訊在 pos 859 > 500 上限;(3) GPT 視主催者不為 guest(commit c82e746)。翻訳者一青窈 假陽性:INTRO {2,6} + MUKAE 無 lookbehind 導致 role+name 連串被誤匹配;修法:max 6→5 + lookbehind + 翻訳者 加入 role list(本 commit)。在審核任何手動合併(merger / admin)操作的計畫前,必須確認以下三件事全部完成:
is_active 必須為 False:合併後設定 merged_into_event_id 但忘記設 is_active=False 是最常見的遺漏步驟。合併後驗證指令:
SELECT id, is_active, merged_into_event_id
FROM events
WHERE merged_into_event_id IS NOT NULL
AND is_active = true;
-- 應為空結果;非空 = 資料不一致
director、release_year、cast_summary、description;合併後確認 work_id 正確連結。只做 event 合併但不更新 works 表,則 works 詳情頁缺少作品資訊。director/performer 欄位也需補充:不能只更新 works 表,events 自身的 director/performer 欄位也需同步設定(用於清單頁/卡片顯示)。is_active=True + merged_into_event_id IS NOT NULL 為已知資料不一致模式:Admin UI 的「⚠ 中繼節點」badge 是偵測工具,但根本防護是每次合併後立即執行上述驗證 SQL。
Reference incident: 2026-05-06 — b891cc5e 合併後 is_active 未更新為 False;ソウル・オブ・ソイル 合併後 works 表同時補充 director=顏蘭權、release_year=2024、cast_summary。
在審核任何 AdminEventTable 或類似 admin 表格中「一行引用另一行 ID」的 UI 計畫前(merged_into、parent_event_id 等),必須確認:
events props 建立,不能從篩選後的 displayEvents 建立:若 map 建立自 displayEvents,被篩選掉的事件 id 在 map 中為 undefined,行號顯示靜默消失(不報錯)。// globalIndexMap — 從完整 events prop 建立,不受 filter 影響
const globalIndexMap = useMemo(() =>
new Map(events.map((e, i) => [e.id, i + 1])),
[events]
);
// rowIndexMap — 從 displayEvents 建立,用於顯示當前篩選下的行號
const rowIndexMap = useMemo(() =>
new Map(displayEvents.map((e, i) => [e.id, i + 1])),
[displayEvents]
);
// merged_into 目標可能被篩選掉 → 優先用 globalIndexMap
const targetIdx = globalIndexMap.get(e.merged_into_event_id) ?? "?";
Map.get() 回傳 T | undefined;undefined 靜默渲染為空字串。這是靜默 UI 錯誤,只能靠人工觀察或 QA 發現。Reference incident: 2026-05-06 — AdminEventTable rowIndexMap 從 displayEvents 建立,merged_into 目標被篩選時行號消失(commits cb1bf83, 979725f)。
在審核任何 admin 表格欄寬設定,或修改 AdminEventTable.tsx Tailwind 寬度 class 的計畫前,必須確認:
w-[Npx] + min-w-[Npx]:只設 max-w-[Npx] 時,表格被其他欄擠壓後該欄仍會縮小(max-w 只設上限,無法防壓縮)。
// ✅ 固定寬度
<td className="w-[160px] min-w-[160px] ...">
// ❌ 可被壓縮
<td className="max-w-[160px] ...">
title_ja,不用 original_title:original_title 是原始語言片名(可能是中/英文),PostgreSQL ORDER BY ASC 將 null 值排末,導致大量 title_ja 有值的日文片名因 original_title=null 而沉底。後台以 title_ja 排序符合日文使用習慣。
.order("title_ja", { nullsFirst: false })
<a href="…" target="_blank">)也必須同步改為 <button> 觸發 modal;不可混用跳頁和 modal。Reference incident: 2026-05-06 — category 欄從 w-96 → max-w-[160px] → w-[160px] min-w-[160px];works 清單從 .order("original_title") 改為 .order("title_ja", { nullsFirst: false });dropdown「新增 work」從 <a> 改為 <button>。
.github/skills/agents/architect/history.md (newest at top).Architect 預設為 read-only(規劃 + 報告)。但在以下情況會直接編輯檔案:revert 操作、緊急修正、小幅文檔更新。直接編輯後必須走完以下其一,禁止留半成品:
絕不允許:編輯完直接呈現 commit hash 或「完成」字樣而沒明確指出 push 狀態。
呈現 git 狀態時,必須用以下三種標籤之一,禁止只給 hash:
<hash> → origin/main(已驗證 Vercel 部署或 push 成功 exit code 0)<hash> (local, not pushed)N files modified (working tree) 並列檔名裸 hash(如「commit cf1e0a9」)會讓用戶誤以為已推送,這是 anti-pattern。
刪除 i18n key、type union member、或任何被多處引用的 symbol 時,同一 commit 必須同時刪所有 caller:
grep_search 找出所有引用點。cd web && npx tsc --noEmit 確認 0 error 才 commit。反例:2026-05-01 撤銷 Tier 1 時刪掉 statsSlaHeader 等 i18n keys,但 stats/page.tsx 仍呼叫 t("statsSlaHeader"),導致工作樹半成品狀態(用戶察覺後手動修復)。
When planning any AEO (AI Engine Optimization) or SEO feature:
web/public/ file added (e.g. llms.txt, IndexNow key .txt, Google verification .html) must have a corresponding proxy.ts matcher exclusion step in the plan. Without it, next-intl i18n middleware 307-redirects the file to a locale path (e.g. /zh/google...html), making it unreachable by external services. Use google[0-9a-f]+\.html to cover all Google verification file formats.<dl>: Never plan "add FAQPage JSON-LD" without also planning "add matching visible <dl> section on the page". Google requires FAQ content to be visually present.ls supabase/migrations/ | sort | tail -5. Two migrations with the same number must use NNN and NNNb_ suffix.INDEXNOW_KEY and NEXT_PUBLIC_SITE_URL as required env vars in both GitHub Actions secrets and (if needed) Vercel.GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN), never service account JWT.access_denied. Plans that include OAuth token generation steps must note this prerequisite.When planning or reviewing changes to web/app/[locale]/events/[id]/opengraph-image.tsx:
N 值應設 ≥ 55(英文基準),而非 36(日文基準)。55 字 → 40px + 截斷至 53 字
After any plan that touches web/lib/types.ts Category union:
multi_replace_string_in_file oldString for union type changes must include ≥3 lines before and after the target member — insufficient context silently truncates adjacent union members (see: retail removed when drama added, commit f9e6b52)cd web && npx tsc --noEmit, confirming all prior union members still compileWhen adding a new optional field to the events table that also appears in the Admin UI, all 7 points must be in the same plan:
ALTER TABLE events ADD COLUMN IF NOT EXISTS <field> <type>; — must be executed in Supabase Dashboard SQL Editor before any Python client seed or upsert referencing the new field. (Error if skipped: PGRST204: Could not find the '<field>' column)scraper/sources/base.py: Add to Event dataclass as Optional[str] = None.web/lib/types.ts: Add to Event interface as field: type | null.AdminEventForm.tsx — two sub-steps:
EMPTY_FORM: add field: ""<input> or <textarea> elementAdminEditClient.tsx: add field: event.field ?? "" to form initialization.web/messages/*.json: add i18n key to all three files (zh, en, ja) simultaneously.web/app/[locale]/events/[id]/page.tsx): if the field is user-visible, add locale-aware rendering (e.g. conditional <a> for URL fields).Missing any point causes silent failures. Particularly:
Missing point 1 → PGRST204 at runtime, not at compile time.
Missing EMPTY_FORM or form init → field appears blank in admin even when DB has a value.
Missing i18n key → raw key string rendered in UI.
Vercel build failure from a TypeScript error does not take the site down — it serves the previous build silently. Regression is invisible to users until manually checked.
All 6 locations must be updated in the same commit (union, CATEGORIES, CATEGORY_GROUPS, zh/en/ja messages). See Engineer SKILL.md § Category Update Protocol for the full list.
sources/{name}/ — per-scraper platform profile(有 applyTo: scraper/sources/*.py)
agents/{name}/ — per-agent operational rules
top-level — workflow/tooling skills only(local-preview, cc-statusline, session-analytics)
任何新的 per-source skill 必須 放在 sources/ 子目錄下,不可直接放頂層
REVOKE ... ON TABLE <view_name> ..., not ON VIEW.GRANT ... ON ...REVOKE ... ON ...ALTER VIEW ... SET (...)SECURITY DEFINER RPC functions that gate admin access, do not rely only on request.jwt.claim.sub.auth.uid() as the primary identity source for real app requests, then fallback to claim only for SQL Editor simulation: coalesce(auth.uid(), v_sub::uuid).set search_path = pg_catalog, schema-qualify cross-schema objects (public.user_roles, auth.users).42501 (admin privileges required).request.jwt.claim.sub set to admin uid: PASSbackfill_categories.py and manually inspect every match before applying to DB.academic), trace which keyword triggered it and tighten the rule immediately.在設計任何使用 schedule: cron 觸發並需要根據「是哪個 cron 觸發的」來 dispatch 不同行為的 workflow 前,必須確認:
-eq 判斷:GitHub Actions cron 啟動有 1–2 小時(甚至更長)的延遲。if [ "$HOUR" -eq 21 ] 在實際執行時幾乎永遠不會匹配。-ge/-lt:每個 cron slot 之間間隔 6 小時,用視窗範圍覆蓋延遲。else fallthrough 必須是安全行為:若用 else 作 fallthrough,部署後要驗證每個分支是否都有被正確觸發,不能假設 else 只有在「預期外情況」才觸發。Reference incident: 2026-05-04 — researcher.yml 所有 4 個 cron 全部 fallthrough 到 else → slot3,slot3 費用 $2.62/週(正常應為 $0.67),slot0/1/2 幾乎未執行。修復:改用 6 小時視窗。
在設計或審核任何 scraper_runs 寫入邏輯前,必須確認:
success=False 必須搭配 notes:只寫 success=False 等於告訴你「失敗了,但不知道為什麼」。notes 欄位必須包含 f"{type(exc).__name__}: {exc}"[:500]。except Exception: pass 是正確的,避免 logging 失敗掩蓋原始錯誤。Reference incident: 2026-05-04 — eurospace 3 次失敗(4/28–4/29)notes 全為 NULL,無法從 DB 追溯原因。修復:main.py except 區塊新增 "notes" 欄位。
在週報或每日報告出現「持續 0 件」來源時,不要立即 dry-run 或修改 scraper,先依以下順序診斷:
last_nonzero = never ≠ 邏輯失效):
sb.table('scraper_runs').select('events_processed,ran_at')
.eq('source', src).order('ran_at').execute()
# 計算 max_events 和 last_nonzero_date
.env 無 key → CI 可能正常,確認 Actions secretPERSISTENT_ZERO_DAYS=30 觸發自動警告。給 doc string 加上活躍期標注(防止未來誤報):
# Active period: Oct–Nov (festival announcement); returns [] outside festival year
Reference incident: 2026-05-04 — 13 個 0 件來源全部屬於正常狀態,透過查歷史+分類診斷在 30 分鐘內確認無需修改任何 scraper。修復:daily_report.py 加入 30 天自動監控。
gpt-4o-mini and gpt-4o have no web browsing. Use gpt-4o-search-preview or a real search API for current data.在審核任何涉及 business_hours 欄位提取的 scraper 或 annotator 計畫前,必須確認:
_extract_hours_from_raw() 同時支援漢字時刻格式:HH 時MM 分(含空格)與 HH時MM分(無空格)。Taiwan Cultural Center(jp.taiwan.culture.tw)使用這種格式:開 演: 13 時30 分。_extract_hours_from_raw):
HH:MM〜HH:MM(冒號範圍)HH〜HH時(小時範圍)HH:MM開演/上映開始/開始 label 後的 HH 時MM 分 → HH:MM〜開場 label 後的 HH 時MM 分 → HH:MM〜HH 時MM 分(最低信心)business_hours 資訊通常出現在 raw_description 末尾的「詳細」區塊(最後 400 字元)。若 DB 的 raw_description 截斷,時刻資訊不會被提取。查詢時必須讀取完整欄位(勿用 [:200] 等截斷)。business_hours=null 的 taiwan_cultural_center 事件:annotator re-annotation 後會自動補填(透過 _pre_hours),無需人工 patch。人工修正後仍需 FC 鎖定。Reference incident: 2026-05-26 — event 16c3fa42(台湾映画上映会 北海道大学):raw_description 末尾有 開 演: 13 時30 分 但 business_hours=null,原因為 _extract_hours_from_raw 只支援 HH:MM 格式。修正:新增漢字時刻 regex patterns(commit 15d06e4),手動 patch business_hours=13:30〜 + FC 鎖定。
在審核任何使用 html.parser.HTMLParser 的 scraper PR 前,必須確認以下三點:
<script>、<style>、<nav>、<header>、<footer> 等噪音標籤必須在 handle_starttag/handle_endtag 中跳過。
_SKIP = frozenset({"script","style","nav","header","footer"}) + _skip 計數器def handle_starttag(self, tag, attrs):
if tag in _SKIP:
self._skip += 1