You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -59,11 +59,20 @@ func (s *issueService) Create(ctx context.Context, c Caller, slug string, in Cre
|
||||
if r != nil {
|
||||
reqID = &r.ID
|
||||
}
|
||||
parentID, err := s.resolveParent(ctx, p, in.ParentNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prio, err := normalizePriority(in.Priority)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in1 := &Issue{
|
||||
ID: uuid.New(), ProjectID: p.ID, RequirementID: reqID,
|
||||
WorkspaceID: in.WorkspaceID,
|
||||
Title: in.Title, Description: in.Description,
|
||||
AssigneeID: in.AssigneeID, CreatedBy: c.UserID,
|
||||
ParentID: parentID, Priority: prio,
|
||||
}
|
||||
out, err := s.createWithRetry(ctx, in1)
|
||||
if err != nil {
|
||||
@@ -76,10 +85,38 @@ func (s *issueService) Create(ctx context.Context, c Caller, slug string, in Cre
|
||||
if in.WorkspaceID != nil {
|
||||
meta["workspace_id"] = in.WorkspaceID.String()
|
||||
}
|
||||
if parentID != nil {
|
||||
meta["parent_id"] = parentID.String()
|
||||
}
|
||||
s.recordAudit(ctx, c, "issue.create", out.ID.String(), meta)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// resolveParent 把 *number 解析为父 issue 的 *id,并校验同 project。number == nil →
|
||||
// (nil, nil)。父 issue 不存在 → NotFound。
|
||||
func (s *issueService) resolveParent(ctx context.Context, p *Project, number *int) (*uuid.UUID, error) {
|
||||
if number == nil {
|
||||
return nil, nil
|
||||
}
|
||||
parent, err := s.repo.GetIssueByNumber(ctx, p.ID, *number)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := parent.ID
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
// normalizePriority 校验 priority 在 0..3,nil → 0(默认)。
|
||||
func normalizePriority(p *int) (int, error) {
|
||||
if p == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if *p < 0 || *p > 3 {
|
||||
return 0, errs.New(errs.CodeInvalidInput, "priority 必须在 0..3")
|
||||
}
|
||||
return *p, nil
|
||||
}
|
||||
|
||||
func (s *issueService) createWithRetry(ctx context.Context, in *Issue) (*Issue, error) {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
out, err := s.repo.CreateIssue(ctx, in)
|
||||
@@ -267,6 +304,137 @@ func (s *issueService) Reopen(ctx context.Context, c Caller, slug string, number
|
||||
return nil
|
||||
}
|
||||
|
||||
// ===== 任务分解 / 依赖图(autonomy roadmap §10)=====
|
||||
|
||||
// CreateSubtask 在 parentNumber 指向的 issue 下创建子任务。复用 Create 的校验/重试/审计,
|
||||
// 把 parentNumber 注入 CreateIssueInput.ParentNumber。
|
||||
func (s *issueService) CreateSubtask(ctx context.Context, c Caller, slug string, parentNumber int, in CreateIssueInput) (*Issue, error) {
|
||||
pn := parentNumber
|
||||
in.ParentNumber = &pn
|
||||
out, err := s.Create(ctx, c, slug, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.recordAudit(ctx, c, "issue.create_subtask", out.ID.String(),
|
||||
map[string]any{"parent_number": parentNumber, "number": out.Number})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AddDependency 新增依赖边:blocked 被 blocker 阻塞。拒绝自依赖、跨 project、重复、成环。
|
||||
func (s *issueService) AddDependency(ctx context.Context, c Caller, slug string, in AddDependencyInput) (*Dependency, error) {
|
||||
if in.BlockedNumber == in.BlockerNumber {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "issue 不能依赖自身")
|
||||
}
|
||||
p, err := loadProjectForWrite(ctx, s.repo, c, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocked, err := s.repo.GetIssueByNumber(ctx, p.ID, in.BlockedNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocker, err := s.repo.GetIssueByNumber(ctx, p.ID, in.BlockerNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 环检查:新增 "blocked blocked-by blocker" 成环,当且仅当 blocked 已能沿 blocked-by
|
||||
// 边到达 blocker(即 blocker 已(传递地)依赖 blocked)。从 blocker 出发遍历其
|
||||
// blocker 集合,若到达 blocked 则成环。
|
||||
cyclic, err := s.reachable(ctx, blocker.ID, blocked.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cyclic {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "新增依赖会形成环")
|
||||
}
|
||||
dep := &Dependency{
|
||||
ID: uuid.New(), ProjectID: p.ID,
|
||||
BlockedID: blocked.ID, BlockerID: blocker.ID, CreatedBy: c.UserID,
|
||||
}
|
||||
out, err := s.repo.CreateDependency(ctx, dep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.recordAudit(ctx, c, "issue.add_dependency", out.ID.String(),
|
||||
map[string]any{"blocked_number": in.BlockedNumber, "blocker_number": in.BlockerNumber})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// reachable 报告从 start 出发沿 blocked-by 边(start 的 blocker、blocker 的 blocker …)
|
||||
// 是否能到达 target。迭代 BFS + visited 防环死循环。
|
||||
func (s *issueService) reachable(ctx context.Context, start, target uuid.UUID) (bool, error) {
|
||||
if start == target {
|
||||
return true, nil
|
||||
}
|
||||
visited := map[uuid.UUID]bool{start: true}
|
||||
queue := []uuid.UUID{start}
|
||||
for len(queue) > 0 {
|
||||
cur := queue[0]
|
||||
queue = queue[1:]
|
||||
blockers, err := s.repo.ListBlockerIDs(ctx, cur)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, b := range blockers {
|
||||
if b == target {
|
||||
return true, nil
|
||||
}
|
||||
if !visited[b] {
|
||||
visited[b] = true
|
||||
queue = append(queue, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// RemoveDependency 删除一条依赖边。边不存在返回 NotFound。
|
||||
func (s *issueService) RemoveDependency(ctx context.Context, c Caller, slug string, blockedNumber, blockerNumber int) error {
|
||||
p, err := loadProjectForWrite(ctx, s.repo, c, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blocked, err := s.repo.GetIssueByNumber(ctx, p.ID, blockedNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blocker, err := s.repo.GetIssueByNumber(ctx, p.ID, blockerNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := s.repo.DeleteDependency(ctx, p.ID, blocked.ID, blocker.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errs.New(errs.CodeNotFound, "依赖不存在")
|
||||
}
|
||||
s.recordAudit(ctx, c, "issue.remove_dependency", blocked.ID.String(),
|
||||
map[string]any{"blocked_number": blockedNumber, "blocker_number": blockerNumber})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDependencies 返回 (blockedBy: 阻塞本 issue 的 issues, blocks: 被本 issue 阻塞的 issues)。
|
||||
func (s *issueService) ListDependencies(ctx context.Context, c Caller, slug string, number int) ([]*Issue, []*Issue, error) {
|
||||
p, err := loadProjectForRead(ctx, s.repo, c, slug)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
iss, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
blockedBy, err := s.repo.ListBlockers(ctx, iss.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
blocks, err := s.repo.ListBlocked(ctx, iss.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return blockedBy, blocks, nil
|
||||
}
|
||||
|
||||
func (s *issueService) recordAudit(ctx context.Context, c Caller, action, targetID string, meta map[string]any) {
|
||||
if s.audit == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user