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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"server-manifest:check": "node --experimental-strip-types scripts/check-server-manifest.ts",
"dead-source-files:check": "node --experimental-strip-types scripts/check-dead-source-files.ts",
"dead-exports:check": "node --experimental-strip-types scripts/check-dead-exports.ts",
"maintainer-associations:check": "node --experimental-strip-types scripts/check-maintainer-association-copies.ts",
"regate-sort-key:check": "node --experimental-strip-types scripts/check-regate-sort-key.ts",
"command-redelivery-guards:check": "node --experimental-strip-types scripts/check-command-redelivery-guards.ts",
"dispatch-gate-reasons:check": "node --experimental-strip-types scripts/check-dispatch-gate-reasons.ts",
Expand Down Expand Up @@ -140,7 +141,7 @@
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts",
"pretest:ci": "npm run check-node-version",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run turbo-inputs:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run dead-exports:check && npm run publishable-deps:check && npm run regate-sort-key:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run turbo-inputs:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run dead-exports:check && npm run publishable-deps:check && npm run regate-sort-key:check && npm run maintainer-associations:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci",
"test:watch": "vitest",
Expand Down
11 changes: 2 additions & 9 deletions packages/loopover-engine/src/scoring/pending-pr-scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CheckSummaryRecord, PullRequestRecord, PullRequestReviewRecord, RoleContext } from "./types.js";
import type { ScorePreviewInput } from "./preview.js";
import { isMaintainerAuthorAssociation } from "../settings/author-association.js";

// Deterministic subset of the backend's `src/scoring/pending-pr-scenarios.ts` (#2282): the pure
// classification/detection logic that only needs already-fetched PR/review/check records. The two
Expand Down Expand Up @@ -43,14 +44,6 @@ const STALE_DAYS = 14;
// "draft-js" from matching. Trailing \s* lets the same pattern also strip the marker for dedup keys.
export const DRAFT_TITLE_PATTERN = /^(?:\[\s*draft\s*\]|draft(?:\s*:|\s+-))\s*/i;

// Mirrors `src/github/commands.ts`'s `isMaintainerAssociation`/`MAINTAINER_ASSOCIATIONS` exactly. Duplicated
// here (rather than imported) because this package cannot reach into `src/`; keep the two in sync by hand.
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);

function isMaintainerAssociation(association: string | null | undefined): boolean {
return Boolean(association && MAINTAINER_ASSOCIATIONS.has(association));
}

export function detectPendingPrScenario(args: {
login: string;
repoFullName: string;
Expand Down Expand Up @@ -147,7 +140,7 @@ export function classifyOpenPullRequest(args: {
reasons.push("Maintainer-lane context for this repo; not counted as outside-contributor pending reward work.");
return { repoFullName: args.pr.repoFullName, number: args.pr.number, title: args.pr.title, classification: "maintainer_lane", reasons };
}
if (isMaintainerAssociation(args.pr.authorAssociation)) {
if (isMaintainerAuthorAssociation(args.pr.authorAssociation)) {
reasons.push("Author association indicates maintainer-authored work.");
return { repoFullName: args.pr.repoFullName, number: args.pr.number, title: args.pr.title, classification: "maintainer_lane", reasons };
}
Expand Down
38 changes: 19 additions & 19 deletions packages/loopover-engine/src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { diffFilePriority } from "../review/diff-file-priority.js";
import type { ImprovementBand, StructuralImprovementAssessment } from "../../../../src/signals/improvement.js";
import type { ImprovementMagnitude } from "../../../../src/services/ai-review.js";
import type { SlopBand } from "./slop.js";
import { MAINTAINER_AUTHOR_ASSOCIATIONS, isMaintainerAuthorAssociation } from "../settings/author-association.js";

export type ParticipationLane = "direct_pr" | "issue_discovery" | "split" | "inactive" | "unknown";
export type SignalFinding = AdvisoryFinding;
Expand Down Expand Up @@ -234,7 +235,7 @@ const MAINTAINER_WIP_LABELS = new Set([

/** True iff a maintainer-authored issue is labelled as the maintainer's own in-progress/internal work. */
function isMaintainerWipIssue(issue: IssueRecord): boolean {
return isMaintainerAssociation(issue.authorAssociation) && issue.labels.some((label) => MAINTAINER_WIP_LABELS.has(label.toLowerCase().trim()));
return isMaintainerAuthorAssociation(issue.authorAssociation) && issue.labels.some((label) => MAINTAINER_WIP_LABELS.has(label.toLowerCase().trim()));
}

/**
Expand All @@ -243,7 +244,7 @@ function isMaintainerWipIssue(issue: IssueRecord): boolean {
* exact condition the issue-watch monitor (#699 path B) notifies subscribers about.
*/
export function isGrabbableHighMultiplierIssue(issue: IssueRecord): boolean {
return issue.state === "open" && isMaintainerAssociation(issue.authorAssociation) && !isMaintainerWipIssue(issue);
return issue.state === "open" && isMaintainerAuthorAssociation(issue.authorAssociation) && !isMaintainerWipIssue(issue);
}

export type ContributorFit = {
Expand Down Expand Up @@ -965,7 +966,7 @@ export function buildQueueHealth(
const unlinkedPullRequests = openPullRequests.filter((pr) => pr.linkedIssues.length === 0);
const stalePullRequests = openPullRequests.filter((pr) => daysSince(pr.updatedAt ?? pr.createdAt) >= 14);
const draftPullRequests = openPullRequests.filter((pr) => pr.isDraft);
const maintainerAuthoredPullRequests = openPullRequests.filter((pr) => isMaintainerAssociation(pr.authorAssociation));
const maintainerAuthoredPullRequests = openPullRequests.filter((pr) => isMaintainerAuthorAssociation(pr.authorAssociation));
const slopFlaggedPullRequests = openPullRequests.filter(
(pr) => pr.slopBand === "elevated" || pr.slopBand === "high",
).length;
Expand Down Expand Up @@ -1261,7 +1262,7 @@ export function buildContributorProfile(
authoredPullRequests.filter((pr) => pr.state === "open" && pr.linkedIssues.length === 0).length,
matchingStats.reduce((sum, stat) => sum + stat.unlinkedPullRequests, 0),
);
const maintainerAssociatedPullRequests = authoredPullRequests.filter((pr) => isMaintainerAssociation(pr.authorAssociation)).length;
const maintainerAssociatedPullRequests = authoredPullRequests.filter((pr) => isMaintainerAuthorAssociation(pr.authorAssociation)).length;
const pullRequestCount = Math.max(authoredPullRequests.length, statPullRequests);
const mergedPullRequestCount = Math.max(mergedPullRequests.length, statMergedPullRequests);
const issueCount = Math.max(authoredIssues.length, statIssues);
Expand Down Expand Up @@ -1297,7 +1298,7 @@ function buildGittensorContributorProfile(
/* v8 ignore next -- Official Gittensor snapshots normally include the canonical GitHub login; request-login fallback protects legacy rows. */
const matchingStats = repoStats.filter((stat) => sameLogin(stat.login, snapshot.githubUsername) || sameLogin(stat.login, login));
const unlinkedOpenPullRequests = matchingStats.reduce((sum, stat) => sum + stat.unlinkedPullRequests, 0);
const maintainerAssociatedPullRequests = pullRequests.filter((pr) => sameLogin(pr.authorLogin, login) && isMaintainerAssociation(pr.authorAssociation)).length;
const maintainerAssociatedPullRequests = pullRequests.filter((pr) => sameLogin(pr.authorLogin, login) && isMaintainerAuthorAssociation(pr.authorAssociation)).length;
const reposTouched = snapshot.repositories
.filter((repo) => repo.pullRequests + repo.openIssues + repo.closedIssues > 0)
.map((repo) => repo.repoFullName)
Expand Down Expand Up @@ -1477,7 +1478,7 @@ export function buildContributorOpportunities(
: quality?.status === "hold"
? -15
: 0;
const maintainerAuthored = isMaintainerAssociation(issue.authorAssociation);
const maintainerAuthored = isMaintainerAuthorAssociation(issue.authorAssociation);
const maintainerWip = isMaintainerWipIssue(issue);
const multiplierTier: ContributorOpportunity["multiplierTier"] = maintainerAuthored ? "maintainer_created" : "community";
const availability: ContributorOpportunity["availability"] = maintainerWip ? "maintainer_wip" : "ready";
Expand Down Expand Up @@ -1637,7 +1638,7 @@ export function buildRoleContext(args: {
role = "collaborator";
source = "github_association";
/* v8 ignore next -- strongestAssociation resolves maintainer associations before this guard; it protects malformed mixed association rows. */
} else if (authoredAssociations.some(isMaintainerAssociation)) {
} else if (authoredAssociations.some(isMaintainerAuthorAssociation)) {
role = "repo_maintainer";
source = "github_association";
} else if (touchedByOfficial) {
Expand Down Expand Up @@ -1887,8 +1888,8 @@ export function buildContributorReconciliationReport(args: {
const maintainerLane =
sameLogin(repo?.owner, args.login) ||
sameLogin(repoOwner, args.login) ||
args.pullRequests.some((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login) && isMaintainerAssociation(pr.authorAssociation)) ||
args.issues.some((issue) => sameRepo(issue.repoFullName, repoFullName) && sameLogin(issue.authorLogin, args.login) && isMaintainerAssociation(issue.authorAssociation));
args.pullRequests.some((pr) => sameRepo(pr.repoFullName, repoFullName) && sameLogin(pr.authorLogin, args.login) && isMaintainerAuthorAssociation(pr.authorAssociation)) ||
args.issues.some((issue) => sameRepo(issue.repoFullName, repoFullName) && sameLogin(issue.authorLogin, args.login) && isMaintainerAuthorAssociation(issue.authorAssociation));
return {
repoFullName,
maintainerLane,
Expand Down Expand Up @@ -2081,7 +2082,7 @@ function normalizeRecentMergedOutcome(
// lane so the record is excluded from outside-contributor statistics rather than silently
// inflating the outside-contributor merge rate with unclassifiable data.
const knownOutsider = association === "NONE" || association === "CONTRIBUTOR" || association === "FIRST_TIME_CONTRIBUTOR" || association === "FIRST_TIMER";
const maintainerLane = isMaintainerAssociation(association) || !knownOutsider;
const maintainerLane = isMaintainerAuthorAssociation(association) || !knownOutsider;
return {
number: record.number,
bucket: "merged",
Expand Down Expand Up @@ -2153,7 +2154,7 @@ export function buildRepoOutcomePatterns(args: {
bucket,
decided: merged || closedUnmerged,
merged,
maintainerLane: isMaintainerAssociation(pr.authorAssociation),
maintainerLane: isMaintainerAuthorAssociation(pr.authorAssociation),
linked: pr.linkedIssues.length > 0 || (mergedDetail?.linkedIssues.length ?? 0) > 0,
labels: [...new Set([...pr.labels, ...(mergedDetail?.labels ?? [])])].sort(),
filePaths,
Expand Down Expand Up @@ -2581,7 +2582,7 @@ export function buildPreflightResult(
// snapshot; "inactive" (zero emission share) is unambiguous either way -- it is only reachable from real
// synced data.
const laneUnavailable = (lane.lane === "unknown" && registryEverSynced) || lane.lane === "inactive";
const maintainerAuthored = isMaintainerAssociation(input.authorAssociation);
const maintainerAuthored = isMaintainerAuthorAssociation(input.authorAssociation);
if (laneUnavailable) {
findings.push({
code: "lane_not_recommended",
Expand Down Expand Up @@ -2738,7 +2739,7 @@ export function buildMaintainerPacket(
.map((pr) => {
const reasons = [
...(pr.linkedIssues.length === 0 ? ["Missing linked issue context."] : []),
...(isMaintainerAssociation(pr.authorAssociation) ? ["Author has maintainer association."] : []),
...(isMaintainerAuthorAssociation(pr.authorAssociation) ? ["Author has maintainer association."] : []),
...(collisions.clusters.some((cluster) => cluster.items.some((item) => item.type === "pull_request" && item.number === pr.number))
? ["Potential overlap with other open work."]
: []),
Expand Down Expand Up @@ -3019,7 +3020,7 @@ export function buildIssueQualityReport(
const linkage = buildIssueLinkageRecord(issue, lifecycleEntry, linkedPrs, linkedMergedPrs);
// #186: maintainer-authored issues must not silently read as "ready" for outside contributors —
// always warn to confirm intent, and downgrade ones labelled as the maintainer's own in-progress work.
const maintainerAuthored = isMaintainerAssociation(issue.authorAssociation);
const maintainerAuthored = isMaintainerAuthorAssociation(issue.authorAssociation);
const maintainerWip = isMaintainerWipIssue(issue);
const reasons = [
...(bodyLength >= 200 ? ["Issue has enough body detail to evaluate."] : []),
Expand Down Expand Up @@ -5238,16 +5239,15 @@ function outcomeFailurePatterns(history: ContributorOutcomeHistory): OutcomePatt
}

function strongestAssociation(values: string[]): string | undefined {
for (const association of ["OWNER", "MEMBER", "COLLABORATOR"]) {
// Iterates MAINTAINER_AUTHOR_ASSOCIATIONS in DECLARATION order, which is its precedence order
// (OWNER > MEMBER > COLLABORATOR). This is the one consumer that depends on that ordering rather than
// on membership alone, so it is stated here rather than left implicit in a re-typed literal.
for (const association of MAINTAINER_AUTHOR_ASSOCIATIONS) {
if (values.includes(association)) return association;
}
return values[0];
}

function isMaintainerAssociation(value: string | null | undefined): boolean {
return value === "OWNER" || value === "MEMBER" || value === "COLLABORATOR";
}

function sameLogin(value: string | null | undefined, login: string): boolean {
return value?.toLowerCase() === login.toLowerCase();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
RepositoryRecord,
} from "../types/predicted-gate-types.js";
import { nowIso } from "../utils/json.js";
import { isMaintainerAuthorAssociation } from "../settings/author-association.js";
import {
bountyIssueKey,
buildCollisionReport,
Expand Down Expand Up @@ -77,16 +78,12 @@ function daysSince(value: string | null | undefined): number {
return Math.floor((Date.now() - parsed) / 86_400_000);
}

function isMaintainerAssociation(value: string | null | undefined): boolean {
return value === "OWNER" || value === "MEMBER" || value === "COLLABORATOR";
}

function sameLogin(value: string | null | undefined, login: string): boolean {
return value?.toLowerCase() === login.toLowerCase();
}

function isMaintainerWipIssue(issue: IssueRecord): boolean {
return isMaintainerAssociation(issue.authorAssociation) && issue.labels.some((label) => MAINTAINER_WIP_LABELS.has(label.toLowerCase().trim()));
return isMaintainerAuthorAssociation(issue.authorAssociation) && issue.labels.some((label) => MAINTAINER_WIP_LABELS.has(label.toLowerCase().trim()));
}

function indexPullRequestsByLinkedIssue<T extends { number: number; linkedIssues: number[] }>(pullRequests: T[]): Map<number, T[]> {
Expand Down Expand Up @@ -253,7 +250,7 @@ export function buildIssueQualityReport(
const bounty = bountyByIssue.get(bountyIssueKey(fullName, issue.number)) ?? null;
const bountyLifecycle: BountyLifecycle | null = bounty ? classifyBountyLifecycle(bounty, issue) : null;
const linkedWorkCount = linkedPrs.length + linkedMergedPrs.length + issue.linkedPrs.length;
const maintainerAuthored = isMaintainerAssociation(issue.authorAssociation);
const maintainerAuthored = isMaintainerAuthorAssociation(issue.authorAssociation);
const maintainerWip = isMaintainerWipIssue(issue);
const reasons = [
...(bodyLength >= 200 ? ["Issue has enough body detail to evaluate."] : []),
Expand Down
Loading
Loading