- gitoevents: Gito 이벤트 클라이언트 및 이벤트 모델 구현 - gitosync: 브리지, 러너, 스캐너 구현으로 브랜치 이벤트 및 작업 항목 동기화 - config: 새 설정 항목 추가 및 테스트 - roadmapsync: plane projection 수정 및 테스트 - agent-task: G07 관련 계획 및 코드 리뷰 로그 추가 - roadmap: PHASE 및 마일스톤 업데이트 - contracts: Flutter core API 후보 노트 업데이트
139 lines
5.5 KiB
Go
139 lines
5.5 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, 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 when present, 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 err := ValidateProjectionInput(p, in); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1. Preserve the original Plane body as a comment before anything mutates it.
|
|
if err := PreserveOriginalComment(ctx, p, in); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Replace title/body with the develop Milestone projection.
|
|
if err := UpdatePlaneBody(ctx, p, in); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 3. Move the work item to the Todo state.
|
|
return MovePlaneTodo(ctx, p, in)
|
|
}
|
|
|
|
// PreserveOriginalComment is step 1 of the Plane Todo projection: it preserves
|
|
// the original Plane body as a `사용자 요청:` comment before any later step
|
|
// overwrites the title/body. It is split out of ProjectPlaneTodo so an
|
|
// orchestrator can record a per-step ledger mark right after the provider call
|
|
// succeeds and skip the step on a resume. Inputs are not re-validated here;
|
|
// ProjectPlaneTodo and the orchestrator validate before calling.
|
|
func PreserveOriginalComment(ctx context.Context, p workitem.Commenter, in ProjectPlaneTodoInput) error {
|
|
body := preserveCommentPrefix
|
|
if strings.TrimSpace(in.OriginalBody) != "" {
|
|
body += " " + in.OriginalBody
|
|
}
|
|
return p.AppendComment(ctx, workitem.CommentInput{
|
|
Ref: in.Ref,
|
|
Body: body,
|
|
Format: workitem.CommentFormatHTML,
|
|
ExternalSource: in.ExternalSource,
|
|
ExternalID: in.ExternalID,
|
|
})
|
|
}
|
|
|
|
// UpdatePlaneBody is step 2 of the Plane Todo projection: it replaces the work
|
|
// item title/body with the develop Milestone projection. Split out for the same
|
|
// per-step ledger reason as PreserveOriginalComment.
|
|
func UpdatePlaneBody(ctx context.Context, p workitem.BodyProjector, in ProjectPlaneTodoInput) error {
|
|
projection := FormatPlaneProjection(in.Candidate.Identity.RoadmapMilestonePath, in.MilestoneMarkdown)
|
|
return p.ProjectBody(ctx, workitem.BodyProjection{
|
|
Ref: in.Ref,
|
|
Title: projection.Title,
|
|
DescriptionHTML: projection.BodyHTML,
|
|
})
|
|
}
|
|
|
|
// MovePlaneTodo is step 3 of the Plane Todo projection: it moves the work item
|
|
// to the Todo state. Split out for the same per-step ledger reason as
|
|
// PreserveOriginalComment.
|
|
func MovePlaneTodo(ctx context.Context, p workitem.StatusProjector, in ProjectPlaneTodoInput) error {
|
|
return p.ProjectStatus(ctx, workitem.StatusProjection{
|
|
Ref: in.Ref,
|
|
ProviderState: in.TodoStateID,
|
|
})
|
|
}
|
|
|
|
// ValidateProjectionInput reports the same input guard ProjectPlaneTodo
|
|
// enforces, so an orchestrator that drives the steps individually rejects the
|
|
// same missing-input cases before calling any provider facet.
|
|
func ValidateProjectionInput(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.TodoStateID) == "" ||
|
|
strings.TrimSpace(in.MilestoneMarkdown) == "" {
|
|
return ErrInvalidProjectionInput
|
|
}
|
|
return nil
|
|
}
|