diff --git a/apps/web/src/components/layout/header.tsx b/apps/web/src/components/layout/header.tsx index 1bad8204e..a4046cf09 100644 --- a/apps/web/src/components/layout/header.tsx +++ b/apps/web/src/components/layout/header.tsx @@ -55,6 +55,8 @@ export function Header({ }, [locale, pathname]) const brandWidth = compact ? 64 : sidebarCollapsed ? 64 : 224 + const iwooosHref = `/${locale}/iwooos` + const iwooosActive = pathname === iwooosHref || pathname.startsWith(`${iwooosHref}/`) return (
)} + {compact && ( + + + )} + {/* Page title */} argparse.Namespace: + parser = argparse.ArgumentParser( + description="Summarize P0-003 using Gitea-only inventory evidence.", + ) + parser.add_argument( + "--gitea-inventory", + type=Path, + default=ROOT / "docs/security/gitea-repo-inventory.snapshot.json", + ) + parser.add_argument( + "--import-acceptance", + type=Path, + default=ROOT / "docs/security/gitea-authenticated-inventory-import-acceptance.snapshot.json", + ) + parser.add_argument( + "--coverage-attestation", + type=Path, + default=ROOT / "docs/security/gitea-inventory-coverage-attestation.snapshot.json", + ) + parser.add_argument( + "--remaining-products", + type=Path, + default=ROOT / "docs/operations/codex-gitea-remaining-products-readback.snapshot.json", + ) + parser.add_argument("--generated-at", help="Override generated_at for stable snapshots.") + parser.add_argument("--output", type=Path, help="Write JSON to this path.") + return parser.parse_args() + + +def load_json(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise SystemExit(f"json_not_object={path}") + return payload + + +def as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + +def as_int(value: Any, default: int = 0) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +def product_row(row: dict[str, Any], state: str) -> dict[str, Any]: + return { + "product_id": str(row.get("product_id", "")), + "state": state, + "status": str(row.get("status") or row.get("candidate_status") or "ready"), + "gitea_repo": str(row.get("gitea_repo", "")), + "remote_main_present": bool(row.get("remote_main_present", False)), + "remote_dev_present": bool(row.get("remote_dev_present", False)), + "next_gate": str(row.get("next_gate", "")), + "blockers": [str(item) for item in as_list(row.get("blockers"))], + "owner_readiness_row_present": bool(row.get("product_id")), + } + + +def build_product_rows(remaining: dict[str, Any]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for row in as_list(remaining.get("ready_products")): + if isinstance(row, dict): + rows.append(product_row(row, "ready")) + for row in as_list(remaining.get("blocked_products")): + if isinstance(row, dict): + rows.append(product_row(row, "blocked")) + return rows + + +def build_scorecard(args: argparse.Namespace) -> dict[str, Any]: + gitea_inventory = load_json(args.gitea_inventory) + import_acceptance = load_json(args.import_acceptance) + coverage_attestation = load_json(args.coverage_attestation) + remaining_products = load_json(args.remaining_products) + + rows = build_product_rows(remaining_products) + summary = remaining_products.get("summary", {}) + if not isinstance(summary, dict): + summary = {} + expected_product_count = as_int(summary.get("product_count"), len(rows)) + missing_row_count = max(expected_product_count - len(rows), 0) + + gitea_status = str(gitea_inventory.get("status", "unknown")) + visibility_scope = str(gitea_inventory.get("visibility_scope", "unknown")) + accepted_payload_count = as_int(import_acceptance.get("accepted_payload_count")) + received_attestation_count = as_int(coverage_attestation.get("received_attestation_count")) + + blockers: list[str] = [] + if gitea_status != "ok": + blockers.append("gitea_repo_inventory_status_not_ok") + if visibility_scope not in {"authenticated", "admin_export"}: + blockers.append("gitea_visibility_scope_public_only_or_unknown") + if accepted_payload_count < 1: + blockers.append("gitea_authenticated_inventory_payload_not_accepted") + if received_attestation_count < 1: + blockers.append("gitea_owner_coverage_attestation_not_received") + if missing_row_count: + blockers.append("active_product_readiness_rows_missing") + + return { + "schema_version": SCHEMA_VERSION, + "generated_at": args.generated_at + or datetime.now().astimezone().isoformat(timespec="seconds"), + "workplan_id": "P0-003", + "status": "ready_for_private_inventory_review" if not blockers else "blocked_waiting_gitea_authenticated_or_owner_export_inventory", + "source_control_authority": "gitea", + "private_inventory_source": "gitea", + "github_status": "stopped_retired_do_not_use", + "github_lane_excluded_from_p0_blocker_count": True, + "github_api_used": False, + "github_cli_used": False, + "github_primary_switch_authorized": False, + "gitea_inventory": { + "schema_version": gitea_inventory.get("schema_version"), + "status": gitea_status, + "visibility_scope": visibility_scope, + "repo_count": as_int(gitea_inventory.get("repo_count")), + "token_present": bool(gitea_inventory.get("token_present", False)), + "blocking_reason": str(gitea_inventory.get("blocking_reason", "")), + }, + "authenticated_import_acceptance": { + "schema_version": import_acceptance.get("schema_version"), + "status": str(import_acceptance.get("status", "unknown")), + "accepted_payload_count": accepted_payload_count, + "token_value_collection_allowed": bool( + import_acceptance.get("token_value_collection_allowed", False) + ), + "execution_authorized": bool(import_acceptance.get("execution_authorized", False)), + }, + "coverage_attestation": { + "schema_version": coverage_attestation.get("schema_version"), + "status": str(coverage_attestation.get("status", "unknown")), + "received_attestation_count": received_attestation_count, + "accepted_attestation_count": as_int( + coverage_attestation.get("accepted_attestation_count") + ), + "execution_authorized": bool(coverage_attestation.get("execution_authorized", False)), + }, + "product_row_coverage": { + "expected_product_count": expected_product_count, + "present_product_row_count": len(rows), + "missing_product_row_count": missing_row_count, + "ready_product_count": len([row for row in rows if row["state"] == "ready"]), + "blocked_product_count": len([row for row in rows if row["state"] == "blocked"]), + "internal_or_authenticated_inventory_required_count": as_int( + summary.get("internal_or_authenticated_inventory_required_count") + ), + "all_active_product_repos_have_gitea_owner_readiness_row": missing_row_count == 0 + and all(row["owner_readiness_row_present"] for row in rows), + }, + "product_rows": rows, + "active_blockers": blockers, + "exit_criteria": [ + "private_inventory_source=gitea", + "github_lane_excluded_from_p0_blocker_count=true", + "gitea_repo_inventory.status=ok", + "gitea_repo_inventory.visibility_scope in authenticated/admin_export", + "all_active_product_repos_have_gitea_owner_readiness_row=true", + ], + "safe_next_step": ( + "obtain_gitea_authenticated_or_admin_export_redacted_inventory_payload_" + "then_validate_import_acceptance_and_owner_attestation" + ), + } + + +def main() -> int: + args = parse_args() + scorecard = build_scorecard(args) + text = json.dumps(scorecard, ensure_ascii=False, indent=2) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text, encoding="utf-8") + else: + print(text, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py b/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py new file mode 100644 index 000000000..b872bbba7 --- /dev/null +++ b/scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "scripts" / "security" / "gitea-private-inventory-p0-scorecard.py" + + +def load_scorecard() -> dict: + result = subprocess.run( + [sys.executable, str(SCRIPT), "--generated-at", "2026-06-29T14:12:00+08:00"], + text=True, + capture_output=True, + check=True, + ) + return json.loads(result.stdout) + + +def test_scorecard_excludes_github_from_p0_blocker_math() -> None: + scorecard = load_scorecard() + + assert scorecard["schema_version"] == "awoooi_gitea_private_inventory_p0_scorecard_v1" + assert scorecard["workplan_id"] == "P0-003" + assert scorecard["source_control_authority"] == "gitea" + assert scorecard["private_inventory_source"] == "gitea" + assert scorecard["github_status"] == "stopped_retired_do_not_use" + assert scorecard["github_lane_excluded_from_p0_blocker_count"] is True + assert scorecard["github_api_used"] is False + assert scorecard["github_cli_used"] is False + assert scorecard["github_primary_switch_authorized"] is False + assert not any("github" in blocker.lower() for blocker in scorecard["active_blockers"]) + + +def test_scorecard_preserves_current_gitea_inventory_blocker() -> None: + scorecard = load_scorecard() + + assert scorecard["status"] == "blocked_waiting_gitea_authenticated_or_owner_export_inventory" + assert scorecard["gitea_inventory"]["status"] == "partial" + assert scorecard["gitea_inventory"]["visibility_scope"] == "public_only" + assert scorecard["gitea_inventory"]["repo_count"] == 2 + assert scorecard["authenticated_import_acceptance"]["accepted_payload_count"] == 0 + assert scorecard["coverage_attestation"]["received_attestation_count"] == 0 + assert "gitea_repo_inventory_status_not_ok" in scorecard["active_blockers"] + assert "gitea_authenticated_inventory_payload_not_accepted" in scorecard["active_blockers"] + + +def test_scorecard_keeps_all_active_product_rows_visible() -> None: + scorecard = load_scorecard() + coverage = scorecard["product_row_coverage"] + + assert coverage["expected_product_count"] == 11 + assert coverage["present_product_row_count"] == 11 + assert coverage["missing_product_row_count"] == 0 + assert coverage["ready_product_count"] == 3 + assert coverage["blocked_product_count"] == 8 + assert coverage["all_active_product_repos_have_gitea_owner_readiness_row"] is True + + product_ids = {row["product_id"] for row in scorecard["product_rows"]} + assert {"awoooi", "momo-pro", "awooogo", "bitan-pharmacy", "tsenyang-website"} <= product_ids