feat(pmflow-os): add execution scope board
This commit is contained in:
parent
d3ef506c97
commit
f33073d1a2
|
|
@ -766,14 +766,14 @@ commit gate 判定:
|
||||||
|
|
||||||
当前已完成:
|
当前已完成:
|
||||||
|
|
||||||
**main-flow-d4.5-execution-run-workspace-redesign:开发执行 Run 工作台重构。**
|
**main-flow-d4.6-execution-scope-board:开发范围总览与需求执行队列。**
|
||||||
|
|
||||||
原因:
|
原因:
|
||||||
|
|
||||||
- `/execution-bridge` 已从模块堆叠重构为 `Execution Run Workspace`:顶部 Run Header、阶段 rail、左侧执行源包、中间当前阶段工作台、右侧 AI 决策面板、底部证据流水。
|
- `/execution-bridge` 已补齐开发范围层:首屏先展示“本轮开发范围”和“需求执行队列”,再进入选中任务的 Run 工作台。
|
||||||
- 执行页现在明确表达 `已确认需求 / 上下文与 Prompt 包 / 开发 / 测试 / 审查合并 / 回写` 的业务流转,所有主按钮由单点 Run 状态模型推导,避免散落按钮各自判断。
|
- 页面现在按 Requirement 聚合正式 Task;同一需求下多个开发任务可在左侧切换,没有任务的已确认需求显示“尚未形成开发任务”,无法关联需求的任务进入“未归属任务”,不反推关系。
|
||||||
- 手动回写从常驻表单收进 drawer;页面主界面优先展示阶段、源头、当前证据和下一步,不再像 dev-sync 调试录入台。
|
- Run Header 现在同时展示范围规模与选中 Run 状态,右侧 AI 副驾增加范围缺口提示;主按钮仍由选中 Run 的单点状态模型推导。
|
||||||
- 本轮只改前端信息架构和测试,复用既有 `execution-summary / execution-capabilities / execution-progress-events`,不新增后端合同,不执行命令,不自动验收、合并或写 Project Memory。
|
- 本轮继续只改前端信息架构和测试,复用既有 `truth-state / execution-summary / execution-capabilities / execution-progress-events`,不新增后端合同,不执行命令,不自动验收、合并或写 Project Memory。
|
||||||
|
|
||||||
### 7.1 下一步推荐 Packet:`main-flow-d5-execution-acceptance-loop`
|
### 7.1 下一步推荐 Packet:`main-flow-d5-execution-acceptance-loop`
|
||||||
|
|
||||||
|
|
@ -808,6 +808,47 @@ commit gate 判定:
|
||||||
|
|
||||||
## 8. 进度台账
|
## 8. 进度台账
|
||||||
|
|
||||||
|
### 2026-05-06 / main-flow-d4.6-execution-scope-board / implemented
|
||||||
|
|
||||||
|
完成内容:
|
||||||
|
|
||||||
|
- 在 `/execution-bridge` 增加“本轮开发范围”总览,展示已确认需求、正式任务、执行中、待生成包、待测试/审查与阻塞数量。
|
||||||
|
- 增加“需求执行队列”,按 Requirement 聚合开发任务;同一需求下多个 Task 可以切换到对应 Run 详情。
|
||||||
|
- 对无法从 `state.requirements/state.tasks` 关联到需求的执行任务,进入“未归属任务”区,明确展示但不反推业务关系。
|
||||||
|
- 保留 D4.5 的单 Run 工作台作为选中详情:中间展示当前任务的阶段 rail 与工作台,右侧展示执行源包、AI 阶段决策与范围缺口。
|
||||||
|
- 新增前端只读范围模型,合并 `truth-state` 的 Requirement/Task 与 `execution-summary.activeDevTasks`,再为选中任务生成 scoped execution summary;不新增后端 API。
|
||||||
|
- 焦点测试覆盖多任务开发范围、需求队列、任务切换和切换后的 Run 主动作。
|
||||||
|
|
||||||
|
涉及文件:
|
||||||
|
|
||||||
|
- `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx`
|
||||||
|
- `frontend/src/features/pmflow-os/store.api.test.tsx`
|
||||||
|
- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md`
|
||||||
|
- `docs/architecture/CODE_REFERENCE_INDEX.md`
|
||||||
|
- `docs/architecture/ARCHITECTURE_INVENTORY.md`
|
||||||
|
- `docs/architecture/RUNTIME_GUARDRAILS.md`
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
- `cd frontend && npx tsc --noEmit --pretty false --incremental false` 通过。
|
||||||
|
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:35 passed。
|
||||||
|
- `cd frontend && npm run build` 通过。
|
||||||
|
- Playwright 浏览器检查 `http://localhost:5178/execution-bridge?demo=1` 通过:桌面与移动端均无页面横向溢出,首屏可见“本轮开发范围 / 需求执行队列”,点击“前置企业接入权限说明”后能切换到对应 Run;截图落点:
|
||||||
|
- `output/playwright/execution-scope-board-d46-desktop.png`
|
||||||
|
- `output/playwright/execution-scope-board-d46-desktop-selected.png`
|
||||||
|
- `output/playwright/execution-scope-board-d46-mobile.png`
|
||||||
|
- `output/playwright/execution-scope-board-d46-mobile-selected.png`
|
||||||
|
- 浏览器控制台仍有既有 `GET /api/ai-readiness/project-launch` 401,属于现有 AI readiness 探针鉴权噪声,本 Packet 未改该链路。
|
||||||
|
|
||||||
|
未完成或风险:
|
||||||
|
|
||||||
|
- Requirement -> Task -> Execution Run 关联当前仍优先消费现有前端读模型和执行摘要;后续若后端提供稳定 run/list 合同,应将 scoped summary 推导下沉到后端。
|
||||||
|
- 本轮不新增真实验收、技术审查、合并或 Project Memory 回写状态合同,这仍属于 `main-flow-d5-execution-acceptance-loop`。
|
||||||
|
|
||||||
|
下一步建议:
|
||||||
|
|
||||||
|
- 进入 `main-flow-d5-execution-acceptance-loop`:在 D4.6 的范围视图基础上,补真实验收、技术审查、合并确认和项目记忆回写状态,供 Space next action 与 Release/Growth 门禁稳定消费。
|
||||||
|
|
||||||
### 2026-05-06 / main-flow-d4.5-execution-run-workspace-redesign / implemented
|
### 2026-05-06 / main-flow-d4.5-execution-run-workspace-redesign / implemented
|
||||||
|
|
||||||
完成内容:
|
完成内容:
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@
|
||||||
- `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`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。
|
- `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`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。
|
||||||
- `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` 进入时通过 `origin=change_review / returnTo=change_review / changeSetId / itemId` 保留审查上下文,主动作返回待确认队列,补充证据才回指挥台;已 committed 的轮次显示为结构化写入快照并转向项目记忆或新变更链路。`ChangeReviewPage.tsx` 是决策与待确认中心,真实模式下顶部识别统计、待确认列表与右侧 AI 副驾评估都只消费后端 `changeSets / changeItems / evidence`,没有真实待审项时显示空态,禁止用 demo 审查项补位;确认写入 Project Memory 前,`pending_review / accepted / edited / deferred` ChangeItem 均可继续 accept/edit/defer,只有 `committed / skipped` 或关闭轮次不可再操作;`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`、`latestValidationRun` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/ROI/compliance/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;页面可展示 recommendation quality grouping,把真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;`ChangeReviewPage.tsx` 会展示 `iteration_recommendation_group` evidence 的来源组、feedback 数、recommendation 数、风险、影响和审查优先级;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`,Iteration Loop P1 已接入 `POST /iteration-recommendations/{recommendation_id}/change-set-drafts` 把 recommendation 回流到 Change Review,Iteration Loop P2 已接入 `POST /iteration-recommendation-groups/{group_key}/change-set-drafts` 并在 `releaseSignals.recommendationGroups` 暴露分组/去重/风险/影响合同;真实 AI extraction 已接入独立 `change_review` protected scene、`pmflow_change_review_extraction_v1` 运行合同、`PROMPT_PMFLOW_CHANGE_REVIEW_EXTRACTION` Prompt 资产与后端 `llm_service.generate_structured_object`,成功时生成 open ChangeSet、pending_review ChangeItems 与 thread message evidence,失败时 fail-closed,不把 mock 当真实失败 fallback,也不复用 Project Launch draft prompt/function。`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` 进入时通过 `origin=change_review / returnTo=change_review / changeSetId / itemId` 保留审查上下文,主动作返回待确认队列,补充证据才回指挥台;已 committed 的轮次显示为结构化写入快照并转向项目记忆或新变更链路。`ChangeReviewPage.tsx` 是决策与待确认中心,真实模式下顶部识别统计、待确认列表与右侧 AI 副驾评估都只消费后端 `changeSets / changeItems / evidence`,没有真实待审项时显示空态,禁止用 demo 审查项补位;确认写入 Project Memory 前,`pending_review / accepted / edited / deferred` ChangeItem 均可继续 accept/edit/defer,只有 `committed / skipped` 或关闭轮次不可再操作;`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`、`latestValidationRun` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/ROI/compliance/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;页面可展示 recommendation quality grouping,把真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;`ChangeReviewPage.tsx` 会展示 `iteration_recommendation_group` evidence 的来源组、feedback 数、recommendation 数、风险、影响和审查优先级;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`,Iteration Loop P1 已接入 `POST /iteration-recommendations/{recommendation_id}/change-set-drafts` 把 recommendation 回流到 Change Review,Iteration Loop P2 已接入 `POST /iteration-recommendation-groups/{group_key}/change-set-drafts` 并在 `releaseSignals.recommendationGroups` 暴露分组/去重/风险/影响合同;真实 AI extraction 已接入独立 `change_review` protected scene、`pmflow_change_review_extraction_v1` 运行合同、`PROMPT_PMFLOW_CHANGE_REVIEW_EXTRACTION` Prompt 资产与后端 `llm_service.generate_structured_object`,成功时生成 open ChangeSet、pending_review ChangeItems 与 thread message evidence,失败时 fail-closed,不把 mock 当真实失败 fallback,也不复用 Project Launch draft prompt/function。`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` 表达“未确认不得写入”的局部提示。
|
||||||
- Execution Result Protocol P1 将 Execution Bridge 的执行结果回写收敛为 `execution_result_protocol_v1`:`backend/app/schemas/pmflow_os.py` 接收 `execution_status` 与 `blocker_summary`,`backend/app/services/pmflow_os_projection_service.py` 写入 `ProgressEvent.payload.protocol_version` 与 `execution_result` envelope,同时保留 `branch_name / commit_sha / pr_number / test_summary / workflow_state` flat 字段兼容现有投影;`frontend/src/api/pmflowOs.ts` 和 `ExecutionBridgePage.tsx` 支持手动记录 status/blocker。该链路只记录 Codex/IDE/Local Bridge/GitHub agent 已产生的结果,不执行本机命令、不推送、不发布、不自动创建正式任务;`execution-summary`、`releaseGate` 与 Team Feed 均从 `ProgressEvent` 只读消费。
|
- Execution Result Protocol P1 将 Execution Bridge 的执行结果回写收敛为 `execution_result_protocol_v1`:`backend/app/schemas/pmflow_os.py` 接收 `execution_status` 与 `blocker_summary`,`backend/app/services/pmflow_os_projection_service.py` 写入 `ProgressEvent.payload.protocol_version` 与 `execution_result` envelope,同时保留 `branch_name / commit_sha / pr_number / test_summary / workflow_state` flat 字段兼容现有投影;`frontend/src/api/pmflowOs.ts` 和 `ExecutionBridgePage.tsx` 支持手动记录 status/blocker。该链路只记录 Codex/IDE/Local Bridge/GitHub agent 已产生的结果,不执行本机命令、不推送、不发布、不自动创建正式任务;`execution-summary`、`releaseGate` 与 Team Feed 均从 `ProgressEvent` 只读消费。
|
||||||
- Main Flow D4/D4.5 已把 Execution Capability Graph 接入并重构 `/execution-bridge` 为开发执行 Run 工作台:`frontend/src/features/pmflow-os/store.tsx` 读取并刷新真实 `GET /pmflow-os/projects/{project_id}/execution-capabilities`,`ExecutionBridgePage.tsx` 通过单点 Run 状态模型推导 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段、当前主动作、证据数和阻塞原因;页面结构为 Run Header、阶段 rail、执行源包、当前阶段工作台、AI 阶段决策面板和底部证据流水。handoff 主按钮受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,技术审查责任缺失时 store 会阻止真实 handoff,缺 repo 时只允许表达交接草案;手动回写在 drawer 中记录已发生的 branch/commit/PR/test/blocker/status。该层仍只做能力判定、交接合同和结果回写展示,不执行本机命令、不触发真实 agent、不自动验收、不自动合并、不自动发布。
|
- Main Flow D4/D4.5/D4.6 已把 Execution Capability Graph 接入并重构 `/execution-bridge` 为开发范围总览 + 选中 Run 工作台:`frontend/src/features/pmflow-os/store.tsx` 读取并刷新真实 `GET /pmflow-os/projects/{project_id}/execution-capabilities`,`ExecutionBridgePage.tsx` 先消费 `truth-state.requirements/tasks` 与 `execution-summary.activeDevTasks` 形成前端只读开发范围模型,展示本轮需求/任务总览、按 Requirement 聚合的需求执行队列、未归属任务和选中任务 Run 详情;选中 Run 继续通过单点状态模型推导 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段、当前主动作、证据数和阻塞原因。handoff 主按钮受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,技术审查责任缺失时 store 会阻止真实 handoff,缺 repo 时只允许表达交接草案;手动回写在 drawer 中记录已发生的 branch/commit/PR/test/blocker/status。该层仍只做能力判定、交接合同和结果回写展示,不执行本机命令、不触发真实 agent、不自动验收、不自动合并、不自动发布。
|
||||||
- Project Knowledge Hub 读模型由 `GET /pmflow-os/projects/{project_id}/project-memory/overview`、`items`、`items/{item_id}`、`assets`、`context-packs`、`writebacks` 承载,后端在 `backend/app/services/pmflow_os_projection_service.py` 从 `truth_nodes / tasks / semantic_facts / pmflow_evidence_links / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items` 聚合首页雷达、全部记忆、项目资产、上下文包和写入版本记录。前端对应 `ProjectMemoryPage.tsx`、`ProjectMemoryCollectionPages.tsx` 与 `projectMemoryUtils.ts`:列表时间使用短格式,抽屉显示完整时间和北京时间;首页卡片与二级列表单条点击打开右侧详情抽屉,展示来源证据、影响对象和 AI 执行动作跳转;全部记忆支持类型、阶段、来源、状态、搜索筛选。该读模型只读,资产可自动索引,进入项目真相层仍必须经过 Change Review 或可信规则确认。
|
- Project Knowledge Hub 读模型由 `GET /pmflow-os/projects/{project_id}/project-memory/overview`、`items`、`items/{item_id}`、`assets`、`context-packs`、`writebacks` 承载,后端在 `backend/app/services/pmflow_os_projection_service.py` 从 `truth_nodes / tasks / semantic_facts / pmflow_evidence_links / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items` 聚合首页雷达、全部记忆、项目资产、上下文包和写入版本记录。前端对应 `ProjectMemoryPage.tsx`、`ProjectMemoryCollectionPages.tsx` 与 `projectMemoryUtils.ts`:列表时间使用短格式,抽屉显示完整时间和北京时间;首页卡片与二级列表单条点击打开右侧详情抽屉,展示来源证据、影响对象和 AI 执行动作跳转;全部记忆支持类型、阶段、来源、状态、搜索筛选。该读模型只读,资产可自动索引,进入项目真相层仍必须经过 Change Review 或可信规则确认。
|
||||||
- `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 消费;P7 将通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,输出 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,继续使用 `pendingConfirmation` 偏好和 `change_review` surface,并声明 reviewerRouting、approval、evidence、audit state 均必须保留。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据、reviewerRouting、approval、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 消费;P7 将通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,输出 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,继续使用 `pendingConfirmation` 偏好和 `change_review` surface,并声明 reviewerRouting、approval、evidence、audit state 均必须保留。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据、reviewerRouting、approval、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`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest 与 iteration recommendation -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;AI feedback 写入 `pmflow_ai_feedback_events`,在 `truth-state.aiFeedback` 恢复当前用户选择,并由后端按 `prompt_key` 生成待复核 `JudgeRun / OptimizerCandidate` 质量优化候选,用户改回赞同时挂起此前候选,Prompt Asset quality summary 会聚合 PMFlow 反馈与优化候选;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。
|
- `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`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest 与 iteration recommendation -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;AI feedback 写入 `pmflow_ai_feedback_events`,在 `truth-state.aiFeedback` 恢复当前用户选择,并由后端按 `prompt_key` 生成待复核 `JudgeRun / OptimizerCandidate` 质量优化候选,用户改回赞同时挂起此前候选,Prompt Asset quality summary 会聚合 PMFlow 反馈与优化候选;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@
|
||||||
- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;登录后路由入口为 `/space`,未登录由 `frontend/src/auth/RouteBoundary.tsx` 跳转 `/login?redirect=/space`;首屏以 `nextActionQueue` 展示“我的下一步”,并保留执行准备度、继续推进、待我确认、发布关注、发布后反馈关注、下一轮迭代草稿关注、执行中 / AI 正在推进、暂存想法、草案箱、缺项目归属补全与最近项目明细;红框三块在首页只作为工作雷达预览,暂存想法、草案箱、最近项目均最多露出少量高优先级条目,完整处理分别进入 `/space/ideas`、`/space/drafts` 与 `/space/projects?view=recent`。`frontend/src/features/personal-space/SpaceCollectionPages.tsx` 承载二级页:`/space/ideas` 支持暂存想法筛选、搜索、补充、归档与转 Project Launch 草案,`/space/drafts` 支持待确认草案筛选、搜索与回主流程确认;这些页面仍使用空间壳,不直接写项目真相层。Space 同时展示 AI readiness 客户视图卡片,管理员可从侧边栏或顶部状态进入 `/ai-config?tab=setup`。空间级 AI 管家在 `frontend/src/features/personal-space/components/SpaceAIConcierge.tsx`,入口只挂 `/space`,向现有 `/rag/ask` 注入个人空间上下文包并复用 QA session 历史能力;它只能围绕个人空间、跨项目下一步、待确认、执行中、草案、暂存想法和 AI 能力状态工作,真实写操作必须回主流程确认或跳转,不得声称已执行。全局 `Layout` 不再挂载旧 `FloatingQAChat` 悬浮球,普通后台页不显示无上下文聊天球,项目页继续使用项目级 `AI 副驾`。Space 左侧导航只承载稳定空间目的地,当前保留“空间首页”“暂存想法”“草案箱”“所有项目”和管理员“AI 使用设置”。点击项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,review attention 待审项进入 `/change-review?projectId=<id>&changeSetId=<id>`,reviewer 缺失 warning 进入已有 `/team-layer?projectId=<id>`,执行中与执行准备度项进入 `/execution-bridge?projectId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`;执行失败和回写失败项进入 `/execution-bridge?projectId=<id>`;Space 聚合项目摘要、PMFlow OS `truth-state.reviewerRouting / truth-state.releases / execution-summary.releaseGate / execution-summary.releaseSignals / execution-summary.recentProgressEvents / execution-summary.recentDeliveries / execution-capabilities` 和真实 `GET /space/saved-ideas`,并兼容读取旧 AI PMO `waiting_manual / draft_pending_confirm` 工作项;`missing_project_scope` 仅在 Space 选择项目后复用 `POST /ai-pmo/work-items/{id}/decision`,不新增旁路接口,不直接写项目真相层。缺数据时显示明确空态或 warning,不虚构 reviewer、审查意见、待审 ChangeSet、执行能力、工具连接、验证、反馈、收入、ROI、合规、iteration recommendation 或 ChangeSet draft;暂存想法是唯一允许 Space 写入的 pre-project seed,保存原始输入、来源、状态、准备度和 draft 关联,生成草案只进入 Project Launch human review;除此之外不直接写 Project Memory、ChangeSet 状态、PMFlowApproval、execution event、ReleaseValidationRun、FeedbackIngest、RevenueEvent 或 ComplianceCheckRun;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该空间 Chrome
|
- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;登录后路由入口为 `/space`,未登录由 `frontend/src/auth/RouteBoundary.tsx` 跳转 `/login?redirect=/space`;首屏以 `nextActionQueue` 展示“我的下一步”,并保留执行准备度、继续推进、待我确认、发布关注、发布后反馈关注、下一轮迭代草稿关注、执行中 / AI 正在推进、暂存想法、草案箱、缺项目归属补全与最近项目明细;红框三块在首页只作为工作雷达预览,暂存想法、草案箱、最近项目均最多露出少量高优先级条目,完整处理分别进入 `/space/ideas`、`/space/drafts` 与 `/space/projects?view=recent`。`frontend/src/features/personal-space/SpaceCollectionPages.tsx` 承载二级页:`/space/ideas` 支持暂存想法筛选、搜索、补充、归档与转 Project Launch 草案,`/space/drafts` 支持待确认草案筛选、搜索与回主流程确认;这些页面仍使用空间壳,不直接写项目真相层。Space 同时展示 AI readiness 客户视图卡片,管理员可从侧边栏或顶部状态进入 `/ai-config?tab=setup`。空间级 AI 管家在 `frontend/src/features/personal-space/components/SpaceAIConcierge.tsx`,入口只挂 `/space`,向现有 `/rag/ask` 注入个人空间上下文包并复用 QA session 历史能力;它只能围绕个人空间、跨项目下一步、待确认、执行中、草案、暂存想法和 AI 能力状态工作,真实写操作必须回主流程确认或跳转,不得声称已执行。全局 `Layout` 不再挂载旧 `FloatingQAChat` 悬浮球,普通后台页不显示无上下文聊天球,项目页继续使用项目级 `AI 副驾`。Space 左侧导航只承载稳定空间目的地,当前保留“空间首页”“暂存想法”“草案箱”“所有项目”和管理员“AI 使用设置”。点击项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,review attention 待审项进入 `/change-review?projectId=<id>&changeSetId=<id>`,reviewer 缺失 warning 进入已有 `/team-layer?projectId=<id>`,执行中与执行准备度项进入 `/execution-bridge?projectId=<id>`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=<id>`;执行失败和回写失败项进入 `/execution-bridge?projectId=<id>`;Space 聚合项目摘要、PMFlow OS `truth-state.reviewerRouting / truth-state.releases / execution-summary.releaseGate / execution-summary.releaseSignals / execution-summary.recentProgressEvents / execution-summary.recentDeliveries / execution-capabilities` 和真实 `GET /space/saved-ideas`,并兼容读取旧 AI PMO `waiting_manual / draft_pending_confirm` 工作项;`missing_project_scope` 仅在 Space 选择项目后复用 `POST /ai-pmo/work-items/{id}/decision`,不新增旁路接口,不直接写项目真相层。缺数据时显示明确空态或 warning,不虚构 reviewer、审查意见、待审 ChangeSet、执行能力、工具连接、验证、反馈、收入、ROI、合规、iteration recommendation 或 ChangeSet draft;暂存想法是唯一允许 Space 写入的 pre-project seed,保存原始输入、来源、状态、准备度和 draft 关联,生成草案只进入 Project Launch human review;除此之外不直接写 Project Memory、ChangeSet 状态、PMFlowApproval、execution event、ReleaseValidationRun、FeedbackIngest、RevenueEvent 或 ComplianceCheckRun;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该空间 Chrome
|
||||||
- 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式登录后路由入口为 `/project-launch`,默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 taskized draft job/confirm API;AI 草案创建链路是 `POST /pmflow-launch/draft-jobs -> GET /pmflow-launch/draft-jobs/{job_id} -> POST /pmflow-launch/drafts/{draft_id}/confirm`,补充一句走 `POST /pmflow-launch/drafts/{draft_id}/revise` 或创建新的 `merge_supplement` job,旧 `POST /ai-steward/project-from-text` 只返回 410 迁移错误;只有显式 `?demo=1` 或 `?dataSource=demo` 才允许匿名并启用 `mockData.ts` 本地演示状态机,真实模式的操作流、生成过程、审查建议、附件引用和计数从当前输入、job 状态与后端返回草案派生,不再使用周报 demo 文案兜底;真实模式下 AI 未就绪时先渲染 `AISetupRequiredPanel`,按权限展示记录想法、查看空间状态或平台 AI 能力处理 CTA,不提供空项目创建入口,避免在缺少结构化上下文时提前创建项目壳。401/无 token 通过 `frontend/src/lib/apiError.ts` 归一为 `auth_required/session_expired`,通过 `frontend/src/lib/pendingIntent.ts` 保留输入;`input_needs_more_info` 422 归一为 `input_invalid` 并停在补充输入失败态,用户可见失败态不得展示 `Request failed with status code 401` 或把 401 归为网络异常
|
- 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式登录后路由入口为 `/project-launch`,默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 taskized draft job/confirm API;AI 草案创建链路是 `POST /pmflow-launch/draft-jobs -> GET /pmflow-launch/draft-jobs/{job_id} -> POST /pmflow-launch/drafts/{draft_id}/confirm`,补充一句走 `POST /pmflow-launch/drafts/{draft_id}/revise` 或创建新的 `merge_supplement` job,旧 `POST /ai-steward/project-from-text` 只返回 410 迁移错误;只有显式 `?demo=1` 或 `?dataSource=demo` 才允许匿名并启用 `mockData.ts` 本地演示状态机,真实模式的操作流、生成过程、审查建议、附件引用和计数从当前输入、job 状态与后端返回草案派生,不再使用周报 demo 文案兜底;真实模式下 AI 未就绪时先渲染 `AISetupRequiredPanel`,按权限展示记录想法、查看空间状态或平台 AI 能力处理 CTA,不提供空项目创建入口,避免在缺少结构化上下文时提前创建项目壳。401/无 token 通过 `frontend/src/lib/apiError.ts` 归一为 `auth_required/session_expired`,通过 `frontend/src/lib/pendingIntent.ts` 保留输入;`input_needs_more_info` 422 归一为 `input_invalid` 并停在补充输入失败态,用户可见失败态不得展示 `Request failed with status code 401` 或把 401 归为网络异常
|
||||||
- 前端身份边界与产品化错误:参考 `frontend/src/auth/RouteBoundary.tsx`、`frontend/src/lib/rbac.ts`、`frontend/src/lib/apiError.ts`、`frontend/src/lib/pendingIntent.ts`;`/space`、真实 `/project-launch`、`/me`、主链 standalone、空间管理入口与平台运营入口均在前端边界收口。空间管理入口使用 workspace role,`/platform/*` 和平台治理入口使用 `platform_context.capabilities`,后端 JWT/RBAC 仍是最终校验;pending intent 只能恢复 project launch 输入或承接已经生成好的 Project Launch draft,不允许登录后自动重放真实写请求
|
- 前端身份边界与产品化错误:参考 `frontend/src/auth/RouteBoundary.tsx`、`frontend/src/lib/rbac.ts`、`frontend/src/lib/apiError.ts`、`frontend/src/lib/pendingIntent.ts`;`/space`、真实 `/project-launch`、`/me`、主链 standalone、空间管理入口与平台运营入口均在前端边界收口。空间管理入口使用 workspace role,`/platform/*` 和平台治理入口使用 `platform_context.capabilities`,后端 JWT/RBAC 仍是最终校验;pending intent 只能恢复 project launch 输入或承接已经生成好的 Project Launch draft,不允许登录后自动重放真实写请求
|
||||||
- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=<id>` 走 React Query 读取真实 `truth-state`、`execution-summary` 与 `execution-capabilities`,真实模式下 `sendMessage` 会先写 thread message,再调用 `createPMFlowExtraction` 触发后端真实 AI extraction,成功后 refetch 出 open ChangeSet / pending_review ChangeItems;显式 `proposed_items` extraction 仍保留给受控 draft 场景;ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 approval、execution event、发布验证记录、发布后信号或下一轮 draft;D4 后 `createExecutionHandoff` 除正式 Task gate 外会读取 `execution-capabilities`,在技术审查责任缺失时阻止 handoff,避免把 agent 执行绕过 reviewer gate;D4.5 后 `/execution-bridge` 的主按钮由页面级 Run 状态模型消费 store 数据统一推导,不再散落在上下文包、回写视图和 AI 副驾卡片里;AI feedback 通过 `createPMFlowAIFeedback` 写入 `pmflow_ai_feedback_events` 并在 `truth-state.aiFeedback` 恢复当前用户选择;AI extraction 失败在 CommandCenter / StructuredExtraction 显示“结构化失败/待补充”,不展示模板假候选;`?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` 与 `execution-capabilities`,真实模式下 `sendMessage` 会先写 thread message,再调用 `createPMFlowExtraction` 触发后端真实 AI extraction,成功后 refetch 出 open ChangeSet / pending_review ChangeItems;显式 `proposed_items` extraction 仍保留给受控 draft 场景;ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 approval、execution event、发布验证记录、发布后信号或下一轮 draft;D4 后 `createExecutionHandoff` 除正式 Task gate 外会读取 `execution-capabilities`,在技术审查责任缺失时阻止 handoff,避免把 agent 执行绕过 reviewer gate;D4.5 后 `/execution-bridge` 的主按钮由页面级 Run 状态模型消费 store 数据统一推导,不再散落在上下文包、回写视图和 AI 副驾卡片里;D4.6 后 `/execution-bridge` 额外消费 `truth-state.requirements/tasks` 与 `execution-summary.activeDevTasks` 形成前端只读开发范围模型,按 Requirement 聚合任务、展示未归属任务并支持切换选中 Run;AI feedback 通过 `createPMFlowAIFeedback` 写入 `pmflow_ai_feedback_events` 并在 `truth-state.aiFeedback` 恢复当前用户选择;AI extraction 失败在 CommandCenter / StructuredExtraction 显示“结构化失败/待补充”,不展示模板假候选;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success`
|
||||||
- Account Settings / `/me` 前端产品域与 P1-P8 偏好合同:参考 `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;P7 将每类通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,稳定返回 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,统一使用 `pendingConfirmation` 偏好和 `change_review` surface,返回 `reviewerRoutingPolicy=preserve_reviewer_routing / approvalPolicy=preserve_approval_state`,偏好关闭只静默页面内提醒,不隐藏 reviewerRouting、approval、evidence 或 audit state;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run、releaseSignals 或下一轮 ChangeSet draft;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入
|
- Account Settings / `/me` 前端产品域与 P1-P8 偏好合同:参考 `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;P7 将每类通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,稳定返回 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,统一使用 `pendingConfirmation` 偏好和 `change_review` surface,返回 `reviewerRoutingPolicy=preserve_reviewer_routing / approvalPolicy=preserve_approval_state`,偏好关闭只静默页面内提醒,不隐藏 reviewerRouting、approval、evidence 或 audit state;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run、releaseSignals 或下一轮 ChangeSet draft;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入
|
||||||
- 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`;`StructuredExtractionPage.tsx` 根据 `origin=change_review / returnTo=change_review / changeSetId / itemId` 区分指挥台读取检查、待确认证据检查和已写入快照,从待确认进入时主动作必须返回 `/change-review` 原轮次,不把用户强制带回指挥台;`ChangeReviewPage.tsx` 真实模式可对当前变更集请求审查、通过、要求修改或拒绝,展示审查状态、建议审查人、审查意见和高风险审批门禁,但不自动 commit,且在确认写入 Project Memory 前允许 `pending_review / accepted / edited / deferred` ChangeItem 继续 accept/edit/defer;`ExecutionBridgePage.tsx` 真实模式读取 `execution-summary` 与 `execution-capabilities`,D4.5 后页面形态为开发执行 Run 工作台:Run Header、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段 rail、左侧执行源包、中间当前阶段工作台、右侧 AI 阶段决策面板和底部证据流水;主动作由单点 Run 状态模型推导,必须受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,缺 repo 只能生成交接草案,技术审查责任缺失时不能发起 handoff;手动回写在 drawer 内记录已发生的 branch/commit/PR/test/blocker/status,不执行命令;执行结果只汇总运行端、PR/commit、测试、阻塞和下一步,不把测试通过自动视为验收完成;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting`、`state.feed` 与 `execution-capabilities`,以 `团队动态 / 决策台 / 角色收件箱 / 智能体工位 / AI 副驾` 展示团队协作态势、待审变更责任路由、执行能力与容量信号、个人执行准备度和工具准备度,只给责任建议、阻塞解释与跳转,不自动分派、不自动审查、不写 ChangeSet 或 Project Memory;决策台与待审路由卡片不得预留独立空白操作列,正文应使用完整卡片宽度,1-2 条路由按双列或单列密度展示;`ReleaseGrowthPage.tsx` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并展示真实 feedback、revenue、ROI、compliance 与 recommendation 风险态,从真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft;生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付、不接真实合规平台;所有真实证据和审计状态仍可见,静态高保真发布/增长数据仅保留在显式 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`;`StructuredExtractionPage.tsx` 根据 `origin=change_review / returnTo=change_review / changeSetId / itemId` 区分指挥台读取检查、待确认证据检查和已写入快照,从待确认进入时主动作必须返回 `/change-review` 原轮次,不把用户强制带回指挥台;`ChangeReviewPage.tsx` 真实模式可对当前变更集请求审查、通过、要求修改或拒绝,展示审查状态、建议审查人、审查意见和高风险审批门禁,但不自动 commit,且在确认写入 Project Memory 前允许 `pending_review / accepted / edited / deferred` ChangeItem 继续 accept/edit/defer;`ExecutionBridgePage.tsx` 真实模式读取 `execution-summary` 与 `execution-capabilities`,D4.6 后页面形态为开发范围总览 + 需求执行队列 + 选中 Run 工作台:首屏展示本轮已确认需求、正式任务、执行中、待生成包、待测试/审查与阻塞数量,左侧按 Requirement 聚合任务、列出未归属任务并允许切换选中任务;选中任务详情继续展示 Run Header、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段 rail、当前阶段工作台、执行源包、AI 阶段决策面板和底部证据流水;主动作由单点 Run 状态模型推导,必须受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,缺 repo 只能生成交接草案,技术审查责任缺失时不能发起 handoff;手动回写在 drawer 内记录已发生的 branch/commit/PR/test/blocker/status,不执行命令;执行结果只汇总运行端、PR/commit、测试、阻塞和下一步,不把测试通过自动视为验收完成;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting`、`state.feed` 与 `execution-capabilities`,以 `团队动态 / 决策台 / 角色收件箱 / 智能体工位 / AI 副驾` 展示团队协作态势、待审变更责任路由、执行能力与容量信号、个人执行准备度和工具准备度,只给责任建议、阻塞解释与跳转,不自动分派、不自动审查、不写 ChangeSet 或 Project Memory;决策台与待审路由卡片不得预留独立空白操作列,正文应使用完整卡片宽度,1-2 条路由按双列或单列密度展示;`ReleaseGrowthPage.tsx` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并展示真实 feedback、revenue、ROI、compliance 与 recommendation 风险态,从真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft;生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付、不接真实合规平台;所有真实证据和审计状态仍可见,静态高保真发布/增长数据仅保留在显式 demo 模式
|
||||||
- 团队层只读动态与协作态势:`TeamLayerPage.tsx` 直接消费 `ProjectTruthState.feed` 聚合的 review request、review decision、变更集状态、execution progress 与 release gate 事件,并结合 `reviewerRouting`、`execution-capabilities` 和容量计划生成首屏摘要;前端只做空态、摘要、中文化展示和跳转,不写审批、不写 ChangeSet、不写通知,也不在真实模式 fallback 到 mock 动态。
|
- 团队层只读动态与协作态势:`TeamLayerPage.tsx` 直接消费 `ProjectTruthState.feed` 聚合的 review request、review decision、变更集状态、execution progress 与 release gate 事件,并结合 `reviewerRouting`、`execution-capabilities` 和容量计划生成首屏摘要;前端只做空态、摘要、中文化展示和跳转,不写审批、不写 ChangeSet、不写通知,也不在真实模式 fallback 到 mock 动态。
|
||||||
- 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`
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
- 技术层 fallback 仅允许用于重试、缓存、解析修复、传输兼容等基础设施环节,禁止上浮为用户可见业务结论。
|
- 技术层 fallback 仅允许用于重试、缓存、解析修复、传输兼容等基础设施环节,禁止上浮为用户可见业务结论。
|
||||||
- AI Steward 与其它生成链的成功/失败判定必须优先使用结构校验、字段完整性和证据一致性。
|
- AI Steward 与其它生成链的成功/失败判定必须优先使用结构校验、字段完整性和证据一致性。
|
||||||
- PMFlow OS `ChangeReviewPage` 真实模式只能展示 `ProjectTruthState.changeSets / changeItems / evidence` 的后端投影;顶部统计、待确认列表和右侧 AI 副驾评估都必须由真实 ChangeItem/Evidence 派生。没有真实待审项时必须显示空态或待补充,禁止用 demo 的知识库搜索、混合召回、固定风险/建议/计数补位。`accepted / edited / deferred` 只表示候选项已被人工处理但尚未写入 Project Memory;只要 ChangeSet 未 committed 且 ChangeItem 未 committed/skipped,就必须允许继续 accept/edit/defer,真正锁定边界是 ChangeSet commit。
|
- PMFlow OS `ChangeReviewPage` 真实模式只能展示 `ProjectTruthState.changeSets / changeItems / evidence` 的后端投影;顶部统计、待确认列表和右侧 AI 副驾评估都必须由真实 ChangeItem/Evidence 派生。没有真实待审项时必须显示空态或待补充,禁止用 demo 的知识库搜索、混合召回、固定风险/建议/计数补位。`accepted / edited / deferred` 只表示候选项已被人工处理但尚未写入 Project Memory;只要 ChangeSet 未 committed 且 ChangeItem 未 committed/skipped,就必须允许继续 accept/edit/defer,真正锁定边界是 ChangeSet commit。
|
||||||
- PMFlow OS 团队审查与执行边界必须保持单向主链:`TeamLayerPage` 只允许消费 `reviewerRouting / reviewDecision / approvalState / TeamFeed` 做责任路由、角色收件箱、团队动态、审查状态、阻塞解释和跳转,不得直接执行 ChangeSet commit、写 Project Memory 或成为第二套 Change Review;Execution Bridge handoff 只能针对 Project Memory commit 后已进入正式项目对象的 Task,禁止从 `pending_review` ChangeItem、候选 Task、reviewer suggestion 或 team routing item 直接创建正式执行分派。`ExecutionBridgePage` 的 Run 工作台只能把 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 作为前端流转表达和证据 gate,不得因为 UI 阶段推进而自动验收、自动合并、自动写 Project Memory 或自动进入发布成功。缺 reviewer、repo/runtime、PromptPackage、测试命令、权限或回写证据时必须返回 blocked/warning,禁止冒充已开始开发或已分配成功。
|
- PMFlow OS 团队审查与执行边界必须保持单向主链:`TeamLayerPage` 只允许消费 `reviewerRouting / reviewDecision / approvalState / TeamFeed` 做责任路由、角色收件箱、团队动态、审查状态、阻塞解释和跳转,不得直接执行 ChangeSet commit、写 Project Memory 或成为第二套 Change Review;Execution Bridge handoff 只能针对 Project Memory commit 后已进入正式项目对象的 Task,禁止从 `pending_review` ChangeItem、候选 Task、reviewer suggestion 或 team routing item 直接创建正式执行分派。`ExecutionBridgePage` 的开发范围总览和 Run 工作台只能把 Requirement/Task 范围、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段作为前端流转表达和证据 gate;未归属任务只能展示为未归属,不得反推需求关系;不得因为 UI 阶段推进而自动验收、自动合并、自动写 Project Memory 或自动进入发布成功。缺 reviewer、repo/runtime、PromptPackage、测试命令、权限或回写证据时必须返回 blocked/warning,禁止冒充已开始开发或已分配成功。
|
||||||
|
|
||||||
### 3.3 业务动作归因主链
|
### 3.3 业务动作归因主链
|
||||||
- 运行时业务动作归因必须优先走:
|
- 运行时业务动作归因必须优先走:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
|
@ -44,9 +44,12 @@ import {
|
||||||
} from "@/features/pmflow-os/components/ExecutionBridgeCards";
|
} from "@/features/pmflow-os/components/ExecutionBridgeCards";
|
||||||
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||||
import type {
|
import type {
|
||||||
|
Requirement as PMFlowRequirement,
|
||||||
PMFlowExecutionCapabilityMode,
|
PMFlowExecutionCapabilityMode,
|
||||||
PMFlowExecutionCapabilitySnapshot,
|
PMFlowExecutionCapabilitySnapshot,
|
||||||
PMFlowExecutionModeCapability,
|
PMFlowExecutionModeCapability,
|
||||||
|
ProjectTruthState,
|
||||||
|
Task as PMFlowProjectTask,
|
||||||
} from "@/features/pmflow-os/types";
|
} from "@/features/pmflow-os/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -144,6 +147,51 @@ interface ExecutionRunState {
|
||||||
sourceItems: Array<{ label: string; value: string; tone?: "default" | "warning" | "success" }>;
|
sourceItems: Array<{ label: string; value: string; tone?: "default" | "warning" | "success" }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecutionScopeTone = "ready" | "running" | "waiting" | "blocked" | "empty";
|
||||||
|
|
||||||
|
interface ExecutionScopeTaskItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
status: string;
|
||||||
|
stage: string;
|
||||||
|
requirementId?: string | null;
|
||||||
|
requirementTitle?: string | null;
|
||||||
|
devTask?: PMFlowDevTask | null;
|
||||||
|
projectTask?: PMFlowProjectTask | null;
|
||||||
|
runStage: string;
|
||||||
|
nextAction: string;
|
||||||
|
evidenceCount: number;
|
||||||
|
tone: ExecutionScopeTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExecutionScopeRequirementGroup {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
status: string;
|
||||||
|
tasks: ExecutionScopeTaskItem[];
|
||||||
|
coverageLabel: string;
|
||||||
|
tone: ExecutionScopeTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExecutionScopeBoard {
|
||||||
|
groups: ExecutionScopeRequirementGroup[];
|
||||||
|
unassignedTasks: ExecutionScopeTaskItem[];
|
||||||
|
allTasks: ExecutionScopeTaskItem[];
|
||||||
|
selectedTaskId: string | null;
|
||||||
|
totals: {
|
||||||
|
requirements: number;
|
||||||
|
tasks: number;
|
||||||
|
running: number;
|
||||||
|
waitingPackage: number;
|
||||||
|
waitingTest: number;
|
||||||
|
waitingReview: number;
|
||||||
|
blocked: number;
|
||||||
|
};
|
||||||
|
nextQueueAction: string;
|
||||||
|
}
|
||||||
|
|
||||||
function isFormalDevTask(task?: PMFlowDevTask | null) {
|
function isFormalDevTask(task?: PMFlowDevTask | null) {
|
||||||
const status = String(task?.status || "").toLowerCase();
|
const status = String(task?.status || "").toLowerCase();
|
||||||
return Boolean(
|
return Boolean(
|
||||||
|
|
@ -294,9 +342,23 @@ export default function ExecutionBridgePage() {
|
||||||
const isLoading = dataSourceMode === "api" && executionSummaryStatus === "loading";
|
const isLoading = dataSourceMode === "api" && executionSummaryStatus === "loading";
|
||||||
const error = dataSourceMode === "api" ? executionSummaryError : null;
|
const error = dataSourceMode === "api" ? executionSummaryError : null;
|
||||||
const [writebackOpen, setWritebackOpen] = useState(false);
|
const [writebackOpen, setWritebackOpen] = useState(false);
|
||||||
const run = summary ? buildExecutionRunState(summary, capabilities) : null;
|
const [selectedScopeTaskId, setSelectedScopeTaskId] = useState<string | null>(null);
|
||||||
|
const baseScope = useMemo(() => (summary ? buildExecutionScopeBoard(state, summary, capabilities, selectedScopeTaskId) : null), [state, summary, capabilities, selectedScopeTaskId]);
|
||||||
|
const selectedTaskId = baseScope?.selectedTaskId || null;
|
||||||
|
const scopedSummary = useMemo(
|
||||||
|
() => (summary && selectedTaskId ? createScopedExecutionSummary(summary, state, selectedTaskId) : summary),
|
||||||
|
[summary, state, selectedTaskId]
|
||||||
|
);
|
||||||
|
const scope = useMemo(() => (scopedSummary ? buildExecutionScopeBoard(state, scopedSummary, capabilities, selectedTaskId) : baseScope), [state, scopedSummary, capabilities, selectedTaskId, baseScope]);
|
||||||
|
const run = scopedSummary ? buildExecutionRunState(scopedSummary, capabilities) : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scope?.selectedTaskId || selectedScopeTaskId) return;
|
||||||
|
setSelectedScopeTaskId(scope.selectedTaskId);
|
||||||
|
}, [scope?.selectedTaskId, selectedScopeTaskId]);
|
||||||
|
|
||||||
const handlePrimaryAction = () => {
|
const handlePrimaryAction = () => {
|
||||||
if (!summary || !run || !run.primaryAction.enabled) return;
|
if (!scopedSummary || !run || !run.primaryAction.enabled) return;
|
||||||
if (run.primaryAction.kind === "create_handoff" && !isDemo && run.formalTask) {
|
if (run.primaryAction.kind === "create_handoff" && !isDemo && run.formalTask) {
|
||||||
createExecutionHandoff({
|
createExecutionHandoff({
|
||||||
task_id: run.formalTask.id,
|
task_id: run.formalTask.id,
|
||||||
|
|
@ -319,25 +381,26 @@ export default function ExecutionBridgePage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (run.primaryAction.kind === "change_review") {
|
if (run.primaryAction.kind === "change_review") {
|
||||||
window.location.assign(`/change-review?projectId=${summary.projectId}`);
|
window.location.assign(`/change-review?projectId=${scopedSummary.projectId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (run.primaryAction.kind === "team_layer") {
|
if (run.primaryAction.kind === "team_layer") {
|
||||||
window.location.assign(`/team-layer?projectId=${summary.projectId}`);
|
window.location.assign(`/team-layer?projectId=${scopedSummary.projectId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (run.primaryAction.kind === "release") {
|
if (run.primaryAction.kind === "release") {
|
||||||
window.location.assign(`/release-growth?projectId=${summary.projectId}`);
|
window.location.assign(`/release-growth?projectId=${scopedSummary.projectId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full bg-[#f7f9fd] pb-6 text-slate-950">
|
<div className="min-h-full bg-[#f7f9fd] pb-6 text-slate-950">
|
||||||
<div className="mx-auto max-w-[1760px] space-y-4 px-4 py-4 lg:px-6">
|
<div className="mx-auto max-w-[1760px] space-y-4 px-4 py-4 lg:px-6">
|
||||||
{run && summary ? (
|
{run && scopedSummary ? (
|
||||||
<ExecutionRunHeader
|
<ExecutionRunHeader
|
||||||
summary={summary}
|
summary={scopedSummary}
|
||||||
run={run}
|
run={run}
|
||||||
|
scope={scope}
|
||||||
dataSourceMode={dataSourceMode}
|
dataSourceMode={dataSourceMode}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
refetchExecutionSummary();
|
refetchExecutionSummary();
|
||||||
|
|
@ -372,38 +435,45 @@ export default function ExecutionBridgePage() {
|
||||||
<ExecutionEmptyState title="缺少项目执行上下文" summary="URL 未提供 projectId,执行桥不会回退到假数据。请从真实项目进入或显式追加 ?demo=1。" />
|
<ExecutionEmptyState title="缺少项目执行上下文" summary="URL 未提供 projectId,执行桥不会回退到假数据。请从真实项目进入或显式追加 ?demo=1。" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{summary && run ? (
|
{scopedSummary && run && scope ? (
|
||||||
<>
|
<>
|
||||||
<ExecutionRunStageRail run={run} />
|
|
||||||
{dataSourceMode === "api" && executionCapabilitiesStatus === "loading" ? (
|
{dataSourceMode === "api" && executionCapabilitiesStatus === "loading" ? (
|
||||||
<ExecutionEmptyState title="正在读取执行能力图谱" summary="主流程已根据执行摘要渲染,执行模式和责任门禁会在能力图谱同步后刷新。" />
|
<ExecutionEmptyState title="正在读取执行能力图谱" summary="主流程已根据执行摘要渲染,执行模式和责任门禁会在能力图谱同步后刷新。" />
|
||||||
) : null}
|
) : null}
|
||||||
{dataSourceMode === "api" && executionCapabilitiesStatus === "error" ? (
|
{dataSourceMode === "api" && executionCapabilitiesStatus === "error" ? (
|
||||||
<ExecutionEmptyState title="执行能力图谱同步失败" summary={executionCapabilitiesError || "执行能力信号暂未同步,主流程只展示执行摘要证据。"} />
|
<ExecutionEmptyState title="执行能力图谱同步失败" summary={executionCapabilitiesError || "执行能力信号暂未同步,主流程只展示执行摘要证据。"} />
|
||||||
) : null}
|
) : null}
|
||||||
<ExecutionWarningList warnings={summary.warnings} />
|
<ExecutionWarningList warnings={scopedSummary.warnings} />
|
||||||
|
<ExecutionScopeOverview scope={scope} />
|
||||||
|
|
||||||
<div className="grid gap-4 min-[1320px]:grid-cols-[360px_minmax(0,1fr)_340px]">
|
<div className="grid gap-4 min-[1320px]:grid-cols-[360px_minmax(0,1fr)_340px]">
|
||||||
<ExecutionRunSourcePanel run={run} summary={summary} />
|
<ExecutionScopeQueuePanel scope={scope} selectedTaskId={selectedTaskId} onSelectTask={setSelectedScopeTaskId} />
|
||||||
<ExecutionRunWorkspacePanel
|
<div className="min-w-0 space-y-4">
|
||||||
run={run}
|
<ExecutionRunStageRail run={run} />
|
||||||
summary={summary}
|
<ExecutionRunWorkspacePanel
|
||||||
onOpenWriteback={() => setWritebackOpen(true)}
|
run={run}
|
||||||
onDemoPush={isDemo ? advanceExecution : undefined}
|
summary={scopedSummary}
|
||||||
/>
|
onOpenWriteback={() => setWritebackOpen(true)}
|
||||||
<ExecutionRunDecisionPanel
|
onDemoPush={isDemo ? advanceExecution : undefined}
|
||||||
run={run}
|
/>
|
||||||
summary={summary}
|
</div>
|
||||||
capabilities={capabilities}
|
<div className="space-y-4">
|
||||||
onPrimaryAction={handlePrimaryAction}
|
<ExecutionRunSourcePanel run={run} summary={scopedSummary} />
|
||||||
onOpenWriteback={() => setWritebackOpen(true)}
|
<ExecutionRunDecisionPanel
|
||||||
/>
|
run={run}
|
||||||
|
summary={scopedSummary}
|
||||||
|
scope={scope}
|
||||||
|
capabilities={capabilities}
|
||||||
|
onPrimaryAction={handlePrimaryAction}
|
||||||
|
onOpenWriteback={() => setWritebackOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExecutionProgressPanel summary={summary} />
|
<ExecutionProgressPanel summary={scopedSummary} />
|
||||||
{writebackOpen && !isDemo ? (
|
{writebackOpen && !isDemo ? (
|
||||||
<ExecutionWritebackDrawer
|
<ExecutionWritebackDrawer
|
||||||
summary={summary}
|
summary={scopedSummary}
|
||||||
onRecordProgress={recordExecutionProgress}
|
onRecordProgress={recordExecutionProgress}
|
||||||
onClose={() => setWritebackOpen(false)}
|
onClose={() => setWritebackOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -418,12 +488,14 @@ export default function ExecutionBridgePage() {
|
||||||
function ExecutionRunHeader({
|
function ExecutionRunHeader({
|
||||||
summary,
|
summary,
|
||||||
run,
|
run,
|
||||||
|
scope,
|
||||||
dataSourceMode,
|
dataSourceMode,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onPrimaryAction,
|
onPrimaryAction,
|
||||||
}: {
|
}: {
|
||||||
summary: PMFlowExecutionSummary;
|
summary: PMFlowExecutionSummary;
|
||||||
run: ExecutionRunState;
|
run: ExecutionRunState;
|
||||||
|
scope?: ExecutionScopeBoard | null;
|
||||||
dataSourceMode: "api" | "demo" | "empty";
|
dataSourceMode: "api" | "demo" | "empty";
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onPrimaryAction: () => void;
|
onPrimaryAction: () => void;
|
||||||
|
|
@ -444,6 +516,7 @@ function ExecutionRunHeader({
|
||||||
{run.task?.title || "等待正式开发任务"} · 当前阶段:<span className="font-semibold text-slate-900">{run.currentStage.label}</span>
|
{run.task?.title || "等待正式开发任务"} · 当前阶段:<span className="font-semibold text-slate-900">{run.currentStage.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||||
|
{scope ? <StatusChip label={`范围 ${scope.totals.requirements} 需求 / ${scope.totals.tasks} 任务`} tone="brand" /> : null}
|
||||||
<StatusChip label={run.result.statusLabel} tone={run.result.tone} />
|
<StatusChip label={run.result.statusLabel} tone={run.result.tone} />
|
||||||
<StatusChip label={run.mode?.label || "执行模式未同步"} tone={modeTone(run.mode?.status)} />
|
<StatusChip label={run.mode?.label || "执行模式未同步"} tone={modeTone(run.mode?.status)} />
|
||||||
<StatusChip label={run.formalTask ? "正式 Task" : "非正式任务"} tone={run.formalTask ? "success" : "warning"} />
|
<StatusChip label={run.formalTask ? "正式 Task" : "非正式任务"} tone={run.formalTask ? "success" : "warning"} />
|
||||||
|
|
@ -505,13 +578,13 @@ function ExecutionRunStageRail({ run }: { run: ExecutionRunState }) {
|
||||||
</div>
|
</div>
|
||||||
<StatusChip label={run.currentStage.label} tone={stageToneToChip(run.currentStage.tone)} />
|
<StatusChip label={run.currentStage.label} tone={stageToneToChip(run.currentStage.tone)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid gap-3 lg:grid-cols-6">
|
<div className="mt-4 flex gap-3 overflow-x-auto pb-1">
|
||||||
{run.stages.map((stage, index) => (
|
{run.stages.map((stage, index) => (
|
||||||
<div key={stage.key} className="relative">
|
<div key={stage.key} className="relative min-w-[150px] flex-1">
|
||||||
{index < run.stages.length - 1 ? <div className="absolute left-[calc(50%+18px)] top-5 hidden h-px w-[calc(100%-36px)] border-t border-dashed border-[#cfd8e7] lg:block" /> : null}
|
{index < run.stages.length - 1 ? <div className="absolute left-[calc(50%+18px)] top-5 hidden h-px w-[calc(100%-36px)] border-t border-dashed border-[#cfd8e7] min-[1700px]:block" /> : null}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative rounded-lg border px-3 py-3",
|
"relative h-full rounded-lg border px-3 py-3",
|
||||||
stage.tone === "done" && "border-emerald-200 bg-emerald-50",
|
stage.tone === "done" && "border-emerald-200 bg-emerald-50",
|
||||||
stage.tone === "current" && "border-[#aac4ff] bg-[#f3f7ff] shadow-[0_8px_20px_rgba(36,99,235,0.08)]",
|
stage.tone === "current" && "border-[#aac4ff] bg-[#f3f7ff] shadow-[0_8px_20px_rgba(36,99,235,0.08)]",
|
||||||
stage.tone === "waiting" && "border-[#e8eef8] bg-white",
|
stage.tone === "waiting" && "border-[#e8eef8] bg-white",
|
||||||
|
|
@ -542,6 +615,129 @@ function ExecutionRunStageRail({ run }: { run: ExecutionRunState }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExecutionScopeOverview({ scope }: { scope: ExecutionScopeBoard }) {
|
||||||
|
return (
|
||||||
|
<section className="rounded-xl border border-[#dbe6ff] bg-white px-5 py-4 shadow-[0_1px_2px_rgba(15,23,42,0.03)]">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-base font-semibold text-slate-900">本轮开发范围</div>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-slate-500">
|
||||||
|
先看所有已确认需求和正式任务,再进入单个 Run;没有关联的任务会进入未归属队列,不反推关系。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StatusChip label={scope.nextQueueAction} tone={scope.totals.blocked > 0 ? "warning" : "brand"} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-6">
|
||||||
|
<ExecutionMetric label="已确认需求" value={`${scope.totals.requirements}`} />
|
||||||
|
<ExecutionMetric label="正式任务" value={`${scope.totals.tasks}`} />
|
||||||
|
<ExecutionMetric label="执行中" value={`${scope.totals.running}`} tone={scope.totals.running ? "brand" : "default"} />
|
||||||
|
<ExecutionMetric label="待生成包" value={`${scope.totals.waitingPackage}`} tone={scope.totals.waitingPackage ? "warning" : "default"} />
|
||||||
|
<ExecutionMetric label="待测试/审查" value={`${scope.totals.waitingTest + scope.totals.waitingReview}`} tone={scope.totals.waitingTest + scope.totals.waitingReview ? "warning" : "default"} />
|
||||||
|
<ExecutionMetric label="阻塞" value={`${scope.totals.blocked}`} tone={scope.totals.blocked ? "warning" : "default"} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExecutionScopeQueuePanel({
|
||||||
|
scope,
|
||||||
|
selectedTaskId,
|
||||||
|
onSelectTask,
|
||||||
|
}: {
|
||||||
|
scope: ExecutionScopeBoard;
|
||||||
|
selectedTaskId: string | null;
|
||||||
|
onSelectTask: (taskId: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<aside className="rounded-xl border border-[#e2e9f5] bg-white shadow-[0_1px_2px_rgba(15,23,42,0.03)]">
|
||||||
|
<div className="border-b border-[#edf2f8] px-5 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PackageCheck className="h-4 w-4 text-[#2463EB]" />
|
||||||
|
<h2 className="text-base font-semibold text-slate-900">需求执行队列</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-500">按 Requirement 分组查看本轮开发范围,点击任务切换 Run 详情。</p>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[980px] space-y-4 overflow-auto p-5">
|
||||||
|
{scope.groups.length ? (
|
||||||
|
scope.groups.map((group) => (
|
||||||
|
<section key={group.id} className="rounded-lg border border-[#e8eef8] bg-[#fbfdff] p-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-semibold text-slate-900">{group.title}</div>
|
||||||
|
<p className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">{group.summary || "该需求暂无摘要。"}</p>
|
||||||
|
</div>
|
||||||
|
<StatusChip label={group.coverageLabel} tone={scopeToneToChip(group.tone)} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{group.tasks.length ? (
|
||||||
|
group.tasks.map((task) => (
|
||||||
|
<ExecutionScopeTaskButton key={task.id} task={task} active={task.id === selectedTaskId} onClick={() => onSelectTask(task.id)} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-dashed border-[#d8e3f4] bg-white px-3 py-3 text-xs leading-5 text-slate-500">
|
||||||
|
尚未形成开发任务;需要先从 Project Memory 拆出正式 Task。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<ExecutionEmptyState title="暂无已确认需求" summary="当前项目记忆里没有可进入开发范围的已确认 Requirement。" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{scope.unassignedTasks.length ? (
|
||||||
|
<section className="rounded-lg border border-amber-200 bg-amber-50 p-3">
|
||||||
|
<div className="text-sm font-semibold text-amber-900">未归属任务</div>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-amber-700">这些任务缺少 Requirement 关联,先展示但不反推需求关系。</p>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{scope.unassignedTasks.map((task) => (
|
||||||
|
<ExecutionScopeTaskButton key={task.id} task={task} active={task.id === selectedTaskId} onClick={() => onSelectTask(task.id)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExecutionScopeTaskButton({
|
||||||
|
task,
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
task: ExecutionScopeTaskItem;
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-lg border px-3 py-3 text-left transition",
|
||||||
|
active ? "border-[#9ebcff] bg-white shadow-[0_8px_18px_rgba(36,99,235,0.08)]" : "border-[#e8eef8] bg-white hover:border-[#bfd4ff]",
|
||||||
|
task.tone === "blocked" && "border-rose-200 bg-rose-50",
|
||||||
|
task.tone === "waiting" && !active && "bg-white",
|
||||||
|
task.tone === "running" && !active && "bg-[#fbfdff]"
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-semibold text-slate-900">{task.title}</div>
|
||||||
|
<div className="mt-1 text-xs leading-5 text-slate-500">{task.runStage} · {task.nextAction}</div>
|
||||||
|
</div>
|
||||||
|
<StatusChip label={taskScopeToneLabel(task.tone)} tone={scopeToneToChip(task.tone)} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-medium text-slate-500">
|
||||||
|
<span>{taskStatusLabel(task.status)}</span>
|
||||||
|
<span>{task.evidenceCount} 证据</span>
|
||||||
|
{task.requirementTitle ? <span className="truncate">{task.requirementTitle}</span> : null}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ExecutionRunSourcePanel({ run, summary }: { run: ExecutionRunState; summary: PMFlowExecutionSummary }) {
|
function ExecutionRunSourcePanel({ run, summary }: { run: ExecutionRunState; summary: PMFlowExecutionSummary }) {
|
||||||
const prompt = run.prompt;
|
const prompt = run.prompt;
|
||||||
return (
|
return (
|
||||||
|
|
@ -766,12 +962,14 @@ function ExecutionResultCompactPanel({ run }: { run: ExecutionRunState }) {
|
||||||
function ExecutionRunDecisionPanel({
|
function ExecutionRunDecisionPanel({
|
||||||
run,
|
run,
|
||||||
summary,
|
summary,
|
||||||
|
scope,
|
||||||
capabilities,
|
capabilities,
|
||||||
onPrimaryAction,
|
onPrimaryAction,
|
||||||
onOpenWriteback,
|
onOpenWriteback,
|
||||||
}: {
|
}: {
|
||||||
run: ExecutionRunState;
|
run: ExecutionRunState;
|
||||||
summary: PMFlowExecutionSummary;
|
summary: PMFlowExecutionSummary;
|
||||||
|
scope?: ExecutionScopeBoard | null;
|
||||||
capabilities?: PMFlowExecutionCapabilitySnapshot | null;
|
capabilities?: PMFlowExecutionCapabilitySnapshot | null;
|
||||||
onPrimaryAction: () => void;
|
onPrimaryAction: () => void;
|
||||||
onOpenWriteback: () => void;
|
onOpenWriteback: () => void;
|
||||||
|
|
@ -834,6 +1032,17 @@ function ExecutionRunDecisionPanel({
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{scope ? (
|
||||||
|
<section className="rounded-lg border border-[#e8eef8] bg-white p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-900">范围缺口</h3>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
|
<ExecutionMetric label="待生成包" value={`${scope.totals.waitingPackage}`} tone={scope.totals.waitingPackage ? "warning" : "default"} />
|
||||||
|
<ExecutionMetric label="阻塞任务" value={`${scope.totals.blocked}`} tone={scope.totals.blocked ? "warning" : "default"} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-xs leading-5 text-slate-500">{scope.nextQueueAction}</p>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<section className="rounded-lg border border-[#e8eef8] bg-white p-4">
|
<section className="rounded-lg border border-[#e8eef8] bg-white p-4">
|
||||||
<h3 className="text-sm font-semibold text-slate-900">下一步建议</h3>
|
<h3 className="text-sm font-semibold text-slate-900">下一步建议</h3>
|
||||||
<ol className="mt-3 space-y-2">
|
<ol className="mt-3 space-y-2">
|
||||||
|
|
@ -1824,6 +2033,240 @@ function StatusChip({ label, tone }: { label: string; tone: "brand" | "success"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildExecutionScopeBoard(
|
||||||
|
state: ProjectTruthState,
|
||||||
|
summary: PMFlowExecutionSummary,
|
||||||
|
capabilities?: PMFlowExecutionCapabilitySnapshot | null,
|
||||||
|
requestedTaskId?: string | null
|
||||||
|
): ExecutionScopeBoard {
|
||||||
|
const taskItems = buildExecutionScopeTasks(state, summary, capabilities);
|
||||||
|
const requirementMap = new Map(state.requirements.map((item) => [item.id, item]));
|
||||||
|
const groupedTaskIds = new Set<string>();
|
||||||
|
const groups = state.requirements
|
||||||
|
.filter((requirement) => ["confirmed", "in_progress", "done", "released"].includes(requirement.status))
|
||||||
|
.map((requirement) => {
|
||||||
|
const tasks = taskItems.filter((task) => task.requirementId === requirement.id || requirement.linkedTaskIds.includes(task.id));
|
||||||
|
tasks.forEach((task) => groupedTaskIds.add(task.id));
|
||||||
|
return buildRequirementScopeGroup(requirement, tasks);
|
||||||
|
});
|
||||||
|
const unassignedTasks = taskItems.filter((task) => !groupedTaskIds.has(task.id) && (!task.requirementId || !requirementMap.has(task.requirementId)));
|
||||||
|
const allTasks = [...taskItems];
|
||||||
|
const selectedTaskId =
|
||||||
|
(requestedTaskId && allTasks.some((task) => task.id === requestedTaskId) ? requestedTaskId : null) ||
|
||||||
|
(summary.execution.taskId && allTasks.some((task) => task.id === summary.execution.taskId) ? summary.execution.taskId : null) ||
|
||||||
|
firstActionableScopeTask(allTasks)?.id ||
|
||||||
|
allTasks[0]?.id ||
|
||||||
|
null;
|
||||||
|
const totals = {
|
||||||
|
requirements: groups.length,
|
||||||
|
tasks: allTasks.length,
|
||||||
|
running: allTasks.filter((task) => task.tone === "running" || task.tone === "ready").length,
|
||||||
|
waitingPackage: allTasks.filter((task) => task.nextAction.includes("上下文包") || task.nextAction.includes("交接")).length,
|
||||||
|
waitingTest: allTasks.filter((task) => task.nextAction.includes("测试")).length,
|
||||||
|
waitingReview: allTasks.filter((task) => task.nextAction.includes("审查") || task.nextAction.includes("验收")).length,
|
||||||
|
blocked: allTasks.filter((task) => task.tone === "blocked").length,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
unassignedTasks,
|
||||||
|
allTasks,
|
||||||
|
selectedTaskId,
|
||||||
|
totals,
|
||||||
|
nextQueueAction: buildScopeNextAction(totals),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExecutionScopeTasks(
|
||||||
|
state: ProjectTruthState,
|
||||||
|
summary: PMFlowExecutionSummary,
|
||||||
|
capabilities?: PMFlowExecutionCapabilitySnapshot | null
|
||||||
|
): ExecutionScopeTaskItem[] {
|
||||||
|
const projectTaskMap = new Map(state.tasks.map((task) => [task.id, task]));
|
||||||
|
const devTaskMap = new Map(summary.activeDevTasks.map((task) => [task.id, task]));
|
||||||
|
const taskIds = new Set<string>([
|
||||||
|
...state.tasks.filter((task) => task.stage === "development" || ["in_progress", "blocked", "done"].includes(task.status)).map((task) => task.id),
|
||||||
|
...summary.activeDevTasks.map((task) => task.id),
|
||||||
|
]);
|
||||||
|
if (summary.execution.taskId) taskIds.add(summary.execution.taskId);
|
||||||
|
|
||||||
|
return Array.from(taskIds)
|
||||||
|
.map((taskId) => {
|
||||||
|
const projectTask = projectTaskMap.get(taskId) || null;
|
||||||
|
const devTask = devTaskMap.get(taskId) || null;
|
||||||
|
const task = devTask || (projectTask ? adaptProjectTaskToDevTask(projectTask, state, summary.repoBindings[0] || null) : null);
|
||||||
|
if (!task) return null;
|
||||||
|
return buildScopeTaskItem(task, projectTask, devTask, state, summary, capabilities);
|
||||||
|
})
|
||||||
|
.filter(Boolean) as ExecutionScopeTaskItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScopeTaskItem(
|
||||||
|
task: PMFlowDevTask,
|
||||||
|
projectTask: PMFlowProjectTask | null,
|
||||||
|
devTask: PMFlowDevTask | null,
|
||||||
|
state: ProjectTruthState,
|
||||||
|
summary: PMFlowExecutionSummary,
|
||||||
|
capabilities?: PMFlowExecutionCapabilitySnapshot | null
|
||||||
|
): ExecutionScopeTaskItem {
|
||||||
|
const requirement =
|
||||||
|
(projectTask?.requirementId ? state.requirements.find((item) => item.id === projectTask.requirementId) : null) ||
|
||||||
|
state.requirements.find((item) => item.linkedTaskIds.includes(task.id)) ||
|
||||||
|
null;
|
||||||
|
const scopedSummary = createScopedExecutionSummary(summary, state, task.id);
|
||||||
|
const run = buildExecutionRunState(scopedSummary, capabilities);
|
||||||
|
const evidence = scopedSummary.recentProgressEvents.length + (scopedSummary.promptPackage ? 1 : 0);
|
||||||
|
const tone = scopeToneFromRun(run);
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
summary: task.description || projectTask?.summary || "任务暂无描述。",
|
||||||
|
status: task.status || projectTask?.status || "unknown",
|
||||||
|
stage: projectTask?.stage || "development",
|
||||||
|
requirementId: requirement?.id || projectTask?.requirementId || task.charter_id || null,
|
||||||
|
requirementTitle: requirement?.title || task.charter_title || null,
|
||||||
|
devTask,
|
||||||
|
projectTask,
|
||||||
|
runStage: run.currentStage.label,
|
||||||
|
nextAction: run.primaryAction.label,
|
||||||
|
evidenceCount: evidence,
|
||||||
|
tone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequirementScopeGroup(
|
||||||
|
requirement: PMFlowRequirement,
|
||||||
|
tasks: ExecutionScopeTaskItem[]
|
||||||
|
): ExecutionScopeRequirementGroup {
|
||||||
|
const blocked = tasks.some((task) => task.tone === "blocked");
|
||||||
|
const running = tasks.some((task) => task.tone === "running" || task.tone === "ready");
|
||||||
|
const waiting = tasks.some((task) => task.tone === "waiting");
|
||||||
|
const tone: ExecutionScopeTone = blocked ? "blocked" : running ? "running" : waiting ? "waiting" : tasks.length ? "ready" : "empty";
|
||||||
|
return {
|
||||||
|
id: requirement.id,
|
||||||
|
title: requirement.title,
|
||||||
|
summary: requirement.summary,
|
||||||
|
status: requirement.status,
|
||||||
|
tasks,
|
||||||
|
coverageLabel: tasks.length ? `${tasks.length} 任务` : "未拆任务",
|
||||||
|
tone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstActionableScopeTask(tasks: ExecutionScopeTaskItem[]) {
|
||||||
|
return (
|
||||||
|
tasks.find((task) => task.tone === "blocked") ||
|
||||||
|
tasks.find((task) => task.tone === "waiting") ||
|
||||||
|
tasks.find((task) => task.tone === "running") ||
|
||||||
|
tasks.find((task) => task.tone === "ready") ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScopeNextAction(totals: ExecutionScopeBoard["totals"]) {
|
||||||
|
if (totals.blocked > 0) return "先处理阻塞";
|
||||||
|
if (totals.waitingPackage > 0) return "生成待执行上下文包";
|
||||||
|
if (totals.waitingTest > 0) return "查看待测试任务";
|
||||||
|
if (totals.waitingReview > 0) return "进入待审查/验收";
|
||||||
|
if (totals.tasks > 0) return "继续跟踪执行证据";
|
||||||
|
return "等待正式开发任务";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScopedExecutionSummary(
|
||||||
|
summary: PMFlowExecutionSummary,
|
||||||
|
state: ProjectTruthState,
|
||||||
|
taskId: string
|
||||||
|
): PMFlowExecutionSummary {
|
||||||
|
const existingDevTask = summary.activeDevTasks.find((task) => task.id === taskId) || null;
|
||||||
|
const projectTask = state.tasks.find((task) => task.id === taskId) || null;
|
||||||
|
const selectedTask = existingDevTask || (projectTask ? adaptProjectTaskToDevTask(projectTask, state, summary.repoBindings[0] || null) : null);
|
||||||
|
const mergedTasks = selectedTask
|
||||||
|
? [selectedTask, ...summary.activeDevTasks.filter((task) => task.id !== selectedTask.id)]
|
||||||
|
: summary.activeDevTasks;
|
||||||
|
const scopedEvents = summary.recentProgressEvents.filter((event) => {
|
||||||
|
const eventTaskId = event.task?.id || null;
|
||||||
|
if (!eventTaskId) return taskId === summary.execution.taskId;
|
||||||
|
return eventTaskId === taskId;
|
||||||
|
});
|
||||||
|
const promptMatches = summary.promptPackage && (!summary.promptPackage.task_id || summary.promptPackage.task_id === taskId);
|
||||||
|
const selectedIsCurrent = taskId === summary.execution.taskId;
|
||||||
|
const scopedExecution = selectedIsCurrent
|
||||||
|
? summary.execution
|
||||||
|
: {
|
||||||
|
...summary.execution,
|
||||||
|
taskId,
|
||||||
|
status: scopedEvents.length ? summary.execution.status : "queued" as const,
|
||||||
|
prStatus: selectedTask?.code_link?.pr_number ? `PR #${selectedTask.code_link.pr_number}` : "pending",
|
||||||
|
testSummary: "pending",
|
||||||
|
writebackStatus: "pending",
|
||||||
|
commitLog: selectedTask?.code_link?.last_commit_sha ? [selectedTask.code_link.last_commit_sha] : [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
execution: scopedExecution,
|
||||||
|
promptPackage: promptMatches ? summary.promptPackage : null,
|
||||||
|
activeDevTasks: mergedTasks,
|
||||||
|
recentProgressEvents: scopedEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptProjectTaskToDevTask(
|
||||||
|
task: PMFlowProjectTask,
|
||||||
|
state: ProjectTruthState,
|
||||||
|
repo: PMFlowRepoBinding | null
|
||||||
|
): PMFlowDevTask {
|
||||||
|
const requirement = task.requirementId ? state.requirements.find((item) => item.id === task.requirementId) : null;
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
description: task.summary,
|
||||||
|
status: task.status,
|
||||||
|
priority: null,
|
||||||
|
project_id: state.project.id,
|
||||||
|
charter_id: requirement?.id || task.requirementId || null,
|
||||||
|
charter_title: requirement?.title || null,
|
||||||
|
assignee: task.assignee ? { id: task.assignee, name: task.assignee, role: null } : null,
|
||||||
|
prompt_package_count: 0,
|
||||||
|
model_binding: null,
|
||||||
|
code_link: repo
|
||||||
|
? {
|
||||||
|
id: `${task.id}-repo-link`,
|
||||||
|
status: "pending",
|
||||||
|
branch_name: null,
|
||||||
|
pr_number: null,
|
||||||
|
last_commit_sha: null,
|
||||||
|
repo,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeToneFromRun(run: ExecutionRunState): ExecutionScopeTone {
|
||||||
|
if (run.hasFailure || run.currentStage.tone === "blocked") return "blocked";
|
||||||
|
if (run.hasAccepted || run.hasMerged) return "ready";
|
||||||
|
if (run.hasHandoff || run.hasCommit || run.hasPr || run.hasTest) return "running";
|
||||||
|
if (!run.prompt || !run.repo || !run.formalTask) return "waiting";
|
||||||
|
return "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeToneToChip(tone: ExecutionScopeTone): "brand" | "success" | "warning" | "risk" | "muted" {
|
||||||
|
if (tone === "ready") return "success";
|
||||||
|
if (tone === "running") return "brand";
|
||||||
|
if (tone === "waiting") return "warning";
|
||||||
|
if (tone === "blocked") return "risk";
|
||||||
|
return "muted";
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskScopeToneLabel(tone: ExecutionScopeTone) {
|
||||||
|
const map: Record<ExecutionScopeTone, string> = {
|
||||||
|
ready: "就绪",
|
||||||
|
running: "推进中",
|
||||||
|
waiting: "待补齐",
|
||||||
|
blocked: "阻塞",
|
||||||
|
empty: "空",
|
||||||
|
};
|
||||||
|
return map[tone];
|
||||||
|
}
|
||||||
|
|
||||||
function buildExecutionRunState(
|
function buildExecutionRunState(
|
||||||
summary: PMFlowExecutionSummary,
|
summary: PMFlowExecutionSummary,
|
||||||
capabilities?: PMFlowExecutionCapabilitySnapshot | null
|
capabilities?: PMFlowExecutionCapabilitySnapshot | null
|
||||||
|
|
|
||||||
|
|
@ -706,6 +706,40 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders execution run workspace with staged flow and a single primary action", async () => {
|
it("renders execution run workspace with staged flow and a single primary action", async () => {
|
||||||
|
const scopedState = realState();
|
||||||
|
scopedState.requirements = [
|
||||||
|
{
|
||||||
|
id: "req-031",
|
||||||
|
title: "REQ-031 执行范围",
|
||||||
|
summary: "本轮需要一起推进的已确认开发需求。",
|
||||||
|
stage: "development",
|
||||||
|
status: "confirmed",
|
||||||
|
linkedTaskIds: ["task-99", "task-extra"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
scopedState.tasks = [
|
||||||
|
{
|
||||||
|
id: "task-99",
|
||||||
|
title: "真实任务",
|
||||||
|
summary: "真实任务描述",
|
||||||
|
stage: "development",
|
||||||
|
status: "in_progress",
|
||||||
|
assignee: "Owner",
|
||||||
|
acceptanceCriteria: ["已有执行证据"],
|
||||||
|
requirementId: "req-031",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "task-extra",
|
||||||
|
title: "补充企业权限说明",
|
||||||
|
summary: "同一需求下的另一个开发任务。",
|
||||||
|
stage: "development",
|
||||||
|
status: "confirmed",
|
||||||
|
assignee: "Owner",
|
||||||
|
acceptanceCriteria: ["权限边界明确"],
|
||||||
|
requirementId: "req-031",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
getTruthStateMock.mockResolvedValueOnce(scopedState);
|
||||||
getExecutionCapabilitiesMock.mockResolvedValueOnce({
|
getExecutionCapabilitiesMock.mockResolvedValueOnce({
|
||||||
projectId: "123",
|
projectId: "123",
|
||||||
executionModes: [
|
executionModes: [
|
||||||
|
|
@ -766,6 +800,10 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
|
|
||||||
expect(await screen.findByText("开发执行流转")).toBeInTheDocument();
|
expect(await screen.findByText("开发执行流转")).toBeInTheDocument();
|
||||||
expect(screen.getByText("开发执行 Run 工作台")).toBeInTheDocument();
|
expect(screen.getByText("开发执行 Run 工作台")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("本轮开发范围")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("需求执行队列")).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("REQ-031 执行范围").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByRole("button", { name: /补充企业权限说明/ })).toBeInTheDocument();
|
||||||
expect(screen.getByText("执行源包")).toBeInTheDocument();
|
expect(screen.getByText("执行源包")).toBeInTheDocument();
|
||||||
expect(screen.getByText("AI 副驾")).toBeInTheDocument();
|
expect(screen.getByText("AI 副驾")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("AI 直接执行").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("AI 直接执行").length).toBeGreaterThan(0);
|
||||||
|
|
@ -777,6 +815,9 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
fireEvent.click(screen.getAllByRole("button", { name: /回写开发 \/ 测试证据/ })[0]);
|
fireEvent.click(screen.getAllByRole("button", { name: /回写开发 \/ 测试证据/ })[0]);
|
||||||
expect(screen.getByText("回写执行证据")).toBeInTheDocument();
|
expect(screen.getByText("回写执行证据")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("运行端已领取,等待 branch、commit、PR 或测试结果回流。").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("运行端已领取,等待 branch、commit、PR 或测试结果回流。").length).toBeGreaterThan(0);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /补充企业权限说明/ }));
|
||||||
|
expect(screen.getAllByText("补充企业权限说明").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByRole("button", { name: /生成上下文包/ }).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders Change Review from real projection and does not show demo fallback rows", async () => {
|
it("renders Change Review from real projection and does not show demo fallback rows", async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue