• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / kvelmo / 23471163553

24 Mar 2026 02:51AM UTC coverage: 49.867% (-1.4%) from 51.3%
23471163553

push

github

k0d3r1s
Update project config and gap analysis commands

Update CLAUDE.md with new CLI commands and package descriptions. Revise
AGENTS.md with current architecture guidance. Add CodeRabbit config
rule. Update lefthook pre-commit hook. Refresh all gap analysis
commands with current feature inventory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

811 of 1374 branches covered (59.02%)

Branch coverage included in aggregate %.

22001 of 44372 relevant lines covered (49.58%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.83
/pkg/graph/parse.go
1
package graph
2

3
import (
4
        "errors"
5
        "fmt"
6
        "log/slog"
7
        "os"
8
        "strings"
9
        "time"
10

11
        "gopkg.in/yaml.v3"
12

13
        "github.com/valksor/kvelmo/pkg/worker"
14
)
15

16
// GraphDef is the YAML-serializable definition of a graph.
17
type GraphDef struct {
18
        Phase string    `yaml:"phase"`
19
        Nodes []NodeDef `yaml:"nodes"`
20
}
21

22
// NodeDef is the YAML-serializable definition of a single graph node.
23
type NodeDef struct {
24
        ID         string   `yaml:"id"`
25
        Label      string   `yaml:"label"`
26
        JobType    string   `yaml:"job_type"`
27
        Prompt     string   `yaml:"prompt"`
28
        DependsOn  []string `yaml:"depends_on"`
29
        Frozen     bool     `yaml:"frozen"`
30
        FailBranch string   `yaml:"fail_branch"`
31

32
        // Iteration
33
        MaxIterations int `yaml:"max_iterations"`
34

35
        // Error strategy
36
        ErrorStrategy string        `yaml:"error_strategy"` // "fail", "default_value", "retry_then_fail"
37
        DefaultOutput string        `yaml:"default_output"`
38
        MaxRetries    int           `yaml:"max_retries"`
39
        RetryDelay    time.Duration `yaml:"retry_delay"`
40

41
        // Approval gate
42
        RequiresApproval bool          `yaml:"requires_approval"`
43
        ApprovalPrompt   string        `yaml:"approval_prompt"`
44
        ApprovalTimeout  time.Duration `yaml:"approval_timeout"`
45

46
        // Sub-task (mutually exclusive with Prompt)
47
        SubTask *SubTaskConfig `yaml:"sub_task"`
48
}
49

50
// ParseGraphDef parses a YAML graph definition and returns a validated Graph.
51
// IterationCheck functions cannot be expressed in YAML; nodes with MaxIterations
52
// will always iterate (the check always returns true). Use Build() for
53
// programmatic graphs that need custom iteration checks.
54
func ParseGraphDef(data []byte) (*Graph, error) {
1✔
55
        var def GraphDef
1✔
56
        if err := yaml.Unmarshal(data, &def); err != nil {
2✔
57
                return nil, fmt.Errorf("graph: parse yaml: %w", err)
1✔
58
        }
1✔
59

60
        return BuildFromDef(&def)
1✔
61
}
62

63
// ParseGraphDefFile reads and parses a YAML graph definition from a file.
64
func ParseGraphDefFile(path string) (*Graph, error) {
1✔
65
        data, err := os.ReadFile(path)
1✔
66
        if err != nil {
2✔
67
                return nil, fmt.Errorf("graph: read file: %w", err)
1✔
68
        }
1✔
69

70
        return ParseGraphDef(data)
1✔
71
}
72

73
// BuildFromDef constructs a Graph from a parsed GraphDef.
74
func BuildFromDef(def *GraphDef) (*Graph, error) {
1✔
75
        if len(def.Nodes) == 0 {
2✔
76
                return nil, errors.New("graph: definition has no nodes")
1✔
77
        }
1✔
78

79
        g := New()
1✔
80

1✔
81
        for i, nd := range def.Nodes {
2✔
82
                if nd.ID == "" {
2✔
83
                        return nil, fmt.Errorf("graph: node %d has no id", i)
1✔
84
                }
1✔
85

86
                node := &Node{
1✔
87
                        ID:            NodeID(nd.ID),
1✔
88
                        Label:         nd.Label,
1✔
89
                        JobType:       resolveJobType(nd.JobType),
1✔
90
                        Prompt:        nd.Prompt,
1✔
91
                        Frozen:        nd.Frozen,
1✔
92
                        MaxIterations: nd.MaxIterations,
1✔
93
                        DefaultOutput: nd.DefaultOutput,
1✔
94
                        MaxRetries:    nd.MaxRetries,
1✔
95
                        RetryDelay:    nd.RetryDelay,
1✔
96
                        SubTask:       nd.SubTask,
1✔
97
                }
1✔
98

1✔
99
                if node.Label == "" {
2✔
100
                        node.Label = nd.ID
1✔
101
                }
1✔
102

103
                // Dependencies
104
                for _, dep := range nd.DependsOn {
2✔
105
                        node.DependsOn = append(node.DependsOn, NodeID(dep))
1✔
106
                }
1✔
107

108
                // Fail branch
109
                if nd.FailBranch != "" {
2✔
110
                        fb := NodeID(nd.FailBranch)
1✔
111
                        node.FailBranch = &fb
1✔
112
                }
1✔
113

114
                // Error strategy
115
                node.ErrorStrategy = resolveErrorStrategy(nd.ErrorStrategy)
1✔
116

1✔
117
                // Iteration: YAML-defined iteration always returns true (re-execute until max).
1✔
118
                if nd.MaxIterations > 0 {
2✔
119
                        node.IterationCheck = func(string) bool { return true }
2✔
120
                }
121

122
                // Approval gate
123
                node.RequiresApproval = nd.RequiresApproval
1✔
124
                node.ApprovalPrompt = nd.ApprovalPrompt
1✔
125
                node.ApprovalTimeout = nd.ApprovalTimeout
1✔
126

1✔
127
                if err := g.AddNode(node); err != nil {
1✔
128
                        return nil, fmt.Errorf("graph: add node %q: %w", nd.ID, err)
×
129
                }
×
130
        }
131

132
        if err := g.Validate(); err != nil {
1✔
133
                return nil, fmt.Errorf("graph: validate: %w", err)
×
134
        }
×
135

136
        return g, nil
1✔
137
}
138

139
// ExpandPromptVars replaces {{var}} placeholders in all node prompts using the
140
// provided variable map. Unknown variables are left as-is.
141
func ExpandPromptVars(g *Graph, vars map[string]string) {
1✔
142
        for _, node := range g.Nodes {
2✔
143
                node.Prompt = expandVars(node.Prompt, vars)
1✔
144
        }
1✔
145
}
146

147
func expandVars(s string, vars map[string]string) string {
1✔
148
        for k, v := range vars {
2✔
149
                s = strings.ReplaceAll(s, "{{"+k+"}}", v)
1✔
150
        }
1✔
151

152
        return s
1✔
153
}
154

155
func resolveJobType(s string) worker.JobType {
1✔
156
        switch strings.ToLower(s) {
1✔
157
        case "plan":
1✔
158
                return worker.JobTypePlan
1✔
159
        case "implement":
1✔
160
                return worker.JobTypeImplement
1✔
161
        case "review":
1✔
162
                return worker.JobTypeReview
1✔
163
        case "simplify":
1✔
164
                return worker.JobTypeSimplify
1✔
165
        case "optimize":
1✔
166
                return worker.JobTypeOptimize
1✔
167
        case "dry_run", "dryrun":
1✔
168
                return worker.JobTypeDryRun
1✔
169
        default:
1✔
170
                slog.Warn("unknown job type in graph definition, using as-is", "job_type", s)
1✔
171

1✔
172
                return worker.JobType(s)
1✔
173
        }
174
}
175

176
func resolveErrorStrategy(s string) ErrorStrategy {
1✔
177
        switch strings.ToLower(s) {
1✔
178
        case "default_value":
1✔
179
                return ErrorDefaultValue
1✔
180
        case "retry_then_fail":
1✔
181
                return ErrorRetryThenFail
1✔
182
        default:
1✔
183
                return ErrorFail
1✔
184
        }
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc