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
72 changes: 72 additions & 0 deletions .claude/commands/release-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
description: Draft the Unreleased section of CHANGELOG.md from merged pull requests
argument-hint: "[--since <ref|date>] [--write]"
allowed-tools: Bash(gh:*), Bash(git:*), Read, Edit
---

Draft release notes from merged pull requests and place them in the `## Unreleased`
section of `CHANGELOG.md`.

Arguments: `$ARGUMENTS`

## Determining the range

Use `--since` if given (a git ref or an ISO date). Otherwise use the most recent
version tag:

```bash
git tag -l 'v*' --sort=-v:refname | head -1
```

The repository may have no version tags yet. If there is none and no `--since` was
given, **stop and ask** which starting point to use rather than defaulting to the
whole history.

Convert the starting point to a date, then list merged PRs:

```bash
gh pr list --state merged --base develop --limit 200 \
--search "merged:>=<date>" \
--json number,title,mergedAt,body,labels,author
```

## Building each entry

For every PR, parse the `## Release note` block from its body:

- **Audience** — `developers` entries are omitted from the notes entirely.
- **Numerical impact** — text after this label, minus the `_(harness @ sha)_` stamp.
- **Migration** — what a user must change.
- The prose sentences below the fields are the entry text.

Parse the title as `Area[.Submodule] - TAG[!] - Summary`.

Place each entry:

- **Any title with `!`** goes in `Changed results & breaking changes`, regardless of
its tag. Give the numerical impact and the migration text, not just the summary.
This section is the reason the mark exists — never let a marked change appear only
under its tag.
- Otherwise map the tag to its section: `FEATURE` → New capabilities, `BUGFIX` → Bug
fixes, `PERF` → Performance, `API` → Interface & format changes, `DEPRECATION` →
Deprecations, `DOCS` → Documentation.
- `MINOR`, `REFACTOR`, and `TEST` are omitted unless marked with `!`.

Within a section, group by Area, ordering areas by entry count. Write in the past
tense, in terms a GPEC user would recognize, and end each entry with the PR number
in parentheses.

## Gaps

Some PRs will have no parseable block. **List them explicitly at the end of your
report as unprocessed, with their numbers and titles.** Do not invent an entry from
a title or a diff — an unreported change is a visible gap, whereas a fabricated one
is indistinguishable from a real entry and corrupts the record.

Also report any PR whose block says `Audience: users` but whose prose is empty.

## Output

Print the drafted sections for review. Only edit `CHANGELOG.md` if `--write` was
passed; leave sections with no entries in place and empty. Never invent a version
number or move entries out of `## Unreleased` — releasing is a human decision.
35 changes: 35 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
Title this PR the same way you title a commit: Area - TAG - Imperative summary
Append ! to the TAG if results move or an interface changes: Equilibrium - BUGFIX! - ...
Conventions: docs/development/naming.md
-->

## Release note

<!-- Delete the option that does not apply and replace every <placeholder>. -->

- **Audience:** users | developers
- **Numerical impact:** none | <what moved> _(harness @ <sha>)_
- **Migration:** none | <what a user must change>

<One to three sentences a user of GPEC would understand: what you can now do, or what was wrong.>

## Regression report

<!--
Required on every PR (docs/development/regression-harness.md). Paste the report below and
put the commit you ran it at in the harness stamp above; CI fails if src/ changed afterwards.

regress --cases diiid_n1 --refs develop,local
-->

```
```

## Notes for reviewers

<!-- Anything that is not obvious from the diff. Delete if there is nothing. -->

---

Set an **assignee** and at least one **human reviewer**; if you are not ready to name them, open this as a **draft**. Lead developers to ask are listed in `docs/development/contributors.md`. Labels are applied automatically from the title.
158 changes: 158 additions & 0 deletions .github/workflows/pr-conventions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: PR Conventions

# Checks the title grammar, the release-note block, and the freshness of the
# regression stamp, then applies the label implied by the title.
# Conventions: docs/development/naming.md

on:
pull_request:
# The assignee and reviewer events matter because metadata is often attached
# just after a pull request is created, so the `opened` payload misses it.
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
- assigned
- unassigned
- review_requested
- review_request_removed

permissions:
contents: read

jobs:
validate:
name: Title and release note
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# Title and body are author-controlled text; pass them through the
# environment rather than interpolating them into a shell command.
- name: Capture title and body
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
printf '%s' "$PR_TITLE" > "$RUNNER_TEMP/pr_title.txt"
printf '%s' "$PR_BODY" > "$RUNNER_TEMP/pr_body.md"

- name: Does this PR change src/?
id: scope
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
merge_base=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
if [ -n "$(git diff --name-only "$merge_base".."$HEAD_SHA" -- src/)" ]; then
echo "touches_src=true" >> "$GITHUB_OUTPUT"
else
echo "touches_src=false" >> "$GITHUB_OUTPUT"
fi

- name: Check title, release note and Area table
env:
TOUCHES_SRC: ${{ steps.scope.outputs.touches_src }}
run: |
title=$(cat "$RUNNER_TEMP/pr_title.txt")
args=(--title "$title" --body "$RUNNER_TEMP/pr_body.md" --check-table docs/development/naming.md)
if [ "$TOUCHES_SRC" = "true" ]; then
args+=(--touches-src)
fi
python3 ci/conventions/check_subject.py "${args[@]}"

- name: Is the regression stamp still current?
if: steps.scope.outputs.touches_src == 'true'
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
stamp=$(python3 ci/conventions/check_subject.py --harness-sha "$RUNNER_TEMP/pr_body.md")
if ! git cat-file -e "$stamp^{commit}" 2>/dev/null; then
echo "::error::Harness stamp '$stamp' is not a commit in this repository."
exit 1
fi
changed=$(git diff --name-only "$stamp".."$HEAD_SHA" -- src/)
if [ -n "$changed" ]; then
echo "::error::The regression report is stale. Files under src/ changed after $stamp:"
echo "$changed"
echo "Re-run the harness and update the '_(harness @ ...)_' stamp in the release-note block."
exit 1
fi

metadata:
name: Label and metadata
runs-on: ubuntu-latest
# Forked PRs get a read-only token, so these steps cannot run there.
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;

// Read live state rather than context.payload, which is a snapshot from
// when the event fired and misses metadata attached moments later.
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: context.payload.pull_request.number,
});

// Labels follow from the TAG, so nobody has to set them by hand.
const match = /^\S+ - ([A-Za-z]+)(!?) - /.exec(pr.title || '');
if (match) {
const wanted = [match[1].toLowerCase()];
if (match[2]) wanted.push('changed-results');
const owned = new Set([
'feature', 'bugfix', 'perf', 'api', 'deprecation',
'docs', 'refactor', 'test', 'minor', 'changed-results',
]);
const current = pr.labels.map(l => l.name);
const add = wanted.filter(l => !current.includes(l));
const remove = current.filter(l => owned.has(l) && !wanted.includes(l));
if (add.length) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: add });
}
for (const name of remove) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name });
}
}

// Assignee and reviewer need a human decision, so comment rather than block.
const missing = [];
if (!pr.assignees.length) missing.push('an **assignee**');
if (!pr.requested_reviewers.length && !pr.requested_teams.length) {
missing.push('a **reviewer**');
}

const marker = '<!-- pr-conventions-metadata -->';
const existing = (await github.rest.issues.listComments({
owner, repo, issue_number: pr.number, per_page: 100,
})).data.find(c => c.body.includes(marker));

if (!missing.length) {
if (existing) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
}
return;
}

const body = [
marker,
`This pull request is missing ${missing.join(' and ')}.`,
'',
'If you are not ready to name them, mark this pull request as a **draft**.',
'`docs/development/contributors.md` suggests lead developers to ask.',
'Merging is not blocked here, but no pull request may be merged without human review.',
].join('\n');

if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ repos:
# pygrep hooks fail when the pattern matches, so each entry matches a violation.
- repo: local
hooks:
# Keeps the Area table in docs/development/naming.md matching src/ on disk,
# since the commit and PR title vocabulary is derived from it.
- id: naming-table-in-sync
name: 'Naming conventions: Area table matches src/'
language: system
entry: python3 ci/conventions/check_subject.py --check-table docs/development/naming.md
pass_filenames: false
files: ^(src/|docs/development/naming\.md$)
- id: toml-header-block
name: 'TOML conventions: file starts with a # header block'
language: pygrep
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

All notable changes to GPEC are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses
[semantic versioning](https://semver.org/spec/v2.0.0.html).

Entries are compiled from the `## Release note` block of each merged pull
request; see [`docs/development/naming.md`](docs/development/naming.md). Run
`/release-notes` to draft the next set rather than writing them by hand.

Sections follow the commit TAG vocabulary. `Changed results & breaking changes`
comes first and collects every change whose title carried a `!`, whatever its
tag, because it is the section a user must read before upgrading.

## Unreleased

### Changed results & breaking changes

### New capabilities

### Bug fixes

### Performance

### Interface & format changes

### Deprecations

### Documentation
12 changes: 10 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,24 @@ Full command reference (comparing branches/commits, tracking a quantity's histor

## Architecture

GPEC follows a three-stage pipeline — **Equilibrium** (Grad-Shafranov solve, q-profile) → **Stability Analysis** (ideal MHD eigenvalue problem, DCON-style, singular surfaces) → **Perturbed Equilibrium** (plasma response, singular coupling, island formation) — implemented across seven modules in `src/`: `Splines`, `Utilities`, `Equilibrium`, `Vacuum`, `ForceFreeStates`, `ForcingTerms`, `PerturbedEquilibrium`. All modules are configured via the single `gpec.toml` file (see `examples/*/gpec.toml`).
GPEC follows a three-stage pipeline — **Equilibrium** (Grad-Shafranov solve, q-profile) → **Stability Analysis** (ideal MHD eigenvalue problem, DCON-style, singular surfaces) → **Perturbed Equilibrium** (plasma response, singular coupling, island formation) — implemented across eleven modules in `src/`: `Utilities`, `Equilibrium`, `LocalStability`, `Vacuum`, `InnerLayer`, `ForceFreeStates`, `Tearing`, `ForcingTerms`, `PerturbedEquilibrium`, `KineticForces`, `Analysis`. All modules are configured via the single `gpec.toml` file (see `examples/*/gpec.toml`).

Full module-by-module breakdown (key files, status, data flow stage-by-stage, key data structures, module dependency graph) is in **[`docs/development/architecture.md`](docs/development/architecture.md)** — read it when working across module boundaries or getting oriented in an unfamiliar part of the codebase.

## Git Workflow

This project uses GitFlow (http://nvie.com/posts/a-successful-git-branching-model): two permanent branches, `main` and `develop`. **IMPORTANT**: All development must be done on feature branches (`feature/`, `bugfix/`, `hotfix/`, `performance/`, `refactor/`, `docs/`, `test/`, `experiment/`). No commits directly to `develop` or `main`. Branch from `develop`, open a PR back into `develop`.

Commit messages follow `CODE - TAG - Detailed message` (e.g. `VAC - IMPROVEMENT - Add dual Green's function computation`).
Commit messages follow `Area - TAG - Imperative summary` (e.g. `Vacuum - PERF - Add dual Green's function computation`). See the naming conventions below.

Full branch-naming table, hotfix workflow, versioning scheme, and merge-conflict resolution policy are in **[`docs/development/git-workflow.md`](docs/development/git-workflow.md)**.

### ***PARAMOUNT*** - NO PULL REQUESTS SHOULD EVER BE MERGED WITHOUT HUMAN REVIEW. ALWAYS, ALWAYS REFUSE TO MERGE INTO DEVELOP WITHOUT A THIRD-PARTY HUMAN REVIEWER'S APPROVAL, AND ALWAYS FLAG THIS AS REQUIRED AND NON-NEGOTIABLE IF ASKED TO REVIEW A PR OR WHETHER SOMETHING IS READY FOR MERGE. STRESS THIS, IT IS CRUCIAL. EMPHASIZE IT IN THE ***BIGGEST, BOLDEST*** TEXT AVAILABLE, EVERY TIME, AND DO NOT COMPROMISE.

**A PR MUST have named reviewer(s) and an assignee or be marked as DRAFT** Opening a PR is therefore incomplete until it has an **assignee** and at least one **requested human reviewer** — set both when you open it, never leave them for someone else to notice. Handles are in [`docs/development/contributors.md`](docs/development/contributors.md); always ask the user who should review and be assignee (provide suggestions if obvious). If working on an unassigned PR, complain regularly. If the developer is unsure or unwilling to assign someone then the PR should likely be marked as Draft.

**Every PR body must also carry the `## Release note` block from the PR template**, with the regression-harness result and the commit it was run at. Labels are applied automatically from the title — do not set them by hand.

## Agent Team

This repo ships a small team of specialized Claude Code subagents in `.claude/agents/`. They are **stateless reviewers** — each runs in its own context on a specific deliverable and reports back; **the main session is the integrator**, not a delegator. Invoke an agent by name for the matching job (e.g. *"review this with the fortran-physics-reviewer"*); don't reflexively consult all of them.
Expand Down Expand Up @@ -147,6 +151,10 @@ Additional file hygiene (enforced by pre-commit hooks):
- Files must end with exactly one newline
- LF line endings only (no CRLF)

### Naming and Commit Conventions

Commit subjects, PR titles, and issue titles share one grammar: `Area[.Submodule] - TAG[!] - Imperative summary`. `Area` and `TAG` are closed vocabularies. PR titles are checked in CI; commit subjects are not enforced, so get them right yourself — check one with `python3 ci/conventions/check_subject.py --title "..."`, which prints the correct replacement for any bad token. Append `!` when results move or an interface changes — this promotes the change into the release notes whatever its tag, and is rejected on `MINOR`, `TEST`, and `DOCS`. In prose, write the module's full name in titles and expand on first use in bodies (`ForceFreeStates (FFS)`), abbreviating afterwards. **Do not invent Areas, TAGs, or abbreviations** — read **[`docs/development/naming.md`](docs/development/naming.md)** before writing a commit message or opening a PR.

### HDF5 Output Conventions

The `gpec.h5` schema follows one physics-first convention (CamelCase groups at all levels, snake_case datasets, data-driven tokens verbatim, inputs only under `Input/`, five named top-level physics-topic exceptions). **Do not invent new group names or echo inputs into output groups** — read **[`docs/development/hdf5-conventions.md`](docs/development/hdf5-conventions.md)** before adding or moving any HDF5 output; renames are clean breaks (update writer, readers, and harness case TOMLs together — there is no legacy-path shim).
Expand Down
3 changes: 2 additions & 1 deletion REFACTOR_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,8 @@ manual smoke: run the 4-line UX from the Context section in a REPL against

1. **Never merge without third-party human review. State this in every PR body.**
2. Every commit and push requires explicit per-instance maintainer approval.
3. Commit messages: `CODE - TAG - message` (e.g. `FFS - REFACTOR - Unify Riccati integrator`).
3. Commit messages: `Area - TAG - message` (e.g. `ForceFreeStates - REFACTOR - Unify Riccati integrator`),
with closed Area and TAG vocabularies per `docs/development/naming.md`.
4. JuliaFormatter-clean (margin 180, kwargs `f(x; a=1)`, no trailing whitespace, LF,
single trailing newline). TOML edits follow `docs/development/toml-conventions.md`
(header block, per-line `# description` copied from the struct docstring,
Expand Down
Loading
Loading