diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 7412f68379..900768a546 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -3408,6 +3408,190 @@ "warnings" ] }, + "policyReadiness": { + "type": "object", + "nullable": true, + "properties": { + "repoFullName": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "focus_manifest_policy" + ] + }, + "previewOnly": { + "type": "boolean" + }, + "present": { + "type": "boolean" + }, + "publicWarnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "contribution_flow", + "direct_pr_policy", + "issue_discovery", + "validation", + "maintainer_burden" + ] + }, + "severity": { + "type": "string", + "enum": [ + "info", + "warning", + "critical" + ] + }, + "title": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "action": { + "type": "string" + } + }, + "required": [ + "code", + "category", + "severity", + "title", + "detail", + "action" + ] + } + }, + "ownerContext": { + "type": "object", + "properties": { + "manifestPresent": { + "type": "boolean" + }, + "manifestSource": { + "type": "string", + "enum": [ + "repo_file", + "api_record", + "none" + ] + }, + "privateNoteCount": { + "type": "number" + }, + "manifestWarningCount": { + "type": "number" + }, + "wantedPathCount": { + "type": "number" + }, + "blockedPathCount": { + "type": "number" + }, + "validationExpectationCount": { + "type": "number" + }, + "queueLevel": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ] + }, + "contributorIntakeLevel": { + "type": "string", + "enum": [ + "healthy", + "watch", + "strained", + "blocked" + ] + }, + "configLevel": { + "type": "string", + "enum": [ + "excellent", + "good", + "needs_attention", + "fragile" + ] + }, + "issuePolicy": { + "type": "string" + }, + "issueDiscoveryPolicy": { + "type": "string", + "enum": [ + "encouraged", + "neutral", + "discouraged" + ] + } + }, + "required": [ + "manifestPresent", + "manifestSource", + "privateNoteCount", + "manifestWarningCount", + "wantedPathCount", + "blockedPathCount", + "validationExpectationCount", + "queueLevel", + "contributorIntakeLevel", + "configLevel", + "issuePolicy", + "issueDiscoveryPolicy" + ] + }, + "droppedPublicWarnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "reason": { + "type": "string", + "enum": [ + "unsafe_public_text" + ] + } + }, + "required": [ + "code", + "reason" + ] + } + }, + "summary": { + "type": "string" + } + }, + "required": [ + "repoFullName", + "source", + "previewOnly", + "present", + "publicWarnings", + "ownerContext", + "droppedPublicWarnings", + "summary" + ] + }, "blockers": { "type": "array", "items": { @@ -3442,6 +3626,7 @@ "contributorIntakeHealth", "docsCompleteness", "githubApp", + "policyReadiness", "blockers", "warnings", "dataQuality" diff --git a/src/api/routes.ts b/src/api/routes.ts index 811d026bc5..8574124dc8 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -2829,10 +2829,11 @@ async function buildRepoOutcomePatternsResponse(env: Env, fullName: string) { async function buildRegistrationReadinessResponse(env: Env, fullName: string) { /* v8 ignore start -- Registration readiness route-level shaping over covered signal helpers. */ - const [intelligence, settings, upstreamReports] = await Promise.all([ + const [intelligence, settings, upstreamReports, focusManifest] = await Promise.all([ buildRepoIntelligenceResponse(env, fullName), getRepositorySettings(env, fullName), listUpstreamDriftReports(env, 20), + loadRepoFocusManifest(env, fullName, { fetcher: async () => null }), ]); const repo = intelligence.repo; const installation = await loadInstallationHealthSummary(env, repo); @@ -2848,6 +2849,7 @@ async function buildRegistrationReadinessResponse(env: Env, fullName: string) { contributorIntakeHealth: intelligence.contributorIntakeHealth as ReturnType, installation, upstreamRegistryDriftWarnings: registryHyperparameterDriftWarningsForRepo(upstreamReports, fullName), + focusManifest, }); return { ...report, dataQuality: intelligence.dataQuality }; /* v8 ignore stop */ diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index f9695ff9cd..66cef2b896 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1698,6 +1698,45 @@ export const RegistrationReadinessSchema = z behavior: z.string(), warnings: z.array(z.string()), }), + policyReadiness: z + .object({ + repoFullName: z.string(), + source: z.enum(["focus_manifest_policy"]), + previewOnly: z.boolean(), + present: z.boolean(), + publicWarnings: z.array( + z.object({ + code: z.string(), + category: z.enum(["contribution_flow", "direct_pr_policy", "issue_discovery", "validation", "maintainer_burden"]), + severity: z.enum(["info", "warning", "critical"]), + title: z.string(), + detail: z.string(), + action: z.string(), + }), + ), + ownerContext: z.object({ + manifestPresent: z.boolean(), + manifestSource: z.enum(["repo_file", "api_record", "none"]), + privateNoteCount: z.number(), + manifestWarningCount: z.number(), + wantedPathCount: z.number(), + blockedPathCount: z.number(), + validationExpectationCount: z.number(), + queueLevel: z.enum(["low", "medium", "high", "critical"]), + contributorIntakeLevel: z.enum(["healthy", "watch", "strained", "blocked"]), + configLevel: z.enum(["excellent", "good", "needs_attention", "fragile"]), + issuePolicy: z.string(), + issueDiscoveryPolicy: z.enum(["encouraged", "neutral", "discouraged"]), + }), + droppedPublicWarnings: z.array( + z.object({ + code: z.string(), + reason: z.enum(["unsafe_public_text"]), + }), + ), + summary: 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/registration-readiness.ts b/src/signals/registration-readiness.ts index 70792c488a..59a14d351c 100644 --- a/src/signals/registration-readiness.ts +++ b/src/signals/registration-readiness.ts @@ -1,6 +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 { buildRepoPolicyReadiness, policyReadinessWarningText, type RepoPolicyReadinessReport } from "./repo-policy-readiness"; export type RegistrationMode = "direct_pr" | "issue_discovery" | "split"; export type IssuePolicy = "issue_discovery_enabled" | "split_pr_and_issue_discovery_enabled" | "direct_pr_requires_linked_issue" | "direct_pr_no_issue_required"; @@ -58,6 +60,7 @@ export type RegistrationReadinessReport = { contributorIntakeHealth: ContributorIntakeHealth; docsCompleteness: { status: string; requiredDocs: string[]; note: string }; githubApp: GithubAppBehavior; + policyReadiness: RepoPolicyReadinessReport | null; blockers: string[]; warnings: string[]; }; @@ -74,6 +77,7 @@ export type RegistrationReadinessInput = { contributorIntakeHealth: ContributorIntakeHealth; installation: InstallationHealthSummary | null; upstreamRegistryDriftWarnings?: string[] | undefined; + focusManifest?: FocusManifest | undefined; }; const REQUIRED_DOCS = ["README", "CONTRIBUTING", "SECURITY", "SUPPORT"]; @@ -142,6 +146,19 @@ export function buildRegistrationReadiness(input: RegistrationReadinessInput): R const testCoverageHealth = buildTestCoverageHealth(labelAudit, settings); const githubApp = buildGithubAppBehavior(repo, settings, installation); + const policyReadiness = + input.focusManifest === undefined + ? null + : buildRepoPolicyReadiness({ + repoFullName, + focusManifest: input.focusManifest, + settings, + lane, + configQuality, + labelAudit, + queueHealth, + contributorIntakeHealth, + }); const blockers = [ ...(!isRegistered ? ["Repository is not registered in the latest Gittensory registry snapshot."] : []), @@ -174,6 +191,7 @@ export function buildRegistrationReadiness(input: RegistrationReadinessInput): R ...(settings.publicSurface === "off" ? ["GitHub App public surface is disabled; maintainers will not get comment/label assistance."] : []), ...testCoverageHealth.warnings, ...labelAudit.missingConfiguredLabels.map((label) => `Configured registry label "${label}" is missing from live GitHub labels.`), + ...(policyReadiness?.publicWarnings.map(policyReadinessWarningText) ?? []), ...upstreamRegistryDriftWarnings, ]; @@ -210,6 +228,7 @@ export function buildRegistrationReadiness(input: RegistrationReadinessInput): R note: "Gittensory validates public repo docs from the local project during CI; remote repo-doc crawling is not enabled in this signal yet.", }, githubApp, + policyReadiness, blockers, warnings, }; diff --git a/src/signals/repo-policy-readiness.ts b/src/signals/repo-policy-readiness.ts new file mode 100644 index 0000000000..0f1448e4a2 --- /dev/null +++ b/src/signals/repo-policy-readiness.ts @@ -0,0 +1,264 @@ +import type { RepositorySettings } from "../types"; +import { isFocusManifestPublicSafe, type FocusManifest } from "./focus-manifest"; +import type { ConfigQuality, ContributorIntakeHealth, LabelAudit, LaneAdvice, QueueHealth } from "./engine"; + +export type RepoPolicyReadinessWarningCategory = + | "contribution_flow" + | "direct_pr_policy" + | "issue_discovery" + | "validation" + | "maintainer_burden"; + +export type RepoPolicyReadinessWarningCode = + | "focus_policy_missing" + | "focus_policy_needs_review" + | "contribution_scope_unclear" + | "blocked_work_without_wanted_scope" + | "direct_pr_policy_unclear" + | "linked_issue_policy_mismatch" + | "issue_discovery_policy_mismatch" + | "issue_discovery_intake_not_ready" + | "validation_expectations_missing" + | "validation_gate_uncertain" + | "maintainer_burden_high"; + +export type RepoPolicyReadinessWarning = { + code: RepoPolicyReadinessWarningCode; + category: RepoPolicyReadinessWarningCategory; + severity: "info" | "warning" | "critical"; + title: string; + detail: string; + action: string; +}; + +export type RepoPolicyReadinessReport = { + repoFullName: string; + source: "focus_manifest_policy"; + previewOnly: true; + present: boolean; + publicWarnings: RepoPolicyReadinessWarning[]; + ownerContext: { + manifestPresent: boolean; + manifestSource: FocusManifest["source"]; + privateNoteCount: number; + manifestWarningCount: number; + wantedPathCount: number; + blockedPathCount: number; + validationExpectationCount: number; + queueLevel: QueueHealth["level"]; + contributorIntakeLevel: ContributorIntakeHealth["level"]; + configLevel: ConfigQuality["level"]; + issuePolicy: string; + issueDiscoveryPolicy: FocusManifest["issueDiscoveryPolicy"]; + }; + droppedPublicWarnings: Array<{ + code: RepoPolicyReadinessWarningCode; + reason: "unsafe_public_text"; + }>; + summary: string; +}; + +export type RepoPolicyReadinessInput = { + repoFullName: string; + focusManifest?: FocusManifest | undefined; + settings: RepositorySettings; + lane: LaneAdvice; + configQuality: ConfigQuality; + labelAudit: LabelAudit; + queueHealth: QueueHealth; + contributorIntakeHealth: ContributorIntakeHealth; +}; + +export function buildRepoPolicyReadiness(input: RepoPolicyReadinessInput): RepoPolicyReadinessReport { + const manifest = input.focusManifest; + const present = Boolean(manifest?.present); + const issuePolicy = resolveIssuePolicy(input.lane, input.settings); + const candidates: RepoPolicyReadinessWarning[] = []; + + if (!present) { + candidates.push({ + code: "focus_policy_missing", + category: "contribution_flow", + severity: "warning", + title: "Focus policy is not cached", + detail: "Repo owners cannot preview explicit contribution scope from a focus manifest yet.", + action: "Add or refresh a focus manifest before inviting broader contributor traffic.", + }); + } else if (manifest) { + if (manifest.warnings.length > 0) { + candidates.push({ + code: "focus_policy_needs_review", + category: "contribution_flow", + severity: "warning", + title: "Focus policy needs owner review", + detail: `${manifest.warnings.length} focus manifest warning(s) were recorded during normalization.`, + action: "Review the focus manifest shape before publishing onboarding guidance.", + }); + } + + if (manifest.wantedPaths.length === 0 && manifest.preferredLabels.length === 0 && manifest.publicNotes.length === 0) { + candidates.push({ + code: "contribution_scope_unclear", + category: "contribution_flow", + severity: "warning", + title: "Contribution scope is unclear", + detail: "The focus manifest does not define wanted paths, preferred labels, or public scope notes.", + action: "Add explicit wanted work areas or public scope notes before increasing contributor traffic.", + }); + } + + if (manifest.blockedPaths.length > 0 && manifest.wantedPaths.length === 0) { + candidates.push({ + code: "blocked_work_without_wanted_scope", + category: "contribution_flow", + severity: "warning", + title: "Blocked work lacks a positive lane", + detail: "The focus manifest blocks work areas but does not define wanted paths.", + action: "Pair blocked areas with wanted work areas so contributors know where to focus.", + }); + } + + if (input.lane.lane === "direct_pr" && manifest.linkedIssuePolicy === "optional" && !input.settings.requireLinkedIssue) { + candidates.push({ + code: "direct_pr_policy_unclear", + category: "direct_pr_policy", + severity: "warning", + title: "Direct PR entry policy is loose", + detail: "Direct PR intake is enabled without a linked-issue expectation in settings or focus policy.", + action: "Decide whether direct PRs should link tracked issues before inviting more direct submissions.", + }); + } + + if (input.settings.requireLinkedIssue && manifest.linkedIssuePolicy === "optional") { + candidates.push({ + code: "linked_issue_policy_mismatch", + category: "direct_pr_policy", + severity: "info", + title: "Linked-issue policy differs by source", + detail: "Repository settings require linked issues, while the focus manifest leaves linked issues optional.", + action: "Align settings and focus policy so owner guidance stays consistent.", + }); + } + + if ((input.lane.lane === "issue_discovery" || input.lane.lane === "split") && manifest.issueDiscoveryPolicy === "discouraged") { + candidates.push({ + code: "issue_discovery_policy_mismatch", + category: "issue_discovery", + severity: "warning", + title: "Issue-discovery lane conflicts with focus policy", + detail: "The registry lane allows issue discovery, but the focus manifest discourages new issue reports.", + action: "Clarify whether issue discovery should stay open before publishing owner guidance.", + }); + } else if (input.lane.lane === "direct_pr" && manifest.issueDiscoveryPolicy === "encouraged") { + candidates.push({ + code: "issue_discovery_policy_mismatch", + category: "issue_discovery", + severity: "info", + title: "Issue-discovery policy differs from registry lane", + detail: "The focus manifest welcomes issue reports while the registry lane is direct-PR-first.", + action: "Keep public guidance direct-PR-first unless maintainers intentionally open issue discovery.", + }); + } + + if (manifest.testExpectations.length === 0) { + candidates.push({ + code: "validation_expectations_missing", + category: "validation", + severity: "warning", + title: "Validation expectations are missing", + detail: "The focus manifest does not define test or validation expectations for incoming work.", + action: "Add expected validation commands or evidence requirements before publishing contribution guidance.", + }); + } + } + + if ((input.lane.lane === "issue_discovery" || input.lane.lane === "split") && input.contributorIntakeHealth.level !== "healthy") { + candidates.push({ + code: "issue_discovery_intake_not_ready", + category: "issue_discovery", + severity: input.contributorIntakeHealth.level === "blocked" ? "critical" : "warning", + title: "Issue-discovery intake needs attention", + detail: `Issue discovery is available, but contributor intake is ${input.contributorIntakeHealth.level}.`, + action: "Stabilize intake and triage capacity before inviting more issue reports.", + }); + } + + if (!input.labelAudit.trustedPipelineReady) { + candidates.push({ + code: "validation_gate_uncertain", + category: "validation", + severity: "warning", + title: "Validation gate is not verified", + detail: "The trusted label pipeline is not verified for this repository.", + action: "Verify label and validation gates before relying on automated readiness guidance.", + }); + } + + if (input.queueHealth.level === "high" || input.queueHealth.level === "critical" || input.contributorIntakeHealth.level === "strained" || input.contributorIntakeHealth.level === "blocked") { + candidates.push({ + code: "maintainer_burden_high", + category: "maintainer_burden", + severity: input.queueHealth.level === "critical" || input.contributorIntakeHealth.level === "blocked" ? "critical" : "warning", + title: "Maintainer burden is elevated", + detail: `Queue burden is ${input.queueHealth.level} and contributor intake is ${input.contributorIntakeHealth.level}.`, + action: "Reduce queue pressure or narrow accepted lanes before inviting more contributor traffic.", + }); + } + + const droppedPublicWarnings: RepoPolicyReadinessReport["droppedPublicWarnings"] = []; + const publicWarnings = dedupeWarnings(candidates).filter((warning) => { + const safe = warningTextValues(warning).every(isFocusManifestPublicSafe); + if (!safe) droppedPublicWarnings.push({ code: warning.code, reason: "unsafe_public_text" }); + return safe; + }); + + return { + repoFullName: input.repoFullName, + source: "focus_manifest_policy", + previewOnly: true, + present, + publicWarnings, + ownerContext: { + manifestPresent: present, + manifestSource: manifest?.source ?? "none", + privateNoteCount: manifest?.maintainerNotes.length ?? 0, + manifestWarningCount: manifest?.warnings.length ?? 0, + wantedPathCount: manifest?.wantedPaths.length ?? 0, + blockedPathCount: manifest?.blockedPaths.length ?? 0, + validationExpectationCount: manifest?.testExpectations.length ?? 0, + queueLevel: input.queueHealth.level, + contributorIntakeLevel: input.contributorIntakeHealth.level, + configLevel: input.configQuality.level, + issuePolicy, + issueDiscoveryPolicy: manifest?.issueDiscoveryPolicy ?? "neutral", + }, + droppedPublicWarnings, + summary: + publicWarnings.length > 0 + ? `${publicWarnings.length} policy readiness warning(s) need owner review before broader contributor traffic.` + : "Policy readiness has no public-safe warnings for owner review.", + }; +} + +export function policyReadinessWarningText(warning: RepoPolicyReadinessWarning): string { + return `${warning.title}: ${warning.detail} ${warning.action}`; +} + +function resolveIssuePolicy(lane: LaneAdvice, settings: RepositorySettings): string { + if (lane.lane === "issue_discovery") return "issue_discovery_enabled"; + if (lane.lane === "split") return "split_pr_and_issue_discovery_enabled"; + return settings.requireLinkedIssue ? "direct_pr_requires_linked_issue" : "direct_pr_no_issue_required"; +} + +function dedupeWarnings(warnings: RepoPolicyReadinessWarning[]): RepoPolicyReadinessWarning[] { + const seen = new Set(); + return warnings.filter((warning) => { + if (seen.has(warning.code)) return false; + seen.add(warning.code); + return true; + }); +} + +function warningTextValues(warning: RepoPolicyReadinessWarning): string[] { + return [warning.title, warning.detail, warning.action, policyReadinessWarningText(warning)]; +} diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 89f9122074..facdf5f6bd 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -31,6 +31,7 @@ import { } from "../../src/db/repositories"; import { createApp } from "../../src/api/routes"; import { BURDEN_FORECAST_MAX_AGE_MS } from "../../src/services/burden-forecast"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { normalizeRegistryPayload } from "../../src/registry/normalize"; import { persistRegistrySnapshot } from "../../src/registry/sync"; import { createTestEnv } from "../helpers/d1"; @@ -4202,6 +4203,41 @@ describe("api routes", () => { warnings: expect.arrayContaining(["GitHub App public surface is disabled; maintainers will not get comment/label assistance."]), }); + await upsertRepoFocusManifest(env, "entrius/allways-ui", { + blockedPaths: ["dist/"], + linkedIssuePolicy: "optional", + issueDiscoveryPolicy: "discouraged", + maintainerNotes: [ + "Private reviewability note with wallet, hotkey, raw trust, and farming details.", + ], + }); + const policyReadiness = await app.request("/v1/repos/entrius/allways-ui/registration-readiness", { headers: apiHeaders(env) }, env); + expect(policyReadiness.status).toBe(200); + const policyPayload = (await policyReadiness.json()) as { + policyReadiness: { publicWarnings: unknown[] }; + warnings: string[]; + }; + expect(policyPayload).toMatchObject({ + policyReadiness: { + previewOnly: true, + present: true, + ownerContext: { + privateNoteCount: 1, + blockedPathCount: 1, + }, + publicWarnings: expect.arrayContaining([ + expect.objectContaining({ code: "blocked_work_without_wanted_scope" }), + expect.objectContaining({ code: "linked_issue_policy_mismatch" }), + expect.objectContaining({ code: "validation_expectations_missing" }), + ]), + }, + warnings: expect.arrayContaining([ + expect.stringContaining("Blocked work lacks a positive lane"), + ]), + }); + expect(JSON.stringify(policyPayload.policyReadiness.publicWarnings)).not.toMatch(FORBIDDEN_PUBLIC_REPORT_TERMS); + expect(JSON.stringify(policyPayload.policyReadiness)).not.toMatch(/wallet|hotkey|raw trust|private[-\s]?reviewability|farming/i); + await persistRegistrySnapshot( env, normalizeRegistryPayload( diff --git a/test/unit/registration-readiness.test.ts b/test/unit/registration-readiness.test.ts index a6d1579325..2bfc9bd8ae 100644 --- a/test/unit/registration-readiness.test.ts +++ b/test/unit/registration-readiness.test.ts @@ -8,6 +8,7 @@ import { buildMaintainerCutReadiness, buildQueueHealth, } from "../../src/signals/engine"; +import { parseFocusManifest } from "../../src/signals/focus-manifest"; import { buildGittensorConfigRecommendation, buildRegistrationReadiness, type InstallationHealthSummary } from "../../src/signals/registration-readiness"; import type { IssueRecord, PullRequestRecord, RepoLabelRecord, RegistryRepoConfig, RepositoryRecord, RepositorySettings } from "../../src/types"; @@ -187,6 +188,33 @@ describe("buildRegistrationReadiness", () => { const report = buildRegistrationReadiness({ repoFullName: repo.fullName, repo, settings: settingsFor(repo.fullName), installation: healthyInstall, ...signalsFor(repo, [], [], [label("bug")]) }); expect(JSON.stringify(report)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); }); + + it("threads focus-manifest policy warnings into owner readiness", () => { + const repo = repoFor("octo/policy", configFor({ repo: "octo/policy" })); + const report = buildRegistrationReadiness({ + repoFullName: repo.fullName, + repo, + settings: settingsFor(repo.fullName, { requireLinkedIssue: false }), + installation: healthyInstall, + ...signalsFor(repo, [], [], [label("bug")]), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "optional", + testExpectations: ["Run npm run test:ci."], + }), + }); + + expect(report.policyReadiness?.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "direct_pr_policy_unclear" }), + ]), + ); + expect(report.warnings).toEqual( + expect.arrayContaining([ + expect.stringContaining("Direct PR entry policy is loose"), + ]), + ); + }); }); describe("buildGittensorConfigRecommendation", () => { diff --git a/test/unit/repo-policy-readiness.test.ts b/test/unit/repo-policy-readiness.test.ts new file mode 100644 index 0000000000..83c036f304 --- /dev/null +++ b/test/unit/repo-policy-readiness.test.ts @@ -0,0 +1,327 @@ +import { describe, expect, it } from "vitest"; +import { isFocusManifestPublicSafe, parseFocusManifest } from "../../src/signals/focus-manifest"; +import { + buildRepoPolicyReadiness, + policyReadinessWarningText, + type RepoPolicyReadinessInput, +} from "../../src/signals/repo-policy-readiness"; +import type { ConfigQuality, ContributorIntakeHealth, LabelAudit, LaneAdvice, QueueHealth } from "../../src/signals/engine"; +import type { RepositorySettings } from "../../src/types"; + +const FORBIDDEN_PUBLIC_LANGUAGE = + /wallet|hotkey|coldkey|mnemonic|payout|reward estimate|raw trust|trust score|public score|private reviewability|private scoreability|farming/i; + +function settings(overrides: Partial = {}): RepositorySettings { + return { + repoFullName: "owner/repo", + commentMode: "detected_contributors_only", + publicSignalLevel: "standard", + checkRunMode: "enabled", + checkRunDetailLevel: "standard", + autoLabelEnabled: true, + gittensorLabel: "gittensor", + createMissingLabel: true, + publicSurface: "comment_and_label", + includeMaintainerAuthors: false, + requireLinkedIssue: false, + backfillEnabled: true, + privateTrustEnabled: true, + ...overrides, + }; +} + +function lane(overrides: Partial = {}): LaneAdvice { + return { + lane: "direct_pr", + repoFullName: "owner/repo", + summary: "Direct PR lane.", + contributorGuidance: "Open focused pull requests.", + maintainerGuidance: "Review focused pull requests.", + ...overrides, + }; +} + +function queue(overrides: Partial = {}): QueueHealth { + return { + repoFullName: "owner/repo", + generatedAt: "2026-06-03T00:00:00.000Z", + burdenScore: 10, + level: "low", + summary: "Queue burden is low.", + signals: { + openIssues: 1, + openPullRequests: 1, + unlinkedPullRequests: 0, + stalePullRequests: 0, + maintainerAuthoredPullRequests: 0, + collisionClusters: 0, + ageBuckets: { under7Days: 1, days7To30: 0, over30Days: 0 }, + likelyReviewablePullRequests: 1, + }, + findings: [], + ...overrides, + }; +} + +function config(overrides: Partial = {}): ConfigQuality { + return { + repoFullName: "owner/repo", + generatedAt: "2026-06-03T00:00:00.000Z", + score: 100, + level: "excellent", + lane: lane(), + configuredLabels: ["bug"], + observedLabels: ["bug"], + notObservedConfiguredLabels: [], + findings: [], + ...overrides, + }; +} + +function labels(overrides: Partial = {}): LabelAudit { + return { + repoFullName: "owner/repo", + generatedAt: "2026-06-03T00:00:00.000Z", + configuredLabels: ["bug"], + liveLabels: ["bug"], + observedLabels: [{ name: "bug", count: 2, configured: true, existsOnGitHub: true }], + missingConfiguredLabels: [], + suspiciousConfiguredLabels: [], + trustedPipelineReady: true, + findings: [], + ...overrides, + }; +} + +function intake(overrides: Partial = {}): ContributorIntakeHealth { + const queueHealth = queue(); + return { + repoFullName: "owner/repo", + generatedAt: "2026-06-03T00:00:00.000Z", + level: "healthy", + score: 90, + queueHealth: { + burdenScore: queueHealth.burdenScore, + level: queueHealth.level, + signals: queueHealth.signals, + }, + configLevel: "excellent", + duplicateClusters: 0, + reviewablePullRequests: 1, + summary: "Contributor intake is healthy.", + findings: [], + ...overrides, + }; +} + +function input(overrides: Partial = {}): RepoPolicyReadinessInput { + return { + repoFullName: "owner/repo", + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "required", + testExpectations: ["Run npm run test:ci."], + publicNotes: ["Prefer small, focused pull requests."], + }), + settings: settings({ requireLinkedIssue: true }), + lane: lane(), + configQuality: config(), + labelAudit: labels(), + queueHealth: queue(), + contributorIntakeHealth: intake(), + ...overrides, + }; +} + +describe("buildRepoPolicyReadiness", () => { + it("warns when direct-PR policy is loose", () => { + const report = buildRepoPolicyReadiness( + input({ + settings: settings({ requireLinkedIssue: false }), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "optional", + testExpectations: ["Run npm run test:ci."], + }), + }), + ); + + expect(report.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "direct_pr_policy_unclear", + category: "direct_pr_policy", + severity: "warning", + }), + ]), + ); + expect(report.publicWarnings.flatMap((warning) => [warning.title, warning.detail, warning.action]).every(isFocusManifestPublicSafe)).toBe(true); + }); + + it("warns about issue-discovery policy conflicts and intake gaps", () => { + const report = buildRepoPolicyReadiness( + input({ + lane: lane({ lane: "split", issueDiscoveryShare: 0.25, directPrShare: 0.75 }), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "required", + issueDiscoveryPolicy: "discouraged", + testExpectations: ["Run npm run test:ci."], + }), + contributorIntakeHealth: intake({ level: "strained" }), + }), + ); + + expect(report.publicWarnings.map((warning) => warning.code)).toEqual( + expect.arrayContaining([ + "issue_discovery_policy_mismatch", + "issue_discovery_intake_not_ready", + "maintainer_burden_high", + ]), + ); + }); + + it("warns when a direct-PR repo has issue-discovery encouraged in focus policy", () => { + const report = buildRepoPolicyReadiness( + input({ + lane: lane({ lane: "direct_pr" }), + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "required", + issueDiscoveryPolicy: "encouraged", + testExpectations: ["Run npm run test:ci."], + }), + }), + ); + + expect(report.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "issue_discovery_policy_mismatch", + severity: "info", + }), + ]), + ); + }); + + it("warns about missing validation expectations and uncertain validation gates", () => { + const report = buildRepoPolicyReadiness( + input({ + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "required", + }), + labelAudit: labels({ trustedPipelineReady: false }), + }), + ); + + expect(report.publicWarnings.map((warning) => warning.code)).toEqual( + expect.arrayContaining(["validation_expectations_missing", "validation_gate_uncertain"]), + ); + }); + + it("warns about maintainer burden before broader contributor traffic", () => { + const highQueue = queue({ level: "critical", burdenScore: 90 }); + const report = buildRepoPolicyReadiness( + input({ + queueHealth: highQueue, + contributorIntakeHealth: intake({ + level: "blocked", + queueHealth: { + burdenScore: highQueue.burdenScore, + level: highQueue.level, + signals: highQueue.signals, + }, + }), + }), + ); + + expect(report.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "maintainer_burden_high", + category: "maintainer_burden", + severity: "critical", + }), + ]), + ); + }); + + it("separates private owner context and keeps public warning text sanitized", () => { + const report = buildRepoPolicyReadiness( + input({ + focusManifest: parseFocusManifest({ + wantedPaths: ["src/"], + linkedIssuePolicy: "required", + testExpectations: ["Run npm run test:ci."], + maintainerNotes: [ + "Private reviewability note with wallet, hotkey, raw trust, and farming details.", + ], + publicNotes: ["Mention the reward estimate.", "Keep pull requests focused."], + }), + }), + ); + + expect(report.ownerContext).toMatchObject({ + privateNoteCount: 1, + manifestPresent: true, + }); + expect(JSON.stringify(report.publicWarnings)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + expect(JSON.stringify(report.ownerContext)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + expect(report.publicWarnings.map(policyReadinessWarningText).every(isFocusManifestPublicSafe)).toBe(true); + }); + + it("surfaces manifest normalization warnings without echoing raw warning text", () => { + const report = buildRepoPolicyReadiness( + input({ + focusManifest: parseFocusManifest({ + wantedPaths: "src/", + linkedIssuePolicy: "required", + testExpectations: ["Run npm run test:ci."], + }), + }), + ); + + expect(report.ownerContext.manifestWarningCount).toBeGreaterThan(0); + expect(report.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "focus_policy_needs_review" }), + ]), + ); + const firstWarning = report.publicWarnings[0]; + expect(firstWarning).toBeDefined(); + expect(policyReadinessWarningText(firstWarning!)).not.toMatch(/wantedPaths must be a list/); + }); + + it("drops public warnings when dynamic signal text is unsafe", () => { + const report = buildRepoPolicyReadiness( + input({ + lane: lane({ lane: "split" }), + contributorIntakeHealth: intake({ level: "wallet" as ContributorIntakeHealth["level"] }), + }), + ); + + expect(report.publicWarnings.map((warning) => warning.code)).not.toContain("issue_discovery_intake_not_ready"); + expect(report.droppedPublicWarnings).toEqual( + expect.arrayContaining([ + { code: "issue_discovery_intake_not_ready", reason: "unsafe_public_text" }, + ]), + ); + }); + + it("emits a preview-only missing-policy warning when no focus manifest is cached", () => { + const report = buildRepoPolicyReadiness(input({ focusManifest: parseFocusManifest(null) })); + + expect(report).toMatchObject({ + source: "focus_manifest_policy", + previewOnly: true, + present: false, + ownerContext: { manifestPresent: false, manifestSource: "none" }, + }); + expect(report.publicWarnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "focus_policy_missing" }), + ]), + ); + }); +});