π§ Semantic Function Clustering Analysis
Scope: pkg/gitutil, pkg/importinpututil (2 non-test files, 20 functions)
Executive Summary
Both in-scope files are internally cohesive and correctly named β no outlier functions need to move out of them for naming reasons. However, semantic comparison against the rest of the repo surfaced one confirmed copy-paste duplicate, one split-brain error classifier, and one scattered inline idiom that re-derives a function that already exists.
| # |
Finding |
Severity |
Sites |
| 1 |
marshalEnvValue duplicates importinpututil.FormatResolvedValue |
High |
2 |
| 2 |
Two divergent "canonical" auth-error classifiers |
Medium |
2 |
| 3 |
Full-SHA check hand-inlined instead of IsValidFullSHA |
Medium |
7 |
| 4 |
Error classifiers sit in gitutil rather than errorutil |
Medium |
2 |
| 5 |
Rate-limit substrings re-listed in isTransientSHAResolutionError |
Low |
1 |
1. Duplicate: marshalEnvValue re-implements importinpututil (High)
pkg/workflow/step_types.go:246 marshalEnvValue is a near-verbatim reimplementation of the entire public surface of the in-scope file pkg/importinpututil/import_input.go β FormatResolvedValue + formatReflectiveValue + marshalValue + normalizeSlice + normalizeMap, inlined into a single function.
The normalizeSlice and normalizeMap bodies are character-for-character identical, including the sorted-key ordering and the Go 1.22 for i := range rv.Len() idiom.
Side-by-side comparison
// pkg/importinpututil/import_input.go:84
func normalizeMap(rv reflect.Value) map[string]any {
keys := make([]string, 0, rv.Len())
for _, key := range rv.MapKeys() {
keys = append(keys, key.String())
}
sort.Strings(keys)
normalized := make(map[string]any, rv.Len())
for _, k := range keys {
normalized[k] = rv.MapIndex(reflect.ValueOf(k)).Interface()
}
return normalized
}
// pkg/workflow/step_types.go:269 β inlined, identical
case reflect.Map:
keys := make([]string, 0, rv.Len())
for _, key := range rv.MapKeys() {
keys = append(keys, key.String())
}
sort.Strings(keys)
normalized := make(map[string]any, rv.Len())
for _, k := range keys {
normalized[k] = rv.MapIndex(reflect.ValueOf(k)).Interface()
}
if b, err := json.Marshal(normalized); err == nil {
return string(b)
}
Behavioral deltas (all reconcilable at the call site):
| Case |
FormatResolvedValue |
marshalEnvValue |
nil |
("", false) |
"" |
| marshal error |
("", false) |
fmt.Sprint(v) |
| scalar |
fmt.Sprintf("%v", v), true |
fmt.Sprint(v) β same output |
Why this is safe to consolidate: pkg/workflow already imports importinpututil (pkg/workflow/expression_extraction.go:13), so there is no new dependency and no import cycle.
Recommendation: reduce marshalEnvValue to a delegation:
func marshalEnvValue(v any) string {
if s, ok := importinpututil.FormatResolvedValue(v); ok {
return s
}
return "" // nil and marshal-failure both fall here
}
Impact: removes ~35 duplicated lines; makes JSON env-var encoding and import-input substitution provably consistent β today they can silently drift.
2. Split-brain auth-error classification (Medium)
Two functions classify the same thing with different marker sets and different case sensitivity:
pkg/gitutil/gitutil.go:37 IsAuthError β case-insensitive, 8 markers, used at 14+ call sites across pkg/parser and pkg/cli
pkg/cli/audit.go:299 isPermissionErrorStr β case-sensitive, 8 different markers
isPermissionErrorStr carries this comment:
// This is the canonical union of all auth-error substrings used across the codebase; update here rather than adding new inline strings.Contains checks in callers.
That claim is not accurate β gitutil.IsAuthError predates it and is far more widely used.
Marker set divergence
| Marker |
IsAuthError |
isPermissionErrorStr |
not logged into |
β
|
β
(full phrase) |
gh_token / GH_TOKEN |
β
(lowercased) |
β
(uppercase only) |
authentication |
β
|
β οΈ authentication required only |
unauthorized |
β
|
β |
forbidden |
β
|
β |
permission denied |
β
|
β οΈ bare permission |
saml enforcement |
β
|
β |
exit status 4 |
β |
β
|
gh auth login |
β |
β
|
Consequence: an error reading "SAML enforcement blocked this request" is an auth error to gitutil but not to audit.go. Conversely "exit status 4" is auth to audit.go only. Because isPermissionErrorStr is case-sensitive, a lowercase gh_token in an error string slips past it.
Recommendation: make isPermissionErrorStr delegate to gitutil.IsAuthError and add only the two gh-CLI-specific markers it uniquely needs, or merge both marker sets into one classifier. Either way, correct the misleading "canonical union" comment.
3. Full-SHA predicate inlined at 7 sites (Medium)
gitutil.IsValidFullSHA exists for exactly this purpose, but 7 call sites hand-write len(x) == 40 && gitutil.IsHexString(x) instead:
pkg/parser/remote_resolve_sha.go:78 if len(sha) != 40 || !gitutil.IsHexString(sha)
pkg/parser/remote_resolve_sha.go:89 if len(ref) == 40 && gitutil.IsHexString(ref)
pkg/parser/remote_resolve_sha.go:155 if len(sha) != 40 || !gitutil.IsHexString(sha)
pkg/parser/remote_resolve_sha.go:207 if result.SHA == "" || len(result.SHA) != 40 || !gitutil.IsHexString(result.SHA)
pkg/parser/remote_download_file.go:402 if len(ref) == 40 && gitutil.IsHexString(ref)
pkg/workflow/action_resolver.go:187 if len(sha) != 40 || !gitutil.IsHexString(sha)
pkg/cli/download_workflow.go:132 isSHA := len(ref) == 40 && gitutil.IsHexString(ref)
β οΈ Not a pure find-and-replace. IsValidFullSHA uses ^[0-9a-f]{40}$ (lowercase only), whereas IsHexString accepts A-F. Swapping in IsValidFullSHA tightens behavior by rejecting uppercase SHAs. GitHub API responses are lowercase, so this is almost certainly the intended semantics β but each site should be reviewed rather than bulk-edited.
Recommendation: replace all 7 with gitutil.IsValidFullSHA, confirming per-site that uppercase SHAs are not a supported input. Note that pkg/workflow/skills_frontmatter.go:35 already combines both correctly for the partial-SHA case, so it should stay as-is.
4. Outlier: GitHub API error classifiers live in gitutil (Medium)
pkg/gitutil clusters into 7 clean groups β hex/SHA validation, git-argument safety, repo-root discovery, HEAD blob reading, repo-path parsing, environment lookup β plus one group that does not belong:
IsRateLimitError (line 28)
IsAuthError (line 37)
Neither touches git. They classify GitHub API / gh CLI error strings β which is the stated charter of a package that already exists:
// Package errorutil provides shared helpers for classifying and inspecting errors returned by the GitHub API and gh CLI.
pkg/errorutil already houses IsNotFoundError, IsForbiddenError, and IsGoneError, and already has the exact private primitive both gitutil functions hand-roll:
// pkg/errorutil/errors.go:73 β identical semantics to the
// strings.ToLower + chained strings.Contains in both gitutil functions
func containsSubstring(value string, substrings ...string) bool {
msg := strings.ToLower(value)
for _, substring := range substrings {
if strings.Contains(msg, substring) { return true }
}
return false
}
Recommendation: move both to pkg/errorutil and reimplement over containsSubstring. This also creates the natural home for resolving finding #2. pkg/errorutil has no dependency on pkg/gitutil, so there is no cycle risk.
5. Rate-limit substrings re-listed (Low)
pkg/cli/fetch.go:269 isTransientSHAResolutionError inlines "rate limit" and "http 429" alongside its network/timeout markers, overlapping gitutil.IsRateLimitError.
This one is largely acceptable β the transient check is intentionally broader (timeouts, connection resets, 5xx) and its "rate limit" prefix match is deliberately looser than the three specific phrases in IsRateLimitError. Flagged only so the overlap is a conscious choice.
Recommendation: optionally delegate the rate-limit clause to gitutil.IsRateLimitError (soon errorutil) and keep the network markers local. Low priority.
Function inventory & cluster analysis
pkg/gitutil/gitutil.go β 14 functions
| Cluster |
Functions |
Verdict |
| GitHub API error classification |
IsRateLimitError, IsAuthError |
β outlier β see #4 |
| Hex / SHA validation |
IsHexString, IsValidFullSHA, isGitObjectID |
β
cohesive |
| Git argument safety (CWE-88) |
ValidateGitRef, ValidateGitPath |
β
cohesive |
| Repo root discovery |
FindGitRoot, FindGitRootFrom |
β
cohesive |
| HEAD blob reading |
ReadFileFromHEAD, resolveHEADBlobID |
β
cohesive |
| Repo path parsing |
ExtractBaseRepo |
β
|
| Environment lookup |
Getwd, UserHomeDir |
β
thin, well-justified wrappers |
At 307 lines across 6 concerns in a single-file package, gitutil.go is near the threshold where splitting (sha.go, validate.go, root.go) would help β but this is a judgment call, not a defect. Removing the error classifiers (#4) is the higher-value change.
pkg/importinpututil/import_input.go β 6 functions
Single cohesive cluster: resolve an import-input path, then format the value for textual substitution. Well-factored β FormatResolvedValue β formatReflectiveValue β marshalValue / normalizeSlice / normalizeMap is a clean decomposition. This file is the correct home; finding #1 is the duplicate, not this.
Checked and cleared
pkg/workflow/git_helpers.go:48 findGitRoot β not a duplicate of gitutil.FindGitRoot; it delegates and swallows the error to return "". Intentional, documented.
pkg/parser/import_input_substitution.go:49-59 β one-line pass-throughs to importinpututil. Trivial indirection, below the reporting threshold.
Implementation Checklist
Analysis metadata
- Files in scope: 2 (
pkg/gitutil/gitutil.go, pkg/importinpututil/import_input.go)
- Functions cataloged: 20
- Clusters identified: 8
- Outliers found: 2 (
IsRateLimitError, IsAuthError)
- Duplicates confirmed: 1 exact-structure, 1 functional, 1 scattered idiom (7 sites)
- Method: Serena semantic analysis (gopls) + repo-wide reference and pattern search
- Import-cycle safety: verified for all consolidation recommendations
Generated by π§ Semantic Function Refactoring Β· sonnet46 Β· 207.3 AIC Β· β 22.7 AIC Β· β 10K Β· β·
π§ Semantic Function Clustering Analysis
Scope:
pkg/gitutil,pkg/importinpututil(2 non-test files, 20 functions)Executive Summary
Both in-scope files are internally cohesive and correctly named β no outlier functions need to move out of them for naming reasons. However, semantic comparison against the rest of the repo surfaced one confirmed copy-paste duplicate, one split-brain error classifier, and one scattered inline idiom that re-derives a function that already exists.
marshalEnvValueduplicatesimportinpututil.FormatResolvedValueIsValidFullSHAgitutilrather thanerrorutilisTransientSHAResolutionError1. Duplicate:
marshalEnvValuere-implementsimportinpututil(High)pkg/workflow/step_types.go:246marshalEnvValueis a near-verbatim reimplementation of the entire public surface of the in-scope filepkg/importinpututil/import_input.goβFormatResolvedValue+formatReflectiveValue+marshalValue+normalizeSlice+normalizeMap, inlined into a single function.The
normalizeSliceandnormalizeMapbodies are character-for-character identical, including the sorted-key ordering and the Go 1.22for i := range rv.Len()idiom.Side-by-side comparison
Behavioral deltas (all reconcilable at the call site):
FormatResolvedValuemarshalEnvValuenil("", false)""("", false)fmt.Sprint(v)fmt.Sprintf("%v", v), truefmt.Sprint(v)β same outputWhy this is safe to consolidate:
pkg/workflowalready importsimportinpututil(pkg/workflow/expression_extraction.go:13), so there is no new dependency and no import cycle.Recommendation: reduce
marshalEnvValueto a delegation:Impact: removes ~35 duplicated lines; makes JSON env-var encoding and import-input substitution provably consistent β today they can silently drift.
2. Split-brain auth-error classification (Medium)
Two functions classify the same thing with different marker sets and different case sensitivity:
pkg/gitutil/gitutil.go:37IsAuthErrorβ case-insensitive, 8 markers, used at 14+ call sites acrosspkg/parserandpkg/clipkg/cli/audit.go:299isPermissionErrorStrβ case-sensitive, 8 different markersisPermissionErrorStrcarries this comment:That claim is not accurate β
gitutil.IsAuthErrorpredates it and is far more widely used.Marker set divergence
IsAuthErrorisPermissionErrorStrnot logged intogh_token/GH_TOKENauthenticationauthentication requiredonlyunauthorizedforbiddenpermission deniedpermissionsaml enforcementexit status 4gh auth loginConsequence: an error reading
"SAML enforcement blocked this request"is an auth error togitutilbut not toaudit.go. Conversely"exit status 4"is auth toaudit.goonly. BecauseisPermissionErrorStris case-sensitive, a lowercasegh_tokenin an error string slips past it.Recommendation: make
isPermissionErrorStrdelegate togitutil.IsAuthErrorand add only the twogh-CLI-specific markers it uniquely needs, or merge both marker sets into one classifier. Either way, correct the misleading "canonical union" comment.3. Full-SHA predicate inlined at 7 sites (Medium)
gitutil.IsValidFullSHAexists for exactly this purpose, but 7 call sites hand-writelen(x) == 40 && gitutil.IsHexString(x)instead:IsValidFullSHAuses^[0-9a-f]{40}$(lowercase only), whereasIsHexStringacceptsA-F. Swapping inIsValidFullSHAtightens behavior by rejecting uppercase SHAs. GitHub API responses are lowercase, so this is almost certainly the intended semantics β but each site should be reviewed rather than bulk-edited.Recommendation: replace all 7 with
gitutil.IsValidFullSHA, confirming per-site that uppercase SHAs are not a supported input. Note thatpkg/workflow/skills_frontmatter.go:35already combines both correctly for the partial-SHA case, so it should stay as-is.4. Outlier: GitHub API error classifiers live in
gitutil(Medium)pkg/gitutilclusters into 7 clean groups β hex/SHA validation, git-argument safety, repo-root discovery, HEAD blob reading, repo-path parsing, environment lookup β plus one group that does not belong:IsRateLimitError(line 28)IsAuthError(line 37)Neither touches git. They classify GitHub API /
ghCLI error strings β which is the stated charter of a package that already exists:pkg/errorutilalready housesIsNotFoundError,IsForbiddenError, andIsGoneError, and already has the exact private primitive bothgitutilfunctions hand-roll:Recommendation: move both to
pkg/errorutiland reimplement overcontainsSubstring. This also creates the natural home for resolving finding #2.pkg/errorutilhas no dependency onpkg/gitutil, so there is no cycle risk.5. Rate-limit substrings re-listed (Low)
pkg/cli/fetch.go:269isTransientSHAResolutionErrorinlines"rate limit"and"http 429"alongside its network/timeout markers, overlappinggitutil.IsRateLimitError.This one is largely acceptable β the transient check is intentionally broader (timeouts, connection resets, 5xx) and its
"rate limit"prefix match is deliberately looser than the three specific phrases inIsRateLimitError. Flagged only so the overlap is a conscious choice.Recommendation: optionally delegate the rate-limit clause to
gitutil.IsRateLimitError(soonerrorutil) and keep the network markers local. Low priority.Function inventory & cluster analysis
pkg/gitutil/gitutil.goβ 14 functionsIsRateLimitError,IsAuthErrorIsHexString,IsValidFullSHA,isGitObjectIDValidateGitRef,ValidateGitPathFindGitRoot,FindGitRootFromReadFileFromHEAD,resolveHEADBlobIDExtractBaseRepoGetwd,UserHomeDirAt 307 lines across 6 concerns in a single-file package,
gitutil.gois near the threshold where splitting (sha.go,validate.go,root.go) would help β but this is a judgment call, not a defect. Removing the error classifiers (#4) is the higher-value change.pkg/importinpututil/import_input.goβ 6 functionsSingle cohesive cluster: resolve an import-input path, then format the value for textual substitution. Well-factored β
FormatResolvedValueβformatReflectiveValueβmarshalValue/normalizeSlice/normalizeMapis a clean decomposition. This file is the correct home; finding #1 is the duplicate, not this.Checked and cleared
pkg/workflow/git_helpers.go:48findGitRootβ not a duplicate ofgitutil.FindGitRoot; it delegates and swallows the error to return"". Intentional, documented.pkg/parser/import_input_substitution.go:49-59β one-line pass-throughs toimportinpututil. Trivial indirection, below the reporting threshold.Implementation Checklist
marshalEnvValueto delegate toimportinpututil.FormatResolvedValue(highest value, zero new deps)IsRateLimitError/IsAuthErrortopkg/errorutil, reimplement overcontainsSubstringisPermissionErrorStrinto the relocatedIsAuthError; fix the inaccurate "canonical union" commentlen==40 && IsHexStringsites withIsValidFullSHA, verifying uppercase-SHA handling per siteisTransientSHAResolutionErrorgo test ./...β findings Add workflow: githubnext/agentics/weekly-researchΒ #2 and Add workflow: githubnext/agentics/weekly-researchΒ #3 change classification behavior at the edgesAnalysis metadata
pkg/gitutil/gitutil.go,pkg/importinpututil/import_input.go)IsRateLimitError,IsAuthError)