@@ -3548,6 +3611,7 @@ let activeGrowthCategoryFilter = '';
// ── 頁面載入 ────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadDashboard();
+ loadAutomationBenchmarkStrip();
loadGrowthOps();
bindActionDelegation();
});
@@ -3636,6 +3700,51 @@ async function loadDashboard() {
}
}
+async function loadAutomationBenchmarkStrip() {
+ try {
+ const response = await fetch('/api/ai/pchome-growth/ai-automation-readiness?include_receipt_replay=true&include_drift_verifier=true&limit=20');
+ const payload = await readJsonResponse(response);
+ const summary = payload.summary || {};
+ const receiptSummary = (payload.receipt_replay || {}).summary || {};
+ const driftSummary = (payload.drift_verifier || {}).summary || {};
+ const primaryGateCount = Number(summary.primary_human_gate_count || 0);
+ const selectorCount = Number(receiptSummary.selector_count || receiptSummary.target_selector_count || 0);
+ const replayPassCount = Number(receiptSummary.receipt_replay_pass_count || receiptSummary.post_apply_readback_pass_count || 0);
+ const driftCount = Number(driftSummary.drift_count || 0);
+ const nextAction = payload.next_machine_action || summary.next_machine_action || '持續回讀與漂移監控';
+
+ setText('aiBenchmarkLanded', selectorCount ? `${formatCount(selectorCount)} 筆已落地` : '閉環已接入');
+ setText('aiBenchmarkLandedDetail', primaryGateCount === 0 ? '主流程無人工阻斷,持續由 AI 受控回讀。' : `仍有 ${formatCount(primaryGateCount)} 個例外需 AI 收斂。`);
+ setText('aiBenchmarkVerified', replayPassCount ? `${formatCount(replayPassCount)} 筆已回讀` : '收據回讀就緒');
+ setText('aiBenchmarkVerifiedDetail', driftCount === 0 ? '目前沒有漂移,證據可追蹤。' : `${formatCount(driftCount)} 筆漂移,進入自動恢復建議。`);
+ setText('aiBenchmarkChange', driftCount === 0 ? '無異動待修復' : `${formatCount(driftCount)} 筆待修復`);
+ setText('aiBenchmarkChangeDetail', '異動只呈現營運狀態,詳細收據留在證據層。');
+ setText('aiBenchmarkNextAction', humanizeMachineAction(nextAction));
+ setText('aiBenchmarkNextActionDetail', '下一步優先顯示可自動執行、可回讀、可回滾的動作。');
+ } catch (error) {
+ setText('aiBenchmarkLanded', '等待回讀');
+ setText('aiBenchmarkVerified', '暫無狀態');
+ setText('aiBenchmarkChange', '讀取延遲');
+ setText('aiBenchmarkNextAction', '重新整理狀態');
+ setText('aiBenchmarkNextActionDetail', '目前未取得 AI 自動化摘要,頁面其餘作戰資料仍可使用。');
+ console.warn('AI automation benchmark strip failed', error);
+ }
+}
+
+function humanizeMachineAction(value) {
+ const text = String(value || '').trim();
+ if (!text) return '持續回讀與漂移監控';
+ return text
+ .replace(/_/g, ' ')
+ .replace(/\bNO OP\b/i, '維持監控')
+ .replace(/\bMONITOR\b/i, '持續監控')
+ .replace(/\bREPLAY\b/i, '重新回讀')
+ .replace(/\bDRIFT\b/i, '檢查異動')
+ .replace(/\bROLLBACK\b/i, '準備回滾證據')
+ .replace(/\bCONTROLLED APPLY\b/i, '受控落地')
+ .slice(0, 42);
+}
+
// ── KPI(high_risk_count 來自後端全量 CTE)─────────
function renderKPIs(stats) {
document.getElementById('kpiSkus').textContent = (stats.total_skus || 0).toLocaleString();
diff --git a/tests/test_ai_surface_benchmark_guardrails.py b/tests/test_ai_surface_benchmark_guardrails.py
new file mode 100644
index 0000000..32f0abb
--- /dev/null
+++ b/tests/test_ai_surface_benchmark_guardrails.py
@@ -0,0 +1,75 @@
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def _read(path: str) -> str:
+ return (ROOT / path).read_text(encoding="utf-8")
+
+
+def test_ai_intelligence_first_viewport_has_benchmark_golden_signals():
+ template = _read("templates/ai_intelligence.html")
+ strip = template.split('aria-label="AI 自動化狀態"', 1)[1].split(
+ '
',
+ 1,
+ )[0]
+
+ assert 'data-benchmark-guardrail="golden-signals"' in strip
+ assert 'data-signal="automated-landing"' in strip
+ assert 'data-signal="verified"' in strip
+ assert 'data-signal="change-state"' in strip
+ assert 'data-signal="next-machine-action"' in strip
+ assert "已自動落地" in strip
+ assert "已驗證" in strip
+ assert "異動狀態" in strip
+ assert "下一步" in strip
+ assert "維持監控" in strip
+ assert "先處理風險" in strip
+
+
+def test_benchmark_guardrail_surfaces_keep_evidence_on_demand_language():
+ combined = "\n".join(
+ [
+ _read("templates/ai_intelligence.html").split('aria-label="AI 自動化狀態"', 1)[1].split(
+ '
',
+ 1,
+ )[0],
+ ]
+ )
+
+ forbidden_visible_terms = [
+ "DB writes",
+ "Artifact",
+ "endpoint",
+ "JSON",
+ "table",
+ "manual review",
+ "human gate",
+ ]
+ for term in forbidden_visible_terms:
+ assert term not in combined