---
schema_id: convention
schema_version: 1
applies_to: aiwiki/conventions/**/*.md
filename_pattern: "{slug}.md"
hard_cap_lines: 100
soft_target_lines: [30, 60]
required_frontmatter:
  schema_id: { type: string, equals: convention }
  schema_version: { type: integer }
  scope: { type: string, pattern: "^(project|folder:.+|module:.+)$" }
  date: { type: date }
  status: { type: enum, values: [active, superseded, retired] }
required_sections:
  - "## The convention"
  - "## Rationale"
  - "## Example"
  - "## Counter-example"
section_order: strict
citation_rule: required-in-example-and-counter-example
---

# Schema: convention (codebase pattern)

## Purpose

A convention records a "how this codebase does X" pattern. The page answers "what's the rule? show me what right looks like, and what wrong looks like." Conventions are short by nature; if you need more than a paragraph to state the rule, it's probably an ADR or an architecture doc.

## When to write one

- A pattern emerges across multiple files during prototype iteration or production build (e.g. how route handlers are named, where shared types live, how errors are formatted)
- A code reviewer flags "this doesn't match how we usually do X" — write the convention so the next contributor knows
- A gotcha cluster signals a missing convention — the convention prevents future occurrences

Do NOT write a convention for: language-level idioms (use a linter or formatter instead), things that are obvious from a reasonable read of the codebase, one-off patterns that exist in a single file.

## File location and naming

- Path: `aiwiki/conventions/{slug}.md`
- Slug: kebab-case, ≤6 words, naming the pattern (not the rationale)

Examples: `route-handler-naming.md`, `error-shape.md`, `shared-types-location.md`.

## Required frontmatter

| Field | Type | Notes |
|---|---|---|
| `schema_id` | string | Must equal `convention` |
| `schema_version` | integer | — |
| `scope` | string | `project` / `folder:<path>` / `module:<name>` — what this convention applies to |
| `date` | ISO date | When the convention was codified |
| `status` | enum | `active` / `superseded` (newer convention takes priority — link it) / `retired` |

## Required sections

| Section | Purpose | Format |
|---|---|---|
| `## The convention` | One paragraph in imperative mood — the rule itself | No fluff, no rationale |
| `## Rationale` | Why this convention exists | Brief; link to ADR or gotcha if one motivates it |
| `## Example` | Code that follows the convention | Cite the example code with `file:line@<sha7>` |
| `## Counter-example` | Code that violates the convention (or what it would look like) | Cite if real, or write a synthetic counter-example |

## Line caps

- Hard cap: 100 lines (LINT fails above)
- Soft target: 30-60 lines

A convention that needs more than 100 lines is probably an architecture doc or a tutorial — write it as one of those instead.

## Citation rules

- `## Example` and `## Counter-example` MUST cite real code (or be marked synthetic explicitly)
- Code references use `file:line@<sha7>` or `symbol` form
- LINT auto-fills missing `@<sha7>` on first save

## Skeleton

```markdown
---
schema_id: convention
schema_version: 1
scope: folder:src/routes
date: 2026-05-10
status: active
---

## The convention

Route handlers live in `src/routes/{resource}.ts` and export a `{resource}Router` constant. Each handler function is exported separately for testing; the router is the wiring layer that mounts them.

## Rationale

Separates handler logic (testable in isolation) from routing (testable as integration). See [aiwiki/decisions/0017-handler-router-split.md](aiwiki/decisions/0017-handler-router-split.md).

## Example

[src/routes/users.ts:1-24@e5f6789](src/routes/users.ts) — `usersRouter` mounts three handlers (`listUsers`, `getUser`, `createUser`) that are individually exported.

## Counter-example

<!-- synthetic example; replace with real counter-example before publishing -->

```ts
// DO NOT do this — handler logic and routing collapsed into a single anonymous function
app.get('/users', async (req, res) => {
  const users = await db.users.findAll();
  res.json(users);
});
```

The closure can't be unit-tested without spinning up the router; the route registration and the handler are entangled.
```
