562 lines
17 KiB
TypeScript
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);
|
|
}
|