topfans/docs/superpowers/plans/2026-07-24-starbook-home-has-more.md
zerosaturation 383b5e042e docs(starbook): add has_more design spec and implementation plan
记录星册首页 More 按钮恢复的方案说明与 TDD 实施计划,作为本次特性 commit e079a6c 的设计依据。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:20:32 +08:00

28 KiB

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:

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:

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:

const homePreviewLimit = 3

Add after registryLogicalCategory:

// 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:

// 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:

if regs, ok := typeGroups[models.AssetTypeRegular]; ok {
    group := s.buildRegularGroupForAssets(allAssets, registryMap, regs, homePreviewLimit)
    if group != nil {
        groups = append(groups, group)
    }
}

In GetAssetsByType:

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:

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:

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:

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:

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:

// 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:

// 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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

[ ] 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:

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.