feat(team): finalize reviewer routing contract
This commit is contained in:
parent
3609bfe687
commit
5b2add2924
|
|
@ -114,7 +114,7 @@ frontend/
|
||||||
- `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`、Team reviewer routing 与 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`、Team reviewer routing 与 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、Team reviewer routing 与 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`;`truth-state.reviewerRouting` 只读生成建议审查人/原因/warning,不写 approval、不自动 assign reviewer;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、Team reviewer routing 与 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`;`truth-state.reviewerRouting` 是 `{status, warningCode, summary, items}` 只读合同,生成建议审查人/原因/来源/风险/跳转 URL,不写 approval、不自动 assign reviewer;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` 主页面;`TeamLayerPage` 真实模式读取 `state.reviewerRouting` 展示待审 ChangeSet、建议审查人、审查原因、风险类型和缺角色 warning,动作只跳转 Change Review,不自动分派 reviewer;`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 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` 主页面;`TeamLayerPage` 真实模式读取 `state.reviewerRouting` 展示 ready/empty/warning、待审 ChangeSet、来源、风险等级、建议审查人或 fallback role、审查原因、待审 item 数和证据数,动作只跳转 `/change-review?projectId=<id>&changeSetId=<id>`,不自动分派 reviewer;`ExecutionBridgePage` 已支持创建 handoff、运行端领取确认和执行进度回写;`ReleaseGrowthPage` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,可创建 release validation run、摄取发布后 feedback、从真实 recommendation 生成下一轮待确认 ChangeSet draft,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据、审计状态或发布后信号;静态发布/增长原型仅保留在显式 demo 模式;后端只记录 progress/release gate/release validation/release feedback/recommendation draft 元数据,不执行本机命令、真实发布命令、外部支付、真实推送、自动 Project Memory commit 或正式任务创建;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。
|
||||||
- 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。
|
- 旧登录实现 `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/` 目录说明(模块化拆分后)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ PMFlowReleaseValidationDecision = Literal["enter_release_candidate", "keep_block
|
||||||
ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"]
|
ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"]
|
||||||
PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"]
|
PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"]
|
||||||
PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"]
|
PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"]
|
||||||
|
PMFlowReviewerRoutingStatus = Literal["empty", "ready", "warning"]
|
||||||
|
PMFlowReviewerRoutingSourceType = Literal["manual", "ai_extraction", "iteration_recommendation", "unknown"]
|
||||||
|
PMFlowReviewerRoutingPriority = Literal["normal", "elevated", "urgent"]
|
||||||
|
PMFlowReviewerConfidence = Literal["low", "medium", "high"]
|
||||||
|
PMFlowFallbackReviewerRole = Literal["OWNER", "REVIEWER", "MEMBER"]
|
||||||
PMFlowObjectType = Literal[
|
PMFlowObjectType = Literal[
|
||||||
"Requirement",
|
"Requirement",
|
||||||
"Decision",
|
"Decision",
|
||||||
|
|
@ -277,23 +282,45 @@ class PMFlowTeamInbox(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class PMFlowReviewerSuggestion(BaseModel):
|
class PMFlowReviewerSuggestion(BaseModel):
|
||||||
userId: str
|
userId: Optional[str] = None
|
||||||
name: str
|
displayName: Optional[str] = None
|
||||||
role: str
|
role: Optional[str] = None
|
||||||
reason: str
|
reason: str
|
||||||
|
confidence: PMFlowReviewerConfidence = "low"
|
||||||
|
|
||||||
|
|
||||||
class PMFlowChangeSetReviewerRouting(BaseModel):
|
class PMFlowReviewerRoutingItem(BaseModel):
|
||||||
|
projectId: str
|
||||||
changeSetId: str
|
changeSetId: str
|
||||||
headline: str
|
changeSetTitle: str
|
||||||
summary: str
|
changeSetStatus: str
|
||||||
|
sourceType: PMFlowReviewerRoutingSourceType = "unknown"
|
||||||
|
reviewReason: str
|
||||||
|
riskLevel: PMFlowRiskLevel = "low"
|
||||||
|
priority: PMFlowReviewerRoutingPriority = "normal"
|
||||||
|
changeTypes: list[str] = Field(default_factory=list)
|
||||||
|
itemCount: int = 0
|
||||||
pendingItemCount: int = 0
|
pendingItemCount: int = 0
|
||||||
highestRiskLevel: PMFlowRiskLevel = "low"
|
evidenceCount: int = 0
|
||||||
riskTypes: list[str] = Field(default_factory=list)
|
suggestedReviewer: Optional[PMFlowReviewerSuggestion] = None
|
||||||
crossDomain: bool = False
|
fallbackReviewerRole: Optional[PMFlowFallbackReviewerRole] = None
|
||||||
suggestedReviewers: list[PMFlowReviewerSuggestion] = Field(default_factory=list)
|
targetUrl: str
|
||||||
reviewReasons: list[str] = Field(default_factory=list)
|
createdAt: Optional[str] = None
|
||||||
warning: Optional[str] = None
|
updatedAt: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PMFlowReviewerRoutingSummary(BaseModel):
|
||||||
|
totalPendingReview: int = 0
|
||||||
|
highRiskCount: int = 0
|
||||||
|
missingReviewerCount: int = 0
|
||||||
|
iterationDraftCount: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PMFlowReviewerRoutingProjection(BaseModel):
|
||||||
|
status: PMFlowReviewerRoutingStatus = "empty"
|
||||||
|
warningCode: Optional[str] = None
|
||||||
|
items: list[PMFlowReviewerRoutingItem] = Field(default_factory=list)
|
||||||
|
summary: PMFlowReviewerRoutingSummary = Field(default_factory=PMFlowReviewerRoutingSummary)
|
||||||
|
|
||||||
|
|
||||||
class PMFlowProjectMemory(BaseModel):
|
class PMFlowProjectMemory(BaseModel):
|
||||||
|
|
@ -322,7 +349,7 @@ class ProjectTruthStateProjection(BaseModel):
|
||||||
approvals: list[dict[str, Any]] = Field(default_factory=list)
|
approvals: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
feed: list[PMFlowFeedEvent] = Field(default_factory=list)
|
feed: list[PMFlowFeedEvent] = Field(default_factory=list)
|
||||||
teamInbox: PMFlowTeamInbox = Field(default_factory=PMFlowTeamInbox)
|
teamInbox: PMFlowTeamInbox = Field(default_factory=PMFlowTeamInbox)
|
||||||
reviewerRouting: list[PMFlowChangeSetReviewerRouting] = Field(default_factory=list)
|
reviewerRouting: PMFlowReviewerRoutingProjection = Field(default_factory=PMFlowReviewerRoutingProjection)
|
||||||
activeChangeSetId: Optional[str] = None
|
activeChangeSetId: Optional[str] = None
|
||||||
selectedTaskId: Optional[str] = None
|
selectedTaskId: Optional[str] = None
|
||||||
execution: PMFlowExecutionContext = Field(default_factory=PMFlowExecutionContext)
|
execution: PMFlowExecutionContext = Field(default_factory=PMFlowExecutionContext)
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,10 @@ class PMFlowOSProjectionService:
|
||||||
active_change_set_id = self._active_change_set_id(review_projection["change_sets"])
|
active_change_set_id = self._active_change_set_id(review_projection["change_sets"])
|
||||||
project_members = await self._load_project_members(project=project)
|
project_members = await self._load_project_members(project=project)
|
||||||
reviewer_routing, reviewer_warnings = self._project_reviewer_routing(
|
reviewer_routing, reviewer_warnings = self._project_reviewer_routing(
|
||||||
|
project_id=project_id,
|
||||||
change_sets=review_projection["change_sets"],
|
change_sets=review_projection["change_sets"],
|
||||||
change_items=review_projection["change_items"],
|
change_items=review_projection["change_items"],
|
||||||
|
evidence=review_projection["evidence"],
|
||||||
members=project_members,
|
members=project_members,
|
||||||
)
|
)
|
||||||
loop_stage = self._review_loop_stage(
|
loop_stage = self._review_loop_stage(
|
||||||
|
|
@ -1052,27 +1054,35 @@ class PMFlowOSProjectionService:
|
||||||
def _project_reviewer_routing(
|
def _project_reviewer_routing(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
project_id: int,
|
||||||
change_sets: list[dict[str, Any]],
|
change_sets: list[dict[str, Any]],
|
||||||
change_items: list[dict[str, Any]],
|
change_items: list[dict[str, Any]],
|
||||||
|
evidence: list[dict[str, Any]],
|
||||||
members: list[User],
|
members: list[User],
|
||||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
) -> tuple[dict[str, Any], list[str]]:
|
||||||
items_by_set: dict[str, list[dict[str, Any]]] = {}
|
items_by_set: dict[str, list[dict[str, Any]]] = {}
|
||||||
for item in change_items:
|
for item in change_items:
|
||||||
if item.get("status") != "pending_review":
|
if item.get("status") != "pending_review":
|
||||||
continue
|
continue
|
||||||
items_by_set.setdefault(str(item.get("changeSetId")), []).append(item)
|
items_by_set.setdefault(str(item.get("changeSetId")), []).append(item)
|
||||||
|
|
||||||
routings: list[dict[str, Any]] = []
|
evidence_by_set: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for row in evidence:
|
||||||
|
evidence_by_set.setdefault(str(row.get("changeSetId") or ""), []).append(row)
|
||||||
|
|
||||||
|
routing_items: list[dict[str, Any]] = []
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
for change_set in change_sets:
|
for change_set in change_sets:
|
||||||
change_set_id = str(change_set.get("id") or "")
|
change_set_id = str(change_set.get("id") or "")
|
||||||
pending_items = items_by_set.get(change_set_id, [])
|
pending_items = items_by_set.get(change_set_id, [])
|
||||||
if not change_set_id or not pending_items:
|
if not change_set_id or not pending_items:
|
||||||
continue
|
continue
|
||||||
|
change_set_evidence = evidence_by_set.get(change_set_id, [])
|
||||||
risk_levels = [str(item.get("riskLevel") or "medium") for item in pending_items]
|
risk_levels = [str(item.get("riskLevel") or "medium") for item in pending_items]
|
||||||
object_types = sorted({str(item.get("objectType") or "Unknown") for item in pending_items})
|
object_types = sorted({str(item.get("objectType") or "Unknown") for item in pending_items})
|
||||||
risk_groups = sorted({str(item.get("group") or "unknown") for item in pending_items})
|
risk_groups = sorted({str(item.get("group") or "unknown") for item in pending_items})
|
||||||
highest_risk = self._highest_risk_level(risk_levels)
|
high_risk_signal = any(str(item.get("objectType") or "").lower() == "risk" or str(item.get("group") or "").lower() == "risk" for item in pending_items)
|
||||||
|
highest_risk = "high" if high_risk_signal else self._highest_risk_level(risk_levels)
|
||||||
cross_domain = len(object_types) > 1 or len(risk_groups) > 1
|
cross_domain = len(object_types) > 1 or len(risk_groups) > 1
|
||||||
review_reasons = self._review_routing_reasons(
|
review_reasons = self._review_routing_reasons(
|
||||||
pending_count=len(pending_items),
|
pending_count=len(pending_items),
|
||||||
|
|
@ -1081,33 +1091,63 @@ class PMFlowOSProjectionService:
|
||||||
risk_groups=risk_groups,
|
risk_groups=risk_groups,
|
||||||
cross_domain=cross_domain,
|
cross_domain=cross_domain,
|
||||||
)
|
)
|
||||||
suggested_reviewers = self._suggest_reviewers(
|
suggested_reviewer = self._suggest_reviewer(
|
||||||
members=members,
|
members=members,
|
||||||
highest_risk=highest_risk,
|
highest_risk=highest_risk,
|
||||||
cross_domain=cross_domain,
|
cross_domain=cross_domain,
|
||||||
)
|
)
|
||||||
warning = None
|
warning = None
|
||||||
|
fallback_role = None
|
||||||
if not members:
|
if not members:
|
||||||
warning = "项目暂无成员,无法给出 reviewer 建议。"
|
warning = "reviewer_missing"
|
||||||
elif not suggested_reviewers:
|
fallback_role = "OWNER"
|
||||||
warning = "项目成员中没有 OWNER / REVIEWER 角色,暂无法给出 reviewer 建议。"
|
elif suggested_reviewer is None:
|
||||||
|
warning = "reviewer_missing"
|
||||||
|
fallback_role = "OWNER"
|
||||||
|
elif not change_set_evidence:
|
||||||
|
warning = "evidence_missing"
|
||||||
if warning:
|
if warning:
|
||||||
warnings.append(f"reviewer_routing_warning:{change_set_id}:{warning}")
|
warnings.append(f"reviewer_routing_warning:{change_set_id}:{warning}")
|
||||||
routings.append(
|
source_type = self._reviewer_routing_source_type(change_set_evidence)
|
||||||
|
priority = self._reviewer_routing_priority(highest_risk=highest_risk, cross_domain=cross_domain)
|
||||||
|
routing_items.append(
|
||||||
{
|
{
|
||||||
|
"projectId": str(project_id),
|
||||||
"changeSetId": change_set_id,
|
"changeSetId": change_set_id,
|
||||||
"headline": str(change_set.get("headline") or "待审 ChangeSet"),
|
"changeSetTitle": str(change_set.get("headline") or "待审 ChangeSet"),
|
||||||
"summary": str(change_set.get("summary") or ""),
|
"changeSetStatus": str(change_set.get("status") or change_set.get("rawStatus") or "open"),
|
||||||
|
"sourceType": source_type,
|
||||||
|
"reviewReason": ";".join(review_reasons),
|
||||||
|
"riskLevel": highest_risk,
|
||||||
|
"priority": priority,
|
||||||
|
"changeTypes": object_types,
|
||||||
|
"itemCount": len(change_set.get("itemIds") or pending_items),
|
||||||
"pendingItemCount": len(pending_items),
|
"pendingItemCount": len(pending_items),
|
||||||
"highestRiskLevel": highest_risk,
|
"evidenceCount": len(change_set_evidence),
|
||||||
"riskTypes": self._risk_type_labels(object_types=object_types, risk_groups=risk_groups, highest_risk=highest_risk),
|
"suggestedReviewer": suggested_reviewer,
|
||||||
"crossDomain": cross_domain,
|
"fallbackReviewerRole": fallback_role,
|
||||||
"suggestedReviewers": suggested_reviewers,
|
"targetUrl": f"/change-review?projectId={int(project_id)}&changeSetId={change_set_id}",
|
||||||
"reviewReasons": review_reasons,
|
"createdAt": change_set.get("createdAt"),
|
||||||
"warning": warning,
|
"updatedAt": change_set.get("updatedAt") or change_set.get("createdAt"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return routings, warnings
|
missing_reviewer_count = sum(1 for item in routing_items if item.get("suggestedReviewer") is None)
|
||||||
|
warning_code = self._reviewer_routing_warning_code(routing_items)
|
||||||
|
status = "empty" if not routing_items else ("warning" if warning_code else "ready")
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
"status": status,
|
||||||
|
"warningCode": warning_code,
|
||||||
|
"items": routing_items,
|
||||||
|
"summary": {
|
||||||
|
"totalPendingReview": sum(int(item.get("pendingItemCount") or 0) for item in routing_items),
|
||||||
|
"highRiskCount": sum(1 for item in routing_items if item.get("riskLevel") == "high"),
|
||||||
|
"missingReviewerCount": missing_reviewer_count,
|
||||||
|
"iterationDraftCount": sum(1 for item in routing_items if item.get("sourceType") == "iteration_recommendation"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warnings,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _highest_risk_level(levels: list[str]) -> str:
|
def _highest_risk_level(levels: list[str]) -> str:
|
||||||
|
|
@ -1119,21 +1159,6 @@ class PMFlowOSProjectionService:
|
||||||
highest = normalized
|
highest = normalized
|
||||||
return highest
|
return highest
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _risk_type_labels(*, object_types: list[str], risk_groups: list[str], highest_risk: str) -> list[str]:
|
|
||||||
labels: list[str] = []
|
|
||||||
if highest_risk == "high":
|
|
||||||
labels.append("高风险")
|
|
||||||
elif highest_risk == "medium":
|
|
||||||
labels.append("中风险")
|
|
||||||
if len(object_types) > 1:
|
|
||||||
labels.append(f"跨对象:{' / '.join(object_types)}")
|
|
||||||
if "risk" in {group.lower() for group in risk_groups}:
|
|
||||||
labels.append("风险变更")
|
|
||||||
if "decision" in {group.lower() for group in risk_groups}:
|
|
||||||
labels.append("决策变更")
|
|
||||||
return labels or ["常规审查"]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _review_routing_reasons(
|
def _review_routing_reasons(
|
||||||
*,
|
*,
|
||||||
|
|
@ -1155,40 +1180,63 @@ class PMFlowOSProjectionService:
|
||||||
return reasons
|
return reasons
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _suggest_reviewers(
|
def _reviewer_routing_priority(*, highest_risk: str, cross_domain: bool) -> str:
|
||||||
|
if highest_risk == "high":
|
||||||
|
return "urgent"
|
||||||
|
if cross_domain:
|
||||||
|
return "elevated"
|
||||||
|
return "normal"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reviewer_routing_source_type(evidence: list[dict[str, Any]]) -> str:
|
||||||
|
source_types = {str(row.get("sourceType") or "").strip() for row in evidence}
|
||||||
|
if "iteration_recommendation" in source_types:
|
||||||
|
return "iteration_recommendation"
|
||||||
|
if "thread_message" in source_types:
|
||||||
|
return "ai_extraction"
|
||||||
|
if source_types:
|
||||||
|
return "manual"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reviewer_routing_warning_code(items: list[dict[str, Any]]) -> Optional[str]:
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
if any(item.get("suggestedReviewer") is None for item in items):
|
||||||
|
return "reviewer_missing"
|
||||||
|
if any(int(item.get("evidenceCount") or 0) == 0 for item in items):
|
||||||
|
return "evidence_missing"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _suggest_reviewer(
|
||||||
*,
|
*,
|
||||||
members: list[User],
|
members: list[User],
|
||||||
highest_risk: str,
|
highest_risk: str,
|
||||||
cross_domain: bool,
|
cross_domain: bool,
|
||||||
) -> list[dict[str, str]]:
|
) -> Optional[dict[str, str]]:
|
||||||
suggestions: list[dict[str, str]] = []
|
|
||||||
seen: set[int] = set()
|
|
||||||
|
|
||||||
def add(user: User, reason: str) -> None:
|
|
||||||
user_id = int(user.id)
|
|
||||||
if user_id in seen:
|
|
||||||
return
|
|
||||||
seen.add(user_id)
|
|
||||||
suggestions.append(
|
|
||||||
{
|
|
||||||
"userId": str(user.id),
|
|
||||||
"name": user.name or user.username or f"User#{user.id}",
|
|
||||||
"role": str(user.role or "MEMBER"),
|
|
||||||
"reason": reason,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
reviewers = [user for user in members if str(user.role or "").upper() == "REVIEWER"]
|
reviewers = [user for user in members if str(user.role or "").upper() == "REVIEWER"]
|
||||||
owners = [user for user in members if str(user.role or "").upper() == "OWNER"]
|
owners = [user for user in members if str(user.role or "").upper() == "OWNER"]
|
||||||
if highest_risk == "high":
|
if reviewers:
|
||||||
for user in reviewers:
|
return PMFlowOSProjectionService._reviewer_payload(
|
||||||
add(user, "REVIEWER 角色,优先复核 high risk 变更。")
|
reviewers[0],
|
||||||
if cross_domain:
|
"REVIEWER 角色,优先复核 high risk / 跨类型 ChangeSet。",
|
||||||
for user in owners:
|
"high" if highest_risk == "high" else "medium",
|
||||||
add(user, "项目 OWNER,适合统一跨对象变更审查口径。")
|
)
|
||||||
for user in reviewers + owners:
|
if owners:
|
||||||
add(user, "具备 OWNER / REVIEWER 审查角色,可作为本轮变更审查人。")
|
reason = "项目 OWNER,适合统一跨对象变更审查口径。" if cross_domain else "项目 OWNER,可作为本轮变更审查负责人。"
|
||||||
return suggestions[:3]
|
return PMFlowOSProjectionService._reviewer_payload(owners[0], reason, "medium")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reviewer_payload(user: User, reason: str, confidence: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"userId": str(user.id),
|
||||||
|
"displayName": user.name or user.username or f"User#{user.id}",
|
||||||
|
"role": str(user.role or "MEMBER"),
|
||||||
|
"reason": reason,
|
||||||
|
"confidence": confidence,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _active_change_set_id(change_sets: list[dict[str, Any]]) -> Optional[str]:
|
def _active_change_set_id(change_sets: list[dict[str, Any]]) -> Optional[str]:
|
||||||
|
|
|
||||||
|
|
@ -1185,9 +1185,7 @@ async def test_truth_state_projects_reviewer_routing_for_pending_changesets(db_e
|
||||||
)
|
)
|
||||||
session.add(change_set)
|
session.add(change_set)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
session.add_all(
|
risk_item = PMFlowChangeItem(
|
||||||
[
|
|
||||||
PMFlowChangeItem(
|
|
||||||
tenant_id=1,
|
tenant_id=1,
|
||||||
change_set_id=change_set.id,
|
change_set_id=change_set.id,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
|
|
@ -1201,8 +1199,8 @@ async def test_truth_state_projects_reviewer_routing_for_pending_changesets(db_e
|
||||||
confidence=0.92,
|
confidence=0.92,
|
||||||
risk_level="high",
|
risk_level="high",
|
||||||
status="pending_review",
|
status="pending_review",
|
||||||
),
|
)
|
||||||
PMFlowChangeItem(
|
task_item = PMFlowChangeItem(
|
||||||
tenant_id=1,
|
tenant_id=1,
|
||||||
change_set_id=change_set.id,
|
change_set_id=change_set.id,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
|
|
@ -1216,8 +1214,22 @@ async def test_truth_state_projects_reviewer_routing_for_pending_changesets(db_e
|
||||||
confidence=0.86,
|
confidence=0.86,
|
||||||
risk_level="medium",
|
risk_level="medium",
|
||||||
status="pending_review",
|
status="pending_review",
|
||||||
),
|
)
|
||||||
]
|
session.add_all([risk_item, task_item])
|
||||||
|
await session.flush()
|
||||||
|
session.add(
|
||||||
|
PMFlowEvidenceLink(
|
||||||
|
tenant_id=1,
|
||||||
|
project_id=project.id,
|
||||||
|
change_set_id=change_set.id,
|
||||||
|
change_item_id=risk_item.id,
|
||||||
|
source_type="iteration_recommendation",
|
||||||
|
source_id="iteration-1",
|
||||||
|
title="迭代建议证据",
|
||||||
|
excerpt="发布后反馈建议下一轮修复权限风险。",
|
||||||
|
display_label="Iteration recommendation",
|
||||||
|
evidence_metadata={},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
|
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
|
||||||
|
|
@ -1227,14 +1239,26 @@ async def test_truth_state_projects_reviewer_routing_for_pending_changesets(db_e
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()["data"]
|
payload = response.json()["data"]
|
||||||
routing = payload["reviewerRouting"][0]
|
routing = payload["reviewerRouting"]
|
||||||
assert routing["changeSetId"] == str(change_set.id)
|
assert routing["status"] == "ready"
|
||||||
assert routing["pendingItemCount"] == 2
|
assert routing["warningCode"] is None
|
||||||
assert routing["highestRiskLevel"] == "high"
|
assert routing["summary"]["totalPendingReview"] == 2
|
||||||
assert routing["crossDomain"] is True
|
assert routing["summary"]["highRiskCount"] == 1
|
||||||
assert "高风险" in routing["riskTypes"]
|
assert routing["summary"]["missingReviewerCount"] == 0
|
||||||
assert {item["role"] for item in routing["suggestedReviewers"]} == {"REVIEWER", "OWNER"}
|
assert routing["summary"]["iterationDraftCount"] == 1
|
||||||
assert any("high risk" in reason for reason in routing["reviewReasons"])
|
item = routing["items"][0]
|
||||||
|
assert item["projectId"] == str(project.id)
|
||||||
|
assert item["changeSetId"] == str(change_set.id)
|
||||||
|
assert item["sourceType"] == "iteration_recommendation"
|
||||||
|
assert item["pendingItemCount"] == 2
|
||||||
|
assert item["evidenceCount"] == 1
|
||||||
|
assert item["riskLevel"] == "high"
|
||||||
|
assert item["priority"] == "urgent"
|
||||||
|
assert item["changeTypes"] == ["Risk", "Task"]
|
||||||
|
assert item["suggestedReviewer"]["role"] == "REVIEWER"
|
||||||
|
assert item["suggestedReviewer"]["displayName"] == "审查负责人"
|
||||||
|
assert item["targetUrl"] == f"/change-review?projectId={project.id}&changeSetId={change_set.id}"
|
||||||
|
assert "high risk" in item["reviewReason"]
|
||||||
assert payload["approvals"] == []
|
assert payload["approvals"] == []
|
||||||
assert not any("reviewer_routing_warning" in warning for warning in payload["projectionWarnings"])
|
assert not any("reviewer_routing_warning" in warning for warning in payload["projectionWarnings"])
|
||||||
|
|
||||||
|
|
@ -1305,12 +1329,40 @@ async def test_truth_state_reviewer_routing_warns_without_owner_or_reviewer_role
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()["data"]
|
payload = response.json()["data"]
|
||||||
routing = payload["reviewerRouting"][0]
|
routing = payload["reviewerRouting"]
|
||||||
assert routing["suggestedReviewers"] == []
|
assert routing["status"] == "warning"
|
||||||
assert "没有 OWNER / REVIEWER" in routing["warning"]
|
assert routing["warningCode"] == "reviewer_missing"
|
||||||
|
assert routing["summary"]["missingReviewerCount"] == 1
|
||||||
|
item = routing["items"][0]
|
||||||
|
assert item["suggestedReviewer"] is None
|
||||||
|
assert item["fallbackReviewerRole"] == "OWNER"
|
||||||
assert any("reviewer_routing_warning" in warning for warning in payload["projectionWarnings"])
|
assert any("reviewer_routing_warning" in warning for warning in payload["projectionWarnings"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_truth_state_reviewer_routing_empty_without_pending_changesets(db_env) -> None:
|
||||||
|
session = db_env
|
||||||
|
user, project = await _seed_user_project(session)
|
||||||
|
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
|
||||||
|
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
response = await client.get(f"/pmflow-os/projects/{project.id}/truth-state")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
routing = response.json()["data"]["reviewerRouting"]
|
||||||
|
assert routing == {
|
||||||
|
"status": "empty",
|
||||||
|
"warningCode": None,
|
||||||
|
"items": [],
|
||||||
|
"summary": {
|
||||||
|
"totalPendingReview": 0,
|
||||||
|
"highRiskCount": 0,
|
||||||
|
"missingReviewerCount": 0,
|
||||||
|
"iterationDraftCount": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@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
|
||||||
|
|
|
||||||
|
|
@ -811,9 +811,9 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
||||||
|
|
||||||
完成内容:
|
完成内容:
|
||||||
|
|
||||||
- 在 `ProjectTruthState` 新增 `reviewerRouting` 只读投影,从 open/pending ChangeSet、待审 change item 风险、跨对象类型、项目成员与 `OWNER / REVIEWER` 角色生成建议审查人、审查原因、风险类型和 warning。
|
- 在 `ProjectTruthState` 新增 `reviewerRouting(status, warningCode, summary, items)` 只读投影,从 open/pending ChangeSet、待审 item 风险、跨对象类型、`iteration_recommendation` evidence、项目成员与 `OWNER / REVIEWER` 角色生成建议审查人、来源、风险等级、优先级、审查原因、fallback role 和目标跳转 URL。
|
||||||
- `TeamLayerPage` 真实模式展示待审 ChangeSet reviewer routing:待审数量、建议审查人、审查原因、风险类型、缺角色 warning,并提供跳转 Change Review 的动作。
|
- `TeamLayerPage` 真实模式展示待审 ChangeSet reviewer routing:ready / empty / warning、来源(下一轮迭代 / AI 提取 / 手动)、风险等级、建议审查人或 fallback role、审查原因、待审 item 数、证据数,并跳转 `/change-review?projectId=<id>&changeSetId=<id>`。
|
||||||
- 保持监督式边界:不新增 route,不新增 migration,不做复杂权限系统,不自动 assign reviewer,不写 approval;缺成员或缺 `OWNER / REVIEWER` 角色时只显示 warning,不虚构 reviewer。
|
- 保持监督式边界:不新增 route,不新增 migration,不做复杂权限系统,不自动 assign reviewer,不写 approval;缺成员、缺 `OWNER / REVIEWER` 角色或缺 evidence 时只显示 warning,不虚构 reviewer。
|
||||||
- 同步 `CLAUDE.md`、`CODE_REFERENCE_INDEX.md`、`ARCHITECTURE_INVENTORY.md` 与本台账。
|
- 同步 `CLAUDE.md`、`CODE_REFERENCE_INDEX.md`、`ARCHITECTURE_INVENTORY.md` 与本台账。
|
||||||
|
|
||||||
涉及文件:
|
涉及文件:
|
||||||
|
|
@ -822,6 +822,8 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
||||||
- `backend/app/services/pmflow_os_projection_service.py`
|
- `backend/app/services/pmflow_os_projection_service.py`
|
||||||
- `backend/tests/test_pmflow_os_projection_api.py`
|
- `backend/tests/test_pmflow_os_projection_api.py`
|
||||||
- `frontend/src/api/pmflowOs.ts`
|
- `frontend/src/api/pmflowOs.ts`
|
||||||
|
- `frontend/src/features/pmflow-os/store.tsx`
|
||||||
|
- `frontend/src/features/pmflow-os/types.ts`
|
||||||
- `frontend/src/features/pmflow-os/pages/TeamLayerPage.tsx`
|
- `frontend/src/features/pmflow-os/pages/TeamLayerPage.tsx`
|
||||||
- `frontend/src/features/pmflow-os/store.api.test.tsx`
|
- `frontend/src/features/pmflow-os/store.api.test.tsx`
|
||||||
- `CLAUDE.md`
|
- `CLAUDE.md`
|
||||||
|
|
@ -832,8 +834,8 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
||||||
验证:
|
验证:
|
||||||
|
|
||||||
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile backend/app/services/pmflow_os_projection_service.py backend/app/schemas/pmflow_os.py` 通过。
|
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile 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` 通过:`21 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
|
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m pytest backend/tests/test_pmflow_os_projection_api.py -q` 通过:`22 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
|
||||||
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`10 passed`;仍有既有 React Router v7 future flag warning。
|
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`11 passed`;仍有既有 React Router v7 future flag warning。
|
||||||
- `cd frontend && npm run typecheck` 通过。
|
- `cd frontend && npm run typecheck` 通过。
|
||||||
- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning(`ai-steward / sstc / charts / governance` chunk 环)。
|
- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning(`ai-steward / sstc / charts / governance` chunk 环)。
|
||||||
- `docker compose restart backend` 已执行,`docker compose ps backend` 确认 `pmflow-backend` 恢复 `healthy`。
|
- `docker compose restart backend` 已执行,`docker compose ps backend` 确认 `pmflow-backend` 恢复 `healthy`。
|
||||||
|
|
|
||||||
|
|
@ -193,13 +193,13 @@
|
||||||
- `frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx`
|
- `frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx`
|
||||||
- AI Steward 问答浮窗现分三层显性化:`FloatingQAChat.tsx` 负责即时回答/历史回放归一化,`QAConversationStatusBar.tsx` 展示对象/来源/仓库/执行通道/plan handoff,`QAResponseMetaCard.tsx` 展示 `fit_brief / external_recommendation / recommended_next_step`,对应前端回归由 `FloatingQAChat.test.tsx` 与 `QAResponseMetaCard.test.tsx` 锁定。
|
- AI Steward 问答浮窗现分三层显性化:`FloatingQAChat.tsx` 负责即时回答/历史回放归一化,`QAConversationStatusBar.tsx` 展示对象/来源/仓库/执行通道/plan handoff,`QAResponseMetaCard.tsx` 展示 `fit_brief / external_recommendation / recommended_next_step`,对应前端回归由 `FloatingQAChat.test.tsx` 与 `QAResponseMetaCard.test.tsx` 锁定。
|
||||||
- 登录页与营销页仍共享 `frontend/src/components/marketing/` 的部分视觉基线;公开登录页现改由 `frontend/src/components/login/` 目录承载,其中 `LoginExperience.tsx` 负责登录壳层、`LoginWorkflowVisual.tsx` 负责左侧场景、`loginScene.ts` / `loginMath.ts` 拆出展示配置与路径几何、`login.css` 负责登录页专属样式边界,`frontend/src/pages/login/LoginPage.tsx` 继续负责真实鉴权、错误态和 redirect 提示;未登录访问 `/` 会经 `frontend/src/router/index.tsx` 直接重定向到 `/login`,无显式 `redirect` 的成功登录现在默认进入 `/command-center`。
|
- 登录页与营销页仍共享 `frontend/src/components/marketing/` 的部分视觉基线;公开登录页现改由 `frontend/src/components/login/` 目录承载,其中 `LoginExperience.tsx` 负责登录壳层、`LoginWorkflowVisual.tsx` 负责左侧场景、`loginScene.ts` / `loginMath.ts` 拆出展示配置与路径几何、`login.css` 负责登录页专属样式边界,`frontend/src/pages/login/LoginPage.tsx` 继续负责真实鉴权、错误态和 redirect 提示;未登录访问 `/` 会经 `frontend/src/router/index.tsx` 直接重定向到 `/login`,无显式 `redirect` 的成功登录现在默认进入 `/command-center`。
|
||||||
- 前端主包治理已切到“同步首屏最小集 + route/page-section lazy”模式:`frontend/src/router/index.tsx` 保留 `LoginPage / Layout` 这组同步入口,其余后台重页面通过 `React.lazy + Suspense` 懒加载;统一 fallback 在 `frontend/src/components/layout/LazyBoundary.tsx`。P0 公共壳冻结后,大模块 route 归属为:`/space` 归 Space lane,`/project-launch` 是启动层入口,`/pmflow-os` 与 `/command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route、尚未实现,新增必须由 Shell / Integration owner 串行处理。`frontend/src/features/personal-space/` 作为空间层入口,通过 `/space` 提供可直接打开的本地预览入口,承载继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口。`frontend/src/features/project-launch/` 作为首次进入页 / 新项目启动台产品域,通过 `/project-launch` 提供可直接打开的入口;Phase B 后默认调用真实 `pmflow-launch` draft/confirm API,创建成功态主动作跳转后端返回的 `/pmflow-os?projectId=<id>`,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 状态机。`frontend/src/features/account-settings/` 作为 Account Settings 产品域,通过 `/me` 提供个人资料、默认语言、AI 回答深度、证据展示偏好、待确认/执行回写/发布门禁/失败提醒通知偏好和账户操作入口;该域通过 `frontend/src/api/accountSettings.ts` 建立前端合同,有 token 时优先调用已挂入 `backend/app/api/router.py` 的 `backend/app/api/account_settings_api/` `GET /account-settings/me` / `PUT /account-settings/me` 同步当前账号偏好,后端不可用时明确回落本机暂存,不修改 OAuth/JWT/auth core。`frontend/src/features/pmflow-os/` 作为新产品域,通过 `PMFlowOsProvider` 在应用根部挂载共享 truth store,并提供 `CommandCenter / ChangeReview / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主入口;当前 PMFlow OS 页面已切到专属轻壳,桌面端默认隐藏旧 TopBar,并由 `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` 统一承接“左侧稳定导航 + 顶部上下文条 + 中央视图 + 右侧 AI 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;`TeamLayerPage.tsx` 现在消费 `ProjectTruthState.reviewerRouting` 展示待审 ChangeSet、建议审查人、审查原因、风险类型和缺角色 warning,动作只跳 Change Review,不自动 assign reviewer;账号退出登录由 `frontend/src/features/pmflow-os/components/AccountControls.tsx` 统一承接桌面头像下拉、退出确认与移动端 bottom sheet,`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 提供 `/me` 入口并委托 Account Settings 域渲染设置页;正式应用壳移动端底部导航统一由 `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 承接,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`,其中 `动态` 当前仍是 `/command-center` 占位。`/command-center` 额外提供免登录产品预览壳,`/pmflow-os` 是真实项目上下文 alias。主视觉按 `#2463EB + 白色` 的 Linear-like 高密度原型展开。`UsersPage` 把创建/删除/重置/导入/批量分配对话框拆为按需 chunk,`UserSkillsPage` 与 `WorkloadDetailPage` 的图表也分别拆到了 `frontend/src/pages/user-skills/components/UserSkillsRadarPanel.tsx` 与 `frontend/src/pages/workload-detail/components/WorkloadTrendChart.tsx`。
|
- 前端主包治理已切到“同步首屏最小集 + route/page-section lazy”模式:`frontend/src/router/index.tsx` 保留 `LoginPage / Layout` 这组同步入口,其余后台重页面通过 `React.lazy + Suspense` 懒加载;统一 fallback 在 `frontend/src/components/layout/LazyBoundary.tsx`。P0 公共壳冻结后,大模块 route 归属为:`/space` 归 Space lane,`/project-launch` 是启动层入口,`/pmflow-os` 与 `/command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route、尚未实现,新增必须由 Shell / Integration owner 串行处理。`frontend/src/features/personal-space/` 作为空间层入口,通过 `/space` 提供可直接打开的本地预览入口,承载继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口。`frontend/src/features/project-launch/` 作为首次进入页 / 新项目启动台产品域,通过 `/project-launch` 提供可直接打开的入口;Phase B 后默认调用真实 `pmflow-launch` draft/confirm API,创建成功态主动作跳转后端返回的 `/pmflow-os?projectId=<id>`,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 状态机。`frontend/src/features/account-settings/` 作为 Account Settings 产品域,通过 `/me` 提供个人资料、默认语言、AI 回答深度、证据展示偏好、待确认/执行回写/发布门禁/失败提醒通知偏好和账户操作入口;该域通过 `frontend/src/api/accountSettings.ts` 建立前端合同,有 token 时优先调用已挂入 `backend/app/api/router.py` 的 `backend/app/api/account_settings_api/` `GET /account-settings/me` / `PUT /account-settings/me` 同步当前账号偏好,后端不可用时明确回落本机暂存,不修改 OAuth/JWT/auth core。`frontend/src/features/pmflow-os/` 作为新产品域,通过 `PMFlowOsProvider` 在应用根部挂载共享 truth store,并提供 `CommandCenter / ChangeReview / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主入口;当前 PMFlow OS 页面已切到专属轻壳,桌面端默认隐藏旧 TopBar,并由 `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` 统一承接“左侧稳定导航 + 顶部上下文条 + 中央视图 + 右侧 AI 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;`TeamLayerPage.tsx` 现在消费 `ProjectTruthState.reviewerRouting(status/summary/items)` 展示待审 ChangeSet、来源、风险等级、建议审查人或 fallback role、审查原因、待审 item 数、证据数和 warning,动作只跳 `/change-review?projectId=<id>&changeSetId=<id>`,不自动 assign reviewer;账号退出登录由 `frontend/src/features/pmflow-os/components/AccountControls.tsx` 统一承接桌面头像下拉、退出确认与移动端 bottom sheet,`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 提供 `/me` 入口并委托 Account Settings 域渲染设置页;正式应用壳移动端底部导航统一由 `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 承接,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`,其中 `动态` 当前仍是 `/command-center` 占位。`/command-center` 额外提供免登录产品预览壳,`/pmflow-os` 是真实项目上下文 alias。主视觉按 `#2463EB + 白色` 的 Linear-like 高密度原型展开。`UsersPage` 把创建/删除/重置/导入/批量分配对话框拆为按需 chunk,`UserSkillsPage` 与 `WorkloadDetailPage` 的图表也分别拆到了 `frontend/src/pages/user-skills/components/UserSkillsRadarPanel.tsx` 与 `frontend/src/pages/workload-detail/components/WorkloadTrendChart.tsx`。
|
||||||
- `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 均调用 `/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 生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`,Iteration Loop P1 已接入 `POST /iteration-recommendations/{recommendation_id}/change-set-drafts` 把 recommendation 回流到 Change Review;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
|
||||||
- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。`ProjectTruthState.reviewerRouting` 是 Team layer 的 reviewer routing 读模型:从 open/pending ChangeSet、待审 item 风险、跨对象类型、项目成员与 `OWNER / REVIEWER` 角色推导建议审查人、审查原因、风险类型和 warning;缺成员或缺角色时 fail-closed 给 warning,不虚构 reviewer,不写 approval,不自动 assign。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` 对应真实表。`ProjectTruthState.reviewerRouting` 是 Team layer 的 reviewer routing 读模型:从 open/pending ChangeSet、待审 item 风险、跨对象类型、`iteration_recommendation` evidence、项目成员与 `OWNER / REVIEWER` 角色推导 `{status, warningCode, summary, items}`;item 内含 `sourceType / riskLevel / priority / suggestedReviewer / fallbackReviewerRole / targetUrl`;缺成员、缺角色或缺证据时 fail-closed 给 warning,不虚构 reviewer,不写 approval,不自动 assign。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用现有 `FeedbackIngest / IterationRecommendation`,在 `ExecutionSummaryProjection.releaseSignals` 中返回真实发布后 feedback、revenue event 和 iteration recommendation 读模型;Iteration Loop P1 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts`,把真实 recommendation 转为 open ChangeSet、pending_review Requirement/Task/Risk/Decision items 与 `iteration_recommendation` evidence,重复调用复用既有 draft 并在 `releaseSignals` 标记 `changeSetCreated/changeSetId`;这些步骤都不直接执行本机命令、真实发布命令、外部支付、真实推送、自动 Project Memory commit 或正式任务创建;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
|
||||||
- PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。
|
- 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、治理页和图表代码。
|
||||||
|
|
|
||||||
|
|
@ -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 与 Team reviewer routing:参考 `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`;`truth-state.reviewerRouting` 会从 open/pending ChangeSet、待审 item 风险、跨对象类型、项目成员与 `OWNER / REVIEWER` 角色生成建议审查人、审查原因、风险类型和 warning,只读展示,不写 approval、不自动 assign reviewer;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 与 Team reviewer routing:参考 `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`;`truth-state.reviewerRouting` 是 `{status, warningCode, summary, items}` 读模型,会从 open/pending ChangeSet、待审 item 风险、跨对象类型、evidence source、项目成员与 `OWNER / REVIEWER` 角色生成 `sourceType / riskLevel / priority / suggestedReviewer / fallbackReviewerRole / targetUrl`,只读展示,不写 approval、不自动 assign reviewer;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 已在 `execution-summary.releaseGate` 中从 branch / commit / PR / test_passed / checks_passed / failed / blocked 等 `ProgressEvent` 推导 `ready / blocked / needs_confirmation / empty`;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,从当前 `releaseGate` 固化 `ReleaseValidationRun` 快照并在 `releaseGate.latestValidationRun` 返回最新记录,无 evidence 时返回 `release_validation_evidence_empty`;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用 `FeedbackIngest / IterationRecommendation` 写入真实发布后反馈,并在 `execution-summary.releaseSignals` 投影真实 `feedback / revenueEvents / recommendations` 与 release gate / validation run 关系;Iteration Loop P1 新增 `POST /pmflow-os/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts`,把真实 `IterationRecommendation` 转为 open ChangeSet、pending_review Requirement/Task/Risk/Decision items 与 iteration evidence,重复调用复用既有 draft;这些步骤均不执行发布命令、外部支付、真实推送或自动 Project Memory commit
|
||||||
- Project Launch 真实创建链:参考 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与模型 `backend/app/models/pmflow_launch.py`;接口为 `POST /pmflow-launch/drafts` 与 `POST /pmflow-launch/drafts/{draft_id}/confirm`,draft 只写 `pmflow_launch_drafts`,confirm 在同一事务内创建 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`
|
- 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`
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
- 新项目启动台 / 首次进入页前端产品域:参考 `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 均调后端并 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`;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting` 展示待审 ChangeSet、建议审查人、审查原因、风险类型和缺角色 warning,动作只跳转 Change Review,不自动分派 reviewer;`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`;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting` 展示 ready/empty/warning、待审 ChangeSet、来源(下一轮迭代 / AI 提取 / 手动)、风险等级、建议审查人或 fallback role、审查原因、待审 item 数、证据数,并只跳转 `/change-review?projectId=<id>&changeSetId=<id>`,不自动分派 reviewer;`ReleaseGrowthPage.tsx` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并从真实 recommendation 生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 demo 模式
|
||||||
- PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航
|
- PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/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`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ChangeItem, PMFlowExecutionContext, PMFlowRiskLevel, ProjectTruthState } from "@/features/pmflow-os/types";
|
import type { ChangeItem, PMFlowExecutionContext, PMFlowRiskLevel, ProjectTruthState } from "@/features/pmflow-os/types";
|
||||||
|
export type { PMFlowReviewerRouting, PMFlowReviewerRoutingItem, PMFlowReviewerSuggestion } from "@/features/pmflow-os/types";
|
||||||
|
|
||||||
type ApiEnvelope<T> = { code: number; message: string; data: T };
|
type ApiEnvelope<T> = { code: number; message: string; data: T };
|
||||||
const API_BASE = "/api";
|
const API_BASE = "/api";
|
||||||
|
|
@ -239,26 +240,6 @@ export interface PMFlowIterationRecommendationChangeSetDraftResult {
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PMFlowReviewerSuggestion {
|
|
||||||
userId: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PMFlowChangeSetReviewerRouting {
|
|
||||||
changeSetId: string;
|
|
||||||
headline: string;
|
|
||||||
summary: string;
|
|
||||||
pendingItemCount: number;
|
|
||||||
highestRiskLevel: PMFlowRiskLevel;
|
|
||||||
riskTypes: string[];
|
|
||||||
crossDomain: boolean;
|
|
||||||
suggestedReviewers: PMFlowReviewerSuggestion[];
|
|
||||||
reviewReasons: string[];
|
|
||||||
warning?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PMFlowExecutionHandoffRequest {
|
export interface PMFlowExecutionHandoffRequest {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
repo_binding_id?: string | null;
|
repo_binding_id?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,8 @@ import {
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { PMFlowChangeSetReviewerRouting } from "@/api/pmflowOs";
|
|
||||||
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||||
import type { PMFlowRiskLevel, ProjectTruthState } from "@/features/pmflow-os/types";
|
import type { PMFlowReviewerRouting, PMFlowReviewerRoutingItem, PMFlowRiskLevel } from "@/features/pmflow-os/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const activityItems = [
|
const activityItems = [
|
||||||
|
|
@ -55,13 +54,12 @@ const agentCards = [
|
||||||
export default function TeamLayerPage() {
|
export default function TeamLayerPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { state, openReview, projectionWarnings } = usePMFlowOsStore();
|
const { state, openReview, projectionWarnings } = usePMFlowOsStore();
|
||||||
const reviewerRouting =
|
const reviewerRouting = state.reviewerRouting || emptyReviewerRouting();
|
||||||
(state as ProjectTruthState & { reviewerRouting?: PMFlowChangeSetReviewerRouting[] }).reviewerRouting || [];
|
|
||||||
const pendingReviewCount = state.changeItems.filter((item) => item.status === "pending_review").length;
|
const pendingReviewCount = state.changeItems.filter((item) => item.status === "pending_review").length;
|
||||||
const reviewerWarnings = projectionWarnings.filter((warning) => warning.includes("reviewer_routing_warning"));
|
const reviewerWarnings = projectionWarnings.filter((warning) => warning.includes("reviewer_routing_warning"));
|
||||||
const handleOpenReview = (changeSetId: string) => {
|
const handleOpenReview = (item: PMFlowReviewerRoutingItem) => {
|
||||||
openReview(changeSetId);
|
openReview(item.changeSetId);
|
||||||
navigate("/change-review");
|
navigate(item.targetUrl || `/change-review?projectId=${item.projectId}&changeSetId=${item.changeSetId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -96,11 +94,11 @@ function ReviewerRoutingPanel({
|
||||||
onOpenReview,
|
onOpenReview,
|
||||||
}: {
|
}: {
|
||||||
pendingReviewCount: number;
|
pendingReviewCount: number;
|
||||||
routings: PMFlowChangeSetReviewerRouting[];
|
routings: PMFlowReviewerRouting;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
onOpenReview: (changeSetId: string) => void;
|
onOpenReview: (item: PMFlowReviewerRoutingItem) => void;
|
||||||
}) {
|
}) {
|
||||||
const visibleRoutings = routings.slice(0, 3);
|
const visibleRoutings = routings.items.slice(0, 3);
|
||||||
return (
|
return (
|
||||||
<Panel>
|
<Panel>
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
|
@ -116,10 +114,15 @@ function ReviewerRoutingPanel({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-2 text-sm sm:grid-cols-3">
|
||||||
<MetricPill label="待审变更" value={String(pendingReviewCount)} />
|
<MetricPill label="待审变更" value={String(pendingReviewCount)} />
|
||||||
<MetricPill label="路由项" value={String(routings.length)} />
|
<MetricPill label="高风险" value={String(routings.summary.highRiskCount)} />
|
||||||
<MetricPill label="警告" value={String(warnings.length)} />
|
<MetricPill label="缺审查人" value={String(routings.summary.missingReviewerCount)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{routings.status === "warning" ? (
|
||||||
|
<div className="mt-4 rounded-xl border border-orange-200 bg-orange-50 px-4 py-3 text-sm leading-6 text-orange-700">
|
||||||
|
{reviewerRoutingWarningText(routings.warningCode, warnings)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{visibleRoutings.length > 0 ? (
|
{visibleRoutings.length > 0 ? (
|
||||||
<div className="mt-5 grid gap-4 xl:grid-cols-3">
|
<div className="mt-5 grid gap-4 xl:grid-cols-3">
|
||||||
{visibleRoutings.map((routing) => (
|
{visibleRoutings.map((routing) => (
|
||||||
|
|
@ -127,61 +130,62 @@ function ReviewerRoutingPanel({
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className={cn("rounded-lg px-2 py-1 text-xs font-semibold", riskTone(routing.highestRiskLevel))}>
|
<span className={cn("rounded-lg px-2 py-1 text-xs font-semibold", riskTone(routing.riskLevel))}>
|
||||||
{riskLabel(routing.highestRiskLevel)}
|
{riskLabel(routing.riskLevel)}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-lg bg-[#eef4ff] px-2 py-1 text-xs font-semibold text-[#2463EB]">
|
||||||
|
{sourceLabel(routing.sourceType)}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-lg bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">
|
||||||
|
{priorityLabel(routing.priority)}
|
||||||
</span>
|
</span>
|
||||||
{routing.crossDomain ? <span className="rounded-lg bg-[#eef4ff] px-2 py-1 text-xs font-semibold text-[#2463EB]">跨域</span> : null}
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-3 text-base font-semibold text-slate-800">{routing.headline}</h3>
|
<h3 className="mt-3 text-base font-semibold text-slate-800">{routing.changeSetTitle}</h3>
|
||||||
<p className="mt-2 text-sm leading-6 text-slate-500">{routing.summary}</p>
|
<p className="mt-2 text-sm leading-6 text-slate-500">{routing.reviewReason}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onOpenReview(routing.changeSetId)}
|
onClick={() => onOpenReview(routing)}
|
||||||
className="inline-flex shrink-0 items-center gap-1 rounded-lg border border-[#cfe0ff] bg-white px-3 py-2 text-sm font-semibold text-[#2463EB] transition hover:border-[#2463EB] hover:bg-[#eef4ff] focus:outline-none focus:ring-2 focus:ring-[#2463EB]/20"
|
className="inline-flex shrink-0 items-center gap-1 rounded-lg border border-[#cfe0ff] bg-white px-3 py-2 text-sm font-semibold text-[#2463EB] transition hover:border-[#2463EB] hover:bg-[#eef4ff] focus:outline-none focus:ring-2 focus:ring-[#2463EB]/20"
|
||||||
>
|
>
|
||||||
去审查 <ArrowRight className="h-4 w-4" />
|
去审查 <ArrowRight className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
{routing.riskTypes.map((type) => (
|
{routing.changeTypes.map((type) => (
|
||||||
<span key={type} className="rounded-lg border border-[#e2e9f5] bg-white px-2 py-1 text-xs font-semibold text-slate-500">
|
<span key={type} className="rounded-lg border border-[#e2e9f5] bg-white px-2 py-1 text-xs font-semibold text-slate-500">
|
||||||
{type}
|
{type}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
<span className="rounded-lg border border-[#e2e9f5] bg-white px-2 py-1 text-xs font-semibold text-slate-500">
|
||||||
|
待审 {routing.pendingItemCount}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-lg border border-[#e2e9f5] bg-white px-2 py-1 text-xs font-semibold text-slate-500">
|
||||||
|
证据 {routing.evidenceCount}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="text-xs font-semibold text-slate-400">建议审查人</div>
|
<div className="text-xs font-semibold text-slate-400">建议审查人</div>
|
||||||
{routing.suggestedReviewers.length > 0 ? (
|
{routing.suggestedReviewer ? (
|
||||||
routing.suggestedReviewers.map((reviewer) => (
|
<div className="rounded-lg border border-[#e2e9f5] bg-white px-3 py-3">
|
||||||
<div key={reviewer.userId} className="rounded-lg border border-[#e2e9f5] bg-white px-3 py-3">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="font-semibold text-slate-800">{reviewer.name}</span>
|
<span className="font-semibold text-slate-800">{routing.suggestedReviewer.displayName}</span>
|
||||||
<span className="rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">{reviewer.role}</span>
|
<span className="rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">{routing.suggestedReviewer.role}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs leading-5 text-slate-500">{reviewer.reason}</p>
|
<p className="mt-2 text-xs leading-5 text-slate-500">{routing.suggestedReviewer.reason}</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-3 text-sm leading-6 text-orange-700">
|
<div className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-3 text-sm leading-6 text-orange-700">
|
||||||
{routing.warning || "缺少可用审查人,暂不虚构建议人。"}
|
当前项目缺少 REVIEWER/OWNER,建议先补充项目审查人。兜底角色:{routing.fallbackReviewerRole || "OWNER"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<div className="text-xs font-semibold text-slate-400">审查原因</div>
|
|
||||||
{routing.reviewReasons.map((reason) => (
|
|
||||||
<p key={reason} className="text-sm leading-6 text-slate-500">
|
|
||||||
{reason}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-5 rounded-xl border border-[#e2e9f5] bg-[#fbfdff] px-4 py-5 text-sm leading-6 text-slate-500">
|
<div className="mt-5 rounded-xl border border-[#e2e9f5] bg-[#fbfdff] px-4 py-5 text-sm leading-6 text-slate-500">
|
||||||
当前没有待审 ChangeSet 审查路由。若后端缺成员或缺 OWNER / REVIEWER 角色,会在这里显示警告,不会补假审查人。
|
{routings.status === "empty" ? "当前没有待团队审查的变更。" : "团队审查路由暂时不可用,请稍后重试。"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
@ -451,3 +455,37 @@ function riskTone(level: PMFlowRiskLevel) {
|
||||||
if (level === "medium") return "bg-orange-50 text-orange-600";
|
if (level === "medium") return "bg-orange-50 text-orange-600";
|
||||||
return "bg-emerald-50 text-emerald-600";
|
return "bg-emerald-50 text-emerald-600";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sourceLabel(sourceType: PMFlowReviewerRoutingItem["sourceType"]) {
|
||||||
|
if (sourceType === "iteration_recommendation") return "下一轮迭代";
|
||||||
|
if (sourceType === "ai_extraction") return "AI 提取";
|
||||||
|
if (sourceType === "manual") return "手动";
|
||||||
|
return "来源未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
function priorityLabel(priority: PMFlowReviewerRoutingItem["priority"]) {
|
||||||
|
if (priority === "urgent") return "紧急";
|
||||||
|
if (priority === "elevated") return "需关注";
|
||||||
|
return "常规";
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewerRoutingWarningText(code: string | null | undefined, warnings: string[]) {
|
||||||
|
if (code === "reviewer_missing") return "当前项目缺少 REVIEWER/OWNER,建议先补充项目审查人。";
|
||||||
|
if (code === "evidence_missing") return "待审变更缺少足够证据,建议回到 Change Review 查看来源。";
|
||||||
|
if (warnings.length > 0) return "团队审查路由存在 warning,请优先检查 reviewer 和证据。";
|
||||||
|
return "团队审查路由暂时不可用,请稍后重试。";
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyReviewerRouting(): PMFlowReviewerRouting {
|
||||||
|
return {
|
||||||
|
status: "empty",
|
||||||
|
warningCode: null,
|
||||||
|
items: [],
|
||||||
|
summary: {
|
||||||
|
totalPendingReview: 0,
|
||||||
|
highRiskCount: 0,
|
||||||
|
missingReviewerCount: 0,
|
||||||
|
iterationDraftCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter, useLocation } from "react-router-dom";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { getAccountSettings } from "@/api/accountSettings";
|
import { getAccountSettings } from "@/api/accountSettings";
|
||||||
|
|
@ -19,14 +19,13 @@ import {
|
||||||
editPMFlowChangeItem,
|
editPMFlowChangeItem,
|
||||||
getPMFlowExecutionSummary,
|
getPMFlowExecutionSummary,
|
||||||
getPMFlowTruthState,
|
getPMFlowTruthState,
|
||||||
type PMFlowChangeSetReviewerRouting,
|
|
||||||
type PMFlowExecutionSummary,
|
type PMFlowExecutionSummary,
|
||||||
type PMFlowReleaseGate,
|
type PMFlowReleaseGate,
|
||||||
} from "@/api/pmflowOs";
|
} from "@/api/pmflowOs";
|
||||||
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
|
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
|
||||||
import TeamLayerPage from "@/features/pmflow-os/pages/TeamLayerPage";
|
import TeamLayerPage from "@/features/pmflow-os/pages/TeamLayerPage";
|
||||||
import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store";
|
import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||||
import type { ProjectTruthState } from "@/features/pmflow-os/types";
|
import type { PMFlowReviewerRouting, ProjectTruthState } from "@/features/pmflow-os/types";
|
||||||
|
|
||||||
vi.mock("@/lib/utils", () => ({
|
vi.mock("@/lib/utils", () => ({
|
||||||
cn: (...inputs: Array<string | false | null | undefined>) => inputs.filter(Boolean).join(" "),
|
cn: (...inputs: Array<string | false | null | undefined>) => inputs.filter(Boolean).join(" "),
|
||||||
|
|
@ -115,28 +114,53 @@ function realState(): ProjectTruthState {
|
||||||
sourceLabel: "项目线程",
|
sourceLabel: "项目线程",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
(state as ProjectTruthState & { reviewerRouting: PMFlowChangeSetReviewerRouting[] }).reviewerRouting = [
|
state.reviewerRouting = reviewerRoutingFixture();
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewerRoutingFixture(): PMFlowReviewerRouting {
|
||||||
|
return {
|
||||||
|
status: "ready",
|
||||||
|
warningCode: null,
|
||||||
|
summary: {
|
||||||
|
totalPendingReview: 1,
|
||||||
|
highRiskCount: 1,
|
||||||
|
missingReviewerCount: 0,
|
||||||
|
iterationDraftCount: 1,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
|
projectId: "123",
|
||||||
changeSetId: "11",
|
changeSetId: "11",
|
||||||
headline: "真实变更集",
|
changeSetTitle: "真实变更集",
|
||||||
summary: "高风险跨域变更待审",
|
changeSetStatus: "open",
|
||||||
|
sourceType: "iteration_recommendation",
|
||||||
|
reviewReason: "包含 high risk 变更,需要 REVIEWER 或 OWNER 复核。",
|
||||||
|
riskLevel: "high",
|
||||||
|
priority: "urgent",
|
||||||
|
changeTypes: ["Risk", "Task"],
|
||||||
|
itemCount: 2,
|
||||||
pendingItemCount: 1,
|
pendingItemCount: 1,
|
||||||
highestRiskLevel: "high",
|
evidenceCount: 1,
|
||||||
riskTypes: ["高风险", "跨对象:Risk / Task"],
|
suggestedReviewer: {
|
||||||
crossDomain: true,
|
|
||||||
suggestedReviewers: [
|
|
||||||
{
|
|
||||||
userId: "9",
|
userId: "9",
|
||||||
name: "审查负责人",
|
displayName: "审查负责人",
|
||||||
role: "REVIEWER",
|
role: "REVIEWER",
|
||||||
reason: "REVIEWER 角色,优先复核 high risk 变更。",
|
reason: "REVIEWER 角色,优先复核 high risk 变更。",
|
||||||
|
confidence: "high",
|
||||||
|
},
|
||||||
|
fallbackReviewerRole: null,
|
||||||
|
targetUrl: "/change-review?projectId=123&changeSetId=11",
|
||||||
|
createdAt: "2026-04-26T00:00:00",
|
||||||
|
updatedAt: "2026-04-26T00:00:00",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
reviewReasons: ["1 个变更项仍处于待审状态。", "包含 high risk 变更,需要 REVIEWER 或 OWNER 复核。"],
|
};
|
||||||
warning: null,
|
}
|
||||||
},
|
|
||||||
];
|
function LocationProbe() {
|
||||||
return state;
|
const location = useLocation();
|
||||||
|
return <div data-testid="location">{`${location.pathname}${location.search}`}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty" = "needs_confirmation"): PMFlowReleaseGate {
|
function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty" = "needs_confirmation"): PMFlowReleaseGate {
|
||||||
|
|
@ -490,6 +514,7 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<TeamLayerPage />
|
<TeamLayerPage />
|
||||||
|
<LocationProbe />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
{ wrapper: wrapper() }
|
{ wrapper: wrapper() }
|
||||||
);
|
);
|
||||||
|
|
@ -498,12 +523,24 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
expect(await screen.findByText("真实变更集")).toBeInTheDocument();
|
expect(await screen.findByText("真实变更集")).toBeInTheDocument();
|
||||||
expect(screen.getByText("审查负责人")).toBeInTheDocument();
|
expect(screen.getByText("审查负责人")).toBeInTheDocument();
|
||||||
expect(screen.getByText("REVIEWER")).toBeInTheDocument();
|
expect(screen.getByText("REVIEWER")).toBeInTheDocument();
|
||||||
expect(screen.getByText("跨域")).toBeInTheDocument();
|
expect(screen.getByText("下一轮迭代")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("证据 1")).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: /去审查/ }));
|
fireEvent.click(screen.getByRole("button", { name: /去审查/ }));
|
||||||
|
await waitFor(() => expect(screen.getByTestId("location").textContent).toBe("/change-review?projectId=123&changeSetId=11"));
|
||||||
expect(acceptItemMock).not.toHaveBeenCalled();
|
expect(acceptItemMock).not.toHaveBeenCalled();
|
||||||
expect(commitChangeSetMock).not.toHaveBeenCalled();
|
expect(commitChangeSetMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps reviewer routing empty when payload omits it", async () => {
|
||||||
|
const state = realState();
|
||||||
|
delete state.reviewerRouting;
|
||||||
|
getTruthStateMock.mockResolvedValueOnce(state);
|
||||||
|
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.dataSourceStatus).toBe("success"));
|
||||||
|
expect(result.current.state.reviewerRouting).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("calls real message and review APIs in api mode and does not locally promote to project memory", async () => {
|
it("calls real message and review APIs in api mode and does not locally promote to project memory", async () => {
|
||||||
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
|
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,17 @@ function createEmptyPMFlowState(projectId: string | null, label = "未选择项
|
||||||
qa: [],
|
qa: [],
|
||||||
growth: [],
|
growth: [],
|
||||||
},
|
},
|
||||||
|
reviewerRouting: {
|
||||||
|
status: "empty",
|
||||||
|
warningCode: null,
|
||||||
|
items: [],
|
||||||
|
summary: {
|
||||||
|
totalPendingReview: 0,
|
||||||
|
highRiskCount: 0,
|
||||||
|
missingReviewerCount: 0,
|
||||||
|
iterationDraftCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
activeChangeSetId: null,
|
activeChangeSetId: null,
|
||||||
selectedTaskId: null,
|
selectedTaskId: null,
|
||||||
execution: {
|
execution: {
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,54 @@ export interface PMFlowTeamInbox {
|
||||||
growth: string[];
|
growth: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PMFlowReviewerRoutingStatus = "empty" | "ready" | "warning";
|
||||||
|
export type PMFlowReviewerRoutingSourceType = "manual" | "ai_extraction" | "iteration_recommendation" | "unknown";
|
||||||
|
export type PMFlowReviewerRoutingPriority = "normal" | "elevated" | "urgent";
|
||||||
|
export type PMFlowReviewerConfidence = "low" | "medium" | "high";
|
||||||
|
export type PMFlowFallbackReviewerRole = "OWNER" | "REVIEWER" | "MEMBER";
|
||||||
|
|
||||||
|
export interface PMFlowReviewerSuggestion {
|
||||||
|
userId?: string | null;
|
||||||
|
displayName?: string | null;
|
||||||
|
role?: string | null;
|
||||||
|
reason: string;
|
||||||
|
confidence: PMFlowReviewerConfidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PMFlowReviewerRoutingItem {
|
||||||
|
projectId: string;
|
||||||
|
changeSetId: string;
|
||||||
|
changeSetTitle: string;
|
||||||
|
changeSetStatus: string;
|
||||||
|
sourceType: PMFlowReviewerRoutingSourceType;
|
||||||
|
reviewReason: string;
|
||||||
|
riskLevel: PMFlowRiskLevel;
|
||||||
|
priority: PMFlowReviewerRoutingPriority;
|
||||||
|
changeTypes: string[];
|
||||||
|
itemCount: number;
|
||||||
|
pendingItemCount: number;
|
||||||
|
evidenceCount: number;
|
||||||
|
suggestedReviewer?: PMFlowReviewerSuggestion | null;
|
||||||
|
fallbackReviewerRole?: PMFlowFallbackReviewerRole | null;
|
||||||
|
targetUrl: string;
|
||||||
|
createdAt?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PMFlowReviewerRoutingSummary {
|
||||||
|
totalPendingReview: number;
|
||||||
|
highRiskCount: number;
|
||||||
|
missingReviewerCount: number;
|
||||||
|
iterationDraftCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PMFlowReviewerRouting {
|
||||||
|
status: PMFlowReviewerRoutingStatus;
|
||||||
|
warningCode?: string | null;
|
||||||
|
items: PMFlowReviewerRoutingItem[];
|
||||||
|
summary: PMFlowReviewerRoutingSummary;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectTruthState {
|
export interface ProjectTruthState {
|
||||||
loopStage: ProjectLoopStage;
|
loopStage: ProjectLoopStage;
|
||||||
lastWritebackAt: string | null;
|
lastWritebackAt: string | null;
|
||||||
|
|
@ -225,6 +273,7 @@ export interface ProjectTruthState {
|
||||||
approvals: Approval[];
|
approvals: Approval[];
|
||||||
feed: PMFlowFeedEvent[];
|
feed: PMFlowFeedEvent[];
|
||||||
teamInbox: PMFlowTeamInbox;
|
teamInbox: PMFlowTeamInbox;
|
||||||
|
reviewerRouting?: PMFlowReviewerRouting;
|
||||||
activeChangeSetId: string | null;
|
activeChangeSetId: string | null;
|
||||||
selectedTaskId: string | null;
|
selectedTaskId: string | null;
|
||||||
execution: PMFlowExecutionContext;
|
execution: PMFlowExecutionContext;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue