Skip to content

整合 PR #16 與 #17:統一風險資料驗證及 L1–L3 流程 - #21

Draft
wweyuiop1205-lgtm wants to merge 20 commits into
mainfrom
codex/integrate-pr16-pr17
Draft

wweyuiop1205-lgtm wants to merge 20 commits into
mainfrom
codex/integrate-pr16-pr17

Conversation

@wweyuiop1205-lgtm

@wweyuiop1205-lgtm wweyuiop1205-lgtm commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

本 PR 將 #16 的第一階段新聞/風險資料規則與 #17 的 L1、L2、L3 功能整合,修正兩者交界處的分析狀態、地區、證據、零值保存及沖銷問題。目標是讓告警、決策與審批讀到同一套有效資料,並在重整或寫入失敗時保持一致。

這是涵蓋 #16#17 的整合版 Draft PR。建議集中 review 本 PR,暫緩分別合併 #16#17;本 PR 合併後,再由維護者關閉被涵蓋的原 PR。 原 PR 目前未被修改或關閉;最終合併由組長決定,不啟用自動合併。

範圍與來源

來源 整合內容
#16 原文與 AI 分析分開保存;成功/失敗/待處理/舊資料未驗證狀態;新聞去重與失敗重試;可設定排程及跨程序鎖;共用地區匹配;0%/0 天與交易保存
#17 L1 告警、已讀/處理中、站內通知、採購對映與最新摘要;L2 分數依據、曝險金額、摘要保存、事件與採購註記、替代提案;L3 證據呈現及審批結果回寫
整合修補 各層統一使用有效分析資料;驗證事件新增與更新;空證據限制;摘要來源追溯;跨分區誤匹配修正;保留零值;交易回滾與並行沖銷保護

本次僅涵蓋這兩個 PR 及必要相容性修補,沒有開始原定第二、三批功能。

衝突取捨及重要行為

  • 依資料流解決重疊檔案,未整份採用任一方。risk_contract.py 統一有效新聞與事件驗證,L1/L2/L3 使用分析地區及摘要,不以原始欄位補值。
  • risk_intelligence.py 保存摘要來源及分析狀態。成功且相關的新聞可作為摘要證據;延遲未知不能當成 0 或 7 天,也不能建立新聞事件。無地區證據的更新/事件會被略過,失敗摘要不取代最近成功摘要。
  • 地區匹配使用共用 resolver,處理台灣/臺灣別名及國家、分區交集,避免跨分區誤命中。
  • 事件新增與更新都驗證來源、類型、地區與天數,保留不同事件類型不互相覆寫的設計。熱圖套用與摘要寫入共用交易,失敗整筆回滾。
  • planner 的採購註記權限不等於交易核准權限;提案審批仍由 approver 執行。沖銷另外要求 admin,使用原始審批、執行收據、SQLite 寫入鎖及唯一鍵防止重複異動。
  • 相容性改變: 舊新聞維持 legacy_unverified,不自動列入有效分析;沒有唯一執行收據的舊審批拒絕自動沖銷,需人工對帳。直接 rollback_inventorycancel_order 入口不能繞過審批綁定。

建議 review 順序

  1. backend/risk_contract.pynews_store.pyrisk_validation.py:狀態、去重、欄位驗證與舊資料升級。
  2. backend/region_matching.pyrisk_intelligence.pysupply_chain_risk.py:地區、證據、摘要、事件、熱圖交易。
  3. backend/l1_monitoring.pypurchase_proposals.pyfrontend/ 對應元件:L1 → L2 → L3、採購註記、權限與重整保存。
  4. backend/approval_reversal.pytool_gateway.pyinventory.pyorders.py:沖銷相容性、並行與回滾。
  5. tests/test_pr16_pr17_integration.py 及各階段測試;完整說明見 整合交付報告

驗證

  • Windows/Python 3.12.3 重新執行整合版完整套件:504 passed in 47.32s,測試時程式版本 933443c;後續只有報告更新。
  • pip checkgit diff --check 通過。
  • 回歸測試包含分析狀態、零/未知、來源重新驗證、分區隔離、摘要保存、熱圖回滾、空庫/合成舊庫升級及重複初始化。
  • L1 → L2 → L3 後端流程及 Streamlit AppTest 元件重整保存;planner 不可直接核准;兩個子程序同時沖銷只產生一次異動。
  • 明確觸發的單次排程:6 則固定新聞成功分析;相同工作鍵再次執行回傳 skipped
  • 隔離 Streamlit 啟動與健康檢查 HTTP 200。
  • 本整合 PR 的 GitHub Actions(Ubuntu/Python 3.11)通過。
  • 組員完成瀏覽器 L1 → L2 → L3 主要流程驗收。
  • 正式資料庫的隔離副本升級驗收,以及 legacy/舊審批相容性取捨確認。

本次固定新聞及模擬 LLM 不代表真實新聞/模型品質驗收。未呼叫付費 API、未發送真實通知、未啟用正式排程或部署;未驗證正式多組織與負載。資料庫、.env、API 金鑰、新聞快照和本機驗收輸出不隨 PR 提交。

組員如何重現

在此 PR 的 checkout/worktree 中,啟用自己的 Python 虛擬環境後執行:

python -m pip install -r requirements.txt -r requirements-dev.txt
python -m pytest tests/ -q
python scripts/run_isolated.py --scenario success --integration-demo --scheduler-once review-v1
python scripts/run_isolated.py --scenario success --integration-demo --port 8513

先執行單次排程載入 6 則固定新聞,再開啟 http://127.0.0.1:8513。登入測試帳號為 viewer/viewerplanner/plannerapprover/approver;沖銷另以 admin/admin 檢查。終端按 Ctrl+C 停止預覽。

啟動器明確設定 ERP_DB_PATH=<此工作樹>/.isolated/erp-batch1-success.db,使用固定新聞與模擬 LLM、封鎖外部網路並關閉背景排程。測試帳號僅供隔離 Demo,不用於正式部署。

基準與提交

送出前已 fetch 最新 main 與兩個 PR,版本未變;與 main 的 git merge-tree --write-tree 模擬合併沒有文字衝突。這不取代 CI、業務驗收與正式資料升級檢查;審查期間若上游更新,需更新整合分支後重新驗證。

ewiwi22255 and others added 20 commits September 12, 2026 21:50
The L1 "最新事件告警" block only listed the first five rows of
supply_chain_events ordered by id, so it stayed empty until an L2 user
manually registered an event and never surfaced overwritten events as
new. Alerts now come from get_latest_event_alerts(), which re-queries the
database on every rerun:

- confirmed: supply_chain_events inside the time window, ordered by
  created_at (add_risk_event overwrites in place, so id order was stale),
  joined to the originating supply_chain_news row for title/URL, with a
  severity derived from impact_days.
- candidates: AI-classified news with estimated_delay > 0 that no event
  references yet. These appear as soon as the scheduler or an L2 refresh
  writes news, before anyone clicks 登錄, and disappear once registered.

The feed requires RISK_OVERVIEW_READ and denies before any data read; the
overview renderer now receives the live actor from the page. No new
write paths, tables, or capabilities.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tible endpoints

OpenCode Go (/zen/go/v1) rejects requests without an x-opencode-session
header, and occasionally holds a request open for minutes without replying.
Neither could be handled from .env before.

- LLM_EXTRA_HEADERS (JSON object) is forwarded as litellm extra_headers on
  every call; malformed values are ignored with a warning instead of
  breaking module import.
- LLM_TIMEOUT (seconds, default 120, was litellm's 600) bounds each request
  so a hung upstream fails over to LLM_FALLBACK_MODELS quickly.
- scripts/check_llm_env.py prints the resolved endpoint settings, verifies
  LLM_MODEL exists on the endpoint's /models list, and with --call sends one
  completion through backend.llm_client to prove the key works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-gated

Six issues on the L2 supply-chain risk page, all visible on the demo data:

1. Heatmap defaults were flat: any matching event added +40, so every
   hotspot with an event sat at 60%. Scoring now weights the most severe
   event by impact days (30d→50, 14d→40, 7d→30, 1d→20), adds +5 per extra
   event (cap 15), halves events older than 30 days, and records a
   human-readable `risk_reason` per hotspot. Country-level events no longer
   light up every hotspot in the same region; only region-only events do.
   Lookback is 200 events by created_at instead of the last 20 ids.

2. "曝險金額" was always $0 because the demo DB has no purchase orders.
   `get_region_exposure(region_key)` returns open-PO count/amount plus
   supplier counts, the card explains "無未結採購單 ・ 供應商 N 家" instead
   of a misleading $0, and init_db seeds one open PO per official supplier
   when ERP_ENABLE_DEMO_SEED is on (opt-in, empty-table only).

3. Cards only recognised manual events, so news-registered hotspots stayed
   "待評估". `events_for_location` / `is_news_event` classify both; a
   hotspot with registered news shows "📰 已登錄 N 則情報" and a one-click
   "建立應變計畫(type・days)" pre-filled from the most severe news event.
   Events are queried once per render instead of once per card.

4. `add_risk_event` overwrote any earlier manual event for the same
   (country, region), silently losing history. The dedupe key now includes
   event_type; `update_risk_event(id, …)` edits a specific event, and the
   card's "更新應變建議" uses it instead of relying on overwrite.

5. The AI summary lived only in session_state. Every result is now stored
   in `risk_ai_summaries` (summary, updates, events, audit, counts, actor);
   L2 reloads the latest on page open, L1 shows it read-only through
   `l1_monitoring.get_latest_risk_summary` (RISK_OVERVIEW_READ), and the
   scheduler path persists too, so its suggested events are no longer
   dropped.

6. Nothing stopped the model from inventing "美國 戰爭 30 天". Updates and
   events are now gated against evidence built from the news rows'
   country/region/category/delay and the registered events: regions with
   no evidence are dropped, delay days are capped at 2× the evidence
   maximum (floor 7), types contradicting classified evidence become
   "其他", and types backed only by unclassified news are flagged for
   human review. The audit list is shown under the summary on L2 and L1.
   The prompt also tells the model to stay within the supplied data.

`get_heatmap_ai_summary` keeps its 3-tuple contract; new code uses
`analyze_heatmap_risk`, which returns the full structured result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The timestamp was taken before the call, so a summary that took two
minutes to generate was recorded as older than the usage log row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… input

Step 5 (替代採購提案) only lists purchase orders that carry an
estimated_delay_days or alternative_suggestion, but no screen wrote those
fields any more: the "受災採購清單 → AI 替代建議 → 寫回採購單" layer was gone
before the public snapshot, leaving get_impacted_pos /
get_ai_alternative_suggestions / update_po_impact as unused imports. In the
normal flow Step 5 was therefore always empty; only the day-1 seed script
could populate it.

Step 3 now opens with "🧾 0. 受影響採購單標記" for the selected event:
- lists open POs whose supplier sits in the event's region (get_impacted_pos,
  now also returning supplier_id, amount, status and raw delay/suggestion)
- "🤖 AI 評估延遲與替代來源" fills per-PO delay and a concrete re-sourcing
  suggestion; the table is editable and works without AI too (default delay
  = event impact days)
- "📌 標記 N 張" writes the reviewed values via update_po_impact, after which
  the PO appears in Step 5 with the reason pre-filled

update_po_impact moves from ERP_POLICY_WRITE to RISK_WORKSPACE_WRITE: the
two columns are assessment evidence for L3, not a change to the PO itself
(supplier, amount, status untouched), so L2 annotates and L3 still decides
in Step 5 — the same propose/approve split as the CSV exchange.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, idempotent reversal

Walking the approver path end to end showed the governed action worked but
was blind and open-ended: proposals never carried the risk event they were
made for (source_event_id was always NULL), the L3 page could not show why
a proposal existed, and after a decision nothing changed on L1/L2 — the
original PO line stayed in Step 5 as if nothing had happened.

- Step 5 binds the proposal to a risk event: the event selected in Step 3
  if any, otherwise the most severe formal event for the PO's supplier
  region. The form shows which event it will cite.
- list_impacted_purchase_options returns per-line proposal status
  (pending / approved / rejected / unsubmitted, approver, decided_at,
  proposed_po_id, rejection reason), derived from pending_approvals via the
  proposal operation id. Step 5 labels each line and explains an approved
  or rejected line before allowing another proposal.
- get_purchase_proposal_context (PROPOSAL_EVIDENCE_READ) gives the L3 page
  the bound event (type, region, delay, description, source news) and the
  affected PO's L2 annotation (status, amount, marked delay, suggestion);
  the approval card renders both above the immutable proposal evidence.
- proposal_status_summary_by_event returns only counts per event for L1.
- 沖銷 (compensating reversal) is now idempotent: get_reversal_record
  looks up a successful retry_approval log for the approval id; the button
  is replaced by "已沖銷於 …" and the handler re-checks before executing.
  (HANDOFF §3 #13)
- The heatmap summary prompt no longer leaks the literal "nan" for events
  without a region (glm actually reported "地區欄位為 nan").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…osal status

L1 was a stateless read: every refresh rebuilt the same alert list, nothing
recorded who had looked at what, the only way to map purchase orders to
events was uploading a CSV even though the ERP now holds open POs, and an
L1 viewer had no way to push a suspicious candidate to L2.

- New capability RISK_ALERT_ACK (tier L1_MONITOR) on every role that has the
  L1 overview. It writes only to risk_alert_states (kind, ref_id, status,
  note, who, when) — monitoring state, not ERP data — so the "L1 never
  writes ERP" rule still holds.
- Confirmed events: editable 處理狀態 (未讀/已讀/處理中) + 備註, saved with
  one button; the table also shows 替代提案 counts (核准/待審/拒絕) from L3.
- Candidates: tick 通知 L2 (or set 已讀); the L2 intelligence page opens with
  "📨 L1 轉來 N 則待確認情報" listing them with who/when/note. Registering the
  news as an event closes the notice and removes the candidate automatically.
- 告警與通知中心 defaults to 系統內未結採購單 (load_open_purchase_rows,
  RISK_OVERVIEW_READ) and maps them in memory exactly like the CSV path,
  which stays available as the second option.
- get_latest_event_alerts merges ack state and proposal counts into each
  item; get_alert_states / list_l1_notifications_for_l2 are read-only and
  fail closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants