diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 22720d2bc..c1bb49faf 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -7132,6 +7132,34 @@ "body": "六項預檢與人工接受目前都還是 0。" } }, + "metadataFieldsTitle": "待補欄位封套", + "metadataFieldsState": "已填", + "metadataFields": { + "ownerRoleTeam": { + "title": "負責人角色 / 團隊", + "body": "只收角色或團隊名稱,不收帳密或個人私密資料。" + }, + "decision": { + "title": "判定", + "body": "必須落在各路線允許的判定值,不等於批准執行。" + }, + "decisionReason": { + "title": "判定理由", + "body": "只收脫敏理由摘要,不保存原始回覆全文。" + }, + "affectedScope": { + "title": "受影響範圍", + "body": "標出 repo、namespace 或來源範圍,不觸發寫入。" + }, + "redactedEvidenceRefs": { + "title": "脫敏證據參照", + "body": "只能指向文件、snapshot 或已脫敏中繼資料。" + }, + "followupOwner": { + "title": "後續負責人", + "body": "只標示後續追蹤對象,不能開執行閘門。" + } + }, "nextGates": { "redactedOwnerResponse": { "title": "收到脫敏回覆", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 22720d2bc..c1bb49faf 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -7132,6 +7132,34 @@ "body": "六項預檢與人工接受目前都還是 0。" } }, + "metadataFieldsTitle": "待補欄位封套", + "metadataFieldsState": "已填", + "metadataFields": { + "ownerRoleTeam": { + "title": "負責人角色 / 團隊", + "body": "只收角色或團隊名稱,不收帳密或個人私密資料。" + }, + "decision": { + "title": "判定", + "body": "必須落在各路線允許的判定值,不等於批准執行。" + }, + "decisionReason": { + "title": "判定理由", + "body": "只收脫敏理由摘要,不保存原始回覆全文。" + }, + "affectedScope": { + "title": "受影響範圍", + "body": "標出 repo、namespace 或來源範圍,不觸發寫入。" + }, + "redactedEvidenceRefs": { + "title": "脫敏證據參照", + "body": "只能指向文件、snapshot 或已脫敏中繼資料。" + }, + "followupOwner": { + "title": "後續負責人", + "body": "只標示後續追蹤對象,不能開執行閘門。" + } + }, "nextGates": { "redactedOwnerResponse": { "title": "收到脫敏回覆", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index fa7ff2252..a8a6d8458 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -105,6 +105,14 @@ type S49OwnerResponseIntakeDeliveryItem = { tone: 'steady' | 'warn' | 'locked' } +type S49OwnerResponseMetadataIntakeField = { + key: string + code: string + state: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type FastProgressItem = { key: string value: string @@ -1143,6 +1151,15 @@ const s49OwnerResponseIntakeDeliveryItems: S49OwnerResponseIntakeDeliveryItem[] { key: 'reviewTrail', value: '0', icon: SearchCheck, tone: 'locked' }, ] +const s49OwnerResponseMetadataIntakeFields: S49OwnerResponseMetadataIntakeField[] = [ + { key: 'ownerRoleTeam', code: 'F1', state: '0', icon: Bell, tone: 'warn' }, + { key: 'decision', code: 'F2', state: '0', icon: ClipboardCheck, tone: 'warn' }, + { key: 'decisionReason', code: 'F3', state: '0', icon: FileText, tone: 'warn' }, + { key: 'affectedScope', code: 'F4', state: '0', icon: Radar, tone: 'warn' }, + { key: 'redactedEvidenceRefs', code: 'F5', state: '0', icon: SearchCheck, tone: 'locked' }, + { key: 'followupOwner', code: 'F6', state: '0', icon: ShieldCheck, tone: 'locked' }, +] + const s49OwnerResponseIntakeBoundaries = [ 's4_9_owner_response_intake_frontstage_lane_count=5', 's4_9_owner_response_intake_next_gate_count=3', @@ -1167,6 +1184,16 @@ const s49OwnerResponseIntakeBoundaries = [ 's4_9_owner_response_intake_handoff_queue_runtime_gate_count=0', 's4_9_owner_response_intake_handoff_queue_raw_payload_allowed=false', 's4_9_owner_response_intake_handoff_queue_action_buttons_allowed=false', + 's4_9_owner_response_metadata_intake_field_count=6', + 's4_9_owner_response_metadata_intake_required_count=6', + 's4_9_owner_response_metadata_intake_filled_count=0', + 's4_9_owner_response_metadata_intake_received_count=0', + 's4_9_owner_response_metadata_intake_accepted_count=0', + 's4_9_owner_response_metadata_intake_runtime_gate_count=0', + 's4_9_owner_response_metadata_intake_redacted_ref_required=true', + 's4_9_owner_response_metadata_intake_raw_payload_allowed=false', + 's4_9_owner_response_metadata_intake_secret_plaintext_allowed=false', + 's4_9_owner_response_metadata_intake_action_buttons_allowed=false', 's4_9_owner_response_intake_runtime_gate_opened=false', 's4_9_owner_response_intake_action_buttons_allowed=false', 'runtime_execution_authorized=false', @@ -11772,6 +11799,52 @@ function IwoooSS49OwnerResponseIntakeBoard() { ) })} +
+
+ {t('metadataFieldsTitle')} +
+
+ {s49OwnerResponseMetadataIntakeFields.map(item => { + const Icon = item.icon + return ( +
+
+ + {item.code} +
+
+ {t(`metadataFields.${item.key}.title` as never)} +
+
+ {t(`metadataFields.${item.key}.body` as never)} +
+
+ {t('metadataFieldsState')}: {item.state} +
+
+ ) + })} +
+
None: "iwooos_projection.summary.s4_9_owner_response_intake_handoff_queue_action_buttons_allowed", iwooos_projection["summary"]["s4_9_owner_response_intake_handoff_queue_action_buttons_allowed"], ) + assert_equal( + "iwooos_projection.summary.s4_9_owner_response_metadata_intake_field_count", + iwooos_projection["summary"]["s4_9_owner_response_metadata_intake_field_count"], + 6, + ) + assert_equal( + "iwooos_projection.summary.s4_9_owner_response_metadata_intake_required_count", + iwooos_projection["summary"]["s4_9_owner_response_metadata_intake_required_count"], + 6, + ) + for count_key in [ + "s4_9_owner_response_metadata_intake_filled_count", + "s4_9_owner_response_metadata_intake_received_count", + "s4_9_owner_response_metadata_intake_accepted_count", + "s4_9_owner_response_metadata_intake_runtime_gate_count", + ]: + assert_equal( + f"iwooos_projection.summary.{count_key}", + iwooos_projection["summary"][count_key], + 0, + ) + assert_true( + "iwooos_projection.summary.s4_9_owner_response_metadata_intake_redacted_ref_required", + iwooos_projection["summary"]["s4_9_owner_response_metadata_intake_redacted_ref_required"], + ) + for false_key in [ + "s4_9_owner_response_metadata_intake_raw_payload_allowed", + "s4_9_owner_response_metadata_intake_secret_plaintext_allowed", + "s4_9_owner_response_metadata_intake_action_buttons_allowed", + ]: + assert_false( + f"iwooos_projection.summary.{false_key}", + iwooos_projection["summary"][false_key], + ) assert_equal( "iwooos_projection.summary.all_product_coverage_snapshot_default_summary_mode", iwooos_projection["summary"]["all_product_coverage_snapshot_default_summary_mode"], @@ -14175,6 +14209,84 @@ def validate(root: Path) -> None: f"iwooos_projection.s4_9_owner_response_intake_delivery_cards.{item['card_id']}.not_authorization", item["not_authorization"], ) + expected_s4_9_metadata_intake_field_ids = [ + "ownerRoleTeam", + "decision", + "decisionReason", + "affectedScope", + "redactedEvidenceRefs", + "followupOwner", + ] + expected_s4_9_metadata_intake_source_fields = [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "evidence_refs", + "followup_owner", + ] + s4_9_owner_response_metadata_intake_fields = iwooos_projection[ + "s4_9_owner_response_metadata_intake_fields" + ] + assert_equal( + "iwooos_projection.s4_9_owner_response_metadata_intake_fields.ids", + [item["field_id"] for item in s4_9_owner_response_metadata_intake_fields], + expected_s4_9_metadata_intake_field_ids, + ) + assert_equal( + "iwooos_projection.s4_9_owner_response_metadata_intake_fields.display_order", + [item["display_order"] for item in s4_9_owner_response_metadata_intake_fields], + list(range(1, len(expected_s4_9_metadata_intake_field_ids) + 1)), + ) + assert_equal( + "iwooos_projection.s4_9_owner_response_metadata_intake_fields.source_required_fields", + [item["source_required_field"] for item in s4_9_owner_response_metadata_intake_fields], + expected_s4_9_metadata_intake_source_fields, + ) + for item in s4_9_owner_response_metadata_intake_fields: + assert_equal( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.display_mode", + item["display_mode"], + "first_screen_s4_9_owner_response_metadata_intake_field", + ) + assert_equal( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.field_status", + item["field_status"], + "waiting_owner_response_metadata", + ) + assert_true( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.required", + item["required"], + ) + for count_key in [ + "filled_count", + "received_response_count", + "accepted_response_count", + "runtime_gate_count", + ]: + assert_equal( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.{count_key}", + item[count_key], + 0, + ) + assert_true( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.redacted_reference_required", + item["redacted_reference_required"], + ) + for false_key in [ + "raw_payload_allowed", + "secret_plaintext_allowed", + "runtime_execution_authorized", + "action_buttons_allowed", + ]: + assert_false( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.{false_key}", + item[false_key], + ) + assert_true( + f"iwooos_projection.s4_9_owner_response_metadata_intake_fields.{item['field_id']}.not_authorization", + item["not_authorization"], + ) for text in [ 'data-testid="iwooos-first-screen-depth-map-board"', 'data-testid="iwooos-first-screen-depth-map-layers"', @@ -14422,6 +14534,8 @@ def validate(root: Path) -> None: 'data-testid="iwooos-s49-owner-response-intake-completion-rail"', 'data-testid="iwooos-s49-owner-response-intake-delivery-cards"', "iwooos-s49-owner-response-intake-delivery-card-${item.key}", + 'data-testid="iwooos-s49-owner-response-metadata-intake-fields"', + "iwooos-s49-owner-response-metadata-intake-field-${item.key}", 'data-testid="iwooos-s49-owner-response-intake-next-gates"', "iwooos-s49-owner-response-intake-next-gate-${item.key}", 'data-testid="iwooos-s49-owner-response-intake-lanes"', @@ -14431,10 +14545,13 @@ def validate(root: Path) -> None: "s49OwnerResponseIntakeNextGates", "s49OwnerResponseIntakeBlockerFocus", "s49OwnerResponseIntakeDeliveryItems", + "s49OwnerResponseMetadataIntakeFields", "s49OwnerResponseIntakeBoundaries", "s49OwnerResponseIntake", "blockerFocus", "deliveryItems", + "metadataFields", + "metadataFieldsTitle", "ownerDecision", "redactedEvidence", "reviewTrail", @@ -14443,9 +14560,20 @@ def validate(root: Path) -> None: "redactedOwnerResponse", "preflightChecks", "humanAcceptance", + "ownerRoleTeam", + "decisionReason", + "affectedScope", + "redactedEvidenceRefs", + "followupOwner", "G1", "G2", "G3", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", "received", "preflight", "acceptance", @@ -14521,6 +14649,16 @@ def validate(root: Path) -> None: "s4_9_owner_response_intake_handoff_queue_runtime_gate_count=0", "s4_9_owner_response_intake_handoff_queue_raw_payload_allowed=false", "s4_9_owner_response_intake_handoff_queue_action_buttons_allowed=false", + "s4_9_owner_response_metadata_intake_field_count=6", + "s4_9_owner_response_metadata_intake_required_count=6", + "s4_9_owner_response_metadata_intake_filled_count=0", + "s4_9_owner_response_metadata_intake_received_count=0", + "s4_9_owner_response_metadata_intake_accepted_count=0", + "s4_9_owner_response_metadata_intake_runtime_gate_count=0", + "s4_9_owner_response_metadata_intake_redacted_ref_required=true", + "s4_9_owner_response_metadata_intake_raw_payload_allowed=false", + "s4_9_owner_response_metadata_intake_secret_plaintext_allowed=false", + "s4_9_owner_response_metadata_intake_action_buttons_allowed=false", "s4_9_owner_response_intake_runtime_gate_opened=false", "s4_9_owner_response_intake_action_buttons_allowed=false", "runtime_execution_authorized=false", diff --git a/scripts/security/source-control-owner-response-guard.py b/scripts/security/source-control-owner-response-guard.py index d954b3a7d..acff9e764 100755 --- a/scripts/security/source-control-owner-response-guard.py +++ b/scripts/security/source-control-owner-response-guard.py @@ -101,6 +101,22 @@ LANES = [ "response-legacy-or-inaccessible-disposition", ], "expected_handoff_queue_required_field_counts": [6, 6, 6, 8, 7], + "expected_metadata_intake_field_ids": [ + "ownerRoleTeam", + "decision", + "decisionReason", + "affectedScope", + "redactedEvidenceRefs", + "followupOwner", + ], + "expected_metadata_intake_source_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "evidence_refs", + "followup_owner", + ], }, { "lane_id": "s4_10_github_target_owner_decision_response", @@ -846,6 +862,93 @@ def validate(root: Path) -> None: f"{lane['lane_id']}.{item['handoff_lane_id']}.still_forbidden.{forbidden_action}", forbidden_action in item["still_forbidden"], ) + expected_metadata_intake_field_ids = lane.get("expected_metadata_intake_field_ids") + if expected_metadata_intake_field_ids is not None: + metadata_envelope = snapshot["owner_response_metadata_intake_envelope"] + expected_metadata_intake_source_fields = lane["expected_metadata_intake_source_fields"] + assert_equal( + f"{lane['lane_id']}.owner_response_metadata_intake_field_count", + summary["owner_response_metadata_intake_field_count"], + len(expected_metadata_intake_field_ids), + ) + assert_equal( + f"{lane['lane_id']}.owner_response_metadata_intake_required_count", + summary["owner_response_metadata_intake_required_count"], + len(expected_metadata_intake_field_ids), + ) + for count_key in [ + "owner_response_metadata_intake_filled_count", + "owner_response_metadata_intake_received_count", + "owner_response_metadata_intake_accepted_count", + "owner_response_metadata_intake_runtime_gate_count", + ]: + assert_equal(f"{lane['lane_id']}.{count_key}", summary[count_key], 0) + assert_true( + f"{lane['lane_id']}.owner_response_metadata_intake_redacted_ref_required", + summary["owner_response_metadata_intake_redacted_ref_required"], + ) + for false_key in [ + "owner_response_metadata_intake_raw_payload_allowed", + "owner_response_metadata_intake_secret_plaintext_allowed", + "owner_response_metadata_intake_action_buttons_allowed", + ]: + assert_false(f"{lane['lane_id']}.{false_key}", summary[false_key]) + assert_equal( + f"{lane['lane_id']}.owner_response_metadata_intake_field_ids", + [item["field_id"] for item in metadata_envelope], + expected_metadata_intake_field_ids, + ) + assert_equal( + f"{lane['lane_id']}.owner_response_metadata_intake_display_order", + [item["display_order"] for item in metadata_envelope], + list(range(1, len(expected_metadata_intake_field_ids) + 1)), + ) + assert_equal( + f"{lane['lane_id']}.owner_response_metadata_intake_source_fields", + [item["source_required_field"] for item in metadata_envelope], + expected_metadata_intake_source_fields, + ) + for item in metadata_envelope: + assert_equal( + f"{lane['lane_id']}.{item['field_id']}.field_status", + item["field_status"], + "waiting_owner_response_metadata", + ) + assert_true(f"{lane['lane_id']}.{item['field_id']}.required", item["required"]) + for count_key in [ + "filled_count", + "received_response_count", + "accepted_response_count", + "runtime_gate_count", + ]: + assert_equal( + f"{lane['lane_id']}.{item['field_id']}.{count_key}", + item[count_key], + 0, + ) + assert_true( + f"{lane['lane_id']}.{item['field_id']}.redacted_reference_required", + item["redacted_reference_required"], + ) + for false_key in [ + "raw_payload_allowed", + "secret_plaintext_allowed", + "action_buttons_allowed", + "execution_authorized", + ]: + assert_false( + f"{lane['lane_id']}.{item['field_id']}.{false_key}", + item[false_key], + ) + assert_true(f"{lane['lane_id']}.{item['field_id']}.not_approval", item["not_approval"]) + for forbidden_action in [ + "mark_received_from_metadata_envelope", + "mark_accepted_from_metadata_envelope", + ]: + assert_true( + f"{lane['lane_id']}.{item['field_id']}.still_forbidden.{forbidden_action}", + forbidden_action in item["still_forbidden"], + ) expected_request_packet_id = lane.get("expected_request_packet_id") if expected_request_packet_id is not None: request_packet = snapshot["owner_response_request_packet"]