From 89e7ca3a606874f5df2b88a5f5461a4028c8e931 Mon Sep 17 00:00:00 2001 From: mendezr Date: Mon, 5 Oct 2026 20:16:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20preserve=20PR=20attributi?= =?UTF-8?q?on=20before=20Hive=20identity=20footer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recognize only the terminal identity block emitted by gh-wrapper, then keep the existing final-attribution-line rule. Cover production footer variants, rejected prose and quoted examples, open/closed PR collection, and effective-model ranking eligibility. Hive-Run: hivecommons/hive#10686 Hive-Plan: identity-footer-attribution Hive-Spec: issue-10686#expected-behavior Co-authored-by: arteeh <35239587+arteeh@users.noreply.github.com> Signed-off-by: mendezr --- ...fixed-10686-attribution-identity-footer.md | 1 + .../contribute_effective_models_test.go | 37 +++++++++ src/pkg/github/attribution.go | 13 ++- .../attribution_identity_footer_test.go | 80 +++++++++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixed-10686-attribution-identity-footer.md create mode 100644 src/pkg/github/attribution_identity_footer_test.go diff --git a/changelog.d/fixed-10686-attribution-identity-footer.md b/changelog.d/fixed-10686-attribution-identity-footer.md new file mode 100644 index 0000000000..af972790a6 --- /dev/null +++ b/changelog.d/fixed-10686-attribution-identity-footer.md @@ -0,0 +1 @@ +- Most effective models now recognizes contributor PR attribution followed by Hive's identity footer, restoring merged counts and ranking eligibility on rescan while still rejecting attribution followed by ordinary prose ([#10686](https://github.com/hivecommons/hive/issues/10686)). diff --git a/src/pkg/dashboard/contribute_effective_models_test.go b/src/pkg/dashboard/contribute_effective_models_test.go index b56909b948..53cb112862 100644 --- a/src/pkg/dashboard/contribute_effective_models_test.go +++ b/src/pkg/dashboard/contribute_effective_models_test.go @@ -2,6 +2,7 @@ package dashboard import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -53,6 +54,42 @@ func TestAggregateContributeEffectiveModelsThresholdRankingAndPrivacy(t *testing } } +func TestAggregateContributeEffectiveModelsIdentityFooterAttribution(t *testing.T) { + now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) + body := "Done\n\n— hive: backend=codex model=gpt-6-astra effort=medium\n\n---\nšŸ **Hive Agent**: `contributor` | **SHA:** `671737a3b`" + var prs []ghpkg.PullRequest + var runs []TaskRunRecord + for number := 1; number <= effectiveModelsDefaultMinMergedPRs; number++ { + meta, ok := ghpkg.ParseAttributionTrailer(body) + if !ok { + t.Fatal("production contributor PR body lost its attribution") + } + prs = append(prs, ghpkg.PullRequest{ + Repo: "org/repo", Number: number, HiveAttributed: ok, + HiveBackend: meta.Backend, HiveModel: meta.Model, + CreatedAt: now, MergedAt: now, Rework: ghpkg.PRReworkStats{FirstPass: true}, + }) + runs = append(runs, TaskRunRecord{ + TS: now.Format(time.RFC3339), Username: "contributor", Backend: "codex", Model: "gpt-6-astra", + Outcome: outcomeCompleted, PRVerified: true, PRURL: fmt.Sprintf("https://github.com/org/repo/pull/%d", number), + }) + } + for _, filter := range []string{"all", "contributor"} { + below := aggregateContributeEffectiveModels(prs[:4], runs, "30d", filter, effectiveModelsDefaultMinMergedPRs, now) + if len(below.Ranked) != 0 || len(below.Insufficient) != 1 || below.Insufficient[0].MergedPRs != 4 { + t.Fatalf("%s below threshold = %+v", filter, below) + } + got := aggregateContributeEffectiveModels(prs, runs, "30d", filter, effectiveModelsDefaultMinMergedPRs, now) + if len(got.Ranked) != 1 || len(got.Insufficient) != 0 { + t.Fatalf("%s at threshold = %+v", filter, got) + } + row := got.Ranked[0] + if row.Model != "gpt-6-astra" || row.Backend != "codex" || row.MergedPRs != 5 || row.PRs != 5 || row.EffectivenessRank != 1 { + t.Fatalf("%s attribution/ranking = %+v", filter, row) + } + } +} + func TestHandleContributeEffectiveModelsPublicAggregate(t *testing.T) { s := covApiServer(t) now := time.Now() diff --git a/src/pkg/github/attribution.go b/src/pkg/github/attribution.go index c0c4d6849c..7cfe941259 100644 --- a/src/pkg/github/attribution.go +++ b/src/pkg/github/attribution.go @@ -296,11 +296,18 @@ func HasAttributionTrailer(body string) bool { return strings.Contains(body, AttributionTrailerPrefix) } +// attributionIdentityFooter matches only the terminal block emitted by +// bin/gh-wrapper.sh's _identity_footer, not arbitrary trailing Markdown. +var attributionIdentityFooter = regexp.MustCompile("\\n[ \\t]*---[ \\t]*\\nšŸ \\*\\*Hive Agent\\*\\*:(?: `[^`\\n]+`)?(?: \\| \\*\\*Instance:\\*\\* `[^`\\n]+`)? \\| \\*\\*SHA:\\*\\* `[^`\\n]+`[ \\t\\n]*$") + // ParseAttributionTrailer extracts the launch metadata from the final visible -// `— hive:` line in a PR body. Mentions of the template earlier in the body are -// deliberately ignored so docs and quoted examples do not count as agent PRs. +// `— hive:` line in a PR body, optionally followed by Hive's identity footer. +// Mentions of the template earlier in the body are deliberately ignored so +// docs and quoted examples do not count as agent PRs. func ParseAttributionTrailer(body string) (InvocationMeta, bool) { - lines := strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") + body = strings.ReplaceAll(body, "\r\n", "\n") + body = attributionIdentityFooter.ReplaceAllString(body, "") + lines := strings.Split(body, "\n") trailer := "" for i := len(lines) - 1; i >= 0; i-- { line := strings.TrimSpace(lines[i]) diff --git a/src/pkg/github/attribution_identity_footer_test.go b/src/pkg/github/attribution_identity_footer_test.go new file mode 100644 index 0000000000..f9f8b95738 --- /dev/null +++ b/src/pkg/github/attribution_identity_footer_test.go @@ -0,0 +1,80 @@ +package github + +import ( + "context" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + gh "github.com/google/go-github/v72/github" +) + +const contributorAttributionBody = "Done\n\n— hive: backend=codex model=gpt-6-astra effort=medium\n\n---\nšŸ **Hive Agent**: `contributor` | **SHA:** `671737a3b`" + +func TestParseAttributionTrailerWithIdentityFooter(t *testing.T) { + for _, body := range []string{ + contributorAttributionBody, + contributorAttributionBody + "\n\n", + strings.ReplaceAll(contributorAttributionBody, "\n", "\r\n"), + strings.ReplaceAll(contributorAttributionBody, " | **SHA:**", " | **Instance:** `hive-bold-fox` | **SHA:**"), + strings.ReplaceAll(contributorAttributionBody, " `contributor`", ""), + } { + meta, ok := ParseAttributionTrailer(body) + if !ok || meta.Backend != "codex" || meta.Model != "gpt-6-astra" { + t.Errorf("ParseAttributionTrailer(%q) = %+v, %v", body, meta, ok) + } + } +} + +func TestParseAttributionTrailerDoesNotScanPastOrdinaryContent(t *testing.T) { + for _, body := range []string{ + contributorAttributionBody + "\nMore prose", + strings.ReplaceAll(contributorAttributionBody, "\n\n---", "\nMore prose\n\n---"), + strings.ReplaceAll(contributorAttributionBody, "\n\n---", "\n\n"), + strings.ReplaceAll(contributorAttributionBody, "**SHA:**", "**Other:**"), + strings.ReplaceAll(contributorAttributionBody, "— hive:", "> — hive:"), + "```text\n" + contributorAttributionBody + "\n```", + strings.ReplaceAll(contributorAttributionBody, "\n\n---", "\n```\n\n---"), + contributorAttributionBody + "\n\n---\nšŸ **Hive Agent**: `contributor` | **SHA:** `unknown`", + } { + if meta, ok := ParseAttributionTrailer(body); ok { + t.Errorf("accepted nonterminal or quoted attribution in %q: %+v", body, meta) + } + } +} + +func TestCollectAttributedPRsWithIdentityFooter(t *testing.T) { + pr := &gh.PullRequest{Number: gh.Ptr(10480), Body: gh.Ptr(contributorAttributionBody)} + prs := collectAttributedPRsFromList("hivecommons/hive", []*gh.PullRequest{pr}) + if len(prs) != 1 || !prs[0].HiveAttributed || prs[0].HiveBackend != "codex" || prs[0].HiveModel != "gpt-6-astra" { + t.Fatalf("attributed PRs = %+v", prs) + } +} + +func TestFetchAttributedClosedPRsWithIdentityFooter(t *testing.T) { + now := time.Now().UTC().Truncate(time.Second) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/repos/o/r/pulls" || r.URL.Query().Get("state") != "closed" { + t.Errorf("unexpected request: %s", r.URL) + http.NotFound(w, r) + return + } + pr := attributedClosedPR(10480, now) + pr["body"] = contributorAttributionBody + pr["merged_at"] = now.Format(time.RFC3339) + writeJSON(t, w, []map[string]any{pr}) + })) + defer srv.Close() + c := NewClientForTest(srv.URL, "o", []string{"r"}, slog.New(slog.NewTextHandler(io.Discard, nil))) + prs, err := c.fetchAttributedClosedPRs(context.Background(), "o", "r", "r") + if err != nil { + t.Fatal(err) + } + if len(prs) != 1 || prs[0].HiveBackend != "codex" || prs[0].HiveModel != "gpt-6-astra" || !prs[0].MergedAt.Equal(now) { + t.Fatalf("merged attributed PRs = %+v", prs) + } +}