diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 2895b6d0..14bd0e41 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -1801,6 +1801,49 @@
"metadata": "runtime gate pointer; active gates=0"
}
}
+ },
+ "hostOwnerDecisionRecordDraftReview": {
+ "title": "Host Owner Decision Record Draft Review Checklist",
+ "subtitle": "Decision record draft packets still require read-only review. This only shows whether drafts have the metadata needed for human decision and does not create formal decision records.",
+ "checkLabel": "Draft review",
+ "guardLabel": "No upgrade",
+ "items": {
+ "scopeStatementComplete": {
+ "title": "Scope statement complete",
+ "body": "Confirm the scope draft includes host, network, service, exclusions, and observation intent so decision record scope is not ambiguous.",
+ "guard": "draft review only; record created=false"
+ },
+ "scanModeStillNotApproval": {
+ "title": "Scan mode still not approval",
+ "body": "Confirm scan mode remains a candidate description and is not read as active scan or credentialed scan authorization.",
+ "guard": "scan authorized=false"
+ },
+ "credentialBoundaryMetadataOnly": {
+ "title": "Credential boundary metadata only",
+ "body": "Confirm credential boundary stays metadata-only and does not request or store sensitive material.",
+ "guard": "secret collection=false"
+ },
+ "maintenanceConstraintsReadable": {
+ "title": "Maintenance constraints readable",
+ "body": "Confirm maintenance window, constraints, and impact boundary are readable without becoming host update approval.",
+ "guard": "host update=false"
+ },
+ "rollbackOwnerReadable": {
+ "title": "Rollback owner readable",
+ "body": "Confirm rollback owner, recovery path, and human contact are readable while no approval record is created.",
+ "guard": "approval record=false"
+ },
+ "validationMetricsLinked": {
+ "title": "Validation metrics linked",
+ "body": "Confirm post-check metrics, baseline, and evidence pointer are linked to the draft for later human review.",
+ "guard": "accepted=0"
+ },
+ "runtimeGateStillClosed": {
+ "title": "Runtime gate still closed",
+ "body": "Confirm decision record draft review does not open runtime gates. Later execution still requires a separate gate.",
+ "guard": "active runtime gates=0; action buttons=false"
+ }
+ }
}
},
"tickets": {
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 2b2406cd..68df91cd 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -1802,6 +1802,49 @@
"metadata": "runtime gate pointer;active gates=0"
}
}
+ },
+ "hostOwnerDecisionRecordDraftReview": {
+ "title": "主機 Owner Decision Record Draft Review Checklist",
+ "subtitle": "Decision record 草稿包後仍需只讀核對。這裡只顯示草稿是否具備人工決策所需 metadata,不會建立正式 decision record。",
+ "checkLabel": "Draft review",
+ "guardLabel": "不可升級",
+ "items": {
+ "scopeStatementComplete": {
+ "title": "Scope statement complete",
+ "body": "確認 scope 草稿已包含主機、網段、服務、排除範圍與觀察目的,避免決策紀錄範圍不清。",
+ "guard": "只核對草稿;record created=false"
+ },
+ "scanModeStillNotApproval": {
+ "title": "Scan mode still not approval",
+ "body": "確認 scan mode 仍只是候選描述,不被解讀成 active scan 或 credentialed scan 授權。",
+ "guard": "scan authorized=false"
+ },
+ "credentialBoundaryMetadataOnly": {
+ "title": "Credential boundary metadata only",
+ "body": "確認 credential boundary 仍維持 metadata-only,沒有要求或保存敏感素材。",
+ "guard": "secret collection=false"
+ },
+ "maintenanceConstraintsReadable": {
+ "title": "Maintenance constraints readable",
+ "body": "確認維護窗口、限制條件與不可影響範圍可讀,但不代表可以更新或調校主機。",
+ "guard": "host update=false"
+ },
+ "rollbackOwnerReadable": {
+ "title": "Rollback owner readable",
+ "body": "確認 rollback owner、復原路徑與人工聯絡點可讀,但尚未建立 approval record。",
+ "guard": "approval record=false"
+ },
+ "validationMetricsLinked": {
+ "title": "Validation metrics linked",
+ "body": "確認 post-check metrics、baseline 與 evidence pointer 已連到草稿,供後續人審使用。",
+ "guard": "accepted=0"
+ },
+ "runtimeGateStillClosed": {
+ "title": "Runtime gate still closed",
+ "body": "確認 decision record 草稿審查不會開 runtime gate,後續執行仍需獨立 gate。",
+ "guard": "active runtime gates=0;action buttons=false"
+ }
+ }
}
},
"tickets": {
diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx
index 4edbfbd0..8ef52ede 100644
--- a/apps/web/src/app/[locale]/iwooos/page.tsx
+++ b/apps/web/src/app/[locale]/iwooos/page.tsx
@@ -165,6 +165,13 @@ type HostOwnerDecisionRecordDraftPacket = {
tone: 'steady' | 'warn' | 'locked'
}
+type HostOwnerDecisionRecordDraftReviewItem = {
+ key: string
+ check: string
+ icon: typeof ShieldCheck
+ tone: 'steady' | 'warn' | 'locked'
+}
+
const postureMetrics: PostureMetric[] = [
{ key: 'overall', value: '58%', tone: 'warn' },
{ key: 'framework', value: '80-85%', tone: 'steady' },
@@ -386,6 +393,16 @@ const hostOwnerDecisionRecordDraftPackets: HostOwnerDecisionRecordDraftPacket[]
{ key: 'runtimeGateDraft', packet: 'DR7', icon: ShieldCheck, tone: 'locked' },
]
+const hostOwnerDecisionRecordDraftReviewItems: HostOwnerDecisionRecordDraftReviewItem[] = [
+ { key: 'scopeStatementComplete', check: 'RV1', icon: Radar, tone: 'warn' },
+ { key: 'scanModeStillNotApproval', check: 'RV2', icon: Activity, tone: 'locked' },
+ { key: 'credentialBoundaryMetadataOnly', check: 'RV3', icon: Lock, tone: 'locked' },
+ { key: 'maintenanceConstraintsReadable', check: 'RV4', icon: Clock3, tone: 'warn' },
+ { key: 'rollbackOwnerReadable', check: 'RV5', icon: FileWarning, tone: 'warn' },
+ { key: 'validationMetricsLinked', check: 'RV6', icon: CheckCircle2, tone: 'warn' },
+ { key: 'runtimeGateStillClosed', check: 'RV7', icon: ShieldCheck, tone: 'locked' },
+]
+
const evidenceItems = [
'iwooos-posture-projection.snapshot.json',
'security-rollout-policy.snapshot.json',
@@ -996,6 +1013,34 @@ function HostOwnerDecisionRecordDraftCard({ item }: { item: HostOwnerDecisionRec
)
}
+function HostOwnerDecisionRecordDraftReviewCard({ item }: { item: HostOwnerDecisionRecordDraftReviewItem }) {
+ const t = useTranslations('iwooos.hostOwnerDecisionRecordDraftReview')
+ const Icon = item.icon
+ return (
+
+
+
+
+ {t('checkLabel')}
+
+
{item.check}
+
+
+ {t(`items.${item.key}.title` as never)}
+
+
+ {t(`items.${item.key}.body` as never)}
+
+
+
{t('guardLabel')}
+
+ {t(`items.${item.key}.guard` as never)}
+
+
+
+ )
+}
+
export default function IwoooSPage({ params }: { params: { locale: string } }) {
const t = useTranslations('iwooos')
@@ -1328,6 +1373,26 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
+
+
+
{t('hostOwnerDecisionRecordDraftReview.title')}
+
+ {t('hostOwnerDecisionRecordDraftReview.subtitle')}
+
+
+
+ {hostOwnerDecisionRecordDraftReviewItems.map(item => (
+
+ ))}
+
+
+
None:
"s2_24_iwooos_host_owner_decision_review_checklist",
"s2_25_iwooos_host_owner_decision_review_outcome_lanes",
"s2_26_iwooos_host_owner_decision_record_draft_packets",
+ "s2_27_iwooos_host_owner_decision_record_draft_review_checklist",
]
assert_equal(
"progress_delta_ledger.delta_ids",
@@ -458,6 +459,15 @@ def validate(root: Path) -> None:
"host_decision_record_validation_metrics_draft_packet",
"host_decision_record_runtime_gate_draft_packet",
]
+ expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids = [
+ "host_decision_record_scope_statement_review_check",
+ "host_decision_record_scan_mode_review_check",
+ "host_decision_record_credential_boundary_review_check",
+ "host_decision_record_maintenance_constraints_review_check",
+ "host_decision_record_rollback_owner_review_check",
+ "host_decision_record_validation_metrics_review_check",
+ "host_decision_record_runtime_gate_review_check",
+ ]
assert_equal(
"iwooos_projection.summary.frontend_surface_coverage_group_count",
iwooos_projection["summary"]["frontend_surface_coverage_group_count"],
@@ -543,6 +553,11 @@ def validate(root: Path) -> None:
iwooos_projection["summary"]["host_owner_decision_record_draft_packet_count"],
len(expected_iwooos_host_owner_decision_record_draft_packet_ids),
)
+ assert_equal(
+ "iwooos_projection.summary.host_owner_decision_record_draft_review_checklist_item_count",
+ iwooos_projection["summary"]["host_owner_decision_record_draft_review_checklist_item_count"],
+ len(expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids),
+ )
iwooos_progress = iwooos_projection["progress"]
assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"])
assert_equal(
@@ -1562,6 +1577,86 @@ def validate(root: Path) -> None:
f"iwooos_projection.host_owner_decision_record_draft_packets.{item['packet_id']}.not_authorization",
item["not_authorization"],
)
+ iwooos_host_owner_decision_record_draft_review_checklist = iwooos_projection[
+ "host_owner_decision_record_draft_review_checklist_items"
+ ]
+ assert_equal(
+ "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.ids",
+ [item["check_id"] for item in iwooos_host_owner_decision_record_draft_review_checklist],
+ expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids,
+ )
+ assert_equal(
+ "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.display_order",
+ [item["display_order"] for item in iwooos_host_owner_decision_record_draft_review_checklist],
+ list(range(1, len(expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids) + 1)),
+ )
+ expected_iwooos_host_owner_decision_record_draft_review_conditions = [
+ "scope_statement_metadata_complete",
+ "scan_mode_not_authorization_confirmed",
+ "credential_boundary_metadata_only_confirmed",
+ "maintenance_constraints_no_change_confirmed",
+ "rollback_owner_recovery_pointer_readable",
+ "validation_metrics_baseline_linked",
+ "runtime_gate_separate_and_closed",
+ ]
+ assert_equal(
+ "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.review_conditions",
+ [item["review_condition"] for item in iwooos_host_owner_decision_record_draft_review_checklist],
+ expected_iwooos_host_owner_decision_record_draft_review_conditions,
+ )
+ for item in iwooos_host_owner_decision_record_draft_review_checklist:
+ assert_equal(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.display_mode",
+ item["display_mode"],
+ "owner_decision_record_draft_review_checklist_only",
+ )
+ assert_equal(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.decision_record_review_passed_count",
+ item["decision_record_review_passed_count"],
+ 0,
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.decision_record_created",
+ item["decision_record_created"],
+ )
+ assert_equal(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_decision_received_count",
+ item["owner_decision_received_count"],
+ 0,
+ )
+ assert_equal(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_decision_accepted_count",
+ item["owner_decision_accepted_count"],
+ 0,
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_approval_record_created",
+ item["owner_approval_record_created"],
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.runtime_gate_opened",
+ item["runtime_gate_opened"],
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.raw_payload_allowed",
+ item["raw_payload_allowed"],
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.secret_value_collection_allowed",
+ item["secret_value_collection_allowed"],
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.runtime_execution_authorized",
+ item["runtime_execution_authorized"],
+ )
+ assert_false(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.action_buttons_allowed",
+ item["action_buttons_allowed"],
+ )
+ assert_true(
+ f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.not_authorization",
+ item["not_authorization"],
+ )
assert_equal(
"iwooos_projection.non_blocking_lane_ids",
iwooos_projection["non_blocking_lane_ids"],
@@ -1597,6 +1692,7 @@ def validate(root: Path) -> None:
"display_host_owner_decision_review_checklist",
"display_host_owner_decision_review_outcome_lanes",
"display_host_owner_decision_record_draft_packets",
+ "display_host_owner_decision_record_draft_review_checklist",
"display_evidence_refs",
"display_forbidden_actions",
]:
@@ -1650,6 +1746,10 @@ def validate(root: Path) -> None:
"create_host_owner_decision_record_from_draft",
"mark_host_owner_decision_record_created",
"open_runtime_gate_from_owner_decision_record_draft",
+ "treat_host_owner_decision_record_draft_review_as_approval",
+ "mark_host_owner_decision_record_draft_review_passed",
+ "create_host_owner_decision_record_from_draft_review",
+ "open_runtime_gate_from_owner_decision_record_draft_review",
"apply_runtime_blocking_control",
"switch_github_primary",
"production_deploy",