diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 2697717d..48d413a5 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -3508,6 +3508,235 @@ "summary" ] }, + "onboardingPackPreview": { + "type": "object", + "nullable": true, + "properties": { + "repoFullName": { + "type": "string" + }, + "generatedAt": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "policy_compiler" + ] + }, + "previewOnly": { + "type": "boolean", + "enum": [ + true + ] + }, + "publicSafe": { + "type": "boolean", + "enum": [ + true + ] + }, + "contributionLanes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "preferredPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "discouragedPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "validationExpectations": { + "type": "array", + "items": { + "type": "string" + } + }, + "publicNotes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "title", + "summary", + "preferredPaths", + "discouragedPaths", + "validationExpectations", + "publicNotes" + ] + } + }, + "labelPolicy": { + "type": "object", + "properties": { + "preferredLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "requiredLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "discouragedLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "note": { + "type": "string", + "nullable": true + } + }, + "required": [ + "preferredLabels", + "requiredLabels", + "discouragedLabels", + "note" + ] + }, + "validationExpectations": { + "type": "array", + "items": { + "type": "string" + } + }, + "readinessWarnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "maintainerExpectations": { + "type": "array", + "items": { + "type": "string" + } + }, + "publicOutputBoundaries": { + "type": "array", + "items": { + "type": "string" + } + }, + "previewMarkdown": { + "type": "string" + }, + "droppedPublicItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "reason": { + "type": "string", + "enum": [ + "empty", + "unsafe_public_text" + ] + } + }, + "required": [ + "field", + "reason" + ] + } + }, + "privateOwnerContext": { + "type": "object", + "properties": { + "itemCount": { + "type": "number" + }, + "includedInPublicPreview": { + "type": "boolean", + "enum": [ + false + ] + } + }, + "required": [ + "itemCount", + "includedInPublicPreview" + ] + }, + "publication": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "preview_only" + ] + }, + "allowed": { + "type": "boolean", + "enum": [ + false + ] + }, + "actions": { + "type": "array", + "items": { + "type": "string" + } + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "allowed", + "actions", + "reason" + ] + } + }, + "required": [ + "repoFullName", + "generatedAt", + "source", + "previewOnly", + "publicSafe", + "contributionLanes", + "labelPolicy", + "validationExpectations", + "readinessWarnings", + "maintainerExpectations", + "publicOutputBoundaries", + "previewMarkdown", + "droppedPublicItems", + "privateOwnerContext", + "publication" + ] + }, "blockers": { "type": "array", "items": { @@ -3543,6 +3772,7 @@ "docsCompleteness", "githubApp", "policyReadiness", + "onboardingPackPreview", "blockers", "warnings", "dataQuality" diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index f402b7ab..8e7be446 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1824,6 +1824,53 @@ export const RegistrationReadinessSchema = z summary: z.string(), }) .nullable(), + onboardingPackPreview: z + .object({ + repoFullName: z.string(), + generatedAt: z.string(), + source: z.enum(["policy_compiler"]), + previewOnly: z.literal(true), + publicSafe: z.literal(true), + contributionLanes: z.array( + z.object({ + id: z.string(), + title: z.string(), + summary: z.string(), + preferredPaths: z.array(z.string()), + discouragedPaths: z.array(z.string()), + validationExpectations: z.array(z.string()), + publicNotes: z.array(z.string()), + }), + ), + labelPolicy: z.object({ + preferredLabels: z.array(z.string()), + requiredLabels: z.array(z.string()), + discouragedLabels: z.array(z.string()), + note: z.string().nullable(), + }), + validationExpectations: z.array(z.string()), + readinessWarnings: z.array(z.string()), + maintainerExpectations: z.array(z.string()), + publicOutputBoundaries: z.array(z.string()), + previewMarkdown: z.string(), + droppedPublicItems: z.array( + z.object({ + field: z.string(), + reason: z.enum(["empty", "unsafe_public_text"]), + }), + ), + privateOwnerContext: z.object({ + itemCount: z.number(), + includedInPublicPreview: z.literal(false), + }), + publication: z.object({ + status: z.enum(["preview_only"]), + allowed: z.literal(false), + actions: z.array(z.string()), + reason: z.string(), + }), + }) + .nullable(), blockers: z.array(z.string()), warnings: z.array(z.string()), dataQuality: z.record(z.string(), z.unknown()), diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 60ad0a6d..3419dd79 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -361,185 +361,248 @@ function summarize(manifest: FocusManifest, blocked: string[], wanted: string[]) return "Maintainer focus manifest applied with no path-specific verdict."; } -// ─── Focus Manifest Policy Schema ──────────────────────────────────────────── - -/** Preference signal for a contribution lane derived from the focus manifest. */ -export type FocusManifestLanePreference = "preferred" | "neutral" | "discouraged"; +// --------------------------------------------------------------------------- +// Policy schema (#296) +// --------------------------------------------------------------------------- -/** - * Public-safe contribution lane preferences derived from the manifest. - * Safe to surface on contributor-facing outputs. - */ -export type FocusManifestPolicyContributionLanes = { - directPrLane: FocusManifestLanePreference; - issueDiscoveryLane: FocusManifestLanePreference; - preferredEntryPaths: string[]; -}; - -/** - * Public-safe discouragement signals: blocked paths and issue-discovery status. - * Safe to surface on contributor-facing outputs. - */ -export type FocusManifestPolicyDiscouragedWork = { - blockedEntryPaths: string[]; - issueDiscoveryDiscouraged: boolean; +export type FocusManifestPolicyContributionLane = { + id: string; + preference: "preferred" | "neutral" | "discouraged"; + title: string; + summary: string; + preferredPaths: string[]; + discouragedPaths: string[]; + validationExpectations: string[]; + publicNotes: string[]; }; -/** - * Public-safe label and linked-issue expectations. - * Safe to surface on contributor-facing outputs. - */ -export type FocusManifestPolicyLabelExpectations = { +export type FocusManifestPolicyLabelPolicy = { preferredLabels: string[]; - linkedIssuePolicy: FocusManifestLinkedIssuePolicy; + required: boolean; }; -/** - * Public-safe validation expectations derived from the manifest's test and issue-link policies. - * Safe to surface on contributor-facing outputs. - */ -export type FocusManifestPolicyValidationExpectations = { - testExpectations: string[]; - linkedIssueRequired: boolean; - linkedIssuePreferred: boolean; +export type FocusManifestPolicyValidation = { + expectations: string[]; + linkedIssuePolicy: FocusManifestLinkedIssuePolicy; }; -/** - * Normalized policy schema compiled from a repo focus manifest. - * - * `publicSafe` fields contain only contributor-safe guidance — they are free of - * maintainer-private notes, scoreability, reviewability, reward/risk, wallet, - * hotkey, and raw trust context. - * - * `authenticated` fields are intended for repo owner and maintainer surfaces only - * and must never be forwarded to contributor-facing GitHub output. - */ export type FocusManifestPolicy = { - present: boolean; + repoFullName: string; + generatedAt: string; source: FocusManifestSource; + present: boolean; publicSafe: { - contributionLanes: FocusManifestPolicyContributionLanes; - discouragedWork: FocusManifestPolicyDiscouragedWork; - labelExpectations: FocusManifestPolicyLabelExpectations; - validationExpectations: FocusManifestPolicyValidationExpectations; - entryGuidance: string[]; - summary: string; + contributionLanes: FocusManifestPolicyContributionLane[]; + labelPolicy: FocusManifestPolicyLabelPolicy; + validation: FocusManifestPolicyValidation; + issueDiscoveryPolicy: FocusManifestIssueDiscoveryPolicy; + publicNotes: string[]; + readinessWarnings: string[]; }; authenticated: { - readinessWarnings: string[]; + manifestSource: FocusManifestSource; + privateNoteCount: number; + manifestWarningCount: number; parseWarnings: string[]; - maintainerContext: string[]; }; }; /** - * Compile a {@link FocusManifest} into a normalized, machine-readable - * {@link FocusManifestPolicy}. - * - * The result is deterministic: the same manifest always produces the same policy. - * Public-safe fields and private/authenticated fields are strictly separated so - * callers can route each subset to the appropriate surface without manual filtering. + * Compile a normalized {@link FocusManifest} into a deterministic, machine-readable + * {@link FocusManifestPolicy}. Public-safe fields are segregated from authenticated + * (owner-only) fields. No reward, wallet, hotkey, raw trust, or private scoring + * language is allowed in public-safe output — unsafe strings are silently dropped. */ -export function compileFocusManifestPolicy(manifest: FocusManifest): FocusManifestPolicy { - if (!manifest.present) { - const readinessWarnings = manifest.warnings.length > 0 - ? manifest.warnings - : ["No maintainer focus manifest found; contribution policy uses deterministic defaults."]; - return { - present: false, - source: manifest.source, - publicSafe: { - contributionLanes: { directPrLane: "neutral", issueDiscoveryLane: "neutral", preferredEntryPaths: [] }, - discouragedWork: { blockedEntryPaths: [], issueDiscoveryDiscouraged: false }, - labelExpectations: { preferredLabels: [], linkedIssuePolicy: "optional" }, - validationExpectations: { testExpectations: [], linkedIssueRequired: false, linkedIssuePreferred: false }, - entryGuidance: [], - summary: "No maintainer focus manifest; contribution policy is unconstrained.", - }, - authenticated: { - readinessWarnings, - parseWarnings: manifest.warnings, - maintainerContext: [], - }, - }; - } - - const directPrLane = policyDirectPrLane(manifest); - const issueDiscoveryLane = policyIssueDiscoveryLane(manifest); - const entryGuidance = policyEntryGuidance(manifest); +export function compileFocusManifestPolicy(repoFullName: string, manifest: FocusManifest, options: { generatedAt?: string } = {}): FocusManifestPolicy { + const generatedAt = options.generatedAt ?? new Date().toISOString(); + const safePublicNotes = manifest.publicNotes.filter(isFocusManifestPublicSafe); + const contributionLanes = buildPolicyContributionLanes(manifest); + const readinessWarnings = buildPolicyReadinessWarnings(manifest); return { - present: true, + repoFullName, + generatedAt, source: manifest.source, + present: manifest.present, publicSafe: { - contributionLanes: { - directPrLane, - issueDiscoveryLane, - preferredEntryPaths: manifest.wantedPaths.filter(isFocusManifestPublicSafe), - }, - discouragedWork: { - blockedEntryPaths: manifest.blockedPaths.filter(isFocusManifestPublicSafe), - issueDiscoveryDiscouraged: manifest.issueDiscoveryPolicy === "discouraged", - }, - labelExpectations: { + contributionLanes, + labelPolicy: { preferredLabels: manifest.preferredLabels.filter(isFocusManifestPublicSafe), - linkedIssuePolicy: manifest.linkedIssuePolicy, + required: manifest.linkedIssuePolicy !== "optional", }, - validationExpectations: { - testExpectations: manifest.testExpectations.filter(isFocusManifestPublicSafe), - linkedIssueRequired: manifest.linkedIssuePolicy === "required", - linkedIssuePreferred: manifest.linkedIssuePolicy === "preferred", + validation: { + expectations: manifest.testExpectations.filter(isFocusManifestPublicSafe), + linkedIssuePolicy: manifest.linkedIssuePolicy, }, - entryGuidance, - summary: policyPublicSummary(manifest, directPrLane, issueDiscoveryLane), + issueDiscoveryPolicy: manifest.issueDiscoveryPolicy, + publicNotes: safePublicNotes, + readinessWarnings, }, authenticated: { - readinessWarnings: policyReadinessWarnings(manifest), + manifestSource: manifest.source, + privateNoteCount: manifest.maintainerNotes.length, + manifestWarningCount: manifest.warnings.length, parseWarnings: manifest.warnings, - maintainerContext: manifest.maintainerNotes, }, }; } -function policyDirectPrLane(manifest: FocusManifest): FocusManifestLanePreference { - if (manifest.issueDiscoveryPolicy === "encouraged") return "discouraged"; - if (manifest.wantedPaths.length > 0) return "preferred"; - return "neutral"; -} +function buildPolicyContributionLanes(manifest: FocusManifest): FocusManifestPolicyContributionLane[] { + if (!manifest.present) return []; + + const lanes: FocusManifestPolicyContributionLane[] = []; + const safeWantedPaths = manifest.wantedPaths.filter(isFocusManifestPublicSafe); + const safeBlockedPaths = manifest.blockedPaths.filter(isFocusManifestPublicSafe); + + if (safeWantedPaths.length > 0 || manifest.testExpectations.length > 0) { + lanes.push({ + id: "direct-pr", + preference: "preferred", + title: "Direct PR lane", + summary: "Contribute changes in maintainer-wanted areas with required validation evidence.", + preferredPaths: safeWantedPaths, + discouragedPaths: safeBlockedPaths, + validationExpectations: manifest.testExpectations.filter(isFocusManifestPublicSafe), + publicNotes: manifest.publicNotes.filter(isFocusManifestPublicSafe), + }); + } + + if (manifest.issueDiscoveryPolicy === "encouraged") { + lanes.push({ + id: "issue-discovery", + preference: "preferred", + title: "Issue discovery lane", + summary: "File well-scoped issue reports that the maintainer has indicated are welcome.", + preferredPaths: [], + discouragedPaths: safeBlockedPaths, + validationExpectations: [], + publicNotes: [], + }); + } else if (manifest.issueDiscoveryPolicy === "discouraged") { + lanes.push({ + id: "issue-discovery", + preference: "discouraged", + title: "Issue discovery lane", + summary: "The maintainer has indicated this repo prefers direct fixes over new issue reports.", + preferredPaths: [], + discouragedPaths: [], + validationExpectations: [], + publicNotes: [], + }); + } -function policyIssueDiscoveryLane(manifest: FocusManifest): FocusManifestLanePreference { - if (manifest.issueDiscoveryPolicy === "encouraged") return "preferred"; - if (manifest.issueDiscoveryPolicy === "discouraged") return "discouraged"; - return "neutral"; + return lanes; } -function policyEntryGuidance(manifest: FocusManifest): string[] { - const guidance: string[] = []; - if (manifest.wantedPaths.length > 0) guidance.push(`Focus changes on maintainer-wanted areas: ${manifest.wantedPaths.slice(0, 5).join(", ")}.`); - if (manifest.blockedPaths.length > 0) guidance.push(`Avoid maintainer-blocked areas: ${manifest.blockedPaths.slice(0, 5).join(", ")}.`); - if (manifest.preferredLabels.length > 0) guidance.push(`Apply a maintainer-preferred label: ${manifest.preferredLabels.slice(0, 3).join(", ")}.`); - if (manifest.linkedIssuePolicy === "required") guidance.push("Link a tracked issue before opening a PR."); - else if (manifest.linkedIssuePolicy === "preferred") guidance.push("Link a tracked issue if one exists."); - if (manifest.issueDiscoveryPolicy === "encouraged") guidance.push("Issue discovery reports are welcomed; search for gaps before opening a PR."); - else if (manifest.issueDiscoveryPolicy === "discouraged") guidance.push("Prefer direct fixes over new issue reports."); - for (const note of manifest.publicNotes) { - if (isFocusManifestPublicSafe(note)) guidance.push(note); +function buildPolicyReadinessWarnings(manifest: FocusManifest): string[] { + if (!manifest.present) return []; + const warnings: string[] = []; + if (manifest.wantedPaths.length === 0 && manifest.preferredLabels.length === 0) { + warnings.push("Focus manifest does not define wanted paths or preferred labels; contribution scope may be unclear to contributors."); } - return [...new Set(guidance)].filter(isFocusManifestPublicSafe); + if (manifest.testExpectations.length === 0) { + warnings.push("Focus manifest does not define validation expectations; contributors may not know what tests to run."); + } + if (manifest.blockedPaths.length > 0 && manifest.wantedPaths.length === 0) { + warnings.push("Focus manifest blocks work areas but does not define wanted paths; pair blocked areas with a positive lane."); + } + return warnings.filter(isFocusManifestPublicSafe); } -function policyReadinessWarnings(manifest: FocusManifest): string[] { +// --------------------------------------------------------------------------- +// Contribution lane derivation (#297) +// --------------------------------------------------------------------------- + +export type ContributionLanePreference = "preferred" | "neutral" | "discouraged"; + +export type ContributionLanes = { + directPrLane: ContributionLanePreference; + issueDiscoveryLane: ContributionLanePreference; + preferredEntryPaths: string[]; + discouragedEntryPaths: string[]; + validationExpectations: string[]; + guidanceText: string[]; + warnings: string[]; + summary: string; +}; + +/** + * Derive public-safe {@link ContributionLanes} from a focus manifest. Output is + * deterministic: identical manifests produce identical lanes. No private scoring, + * reward context, or trust data is included. + */ +export function deriveContributionLanes(manifest: FocusManifest): ContributionLanes { + if (!manifest.present) { + return { + directPrLane: "neutral", + issueDiscoveryLane: "neutral", + preferredEntryPaths: [], + discouragedEntryPaths: [], + validationExpectations: [], + guidanceText: [], + warnings: manifest.warnings, + summary: "No focus manifest is available; using neutral lane defaults.", + }; + } + + const safeWanted = manifest.wantedPaths.filter(isFocusManifestPublicSafe); + const safeBlocked = manifest.blockedPaths.filter(isFocusManifestPublicSafe); + const safeTestExpectations = manifest.testExpectations.filter(isFocusManifestPublicSafe); + const safePublicNotes = manifest.publicNotes.filter(isFocusManifestPublicSafe); + + const directPrLane: ContributionLanePreference = + safeWanted.length > 0 ? "preferred" + : safeBlocked.length > 0 && safeWanted.length === 0 ? "discouraged" + : "neutral"; + + const issueDiscoveryLane: ContributionLanePreference = + manifest.issueDiscoveryPolicy === "encouraged" ? "preferred" + : manifest.issueDiscoveryPolicy === "discouraged" ? "discouraged" + : "neutral"; + + const guidanceText: string[] = []; + + if (manifest.linkedIssuePolicy === "required") { + guidanceText.push("Link a tracked issue before opening a pull request."); + } else if (manifest.linkedIssuePolicy === "preferred") { + guidanceText.push("Linking a tracked issue is preferred before opening a pull request."); + } + + if (manifest.preferredLabels.length > 0) { + const safeLabels = manifest.preferredLabels.filter(isFocusManifestPublicSafe); + if (safeLabels.length > 0) { + guidanceText.push(`Apply a maintainer-preferred label: ${safeLabels.slice(0, 3).join(", ")}.`); + } + } + + guidanceText.push(...safePublicNotes); + const warnings: string[] = []; - if (manifest.blockedPaths.length > 0) warnings.push(`${manifest.blockedPaths.length} blocked area(s) declared; contributors should confirm scope before opening PRs.`); - if (manifest.issueDiscoveryPolicy === "discouraged" && manifest.wantedPaths.length === 0) warnings.push("Issue discovery is discouraged but no wanted paths are declared; contributors have limited guidance on preferred work areas."); - if (manifest.linkedIssuePolicy === "required" && manifest.wantedPaths.length === 0 && manifest.preferredLabels.length === 0) warnings.push("Linked issues are required but no preferred labels or wanted paths are configured; consider adding wanted paths or preferred labels to guide contributors."); - return warnings; -} + if (safeWanted.length === 0 && manifest.preferredLabels.length === 0) { + warnings.push("Contribution scope is unclear; focus manifest lacks wanted paths and preferred labels."); + } + if (safeTestExpectations.length === 0) { + warnings.push("Validation expectations are not defined in the focus manifest."); + } + + const summaryParts: string[] = []; + if (directPrLane === "preferred") summaryParts.push("direct PRs in wanted areas preferred"); + else if (directPrLane === "discouraged") summaryParts.push("direct PRs discouraged outside wanted areas"); + if (issueDiscoveryLane === "preferred") summaryParts.push("issue discovery welcome"); + else if (issueDiscoveryLane === "discouraged") summaryParts.push("issue discovery discouraged"); -function policyPublicSummary(manifest: FocusManifest, directPrLane: FocusManifestLanePreference, issueDiscoveryLane: FocusManifestLanePreference): string { - if (issueDiscoveryLane === "preferred" && directPrLane === "discouraged") return "Issue-discovery is the preferred contribution mode; direct PRs are discouraged."; - if (issueDiscoveryLane === "discouraged" && manifest.wantedPaths.length > 0) return "Direct PRs on the wanted areas are preferred; issue-discovery submissions are discouraged."; - if (directPrLane === "preferred") return "Direct PRs on the maintainer-wanted areas are preferred."; - if (issueDiscoveryLane === "discouraged") return "Direct PRs are preferred; issue-discovery submissions are discouraged."; - return "Contribution policy is guided by the maintainer focus manifest."; + return { + directPrLane, + issueDiscoveryLane, + preferredEntryPaths: safeWanted, + discouragedEntryPaths: safeBlocked, + validationExpectations: safeTestExpectations, + guidanceText: guidanceText.filter(isFocusManifestPublicSafe), + warnings, + summary: summaryParts.length > 0 + ? `Focus manifest lanes: ${summaryParts.join("; ")}.` + : "Focus manifest applied; no specific lane preference is set.", + }; } + +// ─── Focus Manifest Policy Schema ──────────────────────────────────────────── diff --git a/src/signals/onboarding-pack.ts b/src/signals/onboarding-pack.ts index d53db589..c36fb904 100644 --- a/src/signals/onboarding-pack.ts +++ b/src/signals/onboarding-pack.ts @@ -1,4 +1,4 @@ -import { isFocusManifestPublicSafe } from "./focus-manifest"; +import { isFocusManifestPublicSafe, type FocusManifestPolicy } from "./focus-manifest"; import { nowIso } from "../utils/json"; export type RepoPolicyContributionLane = { @@ -78,6 +78,34 @@ export type RepoOnboardingPackPreview = { }; }; +/** + * Adapt a compiled {@link FocusManifestPolicy} into the {@link RepoPolicyCompilerOutput} + * shape expected by {@link buildRepoOnboardingPackPreview}. + */ +export function focusManifestPolicyToCompilerOutput(policy: FocusManifestPolicy): RepoPolicyCompilerOutput { + return { + repoFullName: policy.repoFullName, + generatedAt: policy.generatedAt, + contributionLanes: policy.publicSafe.contributionLanes.map((lane) => ({ + id: lane.id, + title: lane.title, + summary: lane.summary, + preferredPaths: lane.preferredPaths, + discouragedPaths: lane.discouragedPaths, + validationExpectations: lane.validationExpectations, + publicNotes: lane.publicNotes, + })), + labelPolicy: { + preferredLabels: policy.publicSafe.labelPolicy.preferredLabels, + requiredLabels: [], + discouragedLabels: [], + }, + validationExpectations: policy.publicSafe.validation.expectations, + readinessWarnings: policy.publicSafe.readinessWarnings, + privateOwnerContext: policy.authenticated.parseWarnings, + }; +} + const DEFAULT_PUBLIC_OUTPUT_BOUNDARIES = [ "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.", diff --git a/src/signals/registration-readiness.ts b/src/signals/registration-readiness.ts index 59a14d35..0f676acd 100644 --- a/src/signals/registration-readiness.ts +++ b/src/signals/registration-readiness.ts @@ -1,7 +1,8 @@ import type { RegistryRepoConfig, RepositoryRecord, RepositorySettings } from "../types"; import { nowIso } from "../utils/json"; import type { ConfigQuality, ContributorIntakeHealth, LabelAudit, LaneAdvice, MaintainerCutReadiness, QueueHealth } from "./engine"; -import type { FocusManifest } from "./focus-manifest"; +import { compileFocusManifestPolicy, type FocusManifest } from "./focus-manifest"; +import { buildRepoOnboardingPackPreview, focusManifestPolicyToCompilerOutput, type RepoOnboardingPackPreview } from "./onboarding-pack"; import { buildRepoPolicyReadiness, policyReadinessWarningText, type RepoPolicyReadinessReport } from "./repo-policy-readiness"; export type RegistrationMode = "direct_pr" | "issue_discovery" | "split"; @@ -61,6 +62,7 @@ export type RegistrationReadinessReport = { docsCompleteness: { status: string; requiredDocs: string[]; note: string }; githubApp: GithubAppBehavior; policyReadiness: RepoPolicyReadinessReport | null; + onboardingPackPreview: RepoOnboardingPackPreview | null; blockers: string[]; warnings: string[]; }; @@ -160,6 +162,13 @@ export function buildRegistrationReadiness(input: RegistrationReadinessInput): R contributorIntakeHealth, }); + const onboardingPackPreview = + input.focusManifest === undefined + ? null + : buildRepoOnboardingPackPreview( + focusManifestPolicyToCompilerOutput(compileFocusManifestPolicy(repoFullName, input.focusManifest)), + ); + const blockers = [ ...(!isRegistered ? ["Repository is not registered in the latest Gittensory registry snapshot."] : []), ...(configFragile ? ["Repository config quality is fragile."] : []), @@ -229,6 +238,7 @@ export function buildRegistrationReadiness(input: RegistrationReadinessInput): R }, githubApp, policyReadiness, + onboardingPackPreview, blockers, warnings, }; diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 804c2e6b..9c625d18 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -232,25 +232,28 @@ describe("buildFocusManifestGuidance", () => { }); describe("compileFocusManifestPolicy", () => { + const REPO = "JSONbored/gittensory"; + const GENERATED_AT = "2026-06-03T00:00:00.000Z"; + const opts = { generatedAt: GENERATED_AT }; + // ── Minimal: absent manifest ─────────────────────────────────────────── - it("returns unconstrained policy with neutral lanes for an absent manifest", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest(null)); + it("returns an absent policy with empty contribution lanes for a null manifest", () => { + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest(null), opts); expect(policy.present).toBe(false); + expect(policy.repoFullName).toBe(REPO); + expect(policy.generatedAt).toBe(GENERATED_AT); expect(policy.source).toBe("none"); - expect(policy.publicSafe.contributionLanes).toEqual({ directPrLane: "neutral", issueDiscoveryLane: "neutral", preferredEntryPaths: [] }); - expect(policy.publicSafe.discouragedWork).toEqual({ blockedEntryPaths: [], issueDiscoveryDiscouraged: false }); - expect(policy.publicSafe.labelExpectations).toEqual({ preferredLabels: [], linkedIssuePolicy: "optional" }); - expect(policy.publicSafe.validationExpectations).toEqual({ testExpectations: [], linkedIssueRequired: false, linkedIssuePreferred: false }); - expect(policy.publicSafe.entryGuidance).toEqual([]); - expect(policy.publicSafe.summary).toMatch(/unconstrained/i); - expect(policy.authenticated.readinessWarnings.length).toBeGreaterThan(0); + expect(policy.publicSafe.contributionLanes).toEqual([]); + expect(policy.publicSafe.readinessWarnings).toEqual([]); + expect(policy.authenticated.parseWarnings).toEqual([]); + expect(policy.authenticated.privateNoteCount).toBe(0); }); - it("forwards parse warnings into authenticated.readinessWarnings for a malformed manifest", () => { - const policy = compileFocusManifestPolicy(parseFocusManifestContent("{ broken json")); + it("forwards parse warnings into authenticated.parseWarnings for a malformed manifest", () => { + const policy = compileFocusManifestPolicy(REPO, parseFocusManifestContent("{ broken json"), opts); expect(policy.present).toBe(false); expect(policy.authenticated.parseWarnings.join(" ")).toMatch(/not valid JSON/i); - expect(policy.authenticated.readinessWarnings.length).toBeGreaterThan(0); + expect(policy.authenticated.manifestWarningCount).toBeGreaterThan(0); }); // ── Typical: fully specified manifest ───────────────────────────────── @@ -266,98 +269,77 @@ describe("compileFocusManifestPolicy", () => { maintainerNotes: ["Internal: ping @owner before the queue processor."], publicNotes: ["Prefer small, focused PRs."], }); - const policy = compileFocusManifestPolicy(manifest); + const policy = compileFocusManifestPolicy(REPO, manifest, opts); expect(policy.present).toBe(true); expect(policy.source).toBe("repo_file"); - // contribution lanes - expect(policy.publicSafe.contributionLanes.directPrLane).toBe("preferred"); - expect(policy.publicSafe.contributionLanes.issueDiscoveryLane).toBe("discouraged"); - expect(policy.publicSafe.contributionLanes.preferredEntryPaths).toContain("src/"); - - // discouraged work - expect(policy.publicSafe.discouragedWork.blockedEntryPaths).toContain("migrations/"); - expect(policy.publicSafe.discouragedWork.issueDiscoveryDiscouraged).toBe(true); - - // label expectations - expect(policy.publicSafe.labelExpectations.preferredLabels).toContain("bug"); - expect(policy.publicSafe.labelExpectations.linkedIssuePolicy).toBe("required"); - - // validation expectations - expect(policy.publicSafe.validationExpectations.testExpectations).toContain("unit tests for new branches"); - expect(policy.publicSafe.validationExpectations.linkedIssueRequired).toBe(true); - expect(policy.publicSafe.validationExpectations.linkedIssuePreferred).toBe(false); + // label policy + expect(policy.publicSafe.labelPolicy.preferredLabels).toContain("bug"); - // entry guidance - expect(policy.publicSafe.entryGuidance.join(" ")).toMatch(/src\/|bug|linked issue/i); - expect(policy.publicSafe.entryGuidance).toContain("Prefer small, focused PRs."); + // validation + expect(policy.publicSafe.validation.linkedIssuePolicy).toBe("required"); + expect(policy.publicSafe.validation.expectations).toContain("unit tests for new branches"); - // summary - expect(policy.publicSafe.summary).toMatch(/wanted areas|preferred/i); - - // authenticated: maintainerNotes present and not in publicSafe - expect(policy.authenticated.maintainerContext).toContain("Internal: ping @owner before the queue processor."); + // public notes — safe note included, private note excluded + expect(policy.publicSafe.publicNotes).toContain("Prefer small, focused PRs."); expect(JSON.stringify(policy.publicSafe)).not.toMatch(/ping @owner/); + + // authenticated: private note count, no maintainer text in publicSafe + expect(policy.authenticated.privateNoteCount).toBe(1); + expect(policy.authenticated.parseWarnings).toEqual([]); }); // ── Missing-field: partial manifest ─────────────────────────────────── it("handles a partial manifest with only linkedIssuePolicy set", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "preferred" })); + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "preferred" }), opts); expect(policy.present).toBe(true); - expect(policy.publicSafe.contributionLanes.directPrLane).toBe("preferred"); - expect(policy.publicSafe.labelExpectations.linkedIssuePolicy).toBe("preferred"); - expect(policy.publicSafe.validationExpectations.linkedIssueRequired).toBe(false); - expect(policy.publicSafe.validationExpectations.linkedIssuePreferred).toBe(true); - expect(policy.publicSafe.discouragedWork.blockedEntryPaths).toEqual([]); - expect(policy.publicSafe.discouragedWork.issueDiscoveryDiscouraged).toBe(false); - expect(policy.authenticated.maintainerContext).toEqual([]); + expect(policy.publicSafe.validation.linkedIssuePolicy).toBe("preferred"); + expect(policy.authenticated.privateNoteCount).toBe(0); }); it("handles a manifest with only issueDiscoveryPolicy:encouraged", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest({ issueDiscoveryPolicy: "encouraged" })); + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ issueDiscoveryPolicy: "encouraged" }), opts); expect(policy.present).toBe(true); - expect(policy.publicSafe.contributionLanes.directPrLane).toBe("discouraged"); - expect(policy.publicSafe.contributionLanes.issueDiscoveryLane).toBe("preferred"); - expect(policy.publicSafe.discouragedWork.issueDiscoveryDiscouraged).toBe(false); - expect(policy.publicSafe.summary).toMatch(/issue.discovery is the preferred/i); - expect(policy.publicSafe.entryGuidance.join(" ")).toMatch(/welcomed|search for gaps/i); + expect(policy.publicSafe.issueDiscoveryPolicy).toBe("encouraged"); }); it("handles a manifest with only blockedPaths set", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest({ blockedPaths: ["infra/"] })); + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ blockedPaths: ["infra/"] }), opts); expect(policy.present).toBe(true); - expect(policy.publicSafe.discouragedWork.blockedEntryPaths).toContain("infra/"); - expect(policy.publicSafe.contributionLanes.directPrLane).toBe("neutral"); - expect(policy.authenticated.readinessWarnings.join(" ")).toMatch(/blocked area/i); + expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/blocked area|pair blocked/i); }); - it("emits a readiness warning when issue discovery is discouraged but no wanted paths are declared", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest({ issueDiscoveryPolicy: "discouraged" })); - expect(policy.authenticated.readinessWarnings.join(" ")).toMatch(/limited guidance/i); + it("emits a readiness warning when no wanted paths or preferred labels are declared", () => { + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ issueDiscoveryPolicy: "discouraged" }), opts); + expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/does not define wanted paths|contribution scope may be unclear/i); }); - it("emits a readiness warning when linked issues are required but no labels or wanted paths guide contributors", () => { - const policy = compileFocusManifestPolicy(parseFocusManifest({ linkedIssuePolicy: "required" })); - expect(policy.authenticated.readinessWarnings.join(" ")).toMatch(/linked issues are required/i); + it("emits a readiness warning when blocked paths exist but no wanted paths are declared", () => { + const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ linkedIssuePolicy: "required" }), opts); + expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/does not define wanted paths|contribution scope/i); }); // ── Public/private separation ────────────────────────────────────────── - it("keeps maintainerContext out of publicSafe entirely", () => { + it("keeps maintainer notes out of publicSafe entirely", () => { const policy = compileFocusManifestPolicy( + REPO, parseFocusManifest({ wantedPaths: ["src/"], maintainerNotes: ["Private queue note.", "Ping @owner privately."] }), + opts, ); - expect(policy.authenticated.maintainerContext).toEqual(["Private queue note.", "Ping @owner privately."]); + expect(policy.authenticated.privateNoteCount).toBe(2); expect(JSON.stringify(policy.publicSafe)).not.toMatch(/Private queue note|Ping @owner/); }); it("excludes forbidden language from all publicSafe fields even when injected via publicNotes or testExpectations", () => { const policy = compileFocusManifestPolicy( + REPO, parseFocusManifest({ wantedPaths: ["src/"], publicNotes: ["Maximize your reward payout", "Keep PRs focused."], testExpectations: ["Submit wallet seed phrase proof", "npm run test:ci"], }), + opts, ); const publicText = JSON.stringify(policy.publicSafe); expect(publicText).not.toMatch(/reward payout|wallet seed/i); @@ -365,16 +347,10 @@ describe("compileFocusManifestPolicy", () => { expect(publicText).toContain("npm run test:ci"); }); - it("publicSafe.summary never contains forbidden language", () => { - const dangerous = parseFocusManifest({ wantedPaths: ["src/"], publicNotes: ["Boost your raw trust score here"] }); - const policy = compileFocusManifestPolicy(dangerous); - expect(isFocusManifestPublicSafe(policy.publicSafe.summary)).toBe(true); - }); - it("preserves source field from the manifest", () => { - expect(compileFocusManifestPolicy(parseFocusManifest({ wantedPaths: ["src/"] }, "repo_file")).source).toBe("repo_file"); - expect(compileFocusManifestPolicy(parseFocusManifest({ wantedPaths: ["src/"] }, "api_record")).source).toBe("api_record"); - expect(compileFocusManifestPolicy(parseFocusManifest(null)).source).toBe("none"); + expect(compileFocusManifestPolicy(REPO, parseFocusManifest({ wantedPaths: ["src/"] }, "repo_file"), opts).source).toBe("repo_file"); + expect(compileFocusManifestPolicy(REPO, parseFocusManifest({ wantedPaths: ["src/"] }, "api_record"), opts).source).toBe("api_record"); + expect(compileFocusManifestPolicy(REPO, parseFocusManifest(null), opts).source).toBe("none"); }); // ── Property-based sanitizer ─────────────────────────────────────────── @@ -416,14 +392,13 @@ describe("compileFocusManifestPolicy", () => { maintainerNotes: sample(4), publicNotes: sample(4), }); - const policy = compileFocusManifestPolicy(manifest); + const policy = compileFocusManifestPolicy(REPO, manifest, opts); const allPublicText = [ - ...policy.publicSafe.contributionLanes.preferredEntryPaths, - ...policy.publicSafe.discouragedWork.blockedEntryPaths, - ...policy.publicSafe.labelExpectations.preferredLabels, - ...policy.publicSafe.validationExpectations.testExpectations, - ...policy.publicSafe.entryGuidance, - policy.publicSafe.summary, + ...policy.publicSafe.contributionLanes.flatMap((l) => [...l.preferredPaths, ...l.discouragedPaths, ...l.validationExpectations, ...l.publicNotes]), + ...policy.publicSafe.labelPolicy.preferredLabels, + ...policy.publicSafe.validation.expectations, + ...policy.publicSafe.publicNotes, + ...policy.publicSafe.readinessWarnings, ]; expect(allPublicText.every(isFocusManifestPublicSafe)).toBe(true); } diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index ebe658df..e51a7405 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -11,10 +11,19 @@ import { } from "../../src/signals/engine"; import { buildGittensorConfigRecommendation, buildRegistrationReadiness, type InstallationHealthSummary as ReadinessInstallHealth } from "../../src/signals/registration-readiness"; import { buildRepoSettingsPreview, decidePublicSurface, type InstallationHealthSummary as PreviewInstallHealth } from "../../src/signals/settings-preview"; +import { + compileFocusManifestPolicy, + deriveContributionLanes, + isFocusManifestPublicSafe, + parseFocusManifest, +} from "../../src/signals/focus-manifest"; import type { InstallationRecord, IssueRecord, PullRequestRecord, RepoLabelRecord, RegistryRepoConfig, RepositoryRecord, RepositorySettings } from "../../src/types"; const { sanitizeRoleText } = __controlPanelRolesInternals; +const FORBIDDEN_POLICY_PATTERN = + /wallet|hotkey|coldkey|mnemonic|payout|reward estimate|raw trust|trust score|public score estimate|private reviewability|private scoreability|farming/i; + const PRIVATE_TERMS_PATTERN = /wallet|hotkey|coldkey|raw trust|trust score|payout|reward estimate|farming|private reviewability|public score estimate|seed phrase|mnemonic|private key/i; @@ -147,6 +156,10 @@ describe("sanitizeRoleText token redaction", () => { }); }); +// --------------------------------------------------------------------------- +// sanitizeRoleText — private term redaction +// --------------------------------------------------------------------------- + // ─── sanitizeRoleText: private term redaction ───────────────────────────────── describe("sanitizeRoleText private term redaction", () => { @@ -154,6 +167,7 @@ describe("sanitizeRoleText private term redaction", () => { "wallet", "hotkey", "coldkey", + "mnemonic", "raw trust", "trust score", "payout", @@ -168,11 +182,224 @@ describe("sanitizeRoleText private term redaction", () => { for (const term of PRIVATE_TERMS) { it(`returns when text contains "${term}"`, () => { - expect(sanitizeRoleText(`This action involves your ${term} settings.`)).toBe(""); - expect(sanitizeRoleText(term.toUpperCase())).toBe(""); + expect(sanitizeRoleText(`This involves ${term} details.`)).toBe(""); }); } + it("does not redact safe contribution guidance text", () => { + const safe = "Keep pull requests small and tied to accepted repository scope."; + expect(sanitizeRoleText(safe)).toBe(safe); + }); + + it("truncates text to 200 characters", () => { + const long = "a".repeat(300); + expect(sanitizeRoleText(long)).toHaveLength(200); + }); +}); + +// --------------------------------------------------------------------------- +// Contribution lane validation — role cards and onboarding states +// --------------------------------------------------------------------------- + +describe("contribution lane output — public-safe via deriveContributionLanes", () => { + it("produces a neutral result when no manifest is present", () => { + const lanes = deriveContributionLanes(parseFocusManifest(null)); + expect(lanes.directPrLane).toBe("neutral"); + expect(lanes.issueDiscoveryLane).toBe("neutral"); + expect(lanes.preferredEntryPaths).toEqual([]); + expect(lanes.discouragedEntryPaths).toEqual([]); + expect(lanes.summary).toMatch(/neutral lane defaults/i); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("prefers direct PR lane when wanted paths are set", () => { + const manifest = parseFocusManifest({ wantedPaths: ["src/"], testExpectations: ["npm run test:ci"] }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.directPrLane).toBe("preferred"); + expect(lanes.preferredEntryPaths).toContain("src/"); + expect(lanes.validationExpectations).toContain("npm run test:ci"); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("discourages issue discovery when policy is discouraged", () => { + const manifest = parseFocusManifest({ wantedPaths: ["src/"], issueDiscoveryPolicy: "discouraged" }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.issueDiscoveryLane).toBe("discouraged"); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("prefers issue discovery when policy is encouraged", () => { + const manifest = parseFocusManifest({ issueDiscoveryPolicy: "encouraged", wantedPaths: ["src/"] }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.issueDiscoveryLane).toBe("preferred"); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("includes a linked-issue guidance hint when policy is required", () => { + const manifest = parseFocusManifest({ wantedPaths: ["src/"], linkedIssuePolicy: "required" }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.guidanceText.join(" ")).toMatch(/link a tracked issue/i); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("silently drops unsafe public notes from lanes", () => { + const manifest = parseFocusManifest({ + wantedPaths: ["src/"], + publicNotes: ["Maximize your reward payout.", "Keep PRs focused."], + }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.guidanceText).not.toContain("Maximize your reward payout."); + expect(lanes.guidanceText).toContain("Keep PRs focused."); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("emits a warning when contribution scope is unclear", () => { + const manifest = parseFocusManifest({ wantedPaths: [], preferredLabels: [], issueDiscoveryPolicy: "encouraged" }); + const lanes = deriveContributionLanes(manifest); + expect(lanes.warnings.join(" ")).toMatch(/scope is unclear/i); + }); + + it("never exposes forbidden language across 400 property-based iterations", () => { + const stringPool = [ + "src/", + "migrations/", + "Keep PRs focused", + "Maximize your reward payout", + "paste your hotkey here", + "trusted label pipeline", + "raw trust score context", + "npm run test:ci", + ]; + const linkedIssuePolicies = ["required", "preferred", "optional"] as const; + const issueDiscoveryPolicies = ["encouraged", "neutral", "discouraged"] as const; + + let seed = 0x1337cafe; + const next = () => { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + return seed / 0x100000000; + }; + const pick = (items: readonly T[]): T => items[Math.floor(next() * items.length)] as T; + const sample = (max: number): string[] => + Array.from({ length: Math.floor(next() * (max + 1)) }, () => pick(stringPool)); + + for (let i = 0; i < 400; i++) { + const manifest = parseFocusManifest({ + wantedPaths: sample(4), + blockedPaths: sample(2), + preferredLabels: sample(3), + linkedIssuePolicy: pick(linkedIssuePolicies), + issueDiscoveryPolicy: pick(issueDiscoveryPolicies), + publicNotes: sample(3), + testExpectations: sample(2), + }); + const lanes = deriveContributionLanes(manifest); + expect(JSON.stringify(lanes)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + } + }); +}); + +// --------------------------------------------------------------------------- +// Readiness warnings and guidance — compileFocusManifestPolicy +// --------------------------------------------------------------------------- + +describe("compileFocusManifestPolicy — public-safe output boundaries", () => { + const FIXED_DATE = "2026-06-03T00:00:00.000Z"; + + it("returns a present policy for a valid manifest", () => { + const manifest = parseFocusManifest({ + wantedPaths: ["src/signals/"], + testExpectations: ["npm run test:ci"], + linkedIssuePolicy: "required", + preferredLabels: ["feature", "settings"], + publicNotes: ["Keep PRs narrow and tied to accepted scope."], + }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + + expect(policy.present).toBe(true); + expect(policy.repoFullName).toBe("JSONbored/gittensory"); + expect(policy.generatedAt).toBe(FIXED_DATE); + expect(policy.publicSafe.labelPolicy.preferredLabels).toContain("feature"); + expect(policy.publicSafe.validation.linkedIssuePolicy).toBe("required"); + expect(policy.publicSafe.publicNotes).toContain("Keep PRs narrow and tied to accepted scope."); + expect(JSON.stringify(policy.publicSafe)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("keeps private notes out of publicSafe", () => { + const manifest = parseFocusManifest({ + wantedPaths: ["src/"], + maintainerNotes: ["Internal: hotkey validation context only visible to maintainers."], + publicNotes: ["Contribute only to accepted scope."], + }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + + expect(JSON.stringify(policy.publicSafe)).not.toMatch(/hotkey/i); + expect(policy.authenticated.privateNoteCount).toBe(1); + expect(policy.authenticated.parseWarnings).toEqual([]); + }); + + it("drops unsafe text from publicSafe contribution lanes", () => { + const manifest = parseFocusManifest({ + wantedPaths: ["src/"], + publicNotes: ["wallet setup guidance for contributors", "Keep PRs focused."], + }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + + expect(policy.publicSafe.publicNotes).not.toContain("wallet setup guidance for contributors"); + expect(policy.publicSafe.publicNotes).toContain("Keep PRs focused."); + expect(JSON.stringify(policy.publicSafe)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("emits readiness warnings when scope and validation are missing", () => { + const manifest = parseFocusManifest({ issueDiscoveryPolicy: "neutral" }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + expect(policy.present).toBe(false); + expect(policy.publicSafe.readinessWarnings).toEqual([]); + }); + + it("emits a readiness warning for blocked-only manifests with no wanted scope", () => { + const manifest = parseFocusManifest({ blockedPaths: ["migrations/"], wantedPaths: [], preferredLabels: [], testExpectations: [] }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/blocks work areas.*does not define wanted|pair blocked areas/i); + expect(JSON.stringify(policy.publicSafe)).not.toMatch(FORBIDDEN_POLICY_PATTERN); + }); + + it("produces an absent policy for an empty manifest with no parse warnings", () => { + const manifest = parseFocusManifest(null); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + expect(policy.present).toBe(false); + expect(policy.publicSafe.contributionLanes).toEqual([]); + expect(policy.authenticated.parseWarnings).toEqual([]); + }); + + it("records parse warnings in authenticated context without leaking to publicSafe", () => { + const manifest = parseFocusManifest({ wantedPaths: "src/" }); + const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE }); + expect(policy.authenticated.manifestWarningCount).toBeGreaterThan(0); + expect(policy.authenticated.parseWarnings.length).toBeGreaterThan(0); + expect(JSON.stringify(policy.publicSafe)).not.toMatch(/wantedPaths.*must be a list/i); + }); + + it("isFocusManifestPublicSafe blocks all forbidden terms used in policy compiler", () => { + const forbidden = [ + "wallet balance", + "hotkey abc123", + "coldkey xyz", + "mnemonic phrase", + "payout estimate", + "reward estimate value", + "raw trust score", + "trust score context", + "farming strategy", + "private reviewability", + "score context", + "scored output", + ]; + for (const term of forbidden) { + expect(isFocusManifestPublicSafe(term)).toBe(false); + } + expect(isFocusManifestPublicSafe("Keep PRs focused and narrow.")).toBe(true); + }); + it("path regex consumes path-embedded private terms before the term check fires", () => { // The path regex eats the entire /Users/alice/wallet-configs string, so the // remaining text is just which contains no private terms. diff --git a/test/unit/registration-readiness.test.ts b/test/unit/registration-readiness.test.ts index 2bfc9bd8..2209765c 100644 --- a/test/unit/registration-readiness.test.ts +++ b/test/unit/registration-readiness.test.ts @@ -215,6 +215,65 @@ describe("buildRegistrationReadiness", () => { ]), ); }); + + it("produces a null onboardingPackPreview when no focusManifest is provided", () => { + const repo = repoFor("octo/nomanifest", configFor({ repo: "octo/nomanifest" })); + const report = buildRegistrationReadiness({ + repoFullName: repo.fullName, + repo, + settings: settingsFor(repo.fullName), + installation: healthyInstall, + ...signalsFor(repo, [], [], [label("bug")]), + }); + + expect(report.onboardingPackPreview).toBeNull(); + }); + + it("produces an onboardingPackPreview with public-safe lanes when a focusManifest is provided", () => { + const repo = repoFor("octo/manifest", configFor({ repo: "octo/manifest" })); + const report = buildRegistrationReadiness({ + repoFullName: repo.fullName, + repo, + settings: settingsFor(repo.fullName), + installation: healthyInstall, + ...signalsFor(repo, [], [], [label("bug")]), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/signals/"], + testExpectations: ["npm run test:ci"], + linkedIssuePolicy: "required", + preferredLabels: ["feature"], + publicNotes: ["Keep PRs focused and tied to accepted scope."], + }), + }); + + expect(report.onboardingPackPreview).not.toBeNull(); + expect(report.onboardingPackPreview?.source).toBe("policy_compiler"); + expect(report.onboardingPackPreview?.publicSafe).toBe(true); + expect(report.onboardingPackPreview?.previewOnly).toBe(true); + expect(report.onboardingPackPreview?.repoFullName).toBe("octo/manifest"); + expect(report.onboardingPackPreview?.contributionLanes.length).toBeGreaterThan(0); + expect(JSON.stringify(report.onboardingPackPreview)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + }); + + it("onboardingPackPreview strips unsafe public notes via the policy compiler pipeline", () => { + const repo = repoFor("octo/unsafe", configFor({ repo: "octo/unsafe" })); + const report = buildRegistrationReadiness({ + repoFullName: repo.fullName, + repo, + settings: settingsFor(repo.fullName), + installation: healthyInstall, + ...signalsFor(repo, [], [], [label("bug")]), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + publicNotes: ["Maximize payout by contributing to wanted areas.", "Keep PRs focused."], + }), + }); + + const preview = report.onboardingPackPreview; + expect(preview).not.toBeNull(); + expect(JSON.stringify(preview)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + expect(JSON.stringify(preview)).not.toContain("Maximize payout"); + }); }); describe("buildGittensorConfigRecommendation", () => {