chore(repo): reinitialize repository

This commit is contained in:
2026-03-18 11:29:54 +08:00
commit 24871e213a
288 changed files with 44369 additions and 0 deletions
@@ -0,0 +1,47 @@
package humantask
import "fmt"
type Status string
const (
StatusPending Status = "pending"
StatusAnswered Status = "answered"
StatusCancelled Status = "cancelled"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
RoleName string `json:"role_name"`
PromptMessageID string `json:"prompt_message_id"`
Status Status `json:"status"`
AnsweredMessageID string `json:"answered_message_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (r Record) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.RoleName == "" {
return fmt.Errorf("role name is required")
}
if r.PromptMessageID == "" {
return fmt.Errorf("prompt message id is required")
}
switch r.Status {
case StatusPending, StatusAnswered, StatusCancelled:
default:
return fmt.Errorf("invalid human task status %q", r.Status)
}
if r.Status == StatusAnswered && r.AnsweredMessageID == "" {
return fmt.Errorf("answered message id is required")
}
return nil
}
+66
View File
@@ -0,0 +1,66 @@
package lane
import "fmt"
type Status string
const (
StatusDraft Status = "draft"
StatusReady Status = "ready"
StatusRunning Status = "running"
StatusBlocked Status = "blocked"
StatusSucceeded Status = "succeeded"
StatusFailed Status = "failed"
StatusCancelled Status = "cancelled"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Purpose string `json:"purpose,omitempty"`
Status Status `json:"status"`
BaseBranch string `json:"base_branch"`
BranchName string `json:"branch_name"`
HeadCommit string `json:"head_commit,omitempty"`
WorktreePath string `json:"worktree_path"`
ContainerName string `json:"container_name"`
RuntimeEndpoint string `json:"runtime_endpoint"`
CreatedByRoleName string `json:"created_by_role_name"`
ResultSummaryMarkdown string `json:"result_summary_markdown,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
StartedAt string `json:"started_at,omitempty"`
CompletedAt string `json:"completed_at,omitempty"`
}
func ValidateStatus(value Status) error {
switch value {
case StatusDraft, StatusReady, StatusRunning, StatusBlocked, StatusSucceeded, StatusFailed, StatusCancelled:
return nil
default:
return fmt.Errorf("invalid lane status %q", value)
}
}
func (r Record) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.Name == "" {
return fmt.Errorf("lane name is required")
}
if r.Slug == "" {
return fmt.Errorf("lane slug is required")
}
if r.CreatedByRoleName == "" {
return fmt.Errorf("created by role is required")
}
return ValidateStatus(r.Status)
}
@@ -0,0 +1,45 @@
package lane
import (
"path/filepath"
"strings"
"inbox/internal/base/slug"
)
func DefaultBranchName(workspaceSlug, topicID, laneSlug string) string {
return "lane/" + runtimeWorkspaceSlug(workspaceSlug) + "/" + runtimeLaneSlug(laneSlug) + "-" + runtimeTopicSuffix(topicID)
}
func DefaultWorktreePath(workspaceRoot, workspaceSlug, topicID, laneSlug string) string {
return filepath.Join(filepath.Dir(workspaceRoot), runtimeWorkspaceSlug(workspaceSlug)+"--"+runtimeLaneSlug(laneSlug)+"--"+runtimeTopicSuffix(topicID))
}
func DefaultContainerName(workspaceSlug, topicID, laneSlug string) string {
return "lane-" + runtimeWorkspaceSlug(workspaceSlug) + "-" + runtimeLaneSlug(laneSlug) + "-" + runtimeTopicSuffix(topicID)
}
func runtimeWorkspaceSlug(value string) string {
if normalized := slug.Normalize(value); normalized != "" {
return normalized
}
return "workspace"
}
func runtimeLaneSlug(value string) string {
if normalized := slug.Normalize(value); normalized != "" {
return normalized
}
return "lane"
}
func runtimeTopicSuffix(value string) string {
normalized := slug.Normalize(strings.TrimPrefix(strings.TrimSpace(value), "topic-"))
if normalized == "" {
return "topic"
}
if len(normalized) > 12 {
return normalized[len(normalized)-12:]
}
return normalized
}
@@ -0,0 +1,22 @@
package lane
import "testing"
func TestRuntimeNamesIncludeTopicSuffix(t *testing.T) {
topicID := "topic-20260317T030141Z-05c4bd6c9a45"
branch := DefaultBranchName("todo", topicID, "frontend-app")
if branch != "lane/todo/frontend-app-05c4bd6c9a45" {
t.Fatalf("unexpected branch name: %s", branch)
}
worktree := DefaultWorktreePath("/tmp/inbox-worktrees/todo", "todo", topicID, "frontend-app")
if worktree != "/tmp/inbox-worktrees/todo--frontend-app--05c4bd6c9a45" {
t.Fatalf("unexpected worktree path: %s", worktree)
}
container := DefaultContainerName("todo", topicID, "frontend-app")
if container != "lane-todo-frontend-app-05c4bd6c9a45" {
t.Fatalf("unexpected container name: %s", container)
}
}
@@ -0,0 +1,63 @@
package lanesync
import "fmt"
type Status string
const (
StatusApplied Status = "applied"
StatusSkipped Status = "skipped"
StatusFailed Status = "failed"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
DownstreamLaneID string `json:"downstream_lane_id"`
UpstreamLaneID string `json:"upstream_lane_id"`
TaskID string `json:"task_id"`
UpstreamCommit string `json:"upstream_commit"`
MergeCommit string `json:"merge_commit,omitempty"`
Status Status `json:"status"`
ErrorMessage string `json:"error_message,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func ValidateStatus(value Status) error {
switch value {
case StatusApplied, StatusSkipped, StatusFailed:
return nil
default:
return fmt.Errorf("invalid lane sync status %q", value)
}
}
func (r Record) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.DownstreamLaneID == "" {
return fmt.Errorf("downstream lane id is required")
}
if r.UpstreamLaneID == "" {
return fmt.Errorf("upstream lane id is required")
}
if r.TaskID == "" {
return fmt.Errorf("task id is required")
}
if r.UpstreamCommit == "" {
return fmt.Errorf("upstream commit is required")
}
if err := ValidateStatus(r.Status); err != nil {
return err
}
if r.Status == StatusFailed && r.ErrorMessage == "" {
return fmt.Errorf("failed lane sync must include error_message")
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
package message
import "fmt"
type Type string
const (
TypeChat Type = "chat"
TypeProposal Type = "proposal"
TypeQuestion Type = "question"
TypeDecision Type = "decision"
TypeSummary Type = "summary"
)
type DeliveryState string
const (
DeliveryPending DeliveryState = "pending"
DeliveryReceived DeliveryState = "received"
DeliveryArchived DeliveryState = "archived"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
FromRoleName string `json:"from_role_name"`
ToExpr string `json:"to_expr"`
Type Type `json:"type"`
Stage string `json:"stage"`
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
BodyMarkdown string `json:"body_markdown"`
CreatedAt string `json:"created_at"`
}
type DeliveryClaim struct {
Message Record `json:"message"`
RecipientRoleName string `json:"recipient_role_name"`
State DeliveryState `json:"state"`
UpdatedAt string `json:"updated_at"`
}
type PendingDelivery struct {
TopicID string `json:"topic_id"`
RoleName string `json:"role_name"`
Count int `json:"count"`
LastUpdated string `json:"last_updated"`
}
func (r Record) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.FromRoleName == "" {
return fmt.Errorf("from role is required")
}
if r.ToExpr == "" {
return fmt.Errorf("to expr is required")
}
switch r.Type {
case TypeChat, TypeProposal, TypeQuestion, TypeDecision, TypeSummary:
default:
return fmt.Errorf("invalid message type %q", r.Type)
}
return nil
}
+144
View File
@@ -0,0 +1,144 @@
package role
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
type ExecutorKind string
const (
ExecutorKindCodex ExecutorKind = "codex"
ExecutorKindHuman ExecutorKind = "human"
)
type PromptKind string
const (
PromptSystem PromptKind = "system"
)
type Definition struct {
Name string `json:"name"`
Title string `json:"title"`
ExecutorKind ExecutorKind `json:"executor_kind"`
Description string `json:"description"`
IsEnabled bool `json:"is_enabled"`
IsBuiltin bool `json:"is_builtin"`
SortOrder int `json:"sort_order"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (d Definition) Validate() error {
if d.Name == "" {
return errors.New("role name is required")
}
if d.Title == "" {
return errors.New("role title is required")
}
switch d.ExecutorKind {
case ExecutorKindCodex, ExecutorKindHuman:
default:
return fmt.Errorf("invalid role executor kind %q", d.ExecutorKind)
}
if d.Name == "user" && d.ExecutorKind != ExecutorKindHuman {
return fmt.Errorf("user role must use human executor")
}
return nil
}
func DefaultExecutorKind(roleName string) ExecutorKind {
if roleName == "user" {
return ExecutorKindHuman
}
return ExecutorKindCodex
}
func NormalizeDefinition(value Definition) Definition {
if value.ExecutorKind == "" {
value.ExecutorKind = DefaultExecutorKind(value.Name)
}
return value
}
type Config struct {
RoleName string `json:"role_name"`
ConfigTOML string `json:"config_toml"`
AuthJSON string `json:"auth_json"`
}
func (c Config) Validate() error {
if strings.TrimSpace(c.RoleName) == "" {
return errors.New("role name is required")
}
if strings.TrimSpace(c.AuthJSON) == "" {
return nil
}
if !json.Valid([]byte(c.AuthJSON)) {
return fmt.Errorf("role auth_json must be valid json")
}
return nil
}
func NormalizeConfig(value Config) Config {
value.RoleName = strings.TrimSpace(value.RoleName)
value.ConfigTOML = strings.TrimSpace(value.ConfigTOML)
if strings.TrimSpace(value.AuthJSON) == "" {
value.AuthJSON = "{}"
}
return value
}
type Prompt struct {
ID string `json:"id"`
RoleName string `json:"role_name"`
WorkspaceID string `json:"workspace_id,omitempty"`
PromptKind PromptKind `json:"prompt_kind"`
ContentMarkdown string `json:"content_markdown"`
Version int `json:"version"`
UpdatedBy string `json:"updated_by"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (p Prompt) Validate() error {
if p.RoleName == "" {
return errors.New("role name is required")
}
if p.ContentMarkdown == "" {
return errors.New("prompt content is required")
}
switch p.PromptKind {
case PromptSystem:
default:
return fmt.Errorf("invalid prompt kind %q", p.PromptKind)
}
return nil
}
type SkillBinding struct {
ID string `json:"id"`
RoleName string `json:"role_name"`
WorkspaceID string `json:"workspace_id,omitempty"`
SkillID string `json:"skill_id"`
IsEnabled bool `json:"is_enabled"`
SortOrder int `json:"sort_order"`
Config map[string]any `json:"config,omitempty"`
Version int `json:"version"`
UpdatedBy string `json:"updated_by"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (b SkillBinding) Validate() error {
if b.RoleName == "" {
return errors.New("role name is required")
}
if b.SkillID == "" {
return errors.New("skill id is required")
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package skill
import "errors"
type Definition struct {
ID string `json:"id"`
SkillKey string `json:"skill_key"`
Name string `json:"name"`
Description string `json:"description"`
SourceType string `json:"source_type"`
ContentMarkdown string `json:"content_markdown"`
Status string `json:"status"`
Version int `json:"version"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (s Definition) Validate() error {
if s.SkillKey == "" {
return errors.New("skill key is required")
}
if s.Name == "" {
return errors.New("skill name is required")
}
return nil
}
+139
View File
@@ -0,0 +1,139 @@
package task
import "fmt"
type Status string
const (
StatusDraft Status = "draft"
StatusReady Status = "ready"
StatusRunning Status = "running"
StatusBlocked Status = "blocked"
StatusSucceeded Status = "succeeded"
StatusFailed Status = "failed"
StatusCancelled Status = "cancelled"
)
type Kind string
const (
KindExecution Kind = "execution"
KindGate Kind = "gate"
KindVerification Kind = "verification"
KindMilestone Kind = "milestone"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
LaneID string `json:"lane_id"`
Title string `json:"title"`
BodyMarkdown string `json:"body_markdown"`
AcceptanceMarkdown string `json:"acceptance_markdown,omitempty"`
Kind Kind `json:"kind"`
Deliverables []string `json:"deliverables,omitempty"`
BatchKey string `json:"batch_key,omitempty"`
Status Status `json:"status"`
Priority int `json:"priority"`
TaskOrder int `json:"task_order"`
CreatedByRoleName string `json:"created_by_role_name"`
BlockingReasonMarkdown string `json:"blocking_reason_markdown,omitempty"`
ResultSummaryMarkdown string `json:"result_summary_markdown,omitempty"`
AssignedRunID string `json:"assigned_run_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
StartedAt string `json:"started_at,omitempty"`
CompletedAt string `json:"completed_at,omitempty"`
}
type Dependency struct {
TaskID string `json:"task_id"`
DependsOnTaskID string `json:"depends_on_task_id"`
}
type Event struct {
ID string `json:"id"`
TaskID string `json:"task_id"`
EventType string `json:"event_type"`
BodyMarkdown string `json:"body_markdown,omitempty"`
CreatedByRoleName string `json:"created_by_role_name"`
CreatedAt string `json:"created_at"`
}
func ValidateStatus(value Status) error {
switch value {
case StatusDraft, StatusReady, StatusRunning, StatusBlocked, StatusSucceeded, StatusFailed, StatusCancelled:
return nil
default:
return fmt.Errorf("invalid task status %q", value)
}
}
func ValidateKind(value Kind) error {
switch value {
case KindExecution, KindGate, KindVerification, KindMilestone:
return nil
default:
return fmt.Errorf("invalid task kind %q", value)
}
}
func NormalizeRecord(value Record) Record {
if value.Kind == "" {
value.Kind = KindExecution
}
return value
}
func (r Record) Validate() error {
r = NormalizeRecord(r)
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.LaneID == "" {
return fmt.Errorf("lane id is required")
}
if r.Title == "" {
return fmt.Errorf("task title is required")
}
if r.BodyMarkdown == "" {
return fmt.Errorf("task body is required")
}
if r.CreatedByRoleName == "" {
return fmt.Errorf("created by role is required")
}
if err := ValidateKind(r.Kind); err != nil {
return err
}
return ValidateStatus(r.Status)
}
func (d Dependency) Validate() error {
if d.TaskID == "" {
return fmt.Errorf("task id is required")
}
if d.DependsOnTaskID == "" {
return fmt.Errorf("depends_on task id is required")
}
if d.TaskID == d.DependsOnTaskID {
return fmt.Errorf("task cannot depend on itself")
}
return nil
}
func (e Event) Validate() error {
if e.TaskID == "" {
return fmt.Errorf("task id is required")
}
if e.EventType == "" {
return fmt.Errorf("event type is required")
}
if e.CreatedByRoleName == "" {
return fmt.Errorf("created by role is required")
}
return nil
}
@@ -0,0 +1,50 @@
package taskgraph
import "fmt"
type Status string
const (
StatusDraft Status = "draft"
StatusActive Status = "active"
StatusSuperseded Status = "superseded"
StatusCancelled Status = "cancelled"
)
type Record struct {
ID string `json:"id"`
TopicID string `json:"topic_id"`
Version int `json:"version"`
Status Status `json:"status"`
PlanJSON string `json:"plan_json"`
PlanSummaryMarkdown string `json:"plan_summary_markdown"`
CreatedByRoleName string `json:"created_by_role_name"`
CreatedAt string `json:"created_at"`
ConfirmedAt string `json:"confirmed_at,omitempty"`
SupersedesGraphVersionID string `json:"supersedes_graph_version_id,omitempty"`
}
func ValidateStatus(value Status) error {
switch value {
case StatusDraft, StatusActive, StatusSuperseded, StatusCancelled:
return nil
default:
return fmt.Errorf("invalid task graph status %q", value)
}
}
func (r Record) Validate() error {
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.Version <= 0 {
return fmt.Errorf("version must be greater than zero")
}
if r.PlanJSON == "" {
return fmt.Errorf("plan json is required")
}
if r.CreatedByRoleName == "" {
return fmt.Errorf("created by role is required")
}
return ValidateStatus(r.Status)
}
+44
View File
@@ -0,0 +1,44 @@
package topic
import "fmt"
type Space string
const (
SpaceClarify Space = "clarify"
SpaceWorkflow Space = "workflow"
)
type Record struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Slug string `json:"slug"`
Title string `json:"title"`
Space Space `json:"space"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ClosedAt string `json:"closed_at,omitempty"`
}
func (r Record) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.Slug == "" {
return fmt.Errorf("topic slug is required")
}
if r.Title == "" {
return fmt.Errorf("topic title is required")
}
switch r.Space {
case SpaceClarify, SpaceWorkflow:
default:
return fmt.Errorf("invalid topic space %q", r.Space)
}
if r.Status == "" {
return fmt.Errorf("topic status is required")
}
return nil
}
+137
View File
@@ -0,0 +1,137 @@
package workflow
import "fmt"
type Stage string
const (
StageClarification Stage = "clarification"
StageFreeze Stage = "freeze"
StagePlan Stage = "plan"
StageReview Stage = "review"
StageExecution Stage = "execution"
StageVerification Stage = "verification"
)
type RunStatus string
const (
RunStatusRunning RunStatus = "running"
RunStatusSucceeded RunStatus = "succeeded"
RunStatusFailed RunStatus = "failed"
RunStatusCancelled RunStatus = "cancelled"
)
type LogStream string
const (
LogStreamStdout LogStream = "stdout"
LogStreamStderr LogStream = "stderr"
LogStreamSystem LogStream = "system"
)
var stageTransitions = map[Stage][]Stage{
StageClarification: {StagePlan, StageFreeze},
StageFreeze: {StageExecution},
StagePlan: {StageReview, StageExecution},
StageReview: {StageExecution},
StageExecution: {StageVerification},
StageVerification: {},
}
func ValidateRunStatus(status RunStatus) error {
switch status {
case RunStatusRunning, RunStatusSucceeded, RunStatusFailed, RunStatusCancelled:
return nil
default:
return fmt.Errorf("invalid run status %q", status)
}
}
func CanTransition(from, to Stage) bool {
for _, candidate := range stageTransitions[from] {
if candidate == to {
return true
}
}
return false
}
func ValidateStage(stage Stage) error {
switch stage {
case StageClarification, StageFreeze, StagePlan, StageReview, StageExecution, StageVerification:
return nil
default:
return fmt.Errorf("invalid workflow stage %q", stage)
}
}
type Run struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
TopicID string `json:"topic_id"`
RoleName string `json:"role_name"`
Stage Stage `json:"stage"`
Mode string `json:"mode"`
Status RunStatus `json:"status"`
RequestMessageID string `json:"request_message_id,omitempty"`
ConfigSnapshotJSON string `json:"config_snapshot_json"`
CommandJSON string `json:"command_json"`
ReplyMessageID string `json:"reply_message_id,omitempty"`
ExitCode int `json:"exit_code"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
func (r Run) Validate() error {
if r.WorkspaceID == "" {
return fmt.Errorf("workspace id is required")
}
if r.TopicID == "" {
return fmt.Errorf("topic id is required")
}
if r.RoleName == "" {
return fmt.Errorf("role name is required")
}
if err := ValidateStage(r.Stage); err != nil {
return err
}
if err := ValidateRunStatus(r.Status); err != nil {
return err
}
return nil
}
type RunLog struct {
RunID string `json:"run_id"`
Seq int `json:"seq"`
Stream LogStream `json:"stream"`
Content string `json:"content"`
CreatedAt string `json:"created_at"`
}
func ValidateLogStream(stream LogStream) error {
switch stream {
case LogStreamStdout, LogStreamStderr, LogStreamSystem:
return nil
default:
return fmt.Errorf("invalid log stream %q", stream)
}
}
func (l RunLog) Validate() error {
if l.RunID == "" {
return fmt.Errorf("run id is required")
}
if err := ValidateLogStream(l.Stream); err != nil {
return err
}
if l.Content == "" {
return fmt.Errorf("log content is required")
}
if l.Seq < 0 {
return fmt.Errorf("log seq must be >= 0")
}
return nil
}
@@ -0,0 +1,36 @@
package workflow
import "testing"
func TestCanTransition(t *testing.T) {
if !CanTransition(StagePlan, StageReview) {
t.Fatal("expected plan -> review to be allowed")
}
if CanTransition(StageVerification, StagePlan) {
t.Fatal("expected verification -> plan to be disallowed")
}
}
func TestRunValidateRejectsInvalidStatus(t *testing.T) {
run := Run{
WorkspaceID: "ws_1",
TopicID: "topic_1",
RoleName: "backend",
Stage: StageExecution,
Status: RunStatus("broken"),
}
if err := run.Validate(); err == nil {
t.Fatal("Validate() expected invalid status error")
}
}
func TestRunLogValidate(t *testing.T) {
log := RunLog{
RunID: "run_1",
Stream: LogStreamStdout,
Content: "hello",
}
if err := log.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
@@ -0,0 +1,135 @@
package workspace
import (
"fmt"
"path/filepath"
"strings"
)
const DefaultBranch = "main"
const ActiveStatus = "active"
const ManagedRuntimeBackend = "host"
const PendingProvisionState = "pending"
type Project struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
RootPath string `json:"root_path"`
DefaultBranch string `json:"default_branch"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (p Project) Validate() error {
if p.Slug == "" {
return fmt.Errorf("project slug is required")
}
if p.Name == "" {
return fmt.Errorf("project name is required")
}
if p.RootPath == "" {
return fmt.Errorf("project root path is required")
}
if p.DefaultBranch == "" {
return fmt.Errorf("project default branch is required")
}
if p.Status == "" {
return fmt.Errorf("project status is required")
}
return nil
}
func NormalizeProjectForCreate(value Project) Project {
if strings.TrimSpace(value.DefaultBranch) == "" {
value.DefaultBranch = DefaultBranch
}
if strings.TrimSpace(value.Status) == "" {
value.Status = ActiveStatus
}
return value
}
type Workspace struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Slug string `json:"slug"`
Name string `json:"name"`
RootPath string `json:"root_path"`
BaseBranch string `json:"base_branch"`
WorktreeBranch string `json:"worktree_branch"`
RuntimeBackend string `json:"runtime_backend"`
Status string `json:"status"`
ProvisionState string `json:"provision_state"`
ProvisionError string `json:"provision_error"`
LastProvisionedAt string `json:"last_provisioned_at,omitempty"`
ContainerState string `json:"container_state,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (w Workspace) Validate() error {
if w.ProjectID == "" {
return fmt.Errorf("workspace project id is required")
}
if w.Slug == "" {
return fmt.Errorf("workspace slug is required")
}
if w.Name == "" {
return fmt.Errorf("workspace name is required")
}
if w.RootPath == "" {
return fmt.Errorf("workspace root path is required")
}
if w.BaseBranch == "" {
return fmt.Errorf("workspace base branch is required")
}
if w.WorktreeBranch == "" {
return fmt.Errorf("workspace worktree branch is required")
}
if w.RuntimeBackend == "" {
return fmt.Errorf("workspace runtime backend is required")
}
if w.Status == "" {
return fmt.Errorf("workspace status is required")
}
if w.ProvisionState == "" {
return fmt.Errorf("workspace provision state is required")
}
return nil
}
func NormalizeWorkspaceForCreate(value Workspace) Workspace {
if strings.TrimSpace(value.BaseBranch) == "" {
value.BaseBranch = DefaultBranch
}
if strings.TrimSpace(value.WorktreeBranch) == "" {
value.WorktreeBranch = DefaultWorktreeBranch(value.Slug)
}
if strings.TrimSpace(value.RuntimeBackend) == "" {
value.RuntimeBackend = ManagedRuntimeBackend
}
if strings.TrimSpace(value.Status) == "" {
value.Status = ActiveStatus
}
if strings.TrimSpace(value.ProvisionState) == "" {
value.ProvisionState = PendingProvisionState
}
return value
}
func ApplyManagedRuntimeConfig(value Workspace, workspacesDir, baseBranch string) Workspace {
if dir := strings.TrimSpace(workspacesDir); dir != "" {
value.RootPath = filepath.Join(dir, value.Slug)
}
value.BaseBranch = strings.TrimSpace(baseBranch)
value.WorktreeBranch = DefaultWorktreeBranch(value.Slug)
value.RuntimeBackend = ManagedRuntimeBackend
value.Status = ActiveStatus
return value
}
func DefaultWorktreeBranch(slug string) string {
return "worktree/" + strings.TrimSpace(slug)
}
@@ -0,0 +1,45 @@
package workspace
import "testing"
func TestNormalizeProjectForCreateAppliesDefaults(t *testing.T) {
project := NormalizeProjectForCreate(Project{})
if project.DefaultBranch != DefaultBranch {
t.Fatalf("default branch = %q", project.DefaultBranch)
}
if project.Status != ActiveStatus {
t.Fatalf("status = %q", project.Status)
}
}
func TestNormalizeWorkspaceForCreateAppliesDefaults(t *testing.T) {
ws := NormalizeWorkspaceForCreate(Workspace{Slug: "blog"})
if ws.BaseBranch != DefaultBranch {
t.Fatalf("base branch = %q", ws.BaseBranch)
}
if ws.WorktreeBranch != "worktree/blog" {
t.Fatalf("worktree branch = %q", ws.WorktreeBranch)
}
if ws.RuntimeBackend != ManagedRuntimeBackend {
t.Fatalf("runtime backend = %q", ws.RuntimeBackend)
}
if ws.Status != ActiveStatus {
t.Fatalf("status = %q", ws.Status)
}
if ws.ProvisionState != PendingProvisionState {
t.Fatalf("provision state = %q", ws.ProvisionState)
}
}
func TestApplyManagedRuntimeConfig(t *testing.T) {
ws := ApplyManagedRuntimeConfig(Workspace{Slug: "blog"}, "/tmp/workspaces", "release")
if ws.RootPath != "/tmp/workspaces/blog" {
t.Fatalf("root path = %q", ws.RootPath)
}
if ws.BaseBranch != "release" {
t.Fatalf("base branch = %q", ws.BaseBranch)
}
if ws.WorktreeBranch != "worktree/blog" {
t.Fatalf("worktree branch = %q", ws.WorktreeBranch)
}
}