PMFlow/frontend/src/api/pmflowOs.ts

562 lines
17 KiB
TypeScript

import type {
ChangeItem,
PMFlowExecutionContext,
PMFlowReviewDecisionAction,
PMFlowRiskLevel,
ProjectTruthState,
} from "@/features/pmflow-os/types";
export type { PMFlowReviewerRouting, PMFlowReviewerRoutingItem, PMFlowReviewerSuggestion } 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>)) {
return (raw as ApiEnvelope<T>).data;
}
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;
releaseGate: PMFlowReleaseGate;
releaseSignals?: PMFlowReleaseSignals;
devSync: Record<string, unknown>;
promptPackage?: PMFlowPromptPackage | null;
repoBindings: PMFlowRepoBinding[];
activeDevTasks: PMFlowDevTask[];
recentProgressEvents: PMFlowProgressEvent[];
recentDeliveries: PMFlowRecentDelivery[];
warnings: string[];
}
export type PMFlowExecutionRuntimeTarget = "local_bridge" | "codex" | "github_agent" | "manual";
export type PMFlowExecutionProgressSource = "local_bridge" | "codex" | "github_agent" | "manual";
export type PMFlowExecutionProgressEventType =
| "branch_created"
| "commit"
| "pr_opened"
| "pr_synced"
| "test_passed"
| "test_failed"
| "checks_passed"
| "failed"
| "blocked"
| "merged"
| "accepted"
| "manual_progress";
export type PMFlowReleaseGateStatus = "ready" | "blocked" | "needs_confirmation" | "empty";
export type PMFlowReleaseValidationDecision = "enter_release_candidate" | "keep_blocked";
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[];
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 PMFlowReleaseSignals {
status: "active" | "empty" | string;
relationship: {
releaseGateStatus?: string | null;
latestValidationRunId?: string | null;
latestValidationRunStatus?: string | null;
source?: string | null;
changeSetCreated?: boolean;
};
summary: {
feedbackCount?: number;
revenueEventCount?: number;
iterationRecommendationCount?: number;
latestSignalAt?: string | null;
};
feedback: PMFlowReleaseFeedbackIngest[];
revenueEvents: PMFlowReleaseRevenueEvent[];
recommendations: PMFlowIterationRecommendation[];
recommendationGroups?: PMFlowIterationRecommendationGroup[];
nextActions: string[];
warnings: string[];
}
export interface PMFlowReleaseFeedbackIngest {
id: string;
projectId: string;
charterId?: string | null;
sourceType: string;
title?: string | null;
content: string;
metrics: Record<string, unknown>;
samples: Array<Record<string, unknown>>;
status: string;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowReleaseRevenueEvent {
id: string;
projectId: string;
sourceType: string;
category: string;
currency: string;
amountUsd: number;
externalRef?: string | null;
eventPayload: Record<string, unknown>;
occurredAt?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowIterationRecommendation {
id: string;
projectId: string;
charterId?: string | null;
feedbackIngestId?: string | null;
status: string;
priority: string;
recommendationSummary: Record<string, unknown>;
proposedCharterPatch: Record<string, unknown>;
proposedTasks: Array<Record<string, unknown>>;
changeSetCreated: boolean;
changeSetId?: string | null;
groupKey?: string | null;
duplicateOf?: string | null;
mergedCount?: number;
impactLevel?: string | null;
riskLevel?: string | null;
suggestedChangeTypes?: string[];
reviewPriority?: string | null;
quality?: Record<string, unknown>;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowIterationRecommendationGroup {
groupKey: string;
title?: string | null;
recommendationIds: string[];
mergedCount: number;
impactLevel: string;
riskLevel: string;
suggestedChangeTypes: string[];
reviewPriority: string;
changeSetCreated: boolean;
changeSetId?: string | null;
rationale?: string | null;
}
export interface PMFlowReleaseFeedbackIngestRequest {
source_type?: string;
title?: string | null;
content: string;
charter_id?: string | null;
metrics?: Record<string, unknown>;
samples?: Array<Record<string, unknown>>;
}
export interface PMFlowReleaseFeedbackIngestResult {
ingest: PMFlowReleaseFeedbackIngest;
recommendation: PMFlowIterationRecommendation;
releaseSignals: PMFlowReleaseSignals;
executionSummary: PMFlowExecutionSummary;
warnings: string[];
}
export interface PMFlowIterationRecommendationChangeSetDraftRequest {
note?: string | null;
}
export interface PMFlowIterationRecommendationGroupChangeSetDraftRequest {
recommendation_ids?: string[];
note?: string | null;
}
export interface PMFlowIterationRecommendationChangeSetDraftResult {
changeSetDetail: PMFlowChangeSetDetail;
executionSummary: PMFlowExecutionSummary;
warnings: string[];
}
export interface PMFlowExecutionHandoffRequest {
task_id: string;
repo_binding_id?: string | null;
runtime_target: PMFlowExecutionRuntimeTarget;
note?: string | null;
}
export interface PMFlowExecutionHandoffConsumeRequest {
runtime_target?: PMFlowExecutionRuntimeTarget | null;
consumer_label?: string | null;
note?: string | null;
}
export interface PMFlowExecutionProgressEventRequest {
task_id: string;
event_type: PMFlowExecutionProgressEventType;
source?: PMFlowExecutionProgressSource;
repo_binding_id?: string | null;
branch_name?: string | null;
commit_sha?: string | null;
pr_number?: number | null;
test_summary?: string | null;
workflow_state?: string | null;
summary?: string | null;
event_source_id?: string | null;
occurred_at?: string | null;
}
export interface PMFlowPromptPackage {
id: string;
project_id: string;
charter_id: string;
task_id?: string | null;
status: string;
package_kind: string;
package_text: string;
package_payload: Record<string, unknown>;
created_at?: string | null;
}
export interface PMFlowRepoBinding {
id: string;
project_id: string;
provider: string;
owner: string;
repo_name: string;
full_name: string;
default_branch: string;
external_repo_id?: string | null;
installation_id?: string | null;
installation_account_login?: string | null;
app_state?: string;
status: string;
task_link_count?: number;
recent_event_count?: number;
}
export interface PMFlowDevTask {
id: string;
title: string;
description?: string | null;
status: string;
priority?: string | null;
estimated_hours?: number | null;
project_id: string;
charter_id?: string | null;
charter_title?: string | null;
assignee?: { id: string; name?: string | null; role?: string | null } | null;
prompt_package_count?: number;
model_binding?: Record<string, unknown> | null;
code_link?: {
id: string;
status: string;
branch_name?: string | null;
pr_number?: number | null;
last_commit_sha?: string | null;
repo?: PMFlowRepoBinding | null;
} | null;
}
export interface PMFlowProgressEvent {
id: string;
event_type: string;
source: string;
summary?: string | null;
workflow_state?: string | null;
derived_task_status?: string | null;
delivery_id?: string | null;
event_source_id?: string | null;
occurred_at?: string | null;
task?: { id: string; title?: string | null; status?: string | null; priority?: string | null };
user?: { id: string; name?: string | null; role?: string | null } | null;
repo?: PMFlowRepoBinding | null;
branch_name?: string | null;
pr_number?: number | null;
commit_sha?: string | null;
}
export interface PMFlowRecentDelivery {
id: string;
delivery_id: string;
event_type: string;
project_id?: string | null;
repo_binding_id?: string | null;
installation_id?: string | null;
status: string;
failure_reason?: string | null;
repository_full_name?: string | null;
received_at?: string | null;
}
export interface PMFlowThreadMessageResponse {
id: string;
threadId: string;
projectId: string;
role: string;
author: string;
content: string;
createdAt: string;
status: string;
source: string;
metadata: Record<string, unknown>;
}
export interface PMFlowProposedChangeItem {
group: ChangeItem["group"];
object_type: Exclude<ChangeItem["objectType"], "Thread" | "ChangeSet" | "Approval" | "Evidence">;
object_id?: string | null;
title: string;
summary: string;
diff?: string;
payload?: Record<string, unknown>;
confidence?: number;
risk_level?: PMFlowRiskLevel;
}
export interface PMFlowChangeSetDetail {
changeSet: Record<string, unknown>;
items: Array<Record<string, unknown>>;
evidence: Array<Record<string, unknown>>;
approvals: Array<Record<string, unknown>>;
}
export interface PMFlowChangeSetCommitResult {
changeSet: Record<string, unknown>;
committedItems: Array<Record<string, unknown>>;
skippedItems: Array<Record<string, unknown>>;
failedItems: Array<Record<string, unknown>>;
projectTruthState?: ProjectTruthState | null;
}
export interface PMFlowReviewDecisionRequest {
action: PMFlowReviewDecisionAction;
reviewer_id?: number;
reason?: string | null;
item_ids?: string[];
}
export interface PMFlowReviewDecisionResult {
changeSetDetail: PMFlowChangeSetDetail;
truthState: ProjectTruthState;
warnings: string[];
}
export async function getPMFlowTruthState(projectId: string): Promise<ProjectTruthState> {
return get<ProjectTruthState>(`/pmflow-os/projects/${projectId}/truth-state`);
}
export async function getPMFlowExecutionSummary(projectId: string): Promise<PMFlowExecutionSummary> {
return get<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-summary`);
}
export async function createPMFlowExecutionHandoff(
projectId: string,
payload: PMFlowExecutionHandoffRequest
): Promise<PMFlowExecutionSummary> {
return post<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-handoffs`, payload);
}
export async function consumePMFlowExecutionHandoff(
projectId: string,
promptPackageId: string,
payload: PMFlowExecutionHandoffConsumeRequest = {}
): Promise<PMFlowExecutionSummary> {
return post<PMFlowExecutionSummary>(`/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`, payload);
}
export async function createPMFlowExecutionProgressEvent(
projectId: string,
payload: PMFlowExecutionProgressEventRequest
): Promise<PMFlowExecutionSummary> {
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 createPMFlowReleaseFeedbackIngest(
projectId: string,
payload: PMFlowReleaseFeedbackIngestRequest
): Promise<PMFlowReleaseFeedbackIngestResult> {
return post<PMFlowReleaseFeedbackIngestResult>(`/pmflow-os/projects/${projectId}/release-feedback-ingests`, payload);
}
export async function createPMFlowIterationRecommendationChangeSetDraft(
projectId: string,
recommendationId: string,
payload: PMFlowIterationRecommendationChangeSetDraftRequest = {}
): Promise<PMFlowIterationRecommendationChangeSetDraftResult> {
return post<PMFlowIterationRecommendationChangeSetDraftResult>(
`/pmflow-os/projects/${projectId}/iteration-recommendations/${recommendationId}/change-set-drafts`,
payload
);
}
export async function createPMFlowIterationRecommendationGroupChangeSetDraft(
projectId: string,
groupKey: string,
payload: PMFlowIterationRecommendationGroupChangeSetDraftRequest = {}
): Promise<PMFlowIterationRecommendationChangeSetDraftResult> {
return post<PMFlowIterationRecommendationChangeSetDraftResult>(
`/pmflow-os/projects/${projectId}/iteration-recommendation-groups/${encodeURIComponent(groupKey)}/change-set-drafts`,
payload
);
}
export async function createPMFlowThreadMessage(
projectId: string,
threadId: string,
payload: { content: string; source?: string; attachments?: Array<Record<string, unknown>> }
): Promise<PMFlowThreadMessageResponse> {
return post<PMFlowThreadMessageResponse>(`/pmflow-os/projects/${projectId}/threads/${threadId}/messages`, payload);
}
export async function createPMFlowExtraction(
projectId: string,
payload: {
thread_id: string;
message_id: string;
scope?: string;
summary?: string;
headline?: string;
routing_meta?: Record<string, unknown>;
proposed_items: PMFlowProposedChangeItem[];
}
): Promise<PMFlowChangeSetDetail> {
return post<PMFlowChangeSetDetail>(`/pmflow-os/projects/${projectId}/extractions`, payload);
}
export async function getPMFlowChangeSet(projectId: string, changeSetId: string): Promise<PMFlowChangeSetDetail> {
return get<PMFlowChangeSetDetail>(`/pmflow-os/projects/${projectId}/change-sets/${changeSetId}`);
}
export async function acceptPMFlowChangeItem(changeItemId: string): Promise<PMFlowChangeSetDetail> {
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> {
return post<PMFlowChangeSetDetail>(`/pmflow-os/change-items/${changeItemId}/edit`, payload);
}
export async function deferPMFlowChangeItem(changeItemId: string, reason?: string): Promise<PMFlowChangeSetDetail> {
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> {
return post<PMFlowChangeSetCommitResult>(`/pmflow-os/change-sets/${changeSetId}/commit`, { mode: "atomic", ...payload });
}
export async function createPMFlowReviewDecision(
changeSetId: string,
payload: PMFlowReviewDecisionRequest
): Promise<PMFlowReviewDecisionResult> {
return post<PMFlowReviewDecisionResult>(`/pmflow-os/change-sets/${changeSetId}/review-decisions`, payload);
}