Contributing
How we work on Astrarium. Short on purpose — a contributing file nobody reads is worse than none.
Prerequisites
Contributor tooling, in addition to whatever you already use to write code:
git— assumed; not installed for you by anything else.- Node 20+ — required for the dev server, build, tests, lint, and format.
Install via your package manager (e.g.
brew install nodeon macOS). Runnpm installonce after cloning. glab— the GitLab CLI. The workflow below depends on it. Install viabrew install glabon macOS or from the official releases elsewhere. Runglab auth loginonce on first use.
Day-to-day commands (npm run dev, npm run build, npm test, npm run lint,
npm run format) are documented in README.md.
Discussion
Synchronous discussion, design / scoping conversations, and decision-making happen on Discord:
The GitLab issue tracker remains the asynchronous source of truth — every decision still gets recorded on the relevant issue or MR. The channel is for the live conversation that gets us there. GitLab → Discord notifications for MR / issue / pipeline activity are wired up in the same channel (#49), so events flow into Discord automatically; this link is the human entry point.
Slash commands
Astrarium is driven by an AI pair-programmer (see the About this project
note in README.md). The day-to-day workflow is automated by a small set
of project-level slash commands shipped under .claude/commands/. They are
shortcuts for the manual glab flow described below — they don’t replace
it, they accelerate the parts that are mechanical.
Run /init first, every fresh checkout. It loads CLAUDE.md,
CONTRIBUTING.md, and the surrounding context into the AI driver so every
subsequent command operates against an accurate mental model of the
project, and runs a drift check against src/ — if CLAUDE.md has fallen
behind the codebase, /init auto-drafts a task workitem documenting the
gap and asks you to confirm before filing. Skipping /init leads the
driver to hallucinate file paths and ignore conventions documented here.
This isn’t a suggestion.
Project-level commands (one Markdown prompt each, lives in .claude/commands/):
| Command | Phase it accelerates | What it does |
|---|---|---|
/init |
Session start | Orients Claude to the project (loads CLAUDE.md, walks src/), runs a drift check, and auto-drafts a task workitem if CLAUDE.md no longer matches src/. Confirm-before-create. |
/workitem |
File a new idea | Drafts a new GitLab issue from a free-form description, picks a label, asks for confirmation before filing. |
/groom-plan |
Backlog grooming | Walks the open backlog, surfaces 0–3 maintainer decisions, queues mechanical fixes, publishes a grooming-YYYY-MM-DD wiki page. Read-only on the tracker. |
/groom-apply |
Backlog grooming | Reads the maintainer-annotated grooming page and applies the agreed-on adjustments to issues. |
/next |
Pick a ticket | Picks the top item from the latest grooming page’s Next up bucket, runs per-issue verification, optionally self-assigns. |
/work |
Start work | Runs the git-flow ceremony for #N: self-assign, branch off up-to-date main, empty starter commit, push, draft MR. Aborts on any precondition failure; never auto-recovers with destructive git ops. |
Also useful, shipped by Claude Code rather than this project:
/ultrareview— fires a multi-agent cloud review of the current branch (or/ultrareview <PR#>against a specific MR). User-triggered, billed to the user, runs in ~5–10 min. Useful before marking an MR ready.
If a command is added or removed in .claude/commands/, this section is
the one that needs updating.
Git workflow
One issue, one merge request, one branch. Every change must be traceable to a filed GitLab issue.
Single long-lived branch: main. Feature work targets main via MRs. The
production site (astrarium42.gitlab.io/astrarium/) deploys on vX.Y.Z tag
pushes — cutting the tag is the release.
- File the issue first. Agree on scope before writing code. Small fixes still need an issue — it’s the unit of work, not the commit.
- Self-assign the issue so the board reflects who’s working on it:
glab issue update <N> --assignee "@me". An unassigned open issue means nobody’s working on it. If an issue already has an assignee who isn’t you, don’t take it — coordinate on Discord first, have the current assignee unassign, or ask the maintainer to reassign. The--assignee "@me"call replaces the assignee silently; the rule lives here, not in GitLab./nextenforces this automatically; manual self-assigns rely on you reading the page first. - Branch off
mainwith the nameissue-<N>-<slug>(e.g.issue-13-conventions). Pullmainfirst:git checkout main && git pull --ff-only. Never commit directly tomain. - Open the draft MR before writing any code. Make an empty initial commit
and push so GitLab has something to attach the MR to:
Thegit commit --allow-empty -m "Start work on #<N>" git push -u origin issue-<N>-<slug> glab mr create --draft --assignee "@me" \ --squash-before-merge --remove-source-branch --target-branch main \ --title "<short description>" \ --description "Closes #<N>"--draftflag prefixesDraft:on its own — don’t add it to--titleor you’ll end up withDraft: Draft: …. The MR is visible from the first keystroke, mirroring how a human would scaffold the work. Real commits land on top.Closes #<N>in the description auto-closes the issue on merge. - Commit freely on the branch with short, descriptive subjects in the imperative mood. Branch commits get collapsed into a single hand-authored commit at pre-squash time (step 6), so subjects can be working notes — they don’t need to read well in the final history.
- Pre-squash before merge. GitLab’s squash-on-merge auto-message
concatenates branch commit subjects, which is rarely the right summary.
Squash locally and write the merge commit yourself:
The MR is now a single commit. GitLab’s squash-on-merge stays enabled as a safety net, but with one commit on the branch the toggle is a no-op.git fetch origin git rebase -i origin/main # In the editor: pick the first commit, fixup (f) the rest. # Then amend the surviving commit's message: git commit --amend git push --force-with-lease - If CI fails, fix forward with a new commit, then re-squash before merge.
- Ready MRs are frozen. Once an MR is marked ready, no further changes
are accepted on it. If a change becomes necessary — reviewer-requested
edit, rebase, scope tweak — first re-mark the MR as draft
(
glab mr update <N> --draft), push the change, and only then re-request readiness. Keeps ready a meaningful signal: a ready MR is the exact diff being asked to merge, not a moving target. - Merge needs at least one approval. GitLab won’t let an MR merge without it. Treat ready and approved as separate states — the second is a real gate, not a formality. Don’t propose merging until the approval is in.
- Scope discipline. If a new idea surfaces mid-MR, file a separate issue for it. Only fold it in if the reviewer explicitly agrees.
Issue labels
Three labels are in use: bug, feature, task. The criterion is
does the deployed site change?
feature— changes what the deployed site (astrarium42.gitlab.io/astrarium/) does for an end user.task— everything else that isn’t a defect: docs, tooling, CI, dev workflow, refactors, Claude skills/commands, infra. No user-facing change to the deployed site.bug— unintended behavior in either of the above.
This keeps the label honest as a release-notes signal: every bug and
feature ships as a release, every task may not.
Commit messages
- Imperative mood: “Add X”, “Fix Y”, not “Added” or “Fixing”.
- Subject ≤ 72 chars, no trailing period.
- Body wraps at ~72 chars. Explain the why when it isn’t obvious from the diff; skip the body entirely when the subject says enough.
- No conventional-commits prefix for now (revisit if release cadence
warrants — see
docs/releasing.md).
Docs match code
If your change affects anything documented in docs/, update the doc in the
same commit. If the change invalidates a doc entirely, rewrite it or remove
it — do not leave stale copy.
| File | Covers |
|---|---|
docs/architecture.md |
Directory layout, module boundaries, page structure |
docs/physics.md |
Formulas and their derivations, accuracy targets |
docs/scales.md |
Per-view scaling rationale and compressions |
docs/releasing.md |
Release process, tagging, versioning conventions |
CHANGELOG.md |
One [Unreleased] entry per bug / feature MR, written on that same MR (see below) |
This rule is advisory, not CI-enforced. Rely on reviewer judgement.
If your MR’s label is bug or feature, add a one-paragraph entry under
[Unreleased] in CHANGELOG.md as part of the same MR. The release MR’s
job is to rename [Unreleased] to [X.Y.Z] — date and add the
compare-link — not to backfill content. If the maintainer routinely has
to write entries during the release cut, the rule isn’t being followed.
task MRs may add an entry if the change is worth surfacing in release
notes (e.g. workflow changes that affect contributors), but aren’t
required to.
Why glab, not a wrapper?
We invoke glab directly rather than adopting a wrapper like
shipit-cli-go (which does
support GitLab via a project-root .shipit.yml). Recorded so the question
doesn’t keep coming back (discussion):
Shipit’s main value is helping humans remember the multi-step GitLab flow
(branch → push → MR). Claude bridges that gap well enough here that the
wrapper doesn’t pay its keep. Calling glab directly also keeps contributor
tooling to one CLI instead of two.
Not a knock on shipit — a judgement call about what fits this project’s workflow. Revisit if Claude stops being the primary driver.
Releasing
SemVer (vX.Y.Z). Every merged bug fix or feature must ship as a new
patch or minor release — do not leave fixes sitting in [Unreleased]
after merge. Three sources of truth must agree at release time: the
annotated git tag, src/version.js (VERSION), and the CHANGELOG.md
entry. The deploy is gated on the tag push, so cutting the tag is the
release.
See docs/releasing.md for the full process (-dev suffix conventions,
the glab release create invocation).
Deployment
GitLab CI (.gitlab-ci.yml) has a single pages job that fires on vX.Y.Z
tag pushes only. It checks out the tag, runs npm run build, and serves
the resulting dist/ at https://astrarium42.gitlab.io/astrarium/.
Merging to main does not deploy on its own — push a vX.Y.Z tag to ship.
Force HTTPS is enabled in Settings → Deploy → Pages (project setting, not in this repo): plain HTTP requests to the deployed site — including the custom domain — get a 301 to the HTTPS equivalent. The toggle relies on a valid TLS certificate; if the cert ever expires or the domain config breaks, the site starts erroring on HTTP. (#109)
Other pointers
README.md— user-facing project overview.CLAUDE.md— orientation for Claude (architecture, design rules, language note). Workflow content lives here, not there.vite.config.js— dev-server / build configuration.LICENSE— MIT.