整合 PR #16 與 #17:統一風險資料驗證及 L1–L3 流程 - #21
Draft
wweyuiop1205-lgtm wants to merge 20 commits into
Draft
wweyuiop1205-lgtm wants to merge 20 commits into
wweyuiop1205-lgtm wants to merge 20 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
本 PR 將 #16 的第一階段新聞/風險資料規則與 #17 的 L1、L2、L3 功能整合,修正兩者交界處的分析狀態、地區、證據、零值保存及沖銷問題。目標是讓告警、決策與審批讀到同一套有效資料,並在重整或寫入失敗時保持一致。
這是涵蓋 #16、#17 的整合版 Draft PR。建議集中 review 本 PR,暫緩分別合併 #16、#17;本 PR 合併後,再由維護者關閉被涵蓋的原 PR。 原 PR 目前未被修改或關閉;最終合併由組長決定,不啟用自動合併。
範圍與來源
本次僅涵蓋這兩個 PR 及必要相容性修補,沒有開始原定第二、三批功能。
衝突取捨及重要行為
risk_contract.py統一有效新聞與事件驗證,L1/L2/L3 使用分析地區及摘要,不以原始欄位補值。risk_intelligence.py保存摘要來源及分析狀態。成功且相關的新聞可作為摘要證據;延遲未知不能當成 0 或 7 天,也不能建立新聞事件。無地區證據的更新/事件會被略過,失敗摘要不取代最近成功摘要。legacy_unverified,不自動列入有效分析;沒有唯一執行收據的舊審批拒絕自動沖銷,需人工對帳。直接rollback_inventory/cancel_order入口不能繞過審批綁定。建議 review 順序
backend/risk_contract.py、news_store.py、risk_validation.py:狀態、去重、欄位驗證與舊資料升級。backend/region_matching.py、risk_intelligence.py、supply_chain_risk.py:地區、證據、摘要、事件、熱圖交易。backend/l1_monitoring.py、purchase_proposals.py與frontend/對應元件:L1 → L2 → L3、採購註記、權限與重整保存。backend/approval_reversal.py、tool_gateway.py、inventory.py、orders.py:沖銷相容性、並行與回滾。tests/test_pr16_pr17_integration.py及各階段測試;完整說明見 整合交付報告。驗證
933443c;後續只有報告更新。pip check與git diff --check通過。skipped。本次固定新聞及模擬 LLM 不代表真實新聞/模型品質驗收。未呼叫付費 API、未發送真實通知、未啟用正式排程或部署;未驗證正式多組織與負載。資料庫、
.env、API 金鑰、新聞快照和本機驗收輸出不隨 PR 提交。組員如何重現
在此 PR 的 checkout/worktree 中,啟用自己的 Python 虛擬環境後執行:
先執行單次排程載入 6 則固定新聞,再開啟 http://127.0.0.1:8513。登入測試帳號為
viewer/viewer、planner/planner、approver/approver;沖銷另以admin/admin檢查。終端按 Ctrl+C 停止預覽。啟動器明確設定
ERP_DB_PATH=<此工作樹>/.isolated/erp-batch1-success.db,使用固定新聞與模擬 LLM、封鎖外部網路並關閉背景排程。測試帳號僅供隔離 Demo,不用於正式部署。基準與提交
fcc2737ec1d3640fee2c2c6d5aacadc4de7d15147538a410b7d14e6f88e6f9ac98d789109e0145760bc642f9468a50567eb4f765534236cee155e8af2932e1a保留兩個 PR 的提交歷史並完成功能整合;253c01d加入回歸修補與測試。其餘後續提交整理報告。送出前已 fetch 最新 main 與兩個 PR,版本未變;與 main 的
git merge-tree --write-tree模擬合併沒有文字衝突。這不取代 CI、業務驗收與正式資料升級檢查;審查期間若上游更新,需更新整合分支後重新驗證。