Skip to content

Commit aa635d6

Browse files
committed
wire minimal types into list tools, remove collection handling from pipeline
1 parent 86a41c2 commit aa635d6

File tree

7 files changed

+76
-167
lines changed

7 files changed

+76
-167
lines changed

pkg/github/issues.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,9 +1599,14 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
15991599
totalCount = fragment.TotalCount
16001600
}
16011601

1602-
optimizedIssues, err := response.OptimizeList(issues,
1603-
response.WithCollectionExtractors(map[string][]string{"labels": {"name"}}),
1604-
)
1602+
minimalIssues := make([]MinimalIssue, 0, len(issues))
1603+
for _, issue := range issues {
1604+
if issue != nil {
1605+
minimalIssues = append(minimalIssues, convertToMinimalIssue(issue))
1606+
}
1607+
}
1608+
1609+
optimizedIssues, err := response.OptimizeList(minimalIssues)
16051610
if err != nil {
16061611
return nil, nil, fmt.Errorf("failed to optimize issues: %w", err)
16071612
}

pkg/github/minimal_types.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ type MinimalBranch struct {
122122
Protected bool `json:"protected"`
123123
}
124124

125+
// MinimalTag is the trimmed output type for tag objects.
126+
type MinimalTag struct {
127+
Name string `json:"name"`
128+
SHA string `json:"sha"`
129+
}
130+
125131
// MinimalResponse represents a minimal response for all CRUD operations.
126132
// Success is implicit in the HTTP response status, and all other information
127133
// can be derived from the URL or fetched separately if needed.
@@ -672,6 +678,32 @@ func convertToMinimalBranch(branch *github.Branch) MinimalBranch {
672678
}
673679
}
674680

681+
func convertToMinimalRelease(release *github.RepositoryRelease) MinimalRelease {
682+
m := MinimalRelease{
683+
ID: release.GetID(),
684+
TagName: release.GetTagName(),
685+
Name: release.GetName(),
686+
Body: release.GetBody(),
687+
HTMLURL: release.GetHTMLURL(),
688+
Prerelease: release.GetPrerelease(),
689+
Draft: release.GetDraft(),
690+
Author: convertToMinimalUser(release.GetAuthor()),
691+
}
692+
693+
if release.PublishedAt != nil {
694+
m.PublishedAt = release.PublishedAt.Format(time.RFC3339)
695+
}
696+
697+
return m
698+
}
699+
700+
func convertToMinimalTag(tag *github.RepositoryTag) MinimalTag {
701+
return MinimalTag{
702+
Name: tag.GetName(),
703+
SHA: tag.GetCommit().GetSHA(),
704+
}
705+
}
706+
675707
func convertToMinimalReviewThreadsResponse(query reviewThreadsQuery) MinimalReviewThreadsResponse {
676708
threads := query.Repository.PullRequest.ReviewThreads
677709

pkg/github/pullrequests.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,13 +1149,15 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
11491149
}
11501150
}
11511151

1152-
r, err := response.OptimizeList(prs,
1152+
minimalPRs := make([]MinimalPullRequest, 0, len(prs))
1153+
for _, pr := range prs {
1154+
if pr != nil {
1155+
minimalPRs = append(minimalPRs, convertToMinimalPullRequest(pr))
1156+
}
1157+
}
1158+
1159+
r, err := response.OptimizeList(minimalPRs,
11531160
response.WithPreservedFields("html_url", "draft"),
1154-
response.WithCollectionExtractors(map[string][]string{
1155-
"labels": {"name"},
1156-
"requested_reviewers": {"login"},
1157-
"requested_teams": {"name"},
1158-
}),
11591161
)
11601162
if err != nil {
11611163
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil

pkg/github/repositories.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,14 @@ func ListTags(t translations.TranslationHelperFunc) inventory.ServerTool {
15011501
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list tags", resp, body), nil, nil
15021502
}
15031503

1504-
r, err := response.OptimizeList(tags)
1504+
minimalTags := make([]MinimalTag, 0, len(tags))
1505+
for _, tag := range tags {
1506+
if tag != nil {
1507+
minimalTags = append(minimalTags, convertToMinimalTag(tag))
1508+
}
1509+
}
1510+
1511+
r, err := response.OptimizeList(minimalTags)
15051512
if err != nil {
15061513
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
15071514
}
@@ -1674,7 +1681,14 @@ func ListReleases(t translations.TranslationHelperFunc) inventory.ServerTool {
16741681
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list releases", resp, body), nil, nil
16751682
}
16761683

1677-
r, err := response.OptimizeList(releases,
1684+
minimalReleases := make([]MinimalRelease, 0, len(releases))
1685+
for _, release := range releases {
1686+
if release != nil {
1687+
minimalReleases = append(minimalReleases, convertToMinimalRelease(release))
1688+
}
1689+
}
1690+
1691+
r, err := response.OptimizeList(minimalReleases,
16781692
response.WithPreservedFields("html_url", "prerelease"),
16791693
)
16801694
if err != nil {

pkg/github/repositories_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2806,7 +2806,7 @@ func Test_ListTags(t *testing.T) {
28062806
require.Equal(t, len(tc.expectedTags), len(returnedTags))
28072807
for i, expectedTag := range tc.expectedTags {
28082808
assert.Equal(t, *expectedTag.Name, returnedTags[i]["name"])
2809-
assert.Equal(t, *expectedTag.Commit.SHA, returnedTags[i]["commit.sha"])
2809+
assert.Equal(t, *expectedTag.Commit.SHA, returnedTags[i]["sha"])
28102810
}
28112811
})
28122812
}

pkg/response/optimize.go

Lines changed: 9 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ const (
1414

1515
// OptimizeListConfig controls the optimization pipeline behavior.
1616
type OptimizeListConfig struct {
17-
maxDepth int
18-
preservedFields map[string]bool
19-
collectionExtractors map[string][]string
17+
maxDepth int
18+
preservedFields map[string]bool
2019
}
2120

2221
type OptimizeListOption func(*OptimizeListConfig)
@@ -41,20 +40,8 @@ func WithPreservedFields(fields ...string) OptimizeListOption {
4140
}
4241
}
4342

44-
// WithCollectionExtractors controls how array fields are handled instead of being summarized as "[N items]".
45-
// - 1 sub-field: comma-joined into a flat string ("bug, enhancement").
46-
// - Multiple sub-fields: keep the array, but trim each element to only those fields.
47-
//
48-
// These are explicitly exempt from fill-rate filtering; if we asked for the extraction, it's likely important
49-
// to preserve the data even if only one item has it.
50-
func WithCollectionExtractors(extractors map[string][]string) OptimizeListOption {
51-
return func(c *OptimizeListConfig) {
52-
c.collectionExtractors = extractors
53-
}
54-
}
55-
5643
// OptimizeList optimizes a list of items by applying flattening, URL removal, zero-value removal,
57-
// whitespace normalization, collection summarization, and fill-rate filtering.
44+
// whitespace normalization, and fill-rate filtering.
5845
func OptimizeList[T any](items []T, opts ...OptimizeListOption) ([]byte, error) {
5946
cfg := OptimizeListConfig{maxDepth: defaultMaxDepth}
6047
for _, opt := range opts {
@@ -106,7 +93,7 @@ func flattenInto(item map[string]any, prefix string, result map[string]any, dept
10693
}
10794

10895
// filterByFillRate drops keys that appear on less than the threshold proportion of items.
109-
// Preserved fields and extractor keys always survive.
96+
// Preserved fields always survive.
11097
func filterByFillRate(items []map[string]any, threshold float64, cfg OptimizeListConfig) []map[string]any {
11198
keyCounts := make(map[string]int)
11299
for _, item := range items {
@@ -118,8 +105,7 @@ func filterByFillRate(items []map[string]any, threshold float64, cfg OptimizeLis
118105
minCount := int(threshold * float64(len(items)))
119106
keepKeys := make(map[string]bool, len(keyCounts))
120107
for key, count := range keyCounts {
121-
_, hasExtractor := cfg.collectionExtractors[key]
122-
if count > minCount || cfg.preservedFields[key] || hasExtractor {
108+
if count > minCount || cfg.preservedFields[key] {
123109
keepKeys[key] = true
124110
}
125111
}
@@ -138,7 +124,7 @@ func filterByFillRate(items []map[string]any, threshold float64, cfg OptimizeLis
138124
}
139125

140126
// optimizeItem applies per-item strategies in a single pass: remove URLs,
141-
// remove zero-values, normalize whitespace, summarize collections.
127+
// remove zero-values, normalize whitespace.
142128
// Preserved fields skip everything except whitespace normalization.
143129
func optimizeItem(item map[string]any, cfg OptimizeListConfig) map[string]any {
144130
result := make(map[string]any, len(item))
@@ -151,86 +137,16 @@ func optimizeItem(item map[string]any, cfg OptimizeListConfig) map[string]any {
151137
continue
152138
}
153139

154-
switch v := value.(type) {
155-
case string:
156-
result[key] = strings.Join(strings.Fields(v), " ")
157-
case []any:
158-
if len(v) == 0 {
159-
continue
160-
}
161-
162-
if preserved {
163-
result[key] = value
164-
} else if fields, ok := cfg.collectionExtractors[key]; ok {
165-
if len(fields) == 1 {
166-
result[key] = extractSubField(v, fields[0])
167-
} else {
168-
result[key] = trimArrayFields(v, fields)
169-
}
170-
} else {
171-
result[key] = fmt.Sprintf("[%d items]", len(v))
172-
}
173-
default:
140+
if s, ok := value.(string); ok {
141+
result[key] = strings.Join(strings.Fields(s), " ")
142+
} else {
174143
result[key] = value
175144
}
176145
}
177146

178147
return result
179148
}
180149

181-
// extractSubField pulls a named sub-field from each slice element and joins
182-
// them with ", ". Elements missing the field are silently skipped.
183-
func extractSubField(items []any, field string) string {
184-
var vals []string
185-
for _, item := range items {
186-
m, ok := item.(map[string]any)
187-
if !ok {
188-
continue
189-
}
190-
191-
v, ok := m[field]
192-
if !ok || v == nil {
193-
continue
194-
}
195-
196-
switch s := v.(type) {
197-
case string:
198-
if s != "" {
199-
vals = append(vals, s)
200-
}
201-
default:
202-
vals = append(vals, fmt.Sprintf("%v", v))
203-
}
204-
}
205-
206-
return strings.Join(vals, ", ")
207-
}
208-
209-
// trimArrayFields keeps only the specified fields from each object in a slice.
210-
// The trimmed objects are returned as is, no further strategies are applied.
211-
func trimArrayFields(items []any, fields []string) []any {
212-
result := make([]any, 0, len(items))
213-
for _, item := range items {
214-
m, ok := item.(map[string]any)
215-
if !ok {
216-
continue
217-
}
218-
219-
trimmed := make(map[string]any, len(fields))
220-
for _, f := range fields {
221-
if v, exists := m[f]; exists {
222-
trimmed[f] = v
223-
}
224-
}
225-
226-
if len(trimmed) > 0 {
227-
result = append(result, trimmed)
228-
}
229-
}
230-
231-
return result
232-
}
233-
234150
// isURLKey matches "url", "*_url", and their dot-prefixed variants.
235151
func isURLKey(key string) bool {
236152
if idx := strings.LastIndex(key, "."); idx >= 0 {

pkg/response/optimize_test.go

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -71,29 +71,6 @@ func TestFlatten(t *testing.T) {
7171
})
7272
}
7373

74-
func TestTrimArrayFields(t *testing.T) {
75-
cfg := OptimizeListConfig{
76-
collectionExtractors: map[string][]string{
77-
"reviewers": {"login", "state"},
78-
},
79-
}
80-
81-
result := optimizeItem(map[string]any{
82-
"reviewers": []any{
83-
map[string]any{"login": "alice", "state": "approved", "id": float64(1)},
84-
map[string]any{"login": "bob", "state": "changes_requested", "id": float64(2)},
85-
},
86-
"title": "Fix bug",
87-
}, cfg)
88-
89-
expected := []any{
90-
map[string]any{"login": "alice", "state": "approved"},
91-
map[string]any{"login": "bob", "state": "changes_requested"},
92-
}
93-
assert.Equal(t, expected, result["reviewers"])
94-
assert.Equal(t, "Fix bug", result["title"])
95-
}
96-
9774
func TestFilterByFillRate(t *testing.T) {
9875
cfg := OptimizeListConfig{}
9976

@@ -144,7 +121,7 @@ func TestOptimizeList_AllStrategies(t *testing.T) {
144121
"avatar_url": "https://avatars.githubusercontent.com/1",
145122
"draft": false,
146123
"merged_at": nil,
147-
"labels": []any{map[string]any{"name": "bug"}},
124+
"labels": []any{"bug", "fix"},
148125
"user": map[string]any{
149126
"login": "user",
150127
"avatar_url": "https://avatars.githubusercontent.com/1",
@@ -154,7 +131,6 @@ func TestOptimizeList_AllStrategies(t *testing.T) {
154131

155132
raw, err := OptimizeList(items,
156133
WithPreservedFields("html_url", "draft"),
157-
WithCollectionExtractors(map[string][]string{"labels": {"name"}}),
158134
)
159135
require.NoError(t, err)
160136

@@ -167,7 +143,7 @@ func TestOptimizeList_AllStrategies(t *testing.T) {
167143
assert.Equal(t, "line1 line2", result[0]["body"])
168144
assert.Equal(t, "https://github.com/repo/1", result[0]["html_url"])
169145
assert.Equal(t, false, result[0]["draft"])
170-
assert.Equal(t, "bug", result[0]["labels"])
146+
assert.Equal(t, []any{"bug", "fix"}, result[0]["labels"])
171147
assert.Equal(t, "user", result[0]["user.login"])
172148
assert.Nil(t, result[0]["url"])
173149
assert.Nil(t, result[0]["avatar_url"])
@@ -233,40 +209,4 @@ func TestPreservedFields(t *testing.T) {
233209
assert.Contains(t, result, "draft")
234210
assert.NotContains(t, result, "body")
235211
})
236-
237-
t.Run("protects from collection summarization", func(t *testing.T) {
238-
cfg := OptimizeListConfig{
239-
preservedFields: map[string]bool{"assignees": true},
240-
}
241-
242-
assignees := []any{
243-
map[string]any{"login": "alice", "id": float64(1)},
244-
map[string]any{"login": "bob", "id": float64(2)},
245-
}
246-
247-
result := optimizeItem(map[string]any{
248-
"assignees": assignees,
249-
"comments": []any{map[string]any{"id": "1"}, map[string]any{"id": "2"}},
250-
}, cfg)
251-
252-
assert.Equal(t, assignees, result["assignees"])
253-
assert.Equal(t, "[2 items]", result["comments"])
254-
})
255-
}
256-
257-
func TestCollectionFieldExtractors_SurviveFillRate(t *testing.T) {
258-
cfg := OptimizeListConfig{
259-
collectionExtractors: map[string][]string{"labels": {"name"}},
260-
}
261-
262-
items := []map[string]any{
263-
{"title": "PR 1", "labels": "bug"},
264-
{"title": "PR 2"},
265-
{"title": "PR 3"},
266-
{"title": "PR 4"},
267-
}
268-
269-
result := filterByFillRate(items, defaultFillRateThreshold, cfg)
270-
271-
assert.Contains(t, result[0], "labels")
272212
}

0 commit comments

Comments
 (0)