[typist] 🔤 Typist - Go Type Consistency Analysis #55369
Closed
Replies: 1 comment
|
This discussion has been marked as outdated by Typist - Go Type Analysis. A newer discussion is available at Discussion #55753. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Analysis of repository: github/gh-aw — scope: non-test
.gofiles underpkg/Executive Summary
Good news first: this codebase is unusually well-factored for its size. Across 1,075 non-test struct/interface declarations in
pkg/, an exact type-name collision check turned up only 4 name clashes, and on inspection every one of them turned out to be either intentional (a(go/redacted):build js || wasmplatform split forProgressBar/SpinnerWrapperinpkg/console) or already a good pattern (parser.ValidationErrorembedding the sharedvalidationerror.Payload/interface;scanfindings.Findingvscli.Findingare just two unrelated domains that happen to share a name). There's no "we definedConfigfive times" problem here.The real opportunities are more subtle: 5 near-duplicate struct clusters where different type names quietly model the same shape (a hand-rolled
{value, err}cache tuple reinvented four times, a "before/after/changed" delta concept modeled twice — once properly generic, once copy-pasted as flat fields — and a couple of structs that silently duplicate another struct's fields instead of embedding it), plus ~5 untyped-usage spots where the project's own established pattern of named string-enum types (likeGitHubIntegrityLevelandReactionType) wasn't applied consistently to sibling fields that share the exact same value domain. None of this is scary — it's the kind of drift that accumulates naturally in a codebase this size, and every fix below is small and localized.Full Analysis Report
Duplicated Type Definitions
Summary Statistics
Exact-name collisions (informational — no action needed)
ProgressBarpkg/console/progress.go:31,pkg/console/progress_wasm.go:9(go/redacted):build js || wasmimplementationSpinnerWrapperpkg/console/spinner.go:92,pkg/console/spinner_wasm.go:12ValidationErrorpkg/parser/validation_error.go:12(struct, embedsvalidationerror.Payload),pkg/validationerror/validationerror.go:49(interface)Findingpkg/scanfindings/scanfindings.go:109,pkg/cli/audit_report.go:67Cluster 1: Hand-rolled
{value, err}cache-tuple structsOccurrences: 5 (4 named + 1 anonymous) · Impact: Medium — same memoization shape reinvented by hand every time a new cache is added
Locations:
pkg/cli/update_actions_deps.go:41—cachedLatestRelease{version, sha string; err error}pkg/cli/update_actions_deps.go:47—cachedSHA{sha string; err error}pkg/cli/update_actions_deps.go:57— anonymousstruct{ output []byte; err error }pkg/cli/update_workflows.go:45—cachedDefaultBranch{branch string; err error}pkg/cli/update_workflows.go:50—cachedBranchCommit{info latestBranchCommitInfo; err error}All five are the same "one payload field + trailing
err error" shape used purely assync.Map/map memoization entries in two files.Recommendation: Introduce a generic
cacheEntry[T any]{ value T; err error }and instantiate it ascacheEntry[string],cacheEntry[latestBranchCommitInfo], etc. — collapses 5 declarations into 1.Estimated effort: 1-2 hours · Benefits: one shape to reason about, new caches don't need a new hand-written struct
Cluster 2: "Before/after/changed" delta concept modeled twice
Impact: Medium-High — the audit-diff domain solved this once correctly, then re-solved it badly next door
pkg/cli/audit_comparison.go:45,51already has the generalized answer:But
pkg/cli/audit_diff.goindependently re-flattens the same before/after/changed concept as raw field trios at least 5 times instead of reusing it:TokenUsageDiff(:276, 9 separate before/after/change field trios)ToolCallDiffEntry(:302)RunMetricsDiff(:346)GitHubRateLimitDiff(:369)DomainDiffEntry's Allowed/Blocked pairs (:31)Recommendation: Replace the
Run1X/Run2X/XChangefield trios inaudit_diff.gowith a sharedDelta[T]{ Before, After T; Change string }(or reuse/extendAuditComparisonIntDelta/AuditComparisonStringDelta) embedded per metric.Estimated effort: 3-4 hours (touches several call sites) · Benefits: single source of truth for "what changed between two audit runs," easier to add new diffed metrics
Cluster 3:
agentUsageEntryre-declaresTokenCoreMetricsinstead of embedding itImpact: Medium — the one miss in an otherwise well-factored file
pkg/cli/token_usage_types.go:12definesTokenCoreMetricsand explicitly documents it as "the single source of truth ... shared across per-request, per-model, and per-run representations" — andTokenUsageEntry/ModelTokenUsagedo embed it correctly. ButagentUsageEntryatpkg/cli/token_usage_types.go:107independently re-declares the identical 6 fields (InputTokens,OutputTokens,CacheReadTokens,CacheWriteTokens,ReasoningTokens,EffectiveTokens) with identical JSON tags instead of embeddingTokenCoreMetrics.Recommendation: Embed
TokenCoreMetricsinagentUsageEntry; keepProvider,Model,PrimaryModel,AmbientContextTokens,AICreditsas its own fields.Estimated effort: 30 minutes · Benefits: closes the one gap in an already-good consolidation, prevents the two shapes drifting apart
Cluster 4:
DifcFilteredEventduplicates 11 ofGatewayLogEntry's fields verbatimImpact: Medium — both types are parsed from the same
gateway.jsonlsource data, so a strict field-subset relationship maintained by hand will driftGatewayLogEntry(pkg/cli/gateway_logs_types.go:18, 22 fields) is the generic gateway-log record.DifcFilteredEvent(pkg/cli/gateway_logs_types.go:45) re-lists 11 of those same fields (Timestamp,ServerID,ToolName,Description,Reason,SecrecyTags,IntegrityTags,AuthorAssociation,AuthorLogin,HTMLURL,Number) with identical names/types/JSON tags.Recommendation: Extract a
DifcFilteredFieldssub-struct (or projectDifcFilteredEventfromGatewayLogEntry) instead of maintaining a parallel field list.Estimated effort: 1-2 hours · Benefits: one edit updates both representations of the same source record
Cluster 5:
ArgumentandEnvironmentVariable(MCP registry types) model the same "named option" conceptImpact: Low-Medium — external-schema-shaped, so verify against the upstream spec before touching
EnvironmentVariable(pkg/cli/mcp_registry_types.go:87) is an 8-field strict subset ofArgument's 13 fields (pkg/cli/mcp_registry_types.go:70) —Name,Description,IsRequired,IsSecret,Default,Format,Placeholder,Choicesappear in both with identical names/types.Transport.HeadersandRemote.Headersalso reuseEnvironmentVariablefor what is conceptually argument-like data.Recommendation: Factor a shared
NamedOption{ Name, Description, Format, Placeholder string; IsRequired, IsSecret bool; Default string; Choices []string }and embed it in both — but confirm the upstream MCP registry OpenAPI schema permits this shape before refactoring, since these types likely mirror an external contract.Estimated effort: 2-3 hours (schema verification + refactor) · Benefits: one option-shape to validate/serialize, less risk of the two drifting on a shared concept
Untyped Usages
Scope note: the project already has an internal convention (documented in
scratchpad/go-type-patterns.md) thatmap[string]any/anyis the correct, idiomatic choice for dynamic YAML/JSON frontmatter fields — and it's followed consistently (2,500+map[string]anyoccurrences, almost all in frontmatter parsing/config extraction). Those were excluded as intentional. The findings below are places where the codebase's own pattern of named string-enum types (GitHubIntegrityLevel,ReactionType,EngineName,GitHubMCPMode— all correctly applied elsewhere) was not applied to sibling fields sharing the exact same value domain, plus one alias that'sanyin name only.Summary Statistics
any: 1anyparameter safely narrowable to a concrete type: 1Category 1: Untyped fields duplicating an existing enum's value domain
Impact: Medium — the type-safe version already exists two lines away; these fields just don't use it
pkg/workflow/tools_types.go:329—MinIntegrityis correctly typedGitHubIntegrityLevel(enumnone/unapproved/approved/merged, defined attools_types.go:290-302).pkg/workflow/tools_types.go:367,372— sibling fieldsDisapprovalIntegrity stringandEndorserMinIntegrity stringuse the same value domain as plainstring.pkg/workflow/tools_validation_github_integrity_reactions.go:22-33then hard-codes two moremap[string]boolliterals (validDisapprovalIntegrityLevels,validEndorserMinIntegrityLevels) re-listing"none"/"unapproved"/"approved"/"merged"— whiletools_validation_github.go:173builds the type-safemap[GitHubIntegrityLevel]boolfor the one field that's already typed correctly.Suggested fix:
Category 2: Duplicated GraphQL reaction-content string set
Impact: Medium — the codebase already solved this exact problem once for a different reaction set
EndorsementReactions []string/DisapprovalReactions []string(pkg/workflow/tools_types.go:357,363) hold a closed 8-value GraphQLReactionContentset (THUMBS_UP,THUMBS_DOWN,HEART,HOORAY,CONFUSED,ROCKET,EYES,LAUGH), validated via avalidReactionContents map[string]bool(tools_validation_github_integrity_reactions.go:10-19) and then duplicated again as raw literals inpkg/workflow/mcp_github_config.go:359,363(DefaultEndorsementReactions,DefaultDisapprovalReactions).pkg/workflow/reactions.go:12-23already shows the right pattern for the REST-style reaction set (type ReactionType string+ named constants) — it just never got extended to the GraphQL-style set.Suggested fix: add
type GitHubReactionContent stringwith the 8 named constants; retype the two slice fields as[]GitHubReactionContent; replacevalidReactionContentswith amap[GitHubReactionContent]bool(mirrors the existingGitHubIntegrityLevelpattern).Category 3: A named type that provides no type safety
Impact: Low-Medium — misleading more than dangerous, since it's still narrowed correctly downstream
pkg/workflow/tools_types.go:306—type GitHubReposScope any // string or []any (YAML-parsed arrays are []any), used forAllowedRepos/Repos(lines 325, 327). Naming it suggests a real type, butanytyped asGitHubReposScopestill accepts anything. The sole consumer,canonicalReposScopeinpkg/workflow/cache_integrity.go:125-159, only ever handles 3 concrete shapes (string,[]any,[]string) via a type switch with a silentdefaultproducing"".Suggested fix: normalize at parse time into
[]string(plus a companion sentinel/enum for "all"/"public"), or at minimum wrap in a real sum-type struct rather than aliasingany.Category 4:
anyparameter narrowable to a concrete typeImpact: Low — weak evidence, single call site, but a free simplification
pkg/workflow/step_shell_validator.go:118—func checkStepGHToken(step any, workflowHasGHToken bool) stringimmediately doesstepMap, ok := step.(map[string]any). Its only caller (step_shell_validator.go:85) already has amap[string]anyvalue in hand before calling it. The extraanylayer adds no flexibility, just an extra silently-swallowedok == falsepath.Suggested fix: change the parameter to
map[string]anydirectly and drop the assertion.Reviewed and judged idiomatic (no action):
RunsOn any/Engine any/Checkout any/Headers anyinfrontmatter_types.goandruns_on_unmarshal.go, and the variousanyparams inpkg/parser/mcp.go,engine_config_parser.go,workflow_builder_model_overlays.go— all genuinely polymorphic YAML/JSON frontmatter fields normalized into typed structs shortly after ingestion, consistent with the project's documented convention.Refactoring Recommendations
Priority 1 — Quick, safe wins (do first)
TokenCoreMetricsinagentUsageEntry(Cluster 3) — 30 minDisapprovalIntegrity/EndorserMinIntegritytoGitHubIntegrityLeveland delete the two duplicated bool-maps (Untyped Category 1) — 1 hourcheckStepGHToken's parameter tomap[string]any(Untyped Category 4) — 15 minPriority 2 — Medium effort, real drift-prevention value
GitHubReactionContentenum type and retype the GraphQL reaction slices, removing the duplicated literal sets (Untyped Category 2) — 1-2 hourscacheEntry[T any]{ value T; err error }to collapse the 5 cache-tuple structs (Cluster 1) — 1-2 hoursDifcFilteredFieldssoDifcFilteredEventstops hand-duplicatingGatewayLogEntry(Cluster 4) — 1-2 hoursPriority 3 — Larger refactors, plan separately
Run1X/Run2X/XChangetrios inaudit_diff.gowith a sharedDelta[T]type reused fromaudit_comparison.go(Cluster 2) — 3-4 hours, touches multiple call sitesNamedOptionforArgument/EnvironmentVariable— only after confirming the upstream MCP registry schema allows it (Cluster 5) — 2-3 hoursImplementation Checklist
TokenCoreMetricsinagentUsageEntryDisapprovalIntegrity/EndorserMinIntegrityasGitHubIntegrityLevel; remove duplicated validation mapscheckStepGHTokentomap[string]anyGitHubReactionContentenum; retypeEndorsementReactions/DisapprovalReactionscacheEntry[T any]and migrate the 5 cache-tuple structsDifcFilteredEventandGatewayLogEntryaudit_diff.go's flat before/after/change trios with a sharedDelta[T]NamedOptionforArgument/EnvironmentVariableAnalysis Metadata
*.go; type-declaration scan covered all non-test, non-testdata filesReferences:
All reactions