Contributing to RaySpec
Thanks for your interest in improving RaySpec. This guide covers the toolchain, the local workflow, the standards a change is held to, and the licensing terms your contribution is made under.
Before changing anything substantial, read docs/ARCHITECTURE.md (the design and package taxonomy) and docs/concepts.md (the vocabulary). The most valuable contributions respect the platform's core invariant: no product-specific code lives in the platform — everything product-specific arrives as the spec a deployer injects.
Toolchain#
RaySpec is a TypeScript/Node monorepo managed with pnpm and Turborepo.
- Node
>=22. - pnpm
10.12.4— pinned viapackageManagerinpackage.json. Use Corepack (corepack enable) so your pnpm matches the pin exactly. - Turborepo — orchestrates the per-package
build/test/typechecktasks across the workspace. - Biome — formatting and linting (one tool for both).
- Vitest — the test runner.
Local setup#
git clone <this-repo> rayspec && cd rayspec
pnpm install # installs from the frozen lockfile
pnpm build # builds every package
pnpm db:up # a local Postgres for the database-backed testsThe core commands#
Run these from the repo root:
| Command | What it does |
|---|---|
pnpm build | Builds all packages via Turborepo. |
pnpm typecheck | Type-checks all packages (tsc). |
pnpm lint | Runs Biome's format + lint check over the tree. |
pnpm lint:fix | Applies Biome's safe fixes. |
pnpm test | Runs the full Vitest suite across packages. |
pnpm gate | Runs the platform structural-invariant checks (see below). |
Some tests are database-backed and need a reachable Postgres (pnpm db:up provides one). A change should be green on pnpm typecheck, pnpm lint, pnpm build, pnpm test, and pnpm gate before it is proposed.
The structural gate#
pnpm gate runs the platform's structural-invariant checks — automated guards that fail the build when a change would violate one of the load-bearing architectural rules (for example, weakening the tenant chokepoint, or letting an adapter reach past the neutral boundary). Treat a gate failure as a real defect in the change, not as a check to route around: the gates encode the guarantees the security model depends on.
The monorepo layout#
The workspace is organized into dependency tiers under packages/ — kernel, adapters, capabilities, workflow, compose, app, and test. Each tier depends only downward. See the package taxonomy in docs/ARCHITECTURE.md for what lives where; put new code in the lowest tier that fits, and never introduce an upward dependency.
Proposing a change#
- Open an issue first for anything non-trivial, so the approach can be discussed before you invest in an implementation.
- Branch from the default branch and keep the change focused — one logical change per pull request.
- Keep it green. Run the core commands above locally. If you add or adjust a dependency, update the lockfile and confirm
pnpm install --frozen-lockfilestill passes. - Write tests that would fail without your change. A test that passes whether or not the code is correct proves nothing; assert the real behavior. New behavior needs coverage; a bug fix needs a regression test.
- Update the docs when you change an observable behavior, a CLI flag, or the grammar.
- Open a pull request describing what changed and why, and how you verified it.
Coding standards#
- Formatting and linting are Biome-enforced. Run
pnpm lint(orpnpm lint:fix) before pushing; a red Biome check blocks a change. - Fail closed. New parsing/validation surfaces reject the unknown rather than ignoring it — matching the strict, fail-closed posture of the existing grammar.
- Respect the neutral boundary. Backend-specific behavior belongs inside an adapter; the neutral types must not move to accommodate one SDK's shape.
- New stores/tables must be registered as committed source. The tenant chokepoint is deny-by-default: a tenant-scoped table is reachable only if it is registered as committed source, and the deploy step verifies this rather than registering it on the fly. If your change adds a tenant-scoped table, register it in committed source — otherwise a deploy that declares it will fail closed, by design. A new predicate-exempt (genuinely global) table is a deliberate, reviewed exception, not a default.
Tests#
- Use Vitest. Run
pnpm testfor the whole suite, orpnpm --filter <package> testfor one package. - Database-backed tests require Postgres. They must not silently no-op when a database is absent in an environment that expects one — a security-relevant test that skips itself is a false green.
- Make that a hard failure while you work, rather than trusting a green run.
RAYSPEC_REQUIRE_DB_TESTS=trueturns a database-backed suite that finds noDATABASE_URLinto a collection-time failure instead of a skip, andRAYSPEC_REQUIRE_MEDIA_TESTS=truedoes the same for the ffmpeg-backed suites. CI needs neither — a run withCI=truealready requires the database-backed ones. RAYSPEC_REQUIRE_LIVE_TESTS=trueis the opt-in the cross-backend parity smoke requires before any live call: without it every live block there self-skips, whatever credentials are around. It does not reach the other provider-backed tests. The server intake smokes run wheneverDATABASE_URLandOPENAI_API_KEYare both present, and the Deepgram live test wheneverDEEPGRAM_API_KEYis; both packages load the repo-root.envthemselves, so a filled-in.envalone is enough for those to call a real provider and spend. What the opt-in adds for them is a collection-time failure when their credential is absent, instead of a skip.- For the parity smoke the opt-in is weaker than the two gates above: it fails only when no provider credential at all is present, so a box holding one is green with the blocks whose credential is absent skipped.
RAYSPEC_LIVE_BACKENDSis what closes that gap — a comma-separated list drawn fromopenai,pi,anthropicandcodex, naming the backends the run must exercise. Any name in it whose credential is absent, and any name outside those four, fails collection rather than skipping.openaiandpiboth run onOPENAI_API_KEY,anthropiconCLAUDE_CODE_OAUTH_TOKEN, andcodexon~/.codex/auth.json. CI's live lane sets both variables and names its backends explicitly. - A live run also fails collection when
CLAUDE_CODE_OAUTH_TOKENandANTHROPIC_API_KEYare both present. The Anthropic SDK credential precedence isANTHROPIC_API_KEY>CLAUDE_CODE_OAUTH_TOKEN, so the anthropic blocks would authenticate asapi-keyand bill the API instead of using the subscription harness — and theauthModeassertion that catches it runs only once the call has been paid for. UnsetANTHROPIC_API_KEYfor the run. Like the two refusals above it fails the live smoke file as a whole, so it stops theopenai/pi/codexblocks with it; with the subscription token absent the anthropic blocks self-skip and nothing can be billed, so a lone stray key is not refused. CI's live lane sets noANTHROPIC_API_KEY, which is why the refusal is inert there. - For a run where nothing skips for want of configuration, put those variables and
DATABASE_URL/SHADOW_DATABASE_URLin the environment rather than only in a.envfile.pnpm testdrives the suites through turbo in strict env mode, so a task sees only the variablesturbo.jsondeclares for it; most database-backed packages additionally load a repo-root.envthemselves, but@rayspec/cliand the local-boot wrapper do not — so a.envalone leaves those two skipping while the rest run. The live parity blocks are the exception no variable covers: each needs its own backend's credential, so a box holding some of the four still skips the rest.RAYSPEC_LIVE_BACKENDSmakes that a named failure instead of a green run; it does not supply the missing credential.
Certificate of origin#
By opening a pull request you certify that you wrote the contribution, or otherwise have the right to submit it under the project's license.
That certification is what matters, and it comes from submitting the contribution — a per-commit trailer is not required. If you would like to record it explicitly, git commit -s adds a Signed-off-by line:
Signed-off-by: Your Name <you@example.com>Nothing in CI checks for that line, and the history does not carry it.
License#
RaySpec is source-available under the Functional Source License (FSL-1.1-ALv2) — see LICENSE. By contributing, you agree that your contribution is licensed under those same terms. Third-party dependency attributions are recorded in THIRD-PARTY-NOTICES.md.