Keep the codebase maintainable: strict types, small units of work, and simple React components.
- Use the latest stable React and follow current React best practices. Prefer modern APIs (e.g. hooks, concurrent features where applicable) over legacy patterns.
- Effects and event logic: When a
useEffectneeds to call a callback (e.g. a prop likeonOutputLines) that should not be in the dependency array—to avoid re-running the effect when the parent passes a new function reference—useuseEffectEvent(React 19+) to wrap that callback. The Effect Event always sees the latest props/state when it runs but does not trigger the effect to re-run when it changes. This keeps the effect’s dependencies minimal and avoids stale closures. - Custom hooks for stateful logic. If a component has non-trivial state or effects, move that logic into a
useSomethinghook and keep the component mostly presentational. - Composition over complexity. Prefer many small components and clear data flow over large components with many branches.
- Strict types only. The project uses
strict: trueintsconfig.json. Do not relax it. - Avoid
any. Useunknownand narrow with type guards, or define proper interfaces. If you must escape types, use a minimal cast and comment why. - Prefer explicit types for function parameters and return values in public APIs (exports, component props). Let inference do the work inside small, local functions when it’s obvious.
- Model data with interfaces or types. Use Zod schemas as the source of truth for domain shapes; align TypeScript types with them (e.g.
z.infer<typeof MySchema>). - No sloppy object types. Avoid
Record<string, unknown>in public APIs unless you’re truly handling arbitrary data; prefer named properties and interfaces.
- Keep functions short. Aim for a single level of abstraction and one clear responsibility. If a function is long or doing several things, split it.
- Extract helpers for repeated logic or non-trivial calculations. Put them in a small, testable function or a shared util.
- Limit parameters. If a function takes more than a few arguments, consider an options object or a small typed config.
- Keep components small. If a component is long or has many branches, extract subcomponents or custom hooks.
- Avoid overly complex components. Prefer many small components over one large one. Use composition.
- Props: typed and minimal. Define a clear props interface; avoid spreading large objects “just in case.”
- Model operations as commands. When adding new app functionality, consider whether it can be expressed as a CLI command. Commands provide a composable foundation that works in browser, terminal, and for AI agents.
- Environment-agnostic logic. Command implementations should not check
typeof windowor rely on browser/Node-specific APIs. Isolate platform differences in adapters. - Structured results. Commands should return structured
CommandResultobjects for programmatic consumption, not just render output. - Decomposability enables testability. A command that can be tested in isolation—without UI, browser mocks, or complex fixtures—is easier to reason about. If you can test it in a small environment, you can predict how changes will affect it.
This pattern is foundational, not mandatory—direct store access is fine for simple cases. See CLI_COMMAND_UNIFICATION.md for the full architecture.
- Naming: Use clear, consistent names. Prefer
handleSubmitoveronClickfor handlers passed as props; keep event handlers and callbacks obvious. - File length: If a file grows past roughly 200–300 lines, consider splitting (e.g. by component, by feature, or into a small folder).
- Comments: Comment why when it’s not obvious from the code; avoid restating what the code does.
- Tests: Unit tests are required for all features—not optional. Follow existing patterns in
tests/unit/and use the test helpers (e.g.renderWithProviders,createTestStore). If a test is hard to write, refactor the code rather than writing a complex test. See TEST_PLAN.md. - Tests guide design: Use testability as feedback on your design. If you can't imagine a simple test for something, simplify the design before implementation gets complicated. Easy to test = easy to understand and maintain.
- Exception for performance-critical code: Code optimized for performance may be harder to test due to its structure. This is acceptable only if the code is: (1) explicitly marked as performance-critical with a comment explaining why, (2) well bounded—small and focused on one thing, and (3) isolated—minimal dependencies, clear interface. Such code still needs tests, but they may test at the boundary rather than internals.
- When picking a library, prefer ones written in TypeScript with strict typing conventions. Good type definitions (or first-party TypeScript) reduce runtime bugs and make refactors safer; avoid libraries that rely on loose or hand-maintained
@typesif a better-typed alternative exists.
- When a refactor or code change breaks a test, do not automatically fix the test. The developer who made the change is responsible for resolving the failure: either update the code so the test passes again, or change the test only after deciding that the new behavior is correct and the test’s expectations were wrong. Do not silently adjust tests to match new behavior without that decision.
These guidelines apply to src/ and tests/; tooling and config can be more permissive where needed.