Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5fa68df
fix(api): add cross-repo queue health federation endpoint and operato…
dale053 Jun 8, 2026
05d79c3
test(coverage): cover federation route, onboarding 404, and monitor h…
dale053 Jun 8, 2026
f041767
test(coverage): cover session access boundary, public.ts fallbacks, a…
dale053 Jun 8, 2026
b3d62f7
fix(api): wire queue_federation_snapshots cache into federation servi…
dale053 Jun 9, 2026
cab96db
fix(mcp): enforce operator role on queue_health_federation tool and s…
dale053 Jun 9, 2026
6e17d3f
Merge branch 'main' into feat/api-cross-repo-queue-health-federation
dale053 Jun 9, 2026
5294419
chore(ui): sync openapi.json with latest api spec
dale053 Jun 9, 2026
30ea696
Merge branch 'main' into feat/api-cross-repo-queue-health-federation
dale053 Jun 10, 2026
2ea4d7a
fix(mcp): enforce operator role for queue health federation across al…
dale053 Jun 10, 2026
772d567
fix(mcp): allow static identity access to queue health federation
dale053 Jun 10, 2026
3777364
Merge branch 'main' into feat/api-cross-repo-queue-health-federation
dale053 Jun 10, 2026
5d1f71f
test(mcp): cover operator role enforcement for queue health federation
dale053 Jun 10, 2026
6a656ba
build(api): regenerate ui openapi spec
dale053 Jun 10, 2026
d8da829
fix(mcp): block static api identity from queue health federation
dale053 Jun 10, 2026
a8e1816
fix(mcp): enforce operator role check for all identity kinds in queue…
dale053 Jun 10, 2026
9d62359
test(mcp): fix queue health federation output schema test to use oper…
dale053 Jun 10, 2026
a760ad6
Merge branch 'main' into feat/api-cross-repo-queue-health-federation
JSONbored Jun 12, 2026
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
508 changes: 318 additions & 190 deletions apps/gittensory-ui/public/openapi.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions migrations/0024_queue_federation_cache.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Cache table for the federated queue pressure index.
-- TTL enforcement matches the burden forecast pattern (6-hour freshness threshold applied at read time).
CREATE TABLE IF NOT EXISTS queue_federation_snapshots (
id TEXT PRIMARY KEY,
generated_at TEXT NOT NULL,
repo_count INTEGER NOT NULL DEFAULT 0,
payload_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS queue_federation_snapshots_generated_idx ON queue_federation_snapshots (generated_at);
45 changes: 15 additions & 30 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import {
MINIMUM_SUPPORTED_MCP_VERSION,
} from "../services/mcp-compatibility";
import { buildOperatorDashboardPayload } from "../services/operator-dashboard";
import { buildFederatedQueueIndex, FEDERATED_QUEUE_INDEX_MAX_LIMIT } from "../services/queue-federation";
import { buildSelfDogfoodRegistrationPack, resolveSelfDogfoodRepoFullName } from "../services/self-dogfood-registration-pack";
import {
buildWeeklyValueReport,
Expand Down Expand Up @@ -204,7 +205,6 @@ import { fileUpstreamDriftIssues, loadUpstreamStatus, refreshUpstreamDrift, regi
import type {
BountyLifecycleEventRecord,
ControlPanelRoleName,
ContributorEvidenceRecord,
DataQuality,
InstallationHealthRecord,
JobMessage,
Expand Down Expand Up @@ -992,6 +992,20 @@ export function createApp() {
return c.json(await buildOperatorDashboardPayload(c.env));
});

app.get("/v1/app/queue-health/federation", async (c) => {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
const rawLimit = c.req.query("limit");
if (rawLimit !== undefined) {
const parsed = Number(rawLimit);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > FEDERATED_QUEUE_INDEX_MAX_LIMIT) {
return c.json({ error: "invalid_limit", message: `limit must be an integer between 1 and ${FEDERATED_QUEUE_INDEX_MAX_LIMIT}` }, 422);
}
}
const limit = rawLimit !== undefined ? Number(rawLimit) : undefined;
return c.json(await buildFederatedQueueIndex(c.env, limit));
});

app.get("/v1/app/notification-model", async (c) => {
const forbidden = await requireAppRole(c, ["maintainer", "owner", "operator"]);
if (forbidden) return forbidden;
Expand Down Expand Up @@ -3566,35 +3580,6 @@ async function persistSignal(
});
}

function contributorEvidenceFromProfile(profile: {
login: string;
generatedAt: string;
evidence: {
registeredRepoPullRequests: number;
mergedPullRequests: number;
openPullRequests: number;
stalePullRequests: number;
unlinkedPullRequests: number;
issueDiscoveryReports: number;
languageMatches: number;
credibilityAssumption: number;
};
}): ContributorEvidenceRecord {
return {
login: profile.login,
generatedAt: profile.generatedAt,
payload: {
pullRequests: profile.evidence.registeredRepoPullRequests,
mergedPullRequests: profile.evidence.mergedPullRequests,
openPullRequests: profile.evidence.openPullRequests,
stalePullRequests: profile.evidence.stalePullRequests,
unlinkedPullRequests: profile.evidence.unlinkedPullRequests,
issueDiscoveryReports: profile.evidence.issueDiscoveryReports,
languageMatches: profile.evidence.languageMatches,
credibilityAssumption: profile.evidence.credibilityAssumption,
},
};
}

const EXTENSION_PULL_CONTEXT_PATH = "/v1/extension/pull-context";
const EXTENSION_PULL_CONTEXT_SCOPE = "extension:pull_context";
Expand Down
28 changes: 28 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
repositories,
repoGithubTotalsSnapshots,
repoQueueTrendSnapshots,
queueFederationSnapshots,
registryDriftEvents,
repoLabels,
repoSnapshots,
Expand Down Expand Up @@ -75,6 +76,7 @@ import type {
BountyLifecycleEventRecord,
BountyRecord,
BurdenForecastRecord,
QueueFederationSnapshotRecord,
CheckSummaryRecord,
CollisionEdgeRecord,
CommandUsefulnessSummary,
Expand Down Expand Up @@ -1897,6 +1899,32 @@ export async function getBurdenForecast(env: Env, repoFullName: string): Promise
};
}

const QUEUE_FEDERATION_SNAPSHOT_ID = "current";

export async function upsertQueueFederationSnapshot(env: Env, snapshot: QueueFederationSnapshotRecord): Promise<void> {
const db = getDb(env.DB);
await db
.insert(queueFederationSnapshots)
.values({ id: QUEUE_FEDERATION_SNAPSHOT_ID, generatedAt: snapshot.generatedAt, repoCount: snapshot.repoCount, payloadJson: jsonString(snapshot.payload) })
.onConflictDoUpdate({
target: queueFederationSnapshots.id,
set: { generatedAt: snapshot.generatedAt, repoCount: snapshot.repoCount, payloadJson: jsonString(snapshot.payload) },
});
}

export async function getQueueFederationSnapshot(env: Env): Promise<QueueFederationSnapshotRecord | null> {
const db = getDb(env.DB);
const row = await db.select().from(queueFederationSnapshots).where(eq(queueFederationSnapshots.id, QUEUE_FEDERATION_SNAPSHOT_ID)).limit(1);
const first = row[0];
if (!first) return null;
return {
id: first.id,
generatedAt: first.generatedAt,
repoCount: first.repoCount,
payload: parseJson<Record<string, JsonValue>>(first.payloadJson, {}),
};
}

export async function persistRegistryDriftEvents(env: Env, events: RegistryDriftEventRecord[]): Promise<void> {
const db = getDb(env.DB);
for (const event of events) {
Expand Down
8 changes: 8 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,14 @@ export const repoQueueTrendSnapshots = sqliteTable("repo_queue_trend_snapshots",
generatedAt: text("generated_at").notNull().default("CURRENT_TIMESTAMP"),
});

export const queueFederationSnapshots = sqliteTable("queue_federation_snapshots", {
id: text("id").primaryKey(),
generatedAt: text("generated_at").notNull(),
repoCount: integer("repo_count").notNull().default(0),
payloadJson: text("payload_json").notNull().default("{}"),
createdAt: text("created_at").notNull().default("CURRENT_TIMESTAMP"),
});

export const registryDriftEvents = sqliteTable("registry_drift_events", {
id: text("id").primaryKey(),
repoFullName: text("repo_full_name").notNull(),
Expand Down
28 changes: 27 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../
import { buildPublicPrBodyDraft } from "../services/pr-body-draft";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { buildFederatedQueueIndex, FEDERATED_QUEUE_INDEX_MAX_LIMIT } from "../services/queue-federation";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
Expand Down Expand Up @@ -458,6 +459,15 @@ export class GittensoryMcp {
async (input) => this.toolResult(await this.getBurdenForecast(input)),
);

server.registerTool(
"gittensory_queue_health_federation",
{
description: "Return a ranked cross-repo queue pressure index showing the worst-burden registered repos. Operator-only.",
inputSchema: { limit: z.number().int().min(1).max(FEDERATED_QUEUE_INDEX_MAX_LIMIT).optional() },
},
async (input) => this.toolResult(await this.getQueueHealthFederation(input.limit)),
);

server.registerTool(
"gittensory_get_repo_outcome_patterns",
{
Expand Down Expand Up @@ -877,6 +887,22 @@ export class GittensoryMcp {
};
}

private async getQueueHealthFederation(limit?: number): Promise<ToolPayload> {
if (this.identity.kind !== "session") {
throw new Error("Forbidden: gittensory_queue_health_federation requires operator role.");
}
const summary = await loadControlPanelRoleSummary(this.env, this.identity.actor);
if (!summary.roles.includes("operator")) {
throw new Error("Forbidden: gittensory_queue_health_federation requires operator role.");
}
const index = await buildFederatedQueueIndex(this.env, limit);
const criticalCount = index.entries.filter((entry) => entry.level === "critical" || entry.level === "high").length;
return {
summary: `Cross-repo queue pressure index: ${index.repoCount} repo(s) ranked, ${criticalCount} at critical/high burden.`,
data: index as unknown as Record<string, unknown>,
};
}

private async getIssueQuality(input: { owner: string; repo: string }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
if (!(await this.canAccessRepo(fullName))) {
Expand Down Expand Up @@ -1204,7 +1230,7 @@ export class GittensoryMcp {
const result = await extra.sendRequest({ method: "elicitation/create", params: request }, ElicitResultSchema, { timeout: 1000 });
const choices = planningChoicesFromElicitationResult(result);
return { supported: true, requested: true, accepted: result.action === "accept", choices };
} catch {
} catch /* v8 ignore next -- sendRequest throw requires live MCP transport, not testable in-process */ {
return { supported: true, requested: true, accepted: false, choices: {} };
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,29 @@ export const CollisionReportSchema = z
})
.openapi("CollisionReport");

export const FederatedRepoEntrySchema = z
.object({
repoFullName: z.string(),
burdenScore: z.number(),
level: z.enum(["low", "medium", "high", "critical"]),
compositeScore: z.number(),
stalePullRequestRate: z.number().nullable(),
pullRequestGrowth7d: z.number().nullable(),
freshness: z.enum(["fresh", "stale"]),
summary: z.string(),
})
.openapi("FederatedRepoEntry");

export const FederatedQueueIndexSchema = z
.object({
generatedAt: z.string(),
repoCount: z.number(),
limitApplied: z.number(),
source: z.enum(["snapshot", "computed"]),
entries: z.array(FederatedRepoEntrySchema),
})
.openapi("FederatedQueueIndex");

export const QueueHealthSchema = z
.object({
repoFullName: z.string(),
Expand Down
15 changes: 15 additions & 0 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
BountyLifecycleEventsSchema,
BountySchema,
BurdenForecastSchema,
FederatedQueueIndexSchema,
FederatedRepoEntrySchema,
CollisionReportSchema,
ConfigQualitySchema,
CommandPreviewResponseSchema,
Expand Down Expand Up @@ -145,6 +147,8 @@ export function buildOpenApiSpec() {
registry.register("IssueQualityReport", IssueQualityReportSchema);
registry.register("IssueQualityResponse", IssueQualityResponseSchema);
registry.register("BurdenForecast", BurdenForecastSchema);
registry.register("FederatedRepoEntry", FederatedRepoEntrySchema);
registry.register("FederatedQueueIndex", FederatedQueueIndexSchema);
registry.register("ContributorScoringProfile", ContributorScoringProfileSchema);
registry.register("ContributorStrategy", ContributorStrategySchema);
registry.register("RewardRiskAction", RewardRiskActionSchema);
Expand Down Expand Up @@ -668,6 +672,17 @@ export function buildOpenApiSpec() {
401: { description: "Unauthorized" },
},
});
registry.registerPath({
method: "get",
path: "/v1/app/queue-health/federation",
request: { query: z.object({ limit: z.string().optional() }) },
responses: {
200: { description: "Ranked cross-repo queue pressure index (operator only)", content: { "application/json": { schema: FederatedQueueIndexSchema } } },
401: { description: "Unauthorized" },
403: { description: "Insufficient role — operator access required" },
422: { description: "Invalid limit parameter" },
},
});
for (const path of [
"/v1/app/roles",
"/v1/app/miner-dashboard",
Expand Down
8 changes: 8 additions & 0 deletions src/services/operator-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { loadUpstreamStatus, type UpstreamStatus } from "../upstream/ruleset";
import { nowIso } from "../utils/json";
import { buildRecommendationQualityReport, type RecommendationQualityReport } from "./recommendation-quality-report";
import { buildWeeklyValueReport } from "./weekly-value-report";
import { buildFederatedQueueIndex, type FederatedQueueIndex } from "./queue-federation";

export type OperatorDashboardMetric = {
label: string;
Expand Down Expand Up @@ -57,6 +58,7 @@ export type OperatorDashboardPayload = {
registry: RegistrySnapshot | null;
scoringModel: ScoringModelSnapshotRecord | null;
upstreamDrift: UpstreamStatus;
queueFederation: FederatedQueueIndex;
};

const USAGE_WINDOW_DAYS = 7;
Expand Down Expand Up @@ -96,6 +98,10 @@ export async function buildOperatorDashboardPayload(env: Env): Promise<OperatorD
getCommandUsefulnessSummary(env),
buildRecommendationQualityReport(env, { windowDays: 90 }),
]);
const queueFederation = await buildFederatedQueueIndex(env);
const topCriticalRepos = queueFederation.entries.filter(
(entry) => entry.level === "critical" || entry.level === "high",
).length;
const weeklyValueReport = buildWeeklyValueReport({
generatedAt: nowIso(),
variant: "operator",
Expand All @@ -111,6 +117,7 @@ export async function buildOperatorDashboardPayload(env: Env): Promise<OperatorD
usageRollupStatus,
activeSessions,
digestSubscriptions,
topCriticalRepos,
});
const installedRepos = repositories.filter((repo: RepositoryRecord) => repo.isInstalled).length;
const registeredRepos = repositories.filter((repo: RepositoryRecord) => repo.isRegistered).length;
Expand Down Expand Up @@ -177,6 +184,7 @@ export async function buildOperatorDashboardPayload(env: Env): Promise<OperatorD
registry,
scoringModel: scoring,
upstreamDrift,
queueFederation,
};
}

Expand Down
Loading
Loading