merge iteration loop p2 recommendation quality

# Conflicts:
#	docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md
This commit is contained in:
Albert 2026-04-26 15:14:27 +08:00
commit 8d60b18214
13 changed files with 1135 additions and 23 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/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/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/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` 与 iteration recommendation -> ChangeSet draft 调后端,显式 `?demo=1` 才启用本地演示状态机) - `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 与同组 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_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 与 recommendation group 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) - `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 已实现) - `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/`(消息台) - `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/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/P5 的发布后反馈/下一轮草稿关注只读消费 `execution-summary.releaseSignals`,点击只跳 `/release-growth?projectId=<id>` 或 `/change-review?projectId=<id>&changeSetId=<id>`;不在 Space 写 Project Memory、ProgressEvent、ReleaseValidationRun、FeedbackIngest、RevenueEvent、ComplianceCheckRun、ChangeSet 状态或发布执行。`components/SpaceChrome.tsx` 承接空间层左侧栏、顶部条与空间用量组件,并被 `/project-launch` 桌面首次新建壳复用;`frontend/src/pages/PersonalSpacePage.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/P5 的发布后反馈/下一轮草稿关注只读消费 `execution-summary.releaseSignals`,点击只跳 `/release-growth?projectId=<id>` 或 `/change-review?projectId=<id>&changeSetId=<id>`;不在 Space 写 Project Memory、ProgressEvent、ReleaseValidationRun、FeedbackIngest、RevenueEvent、ComplianceCheckRun、ChangeSet 状态或发布执行。`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 / next change review 偏好 helper,`releaseGate / failureAlert / pendingConfirmation` 只影响页面内提醒表达,不代表真实邮件、桌面推送或系统通知中心已接入,不改 OAuth/JWT/auth core;`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 仅做薄入口并复用既有退出登录 bottom sheet。 - `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 / next change review 偏好 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 与 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/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 与同组 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 quality grouping、从真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据、审计状态或发布后信号;`ChangeReviewPage` 会展示来自 release feedback recommendation group 的组、反馈、风险、影响和审查优先级,帮助 reviewer 判断为什么生成这一组待确认变更;静态发布/增长原型仅保留在显式 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/` 是当前唯一登录真源。 - 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。
### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后) ### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后)

View File

@ -18,6 +18,7 @@ from ...schemas.pmflow_os import (
PMFlowExecutionProgressEventRequest, PMFlowExecutionProgressEventRequest,
PMFlowExtractionCreateRequest, PMFlowExtractionCreateRequest,
PMFlowIterationRecommendationChangeSetDraftRequest, PMFlowIterationRecommendationChangeSetDraftRequest,
PMFlowIterationRecommendationGroupChangeSetDraftRequest,
PMFlowReleaseFeedbackIngestRequest, PMFlowReleaseFeedbackIngestRequest,
PMFlowReleaseValidationRunCreateRequest, PMFlowReleaseValidationRunCreateRequest,
PMFlowThreadMessageCreateRequest, PMFlowThreadMessageCreateRequest,
@ -419,6 +420,41 @@ async def create_pmflow_os_iteration_recommendation_change_set_draft(
return success_response(payload) return success_response(payload)
@router.post("/projects/{project_id}/iteration-recommendation-groups/{group_key}/change-set-drafts")
async def create_pmflow_os_iteration_recommendation_group_change_set_draft(
project_id: int,
group_key: str,
body: PMFlowIterationRecommendationGroupChangeSetDraftRequest | 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_group_change_set_draft(
project_id=int(project_id),
group_key=group_key,
body=body or PMFlowIterationRecommendationGroupChangeSetDraftRequest(),
current_user_id=int(current_user.id),
)
except ValueError as exc:
code = str(exc)
if code == "iteration_recommendation_group_not_found":
return JSONResponse(status_code=404, content=error_response(404, "iteration_recommendation_group_not_found"))
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") @router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume")
async def consume_pmflow_os_execution_handoff( async def consume_pmflow_os_execution_handoff(
project_id: int, project_id: int,

View File

@ -150,6 +150,11 @@ class PMFlowIterationRecommendationChangeSetDraftRequest(BaseModel):
note: Optional[str] = Field(default=None, max_length=4000) note: Optional[str] = Field(default=None, max_length=4000)
class PMFlowIterationRecommendationGroupChangeSetDraftRequest(BaseModel):
recommendation_ids: list[str] = Field(default_factory=list)
note: Optional[str] = Field(default=None, max_length=4000)
class PMFlowChangeSetDetailResponse(BaseModel): class PMFlowChangeSetDetailResponse(BaseModel):
changeSet: dict[str, Any] changeSet: dict[str, Any]
items: list[dict[str, Any]] = Field(default_factory=list) items: list[dict[str, Any]] = Field(default_factory=list)

View File

@ -44,6 +44,7 @@ from ..schemas.pmflow_os import (
PMFlowExecutionContext, PMFlowExecutionContext,
PMFlowFeedEvent, PMFlowFeedEvent,
PMFlowIterationRecommendationChangeSetDraftRequest, PMFlowIterationRecommendationChangeSetDraftRequest,
PMFlowIterationRecommendationGroupChangeSetDraftRequest,
PMFlowMetricSignal, PMFlowMetricSignal,
PMFlowModelDecision, PMFlowModelDecision,
PMFlowProjectMemory, PMFlowProjectMemory,
@ -521,6 +522,168 @@ class PMFlowOSProjectionService:
"warnings": [], "warnings": [],
} }
async def create_iteration_recommendation_group_change_set_draft(
self,
*,
project_id: int,
group_key: str,
body: PMFlowIterationRecommendationGroupChangeSetDraftRequest,
current_user_id: Optional[int] = None,
) -> dict[str, Any]:
project = await self._load_project(project_id=project_id)
rows = await self._load_iteration_recommendations(project_id=project_id)
feedback_by_id = await self._load_feedback_ingests_for_recommendations(project_id=project_id, recommendations=rows)
quality_by_id = {
int(row.id): self._recommendation_quality_profile(
recommendation=row,
feedback=feedback_by_id.get(int(row.feedback_ingest_id)) if row.feedback_ingest_id is not None else None,
)
for row in rows
}
requested_ids = self._parse_recommendation_ids(body.recommendation_ids)
normalized_group_key = self._normalize_group_key(group_key)
selected = [
row
for row in rows
if (requested_ids and int(row.id) in requested_ids) or (not requested_ids and quality_by_id[int(row.id)]["groupKey"] == normalized_group_key)
]
if requested_ids and len({int(row.id) for row in selected}) != len(requested_ids):
raise ValueError("iteration_recommendation_not_found")
if not selected:
raise ValueError("iteration_recommendation_group_not_found")
selected_quality = self._finalize_recommendation_quality(
recommendations=selected,
quality_by_id={int(row.id): quality_by_id[int(row.id)] for row in selected},
)
canonical_group_key = str(next(iter(selected_quality.values())).get("groupKey") or normalized_group_key)
existing_change_set = await self._find_existing_recommendation_group_change_set(
project_id=project_id,
recommendations=selected,
)
if existing_change_set is not None:
await self._mark_recommendations_for_group_change_set(
recommendations=selected,
change_set_id=int(existing_change_set.id),
group_key=canonical_group_key,
current_user_id=current_user_id,
reused=True,
)
await self.db.commit()
detail = await PMFlowChangeReviewService(self.db, tenant_id=self.tenant_id).get_change_set_detail(
project_id=project_id,
change_set_id=int(existing_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": ["iteration_recommendation_group_changeset_reused"],
}
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_group_message(
recommendations=selected,
feedback_by_id=feedback_by_id,
quality_by_id=selected_quality,
note=body.note,
),
source="pmflow_os",
attachments=[],
message_metadata={
"source": "iteration_recommendation_group",
"group_key": canonical_group_key,
"recommendation_ids": [str(row.id) for row in selected],
"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_group_change_items(
recommendations=selected,
feedback_by_id=feedback_by_id,
quality_by_id=selected_quality,
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_group_change_set_summary(recommendations=selected, quality_by_id=selected_quality),
headline=self._iteration_group_change_set_headline(recommendations=selected, quality_by_id=selected_quality),
extraction_scope="iteration_recommendation_group",
routing_meta={
"source": "release_feedback_iteration_group",
"groupKey": canonical_group_key,
"recommendationIds": [str(row.id) for row in selected],
"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_group",
source_id=canonical_group_key,
title="发布后反馈迭代建议组",
excerpt=self._iteration_group_excerpt(recommendations=selected, feedback_by_id=feedback_by_id),
display_label="Release Feedback Group",
evidence_metadata={
"groupKey": canonical_group_key,
"recommendationIds": [str(row.id) for row in selected],
"feedbackIngestIds": [
str(row.feedback_ingest_id) for row in selected if row.feedback_ingest_id is not None
],
"quality": next(iter(selected_quality.values()), {}),
"change_set_created": True,
},
)
self.db.add(evidence)
await self._mark_recommendations_for_group_change_set(
recommendations=selected,
change_set_id=int(change_set.id),
group_key=canonical_group_key,
current_user_id=current_user_id,
reused=False,
)
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( async def consume_execution_handoff(
self, self,
*, *,
@ -705,6 +868,14 @@ class PMFlowOSProjectionService:
raise ValueError("iteration_recommendation_not_found") raise ValueError("iteration_recommendation_not_found")
return row return row
async def _load_iteration_recommendations(self, *, project_id: int) -> list[IterationRecommendation]:
result = await self.db.execute(
select(IterationRecommendation)
.where(and_(IterationRecommendation.tenant_id == self.tenant_id, IterationRecommendation.project_id == int(project_id)))
.order_by(IterationRecommendation.updated_at.desc(), IterationRecommendation.created_at.desc(), IterationRecommendation.id.desc())
)
return list(result.scalars().all())
async def _load_recommendation_change_set(self, *, project_id: int, change_set_id: int) -> Optional[PMFlowChangeSet]: async def _load_recommendation_change_set(self, *, project_id: int, change_set_id: int) -> Optional[PMFlowChangeSet]:
result = await self.db.execute( result = await self.db.execute(
select(PMFlowChangeSet).where( select(PMFlowChangeSet).where(
@ -717,6 +888,24 @@ class PMFlowOSProjectionService:
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def _find_existing_recommendation_group_change_set(
self,
*,
project_id: int,
recommendations: list[IterationRecommendation],
) -> Optional[PMFlowChangeSet]:
candidate_ids: list[int] = []
for recommendation in recommendations:
summary = dict(recommendation.recommendation_summary or {})
change_set_id = self._optional_int(summary.get("changeSetId"), error_code="iteration_recommendation_not_found")
if change_set_id is not None:
candidate_ids.append(change_set_id)
for change_set_id in candidate_ids:
row = await self._load_recommendation_change_set(project_id=project_id, change_set_id=change_set_id)
if row is not None:
return row
return None
async def _load_feedback_ingest(self, *, project_id: int, feedback_ingest_id: Optional[int]) -> Optional[FeedbackIngest]: async def _load_feedback_ingest(self, *, project_id: int, feedback_ingest_id: Optional[int]) -> Optional[FeedbackIngest]:
if feedback_ingest_id is None: if feedback_ingest_id is None:
return None return None
@ -731,6 +920,26 @@ class PMFlowOSProjectionService:
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def _load_feedback_ingests_for_recommendations(
self,
*,
project_id: int,
recommendations: list[IterationRecommendation],
) -> dict[int, FeedbackIngest]:
ids = sorted({int(row.feedback_ingest_id) for row in recommendations if row.feedback_ingest_id is not None})
if not ids:
return {}
result = await self.db.execute(
select(FeedbackIngest).where(
and_(
FeedbackIngest.tenant_id == self.tenant_id,
FeedbackIngest.project_id == int(project_id),
FeedbackIngest.id.in_(ids),
)
)
)
return {int(row.id): row for row in result.scalars().all()}
async def _resolve_or_create_iteration_thread(self, *, project: Project) -> PMFlowThreadModel: async def _resolve_or_create_iteration_thread(self, *, project: Project) -> PMFlowThreadModel:
result = await self.db.execute( result = await self.db.execute(
select(PMFlowThreadModel) select(PMFlowThreadModel)
@ -776,7 +985,26 @@ class PMFlowOSProjectionService:
.limit(10) .limit(10)
) )
recommendation_rows = list(recommendation_result.scalars().all()) recommendation_rows = list(recommendation_result.scalars().all())
recommendations = [self._serialize_iteration_recommendation(item) for item in recommendation_rows] feedback_by_id_for_recommendations = await self._load_feedback_ingests_for_recommendations(
project_id=project_id,
recommendations=recommendation_rows,
)
base_quality_by_id = {
int(item.id): self._recommendation_quality_profile(
recommendation=item,
feedback=feedback_by_id_for_recommendations.get(int(item.feedback_ingest_id)) if item.feedback_ingest_id is not None else None,
)
for item in recommendation_rows
}
quality_by_id = self._finalize_recommendation_quality(
recommendations=recommendation_rows,
quality_by_id=base_quality_by_id,
)
recommendations = [
self._serialize_iteration_recommendation(item, quality=quality_by_id.get(int(item.id)))
for item in recommendation_rows
]
recommendation_groups = self._serialize_recommendation_groups(recommendations)
revenue_result = await self.db.execute( revenue_result = await self.db.execute(
select(RevenueEvent) select(RevenueEvent)
@ -818,6 +1046,7 @@ class PMFlowOSProjectionService:
"feedback": feedback_items, "feedback": feedback_items,
"revenueEvents": revenue_events, "revenueEvents": revenue_events,
"recommendations": recommendations, "recommendations": recommendations,
"recommendationGroups": recommendation_groups,
"nextActions": self._release_signal_next_actions( "nextActions": self._release_signal_next_actions(
feedback_items=feedback_items, feedback_items=feedback_items,
recommendations=recommendations, recommendations=recommendations,
@ -1882,10 +2111,16 @@ class PMFlowOSProjectionService:
"updatedAt": ingest.updated_at.isoformat() if ingest.updated_at else None, "updatedAt": ingest.updated_at.isoformat() if ingest.updated_at else None,
} }
def _serialize_iteration_recommendation(self, recommendation: IterationRecommendation) -> dict[str, Any]: def _serialize_iteration_recommendation(
self,
recommendation: IterationRecommendation,
*,
quality: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
summary = dict(recommendation.recommendation_summary or {}) summary = dict(recommendation.recommendation_summary or {})
change_set_id = summary.get("changeSetId") 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) normalized_change_set_id = None if change_set_id is None or str(change_set_id).strip() == "" else str(change_set_id)
quality_payload = dict(quality or {})
return { return {
"id": str(recommendation.id), "id": str(recommendation.id),
"projectId": str(recommendation.project_id), "projectId": str(recommendation.project_id),
@ -1898,10 +2133,141 @@ class PMFlowOSProjectionService:
"proposedTasks": list(recommendation.proposed_tasks or []), "proposedTasks": list(recommendation.proposed_tasks or []),
"changeSetCreated": bool(summary.get("changeSetCreated") or normalized_change_set_id), "changeSetCreated": bool(summary.get("changeSetCreated") or normalized_change_set_id),
"changeSetId": normalized_change_set_id, "changeSetId": normalized_change_set_id,
"groupKey": quality_payload.get("groupKey") or summary.get("groupKey"),
"duplicateOf": quality_payload.get("duplicateOf"),
"mergedCount": int(quality_payload.get("mergedCount") or 1),
"impactLevel": quality_payload.get("impactLevel") or "medium",
"riskLevel": quality_payload.get("riskLevel") or "medium",
"suggestedChangeTypes": list(quality_payload.get("suggestedChangeTypes") or ["Requirement", "Decision"]),
"reviewPriority": quality_payload.get("reviewPriority") or "p2",
"quality": quality_payload,
"createdAt": recommendation.created_at.isoformat() if recommendation.created_at else None, "createdAt": recommendation.created_at.isoformat() if recommendation.created_at else None,
"updatedAt": recommendation.updated_at.isoformat() if recommendation.updated_at else None, "updatedAt": recommendation.updated_at.isoformat() if recommendation.updated_at else None,
} }
def _recommendation_quality_profile(
self,
*,
recommendation: IterationRecommendation,
feedback: Optional[FeedbackIngest],
) -> dict[str, Any]:
summary = dict(recommendation.recommendation_summary or {})
patch = dict(recommendation.proposed_charter_patch or {})
focus_areas = patch.get("focus_areas") or patch.get("focusAreas") or summary.get("focus_areas") or summary.get("focusAreas") or []
if not isinstance(focus_areas, list):
focus_areas = [str(focus_areas)]
focus_areas = [str(item).strip() for item in focus_areas if str(item).strip()]
group_seed = focus_areas[0] if focus_areas else (feedback.title if feedback is not None and feedback.title else f"recommendation-{recommendation.id}")
group_key = self._normalize_group_key(group_seed)
priority = str(recommendation.priority or "medium").lower()
proposed_tasks = [item for item in list(recommendation.proposed_tasks or []) if isinstance(item, dict)]
metrics = dict(feedback.metrics or {}) if feedback is not None else {}
negative_feedback = self._safe_number(metrics.get("negative_feedback_count") or metrics.get("negativeFeedbackCount"))
suggested_change_types = ["Requirement", "Decision"]
if proposed_tasks:
suggested_change_types.append("Task")
impact_level = "high" if priority in {"high", "critical"} or negative_feedback >= 3 or len(proposed_tasks) >= 3 else "medium"
if negative_feedback <= 0 and not proposed_tasks and priority == "low":
impact_level = "low"
risk_level = "high" if priority in {"high", "critical"} else ("medium" if proposed_tasks or impact_level == "medium" else "low")
if risk_level == "high":
suggested_change_types.append("Risk")
review_priority = "p0" if risk_level == "high" and impact_level == "high" else ("p1" if impact_level == "high" or risk_level == "medium" else "p2")
return {
"groupKey": group_key,
"groupLabel": group_seed,
"duplicateOf": None,
"mergedCount": 1,
"impactLevel": impact_level,
"riskLevel": risk_level,
"suggestedChangeTypes": self._unique_strings(suggested_change_types),
"reviewPriority": review_priority,
"rationale": self._recommendation_quality_rationale(
focus_areas=focus_areas,
negative_feedback=negative_feedback,
task_count=len(proposed_tasks),
priority=priority,
),
}
def _finalize_recommendation_quality(
self,
*,
recommendations: list[IterationRecommendation],
quality_by_id: dict[int, dict[str, Any]],
) -> dict[int, dict[str, Any]]:
groups: dict[str, list[int]] = {}
for recommendation in recommendations:
quality = quality_by_id.get(int(recommendation.id)) or {}
groups.setdefault(str(quality.get("groupKey") or f"recommendation-{recommendation.id}"), []).append(int(recommendation.id))
finalized: dict[int, dict[str, Any]] = {}
for group_key, ids in groups.items():
first_id = str(ids[0])
merged_count = len(ids)
for rec_id in ids:
quality = dict(quality_by_id.get(rec_id) or {})
quality["groupKey"] = group_key
quality["mergedCount"] = merged_count
quality["duplicateOf"] = None if str(rec_id) == first_id else first_id
finalized[rec_id] = quality
return finalized
def _serialize_recommendation_groups(self, recommendations: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[str, list[dict[str, Any]]] = {}
for recommendation in recommendations:
group_key = str(recommendation.get("groupKey") or f"recommendation-{recommendation.get('id')}")
groups.setdefault(group_key, []).append(recommendation)
serialized: list[dict[str, Any]] = []
priority_order = {"p0": 0, "p1": 1, "p2": 2}
level_order = {"high": 0, "medium": 1, "low": 2}
for group_key, rows in groups.items():
quality = dict(rows[0].get("quality") or {})
change_set_ids = self._unique_strings([row.get("changeSetId") for row in rows if row.get("changeSetId")])
suggested_types = self._unique_strings(
item for row in rows for item in list(row.get("suggestedChangeTypes") or [])
)
serialized.append(
{
"groupKey": group_key,
"title": f"{quality.get('groupLabel') or group_key} 反馈组",
"recommendationIds": [str(row.get("id")) for row in rows],
"mergedCount": len(rows),
"impactLevel": quality.get("impactLevel") or "medium",
"riskLevel": quality.get("riskLevel") or "medium",
"suggestedChangeTypes": suggested_types,
"reviewPriority": quality.get("reviewPriority") or "p2",
"changeSetCreated": bool(change_set_ids),
"changeSetId": change_set_ids[0] if change_set_ids else None,
"rationale": quality.get("rationale") or "基于同组发布后反馈聚合生成。",
}
)
return sorted(
serialized,
key=lambda row: (
priority_order.get(str(row.get("reviewPriority") or "p2"), 9),
level_order.get(str(row.get("riskLevel") or "medium"), 9),
str(row.get("groupKey") or ""),
),
)
def _recommendation_quality_rationale(
self,
*,
focus_areas: list[str],
negative_feedback: float,
task_count: int,
priority: str,
) -> str:
parts = []
if focus_areas:
parts.append("聚焦 " + "、".join(focus_areas[:4]))
if negative_feedback >= 1:
parts.append(f"负向反馈 {int(negative_feedback)} 条")
if task_count:
parts.append(f"建议任务 {task_count} 个")
parts.append(f"原始优先级 {priority or 'medium'}")
return ";".join(parts) + "。"
def _iteration_recommendation_message( def _iteration_recommendation_message(
self, self,
*, *,
@ -2092,6 +2458,244 @@ class PMFlowOSProjectionService:
) )
return items return items
def _iteration_group_message(
self,
*,
recommendations: list[IterationRecommendation],
feedback_by_id: dict[int, FeedbackIngest],
quality_by_id: dict[int, dict[str, Any]],
note: Optional[str],
) -> str:
first_quality = next(iter(quality_by_id.values()), {})
summaries = [self._recommendation_summary_text(item) for item in recommendations]
feedback_titles = [
feedback_by_id[int(item.feedback_ingest_id)].title or feedback_by_id[int(item.feedback_ingest_id)].content[:60]
for item in recommendations
if item.feedback_ingest_id is not None and int(item.feedback_ingest_id) in feedback_by_id
]
lines = [
"Release/Growth 同组反馈已生成下一轮待确认变更草稿。",
f"建议组:{first_quality.get('groupLabel') or first_quality.get('groupKey') or 'feedback'}",
f"合并建议:{len(recommendations)} 条;风险:{first_quality.get('riskLevel') or 'medium'};影响:{first_quality.get('impactLevel') or 'medium'}。",
]
if feedback_titles:
lines.append("反馈来源:" + ";".join(feedback_titles[:5]))
lines.append("建议摘要:" + ";".join(summaries[:4]))
if note:
lines.append(f"人工备注:{note.strip()}")
lines.append("此消息只作为 Change Review 证据,不自动写入 Project Memory。")
return "\n".join(lines)
def _iteration_group_change_set_headline(
self,
*,
recommendations: list[IterationRecommendation],
quality_by_id: dict[int, dict[str, Any]],
) -> str:
quality = next(iter(quality_by_id.values()), {})
label = quality.get("groupLabel") or quality.get("groupKey") or "feedback"
return f"下一轮反馈变更草稿组:{label}({len(recommendations)} 条建议)"[:255]
def _iteration_group_change_set_summary(
self,
*,
recommendations: list[IterationRecommendation],
quality_by_id: dict[int, dict[str, Any]],
) -> str:
quality = next(iter(quality_by_id.values()), {})
return (
f"基于 {len(recommendations)} 条同组 iteration recommendation 生成下一轮 ChangeSet draft;"
f"reviewPriority={quality.get('reviewPriority') or 'p2'},risk={quality.get('riskLevel') or 'medium'},"
"等待 Change Review accept/edit/defer。"
)
def _iteration_group_excerpt(
self,
*,
recommendations: list[IterationRecommendation],
feedback_by_id: dict[int, FeedbackIngest],
) -> str:
excerpts: list[str] = []
for recommendation in recommendations:
if recommendation.feedback_ingest_id is not None and int(recommendation.feedback_ingest_id) in feedback_by_id:
excerpts.append(feedback_by_id[int(recommendation.feedback_ingest_id)].content.strip())
else:
excerpts.append(self._recommendation_summary_text(recommendation))
return "\n".join(excerpts)[:1200]
def _iteration_recommendation_group_change_items(
self,
*,
recommendations: list[IterationRecommendation],
feedback_by_id: dict[int, FeedbackIngest],
quality_by_id: dict[int, dict[str, Any]],
project_id: int,
) -> list[PMFlowChangeItem]:
first_quality = next(iter(quality_by_id.values()), {})
group_key = str(first_quality.get("groupKey") or "general-feedback")
recommendation_ids = [str(item.id) for item in recommendations]
feedback_ids = [str(item.feedback_ingest_id) for item in recommendations if item.feedback_ingest_id is not None]
focus_areas = self._unique_strings(
area
for recommendation in recommendations
for area in list(dict(recommendation.proposed_charter_patch or {}).get("focus_areas") or dict(recommendation.recommendation_summary or {}).get("focus_areas") or [])
)
summaries = [self._recommendation_summary_text(item) for item in recommendations]
summary_text = ";".join(summaries[:4])
risk_level = "high" if any(str(quality.get("riskLevel")) == "high" for quality in quality_by_id.values()) else "medium"
impact_level = "high" if any(str(quality.get("impactLevel")) == "high" for quality in quality_by_id.values()) else "medium"
base_payload = {
"source": "iteration_recommendation_group",
"groupKey": group_key,
"recommendationIds": recommendation_ids,
"feedbackIngestIds": feedback_ids,
"quality": first_quality,
}
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=f"下一轮迭代方向:{first_quality.get('groupLabel') or group_key}"[:255],
summary=summary_text or "基于发布后反馈建议组生成下一轮迭代方向。",
diff="合并同组发布后反馈,形成下一轮需求方向与成功标准草稿。",
payload={
**base_payload,
"title": f"下一轮迭代方向:{first_quality.get('groupLabel') or group_key}",
"summary": summary_text,
"stage": "planning",
"status": "draft",
"focusAreas": focus_areas,
"acceptanceCriteria": [
f"本组 {len(recommendations)} 条 recommendation 在 Change Review 中完成 accept/edit/defer。",
"接受后再进入 Project Memory,不自动写入。",
],
},
edited_payload=None,
confidence=0.78,
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"确认 {group_key} 反馈组是否作为下一轮迭代输入;影响级别 {impact_level},风险级别 {risk_level}。",
diff="批量生成只进入待确认中心,不直接提交项目记忆。",
payload={
**base_payload,
"title": "确认同组反馈进入下一轮迭代",
"summary": summary_text,
"stage": "growth",
"owner": "PMFlow",
},
edited_payload=None,
confidence=0.74,
risk_level=risk_level,
status="pending_review",
),
]
seen_task_titles: set[str] = set()
for recommendation in recommendations:
for raw_task in list(recommendation.proposed_tasks or []):
if not isinstance(raw_task, dict):
continue
title = str(raw_task.get("title") or raw_task.get("summary") or "").strip()
if not title or title in seen_task_titles:
continue
seen_task_titles.add(title)
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={
**base_payload,
"title": title,
"description": task_summary,
"summary": task_summary,
"stage": "development",
"status": "draft",
"priority": raw_task.get("priority") or first_quality.get("reviewPriority") or "p2",
},
edited_payload=None,
confidence=0.72,
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=f"{group_key} 反馈组存在高风险或高影响建议,需先完成人工审查。",
diff="风险项只进入 Change Review,不直接写入项目记忆。",
payload={
**base_payload,
"title": "同组发布后反馈高风险",
"summary": summary_text,
"stage": "planning",
"level": "high",
"mitigation": "在 Change Review 中确认分组范围、owner 和处理顺序后再提交。",
},
edited_payload=None,
confidence=0.76,
risk_level="high",
status="pending_review",
)
)
return items
async def _mark_recommendations_for_group_change_set(
self,
*,
recommendations: list[IterationRecommendation],
change_set_id: int,
group_key: str,
current_user_id: Optional[int],
reused: bool,
) -> None:
stamped_at = now_cn_naive().isoformat()
for recommendation in recommendations:
summary_payload = dict(recommendation.recommendation_summary or {})
summary_payload.update(
{
"changeSetId": str(change_set_id),
"changeSetCreated": True,
"changeSetCreatedAt": summary_payload.get("changeSetCreatedAt") or stamped_at,
"changeSetCreatedBy": summary_payload.get("changeSetCreatedBy")
or (None if current_user_id is None else str(current_user_id)),
"groupKey": group_key,
"groupChangeSetId": str(change_set_id),
"groupDraftReused": bool(reused),
}
)
recommendation.recommendation_summary = summary_payload
recommendation.status = "changeset_drafted"
flag_modified(recommendation, "recommendation_summary")
def _serialize_revenue_event(self, event: RevenueEvent) -> dict[str, Any]: def _serialize_revenue_event(self, event: RevenueEvent) -> dict[str, Any]:
return { return {
"id": str(event.id), "id": str(event.id),
@ -2128,6 +2732,45 @@ class PMFlowOSProjectionService:
actions.append("继续沉淀 feedback / revenue / compliance 信号,为下一轮 iteration recommendation 做准备。") actions.append("继续沉淀 feedback / revenue / compliance 信号,为下一轮 iteration recommendation 做准备。")
return actions return actions
def _normalize_group_key(self, value: Any) -> str:
text = str(value or "").strip().lower()
chars: list[str] = []
previous_dash = False
for char in text:
if char.isascii() and char.isalnum():
chars.append(char)
previous_dash = False
elif not previous_dash:
chars.append("-")
previous_dash = True
normalized = "".join(chars).strip("-")
return normalized[:64] or "general-feedback"
def _safe_number(self, value: Any) -> float:
if value is None or value == "":
return 0.0
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def _unique_strings(self, values: Any) -> list[str]:
result: list[str] = []
for value in values or []:
text = str(value or "").strip()
if text and text not in result:
result.append(text)
return result
def _parse_recommendation_ids(self, raw_ids: list[str]) -> set[int]:
parsed: set[int] = set()
for raw in raw_ids or []:
text = str(raw or "").strip()
if not text.isdigit():
raise ValueError("iteration_recommendation_not_found")
parsed.add(int(text))
return parsed
def _optional_int(self, value: Optional[Any], *, error_code: str) -> Optional[int]: def _optional_int(self, value: Optional[Any], *, error_code: str) -> Optional[int]:
if value is None or str(value).strip() == "": if value is None or str(value).strip() == "":
return None return None

View File

@ -1112,6 +1112,83 @@ async def test_iteration_recommendation_creates_reusable_changeset_draft(db_env)
assert tasks == [] assert tasks == []
@pytest.mark.asyncio
async def test_iteration_recommendation_group_batches_quality_and_reuses_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:
await client.post(
f"/pmflow-os/projects/{project.id}/release-feedback-ingests",
json={
"source_type": "manual",
"title": "回答质量反馈 A",
"content": "bad answer 多次不准,用户要求下一轮补齐失败样本。",
"metrics": {"negative_feedback_count": 4},
},
)
await client.post(
f"/pmflow-os/projects/{project.id}/release-feedback-ingests",
json={
"source_type": "manual",
"title": "回答质量反馈 B",
"content": "出现幻觉和不准回答,需要改进 prompt quality。",
"metrics": {"negative_feedback_count": 2},
},
)
summary_response = await client.get(f"/pmflow-os/projects/{project.id}/execution-summary")
release_signals = summary_response.json()["data"]["releaseSignals"]
group = release_signals["recommendationGroups"][0]
draft_response = await client.post(
f"/pmflow-os/projects/{project.id}/iteration-recommendation-groups/{group['groupKey']}/change-set-drafts",
json={"recommendation_ids": group["recommendationIds"], "note": "批量进入下一轮待确认"},
)
reuse_response = await client.post(
f"/pmflow-os/projects/{project.id}/iteration-recommendation-groups/{group['groupKey']}/change-set-drafts",
json={"recommendation_ids": group["recommendationIds"]},
)
assert summary_response.status_code == 200
recommendations = release_signals["recommendations"]
assert group["groupKey"] == "quality"
assert group["mergedCount"] == 2
assert group["impactLevel"] == "high"
assert group["riskLevel"] == "high"
assert group["reviewPriority"] == "p0"
assert set(group["suggestedChangeTypes"]).issuperset({"Requirement", "Decision", "Task", "Risk"})
assert {item["mergedCount"] for item in recommendations} == {2}
assert sum(1 for item in recommendations if item["duplicateOf"]) == 1
assert all(item["groupKey"] == "quality" for item in recommendations)
assert draft_response.status_code == 200
payload = draft_response.json()["data"]
detail = payload["changeSetDetail"]
assert detail["changeSet"]["status"] == "open"
assert detail["evidence"][0]["sourceType"] == "iteration_recommendation_group"
assert detail["evidence"][0]["metadata"]["groupKey"] == "quality"
assert set(detail["evidence"][0]["metadata"]["recommendationIds"]) == set(group["recommendationIds"])
object_types = {item["objectType"] for item in detail["items"]}
assert object_types.issubset({"Requirement", "Task", "Risk", "Decision"})
assert {"Requirement", "Decision", "Task", "Risk"}.issubset(object_types)
assert all(item["status"] == "pending_review" for item in detail["items"])
updated_groups = payload["executionSummary"]["releaseSignals"]["recommendationGroups"]
assert updated_groups[0]["changeSetCreated"] is True
assert updated_groups[0]["changeSetId"] == detail["changeSet"]["id"]
assert all(
item["changeSetId"] == detail["changeSet"]["id"]
for item in payload["executionSummary"]["releaseSignals"]["recommendations"]
)
assert reuse_response.status_code == 200
assert reuse_response.json()["data"]["changeSetDetail"]["changeSet"]["id"] == detail["changeSet"]["id"]
assert "iteration_recommendation_group_changeset_reused" in reuse_response.json()["data"]["warnings"]
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 @pytest.mark.asyncio
async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None: async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None:
session = db_env session = db_env

View File

@ -715,16 +715,13 @@ commit gate 判定:
当前最建议下一步执行: 当前最建议下一步执行:
**P7:Iteration Quality / Reviewer / Personal Queue 三线并行。** **P7:Iteration Quality / Reviewer / Personal Queue 收口。**
原因: 原因:
- A3 已完成真实只读投影,Phase B 已完成真实 Project Launch 创建链。 - P6 后主流程已经形成真实闭环:Project Launch 创建项目、Change Review 审查并 commit 项目真相、Execution Bridge 交接与执行回写、Release Validation 记录发布前验证、Release Feedback 摄取发布后信号,并能把 `IterationRecommendation` 送回 Change Review。
- Phase C1 已把项目内消息、结构化候选、审查动作和证据链落到真实表。 - 当前缺口不再是 D.2 execution handoff,而是下一轮建议的质量治理、审查解释和个人队列承接:同类 recommendation 需要分组/去重/评估风险,Reviewer 需要知道“为什么生成这一组待确认变更”,Space/Personal Queue 需要把下一步审查动作排进个人工作面。
- C2 已实现真实 ChangeSet commit,Phase D/D.2/D.3 已把 Execution Bridge 从真实读侧推进到执行交接和执行结果回写。 - P7 不新增战略页面,不重开路由;只沿现有 `ReleaseGrowthPage / ChangeReviewPage / PersonalSpace` 主链补质量、审查和队列能力。
- Release Validation P1、Release Feedback P1、Iteration Loop P1 已把发布前验证、发布后反馈和 recommendation -> 下一轮 ChangeSet draft 串回 Change Review。
- Space P5 已只读展示下一轮迭代草稿关注,Account P6 已把 iteration review 事件映射到待确认提醒偏好。
- 当前缺口不是继续扩页面或重做执行桥,而是让“下一轮待确认变更”更可审、更可分配、更容易被个人队列接住。
### 7.1 P7 并行建议 ### 7.1 P7 并行建议
@ -734,9 +731,8 @@ P7 不新增战略页面,不新增顶层路由,不改移动底部导航;
目标: 目标:
- 提升 recommendation -> ChangeSet draft 的质量:分组、去重、风险级别、证据摘要和 nextActions 更清晰。 - 当前 Main Flow 下一步进入 `iteration-loop-p2-recommendation-quality-batch`:在 `execution-summary.releaseSignals` 中补 recommendation quality fields,支持同组 recommendation 批量生成一个可复用 ChangeSet draft,并在 Change Review 解释来源组、反馈关系、风险和审查优先级。
- 让 ReleaseGrowthPage / ChangeReviewPage 能区分“反馈建议草稿、任务草稿、风险草稿、决策草稿”,但不自动 commit。 - 后续 Reviewer packet 可继续围绕待确认项排序、review checklist、accept/edit/defer 质量说明推进,但不得新增战略页或自动写 Project Memory。
- 保持 item 类型只落 `Requirement / Task / Risk / Decision`;不写 Project Memory、不创建正式 Task、不触发发布。
建议写范围: 建议写范围:
@ -1029,7 +1025,7 @@ Shell / Integration owner 负责:
未完成或风险: 未完成或风险:
- 本阶段只接入 feedback ingest 与 releaseSignals 读模型;RevenueEvent / ComplianceCheckRun 已可被后续扩展继续纳入更完整发布后运营读模型。 - 本阶段只接入 feedback ingest 与 releaseSignals 读模型;RevenueEvent / ComplianceCheckRun 已可被后续扩展继续纳入更完整发布后运营读模型。
- iteration recommendation 目前作为建议底座展示,不自动生成 ChangeSet;下一轮如果要从 recommendation 进入待确认中心,需要单独开 Main Flow packet。 - iteration recommendation 已能生成下一轮 ChangeSet draft;P7 继续推进 recommendation quality batch,使同类建议可分组、去重、评估风险并批量进入待确认中心。
- 全量前端 typecheck/build 需在包含 `frontend/src/lib/*` helper 的集成环境或由 Shell / Integration owner 修正该 ignored 文件边界后重新验证。 - 全量前端 typecheck/build 需在包含 `frontend/src/lib/*` helper 的集成环境或由 Shell / Integration owner 修正该 ignored 文件边界后重新验证。
### 2026-04-26 / iteration-loop-p1-recommendation-to-changeset-draft / implemented ### 2026-04-26 / iteration-loop-p1-recommendation-to-changeset-draft / implemented
@ -1070,6 +1066,46 @@ Shell / Integration owner 负责:
- 本阶段只把 release feedback recommendation 转入 Change Review 待确认中心;不执行部署、不触发外部支付/推送、不自动写 Project Memory、不自动创建正式任务。 - 本阶段只把 release feedback recommendation 转入 Change Review 待确认中心;不执行部署、不触发外部支付/推送、不自动写 Project Memory、不自动创建正式任务。
- Space 仍只读消费 `releaseSignals` 并跳转主流程;Account Settings 只提供偏好 helper,不参与 PMFlow OS 写入。 - Space 仍只读消费 `releaseSignals` 并跳转主流程;Account Settings 只提供偏好 helper,不参与 PMFlow OS 写入。
### 2026-04-26 / iteration-loop-p2-recommendation-quality-batch / implemented
完成内容:
- `execution-summary.releaseSignals.recommendations` 新增 recommendation quality 字段:`groupKey / duplicateOf / mergedCount / impactLevel / riskLevel / suggestedChangeTypes / reviewPriority / quality`,并新增 `recommendationGroups` 聚合同组建议。
- 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendation-groups/{group_key}/change-set-drafts`,支持按同组 recommendation 批量生成一个 open ChangeSet draft;重复调用会复用既有 draft 并返回 `iteration_recommendation_group_changeset_reused`。
- 后端批量 draft 仍只生成 pending_review `Requirement / Task / Risk / Decision` ChangeItem 与 `PMFlowEvidenceLink(source_type='iteration_recommendation_group')`,不自动 commit、不写 Project Memory、不创建正式 Task。
- 已生成 draft 的 recommendation 会在 `recommendation_summary` 追踪 `groupKey / groupChangeSetId / changeSetId / changeSetCreated`,使 releaseSignals 与 Change Review 能解释同组反馈来源。
- `ReleaseGrowthPage` 新增 recommendation group 卡片,展示合并数量、影响、风险、审查优先级、建议变更类型与生成理由,并调用真实批量 API;单条 recommendation 动作保留。
- `ChangeReviewPage` 在打开来自 release feedback iteration group 的 ChangeSet 时展示来源组、recommendation 数、feedback 数、风险/影响/审查优先级与 rationale,帮助 reviewer 判断为什么生成这一组待确认变更。
涉及文件:
- `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`
- `frontend/src/features/pmflow-os/pages/ChangeReviewPage.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` 通过:`20 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`2 files / 10 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。
边界:
- 本阶段只提升下一轮建议质量与批量待确认能力;不执行部署、不接外部支付、不接推送、不自动写 Project Memory、不创建正式 Task。
- Space / Account / router / bottom nav 未改;Personal Queue 承接可在后续 P7 packet 中单独推进。
### 2026-04-26 / big-module-integration-p4 / done ### 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/P5 的 `feedbackAttention` 只读消费 `execution-summary.releaseSignals`,把发布后 feedback、revenue、ROI、compliance、iteration recommendation 与已生成的下一轮 ChangeSet draft 关注项显示到 `/space`。发布验证、发布后信号或下一轮 draft 缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规、建议或 ChangeSet draft 结果。项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`,下一轮 draft 关注项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入、ReleaseValidationRun 写入、FeedbackIngest 写入或发布执行。 - `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/P5 的 `feedbackAttention` 只读消费 `execution-summary.releaseSignals`,把发布后 feedback、revenue、ROI、compliance、iteration recommendation 与已生成的下一轮 ChangeSet draft 关注项显示到 `/space`。发布验证、发布后信号或下一轮 draft 缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规、建议或 ChangeSet draft 结果。项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`,下一轮 draft 关注项进入 `/change-review?projectId=<id>&changeSetId=<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/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/P6 的 `getPostReleaseNotificationPreference(settings, event)` 把发布后 `feedback_received / revenue_signal / compliance_blocked` 映射到 `releaseGate` 偏好,把 `iteration_recommendation / next_change_review` 映射到 `pendingConfirmation` 偏好和 `change_review` surface,并返回 `spaceReadable / evidencePolicy / reminderPolicy` 供 Main Flow 与 Space 消费。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据、recommendation 或 ChangeSet。`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/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/P6 的 `getPostReleaseNotificationPreference(settings, event)` 把发布后 `feedback_received / revenue_signal / compliance_blocked` 映射到 `releaseGate` 偏好,把 `iteration_recommendation / next_change_review` 映射到 `pendingConfirmation` 偏好和 `change_review` surface,并返回 `spaceReadable / evidencePolicy / reminderPolicy` 供 Main Flow 与 Space 消费。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据、recommendation 或 ChangeSet。`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 与 iteration recommendation -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 - `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 与同组 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` 表达“未确认不得写入”的局部提示。 - `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 quality grouping,把真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;`ChangeReviewPage.tsx` 会展示 `iteration_recommendation_group` evidence 的来源组、feedback 数、recommendation 数、风险、影响和审查优先级;同时读取 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,Iteration Loop P2 已接入 `POST /iteration-recommendation-groups/{group_key}/change-set-drafts` 并在 `releaseSignals.recommendationGroups` 暴露分组/去重/风险/影响合同;当前仍不调用真实 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 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`;Iteration Loop P2 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendation-groups/{group_key}/change-set-drafts`,把同组 recommendation 批量转成一个 open ChangeSet draft 与 `iteration_recommendation_group` evidence,并在 `releaseSignals.recommendations / recommendationGroups` 投影 `groupKey / duplicateOf / mergedCount / impactLevel / riskLevel / suggestedChangeTypes / reviewPriority`;这些步骤都不直接执行本机命令、真实发布命令、外部支付、真实推送、自动 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。 - 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/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、治理页和图表代码。 - `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 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee`
- SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}` - SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}`
- SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases` - 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 与 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 - 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;Iteration Loop P2 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendation-groups/{group_key}/change-set-drafts`,并在 `execution-summary.releaseSignals.recommendations` 补 `groupKey / duplicateOf / mergedCount / impactLevel / riskLevel / suggestedChangeTypes / reviewPriority`,在 `recommendationGroups` 聚合同组建议,支持同组 recommendation 批量生成一个可复用 ChangeSet draft 与 `iteration_recommendation_group` evidence;这些步骤均不执行发布命令、外部支付、真实推送、自动 Project Memory commit 或正式 Task 创建
- 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` - 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` - 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`
@ -98,7 +98,7 @@
- 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 大模块并行公共壳:参考 `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 P7 当前推荐任务:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 第 7 节;P6 后真实闭环已到 `feedback -> recommendation -> ChangeSet draft -> Change Review`,下一步推荐并行推进 `main-flow-p7-iteration-quality`、`main-flow-p7-reviewer-decision-desk`、`space-p7-personal-queue`,不新增战略页面、不改 router / bottom nav、不绕过 Change Review 人类确认边界 - PMFlow P7 当前推荐任务:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 第 7 节;P6 后真实闭环已到 `feedback -> recommendation -> ChangeSet draft -> Change Review`,下一步推荐并行推进 `main-flow-p7-iteration-quality`、`main-flow-p7-reviewer-decision-desk`、`space-p7-personal-queue`,不新增战略页面、不改 router / bottom nav、不绕过 Change Review 人类确认边界
- 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 草案 - 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`、`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` - 新增前端 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`、`createPMFlowIterationRecommendationGroupChangeSetDraft`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同,`execution-summary.releaseSignals` 是 Release/Growth 发布后 feedback/revenue/recommendation 信号、recommendation quality grouping 与下一轮 ChangeSet draft 状态前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft`
- AI Steward 页面入口:参考 `frontend/src/pages/ai-steward/` - 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 问答浮窗与上下文显性化组件:参考 `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` - AI Steward QA contract 前端类型(含 `analysis_packet`):参考 `frontend/src/api/aiSteward.ts`
@ -108,8 +108,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>`,已生成 ChangeSet draft 的下一轮建议进入 `/change-review?projectId=<id>&changeSetId=<id>`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate / execution-summary.releaseSignals`,发布验证、发布后信号或 iteration draft 缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规、iteration recommendation 或 ChangeSet draft,不直接写 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/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>`,已生成 ChangeSet draft 的下一轮建议进入 `/change-review?projectId=<id>&changeSetId=<id>`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate / execution-summary.releaseSignals`,发布验证、发布后信号或 iteration draft 缺失时显示明确空态,不虚构验证、反馈、收入、ROI、合规、iteration recommendation 或 ChangeSet draft,不直接写 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` - 新项目启动台 / 首次进入页前端产品域:参考 `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/P6 偏好合同:参考 `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/P6 的 `getPostReleaseNotificationPreference(settings, event)` 供发布后反馈与下一轮待确认消费:`feedback_received / revenue_signal / compliance_blocked` 使用 `releaseGate` 偏好,`iteration_recommendation / next_change_review` 使用 `pendingConfirmation` 偏好并落到 `change_review` surface,返回 `spaceReadable / showInAppReminder / evidencePolicy / reminderPolicy`;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run、releaseSignals 或下一轮 ChangeSet draft;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入 - Account Settings / `/me` 前端产品域与 P1/P2/P3/P5/P6 偏好合同:参考 `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/P6 的 `getPostReleaseNotificationPreference(settings, event)` 供发布后反馈与下一轮待确认消费:`feedback_received / revenue_signal / compliance_blocked` 使用 `releaseGate` 偏好,`iteration_recommendation / next_change_review` 使用 `pendingConfirmation` 偏好并落到 `change_review` surface,返回 `spaceReadable / showInAppReminder / evidencePolicy / reminderPolicy`;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run、releaseSignals 或下一轮 ChangeSet draft;这些 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 与 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 共享 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 与同组 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/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,并按 `recommendationGroups` 批量生成同组下一轮 ChangeSet draft;`ChangeReviewPage.tsx` 会解释来自 `iteration_recommendation_group` evidence 的组、反馈、风险、影响和审查优先级;生成后仍进入 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/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` - 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` - Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts`

View File

@ -164,6 +164,7 @@ export interface PMFlowReleaseSignals {
feedback: PMFlowReleaseFeedbackIngest[]; feedback: PMFlowReleaseFeedbackIngest[];
revenueEvents: PMFlowReleaseRevenueEvent[]; revenueEvents: PMFlowReleaseRevenueEvent[];
recommendations: PMFlowIterationRecommendation[]; recommendations: PMFlowIterationRecommendation[];
recommendationGroups?: PMFlowIterationRecommendationGroup[];
nextActions: string[]; nextActions: string[];
warnings: string[]; warnings: string[];
} }
@ -208,10 +209,32 @@ export interface PMFlowIterationRecommendation {
proposedTasks: Array<Record<string, unknown>>; proposedTasks: Array<Record<string, unknown>>;
changeSetCreated: boolean; changeSetCreated: boolean;
changeSetId?: string | null; changeSetId?: string | null;
groupKey?: string | null;
duplicateOf?: string | null;
mergedCount?: number;
impactLevel?: string | null;
riskLevel?: string | null;
suggestedChangeTypes?: string[];
reviewPriority?: string | null;
quality?: Record<string, unknown>;
createdAt?: string | null; createdAt?: string | null;
updatedAt?: string | null; updatedAt?: string | null;
} }
export interface PMFlowIterationRecommendationGroup {
groupKey: string;
title?: string | null;
recommendationIds: string[];
mergedCount: number;
impactLevel: string;
riskLevel: string;
suggestedChangeTypes: string[];
reviewPriority: string;
changeSetCreated: boolean;
changeSetId?: string | null;
rationale?: string | null;
}
export interface PMFlowReleaseFeedbackIngestRequest { export interface PMFlowReleaseFeedbackIngestRequest {
source_type?: string; source_type?: string;
title?: string | null; title?: string | null;
@ -233,6 +256,11 @@ export interface PMFlowIterationRecommendationChangeSetDraftRequest {
note?: string | null; note?: string | null;
} }
export interface PMFlowIterationRecommendationGroupChangeSetDraftRequest {
recommendation_ids?: string[];
note?: string | null;
}
export interface PMFlowIterationRecommendationChangeSetDraftResult { export interface PMFlowIterationRecommendationChangeSetDraftResult {
changeSetDetail: PMFlowChangeSetDetail; changeSetDetail: PMFlowChangeSetDetail;
executionSummary: PMFlowExecutionSummary; executionSummary: PMFlowExecutionSummary;
@ -445,6 +473,17 @@ export async function createPMFlowIterationRecommendationChangeSetDraft(
); );
} }
export async function createPMFlowIterationRecommendationGroupChangeSetDraft(
projectId: string,
groupKey: string,
payload: PMFlowIterationRecommendationGroupChangeSetDraftRequest = {}
): Promise<PMFlowIterationRecommendationChangeSetDraftResult> {
return post<PMFlowIterationRecommendationChangeSetDraftResult>(
`/pmflow-os/projects/${projectId}/iteration-recommendation-groups/${encodeURIComponent(groupKey)}/change-set-drafts`,
payload
);
}
export async function createPMFlowThreadMessage( export async function createPMFlowThreadMessage(
projectId: string, projectId: string,
threadId: string, threadId: string,

View File

@ -9,12 +9,43 @@ import { usePMFlowOsStore } from "@/features/pmflow-os/store";
const { Filter, ListFilter } = reviewToolbarIcons; const { Filter, ListFilter } = reviewToolbarIcons;
type IterationEvidenceMeta = {
sourceType?: string;
changeSetId?: string;
metadata?: {
groupKey?: string;
recommendationIds?: string[];
feedbackIngestIds?: string[];
quality?: {
impactLevel?: string;
riskLevel?: string;
reviewPriority?: string;
rationale?: string;
};
};
};
function getIterationEvidence(state: ReturnType<typeof usePMFlowOsStore>["state"], changeSetId?: string | null) {
if (!changeSetId) return null;
return (
state.evidence.find((entry) => {
const raw = entry as typeof entry & IterationEvidenceMeta;
return (
raw.changeSetId === changeSetId &&
(raw.sourceType === "iteration_recommendation_group" || raw.sourceType === "iteration_recommendation")
);
}) || null
);
}
export default function ChangeReviewPage() { export default function ChangeReviewPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { state, dataSourceMode, acceptChange, editChange, deferChange, promoteAcceptedToGraph } = usePMFlowOsStore(); const { state, dataSourceMode, acceptChange, editChange, deferChange, promoteAcceptedToGraph } = usePMFlowOsStore();
const activeChangeSet = state.changeSets.find((entry) => entry.id === state.activeChangeSetId) || state.changeSets[0] || null; const activeChangeSet = state.changeSets.find((entry) => entry.id === state.activeChangeSetId) || state.changeSets[0] || null;
const items = state.changeItems.filter((item) => item.changeSetId === activeChangeSet?.id); const items = state.changeItems.filter((item) => item.changeSetId === activeChangeSet?.id);
const selectedItems = items.filter((item) => item.status === "pending_review").slice(0, 2); const selectedItems = items.filter((item) => item.status === "pending_review").slice(0, 2);
const iterationEvidence = getIterationEvidence(state, activeChangeSet?.id);
const iterationMeta = (iterationEvidence as (typeof iterationEvidence & IterationEvidenceMeta) | null)?.metadata;
const writeback = () => { const writeback = () => {
if (!activeChangeSet) return; if (!activeChangeSet) return;
@ -47,6 +78,23 @@ export default function ChangeReviewPage() {
</div> </div>
<ReviewExtractionHero activeChangeSet={activeChangeSet} /> <ReviewExtractionHero activeChangeSet={activeChangeSet} />
{iterationEvidence ? (
<div className="rounded-xl border border-indigo-100 bg-indigo-50 p-4 text-sm leading-6 text-indigo-800">
<div className="font-semibold">
{iterationMeta?.groupKey ? "来自发布后反馈建议组" : "来自发布后反馈建议"}
</div>
<div className="mt-1">
{iterationMeta?.groupKey ? `组 ${iterationMeta.groupKey};` : ""}
recommendation {iterationMeta?.recommendationIds?.length || 1} 条;feedback{" "}
{iterationMeta?.feedbackIngestIds?.length || 0} 条;风险{" "}
{iterationMeta?.quality?.riskLevel || "未评估"};影响 {iterationMeta?.quality?.impactLevel || "未评估"};审查{" "}
{iterationMeta?.quality?.reviewPriority || "p2"}。
</div>
<div className="mt-1 text-xs text-indigo-600">
{iterationMeta?.quality?.rationale || "这些变更只进入 Change Review,接受前不会写入 Project Memory 或创建正式 Task。"}
</div>
</div>
) : null}
<ChangeReviewTable <ChangeReviewTable
items={items} items={items}
evidence={state.evidence} evidence={state.evidence}

View File

@ -23,6 +23,7 @@ import { useState, type ButtonHTMLAttributes, type ReactNode } from "react";
import { getAccountSettings, shouldShowAccountNotification } from "@/api/accountSettings"; import { getAccountSettings, shouldShowAccountNotification } from "@/api/accountSettings";
import type { import type {
PMFlowIterationRecommendation, PMFlowIterationRecommendation,
PMFlowIterationRecommendationGroup,
PMFlowReleaseFeedbackIngest, PMFlowReleaseFeedbackIngest,
PMFlowReleaseGateEvidence, PMFlowReleaseGateEvidence,
PMFlowReleaseRevenueEvent, PMFlowReleaseRevenueEvent,
@ -394,6 +395,98 @@ function recommendationText(item: PMFlowIterationRecommendation) {
return typeof text === "string" && text.trim() ? text : "已基于真实反馈生成下一轮建议,等待进入后续 iteration recommendation 主链。"; return typeof text === "string" && text.trim() ? text : "已基于真实反馈生成下一轮建议,等待进入后续 iteration recommendation 主链。";
} }
function qualityLabel(value?: string | null) {
if (value === "high") return "高";
if (value === "medium") return "中";
if (value === "low") return "低";
return value || "未评估";
}
function priorityLabel(value?: string | null) {
if (value === "p0") return "P0";
if (value === "p1") return "P1";
if (value === "p2") return "P2";
return value || "P2";
}
function qualityTone(value?: string | null): StatusTone {
if (value === "high" || value === "p0") return "danger";
if (value === "medium" || value === "p1") return "warning";
if (value === "low" || value === "p2") return "success";
return "neutral";
}
function RecommendationQualityMeta({ item }: { item: PMFlowIterationRecommendation }) {
return (
<div className="mt-3 flex flex-wrap gap-2 text-xs">
{item.groupKey ? <StatusChip label={`组 ${item.groupKey}`} tone="info" /> : null}
<StatusChip label={`影响 ${qualityLabel(item.impactLevel)}`} tone={qualityTone(item.impactLevel)} />
<StatusChip label={`风险 ${qualityLabel(item.riskLevel)}`} tone={qualityTone(item.riskLevel)} />
<StatusChip label={`审查 ${priorityLabel(item.reviewPriority)}`} tone={qualityTone(item.reviewPriority)} />
{item.mergedCount && item.mergedCount > 1 ? <StatusChip label={`合并 ${item.mergedCount} 条`} tone="info" /> : null}
{item.duplicateOf ? <StatusChip label={`重复于 #${item.duplicateOf}`} tone="neutral" /> : null}
{(item.suggestedChangeTypes || []).slice(0, 4).map((type) => (
<span key={type} className="rounded-md border border-blue-200 bg-white px-2 py-1 text-blue-600">
{type}
</span>
))}
</div>
);
}
function RecommendationGroupRow({
group,
isLoading,
onCreate,
}: {
group: PMFlowIterationRecommendationGroup;
isLoading: boolean;
onCreate: (group: PMFlowIterationRecommendationGroup) => void;
}) {
return (
<div className="rounded-lg border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm leading-6 text-indigo-800">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="font-semibold">{group.title || `反馈建议组 · ${group.groupKey}`}</div>
<div className="mt-1 text-indigo-700">
{group.rationale || "基于同组发布后反馈聚合生成;用于决定是否批量进入下一轮 Change Review。"}
</div>
<div className="mt-3 flex flex-wrap gap-2 text-xs">
<StatusChip label={`组 ${group.groupKey}`} tone="info" />
<StatusChip label={`合并 ${group.mergedCount} 条`} tone="info" />
<StatusChip label={`影响 ${qualityLabel(group.impactLevel)}`} tone={qualityTone(group.impactLevel)} />
<StatusChip label={`风险 ${qualityLabel(group.riskLevel)}`} tone={qualityTone(group.riskLevel)} />
<StatusChip label={`审查 ${priorityLabel(group.reviewPriority)}`} tone={qualityTone(group.reviewPriority)} />
{group.suggestedChangeTypes.slice(0, 4).map((type) => (
<span key={type} className="rounded-md border border-indigo-200 bg-white px-2 py-1 text-indigo-600">
{type}
</span>
))}
</div>
{group.changeSetCreated ? (
<div className="mt-2 text-xs text-indigo-600">
已复用 ChangeSet draft {group.changeSetId ? `#${group.changeSetId}` : ""},同组 recommendation 会追踪到同一个 draft。
</div>
) : (
<div className="mt-2 text-xs text-indigo-600">批量生成只进入 Change Review,不自动 commit、不写 Project Memory、不创建正式 Task。</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",
group.changeSetCreated ? "border border-indigo-200 bg-white text-indigo-500" : "bg-indigo-600 text-white hover:bg-indigo-700"
)}
disabled={isLoading || group.changeSetCreated}
onClick={() => onCreate(group)}
>
{group.changeSetCreated ? <CheckCircle2 className="mr-1.5 h-3.5 w-3.5" /> : <ArrowRight className="mr-1.5 h-3.5 w-3.5" />}
{group.changeSetCreated ? "已生成组草稿" : "批量生成这一组待确认变更"}
</Button>
</div>
</div>
);
}
function checkTone(status?: string): StatusTone { function checkTone(status?: string): StatusTone {
if (status === "passed") return "success"; if (status === "passed") return "success";
if (status === "failed") return "danger"; if (status === "failed") return "danger";
@ -494,6 +587,7 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
createReleaseValidationRun, createReleaseValidationRun,
createReleaseFeedbackIngest, createReleaseFeedbackIngest,
createIterationRecommendationChangeSetDraft, createIterationRecommendationChangeSetDraft,
createIterationRecommendationGroupChangeSetDraft,
} = store; } = store;
const gate = executionSummary?.releaseGate; const gate = executionSummary?.releaseGate;
const releaseSignals = executionSummary?.releaseSignals; const releaseSignals = executionSummary?.releaseSignals;
@ -542,6 +636,13 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
note: "ReleaseGrowthPage 从发布后反馈建议生成下一轮待确认 ChangeSet draft。", note: "ReleaseGrowthPage 从发布后反馈建议生成下一轮待确认 ChangeSet draft。",
}); });
}; };
const handleCreateRecommendationGroupChangeSet = (group: PMFlowIterationRecommendationGroup) => {
if (group.changeSetCreated) return;
createIterationRecommendationGroupChangeSetDraft(group.groupKey, {
recommendation_ids: group.recommendationIds,
note: `ReleaseGrowthPage 从 ${group.groupKey} 同组发布后反馈批量生成下一轮待确认 ChangeSet draft。`,
});
};
return ( return (
<div className="min-h-full bg-[#f7faff] text-slate-950"> <div className="min-h-full bg-[#f7faff] text-slate-950">
@ -678,12 +779,16 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
{(releaseSignals?.revenueEvents || []).map((item) => ( {(releaseSignals?.revenueEvents || []).map((item) => (
<RevenueSignalRow key={item.id} item={item} /> <RevenueSignalRow key={item.id} item={item} />
))} ))}
{(releaseSignals?.recommendationGroups || []).map((group) => (
<RecommendationGroupRow key={group.groupKey} group={group} isLoading={isLoading} onCreate={handleCreateRecommendationGroupChangeSet} />
))}
{(releaseSignals?.recommendations || []).map((item) => ( {(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 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="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div> <div>
<div className="font-semibold">下一轮建议 · {item.priority}</div> <div className="font-semibold">下一轮建议 · {item.priority}</div>
<div className="mt-1">{recommendationText(item)}</div> <div className="mt-1">{recommendationText(item)}</div>
<RecommendationQualityMeta item={item} />
{item.changeSetCreated ? ( {item.changeSetCreated ? (
<div className="mt-2 text-xs text-blue-600"> <div className="mt-2 text-xs text-blue-600">
已生成 ChangeSet draft {item.changeSetId ? `#${item.changeSetId}` : ""},请到 Change Review 继续 accept / edit / defer。 已生成 ChangeSet draft {item.changeSetId ? `#${item.changeSetId}` : ""},请到 Change Review 继续 accept / edit / defer。

View File

@ -10,6 +10,7 @@ import {
consumePMFlowExecutionHandoff, consumePMFlowExecutionHandoff,
createPMFlowExecutionHandoff, createPMFlowExecutionHandoff,
createPMFlowExecutionProgressEvent, createPMFlowExecutionProgressEvent,
createPMFlowIterationRecommendationGroupChangeSetDraft,
createPMFlowIterationRecommendationChangeSetDraft, createPMFlowIterationRecommendationChangeSetDraft,
createPMFlowReleaseValidationRun, createPMFlowReleaseValidationRun,
createPMFlowReleaseFeedbackIngest, createPMFlowReleaseFeedbackIngest,
@ -48,6 +49,7 @@ vi.mock("@/api/pmflowOs", () => ({
consumePMFlowExecutionHandoff: vi.fn(), consumePMFlowExecutionHandoff: vi.fn(),
createPMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionHandoff: vi.fn(),
createPMFlowExecutionProgressEvent: vi.fn(), createPMFlowExecutionProgressEvent: vi.fn(),
createPMFlowIterationRecommendationGroupChangeSetDraft: vi.fn(),
createPMFlowIterationRecommendationChangeSetDraft: vi.fn(), createPMFlowIterationRecommendationChangeSetDraft: vi.fn(),
createPMFlowReleaseValidationRun: vi.fn(), createPMFlowReleaseValidationRun: vi.fn(),
createPMFlowReleaseFeedbackIngest: vi.fn(), createPMFlowReleaseFeedbackIngest: vi.fn(),
@ -62,6 +64,7 @@ const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff); const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff); const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent); const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent);
const createIterationRecommendationGroupChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationGroupChangeSetDraft);
const createIterationRecommendationChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationChangeSetDraft); const createIterationRecommendationChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationChangeSetDraft);
const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun); const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun);
const createReleaseFeedbackIngestMock = vi.mocked(createPMFlowReleaseFeedbackIngest); const createReleaseFeedbackIngestMock = vi.mocked(createPMFlowReleaseFeedbackIngest);
@ -703,6 +706,107 @@ describe("PMFlowOsProvider real change review actions", () => {
); );
}); });
it("creates a grouped ChangeSet draft from recommendation quality groups", 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: 2, revenueEventCount: 0, iterationRecommendationCount: 2, latestSignalAt: "2026-04-26T00:00:00" },
feedback: [],
revenueEvents: [],
recommendations: [
{
id: "rec-1",
projectId: "123",
feedbackIngestId: "feedback-1",
status: "draft",
priority: "high",
recommendationSummary: { summary: "补齐失败样本。" },
proposedCharterPatch: {},
proposedTasks: [],
changeSetCreated: false,
groupKey: "quality",
mergedCount: 2,
impactLevel: "high",
riskLevel: "high",
suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"],
reviewPriority: "p0",
},
{
id: "rec-2",
projectId: "123",
feedbackIngestId: "feedback-2",
status: "draft",
priority: "high",
recommendationSummary: { summary: "修复幻觉回答。" },
proposedCharterPatch: {},
proposedTasks: [],
changeSetCreated: false,
groupKey: "quality",
duplicateOf: "rec-1",
mergedCount: 2,
impactLevel: "high",
riskLevel: "high",
suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"],
reviewPriority: "p0",
},
],
recommendationGroups: [
{
groupKey: "quality",
title: "quality 反馈组",
recommendationIds: ["rec-1", "rec-2"],
mergedCount: 2,
impactLevel: "high",
riskLevel: "high",
suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"],
reviewPriority: "p0",
changeSetCreated: false,
rationale: "聚焦 quality;负向反馈 4 条;建议任务 2 个。",
},
],
nextActions: ["同组 recommendation 可批量生成下一轮待确认 ChangeSet draft。"],
warnings: [],
},
});
createIterationRecommendationGroupChangeSetDraftMock.mockResolvedValue({
changeSetDetail: {
changeSet: { id: "88", status: "open" },
items: [{ id: "89", objectType: "Requirement", status: "pending_review" }],
evidence: [{ id: "90", sourceType: "iteration_recommendation_group", metadata: { groupKey: "quality" } }],
approvals: [],
},
executionSummary: executionSummaryWithGate(releaseGate("ready")),
warnings: [],
});
const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage");
render(<ReleaseGrowthPage />, { wrapper: wrapper() });
expect(await screen.findByText("quality 反馈组")).toBeTruthy();
expect(screen.getByText("聚焦 quality;负向反馈 4 条;建议任务 2 个。")).toBeTruthy();
const action = await screen.findByText("批量生成这一组待确认变更");
fireEvent.click(action);
await waitFor(() =>
expect(createIterationRecommendationGroupChangeSetDraftMock).toHaveBeenCalledWith(
"123",
"quality",
expect.objectContaining({
recommendation_ids: ["rec-1", "rec-2"],
note: expect.stringContaining("quality"),
})
)
);
});
it("writes release feedback through the PMFlow OS api in real mode", async () => { it("writes release feedback through the PMFlow OS api in real mode", async () => {
createReleaseFeedbackIngestMock.mockResolvedValue({ createReleaseFeedbackIngestMock.mockResolvedValue({
ingest: { ingest: {
@ -771,6 +875,7 @@ describe("PMFlowOsProvider real change review actions", () => {
expect(createExecutionProgressEventMock).not.toHaveBeenCalled(); expect(createExecutionProgressEventMock).not.toHaveBeenCalled();
expect(createReleaseValidationRunMock).not.toHaveBeenCalled(); expect(createReleaseValidationRunMock).not.toHaveBeenCalled();
expect(createReleaseFeedbackIngestMock).not.toHaveBeenCalled(); expect(createReleaseFeedbackIngestMock).not.toHaveBeenCalled();
expect(createIterationRecommendationGroupChangeSetDraftMock).not.toHaveBeenCalled();
expect(createIterationRecommendationChangeSetDraftMock).not.toHaveBeenCalled(); expect(createIterationRecommendationChangeSetDraftMock).not.toHaveBeenCalled();
expect(getExecutionSummaryMock).not.toHaveBeenCalled(); expect(getExecutionSummaryMock).not.toHaveBeenCalled();
expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted"); expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted");

View File

@ -7,6 +7,7 @@ import {
createPMFlowExecutionHandoff, createPMFlowExecutionHandoff,
createPMFlowExecutionProgressEvent, createPMFlowExecutionProgressEvent,
createPMFlowExtraction, createPMFlowExtraction,
createPMFlowIterationRecommendationGroupChangeSetDraft,
createPMFlowIterationRecommendationChangeSetDraft, createPMFlowIterationRecommendationChangeSetDraft,
createPMFlowReleaseFeedbackIngest, createPMFlowReleaseFeedbackIngest,
createPMFlowReleaseValidationRun, createPMFlowReleaseValidationRun,
@ -19,6 +20,7 @@ import {
type PMFlowExecutionProgressEventRequest, type PMFlowExecutionProgressEventRequest,
type PMFlowExecutionSummary, type PMFlowExecutionSummary,
type PMFlowIterationRecommendationChangeSetDraftRequest, type PMFlowIterationRecommendationChangeSetDraftRequest,
type PMFlowIterationRecommendationGroupChangeSetDraftRequest,
type PMFlowProposedChangeItem, type PMFlowProposedChangeItem,
type PMFlowReleaseFeedbackIngestRequest, type PMFlowReleaseFeedbackIngestRequest,
type PMFlowReleaseValidationRunRequest, type PMFlowReleaseValidationRunRequest,
@ -549,6 +551,10 @@ interface PMFlowOsStoreValue {
recommendationId: string, recommendationId: string,
payload?: PMFlowIterationRecommendationChangeSetDraftRequest payload?: PMFlowIterationRecommendationChangeSetDraftRequest
) => void; ) => void;
createIterationRecommendationGroupChangeSetDraft: (
groupKey: string,
payload?: PMFlowIterationRecommendationGroupChangeSetDraftRequest
) => void;
advanceExecution: () => void; advanceExecution: () => void;
writeBackExecution: (outcome: "passed" | "failed") => void; writeBackExecution: (outcome: "passed" | "failed") => void;
publishRelease: () => void; publishRelease: () => void;
@ -802,6 +808,18 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
} }
mutateDemoOnly((current) => ingestGrowthSignalMutation(current)); mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
}, },
createIterationRecommendationGroupChangeSetDraft: (groupKey, payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() =>
createPMFlowIterationRecommendationGroupChangeSetDraft(dataSource.projectId || "", groupKey, {
recommendation_ids: payload?.recommendation_ids || [],
note: payload?.note || "ReleaseGrowthPage 批量生成同组下一轮待确认 ChangeSet draft;不自动 commit。",
})
);
return;
}
mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
},
advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)), advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)),
writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)),
publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)), publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),