diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 74505025..80ad5a15 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -358,6 +358,9 @@ from src.services.offsite_escrow_readiness_status import ( from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) +from src.services.p0_cicd_baseline_source_readiness import ( + load_latest_p0_cicd_baseline_source_readiness, +) from src.services.product_code_review_gate import ( load_latest_product_code_review_gate, ) @@ -964,6 +967,37 @@ async def get_delivery_closure_workbench() -> dict[str, Any]: ) from exc +@router.get( + "/p0-cicd-baseline-source-readiness", + response_model=dict[str, Any], + summary="取得 P0-004 CI/CD baseline source readiness", + description=( + "讀取已提交的 P0-004 dev/prod CI/CD baseline source readiness;" + "此端點只檢查 baseline / warning step / apply gate 的 committed source 是否存在。" + "它不修改 `.gitea/workflows`、不觸發 workflow、不建立 repo、不同步 refs、" + "不呼叫 GitHub、不讀 secret、不操作 host / K8s。" + ), +) +async def get_p0_cicd_baseline_source_readiness() -> dict[str, Any]: + """回傳 P0-004 CI/CD baseline source readiness 只讀快照。""" + try: + payload = await asyncio.to_thread( + load_latest_p0_cicd_baseline_source_readiness + ) + 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("p0_cicd_baseline_source_readiness_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P0-004 CI/CD baseline source readiness 快照無效", + ) from exc + + @router.get( "/github-target-private-backup-evidence-gate", response_model=dict[str, Any], diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index 450a370b..50bc771f 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -21,6 +21,9 @@ from src.services.backup_dr_readiness_matrix import ( from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) +from src.services.p0_cicd_baseline_source_readiness import ( + load_latest_p0_cicd_baseline_source_readiness, +) from src.services.runtime_surface_inventory import ( load_latest_runtime_surface_inventory, ) @@ -33,6 +36,7 @@ def load_delivery_closure_workbench() -> dict[str, Any]: status_cleanup = load_latest_awoooi_status_cleanup_dashboard() production_deploy = load_latest_awoooi_production_deploy_readback_blocker() github = _load_github_private_backup_evidence_gate() + cicd_baseline = load_latest_p0_cicd_baseline_source_readiness() gitea = load_latest_gitea_workflow_runner_health() runtime = load_latest_runtime_surface_inventory() backup = load_latest_backup_dr_readiness_matrix() @@ -40,6 +44,7 @@ def load_delivery_closure_workbench() -> dict[str, Any]: status_cleanup=status_cleanup, production_deploy=production_deploy, github=github, + cicd_baseline=cicd_baseline, gitea=gitea, runtime=runtime, backup=backup, @@ -51,6 +56,7 @@ def build_delivery_closure_workbench( status_cleanup: dict[str, Any], production_deploy: dict[str, Any], github: dict[str, Any], + cicd_baseline: dict[str, Any], gitea: dict[str, Any], runtime: dict[str, Any], backup: dict[str, Any], @@ -65,6 +71,8 @@ def build_delivery_closure_workbench( github_preflight.get("internal_governance_writeback") or github.get("internal_governance_writeback") ) + cicd_baseline_readback = _dict(cicd_baseline.get("readback")) + cicd_baseline_rollups = _dict(cicd_baseline.get("rollups")) production_deploy_readback = _dict(production_deploy.get("readback")) production_deploy_rollups = _dict(production_deploy.get("rollups")) gitea_status = _dict(gitea.get("program_status")) @@ -410,6 +418,51 @@ def build_delivery_closure_workbench( or _first_target_action(github.get("targets")) ), }, + { + "id": "cicd_baseline", + "source_id": "p0_cicd_baseline_source_readiness", + "completion_percent": _percent( + cicd_baseline_rollups.get("source_readiness_percent") + ), + "status": str(cicd_baseline.get("status") or "unknown"), + "blocker_count": _int( + cicd_baseline_rollups.get("missing_required_source_count") + ), + "metric": { + "kind": "source_readiness", + "workplan_id": str( + cicd_baseline_readback.get("workplan_id") or "P0-004" + ), + "required_source_count": _int( + cicd_baseline_rollups.get("required_source_count") + ), + "present_required_source_count": _int( + cicd_baseline_rollups.get("present_required_source_count") + ), + "missing_required_source_count": _int( + cicd_baseline_rollups.get("missing_required_source_count") + ), + "source_readiness_percent": _int( + cicd_baseline_rollups.get("source_readiness_percent") + ), + "blocked_source_ids": _strings( + cicd_baseline_rollups.get("blocked_source_ids") + ), + "workflow_modification_allowed": _dict( + cicd_baseline.get("operation_boundaries") + ).get("workflow_modification_allowed") + is True, + "workflow_trigger_allowed": _dict( + cicd_baseline.get("operation_boundaries") + ).get("workflow_trigger_allowed") + is True, + "safe_next_step": str( + cicd_baseline_readback.get("safe_next_step") or "" + ), + }, + "href": "/deployments", + "next_action": _first_string(cicd_baseline.get("next_actions")), + }, { "id": "gitea", "source_id": "gitea_ci_cd", @@ -521,6 +574,7 @@ def build_delivery_closure_workbench( _source_status("status_cleanup", status_cleanup), _source_status("production_deploy_readback", production_deploy), _source_status("github_private_backup", github), + _source_status("p0_cicd_baseline_source_readiness", cicd_baseline), _source_status("gitea_ci_cd", gitea), _source_status("runtime_surface", runtime), _source_status("backup_dr", backup), @@ -569,6 +623,28 @@ def build_delivery_closure_workbench( "workflow_trigger_authorized" ) is True, + "p0_cicd_baseline_status": str(cicd_baseline.get("status") or ""), + "p0_cicd_baseline_workplan_id": str( + cicd_baseline_readback.get("workplan_id") or "" + ), + "p0_cicd_baseline_source_readiness_percent": _int( + cicd_baseline_rollups.get("source_readiness_percent") + ), + "p0_cicd_baseline_required_source_count": _int( + cicd_baseline_rollups.get("required_source_count") + ), + "p0_cicd_baseline_present_required_source_count": _int( + cicd_baseline_rollups.get("present_required_source_count") + ), + "p0_cicd_baseline_missing_required_source_count": _int( + cicd_baseline_rollups.get("missing_required_source_count") + ), + "p0_cicd_baseline_blocked_source_ids": _strings( + cicd_baseline_rollups.get("blocked_source_ids") + ), + "p0_cicd_baseline_safe_next_step": str( + cicd_baseline_readback.get("safe_next_step") or "" + ), "production_deploy_status": str(production_deploy.get("status") or ""), "production_deploy_source_control_main_ready": production_deploy_rollups.get( "source_control_main_ready" diff --git a/apps/api/src/services/p0_cicd_baseline_source_readiness.py b/apps/api/src/services/p0_cicd_baseline_source_readiness.py new file mode 100644 index 00000000..fc7f4542 --- /dev/null +++ b/apps/api/src/services/p0_cicd_baseline_source_readiness.py @@ -0,0 +1,133 @@ +"""P0-004 CI/CD baseline source readiness. + +Loads the committed source-readiness snapshot for the dev/prod CI/CD baseline. +This is read-only: it checks committed source paths and does not modify +workflows, trigger Gitea, create repos, sync refs, read secrets, or touch hosts. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_operations_dir, resolve_repo_root + +_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) +_SNAPSHOT_FILE = "p0-cicd-baseline-source-readiness.snapshot.json" +_SCHEMA_VERSION = "p0_cicd_baseline_source_readiness_v1" + + +def load_latest_p0_cicd_baseline_source_readiness( + operations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the committed P0-004 CI/CD baseline source readiness snapshot.""" + directory = operations_dir or _DEFAULT_OPERATIONS_DIR + path = directory / _SNAPSHOT_FILE + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + repo_root = resolve_repo_root(path) + _enrich_source_presence(payload, repo_root) + _require_schema(payload, str(path)) + _require_operation_boundaries(payload, str(path)) + _require_rollup_consistency(payload, str(path)) + return payload + + +def _enrich_source_presence(payload: dict[str, Any], repo_root: Path) -> None: + sources = _list(payload.get("required_sources")) + required_sources = [source for source in sources if _dict(source).get("required") is True] + present_ids: list[str] = [] + missing_ids: list[str] = [] + for source in required_sources: + item = _dict(source) + relative_path = str(item.get("path") or "") + present = bool(relative_path) and (repo_root / relative_path).is_file() + item["present"] = present + if present: + present_ids.append(str(item.get("id") or relative_path)) + else: + missing_ids.append(str(item.get("id") or relative_path)) + + required_count = len(required_sources) + present_count = len(present_ids) + missing_count = len(missing_ids) + rollups = _dict(payload.get("rollups")) + rollups["required_source_count"] = required_count + rollups["present_required_source_count"] = present_count + rollups["missing_required_source_count"] = missing_count + rollups["source_readiness_percent"] = round( + present_count / max(required_count, 1) * 100 + ) + rollups["blocked_source_ids"] = missing_ids + rollups["hard_blocker_count"] = len(_list(payload.get("blockers"))) + rollups["next_action_count"] = len(_list(payload.get("next_actions"))) + payload["status"] = ( + "ready_for_template_copy_apply_gate" + if missing_count == 0 + else "blocked_required_sources_missing" + ) + + +def _require_schema(payload: dict[str, Any], label: str) -> None: + actual = payload.get("schema_version") + if actual != _SCHEMA_VERSION: + raise ValueError( + f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}" + ) + + +def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = _dict(payload.get("operation_boundaries")) + if boundaries.get("read_only_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + blocked_flags = { + "workflow_modification_allowed", + "workflow_trigger_allowed", + "repo_creation_allowed", + "refs_sync_allowed", + "github_api_allowed", + "host_or_k8s_write_allowed", + "secret_read_allowed", + "raw_session_or_sqlite_read_allowed", + } + allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: operation boundaries must remain false: {allowed}") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + sources = [ + source + for source in _list(payload.get("required_sources")) + if _dict(source).get("required") is True + ] + blockers = _list(payload.get("blockers")) + next_actions = _list(payload.get("next_actions")) + rollups = _dict(payload.get("rollups")) + missing_ids = [ + str(_dict(source).get("id") or _dict(source).get("path") or "") + for source in sources + if _dict(source).get("present") is not True + ] + if rollups.get("required_source_count") != len(sources): + raise ValueError(f"{label}: required_source_count must match sources") + if rollups.get("missing_required_source_count") != len(missing_ids): + raise ValueError(f"{label}: missing_required_source_count mismatch") + if rollups.get("blocked_source_ids") != missing_ids: + raise ValueError(f"{label}: blocked_source_ids must match missing sources") + if rollups.get("hard_blocker_count") != len(blockers): + raise ValueError(f"{label}: hard_blocker_count must match blockers") + if rollups.get("next_action_count") != len(next_actions): + raise ValueError(f"{label}: next_action_count must match next_actions") + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index 1c02977b..c1c32f45 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -19,14 +19,29 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert response.status_code == 200 data = response.json() assert data["schema_version"] == "delivery_closure_workbench_v1" - assert data["summary"]["source_count"] == 6 - assert data["summary"]["loaded_source_count"] == 6 + assert data["summary"]["source_count"] == 7 + assert data["summary"]["loaded_source_count"] == 7 assert data["summary"]["runtime_execution_authorized"] is False assert data["summary"]["remote_write_authorized"] is True assert data["summary"]["repo_creation_authorized"] is True assert data["summary"]["visibility_change_authorized"] is True assert data["summary"]["refs_sync_authorized"] is True assert data["summary"]["workflow_trigger_authorized"] is True + assert data["summary"]["p0_cicd_baseline_status"] == ( + "blocked_required_sources_missing" + ) + assert data["summary"]["p0_cicd_baseline_workplan_id"] == "P0-004" + assert data["summary"]["p0_cicd_baseline_source_readiness_percent"] == 27 + assert data["summary"]["p0_cicd_baseline_required_source_count"] == 11 + assert data["summary"]["p0_cicd_baseline_present_required_source_count"] == 3 + assert data["summary"]["p0_cicd_baseline_missing_required_source_count"] == 8 + assert ( + "warning_step_template_copy_apply_gate_service" + in data["summary"]["p0_cicd_baseline_blocked_source_ids"] + ) + assert data["summary"]["p0_cicd_baseline_safe_next_step"] == ( + "restore_or_recreate_tracked_warning_step_source_before_workflow_enablement" + ) assert data["summary"]["production_deploy_status"] == ( "closure_verified" ) @@ -274,6 +289,7 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): sources = {source["id"]: source for source in data["source_statuses"]} assert sorted(lanes) == [ "backup", + "cicd_baseline", "gitea", "github", "production_deploy", @@ -498,6 +514,23 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): == 0 ) assert lanes["github"]["metric"]["kind"] == "private_backup_verified" + assert lanes["cicd_baseline"]["metric"]["kind"] == "source_readiness" + assert lanes["cicd_baseline"]["status"] == "blocked_required_sources_missing" + assert lanes["cicd_baseline"]["blocker_count"] == 8 + assert lanes["cicd_baseline"]["completion_percent"] == 27 + assert lanes["cicd_baseline"]["metric"]["workplan_id"] == "P0-004" + assert lanes["cicd_baseline"]["metric"]["required_source_count"] == 11 + assert lanes["cicd_baseline"]["metric"]["present_required_source_count"] == 3 + assert lanes["cicd_baseline"]["metric"]["missing_required_source_count"] == 8 + assert ( + "warning_step_template_copy_apply_gate_service" + in lanes["cicd_baseline"]["metric"]["blocked_source_ids"] + ) + assert ( + lanes["cicd_baseline"]["metric"]["workflow_modification_allowed"] + is False + ) + assert lanes["cicd_baseline"]["metric"]["workflow_trigger_allowed"] is False assert lanes["gitea"]["metric"]["kind"] == "workflow_count" assert lanes["runtime"]["metric"]["kind"] == "surface_count" assert lanes["backup"]["metric"]["kind"] == "readiness_row_count" @@ -533,6 +566,7 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert lanes["backup"]["metric"]["credential_marker_write_authorized_count"] == 0 assert lanes["backup"]["metric"]["credential_escrow_forbidden_true_field_count"] == 0 assert sources["github_private_backup"]["loaded"] is True + assert sources["p0_cicd_baseline_source_readiness"]["loaded"] is True assert sources["production_deploy_readback"]["loaded"] is True assert ( sources["production_deploy_readback"]["schema_version"] @@ -543,6 +577,14 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): sources["github_private_backup"]["schema_version"] == "github_target_private_backup_evidence_gate_v1" ) + assert ( + sources["p0_cicd_baseline_source_readiness"]["schema_version"] + == "p0_cicd_baseline_source_readiness_v1" + ) + assert ( + sources["p0_cicd_baseline_source_readiness"]["missing_reason"] + == "" + ) assert sources["github_private_backup"]["missing_reason"] == "" assert lanes["github"]["blocker_count"] == 5 assert ( diff --git a/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py b/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py new file mode 100644 index 00000000..6a96174b --- /dev/null +++ b/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router +from src.services.p0_cicd_baseline_source_readiness import ( + load_latest_p0_cicd_baseline_source_readiness, +) + + +def test_p0_cicd_baseline_source_readiness_loader_reports_missing_sources(): + payload = load_latest_p0_cicd_baseline_source_readiness() + + assert payload["schema_version"] == "p0_cicd_baseline_source_readiness_v1" + assert payload["status"] == "blocked_required_sources_missing" + assert payload["priority"] == "P0-004" + assert payload["readback"]["workplan_id"] == "P0-004" + assert payload["readback"]["scorecard_completion_percent"] == 40 + assert payload["rollups"]["required_source_count"] == 11 + assert payload["rollups"]["present_required_source_count"] == 3 + assert payload["rollups"]["missing_required_source_count"] == 8 + assert payload["rollups"]["source_readiness_percent"] == 27 + assert ( + "warning_step_template_copy_apply_gate_service" + in payload["rollups"]["blocked_source_ids"] + ) + assert payload["operation_boundaries"]["read_only_api_allowed"] is True + assert payload["operation_boundaries"]["workflow_modification_allowed"] is False + assert payload["operation_boundaries"]["workflow_trigger_allowed"] is False + assert payload["operation_boundaries"]["github_api_allowed"] is False + assert payload["operation_boundaries"]["secret_read_allowed"] is False + + +def test_p0_cicd_baseline_source_readiness_endpoint_returns_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/p0-cicd-baseline-source-readiness") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "p0_cicd_baseline_source_readiness_v1" + assert data["status"] == "blocked_required_sources_missing" + assert data["rollups"]["missing_required_source_count"] == 8 + assert data["operation_boundaries"]["workflow_modification_allowed"] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 5872246a..20655a2d 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-06-29 — 12:06 P0-004 CI/CD baseline source readiness readback + +**完成內容**: +- 新增 `docs/operations/p0-cicd-baseline-source-readiness.snapshot.json`,把 P0-004 dev/prod CI/CD baseline 的 tracked source presence 轉成機器可讀 readback。 +- 新增 `p0_cicd_baseline_source_readiness` loader 與 `/api/v1/agents/p0-cicd-baseline-source-readiness` 只讀 API。 +- Delivery Workbench 新增 `cicd_baseline` lane / source,直接呈現 `status=blocked_required_sources_missing`、`required_source_count=11`、`present_required_source_count=3`、`missing_required_source_count=8`、`source_readiness_percent=27`。 +- 此步只補 P0-004 source readiness guard;不啟用 `.gitea/workflows`、不 workflow_dispatch、不建立 repo、不 sync refs、不使用 GitHub。 + +**驗證結果**: +- `python3.11 -m json.tool docs/operations/p0-cicd-baseline-source-readiness.snapshot.json`:通過。 +- `python3.11 -m py_compile apps/api/src/services/p0_cicd_baseline_source_readiness.py apps/api/src/services/delivery_closure_workbench.py apps/api/src/api/v1/agents.py`:通過。 +- `PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py apps/api/tests/test_delivery_closure_workbench_api.py -q`:`4 passed`。 +- `git diff --check`:通過。 + +**邊界**:未讀 token / `.runner` 內容 / cookie / session / secret / auth / `.env`;未使用 GitHub;未操作 host / Docker / K8s;未修改 workflow;未 force push。 + ## 2026-06-29 — 11:35 non-110 CD closure production readback verified **完成內容**: diff --git a/docs/operations/p0-cicd-baseline-source-readiness.snapshot.json b/docs/operations/p0-cicd-baseline-source-readiness.snapshot.json new file mode 100644 index 00000000..c6adec5e --- /dev/null +++ b/docs/operations/p0-cicd-baseline-source-readiness.snapshot.json @@ -0,0 +1,123 @@ +{ + "schema_version": "p0_cicd_baseline_source_readiness_v1", + "generated_at": "2026-06-29T12:06:00+08:00", + "status": "blocked_required_sources_missing", + "priority": "P0-004", + "scope": "dev_prod_cicd_baseline", + "readback": { + "workplan_id": "P0-004", + "workplan_title": "補 dev / prod CI/CD baseline", + "scorecard_completion_percent": 40, + "baseline_matrix_present": true, + "dev_missing_count": 10, + "prod_cicd_gap_count": 10, + "github_mirror_status": "removed_deleted_do_not_use", + "safe_next_step": "restore_or_recreate_tracked_warning_step_source_before_workflow_enablement" + }, + "required_sources": [ + { + "id": "blocked_products_owner_response_preflight_snapshot", + "kind": "committed_snapshot", + "path": "docs/operations/codex-gitea-blocked-products-owner-response-intake-preflight.snapshot.json", + "required": true + }, + { + "id": "blocked_products_owner_response_acceptance_snapshot", + "kind": "committed_snapshot", + "path": "docs/operations/codex-gitea-blocked-products-owner-response-acceptance.snapshot.json", + "required": true + }, + { + "id": "blocked_products_owner_response_templates_snapshot", + "kind": "committed_snapshot", + "path": "docs/operations/codex-gitea-blocked-products-owner-response-templates.snapshot.json", + "required": true + }, + { + "id": "warning_step_owner_package_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_gitea_onboarding_warning_step_owner_package.py", + "required": true + }, + { + "id": "warning_step_owner_response_preflight_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_gitea_onboarding_warning_step_owner_response_preflight.py", + "required": true + }, + { + "id": "warning_step_template_copy_execution_plan_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_execution_plan.py", + "required": true + }, + { + "id": "warning_step_template_copy_apply_gate_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_apply_gate.py", + "required": true + }, + { + "id": "warning_step_dashboard_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_gitea_onboarding_warning_step_dashboard.py", + "required": true + }, + { + "id": "product_onboarding_guard_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_product_onboarding_guard.py", + "required": true + }, + { + "id": "onboarding_reminder_contract_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_onboarding_reminder_contract.py", + "required": true + }, + { + "id": "new_product_onboarding_page_model_service", + "kind": "api_service_source", + "path": "apps/api/src/services/awoooi_new_product_onboarding_page_model.py", + "required": true + } + ], + "blockers": [ + "tracked_warning_step_source_files_missing", + "workflow_enablement_blocked_until_source_readiness_green" + ], + "next_actions": [ + "restore_or_recreate_warning_step_owner_package_preflight_plan_apply_gate_dashboard_sources", + "add_focused_tests_for_recreated_sources_before_any_workflow_copy", + "keep_workflow_modification_allowed_false_until_source_readiness_green" + ], + "rollups": { + "required_source_count": 11, + "present_required_source_count": 3, + "missing_required_source_count": 8, + "source_readiness_percent": 27, + "blocked_source_ids": [ + "warning_step_owner_package_service", + "warning_step_owner_response_preflight_service", + "warning_step_template_copy_execution_plan_service", + "warning_step_template_copy_apply_gate_service", + "warning_step_dashboard_service", + "product_onboarding_guard_service", + "onboarding_reminder_contract_service", + "new_product_onboarding_page_model_service" + ], + "hard_blocker_count": 2, + "next_action_count": 3 + }, + "operation_boundaries": { + "read_only_api_allowed": true, + "workflow_modification_allowed": false, + "workflow_trigger_allowed": false, + "repo_creation_allowed": false, + "refs_sync_allowed": false, + "github_api_allowed": false, + "host_or_k8s_write_allowed": false, + "secret_read_allowed": false, + "raw_session_or_sqlite_read_allowed": false + } +}