diff --git a/.agent/rules/porting.mdc b/.agent/rules/porting.mdc index 0d0fa732..0f5caf5f 100644 --- a/.agent/rules/porting.mdc +++ b/.agent/rules/porting.mdc @@ -71,12 +71,33 @@ const nullishString = z.string().nullish().transform(v => v ?? ''); const goString = z.string().nullish().transform(v => v ?? ''); ``` +## Critical Thinking + +**Do not blindly copy.** Before porting each function, type, or test, ask: + +1. **Does TypeScript need it?** Some Go code exists only because Go requires + explicit opt-in for features that TypeScript provides natively. For example, + Go requires an explicit `Unwrap() error` method for error chain traversal; + TypeScript gets this for free via `Error.cause`. Do not port such code. +2. **Does the test validate our code or the language?** If a Go test verifies + behaviour that is built into the TypeScript runtime (e.g. that `Error.cause` + preserves a reference), skip it — it tests the language, not our + implementation. Explain the skip to the user. + +When in doubt, ask the user before porting. + +## Factory Functions vs Constructors + +When a Go function can return `nil` (e.g. `FromHTTPError` returns `nil` for 2xx +status codes), port it as a **factory function** that returns `T | undefined`. +Constructors in TypeScript always return an instance and cannot express "no +result." Use a standalone function or a static method on the class. + ## Tests -Port **all** test cases from the Go SDK. Do not selectively skip tests. When a -Go test case cannot be directly ported (e.g. because of a language difference -like `json.RawMessage` vs already-parsed JSON), explain the omission to the -user and get confirmation before skipping. +Port **all** test cases from the Go SDK unless they fall under the "Critical +Thinking" exceptions above. Do not selectively skip tests without explaining +the reason to the user. Use the same test structure as the Go SDK. Go table-driven tests map to Vitest's `it.each`: @@ -101,6 +122,22 @@ const testCases: {name: string; input: number; want: number}[] = [ it.each(testCases)('$name', ({input, want}) => { ... }); ``` +## No Go References in Code + +**Never reference Go in code or comments.** This includes variable names, +function names, inline comments, and JSDoc. The TypeScript codebase must read +as if Go does not exist. Do not write comments like "mirrors Go's +tc.want.httpErr" or "equivalent of Go's errors.As." + +```typescript +// Bad — references the Go implementation. +// Input HTTP fields are always preserved (mirrors Go's +// tc.want.httpErr = &httpError{statusCode, header, body}). + +// Good — describes what the code does without referencing Go. +// Input HTTP fields are always preserved. +``` + ## Deviations from Go When the TypeScript implementation must differ from Go (e.g. different function diff --git a/.agent/rules/testing.mdc b/.agent/rules/testing.mdc index b567ce79..3ee7ccce 100644 --- a/.agent/rules/testing.mdc +++ b/.agent/rules/testing.mdc @@ -87,6 +87,48 @@ expect(token.accessToken).toBe('dapi...'); expect(result).toStrictEqual({host: 'example.com', port: 443}); ``` +## Never Silently Skip Assertions + +Do not use `return` in a test path where a failure should be reported. If a +precondition fails, use `expect.fail()` so the test fails loudly instead of +silently passing with no assertions. + +```typescript +// Good — fails loudly if got is unexpectedly undefined. +if (got === undefined) { + expect.fail('expected a result'); +} + +// Bad — silently skips all remaining assertions. +if (got === undefined) { + return; +} +``` + +## Test All Outputs + +When a function returns an object, verify all its fields — not just the ones +that seem interesting. Do not skip fields because they appear "obvious" or are +"just pass-through." If the function sets a field, the test should check it. + +## Use Real Types for Expected Values + +When the expected output is a class instance, use that class as the type of +the `want` field in test tables. Do not invent anonymous object types that +duplicate the class shape. + +```typescript +// Good — want is a real APIError. +const testCases: {desc: string; want?: APIError}[] = [ + {desc: 'not found', want: new APIError({code: Code.NOT_FOUND, ...})}, +]; + +// Bad — anonymous type duplicates APIError's shape. +const testCases: {desc: string; want?: {code: Code; message: string}}[] = [ + {desc: 'not found', want: {code: Code.NOT_FOUND, message: '...'}}, +]; +``` + ## Independence Each test must be independent. Do not rely on execution order. Clean up side diff --git a/.agent/rules/typescript.mdc b/.agent/rules/typescript.mdc index b44cbe59..20c64d80 100644 --- a/.agent/rules/typescript.mdc +++ b/.agent/rules/typescript.mdc @@ -443,6 +443,12 @@ Avoid private static methods; prefer module-level functions instead. Never use Always include parentheses in constructor calls: `new Foo()`, not `new Foo`. +### 7.7 API Surface Minimization + +Default to the smallest public API surface. Only export from `index.ts` what +consumers need. Internal types (options interfaces, schemas, helper functions) +must not be re-exported. + --- ## 8. Control Flow diff --git a/AGENTS.md b/AGENTS.md index 5769cccb..f6e9256d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,4 +99,14 @@ npm run clean with a period. 6. **Back up claims.** When proposing a design decision or asserting a convention, provide concrete references (documentation, API links). Do not - state something is "idiomatic" or "standard" without evidence. + state something is "idiomatic" or "standard" without evidence. Use + authoritative primary sources (language specs, official documentation) — not + blog posts, archived repositories, or npm packages. +7. **Stay in scope.** Only change what was asked. Each change should be + reviewable in isolation. +8. **Never silently remove code.** If a change requires deleting existing + code or tests, explain what is being removed and why **before** proceeding. + Get explicit confirmation from the user. +9. **Match existing patterns.** Before writing new code, check existing code + for established patterns. Do not invent new conventions + when the codebase already has one.