Files
platform/CONVENTIONS.md
T
pastilhasandClaude Opus 5 db9d17d6fe docs: the convention docs were describing a different codebase
Second pass. These three are the ones a new contributor reads first, and all three were
teaching things that are not true here.

src/databases/CLAUDE.md claimed "Three PostgreSQL databases", listed two, and there is
exactly one. Its type examples were Screenshot / Experiment / Company / GanOauth — none of
which have ever existed in this repo; it had been carried over from another project
wholesale. Rewritten against the real schema, queries and types, and it now carries the two
things that actually bite: push-not-migrations, and the rule that the schema is the source
of truth for what the database may CONTAIN, not just its shape — with the sql.raw trap in
check() written down, since getting it wrong breaks push for the whole schema.

src/apps/CLAUDE.md had the same problem in its examples (useExperimentsList, ExperimentCard,
a state/ directory layout that does not exist), listed a `useWebsockets` hook that is not
there while omitting useChatWebSocket, usePanelChannel and useJobs, and closed with links to
three app docs that have never existed. Examples now use real hooks, and it points at the
navigation audit — a frontend doc that did not mention the one rule the platform CLAUDE.md
calls authoritative was a real gap.

CONVENTIONS.md said, in bold, that useMemo and useCallback are "strictly prohibited" because
"React 19's compiler handles memoization automatically". Wrong twice: the React Compiler is
an opt-in build plugin that is NOT installed here, so React 19 memoizes nothing on its own —
and roughly 40 files use each hook regardless, including code added this week. Replaced with
guidance that matches both reality and the actual tradeoff, and says plainly what it used to
claim. A rule that is false and universally ignored makes every other rule in the file look
optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:42:47 +00:00

7.3 KiB

Code Conventions

This document outlines the coding patterns and conventions used in this project.

Component Organization

Feature Folder Pattern

Components that represent a feature or screen should use a folder structure with a barrel export.

Feature/
├── index.tsx      # exports from Feature.tsx
├── Feature.tsx    # main component
├── SubComponent.tsx
└── utils.ts

index.tsx:

export * from './Feature';

Rationale: Keeps imports clean (import { Feature } from './Feature') while allowing the feature to grow into multiple files without changing import paths. Subcomponents that only serve this feature live in the same directory rather than being abstracted to a shared components folder.

Types Alongside Components

Export component prop types from the same file as the component.

export type FeatureProps = {
  value: string;
  onChange: (value: string) => void;
};

export const Feature = ({ value, onChange }: FeatureProps) => {
  // ...
};

Rationale: Keeps types discoverable and colocated with their usage. Consumers can import both the component and its types from the same path.

State Management

Manager Pattern for Hooks

Hooks that manage complex state should return a "manager" object. Components receive this manager as a prop.

// Hook
export const useFeatureManager = () => {
  const [state, setState] = useState('');
  const [filter, setFilter] = useState('');

  const filteredItems = (() => {
    // derived state computation
  })();

  return {
    state,
    setState,
    filter,
    setFilter,
    filteredItems,
  };
};

export type FeatureManager = ReturnType<typeof useFeatureManager>;

// Parent component
const Parent = () => {
  const manager = useFeatureManager();
  return <Child manager={manager} />;
};

// Child component
const Child = ({ manager }: { manager: FeatureManager }) => {
  const { state, filteredItems } = manager;
  // ...
};

Rationale: Centralizes state logic in one place. Child components don't need to know about individual state setters - they just receive the manager. Makes refactoring easier since state shape changes only affect the hook.

State Colocation in Hooks

All local state (search, filters, pagination, expanded states) should live in the feature's hook, not scattered across components.

// Good
export const useFeatureManager = () => {
  const [search, setSearch] = useState('');
  const [currentPage, setCurrentPage] = useState(1);
  const [expandedId, setExpandedId] = useState<string | null>(null);
  // ...
};

// Avoid
const Component = () => {
  const [search, setSearch] = useState(''); // state in component
  // ...
};

Rationale: Single source of truth for feature state. Easier to understand, test, and modify. Prevents state synchronization bugs.

Derived State in Hooks

Computed/derived values should be calculated in the hook, not in components.

export const useFeatureManager = () => {
  const [items, setItems] = useState([]);
  const [filter, setFilter] = useState('');

  // Derived state computed in hook
  const filteredItems = (() => {
    return items.filter((item) => item.name.includes(filter));
  })();

  const itemCount = filteredItems.length;

  return { items, filter, setFilter, filteredItems, itemCount };
};

Rationale: Components stay focused on rendering. Business logic stays in one place. Derived values are computed once and shared across all consuming components.

React Patterns

Reach for useMemo / useCallback only when they do work

Default to writing the code plainly. Most derived values are cheap and re-computing them per render costs less than the memo that guards them.

// ✅ Fine — cheap, so just write it
const filteredItems = items.filter((item) => item.active);
const handleClick = () => doSomething();

But they are ordinary tools, not forbidden ones. Use them where they earn it:

  • a value or callback in a dependency array, where an unstable identity re-runs an effect or re-subscribes a socket every render;
  • a genuinely expensive computation over a large list;
  • a prop passed to a memoized child.
// ✅ Earns it — an unstable callback here would re-subscribe on every render
const onConnectionChange = useCallback((state) => setConnState(state), [setConnState]);

This section used to say "NEVER — React 19's compiler handles memoization automatically", which was wrong twice. The React Compiler is a separate, opt-in build plugin and it is not installed here (there is no babel-plugin-react-compiler in package.json); React 19 on its own memoizes nothing. And the codebase never followed the rule — some 40 files use each hook. A convention that is both false and universally ignored is worse than none, because it makes every other rule in this file look optional.

Computation Functions Outside Components

Extract complex computations into functions declared below the component, not as IIFEs inside.

export const Component = ({ data }: Props) => {
  const metrics = computeMetrics(data);
  const stats = computeStats(data, metrics.total);

  return <div>{/* ... */}</div>;
};

// Functions below component
function computeMetrics(data: Data[]): Metrics {
  // complex computation
}

function computeStats(data: Data[], total: number): Stats {
  // complex computation
}

Rationale: Keeps the component body focused on rendering logic. Functions are testable in isolation. Easier to read and understand the component's purpose.

Fragment Shorthand

Use <> for fragments. Only import and use Fragment when a key prop is required.

// Good - no key needed
return (
  <>
    <Header />
    <Content />
  </>
);

// Good - key required
import { Fragment } from 'react';

return items.map((item) => (
  <Fragment key={item.id}>
    <ItemHeader item={item} />
    <ItemContent item={item} />
  </Fragment>
));

// Avoid - unnecessary Fragment import
import { Fragment } from 'react';

return (
  <Fragment>
    <Header />
    <Content />
  </Fragment>
);

Rationale: <> is cleaner and more concise. Fragment is only needed for the key prop which <> doesn't support.

Imports

Single-Line Imports

Keep imports on a single line. If an import has too many items, split into multiple import statements.

// Good
import { Button, Input, Select } from '@/components/ui';
import { Card, CardHeader, CardContent } from '@/components/ui/card';

// Good - split when too long
import { TableBody, TableCell, TableHead } from '@/components/ui/table';
import { TableHeader, TableRow } from '@/components/ui/table';

// Avoid - multiline imports
import { Button, Input, Select, Card } from '@/components/ui';

Rationale: Single-line imports are easier to scan and take less vertical space. Splitting by source module keeps related imports together.

Utilities

Use Existing Helpers

Prefer existing helper functions over inline implementations.

// Good
import { formatCurrency } from 'helpers/formatters';

const display = formatCurrency(amount / 100, currency, 0);

// Avoid
const display = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: currency,
  minimumFractionDigits: 0,
}).format(amount / 100);

Rationale: Consistent formatting across the app. Single place to modify behavior. Less code duplication and potential for bugs.