Skip to content

Commit 94ed2a8

Browse files
authored
docs: add typescript agent conventions (#76)
1 parent a9f4314 commit 94ed2a8

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,99 @@
55
- Log papercuts and friction (tooling, docs, APIs, tests, conventions) as you hit them with `pnpx frog log`.
66
- Do not add global, system, or internal friction.
77
- Run `pnpx frog list` first to see what is already known.
8+
9+
## TypeScript Conventions
10+
11+
- Treat `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` as design constraints. Include `| undefined` when an optional property can explicitly receive `undefined`, and narrow indexed reads before use.
12+
- Use `readonly T[]` for array types. Preserve mutable arrays only when mutation is part of the contract.
13+
- Use `type` for project-owned shapes. Use `interface` only when declaration merging or an external ambient contract requires it.
14+
- Include `.js` extensions on relative imports and exports so source remains valid under NodeNext ESM.
15+
- Import module-shaped internal files as namespaces (`import * as Store from './Store.js'`) and access members through the module name. Named imports are fine for types, leaf helpers, command handlers, and third-party APIs that are not module namespaces.
16+
- Import Node built-ins as namespaces unless the neighboring code or API is clearer with a named import.
17+
- Re-export public module files as namespaces (`export * as Store from './Store.js'`). Avoid flattening sibling-module symbols into a barrel.
18+
- Use static imports. Reserve dynamic imports for a real runtime or bundle boundary, not dependency-cycle workarounds.
19+
- Use `import type` and `export type` where an import or export is type-only.
20+
- Use functions and plain data for normal APIs. Classes are limited to errors and framework-required entrypoints such as Durable Objects.
21+
- Keep error classes in the module that throws them, below the public functions and types. Set custom error `name` values to the namespaced form, such as `Config.InvalidError`.
22+
- Use unions or `as const` objects instead of enums.
23+
- Prefer `camelCase` constants. Preserve uppercase names only when they mirror an external protocol or established neighboring code.
24+
- Use `const` generic parameters when callers should retain literal or tuple types.
25+
- Default optional option bags in the signature (`options: fn.Options = {}`), not with `options?: fn.Options` and downstream fallback logic.
26+
- Name typed option bags `options`. Use a domain noun only when the value is not an options bag.
27+
- Prefer one named object parameter over several positional parameters. An instance-like receiver such as `client`, `cache`, or `store` may be the first positional parameter, followed by an options bag.
28+
- Put a function's parameter, return, and error types in a matching `declare namespace` (`resolve.Options`, `resolve.ReturnType`, `resolve.ErrorType`). Keep a sibling exported type only when several functions share the domain type.
29+
- Avoid inline object types on local variables. When an explicit local object type improves clarity, name it directly above the use.
30+
- Do not extract a named type until it is reused or makes a difficult shape materially easier to read.
31+
- Keep shared domain types beside the module that owns the concept.
32+
- Let declared return types constrain intermediate expressions. Avoid redundant local annotations.
33+
- Return values directly unless a binding is reused or gives a complex expression a useful name.
34+
- For a fallible local derivation, prefer an IIFE expression over a mutable variable assigned across `try` and `catch` blocks.
35+
- Destructure when reading several properties. When normalizing one field, read `options.field` directly instead of creating a second name.
36+
- Prefer short names whose meaning is clear from local context, such as `options`, `client`, `entry`, and `fn`.
37+
- Keep wire formats, ordered tuples, protocol fields, and other order-sensitive shapes explicit. Do not alphabetize data whose order has meaning.
38+
- Avoid new `any`. Use a precise boundary type, validation, narrowing, or the smallest justified assertion.
39+
- Do not use section-divider comments. Use exports, TSDoc, and whitespace to express module structure.
40+
- Comment invariants and non-obvious reasons, not line-by-line mechanics. Keep comments independent of plans, task IDs, and prior versions.
41+
42+
## Module and Instance Conventions
43+
44+
- Organize each module file as one conceptual namespace containing its public types, constants, functions, and errors. Consumers should read calls as `Module.operation(...)`.
45+
- Prefer stateless module functions for pure behavior. Do not create an instance when inputs fully describe the operation.
46+
- When behavior needs dependencies or lifecycle state, expose a factory such as `Module.create(options)` or `Module.runtime(options)` that returns a plain object of operations and data.
47+
- Construct instances explicitly at the application boundary and pass them down. Do not hide construction in imports or module-scope singletons.
48+
- Scope an instance to the lifecycle that owns its state, such as one CLI run, Worker isolate, request pipeline, or test. Do not share mutable state more broadly than required.
49+
- Inject environment-specific capabilities through factory options or narrow structural types. Keep filesystem, network, cache, clock, and platform bindings out of domain modules.
50+
- Type dependencies by the smallest capability the module consumes, not by the concrete SDK client. This keeps adapters interchangeable without wrapper classes.
51+
- Let factory operations close over shared dependencies and state. Do not return methods that depend on `this` or require binding.
52+
- Name the returned public shape after its role (`Runtime`, `Client`, `Cache`, `Store`) when that role is meaningful. Use `create.ReturnType` for a factory-specific shape that has no independent domain name.
53+
- Keep one authoritative instance value. Derive related helpers and views from that instance instead of duplicating configuration or state.
54+
- Keep mutable caches and registries private to the instance. Expose explicit operations, and provide reset or disposal only when the lifecycle requires it.
55+
- Avoid side-effect registration. `sideEffects: false` means an import used only to install global behavior can disappear from a bundle.
56+
- Keep default implementations directly reachable from the factory or resolver that selects them so bundlers can tree-shake unused paths.
57+
- Pass an existing receiver first to stateless operations (`Github.publish(client, options)`). Create a factory only when several operations genuinely share dependencies, state, or lifecycle.
58+
- Keep transport-independent planning, parsing, and normalization pure. Put filesystem, GitHub, and Worker behavior in thin adapters around that core.
59+
- Keep internal helpers under `internal/` and export them only when another module has a real contract with them.
60+
- Add new public modules through the owning entrypoint as documented namespace exports. Keep the public surface lean and derive values that the library already knows.
61+
- A framework-mandated class should delegate reusable logic to module functions so the class remains a small lifecycle adapter.
62+
63+
## Type Inference Conventions
64+
65+
- Preserve literal inputs through public helpers when those literals affect the output type.
66+
- Keep generic types flowing from inputs through callbacks and return values. Do not erase them to `any` at an internal seam.
67+
- Prevent public callbacks, options, and return values from leaking `any`.
68+
- Add colocated `.test-d.ts` coverage with `expectTypeOf` when public inference or narrowing changes.
69+
- Revisit inference after changing an API. Prefer a narrower useful contract over a broad type that merely compiles.
70+
71+
## Abstraction Conventions
72+
73+
- Start with concrete code and extract only after repeated uses reveal a stable shared contract.
74+
- Prefer small local duplication over an abstraction that adds flags, modes, or call-site-specific branches.
75+
- Wait for at least three concrete uses before introducing a general abstraction unless a hard boundary already exists.
76+
- Optimize for code that is easy to change, not maximum DRYness.
77+
- Keep authoritative state, configuration, schemas, and constants in one place; derive dependent values.
78+
- Avoid wrappers that only rename another function or mirror an SDK without narrowing capabilities or adding a domain contract.
79+
80+
## Documentation Conventions
81+
82+
- Add TSDoc to every public export and public type property. Write or update the contract documentation alongside the implementation.
83+
- Document caller-visible purpose, inputs, output, defaults, errors, and side effects. Keep low-level wiring in nearby implementation comments.
84+
- Keep examples small and focused on the exported behavior.
85+
- Update the owning entrypoint documentation when adding or changing a public module.
86+
87+
## Testing Conventions
88+
89+
- Colocate unit and type tests with the module they cover.
90+
- Give each exported function under test its own `describe('functionName', ...)` block.
91+
- Prefer inline snapshots for stable structured values and thrown errors. Remove nondeterministic fields before snapshotting the remaining object.
92+
- Test observable behavior, meaningful edge cases, and public errors. Do not derive expected values from the implementation under test.
93+
- Exercise pure functions directly and composed behavior through the real adapter boundary. Avoid mocks when a real local implementation or narrow in-memory adapter is practical.
94+
- Add deterministic regression coverage for every bug fix.
95+
- Write behavioral and type tests alongside the implementation rather than after the module is complete.
96+
97+
## Workflow Conventions
98+
99+
- Use the smallest repository script that covers the changed behavior. Run focused tests while iterating.
100+
- Run `pnpm check:types` after TypeScript changes.
101+
- Run `pnpm test <paths>` for focused tests and `pnpm test` when the change warrants the full suite.
102+
- Treat `pnpm check` as mutating because it applies fixes. Inspect and keep only task-related changes.
103+
- Run `git diff --check` and inspect the final diff before reporting completion.

0 commit comments

Comments
 (0)