@tsfpp/agents 1.2.3 → 1.3.1
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 +34 -0
- package/copilot/agents/tsfpp-audit.agent.md +88 -10
- package/copilot/agents/tsfpp-guarded-coding.agent.md +58 -4
- package/copilot/agents/tsfpp-tdd.agent.md +292 -0
- package/copilot/copilot-instructions.md +143 -66
- package/copilot/instructions/tsfpp-api.instructions.md +104 -39
- package/copilot/instructions/tsfpp-base.instructions.md +18 -7
- package/copilot/instructions/tsfpp-prelude.instructions.md +95 -47
- package/copilot/instructions/tsfpp-react.instructions.md +152 -40
- package/copilot/instructions/tsfpp-testing.instructions.md +154 -0
- package/copilot/skills/test-standard/SKILL.md +238 -0
- package/init.mjs +67 -84
- package/package.json +1 -1
|
@@ -1,89 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
# TSF++ coding standard — v1.1.0
|
|
2
|
+
|
|
3
|
+
This repository follows the TSF++ coding standard.
|
|
4
|
+
Canonical source: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` — when this file and the standard conflict, the standard wins.
|
|
5
|
+
|
|
6
|
+
## Axioms (non-negotiable)
|
|
7
|
+
|
|
8
|
+
1. Referential transparency is the norm; effects are reified as `Promise<Result<T, E>>`.
|
|
9
|
+
2. Total functions — partiality is typed via `Option<A>` or `Result<T, E>`. Never concealed.
|
|
10
|
+
3. Algebraic data types are the primary modelling language: sum types via tagged discriminated unions, product types via readonly records.
|
|
11
|
+
4. Compiler first, property tests second, documentation third.
|
|
12
|
+
|
|
13
|
+
## Never
|
|
14
|
+
|
|
15
|
+
- `class` `this` `new` `instanceof` `namespace` `enum`
|
|
16
|
+
- `interface` without `// DEVIATION(1.4): <reason>`
|
|
17
|
+
- `any` — use `unknown` at I/O boundaries, narrow in scope
|
|
18
|
+
- `as` outside a smart constructor body
|
|
19
|
+
- `!` (non-null assertion)
|
|
20
|
+
- `let` `var`
|
|
21
|
+
- `for` `while` `do..while`
|
|
22
|
+
- `.push` `.pop` `.splice` `.sort` `.reverse` `.fill` `delete` (mutating methods)
|
|
23
|
+
- `throw` in core — return `err(...)` instead
|
|
24
|
+
- `==` `!=` or truthiness checks on non-booleans (`if (str)`, `if (value)`)
|
|
25
|
+
- Optional params `?` — use `Option<T>` or a defaults record
|
|
26
|
+
- `default:` in an exhaustive switch — use `absurd(x)` instead
|
|
27
|
+
- `import from 'ramda'` — use `@tsfpp/prelude`
|
|
28
|
+
- `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
|
|
29
|
+
- `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
|
|
30
|
+
- `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
|
|
31
|
+
|
|
32
|
+
## Always
|
|
33
|
+
|
|
34
|
+
- `const` for every binding
|
|
35
|
+
- `readonly` on every record field and `ReadonlyArray<T>` for arrays
|
|
36
|
+
- Explicit return type on every exported function
|
|
37
|
+
- Sum-type dispatch via `switch` ending in `default: return absurd(x)`
|
|
38
|
+
- Errors as data: `Result<T, E>` — never `throw` in core
|
|
39
|
+
- Pipelines via `pipe` from `@tsfpp/prelude`
|
|
40
|
+
- JSDoc on every exported symbol (`@param`, `@returns`, `@law` where applicable)
|
|
41
|
+
- `// DEVIATION(N.M): <reason>` immediately before any necessary rule violation
|
|
42
|
+
|
|
43
|
+
## Size limits
|
|
44
|
+
|
|
45
|
+
| Metric | Limit |
|
|
46
|
+
|--------|-------|
|
|
47
|
+
| Function body | ≤ 40 lines (excl. blank lines and comments) |
|
|
48
|
+
| Cyclomatic complexity | ≤ 10 |
|
|
49
|
+
| Nesting depth | ≤ 4 |
|
|
50
|
+
| Positional arity | ≤ 3 — use a readonly record for ≥ 3 |
|
|
51
|
+
|
|
52
|
+
## Canonical idioms
|
|
14
53
|
|
|
15
54
|
```ts
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
55
|
+
// Sum type — domain ADT uses `kind`
|
|
56
|
+
type Shape =
|
|
57
|
+
| { readonly kind: 'circle'; readonly radius: number }
|
|
58
|
+
| { readonly kind: 'rect'; readonly width: number; readonly height: number }
|
|
59
|
+
|
|
60
|
+
// Exhaustive match with totality witness
|
|
61
|
+
import { absurd } from '@tsfpp/prelude'
|
|
62
|
+
|
|
63
|
+
const area = (s: Shape): number => {
|
|
64
|
+
switch (s.kind) {
|
|
65
|
+
case 'circle': return Math.PI * s.radius ** 2
|
|
66
|
+
case 'rect': return s.width * s.height
|
|
67
|
+
default: return absurd(s)
|
|
68
|
+
}
|
|
23
69
|
}
|
|
70
|
+
|
|
71
|
+
// Branded type — smart constructor only; `as` only inside the guard body
|
|
72
|
+
import { type Brand, some, none, type Option } from '@tsfpp/prelude'
|
|
73
|
+
|
|
74
|
+
type UserId = Brand<string, 'UserId'>
|
|
75
|
+
const mkUserId = (raw: string): Option<UserId> =>
|
|
76
|
+
raw.length > 0
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- DEVIATION(1.6): smart-constructor body
|
|
78
|
+
? some(raw as UserId)
|
|
79
|
+
: none
|
|
80
|
+
|
|
81
|
+
// Total function via Option
|
|
82
|
+
const head = <A>(xs: ReadonlyArray<A>): Option<A> =>
|
|
83
|
+
xs.length > 0 ? some(xs[0] as A) : none
|
|
84
|
+
|
|
85
|
+
// Pipeline
|
|
86
|
+
import { pipe, map, flatMap, tap, tapErr } from '@tsfpp/prelude'
|
|
87
|
+
|
|
88
|
+
pipe(
|
|
89
|
+
parseInput(raw),
|
|
90
|
+
flatMap(validate),
|
|
91
|
+
tap(v => log.debug({ v })),
|
|
92
|
+
tapErr(e => log.warn({ e })),
|
|
93
|
+
)
|
|
24
94
|
```
|
|
25
95
|
|
|
26
|
-
##
|
|
96
|
+
## Import discipline
|
|
27
97
|
|
|
28
|
-
All
|
|
98
|
+
All ADT constructors, combinators, and utilities come from `@tsfpp/prelude`. Never import from `ramda` directly.
|
|
29
99
|
|
|
30
100
|
```ts
|
|
31
101
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
102
|
+
some, none, ok, err, unit,
|
|
103
|
+
isSome, isNone, isOk, isErr,
|
|
104
|
+
map, flatMap, flatMapAsync, tap, tapErr,
|
|
105
|
+
mapO, flatMapO, orElse, getOrElse, fromNullable,
|
|
106
|
+
tryCatch, tryCatchAsync,
|
|
107
|
+
traverseArray, traverseArrayO, sequenceArrayO,
|
|
108
|
+
intoMap, assoc, dissoc, lookup, entriesOfMap,
|
|
109
|
+
intoSet, conj, disj, member,
|
|
110
|
+
isRecord, getStringField, getNumberField, getTypedField,
|
|
111
|
+
pipe, flow, absurd,
|
|
112
|
+
type Option, type Result, type Unit, type Brand,
|
|
113
|
+
} from '@tsfpp/prelude'
|
|
36
114
|
```
|
|
37
115
|
|
|
38
|
-
|
|
116
|
+
## Prelude-first
|
|
39
117
|
|
|
40
|
-
|
|
118
|
+
Before writing any implementation, check if `@tsfpp/prelude` already provides what you need:
|
|
41
119
|
|
|
42
|
-
|
|
120
|
+
| If you need… | Use… |
|
|
121
|
+
|---|---|
|
|
122
|
+
| Nullable → Option | `fromNullable` |
|
|
123
|
+
| Fallible operation | `tryCatch` / `tryCatchAsync` |
|
|
124
|
+
| No-value success | `ok(unit)` — never `ok(undefined)` |
|
|
125
|
+
| Map over array that can fail | `traverseArray` |
|
|
126
|
+
| Key/value store | `intoMap`, `lookup`, `assoc`, `dissoc` |
|
|
127
|
+
| Deduplication / membership | `intoSet`, `conj`, `disj`, `member` |
|
|
128
|
+
| Decode unknown record | `isRecord` + `getStringField` / `getNumberField` / `getTypedField` |
|
|
129
|
+
|
|
130
|
+
## Code markers
|
|
43
131
|
|
|
44
132
|
```ts
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
133
|
+
// TODO(author, YYYY-MM-DD[, TICKET]): description
|
|
134
|
+
// FIXME(author, YYYY-MM-DD): description
|
|
135
|
+
// HACK(author, YYYY-MM-DD): description
|
|
136
|
+
// NOTE(author, YYYY-MM-DD): description
|
|
137
|
+
// OPTIMIZE(author, YYYY-MM-DD): description
|
|
138
|
+
// BUG(author, YYYY-MM-DD): description
|
|
139
|
+
// XXX(author, YYYY-MM-DD): description
|
|
49
140
|
```
|
|
50
141
|
|
|
51
|
-
|
|
142
|
+
## Generation workflow
|
|
52
143
|
|
|
53
|
-
|
|
144
|
+
1. **Types first** — model the domain as sum and product types before writing any function.
|
|
145
|
+
2. **Make it total** — every function returns `Option`/`Result` if it can fail or be absent.
|
|
146
|
+
3. **Pure vs effectful** — `T` means pure; `Promise<Result<T, E>>` means effectful.
|
|
147
|
+
4. **Test the laws** — fast-check property tests for every pure function.
|
|
54
148
|
|
|
55
|
-
|
|
56
|
-
// Yes — Result propagates; mapped once at the boundary
|
|
57
|
-
const result: Result<Track, ApiError> = await createTrack(input)
|
|
58
|
-
return pipe(result, fold(apiErrorToResponse, createdResponse))
|
|
149
|
+
## Editing existing code
|
|
59
150
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
151
|
+
- Never weaken a signature (e.g. do not replace `Option<T>` with `T | undefined`).
|
|
152
|
+
- Preserve `readonly`-ness transitively.
|
|
153
|
+
- Do not introduce forbidden constructs to fix a type error — rethink the types.
|
|
154
|
+
- Deviation: `// DEVIATION(N.M): <one-line justification>` at the violation site.
|
|
63
155
|
|
|
64
|
-
##
|
|
156
|
+
## When a task is ambiguous
|
|
65
157
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```
|
|
158
|
+
Ask one focused question rather than guessing. Do not invent types, error cases, or effect boundaries.
|
|
159
|
+
|
|
160
|
+
## Agents and tooling
|
|
70
161
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| Created | 201 | `createdResponse` |
|
|
77
|
-
| Accepted (async) | 202 | `acceptedResponse` |
|
|
78
|
-
| No content | 204 | `noContentResponse` |
|
|
79
|
-
| Validation failure | 422 | `fromZodError` |
|
|
80
|
-
| Not found | 404 | `problemResponse(mkProblem(404, ...))` |
|
|
81
|
-
| Conflict | 409 | `problemResponse(mkProblem(409, ...))` |
|
|
82
|
-
| Server error | 500 | `problemResponse(mkProblem(500, ...))` |
|
|
83
|
-
|
|
84
|
-
## Security
|
|
85
|
-
|
|
86
|
-
- All routes require authentication unless explicitly marked `// PUBLIC`
|
|
87
|
-
- Never log `principalId`, credentials, or request bodies at `info` level
|
|
88
|
-
- Never reflect user input in error messages without sanitisation
|
|
89
|
-
- Idempotency keys required on mutating operations — use `withIdempotency`
|
|
162
|
+
- TSF++ compliance audit: `.github/agents/tsfpp-audit.agent.md`
|
|
163
|
+
- Guarded implementation: `.github/agents/tsfpp-guarded-coding.agent.md`
|
|
164
|
+
- Refactoring: `.github/agents/tsfpp-refactor-engineer.agent.md`
|
|
165
|
+
- Annotation: `.github/agents/tsfpp-annotate.agent.md`
|
|
166
|
+
- Trunk workflow: `.github/instructions/trunk.instructions.md`
|
|
@@ -6,84 +6,149 @@ applyTo: "{**/routes/**,**/handlers/**,**/api/**}/*.ts"
|
|
|
6
6
|
|
|
7
7
|
Full standard: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
8
8
|
Boundary API: `node_modules/@tsfpp/boundary/README.md`
|
|
9
|
-
Extends: tsfpp-base.instructions.md (all base rules apply)
|
|
9
|
+
Extends: `tsfpp-base.instructions.md` (all base rules apply)
|
|
10
10
|
|
|
11
11
|
## Handler shape
|
|
12
12
|
|
|
13
|
-
Handlers are thin. The only permitted steps are: parse → call use-case → map response
|
|
13
|
+
Handlers are thin. The only permitted steps are: **parse → call use-case → map response**.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import {
|
|
17
|
+
extractContext, fromZodError, apiErrorToResponse,
|
|
18
|
+
createdResponse, okResponse, noContentResponse, acceptedResponse,
|
|
19
|
+
withIdempotency, withRequestLog,
|
|
20
|
+
} from '@tsfpp/boundary'
|
|
21
|
+
import { isErr, pipe } from '@tsfpp/prelude'
|
|
22
|
+
|
|
23
|
+
const createTrackHandler: RawHandler = async (req) => {
|
|
24
|
+
const ctx = extractContext(req, '/v1/tracks') // 1. context — always first
|
|
20
25
|
|
|
21
|
-
const
|
|
22
|
-
|
|
26
|
+
const raw = await req.json().catch(() => null)
|
|
27
|
+
const parsed = CreateTrackSchema.safeParse(raw)
|
|
28
|
+
if (!parsed.success) // 2. validate
|
|
29
|
+
return apiErrorToResponse(fromZodError(parsed.error), ctx)
|
|
30
|
+
|
|
31
|
+
const result = await createTrack(parsed.data) // 3. use-case
|
|
32
|
+
if (isErr(result)) return apiErrorToResponse(result.error, ctx)
|
|
33
|
+
|
|
34
|
+
return createdResponse(result.value, `/v1/tracks/${result.value.id}`, {
|
|
35
|
+
'X-Request-Id': ctx.traceId,
|
|
36
|
+
}) // 4. respond
|
|
23
37
|
}
|
|
24
38
|
```
|
|
25
39
|
|
|
26
40
|
## Boundary imports
|
|
27
41
|
|
|
28
|
-
All HTTP primitives come from `@tsfpp/boundary
|
|
42
|
+
All HTTP primitives come from `@tsfpp/boundary`. Never construct `new Response()` directly.
|
|
29
43
|
|
|
30
44
|
```ts
|
|
31
45
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
// Context
|
|
47
|
+
extractContext,
|
|
48
|
+
// Validation
|
|
49
|
+
fromZodError, mkValidationError,
|
|
50
|
+
// Error mapping
|
|
51
|
+
apiErrorToResponse, apiErrorToProblem,
|
|
52
|
+
// Response builders
|
|
53
|
+
okResponse, createdResponse, noContentResponse,
|
|
54
|
+
acceptedResponse, redirectResponse, jsonResponse, problemResponse, mkProblem,
|
|
55
|
+
// Pagination
|
|
56
|
+
mkPaginated, parsePaginationQuery, encodeCursor, decodeCursor,
|
|
57
|
+
// LRO
|
|
58
|
+
mkRunningOp, mkSucceededOp, mkFailedOp,
|
|
59
|
+
// Bulk
|
|
60
|
+
bulkResponse, mkBulkOkItem, mkBulkErrorItem,
|
|
61
|
+
// Security
|
|
62
|
+
baselineSecurityHeaders, corsHeaders, rateLimitHeaders,
|
|
63
|
+
// Middleware
|
|
64
|
+
withIdempotency, withRequestLog,
|
|
65
|
+
// Webhooks
|
|
66
|
+
signWebhook, verifyWebhook,
|
|
67
|
+
// Types
|
|
68
|
+
type RawHandler, type HandlerFactory, type RequestContext,
|
|
35
69
|
} from '@tsfpp/boundary'
|
|
36
70
|
```
|
|
37
71
|
|
|
38
|
-
Never construct `new Response(...)` directly in a handler.
|
|
39
|
-
|
|
40
72
|
## Validation
|
|
41
73
|
|
|
42
|
-
All input validated with Zod at the boundary
|
|
74
|
+
All input validated with Zod at the boundary via `safeParse` — never `parse` (throws):
|
|
43
75
|
|
|
44
76
|
```ts
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
artistId: z.string().uuid(),
|
|
48
|
-
})
|
|
77
|
+
const parsed = CreateTrackSchema.safeParse(raw)
|
|
78
|
+
if (!parsed.success) return apiErrorToResponse(fromZodError(parsed.error), ctx)
|
|
49
79
|
```
|
|
50
80
|
|
|
51
|
-
Never pass unvalidated `req.
|
|
81
|
+
Never pass unvalidated `req.json()` into the domain.
|
|
52
82
|
|
|
53
|
-
##
|
|
83
|
+
## Error mapping
|
|
54
84
|
|
|
55
85
|
```ts
|
|
56
86
|
// Yes — Result propagates; mapped once at the boundary
|
|
57
|
-
const result
|
|
58
|
-
|
|
87
|
+
const result = await createTrack(input)
|
|
88
|
+
if (isErr(result)) return apiErrorToResponse(result.error, ctx)
|
|
59
89
|
|
|
60
90
|
// No — throw crosses the boundary untyped
|
|
61
91
|
throw new Error('not found')
|
|
62
92
|
```
|
|
63
93
|
|
|
64
|
-
|
|
94
|
+
`dependency` and `internal` ApiError variants contain a `cause` — **log `cause` before calling `apiErrorToResponse`**, it is stripped from the response.
|
|
95
|
+
|
|
96
|
+
## Middleware composition
|
|
97
|
+
|
|
98
|
+
Compose via `pipe`, outermost-last. `withRequestLog` must always be outermost:
|
|
65
99
|
|
|
66
100
|
```ts
|
|
67
|
-
const
|
|
68
|
-
//
|
|
101
|
+
const handler: RawHandler = pipe(
|
|
102
|
+
createTrackHandler, // business logic
|
|
103
|
+
withIdempotency(store), // replay / in-flight guard
|
|
104
|
+
withRequestLog(logger, '/v1/tracks'), // outermost — logs every outcome
|
|
105
|
+
)
|
|
69
106
|
```
|
|
70
107
|
|
|
71
|
-
##
|
|
108
|
+
## Pagination
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
```ts
|
|
111
|
+
const pageQuery = parsePaginationQuery(req.url, 100) // Result<PageQuery, ValidationError>
|
|
112
|
+
if (isErr(pageQuery)) return apiErrorToResponse(pageQuery.error, ctx)
|
|
113
|
+
|
|
114
|
+
const page = mkPaginated(items, nextCursor) // totalCount omitted unless precomputed
|
|
115
|
+
return okResponse(page)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Rate limiting
|
|
119
|
+
|
|
120
|
+
Attach `rateLimitHeaders` to **all** responses on rate-limited endpoints, not only 429s:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
return okResponse(body, rateLimitHeaders(state))
|
|
124
|
+
```
|
|
83
125
|
|
|
84
126
|
## Security
|
|
85
127
|
|
|
128
|
+
```ts
|
|
129
|
+
// Merge baseline headers into every response
|
|
130
|
+
return okResponse(body, { ...baselineSecurityHeaders, ...rateLimitHeaders(state) })
|
|
131
|
+
|
|
132
|
+
// CORS — never reflect Origin blindly; allowedOrigins from config only
|
|
133
|
+
return okResponse(body, corsHeaders(allowedOrigins, requestOrigin))
|
|
134
|
+
```
|
|
135
|
+
|
|
86
136
|
- All routes require authentication unless explicitly marked `// PUBLIC`
|
|
87
|
-
- Never log `principalId`, credentials, or request bodies at `info` level
|
|
137
|
+
- Never log `principalId`, credentials, or full request bodies at `info` level
|
|
88
138
|
- Never reflect user input in error messages without sanitisation
|
|
89
|
-
- Idempotency keys required on mutating operations — use `withIdempotency`
|
|
139
|
+
- Idempotency keys required on state-mutating operations — use `withIdempotency`
|
|
140
|
+
|
|
141
|
+
## Status codes
|
|
142
|
+
|
|
143
|
+
| Situation | Code | Builder |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| Read success | 200 | `okResponse` |
|
|
146
|
+
| Created | 201 | `createdResponse(body, location)` |
|
|
147
|
+
| Accepted (async / LRO) | 202 | `acceptedResponse(operation, pollUrl)` |
|
|
148
|
+
| No content | 204 | `noContentResponse` |
|
|
149
|
+
| Multi-status (bulk) | 207 | `bulkResponse(items)` |
|
|
150
|
+
| Validation failure | 422 | `apiErrorToResponse(fromZodError(e), ctx)` |
|
|
151
|
+
| Not found | 404 | `apiErrorToResponse({ kind: 'not_found', ... }, ctx)` |
|
|
152
|
+
| Conflict | 409 | `apiErrorToResponse({ kind: 'conflict', ... }, ctx)` |
|
|
153
|
+
| Rate limited | 429 | `apiErrorToResponse({ kind: 'rate_limit', ... }, ctx)` |
|
|
154
|
+
| Server error | 500 | `apiErrorToResponse({ kind: 'internal', cause }, ctx)` |
|
|
@@ -21,6 +21,9 @@ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
21
21
|
- Optional params `?` — use `Option<T>` or a defaults record
|
|
22
22
|
- `default:` in an exhaustive switch — use `absurd(x)` instead
|
|
23
23
|
- `import from 'ramda'` — use `@tsfpp/prelude`
|
|
24
|
+
- `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
|
|
25
|
+
- `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
|
|
26
|
+
- `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
|
|
24
27
|
|
|
25
28
|
## Always
|
|
26
29
|
|
|
@@ -46,7 +49,7 @@ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
46
49
|
## ADT patterns
|
|
47
50
|
|
|
48
51
|
```ts
|
|
49
|
-
// Sum type
|
|
52
|
+
// Sum type — domain ADTs use `kind`
|
|
50
53
|
type Shape =
|
|
51
54
|
| { readonly kind: 'circle'; readonly radius: number }
|
|
52
55
|
| { readonly kind: 'rect'; readonly width: number; readonly height: number }
|
|
@@ -58,21 +61,29 @@ switch (shape.kind) {
|
|
|
58
61
|
default: return absurd(shape)
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
// Branded type
|
|
64
|
+
// Branded type — `as` only inside the smart constructor guard body
|
|
62
65
|
type UserId = Brand<string, 'UserId'>
|
|
63
|
-
const mkUserId =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
66
|
+
const mkUserId = (raw: string): Option<UserId> =>
|
|
67
|
+
raw.length > 0
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- DEVIATION(1.6): smart-constructor body
|
|
69
|
+
? some(raw as UserId)
|
|
70
|
+
: none
|
|
67
71
|
|
|
68
72
|
// Total function
|
|
69
73
|
const head = <A>(xs: ReadonlyArray<A>): Option<A> =>
|
|
70
74
|
xs.length > 0 ? some(xs[0] as A) : none
|
|
71
75
|
```
|
|
72
76
|
|
|
77
|
+
## Discriminant convention
|
|
78
|
+
|
|
79
|
+
| ADT origin | Field | Example |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `@tsfpp/prelude` (Result, Option) | `_tag` | accessed via guards only — never `x._tag === 'Ok'` |
|
|
82
|
+
| Domain ADTs | `kind` | `{ kind: 'pending'; ... }` |
|
|
83
|
+
|
|
73
84
|
## Imports
|
|
74
85
|
|
|
75
|
-
All ADT constructors
|
|
86
|
+
All ADT constructors, combinators, and utilities come from `@tsfpp/prelude`. Never import from `ramda` directly.
|
|
76
87
|
|
|
77
88
|
## Markers
|
|
78
89
|
|