@tsfpp/agents 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +216 -31
- package/copilot/agents/tsfpp-annotate.agent.md +99 -119
- package/copilot/agents/tsfpp-audit.agent.md +182 -25
- package/copilot/agents/tsfpp-backfill-tests.agent.md +40 -1
- package/copilot/agents/tsfpp-guarded-coding.agent.md +22 -0
- package/copilot/agents/tsfpp-tdd.agent.md +59 -6
- package/copilot/prompts/trunk-changelog.prompt.md +160 -0
- package/copilot/skills/annotation-standard/SKILL.md +196 -0
- package/init.mjs +211 -197
- package/package.json +1 -1
|
@@ -36,6 +36,13 @@ Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
+
## Before writing any test
|
|
40
|
+
|
|
41
|
+
Load and apply the `/test-standard` skill. Every test you write must conform to
|
|
42
|
+
all rules in that skill. Do not write a single test before the skill is loaded.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
39
46
|
## Session start
|
|
40
47
|
|
|
41
48
|
Infer the layer per task from the user's message:
|
|
@@ -153,6 +160,36 @@ All rules from `TEST_CODING_STANDARD.md` apply. The most critical during test au
|
|
|
153
160
|
|
|
154
161
|
---
|
|
155
162
|
|
|
163
|
+
## Factories
|
|
164
|
+
|
|
165
|
+
Always use typed factory functions from `tests/factories/` for test data.
|
|
166
|
+
Never write raw object literals inline.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// tests/factories/track.factory.ts
|
|
170
|
+
const makeTrack = (overrides: Partial<Track> = {}): Track => ({
|
|
171
|
+
id: mkTrackId('test-track-001'),
|
|
172
|
+
title: 'Default Title',
|
|
173
|
+
artistId: mkArtistId('test-artist-001'),
|
|
174
|
+
...overrides,
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Import and use with overrides for the specific case under test:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// Specific case — override only what matters for this test
|
|
182
|
+
const track = makeTrack({ title: 'Blue Flame' })
|
|
183
|
+
|
|
184
|
+
// Default case — the specific values don't matter
|
|
185
|
+
const track = makeTrack()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Never hard-code raw objects like `{ id: 'abc', title: 'Test', artistId: 'xyz' }` in test bodies.
|
|
189
|
+
Never use production or staging IDs in fixtures.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
156
193
|
## Layer-specific test patterns
|
|
157
194
|
|
|
158
195
|
### `core`
|
|
@@ -164,14 +201,21 @@ import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
|
|
|
164
201
|
describe('mkTrackId', () => {
|
|
165
202
|
describe('when the input is a non-empty string', () => {
|
|
166
203
|
it('returns Some containing a branded TrackId', () => {
|
|
167
|
-
const
|
|
204
|
+
const raw = 'abc'
|
|
205
|
+
|
|
206
|
+
const result = mkTrackId(raw)
|
|
207
|
+
|
|
168
208
|
expect(isSome(result)).toBe(true)
|
|
169
209
|
})
|
|
170
210
|
})
|
|
171
211
|
|
|
172
212
|
describe('when the input is empty', () => {
|
|
173
213
|
it('returns None', () => {
|
|
174
|
-
|
|
214
|
+
const raw = ''
|
|
215
|
+
|
|
216
|
+
const result = mkTrackId(raw)
|
|
217
|
+
|
|
218
|
+
expect(result).toEqual(none)
|
|
175
219
|
})
|
|
176
220
|
})
|
|
177
221
|
|
|
@@ -200,11 +244,13 @@ describe('POST /v1/tracks', () => {
|
|
|
200
244
|
describe('when the request body is valid', () => {
|
|
201
245
|
it('responds with 201 and a Location header', async () => {
|
|
202
246
|
const req = new Request('http://localhost/v1/tracks', {
|
|
203
|
-
method:
|
|
204
|
-
body:
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
205
249
|
headers: { 'Content-Type': 'application/json' },
|
|
206
250
|
})
|
|
251
|
+
|
|
207
252
|
const res = await handler(req)
|
|
253
|
+
|
|
208
254
|
expect(res.status).toBe(201)
|
|
209
255
|
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
210
256
|
})
|
|
@@ -212,12 +258,16 @@ describe('POST /v1/tracks', () => {
|
|
|
212
258
|
|
|
213
259
|
describe('when title is missing', () => {
|
|
214
260
|
it('responds with 422', async () => {
|
|
261
|
+
const input = makeCreateTrackInput({ title: undefined }) // override to trigger validation failure
|
|
262
|
+
|
|
215
263
|
const req = new Request('http://localhost/v1/tracks', {
|
|
216
|
-
method:
|
|
217
|
-
body:
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: JSON.stringify(input),
|
|
218
266
|
headers: { 'Content-Type': 'application/json' },
|
|
219
267
|
})
|
|
268
|
+
|
|
220
269
|
const res = await handler(req)
|
|
270
|
+
|
|
221
271
|
expect(res.status).toBe(422)
|
|
222
272
|
})
|
|
223
273
|
})
|
|
@@ -266,7 +316,9 @@ describe('TrackRepository', () => {
|
|
|
266
316
|
it('returns Some containing the track', async () => {
|
|
267
317
|
const track = makeTrack()
|
|
268
318
|
await repo.save(track)
|
|
319
|
+
|
|
269
320
|
const result = await repo.findById(track.id)
|
|
321
|
+
|
|
270
322
|
expect(isSome(result)).toBe(true)
|
|
271
323
|
})
|
|
272
324
|
})
|
|
@@ -274,6 +326,7 @@ describe('TrackRepository', () => {
|
|
|
274
326
|
describe('when the track does not exist', () => {
|
|
275
327
|
it('returns None', async () => {
|
|
276
328
|
const result = await repo.findById(mkTrackId('nonexistent'))
|
|
329
|
+
|
|
277
330
|
expect(isNone(result)).toBe(true) // will fail — findById not implemented
|
|
278
331
|
})
|
|
279
332
|
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Write changelog entry
|
|
2
|
+
|
|
3
|
+
Inspect the current working tree, derive the correct conventional commit message,
|
|
4
|
+
and append a matching entry to the `## [Unreleased]` section of `CHANGELOG.md`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Step 1 — Inspect changes
|
|
9
|
+
|
|
10
|
+
Run the following to see what has changed:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git diff --stat HEAD
|
|
14
|
+
git status --short
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For each changed or added file, read enough of its diff to understand **what
|
|
18
|
+
changed and why** — not just which lines moved.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git diff HEAD -- <file>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If files are staged but not committed, use:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git diff --cached --stat
|
|
28
|
+
git diff --cached -- <file>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Step 2 — Classify changes
|
|
34
|
+
|
|
35
|
+
Map each change to a Conventional Commits type:
|
|
36
|
+
|
|
37
|
+
| Type | Use when |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `feat` | New behaviour or capability visible to a consumer |
|
|
40
|
+
| `fix` | Corrects incorrect behaviour |
|
|
41
|
+
| `perf` | Improves performance without changing behaviour |
|
|
42
|
+
| `refactor` | Internal restructuring; no behaviour change, no bug fix |
|
|
43
|
+
| `test` | Adds or fixes tests; no production code change |
|
|
44
|
+
| `docs` | Documentation only |
|
|
45
|
+
| `chore` | Tooling, config, dependencies, release machinery |
|
|
46
|
+
| `build` | Build system or external dependency changes |
|
|
47
|
+
| `ci` | CI configuration changes |
|
|
48
|
+
|
|
49
|
+
**Breaking change:** any change that removes or renames a public export, changes
|
|
50
|
+
a function signature, or alters a type in a way that requires consumer updates.
|
|
51
|
+
Mark with `!` after the type (e.g. `feat!`) and add a `BREAKING CHANGE:` footer.
|
|
52
|
+
|
|
53
|
+
**Scope:** the package or module affected — e.g. `prelude`, `boundary`, `agents`,
|
|
54
|
+
`react`, `dal`. Omit if the change is cross-cutting.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 3 — Write the commit message
|
|
59
|
+
|
|
60
|
+
Produce a conventional commit message following this format exactly:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
<type>(<scope>): <imperative summary in sentence case, ≤ 72 chars>
|
|
64
|
+
|
|
65
|
+
<optional body — what changed and why, not how, wrapped at 72 chars>
|
|
66
|
+
|
|
67
|
+
<optional footers>
|
|
68
|
+
BREAKING CHANGE: <description if applicable>
|
|
69
|
+
Closes #<issue> (if applicable)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- Summary is imperative mood: "add", "fix", "remove" — not "added", "fixes"
|
|
74
|
+
- Summary does not end with a period
|
|
75
|
+
- Body explains the **why**, not the **what** (the diff is the what)
|
|
76
|
+
- One commit per logical change; if the diff contains multiple unrelated changes,
|
|
77
|
+
produce one message per change and say so
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Step 4 — Update CHANGELOG.md
|
|
82
|
+
|
|
83
|
+
Find or create the `## [Unreleased]` section at the top of `CHANGELOG.md`.
|
|
84
|
+
If `CHANGELOG.md` does not exist, create it with this header:
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
# Changelog
|
|
88
|
+
|
|
89
|
+
All notable changes to this project will be documented in this file.
|
|
90
|
+
This file is maintained automatically by [release-please](https://github.com/googleapis/release-please)
|
|
91
|
+
and supplemented during development via the `/trunk-changelog` prompt.
|
|
92
|
+
|
|
93
|
+
<!-- do not remove this comment — release-please uses it as an anchor -->
|
|
94
|
+
<!-- RELEASE-PLEASE-INSERTION-POINT -->
|
|
95
|
+
|
|
96
|
+
## [Unreleased]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Append the new entry under `## [Unreleased]`, grouped by type in this order:
|
|
100
|
+
|
|
101
|
+
```markdown
|
|
102
|
+
## [Unreleased]
|
|
103
|
+
|
|
104
|
+
### Breaking changes
|
|
105
|
+
- `feat!(boundary)!: remove legacy `fold` export` — consumers must migrate to `map`/`flatMap`
|
|
106
|
+
|
|
107
|
+
### Features
|
|
108
|
+
- `feat(prelude): add ReadonlyMap combinators` — `intoMap`, `assoc`, `dissoc`, `lookup`, `entriesOfMap`
|
|
109
|
+
|
|
110
|
+
### Bug fixes
|
|
111
|
+
- `fix(agents): init.mjs fails with ReferenceError when run with --yes`
|
|
112
|
+
|
|
113
|
+
### Performance
|
|
114
|
+
- ...
|
|
115
|
+
|
|
116
|
+
### Refactoring
|
|
117
|
+
- ...
|
|
118
|
+
|
|
119
|
+
### Tests
|
|
120
|
+
- ...
|
|
121
|
+
|
|
122
|
+
### Documentation
|
|
123
|
+
- ...
|
|
124
|
+
|
|
125
|
+
### Chores
|
|
126
|
+
- ...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Only include sections that have entries. Do not add empty sections.
|
|
130
|
+
|
|
131
|
+
Each entry is a single line:
|
|
132
|
+
- Backtick-quoted commit summary
|
|
133
|
+
- Em dash
|
|
134
|
+
- One-sentence plain-English explanation of the user-visible impact
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Step 5 — Output
|
|
139
|
+
|
|
140
|
+
Print the proposed commit message in a code block so it can be copied directly
|
|
141
|
+
into the terminal or used with `git commit`:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
git commit -m "<type>(<scope>): <summary>" \
|
|
145
|
+
-m "<body paragraph if needed>"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
If there are multiple logical changes, list each message separately and recommend
|
|
149
|
+
committing them individually with `git add -p` to stage per-change.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Rules
|
|
154
|
+
|
|
155
|
+
- Never invent changes that are not visible in the diff
|
|
156
|
+
- Never write a changelog entry for a change that has no user-visible impact
|
|
157
|
+
(internal renaming, comment edits) — use `chore` or `refactor` in the commit
|
|
158
|
+
message but omit from `## [Unreleased]`
|
|
159
|
+
- Do not modify any section of `CHANGELOG.md` other than `## [Unreleased]`
|
|
160
|
+
- release-please owns every versioned section (`## [1.2.3]`) — never touch those
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: annotation-standard
|
|
3
|
+
description: >
|
|
4
|
+
Normative TSF++ annotation rules for all comments, JSDoc blocks, module
|
|
5
|
+
headers, code markers, and deviation records. Load when writing, reviewing,
|
|
6
|
+
or adding annotations to any TypeScript file: the "why not what" principle,
|
|
7
|
+
JSDoc body content (invariants, rejected alternatives, external contracts,
|
|
8
|
+
accepted imprecision, performance trade-offs), marker taxonomy with format,
|
|
9
|
+
DEVIATION pairing, and what must never be annotated.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# TSF++ annotation standard
|
|
13
|
+
|
|
14
|
+
Full standard: `node_modules/@tsfpp/standard/spec/ANNOTATION_CODING_STANDARD.md`
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The single deciding question
|
|
19
|
+
|
|
20
|
+
> Does this tell the reader something they cannot confidently derive from the code and its types?
|
|
21
|
+
|
|
22
|
+
If no — do not add the comment. If yes — write it.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What only a comment can tell you
|
|
27
|
+
|
|
28
|
+
| Category | Example |
|
|
29
|
+
|---|---|
|
|
30
|
+
| **Why this approach** over the natural alternative | Why linear scan instead of `Map` lookup |
|
|
31
|
+
| **Rejected alternatives** | What was considered and why it was ruled out |
|
|
32
|
+
| **Non-obvious invariants** | Preconditions the type cannot express |
|
|
33
|
+
| **Domain knowledge** | Business rules that live in the problem space |
|
|
34
|
+
| **External contracts** | Field names / values dictated by a third party |
|
|
35
|
+
| **Accepted imprecision** | Known limitations that are intentional |
|
|
36
|
+
| **Performance trade-offs** | Why a non-obvious implementation was chosen |
|
|
37
|
+
| **Temporal context** | Why a workaround exists, when to revisit it |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## JSDoc body: the why, not the what
|
|
42
|
+
|
|
43
|
+
The first sentence is the purpose. Everything after is the reasoning.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
/**
|
|
47
|
+
* Constructs a validated `UserId` from a raw string.
|
|
48
|
+
*
|
|
49
|
+
* Returns `None` if the input is empty. The empty case is excluded rather
|
|
50
|
+
* than mapped to an error because an empty ID indicates a caller bug, not
|
|
51
|
+
* a domain error — the type system prevents this at compile time in
|
|
52
|
+
* internal code; this guard exists for boundary inputs only.
|
|
53
|
+
*
|
|
54
|
+
* @param raw - The raw string to validate. Must be non-empty.
|
|
55
|
+
* @returns `Some(UserId)` if valid; `None` if empty.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* mkUserId('usr-00123') // => some(UserId('usr-00123'))
|
|
59
|
+
* mkUserId('') // => none
|
|
60
|
+
*/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`@param` describes the domain constraint, not the type. `@returns` describes the meaning, not the type. Both are already in the signature.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Module header
|
|
68
|
+
|
|
69
|
+
Required on every file with public exports:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
/**
|
|
73
|
+
* @module user-account
|
|
74
|
+
*
|
|
75
|
+
* Domain model for user accounts. Provides the `UserAccount` sum type,
|
|
76
|
+
* its smart constructors, and the combinators for working with account
|
|
77
|
+
* state and identity.
|
|
78
|
+
*
|
|
79
|
+
* All functions are pure and total. Error cases are modelled via
|
|
80
|
+
* `Option<A>` (absent value) or `Result<T, E>` (fallible computation).
|
|
81
|
+
*
|
|
82
|
+
* @packageDocumentation
|
|
83
|
+
*/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
First sentence: what the module provides. Second paragraph: key design constraints a consumer needs to know. Never describe the implementation.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Inline comment patterns
|
|
91
|
+
|
|
92
|
+
### Rejected alternative
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// NOTE(rob, 2026-05-18): Linear scan rather than `ReadonlyMap` lookup.
|
|
96
|
+
// The active session list is always ≤10 items per user; the allocation
|
|
97
|
+
// overhead of a map outweighs the O(1) lookup benefit at this scale.
|
|
98
|
+
const found = sessions.find(s => s.id === id)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Non-obvious invariant
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// Invariant: `handlers` must be registered before this is called.
|
|
105
|
+
// The runtime guarantees registration order; do not call from a module
|
|
106
|
+
// initialiser that may run before the framework bootstraps.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### External contract
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// IMPORTANT: The field name `client_id` is specified by OAuth2 RFC 6749
|
|
113
|
+
// and must not be renamed despite the camelCase convention.
|
|
114
|
+
// DEVIATION(1.8): Field name required by external protocol.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Accepted imprecision
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// NOTE(rob, 2026-05-18): Timestamp comparison has ≤1 s imprecision due
|
|
121
|
+
// to clock drift between service instances. Acceptable for audit logs;
|
|
122
|
+
// not acceptable for financial ordering.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Code markers
|
|
128
|
+
|
|
129
|
+
Required format — no exceptions:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// MARKER(author, YYYY-MM-DD[, TICKET]): description
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Marker | Use when | Blocks merge? |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `TODO` | Work required before next release | Soft — needs ticket |
|
|
138
|
+
| `FIXME` | Known bug the author is aware of | Yes |
|
|
139
|
+
| `HACK` | Temporary workaround with a deferred correct solution | Yes — needs ticket + revisit condition |
|
|
140
|
+
| `NOTE` | Context a reader needs to understand the code | No |
|
|
141
|
+
| `OPTIMIZE` | Correct but with a known performance concern at scale | No — needs scale threshold |
|
|
142
|
+
| `BUG` | Confirmed bug not yet in a ticket | Yes — convert to FIXME + ticket |
|
|
143
|
+
| `XXX` | Fragile or load-bearing — must not be casually changed | No — use sparingly |
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// TODO(rjansen, 2026-05-18, ARCH-44): Replace with Result-based validation
|
|
147
|
+
// once the boundary refactor lands in v2.0.
|
|
148
|
+
// HACK(rjansen, 2026-05-18, INFRA-12): Forced cast — third-party type
|
|
149
|
+
// definition is wrong. Fixed upstream in v4.x — remove after upgrade.
|
|
150
|
+
// NOTE(rjansen, 2026-05-18): Rate-limit window resets at midnight UTC,
|
|
151
|
+
// not relative to first request. Contractual requirement — do not change.
|
|
152
|
+
// XXX(rjansen, 2026-05-18): Initialisation order is load-bearing. The
|
|
153
|
+
// store must be hydrated before any handler is registered.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Author = GitHub handle or initials. Never an AI. If unknown, use `unknown`.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## DEVIATION format
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// DEVIATION(N.M): <reason the violation could not be avoided>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The justification explains why no alternative was feasible — not what the violation is.
|
|
167
|
+
|
|
168
|
+
Every `eslint-disable` must be paired:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// DEVIATION(1.5): Legacy adapter — raw type narrowed to unknown immediately below.
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
const payload: any = deserialise(raw)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `as` in a smart constructor body:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- DEVIATION(1.6): smart-constructor body
|
|
180
|
+
return some(raw as UserId)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Never annotate
|
|
186
|
+
|
|
187
|
+
| Forbidden | Example |
|
|
188
|
+
|---|---|
|
|
189
|
+
| Paraphrasing the code | `// Check if user is admin` above `if (user.role === 'admin')` |
|
|
190
|
+
| Restating the type | `// Returns a string` on `: string` |
|
|
191
|
+
| Commented-out code | `// const old = legacyParse(raw)` |
|
|
192
|
+
| Section dividers | `// ─────────────` with nothing meaningful |
|
|
193
|
+
| Stale comments | Any comment that no longer matches the code |
|
|
194
|
+
| AI attribution | `// Generated by Claude` |
|
|
195
|
+
| `@throws` on Result functions | Error is in the return type, not thrown |
|
|
196
|
+
| Apologetic comments | `// This is a bit hacky but...` — use `HACK` properly |
|