# GitHub and Git Discipline Conventions for issues, labels, pull requests, branches, commits, and remotes in the PureXML repo. Commit-message format lives in docs/rules/commits.md; this file covers everything else. The repo ships issue forms, a PR template, a label set, and git hooks that enforce most of these rules mechanically. The rules below are the durable conventions those mechanisms encode. ## 1. Issue tracker ### Rule 1.1: Status block at the top, dated Every issue body carries a `## Status (YYYY-MM-DD)` heading as the first section. New-issue form templates make this required. When state changes (work starts, acceptance bullets ship, scope narrows, deps close), edit the status block in place with a new date line. The rest of the body stays as the original framing. **Why**: bodies without a dated status block age into fiction within a month. Issues with status blocks age well; the ones without age into wrong-default-value claims that shallow audits pass as "well-written, keep." ### Rule 1.2: No line numbers in issue bodies File references use symbol names, not `Foo.swift:142`. Lines drift on every PR; symbols do not. When you need a line anchor, write `Foo.swift (the searchSymbols function)` so the symbol survives even if the line moves. **Why**: line anchors like `SearchIndex.swift:2409` go dead within one PR cycle the moment a file is split or reordered. ### Rule 1.3: No phantom paths Every backtick-quoted file path in an issue body must EXIST in the repo (or in a declared sibling repo) at write time. A mechanical check on this is straightforward: extract backtick-quoted paths, check the filesystem, flag missing ones. **Why**: a cited file that was never written is a fabrication that propagates. The work it implies may be real, but the path is a lie until the file exists. File it as an issue instead of citing a path that is not there. ### Rule 1.4: Cross-reference hygiene When citing `#NNN` in blocker phrasing ("blocked on", "pending in", "depends on", "after #N lands", "gated on", "awaits", "waiting on"), the referenced issue must be OPEN at write time. When the referenced issue closes, edit the citing issue's body: either remove the cross-reference, or rewrite the surrounding sentence to say the dep shipped. **Why**: "blocked on #X" where #X closed weeks earlier is a silent dependency lie. Wrong issue numbers are worse: they point a reader at unrelated work. ### Rule 1.5: Schema and code claims are checkable Do not cite `.` shapes, function signatures, or default values you have not verified against current source. When the claim is structural (a schema column, a config default, a flag name), an audit script can mechanically verify it. Migrations move columns; flag defaults change; bodies do not auto-update. **Why**: a body claiming a column lives in table A when a migration moved it to table B, or claiming a flag defaults to `0.5s` when the real default is `0.05s` (off by 10x), reads as authoritative and misleads. Verify before you write. ### Rule 1.6: Issue templates use GitHub forms, not markdown Use `.yml` form templates, not `.github/ISSUE_TEMPLATE/feature.md` style markdown. Forms enforce structure mechanically at filing time; markdown templates only suggest it. Required fields: status date (input), priority (dropdown), complexity (dropdown). Required structured textareas per template kind (goal + acceptance for features; symptom + expected + reproduce + acceptance for bugs). **GitHub forms gotcha**: dropdown selections produce TEXT in the issue body under `### ` headings; they DO NOT auto-apply as labels. To make dropdowns actually apply matching labels, add a labeler workflow that triggers on `issues.opened`, extracts the value under each heading, validates against the known label set, and applies the matching label. **Why**: forms make filing discipline structural rather than suggestive. The labeler gotcha is easy to miss; the dropdowns look like they should label the issue, and they do not. ## 2. Labels ### Rule 2.1: Brutal-minimum 5-label set The canonical label set is **5 labels**. Labels exist only to mechanically partition the issue space; everything else lives in the issue body or in native GitHub primitives (issue state, Milestones, Project boards). | Label | Color | Use | |---|---|---| | `bug` | Red `#FF3B30` | Something ships incorrectly | | `enhancement` | Blue `#007AFF` | New feature, refactor, or design proposal | | `epic` | Purple `#AF52DE` | Aggregation parent of related issues; mechanically excluded from "missing kind" check | | `priority: high` | Red `#FF3B30` | Critical / release-gating / actively-blocking. Absence means "do when you can." | | `good first issue` | Green `#34C759` | Newcomer-friendly; GitHub renders specially in the contributor chooser | Kind is determined at filing time (feature template applies `enhancement`, bug template applies `bug`). Maintainers add `epic`, `priority: high`, `good first issue` post-filing where warranted. **What is not a label here:** - **Complexity.** Read the diff. If you need a label to tell readers the issue is hard, your prose is failing. - **Priority gradient.** `priority: medium` and `priority: low` collapse to "absence of `priority: high`." If the gradient does not change behaviour, the gradient is decoration. - **Topical categorisation** (search-quality, source-expansion, refactor, area: distribution, etc.). These are composition, not type. Encode in the body or in a milestone. - **Status / lifecycle** (`wishlist`, `transitional`, `blocker`, `big-win`, `fixed: awaiting release`, `released-in: v`). Status belongs to GitHub-native primitives (issue state, Milestones, Project boards), not to labels. **Why this size**: each surviving label has at least one mechanical reason it cannot fold into the body. Form templates apply kind at filing time; a staleness script partitions on `epic`; `priority: high` is the only triage tier (absence is implicit "later"); `good first issue` is GitHub-native. Three lenses converge on this set: - **Cut mercilessly**: cut even fine things that do not earn their place. A larger set still carries comfort-tags. - **Classify by type, not status**: every classification must justify its existence; status / lifecycle / release-tracking are not types, route them to native primitives. - **Prose over labels**: if you need a label to convey something, your prose is failing. Labels exist only for what mechanical tools must partition on. ### Rule 2.2: 3-carrier threshold for adding a new label If you feel the urge to add a sixth label, ask: does this label have at least 3 expected open carriers (today or within the next planning cycle)? AND does it survive each of the three lenses above? - Would you ship this label, or delete it? - Is this a type/axis, or is it status/lifecycle/composition pretending to be a type? - Does this label mechanically partition the issue space in a way grep / filter / a CI script must use, or is it commentary that belongs in the body? If any lens fails, fold the categorisation into the issue body. A one-line note in the body is as discoverable as a single-carrier label without the dropdown clutter. **Why**: single-carrier labels are footnotes pretending to be axes. Sprawling label sets stop being navigable past about 10. ### Rule 2.3: Apple system palette The 5 canonical labels use 4 hues from Apple's published system colour palette: | Hex | Color | Use | |---|---|---| | `#FF3B30` | Red | Urgent / blocking: `bug`, `priority: high` | | `#007AFF` | Blue | Active work: `enhancement` | | `#AF52DE` | Purple | Aggregation / epic: `epic` | | `#34C759` | Green | Newcomer-friendly / shipped: `good first issue` | If you add a sixth label under Rule 2.2, draw from Apple's other system colours by semantic grouping: | Hex | Color | Reserved for | |---|---|---| | `#FF9500` | Orange | Medium urgency | | `#FFCC00` | Yellow | Awaiting / pending | | `#00C7BE` | Mint | Low complexity (if you ever bring complexity back) | | `#30B0C7` | Teal | Topical / categorical | | `#32ADE6` | Cyan | Help / outreach | | `#5856D6` | Indigo | High complexity / structural | | `#FF2D55` | Pink | Celebration / high-impact | | `#A2845E` | Brown | Maintainer / internal | | `#8E8E93` | Gray | Speculative / wishlist | **Why Apple's palette**: 13 distinct hues with strong semantic associations. Picking from this set means labels render coherently in any view that supports colour (GitHub UI, project boards, third-party trackers reading from GitHub). ### Rule 2.4: Label deletion is destructive on closed issues `gh label delete ` removes the label from every issue that ever bore it, including closed issues. Use `gh label edit --name ` to rename when historical association matters. **Why**: closed issues are the project's audit trail. A `phase-1` label on closed refactor issues told a later reader "this was Phase 1 of that refactor"; deleting `phase-1` removes that context. Renaming preserves it. When in doubt: prefer rename over delete for labels with closed carriers. Brutal trims that delete closed-carrier labels should be a deliberate, documented choice (acceptable when the information still lives in Release notes, the CHANGELOG, and merge commits). ## 3. Pull requests ### Rule 3.1: One focused change per PR A PR ships one cohesive change. If the diff spans two unrelated concerns, split. Critic-fix iterations on the same change belong in the same PR (separate commits, same branch). Different concerns belong in different PRs even if they touch the same files. ### Rule 3.2: CHANGELOG required for non-trivial changes Projects that maintain a CHANGELOG require an entry per non-trivial PR. "Non-trivial" means: production source touched. Trivial means: docs / tests / scripts / configuration only. A mechanical pre-commit + CI check enforces this. Opt-out via a `[no-changelog]` token in the commit message body when the change genuinely does not warrant an entry. **Why**: PR descriptions live in the merge graph; CHANGELOG lives in the release. Without enforcement, source ships ahead of its description. A mechanical gate prevents the batch of PRs that merge without entries. ### Rule 3.3: Critic-fix loop on every non-trivial PR After opening a PR, do a self-critic pass: read your own diff as a reviewer would, find issues, fix them in additional commits on the same branch. Iterate until critique surfaces nothing new. Commit naming: `critic-fix(): `. The git history shows the iteration as separate commits; the PR diff shows the converged result. **Why**: mechanical edits without re-reading the surrounding paragraph create new bugs. A `sed` that injects a reference inside a sentence saying "this is independent" creates a contradiction. Critic-fix loops catch the self-introduced bugs that the original change did not have. ### Rule 3.4: PR head is never the canonical branch for release merges When merging a release branch into `main` (or `develop` into `main`, etc.), the PR head is a dedicated `release/v` branch, not the canonical working branch. Auto-delete-on-merge would kill the canonical branch otherwise. **Why**: the GitHub auto-delete-branch-on-merge feature is useful for short-lived feature branches and destructive for long-lived development branches. Always interpose a release branch as the head. ## 4. Branches ### Rule 4.1: Branch naming - `fix/-`; bug fixes (e.g. `fix/284-error-page-filter`) - `feat/`; features - `chore/`; tooling, infrastructure, non-functional cleanup - `docs/`; documentation-only changes - `refactor/`; structural code reorganisation - `release/v`; release-prep branches (PR head for canonical-branch merges per Rule 3.4) Issue-anchored prefixes (`fix/-...`) when an issue exists; otherwise topic-anchored. ### Rule 4.2: Branch from canonical base Branch from the current tip of the canonical base branch (the one the PR will target). For Swift projects this is usually `main` or `develop` depending on the project's branching model. `git fetch origin && git checkout -b origin/` is the safe form. Branching from a stale local copy of the base creates merge conflicts later. ### Rule 4.3: Issue-first workflow For every bug fix: file the GitHub issue first, then branch. The branch name carries the issue number; the PR auto-links via `Closes #N` in the description. For features: an issue is optional but recommended when scope is non-trivial or when the change crosses two or more bug-list cycles. **Why**: the issue is the audit trail. A branch + PR without an issue has no place for the symptom / repro / acceptance discussion. The PR body can hold that data but does not survive squash-merge as well as an issue does. ## 5. Commits See docs/rules/commits.md for commit message format. The following are additional GitHub-related conventions. ### Rule 5.1: No AI attribution Generated content carries no AI attribution in any committed text. Specifically forbidden: - Any AI co-author line in commit messages - Tool-attribution lines in PR descriptions, code comments, or documentation - The names of AI tools or vendors, or the words "AI", "LLM", "agent", "assistant", "automated", "machine-generated", or any synonym, in commits / PRs / branch names / file names that could end up in front of a colleague - AI-tell stylistic patterns: emoji signatures, "Here's a quick summary:" preludes, gratuitous emoji-prefixed bullets, "I'll now..." / "Let me..." preambles in committed text - Watermarks: zero-width characters, hidden HTML comments naming a tool, branch suffixes like `-ai` / `-bot` The work must read as entirely human-authored. The standard is "would a colleague reading this guess a tool wrote it?" If yes, remove it. For mechanical enforcement at the commit boundary, install the commit-message style hook described in Section 7. Rule 5.1 plus Rule 5.2 plus the hook together close the loop: the rule states intent, the hook refuses violations at commit time, so no commit reaches `git push` carrying either failure. ### Rule 5.2: No em dashes in committed text The em dash (the U+2014 character) is a recognisable writing tell. Replace with commas, periods, colons, semicolons, or restructure the sentence. Apply to commit messages, PR descriptions, code comments, issue bodies, documentation, README content; everything that lands in the repo or in front of a colleague. This is a stylistic rule, not a syntactic one. En dashes (U+2013) and hyphens (U+002D) are fine. Only the em dash is forbidden. Enforced mechanically by the commit-message style hook in Section 7 for commit messages. For file content, run the same byte check in a `pre-commit` hook over staged diffs. ### Rule 5.3: Verify after `git commit --amend --no-edit` `--no-edit` keeps the previous commit's message verbatim. After any amend with `--no-edit`, run `git log -1` to confirm the message still describes the new tree accurately. If the staged change made the previous message wrong, drop `--no-edit` and rewrite. **Why**: silent message staleness is the second-easiest commit-history bug to introduce (after force-pushing the wrong branch). The previous message gets anchored by `--no-edit` and lies about content that has since changed. ## 6. Remotes ### Rule 6.1: Push conventions Pushes follow the usual conventions: short-lived feature branches push freely; canonical branches (`main` / `develop`) only land via merge of an approved PR. ## 7. Mechanical enforcement A repo that adopts these rules should ship a mechanical backstop: - **Body-drift scan**: a scheduled script that greps open issue bodies for renamed paths, phantom paths, stale cross-refs, stale schema claims, and label drift. Output goes into a single tracking issue that gets updated on each run. - **CHANGELOG gate**: a pre-commit hook + CI gate that refuses commits / PRs touching production source without a CHANGELOG entry. - **Issue forms with labeler**: GitHub form templates for new issues + a workflow on `issues.opened` that translates form dropdown values into matching labels. - **Style-tell commit-msg hook**: a `commit-msg` hook that refuses messages containing em dashes (U+2014), the forbidden AI-attribution phrases from Rule 5.1, or AI-signature emojis. Keep it portable (BSD-grep compatible, no PCRE dependency). Install per repo by symlink or copy into `.git/hooks/commit-msg`. Strip comment lines and the diff scissors block before checking so notes in `# ...` lines are not penalised. **Why mechanical enforcement matters.** A disciplined author following Rule 5.2 can still push a commit whose message contains an em dash, because nothing at the commit boundary stops it. A 3-line `commit-msg` hook prevents this entire failure class at write time, after which `git push` cannot carry a violation regardless of who or what wrote the message. Discipline scales with attention; hooks do not. ## 8. CI workflows and truthful badges ### Rule 8.1: One workflow file per concern, one badge per workflow Each CI concern (style/gates, and each build-or-test platform) is its own workflow file with a unique `name:`. A README badge points at that one workflow's status endpoint, so the badge tracks exactly one job and is truthful. Do NOT use a single multi-job workflow (`ci.yml`) with several shields.io `?label=...` badges that all point at the same workflow. The `actions/workflow/status` endpoint reports the whole-workflow status; the `label=` is cosmetic, so every such badge renders the same result. A failure in any one job turns all of them red, and none of them tracks its named platform. That is a lying badge. Canonical layout (one file each, names are the badge labels): ``` .github/workflows/style.yml # name: Style and namespacing .github/workflows/swift-macos.yml # name: Swift (macOS) .github/workflows/swift-linux.yml # name: Swift (Linux) .github/workflows/swift-windows.yml # name: Swift (Windows) .github/workflows/swift-wasm.yml # name: Swift (WASM) ``` ### Rule 8.2: Native per-workflow badge markdown, plus a license badge Use GitHub's native per-workflow badge (it reflects the real status of that one workflow), not a shields.io workflow-status badge with a cosmetic label: ```markdown [![Style and namespacing](https://github.com///actions/workflows/style.yml/badge.svg)](https://github.com///actions/workflows/style.yml) [![Swift macOS](https://github.com///actions/workflows/swift-macos.yml/badge.svg)](https://github.com///actions/workflows/swift-macos.yml) [![Swift Linux](https://github.com///actions/workflows/swift-linux.yml/badge.svg)](https://github.com///actions/workflows/swift-linux.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ``` The license badge is static and links to the repo's `LICENSE`; match the label to the actual license (`MIT`, `AGPL--3.0`, etc.). ### Rule 8.3: Add a platform job and badge ONLY when that platform is genuinely possible A badge is a claim. Only claim a platform the repo can actually build or test on. Never ship a workflow or badge for a platform the package does not support; an absent badge is honest, a red-or-fictional badge is not. Decide possibility from the repo's topology (see `cross-platform.md`): - **Apple platforms (iOS, visionOS, tvOS, watchOS) and any UI build or test run on macOS runners only.** There is no "iOS CI on Linux" and no Windows/Linux UI. An iOS/visionOS app or a SwiftUI/UIKit/AppKit target is exercised by a macOS-hosted job (simulator or `xcodebuild -destination`), reported under the macOS badge or a dedicated `swift-ios.yml` that still `runs-on: macos`. Do not add a Linux or Windows badge for UI code. - **Linux** badge only when the product builds on a Linux toolchain (libraries, servers, CLIs). A pure Apple-UI repo has no Linux badge. - **Windows** badge only when the targets compile on Windows. If the test tooling is POSIX-only, Windows is a build-only gate: build the portable library targets (debug and release), skip the suite, and say so in a header comment. The badge then truthfully means "compiles on Windows." - **WASM/WASI** badge only when the package builds for `wasm32-unknown-wasip1`. Code that needs Foundation networking, threads, or other WASI-absent surface does not get a WASM badge. WASM is typically a build gate; running wasm tests needs a separate runtime and is usually out of scope. When a platform can build but not run the full suite, the workflow is a build gate, not a test gate. The header comment states the scope so the green badge is not read as "tests pass" when it means "compiles." ### Rule 8.4: Triggers and permissions are uniform across the split Every workflow uses the same trigger surface so the badges refresh together: `on: push: branches: [main]` plus `pull_request:` plus `workflow_dispatch:`. Keep `permissions:` least-privilege (`contents: read`, plus `issues: read` only where a gate reads issues, e.g. a roadmap check). The commit-message attribution gate (Rule 5.1) lives in the style workflow and needs `fetch-depth: 0` on checkout plus `BASE_SHA` / `HEAD_SHA` from the PR event to scan the commit range. ## Triggers (when to load this rule) Load this rule when: - Filing a new GitHub issue (Rule 1.1, Rule 1.2, Rule 1.3, Rule 1.6) - Editing an open issue body (Rule 1.1, Rule 1.4, Rule 1.5) - Adding or deleting a GitHub label (Rule 2.1, Rule 2.2, Rule 2.3, Rule 2.4) - Opening a PR (Rule 3.1, Rule 3.2, Rule 3.3, Rule 3.4) - Creating a branch (Rule 4.1, Rule 4.2, Rule 4.3) - Writing a commit message that will be pushed (Rule 5.1, Rule 5.2) - Running `git commit --amend` (Rule 5.3) - Running `git push` (Rule 6.1, Rule 6.2) - Designing a tracker-discipline mechanism for a new repo (Rule 7) - Setting up CI workflows or README status badges (Rule 8.1, Rule 8.2, Rule 8.3, Rule 8.4) Load on demand per the trigger; do not auto-load.