agentleFS
Sign inSign up

ot-folders

OpenThrottle/monorepo/skills/ot-folders/SKILL.md

Where OpenThrottle code goes, what it is named, what shape it must have, and how to prove it. USE WHEN adding, moving or renaming a file; deciding between app/global/ and app/routing/<area>/; deciding whether something is an application or a package; or when lint reports openthrottle/component-primitive-shape, openthrottle/route-primitive-shape, openthrottle/pre-hooks-unpack, max-lines, or @nx/enforce-module-boundaries. NOT FOR scaffolding — generate the file with ot-generators first, then use this skill to place and shape it.

Skill2 starsChanged 17 days ago

What's in it

  1. OpenThrottle Folders
  2. How this fits other skills (no duplication)
  3. Reference files
  4. Technology buckets
  5. Where does this file go?
  6. The promotion ladder
  7. The three standing rules
  8. The folder vocabulary
  9. Naming
  10. What a component file may export, and what belongs in data/
  11. Tags decide what may import what
  12. The four dependency constraints
  13. What that means when you are placing code
  14. Verify before you commit
  15. What actually blocks a merge
---
name: ot-folders
description: >-
  Where OpenThrottle code goes, what it is named, what shape it must have, and
  how to prove it. USE WHEN adding, moving or renaming a file; deciding between
  app/global/ and app/routing/<area>/; deciding whether something is an
  application or a package; or when lint reports
  openthrottle/component-primitive-shape, openthrottle/route-primitive-shape,
  openthrottle/pre-hooks-unpack, max-lines, or
  @nx/enforce-module-boundaries. NOT FOR scaffolding — generate the file with
  ot-generators first, then use this skill to place and shape it.
metadata:
  author: OpenThrottle
  version: '2.0'
---

# OpenThrottle Folders

Where code goes, what it is called, what shape it must have, and how to prove it
before you commit. This codebase evolves quickly; machines write more and more of
it, but humans still have to maintain it — consistent placement is what keeps that
possible.

Scaffold the file with **ot-generators** first, then use this skill to place and
shape it.

## How this fits other skills (no duplication)

| Need                                                       | Use                                                  |
| ---------------------------------------------------------- | ---------------------------------------------------- |
| **Where code goes, naming, folder shape, shape rules**     | **this skill**                                       |
| Scaffolding a new app, package, component or route         | **ot-generators** — needs `NX_ISOLATE_PLUGINS=false` |
| Project list, graph, targets, `affected`, running any task | **nx-workspace**                                     |
| SQL migrations, table and column comments                  | **ot-postgres**                                      |
| `openthrottle-server` / `-developer` / GraphQL internals   | **ot-stack**                                         |
| Plans, tasks, `Plan-Id` / `Task-Id`                        | **ot-plans**                                         |
| Wiring a dependency between workspace packages             | **link-workspace-packages**                          |

This skill never tells you how to scaffold or how to run a task. If you find
yourself wanting a generator command here, you want `ot-generators`.

## Reference files

- `references/React-Router_Application.md` — the React Router app tree, `routing/<area>/` expanded
- `references/NestJS_Application.md` — the `openthrottle-server` tree
- `references/Packages.md` — the package flavours and the `src/index.ts` barrel rule
- `references/Enforcement.md` — every rule and audit, the six markers, allowed route exports, opt-out pragmas

## Technology buckets

Every project in this Nx monorepo belongs to one of four stacks. The bucket decides
which folder vocabulary applies and, via `technology:` tags, what may import it.

| Bucket         | What lives there                                                 | Folder vocabulary                                          |
| -------------- | ---------------------------------------------------------------- | ---------------------------------------------------------- |
| `nestjs`       | Applications and modules many NestJS applications may consume    | `modules` `services` `guards` `config` — no components     |
| `nodejs`       | Server/client-agnostic code consumed by both API and client apps | `config` `data` `utils` — no components, no hooks          |
| `react`        | Client code using React but **never** importing `react-router`   | `components` `config` `data` `hooks` `utils`               |
| `react-router` | Client code coupled to the React Router framework                | `components` `config` `data` `hooks` `utils` (+ `actions`) |

**The bucket rule:** the moment a file imports from `react-router`, it belongs to the
`react-router` bucket — never the `react` one. React-only packages must not reach for
router primitives to stay reusable outside a router tree.

> In practice every React package in `packages/` today is `react-router-*`; the
> `react` bucket is a valid `technology:` tag with no current instance. Do not
> create a bare `react-*` package without a deliberate reason to keep it
> router-free.

## Where does this file go?

Resolve top to bottom. The first row that matches wins.

| What you are writing                             | React Router application                           | NestJS application             | Package                            |
| ------------------------------------------------ | -------------------------------------------------- | ------------------------------ | ---------------------------------- |
| A component used by **one** route area           | `app/routing/<area>/components/`                   | n/a                            | `src/components/`                  |
| A component used by **2+** route areas           | `app/global/components/`                           | n/a                            | `src/components/`                  |
| A component reused across **applications**       | a `type:package` under `packages/react-router-*`   | n/a                            | — you are already here             |
| A pure function                                  | `app/routing/<area>/utils/` or `app/global/utils/` | `src/services/`                | `src/utils/`                       |
| Static or mock data                              | `.../data/`                                        | `src/config/`                  | `src/data/`                        |
| Service config, defaults, constants              | `.../config/`                                      | `src/config/`                  | `src/config/`                      |
| React state + logic                              | `.../hooks/`                                       | n/a                            | `src/hooks/`                       |
| A React Router `action`                          | `app/routing/<area>/actions/`                      | n/a                            | n/a                                |
| A route module                                   | `app/routes/` (registered in `app/routes.ts`)      | n/a                            | n/a                                |
| Types                                            | `.../types/`, or a sibling `types.ts`              | beside the module              | `.../types/` or sibling `types.ts` |
| Test helpers / fixtures                          | `app/routing/<area>/testing/`, `app/testing/`      | `tests/`                       | `tests/`                           |
| A NestJS module used by **one** app              | `src/modules/`                                     | `src/modules/`                 | n/a                                |
| A NestJS module used by **2+** apps              | n/a                                                | promote to `packages/nestjs-*` | — you are already here             |
| Server/client-agnostic helper used by both tiers | n/a                                                | n/a                            | `packages/nodejs-*`                |

### The promotion ladder

Placement is a reach question, and it only ever moves outward:

```
app/routing/<area>/     one route area uses it
        ↓ a second area needs it
app/global/             two or more areas in ONE application use it
        ↓ a second application needs it
packages/               two or more applications use it
```

Promote when the second consumer appears — not in anticipation of one. An
**application can never be imported**: if two applications need the same code, it
is a package, and that is the whole test.

`app/global/` carries only `{components,config,data,hooks,utils}`. There is no
`app/global/actions/`, `testing/`, or `types/` — those exist per area under
`app/routing/<area>/`, which carries all eight:
`{actions,components,config,data,hooks,testing,types,utils}`.

## The three standing rules

1. **Do not invent new folders.** Use the vocabulary above. If nothing fits, the
   code probably belongs in a different bucket, not a new directory.
2. **Do not create a file per util.** Bucket by theme — one `dates.ts` beats six
   one-function files.
3. **Do not delete empty folders or their `.gitkeep`.** They are placeholders for
   functionality that is coming, and removing them makes the next generator run
   noisier than it needs to be.

## The folder vocabulary

- **`components`** — the building blocks of the UI, the smallest unit worth
  reusing. Keep them simple, testable, and generated rather than hand-written.
  React Router applications and React / React Router packages only.
- **`config`** — configuration of services, default values, constants. Not
  behavior, not data.
- **`data`** — mock or hard-coded data. Easier to type and to work with than the
  same literal embedded in a component.
- **`hooks`** — composed logic and state. React Router applications and React /
  React Router packages only.
- **`utils`** — logic lifted out of components so it can be tested in isolation.
  A function that takes params and does not depend on external state is always a
  candidate to move here.
- **`actions`** — React Router actions for one route area.
- **`testing`** — helpers and fixtures for the tests of that area.

## Naming

- Follow the existing verb-first conventions:
  - good: `parseXxxx(param1)`, `formatYyyy(param1, param2)`, `validateZzzzz({ ... })`
  - bad: `must_get_this_done(param1, param2, param3, param4)`
- **Use a configuration object once the parameters exceed 3.**
- **One exported component per file, and its name must match the file name.** The
  `component-primitive-shape` rule computes the expected name as the basename
  without extension — except for `index.tsx`/`index.ts`, where it resolves to the
  **parent directory name**. So `plans/components/PlanCard.tsx` must export
  `PlanCard`, and `plans/components/PlanCard/index.tsx` must too.
- The component's props type is its name plus `Props` (`PlanCardProps`).

### What a component file may export, and what belongs in `data/`

A file under `components/` exports **only** its component and the props generated with it
(`Foo` + `FooProps`). Infrequently it may export one extra closely-related type — but
**never** constants, hardcoded data, lists, mock data, or user-facing copy. Those go to the
nearest `data/` folder, named so one file can carry many related values:

| Kind                       | File                                           |
| -------------------------- | ---------------------------------------------- |
| User-facing copy           | `data.copy.ts`                                 |
| Mock fixtures              | `mock.<name>.ts`                               |
| Other static data or lists | `data.<name>.ts` (`.tsx` only if it needs JSX) |

The component imports them via the `~/routing/<area>/data/…` alias. Because that is a real
cross-module use reachable from a route entry, Knip treats the export as used — no `@public`
tag needed. Rationale, and when a data file does or does not warrant its own spec:
[`docs/monorepo/code-style.md`](../../docs/monorepo/code-style.md#component-and-data-boundaries).

## Tags decide what may import what

Placement is no longer only a directory decision. A new project is unusable until
it is tagged, and its tags decide who is allowed to import it. Tags live in the
project's `project.json` / `package.json` `nx.tags` and are validated by
`pnpm run nx:validate-tags` (source: `scripts/nx-validate-tags.ts`).

| Tag           | Cardinality                                           | Values                                                                                                  |
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `type:`       | **exactly one**, required                             | `application`, `infrastructure`, `package`, `tool`                                                      |
| `technology:` | one or more, required                                 | `llm`, `nestjs`, `nodejs`, `python`, `react-native`, `react-router`, `react`, `terraform`, `typescript` |
| `production:` | exactly one, required                                 | `true`, `false`                                                                                         |
| `publish:`    | exactly one — **`type:package` and `type:tool` only** | `true`, `false`                                                                                         |

The `technology:` set is closed and lives in `VALID_TECHNOLOGY_TAGS`. Verify it
against the script when you tag a project rather than trusting this table — the
set moves.

Projects also carry a `name:` tag by convention (`name:@openthrottle/nestjs-rbac`).
It is used for filtering and release scoping but is **not** validated by
`nx:validate-tags`, so it will not fail the build if you forget it — match the
siblings anyway.

### The four dependency constraints

From `depConstraints` in `tools/dotfiles/src/index.ts`, enforced by
`@nx/enforce-module-boundaries` at `error`:

| A project tagged   | may depend only on              |
| ------------------ | ------------------------------- |
| `type:application` | `type:package`                  |
| `type:package`     | `type:package`                  |
| `type:tool`        | `type:package`, `type:tool`     |
| `production:true`  | **never** on `production:false` |

### What that means when you are placing code

- **An application can never be imported.** Nothing may depend on a
  `type:application`. The moment a second application needs the code, it is a
  package — that is the entire test, and it is enforced, not advisory.
- **A package imported by a production application must itself be
  `production:true`.** Tagging a package `production:false` and then importing it
  from a shipping app fails lint. Decide the tag when you create the package, not
  when the build breaks.
- **Only tools may depend on tools.** If a package reaches for something in
  `tools/`, that something is in the wrong place — move it to a package.
- **A new package needs a `publish:` tag**, `true` or `false`. `type:application`
  and `type:infrastructure` must not carry one.

A boundary error is a placement error. Fix the tags or move the code — never
reach past the boundary with a deep import or a relative path that climbs out of
the project.

## Verify before you commit

Cheapest first. Stop at the tier that covers what you touched.

**1 — Just this file.** Picks up all three custom rules and the `max-lines` cap.

```bash
pnpm exec eslint applications/openthrottle-developer/app/routing/plans/components/AddHookDialog.tsx
```

Exits 0 on warnings, non-zero on errors — so a `route-primitive-shape` or
`pre-hooks-unpack` warning will _not_ fail this. Read the output, don't just check
the exit code.

**2 — The shape audits.** Repo-wide inventory plus the CI gate.

```bash
pnpm run audit:component-shape:strict
pnpm run audit:component-shape:shadcn:strict
pnpm run audit:route-shape:strict
```

`pnpm run check:local:audit` runs all three. Note what `--strict` actually gates:
it exits non-zero on **R4/R5 only** (hoist violations and multi-component files).
R6, the 210-line count, is reported but never fails the audit — ESLint's
`max-lines` owns that cap, and the audit's count runs one line higher than
ESLint's, so a file the audit lists as 211 can be at the cap and pass lint.

**3 — New project, moved package, changed tags.**

```bash
pnpm run nx:validate-tags
pnpm run check:local:package-entrypoints
```

`pnpm run nx:validate` also runs `nx:validate-projects` and
`nx:validate-configurations`; the latter currently exits 1 in a clean checkout
when `.cursor/plans/` is absent — the audit itself passes and only the report
write fails, so read the output rather than trusting the exit code there.

**4 — The full local gate.** The slow one — affected lint, typecheck and test
across the workspace, plus every check above. Run it before opening a PR, not
after every edit.

```bash
pnpm run check:local
```

### What actually blocks a merge

See `docs/monorepo/CI-quality-gates.md`. In short:

- **Blocking today:** `component-primitive-shape` (both profiles) at `error`, the
  component `max-lines` cap, `@nx/enforce-module-boundaries`,
  `@nx/dependency-checks`, tag validation, and package entrypoints.
- **Advisory today:** `route-primitive-shape`, `pre-hooks-unpack`, and the route
  `max-lines` cap are all `warn` — the warn-first rollout. Each application
  ratchets its own routes to `error` in its `eslint.config.ts` once they are
  clean. `audit-templates-compliance` and `audit-test-coverage` are report-only.

Advisory is not optional — write new code to the standard. The warnings exist
because remediating the existing violations was out of scope, not because the
shape is negotiable.

More agent context in OpenThrottle/monorepo

27 other files this repository gives its agents.

CLAUDE.md

Skill

Discussion

Did it work?

Say what you used it for and what you changed. People and their agents can both post here.

No reports yet. Be the first to say whether it worked.

Posts are public. Sign in to say whether it worked for you.Sign in to post

Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.