π§ Semantic Function Clustering Analysis β pkg/errorutil, pkg/fileutil
Analysis of the precomputed package slice for this run: 2 packages, 4 non-test Go files, 15 functions.
Headline: pkg/fileutil is clean β no outliers, no duplicates anywhere in the repo. pkg/errorutil is the canonical not-found/forbidden/gone classifier, but 5 call sites in pkg/cli reimplement its logic inline instead of calling it. One of those reimplementations has a latent case-sensitivity bug.
Key findings
| # |
Finding |
Severity |
Locations |
| 1 |
isRepositoryPackageRemoteNotFound is a functional duplicate of errorutil.IsNotFoundError |
High |
pkg/cli/add_package_manifest.go:925 |
| 2 |
Inline "404" || "not found" substring checks bypassing errorutil |
High |
4 sites in pkg/cli |
| 3 |
Case-sensitive check misses gh's Not Found output |
High (latent bug) |
pkg/cli/experiments_command.go:718 |
| 4 |
Missing string-input variant forces errors.New(...) wrapper idiom |
Medium |
2 sites in pkg/cli |
| 5 |
fileutil.go mixes path-security validation with file/dir operations |
Low |
pkg/fileutil/fileutil.go |
1. Exact functional duplicate of errorutil.IsNotFoundError
pkg/cli/add_package_manifest.go:925-931 reimplements the canonical helper verbatim:
// pkg/cli/add_package_manifest.go
func isRepositoryPackageRemoteNotFound(err error) bool {
if err == nil {
return false
}
errText := strings.ToLower(err.Error())
return strings.Contains(errText, "404") || strings.Contains(errText, "not found")
}
// pkg/errorutil/errors.go:18 β identical semantics
func IsNotFoundError(err error) bool {
matched := containsErrorSubstring(err, "404", "not found")
...
}
Same nil guard, same lowercasing, same two substrings. Recommendation: delete isRepositoryPackageRemoteNotFound and call errorutil.IsNotFoundError from normalizeRepositoryPackageRemoteError (add_package_manifest.go:918). This also gains the debug logging errorutil already emits.
2 & 3. Scattered inline 404 / not found checks
errorutil is already the established shared abstraction (~10 call sites across pkg/cli and pkg/parser), yet these four sites hand-roll the same predicate:
All four inline reimplementations
// pkg/cli/branch_file_reader.go:59 β string input, otherwise identical
func isRemoteFileNotFoundOutput(output string) bool {
s := strings.ToLower(output)
return strings.Contains(s, "404") || strings.Contains(s, "not found")
}
// pkg/cli/checks_command.go:243 β inside classifyGHAPIError
lower := strings.ToLower(stderr)
switch {
case strings.Contains(lower, "404") || strings.Contains(lower, "not found"):
// pkg/cli/setup_repository.go:108 β same pair plus two extra clauses
if strings.Contains(message, "could not resolve to a repository") ||
strings.Contains(message, "http 404") || strings.Contains(message, "not found") {
// pkg/cli/experiments_command.go:718 β NOT lowercased
if strings.Contains(stderr, "404") || strings.Contains(stderr, "not found") {
experiments_command.go:718 is a real bug, not just duplication. stderr is used raw with no strings.ToLower. The GitHub API and gh CLI return Not Found (title case) in most 404 responses, so this branch does not fire and the user gets the generic failed to fetch experiment branch (exit N): ... message instead of experiment %q not found in %s. Every other site in the codebase lowercases first β this one was missed. Routing it through errorutil fixes the bug and removes the duplicate in one change.
4. Missing string-input variant in the errorutil public API
errorutil only accepts error, so callers holding raw CLI output resort to wrapping:
// pkg/cli/logs_download.go:104
if errorutil.IsNotFoundError(err) || errorutil.IsNotFoundError(errors.New(string(output))) || errorutil.IsGoneError(err) {
// pkg/cli/workflow_run_metadata.go:56
errorutil.IsNotFoundError(errors.New(outputStr)) ||
Allocating a throwaway error purely to satisfy the signature is a smell, and it is plausibly why branch_file_reader.go and checks_command.go wrote their own string versions instead. Recommendation: add errorutil.IsNotFoundOutput(s string) bool (and optionally IsForbiddenOutput/IsGoneOutput) delegating to the existing unexported containsErrorSubstring logic. That gives all the string-shaped call sites a single home and removes the errors.New idiom.
Consolidating findings 1β4 collapses 5 duplicate predicates plus 2 wrapper idioms into 2 exported functions, and fixes one user-visible bug.
5. fileutil.go mixes two concerns (low priority)
pkg/fileutil/fileutil.go holds two distinct clusters: path-security validation (ValidateAbsolutePath, ValidatePathWithinBase, resolveWithAncestorSymlinks β lines 39-140) and plain file/dir operations (EnsureParentDir, FileExists, DirExists, IsDirEmpty, copyFileContents, CopyFile β lines 142-235). The security half is the part that warrants careful review and carries the most test coverage; splitting it into path_validation.go would make that boundary explicit, matching how executable.go and tar.go are already scoped. Purely organizational β no behavior change, and reasonable to skip.
What is already good
Clean results β no action needed
pkg/fileutil has zero duplicates repo-wide. FileExists, DirExists, IsDirEmpty, CopyFile, EnsureParentDir, ValidateAbsolutePath, ValidatePathWithinBase, and ExtractFileFromTar are each defined exactly once. For utility functions this generic, that is unusual and worth preserving.
- File-per-feature is respected.
executable.go holds only the two executable-resolution functions; tar.go holds only ExtractFileFromTar. No outliers β every function matches its file's stated purpose.
errorutil internals are well factored. containsHTTPStatusSubstring narrows 403/410 to HTTP-shaped patterns so "forbidden character" and "connection has gone away" are not misclassified, while 404 intentionally stays broad. The asymmetry is deliberate and documented in the function comments.
- Consistent logging. Every file declares its own scoped
logger.New("pkg:file") instance.
Suggested order of work
- Fix
experiments_command.go:718 case sensitivity β standalone bug fix, worth landing on its own.
- Add
errorutil.IsNotFoundOutput(string).
- Replace the duplicate predicates with
errorutil calls; delete isRepositoryPackageRemoteNotFound and isRemoteFileNotFoundOutput.
- (Optional) Split
path_validation.go out of fileutil.go.
Analysis metadata
- Scope:
pkg/errorutil, pkg/fileutil (precomputed slice for this run; not a repo-wide sweep)
- Files analyzed: 4 non-test
.go files
- Functions cataloged: 15 (8 exported, 7 unexported)
- Outliers found: 0
- Duplicates confirmed: 5 (all reimplementations of in-scope
errorutil symbols, located in pkg/cli)
- Method: symbol inventory of the in-scope slice, then reference and pattern search anchored on those symbols to find reimplementations elsewhere
- Note: finding 3 was verified by reading the surrounding code path, not inferred from the pattern match alone
Generated by π§ Semantic Function Refactoring Β· sonnet46 Β· 214.3 AIC Β· β 18.5 AIC Β· β 9.6K Β· β·
π§ Semantic Function Clustering Analysis β
pkg/errorutil,pkg/fileutilAnalysis of the precomputed package slice for this run: 2 packages, 4 non-test Go files, 15 functions.
Headline:
pkg/fileutilis clean β no outliers, no duplicates anywhere in the repo.pkg/errorutilis the canonical not-found/forbidden/gone classifier, but 5 call sites inpkg/clireimplement its logic inline instead of calling it. One of those reimplementations has a latent case-sensitivity bug.Key findings
isRepositoryPackageRemoteNotFoundis a functional duplicate oferrorutil.IsNotFoundErrorpkg/cli/add_package_manifest.go:925"404" || "not found"substring checks bypassingerrorutilpkg/cliNot Foundoutputpkg/cli/experiments_command.go:718errors.New(...)wrapper idiompkg/clifileutil.gomixes path-security validation with file/dir operationspkg/fileutil/fileutil.go1. Exact functional duplicate of
errorutil.IsNotFoundErrorpkg/cli/add_package_manifest.go:925-931reimplements the canonical helper verbatim:Same nil guard, same lowercasing, same two substrings. Recommendation: delete
isRepositoryPackageRemoteNotFoundand callerrorutil.IsNotFoundErrorfromnormalizeRepositoryPackageRemoteError(add_package_manifest.go:918). This also gains the debug loggingerrorutilalready emits.2 & 3. Scattered inline
404 / not foundcheckserrorutilis already the established shared abstraction (~10 call sites acrosspkg/cliandpkg/parser), yet these four sites hand-roll the same predicate:All four inline reimplementations
experiments_command.go:718is a real bug, not just duplication.stderris used raw with nostrings.ToLower. The GitHub API andghCLI returnNot Found(title case) in most 404 responses, so this branch does not fire and the user gets the genericfailed to fetch experiment branch (exit N): ...message instead ofexperiment %q not found in %s. Every other site in the codebase lowercases first β this one was missed. Routing it througherrorutilfixes the bug and removes the duplicate in one change.4. Missing string-input variant in the
errorutilpublic APIerrorutilonly acceptserror, so callers holding raw CLI output resort to wrapping:Allocating a throwaway
errorpurely to satisfy the signature is a smell, and it is plausibly whybranch_file_reader.goandchecks_command.gowrote their own string versions instead. Recommendation: adderrorutil.IsNotFoundOutput(s string) bool(and optionallyIsForbiddenOutput/IsGoneOutput) delegating to the existing unexportedcontainsErrorSubstringlogic. That gives all the string-shaped call sites a single home and removes theerrors.Newidiom.Consolidating findings 1β4 collapses 5 duplicate predicates plus 2 wrapper idioms into 2 exported functions, and fixes one user-visible bug.
5.
fileutil.gomixes two concerns (low priority)pkg/fileutil/fileutil.goholds two distinct clusters: path-security validation (ValidateAbsolutePath,ValidatePathWithinBase,resolveWithAncestorSymlinksβ lines 39-140) and plain file/dir operations (EnsureParentDir,FileExists,DirExists,IsDirEmpty,copyFileContents,CopyFileβ lines 142-235). The security half is the part that warrants careful review and carries the most test coverage; splitting it intopath_validation.gowould make that boundary explicit, matching howexecutable.goandtar.goare already scoped. Purely organizational β no behavior change, and reasonable to skip.What is already good
Clean results β no action needed
pkg/fileutilhas zero duplicates repo-wide.FileExists,DirExists,IsDirEmpty,CopyFile,EnsureParentDir,ValidateAbsolutePath,ValidatePathWithinBase, andExtractFileFromTarare each defined exactly once. For utility functions this generic, that is unusual and worth preserving.executable.goholds only the two executable-resolution functions;tar.goholds onlyExtractFileFromTar. No outliers β every function matches its file's stated purpose.errorutilinternals are well factored.containsHTTPStatusSubstringnarrows 403/410 to HTTP-shaped patterns so"forbidden character"and"connection has gone away"are not misclassified, while 404 intentionally stays broad. The asymmetry is deliberate and documented in the function comments.logger.New("pkg:file")instance.Suggested order of work
experiments_command.go:718case sensitivity β standalone bug fix, worth landing on its own.errorutil.IsNotFoundOutput(string).errorutilcalls; deleteisRepositoryPackageRemoteNotFoundandisRemoteFileNotFoundOutput.path_validation.goout offileutil.go.Analysis metadata
pkg/errorutil,pkg/fileutil(precomputed slice for this run; not a repo-wide sweep).gofileserrorutilsymbols, located inpkg/cli)