feat: L1–L3 tier completion (risk overview / decision workspace / governed action) - #17
Draft
ewiwi22255 wants to merge 7 commits into
Draft
ewiwi22255 wants to merge 7 commits into
ewiwi22255 wants to merge 7 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>
9 tasks
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.
摘要
L1(風險總覽)/L2(情報與決策)/L3(受治理行動)三個分層的功能完善,重點是把「情報 → 事件 → 應變 → 提案 → 核准」整條鏈接通並閉環。base
main@fcc2737,7 commits,25 files,+3201/−358,414 tests passed。三個 tier 都用 demo 帳號(viewer / planner / approver)在本機實機走過完整流程。已完成項目
L2 情報與決策(
51d731ed4fc8f5b9c755f)ERP_ENABLE_DEMO_SEED時替正式供應商各種一張進行中採購單event_type;新增update_risk_event(id)就地修改risk_ai_summaries落地;L2 重開自動載回;L1 唯讀顯示;排程路徑也落地(建議事件不再丟掉)L3 受治理行動(
39383dc)L1 風險總覽(
d0baf280bc642f)基礎設施(
407bf05)LLM_EXTRA_HEADERS(OpenCode Go 必帶x-opencode-session)、LLM_TIMEOUT(預設 120 秒,逾時換 fallback)scripts/check_llm_env.py --call一鍵檢查端點/金鑰/模型資料表/權限/設定變更(整合請留意)
risk_ai_summaries(AI 摘要落地)、risk_alert_states(L1 告警狀態)— 皆CREATE TABLE IF NOT EXISTS,也有 lazy createRISK_ALERT_ACK(tier L1_MONITOR)→ risk_viewer / supply_planner / procurement_approver / warehouse;只寫risk_alert_states,不碰 ERPupdate_po_impact(採購單延遲/替代建議)由ERP_POLICY_WRITE→RISK_WORKSPACE_WRITE。理由:這兩欄是給 L3 看的評估證據,不動採購單本體;L2 標證據、L3 在步驟 5 核准提案,與 CSV exchange 的 propose/approve 分工一致。授權測試已同步ERP_ENABLE_DEMO_SEED開啟且purchase_orders為空時,替每家正式供應商種一張進行中採購單(opt-in,只跑一次)HEATMAP_AI_SUMMARY_PROMPT_V2加「只依資料、不自行推測」;事件清單不再把 NaN 地區餵給模型LLM_ANALYSIS_MODEL=openai/glm-5.3-flash(新聞分類 15 分鐘 → 74 秒)尚未完成/已知問題
LLM_ANALYSIS_MODEL會套到所有分析任務(含熱圖摘要),這是既有設計測試
python -m pytest tests/ -q→ 414 passedtests/test_l2_risk_workspace.py(33)、tests/test_l3_proposal_closure.py(10)、tests/test_l1_alert_states.py(19)、tests/test_llm_extra_headers.py(6)、tests/test_l1_event_alerts.py(15)tests/test_supply_chain_authorization.py(update_po_impact分組)、tests/test_tier_authorization.py(viewer 多RISK_ALERT_ACK)🤖 Generated with Claude Code