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:
2025-11-03 18:08:14 +08:00
parent aa377bc7ed
commit 3867736858
41 changed files with 4082 additions and 9 deletions

View File

@@ -7,6 +7,8 @@ import (
"os"
"path/filepath"
"time"
"github.com/rogeecn/renamer/internal/ai/prompt"
)
const ledgerFileName = ".renamer"
@@ -26,6 +28,65 @@ type Entry struct {
Metadata map[string]any `json:"metadata,omitempty"`
}
const aiMetadataKey = "ai"
// AIMetadata captures AI-specific ledger metadata for rename batches.
type AIMetadata struct {
PromptHash string `json:"promptHash"`
ResponseHash string `json:"responseHash"`
Model string `json:"model"`
Policies prompt.NamingPolicyConfig `json:"policies"`
BatchSize int `json:"batchSize"`
AppliedAt time.Time `json:"appliedAt"`
}
// AttachAIMetadata records AI metadata alongside the ledger entry.
func (e *Entry) AttachAIMetadata(meta AIMetadata) {
if e.Metadata == nil {
e.Metadata = make(map[string]any)
}
if meta.AppliedAt.IsZero() {
meta.AppliedAt = time.Now().UTC()
}
e.Metadata[aiMetadataKey] = meta
}
// AIMetadata extracts AI metadata from the ledger entry if present.
func (e Entry) AIMetadata() (AIMetadata, bool) {
if e.Metadata == nil {
return AIMetadata{}, false
}
raw, ok := e.Metadata[aiMetadataKey]
if !ok {
return AIMetadata{}, false
}
switch value := raw.(type) {
case AIMetadata:
return value, true
case map[string]any:
var meta AIMetadata
if err := remarshal(value, &meta); err != nil {
return AIMetadata{}, false
}
return meta, true
default:
var meta AIMetadata
if err := remarshal(value, &meta); err != nil {
return AIMetadata{}, false
}
return meta, true
}
}
func remarshal(value any, target any) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return json.Unmarshal(data, target)
}
// Append writes a new entry to the ledger in newline-delimited JSON format.
func Append(workingDir string, entry Entry) error {
entry.Timestamp = time.Now().UTC()