From 8719964748cf566faf22779fce2c3ea2b68ef4de Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:19:04 -0700 Subject: [PATCH 1/3] feat(signals): classify issue-discovery lifecycle Add deterministic issue lifecycle states and include them in issue-quality reports so solved, stale, duplicate, and invalid issue-discovery activity is separated. --- src/openapi/schemas.ts | 1 + src/signals/engine.ts | 90 ++++++++++++++++++++++++++++++++-- test/unit/local-branch.test.ts | 1 + test/unit/signals-v2.test.ts | 15 ++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 0eccc0eadc..154cac88ec 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -888,6 +888,7 @@ export const IssueQualityReportSchema = z z.object({ number: z.number(), title: z.string(), + lifecycle: z.enum(["open", "closed_not_solved", "solved", "valid_solved", "stale", "duplicate", "invalid"]).optional(), status: z.enum(["ready", "needs_proof", "hold", "do_not_use"]), score: z.number(), reasons: z.array(z.string()), diff --git a/src/signals/engine.ts b/src/signals/engine.ts index f114bd9464..f0616591bb 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -436,6 +436,7 @@ export type IssueQualityReport = { issues: Array<{ number: number; title: string; + lifecycle?: IssueDiscoveryLifecycleState | undefined; status: "ready" | "needs_proof" | "hold" | "do_not_use"; score: number; reasons: string[]; @@ -444,6 +445,22 @@ export type IssueQualityReport = { summary: string; }; +export type IssueDiscoveryLifecycleState = "open" | "closed_not_solved" | "solved" | "valid_solved" | "stale" | "duplicate" | "invalid"; + +export type IssueDiscoveryLifecycleReport = { + repoFullName: string; + generatedAt: string; + lane: LaneAdvice; + states: Array<{ + number: number; + title: string; + state: IssueDiscoveryLifecycleState; + solvedByPullRequests: number[]; + reasons: string[]; + }>; + summary: string; +}; + export type BurdenForecast = { repoFullName: string; generatedAt: string; @@ -1895,6 +1912,7 @@ export function buildIssueQualityReport( ): IssueQualityReport { const lane = buildLaneAdvice(repo, fullName); const collisions = prebuiltCollisions ?? buildCollisionReport(fullName, issues, pullRequests, recentMergedPullRequests); + const lifecycleByIssue = new Map(buildIssueDiscoveryLifecycleReport(repo, issues, pullRequests, fullName, recentMergedPullRequests).states.map((entry) => [entry.number, entry])); const reports = issues .filter((issue) => issue.state === "open") .slice(0, 100) @@ -1903,6 +1921,7 @@ export function buildIssueQualityReport( const linkedMergedPrs = recentMergedPullRequests.filter((pr) => pr.linkedIssues.includes(issue.number) || issue.linkedPrs.includes(pr.number)); const issueCollisions = collisions.clusters.filter((cluster) => cluster.items.some((item) => item.type === "issue" && item.number === issue.number)); const age = daysSince(issue.updatedAt ?? issue.createdAt); + const lifecycle = lifecycleByIssue.get(issue.number)?.state ?? "open"; const bodyLength = issue.body?.trim().length ?? 0; const linkedWorkCount = linkedPrs.length + linkedMergedPrs.length + issue.linkedPrs.length; const reasons = [ @@ -1917,18 +1936,19 @@ export function buildIssueQualityReport( ...(issue.linkedPrs.length > 0 && linkedPrs.length === 0 && linkedMergedPrs.length === 0 ? [`Cached issue metadata already references PR(s): ${issue.linkedPrs.map((number) => `#${number}`).join(", ")}.`] : []), ...(issueCollisions.length > 0 ? ["Potential duplicate or overlapping issue/PR context exists."] : []), ...(age > 90 ? ["Issue is stale in cached metadata."] : []), + ...(lifecycle !== "open" ? [`Issue lifecycle is ${lifecycle.replace(/_/g, " ")}.`] : []), ...(lane.lane === "direct_pr" ? ["Repo is direct-PR first; issue filing is not the primary Gittensor lane."] : []), ]; const score = clamp(100 - warnings.length * 18 + reasons.length * 5 - (age > 180 ? 15 : 0), 0, 100); const status: IssueQualityReport["issues"][number]["status"] = - linkedWorkCount > 0 || issueCollisions.some((cluster) => cluster.risk === "high") + linkedWorkCount > 0 || issueCollisions.some((cluster) => cluster.risk === "high") || ["duplicate", "invalid", "solved", "valid_solved"].includes(lifecycle) ? "do_not_use" - : warnings.some((warning) => /thin|stale|direct-PR/i.test(warning)) + : warnings.some((warning) => /thin|stale|direct-PR/i.test(warning)) || lifecycle === "stale" ? "needs_proof" : score < 45 ? "hold" : "ready"; - return { number: issue.number, title: issue.title, status, score, reasons, warnings }; + return { number: issue.number, title: issue.title, lifecycle, status, score, reasons, warnings }; }) .sort((left, right) => right.score - left.score || left.number - right.number); return { @@ -1940,6 +1960,70 @@ export function buildIssueQualityReport( }; } +export function buildIssueDiscoveryLifecycleReport( + repo: RepositoryRecord | null, + issues: IssueRecord[], + pullRequests: PullRequestRecord[], + fullName: string, + recentMergedPullRequests: RecentMergedPullRequestRecord[] = [], +): IssueDiscoveryLifecycleReport { + const lane = buildLaneAdvice(repo, fullName); + const states = issues + .slice(0, 300) + .map((issue) => classifyIssueDiscoveryLifecycle(issue, pullRequests, recentMergedPullRequests, lane)) + .sort((left, right) => lifecycleRank(left.state) - lifecycleRank(right.state) || left.number - right.number); + return { + repoFullName: fullName, + generatedAt: nowIso(), + lane, + states, + summary: `${states.length} issue lifecycle state(s) classified; ${states.filter((entry) => entry.state === "valid_solved").length} valid solved issue(s), ${states.filter((entry) => entry.state === "closed_not_solved").length} closed without solver evidence.`, + }; +} + +function classifyIssueDiscoveryLifecycle( + issue: IssueRecord, + pullRequests: PullRequestRecord[], + recentMergedPullRequests: RecentMergedPullRequestRecord[], + lane: LaneAdvice, +): IssueDiscoveryLifecycleReport["states"][number] { + const linkedOpenPrs = pullRequests.filter((pr) => pr.linkedIssues.includes(issue.number) || issue.linkedPrs.includes(pr.number)); + const linkedMergedPrs = recentMergedPullRequests.filter((pr) => pr.linkedIssues.includes(issue.number) || issue.linkedPrs.includes(pr.number)); + const solvedByPullRequests = [...new Set([...linkedOpenPrs.filter((pr) => pr.mergedAt || pr.state === "merged").map((pr) => pr.number), ...linkedMergedPrs.map((pr) => pr.number), ...issue.linkedPrs])].sort( + (left, right) => left - right, + ); + const labels = issue.labels.map((label) => label.toLowerCase()); + const stale = daysSince(issue.updatedAt ?? issue.createdAt) > 90; + const duplicate = labels.some((label) => /duplicate/.test(label)); + const invalid = labels.some((label) => /invalid|wontfix|not planned|won't fix/.test(label)); + const state: IssueDiscoveryLifecycleState = duplicate + ? "duplicate" + : invalid + ? "invalid" + : solvedByPullRequests.length > 0 + ? lane.lane === "issue_discovery" || lane.lane === "split" + ? "valid_solved" + : "solved" + : issue.state !== "open" + ? "closed_not_solved" + : stale + ? "stale" + : "open"; + const reasons = [ + ...(duplicate ? ["Issue carries duplicate labeling."] : []), + ...(invalid ? ["Issue carries invalid or not-planned labeling."] : []), + ...(solvedByPullRequests.length > 0 ? [`Linked solver PR(s): ${solvedByPullRequests.map((number) => `#${number}`).join(", ")}.`] : []), + ...(issue.state !== "open" && solvedByPullRequests.length === 0 ? ["Issue is closed without cached solver PR evidence."] : []), + ...(stale && issue.state === "open" ? ["Issue is stale in cached metadata."] : []), + ...(lane.lane === "direct_pr" ? ["Repo is direct-PR first; lifecycle should not encourage issue filing."] : []), + ]; + return { number: issue.number, title: issue.title, state, solvedByPullRequests, reasons: reasons.length > 0 ? reasons : ["Issue is open with no solver or duplicate signal."] }; +} + +function lifecycleRank(state: IssueDiscoveryLifecycleState): number { + return { valid_solved: 0, solved: 1, open: 2, stale: 3, closed_not_solved: 4, duplicate: 5, invalid: 6 }[state]; +} + function issueQualityFindings(linkedIssues: number[], issueQuality: IssueQualityReport | null | undefined): SignalFinding[] { if (!issueQuality || linkedIssues.length === 0) return []; const byIssue = new Map(issueQuality.issues.map((issue) => [issue.number, issue])); diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 81863cb06e..5e90f3df6d 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -123,6 +123,7 @@ describe("local branch analysis", () => { { number: 7, title: "Cache refresh fails", + lifecycle: "valid_solved", status: "do_not_use", score: 0, reasons: [], diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 45a3850b0a..e7917da48a 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -11,6 +11,7 @@ import { buildContributorScoringProfile, buildContributorStrategy, buildContributorIntakeHealth, + buildIssueDiscoveryLifecycleReport, buildIssueQualityReport, buildLabelAudit, buildLocalDiffPreflightResult, @@ -473,6 +474,20 @@ describe("v2 signal builders", () => { repo.fullName, ); expect(issueQuality.issues.map((issue) => issue.status)).toEqual(expect.arrayContaining(["ready", "needs_proof", "do_not_use"])); + expect(issueQuality.issues.find((issue) => issue.number === 22)).toMatchObject({ lifecycle: "stale" }); + expect(issueQuality.issues.find((issue) => issue.number === 23)).toMatchObject({ lifecycle: "valid_solved", status: "do_not_use" }); + + const lifecycle = buildIssueDiscoveryLifecycleReport( + { ...repo, registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.5 } }, + [ + ...issueSet, + { repoFullName: repo.fullName, number: 24, title: "Duplicate", state: "closed", body: "", labels: ["duplicate"], linkedPrs: [] }, + { repoFullName: repo.fullName, number: 25, title: "Closed without solver", state: "closed", body: "", labels: [], linkedPrs: [] }, + ], + prSet, + repo.fullName, + ); + expect(lifecycle.states.map((state) => [state.number, state.state])).toEqual(expect.arrayContaining([[23, "valid_solved"], [24, "duplicate"], [25, "closed_not_solved"]])); const collisions = buildCollisionReport(repo.fullName, issueSet, prSet); const forecast = buildBurdenForecast(repo, issueSet, prSet, collisions, 7); From e4ec708416be78c275b91c1fd0b63843d4cf826d Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:29:51 -0700 Subject: [PATCH 2/3] test(readiness): keep freshness fixtures current --- test/integration/api.test.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index cb45405b5d..de4f64d520 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1140,7 +1140,7 @@ describe("api routes", () => { fetchedCount: 2, expectedCount: 2, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: new Date().toISOString(), warnings: [], }); const refreshingReadiness = await app.request("/v1/readiness", { headers: apiHeaders(refreshingEnv) }, refreshingEnv); @@ -2351,6 +2351,9 @@ async function mcpJson(response: Response): Promise { } async function seedSignalData(env: Env): Promise { + const freshAt = new Date().toISOString(); + const previousFreshAt = new Date(Date.now() - 60_000).toISOString(); + await upsertInstallation(env, { installation: { id: 123, @@ -2371,7 +2374,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); const snapshot = normalizeRegistryPayload( { @@ -2384,7 +2387,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/master_repositories.json" }, - "2026-05-23T00:00:00.000Z", + freshAt, ); await persistRegistrySnapshot( env, @@ -2399,7 +2402,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/old_master_repositories.json" }, - "2026-05-22T00:00:00.000Z", + previousFreshAt, ), ); await persistRegistrySnapshot(env, snapshot); @@ -2414,7 +2417,7 @@ async function seedSignalData(env: Env): Promise { id: "scoring-1", sourceKind: "test", sourceUrl: "fixture://scoring", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, activeModel: "current_density_model", constants: { OSS_EMISSION_SHARE: 0.9, @@ -2459,7 +2462,7 @@ async function seedSignalData(env: Env): Promise { closedUnmergedPullRequestsTotal: 0, labelsTotal: 2, sourceKind: "github", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, payload: {}, }); await Promise.all( @@ -2482,7 +2485,7 @@ async function seedSignalData(env: Env): Promise { fetchedCount: record.fetchedCount, expectedCount: record.expectedCount, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: freshAt, warnings: [], }), ), @@ -2516,7 +2519,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "issue_comment", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); await upsertIssueFromGitHub(env, "entrius/allways-ui", { number: 7, @@ -2552,10 +2555,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 12, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertPullRequestFile(env, { repoFullName: "entrius/allways-ui", @@ -2600,10 +2603,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 13, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertRecentMergedPullRequest(env, { repoFullName: "entrius/allways-ui", From 01d548503debbc5d242f0b43586f6d25d709c78a Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Sat, 30 May 2026 00:40:28 -0700 Subject: [PATCH 3/3] fix(signals): require verified solver PR evidence --- src/signals/engine.ts | 2 +- test/unit/signals-v2.test.ts | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index f0616591bb..76494600dc 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1989,7 +1989,7 @@ function classifyIssueDiscoveryLifecycle( ): IssueDiscoveryLifecycleReport["states"][number] { const linkedOpenPrs = pullRequests.filter((pr) => pr.linkedIssues.includes(issue.number) || issue.linkedPrs.includes(pr.number)); const linkedMergedPrs = recentMergedPullRequests.filter((pr) => pr.linkedIssues.includes(issue.number) || issue.linkedPrs.includes(pr.number)); - const solvedByPullRequests = [...new Set([...linkedOpenPrs.filter((pr) => pr.mergedAt || pr.state === "merged").map((pr) => pr.number), ...linkedMergedPrs.map((pr) => pr.number), ...issue.linkedPrs])].sort( + const solvedByPullRequests = [...new Set([...linkedOpenPrs.filter((pr) => pr.mergedAt || pr.state === "merged").map((pr) => pr.number), ...linkedMergedPrs.map((pr) => pr.number)])].sort( (left, right) => left - right, ); const labels = issue.labels.map((label) => label.toLowerCase()); diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index e7917da48a..f6caefb26c 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -459,13 +459,25 @@ describe("v2 signal builders", () => { repoFullName: repo.fullName, number: 44, title: "Fix already solved report", - state: "open", + state: "merged", + mergedAt: "2026-05-01T00:00:00.000Z", linkedIssues: [23], labels: ["bug"], authorLogin: "oktofeesh1", body: "Fixes #23", updatedAt: "2025-01-01T00:00:00.000Z", }, + { + repoFullName: repo.fullName, + number: 45, + title: "Stale contributor branch", + state: "open", + linkedIssues: [], + labels: ["bug"], + authorLogin: "oktofeesh1", + body: "", + updatedAt: "2025-01-01T00:00:00.000Z", + }, ]; const issueQuality = buildIssueQualityReport( { ...repo, registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.5 } }, @@ -489,6 +501,14 @@ describe("v2 signal builders", () => { ); expect(lifecycle.states.map((state) => [state.number, state.state])).toEqual(expect.arrayContaining([[23, "valid_solved"], [24, "duplicate"], [25, "closed_not_solved"]])); + const unverifiedMentionLifecycle = buildIssueDiscoveryLifecycleReport( + { ...repo, registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.5 } }, + [{ repoFullName: repo.fullName, number: 26, title: "Mentioned PR", state: "closed", body: "Maybe PR #123 helps.", labels: [], linkedPrs: [123] }], + [], + repo.fullName, + ); + expect(unverifiedMentionLifecycle.states[0]).toMatchObject({ number: 26, state: "closed_not_solved", solvedByPullRequests: [] }); + const collisions = buildCollisionReport(repo.fullName, issueSet, prSet); const forecast = buildBurdenForecast(repo, issueSet, prSet, collisions, 7); expect(forecast.horizonDays).toBe(7);