Skip to main content
This guide covers the day-to-day development workflow and best practices for contributing to Crimsonland.

Preferred Refactor Style

Migrate Callers, Then Delete Legacy APIs

Default posture: cutover refactors.
1

Update All Internal Callers

Update all internal callers in one wave; delete old APIs/schemas/tests for deprecated behavior.
2

Avoid Compatibility Wrappers

Do not add long-lived compatibility wrappers. If compatibility is absolutely required: keep it small, local, temporary, mark it clearly for removal (with a removal note).
3

Delete Old Paths

Delete old paths rather than supporting two worlds.

Subtract Before You Add

Before introducing a new abstraction or subsystem, remove dead code, obsolete wrappers, and duplicate paths in the target area.

Outcome-Oriented Execution

  • It’s OK for intermediate steps to break if the breakage is scoped and planned
  • Final state must be coherent, verified, and free of permanent temporary scaffolding

Redesign From First Principles

For major architectural or schema changes:
  1. Define the ideal target state as if this requirement existed from day one
  2. Land incrementally toward that target
  3. Do not bolt on special cases that preserve avoidable complexity

Exhaust the Design Space

Only when not dictated by the original binary and there are multiple viable options:
  • Sketch 2–3 concrete alternatives
  • Pick based on parity risk, maintainability, and testability
Skip this for mechanical ports and obvious root-cause fixes.

Code Smells to Avoid

Especially in gameplay/domain code:
Deep in the domain:
  • isinstance, hasattr, “just in case” try/except ValueError
  • .get() / getattr() on typed models to dodge typing (OK for truly dynamic dicts or narrow tooling)
Thin duck typing protocols that blur invariants
Hand-written field-by-field mappers just to satisfy style (prefer serializers/DTOs)
Long-lived “compat” layers when internal callers can be migrated

Encode Lessons in Structure

If a mistake or review comment repeats, convert it into enforcement:
  • Tests / snapshots / invariants
  • ast-grep rules
  • import-linter contracts
  • Typed schemas and decoders
  • Scripts/automation
Text rules are forgettable; structural rules enforce themselves.

Quick Playbooks

Parity Bug Investigation

1

Reproduce

Reproduce using the canonical capture/replay.
2

Generate Report

Generate divergence report and/or verify-capture.
3

Isolate Mismatch

Isolate the first sustained mismatch and identify the subsystem.
4

Fix Root Cause

Fix the root cause (not the symptom).
5

Re-Run

Re-run the same capture/replay and confirm the mismatch moves/disappears for the right reason.
6

Add Regression Test

Add a regression test that locks the discovered behavior.

API/Schema Refactor (Cutover Wave)

1

Define Target

Define target API and invariants (ordering, ownership, serialization boundaries).
2

Migrate Callers

Migrate all internal callers in one wave.
3

Delete Legacy

Delete legacy APIs/re-exports/tests in the same wave.
4

Search References

Search for remaining references to old surfaces and remove them.
5

Verify

Verify with just check / just check-zig and parity tests/artifacts.

Capture-Only Triage

1

Follow Playbook

Follow docs/frida/differential-playbook.md.
2

Record SHA

Record capture SHA and keep notes tied to that artifact.
3

Use Divergence Tools

Use divergence + bisect/focus traces before touching runtime code.
4

Improve Instrumentation

If telemetry is insufficient, improve instrumentation and re-capture.

Docs/Tooling Changes

1

Align Guidance

Keep guidance aligned with parity-first + typed-boundary policy.
2

Encode Structurally

When guidance repeats, encode it structurally (rules/tests) where possible.
3

Verify

Run just check.

Structural Search / Codemods

Prefer ast-grep over regex-only edits for structural transformations.

Python

sg scan                              # Run configured rules
sg test                              # Test ast-grep rules
sg run -p 'pattern' -r 'fix' src    # Apply transformation

Zig

Use sgconfig.local.yml to load the custom Zig parser. Zig metavariables use _VAR syntax (e.g. _EXPR):
sg run -c sgconfig.local.yml -l zig -p 'const _NAME = _TYPE{};' -r 'const $NAME: $TYPE = .{};' crimson-zig/src -U

Pull Requests (gh CLI Hygiene)

When using gh:
1

Conventional Commit Format

PR title must be in the form of a conventional commit:
  • feat: add new weapon system
  • fix: correct RNG seed handling
  • refactor: simplify perk selection logic
2

Use Body Files

To avoid escaping issues, write the PR description to a file:
gh pr create --body-file pr_body.md
gh pr edit --body-file pr_body.md
3

Squash Merge

When merging, use squash:
gh pr merge --squash <pr>

Pre-Commit Verification

Before committing, always run:
just check
Or use pre-commit hooks:
# Install once per clone/worktree
prek install -c prek.toml -t pre-commit -t pre-push

# Manual runs
prek run --stage pre-commit  # Fast checks only
prek run --stage pre-push    # Heavy checks

CI-Equivalent Local Run

For changes to src/, tests/, docs/, tools/:
just check && uv build

Reminder: When in Doubt

Choose Fidelity

Choose fidelity and determinism over cleanliness

Prove Behavior

Prove behavior with decompile/captures/replays/tests, not intuition

Fix Boundaries

Fix schemas/types/contracts at boundaries rather than weakening the domain

Delete Old Paths

Delete old paths rather than supporting two worlds

Build docs developers (and LLMs) love