[typist] Typist Go Type Consistency Analysis - duplicate types and untyped usages in pkg #62624
Closed
Replies: 1 comment
|
This discussion has been marked as outdated by Typist - Go Type Analysis. A newer discussion is available at Discussion #62931. |
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.
🔤 Typist - Go Type Consistency Analysis
Analysis of repository: github/gh-aw
Executive Summary
I scanned roughly 1,375 non-test
.gofiles underpkg/(about 950-1,000 top-level type definitions, concentrated inpkg/workflowandpkg/cli) for duplicated types and weakly-typed (any) usage. The good news first: the codebase already practices a lot of good hygiene here —interface{}is essentially extinct in production code (everything usesany), and several type families (BaseMCPServerConfig,MCPServerStatsBase, thevalidationerror.Payloadembed) show the team has already done real consolidation work. The remaining opportunities cluster tightly in one area: thepkg/cliaudit/logs reporting subsystem, which has grown several near-identical "compare two runs" and "domain analysis" struct families independently instead of sharing one base type.On the untyped-usage side, the vast majority of the ~2,800+
map[string]any/[]anyoccurrences are legitimate — they represent not-yet-validated YAML/JSON frontmatter, which is exactly whatanyis for. But I found about a dozen high-value spots whereany(or a bareint) is standing in for a small, known set of concrete types, including one exported function (WaitForWorkflowCompletion) whosetimeoutMinutes intparameter has no compiler-enforced unit, and five sibling Azure DevOps config structs that each declare their ownTarget anyfield with no validation logic anywhere behind them.Full Analysis Report
Duplicated Type Definitions
Summary Statistics
pkg/)Cluster 1:
AuditComparison*Delta/ diff-entry family — Semantic + ExactType: Exact duplicate (the two Delta types) nested inside a broader semantic duplicate (two independent "diff two runs" frameworks)
Impact: High — both live in
pkg/cli, both power theauditcommand's compare/diff outputLocations:
pkg/cli/audit_comparison.go:45—AuditComparisonIntDelta{Before int; After int; Changed bool}pkg/cli/audit_comparison.go:51—AuditComparisonStringDelta{Before string; After string; Changed bool}(identical to rejig docs #1 except field type)pkg/cli/audit_diff.go:24—DiffEntryBase{Status; IsAnomaly; AnomalyNote}, embedded byDomainDiffEntry:31pkg/cli/audit_diff.go:246,302—MCPToolDiffEntry,ToolCallDiffEntryuse a differently-shapedRun1X/Run2Xpattern for the same "compare a metric across two runs" conceptDefinition Comparison:
Recommendation:
Deltatypes with one generic:type Delta[T comparable] struct { Before, After T; Changed bool }DiffEntryBase-style entries with theRun1X/Run2X-style entries onto one shared shapeCluster 2:
RedactedDomainsAnalysis/RedactedDomainsLogSummary— ExactImpact: Medium — inconsistent with the codebase's own established pattern for this exact scenario
Locations:
pkg/cli/redacted_domains.go:21—RedactedDomainsAnalysis{TotalDomains int; Domains []string}pkg/cli/redacted_domains.go:29—RedactedDomainsLogSummary{TotalDomains int; Domains []string; ByWorkflow map[string]*RedactedDomainsAnalysis}Definition Comparison:
The summary type repeats the analysis type's two fields verbatim instead of embedding it — notably, the sibling pattern
AccessLogSummary/FirewallSummaryBase(pkg/cli/logs_report_firewall.go:11-26) already does this correctly via embedding.Recommendation:
DomainCountBase{TotalDomains int; Domains []string}and embed it in both types, matching the firewall summary patternCluster 3: Domain/firewall "analysis" family — Semantic
Impact: Medium — 3 files model the same "aggregate domain traffic from a log" concept differently
Locations:
pkg/cli/domain_buckets.go:41—AnalysisBase(embedsDomainBuckets)pkg/cli/access_log.go:35—DomainAnalysis(embedsAnalysisBase)pkg/cli/firewall_log.go:133—FirewallAnalysis(embedsAnalysisBase)pkg/cli/redacted_domains.go:21—RedactedDomainsAnalysis(bespoke, non-embedding shape — see Cluster 2)Recommendation: Bring
RedactedDomainsAnalysisonto the sameAnalysisBaseembed as its two siblings once Cluster 2 is fixed.Cluster 4: Rate-limit quota types — Near
Impact: Medium — two authors must hand-sync the same GitHub REST quota fields
Locations:
pkg/cli/logs_rate_limit.go:113—GitHubAPIRateLimitState{Limit int; Remaining int; Reset int64; Used int}pkg/cli/logs_github_rate_limit_usage.go:33—GitHubRateLimitEntry{..., Limit int; Remaining int; Used int; Reset string, ...}Recommendation:
RateLimitQuota{Limit, Remaining, Used int}and embed it in both; keepReset(differently typed:int64snapshot vs.stringlog field) on each specific typeCluster 5:
safeOutputTargetConfigvsSafeOutputTargetConfig— Naming collisionType: Not a true field duplicate, but a same-package naming hazard
Impact: High risk, low volume — case-only difference invites autocomplete/typo bugs
Locations:
pkg/workflow/safe_outputs_validation.go:79— unexportedsafeOutputTargetConfig{name string; target string}pkg/workflow/safe_outputs_parser.go:9— exportedSafeOutputTargetConfig{Target string; TargetRepoSlug string; AllowedRepos []string}Recommendation: Rename the unexported validation-only struct (e.g.
targetValidationEntry) to eliminate the case-only collision withinpkg/workflow.Estimated effort: 30 minutes
Cluster 6:
ValidationErrorfamily — Semantic (mostly already unified)Locations:
pkg/validationerror/validationerror.go:25,49— sharedPayload+ValidationErrorinterfacepkg/parser/validation_error.go:12—ValidationError{validationerror.Payload}pkg/workflow/workflow_errors.go:41—WorkflowValidationError{validationerror.Payload; Severity; Category; Timestamp; File; Line; Column}Already consolidated via the shared
Payloadembed. Flagged only becauseworkflowindependently addsFile/Line/Columnlocation fields that other validation-error consumers might also want promoted into the shared package. Not an action item on its own — worth a quick confirm, not a rewrite.Cluster 7: Safe-output
TitlePrefixfield — Near, low impactLocations:
CreateIssuesConfig(pkg/workflow/create_issue.go:15),CreateDiscussionsConfig(pkg/workflow/create_discussion.go:17),CreateProjectsConfig(pkg/workflow/create_project.go:11) each redeclareTitlePrefix stringinstead of embedding a shared field.Recommendation: Add a
TitlePrefixConfig{TitlePrefix string}embed to match the existingBaseSafeOutputConfig/CloseOlderConfigpattern already used by these same structs. Estimated effort: 30 minutes.Cluster 8: Azure DevOps work-item config family — Near, single file
Locations:
pkg/workflow/safe_outputs_azure_devops.go—CreateWorkItemConfig:19,UpdateWorkItemConfig:32,CommentOnWorkItemConfig:50,AssignWorkItemConfig:55,LinkWorkItemsConfig:62,UploadWorkItemAttachmentConfig:68each embedBaseSafeOutputConfigand independently redeclareTarget any(lines 40, 52, 57, 64, 70).Recommendation: Shared
TargetableWorkItemConfig{Target any}embed (see also the untyped-usage finding on this same field below). Estimated effort: 1 hour.Already-consolidated patterns worth pointing to as precedent
pkg/types/mcp.go:6BaseMCPServerConfig, embedded bypkg/parser/mcp.go:47andpkg/workflow/tools_types.go:502— a deliberate, well-commented consolidation.pkg/cli/logs_models.go:211MCPServerStatsBase, embedded byMCPServerStats,MCPServerHealthDetail, andMCPServerCrossRunHealth— the struct fields are unified, though each embedder still hand-rolls its ownMarshalJSON/UnmarshalJSONto preserve legacy wire keys (audit_expanded.go:101-150,audit_cross_run.go:95-140); that boilerplate could become one shared helper.WorkflowRun(pkg/cli/logs_models.go:68),GitHubWorkflow(pkg/cli/workflows.go:64),JSONWorkflow(pkg/cli/jsonworkflow_to_markdown.go:23),WorkflowFile(pkg/workflow/workflow_file.go:8) — four different representations of "a workflow" at different layers (run instance / API definition / export format / compiled artifact). Field overlap is minimal; this reads as legitimate layering, not copy-paste — informational only.Untyped Usages
Summary Statistics
interface{}usages in production code: ~0 (only appears in linter testdata fixtures, out of scope)anyin function params: ~180anyin return types: ~15-20anyin struct fields: ~45map[string]any/[]any: 2,800+ (the large majority are legitimate — see exclusions below)constdeclarationsCategory 1: Exported function with an unenforced time unit
Impact: High — public API, unit encoded only in the parameter name
pkg/cli/pr_automerge.go:110pkg/cli/run_workflow_execution.go:468, withworkflowCompletionWaitTimeoutMinutes— itself defined asconst workflowCompletionWaitTimeoutMinutes = 6 * 60(run_workflow_execution.go:25), where the* 60is a giveaway this is really a duration in disguisetime.Durationmakes the unit part of the type systemCategory 2:
anystruct fields with no validation behind themImpact: High — polymorphism that's structurally allowed but never actually parsed
pkg/workflow/safe_outputs_azure_devops.go:40,52,57,64,70CreateWorkItemConfig,UpdateWorkItemConfig,CommentOnWorkItemConfig,AssignWorkItemConfig,LinkWorkItemsConfig.string(add_comment.go:18,push_to_pull_request_branch.go:16,safe_outputs_parser.go:10, 10+ sites total). The JSON schema genuinely allowsinteger | "*" | []integer | stringfor Azure DevOps targets, soanyisn't arbitrary — but the only code that touches these fields (addAzureDevOpsTarget/appendAzureDevOpsTargetConstraint, both in the same file) passes the value straight through into amap[string]anyor a%vformat string with no type switch or validation anywhere.type WorkItemTarget struct { ID *int; IDs []int; Wildcard bool; Path string }with a customUnmarshalYAML, replacing all 5 fields — turns an undocumented, unvalidated shape into one validated type used everywhere.Category 3: Untyped duration constants
Impact: Medium — the unit is only documented in a comment, not enforced
pkg/constants/job_constants.go:309-310:DefaultRateLimitWindowshould betime.Duration(e.g.time.Hour);DefaultRateLimitMaxis a plain count and is fine as-is.pkg/workflow/publish_code_coverage.go:18:WaitForProcessingTimeout int(yaml tagwait-for-processing-timeout,omitempty, commented// seconds) at line 40 — same int-seconds-by-convention pattern.pkg/constants/constants.go(e.g.DefaultToolTimeout = 60 * time.Second) —160 * time.Second, field retyped totime.Duration.Category 4:
anystanding in for a small closed set of concrete typesImpact: Medium
pkg/console/console_types.go:50—FormField.Value any(commented "pointer to the value to store the result"). The adjacentType stringdiscriminator ("input"/"password"/"confirm"/"select") implies callers always pass one of a small set of pointer kinds. Suggested fix: a genericFormField[T any]per field kind, or at minimum document/enforce theType→pointer-kind contract with a switch.pkg/workflow/safe_output_handlers.go:13-19— registry table where every entry'sNewConfig func() anyreturns a different concrete*XConfig, immediately type-asserted back at the single call site. Suggested fix: aSafeOutputConfigmarker interface (e.g. requiringValidate() error) instead of bareany, so the registry is self-documenting.pkg/cli/gateway_logs_types.go:179,190—ID any(json tagid) in two separate JSON-RPC structs. Protocol-mandated polymorphism (string/number/null) makesanydefensible here, but the same pattern is duplicated across two structs; a sharedRPCIDwrapper type would avoid two independent sets of ad-hoc type assertions downstream.Category 5: Numeric limit stored as a string constant
pkg/constants/constants.go:417compilerenv/manager.go:238. Defensible, but storing it asconst DefaultMaxDailyAICredits = 5000(int) and callingstrconv.Itoaat the interpolation site keeps the "this is a number" invariant machine-checked from the point of definition.What was excluded as legitimate
anyusage (and why)map[string]any/[]anyfor YAML/JSON frontmatter parsing — the large majority of all hits, concentrated inpkg/parser/*andpkg/workflow/frontmatter_*. This is pre-validation, arbitrary user-authored YAML;anyhere correctly represents "not yet validated," and the values are checked against a JSON Schema before becoming typed structs.pkg/typeutil/convert.go(ParseIntValue,ConvertToInt,ConvertToFloat) — genuinely heterogeneous numeric coercion across 30+ call sites where the source's underlying Go type varies by decoder (intvsfloat64vsstring); a well-documented, textbook-legitimate generic utility.pkg/console/render.go:31RenderStruct(v any)— reflection-based generic pretty-printer, correctly generic by design.Get/Post/Patch(path string, response any) errorinpkg/parser/remote_resolve_sha.go,pkg/cli/update_check.go,pkg/cli/bootstrap_profile_actions_repo.go) and thinencoding/jsonwrappers (pkg/cli/preconditions.go:159,pkg/cli/trial_helpers.go:361,pkg/cli/setup_repository.go:441) — standard Go idiom for unmarshal/marshal targets; a generics rewrite is a stylistic option, not a safety fix.Raw anyfields taggedyaml:"-"(pkg/workflow/repo_memory.go:68,pkg/workflow/tools_types.go:482,488,495) — intentionally preserve the original decoded value for round-tripping/debugging, excluded from serialization by design.pkg/workflow/frontmatter_types.go(Engine,RunsOn,imports,checkout,evals,graders— 6 fields) — genuinely polymorphic per YAML author ergonomics (string-or-array-or-object, matching GitHub Actions' ownruns-on/onschemas). A full sum-type rewrite here would be large and risky for marginal safety gain — lowest priority, informational only.Refactoring Recommendations
Priority 1:
pkg/cliaudit/diff/logs reporting subsystem (Clusters 1-4)Recommendation: Consolidate the diff-delta types, fix the
RedactedDomains*embedding inconsistency, and unify the rate-limit quota fields.Steps:
Delta[T comparable]and migrateAuditComparisonIntDelta/AuditComparisonStringDeltaonto itDomainCountBaseand embed it inRedactedDomainsAnalysis/RedactedDomainsLogSummary, matching the existingAccessLogSummary/FirewallSummaryBasepatternRateLimitQuotaand embed it inGitHubAPIRateLimitState/GitHubRateLimitEntrypkg/clitest suite to verify no JSON-tag/serialization breakageEstimated effort: 6-8 hours combined
Impact: High — removes the bulk of the codebase's real type duplication in one pass
Priority 2: Exported timeout API + Azure DevOps
TargetfieldsRecommendation: Retype
WaitForWorkflowCompletion's timeout parameter totime.Duration, and introduce a validatedWorkItemTargettype for the 5 Azure DevOps config structs.Steps:
WaitForWorkflowCompletionsignature and its one call siteWorkItemTargetwithUnmarshalYAML, replace the 5Target anyfields, add validation inaddAzureDevOpsTargetEstimated effort: 4-6 hours
Impact: High — closes an actual unit-safety gap in an exported function, and turns an unvalidated
anyinto a real contractPriority 3: Duration constants and small cleanups
Recommendation: Retype
DefaultRateLimitWindowanddefaultCodeCoverageWaitForProcessingTimeouttotime.Duration; renamesafeOutputTargetConfigto remove the case-collision withSafeOutputTargetConfig; add theTitlePrefixConfigembed.Estimated effort: 3-4 hours combined
Impact: Medium — clarity and readability wins, low risk
Implementation Checklist
Delta[T]and migrate the twoAuditComparison*DeltatypesRedactedDomainsAnalysis/RedactedDomainsLogSummaryto embed a shared baseRateLimitQuotabase for the two rate-limit typessafeOutputTargetConfigto remove the naming collision withSafeOutputTargetConfigWaitForWorkflowCompletion'stimeoutMinutes inttotime.DurationWorkItemTargetand replace the 5Target anyfields insafe_outputs_azure_devops.goDefaultRateLimitWindowand the code-coverage timeout constant totime.DurationTitlePrefixConfigembed to the three safe-output config structsAnalysis Metadata
pkg/any-in-params, ~45any-in-fields, and 2,800+map[string]any/[]anyoccurrences (the vast majority of the latter are legitimate pre-validation frontmatter parsing)Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.
All reactions