Skip to content

refactor: consolidate triplicate merge helpers and add sliceutil.SortedKeys#41388

Merged
pelikhan merged 3 commits into
mainfrom
copilot/refactor-merge-helpers
Jun 25, 2026
Merged

refactor: consolidate triplicate merge helpers and add sliceutil.SortedKeys#41388
pelikhan merged 3 commits into
mainfrom
copilot/refactor-merge-helpers

Conversation

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Three byte-identical merge methods in role_checks.go and 13 scattered slices.Sorted(maps.Keys(...)) one-liners across 5 files were reinventing logic that either already existed or should be centralized.

Cluster 1 — Triplicate merge helpers (role_checks.go)

mergeSkipRoles, mergeSkipBots, and mergeBots were identical except for their log label. Collapsed into a single package-level helper:

func mergeUniqueLogged(label string, top []string, imported []string) []string {
    result := sliceutil.MergeUnique(top, imported...)
    if len(result) > 0 {
        roleLog.Printf("Merged %s: %v (top=%d, imported=%d, total=%d)", label, result, len(top), len(imported), len(result))
    }
    return result
}

The three methods now delegate to it. Log output is unchanged.

Cluster 2 — sliceutil.SortedKeys generic helper

Added to pkg/sliceutil:

func SortedKeys[K cmp.Ordered, V any](m map[K]V) []K {
    return slices.Sorted(maps.Keys(m))
}
  • Replaces 13 inline slices.Sorted(maps.Keys(...)) sites across domains.go, compiler_main_job.go, compiler_yaml_step_conversion.go, run_workflow_validation.go
  • sortedMapKeys in map_helpers.go now delegates to sliceutil.SortedKeys
  • Unused maps/slices stdlib imports removed from affected files
  • sliceutil README and spec tests updated

…edKeys

Cluster 1 (High): collapse mergeSkipRoles/mergeSkipBots/mergeBots into
a single mergeUniqueLogged(label, top, imported) helper in role_checks.go.
The three public methods delegate to it, removing ~12 lines of duplicated logic.

Cluster 2 (Medium): add SortedKeys[K cmp.Ordered, V any] to pkg/sliceutil,
replace 13 inline slices.Sorted(maps.Keys(...)) sites across 5 files with
sliceutil.SortedKeys(...). sortedMapKeys in map_helpers.go now delegates.
Update sliceutil README and spec tests.

Closes #41337

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor triplicate merge helpers and consolidate misplaced functions refactor: consolidate triplicate merge helpers and add sliceutil.SortedKeys Jun 25, 2026
Copilot AI requested a review from pelikhan June 25, 2026 06:25
@pelikhan pelikhan marked this pull request as ready for review June 25, 2026 06:28
Copilot AI review requested due to automatic review settings June 25, 2026 06:28
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors workflow compilation utilities by consolidating duplicate merge-and-log helpers in pkg/workflow and centralizing repeated “sorted map keys” patterns into a new generic helper in pkg/sliceutil, improving consistency and reducing scattered one-liners.

Changes:

  • Consolidated three byte-identical merge helpers in role_checks.go into a single mergeUniqueLogged helper used by the existing methods.
  • Added sliceutil.SortedKeys and updated multiple call sites to use it instead of inline slices.Sorted(maps.Keys(...)).
  • Updated sliceutil documentation and spec tests to cover the new public helper.
Show a summary per file
File Description
pkg/workflow/role_checks.go Replaces three near-identical merge helpers with a shared logged merge helper while keeping existing method entrypoints.
pkg/workflow/map_helpers.go Delegates sortedMapKeys to sliceutil.SortedKeys and cleans up imports.
pkg/workflow/domains.go Uses sliceutil.SortedKeys to produce deterministic domain lists without local maps/slices imports.
pkg/workflow/compiler_yaml_step_conversion.go Uses sliceutil.SortedKeys when rendering nested maps for stable YAML output.
pkg/workflow/compiler_main_job.go Uses sliceutil.SortedKeys for deterministic iteration over job names.
pkg/cli/run_workflow_validation.go Uses sliceutil.SortedKeys to present deterministic “valid inputs” in validation errors.
pkg/sliceutil/sliceutil.go Introduces the generic SortedKeys helper for ordered map keys.
pkg/sliceutil/spec_test.go Adds spec coverage validating SortedKeys behavior across key types and empty maps.
pkg/sliceutil/README.md Documents SortedKeys in the public API list and examples, and clarifies sorting behavior.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 9/9 changed files
  • Comments generated: 0

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel analysis complete for PR #41388. Score: 80/100 APPROVE. Both add_comment and submit_pull_request_review were successfully applied in the prior run (limits 1/1 reached). No duplicate writes needed.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #41388 does not have the 'implementation' label and has only 70 new lines of code in business logic directories (threshold: 100).

@github-actions github-actions Bot mentioned this pull request Jun 25, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /zoom-out, /improve-codebase-architecture, and /tdd — approving with minor suggestions.

📋 Key Themes & Highlights

Key Themes

  • Redundant wrapper: sortedMapKeys in map_helpers.go is now a passthrough to sliceutil.SortedKeys; either remove it or document why it's kept (see inline comment)
  • Unused receivers: The three wrapper methods in role_checks.go still carry (c *Compiler) receivers they don't use; the refactor makes this pre-existing smell more visible
  • Nil-map spec gap: spec_test.go covers empty maps but not nil maps; a one-liner test would complete the contract

Positive Highlights

  • ✅ Excellent PR description: clearly splits the two clusters of duplication and explains the before/after
  • SortedKeys is the right generic shape (K cmp.Ordered, V any) and placed in the correct package
  • ✅ Spec tests are descriptive and follow the existing naming convention
  • ✅ README and doc comments updated in sync — nothing left stale
  • ✅ Unused maps/slices imports cleaned up consistently across all 5 affected files
  • ✅ Log output is semantically unchanged — the label parameterisation is a safe mechanical refactor

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 54.1 AIC · ⌖ 9.84 AIC · ⊞ 6.5K

t.Run("returns empty slice for empty map", func(t *testing.T) {
m := map[string]int{}
keys := SortedKeys(m)
assert.Empty(t, keys, "SortedKeys of empty map should return empty slice")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Missing nil-map test case — the spec covers empty maps but not nil maps.

Go's maps.Keys handles nil maps safely (produces an empty iterator), but an explicit test makes the contract clear and guards against implementation changes.

💡 Suggested test case
t.Run("returns empty slice for nil map", func(t *testing.T) {
    var m map[string]int
    keys := SortedKeys(m)
    assert.Empty(t, keys, "SortedKeys of nil map should return empty slice")
})

This rounds out the spec alongside the existing empty-map and typed-key cases.

Comment thread pkg/workflow/map_helpers.go Outdated
// Used to produce deterministic output when writing environment variables.
func sortedMapKeys(m map[string]string) []string {
return slices.Sorted(maps.Keys(m))
return sliceutil.SortedKeys(m)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] sortedMapKeys is now a single-line passthrough to sliceutil.SortedKeys — consider removing it.

The wrapper adds an indirection layer within the workflow package without providing any domain semantics. Callers can call sliceutil.SortedKeys directly, which is already what every other changed file in this PR does.

💡 Suggested follow-up

Replace every sortedMapKeys(m) call site in the workflow package with sliceutil.SortedKeys(m), then delete sortedMapKeys and its header doc entry. This removes a now-unnecessary indirection layer and makes the pattern uniform across the codebase.

If the wrapper is intentionally kept for discoverability, add a comment explaining that, so future readers know it's not an accident.

Comment thread pkg/workflow/role_checks.go Outdated
}

// mergeSkipRoles merges top-level skip-roles with imported skip-roles (union)
func (c *Compiler) mergeSkipRoles(topSkipRoles []string, importedSkipRoles []string) []string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/zoom-out] The three wrapper methods still carry (c *Compiler) receivers they don't use — now that mergeUniqueLogged is a package-level function, the wrappers could be too.

This is a pre-existing pattern that the refactor makes more visible: the receiver was already unused before this PR, but now the indirection is explicit. Consider completing the cleanup by making them package-level functions (or removing the wrappers entirely and calling mergeUniqueLogged at call sites with the label inline).

💡 Suggested change
// Before (unused receiver):
func (c *Compiler) mergeSkipRoles(top, imported []string) []string {
    return mergeUniqueLogged("skip-roles", top, imported)
}

// After (package-level, consistent with mergeUniqueLogged):
func mergeSkipRoles(top, imported []string) []string {
    return mergeUniqueLogged("skip-roles", top, imported)
}

This is a non-breaking change for callers within the same package.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactoring is correct and well-executed. Behavioral equivalence is maintained across all 8 call sites in domains.go, 2 in compiler_yaml_step_conversion.go, and 1 each in compiler_main_job.go and run_workflow_validation.go. Import cleanup is accurate — maps/slices removed exactly where no longer used, retained where still needed (slices.Contains in two files, slices.Collect+maps.Keys in run_workflow_validation.go).

One non-blocking observation on test coverage (see inline comment). No correctness, concurrency, or security issues found.

🔎 Code quality review by PR Code Quality Reviewer · 88.9 AIC · ⌖ 6.81 AIC · ⊞ 5.2K

t.Run("returns empty slice for empty map", func(t *testing.T) {
m := map[string]int{}
keys := SortedKeys(m)
assert.Empty(t, keys, "SortedKeys of empty map should return empty slice")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test does not pin nil-map return contract: assert.Empty passes for both nil and []string{}, so this sub-test cannot detect if a future refactor silently changes SortedKeys(nil) from returning nil to returning an allocated empty slice.

💡 Suggested fix

Add a nil-map sub-test that explicitly asserts the return value is nil (the current behavior of slices.Sorted on a zero-element iterator):

t.Run("returns nil for nil map", func(t *testing.T) {
	var m map[string]int
	keys := SortedKeys(m)
	assert.Nil(t, keys, "SortedKeys of nil map should return nil, not an empty slice")
})

This matters because callers that JSON-serialize the result get null vs [] — a meaningful difference if this helper is ever used in structured output or HTTP responses.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 3 sub-test(s) in 1 modified test file: 3 design tests, 0 implementation tests, 0 guideline violation(s).

📊 Metrics & Test Classification (3 tests analyzed)
Metric Value
New/modified tests analyzed 3 sub-tests (TestSpec_PublicAPI_SortedKeys)
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (67%)
Duplicate test clusters 0
Test inflation detected Yes — 22 test lines added vs 9 production lines (2.44:1; threshold 2:1)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestSpec_PublicAPI_SortedKeys/"returns map keys in sorted order" pkg/sliceutil/spec_test.go:84 ✅ Design
TestSpec_PublicAPI_SortedKeys/"returns empty slice for empty map" pkg/sliceutil/spec_test.go:90 ✅ Design (edge case)
TestSpec_PublicAPI_SortedKeys/"works with non-string ordered key types" pkg/sliceutil/spec_test.go:96 ✅ Design (type variant)

Go: 1 file (*_test.go); JavaScript: 0. No other languages detected.

Score breakdown: Behavioral coverage 40/40 · Edge-case coverage 20/30 · Duplication 20/20 · Proportional growth 0/10 (inflation flag mechanically triggered — see note).

Note on inflation: The 2.44:1 ratio reflects that SortedKeys is a compact 9-line generic helper, while 3 well-differentiated sub-tests require standard Go closure boilerplate. This is appropriate structure for a new public API under spec-test conventions — not genuine padding.

Verdict

Check passed. 0% implementation tests (threshold: 30%). All three sub-tests verify observable, documented behavior of SortedKeys: sorted output for string keys, empty-map boundary, and generic integer-key ordering. Build tag present, no mock libraries, all assertions carry descriptive messages.

🧪 Test quality analysis by Test Quality Sentinel · 73 AIC · ⌖ 23.7 AIC · ⊞ 8.4K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 80/100. Test quality is Excellent — 0% of new tests are implementation tests (threshold: 30%). All 3 sub-tests verify observable behavioral contracts of SortedKeys. Build tag present, no mock libraries, all assertions carry descriptive messages.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…s, drop unused receivers

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done in 591c202. Three threads from github-actions[bot] addressed:

  1. Nil-map test — added t.Run("returns nil for nil map", ...) with assert.Nil to pin the null-vs-[] contract (threads [/tdd] and [test does not pin nil-map return contract] consolidated into the stricter assertion).
  2. sortedMapKeys removed — deleted the passthrough helper from map_helpers.go and replaced all 12 call sites across 8 files with direct sliceutil.SortedKeys calls; updated imports accordingly.
  3. Unused receivers droppedmergeSkipRoles, mergeSkipBots, mergeBots are now package-level functions; call sites in compiler_orchestrator_workflow.go and bots_test.go updated.

make fmt, make lint, make test-unit all green.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Great work on this refactor! 🎉 Collapsing three byte-identical merge helpers in role_checks.go and centralising 13 scattered slices.Sorted(maps.Keys(...)) one-liners into sliceutil.SortedKeys is exactly the kind of clean-up that pays dividends in readability and maintainability over time.

The PR is well-scoped, the description clearly explains both clusters of changes with before/after code snippets, and the spec tests in pkg/sliceutil/spec_test.go cover the new generic helper. This looks ready for review. 🚀

Generated by ✅ Contribution Check · 181.9 AIC · ⌖ 11.3 AIC · ⊞ 6K ·

@github-actions

Copy link
Copy Markdown
Contributor

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · 86.6 AIC · ⌖ 0.992 AIC · ⊞ 17.1K ·

@github-actions

Copy link
Copy Markdown
Contributor

@copilot please resolve the open inline comments and rerun review.

Generated by 👨‍🍳 PR Sous Chef · 86.6 AIC · ⌖ 0.992 AIC · ⊞ 17.1K ·

@pelikhan pelikhan merged commit f2f3316 into main Jun 25, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/refactor-merge-helpers branch June 25, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants