From e90adbc2ebaa64e8f16d9c5f2decba89b1533f1e Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 26 Apr 2026 11:24:55 +0800 Subject: [PATCH] feat(pmflow-os): gate releases on execution evidence --- backend/app/schemas/pmflow_os.py | 3 + .../services/pmflow_os_projection_service.py | 185 ++++++++++++++- .../tests/test_pmflow_os_projection_api.py | 144 ++++++++++++ docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md | 41 +++- docs/architecture/ARCHITECTURE_INVENTORY.md | 6 +- docs/architecture/CODE_REFERENCE_INDEX.md | 8 +- frontend/src/api/pmflowOs.ts | 34 +++ .../pmflow-os/pages/ExecutionBridgePage.tsx | 22 ++ .../pmflow-os/pages/ReleaseGrowthPage.tsx | 221 ++++++++++++++++++ .../src/features/pmflow-os/store.api.test.tsx | 23 ++ 10 files changed, 677 insertions(+), 10 deletions(-) diff --git a/backend/app/schemas/pmflow_os.py b/backend/app/schemas/pmflow_os.py index 1a74475f..2fda1d59 100644 --- a/backend/app/schemas/pmflow_os.py +++ b/backend/app/schemas/pmflow_os.py @@ -22,11 +22,13 @@ PMFlowExecutionProgressEventType = Literal[ "test_passed", "test_failed", "checks_passed", + "failed", "blocked", "merged", "accepted", "manual_progress", ] +PMFlowReleaseGateStatus = Literal["ready", "blocked", "needs_confirmation", "empty"] ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"] PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"] PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"] @@ -291,6 +293,7 @@ class ProjectTruthStateProjection(BaseModel): class ExecutionSummaryProjection(BaseModel): projectId: str execution: PMFlowExecutionContext + releaseGate: dict[str, Any] = Field(default_factory=dict) devSync: dict[str, Any] = Field(default_factory=dict) promptPackage: Optional[dict[str, Any]] = None repoBindings: list[dict[str, Any]] = Field(default_factory=list) diff --git a/backend/app/services/pmflow_os_projection_service.py b/backend/app/services/pmflow_os_projection_service.py index 5a0c988e..a0f0b18c 100644 --- a/backend/app/services/pmflow_os_projection_service.py +++ b/backend/app/services/pmflow_os_projection_service.py @@ -203,6 +203,7 @@ class PMFlowOSProjectionService: testSummary=test_summary, writebackStatus=writeback, ), + releaseGate=self._build_release_gate(events=events, deliveries=deliveries), devSync=dev_sync.get("summary") or {}, promptPackage=None if latest_package is None else self._serialize_prompt_package(latest_package), repoBindings=repos, @@ -1212,6 +1213,188 @@ class PMFlowOSProjectionService: return "pending" return str(event.get("workflow_state") or event.get("summary") or event.get("event_type") or "progress event recorded") + def _build_release_gate(self, *, events: list[dict[str, Any]], deliveries: list[dict[str, Any]]) -> dict[str, Any]: + evidence_events = [event for event in events if self._is_release_evidence_event(event)] + if not evidence_events: + return { + "status": "empty", + "title": "缺少执行证据", + "summary": "还没有 branch、commit、PR、test 或 failed 回写,发布门禁不能生成判断。", + "blockingReasons": [], + "nextActions": ["先从 Execution Bridge 回写 branch / commit / PR / test 事件。"], + "checks": [ + {"key": "execution_evidence", "label": "执行证据", "status": "missing", "detail": "progress_events_empty"}, + ], + "evidence": [], + "updatedAt": None, + } + + failed_events = [event for event in evidence_events if self._is_failed_evidence_event(event)] + if failed_events: + return { + "status": "blocked", + "title": "发布阻塞", + "summary": "执行回写中存在失败或阻塞事件,发布门禁已关闭。", + "blockingReasons": [self._event_reason(event) for event in failed_events[:5]], + "nextActions": [ + "先修复失败事件对应的任务或测试。", + "修复后重新回写 test_passed 或 checks_passed 证据。", + ], + "checks": self._release_gate_checks(evidence_events), + "evidence": self._release_gate_evidence(evidence_events), + "updatedAt": str(evidence_events[0].get("occurred_at") or "") or None, + } + + has_test_passed = any(self._event_type(event) in {"test_passed", "checks_passed"} for event in evidence_events) + has_pr = any(self._event_type(event) in {"pr_opened", "pr_synced", "merged", "accepted"} or event.get("pr_number") for event in evidence_events) + has_synced = any(self._event_type(event) in {"merged", "accepted"} or "synced" in self._event_text(event) for event in evidence_events) + failed_deliveries = [item for item in deliveries if str(item.get("status") or "").lower() == "failed"] + if failed_deliveries: + return { + "status": "blocked", + "title": "发布阻塞", + "summary": "GitHub delivery 存在失败记录,发布前需要先确认回写链路。", + "blockingReasons": [ + str(item.get("failure_reason") or item.get("delivery_id") or "GitHub delivery failed") + for item in failed_deliveries[:5] + ], + "nextActions": ["检查 GitHub webhook delivery 失败原因。", "恢复回写链路后重新生成发布判断。"], + "checks": self._release_gate_checks(evidence_events), + "evidence": self._release_gate_evidence(evidence_events), + "updatedAt": str(evidence_events[0].get("occurred_at") or "") or None, + } + + if has_test_passed and has_synced: + return { + "status": "ready", + "title": "可发布", + "summary": "已有 PR/合并与通过测试证据,发布门禁给出可发布判断。", + "blockingReasons": [], + "nextActions": ["由负责人进行最终人工确认;PMFlow 不自动执行发布命令。"], + "checks": self._release_gate_checks(evidence_events), + "evidence": self._release_gate_evidence(evidence_events), + "updatedAt": str(evidence_events[0].get("occurred_at") or "") or None, + } + + if has_test_passed: + return { + "status": "needs_confirmation", + "title": "发布候选,需人工确认", + "summary": "测试已通过,但仍需要负责人确认 PR 状态、发布范围和回滚预案。", + "blockingReasons": [], + "nextActions": [ + "确认 PR 是否已审查或合并。", + "确认本次发布范围与回滚方案后再进入发布。", + ], + "checks": self._release_gate_checks(evidence_events), + "evidence": self._release_gate_evidence(evidence_events), + "updatedAt": str(evidence_events[0].get("occurred_at") or "") or None, + } + + return { + "status": "needs_confirmation", + "title": "需要人工确认", + "summary": "已有执行回写,但缺少通过测试证据,不能直接给出可发布判断。", + "blockingReasons": ["缺少 test_passed 或 checks_passed 证据。"], + "nextActions": ["补充测试回写;若确需跳过测试,需要负责人显式确认。"], + "checks": self._release_gate_checks(evidence_events), + "evidence": self._release_gate_evidence(evidence_events), + "updatedAt": str(evidence_events[0].get("occurred_at") or "") or None, + } + + def _release_gate_checks(self, events: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + { + "key": "branch", + "label": "分支证据", + "status": "passed" if any(event.get("branch_name") or self._event_type(event) == "branch_created" for event in events) else "missing", + "detail": self._first_event_detail(events, {"branch_created", "commit", "pr_opened", "pr_synced"}), + }, + { + "key": "commit", + "label": "提交证据", + "status": "passed" if any(event.get("commit_sha") or self._event_type(event) == "commit" for event in events) else "missing", + "detail": self._first_event_detail(events, {"commit"}), + }, + { + "key": "pr", + "label": "PR 证据", + "status": "passed" if any(event.get("pr_number") or self._event_type(event) in {"pr_opened", "pr_synced", "merged", "accepted"} for event in events) else "missing", + "detail": self._first_event_detail(events, {"pr_opened", "pr_synced", "merged", "accepted"}), + }, + { + "key": "test", + "label": "测试证据", + "status": "failed" if any(self._event_type(event) == "test_failed" for event in events) else ("passed" if any(self._event_type(event) in {"test_passed", "checks_passed"} for event in events) else "missing"), + "detail": self._first_event_detail(events, {"test_passed", "checks_passed", "test_failed"}), + }, + { + "key": "failure", + "label": "失败事件", + "status": "failed" if any(self._is_failed_evidence_event(event) for event in events) else "passed", + "detail": self._first_event_detail(events, {"failed", "blocked", "test_failed"}) or "未发现失败回写", + }, + ] + + def _release_gate_evidence(self, events: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + { + "id": str(event.get("id") or ""), + "type": self._event_type(event), + "summary": self._event_reason(event), + "taskTitle": (event.get("task") or {}).get("title") if isinstance(event.get("task"), dict) else None, + "repo": (event.get("repo") or {}).get("full_name") if isinstance(event.get("repo"), dict) else None, + "branchName": event.get("branch_name"), + "commitSha": event.get("commit_sha"), + "prNumber": event.get("pr_number"), + "occurredAt": event.get("occurred_at"), + } + for event in events[:12] + ] + + @staticmethod + def _event_type(event: dict[str, Any]) -> str: + return str(event.get("event_type") or "").strip().lower() + + def _event_text(self, event: dict[str, Any]) -> str: + return " ".join( + [ + self._event_type(event), + str(event.get("summary") or ""), + str(event.get("workflow_state") or ""), + str(event.get("derived_task_status") or ""), + ] + ).lower() + + def _is_release_evidence_event(self, event: dict[str, Any]) -> bool: + event_type = self._event_type(event) + return event_type in { + "branch_created", + "commit", + "pr_opened", + "pr_synced", + "test_passed", + "test_failed", + "checks_passed", + "failed", + "blocked", + "merged", + "accepted", + } or bool(event.get("branch_name") or event.get("commit_sha") or event.get("pr_number")) + + def _is_failed_evidence_event(self, event: dict[str, Any]) -> bool: + text = self._event_text(event) + return any(token in text for token in ("test_failed", "failed", "blocked", "失败", "阻塞", "error")) + + def _event_reason(self, event: dict[str, Any]) -> str: + return str(event.get("summary") or event.get("workflow_state") or event.get("event_type") or "执行证据").strip() + + def _first_event_detail(self, events: list[dict[str, Any]], event_types: set[str]) -> str: + for event in events: + if self._event_type(event) in event_types: + return self._event_reason(event) + return "" + def _serialize_prompt_package(self, package: PromptPackage) -> dict[str, Any]: return { "id": str(package.id), @@ -1452,7 +1635,7 @@ class PMFlowOSProjectionService: return "testing" if value in {"merged", "accepted"}: return "synced" - if value == "blocked": + if value in {"blocked", "failed"}: return "failed" return "manual_progress" diff --git a/backend/tests/test_pmflow_os_projection_api.py b/backend/tests/test_pmflow_os_projection_api.py index 4165709f..b1a870b5 100644 --- a/backend/tests/test_pmflow_os_projection_api.py +++ b/backend/tests/test_pmflow_os_projection_api.py @@ -280,6 +280,8 @@ async def test_execution_summary_empty_state_is_pending_with_warnings(db_env) -> assert payload["execution"]["status"] == "queued" assert payload["execution"]["prompt"] == "" assert payload["execution"]["testSummary"] == "pending" + assert payload["releaseGate"]["status"] == "empty" + assert "缺少执行证据" in payload["releaseGate"]["title"] assert "prompt_package_empty" in " ".join(payload["warnings"]) assert payload["activeDevTasks"] == [] @@ -689,6 +691,8 @@ async def test_execution_progress_writeback_records_branch_pr_commit_and_test(db test_payload = test_response.json()["data"] assert test_payload["execution"]["status"] == "failed" assert test_payload["execution"]["testSummary"] == "单测失败:ExecutionBridgePage 回写表单缺少断言" + assert test_payload["releaseGate"]["status"] == "blocked" + assert "单测失败" in " ".join(test_payload["releaseGate"]["blockingReasons"]) assert test_payload["recentProgressEvents"][0]["event_type"] == "test_failed" assert test_payload["recentProgressEvents"][0]["repo"]["full_name"] == "pmflow/writeback" @@ -700,6 +704,146 @@ async def test_execution_progress_writeback_records_branch_pr_commit_and_test(db assert [event.event_type for event in events] == ["commit", "pr_opened", "test_failed"] +@pytest.mark.asyncio +async def test_release_gate_marks_test_passed_execution_as_candidate(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=project.id, + created_by=user.id, + title="发布候选宪章", + content={"business_goal": "测试通过后进入发布候选"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload={"acceptance_criteria": ["测试通过时生成发布候选"]}, + manual_overrides=None, + title="发布候选任务", + description="提供 commit / PR / test evidence", + required_skill_tag_ids=[], + status="confirmed", + priority="high", + ) + repo = RepoBinding( + tenant_id=1, + project_id=project.id, + provider="github", + owner="pmflow", + repo_name="release-candidate", + default_branch="main", + status="active", + ) + session.add_all([task, repo]) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + await client.post( + f"/pmflow-os/projects/{project.id}/execution-progress-events", + json={ + "task_id": str(task.id), + "repo_binding_id": str(repo.id), + "event_type": "commit", + "source": "codex", + "branch_name": "codex/release-candidate", + "commit_sha": "abc123def456", + }, + ) + await client.post( + f"/pmflow-os/projects/{project.id}/execution-progress-events", + json={ + "task_id": str(task.id), + "repo_binding_id": str(repo.id), + "event_type": "pr_opened", + "source": "github_agent", + "branch_name": "codex/release-candidate", + "pr_number": 99, + }, + ) + response = await client.post( + f"/pmflow-os/projects/{project.id}/execution-progress-events", + json={ + "task_id": str(task.id), + "repo_binding_id": str(repo.id), + "event_type": "test_passed", + "source": "local_bridge", + "branch_name": "codex/release-candidate", + "test_summary": "单测与构建全部通过", + }, + ) + + assert response.status_code == 200 + payload = response.json()["data"] + assert payload["releaseGate"]["status"] == "needs_confirmation" + assert "发布候选" in payload["releaseGate"]["title"] + assert payload["releaseGate"]["blockingReasons"] == [] + assert any(item["key"] == "test" and item["status"] == "passed" for item in payload["releaseGate"]["checks"]) + assert any(item["type"] == "test_passed" for item in payload["releaseGate"]["evidence"]) + + +@pytest.mark.asyncio +async def test_release_gate_blocks_explicit_failed_event(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=project.id, + created_by=user.id, + title="失败回写宪章", + content={"business_goal": "failed event 必须阻塞发布"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload=None, + manual_overrides=None, + title="失败回写任务", + description="显式 failed event", + required_skill_tag_ids=[], + status="confirmed", + priority="high", + ) + session.add(task) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + f"/pmflow-os/projects/{project.id}/execution-progress-events", + json={ + "task_id": str(task.id), + "event_type": "failed", + "source": "codex", + "summary": "Codex 执行失败:typecheck 未通过", + }, + ) + + assert response.status_code == 200 + payload = response.json()["data"] + assert payload["execution"]["status"] == "failed" + assert payload["releaseGate"]["status"] == "blocked" + assert "typecheck 未通过" in " ".join(payload["releaseGate"]["blockingReasons"]) + assert payload["releaseGate"]["evidence"][0]["type"] == "failed" + + @pytest.mark.asyncio async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None: session = db_env diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index 45f100f8..5f60d1ee 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -86,7 +86,7 @@ 3. `pmflow-os` 本地 store 还没有接后端读写,当前仍是 mock truth store。 4. `ChangeReview` 的接受、修改、暂缓还没有写入正式对象、审批记录和证据链。 5. `ExecutionBridge` 页面还没有消费 dev-sync / GitHub / PromptPackage 真实接口。 -6. `ReleaseGrowth` 还没有把 release validation、feedback、revenue、ROI、compliance 真实数据串成发布后反馈循环。 +6. `ReleaseGrowth` 已接入 execution evidence gate,但 release validation、feedback、revenue、ROI、compliance 还没有串成完整发布后反馈循环。 7. `TeamLayer` 仍是产品表达层,缺少真实成员角色、Role Inbox、Decision Desk、Team Feed 的数据模型和审查流程。 ## 2. 下一步推进方向 @@ -270,7 +270,7 @@ 范围: -- `ReleaseGrowthPage` 消费 release validation。 +- `ReleaseGrowthPage` 已消费 execution evidence gate;后续继续接 release validation。 - 支持 feedback ingest。 - 支持 revenue event 或基础 ROI snapshot。 - 支持 compliance check 的轻量门禁。 @@ -807,6 +807,43 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 ## 8. 进度台账 +### 2026-04-26 / release-growth-p1-execution-evidence-gate / implemented + +完成内容: + +- 在 `ExecutionSummaryProjection` 中新增 `releaseGate` 投影,从 `ProgressEvent` 的 branch、commit、PR、test、failed / blocked 证据推导发布判断。 +- `releaseGate.status` 覆盖 `ready / blocked / needs_confirmation / empty`:测试通过可进入发布候选,failed / test_failed / blocked 事件 fail-closed 阻塞发布,无 execution evidence 时返回明确 empty/warning。 +- 扩展 execution progress event 合同,允许显式 `failed` event 回写。 +- `ReleaseGrowthPage` 真实模式改为只消费 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作和 warning;不再展示静态“已发布/增长指标”mock 结果。 +- 静态高保真发布/增长原型只保留在显式 `?demo=1` / `dataSource=demo` 模式。 +- 本轮不执行发布命令、不做真实部署、不新增 migration。 + +涉及文件: + +- `backend/app/schemas/pmflow_os.py` +- `backend/app/services/pmflow_os_projection_service.py` +- `backend/tests/test_pmflow_os_projection_api.py` +- `frontend/src/api/pmflowOs.ts` +- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx` +- `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx` +- `frontend/src/features/pmflow-os/store.api.test.tsx` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile backend/app/api/pmflow_os_api/__init__.py backend/app/services/pmflow_os_projection_service.py backend/app/schemas/pmflow_os.py` 通过。 +- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m pytest backend/tests/test_pmflow_os_projection_api.py -q` 通过:`14 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。 +- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`4 passed`。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning,但构建成功。 + +未完成或风险: + +- ReleaseGrowth P1 只生成发布判断和下一步动作,不触发真实部署、不写 release validation run。 +- 增长反馈、收入、ROI 和 compliance 的真实发布后反馈循环仍待后续 Packet 接入。 + ### 2026-04-26 / big-module-integration-p2 / done 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index b31ad565..4d163833 100644 --- a/docs/architecture/ARCHITECTURE_INVENTORY.md +++ b/docs/architecture/ARCHITECTURE_INVENTORY.md @@ -197,9 +197,9 @@ - `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、执行中 / AI 正在推进、暂存想法、草案箱、最近项目摘要;项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入或 execution event 写入。 - `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 生成草案并确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。 - `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。 -- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 -- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。 -- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。 +- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 与 execution progress writeback 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 +- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因和下一步动作,不执行发布命令、不展示 mock 发布结果;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。 +- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。 - PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。 - `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。 - `frontend/vite.config.ts` 现补了固定 vendor chunk 分组(`framework / state-query / ui-radix / icons / charts / markdown`)与业务 chunk 分组(`ai-steward / sstc / governance`),目标是让登录页与基础导航不再背 AI Steward、SSTC、治理页和图表代码。 diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index e7911884..35f022db 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -89,7 +89,7 @@ - SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee` - SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}` - SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases` -- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit 与 Execution Handoff:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令 +- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff 与 Release Evidence Gate:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 已在 `execution-summary.releaseGate` 中从 branch / commit / PR / test_passed / checks_passed / failed / blocked 等 `ProgressEvent` 推导 `ready / blocked / needs_confirmation / empty`,只生成发布判断、阻塞原因和下一步动作,不执行发布命令 - Project Launch 真实创建链:参考 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与模型 `backend/app/models/pmflow_launch.py`;接口为 `POST /pmflow-launch/drafts` 与 `POST /pmflow-launch/drafts/{draft_id}/confirm`,draft 只写 `pmflow_launch_drafts`,confirm 在同一事务内创建 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs` - PMFlow Change Review 模型与 migration:参考 `backend/app/models/pmflow_change_review.py` 与 `backend/alembic/versions/20260426_add_pmflow_change_review_tables.py`;只包含 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`,本阶段不得新增或依赖 `/commit` @@ -97,7 +97,7 @@ - PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 - PMFlow 大模块并行公共壳:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 的 `4.4.1-4.4.3` 与 `frontend/src/router/index.tsx`、`frontend/src/main.tsx`、`frontend/src/features/pmflow-os/components/MobileBottomNav.tsx`、`frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx`;`/space` 归 Space lane,`/pmflow-os / /command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route,新增或改动必须由 Shell / Integration owner 串行处理 - PMFlow OS 产品合同真源:先读 `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`,再实现 `pmflow-os` 真实后端接入;该文档锁定 `ProjectTruthState / Thread / ChangeSet / ChangeItem / Evidence / Approval / Project Memory / ExecutionContext` 与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界和后续 API 草案 -- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory 或 execution event;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory 或 execution event;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` - AI Steward 页面入口:参考 `frontend/src/pages/ai-steward/` - AI Steward 问答浮窗与上下文显性化组件:参考 `frontend/src/components/ai-steward/FloatingQAChat.tsx`、`frontend/src/components/ai-steward/QAConversationStatusBar.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.tsx` - AI Steward QA contract 前端类型(含 `analysis_packet`):参考 `frontend/src/api/aiSteward.ts` @@ -107,8 +107,8 @@ - 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 - 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch` - Account Settings / `/me` 前端产品域与 P1 后端同步合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`,不得假装云端账号保存 -- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` -- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx` +- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` +- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`;`ReleaseGrowthPage.tsx` 真实模式只展示 `execution-summary.releaseGate`、execution evidence、阻塞原因和下一步动作,缺证据时显示 empty/warning,不展示 mock 发布结果,静态高保真发布/增长数据仅保留在显式 demo 模式 - PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航 - PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx` - Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts` diff --git a/frontend/src/api/pmflowOs.ts b/frontend/src/api/pmflowOs.ts index a3933394..49a86f09 100644 --- a/frontend/src/api/pmflowOs.ts +++ b/frontend/src/api/pmflowOs.ts @@ -13,6 +13,7 @@ function unwrap(raw: ApiEnvelope | T): T { export interface PMFlowExecutionSummary { projectId: string; execution: PMFlowExecutionContext; + releaseGate: PMFlowReleaseGate; devSync: Record; promptPackage?: PMFlowPromptPackage | null; repoBindings: PMFlowRepoBinding[]; @@ -32,11 +33,44 @@ export type PMFlowExecutionProgressEventType = | "test_passed" | "test_failed" | "checks_passed" + | "failed" | "blocked" | "merged" | "accepted" | "manual_progress"; +export type PMFlowReleaseGateStatus = "ready" | "blocked" | "needs_confirmation" | "empty"; + +export interface PMFlowReleaseGateCheck { + key: string; + label: string; + status: "passed" | "failed" | "missing"; + detail?: string | null; +} + +export interface PMFlowReleaseGateEvidence { + id: string; + type: string; + summary: string; + taskTitle?: string | null; + repo?: string | null; + branchName?: string | null; + commitSha?: string | null; + prNumber?: number | null; + occurredAt?: string | null; +} + +export interface PMFlowReleaseGate { + status: PMFlowReleaseGateStatus; + title: string; + summary: string; + blockingReasons: string[]; + nextActions: string[]; + checks: PMFlowReleaseGateCheck[]; + evidence: PMFlowReleaseGateEvidence[]; + updatedAt?: string | null; +} + export interface PMFlowExecutionHandoffRequest { task_id: string; repo_binding_id?: string | null; diff --git a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx index e20951b2..b3f96e6a 100644 --- a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx +++ b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx @@ -680,6 +680,28 @@ function createDemoExecutionSummary(state: ReturnType[" return { projectId: state.project.id, execution: state.execution, + releaseGate: { + status: state.execution.status === "failed" ? "blocked" : state.execution.status === "synced" ? "needs_confirmation" : "empty", + title: state.execution.status === "failed" ? "发布阻塞" : state.execution.status === "synced" ? "发布候选,需人工确认" : "缺少执行证据", + summary: + state.execution.status === "failed" + ? state.execution.testSummary + : state.execution.status === "synced" + ? "Demo 执行已回写,真实发布判断只在 API 模式下由 execution evidence 生成。" + : "Demo 状态尚未形成可用于发布判断的执行证据。", + blockingReasons: state.execution.status === "failed" ? [state.execution.testSummary] : [], + nextActions: ["进入发布增长页查看真实 execution evidence gate。"], + checks: [ + { + key: "demo_execution", + label: "Demo 执行证据", + status: state.execution.status === "failed" ? "failed" : state.execution.status === "synced" ? "passed" : "missing", + detail: state.execution.writebackStatus, + }, + ], + evidence: [], + updatedAt: null, + }, devSync: { repo_binding_count: 1, active_task_count: task ? 1 : 0, diff --git a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx index d17a5b40..a437fc03 100644 --- a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx +++ b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx @@ -1,18 +1,27 @@ import type { LucideIcon } from "lucide-react"; import { ArrowRight, + AlertTriangle, Bot, CheckCircle2, ChevronDown, CircleCheck, Cloud, FlaskConical, + GitBranch, + GitCommit, + GitPullRequest, + Loader2, MessageSquareText, RefreshCcw, + ShieldAlert, + TestTube2, X, } from "lucide-react"; import type { ReactNode } from "react"; import { Button } from "@/components/ui/button"; +import type { PMFlowReleaseGateEvidence } from "@/api/pmflowOs"; +import { usePMFlowOsStore } from "@/features/pmflow-os/store"; import { cn } from "@/lib/utils"; type StatusTone = "success" | "warning" | "danger" | "neutral" | "info"; @@ -312,7 +321,219 @@ function AiCopilot() { ); } +type PMFlowOsStoreSnapshot = ReturnType; + +function gateTone(status?: string): StatusTone { + if (status === "ready") return "success"; + if (status === "blocked") return "danger"; + if (status === "needs_confirmation") return "warning"; + return "neutral"; +} + +function gateLabel(status?: string) { + if (status === "ready") return "可发布"; + if (status === "blocked") return "阻塞"; + if (status === "needs_confirmation") return "需要人工确认"; + return "缺少执行证据"; +} + +function checkTone(status?: string): StatusTone { + if (status === "passed") return "success"; + if (status === "failed") return "danger"; + return "warning"; +} + +function evidenceIcon(type: string) { + if (type.includes("branch")) return GitBranch; + if (type.includes("commit")) return GitCommit; + if (type.includes("pr") || type.includes("merged")) return GitPullRequest; + if (type.includes("test") || type.includes("check")) return TestTube2; + if (type.includes("fail") || type.includes("block")) return AlertTriangle; + return CheckCircle2; +} + +function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) { + const Icon = evidenceIcon(item.type); + return ( +
+ + + +
+
+ + {item.taskTitle ? {item.taskTitle} : null} +
+

{item.summary}

+
+ {item.repo ? {item.repo} : null} + {item.branchName ? branch {item.branchName} : null} + {item.commitSha ? commit {item.commitSha.slice(0, 12)} : null} + {item.prNumber ? PR #{item.prNumber} : null} +
+
+
{item.occurredAt ? new Date(item.occurredAt).toLocaleString("zh-CN") : "未记录时间"}
+
+ ); +} + +function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { + const { + state, + dataSourceMode, + dataSourceStatus, + dataSourceError, + executionSummary, + executionSummaryStatus, + executionSummaryError, + projectionWarnings, + refetchExecutionSummary, + } = store; + const gate = executionSummary?.releaseGate; + const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading"; + const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])]; + const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate; + + return ( +
+
+ +
+
+
Release Gate
+

发布 / 增长判断

+

+ 本页只读取 Execution Bridge 回写的 branch、commit、PR、test、failed 证据生成发布判断;PMFlow 不在这里执行发布命令。 +

+
+ +
+
+ + {dataSourceError || executionSummaryError ? ( + +
+ + {dataSourceError || executionSummaryError} +
+
+ ) : null} + +
+
+ +
+
+ +

{gate?.title || "缺少执行证据"}

+

+ {gate?.summary || "URL 缺少 projectId 或后端尚未返回 execution-summary,无法生成发布判断。"} +

+
+
+
当前项目
+
{state.project.name}
+
执行状态
+
{executionSummary?.execution.status || state.execution.status}
+
+
+
+ + {emptyMode ? ( + +
+ +
+

没有可用于发布判断的执行证据

+

+ 需要先从执行桥回写 branch、commit、PR、test 或 failed 事件。当前页面不会用 mock 发布结果补位。 +

+
+
+
+ ) : null} + + + +
+ {(gate?.checks || []).map((item) => ( +
+
+
{item.label}
+ +
+
{item.detail || "暂无对应证据"}
+
+ ))} + {!gate?.checks?.length ?
暂无门禁检查项。
: null} +
+
+ + + +
+ {(gate?.evidence || []).map((item) => ( + + ))} + {!gate?.evidence?.length ?
暂无真实 execution evidence。
: null} +
+
+
+ + +
+
+
+ ); +} + export default function ReleaseGrowthPage() { + const store = usePMFlowOsStore(); + if (store.dataSourceMode !== "demo") { + return ; + } + return (
diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index 88d0703e..72b99772 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -14,6 +14,7 @@ import { editPMFlowChangeItem, getPMFlowExecutionSummary, getPMFlowTruthState, + type PMFlowReleaseGate, } from "@/api/pmflowOs"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store"; @@ -90,6 +91,23 @@ function realState(): ProjectTruthState { return state; } +function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty" = "needs_confirmation"): PMFlowReleaseGate { + return { + status, + title: status === "blocked" ? "发布阻塞" : "发布候选,需人工确认", + summary: "来自真实 execution-summary 的发布门禁判断", + blockingReasons: status === "blocked" ? ["测试失败"] : [], + nextActions: ["负责人确认后再发布"], + checks: [ + { key: "test", label: "测试证据", status: status === "blocked" ? "failed" : "passed", detail: "真实测试回写" }, + ], + evidence: [ + { id: "703", type: "test_passed", summary: "真实测试回写", prNumber: 88 }, + ], + updatedAt: "2026-04-26T00:00:00", + }; +} + function wrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return function Wrapper({ children }: { children: ReactNode }) { @@ -117,6 +135,7 @@ describe("PMFlowOsProvider real change review actions", () => { testSummary: "pending", writebackStatus: "coding", }, + releaseGate: releaseGate(), devSync: { active_task_count: 1 }, promptPackage: { id: "501", @@ -192,6 +211,7 @@ describe("PMFlowOsProvider real change review actions", () => { testSummary: "pending", writebackStatus: "pushed", }, + releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 1 }, promptPackage: { id: "501", @@ -241,6 +261,7 @@ describe("PMFlowOsProvider real change review actions", () => { testSummary: "pending", writebackStatus: "coding", }, + releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 2 }, promptPackage: { id: "501", @@ -279,6 +300,7 @@ describe("PMFlowOsProvider real change review actions", () => { testSummary: "pending", writebackStatus: "in_review", }, + releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 3 }, promptPackage: { id: "501", @@ -320,6 +342,7 @@ describe("PMFlowOsProvider real change review actions", () => { await waitFor(() => expect(result.current.executionSummaryStatus).toBe("success")); expect(getExecutionSummaryMock).toHaveBeenCalledWith("123"); expect(result.current.executionSummary?.promptPackage?.package_text).toBe("真实 prompt package"); + expect(result.current.executionSummary?.releaseGate.status).toBe("needs_confirmation"); act(() => result.current.sendMessage("继续补充")); await waitFor(() => expect(createThreadMessageMock).toHaveBeenCalledWith("123", "7", expect.objectContaining({ content: "继续补充" })));