// Package project 承载 Project / Requirement / Issue 三个 PM 聚合根的领域类型与 // Service 接口契约。本文件只做类型与签名声明,业务实现在 *_service.go。 // // 三个聚合根共享同一个 Go 包是有意为之:它们在 schema 上紧耦合(Issue 引用 // Requirement,Requirement 属于 Project),且对外只暴露一组 chi route,拆包 // 反而要互相 import。 package project import ( "context" "time" "github.com/google/uuid" ) // Phase 是 Requirement 的六阶段枚举。phase 不强制单向流动,可任意跳跃/回退。 // 字符串常量保持与 DB CHECK 约束一致。 type Phase string // 六阶段枚举值,与最新 phase CHECK 约束一致(prototyping 由 migrations/0011 加入)。 const ( PhasePlanning Phase = "planning" PhasePrototyping Phase = "prototyping" PhaseAuditing Phase = "auditing" PhaseImplementing Phase = "implementing" PhaseReviewing Phase = "reviewing" PhaseDone Phase = "done" ) // AllPhases 用于校验 phase 字符串合法性,顺序也对应前端看板/流转条的列序。 var AllPhases = []Phase{PhasePlanning, PhasePrototyping, PhaseAuditing, PhaseImplementing, PhaseReviewing, PhaseDone} // IsValid 报告 p 是否在枚举集合内。 func (p Phase) IsValid() bool { for _, x := range AllPhases { if p == x { return true } } return false } // ArtifactPhases 是允许产出文档型产物的五个阶段(与 requirement_artifacts.phase // CHECK 约束一致):implementing/reviewing 的产出是审计报告 / 评审记录(也是文档型 // 产物),done 阶段是汇总视图、不单独建产物,故只留前五阶段。 var ArtifactPhases = []Phase{PhasePlanning, PhasePrototyping, PhaseAuditing, PhaseImplementing, PhaseReviewing} // IsArtifactPhase 报告 p 是否允许创建产物版本。 func (p Phase) IsArtifactPhase() bool { for _, x := range ArtifactPhases { if p == x { return true } } return false } // Verdict 是单个产物版本的评审结论,与 requirement_artifacts.verdict CHECK 一致。 // 'none' 为默认(未评审);'pass' 表示通过、可作为阶段网关的"已批准产物";'fail' // 表示驳回。仅 'pass' 会被 ArtifactService.Approve 写入阶段指针。 type Verdict string // 评审结论枚举值。 const ( VerdictNone Verdict = "none" VerdictPass Verdict = "pass" VerdictFail Verdict = "fail" ) // IsValid 报告 v 是否在枚举集合内。 func (v Verdict) IsValid() bool { return v == VerdictNone || v == VerdictPass || v == VerdictFail } // Status 是 Requirement / Issue 通用的开/关状态。 type Status string // 开/关状态枚举值,与 DB CHECK 约束一致。 const ( StatusOpen Status = "open" StatusClosed Status = "closed" ) // Visibility 控制 Project 的可见域:private 仅 owner+admin 可读, // internal 所有登录用户可读;写权限两者都仅限 owner+admin。 type Visibility string // 可见性枚举值,与 DB CHECK 约束一致。 const ( VisibilityPrivate Visibility = "private" VisibilityInternal Visibility = "internal" ) // IsValid 报告 v 是否在枚举集合内。 func (v Visibility) IsValid() bool { return v == VisibilityPrivate || v == VisibilityInternal } // Project 是顶层聚合根。ArchivedAt 为 nil 表示未归档。 type Project struct { ID uuid.UUID Slug string Name string Description string Visibility Visibility OwnerID uuid.UUID ArchivedAt *time.Time CreatedAt time.Time UpdatedAt time.Time } // Requirement 表示一个项目内的需求。Number 在 (project_id) 内自增。 // WorkspaceID 为 nil 表示未关联任何 workspace;非 nil 时由 Service 层保证 // 该 workspace 与本 requirement 同 project(Plan 3 引入)。 type Requirement struct { ID uuid.UUID ProjectID uuid.UUID WorkspaceID *uuid.UUID Number int Title string Description string Phase Phase Status Status OwnerID uuid.UUID ClosedAt *time.Time CreatedAt time.Time UpdatedAt time.Time } // Issue 可挂载到某个 Requirement,也可游离(RequirementID == nil)。 // AssigneeID 为 nil 表示未指派。CreatedBy 永远存在(DELETE RESTRICT)。 // WorkspaceID 为 nil 表示未关联 workspace;非 nil 时 DB 通过复合外键 // (project_id, workspace_id) 拒绝跨 project 关联(Plan 3 引入)。 type Issue struct { ID uuid.UUID ProjectID uuid.UUID RequirementID *uuid.UUID WorkspaceID *uuid.UUID Number int Title string Description string Status Status AssigneeID *uuid.UUID CreatedBy uuid.UUID ClosedAt *time.Time CreatedAt time.Time UpdatedAt time.Time // ParentID 非 nil 表示本 issue 是某 issue 的子任务(任务分解)。父删除时 // ON DELETE SET NULL 置空(orphan,不级联删子)。 ParentID *uuid.UUID // Priority 0..3,越大越早被调度器选取。默认 0(normal)。 Priority int } // Dependency 表示一条 issue 依赖边:BlockedID 被 BlockerID 阻塞(blocked-by)。 // BlockedID 仅在 BlockerID 关闭后才"就绪"。ProjectID 冗余存储用于作用域校验。 type Dependency struct { ID uuid.UUID ProjectID uuid.UUID BlockedID uuid.UUID BlockerID uuid.UUID CreatedBy uuid.UUID CreatedAt time.Time } // Artifact 是某条 Requirement 在某阶段的一个产物文档版本快照。Version 在 // (RequirementID, Phase) 内自增(max+1),CreatedBy 永远存在(DELETE RESTRICT)。 // SourceMessageID 非 nil 时指向该产物派生自的 chat assistant 消息(仅追溯用, // schema 层 FK ON DELETE SET NULL,代码层不依赖 chat 模块)。 type Artifact struct { ID uuid.UUID RequirementID uuid.UUID Phase Phase Version int Content string Note string SourceMessageID *int64 CreatedBy uuid.UUID CreatedAt time.Time // Verdict 是该版本的评审结论(none/pass/fail)。新建产物默认 none。 Verdict Verdict } // PhasePointer 是某条 Requirement 在某阶段的"已批准/当前"产物指针。仅在首次 // Approve(verdict=pass) 后才存在一行;ApprovedArtifactID 在被指向的产物删除时 // 由 DB(ON DELETE SET NULL)置空。Done 等阶段网关据此判定前置产物是否就绪。 type PhasePointer struct { RequirementID uuid.UUID Phase Phase ApprovedArtifactID *uuid.UUID ApprovedBy *uuid.UUID ApprovedAt *time.Time } // Caller 表示发起请求的用户上下文。Service 层据此判定鉴权。 type Caller struct { UserID uuid.UUID IsAdmin bool } // Role 是 project_members.role 的角色枚举(与 0026 迁移的 CHECK 约束一致)。 // 语义(在 owner+global-admin+internal-visibility 既有规则之上"叠加"授权, // 只增不减): // // admin 读 + 写 + 成员管理(owner 经迁移回填为本角色) // member 读 + 写(需求/issue 等) // viewer 只读 // // RoleNone 是非成员的零值,仅在代码内部表示"无成员关系",不写入 DB。 type Role string // 角色枚举值。注意 DB CHECK 仅允许 viewer/member/admin;RoleNone 不入库。 const ( RoleNone Role = "" RoleViewer Role = "viewer" RoleMember Role = "member" RoleAdmin Role = "admin" ) // IsValid 报告 r 是否为可写入 DB 的合法角色(不含 RoleNone)。 func (r Role) IsValid() bool { return r == RoleViewer || r == RoleMember || r == RoleAdmin } // CanWrite 报告该角色是否授予写权限(member/admin)。 func (r Role) CanWrite() bool { return r == RoleMember || r == RoleAdmin } // CanRead 报告该角色是否授予读权限(任何已建立的成员关系都可读)。 func (r Role) CanRead() bool { return r == RoleViewer || r == RoleMember || r == RoleAdmin } // CanManageMembers 报告该角色是否可管理项目成员(仅 admin)。 func (r Role) CanManageMembers() bool { return r == RoleAdmin } // Member 是一条 project_members 关系:某用户在某 project 的角色。 type Member struct { ProjectID uuid.UUID UserID uuid.UUID Role Role AddedBy *uuid.UUID CreatedAt time.Time } // ProjectService 暴露 Project 聚合根的应用服务方法。包内有三个并列服务 // (Project/Requirement/Issue),这里保留聚合名前缀以避免歧义。 // //nolint:revive // 名字带聚合根前缀比 "Service" 更准确:包里有 3 个并列服务接口 type ProjectService interface { Create(ctx context.Context, c Caller, in CreateProjectInput) (*Project, error) Get(ctx context.Context, c Caller, slug string) (*Project, error) GetByID(ctx context.Context, c Caller, id uuid.UUID) (*Project, error) List(ctx context.Context, c Caller, includeArchived bool) ([]*Project, error) Update(ctx context.Context, c Caller, slug string, in UpdateProjectInput) (*Project, error) Archive(ctx context.Context, c Caller, slug string) error Unarchive(ctx context.Context, c Caller, slug string) error // ===== 成员/角色 ACL(autonomy roadmap §11)===== // ListMembers 返回 slug 项目的全部成员。要求 caller 对项目可读。 ListMembers(ctx context.Context, c Caller, slug string) ([]*Member, error) // AddMember 新增或更新一名成员的角色。仅 project-admin 或 global-admin 可调用。 AddMember(ctx context.Context, c Caller, slug string, in AddMemberInput) (*Member, error) // RemoveMember 移除一名成员。仅 project-admin 或 global-admin 可调用。 RemoveMember(ctx context.Context, c Caller, slug string, userID uuid.UUID) error // MemberRole 返回 userID 在 projectID 的成员角色(非成员返回 RoleNone)。 // 供 app 层 projectACL 桥接成员感知鉴权;不做可见性校验(调用方已校验)。 MemberRole(ctx context.Context, projectID, userID uuid.UUID) (Role, error) } // RequirementService 暴露 Requirement 聚合根的应用服务方法。 type RequirementService interface { Create(ctx context.Context, c Caller, projectSlug string, in CreateRequirementInput) (*Requirement, error) Get(ctx context.Context, c Caller, projectSlug string, number int) (*Requirement, error) List(ctx context.Context, c Caller, projectSlug string, filter RequirementFilter) ([]*Requirement, error) Update(ctx context.Context, c Caller, projectSlug string, number int, in UpdateRequirementInput) (*Requirement, error) ChangePhase(ctx context.Context, c Caller, projectSlug string, number int, to Phase) (*Requirement, error) Close(ctx context.Context, c Caller, projectSlug string, number int) error Reopen(ctx context.Context, c Caller, projectSlug string, number int) error } // IssueService 暴露 Issue 聚合根的应用服务方法。 type IssueService interface { Create(ctx context.Context, c Caller, projectSlug string, in CreateIssueInput) (*Issue, error) Get(ctx context.Context, c Caller, projectSlug string, number int) (*Issue, error) List(ctx context.Context, c Caller, projectSlug string, filter IssueFilter) ([]*Issue, error) Update(ctx context.Context, c Caller, projectSlug string, number int, in UpdateIssueInput) (*Issue, error) Assign(ctx context.Context, c Caller, projectSlug string, number int, assignee *uuid.UUID) (*Issue, error) Close(ctx context.Context, c Caller, projectSlug string, number int) error Reopen(ctx context.Context, c Caller, projectSlug string, number int) error // ===== 任务分解 / 依赖图(autonomy roadmap §10)===== // CreateSubtask 在 parentNumber 指向的 issue 下创建一个子任务(设置 parent_id + // priority),继承 project。父 issue 须与目标 project 同属。 CreateSubtask(ctx context.Context, c Caller, projectSlug string, parentNumber int, in CreateIssueInput) (*Issue, error) // AddDependency 新增一条依赖边:blocked 被 blocker 阻塞。拒绝自依赖、跨 project、 // 重复、以及会形成环的边(沿 blocked-by 边做可达性检查)。 AddDependency(ctx context.Context, c Caller, projectSlug string, in AddDependencyInput) (*Dependency, error) // RemoveDependency 删除一条依赖边(plan 修正用)。边不存在返回 NotFound。 RemoveDependency(ctx context.Context, c Caller, projectSlug string, blockedNumber, blockerNumber int) error // ListDependencies 返回 number 指向 issue 的 (被谁阻塞 blockedBy, 阻塞了谁 blocks)。 ListDependencies(ctx context.Context, c Caller, projectSlug string, number int) (blockedBy []*Issue, blocks []*Issue, err error) } // ArtifactService 暴露 Requirement 阶段产物版本的应用服务方法。 type ArtifactService interface { Create(ctx context.Context, c Caller, projectSlug string, reqNumber int, in CreateArtifactInput) (*Artifact, error) // List 的 phase 传零值("")表示返回全部阶段产物(done 阶段汇总视图用)。 List(ctx context.Context, c Caller, projectSlug string, reqNumber int, phase Phase) ([]*Artifact, error) GetByVersion(ctx context.Context, c Caller, projectSlug string, reqNumber int, phase Phase, version int) (*Artifact, error) // Approve 给指定 (phase, version) 产物打 verdict(pass/fail)。verdict==pass 时 // 同时把该阶段指针指向此产物(阶段网关据此放行后续 phase)。verdict==fail 仅 // 落 verdict、不动指针。已关闭 requirement 拒绝批准。 Approve(ctx context.Context, c Caller, projectSlug string, reqNumber int, phase Phase, version int, verdict Verdict) (*Artifact, error) } // PhaseChangeEvent 是一次 requirement phase 流转的领域事件载荷。下游自动化 // (如 webhook、触发 ACP run)通过 PhaseEventEmitter 订阅。 type PhaseChangeEvent struct { ProjectID uuid.UUID ProjectSlug string RequirementID uuid.UUID RequirementNumber int From Phase To Phase ActorID uuid.UUID // OwnerID 是该需求的 owner——phase 变更通知的主要接收人(owner 关心进度,而非 // 发起变更的 actor,后者可能是编排器/其它协作者)。 OwnerID uuid.UUID } // PhaseEventEmitter 把 phase 流转事件投递到通知/自动化总线。project 是领域模块, // 不直接 import infra/notify;由 app 层用 notify.Dispatcher 实现该窄接口。 // 实现必须是 fire-and-forget 语义:投递失败不得阻塞 phase 流转。 type PhaseEventEmitter interface { EmitPhaseChange(ctx context.Context, e PhaseChangeEvent) } // WorkspaceStateLookup 是 Done 网关查询 workspace PR/合并状态的窄接口。由 app // 层用 changerequest/workspace 服务实现,避免 project 直接依赖那些模块。 type WorkspaceStateLookup interface { // PullRequestMerged 报告该 requirement 是否已有合入主干的 PR(merged)。按 // requirementID 过滤而非整个 workspace,避免同一 workspace 下其它需求的已合 PR 误判。 PullRequestMerged(ctx context.Context, workspaceID, requirementID uuid.UUID) (bool, error) } // GateRunner 在 ChangePhase 写库前运行目标 phase 的所有 entry 网关,全部通过才 // 返回 nil;任一失败返回 CodePhaseGateFailed。实现见 phase_gate.go。 type GateRunner interface { Check(ctx context.Context, r *Requirement, from, to Phase) error } // ===== 输入 DTO ===== // // 约定(适用于全部 Update*Input):指针字段为 nil 表示"未提供,保持原值"; // 非 nil 表示"用解引用值替换"。Filter 类型使用零值(""、nil)表示"不过滤"。 // CreateProjectInput 携带创建 Project 必需的字段。Visibility 留空时由 Service 默认 private。 type CreateProjectInput struct { Slug string Name string Description string Visibility Visibility } // UpdateProjectInput 是 Project 的 patch 输入:每个非 nil 字段表示要替换的值; // nil 字段保持现有值不变。Slug / Owner / ArchivedAt 不在此处修改。 type UpdateProjectInput struct { Name *string Description *string Visibility *Visibility } // AddMemberInput 携带新增/更新一名 project 成员所需字段。Role 必须是合法角色 // (viewer/member/admin)。 type AddMemberInput struct { UserID uuid.UUID Role Role } // CreateRequirementInput 创建一条需求。OwnerID 为 nil 时由 Service 默认为调用者。 // WorkspaceID 为 nil 表示不关联任何 workspace;非 nil 时直接落库(FK 由 PG 校验)。 type CreateRequirementInput struct { Title string Description string OwnerID *uuid.UUID WorkspaceID *uuid.UUID } // UpdateRequirementInput 同 UpdateProjectInput 的 patch 语义。Phase / Status 不在此处修改, // 走专用方法 ChangePhase / Close / Reopen 以便 audit 区分。 // // WorkspaceID 走"uuid.Nil sentinel"三态语义(与 RequirementNumber 的 **int 不同): // // nil —— 未提供(保持原 workspace_id 不变) // &uuid.Nil —— 已提供,要求"解绑"(workspace_id 置 NULL) // &id (非 Nil) —— 已提供,要求关联到指定 workspace type UpdateRequirementInput struct { Title *string Description *string OwnerID *uuid.UUID WorkspaceID *uuid.UUID } // RequirementFilter 控制 List 时的过滤维度。Phase / Status 留零值("" / Status(""))表示 // 不限制;非零值必须落在对应枚举集合内(IsValid 校验在 Service 层做)。 type RequirementFilter struct { Phase Phase Status Status } // CreateIssueInput 创建 Issue。RequirementNumber == nil 表示游离 Issue(不挂载到任何 // Requirement);非 nil 时 Service 会校验 1) 该 Requirement 与目标 Project 同属, // 2) 未关闭。 // WorkspaceID 为 nil 表示不关联 workspace;非 nil 时 DB 复合 FK 保证同 project。 type CreateIssueInput struct { Title string Description string RequirementNumber *int AssigneeID *uuid.UUID WorkspaceID *uuid.UUID // ParentNumber 非 nil 表示创建为该编号 issue 的子任务(任务分解)。Service 校验 // 父 issue 与目标 project 同属。 ParentNumber *int // Priority 非 nil 时设置优先级(0..3);nil 时默认 0。 Priority *int } // AddDependencyInput 携带新增依赖边的两端 issue 编号。语义:BlockedNumber 被 // BlockerNumber 阻塞。两端须在同一 project(由 Service 在同 project 上下文解析保证)。 type AddDependencyInput struct { BlockedNumber int BlockerNumber int } // CreateArtifactInput 创建一个阶段产物版本。Version 由 Service 自动计算 // ((requirement, phase) 内 max+1),不在此处提供。Phase 必须是 ArtifactPhases // 之一;Note 可为空;SourceMessageID 仅在产物派生自 chat 消息时提供。 type CreateArtifactInput struct { Phase Phase Content string Note string SourceMessageID *int64 } // UpdateIssueInput 是 Issue 的 patch 输入。RequirementNumber 为指针的指针,承载三种状态: // // nil —— 未提供(不修改 issue.requirement_id) // *x == nil —— 已提供,但要求"取消挂载"(issue.requirement_id 置 NULL) // *x != nil —— 已提供,要求"挂载到本项目下 number=**x 的 Requirement" // // HTTP 层用 json.RawMessage 区分 "字段缺省 vs 显式 null vs 数字",再翻译为本三态。 // // WorkspaceID 走"uuid.Nil sentinel"三态语义(不再嵌一层指针): // // nil —— 未提供(保持原 workspace_id 不变) // &uuid.Nil —— 已提供,要求"解绑"(workspace_id 置 NULL) // &id (非 Nil) —— 已提供,要求关联到指定 workspace(DB FK 校验同 project) type UpdateIssueInput struct { Title *string Description *string RequirementNumber **int WorkspaceID *uuid.UUID } // IssueFilter 控制 List 时的过滤。Requirement 字段三态(与 sqlc requirement_filter // narg 对应): // // nil —— 不按 requirement 过滤 // *Requirement == 0 —— 仅游离(requirement_id IS NULL)—— 0 是 sentinel,因为 // Issue.Number 自增从 1 起 // *Requirement > 0 —— 仅匹配 number == *Requirement 的 Requirement // // AssigneeID nil 表示不按 assignee 过滤;Status 留零值表示不按状态过滤。 type IssueFilter struct { Status Status Requirement *int AssigneeID *uuid.UUID }