feat(pmflow-os): record release validation runs
This commit is contained in:
parent
199be13681
commit
875b9292e9
|
|
@ -17,6 +17,7 @@ from ...schemas.pmflow_os import (
|
|||
PMFlowExecutionHandoffRequest,
|
||||
PMFlowExecutionProgressEventRequest,
|
||||
PMFlowExtractionCreateRequest,
|
||||
PMFlowReleaseValidationRunCreateRequest,
|
||||
PMFlowThreadMessageCreateRequest,
|
||||
)
|
||||
from ...services.auth import has_any_role
|
||||
|
|
@ -319,6 +320,37 @@ async def create_pmflow_os_execution_progress_event(
|
|||
return success_response(payload.model_dump(mode="json"))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/release-validation-runs")
|
||||
async def create_pmflow_os_release_validation_run(
|
||||
project_id: int,
|
||||
body: PMFlowReleaseValidationRunCreateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = int(current_user.tenant_id)
|
||||
await _ensure_project_member_or_manager(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
project_id=int(project_id),
|
||||
current_user=current_user,
|
||||
)
|
||||
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
||||
try:
|
||||
payload = await service.create_release_validation_run(
|
||||
project_id=int(project_id),
|
||||
body=body,
|
||||
current_user_id=int(current_user.id),
|
||||
)
|
||||
except ValueError as exc:
|
||||
code = str(exc)
|
||||
if code == "release_validation_evidence_empty":
|
||||
return JSONResponse(status_code=409, content=error_response(409, "release_validation_evidence_empty"))
|
||||
if code == "project_not_found":
|
||||
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
||||
raise
|
||||
return success_response(payload)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume")
|
||||
async def consume_pmflow_os_execution_handoff(
|
||||
project_id: int,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ PMFlowExecutionProgressEventType = Literal[
|
|||
"manual_progress",
|
||||
]
|
||||
PMFlowReleaseGateStatus = Literal["ready", "blocked", "needs_confirmation", "empty"]
|
||||
PMFlowReleaseValidationDecision = Literal["enter_release_candidate", "keep_blocked"]
|
||||
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"]
|
||||
|
|
@ -131,6 +132,11 @@ class PMFlowExecutionProgressEventRequest(BaseModel):
|
|||
occurred_at: Optional[str] = None
|
||||
|
||||
|
||||
class PMFlowReleaseValidationRunCreateRequest(BaseModel):
|
||||
decision: PMFlowReleaseValidationDecision
|
||||
note: Optional[str] = Field(default=None, max_length=4000)
|
||||
|
||||
|
||||
class PMFlowChangeSetDetailResponse(BaseModel):
|
||||
changeSet: dict[str, Any]
|
||||
items: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from ..schemas.pmflow_os import (
|
|||
PMFlowProjectMemory,
|
||||
PMFlowProjectSummary,
|
||||
PMFlowRelease,
|
||||
PMFlowReleaseValidationRunCreateRequest,
|
||||
PMFlowRequirement,
|
||||
PMFlowRisk,
|
||||
PMFlowTask,
|
||||
|
|
@ -191,6 +192,10 @@ class PMFlowOSProjectionService:
|
|||
pr_status = self._project_pr_status(active_tasks=active_tasks, task_id=task_id, events=events)
|
||||
test_summary = self._project_test_summary(events)
|
||||
writeback = self._project_writeback_status(latest_event)
|
||||
latest_release_validation = await self._load_latest_release_validation_run(project_id=project_id)
|
||||
release_gate = self._build_release_gate(events=events, deliveries=deliveries)
|
||||
if latest_release_validation is not None:
|
||||
release_gate["latestValidationRun"] = self._serialize_release_validation_run(latest_release_validation)
|
||||
|
||||
return ExecutionSummaryProjection(
|
||||
projectId=str(project_id),
|
||||
|
|
@ -203,7 +208,7 @@ class PMFlowOSProjectionService:
|
|||
testSummary=test_summary,
|
||||
writebackStatus=writeback,
|
||||
),
|
||||
releaseGate=self._build_release_gate(events=events, deliveries=deliveries),
|
||||
releaseGate=release_gate,
|
||||
devSync=dev_sync.get("summary") or {},
|
||||
promptPackage=None if latest_package is None else self._serialize_prompt_package(latest_package),
|
||||
repoBindings=repos,
|
||||
|
|
@ -267,6 +272,55 @@ class PMFlowOSProjectionService:
|
|||
await self.db.commit()
|
||||
return await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
|
||||
|
||||
async def create_release_validation_run(
|
||||
self,
|
||||
*,
|
||||
project_id: int,
|
||||
body: PMFlowReleaseValidationRunCreateRequest,
|
||||
current_user_id: Optional[int] = None,
|
||||
) -> dict[str, Any]:
|
||||
await self._load_project(project_id=project_id)
|
||||
summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
|
||||
release_gate = dict(summary.releaseGate or {})
|
||||
evidence = list(release_gate.get("evidence") or [])
|
||||
if str(release_gate.get("status") or "empty") == "empty" or not evidence:
|
||||
raise ValueError("release_validation_evidence_empty")
|
||||
|
||||
status = self._release_validation_status(gate_status=str(release_gate.get("status") or ""), decision=body.decision)
|
||||
run = ReleaseValidationRun(
|
||||
tenant_id=self.tenant_id,
|
||||
project_id=int(project_id),
|
||||
status=status,
|
||||
score=self._release_validation_score(status),
|
||||
key_metrics=self._release_validation_metrics(release_gate),
|
||||
risk_items=self._release_validation_risks(release_gate, decision=body.decision),
|
||||
rollback_plan=self._release_validation_rollback_plan(release_gate),
|
||||
validation_summary={
|
||||
"source": "pmflow_os_release_gate",
|
||||
"projectId": str(project_id),
|
||||
"status": status,
|
||||
"releaseGateStatus": release_gate.get("status"),
|
||||
"humanDecision": body.decision,
|
||||
"note": body.note,
|
||||
"createdBy": None if current_user_id is None else str(current_user_id),
|
||||
"createdAt": now_cn_naive().isoformat(),
|
||||
"deploymentExecuted": False,
|
||||
"evidence": evidence,
|
||||
"blockingReasons": list(release_gate.get("blockingReasons") or []),
|
||||
"nextActions": list(release_gate.get("nextActions") or []),
|
||||
"checks": list(release_gate.get("checks") or []),
|
||||
},
|
||||
)
|
||||
self.db.add(run)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(run)
|
||||
updated_summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
|
||||
return {
|
||||
"releaseValidationRun": self._serialize_release_validation_run(run),
|
||||
"executionSummary": updated_summary.model_dump(mode="json"),
|
||||
"warnings": [] if status != "blocked" else list(release_gate.get("blockingReasons") or []),
|
||||
}
|
||||
|
||||
async def consume_execution_handoff(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -427,6 +481,15 @@ class PMFlowOSProjectionService:
|
|||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _load_latest_release_validation_run(self, *, project_id: int) -> Optional[ReleaseValidationRun]:
|
||||
result = await self.db.execute(
|
||||
select(ReleaseValidationRun)
|
||||
.where(and_(ReleaseValidationRun.tenant_id == self.tenant_id, ReleaseValidationRun.project_id == int(project_id)))
|
||||
.order_by(ReleaseValidationRun.updated_at.desc(), ReleaseValidationRun.created_at.desc(), ReleaseValidationRun.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _load_active_prompt_package_for_task(self, *, project_id: int, task_id: int) -> Optional[PromptPackage]:
|
||||
result = await self.db.execute(
|
||||
select(PromptPackage)
|
||||
|
|
@ -1395,6 +1458,79 @@ class PMFlowOSProjectionService:
|
|||
return self._event_reason(event)
|
||||
return ""
|
||||
|
||||
def _release_validation_status(self, *, gate_status: str, decision: str) -> str:
|
||||
if decision == "keep_blocked" or gate_status == "blocked":
|
||||
return "blocked"
|
||||
if gate_status == "ready":
|
||||
return "ready"
|
||||
return "needs_confirmation"
|
||||
|
||||
def _release_validation_score(self, status: str) -> float:
|
||||
if status == "ready":
|
||||
return 1.0
|
||||
if status == "needs_confirmation":
|
||||
return 0.6
|
||||
return 0.0
|
||||
|
||||
def _release_validation_metrics(self, release_gate: dict[str, Any]) -> dict[str, Any]:
|
||||
checks = list(release_gate.get("checks") or [])
|
||||
evidence = list(release_gate.get("evidence") or [])
|
||||
return {
|
||||
"releaseGateStatus": release_gate.get("status"),
|
||||
"evidenceCount": len(evidence),
|
||||
"passedCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "passed"]),
|
||||
"failedCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "failed"]),
|
||||
"missingCheckCount": len([item for item in checks if isinstance(item, dict) and item.get("status") == "missing"]),
|
||||
}
|
||||
|
||||
def _release_validation_risks(self, release_gate: dict[str, Any], *, decision: str) -> list[dict[str, Any]]:
|
||||
risks = [
|
||||
{"label": reason, "status": "blocked", "source": "release_gate_blocking_reason"}
|
||||
for reason in list(release_gate.get("blockingReasons") or [])
|
||||
]
|
||||
for check in list(release_gate.get("checks") or []):
|
||||
if not isinstance(check, dict) or check.get("status") == "passed":
|
||||
continue
|
||||
risks.append(
|
||||
{
|
||||
"label": check.get("label") or check.get("key") or "发布检查项",
|
||||
"status": check.get("status"),
|
||||
"detail": check.get("detail"),
|
||||
"source": "release_gate_check",
|
||||
}
|
||||
)
|
||||
if decision == "keep_blocked" and not risks:
|
||||
risks.append({"label": "负责人选择保持阻塞,等待补充发布前确认。", "status": "blocked", "source": "human_decision"})
|
||||
return risks
|
||||
|
||||
def _release_validation_rollback_plan(self, release_gate: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"deploymentExecuted": False,
|
||||
"requiredBeforeDeploy": [
|
||||
"完成人工发布确认。",
|
||||
"确认回滚方案和发布范围。",
|
||||
],
|
||||
"nextActions": list(release_gate.get("nextActions") or []),
|
||||
}
|
||||
|
||||
def _serialize_release_validation_run(self, run: ReleaseValidationRun) -> dict[str, Any]:
|
||||
summary = self._payload(run.validation_summary)
|
||||
return {
|
||||
"id": str(run.id),
|
||||
"projectId": str(run.project_id),
|
||||
"charterId": None if run.charter_id is None else str(run.charter_id),
|
||||
"releaseNodeId": None if run.release_node_id is None else str(run.release_node_id),
|
||||
"status": run.status,
|
||||
"score": self._jsonable(run.score),
|
||||
"keyMetrics": run.key_metrics or {},
|
||||
"riskItems": run.risk_items or [],
|
||||
"rollbackPlan": run.rollback_plan or {},
|
||||
"validationSummary": summary,
|
||||
"createdBy": summary.get("createdBy"),
|
||||
"createdAt": run.created_at.isoformat() if run.created_at else summary.get("createdAt"),
|
||||
"updatedAt": run.updated_at.isoformat() if run.updated_at else None,
|
||||
}
|
||||
|
||||
def _serialize_prompt_package(self, package: PromptPackage) -> dict[str, Any]:
|
||||
return {
|
||||
"id": str(package.id),
|
||||
|
|
|
|||
|
|
@ -844,6 +844,177 @@ async def test_release_gate_blocks_explicit_failed_event(db_env) -> None:
|
|||
assert payload["releaseGate"]["evidence"][0]["type"] == "failed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_validation_run_creates_ready_record_from_ready_gate(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="Ready 发布验证宪章",
|
||||
content={"business_goal": "ready evidence 可以固化为发布验证记录"},
|
||||
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="Ready 发布任务",
|
||||
description="测试通过并合并",
|
||||
required_skill_tag_ids=[],
|
||||
status="confirmed",
|
||||
priority="high",
|
||||
)
|
||||
repo = RepoBinding(
|
||||
tenant_id=1,
|
||||
project_id=project.id,
|
||||
provider="github",
|
||||
owner="pmflow",
|
||||
repo_name="ready-release",
|
||||
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": "test_passed",
|
||||
"source": "local_bridge",
|
||||
"branch_name": "codex/ready-release",
|
||||
"test_summary": "单测、typecheck、build 全部通过",
|
||||
},
|
||||
)
|
||||
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": "merged",
|
||||
"source": "github_agent",
|
||||
"branch_name": "codex/ready-release",
|
||||
"commit_sha": "ready123456789",
|
||||
"pr_number": 108,
|
||||
"summary": "PR #108 已合并",
|
||||
},
|
||||
)
|
||||
response = await client.post(
|
||||
f"/pmflow-os/projects/{project.id}/release-validation-runs",
|
||||
json={"decision": "enter_release_candidate", "note": "确认进入发布候选"},
|
||||
)
|
||||
summary_response = await client.get(f"/pmflow-os/projects/{project.id}/execution-summary")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()["data"]
|
||||
run_payload = payload["releaseValidationRun"]
|
||||
assert run_payload["status"] == "ready"
|
||||
assert run_payload["projectId"] == str(project.id)
|
||||
assert run_payload["validationSummary"]["releaseGateStatus"] == "ready"
|
||||
assert run_payload["validationSummary"]["humanDecision"] == "enter_release_candidate"
|
||||
assert run_payload["validationSummary"]["deploymentExecuted"] is False
|
||||
assert run_payload["validationSummary"]["createdBy"] == str(user.id)
|
||||
assert run_payload["validationSummary"]["evidence"]
|
||||
assert run_payload["rollbackPlan"]["deploymentExecuted"] is False
|
||||
assert payload["executionSummary"]["releaseGate"]["latestValidationRun"]["id"] == run_payload["id"]
|
||||
assert summary_response.json()["data"]["releaseGate"]["latestValidationRun"]["status"] == "ready"
|
||||
rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].status == "ready"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_validation_run_creates_blocked_record_from_failed_gate(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="Blocked 发布验证宪章",
|
||||
content={"business_goal": "失败 evidence 固化为阻塞记录"},
|
||||
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="Blocked 发布任务",
|
||||
description="测试失败",
|
||||
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:
|
||||
await client.post(
|
||||
f"/pmflow-os/projects/{project.id}/execution-progress-events",
|
||||
json={
|
||||
"task_id": str(task.id),
|
||||
"event_type": "test_failed",
|
||||
"source": "codex",
|
||||
"test_summary": "发布前验证失败:回归测试未通过",
|
||||
},
|
||||
)
|
||||
response = await client.post(
|
||||
f"/pmflow-os/projects/{project.id}/release-validation-runs",
|
||||
json={"decision": "keep_blocked", "note": "阻塞发布"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()["data"]
|
||||
run_payload = payload["releaseValidationRun"]
|
||||
assert run_payload["status"] == "blocked"
|
||||
assert run_payload["validationSummary"]["releaseGateStatus"] == "blocked"
|
||||
assert "回归测试未通过" in " ".join(run_payload["validationSummary"]["blockingReasons"])
|
||||
assert any(item["status"] == "blocked" for item in run_payload["riskItems"])
|
||||
rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].status == "blocked"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_validation_run_rejects_empty_gate(db_env) -> None:
|
||||
session = db_env
|
||||
user, project = await _seed_user_project(session)
|
||||
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}/release-validation-runs",
|
||||
json={"decision": "enter_release_candidate", "note": "证据为空时不能成功"},
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.json()["message"] == "release_validation_evidence_empty"
|
||||
rows = (await session.execute(select(ReleaseValidationRun).where(ReleaseValidationRun.project_id == project.id))).scalars().all()
|
||||
assert rows == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None:
|
||||
session = db_env
|
||||
|
|
|
|||
|
|
@ -807,6 +807,44 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
|||
|
||||
## 8. 进度台账
|
||||
|
||||
### 2026-04-26 / release-validation-p1-real-run-records / implemented
|
||||
|
||||
完成内容:
|
||||
|
||||
- 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用 PMFlow OS 项目成员/OWNER/REVIEWER 权限边界和统一 `{code,message,data}` 返回包络。
|
||||
- 复用现有 `ReleaseValidationRun` 模型,不新增 migration;从当前 `execution-summary.releaseGate` 固化 `status / evidence / blockingReasons / nextActions / createdBy / createdAt / humanDecision`。
|
||||
- ready evidence + `enter_release_candidate` 生成 `ready` run;needs confirmation evidence 生成 `needs_confirmation` run;failed / blocked evidence 或 `keep_blocked` 生成 `blocked` run。
|
||||
- 无 execution evidence 或 `releaseGate.status=empty` 时返回 `release_validation_evidence_empty`,不创建成功 run,也不使用 mock validation。
|
||||
- `execution-summary.releaseGate.latestValidationRun` 返回最新发布验证记录,`ReleaseGrowthPage` 真实模式新增“发布验证记录”区域和“生成发布验证记录 / 记录保持阻塞”动作;页面仍不执行真实部署。
|
||||
- `frontend/src/api/pmflowOs.ts` 改为 PMFlow OS API 层自包含 `/api` fetch 包络解析,避免当前工作树缺失共享 `@/lib/api` 时影响 PMFlow OS focused tests;不改全局 API client。
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `backend/app/api/pmflow_os_api/__init__.py`
|
||||
- `backend/app/services/pmflow_os_projection_service.py`
|
||||
- `backend/app/schemas/pmflow_os.py`
|
||||
- `backend/tests/test_pmflow_os_projection_api.py`
|
||||
- `frontend/src/api/pmflowOs.ts`
|
||||
- `frontend/src/features/pmflow-os/store.tsx`
|
||||
- `frontend/src/features/pmflow-os/store.api.test.tsx`
|
||||
- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx`
|
||||
- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md`
|
||||
- `docs/architecture/CODE_REFERENCE_INDEX.md`
|
||||
- `docs/architecture/ARCHITECTURE_INVENTORY.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` 通过:`17 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` 未通过:当前前端树缺失整组共享 `src/lib/*` 模块(例如 `@/lib/api`、`@/lib/utils`、`@/lib/datetime`、`@/lib/display`、`@/lib/rbac`),影响全仓大量既有文件和 forbidden `router/index.tsx`;本 Packet 未越界修复。
|
||||
- `cd frontend && npm run build` 同样停在上述共享 `src/lib/*` 缺失的 TypeScript 解析错误,未进入 Vite build。
|
||||
|
||||
未完成或风险:
|
||||
|
||||
- 本阶段只创建发布验证记录和人工确认快照,不执行部署命令、不写外部系统。
|
||||
- 全量前端 typecheck/build 需由 Shell / Integration owner 先恢复共享 `frontend/src/lib/*` 基础模块后再重新验证。
|
||||
|
||||
### 2026-04-26 / big-module-integration-p3 / done
|
||||
|
||||
完成内容:
|
||||
|
|
|
|||
|
|
@ -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 正在推进、暂存想法、草案箱、最近项目摘要;Space P2 的 `releaseAttention` 只消费 `execution-summary.releaseGate`,把测试失败、PR 未合并、发布门禁阻塞或可发布候选显示到 `/space`,项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,发布关注项进入 `/release-growth?projectId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入或发布执行。
|
||||
- `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=<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。
|
||||
- `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 与 release validation run create 均调用 `/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` 与 `latestValidationRun`,从 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,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照;当前仍不调用真实 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;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;这些步骤都不直接执行本机命令或真实发布命令;`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、治理页和图表代码。
|
||||
|
|
|
|||
|
|
@ -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 与 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`,只生成发布判断、阻塞原因和下一步动作,不执行发布命令
|
||||
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff、Release Evidence Gate 与 Release Validation Run:参考 `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`;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,从当前 `releaseGate` 固化 `ReleaseValidationRun` 快照并在 `releaseGate.latestValidationRun` 返回最新记录,无 evidence 时返回 `release_validation_evidence_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、ChangeSet 状态或 execution event;Space P2 已从 `PMFlowExecutionSummary.releaseGate` 派生 `releaseAttention`,用于在 `/space` 显示测试失败、PR 未合并、发布门禁阻塞或可发布候选,所有动作只跳转 `/release-growth?projectId=<id>`;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 状态或 execution event;Space P2 已从 `PMFlowExecutionSummary.releaseGate` 派生 `releaseAttention`,用于在 `/space` 显示测试失败、PR 未合并、发布门禁阻塞或可发布候选,所有动作只跳转 `/release-growth?projectId=<id>`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowReleaseValidationRun`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同;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>`,发布关注项进入 `/release-growth?projectId=<id>`;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/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=<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 共享 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、release validation run create 均调后端并 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、阻塞原因、下一步动作和 `latestValidationRun`,缺证据时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run,但仍不执行部署命令;静态高保真发布/增长数据仅保留在显式 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`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { api } from "@/lib/api";
|
||||
import type { ChangeItem, PMFlowExecutionContext, PMFlowRiskLevel, ProjectTruthState } from "@/features/pmflow-os/types";
|
||||
|
||||
type ApiEnvelope<T> = { code: number; message: string; data: T };
|
||||
const API_BASE = "/api";
|
||||
|
||||
function unwrap<T>(raw: ApiEnvelope<T> | T): T {
|
||||
if (raw && typeof raw === "object" && "data" in (raw as Record<string, unknown>)) {
|
||||
|
|
@ -10,6 +10,51 @@ function unwrap<T>(raw: ApiEnvelope<T> | T): T {
|
|||
return raw as T;
|
||||
}
|
||||
|
||||
function extractErrorMessage(payload: unknown, fallback: string): string {
|
||||
if (payload && typeof payload === "object") {
|
||||
const record = payload as Record<string, unknown>;
|
||||
if (typeof record.message === "string" && record.message.trim()) return record.message.trim();
|
||||
if (typeof record.detail === "string" && record.detail.trim()) return record.detail.trim();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function storedAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return window.localStorage.getItem("auth_token") || window.localStorage.getItem("token") || window.localStorage.getItem("access_token");
|
||||
}
|
||||
|
||||
async function request<T>(endpoint: string, init: { method: "GET" | "POST"; body?: unknown }): Promise<T> {
|
||||
const token = storedAuthToken();
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: init.method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(extractErrorMessage(payload, `HTTP ${response.status}`));
|
||||
}
|
||||
if (payload && typeof payload === "object" && "code" in payload) {
|
||||
const code = typeof (payload as Record<string, unknown>).code === "number" ? (payload as Record<string, unknown>).code : 0;
|
||||
if (code !== 0) {
|
||||
throw new Error(extractErrorMessage(payload, `HTTP ${response.status}`));
|
||||
}
|
||||
}
|
||||
return unwrap(payload as ApiEnvelope<T> | T);
|
||||
}
|
||||
|
||||
function get<T>(endpoint: string): Promise<T> {
|
||||
return request<T>(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
function post<T>(endpoint: string, body: unknown = {}): Promise<T> {
|
||||
return request<T>(endpoint, { method: "POST", body });
|
||||
}
|
||||
|
||||
export interface PMFlowExecutionSummary {
|
||||
projectId: string;
|
||||
execution: PMFlowExecutionContext;
|
||||
|
|
@ -40,6 +85,7 @@ export type PMFlowExecutionProgressEventType =
|
|||
| "manual_progress";
|
||||
|
||||
export type PMFlowReleaseGateStatus = "ready" | "blocked" | "needs_confirmation" | "empty";
|
||||
export type PMFlowReleaseValidationDecision = "enter_release_candidate" | "keep_blocked";
|
||||
|
||||
export interface PMFlowReleaseGateCheck {
|
||||
key: string;
|
||||
|
|
@ -68,9 +114,37 @@ export interface PMFlowReleaseGate {
|
|||
nextActions: string[];
|
||||
checks: PMFlowReleaseGateCheck[];
|
||||
evidence: PMFlowReleaseGateEvidence[];
|
||||
latestValidationRun?: PMFlowReleaseValidationRun | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PMFlowReleaseValidationRun {
|
||||
id: string;
|
||||
projectId: string;
|
||||
charterId?: string | null;
|
||||
releaseNodeId?: string | null;
|
||||
status: "ready" | "blocked" | "needs_confirmation" | string;
|
||||
score?: number | null;
|
||||
keyMetrics: Record<string, unknown>;
|
||||
riskItems: Array<Record<string, unknown>>;
|
||||
rollbackPlan: Record<string, unknown>;
|
||||
validationSummary: Record<string, unknown>;
|
||||
createdBy?: string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PMFlowReleaseValidationRunRequest {
|
||||
decision: PMFlowReleaseValidationDecision;
|
||||
note?: string | null;
|
||||
}
|
||||
|
||||
export interface PMFlowReleaseValidationRunResult {
|
||||
releaseValidationRun: PMFlowReleaseValidationRun;
|
||||
executionSummary: PMFlowExecutionSummary;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface PMFlowExecutionHandoffRequest {
|
||||
task_id: string;
|
||||
repo_binding_id?: string | null;
|
||||
|
|
@ -223,26 +297,18 @@ export interface PMFlowChangeSetCommitResult {
|
|||
}
|
||||
|
||||
export async function getPMFlowTruthState(projectId: string): Promise<ProjectTruthState> {
|
||||
const { data } = await api.get<ApiEnvelope<ProjectTruthState>>(`/pmflow-os/projects/${projectId}/truth-state`);
|
||||
return unwrap(data);
|
||||
return get<ProjectTruthState>(`/pmflow-os/projects/${projectId}/truth-state`);
|
||||
}
|
||||
|
||||
export async function getPMFlowExecutionSummary(projectId: string): Promise<PMFlowExecutionSummary> {
|
||||
const { data } = await api.get<ApiEnvelope<PMFlowExecutionSummary>>(
|
||||
`/pmflow-os/projects/${projectId}/execution-summary`
|
||||
);
|
||||
return unwrap(data);
|
||||
return get<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-summary`);
|
||||
}
|
||||
|
||||
export async function createPMFlowExecutionHandoff(
|
||||
projectId: string,
|
||||
payload: PMFlowExecutionHandoffRequest
|
||||
): Promise<PMFlowExecutionSummary> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowExecutionSummary>>(
|
||||
`/pmflow-os/projects/${projectId}/execution-handoffs`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-handoffs`, payload);
|
||||
}
|
||||
|
||||
export async function consumePMFlowExecutionHandoff(
|
||||
|
|
@ -250,22 +316,21 @@ export async function consumePMFlowExecutionHandoff(
|
|||
promptPackageId: string,
|
||||
payload: PMFlowExecutionHandoffConsumeRequest = {}
|
||||
): Promise<PMFlowExecutionSummary> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowExecutionSummary>>(
|
||||
`/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`, payload);
|
||||
}
|
||||
|
||||
export async function createPMFlowExecutionProgressEvent(
|
||||
projectId: string,
|
||||
payload: PMFlowExecutionProgressEventRequest
|
||||
): Promise<PMFlowExecutionSummary> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowExecutionSummary>>(
|
||||
`/pmflow-os/projects/${projectId}/execution-progress-events`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-progress-events`, payload);
|
||||
}
|
||||
|
||||
export async function createPMFlowReleaseValidationRun(
|
||||
projectId: string,
|
||||
payload: PMFlowReleaseValidationRunRequest
|
||||
): Promise<PMFlowReleaseValidationRunResult> {
|
||||
return post<PMFlowReleaseValidationRunResult>(`/pmflow-os/projects/${projectId}/release-validation-runs`, payload);
|
||||
}
|
||||
|
||||
export async function createPMFlowThreadMessage(
|
||||
|
|
@ -273,11 +338,7 @@ export async function createPMFlowThreadMessage(
|
|||
threadId: string,
|
||||
payload: { content: string; source?: string; attachments?: Array<Record<string, unknown>> }
|
||||
): Promise<PMFlowThreadMessageResponse> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowThreadMessageResponse>>(
|
||||
`/pmflow-os/projects/${projectId}/threads/${threadId}/messages`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowThreadMessageResponse>(`/pmflow-os/projects/${projectId}/threads/${threadId}/messages`, payload);
|
||||
}
|
||||
|
||||
export async function createPMFlowExtraction(
|
||||
|
|
@ -292,51 +353,31 @@ export async function createPMFlowExtraction(
|
|||
proposed_items: PMFlowProposedChangeItem[];
|
||||
}
|
||||
): Promise<PMFlowChangeSetDetail> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowChangeSetDetail>>(
|
||||
`/pmflow-os/projects/${projectId}/extractions`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowChangeSetDetail>(`/pmflow-os/projects/${projectId}/extractions`, payload);
|
||||
}
|
||||
|
||||
export async function getPMFlowChangeSet(projectId: string, changeSetId: string): Promise<PMFlowChangeSetDetail> {
|
||||
const { data } = await api.get<ApiEnvelope<PMFlowChangeSetDetail>>(
|
||||
`/pmflow-os/projects/${projectId}/change-sets/${changeSetId}`
|
||||
);
|
||||
return unwrap(data);
|
||||
return get<PMFlowChangeSetDetail>(`/pmflow-os/projects/${projectId}/change-sets/${changeSetId}`);
|
||||
}
|
||||
|
||||
export async function acceptPMFlowChangeItem(changeItemId: string): Promise<PMFlowChangeSetDetail> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowChangeSetDetail>>(`/pmflow-os/change-items/${changeItemId}/accept`);
|
||||
return unwrap(data);
|
||||
return post<PMFlowChangeSetDetail>(`/pmflow-os/change-items/${changeItemId}/accept`);
|
||||
}
|
||||
|
||||
export async function editPMFlowChangeItem(
|
||||
changeItemId: string,
|
||||
payload: { summary?: string; diff?: string; payload_patch?: Record<string, unknown>; reason?: string }
|
||||
): Promise<PMFlowChangeSetDetail> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowChangeSetDetail>>(
|
||||
`/pmflow-os/change-items/${changeItemId}/edit`,
|
||||
payload
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowChangeSetDetail>(`/pmflow-os/change-items/${changeItemId}/edit`, payload);
|
||||
}
|
||||
|
||||
export async function deferPMFlowChangeItem(changeItemId: string, reason?: string): Promise<PMFlowChangeSetDetail> {
|
||||
const { data } = await api.post<ApiEnvelope<PMFlowChangeSetDetail>>(
|
||||
`/pmflow-os/change-items/${changeItemId}/defer`,
|
||||
reason ? { reason } : {}
|
||||
);
|
||||
return unwrap(data);
|
||||
return post<PMFlowChangeSetDetail>(`/pmflow-os/change-items/${changeItemId}/defer`, reason ? { reason } : {});
|
||||
}
|
||||
|
||||
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);
|
||||
return post<PMFlowChangeSetCommitResult>(`/pmflow-os/change-sets/${changeSetId}/commit`, { mode: "atomic", ...payload });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ import {
|
|||
} from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { PMFlowReleaseGateEvidence } from "@/api/pmflowOs";
|
||||
import type {
|
||||
PMFlowReleaseGateEvidence,
|
||||
PMFlowReleaseValidationDecision,
|
||||
PMFlowReleaseValidationRun,
|
||||
} from "@/api/pmflowOs";
|
||||
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -337,6 +341,20 @@ function gateLabel(status?: string) {
|
|||
return "缺少执行证据";
|
||||
}
|
||||
|
||||
function validationRunLabel(status?: string) {
|
||||
if (status === "ready") return "发布候选";
|
||||
if (status === "blocked") return "保持阻塞";
|
||||
if (status === "needs_confirmation") return "需要确认";
|
||||
return "未记录";
|
||||
}
|
||||
|
||||
function validationSummaryText(run?: PMFlowReleaseValidationRun | null) {
|
||||
if (!run) return "尚未创建 release validation run。";
|
||||
const summary = run.validationSummary || {};
|
||||
const sourceStatus = typeof summary.releaseGateStatus === "string" ? summary.releaseGateStatus : run.status;
|
||||
return `基于 ${sourceStatus} release gate 固化,部署执行:${summary.deploymentExecuted ? "已执行" : "未执行"}`;
|
||||
}
|
||||
|
||||
function checkTone(status?: string): StatusTone {
|
||||
if (status === "passed") return "success";
|
||||
if (status === "failed") return "danger";
|
||||
|
|
@ -388,11 +406,21 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
|||
executionSummaryError,
|
||||
projectionWarnings,
|
||||
refetchExecutionSummary,
|
||||
createReleaseValidationRun,
|
||||
} = store;
|
||||
const gate = executionSummary?.releaseGate;
|
||||
const latestValidationRun = gate?.latestValidationRun;
|
||||
const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading";
|
||||
const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])];
|
||||
const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate;
|
||||
const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length);
|
||||
const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => {
|
||||
const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞";
|
||||
createReleaseValidationRun({
|
||||
decision,
|
||||
note: `ReleaseGrowthPage 人工确认:${decisionLabel};PMFlow 仅记录发布前验证,不执行部署。`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f7faff] text-slate-950">
|
||||
|
|
@ -507,6 +535,60 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<SectionTitle title="发布验证记录" icon={ShieldAlert} />
|
||||
<div className="mt-4 rounded-lg border border-[#e6edf7] bg-[#fbfdff] p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<StatusChip label={validationRunLabel(latestValidationRun?.status)} tone={gateTone(latestValidationRun?.status)} />
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">{validationSummaryText(latestValidationRun)}</p>
|
||||
</div>
|
||||
{latestValidationRun?.createdAt ? (
|
||||
<div className="shrink-0 text-xs text-slate-400">
|
||||
{new Date(latestValidationRun.createdAt).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-4 text-xs leading-5 text-slate-500">
|
||||
记录会固化当前 evidence、阻塞原因、下一步动作和人工确认结果;这里不会触发真实部署。
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2">
|
||||
{gate?.status === "blocked" ? (
|
||||
<Button
|
||||
className="h-10 rounded-lg bg-[#2463EB] text-white hover:bg-[#1f57d1]"
|
||||
disabled={!canCreateValidationRun || isLoading}
|
||||
onClick={() => handleCreateValidationRun("keep_blocked")}
|
||||
>
|
||||
记录保持阻塞
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className="h-10 rounded-lg bg-[#2463EB] text-white hover:bg-[#1f57d1]"
|
||||
disabled={!canCreateValidationRun || isLoading}
|
||||
onClick={() => handleCreateValidationRun("enter_release_candidate")}
|
||||
>
|
||||
生成发布验证记录
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 rounded-lg border-[#dbe6ff] text-[#2463EB]"
|
||||
disabled={!canCreateValidationRun || isLoading}
|
||||
onClick={() => handleCreateValidationRun("keep_blocked")}
|
||||
>
|
||||
人工保持阻塞
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!canCreateValidationRun ? (
|
||||
<div className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
|
||||
缺少真实执行证据,不能创建成功的发布验证记录。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<SectionTitle title="运行警告" />
|
||||
<div className="mt-4 space-y-2">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
consumePMFlowExecutionHandoff,
|
||||
createPMFlowExecutionHandoff,
|
||||
createPMFlowExecutionProgressEvent,
|
||||
createPMFlowReleaseValidationRun,
|
||||
createPMFlowThreadMessage,
|
||||
deferPMFlowChangeItem,
|
||||
editPMFlowChangeItem,
|
||||
|
|
@ -31,6 +32,7 @@ vi.mock("@/api/pmflowOs", () => ({
|
|||
consumePMFlowExecutionHandoff: vi.fn(),
|
||||
createPMFlowExecutionHandoff: vi.fn(),
|
||||
createPMFlowExecutionProgressEvent: vi.fn(),
|
||||
createPMFlowReleaseValidationRun: vi.fn(),
|
||||
editPMFlowChangeItem: vi.fn(),
|
||||
deferPMFlowChangeItem: vi.fn(),
|
||||
}));
|
||||
|
|
@ -41,6 +43,7 @@ const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
|
|||
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
|
||||
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
|
||||
const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent);
|
||||
const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun);
|
||||
const acceptItemMock = vi.mocked(acceptPMFlowChangeItem);
|
||||
const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet);
|
||||
const editItemMock = vi.mocked(editPMFlowChangeItem);
|
||||
|
|
@ -329,6 +332,63 @@ describe("PMFlowOsProvider real change review actions", () => {
|
|||
recentDeliveries: [],
|
||||
warnings: [],
|
||||
});
|
||||
createReleaseValidationRunMock.mockResolvedValue({
|
||||
releaseValidationRun: {
|
||||
id: "901",
|
||||
projectId: "123",
|
||||
status: "needs_confirmation",
|
||||
score: 0.6,
|
||||
keyMetrics: { evidenceCount: 1 },
|
||||
riskItems: [],
|
||||
rollbackPlan: { deploymentExecuted: false },
|
||||
validationSummary: {
|
||||
source: "pmflow_os_release_gate",
|
||||
releaseGateStatus: "needs_confirmation",
|
||||
deploymentExecuted: false,
|
||||
},
|
||||
createdBy: "1",
|
||||
createdAt: "2026-04-26T00:00:00",
|
||||
},
|
||||
executionSummary: {
|
||||
projectId: "123",
|
||||
execution: {
|
||||
taskId: "task-99",
|
||||
status: "testing",
|
||||
prompt: "真实 prompt package",
|
||||
commitLog: ["发布验证记录已创建"],
|
||||
prStatus: "PR #88 · linked",
|
||||
testSummary: "真实测试回写",
|
||||
writebackStatus: "testing",
|
||||
},
|
||||
releaseGate: {
|
||||
...releaseGate(),
|
||||
latestValidationRun: {
|
||||
id: "901",
|
||||
projectId: "123",
|
||||
status: "needs_confirmation",
|
||||
score: 0.6,
|
||||
keyMetrics: { evidenceCount: 1 },
|
||||
riskItems: [],
|
||||
rollbackPlan: { deploymentExecuted: false },
|
||||
validationSummary: {
|
||||
source: "pmflow_os_release_gate",
|
||||
releaseGateStatus: "needs_confirmation",
|
||||
deploymentExecuted: false,
|
||||
},
|
||||
createdBy: "1",
|
||||
createdAt: "2026-04-26T00:00:00",
|
||||
},
|
||||
},
|
||||
devSync: { active_task_count: 1 },
|
||||
promptPackage: null,
|
||||
repoBindings: [],
|
||||
activeDevTasks: [],
|
||||
recentProgressEvents: [],
|
||||
recentDeliveries: [],
|
||||
warnings: [],
|
||||
},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -405,6 +465,19 @@ describe("PMFlowOsProvider real change review actions", () => {
|
|||
)
|
||||
);
|
||||
|
||||
act(() =>
|
||||
result.current.createReleaseValidationRun({
|
||||
decision: "enter_release_candidate",
|
||||
note: "人工确认进入发布候选",
|
||||
})
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(createReleaseValidationRunMock).toHaveBeenCalledWith(
|
||||
"123",
|
||||
expect.objectContaining({ decision: "enter_release_candidate", note: "人工确认进入发布候选" })
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.current.state.requirements).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -430,6 +503,7 @@ describe("PMFlowOsProvider real change review actions", () => {
|
|||
expect(createExecutionHandoffMock).not.toHaveBeenCalled();
|
||||
expect(consumeExecutionHandoffMock).not.toHaveBeenCalled();
|
||||
expect(createExecutionProgressEventMock).not.toHaveBeenCalled();
|
||||
expect(createReleaseValidationRunMock).not.toHaveBeenCalled();
|
||||
expect(getExecutionSummaryMock).not.toHaveBeenCalled();
|
||||
expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
createPMFlowExecutionHandoff,
|
||||
createPMFlowExecutionProgressEvent,
|
||||
createPMFlowExtraction,
|
||||
createPMFlowReleaseValidationRun,
|
||||
createPMFlowThreadMessage,
|
||||
deferPMFlowChangeItem,
|
||||
editPMFlowChangeItem,
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
type PMFlowExecutionProgressEventRequest,
|
||||
type PMFlowExecutionSummary,
|
||||
type PMFlowProposedChangeItem,
|
||||
type PMFlowReleaseValidationRunRequest,
|
||||
} from "@/api/pmflowOs";
|
||||
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
|
||||
import type {
|
||||
|
|
@ -537,6 +539,7 @@ interface PMFlowOsStoreValue {
|
|||
createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void;
|
||||
consumeExecutionHandoff: (promptPackageId: string) => void;
|
||||
recordExecutionProgress: (payload: PMFlowExecutionProgressEventRequest) => void;
|
||||
createReleaseValidationRun: (payload: PMFlowReleaseValidationRunRequest) => void;
|
||||
advanceExecution: () => void;
|
||||
writeBackExecution: (outcome: "passed" | "failed") => void;
|
||||
publishRelease: () => void;
|
||||
|
|
@ -767,6 +770,11 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
mutateDemoOnly((current) => advanceExecutionMutation(current));
|
||||
},
|
||||
createReleaseValidationRun: (payload) => {
|
||||
if (dataSource.mode === "api" && dataSource.projectId) {
|
||||
runApiAction(() => createPMFlowReleaseValidationRun(dataSource.projectId || "", payload));
|
||||
}
|
||||
},
|
||||
advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)),
|
||||
writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)),
|
||||
publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue