diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ec12bc07..3c6853a6 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -10962,8 +10962,9 @@ } }, "firstProgressUnlockPath": { + "eyebrow": "61% 下一步", "title": "第一個進度解鎖路徑", - "subtitle": "S2.114 把 61% 下一個真正能往前的路徑收斂到 S4.9 負責人回覆:先收到可追溯回覆,再補齊脫敏證據參照,通過收件預檢與審查接受後,才可能成為 重點 審查 候選。", + "subtitle": "把 61% 下一個真正能往前的路徑放到首頁首層:先收 S4.9 負責人可追溯回覆,再補齊脫敏證據參照,通過收件預檢與審查接受後,才可能成為進度審查候選。", "stepLabel": "步驟", "boundaryTitle": "第一解鎖路徑邊界", "boundaryIntro": "以下鍵值固定:這是 S4.9 第一解鎖路徑的只讀收斂,不是送件完成、回覆已收到、批准、掃描、修復、部署或執行期入口。", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index ec12bc07..3c6853a6 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -10962,8 +10962,9 @@ } }, "firstProgressUnlockPath": { + "eyebrow": "61% 下一步", "title": "第一個進度解鎖路徑", - "subtitle": "S2.114 把 61% 下一個真正能往前的路徑收斂到 S4.9 負責人回覆:先收到可追溯回覆,再補齊脫敏證據參照,通過收件預檢與審查接受後,才可能成為 重點 審查 候選。", + "subtitle": "把 61% 下一個真正能往前的路徑放到首頁首層:先收 S4.9 負責人可追溯回覆,再補齊脫敏證據參照,通過收件預檢與審查接受後,才可能成為進度審查候選。", "stepLabel": "步驟", "boundaryTitle": "第一解鎖路徑邊界", "boundaryIntro": "以下鍵值固定:這是 S4.9 第一解鎖路徑的只讀收斂,不是送件完成、回覆已收到、批准、掃描、修復、部署或執行期入口。", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 56052365..e0afe792 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -8868,99 +8868,133 @@ function IwoooSFirstProgressUnlockPathBoard() { const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } return ( -
-
-
-
-
-

{t('title')}

-

- {t('subtitle')} -

+
+
+
+
+
+ + {t('eyebrow')}
-
- {summaryItems.map(item => ( -
-
- {t(`summary.${item.key}.label` as never)} - -
-
- {item.value} -
-

- {t(`summary.${item.key}.detail` as never)} -

-
- ))} -
-
- {iwooosFirstProgressUnlockPathSteps.map(item => { - const Icon = item.icon - return ( -
-
- - {t('stepLabel')} {item.step} - - -
-
- {item.state} -
-

- {t(`items.${item.key}.title` as never)} -

-

- {t(`items.${item.key}.body` as never)} -

-
- ) - })} -
-
-
-
- -

{t('boundaryTitle')}

-
-

- {t('boundaryIntro')} +

{t('title')}

+

+ {t('subtitle')}

-
- {iwooosFirstProgressUnlockPathBoundaries.map(item => ( - - {item} - - ))} -
+
+ +
+ {summaryItems.map(item => ( +
+
+ {t(`summary.${item.key}.label` as never)} + +
+
+ {item.value} +
+

+ {t(`summary.${item.key}.detail` as never)} +

+
+ ))}
+ +
+ {iwooosFirstProgressUnlockPathSteps.map(item => ( + + ))} +
+ +
+ {iwooosFirstProgressUnlockPathSteps.map(item => { + const Icon = item.icon + return ( +
+
+ + {t('stepLabel')} {item.step} + + +
+
+ {item.state} +
+

+ {t(`items.${item.key}.title` as never)} +

+

+ {t(`items.${item.key}.body` as never)} +

+
+ ) + })} +
+ +
+ + {t('boundaryTitle')} + +

+ {t('boundaryIntro')} +

+
+ {iwooosFirstProgressUnlockPathBoundaries.map(item => ( + + {item} + + ))} +
+
) @@ -13685,6 +13719,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
+ @@ -13956,8 +13991,6 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { - - diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 66d7602f..b8b97776 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,85 @@ +## 2026-05-31|IwoooS 第一解鎖路徑首層化 + +**背景**: + +- 使用者指出 `/zh-TW/iwooos` 仍像長文字頁,難以第一眼理解目前資安工作到底完成什麼、下一步為什麼卡在 61%、Kali 112 與所有主機 / 產品 / 工具是否真的納入。 +- 本輪不新增執行權限、不提高管控強度;只把真正能讓 61% 往前的第一個 evidence 路徑放到首層,讓使用者先看懂下一步,而不是翻到深層收合區。 + +**本次調整**: + +- `apps/web/src/app/[locale]/iwooos/page.tsx`: + - 將 `IwoooSFirstProgressUnlockPathBoard` 從深層「下一步與阻塞解除」區移到標題區正下方,並放在視覺指揮板之前。 + - 看板改成「指標卡 + 五段進度軌 + compact step cards」;詳細 boundary 鍵值改為預設收合,降低首頁文字壓力。 + - 保留只讀邊界:S4.9 負責人回覆已收到=0、已接受=0、headline review 未開、active runtime gate=0。 +- `apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`: + - 補上「61% 下一步」首層文案;`en.json` 維持繁中鏡像。 +- `docs/security/iwooos-posture-projection.snapshot.json`: + - 新增 `first_progress_unlock_path_steps` 五步驟與首層可見 / boundary 收合 / runtime gate 0 的只讀投影。 +- `docs/security/security-mirror-status-rollup.snapshot.json`: + - 新增 `s2_142_iwooos_first_unlock_path_first_layer`,明確記錄這是 framework_detail 增量,headline 不提升、runtime 不開閘。 +- `scripts/security/security-mirror-progress-guard.py`: + - 新增 guard:第一解鎖路徑只能渲染一次,必須出現在視覺指揮板與深層 section 之前,且 S4.9 / runtime / action buttons 仍不可被前端提升成授權。 + +**驗證**: + +```text +python3 -m json.tool apps/web/messages/zh-TW.json / en.json -> pass +python3 -m json.tool docs/security/iwooos-posture-projection.snapshot.json -> pass +python3 -m json.tool docs/security/security-mirror-status-rollup.snapshot.json -> pass +cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json -> pass +python3 scripts/security/security-mirror-progress-guard.py -> SECURITY_MIRROR_PROGRESS_GUARD_OK +python3 scripts/security/source-control-owner-response-guard.py -> SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK +git diff --check -> pass +pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-iwooos-unlock-path-20260531-r2.tsbuildinfo -> pass +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 pnpm --dir apps/web run build -> pass +``` + +**本機瀏覽器檢查**: + +```text +http://localhost:3021/zh-TW/iwooos?_v=unlock-path-local-r2 + +in-app browser desktop: + hasBoard=true + boardTop=245 + boardBeforeVisual=true + boardBeforeHost=true + boundaryOpen=false + hasS49=true + hasFirstUnlock=true + hasNoScanText=true + horizontalOverflow=false + +Playwright viewport checks: + desktop 1440x1000: + boardTop=245 + visualTop=626 + horizontalOverflow=false + screenshot=/tmp/iwooos-unlock-path-r2-desktop.png + mobile 390x844: + boardTop=449 + visualTop=1699 + horizontalOverflow=false + screenshot=/tmp/iwooos-unlock-path-r2-mobile.png +``` + +**進度邊界**: + +```text +overall_percent=61 +framework_detail += s2_142_iwooos_first_unlock_path_first_layer +owner_response_received_count=0 +owner_response_accepted_count=0 +runtime_execution_authorized=false +active_runtime_gate_count=0 +repo_creation_authorized=false +refs_sync_authorized=false +workflow_modification_authorized=false +github_primary_switch_authorized=false +kali_execution_authorized=false +host_mutation_authorized=false +``` + ## 2026-05-31|Alerts 告警入口接上 Incident 真相鏈 **背景**: diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index d4b51827..a6216ebc 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -41,6 +41,14 @@ "professional_security_experience_response_flow_step_count": 5, "concrete_work_snapshot_first_layer": true, "concrete_work_snapshot_workstream_count": 6, + "first_progress_unlock_path_first_layer": true, + "first_progress_unlock_path_default_visible": true, + "first_progress_unlock_path_above_visual_dashboard": true, + "first_progress_unlock_path_step_count": 5, + "first_progress_unlock_path_boundary_details_collapsed": true, + "first_progress_unlock_path_owner_response_received_count": 0, + "first_progress_unlock_path_owner_response_accepted_count": 0, + "first_progress_unlock_path_runtime_gate_count": 0, "long_form_sections_default_collapsed": true, "owner_response_validation_received_count": 0, "owner_response_validation_accepted_count": 0, @@ -120,6 +128,88 @@ "headline_status": "reviewed_after_awooop_read_only_production_landing_evidence", "not_authorization": true }, + "first_progress_unlock_path_steps": [ + { + "step_id": "owner_response_scope", + "display_order": 1, + "source_focus": "s4_9_owner_response", + "display_state": "waiting_owner_response", + "display_mode": "first_layer_progress_unlock_path", + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "redacted_evidence_pointer_count": 0, + "preflight_passed_count": 0, + "headline_review_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "not_authorization": true + }, + { + "step_id": "redacted_evidence_pointer", + "display_order": 2, + "source_focus": "s4_9_owner_response", + "display_state": "waiting_redacted_evidence_pointer", + "display_mode": "first_layer_progress_unlock_path", + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "redacted_evidence_pointer_count": 0, + "preflight_passed_count": 0, + "headline_review_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "not_authorization": true + }, + { + "step_id": "intake_preflight", + "display_order": 3, + "source_focus": "s4_9_owner_response", + "display_state": "waiting_intake_preflight", + "display_mode": "first_layer_progress_unlock_path", + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "redacted_evidence_pointer_count": 0, + "preflight_passed_count": 0, + "headline_review_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "not_authorization": true + }, + { + "step_id": "review_acceptance", + "display_order": 4, + "source_focus": "s4_9_owner_response", + "display_state": "waiting_reviewer_acceptance", + "display_mode": "first_layer_progress_unlock_path", + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "redacted_evidence_pointer_count": 0, + "preflight_passed_count": 0, + "headline_review_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "not_authorization": true + }, + { + "step_id": "headline_review_candidate", + "display_order": 5, + "source_focus": "s4_9_owner_response", + "display_state": "not_opened", + "display_mode": "first_layer_progress_unlock_path", + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "redacted_evidence_pointer_count": 0, + "preflight_passed_count": 0, + "headline_review_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "action_buttons_allowed": false, + "not_authorization": true + } + ], "posture_pillars": [ { "pillar_id": "exposure_posture", diff --git a/docs/security/security-mirror-status-rollup.snapshot.json b/docs/security/security-mirror-status-rollup.snapshot.json index a7210b89..13bda3d4 100644 --- a/docs/security/security-mirror-status-rollup.snapshot.json +++ b/docs/security/security-mirror-status-rollup.snapshot.json @@ -2173,6 +2173,18 @@ "runtime_delta": false, "execution_authorized": false, "not_authorization": true + }, + { + "delta_id": "s2_142_iwooos_first_unlock_path_first_layer", + "display_order": 171, + "completed_stage": "S2.142 IwoooS 第一解鎖路徑首層化", + "progress_axis": "framework_detail", + "headline_percent_delta": 0, + "framework_delta_visible": true, + "why_headline_unchanged": "IwoooS 只把第一個可讓 61% 真正往前的 S4.9 負責人回覆路徑提升到首頁首層,並放在視覺指揮板之前,同時把詳細 boundary 鍵值預設收合;iwooos_first_unlock_path_step_count=5、iwooos_first_unlock_path_current_focus=s4_9_owner_response、iwooos_first_unlock_path_owner_response_received_count=0、iwooos_first_unlock_path_owner_response_accepted_count=0、iwooos_first_unlock_path_above_visual_dashboard=true、iwooos_first_unlock_path_boundary_details_collapsed=true、runtime_execution_authorized=false、active_runtime_gate_count=0,不把首層顯示當 owner response 已收到、已接受、headline review、runtime gate、Kali 執行、主機變更、repo/refs/workflow/secret 操作、GitHub primary 切換、Gitea 停用或正式部署。", + "runtime_delta": false, + "execution_authorized": false, + "not_authorization": true } ], "next_safe_actions": [ diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 973cf318..92727f86 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -45,6 +45,17 @@ def assert_text_not_contains(label: str, text: str, forbidden: str) -> None: raise SystemExit(f"BLOCKED {label}: forbidden {forbidden!r}") +def assert_text_before(label: str, text: str, first: str, second: str) -> None: + first_index = text.find(first) + second_index = text.find(second) + if first_index == -1: + raise SystemExit(f"BLOCKED {label}: missing first marker {first!r}") + if second_index == -1: + raise SystemExit(f"BLOCKED {label}: missing second marker {second!r}") + if first_index >= second_index: + raise SystemExit(f"BLOCKED {label}: expected {first!r} before {second!r}") + + def collect_string_values(value: Any) -> list[str]: if isinstance(value, str): return [value] @@ -587,6 +598,7 @@ def validate(root: Path) -> None: "s2_139_iwooos_product_evidence_wiring_preflight_retry_review_candidate_preflight_recovery_ledger", "s2_140_iwooos_product_evidence_wiring_preflight_retry_review_candidate_preflight_recovery_retry_gates", "s2_141_iwooos_all_product_coverage_snapshot", + "s2_142_iwooos_first_unlock_path_first_layer", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -1411,6 +1423,42 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["all_product_coverage_snapshot_default_summary_mode"], "compact_first", ) + assert_true( + "iwooos_projection.summary.first_progress_unlock_path_first_layer", + iwooos_projection["summary"]["first_progress_unlock_path_first_layer"], + ) + assert_true( + "iwooos_projection.summary.first_progress_unlock_path_default_visible", + iwooos_projection["summary"]["first_progress_unlock_path_default_visible"], + ) + assert_true( + "iwooos_projection.summary.first_progress_unlock_path_above_visual_dashboard", + iwooos_projection["summary"]["first_progress_unlock_path_above_visual_dashboard"], + ) + assert_equal( + "iwooos_projection.summary.first_progress_unlock_path_step_count", + iwooos_projection["summary"]["first_progress_unlock_path_step_count"], + 5, + ) + assert_true( + "iwooos_projection.summary.first_progress_unlock_path_boundary_details_collapsed", + iwooos_projection["summary"]["first_progress_unlock_path_boundary_details_collapsed"], + ) + assert_equal( + "iwooos_projection.summary.first_progress_unlock_path_owner_response_received_count", + iwooos_projection["summary"]["first_progress_unlock_path_owner_response_received_count"], + 0, + ) + assert_equal( + "iwooos_projection.summary.first_progress_unlock_path_owner_response_accepted_count", + iwooos_projection["summary"]["first_progress_unlock_path_owner_response_accepted_count"], + 0, + ) + assert_equal( + "iwooos_projection.summary.first_progress_unlock_path_runtime_gate_count", + iwooos_projection["summary"]["first_progress_unlock_path_runtime_gate_count"], + 0, + ) assert_true( "iwooos_projection.summary.all_product_coverage_snapshot_detail_ledger_collapsed", iwooos_projection["summary"]["all_product_coverage_snapshot_detail_ledger_collapsed"], @@ -11602,6 +11650,88 @@ def validate(root: Path) -> None: iwooos_projection_page, "IwoooSFirstProgressUnlockPathBoard", ) + assert_equal( + "iwooos_page.first_progress_unlock_path_render_count", + iwooos_projection_page.count(""), + 1, + ) + assert_text_before( + "iwooos_page.first_progress_unlock_path_before_visual_dashboard", + iwooos_projection_page, + "", + "", + ) + assert_text_before( + "iwooos_page.first_progress_unlock_path_first_layer_order", + iwooos_projection_page, + "", + "", + ) + assert_text_before( + "iwooos_page.first_progress_unlock_path_before_deep_sections", + iwooos_projection_page, + "", + " None: list(web_messages_en["iwooos"].keys()), "firstProgressUnlockPath", ) - for key in ["title", "subtitle", "summary", "items", "stepLabel", "boundaryTitle", "boundaryIntro"]: + for key in ["eyebrow", "title", "subtitle", "summary", "items", "stepLabel", "boundaryTitle", "boundaryIntro"]: assert_contains( "web_messages.zh-TW.iwooos.firstProgressUnlockPath.keys", list(web_messages_zh["iwooos"]["firstProgressUnlockPath"].keys()),