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
33 changes: 33 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14027,6 +14027,39 @@
}
]
}
},
"/v1/repos/{owner}/{repo}/onboarding-pack/preview": {
"get": {
"responses": {
"200": {
"description": "Preview-only repo onboarding pack for accepted repositories",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"403": {
"description": "Insufficient role"
},
"404": {
"description": "Repository is not accepted or preview unavailable"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
}
},
"servers": [
Expand Down
14 changes: 14 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ import { buildPullRequestReviewability, type PullRequestReviewability } from "..
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoOnboardingPackPreviewForRepo } from "../services/repo-onboarding-pack";
import { buildRepoSettingsPreview, type PublicSurfaceSkipReason } from "../signals/settings-preview";
import { buildGittensorConfigRecommendation, buildRegistrationReadiness, type InstallationHealthSummary } from "../signals/registration-readiness";
import { fileUpstreamDriftIssues, loadUpstreamStatus, refreshUpstreamDrift, registryHyperparameterDriftWarningsForRepo } from "../upstream/ruleset";
Expand Down Expand Up @@ -1479,6 +1480,19 @@ export function createApp() {
return c.json(await buildGittensorConfigRecommendationResponse(c.env, fullName));
});

app.get("/v1/repos/:owner/:repo/onboarding-pack/preview", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
const forbidden = await requireAppRole(c, ["maintainer", "owner", "operator"]);
if (forbidden) return forbidden;
const response = await buildRepoOnboardingPackPreviewForRepo(c.env, fullName, {
refreshManifest: c.req.query("refresh") === "true",
});
if ("error" in response) {
return c.json(response, 404);
}
return c.json(response);
});

app.get("/v1/repos/:owner/:repo/settings", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
return c.json(await getRepositorySettings(c.env, fullName));
Expand Down
9 changes: 9 additions & 0 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,15 @@ export function buildOpenApiSpec() {
200: { description: "Private Gittensor config recommendation for repo owners", content: { "application/json": { schema: GittensorConfigRecommendationSchema } } },
},
});
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/onboarding-pack/preview",
responses: {
200: { description: "Preview-only repo onboarding pack for accepted repositories", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
403: { description: "Insufficient role" },
404: { description: "Repository is not accepted or preview unavailable" },
},
});
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/settings",
Expand Down
52 changes: 52 additions & 0 deletions src/services/repo-onboarding-pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { getRepository } from "../db/repositories";
import type { FocusManifest } from "../signals/focus-manifest";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import {
buildRepoOnboardingPackPreview,
type RepoOnboardingPackPreview,
type RepoPolicyCompilerOutput,
} from "../signals/onboarding-pack";
import { compileRepoPolicyCompilerOutput } from "../signals/repo-policy-compiler";

export type RepoOnboardingPackPreviewResponse = {
repoFullName: string;
accepted: boolean;
preview: RepoOnboardingPackPreview;
policySource: "policy_compiler";
};

export function buildRepoOnboardingPackPreviewFromManifest(
repoFullName: string,
manifest: FocusManifest,
): { preview: RepoOnboardingPackPreview; policyOutput: RepoPolicyCompilerOutput } {
const policyOutput = compileRepoPolicyCompilerOutput({ repoFullName, manifest });
const preview = buildRepoOnboardingPackPreview(policyOutput);
return { preview, policyOutput };
}

/**
* Build a sanitized onboarding-pack preview for an accepted (registered) repository.
*/
export async function buildRepoOnboardingPackPreviewForRepo(
env: Env,
repoFullName: string,
options: { refreshManifest?: boolean } = {},
): Promise<RepoOnboardingPackPreviewResponse | { error: string; repoFullName: string }> {
const repo = await getRepository(env, repoFullName);
if (!repo?.isRegistered) {
return {
error: "repo_not_accepted",
repoFullName,
};
}

const manifest = await loadRepoFocusManifest(env, repoFullName, { refresh: options.refreshManifest === true });
const { preview } = buildRepoOnboardingPackPreviewFromManifest(repoFullName, manifest);

return {
repoFullName,
accepted: true,
preview,
policySource: "policy_compiler",
};
}
113 changes: 113 additions & 0 deletions src/signals/repo-policy-compiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
compileFocusManifestPolicy,
isFocusManifestPublicSafe,
type FocusManifest,
type FocusManifestLanePreference,
} from "./focus-manifest";
import type { RepoPolicyCompilerOutput, RepoPolicyContributionLane } from "./onboarding-pack";
import { nowIso } from "../utils/json";

export type RepoPolicyCompilerInput = {
repoFullName: string;
manifest: FocusManifest;
generatedAt?: string | undefined;
};

/**
* Compile a normalized focus manifest into policy output consumed by onboarding-pack generation (#277 → #248).
*/
export function compileRepoPolicyCompilerOutput(input: RepoPolicyCompilerInput): RepoPolicyCompilerOutput {
const policy = compileFocusManifestPolicy(input.manifest);
const contributionLanes: RepoPolicyContributionLane[] = [];

if (policy.present) {
contributionLanes.push(buildDirectPrLane(policy.publicSafe.contributionLanes.directPrLane, policy));
contributionLanes.push(buildIssueDiscoveryLane(policy.publicSafe.contributionLanes.issueDiscoveryLane, policy));
}

const publicReadinessWarnings = policy.authenticated.readinessWarnings.filter(isFocusManifestPublicSafe);
const publicParseWarnings = policy.authenticated.parseWarnings.filter(isFocusManifestPublicSafe);

return {
repoFullName: input.repoFullName,
generatedAt: input.generatedAt ?? nowIso(),
contributionLanes,
labelPolicy: {
preferredLabels: policy.publicSafe.labelExpectations.preferredLabels,
requiredLabels: [],
discouragedLabels: [],
note: labelPolicyNote(policy.publicSafe.labelExpectations.linkedIssuePolicy),
},
validationExpectations: policy.publicSafe.validationExpectations.testExpectations,
readinessWarnings: [
...publicReadinessWarnings,
...publicParseWarnings,
"Confirm contribution guidance stays previewable before publication.",
"Keep public material separated from maintainer-only context.",
].filter(isFocusManifestPublicSafe),
maintainerExpectations: [
"Keep pull requests narrow and tied to accepted repository policy.",
"Shape PR descriptions around maintainer public notes and validation expectations.",
],
publicOutputBoundaries: [
"Keep sensitive credentials, account secrets, compensation estimates, private maintainer evidence, and local paths out of public contribution text.",
"Keep the pack as guidance for accepted work, not as automated GitHub action.",
...input.manifest.publicNotes.filter(isFocusManifestPublicSafe),
],
privateOwnerContext: policy.authenticated.maintainerContext,
};
}

function buildDirectPrLane(
preference: FocusManifestLanePreference,
policy: ReturnType<typeof compileFocusManifestPolicy>,
): RepoPolicyContributionLane {
return {
id: "direct-pr",
title: laneTitle("Direct pull request lane", preference),
summary: directPrSummary(preference, policy.publicSafe.summary),
preferredPaths: policy.publicSafe.contributionLanes.preferredEntryPaths,
discouragedPaths: policy.publicSafe.discouragedWork.blockedEntryPaths,
validationExpectations: policy.publicSafe.validationExpectations.testExpectations,
publicNotes: policy.publicSafe.entryGuidance,
};
}

function buildIssueDiscoveryLane(
preference: FocusManifestLanePreference,
policy: ReturnType<typeof compileFocusManifestPolicy>,
): RepoPolicyContributionLane {
return {
id: "issue-discovery",
title: laneTitle("Issue discovery lane", preference),
summary: issueDiscoverySummary(preference, policy.publicSafe.summary),
preferredPaths: policy.publicSafe.contributionLanes.preferredEntryPaths,
discouragedPaths: policy.publicSafe.discouragedWork.blockedEntryPaths,
validationExpectations: policy.publicSafe.validationExpectations.testExpectations,
publicNotes: policy.publicSafe.entryGuidance.filter((note) => !note.toLowerCase().includes("direct")),
};
}

function laneTitle(base: string, preference: FocusManifestLanePreference): string {
if (preference === "preferred") return `${base} (preferred)`;
if (preference === "discouraged") return `${base} (discouraged)`;
return base;
}

function directPrSummary(preference: FocusManifestLanePreference, summary: string): string {
if (preference === "discouraged") return "Direct pull requests are discouraged for this repository.";
if (preference === "preferred") return summary;
return "Direct pull requests are accepted when they stay inside maintainer-wanted scope.";
}

function issueDiscoverySummary(preference: FocusManifestLanePreference, summary: string): string {
if (preference === "discouraged") return "Prefer direct fixes over new issue reports.";
if (preference === "preferred") return summary;
return "Issue discovery is optional; confirm maintainer scope before filing new issues.";
}

function labelPolicyNote(linkedIssuePolicy: string): string {
if (linkedIssuePolicy === "required") return "Link a tracked issue before opening a pull request.";
if (linkedIssuePolicy === "preferred") return "Link a tracked issue when one exists.";
return "Use labels to explain accepted scope, not to promise outcomes.";
}
60 changes: 60 additions & 0 deletions test/unit/onboarding-pack.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { describe, expect, it } from "vitest";
import {
buildRepoOnboardingPackPreviewForRepo,
buildRepoOnboardingPackPreviewFromManifest,
} from "../../src/services/repo-onboarding-pack";
import { createTestEnv } from "../helpers/d1";
import { upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { parseFocusManifestContent } from "../../src/signals/focus-manifest";
import { compileRepoPolicyCompilerOutput } from "../../src/signals/repo-policy-compiler";
import {
buildRepoOnboardingPackPreview,
isRepoOnboardingPackPublicSafe,
Expand Down Expand Up @@ -200,6 +208,26 @@ describe("buildRepoOnboardingPackPreview", () => {
expect(isRepoOnboardingPackPublicSafe(preview)).toBe(true);
});

it("compiler fixture pipeline matches buildRepoOnboardingPackPreviewFromManifest", () => {
const manifest = parseFocusManifestContent(
JSON.stringify({
wantedPaths: ["src/"],
testExpectations: ["npm run test:ci"],
publicNotes: ["Stay advisory."],
}),
"repo_file",
);
const compiled = compileRepoPolicyCompilerOutput({
repoFullName: "JSONbored/gittensory",
manifest,
});
const fromCompiler = buildRepoOnboardingPackPreview(compiled);
const fromService = buildRepoOnboardingPackPreviewFromManifest("JSONbored/gittensory", manifest);
expect(fromService.preview.contributionLanes.length).toBe(fromCompiler.contributionLanes.length);
expect(fromService.preview.publication.status).toBe("preview_only");
expect(isRepoOnboardingPackPublicSafe(fromService.preview)).toBe(true);
});

it("uses stable defaults when optional policy sections are omitted", () => {
const preview = buildRepoOnboardingPackPreview(
{
Expand Down Expand Up @@ -235,3 +263,35 @@ describe("buildRepoOnboardingPackPreview", () => {
expect(isRepoOnboardingPackPublicSafe(preview)).toBe(true);
});
});

describe("buildRepoOnboardingPackPreviewForRepo", () => {
it("returns preview_only pack for accepted registered repos", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(
env,
{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } },
1,
);
await env.DB.prepare("UPDATE repositories SET is_registered = 1 WHERE full_name = ?")
.bind("JSONbored/gittensory")
.run();
const response = await buildRepoOnboardingPackPreviewForRepo(env, "JSONbored/gittensory");
expect("error" in response).toBe(false);
if ("error" in response) return;
expect(response.accepted).toBe(true);
expect(response.preview.previewOnly).toBe(true);
expect(response.preview.publication.allowed).toBe(false);
expect(isRepoOnboardingPackPublicSafe(response.preview)).toBe(true);
});

it("rejects onboarding pack preview for unregistered repos", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(
env,
{ name: "unregistered", full_name: "owner/unregistered", private: false, owner: { login: "owner" } },
1,
);
const response = await buildRepoOnboardingPackPreviewForRepo(env, "owner/unregistered");
expect(response).toMatchObject({ error: "repo_not_accepted", repoFullName: "owner/unregistered" });
});
});
Loading