Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .claude/skills/open-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ git diff --name-only origin/main...
A `/verify-*` gate only counts if the **diff's own flow** was driven —
rendering a proxy or asserting a 200 is not verification.

**Changelog gate:** every non-exempt PR adds its entries to
`CHANGELOG.md` under `[Unreleased]` before the PR opens (rule +
exemptions in CLAUDE.md; the `/pull_request` command enforces the
same gate and appends the PR number after creation).
**Changelog gate:** every non-exempt PR adds `changelog.d/<slug>.md`
before the PR opens and touches `CHANGELOG.md` not at all — that shared
spot is where sibling PRs conflict (rule + exemptions in CLAUDE.md,
format in `changelog.d/README.md`; the `/pull_request` command enforces
the same gate and appends the PR number after creation). Locally:
`uv run python -m tools.changelog check --base origin/main`.

Then the local CI equivalents — the same commands the pipeline runs.
**Hard gate: do not open the PR while any of these is red.**
Expand Down
8 changes: 4 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ A companion guide `CLAUDE.md` at the repo root carries the shared rules for Clau

- **Always write in English** - All output text (code comments, commit messages, PR descriptions, issue comments, documentation) must be in English, even if the user writes in another language.
- **Repository prose follows the Google developer documentation style guide** - `docs/`, `README.md`, `agentic/docs/`, changelog entries, and PR/issue text use [Google style](https://developers.google.com/style) (sentence-case headings, second person, numbered procedures); the concrete rules and the house-style exception (`docs/reference/style-guide.md` governs website/brand surfaces) live in the `write-docs` skill (`.claude/skills/write-docs/SKILL.md`). Existing docs migrate on touch, not via bulk rewrites.
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR updates `CHANGELOG.md`, a release moves the section and bumps the version files via a PR, and the GitHub release is that section condensed, never copied. When reviewing a non-exempt PR that lacks a changelog entry, flag it.
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR adds a fragment under `changelog.d/` and never edits `CHANGELOG.md`, a release folds the fragments and bumps the version files via a PR, and the GitHub release is that section condensed, never copied. When reviewing a non-exempt PR that lacks a fragment — or that writes a bullet into `[Unreleased]` — flag it.
- **Never echo secret values into transcripts or logs** - Verify secrets by exit code or metadata, never by printing them.
- **Structural fix over symptomatic fix** - When a cheap symptomatic fix and a correct structural fix compete, take the structural one: fix the cause, never mute the alarm. Never modify working code to make a broken test pass — fix the test or flag it.
- **Never manually merge a pipeline PR, and never bypass the pipeline** - Specifications and implementations go through the GitHub Actions workflows: `spec-create.yml` writes `plots/{spec-id}/specification.md` and `.yaml`, `impl-merge.yml` merges implementation PRs and creates their `metadata/*.yaml`, promotes the preview images to GCS and sets the `impl:{library}:done` label. Merging one by hand skips all of that and leaves `quality_score: null`, no review data, missing GCS images and the issue open. So: never write those files by hand, never run `gh pr merge` on a spec or implementation PR, and add the `approved` label to the **issue**, never to the PR. The full DON'T/DO table is in `CLAUDE.md`. Ordinary (non-pipeline) PRs are merged by the repository owner, not by an agent.

## Changelog + releases

- **Every PR updates `CHANGELOG.md`** under `[Unreleased]` (Keep-a-Changelog categories, English, bold-titled bullets for headline entries, PR refs — like the existing entries) — that file is how releases get posted; a PR without its entry is incomplete. **Exempt:** the automated plot pipeline's output (spec-create, impl-generate/review/repair/merge, spec auto-polish, daily-regen PRs) and individual Dependabot bumps — those are summarized in aggregate at release time (see `agentic/commands/release.md`). This rule is duplicated in `CLAUDE.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **A release** moves `[Unreleased]` under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), adds the aggregate lines for the exempt classes (the italic *Catalog* line, the single **Dependencies:** bullet), and bumps the version files (`pyproject.toml` `project.version`, `uv.lock`, `app/package.json`) in the same commit, via a `release/vX.Y.Z` PR. Every bullet already carries its PR reference — `/pull_request` appends it when the PR opens. After the merge the tag goes on the merge commit and the GitHub release is created from the section.
- **Every PR adds `changelog.d/<slug>.md`, and NEVER a bullet in `CHANGELOG.md`** — the fragment is a slice of the changelog in the changelog's own format (`### Category` over bold-titled English bullets with PR refs; the rules and an example are in `changelog.d/README.md`). That shared `[Unreleased]` spot is where sibling PRs used to conflict each other — three times in one night on 2026-09-02/03 — and the CI job "Changelog (fragment)" refuses both a missing fragment and a bullet written into `[Unreleased]` directly. Same check locally: `uv run python -m tools.changelog check --base origin/main`. **Exempt:** catalogue-only PRs (everything under `plots/`), the automated plot pipeline (`github-actions[bot]`) and Dependabot summarized in aggregate at release time (see `agentic/commands/release.md`) — plus any PR labelled `skip-changelog`. This rule is duplicated in `CLAUDE.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **A release** runs `uv run python -m tools.changelog release X.Y.Z --title "<codename>"`, which folds every fragment under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), bumps the version files (`pyproject.toml` `project.version`, `uv.lock`, `app/package.json`), repoints the compare links and deletes the fragments. The two aggregate lines for the exempt classes stay by hand (the italic *Catalog* line, the single **Dependencies:** bullet) because they summarize a window rather than a PR. All of it on a `release/vX.Y.Z` PR. Every bullet already carries its PR reference — `/pull_request` appends it when the PR opens. After the merge the tag goes on the merge commit and the GitHub release is created from the section.
- **A GitHub release is that section condensed, never copied:** an intro line (merge count, PR range, link to `CHANGELOG.md`); the section's own `### Added / Changed / Removed / Fixed` headings in the section's order (an empty one is omitted); one bullet per NOTABLE entry — chores, dependency bumps and small fixes are left out, no fixed count — each at most two lines: its bold title, one clause with the essence or the headline number, its PR reference; a compare link (`compare/vPREV...vNEW`) as the last line. Numbers are copied exactly; only PR numbers from the section are cited; the full text lives only in the CHANGELOG — the release page is the index into it. The cut procedure itself is `agentic/commands/release.md`.

## Task Suitability
Expand Down Expand Up @@ -276,7 +276,7 @@ Before completing any task:
4. Type hints are included for all new functions
5. Docstrings follow Google style for public functions
6. Add integration tests for database-related changes (repositories, models)
7. Non-exempt changes have a `CHANGELOG.md` entry under `[Unreleased]` (see Important Rules)
7. Non-exempt changes carry a `changelog.d/<slug>.md` fragment and leave `CHANGELOG.md` alone (see Important Rules)

## Database

Expand Down
5 changes: 3 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Dependabot bumps ignore this template — they are summarized at release time.

## Checklist

- [ ] `CHANGELOG.md` updated under `[Unreleased]` (Keep-a-Changelog categories,
bold-titled bullets, PR ref) — required for every non-pipeline PR
- [ ] `changelog.d/<slug>.md` added (Keep-a-Changelog categories, bold-titled
bullets, PR ref — `changelog.d/README.md`), and `CHANGELOG.md` left alone
— required for every non-pipeline PR
- [ ] Related documentation updated (e.g. `docs/reference/`, `docs/workflows/`,
`docs/contributing.md`) if behavior changed
78 changes: 78 additions & 0 deletions .github/workflows/ci-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Every PR carries a changelog fragment (changelog.d/<slug>.md) instead of a
# bullet in the shared CHANGELOG.md.
#
# That shared spot is where sibling PRs conflicted each other — three times in
# one night on 2026-09-02/03, each a hand-resolved rebase for text neither
# branch disagreed about. The .gitattributes line endings do nothing for it, a
# union merge driver would heal only the local rebase (GitHub's own mergeability
# check ignores merge drivers), and a branch that MOVES changelog lines comes out
# of a union rebase with the block duplicated. Fragments remove the spot instead
# of healing it. The sibling repo kurrentschrift runs the same gate over the same
# tool; keep the two in the same shape.
#
# Only runs on pull_request: a push to main has no base to diff against, and the
# gate is a statement about a PR, not about a commit.

name: "CI: Changelog"
run-name: "Changelog: ${{ github.ref_name }}"

on:
pull_request:
# `labeled`/`unlabeled` on top of the default three. The `skip-changelog`
# escape hatch is applied AFTER the gate has gone red, and the default
# activity types do not include labelling — so without these the label would
# be documented as the way out while the check stayed red until the next
# push (Copilot review).
types: [opened, synchronize, reopened, labeled, unlabeled]
branches:
- main
- develop
- 'specification/**'
- 'implementation/**'

concurrency:
group: ci-changelog-${{ github.ref }}
cancel-in-progress: true

jobs:
fragment:
name: Changelog (fragment)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
# The two exempt authors, which are exactly the classes CLAUDE.md already
# excuses and a release summarizes in aggregate:
#
# * github-actions[bot] — the automated plot pipeline (spec-create,
# impl-generate/review/repair/merge, spec auto-polish, daily-regen). Its
# PRs touch only `plots/`, so the tool's own path exemption would pass
# them anyway; skipping by author as well keeps the gate off the hundreds
# of them per window entirely.
# * dependabot[bot] — a bot can neither write a fragment nor reach for the
# label, so the gate would sit red on every Monday batch. A bump that DOES
# deserve a line reaches the changelog through the human PR carrying it.
#
# `skip-changelog` is the human escape hatch, for a PR that truly changes
# nothing worth a line.
if: >-
!contains(github.event.pull_request.labels.*.name, 'skip-changelog')
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& github.event.pull_request.user.login != 'github-actions[bot]'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The gate diffs against the base branch, which a shallow checkout lacks.
fetch-depth: 0

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

# `--no-project`: the tool is standard library only, so the gate answers
# in seconds without syncing pandas, scipy and scikit-learn.
- name: Fragment present and well-formed
run: uv run --no-project python -m tools.changelog check --base "origin/${{ github.base_ref }}"
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ All notable changes to this project are documented here. The format is based on
rather than library SemVer: major for milestone releases, minor for feature batches, patch for
fix-only (see `agentic/commands/release.md`).

Every non-exempt PR adds its entries under `[Unreleased]`, each bullet with its PR reference. A
release moves that section under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`)
and bumps `pyproject.toml` (`project.version`), `uv.lock` and `app/package.json` in the same
commit, via a PR (procedure: `agentic/commands/release.md`). After the merge the tag goes on the
**Do not edit this file in a feature PR.** Every non-exempt PR adds one fragment,
`changelog.d/<slug>.md`, in this file's own format, each bullet with its PR reference. The shared
`[Unreleased]` spot is where sibling PRs used to conflict each other, so the CI job
"Changelog (fragment)" refuses both a missing fragment and a bullet written in here directly
(format and exemptions: `changelog.d/README.md`). A release runs
`uv run python -m tools.changelog release X.Y.Z --title "<codename>"`, which folds every fragment
under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), bumps `pyproject.toml`
(`project.version`), `uv.lock` and `app/package.json`, repoints the compare links at the bottom of
this file and deletes the fragments — one commit, via a PR (procedure:
`agentic/commands/release.md`). After the merge the tag goes on the
merge commit and the GitHub release is created from the section — condensed, never copied (rule
of 2026-08-28): an intro line with the merge count, the PR range and a link to this file; the
section's own headings in the section's order (an empty one is omitted); one bullet per NOTABLE
Expand Down
Loading
Loading