diff --git a/apps/gittensory-ui/src/components/site/check-run-readiness-model.ts b/apps/gittensory-ui/src/components/site/check-run-readiness-model.ts
new file mode 100644
index 0000000000..f1a6cf74fb
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/check-run-readiness-model.ts
@@ -0,0 +1,49 @@
+// Check-run details-page readiness table model (#2216). Pure helpers + types for the Context check
+// details slice — mirrors the public-safe band shape from buildExtensionPrStatus (src/signals/
+// extension-contributor-context.ts) so the UI never renders raw readiness scores.
+
+export type CheckRunDetailLevel = "minimal" | "standard" | "deep";
+
+export type ReadinessComponentBand = "met" | "partial" | "unmet";
+
+export type ContributorReadinessBand = "strong" | "developing" | "early";
+
+export type CheckRunReadinessRow = {
+ key: string;
+ label: string;
+ band: ReadinessComponentBand;
+ evidence: string;
+ action: string;
+};
+
+export type CheckRunReadinessTableData = {
+ readinessBand: ContributorReadinessBand;
+ components: CheckRunReadinessRow[];
+};
+
+/** Context check details publish the readiness table only at standard/deep detail levels. */
+export function shouldShowCheckRunReadinessTable(detailLevel: CheckRunDetailLevel): boolean {
+ return detailLevel !== "minimal";
+}
+
+/** Gate the table on detail level AND the presence of readiness rows (empty set → hide). */
+export function resolveCheckRunReadinessView(args: {
+ detailLevel: CheckRunDetailLevel | null | undefined;
+ readiness: CheckRunReadinessTableData | null | undefined;
+}): CheckRunReadinessTableData | null {
+ if (!args.detailLevel || !shouldShowCheckRunReadinessTable(args.detailLevel)) return null;
+ if (!args.readiness || args.readiness.components.length === 0) return null;
+ return args.readiness;
+}
+
+export const READINESS_BAND_LABEL: Record
= {
+ strong: "Strong",
+ developing: "Developing",
+ early: "Early",
+};
+
+export const COMPONENT_BAND_LABEL: Record = {
+ met: "Met",
+ partial: "Partial",
+ unmet: "Unmet",
+};
diff --git a/apps/gittensory-ui/src/components/site/check-run-readiness-table.test.tsx b/apps/gittensory-ui/src/components/site/check-run-readiness-table.test.tsx
new file mode 100644
index 0000000000..5335f1dc10
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/check-run-readiness-table.test.tsx
@@ -0,0 +1,74 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { CheckRunReadinessTable } from "@/components/site/check-run-readiness-table";
+import {
+ resolveCheckRunReadinessView,
+ shouldShowCheckRunReadinessTable,
+ type CheckRunReadinessTableData,
+} from "@/components/site/check-run-readiness-model";
+
+const SAMPLE: CheckRunReadinessTableData = {
+ readinessBand: "developing",
+ components: [
+ {
+ key: "validation",
+ label: "Validation posture",
+ band: "partial",
+ evidence: "Test plan noted but not verified.",
+ action: "Run the documented test plan.",
+ },
+ ],
+};
+
+describe("shouldShowCheckRunReadinessTable", () => {
+ it("shows at standard and deep, hides at minimal", () => {
+ expect(shouldShowCheckRunReadinessTable("minimal")).toBe(false);
+ expect(shouldShowCheckRunReadinessTable("standard")).toBe(true);
+ expect(shouldShowCheckRunReadinessTable("deep")).toBe(true);
+ });
+});
+
+describe("resolveCheckRunReadinessView", () => {
+ it("returns null below standard detail level even when readiness is present", () => {
+ expect(resolveCheckRunReadinessView({ detailLevel: "minimal", readiness: SAMPLE })).toBeNull();
+ });
+
+ it("returns null for an empty readiness component set at standard (both gate-off and gate-on shapes)", () => {
+ const empty: CheckRunReadinessTableData = { readinessBand: "early", components: [] };
+ expect(resolveCheckRunReadinessView({ detailLevel: "standard", readiness: empty })).toBeNull();
+ expect(resolveCheckRunReadinessView({ detailLevel: "deep", readiness: empty })).toBeNull();
+ });
+
+ it("returns the readiness payload at standard when components are present", () => {
+ expect(resolveCheckRunReadinessView({ detailLevel: "standard", readiness: SAMPLE })).toEqual(
+ SAMPLE,
+ );
+ });
+});
+
+describe("CheckRunReadinessTable", () => {
+ it("renders the table at standard detail level", () => {
+ render();
+ expect(screen.getByText("Context check readiness")).toBeTruthy();
+ expect(screen.getByText("Validation posture")).toBeTruthy();
+ expect(screen.getByText("Test plan noted but not verified.")).toBeTruthy();
+ expect(screen.getByText("Developing")).toBeTruthy();
+ expect(screen.getByText("Partial")).toBeTruthy();
+ });
+
+ it("hides the table at minimal detail level", () => {
+ render();
+ expect(screen.queryByText("Context check readiness")).toBeNull();
+ });
+
+ it("hides the table when the readiness component set is empty", () => {
+ render(
+ ,
+ );
+ expect(screen.queryByText("Context check readiness")).toBeNull();
+ });
+});
diff --git a/apps/gittensory-ui/src/components/site/check-run-readiness-table.tsx b/apps/gittensory-ui/src/components/site/check-run-readiness-table.tsx
new file mode 100644
index 0000000000..b8f0cfcc93
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/check-run-readiness-table.tsx
@@ -0,0 +1,90 @@
+import { StatusPill, type Status } from "@/components/site/control-primitives";
+import {
+ COMPONENT_BAND_LABEL,
+ READINESS_BAND_LABEL,
+ resolveCheckRunReadinessView,
+ type CheckRunDetailLevel,
+ type CheckRunReadinessTableData,
+ type ContributorReadinessBand,
+ type ReadinessComponentBand,
+} from "@/components/site/check-run-readiness-model";
+import { cn } from "@/lib/utils";
+
+const READINESS_BAND_TONE: Record = {
+ strong: "ready",
+ developing: "warn",
+ early: "info",
+};
+
+const COMPONENT_BAND_TONE: Record = {
+ met: "ready",
+ partial: "warn",
+ unmet: "blocked",
+};
+
+/**
+ * Scannable readiness table for the Context check details page (#2216). Consumes the public-safe
+ * band payload from settings-preview (`checkRunReadiness`); hidden below `standard` detail level.
+ */
+export function CheckRunReadinessTable({
+ detailLevel,
+ readiness,
+ className,
+}: {
+ detailLevel: CheckRunDetailLevel | null | undefined;
+ readiness: CheckRunReadinessTableData | null | undefined;
+ className?: string;
+}) {
+ const view = resolveCheckRunReadinessView({ detailLevel, readiness });
+ if (!view) return null;
+
+ return (
+
+
+
+
+ Context check readiness
+
+
+ Public-safe bands from the same readiness rubric as the PR panel — no raw scores.
+
+
+
+ {READINESS_BAND_LABEL[view.readinessBand]}
+
+
+
+
+
+
+
+ | Signal |
+ Band |
+ Evidence |
+ Action |
+
+
+
+ {view.components.map((row) => (
+
+ | {row.label} |
+
+
+ {COMPONENT_BAND_LABEL[row.band]}
+
+ |
+ {row.evidence} |
+
+ {row.action}
+ |
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/gittensory-ui/src/routes/app.index.tsx b/apps/gittensory-ui/src/routes/app.index.tsx
index f903c91bf9..5f70627b4c 100644
--- a/apps/gittensory-ui/src/routes/app.index.tsx
+++ b/apps/gittensory-ui/src/routes/app.index.tsx
@@ -1,4 +1,5 @@
-import { createFileRoute, Link } from "@tanstack/react-router";
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
import {
ArrowRight,
Activity,
@@ -103,6 +104,20 @@ function AppOverview() {
const { session } = useSession();
const { status, connection } = useApiStatus();
const overview = useApiResource("/v1/app/overview", "App overview");
+ const navigate = useNavigate();
+
+ // Context check details_url uses /app?view=maintainer&repo=… — route to the maintainer console (#2216).
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ if (params.get("view") !== "maintainer") return;
+ const repo = params.get("repo")?.trim();
+ void navigate({
+ to: "/app/maintainer",
+ search: repo ? { repo } : {},
+ replace: true,
+ });
+ }, [navigate]);
+
if (!session) return null;
const live = connection === "online" && (status === "ok" || status === "degraded");
diff --git a/apps/gittensory-ui/src/routes/app.maintainer.tsx b/apps/gittensory-ui/src/routes/app.maintainer.tsx
index bda0d717ee..72c4725650 100644
--- a/apps/gittensory-ui/src/routes/app.maintainer.tsx
+++ b/apps/gittensory-ui/src/routes/app.maintainer.tsx
@@ -1,13 +1,21 @@
import { createFileRoute } from "@tanstack/react-router";
+import { z } from "zod";
import { MaintainerPanel } from "@/components/site/app-panels/maintainer-panel";
import { PageHeader } from "@/components/site/primitives";
+const searchSchema = z.object({
+ repo: z.string().optional(),
+});
+
export const Route = createFileRoute("/app/maintainer")({
+ validateSearch: (search) => searchSchema.parse(search),
component: MaintainerRoute,
});
function MaintainerRoute() {
+ const { repo } = Route.useSearch();
+
return (
);
}
diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts
index 48e4a4c849..6ffda995ec 100644
--- a/src/openapi/schemas.ts
+++ b/src/openapi/schemas.ts
@@ -933,6 +933,20 @@ export const RepoSettingsPreviewSchema = z
detailLevel: z.enum(["minimal", "standard", "deep"]),
})
.nullable(),
+ checkRunReadiness: z
+ .object({
+ readinessBand: z.enum(["strong", "developing", "early"]),
+ components: z.array(
+ z.object({
+ key: z.enum(["traceability", "related_work", "change_scope", "validation", "pr_state", "queue_pressure"]),
+ label: z.string(),
+ band: z.enum(["met", "partial", "unmet"]),
+ evidence: z.string(),
+ action: z.string(),
+ }),
+ ),
+ })
+ .nullable(),
installPreview: z.object({
status: z.enum(["ready", "needs_attention", "blocked"]),
summary: z.string(),
diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts
index cad577bf06..cd32be3b8b 100644
--- a/src/signals/settings-preview.ts
+++ b/src/signals/settings-preview.ts
@@ -10,9 +10,11 @@ import {
buildContributorProfile,
buildPreflightResult,
buildPublicPrIntelligenceComment,
+ buildPublicReadinessScore,
buildQueueHealth,
type ContributorDetection,
} from "./engine";
+import { buildExtensionPrStatus, type ExtensionPrStatus } from "./extension-contributor-context";
import { REQUIRED_INSTALLATION_PERMISSIONS } from "../github/backfill";
import type { GittensoryFooterEnv } from "../github/footer";
import { GITTENSORY_GATE_CHECK_NAME, shouldPublishReviewCheck } from "../review/check-names";
@@ -243,6 +245,9 @@ export type RepoSettingsPreview = {
previewComment: string | null;
appliedLabel: string | null;
checkRun: { willCreate: boolean; title: string; detailLevel: RepositorySettings["checkRunDetailLevel"] } | null;
+ /** Public-safe readiness bands for the Context check details page (#2216). Null when check runs are off,
+ * detail level is minimal, or the sample would not publish a check run. */
+ checkRunReadiness: Pick | null;
installPreview: RepoInstallPreview;
warnings: string[];
summary: string;
@@ -359,6 +364,16 @@ export function buildRepoSettingsPreview(args: {
previewComment,
appliedLabel: decision.willLabel ? settings.gittensorLabel : null,
checkRun: decision.willCheckRun ? { willCreate: true, title: "Gittensory Context", detailLevel: settings.checkRunDetailLevel } : null,
+ checkRunReadiness: buildSampleCheckRunReadiness({
+ repoFullName,
+ repo,
+ settings,
+ issues: args.issues,
+ pullRequests: args.pullRequests,
+ sample,
+ body: args.sample.body ?? null,
+ decision,
+ }),
installPreview,
warnings,
summary: decision.skipped
@@ -586,6 +601,50 @@ function publicOutputSummary(decision: PublicSurfaceDecision): string {
return decision.actions.includes("none") ? "The sample qualifies, but no public output action is enabled." : `Current sample would create: ${decision.actions.join(", ")}.`;
}
+/** Build the public-safe readiness table payload for the Context check details page (#2216). */
+export function buildSampleCheckRunReadiness(args: {
+ repoFullName: string;
+ repo: RepositoryRecord | null;
+ settings: RepositorySettings;
+ issues: IssueRecord[];
+ pullRequests: PullRequestRecord[];
+ sample: { authorLogin: string; authorAssociation: string; minerStatus: "confirmed" | "not_found" | "unavailable"; title: string; labels: string[]; linkedIssues: number[] };
+ body: string | null;
+ decision: PublicSurfaceDecision;
+}): Pick | null {
+ if (!args.decision.willCheckRun || args.settings.checkRunDetailLevel === "minimal") return null;
+ const samplePr: PullRequestRecord = {
+ repoFullName: args.repoFullName,
+ number: 0,
+ title: args.sample.title,
+ state: "open",
+ authorLogin: args.sample.authorLogin,
+ authorAssociation: args.sample.authorAssociation,
+ labels: args.sample.labels,
+ linkedIssues: args.sample.linkedIssues,
+ body: args.body,
+ };
+ const collisions = buildCollisionReport(args.repoFullName, args.issues, args.pullRequests);
+ const queueHealth = buildQueueHealth(args.repo, args.issues, args.pullRequests, collisions);
+ const preflight = buildPreflightResult(
+ {
+ repoFullName: args.repoFullName,
+ contributorLogin: args.sample.authorLogin,
+ title: args.sample.title,
+ body: args.body ?? undefined,
+ labels: args.sample.labels,
+ linkedIssues: args.sample.linkedIssues,
+ authorAssociation: args.sample.authorAssociation,
+ },
+ args.repo,
+ args.issues,
+ args.pullRequests,
+ );
+ const readiness = buildPublicReadinessScore({ pr: samplePr, preflight, queueHealth });
+ const status = buildExtensionPrStatus({ repoFullName: args.repoFullName, pullNumber: 0, readiness });
+ return { readinessBand: status.readinessBand, components: status.components };
+}
+
function buildSamplePreviewComment(args: {
repoFullName: string;
repo: RepositoryRecord | null;
diff --git a/test/unit/settings-preview.test.ts b/test/unit/settings-preview.test.ts
index 4e14fd177d..f855466346 100644
--- a/test/unit/settings-preview.test.ts
+++ b/test/unit/settings-preview.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { buildRepoSettingsPreview, decidePublicSurface, type InstallationHealthSummary } from "../../src/signals/settings-preview";
+import { buildRepoSettingsPreview, buildSampleCheckRunReadiness, decidePublicSurface, type InstallationHealthSummary } from "../../src/signals/settings-preview";
import { REQUIRED_INSTALLATION_PERMISSIONS } from "../../src/github/backfill";
import { RepoSettingsPreviewSchema } from "../../src/openapi/schemas";
import type { IssueRecord, PullRequestRecord, RepositoryRecord, RepositorySettings } from "../../src/types";
@@ -254,6 +254,76 @@ describe("buildRepoSettingsPreview", () => {
expect(withoutChecks.warnings.some((warning) => /Checks: write/.test(warning))).toBe(false);
});
+ it("REGRESSION (#2216): omits checkRunReadiness at minimal detail level and includes public-safe bands at standard", () => {
+ const minimal = buildRepoSettingsPreview({env: {},
+ ...base,
+ settings: settings({ checkRunMode: "enabled", checkRunDetailLevel: "minimal" }),
+ installation: healthyInstall,
+ sample: { authorLogin: "miner", minerStatus: "confirmed" },
+ });
+ expect(minimal.checkRun).toMatchObject({ willCreate: true, detailLevel: "minimal" });
+ expect(minimal.checkRunReadiness).toBeNull();
+
+ const standard = buildRepoSettingsPreview({env: {},
+ ...base,
+ settings: settings({ checkRunMode: "enabled", checkRunDetailLevel: "standard" }),
+ installation: healthyInstall,
+ sample: { authorLogin: "miner", minerStatus: "confirmed" },
+ });
+ expect(standard.checkRunReadiness).toMatchObject({
+ readinessBand: expect.stringMatching(/^(strong|developing|early)$/),
+ components: expect.arrayContaining([
+ expect.objectContaining({
+ key: expect.any(String),
+ label: expect.any(String),
+ band: expect.stringMatching(/^(met|partial|unmet)$/),
+ evidence: expect.any(String),
+ action: expect.any(String),
+ }),
+ ]),
+ });
+ expect(JSON.stringify(standard.checkRunReadiness)).not.toMatch(FORBIDDEN_INSTALL_PREVIEW_PUBLIC_LANGUAGE);
+ });
+
+ it("buildSampleCheckRunReadiness returns null when the surface decision will not publish a check run", () => {
+ const skipped = buildSampleCheckRunReadiness({
+ repoFullName: repo.fullName,
+ repo,
+ settings: settings({ checkRunMode: "off", checkRunDetailLevel: "standard" }),
+ issues,
+ pullRequests,
+ sample: { authorLogin: "miner", authorAssociation: "CONTRIBUTOR", minerStatus: "confirmed", title: "Sample", labels: [], linkedIssues: [] },
+ body: null,
+ decision: decidePublicSurface({ settings: settings({ checkRunMode: "off" }), authorLogin: "miner", minerStatus: "confirmed" }),
+ });
+ expect(skipped).toBeNull();
+ });
+
+ it("buildSampleCheckRunReadiness stays available with gate enabled or disabled (both gate branches)", () => {
+ const gateOff = buildSampleCheckRunReadiness({
+ repoFullName: repo.fullName,
+ repo,
+ settings: settings({ checkRunMode: "enabled", checkRunDetailLevel: "standard", gateCheckMode: "off" }),
+ issues,
+ pullRequests,
+ sample: { authorLogin: "miner", authorAssociation: "CONTRIBUTOR", minerStatus: "confirmed", title: "Sample", labels: [], linkedIssues: [] },
+ body: null,
+ decision: decidePublicSurface({ settings: settings({ checkRunMode: "enabled" }), authorLogin: "miner", minerStatus: "confirmed" }),
+ });
+ const gateOn = buildSampleCheckRunReadiness({
+ repoFullName: repo.fullName,
+ repo,
+ settings: settings({ checkRunMode: "enabled", checkRunDetailLevel: "standard", gateCheckMode: "enabled" }),
+ issues,
+ pullRequests,
+ sample: { authorLogin: "miner", authorAssociation: "CONTRIBUTOR", minerStatus: "confirmed", title: "Sample", labels: [], linkedIssues: [] },
+ body: null,
+ decision: decidePublicSurface({ settings: settings({ checkRunMode: "enabled", gateCheckMode: "enabled" }), authorLogin: "miner", minerStatus: "confirmed" }),
+ });
+ expect(gateOff?.components.length).toBeGreaterThan(0);
+ expect(gateOn?.components.length).toBeGreaterThan(0);
+ });
+
it("requires Issues: write for detected-contributors comment mode even when previewing a non-confirmed sample", () => {
// detected_contributors_only + comment_only comments for confirmed miners, so the repo needs
// issues:write regardless of the previewed sample's miner status. Previewing a non-confirmed