feat(pmflow-os): gate releases on execution evidence

This commit is contained in:
Albert 2026-04-26 11:24:55 +08:00
parent 8f1edbf423
commit e90adbc2eb
10 changed files with 677 additions and 10 deletions

View File

@ -22,11 +22,13 @@ PMFlowExecutionProgressEventType = Literal[
"test_passed",
"test_failed",
"checks_passed",
"failed",
"blocked",
"merged",
"accepted",
"manual_progress",
]
PMFlowReleaseGateStatus = Literal["ready", "blocked", "needs_confirmation", "empty"]
ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"]
PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"]
PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"]
@ -291,6 +293,7 @@ class ProjectTruthStateProjection(BaseModel):
class ExecutionSummaryProjection(BaseModel):
projectId: str
execution: PMFlowExecutionContext
releaseGate: dict[str, Any] = Field(default_factory=dict)
devSync: dict[str, Any] = Field(default_factory=dict)
promptPackage: Optional[dict[str, Any]] = None
repoBindings: list[dict[str, Any]] = Field(default_factory=list)

View File

@ -203,6 +203,7 @@ class PMFlowOSProjectionService:
testSummary=test_summary,
writebackStatus=writeback,
),
releaseGate=self._build_release_gate(events=events, deliveries=deliveries),
devSync=dev_sync.get("summary") or {},
promptPackage=None if latest_package is None else self._serialize_prompt_package(latest_package),
repoBindings=repos,
@ -1212,6 +1213,188 @@ class PMFlowOSProjectionService:
return "pending"
return str(event.get("workflow_state") or event.get("summary") or event.get("event_type") or "progress event recorded")
def _build_release_gate(self, *, events: list[dict[str, Any]], deliveries: list[dict[str, Any]]) -> dict[str, Any]:
evidence_events = [event for event in events if self._is_release_evidence_event(event)]
if not evidence_events:
return {
"status": "empty",
"title": "缺少执行证据",
"summary": "还没有 branch、commit、PR、test 或 failed 回写,发布门禁不能生成判断。",
"blockingReasons": [],
"nextActions": ["先从 Execution Bridge 回写 branch / commit / PR / test 事件。"],
"checks": [
{"key": "execution_evidence", "label": "执行证据", "status": "missing", "detail": "progress_events_empty"},
],
"evidence": [],
"updatedAt": None,
}
failed_events = [event for event in evidence_events if self._is_failed_evidence_event(event)]
if failed_events:
return {
"status": "blocked",
"title": "发布阻塞",
"summary": "执行回写中存在失败或阻塞事件,发布门禁已关闭。",
"blockingReasons": [self._event_reason(event) for event in failed_events[:5]],
"nextActions": [
"先修复失败事件对应的任务或测试。",
"修复后重新回写 test_passed 或 checks_passed 证据。",
],
"checks": self._release_gate_checks(evidence_events),
"evidence": self._release_gate_evidence(evidence_events),
"updatedAt": str(evidence_events[0].get("occurred_at") or "") or None,
}
has_test_passed = any(self._event_type(event) in {"test_passed", "checks_passed"} for event in evidence_events)
has_pr = any(self._event_type(event) in {"pr_opened", "pr_synced", "merged", "accepted"} or event.get("pr_number") for event in evidence_events)
has_synced = any(self._event_type(event) in {"merged", "accepted"} or "synced" in self._event_text(event) for event in evidence_events)
failed_deliveries = [item for item in deliveries if str(item.get("status") or "").lower() == "failed"]
if failed_deliveries:
return {
"status": "blocked",
"title": "发布阻塞",
"summary": "GitHub delivery 存在失败记录,发布前需要先确认回写链路。",
"blockingReasons": [
str(item.get("failure_reason") or item.get("delivery_id") or "GitHub delivery failed")
for item in failed_deliveries[:5]
],
"nextActions": ["检查 GitHub webhook delivery 失败原因。", "恢复回写链路后重新生成发布判断。"],
"checks": self._release_gate_checks(evidence_events),
"evidence": self._release_gate_evidence(evidence_events),
"updatedAt": str(evidence_events[0].get("occurred_at") or "") or None,
}
if has_test_passed and has_synced:
return {
"status": "ready",
"title": "可发布",
"summary": "已有 PR/合并与通过测试证据,发布门禁给出可发布判断。",
"blockingReasons": [],
"nextActions": ["由负责人进行最终人工确认;PMFlow 不自动执行发布命令。"],
"checks": self._release_gate_checks(evidence_events),
"evidence": self._release_gate_evidence(evidence_events),
"updatedAt": str(evidence_events[0].get("occurred_at") or "") or None,
}
if has_test_passed:
return {
"status": "needs_confirmation",
"title": "发布候选,需人工确认",
"summary": "测试已通过,但仍需要负责人确认 PR 状态、发布范围和回滚预案。",
"blockingReasons": [],
"nextActions": [
"确认 PR 是否已审查或合并。",
"确认本次发布范围与回滚方案后再进入发布。",
],
"checks": self._release_gate_checks(evidence_events),
"evidence": self._release_gate_evidence(evidence_events),
"updatedAt": str(evidence_events[0].get("occurred_at") or "") or None,
}
return {
"status": "needs_confirmation",
"title": "需要人工确认",
"summary": "已有执行回写,但缺少通过测试证据,不能直接给出可发布判断。",
"blockingReasons": ["缺少 test_passed 或 checks_passed 证据。"],
"nextActions": ["补充测试回写;若确需跳过测试,需要负责人显式确认。"],
"checks": self._release_gate_checks(evidence_events),
"evidence": self._release_gate_evidence(evidence_events),
"updatedAt": str(evidence_events[0].get("occurred_at") or "") or None,
}
def _release_gate_checks(self, events: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
{
"key": "branch",
"label": "分支证据",
"status": "passed" if any(event.get("branch_name") or self._event_type(event) == "branch_created" for event in events) else "missing",
"detail": self._first_event_detail(events, {"branch_created", "commit", "pr_opened", "pr_synced"}),
},
{
"key": "commit",
"label": "提交证据",
"status": "passed" if any(event.get("commit_sha") or self._event_type(event) == "commit" for event in events) else "missing",
"detail": self._first_event_detail(events, {"commit"}),
},
{
"key": "pr",
"label": "PR 证据",
"status": "passed" if any(event.get("pr_number") or self._event_type(event) in {"pr_opened", "pr_synced", "merged", "accepted"} for event in events) else "missing",
"detail": self._first_event_detail(events, {"pr_opened", "pr_synced", "merged", "accepted"}),
},
{
"key": "test",
"label": "测试证据",
"status": "failed" if any(self._event_type(event) == "test_failed" for event in events) else ("passed" if any(self._event_type(event) in {"test_passed", "checks_passed"} for event in events) else "missing"),
"detail": self._first_event_detail(events, {"test_passed", "checks_passed", "test_failed"}),
},
{
"key": "failure",
"label": "失败事件",
"status": "failed" if any(self._is_failed_evidence_event(event) for event in events) else "passed",
"detail": self._first_event_detail(events, {"failed", "blocked", "test_failed"}) or "未发现失败回写",
},
]
def _release_gate_evidence(self, events: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
{
"id": str(event.get("id") or ""),
"type": self._event_type(event),
"summary": self._event_reason(event),
"taskTitle": (event.get("task") or {}).get("title") if isinstance(event.get("task"), dict) else None,
"repo": (event.get("repo") or {}).get("full_name") if isinstance(event.get("repo"), dict) else None,
"branchName": event.get("branch_name"),
"commitSha": event.get("commit_sha"),
"prNumber": event.get("pr_number"),
"occurredAt": event.get("occurred_at"),
}
for event in events[:12]
]
@staticmethod
def _event_type(event: dict[str, Any]) -> str:
return str(event.get("event_type") or "").strip().lower()
def _event_text(self, event: dict[str, Any]) -> str:
return " ".join(
[
self._event_type(event),
str(event.get("summary") or ""),
str(event.get("workflow_state") or ""),
str(event.get("derived_task_status") or ""),
]
).lower()
def _is_release_evidence_event(self, event: dict[str, Any]) -> bool:
event_type = self._event_type(event)
return event_type in {
"branch_created",
"commit",
"pr_opened",
"pr_synced",
"test_passed",
"test_failed",
"checks_passed",
"failed",
"blocked",
"merged",
"accepted",
} or bool(event.get("branch_name") or event.get("commit_sha") or event.get("pr_number"))
def _is_failed_evidence_event(self, event: dict[str, Any]) -> bool:
text = self._event_text(event)
return any(token in text for token in ("test_failed", "failed", "blocked", "失败", "阻塞", "error"))
def _event_reason(self, event: dict[str, Any]) -> str:
return str(event.get("summary") or event.get("workflow_state") or event.get("event_type") or "执行证据").strip()
def _first_event_detail(self, events: list[dict[str, Any]], event_types: set[str]) -> str:
for event in events:
if self._event_type(event) in event_types:
return self._event_reason(event)
return ""
def _serialize_prompt_package(self, package: PromptPackage) -> dict[str, Any]:
return {
"id": str(package.id),
@ -1452,7 +1635,7 @@ class PMFlowOSProjectionService:
return "testing"
if value in {"merged", "accepted"}:
return "synced"
if value == "blocked":
if value in {"blocked", "failed"}:
return "failed"
return "manual_progress"

View File

@ -280,6 +280,8 @@ async def test_execution_summary_empty_state_is_pending_with_warnings(db_env) ->
assert payload["execution"]["status"] == "queued"
assert payload["execution"]["prompt"] == ""
assert payload["execution"]["testSummary"] == "pending"
assert payload["releaseGate"]["status"] == "empty"
assert "缺少执行证据" in payload["releaseGate"]["title"]
assert "prompt_package_empty" in " ".join(payload["warnings"])
assert payload["activeDevTasks"] == []
@ -689,6 +691,8 @@ async def test_execution_progress_writeback_records_branch_pr_commit_and_test(db
test_payload = test_response.json()["data"]
assert test_payload["execution"]["status"] == "failed"
assert test_payload["execution"]["testSummary"] == "单测失败:ExecutionBridgePage 回写表单缺少断言"
assert test_payload["releaseGate"]["status"] == "blocked"
assert "单测失败" in " ".join(test_payload["releaseGate"]["blockingReasons"])
assert test_payload["recentProgressEvents"][0]["event_type"] == "test_failed"
assert test_payload["recentProgressEvents"][0]["repo"]["full_name"] == "pmflow/writeback"
@ -700,6 +704,146 @@ async def test_execution_progress_writeback_records_branch_pr_commit_and_test(db
assert [event.event_type for event in events] == ["commit", "pr_opened", "test_failed"]
@pytest.mark.asyncio
async def test_release_gate_marks_test_passed_execution_as_candidate(db_env) -> None:
session = db_env
user, project = await _seed_user_project(session)
charter = TruthNode(
tenant_id=1,
node_type="charter",
project_id=project.id,
created_by=user.id,
title="发布候选宪章",
content={"business_goal": "测试通过后进入发布候选"},
version=1,
status="aligned",
)
session.add(charter)
await session.flush()
task = Task(
tenant_id=1,
truth_node_id=charter.id,
project_id=project.id,
assignee_id=user.id,
ai_recommended_assignees=None,
ai_breakdown_payload={"acceptance_criteria": ["测试通过时生成发布候选"]},
manual_overrides=None,
title="发布候选任务",
description="提供 commit / PR / test evidence",
required_skill_tag_ids=[],
status="confirmed",
priority="high",
)
repo = RepoBinding(
tenant_id=1,
project_id=project.id,
provider="github",
owner="pmflow",
repo_name="release-candidate",
default_branch="main",
status="active",
)
session.add_all([task, repo])
await session.commit()
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
await client.post(
f"/pmflow-os/projects/{project.id}/execution-progress-events",
json={
"task_id": str(task.id),
"repo_binding_id": str(repo.id),
"event_type": "commit",
"source": "codex",
"branch_name": "codex/release-candidate",
"commit_sha": "abc123def456",
},
)
await client.post(
f"/pmflow-os/projects/{project.id}/execution-progress-events",
json={
"task_id": str(task.id),
"repo_binding_id": str(repo.id),
"event_type": "pr_opened",
"source": "github_agent",
"branch_name": "codex/release-candidate",
"pr_number": 99,
},
)
response = await client.post(
f"/pmflow-os/projects/{project.id}/execution-progress-events",
json={
"task_id": str(task.id),
"repo_binding_id": str(repo.id),
"event_type": "test_passed",
"source": "local_bridge",
"branch_name": "codex/release-candidate",
"test_summary": "单测与构建全部通过",
},
)
assert response.status_code == 200
payload = response.json()["data"]
assert payload["releaseGate"]["status"] == "needs_confirmation"
assert "发布候选" in payload["releaseGate"]["title"]
assert payload["releaseGate"]["blockingReasons"] == []
assert any(item["key"] == "test" and item["status"] == "passed" for item in payload["releaseGate"]["checks"])
assert any(item["type"] == "test_passed" for item in payload["releaseGate"]["evidence"])
@pytest.mark.asyncio
async def test_release_gate_blocks_explicit_failed_event(db_env) -> None:
session = db_env
user, project = await _seed_user_project(session)
charter = TruthNode(
tenant_id=1,
node_type="charter",
project_id=project.id,
created_by=user.id,
title="失败回写宪章",
content={"business_goal": "failed event 必须阻塞发布"},
version=1,
status="aligned",
)
session.add(charter)
await session.flush()
task = Task(
tenant_id=1,
truth_node_id=charter.id,
project_id=project.id,
assignee_id=user.id,
ai_recommended_assignees=None,
ai_breakdown_payload=None,
manual_overrides=None,
title="失败回写任务",
description="显式 failed event",
required_skill_tag_ids=[],
status="confirmed",
priority="high",
)
session.add(task)
await session.commit()
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.post(
f"/pmflow-os/projects/{project.id}/execution-progress-events",
json={
"task_id": str(task.id),
"event_type": "failed",
"source": "codex",
"summary": "Codex 执行失败:typecheck 未通过",
},
)
assert response.status_code == 200
payload = response.json()["data"]
assert payload["execution"]["status"] == "failed"
assert payload["releaseGate"]["status"] == "blocked"
assert "typecheck 未通过" in " ".join(payload["releaseGate"]["blockingReasons"])
assert payload["releaseGate"]["evidence"][0]["type"] == "failed"
@pytest.mark.asyncio
async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None:
session = db_env

View File

@ -86,7 +86,7 @@
3. `pmflow-os` 本地 store 还没有接后端读写,当前仍是 mock truth store。
4. `ChangeReview` 的接受、修改、暂缓还没有写入正式对象、审批记录和证据链。
5. `ExecutionBridge` 页面还没有消费 dev-sync / GitHub / PromptPackage 真实接口。
6. `ReleaseGrowth` 还没有把 release validation、feedback、revenue、ROI、compliance 真实数据串成发布后反馈循环。
6. `ReleaseGrowth` 已接入 execution evidence gate,但 release validation、feedback、revenue、ROI、compliance 还没有串成完整发布后反馈循环。
7. `TeamLayer` 仍是产品表达层,缺少真实成员角色、Role Inbox、Decision Desk、Team Feed 的数据模型和审查流程。
## 2. 下一步推进方向
@ -270,7 +270,7 @@
范围:
- `ReleaseGrowthPage` 消费 release validation。
- `ReleaseGrowthPage` 已消费 execution evidence gate;后续继续接 release validation。
- 支持 feedback ingest。
- 支持 revenue event 或基础 ROI snapshot。
- 支持 compliance check 的轻量门禁。
@ -807,6 +807,43 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
## 8. 进度台账
### 2026-04-26 / release-growth-p1-execution-evidence-gate / implemented
完成内容:
- 在 `ExecutionSummaryProjection` 中新增 `releaseGate` 投影,从 `ProgressEvent` 的 branch、commit、PR、test、failed / blocked 证据推导发布判断。
- `releaseGate.status` 覆盖 `ready / blocked / needs_confirmation / empty`:测试通过可进入发布候选,failed / test_failed / blocked 事件 fail-closed 阻塞发布,无 execution evidence 时返回明确 empty/warning。
- 扩展 execution progress event 合同,允许显式 `failed` event 回写。
- `ReleaseGrowthPage` 真实模式改为只消费 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作和 warning;不再展示静态“已发布/增长指标”mock 结果。
- 静态高保真发布/增长原型只保留在显式 `?demo=1` / `dataSource=demo` 模式。
- 本轮不执行发布命令、不做真实部署、不新增 migration。
涉及文件:
- `backend/app/schemas/pmflow_os.py`
- `backend/app/services/pmflow_os_projection_service.py`
- `backend/tests/test_pmflow_os_projection_api.py`
- `frontend/src/api/pmflowOs.ts`
- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx`
- `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx`
- `frontend/src/features/pmflow-os/store.api.test.tsx`
- `docs/architecture/CODE_REFERENCE_INDEX.md`
- `docs/architecture/ARCHITECTURE_INVENTORY.md`
- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md`
验证:
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile backend/app/api/pmflow_os_api/__init__.py backend/app/services/pmflow_os_projection_service.py backend/app/schemas/pmflow_os.py` 通过。
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m pytest backend/tests/test_pmflow_os_projection_api.py -q` 通过:`14 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`4 passed`。
- `cd frontend && npm run typecheck` 通过。
- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning,但构建成功。
未完成或风险:
- ReleaseGrowth P1 只生成发布判断和下一步动作,不触发真实部署、不写 release validation run。
- 增长反馈、收入、ROI 和 compliance 的真实发布后反馈循环仍待后续 Packet 接入。
### 2026-04-26 / big-module-integration-p2 / done
完成内容:

View File

@ -197,9 +197,9 @@
- `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、执行中 / AI 正在推进、暂存想法、草案箱、最近项目摘要;项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入或 execution event 写入。
- `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/`,失败时回落浏览器本机暂存。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。
- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;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;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 与 execution progress writeback 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。
- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因和下一步动作,不执行发布命令、不展示 mock 发布结果;静态发布/增长原型只保留在显式 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;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
- PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。
- `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。
- `frontend/vite.config.ts` 现补了固定 vendor chunk 分组(`framework / state-query / ui-radix / icons / charts / markdown`)与业务 chunk 分组(`ai-steward / sstc / governance`),目标是让登录页与基础导航不再背 AI Steward、SSTC、治理页和图表代码。

View File

@ -89,7 +89,7 @@
- SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee`
- SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}`
- SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases`
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit 与 Execution Handoff:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff 与 Release Evidence Gate:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 已在 `execution-summary.releaseGate` 中从 branch / commit / PR / test_passed / checks_passed / failed / blocked 等 `ProgressEvent` 推导 `ready / blocked / needs_confirmation / empty`,只生成发布判断、阻塞原因和下一步动作,不执行发布命令
- Project Launch 真实创建链:参考 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与模型 `backend/app/models/pmflow_launch.py`;接口为 `POST /pmflow-launch/drafts` 与 `POST /pmflow-launch/drafts/{draft_id}/confirm`,draft 只写 `pmflow_launch_drafts`,confirm 在同一事务内创建 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`
- PMFlow Change Review 模型与 migration:参考 `backend/app/models/pmflow_change_review.py` 与 `backend/alembic/versions/20260426_add_pmflow_change_review_tables.py`;只包含 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`,本阶段不得新增或依赖 `/commit`
@ -97,7 +97,7 @@
- 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 或 execution event;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;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 或 execution event;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`
@ -107,8 +107,8 @@
- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 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 后端同步合同:参考 `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 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`,不得假装云端账号保存
- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=<id>` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success`
- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`
- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=<id>` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success`
- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`;`ReleaseGrowthPage.tsx` 真实模式只展示 `execution-summary.releaseGate`、execution evidence、阻塞原因和下一步动作,缺证据时显示 empty/warning,不展示 mock 发布结果,静态高保真发布/增长数据仅保留在显式 demo 模式
- PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航
- PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx`
- Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts`

View File

@ -13,6 +13,7 @@ function unwrap<T>(raw: ApiEnvelope<T> | T): T {
export interface PMFlowExecutionSummary {
projectId: string;
execution: PMFlowExecutionContext;
releaseGate: PMFlowReleaseGate;
devSync: Record<string, unknown>;
promptPackage?: PMFlowPromptPackage | null;
repoBindings: PMFlowRepoBinding[];
@ -32,11 +33,44 @@ export type PMFlowExecutionProgressEventType =
| "test_passed"
| "test_failed"
| "checks_passed"
| "failed"
| "blocked"
| "merged"
| "accepted"
| "manual_progress";
export type PMFlowReleaseGateStatus = "ready" | "blocked" | "needs_confirmation" | "empty";
export interface PMFlowReleaseGateCheck {
key: string;
label: string;
status: "passed" | "failed" | "missing";
detail?: string | null;
}
export interface PMFlowReleaseGateEvidence {
id: string;
type: string;
summary: string;
taskTitle?: string | null;
repo?: string | null;
branchName?: string | null;
commitSha?: string | null;
prNumber?: number | null;
occurredAt?: string | null;
}
export interface PMFlowReleaseGate {
status: PMFlowReleaseGateStatus;
title: string;
summary: string;
blockingReasons: string[];
nextActions: string[];
checks: PMFlowReleaseGateCheck[];
evidence: PMFlowReleaseGateEvidence[];
updatedAt?: string | null;
}
export interface PMFlowExecutionHandoffRequest {
task_id: string;
repo_binding_id?: string | null;

View File

@ -680,6 +680,28 @@ function createDemoExecutionSummary(state: ReturnType<typeof usePMFlowOsStore>["
return {
projectId: state.project.id,
execution: state.execution,
releaseGate: {
status: state.execution.status === "failed" ? "blocked" : state.execution.status === "synced" ? "needs_confirmation" : "empty",
title: state.execution.status === "failed" ? "发布阻塞" : state.execution.status === "synced" ? "发布候选,需人工确认" : "缺少执行证据",
summary:
state.execution.status === "failed"
? state.execution.testSummary
: state.execution.status === "synced"
? "Demo 执行已回写,真实发布判断只在 API 模式下由 execution evidence 生成。"
: "Demo 状态尚未形成可用于发布判断的执行证据。",
blockingReasons: state.execution.status === "failed" ? [state.execution.testSummary] : [],
nextActions: ["进入发布增长页查看真实 execution evidence gate。"],
checks: [
{
key: "demo_execution",
label: "Demo 执行证据",
status: state.execution.status === "failed" ? "failed" : state.execution.status === "synced" ? "passed" : "missing",
detail: state.execution.writebackStatus,
},
],
evidence: [],
updatedAt: null,
},
devSync: {
repo_binding_count: 1,
active_task_count: task ? 1 : 0,

View File

@ -1,18 +1,27 @@
import type { LucideIcon } from "lucide-react";
import {
ArrowRight,
AlertTriangle,
Bot,
CheckCircle2,
ChevronDown,
CircleCheck,
Cloud,
FlaskConical,
GitBranch,
GitCommit,
GitPullRequest,
Loader2,
MessageSquareText,
RefreshCcw,
ShieldAlert,
TestTube2,
X,
} from "lucide-react";
import type { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import type { PMFlowReleaseGateEvidence } from "@/api/pmflowOs";
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
import { cn } from "@/lib/utils";
type StatusTone = "success" | "warning" | "danger" | "neutral" | "info";
@ -312,7 +321,219 @@ function AiCopilot() {
);
}
type PMFlowOsStoreSnapshot = ReturnType<typeof usePMFlowOsStore>;
function gateTone(status?: string): StatusTone {
if (status === "ready") return "success";
if (status === "blocked") return "danger";
if (status === "needs_confirmation") return "warning";
return "neutral";
}
function gateLabel(status?: string) {
if (status === "ready") return "可发布";
if (status === "blocked") return "阻塞";
if (status === "needs_confirmation") return "需要人工确认";
return "缺少执行证据";
}
function checkTone(status?: string): StatusTone {
if (status === "passed") return "success";
if (status === "failed") return "danger";
return "warning";
}
function evidenceIcon(type: string) {
if (type.includes("branch")) return GitBranch;
if (type.includes("commit")) return GitCommit;
if (type.includes("pr") || type.includes("merged")) return GitPullRequest;
if (type.includes("test") || type.includes("check")) return TestTube2;
if (type.includes("fail") || type.includes("block")) return AlertTriangle;
return CheckCircle2;
}
function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) {
const Icon = evidenceIcon(item.type);
return (
<div className="grid gap-3 rounded-lg border border-[#e6edf7] bg-white p-4 md:grid-cols-[32px_minmax(0,1fr)_auto]">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#eef4ff] text-[#2463EB]">
<Icon className="h-4 w-4" />
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<StatusChip label={item.type} tone={item.type.includes("fail") || item.type.includes("block") ? "danger" : "info"} />
{item.taskTitle ? <span className="text-sm font-semibold text-slate-800">{item.taskTitle}</span> : null}
</div>
<p className="mt-2 text-sm leading-6 text-slate-600">{item.summary}</p>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-400">
{item.repo ? <span>{item.repo}</span> : null}
{item.branchName ? <span>branch {item.branchName}</span> : null}
{item.commitSha ? <span>commit {item.commitSha.slice(0, 12)}</span> : null}
{item.prNumber ? <span>PR #{item.prNumber}</span> : null}
</div>
</div>
<div className="text-xs text-slate-400">{item.occurredAt ? new Date(item.occurredAt).toLocaleString("zh-CN") : "未记录时间"}</div>
</div>
);
}
function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
const {
state,
dataSourceMode,
dataSourceStatus,
dataSourceError,
executionSummary,
executionSummaryStatus,
executionSummaryError,
projectionWarnings,
refetchExecutionSummary,
} = store;
const gate = executionSummary?.releaseGate;
const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading";
const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])];
const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate;
return (
<div className="min-h-full bg-[#f7faff] text-slate-950">
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-5 px-4 py-5 md:px-6 lg:px-8">
<Card className="p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-[#2463EB]">Release Gate</div>
<h1 className="mt-2 text-2xl font-semibold text-slate-950">发布 / 增长判断</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
本页只读取 Execution Bridge 回写的 branch、commit、PR、test、failed 证据生成发布判断;PMFlow 不在这里执行发布命令。
</p>
</div>
<Button variant="outline" className="h-10 rounded-lg border-[#dbe6ff] text-[#2463EB]" onClick={refetchExecutionSummary}>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCcw className="mr-2 h-4 w-4" />}
刷新执行证据
</Button>
</div>
</Card>
{dataSourceError || executionSummaryError ? (
<Card className="border-rose-100 bg-rose-50 p-4">
<div className="flex gap-3 text-sm text-rose-700">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{dataSourceError || executionSummaryError}</span>
</div>
</Card>
) : null}
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
<main className="space-y-5">
<Card className={cn("p-5", gate?.status === "blocked" && "border-rose-200 bg-rose-50", gate?.status === "ready" && "border-emerald-200 bg-emerald-50")}>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<StatusChip label={gateLabel(gate?.status)} tone={gateTone(gate?.status)} />
<h2 className="mt-4 text-2xl font-semibold text-slate-950">{gate?.title || "缺少执行证据"}</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
{gate?.summary || "URL 缺少 projectId 或后端尚未返回 execution-summary,无法生成发布判断。"}
</p>
</div>
<div className="rounded-lg border border-white/70 bg-white/80 px-4 py-3 text-sm">
<div className="text-xs text-slate-400">当前项目</div>
<div className="mt-1 font-semibold text-slate-900">{state.project.name}</div>
<div className="mt-3 text-xs text-slate-400">执行状态</div>
<div className="mt-1 font-semibold text-slate-700">{executionSummary?.execution.status || state.execution.status}</div>
</div>
</div>
</Card>
{emptyMode ? (
<Card className="border-orange-100 bg-orange-50 p-5">
<div className="flex gap-3">
<ShieldAlert className="mt-1 h-5 w-5 shrink-0 text-orange-500" />
<div>
<h2 className="text-lg font-semibold text-slate-950">没有可用于发布判断的执行证据</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
需要先从执行桥回写 branch、commit、PR、test 或 failed 事件。当前页面不会用 mock 发布结果补位。
</p>
</div>
</div>
</Card>
) : null}
<Card className="p-5">
<SectionTitle title="门禁检查" meta={gate?.updatedAt ? `更新于 ${new Date(gate.updatedAt).toLocaleString("zh-CN")}` : undefined} />
<div className="mt-4 grid gap-3 md:grid-cols-2">
{(gate?.checks || []).map((item) => (
<div key={item.key} className="rounded-lg border border-[#e6edf7] bg-white p-4">
<div className="flex items-center justify-between gap-3">
<div className="font-semibold text-slate-800">{item.label}</div>
<StatusChip label={item.status === "passed" ? "通过" : item.status === "failed" ? "失败" : "缺失"} tone={checkTone(item.status)} />
</div>
<div className="mt-2 text-sm leading-6 text-slate-500">{item.detail || "暂无对应证据"}</div>
</div>
))}
{!gate?.checks?.length ? <div className="text-sm text-slate-500">暂无门禁检查项。</div> : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="执行证据" meta={`${gate?.evidence?.length || 0} 条`} />
<div className="mt-4 space-y-3">
{(gate?.evidence || []).map((item) => (
<EvidenceRow key={`${item.id}-${item.type}`} item={item} />
))}
{!gate?.evidence?.length ? <div className="rounded-lg border border-dashed border-[#d6e2f2] p-6 text-sm text-slate-500">暂无真实 execution evidence。</div> : null}
</div>
</Card>
</main>
<aside className="space-y-5">
<Card className="p-5">
<SectionTitle title="阻塞原因" icon={AlertTriangle} />
<div className="mt-4 space-y-2">
{(gate?.blockingReasons || []).map((item) => (
<div key={item} className="rounded-lg border border-rose-100 bg-rose-50 px-3 py-2 text-sm leading-6 text-rose-700">
{item}
</div>
))}
{!gate?.blockingReasons?.length ? <div className="text-sm leading-6 text-slate-500">当前没有阻塞原因。</div> : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="下一步动作" icon={CircleCheck} />
<div className="mt-4 space-y-2">
{(gate?.nextActions || ["先补充执行回写证据。"]).map((item) => (
<div key={item} className="rounded-lg border border-[#e6edf7] bg-[#fbfdff] px-3 py-2 text-sm leading-6 text-slate-600">
{item}
</div>
))}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="运行警告" />
<div className="mt-4 space-y-2">
{warnings.length ? (
warnings.map((item) => (
<div key={item} className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
{item}
</div>
))
) : (
<div className="text-sm leading-6 text-slate-500">暂无 projection warning。</div>
)}
</div>
</Card>
</aside>
</div>
</div>
</div>
);
}
export default function ReleaseGrowthPage() {
const store = usePMFlowOsStore();
if (store.dataSourceMode !== "demo") {
return <RealReleaseGrowthPage store={store} />;
}
return (
<div className="min-h-full bg-[#f7faff] text-slate-950">
<div className="grid gap-5 min-[1440px]:grid-cols-[minmax(0,1fr)_360px]">

View File

@ -14,6 +14,7 @@ import {
editPMFlowChangeItem,
getPMFlowExecutionSummary,
getPMFlowTruthState,
type PMFlowReleaseGate,
} from "@/api/pmflowOs";
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store";
@ -90,6 +91,23 @@ function realState(): ProjectTruthState {
return state;
}
function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty" = "needs_confirmation"): PMFlowReleaseGate {
return {
status,
title: status === "blocked" ? "发布阻塞" : "发布候选,需人工确认",
summary: "来自真实 execution-summary 的发布门禁判断",
blockingReasons: status === "blocked" ? ["测试失败"] : [],
nextActions: ["负责人确认后再发布"],
checks: [
{ key: "test", label: "测试证据", status: status === "blocked" ? "failed" : "passed", detail: "真实测试回写" },
],
evidence: [
{ id: "703", type: "test_passed", summary: "真实测试回写", prNumber: 88 },
],
updatedAt: "2026-04-26T00:00:00",
};
}
function wrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return function Wrapper({ children }: { children: ReactNode }) {
@ -117,6 +135,7 @@ describe("PMFlowOsProvider real change review actions", () => {
testSummary: "pending",
writebackStatus: "coding",
},
releaseGate: releaseGate(),
devSync: { active_task_count: 1 },
promptPackage: {
id: "501",
@ -192,6 +211,7 @@ describe("PMFlowOsProvider real change review actions", () => {
testSummary: "pending",
writebackStatus: "pushed",
},
releaseGate: releaseGate(),
devSync: { active_task_count: 1, recent_event_count: 1 },
promptPackage: {
id: "501",
@ -241,6 +261,7 @@ describe("PMFlowOsProvider real change review actions", () => {
testSummary: "pending",
writebackStatus: "coding",
},
releaseGate: releaseGate(),
devSync: { active_task_count: 1, recent_event_count: 2 },
promptPackage: {
id: "501",
@ -279,6 +300,7 @@ describe("PMFlowOsProvider real change review actions", () => {
testSummary: "pending",
writebackStatus: "in_review",
},
releaseGate: releaseGate(),
devSync: { active_task_count: 1, recent_event_count: 3 },
promptPackage: {
id: "501",
@ -320,6 +342,7 @@ describe("PMFlowOsProvider real change review actions", () => {
await waitFor(() => expect(result.current.executionSummaryStatus).toBe("success"));
expect(getExecutionSummaryMock).toHaveBeenCalledWith("123");
expect(result.current.executionSummary?.promptPackage?.package_text).toBe("真实 prompt package");
expect(result.current.executionSummary?.releaseGate.status).toBe("needs_confirmation");
act(() => result.current.sendMessage("继续补充"));
await waitFor(() => expect(createThreadMessageMock).toHaveBeenCalledWith("123", "7", expect.objectContaining({ content: "继续补充" })));