nomadcode/services/core/internal/roadmapsync/plane_projection.go
toki fad5627ae6 feat: milestone work item creation/sync 구현 및 roadmap sync 기능 추가
- Plane Todo projection 및 idempotency/retry 메커니즘 구현
- Roadmap sync identity 및 step 모델/DB 마이그레이션/쿼리 추가
- OpenAI 및 Plane adapter 개선
- roadmaps sync package: plane_format, plane_projection, retry 모듈 추가
- Storage layer: roadmap_sync_identities, roadmap_sync_steps 추가
- agent-task 아카이브 정리
2026-06-14 04:08:15 +09:00

102 lines
3.9 KiB
Go

package roadmapsync
import (
"context"
"errors"
"strings"
"github.com/nomadcode/nomadcode-core/internal/workitem"
)
// ErrNotReady is returned when the develop projection candidate is not ready,
// so a caller never silently projects a Plane Todo for an unmatched or unpushed
// Milestone.
var ErrNotReady = errors.New("develop projection candidate is not ready")
// ErrInvalidProjectionInput is returned when a required projection input
// (provider ref, original body, todo state, or milestone content) is missing.
var ErrInvalidProjectionInput = errors.New("invalid plane todo projection input")
// preserveCommentPrefix opens the comment that preserves the original Plane
// body before the develop content replaces it. Kept as a stable prefix so the
// preserved request is recognizable in the Plane timeline.
const preserveCommentPrefix = "사용자 요청:"
// PlaneTodoProvider is the minimal set of provider facets the Todo projection
// drives, in execution order: preserve the original body as a comment, replace
// title/body, then move the work item to the Todo state. It is satisfied by the
// Plane adapter Client and by test fakes.
type PlaneTodoProvider interface {
workitem.Commenter
workitem.BodyProjector
workitem.StatusProjector
}
// ProjectPlaneTodoInput carries everything ProjectPlaneTodo needs without
// reaching into provider DTOs: the develop match decision, the original Plane
// body to preserve, the Todo state id to move to, and the develop Milestone
// markdown to project. Ref is the provider work item the projection targets.
type ProjectPlaneTodoInput struct {
Candidate ProjectionCandidate
Ref workitem.Ref
OriginalBody string
TodoStateID string
// MilestoneMarkdown is the develop Milestone content used to build the new
// Plane title/body via FormatPlaneProjection.
MilestoneMarkdown string
// ExternalSource/ExternalID tag the preservation comment so a re-run can be
// recognized as nomadcode-authored rather than a user request.
ExternalSource string
ExternalID string
}
// ProjectPlaneTodo runs the Plane Todo projection in the order the Milestone
// requires (doc lines 57/102): preserve the original body as a `사용자 요청:`
// comment, then replace the title/body with the develop Milestone content, then
// move the work item to the Todo state. A failure at any step stops the
// sequence so a later step never runs on an unpreserved or unupdated ticket.
//
// When the develop projection candidate is not ready it returns ErrNotReady
// without calling the provider, so a Plane Todo is never produced for an
// unmatched or unpushed Milestone.
func ProjectPlaneTodo(ctx context.Context, p PlaneTodoProvider, in ProjectPlaneTodoInput) error {
if !in.Candidate.Ready {
return ErrNotReady
}
if p == nil ||
strings.TrimSpace(string(in.Ref.Provider)) == "" ||
strings.TrimSpace(in.Ref.ID) == "" ||
strings.TrimSpace(in.OriginalBody) == "" ||
strings.TrimSpace(in.TodoStateID) == "" ||
strings.TrimSpace(in.MilestoneMarkdown) == "" {
return ErrInvalidProjectionInput
}
projection := FormatPlaneProjection(in.Candidate.Identity.RoadmapMilestonePath, in.MilestoneMarkdown)
// 1. Preserve the original Plane body as a comment before anything mutates it.
if err := p.AppendComment(ctx, workitem.CommentInput{
Ref: in.Ref,
Body: preserveCommentPrefix + " " + in.OriginalBody,
Format: workitem.CommentFormatHTML,
ExternalSource: in.ExternalSource,
ExternalID: in.ExternalID,
}); err != nil {
return err
}
// 2. Replace title/body with the develop Milestone projection.
if err := p.ProjectBody(ctx, workitem.BodyProjection{
Ref: in.Ref,
Title: projection.Title,
DescriptionHTML: projection.BodyHTML,
}); err != nil {
return err
}
// 3. Move the work item to the Todo state.
return p.ProjectStatus(ctx, workitem.StatusProjection{
Ref: in.Ref,
ProviderState: in.TodoStateID,
})
}