# Starbook Home Has-More Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Restore the starbook home preview so every regular grade, collection group, and activity group returns the three highest-liked assets and sets `has_more=true` when additional assets exist, while the More endpoint returns every matching asset in the same order. **Architecture:** Keep the existing shared group builders and add a `previewLimit int` parameter. A single helper sorts valid `AssetItem` values by authoritative `models.Asset.LikeCount DESC, models.Asset.ID ASC`, truncates only when `previewLimit > 0`, and reports whether truncation occurred. `GetMyAssets` passes `3`; `GetAssetsByType` passes `0`. **Tech Stack:** Go 1.25, Dubbo Triple protobuf DTOs, testify, GORM/PostgreSQL integration tests. ## Global Constraints - Use `models.Asset.LikeCount` as the only ordering source; never sort from `AssetRegistry.LikeCount`. - Stable order is `Asset.LikeCount DESC, Asset.ID ASC` for regular, collection, and activity assets. - `GetMyAssets` uses the fixed `homePreviewLimit = 3`. - `GetAssetsByType` uses `previewLimit = 0`, returns all matches, and leaves group/grade `has_more=false`. - Count only registry rows that resolve to a real `models.Asset`; dangling registries do not affect `total_count` or `has_more`. - Do not modify `frontend/pages/components/StarbookContent.vue`; its existing `v-if` expressions consume the corrected fields. - Do not modify `backend/proto/asset.proto` or generated protobuf files for this feature. - Do not add a new dependency, configuration flag, cache, migration, or repository pagination abstraction. - Do not run `git commit` or `git push`; repository rules prohibit AI commits unless explicitly requested. --- ## File Structure - Create `backend/services/assetService/service/asset_service_group_test.go`: DB-free unit tests for sorting, preview truncation, valid-item counting, authoritative like source, and all three group builders. - Modify `backend/services/assetService/service/asset_service.go`: add the preview constant/helper, parameterize the three builders, and pass the correct limit from the home and More flows. - Modify `backend/services/assetService/provider/asset_provider_test.go`: add a DB-backed end-to-end regression proving `GetMyAssets` returns top three while `GetAssetsByType` returns all four in the same order. - No frontend or protobuf source/generated files change. ### Task 1: Add the shared ordering helper and regular-grade preview **Files:** - Create: `backend/services/assetService/service/asset_service_group_test.go` - Modify: `backend/services/assetService/service/asset_service.go:22-46,159-183,309-324,371-438` **Interfaces:** - Produces: `const homePreviewLimit = 3` - Produces: `sortAndLimitAssetItems(items []*pb.AssetItem, previewLimit int) ([]*pb.AssetItem, bool)` - Changes: `buildRegularGroupForAssets(..., previewLimit int) *pb.AssetGroup` - `previewLimit <= 0` means sorted but unlimited, with `hasMore=false`. - [ ] **Step 1: Write the failing regular preview tests** Create `backend/services/assetService/service/asset_service_group_test.go`: ```go package service import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/asset" ) func assetIDs(items []*pb.AssetItem) []int64 { ids := make([]int64, 0, len(items)) for _, item := range items { ids = append(ids, item.AssetId) } return ids } func rankedAssets() []*models.Asset { return []*models.Asset{ {ID: 1, Name: "ten", LikeCount: 10}, {ID: 2, Name: "thirty-first", LikeCount: 30}, {ID: 3, Name: "twenty", LikeCount: 20}, {ID: 4, Name: "thirty-second", LikeCount: 30}, } } func TestBuildRegularGroupForAssets_HomePreviewUsesAssetLikes(t *testing.T) { grade := int32(2) registries := []*models.AssetRegistry{ {AssetID: 1, AssetType: models.AssetTypeRegular, Grade: &grade, LikeCount: 1000}, {AssetID: 2, AssetType: models.AssetTypeRegular, Grade: &grade, LikeCount: 1}, {AssetID: 3, AssetType: models.AssetTypeRegular, Grade: &grade, LikeCount: 900}, {AssetID: 4, AssetType: models.AssetTypeRegular, Grade: &grade, LikeCount: 2}, {AssetID: 999, AssetType: models.AssetTypeRegular, Grade: &grade, LikeCount: 9999}, } group := (&assetService{}).buildRegularGroupForAssets(rankedAssets(), nil, registries, 3) require.NotNil(t, group) require.Len(t, group.Grades, 1) section := group.Grades[0] assert.Equal(t, []int64{2, 4, 3}, assetIDs(section.Items)) assert.Equal(t, int32(4), section.TotalCount, "dangling registry must not count") assert.True(t, section.HasMore) assert.Equal(t, int32(4), group.TotalCount) assert.True(t, group.HasMore) } func TestBuildRegularGroupForAssets_UnlimitedReturnsAllSorted(t *testing.T) { grade := int32(2) registries := []*models.AssetRegistry{ {AssetID: 1, AssetType: models.AssetTypeRegular, Grade: &grade}, {AssetID: 2, AssetType: models.AssetTypeRegular, Grade: &grade}, {AssetID: 3, AssetType: models.AssetTypeRegular, Grade: &grade}, {AssetID: 4, AssetType: models.AssetTypeRegular, Grade: &grade}, } group := (&assetService{}).buildRegularGroupForAssets(rankedAssets(), nil, registries, 0) require.Len(t, group.Grades, 1) assert.Equal(t, []int64{2, 4, 3, 1}, assetIDs(group.Grades[0].Items)) assert.False(t, group.Grades[0].HasMore) assert.False(t, group.HasMore) } ``` - [ ] **Step 2: Run the tests and verify RED** Run: ```bash go -C backend/services/assetService test -count=1 ./service -run 'TestBuildRegularGroupForAssets' -v ``` Expected: compilation fails because `buildRegularGroupForAssets` currently accepts three arguments, not four. This is the intended RED failure. - [ ] **Step 3: Add the shared preview constant and helper** In `backend/services/assetService/service/asset_service.go`, add near the service definitions: ```go const homePreviewLimit = 3 ``` Add after `registryLogicalCategory`: ```go // sortAndLimitAssetItems applies the starbook ordering contract and optionally // truncates a home-page preview. previewLimit <= 0 means unlimited. func sortAndLimitAssetItems(items []*pb.AssetItem, previewLimit int) ([]*pb.AssetItem, bool) { sort.Slice(items, func(i, j int) bool { if items[i].LikeCount != items[j].LikeCount { return items[i].LikeCount > items[j].LikeCount } return items[i].AssetId < items[j].AssetId }) if previewLimit > 0 && len(items) > previewLimit { return items[:previewLimit], true } return items, false } ``` - [ ] **Step 4: Parameterize and implement the regular builder** Replace `buildRegularGroupForAssets` with: ```go // buildRegularGroupForAssets 构建原创藏品分组 func (s *assetService) buildRegularGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry, previewLimit int) *pb.AssetGroup { assetMap := make(map[int64]*models.Asset) for _, asset := range allAssets { assetMap[asset.ID] = asset } gradeGroups := make(map[int32][]*models.AssetRegistry) for _, reg := range registries { if reg.Grade != nil { gradeGroups[*reg.Grade] = append(gradeGroups[*reg.Grade], reg) } } grades := make([]*pb.GradeSection, 0) for grade, regs := range gradeGroups { items := make([]*pb.AssetItem, 0, len(regs)) for _, reg := range regs { asset := assetMap[reg.AssetID] if asset == nil { continue } items = append(items, &pb.AssetItem{ AssetId: asset.ID, Name: asset.Name, CoverUrlSigned: asset.CoverURL, LikeCount: asset.LikeCount, CreatedAt: asset.CreatedAt, Category: "castlove", Grade: grade, DisplayStatus: reg.DisplayStatus, }) } totalCount := int32(len(items)) items, hasMore := sortAndLimitAssetItems(items, previewLimit) grades = append(grades, &pb.GradeSection{ Grade: grade, Items: items, TotalCount: totalCount, HasMore: hasMore, }) } sort.Slice(grades, func(i, j int) bool { return grades[i].Grade > grades[j].Grade }) totalCount := int32(0) hasMore := false for _, grade := range grades { totalCount += grade.TotalCount hasMore = hasMore || grade.HasMore } return &pb.AssetGroup{ Type: models.AssetTypeRegular, Category: "castlove", CategoryName: "原创", Grades: grades, TotalCount: totalCount, HasMore: hasMore, } } ``` Keep the existing `registryMap` parameter in this task to avoid an unrelated signature/refactor change; it remains part of all three existing builder contracts. - [ ] **Step 5: Pass the correct limit at regular call sites** In `GetMyAssets`: ```go if regs, ok := typeGroups[models.AssetTypeRegular]; ok { group := s.buildRegularGroupForAssets(allAssets, registryMap, regs, homePreviewLimit) if group != nil { groups = append(groups, group) } } ``` In `GetAssetsByType`: ```go if regs, ok := typeGroups[models.AssetTypeRegular]; ok { if group := s.buildRegularGroupForAssets(allAssets, registryMap, regs, 0); group != nil { groups = append(groups, group) } } ``` - [ ] **Step 6: Run the regular tests and verify GREEN** Run: ```bash gofmt -w backend/services/assetService/service/asset_service.go backend/services/assetService/service/asset_service_group_test.go go -C backend/services/assetService test -count=1 ./service -run 'TestBuildRegularGroupForAssets' -v ``` Expected: both regular tests pass; no DB is required. - [ ] **Step 7: Review checkpoint** Run: ```bash git diff --check -- backend/services/assetService/service/asset_service.go backend/services/assetService/service/asset_service_group_test.go git diff -- backend/services/assetService/service/asset_service.go backend/services/assetService/service/asset_service_group_test.go ``` Confirm the diff contains only the shared helper, regular preview behavior, call-site limits, and tests. Do not commit. ### Task 2: Apply the same contract to collection and activity groups **Files:** - Modify: `backend/services/assetService/service/asset_service_group_test.go` - Modify: `backend/services/assetService/service/asset_service.go:169-183,315-324,440-522` **Interfaces:** - Consumes: `sortAndLimitAssetItems(items []*pb.AssetItem, previewLimit int) ([]*pb.AssetItem, bool)` from Task 1. - Changes: `buildCollectionGroupForAssets(..., previewLimit int) *pb.AssetGroup` - Changes: `buildActivityGroupForAssets(..., previewLimit int) *pb.AssetGroup` - [ ] **Step 1: Append failing collection and activity tests** Append to `asset_service_group_test.go`: ```go func strPtr(value string) *string { return &value } func TestBuildCollectionGroupForAssets_PreviewAndUnlimited(t *testing.T) { registries := []*models.AssetRegistry{ {AssetID: 1, AssetType: models.AssetTypeCollection, CollectionCategory: strPtr("cards")}, {AssetID: 2, AssetType: models.AssetTypeCollection, CollectionCategory: strPtr("cards")}, {AssetID: 3, AssetType: models.AssetTypeCollection, CollectionCategory: strPtr("cards")}, {AssetID: 4, AssetType: models.AssetTypeCollection, CollectionCategory: strPtr("cards")}, {AssetID: 999, AssetType: models.AssetTypeCollection, CollectionCategory: strPtr("cards")}, } svc := &assetService{} preview := svc.buildCollectionGroupForAssets(rankedAssets(), nil, registries, 3) assert.Equal(t, []int64{2, 4, 3}, assetIDs(preview.Items)) assert.Equal(t, int32(4), preview.TotalCount) assert.True(t, preview.HasMore) full := svc.buildCollectionGroupForAssets(rankedAssets(), nil, registries, 0) assert.Equal(t, []int64{2, 4, 3, 1}, assetIDs(full.Items)) assert.Equal(t, int32(4), full.TotalCount) assert.False(t, full.HasMore) } func TestBuildActivityGroupForAssets_PreviewAndUnlimited(t *testing.T) { registries := []*models.AssetRegistry{ {AssetID: 1, AssetType: models.AssetTypeActivity, ActivityType: strPtr("offline")}, {AssetID: 2, AssetType: models.AssetTypeActivity, ActivityType: strPtr("offline")}, {AssetID: 3, AssetType: models.AssetTypeActivity, ActivityType: strPtr("offline")}, {AssetID: 4, AssetType: models.AssetTypeActivity, ActivityType: strPtr("offline")}, {AssetID: 999, AssetType: models.AssetTypeActivity, ActivityType: strPtr("offline")}, } svc := &assetService{} preview := svc.buildActivityGroupForAssets(rankedAssets(), nil, registries, 3) assert.Equal(t, []int64{2, 4, 3}, assetIDs(preview.Items)) assert.Equal(t, int32(4), preview.TotalCount) assert.True(t, preview.HasMore) full := svc.buildActivityGroupForAssets(rankedAssets(), nil, registries, 0) assert.Equal(t, []int64{2, 4, 3, 1}, assetIDs(full.Items)) assert.Equal(t, int32(4), full.TotalCount) assert.False(t, full.HasMore) } ``` - [ ] **Step 2: Run the tests and verify RED** Run: ```bash go -C backend/services/assetService test -count=1 ./service -run 'TestBuild(Collection|Activity)GroupForAssets' -v ``` Expected: compilation fails because the collection/activity builders still accept three arguments. - [ ] **Step 3: Parameterize the collection builder** Replace the collection builder with: ```go // buildCollectionGroupForAssets 构建典收藏品分组 func (s *assetService) buildCollectionGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry, previewLimit int) *pb.AssetGroup { assetMap := make(map[int64]*models.Asset) for _, asset := range allAssets { assetMap[asset.ID] = asset } items := make([]*pb.AssetItem, 0, len(registries)) for _, reg := range registries { asset := assetMap[reg.AssetID] if asset == nil { continue } category := "" if reg.CollectionCategory != nil { category = *reg.CollectionCategory } items = append(items, &pb.AssetItem{ AssetId: asset.ID, Name: asset.Name, CoverUrlSigned: asset.CoverURL, LikeCount: asset.LikeCount, CreatedAt: asset.CreatedAt, Category: category, Grade: 0, DisplayStatus: reg.DisplayStatus, }) } totalCount := int32(len(items)) items, hasMore := sortAndLimitAssetItems(items, previewLimit) return &pb.AssetGroup{ Type: models.AssetTypeCollection, Category: "", CategoryName: "典藏", Items: items, TotalCount: totalCount, HasMore: hasMore, } } ``` - [ ] **Step 4: Parameterize the activity builder** Replace the activity builder with: ```go // buildActivityGroupForAssets 构建活动藏品分组 func (s *assetService) buildActivityGroupForAssets(allAssets []*models.Asset, registryMap map[int64]*models.AssetRegistry, registries []*models.AssetRegistry, previewLimit int) *pb.AssetGroup { assetMap := make(map[int64]*models.Asset) for _, asset := range allAssets { assetMap[asset.ID] = asset } items := make([]*pb.AssetItem, 0, len(registries)) for _, reg := range registries { asset := assetMap[reg.AssetID] if asset == nil { continue } activityType := "" if reg.ActivityType != nil { activityType = *reg.ActivityType } items = append(items, &pb.AssetItem{ AssetId: asset.ID, Name: asset.Name, CoverUrlSigned: asset.CoverURL, LikeCount: asset.LikeCount, CreatedAt: asset.CreatedAt, Category: activityType, Grade: 0, DisplayStatus: reg.DisplayStatus, }) } totalCount := int32(len(items)) items, hasMore := sortAndLimitAssetItems(items, previewLimit) return &pb.AssetGroup{ Type: models.AssetTypeActivity, Category: "", CategoryName: "活动", Items: items, TotalCount: totalCount, HasMore: hasMore, } } ``` - [ ] **Step 5: Pass limits at all collection/activity call sites** In `GetMyAssets`, pass `homePreviewLimit`: ```go if regs, ok := typeGroups[models.AssetTypeCollection]; ok { group := s.buildCollectionGroupForAssets(allAssets, registryMap, regs, homePreviewLimit) if group != nil { groups = append(groups, group) } } if regs, ok := typeGroups[models.AssetTypeActivity]; ok { group := s.buildActivityGroupForAssets(allAssets, registryMap, regs, homePreviewLimit) if group != nil { groups = append(groups, group) } } ``` In `GetAssetsByType`, pass `0`: ```go if regs, ok := typeGroups[models.AssetTypeCollection]; ok { if group := s.buildCollectionGroupForAssets(allAssets, registryMap, regs, 0); group != nil { groups = append(groups, group) } } if regs, ok := typeGroups[models.AssetTypeActivity]; ok { if group := s.buildActivityGroupForAssets(allAssets, registryMap, regs, 0); group != nil { groups = append(groups, group) } } ``` - [ ] **Step 6: Run all DB-free group tests and verify GREEN** Run: ```bash gofmt -w backend/services/assetService/service/asset_service.go backend/services/assetService/service/asset_service_group_test.go go -C backend/services/assetService test -count=1 ./service -run 'TestBuild(Regular|Collection|Activity)GroupForAssets' -v ``` Expected: all four group-builder tests pass without a database. - [ ] **Step 7: Review checkpoint** Run: ```bash git diff --check -- backend/services/assetService/service/asset_service.go backend/services/assetService/service/asset_service_group_test.go git diff --stat ``` Confirm no frontend, proto, generated proto, migration, or dependency file was changed by this task. Do not commit. ### Task 3: Verify the home and More call paths end-to-end **Files:** - Modify: `backend/services/assetService/provider/asset_provider_test.go:89-145,206-220` **Interfaces:** - Consumes: `GetMyAssets` with `homePreviewLimit=3` wired internally. - Consumes: `GetAssetsByType` with `previewLimit=0` wired internally. - Verifies: both paths use `Asset.LikeCount DESC, Asset.ID ASC`. - [ ] **Step 1: Add a ranked collection fixture helper** Append after `sbSeed` in `asset_provider_test.go`: ```go func sbAddRankedCollectionAssets(t *testing.T, db *gorm.DB) { t.Helper() var existing models.Asset require.NoError(t, db.Where( "owner_uid = ? AND star_id = ? AND name = ?", sbTestOwnerUID, sbTestStarID, "collection_x", ).First(&existing).Error) require.NoError(t, db.Model(&existing).Update("like_count", int32(5)).Error) require.NoError(t, db.Model(&models.AssetRegistry{}). Where("asset_id = ?", existing.ID). Update("like_count", int32(999)).Error) category := "手办" now := time.Now().UnixMilli() for index, likeCount := range []int32{20, 40, 30} { asset := &models.Asset{ OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Name: fmt.Sprintf("collection_ranked_%d", index), CoverURL: fmt.Sprintf("https://cdn/collection_ranked_%d.png", index), Status: 1, IsActive: true, LikeCount: likeCount, CreatedAt: now + int64(index+1), UpdatedAt: now + int64(index+1), } require.NoError(t, db.Create(asset).Error) require.NoError(t, db.Create(&models.AssetRegistry{ AssetID: asset.ID, AssetType: models.AssetTypeCollection, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, CollectionCategory: &category, LikeCount: 1000 - likeCount, DisplayStatus: 1, }).Error) } } func requireGroupByType(t *testing.T, groups []*pb.AssetGroup, assetType string) *pb.AssetGroup { t.Helper() for _, group := range groups { if group.Type == assetType { return group } } t.Fatalf("group type %q not found", assetType) return nil } func itemLikeCounts(items []*pb.AssetItem) []int32 { counts := make([]int32, 0, len(items)) for _, item := range items { counts = append(counts, item.LikeCount) } return counts } ``` Add `fmt` to the existing import block. - [ ] **Step 2: Add the failing home-versus-More regression test** Append after `TestGetAssetsByType_FilterByCategory`: ```go func TestStarbookHomePreviewAndMoreUseAssetLikeRanking(t *testing.T) { db := starbookTestDB(t) sbSeed(t, db) sbAddRankedCollectionAssets(t, db) provider := sbProvider(db) ctx := sbCtx(sbTestOwnerUID, sbTestStarID) homeResp, err := provider.GetMyAssets(ctx, &pb.GetMyAssetsRequest{}) require.NoError(t, err) require.NotNil(t, homeResp.Data) homeGroup := requireGroupByType(t, homeResp.Data.Groups, models.AssetTypeCollection) assert.Equal(t, []int32{40, 30, 20}, itemLikeCounts(homeGroup.Items)) assert.Equal(t, int32(4), homeGroup.TotalCount) assert.True(t, homeGroup.HasMore) moreResp, err := provider.GetAssetsByType(ctx, &pb.GetAssetsByTypeRequest{ Type: models.AssetTypeCollection, Category: "手办", }) require.NoError(t, err) require.NotNil(t, moreResp.Data) moreGroup := requireGroupByType(t, moreResp.Data.Groups, models.AssetTypeCollection) assert.Equal(t, []int32{40, 30, 20, 5}, itemLikeCounts(moreGroup.Items)) assert.Equal(t, int32(4), moreGroup.TotalCount) assert.False(t, moreGroup.HasMore) } ``` - [ ] **Step 3: Prove the integration test would catch reversed call-site limits** Before accepting GREEN, temporarily change only the `GetMyAssets` collection call from `homePreviewLimit` to `0`, then run: ```bash go -C backend/services/assetService test -count=1 ./provider -run '^TestStarbookHomePreviewAndMoreUseAssetLikeRanking$' -v ``` Expected: FAIL because home returns four items instead of `[40, 30, 20]` and `HasMore` is false. Restore `homePreviewLimit` immediately after observing the expected failure. - [ ] **Step 4: Run the restored integration test and verify GREEN** Run: ```bash gofmt -w backend/services/assetService/provider/asset_provider_test.go go -C backend/services/assetService test -count=1 ./provider -run 'Test(GetAssetsByType|StarbookHomePreviewAndMore)' -v ``` Expected: existing filter/auth tests and the new home/More ranking test pass. If PostgreSQL is unavailable, the DB-backed tests explicitly report SKIP; the DB-free Task 1/2 tests must still pass and are not optional. - [ ] **Step 5: Review checkpoint** Run: ```bash git diff --check -- backend/services/assetService/provider/asset_provider_test.go git diff -- backend/services/assetService/provider/asset_provider_test.go ``` Confirm the fixture manually specifies no IDs, so the PostgreSQL sequence rule is not newly triggered. Existing `sbSeed` manual user/star IDs retain their existing `setval` calls. Do not commit. ### Task 4: Full regression and cross-impact verification **Files:** - Verify only; no planned source changes. **Interfaces:** - Verifies all outputs from Tasks 1-3. - [ ] **Step 1: Run the complete DB-free group regression** Run: ```bash go -C backend/services/assetService test -count=1 ./service -run 'TestBuild(Regular|Collection|Activity)GroupForAssets' -v ``` Expected: all group-builder tests pass. - [ ] **Step 2: Run starbook provider and gateway tests** Run: ```bash go -C backend/services/assetService test -count=1 ./provider -run 'Test(GetAssetsByType|StarbookHomePreviewAndMore)' -v go -C backend/gateway test -count=1 ./controller -run '^TestStarbook' -v ``` Expected: all selected tests pass, or only DB-backed provider tests explicitly SKIP because PostgreSQL is unavailable. No selected test may fail. - [ ] **Step 3: Compile gateway and assetService** Run: ```bash go -C backend/gateway build ./... go -C backend/services/assetService build ./... ``` Expected: both commands exit 0. - [ ] **Step 4: Precompile every service from `dev.sh` without overwriting repository binaries** Run: ```bash tmp=$(mktemp -d /tmp/topfans-starbook-build.XXXXXX) trap 'rm -rf "$tmp"' EXIT for spec in \ 'gateway:gateway' \ 'userService:services/userService' \ 'assetService:services/assetService' \ 'socialService:services/socialService' \ 'galleryService:services/galleryService' \ 'activityService:services/activityService' \ 'taskService:services/taskService' \ 'aiChatService:services/aiChatService' \ 'statisticService:services/statisticService' \ 'notificationService:services/notificationService' \ 'moderationService:services/moderationService'; do name=${spec%%:*} dir=${spec#*:} go -C "backend/$dir" build -o "$tmp/$name" . || exit 1 done ``` Expected: all 11 builds exit 0. - [ ] **Step 5: Re-run graph cross-impact checks** Using code-review-graph: 1. Incrementally update the graph for `backend/services/assetService/service/asset_service.go` and the two test files. 2. Query `callers_of` for all three `build*GroupForAssets` functions. 3. Verify `GetMyAssets` passes `homePreviewLimit` and `GetAssetsByType` passes `0` for regular, collection, and activity. 4. Query `tests_for` for `GetMyAssets` and `GetAssetsByType`; record any graph limitation if the new tests are not linked automatically. Expected: no stale three-argument builder caller remains. - [ ] **Step 6: Perform the mandatory global self-review** Check every source requirement against the diff: ```text [ ] regular uses Asset.LikeCount DESC, Asset.ID ASC [ ] collection uses Asset.LikeCount DESC, Asset.ID ASC [ ] activity uses Asset.LikeCount DESC, Asset.ID ASC [ ] home returns at most 3 per regular grade / collection group / activity group [ ] home has_more is true only when valid items were omitted [ ] More returns all valid items in the same order [ ] dangling registries do not affect total_count or has_more [ ] frontend and protobuf files are unchanged by this feature [ ] existing unrelated working-tree changes remain intact ``` Run: ```bash git diff --check git status --short git diff --stat ``` Expected: no whitespace errors; only intended feature files plus the pre-existing protobuf/gateway changes and design/plan documents are present. Do not commit or push. --- ## Plan Self-Review ### Spec coverage - Home limit 3: Tasks 1-2 implement; Task 3 verifies through `GetMyAssets`. - Highest likes first for all types: shared helper in Task 1, all builders in Tasks 1-2. - Authoritative source is `Asset.LikeCount`: fixture conflicts with registry likes in Tasks 1 and 3. - Stable tie ordering `Asset.ID ASC`: regular test IDs 2 and 4 both have 30 likes; expected order proves the tie rule. - `has_more`: builder tests cover preview and unlimited modes; integration verifies serialization DTO values before gateway JSON. - More returns all: Tasks 1-2 unit tests plus Task 3 end-to-end test. - Invalid registry exclusion: every builder test includes asset ID 999 without a matching asset. - No frontend/proto changes: enforced in global constraints and Task 4 review. - Regression: targeted tests, two builds, all-service precompile, graph checks, and global self-review are explicit. ### Placeholder scan The plan contains no `TBD`, `TODO`, “implement later,” unspecified test request, or undefined interface. Every code-changing step includes concrete code and an exact verification command. ### Type consistency - `LikeCount` is `int32` in `models.Asset`, `models.AssetRegistry`, and `pb.AssetItem`. - `Asset.ID` and `pb.AssetItem.AssetId` are `int64`. - `previewLimit` is consistently `int`. - The helper returns `([]*pb.AssetItem, bool)` and every builder consumes both return values. - `TotalCount` remains protobuf-compatible `int32`.