From 887b9bed9b35c9388869ce9a70b6853276d4f7f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:34:15 +0000 Subject: [PATCH 1/4] Initial plan From 72487072f80ffb9710b66d2e105c1f659cb60511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:26:42 +0000 Subject: [PATCH 2/4] Add ParseTaskFrontmatter method and custom struct support - Added Result.ParseTaskFrontmatter() method to parse task frontmatter into custom structs - Added comprehensive tests for parsing into custom structs - Added example code demonstrating library usage - Updated .gitignore to exclude test binaries - All tests pass, code lints successfully Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- .gitignore | 1 + pkg/codingcontext/example_test.go | 83 ++++++++++++ pkg/codingcontext/markdown_test.go | 118 +++++++++++++++++ pkg/codingcontext/result.go | 38 ++++++ pkg/codingcontext/result_test.go | 197 +++++++++++++++++++++++++++++ 5 files changed, 437 insertions(+) create mode 100644 pkg/codingcontext/example_test.go create mode 100644 pkg/codingcontext/result_test.go diff --git a/.gitignore b/.gitignore index 34f540d9..d60ce103 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Binary output coding-context coding-context-cli +*.test diff --git a/pkg/codingcontext/example_test.go b/pkg/codingcontext/example_test.go new file mode 100644 index 00000000..f7481907 --- /dev/null +++ b/pkg/codingcontext/example_test.go @@ -0,0 +1,83 @@ +package codingcontext_test + +import ( + "context" + "fmt" + "log" + + "github.com/kitproj/coding-context-cli/pkg/codingcontext" +) + +// TaskMetadata represents the custom frontmatter structure for a task +type TaskMetadata struct { + TaskName string `yaml:"task_name"` + Resume bool `yaml:"resume"` + Priority string `yaml:"priority"` + Environment string `yaml:"environment"` + Selectors map[string]any `yaml:"selectors"` +} + +// ExampleResult_ParseTaskFrontmatter demonstrates how to parse task frontmatter +// into a custom struct when using the coding-context library. +func ExampleResult_ParseTaskFrontmatter() { + // Create a context and run it to get a result + // In a real application, you would configure this properly + cc := codingcontext.New( + codingcontext.WithWorkDir("."), + ) + + // Assuming there's a task file with frontmatter like: + // --- + // task_name: deploy + // priority: high + // environment: production + // --- + result, err := cc.Run(context.Background(), "deploy") + if err != nil { + log.Fatal(err) + } + + // Parse the task frontmatter into your custom struct + var taskMeta TaskMetadata + if err := result.ParseTaskFrontmatter(&taskMeta); err != nil { + log.Fatal(err) + } + + // Now you can use the strongly-typed task metadata + fmt.Printf("Task: %s\n", taskMeta.TaskName) + fmt.Printf("Priority: %s\n", taskMeta.Priority) + fmt.Printf("Environment: %s\n", taskMeta.Environment) + + // You can also access the generic frontmatter map directly + if priority, ok := result.Task.FrontMatter["priority"]; ok { + fmt.Printf("Priority from map: %v\n", priority) + } +} + +// ExampleParseMarkdownFile demonstrates how to parse a markdown file +// with frontmatter into a custom struct. +func ExampleParseMarkdownFile() { + // Define your custom struct with yaml tags + type TaskFrontmatter struct { + TaskName string `yaml:"task_name"` + Resume bool `yaml:"resume"` + Priority string `yaml:"priority"` + Tags []string `yaml:"tags"` + } + + // Parse the markdown file + var frontmatter TaskFrontmatter + content, err := codingcontext.ParseMarkdownFile("path/to/task.md", &frontmatter) + if err != nil { + log.Fatal(err) + } + + // Access the parsed frontmatter + fmt.Printf("Task: %s\n", frontmatter.TaskName) + fmt.Printf("Resume: %v\n", frontmatter.Resume) + fmt.Printf("Priority: %s\n", frontmatter.Priority) + fmt.Printf("Tags: %v\n", frontmatter.Tags) + + // Access the content + fmt.Printf("Content length: %d\n", len(content)) +} diff --git a/pkg/codingcontext/markdown_test.go b/pkg/codingcontext/markdown_test.go index 23c52a0a..96a62e0d 100644 --- a/pkg/codingcontext/markdown_test.go +++ b/pkg/codingcontext/markdown_test.go @@ -102,3 +102,121 @@ func TestParseMarkdownFile_FileNotFound(t *testing.T) { t.Error("ParseMarkdownFile() expected error for non-existent file, got nil") } } + +func TestParseMarkdownFile_CustomStruct(t *testing.T) { + // Define a custom struct for task frontmatter + type TaskFrontmatter struct { + TaskName string `yaml:"task_name"` + Resume bool `yaml:"resume"` + Priority string `yaml:"priority"` + Tags []string `yaml:"tags"` + } + + tests := []struct { + name string + content string + wantContent string + wantTaskName string + wantResume bool + wantPriority string + wantTags []string + wantErr bool + }{ + { + name: "parse task with all fields", + content: `--- +task_name: fix-bug +resume: false +priority: high +tags: + - backend + - urgent +--- +# Fix Bug + +Please fix the bug in the backend service. +`, + wantContent: "# Fix Bug\n\nPlease fix the bug in the backend service.\n", + wantTaskName: "fix-bug", + wantResume: false, + wantPriority: "high", + wantTags: []string{"backend", "urgent"}, + wantErr: false, + }, + { + name: "parse task with partial fields", + content: `--- +task_name: deploy +resume: true +--- +# Deploy Application + +Deploy the application to staging. +`, + wantContent: "# Deploy Application\n\nDeploy the application to staging.\n", + wantTaskName: "deploy", + wantResume: true, + wantPriority: "", // zero value for missing field + wantTags: nil, + wantErr: false, + }, + { + name: "parse without frontmatter", + content: `# Simple Task + +This task has no frontmatter. +`, + wantContent: "# Simple Task\n\nThis task has no frontmatter.\n", + wantTaskName: "", // zero value + wantResume: false, + wantPriority: "", + wantTags: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test.md") + if err := os.WriteFile(tmpFile, []byte(tt.content), 0644); err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + + // Parse the file into custom struct + var frontmatter TaskFrontmatter + content, err := ParseMarkdownFile(tmpFile, &frontmatter) + + // Check error + if (err != nil) != tt.wantErr { + t.Errorf("ParseMarkdownFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Check content + if content != tt.wantContent { + t.Errorf("ParseMarkdownFile() content = %q, want %q", content, tt.wantContent) + } + + // Check frontmatter fields + if frontmatter.TaskName != tt.wantTaskName { + t.Errorf("frontmatter.TaskName = %q, want %q", frontmatter.TaskName, tt.wantTaskName) + } + if frontmatter.Resume != tt.wantResume { + t.Errorf("frontmatter.Resume = %v, want %v", frontmatter.Resume, tt.wantResume) + } + if frontmatter.Priority != tt.wantPriority { + t.Errorf("frontmatter.Priority = %q, want %q", frontmatter.Priority, tt.wantPriority) + } + if len(frontmatter.Tags) != len(tt.wantTags) { + t.Errorf("frontmatter.Tags length = %d, want %d", len(frontmatter.Tags), len(tt.wantTags)) + } + for i, tag := range tt.wantTags { + if i < len(frontmatter.Tags) && frontmatter.Tags[i] != tag { + t.Errorf("frontmatter.Tags[%d] = %q, want %q", i, frontmatter.Tags[i], tag) + } + } + }) + } +} diff --git a/pkg/codingcontext/result.go b/pkg/codingcontext/result.go index 4d7931a4..6175833e 100644 --- a/pkg/codingcontext/result.go +++ b/pkg/codingcontext/result.go @@ -1,8 +1,11 @@ package codingcontext import ( + "fmt" "path/filepath" "strings" + + yaml "github.com/goccy/go-yaml" ) // Markdown represents a markdown file with frontmatter and content @@ -29,3 +32,38 @@ type Result struct { Rules []Markdown // List of included rule files Task Markdown // Task file with frontmatter and content } + +// ParseTaskFrontmatter unmarshals the task frontmatter into the provided struct. +// The target parameter should be a pointer to a struct with yaml tags. +// Returns an error if the frontmatter cannot be unmarshaled into the target. +// +// Example: +// +// type TaskMeta struct { +// TaskName string `yaml:"task_name"` +// Resume bool `yaml:"resume"` +// Priority string `yaml:"priority"` +// } +// +// var meta TaskMeta +// if err := result.ParseTaskFrontmatter(&meta); err != nil { +// // handle error +// } +func (r *Result) ParseTaskFrontmatter(target any) error { + if r.Task.FrontMatter == nil { + return fmt.Errorf("task frontmatter is nil") + } + + // Marshal the frontmatter map to YAML bytes + yamlBytes, err := yaml.Marshal(r.Task.FrontMatter) + if err != nil { + return fmt.Errorf("failed to marshal frontmatter: %w", err) + } + + // Unmarshal the YAML bytes into the target struct + if err := yaml.Unmarshal(yamlBytes, target); err != nil { + return fmt.Errorf("failed to unmarshal frontmatter into target: %w", err) + } + + return nil +} diff --git a/pkg/codingcontext/result_test.go b/pkg/codingcontext/result_test.go new file mode 100644 index 00000000..866d134e --- /dev/null +++ b/pkg/codingcontext/result_test.go @@ -0,0 +1,197 @@ +package codingcontext + +import ( + "testing" +) + +func TestResult_ParseTaskFrontmatter(t *testing.T) { + tests := []struct { + name string + frontmatter FrontMatter + target any + wantErr bool + validate func(t *testing.T, target any) + }{ + { + name: "parse into struct with basic fields", + frontmatter: FrontMatter{ + "task_name": "fix-bug", + "resume": false, + "priority": "high", + }, + target: &struct { + TaskName string `yaml:"task_name"` + Resume bool `yaml:"resume"` + Priority string `yaml:"priority"` + }{}, + wantErr: false, + validate: func(t *testing.T, target any) { + meta := target.(*struct { + TaskName string `yaml:"task_name"` + Resume bool `yaml:"resume"` + Priority string `yaml:"priority"` + }) + if meta.TaskName != "fix-bug" { + t.Errorf("TaskName = %q, want %q", meta.TaskName, "fix-bug") + } + if meta.Resume != false { + t.Errorf("Resume = %v, want %v", meta.Resume, false) + } + if meta.Priority != "high" { + t.Errorf("Priority = %q, want %q", meta.Priority, "high") + } + }, + }, + { + name: "parse with nested selectors", + frontmatter: FrontMatter{ + "task_name": "implement-feature", + "selectors": map[string]any{ + "language": "Go", + "stage": "implementation", + }, + }, + target: &struct { + TaskName string `yaml:"task_name"` + Selectors map[string]any `yaml:"selectors"` + }{}, + wantErr: false, + validate: func(t *testing.T, target any) { + meta := target.(*struct { + TaskName string `yaml:"task_name"` + Selectors map[string]any `yaml:"selectors"` + }) + if meta.TaskName != "implement-feature" { + t.Errorf("TaskName = %q, want %q", meta.TaskName, "implement-feature") + } + if len(meta.Selectors) != 2 { + t.Errorf("Selectors length = %d, want 2", len(meta.Selectors)) + } + if meta.Selectors["language"] != "Go" { + t.Errorf("Selectors[language] = %v, want Go", meta.Selectors["language"]) + } + }, + }, + { + name: "parse with array values", + frontmatter: FrontMatter{ + "task_name": "test-code", + "languages": []any{"Go", "Python", "JavaScript"}, + }, + target: &struct { + TaskName string `yaml:"task_name"` + Languages []string `yaml:"languages"` + }{}, + wantErr: false, + validate: func(t *testing.T, target any) { + meta := target.(*struct { + TaskName string `yaml:"task_name"` + Languages []string `yaml:"languages"` + }) + if meta.TaskName != "test-code" { + t.Errorf("TaskName = %q, want %q", meta.TaskName, "test-code") + } + if len(meta.Languages) != 3 { + t.Errorf("Languages length = %d, want 3", len(meta.Languages)) + } + if meta.Languages[0] != "Go" { + t.Errorf("Languages[0] = %q, want Go", meta.Languages[0]) + } + }, + }, + { + name: "parse with optional fields", + frontmatter: FrontMatter{ + "task_name": "deploy", + }, + target: &struct { + TaskName string `yaml:"task_name"` + Environment string `yaml:"environment"` + Priority string `yaml:"priority"` + }{}, + wantErr: false, + validate: func(t *testing.T, target any) { + meta := target.(*struct { + TaskName string `yaml:"task_name"` + Environment string `yaml:"environment"` + Priority string `yaml:"priority"` + }) + if meta.TaskName != "deploy" { + t.Errorf("TaskName = %q, want %q", meta.TaskName, "deploy") + } + // Optional fields should be zero values + if meta.Environment != "" { + t.Errorf("Environment = %q, want empty string", meta.Environment) + } + if meta.Priority != "" { + t.Errorf("Priority = %q, want empty string", meta.Priority) + } + }, + }, + { + name: "nil frontmatter returns error", + frontmatter: nil, + target: &struct { + TaskName string `yaml:"task_name"` + }{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := &Result{ + Task: Markdown{ + Path: "/test/task.md", + FrontMatter: tt.frontmatter, + Content: "Test content", + }, + } + + err := result.ParseTaskFrontmatter(tt.target) + + if (err != nil) != tt.wantErr { + t.Errorf("ParseTaskFrontmatter() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.validate != nil { + tt.validate(t, tt.target) + } + }) + } +} + +func TestMarkdown_BootstrapPath(t *testing.T) { + tests := []struct { + name string + path string + want string + }{ + { + name: "md file", + path: "/path/to/task.md", + want: "/path/to/task-bootstrap", + }, + { + name: "mdc file", + path: "/path/to/rule.mdc", + want: "/path/to/rule-bootstrap", + }, + { + name: "empty path", + path: "", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Markdown{Path: tt.path} + got := m.BootstrapPath() + if got != tt.want { + t.Errorf("BootstrapPath() = %q, want %q", got, tt.want) + } + }) + } +} From 761412661e8d86991130eabb3ebd1ac0333246dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:28:24 +0000 Subject: [PATCH 3/4] Add nil target validation to ParseTaskFrontmatter - Added validation to check for nil target parameter - Added test case for nil target - Added comment explaining why marshal-unmarshal approach is used - All tests pass Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- pkg/codingcontext/result.go | 7 ++++++- pkg/codingcontext/result_test.go | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/codingcontext/result.go b/pkg/codingcontext/result.go index 6175833e..693c573b 100644 --- a/pkg/codingcontext/result.go +++ b/pkg/codingcontext/result.go @@ -50,11 +50,16 @@ type Result struct { // // handle error // } func (r *Result) ParseTaskFrontmatter(target any) error { + if target == nil { + return fmt.Errorf("target cannot be nil") + } + if r.Task.FrontMatter == nil { return fmt.Errorf("task frontmatter is nil") } - // Marshal the frontmatter map to YAML bytes + // Marshal the frontmatter map to YAML bytes, then unmarshal into target + // This approach leverages the existing YAML library without adding new dependencies yamlBytes, err := yaml.Marshal(r.Task.FrontMatter) if err != nil { return fmt.Errorf("failed to marshal frontmatter: %w", err) diff --git a/pkg/codingcontext/result_test.go b/pkg/codingcontext/result_test.go index 866d134e..0ffafaaa 100644 --- a/pkg/codingcontext/result_test.go +++ b/pkg/codingcontext/result_test.go @@ -136,6 +136,14 @@ func TestResult_ParseTaskFrontmatter(t *testing.T) { }{}, wantErr: true, }, + { + name: "nil target returns error", + frontmatter: FrontMatter{ + "task_name": "test", + }, + target: nil, + wantErr: true, + }, } for _, tt := range tests { From 64a36d7b5e3f4682ccbb2eeba35af739e769f070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 04:01:29 +0000 Subject: [PATCH 4/4] Move ParseFrontmatter to Markdown type - Moved ParseFrontmatter method from Result to Markdown type - Renamed from ParseTaskFrontmatter to ParseFrontmatter for generality - Updated all tests to use markdown.ParseFrontmatter() - Updated examples to demonstrate parsing both task and rule frontmatter - Method now works on any Markdown instance, not just tasks - All tests pass, code lints successfully Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- pkg/codingcontext/example_test.go | 18 ++++++++++++------ pkg/codingcontext/result.go | 24 ++++++++++++------------ pkg/codingcontext/result_test.go | 16 +++++++--------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/pkg/codingcontext/example_test.go b/pkg/codingcontext/example_test.go index f7481907..5bf32740 100644 --- a/pkg/codingcontext/example_test.go +++ b/pkg/codingcontext/example_test.go @@ -17,9 +17,9 @@ type TaskMetadata struct { Selectors map[string]any `yaml:"selectors"` } -// ExampleResult_ParseTaskFrontmatter demonstrates how to parse task frontmatter +// ExampleMarkdown_ParseFrontmatter demonstrates how to parse task frontmatter // into a custom struct when using the coding-context library. -func ExampleResult_ParseTaskFrontmatter() { +func ExampleMarkdown_ParseFrontmatter() { // Create a context and run it to get a result // In a real application, you would configure this properly cc := codingcontext.New( @@ -39,7 +39,7 @@ func ExampleResult_ParseTaskFrontmatter() { // Parse the task frontmatter into your custom struct var taskMeta TaskMetadata - if err := result.ParseTaskFrontmatter(&taskMeta); err != nil { + if err := result.Task.ParseFrontmatter(&taskMeta); err != nil { log.Fatal(err) } @@ -48,9 +48,15 @@ func ExampleResult_ParseTaskFrontmatter() { fmt.Printf("Priority: %s\n", taskMeta.Priority) fmt.Printf("Environment: %s\n", taskMeta.Environment) - // You can also access the generic frontmatter map directly - if priority, ok := result.Task.FrontMatter["priority"]; ok { - fmt.Printf("Priority from map: %v\n", priority) + // You can also parse rule frontmatter the same way + for _, rule := range result.Rules { + var ruleMeta struct { + Language string `yaml:"language"` + Stage string `yaml:"stage"` + } + if err := rule.ParseFrontmatter(&ruleMeta); err == nil { + fmt.Printf("Rule: language=%s, stage=%s\n", ruleMeta.Language, ruleMeta.Stage) + } } } diff --git a/pkg/codingcontext/result.go b/pkg/codingcontext/result.go index 693c573b..98b4723a 100644 --- a/pkg/codingcontext/result.go +++ b/pkg/codingcontext/result.go @@ -27,13 +27,7 @@ func (m *Markdown) BootstrapPath() string { return baseNameWithoutExt + "-bootstrap" } -// Result holds the assembled context from running a task -type Result struct { - Rules []Markdown // List of included rule files - Task Markdown // Task file with frontmatter and content -} - -// ParseTaskFrontmatter unmarshals the task frontmatter into the provided struct. +// ParseFrontmatter unmarshals the frontmatter into the provided struct. // The target parameter should be a pointer to a struct with yaml tags. // Returns an error if the frontmatter cannot be unmarshaled into the target. // @@ -46,21 +40,21 @@ type Result struct { // } // // var meta TaskMeta -// if err := result.ParseTaskFrontmatter(&meta); err != nil { +// if err := markdown.ParseFrontmatter(&meta); err != nil { // // handle error // } -func (r *Result) ParseTaskFrontmatter(target any) error { +func (m *Markdown) ParseFrontmatter(target any) error { if target == nil { return fmt.Errorf("target cannot be nil") } - if r.Task.FrontMatter == nil { - return fmt.Errorf("task frontmatter is nil") + if m.FrontMatter == nil { + return fmt.Errorf("frontmatter is nil") } // Marshal the frontmatter map to YAML bytes, then unmarshal into target // This approach leverages the existing YAML library without adding new dependencies - yamlBytes, err := yaml.Marshal(r.Task.FrontMatter) + yamlBytes, err := yaml.Marshal(m.FrontMatter) if err != nil { return fmt.Errorf("failed to marshal frontmatter: %w", err) } @@ -72,3 +66,9 @@ func (r *Result) ParseTaskFrontmatter(target any) error { return nil } + +// Result holds the assembled context from running a task +type Result struct { + Rules []Markdown // List of included rule files + Task Markdown // Task file with frontmatter and content +} diff --git a/pkg/codingcontext/result_test.go b/pkg/codingcontext/result_test.go index 0ffafaaa..d976bbb4 100644 --- a/pkg/codingcontext/result_test.go +++ b/pkg/codingcontext/result_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestResult_ParseTaskFrontmatter(t *testing.T) { +func TestMarkdown_ParseFrontmatter(t *testing.T) { tests := []struct { name string frontmatter FrontMatter @@ -148,18 +148,16 @@ func TestResult_ParseTaskFrontmatter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := &Result{ - Task: Markdown{ - Path: "/test/task.md", - FrontMatter: tt.frontmatter, - Content: "Test content", - }, + markdown := &Markdown{ + Path: "/test/task.md", + FrontMatter: tt.frontmatter, + Content: "Test content", } - err := result.ParseTaskFrontmatter(tt.target) + err := markdown.ParseFrontmatter(tt.target) if (err != nil) != tt.wantErr { - t.Errorf("ParseTaskFrontmatter() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ParseFrontmatter() error = %v, wantErr %v", err, tt.wantErr) return }