Thanks for helping. This page covers setup, the checks a change must pass, and the rules the codebase depends on.
You need Node 22 or later and pnpm 10.
pnpm installRun the editor while you work:
pnpm --filter @horizon36596/zenith-web devEvery change must pass the build and the test suite. Build first: the MCP server's tests load the built output.
pnpm install; pnpm build; pnpm testpnpm test runs every package's vitest suite and the purity check described below.
The web app and the desktop app also have end-to-end tests, written with Playwright:
pnpm --filter @horizon36596/zenith-web test:e2epnpm --filter @horizon36596/zenith-desktop test:e2eRun these when you change anything in apps/.
- Conventional commits:
feat(core): ...,fix(web): ...,docs: ...,test(cli): .... - Small. One change per commit, so a reviewer can follow it.
- Plain full sentences in the message body, code and comments.
- No attribution lines and no
Co-Authored-Bytrailers.
packages/core and packages/schema use no DOM, no clock, no randomness and no Node APIs. That is
what makes the CLI, the editor, the MCP server and a reviewer agree on every number.
pnpm test enforces it: it searches those packages for Math.random, Date.now,
performance.now, window, document and process., and fails on any hit. If you need one of
those, the code belongs in a different package, which then passes the value in.
Every Zenith file has one canonical form: key order, number formatting and layout are fixed, so a
git diff of an auto reads like a description of the change.
A change that alters canonical form, or the meaning of any field, must:
- bump that file kind's
formatVersion, - ship a migration from the previous version,
- add a fixture: a file in the old format and the migrated result, with a test that checks one becomes the other.
Files written by an older version must keep loading.
- Units in files are inches, radians and seconds, and every numeric key names its unit (
xIn,headingRad,estimateS). Degrees appear only in the UI. - Never invent a number. Every constant in
robot.jsonandfield.jsoncarries a provenance label: MEASURED, SET BY HAND, SPEC, NEEDS MEASUREMENT, CARRIED OVER, PLACEHOLDER, APPROX, SET FROM SIM, SET FROM EDITOR, CALIBRATED FROM SIM or CALIBRATED FROM ROBOT. If you do not know a value, use PLACEHOLDER and say so.
Read the Seasons page first. It explains the
SeasonRules interface and walks through BIOBUZZ.
packages/season-biobuzz is the reference implementation. In short:
- Write the season's
field.json, withrules.pluginset to your plugin's id and a provenance label on every number. - Create a package that exports a
SeasonRulesobject and aloadSeason(field)function. Every rule reads from the field file; nothing is a constant typed from the game manual. - Register it in
packages/seasons/src/index.tsand add it as a dependency there. - Add tests for the kickoff state, launches, collects, the approach rule and each start rule.
If you want a season supported but cannot write the plugin, open a season request issue instead.
Fill in the pull request template. Say which gates you ran, whether the change touches canonical form
or formatVersion, and add a screenshot if the UI changed.
By contributing, you agree that your contribution is licensed under the MIT licence in LICENSE.
Everyone taking part is expected to follow the Code of Conduct.