diff --git a/internal/store/clusters.go b/internal/store/clusters.go index 9e0b838..f217904 100644 --- a/internal/store/clusters.go +++ b/internal/store/clusters.go @@ -181,7 +181,7 @@ func (s *Store) ListRunClusterSummaries(ctx context.Context, options ClusterSumm return nil, fmt.Errorf("scan run cluster summary: %w", err) } summary.Source = ClusterSourceRun - summary.StableSlug = fmt.Sprintf("cluster-%d", summary.ID) + summary.StableSlug = clusterHumanName(options.RepoID, repThreadID.Int64, summary.ID) summary.Status = "active" if closeReason.Valid || closedMemberCount >= summary.MemberCount { summary.Status = "closed" @@ -1097,9 +1097,12 @@ func (s *Store) clusterSummaryByID(ctx context.Context, repoID, clusterID int64, select cg.id, cg.stable_slug, cg.status, cg.title, cg.representative_thread_id, rt.number, rt.kind, rt.title, count(cm.thread_id) as member_count, - cg.updated_at, cg.closed_at + cg.updated_at, coalesce(cc.updated_at, cg.closed_at) as closed_at, + sum(case when t.closed_at_local is not null or t.state <> 'open' then 1 else 0 end) as closed_member_count from cluster_groups cg + left join cluster_closures cc on cc.cluster_id = cg.id left join cluster_memberships cm on cm.cluster_id = cg.id and cm.state = 'active' + left join threads t on t.id = cm.thread_id left join threads rt on rt.id = cg.representative_thread_id where `+where+` group by cg.id @@ -1108,13 +1111,17 @@ func (s *Store) clusterSummaryByID(ctx context.Context, repoID, clusterID int64, var title, closedAt, repKind, repTitle sql.NullString var repThreadID sql.NullInt64 var repNumber sql.NullInt64 - if err := row.Scan(&summary.ID, &summary.StableSlug, &summary.Status, &title, &repThreadID, &repNumber, &repKind, &repTitle, &summary.MemberCount, &summary.UpdatedAt, &closedAt); err != nil { + var closedMemberCount int + if err := row.Scan(&summary.ID, &summary.StableSlug, &summary.Status, &title, &repThreadID, &repNumber, &repKind, &repTitle, &summary.MemberCount, &summary.UpdatedAt, &closedAt, &closedMemberCount); err != nil { if err == sql.ErrNoRows { return ClusterSummary{}, fmt.Errorf("cluster %d was not found", clusterID) } return ClusterSummary{}, fmt.Errorf("scan cluster summary: %w", err) } summary.Source = ClusterSourceDurable + if summary.Status == "active" && summary.MemberCount > 0 && closedMemberCount >= summary.MemberCount { + summary.Status = "closed" + } summary.Title = title.String summary.ClosedAt = closedAt.String summary.RepresentativeThreadID = repThreadID.Int64 @@ -1187,7 +1194,7 @@ func (s *Store) runClusterSummaryByID(ctx context.Context, repoID, clusterID int return ClusterSummary{}, 0, fmt.Errorf("scan run cluster summary: %w", err) } summary.Source = ClusterSourceRun - summary.StableSlug = fmt.Sprintf("cluster-%d", summary.ID) + summary.StableSlug = clusterHumanName(repoID, repThreadID.Int64, summary.ID) summary.Status = "active" if closeReason.Valid || closedMemberCount >= summary.MemberCount { summary.Status = "closed" diff --git a/internal/store/human_key.go b/internal/store/human_key.go new file mode 100644 index 0000000..16a0a17 --- /dev/null +++ b/internal/store/human_key.go @@ -0,0 +1,53 @@ +package store + +import ( + "crypto/sha256" + "fmt" +) + +var humanKeyWords = []string{ + "able", "acid", "acre", "actor", "acute", "admin", "aisle", "album", + "alert", "alias", "amber", "angle", "apple", "apron", "array", "asset", + "atlas", "audio", "badge", "basic", "batch", "beach", "beacon", "bench", + "binary", "block", "bonus", "border", "branch", "bridge", "brief", "buffer", + "build", "bundle", "cable", "cache", "canal", "canvas", "carbon", "cargo", + "cedar", "center", "chance", "change", "charge", "chart", "cipher", "circle", + "civic", "clear", "client", "cloud", "cobalt", "column", "comet", "common", + "copper", "corner", "course", "credit", "crisp", "cycle", "daily", "data", + "delta", "detail", "device", "domain", "draft", "drift", "driver", "early", + "earth", "echo", "edge", "ember", "engine", "entry", "error", "event", + "fabric", "factor", "field", "filter", "final", "focus", "forge", "format", + "frame", "fresh", "future", "garden", "gentle", "glide", "golden", "graph", + "grid", "group", "harbor", "header", "helix", "hidden", "hollow", "honest", + "icon", "index", "input", "island", "kernel", "key", "keystone", "label", + "lantern", "laser", "latest", "lattice", "layer", "ledger", "level", "light", + "limit", "linear", "local", "logic", "major", "maple", "margin", "matrix", + "meadow", "medium", "memory", "merge", "method", "mirror", "mobile", "module", + "motion", "native", "needle", "noble", "normal", "notion", "nova", "number", + "object", "ocean", "offset", "olive", "online", "option", "orbit", "origin", + "output", "packet", "panel", "parcel", "patch", "pattern", "phase", "pillar", + "pixel", "plain", "planet", "plume", "point", "portal", "prime", "profile", + "prompt", "proper", "public", "pulse", "query", "quartz", "quiet", "radar", + "range", "rapid", "record", "region", "relay", "render", "reply", "report", + "result", "ripple", "river", "route", "sample", "schema", "screen", "script", + "search", "second", "section", "secure", "select", "shadow", "signal", "silver", + "simple", "single", "sketch", "socket", "solar", "source", "space", "span", + "spiral", "spring", "stable", "static", "status", "steady", "stone", "stream", + "strict", "studio", "subtle", "summit", "switch", "system", "table", "target", + "thread", "timber", "token", "trace", "transit", "union", "update", "usage", + "valid", "vector", "velvet", "vertex", "vessel", "view", "violet", "virtual", + "vista", "visual", "volume", "wave", "window", "yellow", "zenith", "zero", +} + +func clusterHumanName(repoID, representativeThreadID, clusterID int64) string { + key := fmt.Sprintf("repo:%d:cluster:%d", repoID, clusterID) + if representativeThreadID != 0 { + key = fmt.Sprintf("repo:%d:cluster-representative:%d", repoID, representativeThreadID) + } + hash := sha256.Sum256([]byte(key)) + return fmt.Sprintf("%s-%s-%s", + humanKeyWords[int(hash[0])%len(humanKeyWords)], + humanKeyWords[int(hash[1])%len(humanKeyWords)], + humanKeyWords[int(hash[2])%len(humanKeyWords)], + ) +}