From ea8f9d082ef3de3bad840d40d5c24df19511eb69 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 26 Apr 2026 14:20:48 +0800 Subject: [PATCH] feat(pmflow-os): draft changesets from iteration recommendations --- CLAUDE.md | 6 +- backend/app/api/pmflow_os_api/__init__.py | 34 ++ backend/app/schemas/pmflow_os.py | 4 + .../services/pmflow_os_projection_service.py | 412 +++++++++++++++++- .../tests/test_pmflow_os_projection_api.py | 55 +++ docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md | 38 ++ docs/architecture/ARCHITECTURE_INVENTORY.md | 6 +- docs/architecture/CODE_REFERENCE_INDEX.md | 8 +- frontend/src/api/pmflowOs.ts | 22 + .../pmflow-os/pages/ReleaseGrowthPage.tsx | 38 +- .../src/features/pmflow-os/store.api.test.tsx | 67 ++- frontend/src/features/pmflow-os/store.tsx | 17 + 12 files changed, 688 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 13208380..fa87995d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,8 +113,8 @@ frontend/ - `frontend/src/features/personal-space/`(个人空间首页;通过 `frontend/src/api/space.ts` 只读聚合项目摘要、PMFlow OS truth-state / execution-summary,承载继续推进、待确认、AI 正在推进、草案箱和最近项目入口) - `frontend/src/features/project-launch/`(首次进入页 / 新项目启动台;默认调用真实 `frontend/src/api/pmflowLaunch.ts` draft/confirm API 创建项目,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 演示状态机) - `frontend/src/features/account-settings/`(`/me` 个人中心与设置;通过 `frontend/src/api/accountSettings.ts` 管理个人资料、默认语言、AI 回答深度、证据展示偏好和通知偏好,移动端使用分组列表与 bottom sheet,不进入 auth core) - - `frontend/src/features/pmflow-os/`(PMFlow OS 前端产品域;共享 truth store 已支持 `?projectId=` 读取真实后端 projection,真实模式下 message、`proposed_items` extraction、ChangeItem `accept/edit/defer` 与 ChangeSet `commit` 调后端,显式 `?demo=1` 才启用本地演示状态机) - - `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` + `backend/app/models/pmflow_change_review.py`(PMFlow OS A3/C1/C2 真实路径;提供 `truth-state`、`execution-summary`、thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`、ChangeSet `commit`、execution handoff/progress writeback、release validation run 与 release feedback ingest;复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync / FeedbackIngest / IterationRecommendation` 并投影 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C2 commit 默认 atomic,`ModelDecision / MetricSignal` 暂 fail-closed) + - `frontend/src/features/pmflow-os/`(PMFlow OS 前端产品域;共享 truth store 已支持 `?projectId=` 读取真实后端 projection,真实模式下 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit` 与 iteration recommendation -> ChangeSet draft 调后端,显式 `?demo=1` 才启用本地演示状态机) + - `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` + `backend/app/models/pmflow_change_review.py`(PMFlow OS A3/C1/C2 真实路径;提供 `truth-state`、`execution-summary`、thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`、ChangeSet `commit`、execution handoff/progress writeback、release validation run、release feedback ingest 与 recommendation change-set draft;复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync / FeedbackIngest / IterationRecommendation` 并投影 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C2 commit 默认 atomic,`ModelDecision / MetricSignal` 暂 fail-closed) - `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`(Project Launch Phase B 真实创建链;`drafts` 只写 `pmflow_launch_drafts`,`confirm` 在同一事务写真实 Project、启动线程、启动消息、初始 truth_nodes、tasks 与 semantic_facts seed) - `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`(PMFlow OS 产品合同真源;真实后端接入前先读,锁定本地 truth store 对象与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的映射;Phase C1 review 表和审查 API 已实现,Phase C2 backend/frontend commit 已实现) - `frontend/src/pages/inbox/`(消息台) @@ -134,7 +134,7 @@ frontend/ - `frontend/src/features/project-launch/` 是新项目启动台独立产品域:`types.ts / useProjectLaunchState.ts` 承载状态合同和真实 API 编排,`mockData.ts` 只在显式 demo 模式下使用;默认路径调用 `POST /pmflow-launch/drafts` 生成可审查草案,确认后调用 `POST /pmflow-launch/drafts/{draft_id}/confirm` 创建真实项目并跳转 `/pmflow-os?projectId=`。`LaunchShell` 桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,主画布仍由 `IntentInputPanel / AIProgressCanvas / HumanReviewCanvas / DraftEditCanvas / FailureStatePanel / SuccessTransitionPanel` 承接,移动端仍由 `MobileLaunchLayout` 承接;`frontend/src/pages/ProjectLaunchPage.tsx` 仅做薄入口。 - `frontend/src/features/personal-space/` 是空间层入口:`PersonalSpacePage.tsx` 承接桌面个人工作面与独立移动单手布局,`frontend/src/api/space.ts` 只读聚合 `projects` summary、PMFlow OS `truth-state`、`execution-summary` 并生成继续推进、待确认、发布关注、发布后反馈关注、AI 正在推进、草案箱、最近项目摘要;Space P3 的发布关注优先消费 `truth-state.releases` 的 release validation 投影,没有验证投影时才回落 `execution-summary.releaseGate`;Space P4 的发布后反馈关注只读消费 `execution-summary.releaseSignals`,点击只跳 `/release-growth?projectId=`;不在 Space 写 Project Memory、ProgressEvent、ReleaseValidationRun、FeedbackIngest、RevenueEvent、ComplianceCheckRun 或发布执行。`components/SpaceChrome.tsx` 承接空间层左侧栏、顶部条与空间用量组件,并被 `/project-launch` 桌面首次新建壳复用;`frontend/src/pages/PersonalSpacePage.tsx` 仅做薄入口。 - `frontend/src/features/account-settings/` 是 Account Settings 独立产品域:`AccountSettingsPanel.tsx` 承接 `/me` 桌面设置页、移动端分组列表与 bottom sheet,`frontend/src/api/accountSettings.ts` 定义个人资料、默认语言、AI 回答深度、证据展示偏好和四类通知偏好合同;当前已有账号同步 API fallback、本地 UI 通知消费模型、Release/Growth 偏好 helper 和 post-release 偏好 helper,`releaseGate / failureAlert / pendingConfirmation` 只影响页面内提醒表达,不代表真实邮件、桌面推送或系统通知中心已接入,不改 OAuth/JWT/auth core;`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 仅做薄入口并复用既有退出登录 bottom sheet。 -- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit`、Execution Handoff create/consume、execution progress writeback、release validation run create 与 release feedback ingest API,成功后 refetch truth-state / execution-summary;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ExecutionBridgePage` 已支持创建 handoff、运行端领取确认和执行进度回写;`ReleaseGrowthPage` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,可创建 release validation run、摄取发布后 feedback,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据、审计状态或发布后信号;静态发布/增长原型仅保留在显式 demo 模式;后端只记录 progress/release gate/release validation/release feedback 元数据,不执行本机命令、真实发布命令、外部支付或真实推送;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。 +- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit`、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest 与 iteration recommendation -> ChangeSet draft API,成功后 refetch truth-state / execution-summary;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ExecutionBridgePage` 已支持创建 handoff、运行端领取确认和执行进度回写;`ReleaseGrowthPage` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,可创建 release validation run、摄取发布后 feedback、从真实 recommendation 生成下一轮待确认 ChangeSet draft,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据、审计状态或发布后信号;静态发布/增长原型仅保留在显式 demo 模式;后端只记录 progress/release gate/release validation/release feedback/recommendation draft 元数据,不执行本机命令、真实发布命令、外部支付、真实推送、自动 Project Memory commit 或正式任务创建;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。 - 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。 ### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后) diff --git a/backend/app/api/pmflow_os_api/__init__.py b/backend/app/api/pmflow_os_api/__init__.py index 722e2f27..dae23e14 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, + PMFlowIterationRecommendationChangeSetDraftRequest, PMFlowReleaseFeedbackIngestRequest, PMFlowReleaseValidationRunCreateRequest, PMFlowThreadMessageCreateRequest, @@ -385,6 +386,39 @@ async def create_pmflow_os_release_feedback_ingest( return success_response(payload) +@router.post("/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts") +async def create_pmflow_os_iteration_recommendation_change_set_draft( + project_id: int, + recommendation_id: int, + body: PMFlowIterationRecommendationChangeSetDraftRequest | None = None, + 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_iteration_recommendation_change_set_draft( + project_id=int(project_id), + recommendation_id=int(recommendation_id), + body=body or PMFlowIterationRecommendationChangeSetDraftRequest(), + current_user_id=int(current_user.id), + ) + except ValueError as exc: + code = str(exc) + if code == "iteration_recommendation_not_found": + return JSONResponse(status_code=404, content=error_response(404, "iteration_recommendation_not_found")) + 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 1a7e8467..395b597d 100644 --- a/backend/app/schemas/pmflow_os.py +++ b/backend/app/schemas/pmflow_os.py @@ -146,6 +146,10 @@ class PMFlowReleaseFeedbackIngestRequest(BaseModel): samples: list[dict[str, Any]] = Field(default_factory=list) +class PMFlowIterationRecommendationChangeSetDraftRequest(BaseModel): + 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 be3518b3..c57d8178 100644 --- a/backend/app/services/pmflow_os_projection_service.py +++ b/backend/app/services/pmflow_os_projection_service.py @@ -43,6 +43,7 @@ from ..schemas.pmflow_os import ( PMFlowExecutionProgressEventRequest, PMFlowExecutionContext, PMFlowFeedEvent, + PMFlowIterationRecommendationChangeSetDraftRequest, PMFlowMetricSignal, PMFlowModelDecision, PMFlowProjectMemory, @@ -377,6 +378,149 @@ class PMFlowOSProjectionService: "warnings": list(release_signals.get("warnings") or []), } + async def create_iteration_recommendation_change_set_draft( + self, + *, + project_id: int, + recommendation_id: int, + body: PMFlowIterationRecommendationChangeSetDraftRequest, + current_user_id: Optional[int] = None, + ) -> dict[str, Any]: + project = await self._load_project(project_id=project_id) + recommendation = await self._load_iteration_recommendation( + project_id=project_id, + recommendation_id=recommendation_id, + ) + summary_payload = dict(recommendation.recommendation_summary or {}) + existing_change_set_id = self._optional_int( + summary_payload.get("changeSetId"), + error_code="iteration_recommendation_not_found", + ) + if existing_change_set_id is not None: + existing = await self._load_recommendation_change_set( + project_id=project_id, + change_set_id=existing_change_set_id, + ) + if existing is not None: + detail = await PMFlowChangeReviewService(self.db, tenant_id=self.tenant_id).get_change_set_detail( + project_id=project_id, + change_set_id=int(existing.id), + ) + updated_summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + return { + "changeSetDetail": detail, + "executionSummary": updated_summary.model_dump(mode="json"), + "warnings": ["iteration_recommendation_changeset_reused"], + } + + feedback = await self._load_feedback_ingest( + project_id=project_id, + feedback_ingest_id=recommendation.feedback_ingest_id, + ) + thread = await self._resolve_or_create_iteration_thread(project=project) + message = PMFlowThreadMessage( + tenant_id=self.tenant_id, + thread_id=int(thread.id), + project_id=int(project_id), + role="assistant", + content=self._iteration_recommendation_message( + recommendation=recommendation, + feedback=feedback, + note=body.note, + ), + source="pmflow_os", + attachments=[], + message_metadata={ + "source": "iteration_recommendation", + "recommendation_id": str(recommendation.id), + "feedback_ingest_id": None if feedback is None else str(feedback.id), + "created_by": None if current_user_id is None else int(current_user_id), + "auto_commit": False, + "note": body.note, + }, + status="sent", + ) + self.db.add(message) + await self.db.flush() + + items = self._iteration_recommendation_change_items( + recommendation=recommendation, + feedback=feedback, + project_id=project_id, + ) + change_set = PMFlowChangeSet( + tenant_id=self.tenant_id, + project_id=int(project_id), + thread_id=int(thread.id), + source_message_id=int(message.id), + status="open", + summary=self._iteration_change_set_summary(recommendation=recommendation, feedback=feedback), + headline=self._iteration_change_set_headline(recommendation=recommendation, feedback=feedback), + extraction_scope="iteration_recommendation", + routing_meta={ + "source": "release_feedback_iteration_loop", + "recommendationId": str(recommendation.id), + "feedbackIngestId": None if feedback is None else str(feedback.id), + "releaseFeedbackLoop": True, + "autoCommit": False, + "note": body.note, + }, + llm_call_id=None, + created_by=int(current_user_id or project.owner_id), + ) + self.db.add(change_set) + await self.db.flush() + + for item in items: + item.change_set_id = int(change_set.id) + self.db.add(item) + await self.db.flush() + + evidence = PMFlowEvidenceLink( + tenant_id=self.tenant_id, + project_id=int(project_id), + change_set_id=int(change_set.id), + change_item_id=None, + source_type="iteration_recommendation", + source_id=str(recommendation.id), + title="发布后反馈迭代建议", + excerpt=self._iteration_recommendation_excerpt(recommendation=recommendation, feedback=feedback), + display_label="Release Feedback", + evidence_metadata={ + "recommendation_id": str(recommendation.id), + "feedback_ingest_id": None if feedback is None else str(feedback.id), + "feedback_title": None if feedback is None else feedback.title, + "priority": recommendation.priority, + "change_set_created": True, + }, + ) + self.db.add(evidence) + + summary_payload.update( + { + "changeSetId": str(change_set.id), + "changeSetCreated": True, + "changeSetCreatedAt": now_cn_naive().isoformat(), + "changeSetCreatedBy": None if current_user_id is None else str(current_user_id), + } + ) + recommendation.recommendation_summary = summary_payload + recommendation.status = "changeset_drafted" + flag_modified(recommendation, "recommendation_summary") + await self.db.commit() + await self.db.refresh(change_set) + + detail = await PMFlowChangeReviewService(self.db, tenant_id=self.tenant_id).get_change_set_detail( + project_id=project_id, + change_set_id=int(change_set.id), + ) + updated_summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + return { + "changeSetDetail": detail, + "executionSummary": updated_summary.model_dump(mode="json"), + "warnings": [], + } + async def consume_execution_handoff( self, *, @@ -546,6 +690,69 @@ class PMFlowOSProjectionService: ) return result.scalar_one_or_none() + async def _load_iteration_recommendation(self, *, project_id: int, recommendation_id: int) -> IterationRecommendation: + result = await self.db.execute( + select(IterationRecommendation).where( + and_( + IterationRecommendation.tenant_id == self.tenant_id, + IterationRecommendation.project_id == int(project_id), + IterationRecommendation.id == int(recommendation_id), + ) + ) + ) + row = result.scalar_one_or_none() + if row is None: + raise ValueError("iteration_recommendation_not_found") + return row + + async def _load_recommendation_change_set(self, *, project_id: int, change_set_id: int) -> Optional[PMFlowChangeSet]: + result = await self.db.execute( + select(PMFlowChangeSet).where( + and_( + PMFlowChangeSet.tenant_id == self.tenant_id, + PMFlowChangeSet.project_id == int(project_id), + PMFlowChangeSet.id == int(change_set_id), + ) + ) + ) + return result.scalar_one_or_none() + + async def _load_feedback_ingest(self, *, project_id: int, feedback_ingest_id: Optional[int]) -> Optional[FeedbackIngest]: + if feedback_ingest_id is None: + return None + result = await self.db.execute( + select(FeedbackIngest).where( + and_( + FeedbackIngest.tenant_id == self.tenant_id, + FeedbackIngest.project_id == int(project_id), + FeedbackIngest.id == int(feedback_ingest_id), + ) + ) + ) + return result.scalar_one_or_none() + + async def _resolve_or_create_iteration_thread(self, *, project: Project) -> PMFlowThreadModel: + result = await self.db.execute( + select(PMFlowThreadModel) + .where(and_(PMFlowThreadModel.tenant_id == self.tenant_id, PMFlowThreadModel.project_id == int(project.id))) + .order_by(PMFlowThreadModel.created_at.asc(), PMFlowThreadModel.id.asc()) + .limit(1) + ) + row = result.scalar_one_or_none() + if row is not None: + return row + row = PMFlowThreadModel( + tenant_id=self.tenant_id, + project_id=int(project.id), + title=f"{project.title or project.name} 项目线程", + objective=self._project_goal(project=project, truth_nodes=[]), + status="active", + source="iteration_loop", + ) + self.db.add(row) + await self.db.flush() + return row + async def _build_release_signals( self, *, @@ -600,7 +807,7 @@ class PMFlowOSProjectionService: "latestValidationRunId": None if latest_release_validation is None else str(latest_release_validation.id), "latestValidationRunStatus": None if latest_release_validation is None else latest_release_validation.status, "source": "pmflow_os_release_growth", - "changeSetCreated": False, + "changeSetCreated": any(bool(item.get("changeSetCreated")) for item in recommendations), }, "summary": { "feedbackCount": len(feedback_items), @@ -1676,6 +1883,9 @@ class PMFlowOSProjectionService: } def _serialize_iteration_recommendation(self, recommendation: IterationRecommendation) -> dict[str, Any]: + summary = dict(recommendation.recommendation_summary or {}) + change_set_id = summary.get("changeSetId") + normalized_change_set_id = None if change_set_id is None or str(change_set_id).strip() == "" else str(change_set_id) return { "id": str(recommendation.id), "projectId": str(recommendation.project_id), @@ -1683,14 +1893,205 @@ class PMFlowOSProjectionService: "feedbackIngestId": None if recommendation.feedback_ingest_id is None else str(recommendation.feedback_ingest_id), "status": recommendation.status, "priority": recommendation.priority, - "recommendationSummary": dict(recommendation.recommendation_summary or {}), + "recommendationSummary": summary, "proposedCharterPatch": dict(recommendation.proposed_charter_patch or {}), "proposedTasks": list(recommendation.proposed_tasks or []), - "changeSetCreated": False, + "changeSetCreated": bool(summary.get("changeSetCreated") or normalized_change_set_id), + "changeSetId": normalized_change_set_id, "createdAt": recommendation.created_at.isoformat() if recommendation.created_at else None, "updatedAt": recommendation.updated_at.isoformat() if recommendation.updated_at else None, } + def _iteration_recommendation_message( + self, + *, + recommendation: IterationRecommendation, + feedback: Optional[FeedbackIngest], + note: Optional[str], + ) -> str: + summary = self._recommendation_summary_text(recommendation) + task_titles = [ + str(item.get("title") or item.get("summary") or "").strip() + for item in list(recommendation.proposed_tasks or []) + if isinstance(item, dict) and str(item.get("title") or item.get("summary") or "").strip() + ] + lines = [ + "Release/Growth 反馈已生成下一轮待确认变更草稿。", + f"反馈:{feedback.title or feedback.content[:80] if feedback is not None else '未绑定 feedback ingest'}", + f"建议:{summary}", + ] + if task_titles: + lines.append("建议任务:" + ";".join(task_titles[:5])) + if note: + lines.append(f"人工备注:{note.strip()}") + lines.append("此消息只作为 Change Review 证据,不自动写入 Project Memory。") + return "\n".join(lines) + + def _iteration_change_set_headline( + self, + *, + recommendation: IterationRecommendation, + feedback: Optional[FeedbackIngest], + ) -> str: + source = feedback.title if feedback is not None and feedback.title else f"Recommendation {recommendation.id}" + return f"下一轮反馈变更草稿:{source}"[:255] + + def _iteration_change_set_summary( + self, + *, + recommendation: IterationRecommendation, + feedback: Optional[FeedbackIngest], + ) -> str: + source = feedback.title or feedback.content[:80] if feedback is not None else "发布后反馈" + return f"基于 {source} 生成下一轮 ChangeSet draft,等待 Change Review accept/edit/defer。" + + def _iteration_recommendation_excerpt( + self, + *, + recommendation: IterationRecommendation, + feedback: Optional[FeedbackIngest], + ) -> str: + feedback_text = "" if feedback is None else feedback.content.strip() + return (feedback_text or self._recommendation_summary_text(recommendation))[:1200] + + def _recommendation_summary_text(self, recommendation: IterationRecommendation) -> str: + summary = dict(recommendation.recommendation_summary or {}) + value = summary.get("summary") or summary.get("recommendation") or summary.get("title") + if value: + return str(value).strip() + focus = summary.get("focus_areas") or summary.get("focusAreas") + if isinstance(focus, list) and focus: + return "围绕 " + "、".join(str(item) for item in focus[:4]) + " 推进下一轮迭代。" + return "基于发布后反馈生成下一轮迭代建议。" + + def _iteration_recommendation_change_items( + self, + *, + recommendation: IterationRecommendation, + feedback: Optional[FeedbackIngest], + project_id: int, + ) -> list[PMFlowChangeItem]: + patch = dict(recommendation.proposed_charter_patch or {}) + summary_text = self._recommendation_summary_text(recommendation) + focus_areas = patch.get("focus_areas") or patch.get("focusAreas") or dict(recommendation.recommendation_summary or {}).get("focus_areas") or [] + success_criteria = patch.get("updated_success_criteria") or patch.get("successCriteria") or [] + risk_level = "high" if str(recommendation.priority or "").lower() in {"high", "critical"} else "medium" + items: list[PMFlowChangeItem] = [ + PMFlowChangeItem( + tenant_id=self.tenant_id, + change_set_id=0, + project_id=int(project_id), + group="new_object", + object_type="Requirement", + object_id=None, + title="下一轮迭代方向", + summary=summary_text, + diff="根据发布后反馈补充下一轮需求方向与成功标准。", + payload={ + "title": "下一轮迭代方向", + "summary": summary_text, + "stage": "planning", + "status": "draft", + "source": "iteration_recommendation", + "recommendationId": str(recommendation.id), + "feedbackIngestId": None if feedback is None else str(feedback.id), + "focusAreas": list(focus_areas) if isinstance(focus_areas, list) else [str(focus_areas)], + "acceptanceCriteria": list(success_criteria) if isinstance(success_criteria, list) else [str(success_criteria)], + }, + edited_payload=None, + confidence=0.76, + risk_level=risk_level, + status="pending_review", + ), + PMFlowChangeItem( + tenant_id=self.tenant_id, + change_set_id=0, + project_id=int(project_id), + group="decision", + object_type="Decision", + object_id=None, + title="确认下一轮反馈驱动迭代方向", + summary=f"确认是否将 recommendation {recommendation.id} 纳入下一轮 Change Review。", + diff="将发布后反馈从 Release/Growth 回流到 Change Review,而不是直接写入项目记忆。", + payload={ + "title": "确认下一轮反馈驱动迭代方向", + "summary": summary_text, + "stage": "growth", + "owner": "PMFlow", + "source": "iteration_recommendation", + "recommendationId": str(recommendation.id), + }, + edited_payload=None, + confidence=0.72, + risk_level="medium", + status="pending_review", + ), + ] + + for raw_task in list(recommendation.proposed_tasks or [])[:4]: + if not isinstance(raw_task, dict): + continue + title = str(raw_task.get("title") or raw_task.get("summary") or "").strip() + if not title: + continue + task_summary = str(raw_task.get("summary") or raw_task.get("description") or title).strip() + items.append( + PMFlowChangeItem( + tenant_id=self.tenant_id, + change_set_id=0, + project_id=int(project_id), + group="new_object", + object_type="Task", + object_id=None, + title=title[:255], + summary=task_summary, + diff="新增下一轮可审查任务草稿,待 Change Review 决定是否接受。", + payload={ + "title": title, + "description": task_summary, + "summary": task_summary, + "stage": "development", + "status": "draft", + "priority": raw_task.get("priority") or recommendation.priority, + "source": "iteration_recommendation", + "recommendationId": str(recommendation.id), + }, + edited_payload=None, + confidence=0.7, + risk_level="medium", + status="pending_review", + ) + ) + + if risk_level == "high": + items.append( + PMFlowChangeItem( + tenant_id=self.tenant_id, + change_set_id=0, + project_id=int(project_id), + group="risk", + object_type="Risk", + object_id=None, + title="发布后反馈高优先级风险", + summary=summary_text, + diff="将高优先级反馈沉淀为风险项,避免直接跳过人工审查。", + payload={ + "title": "发布后反馈高优先级风险", + "summary": summary_text, + "stage": "planning", + "level": "high", + "mitigation": "在 Change Review 中确认处理策略后再进入 Project Memory。", + "source": "iteration_recommendation", + "recommendationId": str(recommendation.id), + }, + edited_payload=None, + confidence=0.74, + risk_level="high", + status="pending_review", + ) + ) + return items + def _serialize_revenue_event(self, event: RevenueEvent) -> dict[str, Any]: return { "id": str(event.id), @@ -1717,7 +2118,10 @@ class PMFlowOSProjectionService: if not feedback_items: actions.append("发布后先摄取用户反馈、运营记录或人工观察,避免用 mock 增长数据判断下一轮。") if feedback_items and recommendations: - actions.append("基于真实反馈查看 iteration recommendation,但本阶段不自动创建 ChangeSet。") + if any(bool(item.get("changeSetCreated")) for item in recommendations): + actions.append("下一轮 ChangeSet draft 已进入 Change Review,继续 accept / edit / defer 后再决定是否写入 Project Memory。") + else: + actions.append("基于真实反馈选择 iteration recommendation,生成下一轮待确认 ChangeSet draft。") if str(release_gate.get("status") or "") == "blocked": actions.append("发布门禁仍阻塞时,先处理 release gate,再把反馈作为下一轮规划输入。") if not actions: diff --git a/backend/tests/test_pmflow_os_projection_api.py b/backend/tests/test_pmflow_os_projection_api.py index cfb748c9..ef328b11 100644 --- a/backend/tests/test_pmflow_os_projection_api.py +++ b/backend/tests/test_pmflow_os_projection_api.py @@ -1057,6 +1057,61 @@ async def test_release_feedback_ingest_creates_signal_and_iteration_recommendati assert len(recommendation_rows) == 1 +@pytest.mark.asyncio +async def test_iteration_recommendation_creates_reusable_changeset_draft(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: + ingest_response = await client.post( + f"/pmflow-os/projects/{project.id}/release-feedback-ingests", + json={ + "source_type": "manual", + "title": "发布后反馈回流", + "content": "用户反馈首次配置成功率低,希望下一轮优化 onboarding 和错误提示。", + "metrics": {"negative_feedback_count": 5}, + }, + ) + recommendation_id = ingest_response.json()["data"]["recommendation"]["id"] + draft_response = await client.post( + f"/pmflow-os/projects/{project.id}/iteration-recommendations/{recommendation_id}/change-set-drafts", + json={"note": "进入下一轮待确认变更"}, + ) + reuse_response = await client.post( + f"/pmflow-os/projects/{project.id}/iteration-recommendations/{recommendation_id}/change-set-drafts", + json={}, + ) + truth_response = await client.get(f"/pmflow-os/projects/{project.id}/truth-state") + + assert draft_response.status_code == 200 + payload = draft_response.json()["data"] + detail = payload["changeSetDetail"] + assert detail["changeSet"]["status"] == "open" + assert detail["changeSet"]["rawStatus"] == "open" + assert detail["evidence"][0]["sourceType"] == "iteration_recommendation" + object_types = {item["objectType"] for item in detail["items"]} + assert object_types.issubset({"Requirement", "Task", "Risk", "Decision"}) + assert {"Requirement", "Task", "Decision"}.issubset(object_types) + assert all(item["status"] == "pending_review" for item in detail["items"]) + assert payload["executionSummary"]["releaseSignals"]["relationship"]["changeSetCreated"] is True + assert payload["executionSummary"]["releaseSignals"]["recommendations"][0]["changeSetCreated"] is True + assert payload["executionSummary"]["releaseSignals"]["recommendations"][0]["changeSetId"] == detail["changeSet"]["id"] + + assert reuse_response.status_code == 200 + assert reuse_response.json()["data"]["changeSetDetail"]["changeSet"]["id"] == detail["changeSet"]["id"] + assert "iteration_recommendation_changeset_reused" in reuse_response.json()["data"]["warnings"] + truth_payload = truth_response.json()["data"] + assert truth_payload["activeChangeSetId"] == detail["changeSet"]["id"] + assert truth_payload["changeItems"] + assert all(item["status"] == "pending_review" for item in truth_payload["changeItems"]) + + change_sets = (await session.execute(select(PMFlowChangeSet).where(PMFlowChangeSet.project_id == project.id))).scalars().all() + tasks = (await session.execute(select(Task).where(Task.project_id == project.id))).scalars().all() + assert len(change_sets) == 1 + assert tasks == [] + + @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 219334b8..99604e71 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -930,6 +930,44 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 - iteration recommendation 目前作为建议底座展示,不自动生成 ChangeSet;下一轮如果要从 recommendation 进入待确认中心,需要单独开 Main Flow packet。 - 全量前端 typecheck/build 需在包含 `frontend/src/lib/*` helper 的集成环境或由 Shell / Integration owner 修正该 ignored 文件边界后重新验证。 +### 2026-04-26 / iteration-loop-p1-recommendation-to-changeset-draft / implemented + +完成内容: + +- 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts`,复用项目成员/OWNER/REVIEWER 权限边界和统一 `{code,message,data}` 包络。 +- 后端从真实 `IterationRecommendation` 生成 open `PMFlowChangeSet`、pending_review `PMFlowChangeItem` 和 `PMFlowEvidenceLink(source_type='iteration_recommendation')`;item 类型限定在 `Requirement / Task / Risk / Decision`,不自动 commit、不写 Project Memory、不创建正式 Task。 +- recommendation 的 `recommendation_summary` 会记录 `changeSetId / changeSetCreated / changeSetCreatedAt`,重复调用复用既有 draft 并返回 `iteration_recommendation_changeset_reused` warning。 +- `execution-summary.releaseSignals` 会把 recommendation 的 `changeSetCreated / changeSetId` 和 relationship 聚合状态投影给前端,nextActions 从“只展示建议”推进为“生成下一轮待确认 ChangeSet draft”。 +- `ReleaseGrowthPage` 在真实 recommendation 卡片上新增“生成下一轮待确认变更”动作,成功后通过 store refetch truth-state / execution-summary,使 Change Review 能继续 accept / edit / defer。 + +涉及文件: + +- `backend/app/api/pmflow_os_api/__init__.py` +- `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/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` +- `CLAUDE.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` 通过:`19 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。 +- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`2 files / 9 tests passed`。 +- `cd frontend && npm run typecheck` 未通过:当前独立 worktree 没有被 `.gitignore` 忽略的 `frontend/src/lib/*` 本地 helper 文件,导致全仓 `@/lib/api`、`@/lib/utils`、`@/lib/datetime`、`@/lib/display`、`@/lib/rbac` 等 alias 解析失败;这些文件不在本 Packet write scope,未越界补入。 +- `cd frontend && npm run build` 同样停在上述 `frontend/src/lib/*` 缺失的 TypeScript 解析错误,未进入 Vite build。 + +边界: + +- 本阶段只把 release feedback recommendation 转入 Change Review 待确认中心;不执行部署、不触发外部支付/推送、不自动写 Project Memory、不自动创建正式任务。 +- Space 仍只读消费 `releaseSignals` 并跳转主流程;Account Settings 只提供偏好 helper,不参与 PMFlow OS 写入。 + ### 2026-04-26 / big-module-integration-p4 / done 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index b31d0d09..48e00b8e 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` 的发布候选或发布门禁阻塞;Space P4 的 `feedbackAttention` 只读消费 `execution-summary.releaseSignals`,把发布后 feedback、revenue、ROI、compliance 与 iteration recommendation 关注项显示到 `/space`。发布验证或发布后信号缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规或建议结果。项目主动作进入 `/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 写入、FeedbackIngest 写入或发布执行。 - `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` 偏好;P5 新增 `getPostReleaseNotificationPreference(settings, event)`,把发布后 `feedback_received / revenue_signal / compliance_blocked` 映射到 `releaseGate` 偏好,把 `iteration_recommendation` 映射到 `pendingConfirmation` 偏好,并返回 `spaceReadable / evidencePolicy / reminderPolicy` 供 Main Flow 与 Space 消费。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据。`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、release validation run create 与 release feedback ingest 均调用 `/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` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 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` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`;当前仍不调用真实 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` 返回最新记录;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用现有 `FeedbackIngest / IterationRecommendation`,在 `ExecutionSummaryProjection.releaseSignals` 中返回真实发布后 feedback、revenue event 和 iteration recommendation 读模型;这些步骤都不直接执行本机命令、真实发布命令、外部支付或真实推送;`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、release feedback ingest 与 iteration recommendation -> ChangeSet draft 均调用 `/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` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;页面可把真实 recommendation 生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 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` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`,Iteration Loop P1 已接入 `POST /iteration-recommendations/{recommendation_id}/change-set-drafts` 把 recommendation 回流到 Change Review;当前仍不调用真实 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` 返回最新记录;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用现有 `FeedbackIngest / IterationRecommendation`,在 `ExecutionSummaryProjection.releaseSignals` 中返回真实发布后 feedback、revenue event 和 iteration recommendation 读模型;Iteration Loop P1 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts`,把真实 recommendation 转为 open ChangeSet、pending_review Requirement/Task/Risk/Decision items 与 `iteration_recommendation` evidence,重复调用复用既有 draft 并在 `releaseSignals` 标记 `changeSetCreated/changeSetId`;这些步骤都不直接执行本机命令、真实发布命令、外部支付、真实推送、自动 Project Memory commit 或正式任务创建;`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 45cbd299..53e9d553 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、Release Validation Run 与 Release Feedback Signal:参考 `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`;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用 `FeedbackIngest / IterationRecommendation` 写入真实发布后反馈,并在 `execution-summary.releaseSignals` 投影真实 `feedback / revenueEvents / recommendations` 与 release gate / validation run 关系;这些步骤均不执行发布命令、外部支付或真实推送 +- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff、Release Evidence Gate、Release Validation Run、Release Feedback Signal 与 Iteration Loop draft:参考 `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`;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用 `FeedbackIngest / IterationRecommendation` 写入真实发布后反馈,并在 `execution-summary.releaseSignals` 投影真实 `feedback / revenueEvents / recommendations` 与 release gate / validation run 关系;Iteration Loop P1 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts`,把真实 `IterationRecommendation` 转为 open ChangeSet、pending_review Requirement/Task/Risk/Decision items 与 iteration evidence,重复调用复用既有 draft;这些步骤均不执行发布命令、外部支付、真实推送或自动 Project Memory commit - 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`、`createPMFlowReleaseValidationRun`、`createPMFlowReleaseFeedbackIngest`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同,`execution-summary.releaseSignals` 是 Release/Growth 发布后 feedback/revenue/recommendation 信号前端合同;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`、`createPMFlowReleaseFeedbackIngest`、`createPMFlowIterationRecommendationChangeSetDraft`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同,`execution-summary.releaseSignals` 是 Release/Growth 发布后 feedback/revenue/recommendation 信号与下一轮 ChangeSet draft 状态前端合同;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 / execution-summary.releaseSignals`,发布验证或发布后信号缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规或 iteration recommendation 结果,不直接写 Project Memory、ChangeSet 状态、execution event、ReleaseValidationRun、FeedbackIngest、RevenueEvent 或 ComplianceCheckRun;暂存想法后端只读聚合尚未接入时必须显示 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/P5 偏好合同:参考 `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` 偏好;P5 新增 `getPostReleaseNotificationPreference(settings, event)` 供发布后反馈消费:`feedback_received / revenue_signal / compliance_blocked` 使用 `releaseGate` 偏好,`iteration_recommendation` 使用 `pendingConfirmation` 偏好,并返回 `spaceReadable / showInAppReminder / evidencePolicy / reminderPolicy`;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;这些 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、release validation run create 与 release feedback ingest 均调后端并 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` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run 和摄取 release feedback,但仍不执行部署命令、不自动创建 ChangeSet、不接外部支付;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 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、release feedback ingest 与 iteration recommendation -> ChangeSet draft 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 execution event、发布验证记录、发布后信号或下一轮 draft;`?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` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并从真实 recommendation 生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 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 dec0dd1e..d37d4fff 100644 --- a/frontend/src/api/pmflowOs.ts +++ b/frontend/src/api/pmflowOs.ts @@ -207,6 +207,7 @@ export interface PMFlowIterationRecommendation { proposedCharterPatch: Record; proposedTasks: Array>; changeSetCreated: boolean; + changeSetId?: string | null; createdAt?: string | null; updatedAt?: string | null; } @@ -228,6 +229,16 @@ export interface PMFlowReleaseFeedbackIngestResult { warnings: string[]; } +export interface PMFlowIterationRecommendationChangeSetDraftRequest { + note?: string | null; +} + +export interface PMFlowIterationRecommendationChangeSetDraftResult { + changeSetDetail: PMFlowChangeSetDetail; + executionSummary: PMFlowExecutionSummary; + warnings: string[]; +} + export interface PMFlowExecutionHandoffRequest { task_id: string; repo_binding_id?: string | null; @@ -423,6 +434,17 @@ export async function createPMFlowReleaseFeedbackIngest( return post(`/pmflow-os/projects/${projectId}/release-feedback-ingests`, payload); } +export async function createPMFlowIterationRecommendationChangeSetDraft( + projectId: string, + recommendationId: string, + payload: PMFlowIterationRecommendationChangeSetDraftRequest = {} +): Promise { + return post( + `/pmflow-os/projects/${projectId}/iteration-recommendations/${recommendationId}/change-set-drafts`, + payload + ); +} + export async function createPMFlowThreadMessage( projectId: string, threadId: string, diff --git a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx index 2c5e6199..d0739782 100644 --- a/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx +++ b/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx @@ -493,6 +493,7 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { refetchExecutionSummary, createReleaseValidationRun, createReleaseFeedbackIngest, + createIterationRecommendationChangeSetDraft, } = store; const gate = executionSummary?.releaseGate; const releaseSignals = executionSummary?.releaseSignals; @@ -535,6 +536,12 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { setFeedbackTitle(""); setFeedbackContent(""); }; + const handleCreateRecommendationChangeSet = (item: PMFlowIterationRecommendation) => { + if (item.changeSetCreated) return; + createIterationRecommendationChangeSetDraft(item.id, { + note: "ReleaseGrowthPage 从发布后反馈建议生成下一轮待确认 ChangeSet draft。", + }); + }; return (
@@ -660,7 +667,8 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
信号关系
release gate: {releaseSignals?.relationship?.releaseGateStatus || gate?.status || "empty"};validation run:{" "} - {releaseSignals?.relationship?.latestValidationRunId || "未固化"};本阶段不自动创建 ChangeSet。 + {releaseSignals?.relationship?.latestValidationRunId || "未固化"};ChangeSet draft:{" "} + {releaseSignals?.relationship?.changeSetCreated ? "已生成" : "待人工生成"}。
@@ -672,8 +680,30 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { ))} {(releaseSignals?.recommendations || []).map((item) => (
-
下一轮建议 · {item.priority}
-
{recommendationText(item)}
+
+
+
下一轮建议 · {item.priority}
+
{recommendationText(item)}
+ {item.changeSetCreated ? ( +
+ 已生成 ChangeSet draft {item.changeSetId ? `#${item.changeSetId}` : ""},请到 Change Review 继续 accept / edit / defer。 +
+ ) : ( +
不会自动写 Project Memory,生成后进入 Change Review 待确认。
+ )} +
+ +
))} {!hasReleaseSignals ? ( @@ -709,7 +739,7 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) { 保存反馈信号
- 保存后会写入 FeedbackIngest 并生成 iteration recommendation 草稿;本阶段不创建 ChangeSet、不触发发布或外部推送。 + 保存后会写入 FeedbackIngest 并生成 iteration recommendation 草稿;可手动生成 ChangeSet draft,但不触发发布或外部推送。
diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index 52cf05f6..6c8fbf04 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { act, cleanup, render, renderHook, screen, waitFor } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -10,6 +10,7 @@ import { consumePMFlowExecutionHandoff, createPMFlowExecutionHandoff, createPMFlowExecutionProgressEvent, + createPMFlowIterationRecommendationChangeSetDraft, createPMFlowReleaseValidationRun, createPMFlowReleaseFeedbackIngest, createPMFlowThreadMessage, @@ -47,6 +48,7 @@ vi.mock("@/api/pmflowOs", () => ({ consumePMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionProgressEvent: vi.fn(), + createPMFlowIterationRecommendationChangeSetDraft: vi.fn(), createPMFlowReleaseValidationRun: vi.fn(), createPMFlowReleaseFeedbackIngest: vi.fn(), editPMFlowChangeItem: vi.fn(), @@ -60,6 +62,7 @@ const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff); const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff); const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent); +const createIterationRecommendationChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationChangeSetDraft); const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun); const createReleaseFeedbackIngestMock = vi.mocked(createPMFlowReleaseFeedbackIngest); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); @@ -636,9 +639,70 @@ describe("PMFlowOsProvider real change review actions", () => { expect(await screen.findByText("发布后反馈信号")).toBeTruthy(); expect(await screen.findByText("首批试用反馈")).toBeTruthy(); expect(screen.getByText("优先优化 onboarding 上传入口。")).toBeTruthy(); + expect(screen.getByText("生成下一轮待确认变更")).toBeTruthy(); expect(screen.queryByText(/页面不会用 mock 成功数据/)).toBeNull(); }); + it("creates a reviewable ChangeSet draft from a release recommendation", async () => { + getExecutionSummaryMock.mockResolvedValue({ + ...executionSummaryWithGate(releaseGate("ready")), + releaseSignals: { + status: "active", + relationship: { + releaseGateStatus: "ready", + latestValidationRunId: "900", + latestValidationRunStatus: "ready", + source: "pmflow_os_release_growth", + changeSetCreated: false, + }, + summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1, latestSignalAt: "2026-04-26T00:00:00" }, + feedback: [], + revenueEvents: [], + recommendations: [ + { + id: "rec-1", + projectId: "123", + feedbackIngestId: "feedback-1", + status: "draft", + priority: "high", + recommendationSummary: { summary: "优先优化 onboarding 上传入口。" }, + proposedCharterPatch: {}, + proposedTasks: [], + changeSetCreated: false, + createdAt: "2026-04-26T00:00:00", + updatedAt: "2026-04-26T00:00:00", + }, + ], + nextActions: ["基于真实反馈选择 iteration recommendation,生成下一轮待确认 ChangeSet draft。"], + warnings: [], + }, + }); + createIterationRecommendationChangeSetDraftMock.mockResolvedValue({ + changeSetDetail: { + changeSet: { id: "77", status: "open" }, + items: [{ id: "78", objectType: "Requirement", status: "pending_review" }], + evidence: [{ id: "79", sourceType: "iteration_recommendation" }], + approvals: [], + }, + executionSummary: executionSummaryWithGate(releaseGate("ready")), + warnings: [], + }); + const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage"); + + render(, { wrapper: wrapper() }); + + const action = await screen.findByText("生成下一轮待确认变更"); + fireEvent.click(action); + + await waitFor(() => + expect(createIterationRecommendationChangeSetDraftMock).toHaveBeenCalledWith( + "123", + "rec-1", + expect.objectContaining({ note: expect.stringContaining("ReleaseGrowthPage") }) + ) + ); + }); + it("writes release feedback through the PMFlow OS api in real mode", async () => { createReleaseFeedbackIngestMock.mockResolvedValue({ ingest: { @@ -707,6 +771,7 @@ describe("PMFlowOsProvider real change review actions", () => { expect(createExecutionProgressEventMock).not.toHaveBeenCalled(); expect(createReleaseValidationRunMock).not.toHaveBeenCalled(); expect(createReleaseFeedbackIngestMock).not.toHaveBeenCalled(); + expect(createIterationRecommendationChangeSetDraftMock).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 e9e1885f..78cf845f 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, + createPMFlowIterationRecommendationChangeSetDraft, createPMFlowReleaseFeedbackIngest, createPMFlowReleaseValidationRun, createPMFlowThreadMessage, @@ -17,6 +18,7 @@ import { type PMFlowExecutionHandoffRequest, type PMFlowExecutionProgressEventRequest, type PMFlowExecutionSummary, + type PMFlowIterationRecommendationChangeSetDraftRequest, type PMFlowProposedChangeItem, type PMFlowReleaseFeedbackIngestRequest, type PMFlowReleaseValidationRunRequest, @@ -543,6 +545,10 @@ interface PMFlowOsStoreValue { recordExecutionProgress: (payload: PMFlowExecutionProgressEventRequest) => void; createReleaseValidationRun: (payload: PMFlowReleaseValidationRunRequest) => void; createReleaseFeedbackIngest: (payload: PMFlowReleaseFeedbackIngestRequest) => void; + createIterationRecommendationChangeSetDraft: ( + recommendationId: string, + payload?: PMFlowIterationRecommendationChangeSetDraftRequest + ) => void; advanceExecution: () => void; writeBackExecution: (outcome: "passed" | "failed") => void; publishRelease: () => void; @@ -785,6 +791,17 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { } mutateDemoOnly((current) => ingestGrowthSignalMutation(current)); }, + createIterationRecommendationChangeSetDraft: (recommendationId, payload) => { + if (dataSource.mode === "api" && dataSource.projectId) { + runApiAction(() => + createPMFlowIterationRecommendationChangeSetDraft(dataSource.projectId || "", recommendationId, { + note: payload?.note || "ReleaseGrowthPage 生成下一轮待确认变更;不自动 commit。", + }) + ); + return; + } + mutateDemoOnly((current) => ingestGrowthSignalMutation(current)); + }, advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),