From 8064b42e6cf348e6b6cb85578bb98f1609c20456 Mon Sep 17 00:00:00 2001 From: Naoto Takai Date: Mon, 19 Jan 2026 20:31:22 +0900 Subject: [PATCH] refactor(prompt): migrate to template-based prompt construction - Replace manual string building with template system for better maintainability and consistency - Remove redundant output rules from convention asset files, now centralized in template - Add PromptData struct for cleaner template data passing - Improve error handling with fallback concatenation - Expand test coverage to validate template sections and output rules --- internal/config/assets/conventional.md | 5 --- internal/config/assets/default.md | 5 --- internal/config/assets/gitmoji.md | 5 --- internal/config/assets/karma.md | 6 --- internal/prompt/prompt.go | 39 +++++++++++------ internal/prompt/prompt.tmpl | 19 ++++++++ internal/prompt/prompt_test.go | 60 ++++++++++++++++++++++++-- 7 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 internal/prompt/prompt.tmpl diff --git a/internal/config/assets/conventional.md b/internal/config/assets/conventional.md index 6fa4862..5d9177c 100644 --- a/internal/config/assets/conventional.md +++ b/internal/config/assets/conventional.md @@ -12,8 +12,3 @@ Rules: - Describe the breaking change clearly in the footer using "BREAKING CHANGE:" - If useful, add a body explaining *why* the change was made, not just what changed - Wrap body lines at approximately 72 characters -- Do not mention the diff, filenames, or line numbers explicitly -- Do not wrap the message in backticks or code fences -- Output only the commit message, with no extra commentary - -Write the commit message based on the following git diff: diff --git a/internal/config/assets/default.md b/internal/config/assets/default.md index 241d13c..ff609d5 100644 --- a/internal/config/assets/default.md +++ b/internal/config/assets/default.md @@ -20,8 +20,3 @@ Rules: - Use full sentences and paragraphs as needed - Do not include metadata such as timestamps or author names -- Do not mention the diff, file names, or line numbers explicitly -- Do not wrap the message in backticks or code fences -- Output only the commit message, with no additional explanations - -Write the commit message based on the following git diff: diff --git a/internal/config/assets/gitmoji.md b/internal/config/assets/gitmoji.md index b248b97..6abf757 100644 --- a/internal/config/assets/gitmoji.md +++ b/internal/config/assets/gitmoji.md @@ -26,8 +26,3 @@ Rules: - Use only one emoji per commit - Do not include Conventional Commits types (feat, fix, etc.) - Do not mix multiple conventions in a single commit -- Do not mention the diff, file names, or line numbers explicitly -- Do not wrap the message in backticks or code fences -- Output only the commit message, with no explanations or metadata - -Write the commit message based on the following git diff: diff --git a/internal/config/assets/karma.md b/internal/config/assets/karma.md index 2053f44..706a126 100644 --- a/internal/config/assets/karma.md +++ b/internal/config/assets/karma.md @@ -34,9 +34,3 @@ Rules: - If the change introduces a breaking change: - Use BREAKING CHANGE in the footer - The subject must describe the change, not the migration steps - -- Do not reference the diff, file names, or line numbers directly -- Do not wrap the message in backticks or code fences -- Output only the commit message, without any explanations or metadata - -Write the commit message based on the following git diff: diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index f08a6be..d283e48 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -1,20 +1,33 @@ package prompt -import "strings" +import ( + "bytes" + _ "embed" + "strings" + "text/template" +) + +//go:embed prompt.tmpl +var promptTemplateText string + +var promptTemplate = template.Must(template.New("prompt").Parse(promptTemplateText)) + +type PromptData struct { + SystemPrompt string + Context string + Diff string +} func Build(systemPrompt, context, diff string) string { - var b strings.Builder - if systemPrompt != "" { - b.WriteString("System Prompt:\n") - b.WriteString(systemPrompt) - b.WriteString("\n\n") + data := PromptData{ + SystemPrompt: strings.TrimSpace(systemPrompt), + Context: strings.TrimSpace(context), + Diff: diff, } - if context != "" { - b.WriteString("Context:\n") - b.WriteString(context) - b.WriteString("\n\n") + var buf bytes.Buffer + if err := promptTemplate.Execute(&buf, data); err != nil { + // Fallback to simple concatenation on template error + return systemPrompt + "\n\n" + context + "\n\n" + diff } - b.WriteString("Git Diff:\n") - b.WriteString(diff) - return b.String() + return buf.String() } diff --git a/internal/prompt/prompt.tmpl b/internal/prompt/prompt.tmpl new file mode 100644 index 0000000..39d2477 --- /dev/null +++ b/internal/prompt/prompt.tmpl @@ -0,0 +1,19 @@ +=== INSTRUCTIONS === +{{.SystemPrompt}} + +OUTPUT RULES: +- Output only a Git commit message +- Begin with the commit subject line +- Exclude explanations, preambles, and meta commentary +- DO NOT reference diffs, file names, or line numbers +- DO NOT use code fences or backticks + +{{if .Context}} +=== CONTEXT === +{{.Context}} +{{end}} + +=== GIT DIFF === +{{.Diff}} + +=== OUTPUT === diff --git a/internal/prompt/prompt_test.go b/internal/prompt/prompt_test.go index 793d36c..8cb6e4b 100644 --- a/internal/prompt/prompt_test.go +++ b/internal/prompt/prompt_test.go @@ -1,11 +1,63 @@ package prompt -import "testing" +import ( + "strings" + "testing" +) func TestBuild(t *testing.T) { got := Build("sys", "ctx", "diff") - expected := "System Prompt:\nsys\n\nContext:\nctx\n\nGit Diff:\ndiff" - if got != expected { - t.Fatalf("Build = %q", got) + + // Check that output contains expected sections + if !strings.Contains(got, "=== INSTRUCTIONS ===") { + t.Fatal("Build output should contain INSTRUCTIONS section") + } + if !strings.Contains(got, "sys") { + t.Fatal("Build output should contain system prompt") + } + if !strings.Contains(got, "OUTPUT RULES:") { + t.Fatal("Build output should contain OUTPUT RULES section") + } + if !strings.Contains(got, "=== CONTEXT ===") { + t.Fatal("Build output should contain CONTEXT section when context provided") + } + if !strings.Contains(got, "ctx") { + t.Fatal("Build output should contain context content") + } + if !strings.Contains(got, "=== GIT DIFF ===") { + t.Fatal("Build output should contain GIT DIFF section") + } + if !strings.Contains(got, "diff") { + t.Fatal("Build output should contain diff content") + } + if !strings.Contains(got, "=== OUTPUT ===") { + t.Fatal("Build output should contain OUTPUT section") + } +} + +func TestBuildWithoutContext(t *testing.T) { + got := Build("sys", "", "diff") + + // Check that output omits CONTEXT section when empty + if strings.Contains(got, "=== CONTEXT ===") { + t.Fatal("Build output should not contain CONTEXT section when context is empty") + } + if !strings.Contains(got, "=== INSTRUCTIONS ===") { + t.Fatal("Build output should contain INSTRUCTIONS section") + } + if !strings.Contains(got, "=== GIT DIFF ===") { + t.Fatal("Build output should contain GIT DIFF section") + } +} + +func TestBuildOutputRules(t *testing.T) { + got := Build("sys", "ctx", "diff") + + // Verify output rules are included + if !strings.Contains(got, "Exclude explanations, preambles") { + t.Fatal("Build output should contain preamble suppression rule") + } + if !strings.Contains(got, "Begin with the commit subject line") { + t.Fatal("Build output should contain direct start instruction") } }