diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index f8fc1a82..e063985b 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -554,15 +554,15 @@ commit gate 判定: 当前最建议下一步执行: -**Phase C2:ChangeSet commit 前端接入。** +**Phase D:Execution Bridge 真实数据接入。** 原因: - A3 已完成真实只读投影,Phase B 已完成真实 Project Launch 创建链。 - Phase C1 已把项目内消息、结构化候选、审查动作和证据链落到真实表。 - C2-contract 已锁定 commit API、原子提交规则、响应合同和失败语义。 -- C2-backend 已按合同实现 atomic commit route/service/schema/tests。 -- 当前缺口是前端真实模式仍未调用 `POST /pmflow-os/change-sets/{change_set_id}/commit`,需要替换 demo-only `promoteAcceptedToGraph`。 +- C2-backend 已按合同实现 atomic commit route/service/schema/tests,C2-frontend 已把真实模式写入接到 commit API。 +- 当前缺口是执行桥页面仍偏 demo 编排,下一步应把 `ExecutionBridgePage` 切到 `execution-summary` 与 dev-sync 真实数据。 ### 7.1 Phase A 建议拆成三个小任务 @@ -612,6 +612,46 @@ commit gate 判定: ## 8. 进度台账 +### 2026-04-26 / Phase C2 Frontend - ChangeSet Commit Wiring / done + +完成内容: + +- 在 `frontend/src/api/pmflowOs.ts` 增加 `commitPMFlowChangeSet`,调用 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`。 +- 扩展 PMFlow OS 前端类型,允许后端投影返回 `ChangeSet.status=committed/partially_committed` 与 `ChangeItem.status=committed/skipped`。 +- 将 `PMFlowOsProvider.promoteAcceptedToGraph` 的真实模式接到后端 commit API,commit 后 refetch `truth-state`;demo 模式继续使用本地 mutation。 +- 将 `ChangeReviewPage` 的真实模式主按钮从“下一阶段开放”改为“确认写入项目记忆”,不再用前端本地状态冒充正式写入。 +- 补充 API 模式 store 测试,确认 `promoteAcceptedToGraph("11")` 会调用 `commitPMFlowChangeSet("11", { mode: "atomic", ... })`,且不会本地写入 `requirements`。 + +涉及文件: + +- `frontend/src/api/pmflowOs.ts` +- `frontend/src/features/pmflow-os/types.ts` +- `frontend/src/features/pmflow-os/store.tsx` +- `frontend/src/features/pmflow-os/store.api.test.tsx` +- `frontend/src/features/pmflow-os/pages/ChangeReviewPage.tsx` +- `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` +- `frontend/src/features/pmflow-os/components/chrome.tsx` +- `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `npm run test -- src/features/pmflow-os/store.api.test.tsx` 通过:`3 passed`。 +- `npm run test -- src/features/pmflow-os` 通过:`4 passed`。 +- `npm run typecheck` 通过。 +- `npm run build` 通过;Vite 仍输出既有 manual chunk circular warning,未阻断构建。 + +未完成或风险: + +- 前端真实 commit 依赖后端已完成审查状态;若仍存在 `pending_review` item,后端会按合同显式返回 `changeset_not_reviewed`。 +- Release / ModelDecision / MetricSignal commit 仍按 C2 合同暂缓或 fail-closed。 +- 当前为本地开发工作区验证,未执行部署环境热重启。 + +下一步建议: + +- 执行 Phase D:把 `ExecutionBridgePage` 切到 `GET /pmflow-os/projects/{project_id}/execution-summary` 与 dev-sync 真实数据,继续保留 demo fallback。 + ### 2026-04-26 / Phase C2 Backend - ChangeSet Atomic Commit / done 完成内容: @@ -642,14 +682,13 @@ commit gate 判定: 未完成或风险: -- 前端真实模式尚未调用 commit API,`promoteAcceptedToGraph` 在 API 模式仍不会写入项目记忆。 - C2 V1 暂不支持 `ModelDecision / MetricSignal` commit;按合同必须 fail-closed。 - 本地仍有未提交删除痕迹,未纳入本次提交。 - 当前为本地开发工作区验证,未执行部署环境热重启。 下一步建议: -- 执行 `C2-frontend`:在 `frontend/src/api/pmflowOs.ts` 增加 commit wrapper,并让 `frontend/src/features/pmflow-os/store.tsx` 真实模式调用 commit 后 refetch truth-state。 +- 已执行 `C2-frontend`;下一步进入 Phase D Execution Bridge 真实数据接入。 ### 2026-04-26 / G0 Freeze + C2 Contract / done diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index 9648d39f..b1d3f35d 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -96,7 +96,7 @@ ## 3. 前端参考 - PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 - 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`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;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` diff --git a/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md b/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md index 3a7006de..b0affc7c 100644 --- a/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md +++ b/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md @@ -40,7 +40,7 @@ pmflow_approvals pmflow_evidence_links ``` -`/commit` 后端代码已实现;前端真实模式接入完成前,仍不得用本地 `promoteAcceptedToGraph` 冒充正式 Project Memory 写入。C2 后续前端实现必须遵守本文 4.6 和第 5 节合同。 +`/commit` 后端代码与前端真实模式接入已实现;真实模式下 `promoteAcceptedToGraph` 必须调用后端 `POST /pmflow-os/change-sets/{change_set_id}/commit`,不得用本地 mutation 冒充正式 Project Memory 写入。 ## 2. 对象映射表 @@ -440,7 +440,7 @@ PMFlow OS 不重造开发执行系统。执行桥复用: 2. Phase B:已把 `/project-launch` 的草案确认接入真实项目创建和初始 Project Memory 写入。 3. Phase C1:已实现 ChangeSet/ChangeItem/Approval/EvidenceLink 表与 `accept / edit / defer` 审查 API。 4. Phase C2 backend:已实现 ChangeSet atomic commit,把 `accepted / edited` 写入 `truth_nodes / tasks / semantic_facts` 并补齐高风险 approval gate。 -5. Phase C2 frontend:下一步把真实模式下的 `promoteAcceptedToGraph` 接到 `POST /pmflow-os/change-sets/{change_set_id}/commit`。 +5. Phase C2 frontend:已把真实模式下的 `promoteAcceptedToGraph` 接到 `POST /pmflow-os/change-sets/{change_set_id}/commit`,commit 后 refetch truth-state。 6. Phase D:把 `ExecutionBridgePage` 切到 `execution-summary` 和 dev-sync 真实数据。 7. Phase E:把 Release/Growth 投影接入 release validation、feedback、revenue、ROI、compliance。 diff --git a/frontend/src/api/pmflowOs.ts b/frontend/src/api/pmflowOs.ts index 87695cf5..c55165e4 100644 --- a/frontend/src/api/pmflowOs.ts +++ b/frontend/src/api/pmflowOs.ts @@ -54,6 +54,14 @@ export interface PMFlowChangeSetDetail { approvals: Array>; } +export interface PMFlowChangeSetCommitResult { + changeSet: Record; + committedItems: Array>; + skippedItems: Array>; + failedItems: Array>; + projectTruthState?: ProjectTruthState | null; +} + export async function getPMFlowTruthState(projectId: string): Promise { const { data } = await api.get>(`/pmflow-os/projects/${projectId}/truth-state`); return unwrap(data); @@ -127,3 +135,14 @@ export async function deferPMFlowChangeItem(changeItemId: string, reason?: strin ); return unwrap(data); } + +export async function commitPMFlowChangeSet( + changeSetId: string, + payload: { item_ids?: string[]; mode?: "atomic"; note?: string } = {} +): Promise { + const { data } = await api.post>( + `/pmflow-os/change-sets/${changeSetId}/commit`, + { mode: "atomic", ...payload } + ); + return unwrap(data); +} diff --git a/frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx b/frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx index 4e824cae..92ea827d 100644 --- a/frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx +++ b/frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx @@ -30,7 +30,7 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { InlineNotice } from "@/features/pmflow-os/components/FeedbackPrimitives"; import { PMFlowMark } from "@/features/pmflow-os/components/chrome"; -import type { ChangeItem, Evidence, PMFlowObjectType, PMFlowRiskLevel } from "@/features/pmflow-os/types"; +import type { ChangeItem, Evidence, PMFlowChangeStatus, PMFlowObjectType, PMFlowRiskLevel } from "@/features/pmflow-os/types"; import { cn } from "@/lib/utils"; export function StatusLabel({ @@ -116,8 +116,19 @@ export function ObjectChangeCard({ onDefer?: (id: string) => void; }) { const Icon = objectIcon(item.objectType); - const statusTone = item.status === "accepted" ? "success" : item.status === "edited" ? "brand" : item.status === "deferred" ? "muted" : "brand"; - const statusText = item.status === "accepted" ? "已接受" : item.status === "edited" ? "已修改" : item.status === "deferred" ? "暂不落库" : "待你确认"; + const statusTone = item.status === "accepted" || item.status === "committed" ? "success" : item.status === "edited" ? "brand" : item.status === "deferred" || item.status === "skipped" ? "muted" : "brand"; + const statusText = + item.status === "accepted" + ? "已接受" + : item.status === "edited" + ? "已修改" + : item.status === "deferred" + ? "暂不落库" + : item.status === "committed" + ? "已入库" + : item.status === "skipped" + ? "已跳过" + : "待你确认"; return (
@@ -195,7 +206,7 @@ type ReviewDisplayRow = { confidence: number; risk: PMFlowRiskLevel; relation: string; - status?: "pending_review" | "accepted" | "edited" | "deferred"; + status?: PMFlowChangeStatus; expanded?: boolean; details?: { description: string; @@ -213,6 +224,8 @@ const reviewStatusText: Record, string> accepted: "已接受", edited: "已修改", deferred: "暂不落库", + committed: "已入库", + skipped: "已跳过", }; export function ReviewExtractionHero({ activeChangeSet }: { activeChangeSet?: { createdAt?: string } | null }) { diff --git a/frontend/src/features/pmflow-os/components/chrome.tsx b/frontend/src/features/pmflow-os/components/chrome.tsx index b06a90e3..015630ac 100644 --- a/frontend/src/features/pmflow-os/components/chrome.tsx +++ b/frontend/src/features/pmflow-os/components/chrome.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { PMFlowExecutionStatus, + PMFlowChangeStatus, PMFlowObjectStatus, PMFlowReleaseStatus, PMFlowRiskLevel, @@ -26,7 +27,7 @@ export function statusTone( } export function formatStatusLabel( - status: PMFlowObjectStatus | PMFlowExecutionStatus | PMFlowReleaseStatus | PMFlowRiskLevel | "pending_review" | "accepted" | "edited" | "deferred" + status: PMFlowObjectStatus | PMFlowExecutionStatus | PMFlowReleaseStatus | PMFlowRiskLevel | PMFlowChangeStatus ) { const labelMap: Record = { low: "低风险", @@ -42,6 +43,8 @@ export function formatStatusLabel( accepted: "已接受", edited: "已修改", deferred: "暂缓", + committed: "已入库", + skipped: "已跳过", queued: "待推送", pushed: "已推送", coding: "开发中", diff --git a/frontend/src/features/pmflow-os/pages/ChangeReviewPage.tsx b/frontend/src/features/pmflow-os/pages/ChangeReviewPage.tsx index 3307450a..a426772f 100644 --- a/frontend/src/features/pmflow-os/pages/ChangeReviewPage.tsx +++ b/frontend/src/features/pmflow-os/pages/ChangeReviewPage.tsx @@ -17,10 +17,14 @@ export default function ChangeReviewPage() { const selectedItems = items.filter((item) => item.status === "pending_review").slice(0, 2); const writeback = () => { - if (dataSourceMode !== "demo") return; - selectedItems.forEach((item) => acceptChange(item.id)); + if (!activeChangeSet) return; + if (dataSourceMode === "demo") { + selectedItems.forEach((item) => acceptChange(item.id)); + } promoteAcceptedToGraph(activeChangeSet?.id); - navigate("/memory-writeback-result"); + if (dataSourceMode === "demo") { + navigate("/memory-writeback-result"); + } }; return ( @@ -53,8 +57,8 @@ export default function ChangeReviewPage() {
diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index b6299dd9..e01b741f 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { acceptPMFlowChangeItem, + commitPMFlowChangeSet, createPMFlowThreadMessage, deferPMFlowChangeItem, editPMFlowChangeItem, @@ -20,6 +21,7 @@ vi.mock("@/api/pmflowOs", () => ({ createPMFlowExtraction: vi.fn(), getPMFlowChangeSet: vi.fn(), acceptPMFlowChangeItem: vi.fn(), + commitPMFlowChangeSet: vi.fn(), editPMFlowChangeItem: vi.fn(), deferPMFlowChangeItem: vi.fn(), })); @@ -27,6 +29,7 @@ vi.mock("@/api/pmflowOs", () => ({ const getTruthStateMock = vi.mocked(getPMFlowTruthState); const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); +const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet); const editItemMock = vi.mocked(editPMFlowChangeItem); const deferItemMock = vi.mocked(deferPMFlowChangeItem); @@ -104,6 +107,12 @@ describe("PMFlowOsProvider real change review actions", () => { metadata: {}, }); acceptItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); + commitChangeSetMock.mockResolvedValue({ + changeSet: { id: "11", status: "committed" }, + committedItems: [{ id: "21", status: "committed" }], + skippedItems: [], + failedItems: [], + }); editItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); deferItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); }); @@ -129,6 +138,14 @@ describe("PMFlowOsProvider real change review actions", () => { act(() => result.current.deferChange("21")); await waitFor(() => expect(deferItemMock).toHaveBeenCalledWith("21", "用户暂缓")); + act(() => result.current.promoteAcceptedToGraph("11")); + await waitFor(() => + expect(commitChangeSetMock).toHaveBeenCalledWith( + "11", + expect.objectContaining({ mode: "atomic", note: "用户确认写入项目记忆" }) + ) + ); + expect(result.current.state.requirements).toEqual([]); }); diff --git a/frontend/src/features/pmflow-os/store.tsx b/frontend/src/features/pmflow-os/store.tsx index 1b2e7281..93a1e827 100644 --- a/frontend/src/features/pmflow-os/store.tsx +++ b/frontend/src/features/pmflow-os/store.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode import { useQuery, useQueryClient } from "@tanstack/react-query"; import { acceptPMFlowChangeItem, + commitPMFlowChangeSet, createPMFlowExtraction, createPMFlowThreadMessage, deferPMFlowChangeItem, @@ -665,8 +666,23 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { } mutateDemoOnly((current) => deferChangeMutation(current, changeId)); }, - promoteAcceptedToGraph: (changeSetId) => - mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId)), + promoteAcceptedToGraph: (changeSetId) => { + if (dataSource.mode === "api") { + const targetChangeSetId = changeSetId || state.activeChangeSetId; + if (!targetChangeSetId) { + setReadError("缺少可提交的 ChangeSet,无法写入项目记忆。"); + return; + } + runApiAction(() => + commitPMFlowChangeSet(targetChangeSetId, { + mode: "atomic", + note: "用户确认写入项目记忆", + }) + ); + return; + } + mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId)); + }, pushTaskToExecution: (taskId) => mutateDemoOnly((current) => pushTaskToExecutionMutation(current, taskId)), advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), diff --git a/frontend/src/features/pmflow-os/types.ts b/frontend/src/features/pmflow-os/types.ts index ea7d25ae..82ed0fe0 100644 --- a/frontend/src/features/pmflow-os/types.ts +++ b/frontend/src/features/pmflow-os/types.ts @@ -8,7 +8,7 @@ export type PMFlowStage = | "growth"; export type PMFlowObjectStatus = "draft" | "confirmed" | "in_progress" | "blocked" | "done" | "released"; -export type PMFlowChangeStatus = "pending_review" | "accepted" | "edited" | "deferred"; +export type PMFlowChangeStatus = "pending_review" | "accepted" | "edited" | "deferred" | "committed" | "skipped"; export type PMFlowExecutionStatus = "queued" | "pushed" | "coding" | "pr_open" | "testing" | "synced" | "failed"; export type PMFlowReleaseStatus = "check_pending" | "ready" | "shipping" | "live" | "watch"; export type ProjectLoopStage = @@ -84,7 +84,7 @@ export interface ChangeSet { summary: string; headline: string; createdAt: string; - status: "open" | "reviewed"; + status: "open" | "reviewed" | "committed" | "partially_committed"; itemIds: string[]; }