Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import { PREFLIGHT_LIMITS } from "../signals/preflight-limits";
import {
buildBountyAdvisory,
buildBurdenForecast,
Expand Down Expand Up @@ -306,21 +307,21 @@ const PR_VISIBILITY_SKIP_REASONS = [
] as const satisfies readonly PublicSurfaceSkipReason[];

const preflightSchema = z.object({
repoFullName: z.string().min(3),
contributorLogin: z.string().min(1).optional(),
title: z.string().min(1),
body: z.string().optional(),
labels: z.array(z.string()).optional(),
changedFiles: z.array(z.string()).optional(),
linkedIssues: z.array(z.number().int().positive()).optional(),
tests: z.array(z.string()).optional(),
authorAssociation: z.string().optional(),
repoFullName: z.string().min(3).max(PREFLIGHT_LIMITS.repoFullNameChars),
contributorLogin: z.string().min(1).max(PREFLIGHT_LIMITS.contributorLoginChars).optional(),
title: z.string().min(1).max(PREFLIGHT_LIMITS.titleChars),
body: z.string().max(PREFLIGHT_LIMITS.bodyChars).optional(),
labels: z.array(z.string().max(PREFLIGHT_LIMITS.labelChars)).max(PREFLIGHT_LIMITS.labels).optional(),
changedFiles: z.array(z.string().max(PREFLIGHT_LIMITS.changedFileChars)).max(PREFLIGHT_LIMITS.changedFiles).optional(),
linkedIssues: z.array(z.number().int().positive()).max(PREFLIGHT_LIMITS.linkedIssues).optional(),
tests: z.array(z.string().max(PREFLIGHT_LIMITS.testChars)).max(PREFLIGHT_LIMITS.tests).optional(),
authorAssociation: z.string().max(PREFLIGHT_LIMITS.authorAssociationChars).optional(),
});

const localDiffPreflightSchema = preflightSchema.extend({
changedLineCount: z.number().int().min(0).optional(),
testFiles: z.array(z.string()).optional(),
commitMessage: z.string().optional(),
testFiles: z.array(z.string().max(PREFLIGHT_LIMITS.changedFileChars)).max(PREFLIGHT_LIMITS.changedFiles).optional(),
commitMessage: z.string().max(PREFLIGHT_LIMITS.bodyChars).optional(),
});

const skippedPrAuditQuerySchema = z
Expand Down
23 changes: 12 additions & 11 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-mo
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoDataQuality } from "../signals/data-quality";
import { PREFLIGHT_LIMITS } from "../signals/preflight-limits";
import { loadUpstreamStatus } from "../upstream/ruleset";

type AppContext = Context<{ Bindings: Env }>;
Expand Down Expand Up @@ -111,22 +112,22 @@ const bountyShape = {
};

const preflightShape = {
repoFullName: z.string().min(3),
contributorLogin: z.string().min(1).optional(),
title: z.string().min(1),
body: z.string().optional(),
labels: z.array(z.string()).optional(),
changedFiles: z.array(z.string()).optional(),
linkedIssues: z.array(z.number().int().positive()).optional(),
tests: z.array(z.string()).optional(),
authorAssociation: z.string().optional(),
repoFullName: z.string().min(3).max(PREFLIGHT_LIMITS.repoFullNameChars),
contributorLogin: z.string().min(1).max(PREFLIGHT_LIMITS.contributorLoginChars).optional(),
title: z.string().min(1).max(PREFLIGHT_LIMITS.titleChars),
body: z.string().max(PREFLIGHT_LIMITS.bodyChars).optional(),
labels: z.array(z.string().max(PREFLIGHT_LIMITS.labelChars)).max(PREFLIGHT_LIMITS.labels).optional(),
changedFiles: z.array(z.string().max(PREFLIGHT_LIMITS.changedFileChars)).max(PREFLIGHT_LIMITS.changedFiles).optional(),
linkedIssues: z.array(z.number().int().positive()).max(PREFLIGHT_LIMITS.linkedIssues).optional(),
tests: z.array(z.string().max(PREFLIGHT_LIMITS.testChars)).max(PREFLIGHT_LIMITS.tests).optional(),
authorAssociation: z.string().max(PREFLIGHT_LIMITS.authorAssociationChars).optional(),
};

const localDiffPreflightShape = {
...preflightShape,
changedLineCount: z.number().int().min(0).optional(),
testFiles: z.array(z.string()).optional(),
commitMessage: z.string().optional(),
testFiles: z.array(z.string().max(PREFLIGHT_LIMITS.changedFileChars)).max(PREFLIGHT_LIMITS.changedFiles).optional(),
commitMessage: z.string().max(PREFLIGHT_LIMITS.bodyChars).optional(),
};

const branchEligibilityShape = {
Expand Down
43 changes: 37 additions & 6 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { PublicContributorProfile } from "../github/public";
import type { GittensorContributorSnapshot } from "../gittensor/api";
import { nowIso } from "../utils/json";
import { hasLocalTestEvidence } from "./test-evidence";
import { PREFLIGHT_LIMITS } from "./preflight-limits";

export type ParticipationLane = "direct_pr" | "issue_discovery" | "split" | "inactive" | "unknown";
export type SignalFinding = AdvisoryFinding;
Expand Down Expand Up @@ -823,7 +824,7 @@ export function buildCollisionReport(
/* v8 ignore stop */

const clusterList = [...clusters.values()].sort((left, right) => riskRank(right.risk) - riskRank(left.risk));
return {
const report = {
repoFullName,
generatedAt: nowIso(),
summary: {
Expand All @@ -833,6 +834,8 @@ export function buildCollisionReport(
},
clusters: clusterList,
};
collisionReportTermCache.set(report, itemTerms);
return report;
}

export function buildQueueHealth(
Expand Down Expand Up @@ -2343,7 +2346,9 @@ export function buildPreflightResult(
issueQuality?: IssueQualityReport | null | undefined,
): PreflightResult {
const lane = buildLaneAdvice(repo, input.repoFullName);
const linkedIssues = [...new Set([...(input.linkedIssues ?? []), ...extractLinkedIssueNumbers(input.body ?? "")])].sort((left, right) => left - right);
const linkedIssues = [...new Set([...(input.linkedIssues ?? []), ...extractLinkedIssueNumbers(truncateText(input.body ?? "", PREFLIGHT_LIMITS.bodyChars))])].sort(
(left, right) => left - right,
);
// Flag an existing open-work cluster as a possible duplicate when it shares a
// linked issue, OR when its title/body meaningfully overlaps the planned
// contribution. The previous check used `item.title.includes(input.title)`,
Expand All @@ -2354,12 +2359,14 @@ export function buildPreflightResult(
// symmetric term-overlap heuristic `buildCollisionReport` uses between items
// (>=2 shared meaningful terms), which is direction-independent.
const plannedTerms = plannedContributionTerms(input);
const collisions = buildCollisionReport(input.repoFullName, issues, pullRequests).clusters.filter((cluster) =>
const collisionReport = buildCollisionReport(input.repoFullName, issues, pullRequests);
const itemTerms = collisionReportTermCache.get(collisionReport) ?? new Map<string, CollisionTerms>();
const collisions = collisionReport.clusters.filter((cluster) =>
cluster.items.some((item) => {
if (linkedIssues.includes(item.number)) {
return true;
}
const overlap = termOverlap(plannedTerms, collisionTerms(item));
const overlap = termOverlap(plannedTerms, itemTerms.get(itemKey(item)) ?? collisionTerms(item));
return overlap.shared >= 2 && overlap.score >= 0.5;
}),
);
Expand Down Expand Up @@ -4067,6 +4074,8 @@ type CollisionTerms = {
size: number;
};

const collisionReportTermCache = new WeakMap<CollisionReport, Map<string, CollisionTerms>>();

function collisionTerms(item: CollisionItem): CollisionTerms {
const terms = new Set(tokenize(collisionItemText(item)));
return { terms, size: terms.size };
Expand All @@ -4079,7 +4088,15 @@ function collisionTerms(item: CollisionItem): CollisionTerms {
* uses between items, rather than a one-direction substring test.
*/
function plannedContributionTerms(input: PreflightInput): CollisionTerms {
const terms = new Set(tokenize([input.title, ...(input.labels ?? []), ...(input.changedFiles ?? [])].join(" ")));
const terms = new Set(
tokenize(
[
truncateText(input.title, PREFLIGHT_LIMITS.titleChars),
...boundedTextItems(input.labels, PREFLIGHT_LIMITS.labels, PREFLIGHT_LIMITS.labelChars),
...boundedTextItems(input.changedFiles, PREFLIGHT_LIMITS.changedFiles, PREFLIGHT_LIMITS.changedFileChars),
].join(" "),
),
);
return { terms, size: terms.size };
}

Expand All @@ -4094,7 +4111,21 @@ function termOverlap(left: CollisionTerms, right: CollisionTerms): { score: numb
}

function collisionItemText(item: CollisionItem): string {
return [item.title, ...(item.labels ?? []), ...(item.changedFiles ?? [])].filter(Boolean).join(" ");
return [
truncateText(item.title, PREFLIGHT_LIMITS.titleChars),
...boundedTextItems(item.labels, PREFLIGHT_LIMITS.labels, PREFLIGHT_LIMITS.labelChars),
...boundedTextItems(item.changedFiles, PREFLIGHT_LIMITS.changedFiles, PREFLIGHT_LIMITS.changedFileChars),
]
.filter(Boolean)
.join(" ");
}

function boundedTextItems(values: string[] | undefined, maxItems: number, maxChars: number): string[] {
return (values ?? []).slice(0, maxItems).map((value) => truncateText(value, maxChars));
}

function truncateText(value: string, maxChars: number): string {
return value.length > maxChars ? value.slice(0, maxChars) : value;
}

function tokenize(value: string): string[] {
Expand Down
14 changes: 14 additions & 0 deletions src/signals/preflight-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const PREFLIGHT_LIMITS = {
repoFullNameChars: 200,
contributorLoginChars: 100,
titleChars: 300,
bodyChars: 20_000,
labelChars: 100,
changedFileChars: 300,
testChars: 300,
authorAssociationChars: 100,
labels: 50,
changedFiles: 200,
linkedIssues: 100,
tests: 50,
} as const;
30 changes: 30 additions & 0 deletions test/unit/signals-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
buildPullRequestReviewability,
buildRepoRewardRisk,
} from "../../src/signals/reward-risk";
import { PREFLIGHT_LIMITS } from "../../src/signals/preflight-limits";
import type { GittensorContributorSnapshot } from "../../src/gittensor/api";
import type {
ContributorRepoStatRecord,
Expand Down Expand Up @@ -604,6 +605,35 @@ describe("signal coverage edge cases", () => {
expect(preflight.findings.map((finding) => finding.code)).not.toContain("possible_duplicate_work");
});

it("bounds preflight body scanning for linked issue extraction", () => {
const directRepo = repo("owner/direct");
const linkedInsideLimit = buildPreflightResult(
{
repoFullName: directRepo.fullName,
title: "Bounded body scan",
body: `Fixes #99 ${"x".repeat(PREFLIGHT_LIMITS.bodyChars + 100)}`,
linkedIssues: [],
},
directRepo,
[],
[],
);
const linkedPastLimit = buildPreflightResult(
{
repoFullName: directRepo.fullName,
title: "Bounded body scan",
body: `${"x".repeat(PREFLIGHT_LIMITS.bodyChars)} Fixes #100`,
linkedIssues: [],
},
directRepo,
[],
[],
);

expect(linkedInsideLimit.linkedIssues).toContain(99);
expect(linkedPastLimit.linkedIssues).not.toContain(100);
});

it("sanitizes public PR comments and supports minimal public signal level", () => {
const directRepo = repo("owner/direct");
const prRecord = pr(directRepo.fullName, 55, "Fix cache", { authorLogin: "miner", linkedIssues: [] });
Expand Down