diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 373441920..9c703a170 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -386,12 +386,12 @@ from src.services.dependency_upgrade_approval_package_template import ( from src.services.docker_build_surface_inventory import ( load_latest_docker_build_surface_inventory, ) -from src.services.gitea_authenticated_inventory_payload_validation import ( - validate_gitea_authenticated_inventory_payload, -) from src.services.gitea_actions_secret_variable_name_inventory import ( load_latest_gitea_actions_secret_variable_name_inventory, ) +from src.services.gitea_authenticated_inventory_payload_validation import ( + validate_gitea_authenticated_inventory_payload, +) from src.services.gitea_branch_protection_required_checks_readback import ( load_latest_gitea_branch_protection_required_checks_readback, ) @@ -461,6 +461,9 @@ from src.services.product_awoooi_manifest_standard import ( from src.services.product_code_review_gate import ( load_latest_product_code_review_gate, ) +from src.services.product_governance_matrix_readback import ( + load_latest_product_governance_matrix_readback, +) from src.services.public_redaction import redact_public_lan_topology from src.services.reboot_auto_recovery_drill_preflight import ( load_latest_reboot_auto_recovery_drill_preflight, @@ -1875,6 +1878,38 @@ async def get_gitea_private_inventory_p0_scorecard() -> dict[str, Any]: ) from exc +@router.get( + "/product-governance-matrix-readback", + response_model=dict[str, Any], + summary="取得全產品 product governance matrix 讀回", + description=( + "整合已提交的 Gitea private inventory 與 dev/prod repo truth," + "輸出每個產品的 canonical repo、main/dev branch、visibility、" + "owner readiness 與 backup evidence ref。此端點只讀 snapshot;" + "不呼叫 GitHub、不建立 repo、不改 visibility、不同步 refs、" + "不觸發 workflow、不讀 secret、不讀 raw session/SQLite。" + ), +) +async def get_product_governance_matrix_readback() -> dict[str, Any]: + """回傳全產品 Gitea source-truth governance matrix。""" + try: + payload = await asyncio.to_thread( + load_latest_product_governance_matrix_readback + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("product_governance_matrix_readback_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Product governance matrix readback 快照無效", + ) from exc + + @router.post( "/gitea-private-inventory-p0-scorecard/validate-authenticated-inventory-payload", response_model=dict[str, Any], diff --git a/apps/api/src/services/product_governance_matrix_readback.py b/apps/api/src/services/product_governance_matrix_readback.py new file mode 100644 index 000000000..0dc292702 --- /dev/null +++ b/apps/api/src/services/product_governance_matrix_readback.py @@ -0,0 +1,317 @@ +"""Product governance matrix readback. + +This service promotes committed Gitea source-truth snapshots into a product +governance matrix. It is read-only: no GitHub calls, no Gitea writes, no secret +reads, no workflow dispatch, and no runtime host action. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path +from typing import Any +from zoneinfo import ZoneInfo + +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) +from src.services.snapshot_paths import default_operations_dir + +SCHEMA_VERSION = "product_governance_matrix_readback_v1" +TZ_TAIPEI = ZoneInfo("Asia/Taipei") +_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) +_PRIVATE_INVENTORY_FILE = "awoooi-gitea-private-inventory-p0-scorecard.snapshot.json" +_DEV_PROD_REPO_READBACK_FILE = "gitea-all-product-dev-prod-repo-readback.snapshot.json" + + +def load_latest_product_governance_matrix_readback( + operations_dir: Path | None = None, + *, + repo_bundle_backup_readback: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Load the committed product source-truth matrix.""" + directory = operations_dir or _DEFAULT_OPERATIONS_DIR + private_inventory_path = directory / _PRIVATE_INVENTORY_FILE + dev_prod_path = directory / _DEV_PROD_REPO_READBACK_FILE + private_inventory_source = _load_json_object(private_inventory_path) + dev_prod_source = _load_json_object(dev_prod_path) + private_inventory = load_latest_gitea_private_inventory_p0_scorecard(directory) + return build_product_governance_matrix_readback( + private_inventory_source=private_inventory_source, + dev_prod_repo_readback=dev_prod_source, + private_inventory_readback=private_inventory, + repo_bundle_backup_readback=repo_bundle_backup_readback, + source_refs={ + "private_inventory": f"docs/operations/{private_inventory_path.name}", + "dev_prod_repo_readback": f"docs/operations/{dev_prod_path.name}", + "repo_bundle_backup_readback": ( + "GET /api/v1/agents/gitea-repo-bundle-backup-readback" + ), + }, + ) + + +def build_product_governance_matrix_readback( + *, + private_inventory_source: dict[str, Any], + dev_prod_repo_readback: dict[str, Any], + private_inventory_readback: dict[str, Any], + repo_bundle_backup_readback: dict[str, Any] | None = None, + generated_at: str | None = None, + source_refs: dict[str, str] | None = None, +) -> dict[str, Any]: + """Build a matrix from source-control truth and optional backup evidence.""" + refs = source_refs or {} + private_rollups = _dict(private_inventory_readback.get("rollups")) + dev_prod_summary = _dict(dev_prod_repo_readback.get("summary")) + product_rows = _list(private_inventory_source.get("product_rows")) + bundle_rows = _bundle_repo_index(repo_bundle_backup_readback) + bundle_summary = _dict((repo_bundle_backup_readback or {}).get("summary")) + bundle_ready = (repo_bundle_backup_readback or {}).get("ready") is True + + products = [ + _product_row( + row, + bundle_rows=bundle_rows, + bundle_ready=bundle_ready, + bundle_ref=refs.get( + "repo_bundle_backup_readback", + "GET /api/v1/agents/gitea-repo-bundle-backup-readback", + ), + ) + for row in product_rows + ] + ready_count = sum(1 for row in products if row["source_control_ready"]) + blocker_ids = sorted( + { + blocker + for row in products + for blocker in _strings(row.get("blockers")) + } + ) + all_rows_ready = len(products) > 0 and ready_count == len(products) + status_value = ( + "product_governance_matrix_ready" + if all_rows_ready and not blocker_ids + else "product_governance_matrix_action_required" + ) + + return { + "schema_version": SCHEMA_VERSION, + "generated_at": generated_at + or datetime.now(TZ_TAIPEI).isoformat(timespec="seconds"), + "status": status_value, + "scope": "product_governance_matrix", + "source_control_authority": "gitea", + "source_refs": { + "private_inventory": refs.get( + "private_inventory", + f"docs/operations/{_PRIVATE_INVENTORY_FILE}", + ), + "dev_prod_repo_readback": refs.get( + "dev_prod_repo_readback", + f"docs/operations/{_DEV_PROD_REPO_READBACK_FILE}", + ), + "repo_bundle_backup_readback": refs.get( + "repo_bundle_backup_readback", + "GET /api/v1/agents/gitea-repo-bundle-backup-readback", + ), + }, + "summary": { + "product_count": len(products), + "ready_product_count": ready_count, + "blocked_product_count": len(products) - ready_count, + "expected_product_count": _int( + private_rollups.get("expected_product_count") + or dev_prod_summary.get("expected_product_count") + ), + "canonical_repo_count": len( + {row["canonical_repo"] for row in products if row["canonical_repo"]} + ), + "ssh_verified_product_repo_count": _int( + private_rollups.get("ssh_verified_product_repo_count") + or dev_prod_summary.get("ssh_repo_present_count") + ), + "main_branch_present_product_repo_count": _int( + private_rollups.get("main_branch_present_product_repo_count") + or dev_prod_summary.get("main_branch_present_count") + ), + "dev_branch_present_product_repo_count": _int( + private_rollups.get("dev_branch_present_product_repo_count") + or dev_prod_summary.get("dev_branch_present_count") + ), + "dev_prod_environment_split_ready_count": _int( + private_rollups.get("dev_prod_environment_split_ready_count") + or dev_prod_summary.get("environment_split_ready_count") + ), + "public_visible_product_repo_count": _int( + private_rollups.get("public_visible_product_repo_count") + or dev_prod_summary.get("public_visible_count") + ), + "private_or_auth_only_product_repo_count": _int( + private_rollups.get("private_or_auth_only_product_repo_count") + or dev_prod_summary.get("private_or_auth_only_count") + ), + "tokenless_http_404_private_or_auth_product_repo_count": _int( + private_rollups.get( + "tokenless_http_404_private_or_auth_product_repo_count" + ) + or dev_prod_summary.get("tokenless_http_404_private_or_auth_count") + ), + "missing_product_repo_count": _int( + private_rollups.get("missing_product_repo_count") + or dev_prod_summary.get("missing_repo_count") + ), + "owner_readiness_row_present_count": sum( + 1 for row in products if row["owner_readiness_row_present"] + ), + "all_products_have_source_control_ready": all_rows_ready, + "all_active_product_repos_have_owner_readiness_row": ( + private_rollups.get( + "all_active_product_repos_have_gitea_owner_readiness_row" + ) + is True + ), + "all_expected_product_repos_have_dev_and_main": ( + private_rollups.get("all_expected_product_repos_have_dev_and_main") + is True + or dev_prod_summary.get("all_expected_product_repos_have_dev_and_main") + is True + ), + "backup_bundle_ready": bundle_ready, + "backup_bundle_expected_repo_count": _int( + bundle_summary.get("expected_repo_count") + ), + "backup_evidence_linked_product_count": sum( + 1 for row in products if row["backup_evidence_ref"] + ), + "backup_bundle_verified_product_count": sum( + 1 + for row in products + if row["backup_evidence_state"] == "bundle_backup_verified" + ), + "active_blocker_count": len(blocker_ids), + }, + "products": products, + "active_blockers": blocker_ids, + "next_action": ( + "keep_product_governance_matrix_as_source_truth_for_KM_RAG_MCP_LOG_wiring" + if status_value == "product_governance_matrix_ready" + else "repair_missing_product_source_truth_before_governance_rollout" + ), + "operation_boundaries": { + "read_only_api_allowed": True, + "github_api_allowed": False, + "github_cli_allowed": False, + "gitea_repo_write_allowed": False, + "gitea_refs_sync_allowed": False, + "gitea_visibility_change_allowed": False, + "workflow_dispatch_allowed": False, + "secret_value_collection_allowed": False, + "raw_session_or_sqlite_read_allowed": False, + }, + } + + +def _product_row( + row: dict[str, Any], + *, + bundle_rows: dict[str, dict[str, Any]], + bundle_ready: bool, + bundle_ref: str, +) -> dict[str, Any]: + repo = str(row.get("gitea_repo") or "") + backup_row = bundle_rows.get(repo) + blockers = _strings(row.get("blockers")) + source_control_ready = ( + row.get("ssh_refs_readable") is True + and row.get("prod_branch_present") is True + and row.get("dev_branch_present") is True + and row.get("environment_split_ready") is True + and row.get("owner_readiness_row_present") is True + and not blockers + ) + visibility = str(row.get("visibility_readback") or "unknown") + private_or_auth_only = visibility == "private_or_auth_only" + if backup_row: + backup_state = ( + "bundle_backup_verified" + if bundle_ready and backup_row.get("ok") is True + else "bundle_backup_readback_linked" + ) + else: + backup_state = "backup_evidence_ref_only" + return { + "product_id": str(row.get("product_id") or ""), + "status": "ready" if source_control_ready else "action_required", + "canonical_repo": repo, + "source_control_authority": "gitea", + "prod_branch": str(row.get("prod_environment_branch") or "main"), + "prod_sha_short": str(row.get("prod_branch_sha") or "")[:8], + "prod_branch_present": row.get("prod_branch_present") is True, + "dev_branch": str(row.get("dev_environment_branch") or "dev"), + "dev_sha_short": str(row.get("dev_branch_sha") or "")[:8], + "dev_branch_present": row.get("dev_branch_present") is True, + "ssh_refs_readable": row.get("ssh_refs_readable") is True, + "environment_split_ready": row.get("environment_split_ready") is True, + "owner_readiness_row_present": ( + row.get("owner_readiness_row_present") is True + ), + "visibility_readback": visibility, + "public_ui_visible": row.get("public_ui_visible") is True, + "private_or_auth_only": private_or_auth_only, + "tokenless_http_status": _int(row.get("tokenless_http_status")), + "tokenless_404_is_visibility_auth": ( + private_or_auth_only and _int(row.get("tokenless_http_status")) == 404 + ), + "backup_evidence_ref": f"{bundle_ref}#repo={repo}", + "backup_evidence_state": backup_state, + "source_control_ready": source_control_ready, + "blockers": blockers, + "next_gate": str(row.get("next_gate") or ""), + } + + +def _bundle_repo_index(payload: dict[str, Any] | None) -> dict[str, dict[str, Any]]: + rows = _list((payload or {}).get("repos")) + indexed: dict[str, dict[str, Any]] = {} + for row in rows: + repo = str(row.get("repo") or "") + if not repo: + continue + indexed[repo] = row + return indexed + + +def _load_json_object(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + return payload + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _list(value: Any) -> list[dict[str, Any]]: + return [row for row in value if isinstance(row, dict)] if isinstance(value, list) else [] + + +def _strings(value: Any) -> list[str]: + return [str(item) for item in value if str(item)] if isinstance(value, list) else [] + + +def _int(value: Any) -> int: + if isinstance(value, bool): + return int(value) + if isinstance(value, int | float): + return int(value) + if isinstance(value, str): + try: + return int(float(value)) + except ValueError: + return 0 + return 0 diff --git a/apps/api/tests/test_product_governance_matrix_readback_api.py b/apps/api/tests/test_product_governance_matrix_readback_api.py new file mode 100644 index 000000000..935f33677 --- /dev/null +++ b/apps/api/tests/test_product_governance_matrix_readback_api.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +# ruff: noqa: E402 +import json +import os +from pathlib import Path + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") + +from src.api.v1 import agents +from src.api.v1.agents import router +from src.services.gitea_repo_bundle_backup_readback import ( + build_gitea_repo_bundle_backup_readback, +) +from src.services.product_governance_matrix_readback import ( + build_product_governance_matrix_readback, + load_latest_product_governance_matrix_readback, +) + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_OPERATIONS_DIR = _REPO_ROOT / "docs" / "operations" +_PRIVATE_INVENTORY_SNAPSHOT = ( + _OPERATIONS_DIR / "awoooi-gitea-private-inventory-p0-scorecard.snapshot.json" +) +_DEV_PROD_SNAPSHOT = ( + _OPERATIONS_DIR / "gitea-all-product-dev-prod-repo-readback.snapshot.json" +) + + +def _sample(name: str, value: float, **labels: str) -> dict: + merged = {"host": "188", **labels} + return {"name": name, "labels": merged, "value": value} + + +def _repo_refs() -> list[str]: + data = json.loads(_DEV_PROD_SNAPSHOT.read_text(encoding="utf-8")) + return sorted(row["gitea_repo"] for row in data["products"]) + + +def _repo_bundle_payload() -> dict: + refs = _repo_refs() + samples = [ + _sample("awoooi_gitea_bundle_expected_repo_count", len(refs)), + _sample("awoooi_gitea_bundle_expected_repo_missing_count", 0), + _sample("awoooi_gitea_bundle_failed_repo_count", 0), + _sample("awoooi_gitea_bundle_checksum_missing_count", 0), + _sample("awoooi_gitea_bundle_age_seconds", 600), + _sample("awoooi_gitea_bundle_fresh", 1), + _sample("awoooi_gitea_bundle_all_expected_ok", 1), + _sample("awoooi_gitea_bundle_sample_restore_dry_run_ok", 1), + ] + for repo in refs: + samples.extend( + [ + _sample("awoooi_gitea_bundle_repo_present", 1, repo=repo), + _sample("awoooi_gitea_bundle_repo_ok", 1, repo=repo), + _sample("awoooi_gitea_bundle_repo_head_count", 2, repo=repo), + _sample( + "awoooi_gitea_bundle_checksum_digest_present", + 1, + repo=repo, + ), + ] + ) + return build_gitea_repo_bundle_backup_readback( + metric_samples=samples, + generated_at="2026-07-10T12:00:00+08:00", + dev_prod_repo_refs=refs, + dev_prod_repo_truth_ref=( + "docs/operations/gitea-all-product-dev-prod-repo-readback.snapshot.json" + ), + ) + + +def test_product_governance_matrix_loads_committed_gitea_source_truth() -> None: + payload = load_latest_product_governance_matrix_readback(_OPERATIONS_DIR) + + assert payload["schema_version"] == "product_governance_matrix_readback_v1" + assert payload["status"] == "product_governance_matrix_ready" + assert payload["source_control_authority"] == "gitea" + assert payload["summary"]["product_count"] == 12 + assert payload["summary"]["ready_product_count"] == 12 + assert payload["summary"]["canonical_repo_count"] == 12 + assert payload["summary"]["ssh_verified_product_repo_count"] == 12 + assert payload["summary"]["main_branch_present_product_repo_count"] == 12 + assert payload["summary"]["dev_branch_present_product_repo_count"] == 12 + assert payload["summary"]["private_or_auth_only_product_repo_count"] == 6 + assert payload["summary"]["backup_evidence_linked_product_count"] == 12 + assert payload["summary"]["backup_bundle_verified_product_count"] == 0 + assert payload["summary"]["active_blocker_count"] == 0 + assert payload["operation_boundaries"]["github_api_allowed"] is False + assert payload["operation_boundaries"]["secret_value_collection_allowed"] is False + + rows = {row["product_id"]: row for row in payload["products"]} + assert rows["awoooi"]["canonical_repo"] == "wooo/awoooi" + assert rows["awoooi"]["prod_branch"] == "main" + assert rows["awoooi"]["dev_branch"] == "dev" + assert rows["awoooi"]["source_control_ready"] is True + assert rows["awooogo"]["canonical_repo"] == "wooo/AwoooGo" + assert rows["awooogo"]["private_or_auth_only"] is True + assert rows["awooogo"]["tokenless_http_status"] == 404 + assert rows["awooogo"]["tokenless_404_is_visibility_auth"] is True + assert rows["awooogo"]["source_control_ready"] is True + assert rows["awooogo"]["backup_evidence_state"] == "backup_evidence_ref_only" + + +def test_product_governance_matrix_applies_bundle_backup_overlay() -> None: + payload = load_latest_product_governance_matrix_readback( + _OPERATIONS_DIR, + repo_bundle_backup_readback=_repo_bundle_payload(), + ) + + assert payload["summary"]["backup_bundle_ready"] is True + assert payload["summary"]["backup_bundle_expected_repo_count"] == 12 + assert payload["summary"]["backup_evidence_linked_product_count"] == 12 + assert payload["summary"]["backup_bundle_verified_product_count"] == 12 + assert all( + row["backup_evidence_state"] == "bundle_backup_verified" + for row in payload["products"] + ) + + +def test_product_governance_matrix_builder_blocks_missing_product_truth() -> None: + private_inventory_source = json.loads( + _PRIVATE_INVENTORY_SNAPSHOT.read_text(encoding="utf-8") + ) + dev_prod = json.loads(_DEV_PROD_SNAPSHOT.read_text(encoding="utf-8")) + private_readback = load_latest_product_governance_matrix_readback( + _OPERATIONS_DIR + ) + source_rows = private_inventory_source["product_rows"] + source_rows[0] = {**source_rows[0], "dev_branch_present": False} + + payload = build_product_governance_matrix_readback( + private_inventory_source=private_inventory_source, + dev_prod_repo_readback=dev_prod, + private_inventory_readback={ + "rollups": { + **private_readback["summary"], + "all_active_product_repos_have_gitea_owner_readiness_row": True, + "all_expected_product_repos_have_dev_and_main": False, + } + }, + generated_at="2026-07-10T12:30:00+08:00", + ) + + assert payload["status"] == "product_governance_matrix_action_required" + assert payload["summary"]["ready_product_count"] == 11 + assert payload["products"][0]["status"] == "action_required" + + +def test_product_governance_matrix_endpoint_returns_payload(monkeypatch) -> None: + expected = load_latest_product_governance_matrix_readback(_OPERATIONS_DIR) + monkeypatch.setattr( + agents, + "load_latest_product_governance_matrix_readback", + lambda: expected, + ) + + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/product-governance-matrix-readback") + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "product_governance_matrix_readback_v1" + assert data["summary"]["product_count"] == 12 + assert data["products"][0]["canonical_repo"].startswith("wooo/") diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a193ee3ee..17b2721bc 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9549,6 +9549,29 @@ "deferred": "Deferred" } }, + "productGovernance": { + "eyebrow": "Product Governance", + "title": "All-Product Source Truth Matrix", + "subtitle": "Turns committed Gitea inventory, dev/main branches, private visibility, and backup evidence refs into a product governance matrix for later KM/RAG/MCP/LOG classification.", + "status": "Status", + "authority": "Authority", + "ready": "ready", + "actionRequired": "action", + "prod": "prod", + "dev": "dev", + "privateAuth": "private/auth", + "publicVisible": "public", + "backupVerified": "backup verified", + "backupLinked": "backup linked", + "nextAction": "Next action", + "metrics": { + "products": "Products ready", + "ssh": "SSH refs", + "devMain": "dev/main", + "private": "private", + "backup": "backup evidence" + } + }, "commanderInsertedRequirements": { "eyebrow": "Mainline priority", "title": "Commander Inserted Requirement Work Items", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 37c766e21..87cf8869e 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9549,6 +9549,29 @@ "deferred": "延後" } }, + "productGovernance": { + "eyebrow": "Product Governance", + "title": "全產品 Source Truth Matrix", + "subtitle": "把 committed Gitea inventory、dev/main branch、private visibility 與 backup evidence ref 收成產品治理矩陣,後續 KM/RAG/MCP/LOG 都以此分類。", + "status": "狀態", + "authority": "Authority", + "ready": "ready", + "actionRequired": "action", + "prod": "prod", + "dev": "dev", + "privateAuth": "private/auth", + "publicVisible": "public", + "backupVerified": "backup verified", + "backupLinked": "backup linked", + "nextAction": "下一步", + "metrics": { + "products": "產品 ready", + "ssh": "SSH refs", + "devMain": "dev/main", + "private": "private", + "backup": "backup evidence" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 6e3d73b66..db4057011 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1469,6 +1469,61 @@ type SecurityCockpitRegistryReadback = { } | null; }; +type ProductGovernanceMatrixProduct = { + product_id?: string | null; + status?: string | null; + canonical_repo?: string | null; + prod_branch?: string | null; + prod_sha_short?: string | null; + dev_branch?: string | null; + dev_sha_short?: string | null; + ssh_refs_readable?: boolean | null; + environment_split_ready?: boolean | null; + owner_readiness_row_present?: boolean | null; + visibility_readback?: string | null; + public_ui_visible?: boolean | null; + private_or_auth_only?: boolean | null; + tokenless_http_status?: number | null; + tokenless_404_is_visibility_auth?: boolean | null; + backup_evidence_ref?: string | null; + backup_evidence_state?: string | null; + source_control_ready?: boolean | null; + blockers?: string[] | null; + next_gate?: string | null; +}; + +type ProductGovernanceMatrixResponse = { + schema_version?: string | null; + status?: string | null; + source_control_authority?: string | null; + source_refs?: Record | null; + summary?: { + product_count?: number | null; + ready_product_count?: number | null; + blocked_product_count?: number | null; + canonical_repo_count?: number | null; + ssh_verified_product_repo_count?: number | null; + main_branch_present_product_repo_count?: number | null; + dev_branch_present_product_repo_count?: number | null; + dev_prod_environment_split_ready_count?: number | null; + public_visible_product_repo_count?: number | null; + private_or_auth_only_product_repo_count?: number | null; + tokenless_http_404_private_or_auth_product_repo_count?: number | null; + owner_readiness_row_present_count?: number | null; + all_products_have_source_control_ready?: boolean | null; + all_active_product_repos_have_owner_readiness_row?: boolean | null; + all_expected_product_repos_have_dev_and_main?: boolean | null; + backup_bundle_ready?: boolean | null; + backup_bundle_expected_repo_count?: number | null; + backup_evidence_linked_product_count?: number | null; + backup_bundle_verified_product_count?: number | null; + active_blocker_count?: number | null; + } | null; + products?: ProductGovernanceMatrixProduct[] | null; + active_blockers?: string[] | null; + next_action?: string | null; +}; + const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = { summary: { active_p0_state: "blocked_reboot_auto_recovery_slo_not_ready", @@ -1824,6 +1879,7 @@ type Telemetry = { aiRouteStatus: AiRouteStatusResponse | null; reportSourceHealth: ReportSourceHealthResponse | null; priorityWorkOrder: PriorityWorkOrderResponse | null; + productGovernanceMatrix: ProductGovernanceMatrixResponse | null; }; type AutomationAssetLedgerKey = "km" | "playbook" | "script" | "schedule" | "verifier"; @@ -11276,6 +11332,181 @@ function MainlineWorkProgressStrip({ ); } +function ProductGovernanceMatrixPanel({ + matrix, + loading, +}: { + matrix: ProductGovernanceMatrixResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.productGovernance"); + const summary = matrix?.summary; + const products = (matrix?.products ?? []).slice(0, 6); + const total = toCount(summary?.product_count ?? products.length); + const ready = toCount(summary?.ready_product_count); + const sshVerified = toCount(summary?.ssh_verified_product_repo_count); + const devMainReady = toCount(summary?.dev_prod_environment_split_ready_count); + const privateRepos = toCount(summary?.private_or_auth_only_product_repo_count); + const backupLinked = toCount(summary?.backup_evidence_linked_product_count); + const activeBlockers = toCount(summary?.active_blocker_count); + const sourceReady = + summary?.all_products_have_source_control_ready === true || + (total > 0 && ready === total && activeBlockers === 0); + const metricCards = [ + { + key: "products", + label: t("metrics.products"), + value: loading ? "--" : `${ready}/${total}`, + icon: Database, + tone: sourceReady ? "border-[#bfdcc4] bg-[#f2fbf3]" : "border-[#ead5bd] bg-[#fff8ef]", + }, + { + key: "ssh", + label: t("metrics.ssh"), + value: loading ? "--" : sshVerified, + icon: GitBranch, + tone: "border-[#c9d8ea] bg-[#eef5ff]", + }, + { + key: "devmain", + label: t("metrics.devMain"), + value: loading ? "--" : devMainReady, + icon: GitPullRequest, + tone: "border-[#d8d3c7] bg-white", + }, + { + key: "private", + label: t("metrics.private"), + value: loading ? "--" : privateRepos, + icon: Lock, + tone: "border-[#e6d0d0] bg-[#fff6f6]", + }, + { + key: "backup", + label: t("metrics.backup"), + value: loading ? "--" : backupLinked, + icon: Archive, + tone: "border-[#d5d6ea] bg-[#f6f6ff]", + }, + ]; + + return ( +
+
+
+
+
+

+ {t("title")} +

+
+
+
+ {t("status")} +
+
+ {loading ? "--" : sourceReady ? t("ready") : t("actionRequired")} +
+
+
+
+ {t("authority")} +
+
+ {matrix?.source_control_authority ?? "gitea"} +
+
+
+

+ {t("subtitle")} +

+
+ +
+
+ {metricCards.map((metric) => { + const Icon = metric.icon; + return ( +
+
+ {metric.label} +
+
+ {metric.value} +
+
+ ); + })} +
+ +
+ {products.map((product) => { + const rowReady = product.source_control_ready === true; + return ( +
+
+
+ {product.product_id} +
+ + {rowReady ? t("ready") : t("actionRequired")} + +
+
+ {product.canonical_repo} +
+
+ + {t("prod")} {product.prod_branch}:{product.prod_sha_short} + + + {t("dev")} {product.dev_branch}:{product.dev_sha_short} + + + {product.private_or_auth_only ? t("privateAuth") : t("publicVisible")} + + + {product.backup_evidence_state === "bundle_backup_verified" + ? t("backupVerified") + : t("backupLinked")} + +
+
+ ); + })} +
+ +
+ {t("nextAction")}{" "} + {loading ? "--" : matrix?.next_action ?? "--"} +
+
+
+
+ ); +} + function CommanderInsertedRequirementsPanel({ priority, loading, @@ -11706,6 +11937,7 @@ export default function AwoooPWorkItemsPage() { aiRouteStatus: null, reportSourceHealth: null, priorityWorkOrder: null, + productGovernanceMatrix: null, }); const [loading, setLoading] = useState(true); const [priorityWorkOrderReadback, setPriorityWorkOrderReadback] = @@ -11786,6 +12018,8 @@ export default function AwoooPWorkItemsPage() { projectId ); const reportSourceHealthUrl = `${API_BASE}/api/v1/agents/agent-report-source-health`; + const productGovernanceMatrixUrl = + `${API_BASE}/api/v1/agents/product-governance-matrix-readback`; const priorityWorkOrderUrl = `${API_BASE}/api/v1/agents/awoooi-priority-work-order-readback`; const priorityWorkOrderPromise = fetchJson(priorityWorkOrderUrl, 12000); @@ -11819,6 +12053,7 @@ export default function AwoooPWorkItemsPage() { callbackReplies, aiRouteStatus, reportSourceHealth, + productGovernanceMatrix, priorityWorkOrder, ] = await Promise.all([ fetchJson(qualityUrl, 15000), @@ -11839,6 +12074,7 @@ export default function AwoooPWorkItemsPage() { fetchJson(callbackRepliesUrl, 12000), fetchJson(aiRouteStatusUrl, 12000), fetchJson(reportSourceHealthUrl, 12000), + fetchJson(productGovernanceMatrixUrl, 12000), priorityWorkOrderPromise, ]); @@ -11888,6 +12124,7 @@ export default function AwoooPWorkItemsPage() { incidentTimeline, aiRouteStatus, reportSourceHealth, + productGovernanceMatrix, priorityWorkOrder, }); setLastUpdated(new Date()); @@ -11991,6 +12228,11 @@ export default function AwoooPWorkItemsPage() { loading={priorityWorkOrderLoading && !priorityWorkOrder} /> + +