diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index 2847f32f..929a805e 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -807,6 +807,36 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 ## 8. 进度台账 +### 2026-04-26 / space-p3-validation-run-attention / implemented + +完成内容: + +- 在 `frontend/src/api/space.ts` 将 `releaseAttention` 从单纯 release gate 关注升级为 `truth-state.releases` 发布验证投影 + `execution-summary.releaseGate` 的只读聚合。 +- `/space` 桌面端和移动端“发布关注”区块现在优先显示“发布验证阻塞 / 发布验证待确认”;没有发布验证投影时才回落显示“发布候选 / 发布门禁阻塞”。 +- 发布关注项只展示来源标签、验证/门禁状态、检查项摘要、阻塞原因和更新时间;点击只跳转 `/release-growth?projectId=`。 +- 发布验证缺失时显示“暂无发布验证记录”空态,不使用 mock 或默认成功结果冒充真实验证。 +- 本轮不创建 validation run、不写 ProgressEvent、不写 ReleaseValidationRun、不新增后端接口、不执行发布命令。 + +涉及文件: + +- `frontend/src/api/space.ts` +- `frontend/src/features/personal-space/PersonalSpacePage.tsx` +- `frontend/src/features/personal-space/PersonalSpacePage.test.tsx` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `cd frontend && npm run test -- src/features/personal-space/PersonalSpacePage.test.tsx` 通过:`2 passed`;仍有既有 React Router v7 future flag warning。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning,但构建成功。 + +未完成或风险: + +- Space P3 只消费 release validation 投影,不负责创建/更新 release validation run;真实运行记录仍由 Release/Growth lane 维护。 +- 当前 Space 侧使用 `truth-state.releases` 的状态和 gateItems 表达验证结果;若后续 PMFlow OS 暴露独立 `validationRuns` 合同,需要由 Space lane 再切换到更精确字段。 + ### 2026-04-26 / big-module-integration-p3 / done 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index 4d57d370..79579e4b 100644 --- a/docs/architecture/ARCHITECTURE_INVENTORY.md +++ b/docs/architecture/ARCHITECTURE_INVENTORY.md @@ -194,7 +194,7 @@ - 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`。 - 前端主包治理已切到“同步首屏最小集 + 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=`,只有显式 `?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 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;账号退出登录由 `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 P2 的 `releaseAttention` 只消费 `execution-summary.releaseGate`,把测试失败、PR 未合并、发布门禁阻塞或可发布候选显示到 `/space`,项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,发布关注项进入 `/release-growth?projectId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入或发布执行。 +- `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` 的发布候选或发布门禁阻塞;发布验证缺失时显示明确空态,不虚构验证结果。项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,发布关注项进入 `/release-growth?projectId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入、ReleaseValidationRun 写入或发布执行。 - `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 和失败提醒显示/静默状态,不代表邮件、桌面推送或系统通知中心已接入。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。 - `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 与 execution progress writeback 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index 4c9cca13..92b2ab5f 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -97,14 +97,14 @@ - PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 - PMFlow 大模块并行公共壳:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 的 `4.4.1-4.4.3` 与 `frontend/src/router/index.tsx`、`frontend/src/main.tsx`、`frontend/src/features/pmflow-os/components/MobileBottomNav.tsx`、`frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx`;`/space` 归 Space lane,`/pmflow-os / /command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route,新增或改动必须由 Shell / Integration owner 串行处理 - PMFlow OS 产品合同真源:先读 `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`,再实现 `pmflow-os` 真实后端接入;该文档锁定 `ProjectTruthState / Thread / ChangeSet / ChangeItem / Evidence / Approval / Project Memory / ExecutionContext` 与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界和后续 API 草案 -- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory、ChangeSet 状态或 execution event;Space P2 已从 `PMFlowExecutionSummary.releaseGate` 派生 `releaseAttention`,用于在 `/space` 显示测试失败、PR 未合并、发布门禁阻塞或可发布候选,所有动作只跳转 `/release-growth?projectId=`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory、ChangeSet 状态、ProgressEvent 或 ReleaseValidationRun;Space P3 的 `releaseAttention` 优先从 `truth-state.releases` 的 release validation 投影生成“发布验证阻塞 / 发布验证待确认”,没有发布验证投影时才回落到 `PMFlowExecutionSummary.releaseGate` 的“发布候选 / 发布门禁阻塞”,所有动作只跳转 `/release-growth?projectId=`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` - AI Steward 页面入口:参考 `frontend/src/pages/ai-steward/` - AI Steward 问答浮窗与上下文显性化组件:参考 `frontend/src/components/ai-steward/FloatingQAChat.tsx`、`frontend/src/components/ai-steward/QAConversationStatusBar.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.tsx` - AI Steward QA contract 前端类型(含 `analysis_packet`):参考 `frontend/src/api/aiSteward.ts` - AI Steward handoff 元卡片回归:参考 `frontend/src/components/ai-steward/FloatingQAChat.test.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx` - AI Steward / QA session 前端 contract:参考 `frontend/src/api/aiSteward.ts` - PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/` -- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`,发布关注项进入 `/release-growth?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary.releaseGate`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 +- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`,发布关注项进入 `/release-growth?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate`,发布验证缺失时显示明确空态,不虚构验证结果,不直接写 Project Memory、ChangeSet 状态、execution event 或 ReleaseValidationRun;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 - 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch` - Account Settings / `/me` 前端产品域与 P1/P2 偏好合同:参考 `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`,把通知偏好映射为页面内提醒消费模型,只控制本地 UI 的待确认、执行回写、发布门禁和失败提醒展示,不代表真实邮件/桌面推送已接入 - PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` diff --git a/frontend/src/api/space.ts b/frontend/src/api/space.ts index 982a80ff..7b1f23a8 100644 --- a/frontend/src/api/space.ts +++ b/frontend/src/api/space.ts @@ -4,6 +4,7 @@ import type { ChangeItem, PMFlowExecutionStatus, PMFlowObjectStatus, + Release, ProjectTruthState, } from "@/features/pmflow-os/types"; @@ -51,6 +52,8 @@ export interface SpaceReleaseAttentionItem { id: string; projectId: string; projectTitle: string; + source: "release_gate" | "release_validation"; + sourceLabel: string; title: string; summary: string; gateStatus: "ready" | "blocked" | "needs_confirmation"; @@ -337,7 +340,7 @@ function deriveAIProgress(snapshot: ProjectSnapshot): SpaceAIProgressItem[] { function releaseAttentionTitle(status: SpaceReleaseAttentionItem["gateStatus"]): string { if (status === "blocked") return "发布门禁阻塞"; - if (status === "ready") return "可发布候选待确认"; + if (status === "ready") return "发布候选"; return "发布需要人工确认"; } @@ -372,7 +375,73 @@ function releaseAttentionReasons(execution: PMFlowExecutionSummary): string[] { return [...gate.blockingReasons, ...checkReasons].filter(Boolean).slice(0, 4); } -function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentionItem[] { +function releaseValidationStatus(release: Release): SpaceReleaseAttentionItem["gateStatus"] | null { + const hasFailedGateItem = release.gateItems.some((item) => !item.passed); + if (hasFailedGateItem) return "blocked"; + if (release.status === "ready" || release.status === "shipping" || release.status === "live" || release.status === "watch") { + return "needs_confirmation"; + } + if (release.status === "check_pending" && release.gateItems.length > 0) return "needs_confirmation"; + return null; +} + +function releaseValidationTitle(status: SpaceReleaseAttentionItem["gateStatus"]): string { + if (status === "blocked") return "发布验证阻塞"; + return "发布验证待确认"; +} + +function releaseValidationSummary(release: Release, status: SpaceReleaseAttentionItem["gateStatus"]): string { + if (release.summary) return release.summary; + if (status === "blocked") return "发布验证记录存在未通过检查,需要回到 Release Growth 处理阻塞项。"; + return "发布验证已有记录,需要人工确认后再推进发布判断。"; +} + +function releaseValidationReasons(release: Release): string[] { + return release.gateItems + .filter((item) => !item.passed) + .map((item) => `${item.label}: 未通过`) + .slice(0, 4); +} + +function deriveValidationAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentionItem[] { + const { project, truth } = snapshot; + if (!truth) return []; + const projectId = String(project.id); + const projectTitle = project.title || truth.project.name || `项目 ${projectId}`; + + return truth.releases + .map((release): SpaceReleaseAttentionItem | null => { + const gateStatus = releaseValidationStatus(release); + if (!gateStatus) return null; + const failedItems = releaseValidationReasons(release); + const passedCount = release.gateItems.filter((item) => item.passed).length; + const evidenceLabels = [ + "发布验证", + release.status === "ready" ? "验证通过" : null, + gateStatus === "blocked" ? "检查阻塞" : null, + release.gateItems.length ? `${passedCount}/${release.gateItems.length} 检查项通过` : null, + ].filter((item): item is string => Boolean(item)); + + return { + id: `release-validation-${projectId}-${release.id}`, + projectId, + projectTitle, + source: "release_validation" as const, + sourceLabel: "发布验证记录", + title: releaseValidationTitle(gateStatus), + summary: releaseValidationSummary(release, gateStatus), + gateStatus, + severity: releaseAttentionSeverity(gateStatus), + reasons: failedItems, + evidenceLabels, + updatedAt: project.updated_at, + href: releaseGrowthHref(projectId), + }; + }) + .filter((item): item is SpaceReleaseAttentionItem => Boolean(item)); +} + +function deriveGateAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentionItem[] { const { project, truth, execution } = snapshot; const gate = execution?.releaseGate; if (!gate || gate.status === "empty") return []; @@ -392,6 +461,8 @@ function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentio id: `release-attention-${projectId}`, projectId, projectTitle, + source: "release_gate", + sourceLabel: "发布门禁", title: gate.title || releaseAttentionTitle(gateStatus), summary: gate.summary || fallbackSummary, gateStatus, @@ -404,6 +475,12 @@ function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentio ]; } +function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentionItem[] { + const validationAttention = deriveValidationAttention(snapshot); + if (validationAttention.length) return validationAttention; + return deriveGateAttention(snapshot); +} + function deriveDrafts(snapshot: ProjectSnapshot): SpaceDraftItem[] { const { project, truth } = snapshot; if (!truth) return []; diff --git a/frontend/src/features/personal-space/PersonalSpacePage.test.tsx b/frontend/src/features/personal-space/PersonalSpacePage.test.tsx index b4802446..6f398f0b 100644 --- a/frontend/src/features/personal-space/PersonalSpacePage.test.tsx +++ b/frontend/src/features/personal-space/PersonalSpacePage.test.tsx @@ -41,6 +41,7 @@ describe("PersonalSpacePage", () => { expect(screen.getAllByText("继续推进")).not.toHaveLength(0); expect(screen.getAllByText("待我确认")).not.toHaveLength(0); expect(screen.getAllByText("发布关注")).not.toHaveLength(0); + expect(screen.getAllByText("暂无发布验证记录")).not.toHaveLength(0); expect(screen.getAllByText("AI 正在推进")).not.toHaveLength(0); expect(screen.getAllByText("暂存想法")).not.toHaveLength(0); expect(screen.getAllByText("草案箱")).not.toHaveLength(0); @@ -101,15 +102,47 @@ describe("PersonalSpacePage", () => { id: "release-attention-17", projectId: "17", projectTitle: "真实项目", - title: "发布门禁阻塞", + source: "release_validation", + sourceLabel: "发布验证记录", + title: "发布验证阻塞", summary: "测试失败,PR 未合并,不能发布。", gateStatus: "blocked", severity: "high", reasons: ["测试失败: typecheck 未通过", "PR: 未合并"], - evidenceLabels: ["PR", "测试失败"], + evidenceLabels: ["发布验证", "检查阻塞", "1/2 检查项通过"], updatedAt: "2026-04-26T08:00:00.000Z", href: "/release-growth?projectId=17", }, + { + id: "release-validation-18", + projectId: "18", + projectTitle: "待确认项目", + source: "release_validation", + sourceLabel: "发布验证记录", + title: "发布验证待确认", + summary: "发布验证已通过,需要人工确认。", + gateStatus: "needs_confirmation", + severity: "medium", + reasons: [], + evidenceLabels: ["发布验证", "验证通过"], + updatedAt: "2026-04-26T08:00:00.000Z", + href: "/release-growth?projectId=18", + }, + { + id: "release-gate-19", + projectId: "19", + projectTitle: "候选项目", + source: "release_gate", + sourceLabel: "发布门禁", + title: "发布候选", + summary: "执行证据已进入发布判断。", + gateStatus: "ready", + severity: "low", + reasons: [], + evidenceLabels: ["测试通过", "PR"], + updatedAt: "2026-04-26T08:00:00.000Z", + href: "/release-growth?projectId=19", + }, ], drafts: [ { @@ -156,10 +189,19 @@ describe("PersonalSpacePage", () => { "href", "/execution-bridge?projectId=17" ); - expect(screen.getAllByRole("link", { name: /发布门禁阻塞/ })[0]).toHaveAttribute( + expect(screen.getAllByRole("link", { name: /发布验证阻塞/ })[0]).toHaveAttribute( "href", "/release-growth?projectId=17" ); + expect(screen.getAllByRole("link", { name: /发布验证待确认/ })[0]).toHaveAttribute( + "href", + "/release-growth?projectId=18" + ); + expect(screen.getAllByRole("link", { name: /发布候选/ })[0]).toHaveAttribute( + "href", + "/release-growth?projectId=19" + ); expect(screen.getAllByText("测试失败: typecheck 未通过")).not.toHaveLength(0); + expect(screen.queryByRole("button", { name: /创建发布验证|运行发布验证|重新验证/ })).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/features/personal-space/PersonalSpacePage.tsx b/frontend/src/features/personal-space/PersonalSpacePage.tsx index 68ce1f01..deaea6f4 100644 --- a/frontend/src/features/personal-space/PersonalSpacePage.tsx +++ b/frontend/src/features/personal-space/PersonalSpacePage.tsx @@ -237,7 +237,7 @@ function MobileSpace({ {data.releaseAttention.length ? ( data.releaseAttention.slice(0, 4).map((item) => ) ) : ( - + )} @@ -435,7 +435,7 @@ function ReleaseAttentionPanel({ items }: { items: SpaceReleaseAttentionItem[] }
@@ -443,7 +443,7 @@ function ReleaseAttentionPanel({ items }: { items: SpaceReleaseAttentionItem[] } {items.length ? ( items.slice(0, 5).map((item) => ) ) : ( - + )}
@@ -464,7 +464,7 @@ function ReleaseAttentionRow({ item }: { item: SpaceReleaseAttentionItem }) { {item.summary} - {item.projectTitle} · {item.evidenceLabels.length ? item.evidenceLabels.join(" / ") : "缺少可用证据"} · {formatRelativeTime(item.updatedAt)} + {item.projectTitle} · {item.sourceLabel} · {item.evidenceLabels.length ? item.evidenceLabels.join(" / ") : "缺少可用证据"} · {formatRelativeTime(item.updatedAt)} {item.reasons.length ? {item.reasons[0]} : null} @@ -714,7 +714,7 @@ function MobileReleaseAttention({ item }: { item: SpaceReleaseAttentionItem }) { {item.title} {item.summary} - {item.projectTitle} · {item.evidenceLabels.join(" / ") || "缺少可用证据"} + {item.projectTitle} · {item.sourceLabel} · {item.evidenceLabels.join(" / ") || "缺少可用证据"} {releaseGateLabel(item.gateStatus)}