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
20 changes: 20 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,15 @@ settings:
# The label name to apply. String. Default: gittensor.
gittensorLabel: gittensor

# Apply the per-PR TYPE/taxonomy label -- exactly one of gittensor:bug / gittensor:feature /
# gittensor:priority, classified from the PR title + changed paths (#label-decoupling). Independent of
# `autoLabelEnabled` above (which only governs the base `gittensorLabel` context label): a type label is
# internal triage metadata, so it applies to every PR regardless of author type (bot/maintainer/missing
# author) or `publicAudienceMode`/`publicSurface`, UNLESS `publicAudienceMode: gittensor_only` is muting
# this PR's author entirely (that mode's whole point is total silence for a non-confirmed-miner author).
# Bool. Default: true.
typeLabelsEnabled: true

# Create the label if it does not yet exist. Bool. Default: true.
createMissingLabel: true

Expand Down Expand Up @@ -311,6 +320,17 @@ settings:
# Bool. Default: false.
agentDryRun: false

# Four independent label families, none of which gates or silently disables another (#label-decoupling,
# #label-scoping):
# 1. Context label (`gittensorLabel`, gated by `autoLabelEnabled` above) — the base per-PR marker shown
# to the public surface, subject to `publicAudienceMode`/`publicSurface`/author-qualification.
# 2. Taxonomy/type label (`gittensor:bug`/`gittensor:feature`/`gittensor:priority`, gated by
# `typeLabelsEnabled` above) — internal triage metadata, applied independently of #1.
# 3. Autonomy-outcome labels (`gittensory:ready-to-merge` etc.) — gated by the `review_state_label`
# autonomy class below, advisory commentary on the bot's own verdict.
# 4. Anti-abuse enforcement labels (blacklist/contributor-cap/review-nag) — gated by the `close`
# autonomy class below, applied alongside the close action they accompany.
#
# Autonomy dial — per-action-class level (observe … auto). Classes: review, request_changes, approve,
# merge, close, label, review_state_label, update_branch. Map. Default: {} (= observe everywhere,
# deny-by-default).
Expand Down
8 changes: 8 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8966,6 +8966,9 @@
"contributorCapCancelCi": {
"type": "boolean",
"nullable": true
},
"typeLabelsEnabled": {
"type": "boolean"
}
},
"required": [
Expand All @@ -8991,6 +8994,7 @@
"aiReviewAllAuthors",
"closeOwnerAuthors",
"autoLabelEnabled",
"typeLabelsEnabled",
"gittensorLabel",
"blacklistLabel",
"createMissingLabel",
Expand Down Expand Up @@ -9624,6 +9628,9 @@
"defaultAllowed",
"commandOverrides"
]
},
"typeLabelsEnabled": {
"type": "boolean"
}
},
"required": [
Expand All @@ -9644,6 +9651,7 @@
"selfAuthoredLinkedIssueGateMode",
"firstTimeContributorGrace",
"autoLabelEnabled",
"typeLabelsEnabled",
"gittensorLabel",
"blacklistLabel",
"createMissingLabel",
Expand Down
9 changes: 9 additions & 0 deletions migrations/0101_type_labels_enabled.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Label decoupling (#label-decoupling): the per-PR TYPE label (gittensor:bug/feature/priority,
-- classified from the PR title + changed paths) was previously gated by autoLabelEnabled nested
-- inside decidePublicSurface's public-contributor-surface decision -- so miner-detection status,
-- publicAudienceMode, or a maintainer-authored PR could silently suppress it, even though type
-- labels are internal triage metadata meant to apply unconditionally. This column independently
-- controls the type label, separate from autoLabelEnabled (the base gittensor context label).
-- Default true matches the prior de-facto behavior (autoLabelEnabled defaults true too), so
-- existing repos see no behavior change until they explicitly opt out.
ALTER TABLE repository_settings ADD COLUMN type_labels_enabled INTEGER NOT NULL DEFAULT 1;
5 changes: 5 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewAllAuthors: false,
closeOwnerAuthors: false,
autoLabelEnabled: true,
typeLabelsEnabled: true,
gittensorLabel: "gittensor",
blacklistLabel: "slop",
createMissingLabel: true,
Expand Down Expand Up @@ -549,6 +550,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewAllAuthors: row.aiReviewAllAuthors,
closeOwnerAuthors: row.closeOwnerAuthors,
autoLabelEnabled: row.autoLabelEnabled,
typeLabelsEnabled: row.typeLabelsEnabled,
gittensorLabel: row.gittensorLabel,
blacklistLabel: row.blacklistLabel,
createMissingLabel: row.createMissingLabel,
Expand Down Expand Up @@ -646,6 +648,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewAllAuthors: settings.aiReviewAllAuthors ?? false,
closeOwnerAuthors: settings.closeOwnerAuthors ?? false,
autoLabelEnabled: settings.autoLabelEnabled ?? true,
typeLabelsEnabled: settings.typeLabelsEnabled ?? true,
gittensorLabel: settings.gittensorLabel ?? "gittensor",
blacklistLabel: settings.blacklistLabel ?? "slop",
createMissingLabel: settings.createMissingLabel ?? true,
Expand Down Expand Up @@ -709,6 +712,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewAllAuthors: resolved.aiReviewAllAuthors,
closeOwnerAuthors: resolved.closeOwnerAuthors,
autoLabelEnabled: resolved.autoLabelEnabled,
typeLabelsEnabled: resolved.typeLabelsEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
createMissingLabel: resolved.createMissingLabel,
Expand Down Expand Up @@ -773,6 +777,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewAllAuthors: resolved.aiReviewAllAuthors,
closeOwnerAuthors: resolved.closeOwnerAuthors,
autoLabelEnabled: resolved.autoLabelEnabled,
typeLabelsEnabled: resolved.typeLabelsEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
createMissingLabel: resolved.createMissingLabel,
Expand Down
3 changes: 3 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export const repositorySettings = sqliteTable("repository_settings", {
// regardless of the label a repo uses.
blacklistLabel: text("blacklist_label").notNull().default("slop"),
createMissingLabel: integer("create_missing_label", { mode: "boolean" }).notNull().default(true),
// #label-decoupling: independently gates the per-PR TYPE label (gittensor:bug/feature/priority),
// distinct from autoLabelEnabled (the base gittensor context label) and the public-surface gate.
typeLabelsEnabled: integer("type_labels_enabled", { mode: "boolean" }).notNull().default(true),
publicSurface: text("public_surface").notNull().default("comment_and_label"),
includeMaintainerAuthors: integer("include_maintainer_authors", { mode: "boolean" }).notNull().default(false),
requireLinkedIssue: integer("require_linked_issue", { mode: "boolean" }).notNull().default(false),
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ export const RepositorySettingsSchema = z
.optional(),
closeOwnerAuthors: z.boolean(),
autoLabelEnabled: z.boolean(),
typeLabelsEnabled: z.boolean(),
gittensorLabel: z.string(),
blacklistLabel: z.string().nullable(),
createMissingLabel: z.boolean(),
Expand Down Expand Up @@ -698,6 +699,7 @@ export const RepoSettingsPreviewSchema = z
firstTimeContributorGrace: z.boolean(),
slopGateMinScore: z.number().nullable().optional(),
autoLabelEnabled: z.boolean(),
typeLabelsEnabled: z.boolean(),
gittensorLabel: z.string(),
blacklistLabel: z.string(),
createMissingLabel: z.boolean(),
Expand Down
148 changes: 98 additions & 50 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6252,6 +6252,104 @@ async function maybePublishPrPublicSurface(
return undefined;
}

// Per-PR TYPE label (reviewbot auto-label parity): exactly ONE of gittensor:bug/feature/priority by the PR
// title + changed paths. Gated by `typeLabelsEnabled` (#label-decoupling), NOT `decision.willLabel` -- type
// labels are internal triage metadata, applied regardless of author type (bot/maintainer/missing-author) or
// the narrower reasons `willLabel` itself can be false (`oss_maintainer` mode + an unconfirmed miner,
// `autoLabelEnabled`, or the repo's `publicSurface` mode) -- see `typeLabelsEnabled`'s doc comment in
// types.ts. The ONE thing still respected is `publicAudienceMode: "gittensor_only"`'s stricter promise to
// stay entirely quiet for a non-confirmed-miner author (`not_official_gittensor_miner` /
// `miner_detection_unavailable`) -- that mode's whole point is total silence for that audience, not merely
// suppressing the context label, so a type label would violate it same as a comment would.
// `typeLabelsEnabled` is optional only for RepositorySettings-fixture-construction backward compat (see
// its doc comment in types.ts); getRepositorySettings always resolves it to a concrete boolean, so the
// `?? true` fallback is unreachable on this webhook-integration path (unlike a pure function such as
// buildRepoSettingsPreview, which a unit test can call with a hand-built, genuinely-undefined settings object).
/* v8 ignore next -- see the comment above */
const typeLabelsEnabled = settings.typeLabelsEnabled ?? true;
if (
typeLabelsEnabled &&
!settings.agentPaused &&
decision.skipReason !== "miner_detection_unavailable" &&
decision.skipReason !== "not_official_gittensor_miner"
) {
try {
const contentGlobs =
(settings as { contentGlobs?: string[] }).contentGlobs ?? [];
// contentGlobs is a forward-compat hook, not yet wired to any real settings field (no caller ever
// populates it), so it is always [] today -- the content-glob branch below is unreachable in
// production and cannot be exercised by a realistic test.
/* v8 ignore start */
const typeFiles = contentGlobs.length > 0
? await resolvePullRequestFilesForReview(env, {
installationId,
repoFullName,
pullNumber: pr.number,
}).catch(() => [] as Awaited<ReturnType<typeof listPullRequestFiles>>)
: [];
/* v8 ignore stop */
const chosenType = resolvePrTypeLabel({
title: pr.title,
/* v8 ignore next -- see the contentGlobs note above; typeFiles is always [] today so this callback never runs */
changedPaths: typeFiles.map((file) => file.path),
contentGlobs,
});
await ensurePullRequestLabel(
env,
installationId,
repoFullName,
pr.number,
chosenType,
{ createMissingLabel: true, mode },
);
for (const other of ALL_TYPE_LABELS.filter(
(label) => label !== chosenType,
)) {
await removePullRequestLabel(
env,
installationId,
repoFullName,
pr.number,
other,
mode,
);
}
console.log(
JSON.stringify({
ev: "type_label_decision",
repoFullName,
pull: pr.number,
applied: true,
label: chosenType,
}),
);
} catch (error) {
console.log(
JSON.stringify({
ev: "type_label_error",
repoFullName,
pull: pr.number,
message: errorMessage(error).slice(0, 150),
}),
);
}
} else {
console.log(
JSON.stringify({
ev: "type_label_decision",
repoFullName,
pull: pr.number,
applied: false,
reason: settings.agentPaused
? "agent_paused"
: decision.skipReason === "miner_detection_unavailable" ||
decision.skipReason === "not_official_gittensor_miner"
? decision.skipReason
: "typeLabelsEnabled_false",
}),
);
}

// Respect the per-repo agent pause: suppress all public surface mutations (label, comment, context
// check run) so a paused repo sees no gittensory-authored GitHub content. The review-agent check
// run still posts so the required-check status is not broken (#agent-pause).
Expand Down Expand Up @@ -7853,56 +7951,6 @@ async function maybePublishPrPublicSurface(
);
if (isGitHubRateLimitedError(error)) throw error;
}
// Per-PR TYPE label (reviewbot auto-label parity): exactly ONE of gittensor:bug/feature/priority by the PR
// title + changed paths. Review-time + neutral, BEST-EFFORT + independent of the context label above so a
// type-label hiccup never drops the "label" output. Files are only fetched when content globs are configured
// (otherwise the label is title-derived). The status labels (ready-to-merge etc.) remain the autonomy layer's.
if (settings.autoLabelEnabled) {
try {
const contentGlobs =
(settings as { contentGlobs?: string[] }).contentGlobs ?? [];
const typeFiles =
contentGlobs.length > 0
? await getReviewFiles().catch(
() => [] as Awaited<ReturnType<typeof getReviewFiles>>,
)
: [];
const chosenType = resolvePrTypeLabel({
title: pr.title,
changedPaths: typeFiles.map((file) => file.path),
contentGlobs,
});
await ensurePullRequestLabel(
env,
installationId,
repoFullName,
pr.number,
chosenType,
{ createMissingLabel: true, mode },
);
for (const other of ALL_TYPE_LABELS.filter(
(label) => label !== chosenType,
)) {
await removePullRequestLabel(
env,
installationId,
repoFullName,
pr.number,
other,
mode,
);
}
} catch (error) {
console.log(
JSON.stringify({
ev: "type_label_error",
repoFullName,
pull: pr.number,
message: errorMessage(error).slice(0, 150),
}),
);
}
}
}
return finishPublicSurfacePublication();
}
Expand Down
3 changes: 2 additions & 1 deletion src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export type FocusManifestSettings = Partial<
| "aiReviewAllAuthors"
| "closeOwnerAuthors"
| "autoLabelEnabled"
| "typeLabelsEnabled"
| "badgeEnabled"
| "gittensorLabel"
| "createMissingLabel"
Expand Down Expand Up @@ -940,7 +941,7 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
}
const publicSurface = normalizeOptionalEnum(r.publicSurface, "settings.publicSurface", ["off", "comment_and_label", "comment_only", "label_only"] as const, warnings);
if (publicSurface !== null) out.publicSurface = publicSurface;
for (const key of ["aiReviewByok", "aiReviewAllAuthors", "closeOwnerAuthors", "autoLabelEnabled", "badgeEnabled", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "privateTrustEnabled", "agentPaused", "agentDryRun"] as const) {
for (const key of ["aiReviewByok", "aiReviewAllAuthors", "closeOwnerAuthors", "autoLabelEnabled", "typeLabelsEnabled", "badgeEnabled", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "privateTrustEnabled", "agentPaused", "agentDryRun"] as const) {
const flag = normalizeOptionalBoolean(r[key], `settings.${key}`, warnings);
if (flag !== null) out[key] = flag;
}
Expand Down
2 changes: 2 additions & 0 deletions src/signals/settings-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export type RepoSettingsPreview = {
firstTimeContributorGrace: boolean;
slopGateMinScore?: number | null | undefined;
autoLabelEnabled: boolean;
typeLabelsEnabled: boolean;
gittensorLabel: string;
blacklistLabel: string;
createMissingLabel: boolean;
Expand Down Expand Up @@ -320,6 +321,7 @@ export function buildRepoSettingsPreview(args: {
firstTimeContributorGrace: settings.firstTimeContributorGrace,
slopGateMinScore: settings.slopGateMinScore ?? null,
autoLabelEnabled: settings.autoLabelEnabled,
typeLabelsEnabled: settings.typeLabelsEnabled ?? true,
gittensorLabel: settings.gittensorLabel,
blacklistLabel: settings.blacklistLabel ?? "slop",
createMissingLabel: settings.createMissingLabel,
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,18 @@ export type RepositorySettings = {
autoLabelEnabled: boolean;
gittensorLabel: string;
createMissingLabel: boolean;
/** #label-decoupling: independently gates the per-PR TYPE/taxonomy label (exactly one of
* `gittensor:bug`/`gittensor:feature`/`gittensor:priority`, classified from the PR title +
* changed paths — see `resolvePrTypeLabel` in `settings/pr-type-label.ts`). Distinct from
* {@link autoLabelEnabled} (which governs only the base {@link gittensorLabel} context label) and
* from `decidePublicSurface`'s public-surface gate (miner detection / `publicAudienceMode` /
* `includeMaintainerAuthors` / bot-author exclusion) — type labels are internal triage metadata
* applied unconditionally to every PR, not a contributor-facing signal, so neither of those
* public-surface conditions should suppress them. Default TRUE (matches the prior de-facto
* behavior before this field existed, when type labels were gated by `autoLabelEnabled` nested
* inside the public-surface check). Always populated by the DB layer; optional so existing
* settings fixtures/callers need not be touched. */
typeLabelsEnabled?: boolean | undefined;
publicSurface: "off" | "comment_and_label" | "comment_only" | "label_only";
includeMaintainerAuthors: boolean;
requireLinkedIssue: boolean;
Expand Down
14 changes: 14 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,20 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
expect(eff.badgeEnabled).toBe(true); // settings: override wins over the DB-stored value
});

it("wires settings.typeLabelsEnabled into the manifest parser and lets a per-repo override win over a global default (#label-decoupling)", () => {
const parsedTrue = parseFocusManifest({ settings: { typeLabelsEnabled: true } });
expect(parsedTrue.settings.typeLabelsEnabled).toBe(true);
expect(parsedTrue.warnings).toEqual([]);
const parsedFalse = parseFocusManifest({ settings: { typeLabelsEnabled: false } });
expect(parsedFalse.settings.typeLabelsEnabled).toBe(false);

// Simulates PR #1's private-config layering: a global default of `true` (DB, standing in for the
// global .gittensory.yml layer already merged upstream) overridden by a per-repo `settings:` block.
const db = { typeLabelsEnabled: true } as unknown as RepositorySettings;
const eff = resolveEffectiveSettings(db, parseFocusManifest({ settings: { typeLabelsEnabled: false } }));
expect(eff.typeLabelsEnabled).toBe(false); // settings: override wins over the DB/global-default value
});

it("parses aiReview from settings: and lets gate.aiReview win in resolveEffectiveSettings", () => {
const parsed = parseFocusManifest({ settings: { aiReviewMode: "advisory", aiReviewByok: true } });
expect(parsed.settings.aiReviewMode).toBe("advisory");
Expand Down
Loading
Loading