matrix-builder / web /src /lib /build-mapping.ts
ruslanmv
Deploy: metrics + docs (Batch 12)
22b729d
Raw
History Blame Contribute Delete
3.55 kB
// SavedBuild ⇄ {Project, Version} mapping.
//
// The "My Builds" and active-build screens render a `SavedBuild` (the localStorage shape). The
// control plane instead stores a Project (the app, owns the slug/description) plus one or more
// Versions (each a bundle iteration with its own timeline of batches/commits). This module is the
// single seam that converts between the two, so screens stay source-agnostic: whether a build
// came from localStorage or from `GET /projects` + `GET /versions/{id}`, it reaches the UI as the
// same `SavedBuild`. When Phase 3 swaps the store for the API, only the callers change — not the
// screens, and not this mapping.
import { STAGES } from "./build-batches";
import type { SavedBuild } from "./builds-store";
import type { BuildStatus } from "./saved-bundles";
import type {
ProjectCreate,
ProjectResponse,
VersionCreate,
VersionResponse,
} from "./workflow-types";
import type { CoderId } from "@/types/coder";
export function slugify(text: string): string {
const slug = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.slice(0, 72) || "matrix-project";
}
// Workflow version/commit status (draft|ready|running|committed|approved|needs-repair|…) collapses
// onto the three states the cards understand. A fully-validated timeline always wins.
export function toBuildStatus(versionStatus: string, passed = 0): BuildStatus {
if (passed >= STAGES.length && STAGES.length > 0) return "validated";
switch (versionStatus) {
case "approved":
case "committed":
case "validated":
return "validated";
case "ready":
case "running":
case "active":
return "ready";
default:
return "draft";
}
}
export function toVersionStatus(status: BuildStatus): string {
if (status === "validated") return "approved";
if (status === "ready") return "ready";
return "draft";
}
// Per-build presentation stats that live on neither Project nor Version (they're derived from the
// bundle/timeline in Phase 2/3). Callers pass what they know; sane defaults fill the rest.
export interface BuildStats {
files?: number;
stack?: string[];
coder?: CoderId;
passed?: number;
candidateId?: SavedBuild["candidateId"];
}
export function toSavedBuild(
project: ProjectResponse,
version: VersionResponse,
stats: BuildStats = {},
): SavedBuild {
const passed = stats.passed ?? 0;
return {
// The version is the reopen target: timeline, batches and commits all hang off it.
id: version.id,
name: project.title,
description: project.description,
status: toBuildStatus(version.status, passed),
version: version.version_label,
files: stats.files ?? 0,
stack: stats.stack ?? [],
coder: stats.coder,
passed,
updatedAt: Date.parse(version.created_at) || Date.now(),
idea: version.requirements_md || project.description,
candidateId: stats.candidateId,
};
}
export function toProjectCreate(build: Pick<SavedBuild, "name" | "description">): ProjectCreate {
return {
title: build.name,
slug: slugify(build.name),
description: build.description,
};
}
export function toVersionCreate(
build: Pick<SavedBuild, "name" | "version" | "idea" | "description">,
projectId: string,
parentVersionId: string | null = null,
): VersionCreate {
return {
project_id: projectId,
title: build.name,
version_label: build.version,
requirements_md: build.idea ?? build.description,
parent_version_id: parentVersionId,
};
}