feat(pmflow-os): draft changesets from iteration recommendations

This commit is contained in:
Albert 2026-04-26 14:20:48 +08:00
parent 8594d8eef7
commit ea8f9d082e
12 changed files with 688 additions and 19 deletions

View File

@ -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=<id>` 读取真实后端 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=<id>` 读取真实后端 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=<id>`。`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=<id>`;不在 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=<id>` 调用 `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=<id>` 调用 `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/` 目录说明(模块化拆分后)

View File

@ -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,

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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
完成内容:

View File

@ -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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 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=<id>` 时通过 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=<id>` 时通过 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、治理页和图表代码。

View File

@ -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=<id>`;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=<id>`;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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`;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=<id>` 走 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=<id>` 走 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`

View File

@ -207,6 +207,7 @@ export interface PMFlowIterationRecommendation {
proposedCharterPatch: Record<string, unknown>;
proposedTasks: Array<Record<string, unknown>>;
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<PMFlowReleaseFeedbackIngestResult>(`/pmflow-os/projects/${projectId}/release-feedback-ingests`, payload);
}
export async function createPMFlowIterationRecommendationChangeSetDraft(
projectId: string,
recommendationId: string,
payload: PMFlowIterationRecommendationChangeSetDraftRequest = {}
): Promise<PMFlowIterationRecommendationChangeSetDraftResult> {
return post<PMFlowIterationRecommendationChangeSetDraftResult>(
`/pmflow-os/projects/${projectId}/iteration-recommendations/${recommendationId}/change-set-drafts`,
payload
);
}
export async function createPMFlowThreadMessage(
projectId: string,
threadId: string,

View File

@ -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 (
<div className="min-h-full bg-[#f7faff] text-slate-950">
@ -660,7 +667,8 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
<div className="font-semibold text-slate-800">信号关系</div>
<div className="mt-1">
release gate: {releaseSignals?.relationship?.releaseGateStatus || gate?.status || "empty"};validation run:{" "}
{releaseSignals?.relationship?.latestValidationRunId || "未固化"};本阶段不自动创建 ChangeSet。
{releaseSignals?.relationship?.latestValidationRunId || "未固化"};ChangeSet draft:{" "}
{releaseSignals?.relationship?.changeSetCreated ? "已生成" : "待人工生成"}。
</div>
</div>
<div className="mt-4 space-y-3">
@ -672,8 +680,30 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
))}
{(releaseSignals?.recommendations || []).map((item) => (
<div key={item.id} className="rounded-lg border border-blue-100 bg-blue-50 px-4 py-3 text-sm leading-6 text-blue-700">
<div className="font-semibold">下一轮建议 · {item.priority}</div>
<div className="mt-1">{recommendationText(item)}</div>
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<div className="font-semibold">下一轮建议 · {item.priority}</div>
<div className="mt-1">{recommendationText(item)}</div>
{item.changeSetCreated ? (
<div className="mt-2 text-xs text-blue-600">
已生成 ChangeSet draft {item.changeSetId ? `#${item.changeSetId}` : ""},请到 Change Review 继续 accept / edit / defer。
</div>
) : (
<div className="mt-2 text-xs text-blue-600">不会自动写 Project Memory,生成后进入 Change Review 待确认。</div>
)}
</div>
<Button
className={cn(
"min-h-9 shrink-0 whitespace-normal rounded-lg px-3 py-2 text-center text-xs leading-4 md:whitespace-nowrap",
item.changeSetCreated ? "border border-blue-200 bg-white text-blue-500" : "bg-[#2463EB] text-white hover:bg-[#1f57d1]"
)}
disabled={isLoading || item.changeSetCreated}
onClick={() => handleCreateRecommendationChangeSet(item)}
>
{item.changeSetCreated ? <CheckCircle2 className="mr-1.5 h-3.5 w-3.5" /> : <ArrowRight className="mr-1.5 h-3.5 w-3.5" />}
{item.changeSetCreated ? "已生成草稿" : "生成下一轮待确认变更"}
</Button>
</div>
</div>
))}
{!hasReleaseSignals ? (
@ -709,7 +739,7 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
保存反馈信号
</Button>
<div className="text-xs leading-5 text-slate-500">
保存后会写入 FeedbackIngest 并生成 iteration recommendation 草稿;本阶段不创建 ChangeSet、不触发发布或外部推送。
保存后会写入 FeedbackIngest 并生成 iteration recommendation 草稿;可手动生成 ChangeSet draft,但不触发发布或外部推送。
</div>
</div>
</Card>

View File

@ -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(<ReleaseGrowthPage />, { 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");
});

View File

@ -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)),