Skip to content

Commit 7af73c3

Browse files
committed
(docs) add packages/cli/AGENTS.md
1 parent e946697 commit 7af73c3

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

packages/cli/AGENTS.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# AGENTS.md
2+
3+
Guidance for AI coding agents working in this repository. See
4+
<https://agents.md/> for the file convention.
5+
6+
## Coding Guidelines
7+
8+
TypeScript conventions for this project. Rules are normative: **MUST** = enforced, **PREFER** = default unless there's a reason. Each rule has a minimal example. Consistency with existing code beats any rule below.
9+
10+
## Type safety
11+
12+
- **MUST** enable `strict` in tsconfig, plus `noUncheckedIndexedAccess` and `noImplicitReturns`.
13+
- **MUST NOT** use `any`. Use `unknown` at boundaries, then narrow.
14+
```ts
15+
// bad
16+
function parse(s: string): any { return JSON.parse(s); }
17+
// good
18+
function parse(s: string): unknown { return JSON.parse(s); }
19+
```
20+
- **MUST** annotate exported function signatures and public boundaries explicitly. Let inference handle locals.
21+
- **MUST** validate all external input (args, env, config, network, `JSON.parse`) at the boundary with a runtime schema (e.g. Zod), so static types match reality.
22+
```ts
23+
const User = z.object({ id: z.string(), name: z.string() });
24+
type User = z.infer<typeof User>;
25+
const user = User.parse(await res.json()); // throws on bad shape
26+
```
27+
28+
## Modeling data — make illegal states unrepresentable
29+
30+
- **PREFER** discriminated unions over objects with many optional fields. Tag with a literal field.
31+
```ts
32+
// bad: { status: string; data?: T; error?: string }
33+
type Req<T> =
34+
| { status: "loading" }
35+
| { status: "success"; data: T }
36+
| { status: "error"; error: string };
37+
```
38+
- **MUST** use exhaustive `switch` with a `never` guard on unions, so adding a variant is a compile error.
39+
```ts
40+
default: return assertNever(x); // assertNever(x: never): never
41+
```
42+
- **PREFER** precise literal unions over open primitives: `"admin" | "guest"`, not `string`.
43+
- **PREFER** `readonly` fields and `as const` by default; mutate deliberately.
44+
- **PREFER** branded types for values that share a runtime type but not a meaning (IDs, units).
45+
```ts
46+
type UserId = string & { readonly __brand: "UserId" };
47+
```
48+
49+
## Errors and results
50+
51+
- **PREFER** returning a `Result` for *expected* failures; reserve `throw` for programmer error and unrecoverable states.
52+
```ts
53+
type Result<T, E = Error> =
54+
| { ok: true; value: T }
55+
| { ok: false; error: E };
56+
```
57+
- **MUST** model a function's failure modes in its return type when they're part of the domain (not found, invalid, over-limit).
58+
59+
## Functions
60+
61+
- **MUST** keep functions single-responsibility and side-effect-free where possible. Push I/O and mutation to the edges; keep a pure core.
62+
- **MUST** give exported functions explicit return types.
63+
- **PREFER** a union parameter over overloads when the body is shared.
64+
65+
## Classes — use only when justified
66+
67+
- **MUST NOT** use a class when a function or type suffices. A class needs **identity + mutable state + an invariant to protect + behavior**. No state → function. No behavior → type.
68+
```ts
69+
// bad: class Point { constructor(public x: number, public y: number) {} }
70+
type Point = { x: number; y: number };
71+
```
72+
- **MUST** justify every class by an invariant. Ask "what can't happen to an instance?" If the answer is "nothing," it shouldn't be a class. Getters+setters that just wrap a field are not encapsulation.
73+
- **MUST** validate in a static factory when construction can fail; keep the constructor private and do no I/O in it.
74+
```ts
75+
class Email {
76+
private constructor(readonly value: string) {}
77+
static create(s: string): Result<Email, "invalid"> {
78+
return s.includes("@") ? ok(new Email(s)) : err("invalid");
79+
}
80+
}
81+
```
82+
- **MUST NOT** do work in constructors (no I/O, no network, no throwing for expected conditions). Use named async factories: `Config.load()`, `User.fromDatabase()`.
83+
- **MUST** use `#private` fields to protect invariants; expose read via getters, never a bare setter.
84+
- **PREFER** "tell, don't ask": give the object an intent, let it enforce its own rules — don't pull data out to decide, then push a change back in.
85+
```ts
86+
cart.checkout(); // returns Result — not: if (cart.total < limit) cart.status = ...
87+
```
88+
- **MUST** keep the public surface minimal. Default methods to private; promote only when a caller needs them.
89+
- **PREFER** composition over inheritance. Reserve inheritance for genuinely different *behavior*; represent "kinds of" as a discriminated field built by different factories.
90+
- **PREFER** `readonly` fields; keep the few mutable ones obvious.
91+
92+
## Dependency injection
93+
94+
- **MUST** inject dependencies (clock, store, logger) rather than constructing them internally, so logic is testable with fakes.
95+
- **MUST** define interfaces from the *consumer's* view, listing only the methods it uses — not the full capability of what's passed in.
96+
```ts
97+
interface SessionStore { save(id: string, exp: number): Promise<void>; }
98+
// not: constructor(private db: Database) // 40 methods, uses one
99+
```
100+
101+
## File & project structure
102+
103+
- **MUST** keep one primary export per file, and name the file after it: `Email.ts``Email`, `parseConfig.ts``parseConfig`.
104+
- **MUST NOT** create `utils.ts` / `helpers.ts` / `misc/` grab-bags. If you can't name the file after its contents, the contents don't belong together.
105+
- **MUST** enforce a one-directional dependency rule: delivery layer (CLI commands, HTTP routes) → core logic → nothing. Core **MUST NOT** import from I/O or delivery layers.
106+
- **PREFER** casing that signals the export kind, applied consistently: `PascalCase` for files exporting a class/type, `camelCase` for files exporting functions. (Uniform `kebab-case` is also acceptable — pick one project-wide.)
107+
- **PREFER** naming by domain role, not redundant technical suffix. Drop `Model`/`Impl`/`Class`; keep suffixes only when they disambiguate real roles (`User.ts`, `UserRepository.ts`, `UserSchema.ts`).
108+
- **MUST** keep `index.ts` as a re-export doorway only — no logic.
109+
- **MUST** avoid folder/file stutter: `config/loader.ts`, not `config/configLoader.ts`.
110+
- **MUST** colocate tests with a matching name: `planner.ts``planner.test.ts`.
111+
112+
## Meta
113+
114+
- **MUST** match an existing convention over introducing a "better" second one. Predictability is the point; a codebase with two conventions is worse than either alone.

0 commit comments

Comments
 (0)