feat: implement AI-assisted rename prompting feature
- Added data model for AI-assisted renaming including structures for prompts, responses, and policies. - Created implementation plan detailing the integration of Google Genkit into the CLI for renaming tasks. - Developed quickstart guide for setting up and using the new AI rename functionality. - Documented research decisions regarding Genkit orchestration and prompt composition. - Established tasks for phased implementation, including setup, foundational work, and user stories. - Implemented contract tests to ensure AI rename policies and ledger metadata are correctly applied. - Developed integration tests for validating AI rename flows, including preview, apply, and undo functionalities. - Added tooling to pin Genkit dependency for consistent builds.
This commit is contained in:
190
internal/ai/plan/mapper.go
Normal file
190
internal/ai/plan/mapper.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Candidate represents a file considered for AI renaming.
|
||||
type Candidate struct {
|
||||
OriginalPath string
|
||||
SizeBytes int64
|
||||
Depth int
|
||||
Extension string
|
||||
}
|
||||
|
||||
// MapInput configures the mapping behaviour.
|
||||
type MapInput struct {
|
||||
Candidates []Candidate
|
||||
SequenceWidth int
|
||||
}
|
||||
|
||||
// PreviewPlan aggregates entries ready for preview rendering.
|
||||
type PreviewPlan struct {
|
||||
Entries []PreviewEntry
|
||||
Warnings []string
|
||||
PromptHash string
|
||||
Model string
|
||||
Conflicts []Conflict
|
||||
}
|
||||
|
||||
// PreviewEntry is a single row in the preview table.
|
||||
type PreviewEntry struct {
|
||||
Sequence int
|
||||
SequenceLabel string
|
||||
OriginalPath string
|
||||
ProposedPath string
|
||||
SanitizedSegments []string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// MapResponse converts a validated response into a preview plan.
|
||||
func MapResponse(input MapInput, validation ValidationResult) (PreviewPlan, error) {
|
||||
if input.SequenceWidth <= 0 {
|
||||
input.SequenceWidth = 3
|
||||
}
|
||||
|
||||
itemByOriginal := make(map[string]struct {
|
||||
item promptRenameItem
|
||||
}, len(validation.Items))
|
||||
for _, item := range validation.Items {
|
||||
key := normalizePath(item.Original)
|
||||
itemByOriginal[key] = struct{ item promptRenameItem }{item: promptRenameItem{
|
||||
Original: item.Original,
|
||||
Proposed: item.Proposed,
|
||||
Sequence: item.Sequence,
|
||||
Notes: item.Notes,
|
||||
}}
|
||||
}
|
||||
|
||||
entries := make([]PreviewEntry, 0, len(input.Candidates))
|
||||
for _, candidate := range input.Candidates {
|
||||
key := normalizePath(candidate.OriginalPath)
|
||||
entryData, ok := itemByOriginal[key]
|
||||
if !ok {
|
||||
return PreviewPlan{}, fmt.Errorf("ai plan: missing response for %s", candidate.OriginalPath)
|
||||
}
|
||||
|
||||
item := entryData.item
|
||||
label := formatSequence(item.Sequence, input.SequenceWidth)
|
||||
sanitized := computeSanitizedSegments(candidate.OriginalPath, item.Proposed)
|
||||
|
||||
entries = append(entries, PreviewEntry{
|
||||
Sequence: item.Sequence,
|
||||
SequenceLabel: label,
|
||||
OriginalPath: candidate.OriginalPath,
|
||||
ProposedPath: item.Proposed,
|
||||
SanitizedSegments: sanitized,
|
||||
Notes: item.Notes,
|
||||
})
|
||||
}
|
||||
|
||||
return PreviewPlan{
|
||||
Entries: entries,
|
||||
Warnings: append([]string(nil), validation.Warnings...),
|
||||
PromptHash: validation.PromptHash,
|
||||
Model: validation.Model,
|
||||
Conflicts: detectConflicts(validation.Items),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type promptRenameItem struct {
|
||||
Original string
|
||||
Proposed string
|
||||
Sequence int
|
||||
Notes string
|
||||
}
|
||||
|
||||
func formatSequence(seq, width int) string {
|
||||
if seq <= 0 {
|
||||
return ""
|
||||
}
|
||||
label := fmt.Sprintf("%0*d", width, seq)
|
||||
if len(label) < len(fmt.Sprintf("%d", seq)) {
|
||||
return fmt.Sprintf("%d", seq)
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
return strings.TrimSpace(strings.ReplaceAll(path, "\\", "/"))
|
||||
}
|
||||
|
||||
func computeSanitizedSegments(original, proposed string) []string {
|
||||
origStem := stem(original)
|
||||
propStem := stem(proposed)
|
||||
|
||||
origTokens := tokenize(origStem)
|
||||
propTokens := make(map[string]struct{}, len(origTokens))
|
||||
for _, token := range tokenize(propStem) {
|
||||
propTokens[token] = struct{}{}
|
||||
}
|
||||
|
||||
var sanitized []string
|
||||
seen := make(map[string]struct{})
|
||||
for _, token := range origTokens {
|
||||
if _, ok := propTokens[token]; ok {
|
||||
continue
|
||||
}
|
||||
if _, already := seen[token]; already {
|
||||
continue
|
||||
}
|
||||
if isNumericToken(token) {
|
||||
continue
|
||||
}
|
||||
seen[token] = struct{}{}
|
||||
sanitized = append(sanitized, token)
|
||||
}
|
||||
if len(sanitized) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(sanitized)
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func stem(path string) string {
|
||||
base := filepath.Base(path)
|
||||
ext := filepath.Ext(base)
|
||||
if ext != "" {
|
||||
return base[:len(base)-len(ext)]
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func tokenize(value string) []string {
|
||||
fields := strings.FieldsFunc(value, func(r rune) bool {
|
||||
if r >= '0' && r <= '9' {
|
||||
return false
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return false
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
tokens := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
normalized := strings.ToLower(field)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
tokens = append(tokens, normalized)
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func isNumericToken(token string) bool {
|
||||
if token == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range token {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user