From f2e9b27df514a3257c2b6c4fe218f5ba3ef9df1a Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:59:45 +0530 Subject: [PATCH] adding CONTRIBUTING.md --- .github/ISSUE_TEMPLATE/bug_report.yml | 218 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 83 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 47 ++++ .github/workflows/close-unlabeled-issues.yml | 83 +++++++ .github/workflows/pr-template-check.yml | 74 +++++++ .github/workflows/stale-needs-info.yml | 70 ++++++ .github/workflows/triage-needs-info.yml | 154 +++++++++++++ CONTRIBUTING.md | 116 ++++++++++ 9 files changed, 853 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-unlabeled-issues.yml create mode 100644 .github/workflows/pr-template-check.yml create mode 100644 .github/workflows/stale-needs-info.yml create mode 100644 .github/workflows/triage-needs-info.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..48a9a8f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,218 @@ +name: Bug report +description: Report a reproducible bug (one per issue). +title: "[Bug]: short summary here" +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. + + If we can reproduce it, we can usually fix it. This form is just to get the basics in one place. + Please replace the default title with a short summary of the actual problem. + + If the app crashes, logs are required. Crash reports without logs may be labeled `needs-info`. + + - type: markdown + attributes: + value: | + ## Quick checks + + - type: checkboxes + id: checks + attributes: + label: Pre-flight checks + options: + - label: I searched existing issues and this is not a duplicate. + required: true + - label: I can reproduce this on the latest release or latest main build. + required: false + - label: This issue is limited to a single bug (not multiple unrelated problems). + required: true + - label: This is not a source/stream-specific error (the issue happens regardless of which source is used). + required: true + + - type: markdown + attributes: + value: | + ## Version & device + + - type: input + id: app_version + attributes: + label: App version / commit + description: Release version (e.g. 1.2.3) or commit hash. If unsure, say where you got the build from. + placeholder: "e.g. 1.2.3, or main@abc1234" + validations: + required: true + + - type: dropdown + id: install_method + attributes: + label: Install method + options: + - GitHub Release / App Store / Play Store + - TestFlight + - CI build / Nightly + - Built from source + - Other (please describe below) + validations: + required: true + + - type: dropdown + id: platform + attributes: + label: Platform + options: + - Android (phone/tablet) + - iOS (iPhone) + - iOS (iPad) + - macOS (desktop) + - Windows (desktop) + - Linux (desktop) + - Android emulator + - iOS simulator + - Other (please describe below) + validations: + required: true + + - type: input + id: device_model + attributes: + label: Device model + description: "Example: iPhone 15 Pro, Pixel 8, MacBook Air M2, etc." + placeholder: "e.g. iPhone 15 Pro" + validations: + required: true + + - type: input + id: os_version + attributes: + label: OS version + placeholder: "e.g. iOS 18.1, Android 14, macOS 15.2" + validations: + required: true + + - type: markdown + attributes: + value: | + ## What happened? + + - type: dropdown + id: area + attributes: + label: Area (tag) + description: Pick the closest match. It helps triage. + options: + - Playback (start/stop/buffering) + - Streams / Sources (selection, loading, errors) + - Next Episode / Auto-play + - Watch Progress (resume, watched state, history) + - Subtitles (download, styling, sync) + - Audio tracks + - Navigation / Gestures + - UI / Layout + - Settings + - Sync (Trakt / remote) + - Downloads / Offline + - Platform-specific (iOS only / Android only / Desktop only) + - Other + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Exact steps. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links. + placeholder: | + 1. Open ... + 2. Navigate to ... + 3. Tap / Click ... + 4. Observe ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + placeholder: "What you expected to happen." + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + placeholder: "What actually happened (include any on-screen error text/codes)." + validations: + required: true + + - type: dropdown + id: frequency + attributes: + label: Frequency + options: + - Always + - Often (more than 50%) + - Sometimes + - Rarely + - Once + validations: + required: true + + - type: dropdown + id: regression + attributes: + label: Did this work before? + options: + - Not sure + - Yes, it used to work + - No, it never worked + validations: + required: true + + - type: markdown + attributes: + value: | + ## Extra context (optional) + + - type: textarea + id: media_details + attributes: + label: Media details (optional) + description: Only include what you can safely share. + placeholder: | + - Content type: series/movie + - Season/Episode: S1E2 + - Stream/source: (addon name / source label) + - Video format: (if known) + validations: + required: false + + - type: textarea + id: logs + attributes: + label: Logs (required for crash reports) + description: | + Required if the app crashes or force closes. + For other bug reports, logs are optional but still helpful. + + **Android:** `adb logcat -d | tail -n 300` + **iOS:** Crash log from Xcode Organizer or Console.app + **Desktop:** Terminal/console output from around the time the issue occurred + render: shell + placeholder: | + Paste logs here + validations: + required: false + + - type: textarea + id: extra + attributes: + label: Anything else? (optional) + description: Screenshots/recordings, related issues, workarounds, etc. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..421f8d0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Downloads / Releases + url: https://github.com/NuvioMedia/NuvioMobile/releases + about: Grab the latest release here. + - name: Documentation + url: https://github.com/NuvioMedia/NuvioMobile/blob/main/README.md + about: Read the README for setup and usage details. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..e77efb55 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,83 @@ +name: Feature request +description: Suggest an improvement or new feature. +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + One feature request per issue, please. The more real-world your use case is, the easier it is to evaluate. + + Feature requests are reviewed as product proposals first. + Please do not open a pull request for a new feature, major UX change, or broad cosmetic update unless a maintainer has explicitly approved it first. + Unapproved feature PRs will usually be closed. + + **Any large PR or change that is not a simple bug fix MUST be discussed and approved via a feature request issue first.** + PRs that introduce large changes without a linked, approved feature request **will not be reviewed at all** and will be closed immediately. No exceptions. + + - type: dropdown + id: area + attributes: + label: Area (tag) + options: + - Playback + - Streams / Sources + - Next Episode / Auto-play + - Watch Progress + - Subtitles + - Audio + - Navigation / Gestures + - UI / Layout + - Settings + - Sync (Trakt / remote) + - Downloads / Offline + - Platform-specific (iOS only / Android only / Desktop only) + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem statement + description: What problem are you trying to solve? + placeholder: "I want to be able to..." + validations: + required: true + + - type: textarea + id: proposed + attributes: + label: Proposed solution + description: What would you like the app to do? + validations: + required: true + + - type: dropdown + id: contribution_plan + attributes: + label: Are you planning to implement this yourself? + description: Major features are usually implemented in-house unless approved first. + options: + - No, this is only a proposal + - Maybe, but only if approved first + - Yes, but I understand implementation still needs maintainer approval + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered (optional) + description: Any workarounds or other approaches you considered. + validations: + required: false + + - type: textarea + id: extra + attributes: + label: Additional context (optional) + description: Mockups, examples from other apps, etc. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..deca0aac --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,47 @@ +## Summary + + + +## PR type + + +- Bug fix +- Small maintenance improvement +- Docs fix +- Translation update +- Approved larger change (link approval below) + +## Why + + + +## Policy check + + +- [ ] This PR is not cosmetic-only, unless it is a translation PR. +- [ ] This PR does not add a new major feature without prior approval. +- [ ] This PR is small in scope and focused on one problem. +- [ ] If this is a larger or directional change, I linked the **approved** feature request issue below. + +> **Large PRs without a linked, approved feature request issue will be closed immediately without review. No exceptions.** + +## Approved feature request (required for large/non-trivial PRs) + + + + +## Testing + + + +## Screenshots / Video (UI changes only) + + + +## Breaking changes + + + +## Linked issues + + diff --git a/.github/workflows/close-unlabeled-issues.yml b/.github/workflows/close-unlabeled-issues.yml new file mode 100644 index 00000000..90e31b65 --- /dev/null +++ b/.github/workflows/close-unlabeled-issues.yml @@ -0,0 +1,83 @@ +name: Close unlabeled issues + +on: + schedule: + - cron: "23 6 * * *" # daily + workflow_dispatch: + inputs: + dry_run: + description: Log matching issues without commenting or closing them + required: false + type: boolean + default: false + +permissions: + issues: write + +jobs: + close_unlabeled: + runs-on: ubuntu-latest + steps: + - name: Close open issues with no labels + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const inputs = context.payload.inputs || {}; + + const dryRun = String(inputs.dry_run || "false").toLowerCase() === "true"; + const closeMarker = ""; + + const items = await github.paginate(github.rest.search.issuesAndPullRequests, { + q: `repo:${owner}/${repo} is:issue is:open no:label`, + per_page: 100, + }); + + core.info(`Found ${items.length} open unlabeled issues.`); + + for (const item of items) { + const issueNumber = item.number; + + if (dryRun) { + core.info(`#${issueNumber}: would comment and close.`); + continue; + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issueNumber, + per_page: 100, + }); + + const alreadyCommented = comments.some(comment => + (comment.body || "").includes(closeMarker) + ); + + if (!alreadyCommented) { + const body = + `${closeMarker}\n` + + `Sorry about the churn here, and thanks for taking the time to report this.\n\n` + + `Closing this issue because it does not have any labels.\n\n` + + `Issue labels and triage rules were introduced later, so older unlabeled issues are no longer tracked reliably.\n\n` + + `If you are still facing this problem, please open a new issue using the appropriate issue template and label so it can be triaged correctly.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + }); + } + + await github.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + state: "closed", + state_reason: "not_planned", + }); + + core.info(`#${issueNumber}: closed.`); + } diff --git a/.github/workflows/pr-template-check.yml b/.github/workflows/pr-template-check.yml new file mode 100644 index 00000000..a24b4709 --- /dev/null +++ b/.github/workflows/pr-template-check.yml @@ -0,0 +1,74 @@ +name: PR Template Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + validate_pr_body: + runs-on: ubuntu-latest + steps: + - name: Validate required PR sections + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const body = (pr.body || "").trim(); + + function sectionContent(title) { + const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const re = new RegExp(`^##\\s+${escaped}\\s*$`, "m"); + const match = body.match(re); + if (!match) return null; + const start = match.index + match[0].length; + const rest = body.slice(start); + const next = rest.search(/^##\s+/m); + return (next === -1 ? rest : rest.slice(0, next)).trim(); + } + + const required = ["Summary", "Why", "Testing", "Breaking changes", "Linked issues"]; + const missing = []; + const empty = []; + + for (const name of required) { + const content = sectionContent(name); + if (content === null) { + missing.push(name); + continue; + } + + const cleaned = content + .replace(//g, "") + .replace(/`/g, "") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + if ( + cleaned.length < 4 || + cleaned === "none" || + cleaned.includes("what changed in this pr") || + cleaned.includes("why this change is needed") || + cleaned.includes("what you tested") || + cleaned.includes("example: fixes #123") + ) { + empty.push(name); + } + } + + if (missing.length || empty.length) { + const lines = [ + "PR description is missing required detail.", + "", + ]; + if (missing.length) lines.push(`Missing sections: ${missing.join(", ")}`); + if (empty.length) lines.push(`Incomplete sections: ${empty.join(", ")}`); + lines.push(""); + lines.push("Please fill the PR template before merging."); + core.setFailed(lines.join("\n")); + } else { + core.info("PR template check passed."); + } diff --git a/.github/workflows/stale-needs-info.yml b/.github/workflows/stale-needs-info.yml new file mode 100644 index 00000000..803b4334 --- /dev/null +++ b/.github/workflows/stale-needs-info.yml @@ -0,0 +1,70 @@ +name: Close stale needs-info issues + +on: + schedule: + - cron: "17 6 * * *" # daily + workflow_dispatch: {} + +permissions: + issues: write + +jobs: + close_stale: + runs-on: ubuntu-latest + steps: + - name: Warn then close inactive needs-info + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const NEEDS_INFO = "needs-info"; + const WARN_AFTER_DAYS = 14; + const CLOSE_AFTER_DAYS = 21; + + const warnMarker = ""; + const closeMarker = ""; + + const now = Date.now(); + const warnCutoff = now - WARN_AFTER_DAYS * 24 * 60 * 60 * 1000; + const closeCutoff = now - CLOSE_AFTER_DAYS * 24 * 60 * 60 * 1000; + + async function listOpenNeedsInfoIssues() { + const q = `repo:${owner}/${repo} is:issue is:open label:"${NEEDS_INFO}"`; + const res = await github.rest.search.issuesAndPullRequests({ q, per_page: 50 }); + return res.data.items || []; + } + + const items = await listOpenNeedsInfoIssues(); + for (const item of items) { + const issue_number = item.number; + const updatedAtMs = new Date(item.updated_at).getTime(); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + const hasWarned = comments.some(c => (c.body || "").includes(warnMarker)); + const hasClosedComment = comments.some(c => (c.body || "").includes(closeMarker)); + + if (updatedAtMs <= closeCutoff && hasWarned && !hasClosedComment) { + const body = + `${closeMarker}\n` + + `Closing this for now since we didn't get the requested details.\n\n` + + `If you can share the missing info, reply here and we can reopen.`; + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + await github.rest.issues.update({ owner, repo, issue_number, state: "closed" }); + continue; + } + + if (updatedAtMs <= warnCutoff && !hasWarned) { + const body = + `${warnMarker}\n` + + `Just a quick ping: this issue is labeled \`${NEEDS_INFO}\` and hasn't had any updates in a bit.\n\n` + + `If you can add the missing details, we can keep going. Otherwise it may be closed after a grace period.`; + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + } diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml new file mode 100644 index 00000000..1073a678 --- /dev/null +++ b/.github/workflows/triage-needs-info.yml @@ -0,0 +1,154 @@ +name: Triage (needs-info) + +on: + issues: + types: [opened, edited, reopened] + +permissions: + issues: write + +jobs: + needs_info: + runs-on: ubuntu-latest + steps: + - name: Label low-context issues + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + const issue = context.payload.issue; + const title = (issue.title || "").trim(); + const body = issue.body || ""; + const labels = (issue.labels || []).map(l => (typeof l === "string" ? l : l.name).toLowerCase()); + + const NEEDS_INFO = "needs-info"; + const NEEDS_INFO_COLOR = "d4c5f9"; + const NEEDS_INFO_DESC = "More details needed to reproduce / triage."; + + function hasLabel(name) { + return labels.includes(name.toLowerCase()); + } + + function extractSection(title) { + const re = new RegExp(`^###\\s+${title.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*$`, "m"); + const match = body.match(re); + if (!match) return ""; + const start = match.index + match[0].length; + const rest = body.slice(start); + const next = rest.search(/^###\s+/m); + const section = (next === -1 ? rest : rest.slice(0, next)); + return section.trim(); + } + + function extractFirstSection(titles) { + for (const sectionTitle of titles) { + const value = extractSection(sectionTitle); + if (value) return value; + } + return ""; + } + + function normalizeText(value) { + return (value || "").replace(/\s+/g, " ").trim(); + } + + function stripIssuePrefix(value) { + return normalizeText(value).replace(/^\[[^\]]+\]:\s*/i, "").trim(); + } + + const steps = extractSection("Steps to reproduce"); + const expected = extractSection("Expected behavior"); + const actual = extractSection("Actual behavior"); + const logs = extractFirstSection([ + "Logs (required for crash reports)", + "Logs (optional but helpful)", + ]); + const extra = extractSection("Anything else? (optional)"); + const summaryTitle = stripIssuePrefix(title); + + const looksLikeBugForm = !!(steps || expected || actual); + const isBugIssue = hasLabel("bug") || looksLikeBugForm; + const isFeatureIssue = + hasLabel("enhancement") || + hasLabel("feature") || + hasLabel("feature request"); + + if (!isBugIssue || isFeatureIssue) { + return; + } + + const problems = []; + const genericTitle = /^(bug|issue|problem|help|question|crash|broken|error|bug report|short summary here|title here)$/i; + const numericOnlyTitle = /^#?\d+$/; + const crashPattern = /\b(crash|crashes|crashed|crashing|force close|force closes|force closed|fatal exception|app closes|app closed unexpectedly)\b/i; + const crashContext = [summaryTitle, steps, actual, extra].map(normalizeText).join("\n"); + const isCrashIssue = crashPattern.test(crashContext); + const normalizedLogs = normalizeText(logs); + const hasLogs = normalizedLogs.length >= 20 && !/^(n\/a|na|none|no|not available)$/i.test(normalizedLogs); + + if (!summaryTitle || summaryTitle.length < 8 || genericTitle.test(summaryTitle) || numericOnlyTitle.test(summaryTitle)) { + problems.push("Issue title (replace the default `[Bug]:` prefix with a short summary of the actual problem)"); + } + if (!steps || steps.length < 30) problems.push("Steps to reproduce (please list exact steps)"); + if (!expected || expected.length < 10) problems.push("Expected behavior"); + if (!actual || actual.length < 10) problems.push("Actual behavior (include any on-screen error text)"); + if (isCrashIssue && !hasLogs) { + problems.push("Logs (required for crash reports; include a log snippet or stack trace)"); + } + + async function ensureLabel(name, color, description) { + try { + await github.rest.issues.getLabel({ owner, repo, name }); + } catch (e) { + try { + await github.rest.issues.createLabel({ owner, repo, name, color, description }); + } catch (_) {} + } + } + + const hasNeedsInfo = hasLabel(NEEDS_INFO); + + if (problems.length > 0) { + await ensureLabel(NEEDS_INFO, NEEDS_INFO_COLOR, NEEDS_INFO_DESC); + if (!hasNeedsInfo) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [NEEDS_INFO], + }); + } + + const marker = ""; + const commentBody = + `${marker}\n` + + `Thanks for the report. Could you add a bit more detail so we can reproduce it?\n\n` + + `Missing / too short:\n` + + problems.map(p => `- ${p}`).join("\n") + + `\n\n` + + `Use a specific title, for example: \`[Bug]: Playback freezes when switching audio tracks on iOS\`.\n` + + `${isCrashIssue ? `Crash reports must include logs.\n` : `Logs are optional for most issues, but they help a lot.\n`}`; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + const alreadyCommented = comments.some(c => (c.body || "").includes(marker)); + if (!alreadyCommented) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: commentBody, + }); + } + } else if (hasNeedsInfo) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name: NEEDS_INFO }); + } catch (_) {} + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3eaf8ae3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing + +Thanks for helping improve Nuvio. + +## Strict rules — read before opening anything + +These rules are enforced strictly. Issues and PRs that do not follow them will be closed without review. + +--- + +## PR policy + +Pull requests are currently intended for: + +- Reproducible bug fixes +- Small stability improvements +- Minor maintenance work +- Small documentation fixes that improve accuracy +- Translation updates + +Pull requests are generally **not** accepted for: + +- New major features +- Product direction changes +- Large UX / UI redesigns +- Cosmetic-only changes +- Refactors without a clear user-facing or maintenance benefit + +Translation PRs are allowed, as long as they stay focused on translation/localization work and do not bundle unrelated feature or UI changes. + +### Large PRs and large changes + +**Any large PR or change that is not a simple bug fix must be discussed and approved via a feature request issue first.** + +1. Open a **Feature Request** issue describing the change. +2. Wait for explicit maintainer approval on that issue. +3. Link the approved issue in your PR description. + +PRs that introduce large changes without a linked, approved feature request **will not be reviewed at all** and will be closed immediately. No exceptions. + +This applies to — but is not limited to — UI changes, new features, architecture changes, dependency additions, and large refactors. + +--- + +## Where to ask questions + +- Use **Issues** for bugs, feature requests, setup help, and general support. + +--- + +## Bug reports (rules) + +To keep issues fixable, bug reports should include: + +- A short, specific issue title that describes the bug +- App version (release version or commit hash) +- Platform (Android / iOS / Desktop) + device model + OS version +- Install method (release build / TestFlight / CI / built from source) +- Steps to reproduce (exact steps) +- Expected vs actual behavior +- Frequency (always/sometimes/once) + +Do not leave the title as just `[Bug]:` or another generic placeholder. + +Logs are optional for most issues, but they are **required** for crash / force-close reports. + +### How to capture logs (optional) + +**Android:** + +```sh +adb logcat -d | tail -n 300 +``` + +**iOS:** + +Attach a crash log from Xcode Organizer or Console.app, or reproduce while connected to Xcode and copy the relevant log output. + +**Desktop:** + +Copy the relevant terminal/console output from around the time the issue occurred. + +--- + +## Feature requests (rules) + +Please include: + +- The problem you are solving (use case) +- Your proposed solution +- Alternatives considered (if any) + +Opening a feature request does **not** mean a pull request will be accepted for it. If the feature affects product scope, UX direction, or adds a significant new surface area, do not start implementation unless a maintainer explicitly approves it first. + +**Large changes require an approved feature request before any PR is submitted.** See the [Large PRs and large changes](#large-prs-and-large-changes) section above. + +--- + +## Before opening a PR + +Please make sure your PR is all of the following: + +- Small in scope +- Focused on one problem +- Clearly aligned with the current direction of the project +- Not cosmetic-only, unless it is a translation PR +- Not a new major feature unless it was discussed and approved first +- **If large or non-trivial: linked to an approved feature request issue** + +PRs that do not fit this policy will be closed without merge so review time can stay focused on bugs, regressions, and small improvements. + +--- + +## One issue per problem + +Please open separate issues for separate bugs/features. It makes tracking, fixing, and closing issues much faster.