fix: hide closed run cluster members
Some checks are pending
CI / Go / ${{ matrix.os }} (macos-latest) (push) Waiting to run
CI / Go / ${{ matrix.os }} (ubuntu-latest) (push) Waiting to run

This commit is contained in:
Peter Steinberger 2026-04-28 11:23:59 +01:00
parent fa45854597
commit 67861b5d32
No known key found for this signature in database
3 changed files with 53 additions and 13 deletions

View File

@ -11,3 +11,4 @@
- Split generated clusters with bounded nearest-neighbor graph safeguards, GitHub reference evidence, and cross-kind score pruning so weak similarity bridges stop merging unrelated reports into one mega-cluster.
- Tighten clustering precision by ignoring ambiguous one-digit prose references and requiring weak embedding edges to share concrete title tokens unless they have high similarity or direct GitHub reference evidence.
- Treat later body-only issue references as weak evidence unless they share title overlap, while still preserving title and lead-body references for canonical issue/PR fix clusters.
- Hide GitHub-closed members from latest-run cluster summaries and details by default; `--include-closed` still shows the full historical cluster.

View File

@ -143,16 +143,20 @@ func (s *Store) ListRunClusterSummaries(ctx context.Context, options ClusterSumm
}
where := `c.repo_id = ? and c.cluster_run_id = ?`
args := []any{options.RepoID, runID, minSize}
having := `c.member_count >= ?`
memberCountExpr := `c.member_count`
updatedAtExpr := `coalesce(max(coalesce(t.updated_at_gh, t.updated_at)), c.created_at)`
having := memberCountExpr + ` >= ?`
if !options.IncludeClosed {
having += ` and c.close_reason_local is null and closed_member_count < c.member_count`
memberCountExpr = `sum(case when t.state = 'open' and t.closed_at_local is null then 1 else 0 end)`
updatedAtExpr = `coalesce(max(case when t.state = 'open' and t.closed_at_local is null then coalesce(t.updated_at_gh, t.updated_at) end), c.created_at)`
having = memberCountExpr + ` >= ? and c.close_reason_local is null`
}
args = append(args, limit)
rows, err := s.db.QueryContext(ctx, `
select c.id, c.representative_thread_id,
rt.number, rt.kind, rt.title,
c.member_count,
coalesce(max(coalesce(t.updated_at_gh, t.updated_at)), c.created_at) as latest_updated_at,
`+memberCountExpr+` as member_count,
`+updatedAtExpr+` as latest_updated_at,
c.closed_at_local, c.close_reason_local,
sum(case when t.closed_at_local is not null or t.state <> 'open' then 1 else 0 end) as closed_member_count
from clusters c
@ -515,7 +519,7 @@ func (s *Store) RunClusterDetail(ctx context.Context, options ClusterDetailOptio
where := `cm.cluster_id = ?`
args := []any{options.ClusterID}
if !options.IncludeClosed {
where += ` and t.closed_at_local is null`
where += ` and t.state = 'open' and t.closed_at_local is null`
}
args = append(args, limit)
rows, err := s.db.QueryContext(ctx, `
@ -1195,14 +1199,18 @@ func (s *Store) runClusterSummaryByID(ctx context.Context, repoID, clusterID int
return ClusterSummary{}, 0, fmt.Errorf("cluster %d was not found", clusterID)
}
having := `1 = 1`
memberCountExpr := `c.member_count`
updatedAtExpr := `coalesce(max(coalesce(t.updated_at_gh, t.updated_at)), c.created_at)`
if !includeClosed {
having = `c.close_reason_local is null and closed_member_count < c.member_count`
memberCountExpr = `sum(case when t.state = 'open' and t.closed_at_local is null then 1 else 0 end)`
updatedAtExpr = `coalesce(max(case when t.state = 'open' and t.closed_at_local is null then coalesce(t.updated_at_gh, t.updated_at) end), c.created_at)`
having = memberCountExpr + ` > 0 and c.close_reason_local is null`
}
row := s.db.QueryRowContext(ctx, `
select c.id, c.representative_thread_id,
rt.number, rt.kind, rt.title,
c.member_count,
coalesce(max(coalesce(t.updated_at_gh, t.updated_at)), c.created_at) as latest_updated_at,
`+memberCountExpr+` as member_count,
`+updatedAtExpr+` as latest_updated_at,
c.closed_at_local, c.close_reason_local,
sum(case when t.closed_at_local is not null or t.state <> 'open' then 1 else 0 end) as closed_member_count
from clusters c

View File

@ -189,6 +189,14 @@ func TestListDisplayClusterSummariesPrefersLatestRawRun(t *testing.T) {
if err != nil {
t.Fatalf("raw second thread: %v", err)
}
rawClosed, err := st.UpsertThread(ctx, Thread{
RepoID: repoID, GitHubID: "103", Number: 103, Kind: "issue", State: "closed",
Title: "raw closed", HTMLURL: "https://github.com/openclaw/openclaw/issues/103",
LabelsJSON: "[]", AssigneesJSON: "[]", RawJSON: "{}", ContentHash: "raw-103", UpdatedAt: "2026-04-26T04:00:00Z",
})
if err != nil {
t.Fatalf("raw closed thread: %v", err)
}
durableID, err := st.UpsertThread(ctx, Thread{
RepoID: repoID, GitHubID: "201", Number: 201, Kind: "issue", State: "open",
Title: "durable member", HTMLURL: "https://github.com/openclaw/openclaw/issues/201",
@ -201,15 +209,16 @@ func TestListDisplayClusterSummariesPrefersLatestRawRun(t *testing.T) {
insert into cluster_runs(id, repo_id, scope, status, started_at, finished_at, stats_json)
values(7, ?, 'repo', 'completed', '2026-04-26T00:00:00Z', '2026-04-26T00:01:00Z', '{}');
insert into clusters(id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at)
values(70, ?, 7, ?, 2, '2026-04-26T00:01:00Z');
values(70, ?, 7, ?, 3, '2026-04-26T00:01:00Z');
`, repoID, repoID, rawOne); err != nil {
t.Fatalf("seed raw cluster: %v", err)
}
if _, err := st.DB().ExecContext(ctx, `
insert into cluster_members(cluster_id, thread_id, score_to_representative, created_at)
values(70, ?, 1.0, '2026-04-26T00:01:00Z'),
(70, ?, 0.91, '2026-04-26T00:01:00Z');
`, rawOne, rawTwo); err != nil {
(70, ?, 0.91, '2026-04-26T00:01:00Z'),
(70, ?, 0.90, '2026-04-26T00:01:00Z');
`, rawOne, rawTwo, rawClosed); err != nil {
t.Fatalf("seed raw members: %v", err)
}
if _, err := st.DB().ExecContext(ctx, `
@ -221,11 +230,33 @@ func TestListDisplayClusterSummariesPrefersLatestRawRun(t *testing.T) {
t.Fatalf("seed durable cluster: %v", err)
}
activeDisplay, err := st.ListDisplayClusterSummaries(ctx, ClusterSummaryOptions{RepoID: repoID, IncludeClosed: false, MinSize: 1, Limit: 20, Sort: "size"})
if err != nil {
t.Fatalf("list active display clusters: %v", err)
}
if len(activeDisplay) != 1 || activeDisplay[0].ID != 70 || activeDisplay[0].MemberCount != 2 {
t.Fatalf("active display clusters should hide closed raw members, got %#v", activeDisplay)
}
activeDetail, err := st.ClusterDetail(ctx, ClusterDetailOptions{RepoID: repoID, ClusterID: 70, IncludeClosed: false, MemberLimit: 10})
if err != nil {
t.Fatalf("active raw detail: %v", err)
}
if len(activeDetail.Members) != 2 || activeDetail.Members[0].Thread.Number != 101 || activeDetail.Members[1].Thread.Number == 103 {
t.Fatalf("active raw detail should hide closed members, got %#v", activeDetail)
}
hiddenByMinSize, err := st.ListDisplayClusterSummaries(ctx, ClusterSummaryOptions{RepoID: repoID, IncludeClosed: false, MinSize: 3, Limit: 20, Sort: "size"})
if err != nil {
t.Fatalf("list active display clusters with min size: %v", err)
}
if len(hiddenByMinSize) != 0 {
t.Fatalf("active display min-size should count visible members only, got %#v", hiddenByMinSize)
}
display, err := st.ListDisplayClusterSummaries(ctx, ClusterSummaryOptions{RepoID: repoID, IncludeClosed: true, MinSize: 1, Limit: 20, Sort: "size"})
if err != nil {
t.Fatalf("list display clusters: %v", err)
}
if len(display) != 1 || display[0].ID != 70 || display[0].Source != ClusterSourceRun || display[0].MemberCount != 2 {
if len(display) != 1 || display[0].ID != 70 || display[0].Source != ClusterSourceRun || display[0].MemberCount != 3 {
t.Fatalf("display clusters should prefer raw run, got %#v", display)
}
durable, err := st.ListClusterSummaries(ctx, ClusterSummaryOptions{RepoID: repoID, IncludeClosed: true, MinSize: 1, Limit: 20, Sort: "size"})
@ -240,7 +271,7 @@ func TestListDisplayClusterSummariesPrefersLatestRawRun(t *testing.T) {
if err != nil {
t.Fatalf("raw detail: %v", err)
}
if detail.Cluster.Source != ClusterSourceRun || len(detail.Members) != 2 || detail.Members[0].Thread.Number != 101 {
if detail.Cluster.Source != ClusterSourceRun || len(detail.Members) != 3 || detail.Members[0].Thread.Number != 101 {
t.Fatalf("unexpected raw detail: %#v", detail)
}
}