Skip to content

Commit 2480098

Browse files
authored
refactor: eliminate duplicate git root detection and typeutil alias wrappers (#25283)
1 parent adff490 commit 2480098

16 files changed

+167
-217
lines changed

pkg/cli/dependency_graph.go

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,12 @@ func (g *DependencyGraph) resolveImportPath(importPath string, baseDir string) s
247247

248248
// If that fails, try resolving with parser's cache-aware resolution
249249
// Note: We create a minimal cache here just for resolution
250-
importCache := parser.NewImportCache(g.findGitRoot())
250+
gitRoot, err := findGitRootForPath(g.workflowsDir)
251+
if err != nil {
252+
depGraphLog.Printf("Failed to find git root, using fallback: %v", err)
253+
gitRoot = g.workflowsDir
254+
}
255+
importCache := parser.NewImportCache(gitRoot)
251256
fullPath, err := parser.ResolveIncludePath(importPath, baseDir, importCache)
252257
if err != nil {
253258
depGraphLog.Printf("Failed to resolve import path %s: %v", importPath, err)
@@ -258,29 +263,6 @@ func (g *DependencyGraph) resolveImportPath(importPath string, baseDir string) s
258263
return fullPath
259264
}
260265

261-
// findGitRoot finds the git repository root
262-
func (g *DependencyGraph) findGitRoot() string {
263-
depGraphLog.Printf("Finding git root starting from: %s", g.workflowsDir)
264-
// Start from workflows directory and walk up
265-
dir := g.workflowsDir
266-
for {
267-
gitDir := filepath.Join(dir, ".git")
268-
if _, err := os.Stat(gitDir); err == nil {
269-
depGraphLog.Printf("Found git root at: %s", dir)
270-
return dir
271-
}
272-
parent := filepath.Dir(dir)
273-
if parent == dir {
274-
// Reached filesystem root
275-
depGraphLog.Printf("Reached filesystem root, no .git directory found")
276-
break
277-
}
278-
dir = parent
279-
}
280-
depGraphLog.Printf("Using fallback git root: %s", g.workflowsDir)
281-
return g.workflowsDir // Fallback to workflows dir
282-
}
283-
284266
// GetAffectedWorkflows returns the list of workflows that need to be recompiled
285267
// when the given file is modified
286268
func (g *DependencyGraph) GetAffectedWorkflows(modifiedPath string) []string {

pkg/cli/logs_parsing_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/github/gh-aw/pkg/testutil"
11+
"github.com/github/gh-aw/pkg/typeutil"
1112
"github.com/github/gh-aw/pkg/workflow"
1213
)
1314

@@ -165,7 +166,7 @@ func TestConvertToInt(t *testing.T) {
165166
}
166167

167168
for _, tt := range tests {
168-
result := workflow.ConvertToInt(tt.value)
169+
result := typeutil.ConvertToInt(tt.value)
169170
if result != tt.expected {
170171
t.Errorf("ConvertToInt(%v) = %d, expected %d", tt.value, result, tt.expected)
171172
}
@@ -186,7 +187,7 @@ func TestConvertToFloat(t *testing.T) {
186187
}
187188

188189
for _, tt := range tests {
189-
result := workflow.ConvertToFloat(tt.value)
190+
result := typeutil.ConvertToFloat(tt.value)
190191
if result != tt.expected {
191192
t.Errorf("ConvertToFloat(%v) = %f, expected %f", tt.value, result, tt.expected)
192193
}

pkg/parser/frontmatter_hash.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@ import (
1414
"strings"
1515

1616
"github.com/github/gh-aw/pkg/logger"
17+
"github.com/github/gh-aw/pkg/typeutil"
1718
)
1819

1920
var frontmatterHashLog = logger.New("parser:frontmatter_hash")
2021

2122
// parseBoolFromFrontmatter extracts a boolean value from a frontmatter map.
2223
// Returns false if the key is absent, the map is nil, or the value is not a bool.
2324
func parseBoolFromFrontmatter(m map[string]any, key string) bool {
24-
if m == nil {
25-
return false
26-
}
27-
if v, ok := m[key]; ok {
28-
b, _ := v.(bool)
29-
return b
30-
}
31-
return false
25+
return typeutil.ParseBool(m, key)
3226
}
3327

3428
// FileReader is a function type that reads file content

pkg/typeutil/convert.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
// the caller needs to distinguish "missing/invalid" from a zero value, or when string
1212
// inputs are not expected (e.g. YAML config field parsing).
1313
//
14+
// Bool Extraction:
15+
// - ParseBool() - Extract a bool from map[string]any by key; returns false on missing, nil map, or non-bool.
16+
//
1417
// Safe Conversions (return 0 on overflow or invalid input):
1518
// - SafeUint64ToInt() - Convert uint64 to int, returning 0 on overflow
1619
// - SafeUintToInt() - Convert uint to int, returning 0 on overflow
@@ -110,6 +113,19 @@ func ConvertToInt(val any) int {
110113
return 0
111114
}
112115

116+
// ParseBool extracts a boolean value from a map[string]any by key.
117+
// Returns false if the map is nil, the key is absent, or the value is not a bool.
118+
func ParseBool(m map[string]any, key string) bool {
119+
if m == nil {
120+
return false
121+
}
122+
if v, ok := m[key]; ok {
123+
b, _ := v.(bool)
124+
return b
125+
}
126+
return false
127+
}
128+
113129
// ConvertToFloat safely converts any value to float64, returning 0 on failure.
114130
//
115131
// Supported input types: float64, int, int64, and string (parsed via strconv.ParseFloat).

pkg/workflow/claude_logs.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/github/gh-aw/pkg/logger"
10+
"github.com/github/gh-aw/pkg/typeutil"
1011
)
1112

1213
var claudeLogsLog = logger.New("workflow:claude_logs")
@@ -106,18 +107,18 @@ func (e *ClaudeEngine) extractClaudeResultMetrics(line string) LogMetrics {
106107

107108
// Extract total_cost_usd directly
108109
if totalCost, exists := jsonData["total_cost_usd"]; exists {
109-
if cost := ConvertToFloat(totalCost); cost > 0 {
110+
if cost := typeutil.ConvertToFloat(totalCost); cost > 0 {
110111
metrics.EstimatedCost = cost
111112
}
112113
}
113114

114115
// Extract usage information with all token types
115116
if usage, exists := jsonData["usage"]; exists {
116117
if usageMap, ok := usage.(map[string]any); ok {
117-
inputTokens := ConvertToInt(usageMap["input_tokens"])
118-
outputTokens := ConvertToInt(usageMap["output_tokens"])
119-
cacheCreationTokens := ConvertToInt(usageMap["cache_creation_input_tokens"])
120-
cacheReadTokens := ConvertToInt(usageMap["cache_read_input_tokens"])
118+
inputTokens := typeutil.ConvertToInt(usageMap["input_tokens"])
119+
outputTokens := typeutil.ConvertToInt(usageMap["output_tokens"])
120+
cacheCreationTokens := typeutil.ConvertToInt(usageMap["cache_creation_input_tokens"])
121+
cacheReadTokens := typeutil.ConvertToInt(usageMap["cache_read_input_tokens"])
121122

122123
totalTokens := inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens
123124
if totalTokens > 0 {
@@ -128,7 +129,7 @@ func (e *ClaudeEngine) extractClaudeResultMetrics(line string) LogMetrics {
128129

129130
// Extract number of turns
130131
if numTurns, exists := jsonData["num_turns"]; exists {
131-
if turns := ConvertToInt(numTurns); turns > 0 {
132+
if turns := typeutil.ConvertToInt(numTurns); turns > 0 {
132133
metrics.Turns = turns
133134
}
134135
}
@@ -241,18 +242,18 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
241242
if typeStr, ok := entryType.(string); ok && typeStr == "result" {
242243
// Found the result payload, extract cost and token data
243244
if totalCost, exists := entry["total_cost_usd"]; exists {
244-
if cost := ConvertToFloat(totalCost); cost > 0 {
245+
if cost := typeutil.ConvertToFloat(totalCost); cost > 0 {
245246
metrics.EstimatedCost = cost
246247
}
247248
}
248249

249250
// Extract usage information with all token types
250251
if usage, exists := entry["usage"]; exists {
251252
if usageMap, ok := usage.(map[string]any); ok {
252-
inputTokens := ConvertToInt(usageMap["input_tokens"])
253-
outputTokens := ConvertToInt(usageMap["output_tokens"])
254-
cacheCreationTokens := ConvertToInt(usageMap["cache_creation_input_tokens"])
255-
cacheReadTokens := ConvertToInt(usageMap["cache_read_input_tokens"])
253+
inputTokens := typeutil.ConvertToInt(usageMap["input_tokens"])
254+
outputTokens := typeutil.ConvertToInt(usageMap["output_tokens"])
255+
cacheCreationTokens := typeutil.ConvertToInt(usageMap["cache_creation_input_tokens"])
256+
cacheReadTokens := typeutil.ConvertToInt(usageMap["cache_read_input_tokens"])
256257

257258
totalTokens := inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens
258259
if totalTokens > 0 {
@@ -263,7 +264,7 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
263264

264265
// Extract number of turns
265266
if numTurns, exists := entry["num_turns"]; exists {
266-
if turns := ConvertToInt(numTurns); turns > 0 {
267+
if turns := typeutil.ConvertToInt(numTurns); turns > 0 {
267268
metrics.Turns = turns
268269
}
269270
}

pkg/workflow/config_helpers.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"fmt"
4242

4343
"github.com/github/gh-aw/pkg/logger"
44+
"github.com/github/gh-aw/pkg/typeutil"
4445
"github.com/goccy/go-yaml"
4546
)
4647

@@ -182,18 +183,14 @@ func preprocessExpiresField(configData map[string]any, log *logger.Logger) bool
182183
// Returns the boolean value, or false if not present or invalid.
183184
// If log is provided, it will log the extracted value for debugging.
184185
func ParseBoolFromConfig(m map[string]any, key string, log *logger.Logger) bool {
185-
if value, exists := m[key]; exists {
186-
if log != nil {
187-
log.Printf("Parsing %s from config", key)
188-
}
189-
if boolValue, ok := value.(bool); ok {
190-
if log != nil {
191-
log.Printf("Parsed %s from config: %t", key, boolValue)
192-
}
193-
return boolValue
194-
}
186+
if log != nil {
187+
log.Printf("Parsing %s from config", key)
188+
}
189+
result := typeutil.ParseBool(m, key)
190+
if log != nil {
191+
log.Printf("Parsed %s from config: %t", key, result)
195192
}
196-
return false
193+
return result
197194
}
198195

199196
// unmarshalConfig unmarshals a config value from a map into a typed struct using YAML.

pkg/workflow/frontmatter_extraction_metadata.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/github/gh-aw/pkg/logger"
10+
"github.com/github/gh-aw/pkg/typeutil"
1011
)
1112

1213
var frontmatterMetadataLog = logger.New("workflow:frontmatter_extraction_metadata")
@@ -172,9 +173,9 @@ func (c *Compiler) extractToolsTimeout(tools map[string]any) (string, error) {
172173
case int64:
173174
timeout = int(v)
174175
case uint:
175-
timeout = safeUintToInt(v) // Safe conversion to prevent overflow (alert #418)
176+
timeout = typeutil.SafeUintToInt(v) // Safe conversion to prevent overflow (alert #418)
176177
case uint64:
177-
timeout = safeUint64ToInt(v) // Safe conversion to prevent overflow (alert #416)
178+
timeout = typeutil.SafeUint64ToInt(v) // Safe conversion to prevent overflow (alert #416)
178179
case float64:
179180
timeout = int(v)
180181
default:
@@ -221,9 +222,9 @@ func (c *Compiler) extractToolsStartupTimeout(tools map[string]any) (string, err
221222
case int64:
222223
timeout = int(v)
223224
case uint:
224-
timeout = safeUintToInt(v) // Safe conversion to prevent overflow (alert #417)
225+
timeout = typeutil.SafeUintToInt(v) // Safe conversion to prevent overflow (alert #417)
225226
case uint64:
226-
timeout = safeUint64ToInt(v) // Safe conversion to prevent overflow (alert #415)
227+
timeout = typeutil.SafeUint64ToInt(v) // Safe conversion to prevent overflow (alert #415)
227228
case float64:
228229
timeout = int(v)
229230
default:

pkg/workflow/map_helpers.go

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,39 @@
1-
// This file provides generic map and type conversion utilities.
1+
// This file provides generic map utilities.
22
//
33
// This file contains low-level helper functions for working with map[string]any
4-
// structures and type conversions. These utilities are used throughout the workflow
5-
// compilation process to safely parse and manipulate configuration data.
4+
// structures. These utilities are used throughout the workflow compilation process
5+
// to safely parse and manipulate configuration data.
66
//
77
// # Organization Rationale
88
//
99
// These functions are grouped in a helper file because they:
1010
// - Provide generic, reusable utilities (used by 10+ files)
11-
// - Have no specific domain focus (work with any map/type data)
11+
// - Have no specific domain focus (work with any map data)
1212
// - Are small, stable functions (< 50 lines each)
1313
// - Follow clear, single-purpose patterns
1414
//
1515
// This follows the helper file conventions documented in skills/developer/SKILL.md.
1616
//
1717
// # Key Functions
1818
//
19-
// Type Conversion (delegated to pkg/typeutil for general-purpose reuse):
20-
// - parseIntValue() - Strictly parse numeric types to int; returns (value, ok). Use when
21-
// the caller needs to distinguish "missing/invalid" from a zero value, or when string
22-
// inputs are not expected (e.g. YAML config field parsing). Delegates to typeutil.ParseIntValue.
23-
// - safeUint64ToInt() - Convert uint64 to int, returning 0 on overflow. Delegates to typeutil.SafeUint64ToInt.
24-
// - safeUintToInt() - Convert uint to int, returning 0 on overflow. Delegates to typeutil.SafeUintToInt.
25-
// - ConvertToInt() - Leniently convert any value (int/int64/float64/string) to int, returning 0
26-
// on failure. Use when the input may come from heterogeneous sources such as JSON metrics,
27-
// log parsing, or user-provided strings where a zero default on failure is acceptable.
28-
// Delegates to typeutil.ConvertToInt.
29-
// - ConvertToFloat() - Safely convert any value (float64/int/int64/string) to float64.
30-
// Delegates to typeutil.ConvertToFloat.
31-
//
3219
// Map Operations:
3320
// - excludeMapKeys() - Create new map excluding specified keys
3421
// - sortedMapKeys() - Return sorted keys of a map[string]string
3522
//
36-
// These utilities handle common type conversion and map manipulation patterns that
37-
// occur frequently during YAML-to-struct parsing and configuration processing.
38-
39-
package workflow
40-
41-
import (
42-
"sort"
43-
44-
"github.com/github/gh-aw/pkg/typeutil"
45-
)
46-
47-
// parseIntValue strictly parses numeric types to int, returning (value, true) on success
48-
// and (0, false) for any unrecognized or non-numeric type.
49-
//
50-
// Use this when the caller needs to distinguish a missing/invalid value from a legitimate
51-
// zero, or when string inputs are not expected (e.g. YAML config field parsing where the
52-
// YAML library has already produced a typed numeric value).
53-
//
54-
// For lenient conversion that also handles string inputs and returns 0 on failure, use
55-
// ConvertToInt instead.
23+
// For type conversion utilities, use pkg/typeutil directly:
24+
// - typeutil.ParseIntValue() - Strictly parse numeric types to int; returns (value, ok).
25+
// - typeutil.SafeUint64ToInt() - Convert uint64 to int, returning 0 on overflow.
26+
// - typeutil.SafeUintToInt() - Convert uint to int, returning 0 on overflow.
27+
// - typeutil.ConvertToInt() - Leniently convert any value to int, returning 0 on failure.
28+
// - typeutil.ConvertToFloat() - Safely convert any value to float64.
29+
// - typeutil.ParseBool() - Extract a bool from map[string]any by key.
5630
//
57-
// This is a package-private alias for typeutil.ParseIntValue.
58-
func parseIntValue(value any) (int, bool) { return typeutil.ParseIntValue(value) }
31+
// These utilities handle common map manipulation patterns that occur frequently
32+
// during YAML-to-struct parsing and configuration processing.
5933

60-
// safeUint64ToInt safely converts uint64 to int, returning 0 if overflow would occur.
61-
// This is a package-private alias for typeutil.SafeUint64ToInt.
62-
func safeUint64ToInt(u uint64) int { return typeutil.SafeUint64ToInt(u) }
34+
package workflow
6335

64-
// safeUintToInt safely converts uint to int, returning 0 if overflow would occur.
65-
// This is a package-private alias for typeutil.SafeUintToInt.
66-
func safeUintToInt(u uint) int { return typeutil.SafeUintToInt(u) }
36+
import "sort"
6737

6838
// excludeMapKeys creates a new map excluding the specified keys
6939
func excludeMapKeys(original map[string]any, excludeKeys ...string) map[string]any {
@@ -91,26 +61,3 @@ func sortedMapKeys(m map[string]string) []string {
9161
sort.Strings(keys)
9262
return keys
9363
}
94-
95-
// ConvertToInt leniently converts any value to int, returning 0 on failure.
96-
//
97-
// Unlike parseIntValue, this function also handles string inputs via strconv.Atoi,
98-
// making it suitable for heterogeneous sources such as JSON metrics, log-parsed data,
99-
// or user-provided configuration where a zero default on failure is acceptable and
100-
// the caller does not need to distinguish "invalid" from a genuine zero.
101-
//
102-
// For strict numeric-only parsing where the caller must distinguish missing/invalid
103-
// values from zero, use parseIntValue instead.
104-
//
105-
// This is a workflow-package alias for typeutil.ConvertToInt. For new code outside
106-
// this package, prefer using typeutil.ConvertToInt directly.
107-
func ConvertToInt(val any) int { return typeutil.ConvertToInt(val) }
108-
109-
// ConvertToFloat safely converts any value to float64, returning 0 on failure.
110-
//
111-
// Supported input types: float64, int, int64, and string (parsed via strconv.ParseFloat).
112-
// Returns 0 for any other type or for strings that cannot be parsed as a float.
113-
//
114-
// This is a workflow-package alias for typeutil.ConvertToFloat. For new code outside
115-
// this package, prefer using typeutil.ConvertToFloat directly.
116-
func ConvertToFloat(val any) float64 { return typeutil.ConvertToFloat(val) }

0 commit comments

Comments
 (0)