97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
// Package plancomments owns canonical prompt formatting for task plan feedback.
|
|
package plancomments
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
const placeholder = "\x00kandev-plan-comments\x00"
|
|
|
|
const (
|
|
MetadataRefs = "plan_comment_refs"
|
|
MetadataRequestFingerprint = "plan_comment_request_fingerprint"
|
|
MetadataClientQueueID = "client_queue_id"
|
|
MetadataClientMessageFingerprint = "client_message_request_fingerprint"
|
|
)
|
|
|
|
// WithPlaceholder reserves the task-comment position in content that may be wrapped later.
|
|
func WithPlaceholder(content string) string {
|
|
return placeholder + content
|
|
}
|
|
|
|
// ContainsReservedPlaceholder reports whether user-controlled content contains
|
|
// the server-owned marker used while comments are resolved transactionally.
|
|
func ContainsReservedPlaceholder(content string) bool {
|
|
return strings.Contains(content, placeholder)
|
|
}
|
|
|
|
// ResolvePlaceholder replaces the server-owned marker with canonical comment Markdown.
|
|
func ResolvePlaceholder(content string, comments []*models.TaskPlanComment) (string, error) {
|
|
if strings.Count(content, placeholder) != 1 {
|
|
return "", errors.New("plan comment placeholder must occur exactly once")
|
|
}
|
|
if len(comments) == 0 {
|
|
return "", errors.New("plan comment placeholder requires comments")
|
|
}
|
|
return strings.Replace(content, placeholder, formatMarkdown(comments), 1), nil
|
|
}
|
|
|
|
// Fingerprint returns a stable digest for replay identity data.
|
|
func Fingerprint(value interface{}) (string, error) {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal plan comment replay identity: %w", err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
return hex.EncodeToString(digest[:]), nil
|
|
}
|
|
|
|
// MetadataRefsMatch reports whether persisted delivery metadata names exactly refs.
|
|
func MetadataRefsMatch(metadata map[string]interface{}, refs []models.TaskPlanCommentRef) bool {
|
|
raw, exists := metadata[MetadataRefs]
|
|
if !exists {
|
|
return len(refs) == 0
|
|
}
|
|
data, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
var stored []models.TaskPlanCommentRef
|
|
if err := json.Unmarshal(data, &stored); err != nil || len(stored) != len(refs) {
|
|
return false
|
|
}
|
|
for i := range refs {
|
|
if stored[i] != refs[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func formatMarkdown(comments []*models.TaskPlanComment) string {
|
|
var out strings.Builder
|
|
out.WriteString("### Plan Comments\n\n")
|
|
for _, comment := range comments {
|
|
if comment.SelectedText != "" {
|
|
out.WriteString("```\n")
|
|
out.WriteString(comment.SelectedText)
|
|
out.WriteString("\n```\n")
|
|
}
|
|
for _, line := range strings.Split(comment.Body, "\n") {
|
|
out.WriteString("> ")
|
|
out.WriteString(line)
|
|
out.WriteByte('\n')
|
|
}
|
|
out.WriteByte('\n')
|
|
}
|
|
out.WriteString("---\n\n")
|
|
return out.String()
|
|
}
|