diff --git a/backend/app/api/pmflow_os_api/__init__.py b/backend/app/api/pmflow_os_api/__init__.py index 1c6e8de6..2c270843 100644 --- a/backend/app/api/pmflow_os_api/__init__.py +++ b/backend/app/api/pmflow_os_api/__init__.py @@ -17,6 +17,7 @@ from ...schemas.pmflow_os import ( PMFlowExecutionHandoffRequest, PMFlowExecutionProgressEventRequest, PMFlowExtractionCreateRequest, + PMFlowReleaseValidationRunCreateRequest, PMFlowThreadMessageCreateRequest, ) from ...services.auth import has_any_role @@ -319,6 +320,37 @@ async def create_pmflow_os_execution_progress_event( return success_response(payload.model_dump(mode="json")) +@router.post("/projects/{project_id}/release-validation-runs") +async def create_pmflow_os_release_validation_run( + project_id: int, + body: PMFlowReleaseValidationRunCreateRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + tenant_id = int(current_user.tenant_id) + await _ensure_project_member_or_manager( + db, + tenant_id=tenant_id, + project_id=int(project_id), + current_user=current_user, + ) + service = PMFlowOSProjectionService(db, tenant_id=tenant_id) + try: + payload = await service.create_release_validation_run( + project_id=int(project_id), + body=body, + current_user_id=int(current_user.id), + ) + except ValueError as exc: + code = str(exc) + if code == "release_validation_evidence_empty": + return JSONResponse(status_code=409, content=error_response(409, "release_validation_evidence_empty")) + if code == "project_not_found": + raise HTTPException(status_code=404, detail="project_not_found") from exc + raise + return success_response(payload) + + @router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume") async def consume_pmflow_os_execution_handoff( project_id: int, diff --git a/backend/app/schemas/pmflow_os.py b/backend/app/schemas/pmflow_os.py index 2fda1d59..8e877153 100644 --- a/backend/app/schemas/pmflow_os.py +++ b/backend/app/schemas/pmflow_os.py @@ -29,6 +29,7 @@ PMFlowExecutionProgressEventType = Literal[ "manual_progress", ] PMFlowReleaseGateStatus = Literal["ready", "blocked", "needs_confirmation", "empty"] +PMFlowReleaseValidationDecision = Literal["enter_release_candidate", "keep_blocked"] 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"] @@ -131,6 +132,11 @@ class PMFlowExecutionProgressEventRequest(BaseModel): occurred_at: Optional[str] = None +class PMFlowReleaseValidationRunCreateRequest(BaseModel): + decision: PMFlowReleaseValidationDecision + note: Optional[str] = Field(default=None, max_length=4000) + + class PMFlowChangeSetDetailResponse(BaseModel): changeSet: dict[str, Any] items: 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 a0f0b18c..5ef2134a 100644 --- a/backend/app/services/pmflow_os_projection_service.py +++ b/backend/app/services/pmflow_os_projection_service.py @@ -47,6 +47,7 @@ from ..schemas.pmflow_os import ( PMFlowProjectMemory, PMFlowProjectSummary, PMFlowRelease, + PMFlowReleaseValidationRunCreateRequest, PMFlowRequirement, PMFlowRisk, PMFlowTask, @@ -191,6 +192,10 @@ class PMFlowOSProjectionService: pr_status = self._project_pr_status(active_tasks=active_tasks, task_id=task_id, events=events) test_summary = self._project_test_summary(events) writeback = self._project_writeback_status(latest_event) + latest_release_validation = await self._load_latest_release_validation_run(project_id=project_id) + release_gate = self._build_release_gate(events=events, deliveries=deliveries) + if latest_release_validation is not None: + release_gate["latestValidationRun"] = self._serialize_release_validation_run(latest_release_validation) return ExecutionSummaryProjection( projectId=str(project_id), @@ -203,7 +208,7 @@ class PMFlowOSProjectionService: testSummary=test_summary, writebackStatus=writeback, ), - releaseGate=self._build_release_gate(events=events, deliveries=deliveries), + releaseGate=release_gate, devSync=dev_sync.get("summary") or {}, promptPackage=None if latest_package is None else self._serialize_prompt_package(latest_package), repoBindings=repos, @@ -267,6 +272,55 @@ class PMFlowOSProjectionService: await self.db.commit() return await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + async def create_release_validation_run( + self, + *, + project_id: int, + body: PMFlowReleaseValidationRunCreateRequest, + current_user_id: Optional[int] = None, + ) -> dict[str, Any]: + await self._load_project(project_id=project_id) + summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + release_gate = dict(summary.releaseGate or {}) + evidence = list(release_gate.get("evidence") or []) + if str(release_gate.get("status") or "empty") == "empty" or not evidence: + raise ValueError("release_validation_evidence_empty") + + status = self._release_validation_status(gate_status=str(release_gate.get("status") or ""), decision=body.decision) + run = ReleaseValidationRun( + tenant_id=self.tenant_id, + project_id=int(project_id), + status=status, + score=self._release_validation_score(status), + key_metrics=self._release_validation_metrics(release_gate), + risk_items=self._release_validation_risks(release_gate, decision=body.decision), + rollback_plan=self._release_validation_rollback_plan(release_gate), + validation_summary={ + "source": "pmflow_os_release_gate", + "projectId": str(project_id), + "status": status, + "releaseGateStatus": release_gate.get("status"), + "humanDecision": body.decision, + "note": body.note, + "createdBy": None if current_user_id is None else str(current_user_id), + "createdAt": now_cn_naive().isoformat(), + "deploymentExecuted": False, + "evidence": evidence, + "blockingReasons": list(release_gate.get("blockingReasons") or []), + "nextActions": list(release_gate.get("nextActions") or []), + "checks": list(release_gate.get("checks") or []), + }, + ) + self.db.add(run) + await self.db.commit() + await self.db.refresh(run) + updated_summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + return { + "releaseValidationRun": self._serialize_release_validation_run(run), + "executionSummary": updated_summary.model_dump(mode="json"), + "warnings": [] if status != "blocked" else list(release_gate.get("blockingReasons") or []), + } + async def consume_execution_handoff( self, *, @@ -427,6 +481,15 @@ class PMFlowOSProjectionService: ) return result.scalar_one_or_none() + async def _load_latest_release_validation_run(self, *, project_id: int) -> Optional[ReleaseValidationRun]: + result = await self.db.execute( + select(ReleaseValidationRun) + .where(and_(ReleaseValidationRun.tenant_id == self.tenant_id, ReleaseValidationRun.project_id == int(project_id))) + .order_by(ReleaseValidationRun.updated_at.desc(), ReleaseValidationRun.created_at.desc(), ReleaseValidationRun.id.desc()) + .limit(1) + ) + return result.scalar_one_or_none() + async def _load_active_prompt_package_for_task(self, *, project_id: int, task_id: int) -> Optional[PromptPackage]: result = await self.db.execute( select(PromptPackage) @@ -1395,6 +1458,79 @@ class PMFlowOSProjectionService: return self._event_reason(event) return "" + def _release_validation_status(self, *, gate_status: str, decision: str) -> str: + if decision == "keep_blocked" or gate_status == "blocked": + return "blocked" + if gate_status == "ready": + return "ready" + return "needs_confirmation" + + def _release_validation_score(self, status: str) -> float: + if status == "ready": + return 1.0 + if status == "needs_confirmation": + return 0.6 + return 0.0 + + def _release_validation_metrics(self, release_gate: dict[str, Any]) -> dict[str, Any]: + checks = list(release_gate.get("checks") or []) + evidence = list(release_gate.get("evidence") or []) + return { + "releaseGateStatus": release_gate.get("status"), + "evidenceCount": len(evidence), + "passedCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "passed"]), + "failedCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "failed"]), + "missingCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "missing"]), + } + + def _release_validation_risks(self, release_gate: dict[str, Any], *, decision: str) -> list[dict[str, Any]]: + risks = [ + {"label": reason, "status": "blocked", "source": "release_gate_blocking_reason"} + for reason in list(release_gate.get("blockingReasons") or []) + ] + for check in list(release_gate.get("checks") or []): + if not isinstance(check, dict) or check.get("status") == "passed": + continue + risks.append( + { + "label": check.get("label") or check.get("key") or "发布检查项", + "status": check.get("status"), + "detail": check.get("detail"), + "source": "release_gate_check", + } + ) + if decision == "keep_blocked" and not risks: + risks.append({"label": "负责人选择保持阻塞,等待补充发布前确认。", "status": "blocked", "source": "human_decision"}) + return risks + + def _release_validation_rollback_plan(self, release_gate: dict[str, Any]) -> dict[str, Any]: + return { + "deploymentExecuted": False, + "requiredBeforeDeploy": [ + "完成人工发布确认。", + "确认回滚方案和发布范围。", + ], + "nextActions": list(release_gate.get("nextActions") or []), + } + + def _serialize_release_validation_run(self, run: ReleaseValidationRun) -> dict[str, Any]: + summary = self._payload(run.validation_summary) + return { + "id": str(run.id), + "projectId": str(run.project_id), + "charterId": None if run.charter_id is None else str(run.charter_id), + "releaseNodeId": None if run.release_node_id is None else str(run.release_node_id), + "status": run.status, + "score": self._jsonable(run.score), + "keyMetrics": run.key_metrics or {}, + "riskItems": run.risk_items or [], + "rollbackPlan": run.rollback_plan or {}, + "validationSummary": summary, + "createdBy": summary.get("createdBy"), + "createdAt": run.created_at.isoformat() if run.created_at else summary.get("createdAt"), + "updatedAt": run.updated_at.isoformat() if run.updated_at else None, + } + def _serialize_prompt_package(self, package: PromptPackage) -> dict[str, Any]: return { "id": str(package.id), diff --git a/backend/tests/test_pmflow_os_projection_api.py b/backend/tests/test_pmflow_os_projection_api.py index b1a870b5..9e0a132a 100644 --- a/backend/tests/test_pmflow_os_projection_api.py +++ b/backend/tests/test_pmflow_os_projection_api.py @@ -844,6 +844,177 @@ async def test_release_gate_blocks_explicit_failed_event(db_env) -> None: assert payload["releaseGate"]["evidence"][0]["type"] == "failed" +@pytest.mark.asyncio +async def test_release_validation_run_creates_ready_record_from_ready_gate(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="Ready 发布验证宪章", + content={"business_goal": "ready evidence 可以固化为发布验证记录"}, + 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="Ready 发布任务", + description="测试通过并合并", + required_skill_tag_ids=[], + status="confirmed", + priority="high", + ) + repo = RepoBinding( + tenant_id=1, + project_id=project.id, + provider="github", + owner="pmflow", + repo_name="ready-release", + 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": "test_passed", + "source": "local_bridge", + "branch_name": "codex/ready-release", + "test_summary": "单测、typecheck、build 全部通过", + }, + ) + 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": "merged", + "source": "github_agent", + "branch_name": "codex/ready-release", + "commit_sha": "ready123456789", + "pr_number": 108, + "summary": "PR #108 已合并", + }, + ) + response = await client.post( + f"/pmflow-os/projects/{project.id}/release-validation-runs", + json={"decision": "enter_release_candidate", "note": "确认进入发布候选"}, + ) + summary_response = await client.get(f"/pmflow-os/projects/{project.id}/execution-summary") + + assert response.status_code == 200 + payload = response.json()["data"] + run_payload = payload["releaseValidationRun"] + assert run_payload["status"] == "ready" + assert run_payload["projectId"] == str(project.id) + assert run_payload["validationSummary"]["releaseGateStatus"] == "ready" + assert run_payload["validationSummary"]["humanDecision"] == "enter_release_candidate" + assert run_payload["validationSummary"]["deploymentExecuted"] is False + assert run_payload["validationSummary"]["createdBy"] == str(user.id) + assert run_payload["validationSummary"]["evidence"] + assert run_payload["rollbackPlan"]["deploymentExecuted"] is False + assert payload["executionSummary"]["releaseGate"]["latestValidationRun"]["id"] == run_payload["id"] + assert summary_response.json()["data"]["releaseGate"]["latestValidationRun"]["status"] == "ready" + rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all() + assert len(rows) == 1 + assert rows[0].status == "ready" + + +@pytest.mark.asyncio +async def test_release_validation_run_creates_blocked_record_from_failed_gate(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="Blocked 发布验证宪章", + content={"business_goal": "失败 evidence 固化为阻塞记录"}, + 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="Blocked 发布任务", + description="测试失败", + 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: + await client.post( + f"/pmflow-os/projects/{project.id}/execution-progress-events", + json={ + "task_id": str(task.id), + "event_type": "test_failed", + "source": "codex", + "test_summary": "发布前验证失败:回归测试未通过", + }, + ) + response = await client.post( + f"/pmflow-os/projects/{project.id}/release-validation-runs", + json={"decision": "keep_blocked", "note": "阻塞发布"}, + ) + + assert response.status_code == 200 + payload = response.json()["data"] + run_payload = payload["releaseValidationRun"] + assert run_payload["status"] == "blocked" + assert run_payload["validationSummary"]["releaseGateStatus"] == "blocked" + assert "回归测试未通过" in " ".join(run_payload["validationSummary"]["blockingReasons"]) + assert any(item["status"] == "blocked" for item in run_payload["riskItems"]) + rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all() + assert len(rows) == 1 + assert rows[0].status == "blocked" + + +@pytest.mark.asyncio +async def test_release_validation_run_rejects_empty_gate(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + 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}/release-validation-runs", + json={"decision": "enter_release_candidate", "note": "证据为空时不能成功"}, + ) + + assert response.status_code == 409 + assert response.json()["message"] == "release_validation_evidence_empty" + rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all() + assert rows == [] + + @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 68d070c6..4c77edef 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -807,6 +807,44 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 ## 8. 进度台账 +### 2026-04-26 / release-validation-p1-real-run-records / implemented + +完成内容: + +- 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用 PMFlow OS 项目成员/OWNER/REVIEWER 权限边界和统一 `{code,message,data}` 返回包络。 +- 复用现有 `ReleaseValidationRun` 模型,不新增 migration;从当前 `execution-summary.releaseGate` 固化 `status / evidence / blockingReasons / nextActions / createdBy / createdAt / humanDecision`。 +- ready evidence + `enter_release_candidate` 生成 `ready` run;needs confirmation evidence 生成 `needs_confirmation` run;failed / blocked evidence 或 `keep_blocked` 生成 `blocked` run。 +- 无 execution evidence 或 `releaseGate.status=empty` 时返回 `release_validation_evidence_empty`,不创建成功 run,也不使用 mock validation。 +- `execution-summary.releaseGate.latestValidationRun` 返回最新发布验证记录,`ReleaseGrowthPage` 真实模式新增“发布验证记录”区域和“生成发布验证记录 / 记录保持阻塞”动作;页面仍不执行真实部署。 +- `frontend/src/api/pmflowOs.ts` 改为 PMFlow OS API 层自包含 `/api` fetch 包络解析,避免当前工作树缺失共享 `@/lib/api` 时影响 PMFlow OS focused tests;不改全局 API client。 + +涉及文件: + +- `backend/app/api/pmflow_os_api/__init__.py` +- `backend/app/services/pmflow_os_projection_service.py` +- `backend/app/schemas/pmflow_os.py` +- `backend/tests/test_pmflow_os_projection_api.py` +- `frontend/src/api/pmflowOs.ts` +- `frontend/src/features/pmflow-os/store.tsx` +- `frontend/src/features/pmflow-os/store.api.test.tsx` +- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.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` 通过:`17 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` 未通过:当时前端树缺失整组共享 `src/lib/*` 模块(例如 `@/lib/api`、`@/lib/utils`、`@/lib/datetime`、`@/lib/display`、`@/lib/rbac`),影响全仓大量既有文件和 forbidden `router/index.tsx`;本 Packet 未越界修复。 +- `cd frontend && npm run build` 同样停在上述共享 `src/lib/*` 缺失的 TypeScript 解析错误,未进入 Vite build。 + +未完成或风险: + +- 本阶段只创建发布验证记录和人工确认快照,不执行部署命令、不写外部系统。 +- 全量前端 typecheck/build 需由 Shell / Integration owner 在整合最新主线后重新验证。 + ### 2026-04-26 / account-p3-release-notification-consumption-contract / implemented 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index 4f5ecd04..d6a7aae5 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 正在推进、暂存想法、草案箱、最近项目摘要;Space P3 的 `releaseAttention` 优先消费 `truth-state.releases` 中由 release validation run 投影出的 release 状态与检查项,把“发布验证阻塞 / 发布验证待确认”显示到 `/space`,没有发布验证投影时才回落到 `execution-summary.releaseGate` 的发布候选或发布门禁阻塞;发布验证缺失时显示明确空态,不虚构验证结果。项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,发布关注项进入 `/release-growth?projectId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入、ReleaseValidationRun 写入或发布执行。 - `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/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态;P3 新增 `getReleaseGateNotificationPreference(settings, event)`,把 Release/Growth 的 `release_gate_* / release_validation_*` 事件映射到 `releaseGate` 偏好,把 `failed_event` 映射到 `failureAlert` 偏好,并返回 `inAppEnabled / desktopPreference / emailDigestPreference` 供后续 Main Flow 页面消费。该合同不代表邮件、桌面推送或系统通知中心已接入。`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 与 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。 +- `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 与 release validation run create 均调用 `/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` 与 `latestValidationRun`,从 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,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照;当前仍不调用真实 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;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;这些步骤都不直接执行本机命令或真实发布命令;`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 6196e4bd..deced06f 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 与 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`,只生成发布判断、阻塞原因和下一步动作,不执行发布命令 +- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff、Release Evidence Gate 与 Release Validation Run:参考 `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`;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,从当前 `releaseGate` 固化 `ReleaseValidationRun` 快照并在 `releaseGate.latestValidationRun` 返回最新记录,无 evidence 时返回 `release_validation_evidence_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、ChangeSet 状态、ProgressEvent 或 ReleaseValidationRun;Space P3 的 `releaseAttention` 优先从 `truth-state.releases` 的 release validation 投影生成“发布验证阻塞 / 发布验证待确认”,没有发布验证投影时才回落到 `PMFlowExecutionSummary.releaseGate` 的“发布候选 / 发布门禁阻塞”,所有动作只跳转 `/release-growth?projectId=`;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` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory、ChangeSet 状态、ProgressEvent 或 ReleaseValidationRun;Space P3 的 `releaseAttention` 优先从 `truth-state.releases` 的 release validation 投影生成“发布验证阻塞 / 发布验证待确认”,没有发布验证投影时才回落到 `PMFlowExecutionSummary.releaseGate` 的“发布候选 / 发布门禁阻塞”,所有动作只跳转 `/release-growth?projectId=`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowReleaseValidationRun`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同;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=`,发布关注项进入 `/release-growth?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate`,发布验证缺失时显示明确空态,不虚构验证结果,不直接写 Project Memory、ChangeSet 状态、execution event 或 ReleaseValidationRun;暂存想法后端只读聚合尚未接入时必须显示 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/P2/P3 偏好合同:参考 `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 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`;P2 在 `frontend/src/api/accountSettings.ts` 提供 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,把通知偏好映射为页面内提醒消费模型;P3 新增 `getReleaseGateNotificationPreference(settings, event)` 供 Release/Growth 后续消费:`release_gate_*` 与 `release_validation_*` 使用 `releaseGate` 偏好,`failed_event` 使用 `failureAlert` 偏好,返回 `inAppEnabled / desktopPreference / emailDigestPreference`;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入 -- 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 共享 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、release validation run create 均调后端并 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、阻塞原因、下一步动作和 `latestValidationRun`,缺证据时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run,但仍不执行部署命令;静态高保真发布/增长数据仅保留在显式 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 49a86f09..c8434d0a 100644 --- a/frontend/src/api/pmflowOs.ts +++ b/frontend/src/api/pmflowOs.ts @@ -1,7 +1,7 @@ -import { api } from "@/lib/api"; import type { ChangeItem, PMFlowExecutionContext, PMFlowRiskLevel, ProjectTruthState } from "@/features/pmflow-os/types"; type ApiEnvelope = { code: number; message: string; data: T }; +const API_BASE = "/api"; function unwrap(raw: ApiEnvelope | T): T { if (raw && typeof raw === "object" && "data" in (raw as Record)) { @@ -10,6 +10,51 @@ function unwrap(raw: ApiEnvelope | T): T { return raw as T; } +function extractErrorMessage(payload: unknown, fallback: string): string { + if (payload && typeof payload === "object") { + const record = payload as Record; + if (typeof record.message === "string" && record.message.trim()) return record.message.trim(); + if (typeof record.detail === "string" && record.detail.trim()) return record.detail.trim(); + } + return fallback; +} + +function storedAuthToken(): string | null { + if (typeof window === "undefined") return null; + return window.localStorage.getItem("auth_token") || window.localStorage.getItem("token") || window.localStorage.getItem("access_token"); +} + +async function request(endpoint: string, init: { method: "GET" | "POST"; body?: unknown }): Promise { + const token = storedAuthToken(); + const response = await fetch(`${API_BASE}${endpoint}`, { + method: init.method, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: init.body === undefined ? undefined : JSON.stringify(init.body), + }); + const payload = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(extractErrorMessage(payload, `HTTP ${response.status}`)); + } + if (payload && typeof payload === "object" && "code" in payload) { + const code = typeof (payload as Record).code === "number" ? (payload as Record).code : 0; + if (code !== 0) { + throw new Error(extractErrorMessage(payload, `HTTP ${response.status}`)); + } + } + return unwrap(payload as ApiEnvelope | T); +} + +function get(endpoint: string): Promise { + return request(endpoint, { method: "GET" }); +} + +function post(endpoint: string, body: unknown = {}): Promise { + return request(endpoint, { method: "POST", body }); +} + export interface PMFlowExecutionSummary { projectId: string; execution: PMFlowExecutionContext; @@ -40,6 +85,7 @@ export type PMFlowExecutionProgressEventType = | "manual_progress"; export type PMFlowReleaseGateStatus = "ready" | "blocked" | "needs_confirmation" | "empty"; +export type PMFlowReleaseValidationDecision = "enter_release_candidate" | "keep_blocked"; export interface PMFlowReleaseGateCheck { key: string; @@ -68,9 +114,37 @@ export interface PMFlowReleaseGate { nextActions: string[]; checks: PMFlowReleaseGateCheck[]; evidence: PMFlowReleaseGateEvidence[]; + latestValidationRun?: PMFlowReleaseValidationRun | null; updatedAt?: string | null; } +export interface PMFlowReleaseValidationRun { + id: string; + projectId: string; + charterId?: string | null; + releaseNodeId?: string | null; + status: "ready" | "blocked" | "needs_confirmation" | string; + score?: number | null; + keyMetrics: Record; + riskItems: Array>; + rollbackPlan: Record; + validationSummary: Record; + createdBy?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} + +export interface PMFlowReleaseValidationRunRequest { + decision: PMFlowReleaseValidationDecision; + note?: string | null; +} + +export interface PMFlowReleaseValidationRunResult { + releaseValidationRun: PMFlowReleaseValidationRun; + executionSummary: PMFlowExecutionSummary; + warnings: string[]; +} + export interface PMFlowExecutionHandoffRequest { task_id: string; repo_binding_id?: string | null; @@ -223,26 +297,18 @@ export interface PMFlowChangeSetCommitResult { } export async function getPMFlowTruthState(projectId: string): Promise { - const { data } = await api.get>(`/pmflow-os/projects/${projectId}/truth-state`); - return unwrap(data); + return get(`/pmflow-os/projects/${projectId}/truth-state`); } export async function getPMFlowExecutionSummary(projectId: string): Promise { - const { data } = await api.get>( - `/pmflow-os/projects/${projectId}/execution-summary` - ); - return unwrap(data); + return get(`/pmflow-os/projects/${projectId}/execution-summary`); } export async function createPMFlowExecutionHandoff( projectId: string, payload: PMFlowExecutionHandoffRequest ): Promise { - const { data } = await api.post>( - `/pmflow-os/projects/${projectId}/execution-handoffs`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/projects/${projectId}/execution-handoffs`, payload); } export async function consumePMFlowExecutionHandoff( @@ -250,22 +316,21 @@ export async function consumePMFlowExecutionHandoff( promptPackageId: string, payload: PMFlowExecutionHandoffConsumeRequest = {} ): Promise { - const { data } = await api.post>( - `/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`, payload); } export async function createPMFlowExecutionProgressEvent( projectId: string, payload: PMFlowExecutionProgressEventRequest ): Promise { - const { data } = await api.post>( - `/pmflow-os/projects/${projectId}/execution-progress-events`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/projects/${projectId}/execution-progress-events`, payload); +} + +export async function createPMFlowReleaseValidationRun( + projectId: string, + payload: PMFlowReleaseValidationRunRequest +): Promise { + return post(`/pmflow-os/projects/${projectId}/release-validation-runs`, payload); } export async function createPMFlowThreadMessage( @@ -273,11 +338,7 @@ export async function createPMFlowThreadMessage( threadId: string, payload: { content: string; source?: string; attachments?: Array> } ): Promise { - const { data } = await api.post>( - `/pmflow-os/projects/${projectId}/threads/${threadId}/messages`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/projects/${projectId}/threads/${threadId}/messages`, payload); } export async function createPMFlowExtraction( @@ -292,51 +353,31 @@ export async function createPMFlowExtraction( proposed_items: PMFlowProposedChangeItem[]; } ): Promise { - const { data } = await api.post>( - `/pmflow-os/projects/${projectId}/extractions`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/projects/${projectId}/extractions`, payload); } export async function getPMFlowChangeSet(projectId: string, changeSetId: string): Promise { - const { data } = await api.get>( - `/pmflow-os/projects/${projectId}/change-sets/${changeSetId}` - ); - return unwrap(data); + return get(`/pmflow-os/projects/${projectId}/change-sets/${changeSetId}`); } export async function acceptPMFlowChangeItem(changeItemId: string): Promise { - const { data } = await api.post>(`/pmflow-os/change-items/${changeItemId}/accept`); - return unwrap(data); + return post(`/pmflow-os/change-items/${changeItemId}/accept`); } export async function editPMFlowChangeItem( changeItemId: string, payload: { summary?: string; diff?: string; payload_patch?: Record; reason?: string } ): Promise { - const { data } = await api.post>( - `/pmflow-os/change-items/${changeItemId}/edit`, - payload - ); - return unwrap(data); + return post(`/pmflow-os/change-items/${changeItemId}/edit`, payload); } export async function deferPMFlowChangeItem(changeItemId: string, reason?: string): Promise { - const { data } = await api.post>( - `/pmflow-os/change-items/${changeItemId}/defer`, - reason ? { reason } : {} - ); - return unwrap(data); + return post(`/pmflow-os/change-items/${changeItemId}/defer`, reason ? { reason } : {}); } export async function commitPMFlowChangeSet( changeSetId: string, payload: { item_ids?: string[]; mode?: "atomic"; note?: string } = {} ): Promise { - const { data } = await api.post>( - `/pmflow-os/change-sets/${changeSetId}/commit`, - { mode: "atomic", ...payload } - ); - return unwrap(data); + return post(`/pmflow-os/change-sets/${changeSetId}/commit`, { mode: "atomic", ...payload }); } diff --git a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx index a437fc03..6472f827 100644 --- a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx +++ b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx @@ -20,7 +20,11 @@ import { } from "lucide-react"; import type { ReactNode } from "react"; import { Button } from "@/components/ui/button"; -import type { PMFlowReleaseGateEvidence } from "@/api/pmflowOs"; +import type { + PMFlowReleaseGateEvidence, + PMFlowReleaseValidationDecision, + PMFlowReleaseValidationRun, +} from "@/api/pmflowOs"; import { usePMFlowOsStore } from "@/features/pmflow-os/store"; import { cn } from "@/lib/utils"; @@ -337,6 +341,20 @@ function gateLabel(status?: string) { return "缺少执行证据"; } +function validationRunLabel(status?: string) { + if (status === "ready") return "发布候选"; + if (status === "blocked") return "保持阻塞"; + if (status === "needs_confirmation") return "需要确认"; + return "未记录"; +} + +function validationSummaryText(run?: PMFlowReleaseValidationRun | null) { + if (!run) return "尚未创建 release validation run。"; + const summary = run.validationSummary || {}; + const sourceStatus = typeof summary.releaseGateStatus === "string" ? summary.releaseGateStatus : run.status; + return `基于 ${sourceStatus} release gate 固化,部署执行:${summary.deploymentExecuted ? "已执行" : "未执行"}`; +} + function checkTone(status?: string): StatusTone { if (status === "passed") return "success"; if (status === "failed") return "danger"; @@ -388,11 +406,21 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { executionSummaryError, projectionWarnings, refetchExecutionSummary, + createReleaseValidationRun, } = store; const gate = executionSummary?.releaseGate; + const latestValidationRun = gate?.latestValidationRun; const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading"; const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])]; const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate; + const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length); + const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => { + const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞"; + createReleaseValidationRun({ + decision, + note: `ReleaseGrowthPage 人工确认:${decisionLabel};PMFlow 仅记录发布前验证,不执行部署。`, + }); + }; return (
@@ -507,6 +535,60 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
+ + +
+
+
+ +

{validationSummaryText(latestValidationRun)}

+
+ {latestValidationRun?.createdAt ? ( +
+ {new Date(latestValidationRun.createdAt).toLocaleString("zh-CN")} +
+ ) : null} +
+
+ 记录会固化当前 evidence、阻塞原因、下一步动作和人工确认结果;这里不会触发真实部署。 +
+
+
+ {gate?.status === "blocked" ? ( + + ) : ( + <> + + + + )} + {!canCreateValidationRun ? ( +
+ 缺少真实执行证据,不能创建成功的发布验证记录。 +
+ ) : null} +
+
+
diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index 72b99772..23e83f50 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -9,6 +9,7 @@ import { consumePMFlowExecutionHandoff, createPMFlowExecutionHandoff, createPMFlowExecutionProgressEvent, + createPMFlowReleaseValidationRun, createPMFlowThreadMessage, deferPMFlowChangeItem, editPMFlowChangeItem, @@ -31,6 +32,7 @@ vi.mock("@/api/pmflowOs", () => ({ consumePMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionProgressEvent: vi.fn(), + createPMFlowReleaseValidationRun: vi.fn(), editPMFlowChangeItem: vi.fn(), deferPMFlowChangeItem: vi.fn(), })); @@ -41,6 +43,7 @@ const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff); const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff); const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent); +const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet); const editItemMock = vi.mocked(editPMFlowChangeItem); @@ -329,6 +332,63 @@ describe("PMFlowOsProvider real change review actions", () => { recentDeliveries: [], warnings: [], }); + createReleaseValidationRunMock.mockResolvedValue({ + releaseValidationRun: { + id: "901", + projectId: "123", + status: "needs_confirmation", + score: 0.6, + keyMetrics: { evidenceCount: 1 }, + riskItems: [], + rollbackPlan: { deploymentExecuted: false }, + validationSummary: { + source: "pmflow_os_release_gate", + releaseGateStatus: "needs_confirmation", + deploymentExecuted: false, + }, + createdBy: "1", + createdAt: "2026-04-26T00:00:00", + }, + executionSummary: { + projectId: "123", + execution: { + taskId: "task-99", + status: "testing", + prompt: "真实 prompt package", + commitLog: ["发布验证记录已创建"], + prStatus: "PR #88 · linked", + testSummary: "真实测试回写", + writebackStatus: "testing", + }, + releaseGate: { + ...releaseGate(), + latestValidationRun: { + id: "901", + projectId: "123", + status: "needs_confirmation", + score: 0.6, + keyMetrics: { evidenceCount: 1 }, + riskItems: [], + rollbackPlan: { deploymentExecuted: false }, + validationSummary: { + source: "pmflow_os_release_gate", + releaseGateStatus: "needs_confirmation", + deploymentExecuted: false, + }, + createdBy: "1", + createdAt: "2026-04-26T00:00:00", + }, + }, + devSync: { active_task_count: 1 }, + promptPackage: null, + repoBindings: [], + activeDevTasks: [], + recentProgressEvents: [], + recentDeliveries: [], + warnings: [], + }, + warnings: [], + }); }); afterEach(() => { @@ -405,6 +465,19 @@ describe("PMFlowOsProvider real change review actions", () => { ) ); + act(() => + result.current.createReleaseValidationRun({ + decision: "enter_release_candidate", + note: "人工确认进入发布候选", + }) + ); + await waitFor(() => + expect(createReleaseValidationRunMock).toHaveBeenCalledWith( + "123", + expect.objectContaining({ decision: "enter_release_candidate", note: "人工确认进入发布候选" }) + ) + ); + expect(result.current.state.requirements).toEqual([]); }); @@ -430,6 +503,7 @@ describe("PMFlowOsProvider real change review actions", () => { expect(createExecutionHandoffMock).not.toHaveBeenCalled(); expect(consumeExecutionHandoffMock).not.toHaveBeenCalled(); expect(createExecutionProgressEventMock).not.toHaveBeenCalled(); + expect(createReleaseValidationRunMock).not.toHaveBeenCalled(); expect(getExecutionSummaryMock).not.toHaveBeenCalled(); expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted"); }); diff --git a/frontend/src/features/pmflow-os/store.tsx b/frontend/src/features/pmflow-os/store.tsx index 8f9fad1f..b9042156 100644 --- a/frontend/src/features/pmflow-os/store.tsx +++ b/frontend/src/features/pmflow-os/store.tsx @@ -7,6 +7,7 @@ import { createPMFlowExecutionHandoff, createPMFlowExecutionProgressEvent, createPMFlowExtraction, + createPMFlowReleaseValidationRun, createPMFlowThreadMessage, deferPMFlowChangeItem, editPMFlowChangeItem, @@ -16,6 +17,7 @@ import { type PMFlowExecutionProgressEventRequest, type PMFlowExecutionSummary, type PMFlowProposedChangeItem, + type PMFlowReleaseValidationRunRequest, } from "@/api/pmflowOs"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; import type { @@ -537,6 +539,7 @@ interface PMFlowOsStoreValue { createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void; consumeExecutionHandoff: (promptPackageId: string) => void; recordExecutionProgress: (payload: PMFlowExecutionProgressEventRequest) => void; + createReleaseValidationRun: (payload: PMFlowReleaseValidationRunRequest) => void; advanceExecution: () => void; writeBackExecution: (outcome: "passed" | "failed") => void; publishRelease: () => void; @@ -767,6 +770,11 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { } mutateDemoOnly((current) => advanceExecutionMutation(current)); }, + createReleaseValidationRun: (payload) => { + if (dataSource.mode === "api" && dataSource.projectId) { + runApiAction(() => createPMFlowReleaseValidationRun(dataSource.projectId || "", payload)); + } + }, advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),