feat: wire PMFlow changeset commit frontend
This commit is contained in:
parent
b87951935a
commit
1d34e32788
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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。
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ export interface PMFlowChangeSetDetail {
|
|||
approvals: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface PMFlowChangeSetCommitResult {
|
||||
changeSet: Record<string, unknown>;
|
||||
committedItems: Array<Record<string, unknown>>;
|
||||
skippedItems: Array<Record<string, unknown>>;
|
||||
failedItems: Array<Record<string, unknown>>;
|
||||
projectTruthState?: ProjectTruthState | null;
|
||||
}
|
||||
|
||||
export async function getPMFlowTruthState(projectId: string): Promise<ProjectTruthState> {
|
||||
const { data } = await api.get<ApiEnvelope<ProjectTruthState>>(`/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<PMFlowChangeSetCommitResult> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowChangeSetCommitResult>>(
|
||||
`/pmflow-os/change-sets/${changeSetId}/commit`,
|
||||
{ mode: "atomic", ...payload }
|
||||
);
|
||||
return unwrap(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<article className={cn("rounded-xl border bg-white p-4", selected ? "border-[#2463EB] shadow-[0_14px_34px_rgba(36,99,235,0.10)]" : "border-[#e6edf7]")}>
|
||||
<div className="grid gap-4 md:grid-cols-[28px_minmax(0,1fr)] md:items-start">
|
||||
|
|
@ -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<NonNullable<ReviewDisplayRow["status"]>, string>
|
|||
accepted: "已接受",
|
||||
edited: "已修改",
|
||||
deferred: "暂不落库",
|
||||
committed: "已入库",
|
||||
skipped: "已跳过",
|
||||
};
|
||||
|
||||
export function ReviewExtractionHero({ activeChangeSet }: { activeChangeSet?: { createdAt?: string } | null }) {
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
low: "低风险",
|
||||
|
|
@ -42,6 +43,8 @@ export function formatStatusLabel(
|
|||
accepted: "已接受",
|
||||
edited: "已修改",
|
||||
deferred: "暂缓",
|
||||
committed: "已入库",
|
||||
skipped: "已跳过",
|
||||
queued: "待推送",
|
||||
pushed: "已推送",
|
||||
coding: "开发中",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</main>
|
||||
<div className="hidden min-[1440px]:block">
|
||||
<ChangeReviewAiCopilotPanel
|
||||
onPrimary={dataSourceMode === "demo" ? writeback : undefined}
|
||||
primaryLabel={dataSourceMode === "demo" ? "接受高置信项" : "提交入库将在下一阶段开放"}
|
||||
onPrimary={activeChangeSet ? writeback : undefined}
|
||||
primaryLabel={dataSourceMode === "demo" ? "接受高置信项" : "确认写入项目记忆"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue