@tsfpp/agents 1.4.0 → 1.5.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 +15 -0
- package/README.md +2 -0
- package/bin/bootstrap.sh +0 -0
- package/copilot/agents/tsfpp-audit.agent.md +85 -15
- package/copilot/agents/tsfpp-guarded-coding.agent.md +3 -0
- package/copilot/instructions/tsfpp-base.instructions.md +5 -0
- package/copilot/skills/config-standard/SKILL.md +205 -0
- package/copilot/skills/log-standard/SKILL.md +148 -0
- package/init.mjs +2 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [1.5.0] - 2026-05-18
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added `copilot/skills/config-standard/SKILL.md`.
|
|
18
|
+
- Added `copilot/skills/log-standard/SKILL.md`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Updated `README.md` with latest guidance updates.
|
|
23
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` with expanded checks and release hygiene.
|
|
24
|
+
- Updated `copilot/agents/tsfpp-guarded-coding.agent.md` with guarded workflow refinements.
|
|
25
|
+
- Updated `copilot/instructions/tsfpp-base.instructions.md` with aligned base-level standards guidance.
|
|
26
|
+
- Updated `init.mjs` to include installer wiring for newly added skills and updates.
|
|
27
|
+
|
|
13
28
|
## [1.4.0] - 2026-05-18
|
|
14
29
|
|
|
15
30
|
### Added
|
package/README.md
CHANGED
|
@@ -256,6 +256,8 @@ Skills are loaded automatically by Copilot based on semantic match with what is
|
|
|
256
256
|
| `react-coding-standard` | Writing React components, hooks, forms, or stores |
|
|
257
257
|
| `test-standard` | Writing or reviewing test files; discussing coverage or test structure |
|
|
258
258
|
| `annotation-standard` | Writing or reviewing any comment, JSDoc block, module header, code marker, or DEVIATION; discussing what to annotate and why |
|
|
259
|
+
| `log-standard` | Writing or reviewing logging code, Logger port implementations, `withRequestLog` usage, or `tap`/`tapErr` side effects |
|
|
260
|
+
| `config-standard` | Writing or reviewing config loaders, `loadConfig` usage, `Config` types, `.env.example`, or `process.env` access |
|
|
259
261
|
|
|
260
262
|
Skills are distilled from the full standard documents — concise enough to fit in the model's context without crowding out code.
|
|
261
263
|
|
package/bin/bootstrap.sh
CHANGED
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: TSF++ standards compliance auditor. Produces a structured markdown report in docs/audits/ with per-slice checkboxes.
|
|
3
3
|
name: tsfpp-audit
|
|
4
|
-
argument-hint: "target=<path|package|layer> focus=<all|types|boundary|complexity|loc|annotations|security|react|data|prelude|test>"
|
|
4
|
+
argument-hint: "target=<path|package|layer> focus=<all|types|boundary|complexity|loc|annotations|security|react|data|prelude|test|log|config>"
|
|
5
5
|
tools:
|
|
6
6
|
- edit/createFile
|
|
7
7
|
- edit/editFiles
|
|
@@ -44,7 +44,7 @@ If `target` and `focus` are present in the message (e.g. `target=src/ focus=test
|
|
|
44
44
|
If and only if either is missing and cannot be inferred, ask once:
|
|
45
45
|
|
|
46
46
|
> **Target** — path, package name, or layer to audit (e.g. `src/domain`, `@tsfpp/prelude`, `api layer`)?
|
|
47
|
-
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · or comma-separated combination?
|
|
47
|
+
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · `log` · `config` · or comma-separated combination?
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -160,12 +160,26 @@ Append each completed slice to the report:
|
|
|
160
160
|
- [ ] 6.3 — No `null`/`undefined` propagation; use `Option<A>`
|
|
161
161
|
- [ ] 6.6 — `Promise.allSettled` over `Promise.all` when partial failure is meaningful
|
|
162
162
|
|
|
163
|
-
**Annotations (§7)**
|
|
164
|
-
- [ ]
|
|
163
|
+
**Annotations (§7 + ANNOTATION_CODING_STANDARD — cross-cutting, always checked)**
|
|
164
|
+
- [ ] Module-level JSDoc block present on all files with public exports
|
|
165
|
+
- [ ] Every exported symbol has a JSDoc block
|
|
166
|
+
- [ ] `@param` describes domain constraint (not the type); `@returns` describes meaning (not the type)
|
|
167
|
+
- [ ] `@law` present on all combinators with algebraic properties
|
|
168
|
+
- [ ] `@example` present on smart constructors and non-obvious combinators
|
|
169
|
+
- [ ] No comments that paraphrase the code; no commented-out code
|
|
170
|
+
- [ ] Code markers follow `// MARKER(author, YYYY-MM-DD[, TICKET]): description` format
|
|
171
|
+
- [ ] Every `eslint-disable` paired with a `// DEVIATION(N.M): <reason>` comment
|
|
172
|
+
- [ ] For full annotation audit: use `focus=annotations`
|
|
173
|
+
|
|
174
|
+
**Security (SECURITY_CODING_STANDARD — cross-cutting, always checked)**
|
|
175
|
+
- [ ] No secrets, credentials, or tokens in source code or committed config
|
|
176
|
+
- [ ] No sensitive data (PII, credentials, tokens) in error messages or log output
|
|
177
|
+
- [ ] No `eval`, `Function()`, or dynamic `import()` with user-controlled input
|
|
178
|
+
- [ ] User input not reflected in error responses without sanitisation
|
|
179
|
+
- [ ] For full security audit: use `focus=security`
|
|
165
180
|
|
|
166
|
-
**Boundary and
|
|
167
|
-
- [ ] 8.4 — Parse, don't validate: `unknown` converted to domain types at the boundary
|
|
168
|
-
- [ ] 9.x — No `import from 'ramda'`; use `@tsfpp/prelude`
|
|
181
|
+
**Boundary and parse (§8)**
|
|
182
|
+
- [ ] 8.4 — Parse, don't validate: `unknown` converted to domain types at the boundary via smart constructors or Zod
|
|
169
183
|
|
|
170
184
|
**Size limits (§11)**
|
|
171
185
|
- [ ] 11.1 — One type / one responsibility per file
|
|
@@ -345,7 +359,6 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
|
|
|
345
359
|
| `.map()` on a fallible function | MUST | `traverseArray` |
|
|
346
360
|
| `new Map()` | MUST | `intoMap([...])` |
|
|
347
361
|
| `new Set()` | MUST | `intoSet([...])` |
|
|
348
|
-
| `import ... from 'ramda'` | MUST | `@tsfpp/prelude` |
|
|
349
362
|
| `result._tag === 'Ok'` | MUST | `isOk(result)` |
|
|
350
363
|
| `option._tag === 'Some'` | MUST | `isSome(option)` |
|
|
351
364
|
| `Result<void, E>` | MUST | `Result<Unit, E>` with `ok(unit)` |
|
|
@@ -360,7 +373,6 @@ Checklist:
|
|
|
360
373
|
- [ ] No `try/catch` outside adapter boundaries — use `tryCatch`/`tryCatchAsync`
|
|
361
374
|
- [ ] No `.map()` on fallible function — use `traverseArray`
|
|
362
375
|
- [ ] No `new Map()` / `new Set()` — use `intoMap` / `intoSet`
|
|
363
|
-
- [ ] No `import from 'ramda'`
|
|
364
376
|
- [ ] Prelude ADTs accessed via exported guards (`isOk`, `isSome`), never `._tag` directly
|
|
365
377
|
- [ ] No `Result<void, E>` — use `Result<Unit, E>`
|
|
366
378
|
- [ ] Side effects in pipelines via `tap` / `tapErr`
|
|
@@ -412,8 +424,60 @@ Checklist:
|
|
|
412
424
|
- [ ] 4.4 DAL — insert+read round-trip tested; not-found returns `None`
|
|
413
425
|
- [ ] 4.5 React — loading state, error state, and user interactions all covered
|
|
414
426
|
|
|
427
|
+
### `log`
|
|
428
|
+
Full reference: `node_modules/@tsfpp/standard/spec/LOG_CODING_STANDARD.md`
|
|
429
|
+
|
|
430
|
+
Cross-cutting — apply to every file regardless of other focus selections.
|
|
431
|
+
|
|
432
|
+
- [ ] No `console.*` calls outside `main.ts` / `server.ts`
|
|
433
|
+
- [ ] `Logger` port imported from `@tsfpp/prelude`; never a concrete library
|
|
434
|
+
- [ ] `Logger` injected as a dependency; never imported as a singleton
|
|
435
|
+
- [ ] All `message` fields use dot-separated event-name format (`user.created`, not `"User was created"`)
|
|
436
|
+
- [ ] Every request-scoped log entry includes `traceId`
|
|
437
|
+
- [ ] Every `error`-level entry includes `code`
|
|
438
|
+
- [ ] `cause` logged before `apiErrorToResponse` on `dependency` / `internal` errors
|
|
439
|
+
- [ ] No PII in any log field at any level
|
|
440
|
+
- [ ] No credentials, tokens, or secrets in any log field
|
|
441
|
+
- [ ] No full request or response bodies logged at `info` or above
|
|
442
|
+
- [ ] No stack traces in production log output (`err.message` not `err.stack`)
|
|
443
|
+
- [ ] `withRequestLog` used for HTTP request logging; no manual request logging in handlers
|
|
444
|
+
- [ ] `routeTemplate` is parameterised, not the resolved URL
|
|
445
|
+
- [ ] Pipelines use `tap` / `tapErr` for logging; pipeline not broken for a log call
|
|
446
|
+
- [ ] Tests receive `silentLogger`, not the production logger
|
|
447
|
+
- [ ] Production logger emits newline-delimited JSON
|
|
448
|
+
- [ ] Log level configurable via environment variable
|
|
449
|
+
|
|
450
|
+
### `config`
|
|
451
|
+
Full reference: `node_modules/@tsfpp/standard/spec/CONFIG_CODING_STANDARD.md`
|
|
452
|
+
|
|
453
|
+
Cross-cutting — apply to entry points, config loaders, and any module that accesses configuration.
|
|
454
|
+
|
|
455
|
+
- [ ] No `process.env` access outside the config loader
|
|
456
|
+
- [ ] No config singleton imported by application modules
|
|
457
|
+
- [ ] `loadConfig` from `@tsfpp/boundary` used in the loader
|
|
458
|
+
- [ ] Loader returns `Result<Config, ConfigError>`; never throws
|
|
459
|
+
- [ ] All type coercion (`string → number`, `string → boolean`) in Zod schema, not application code
|
|
460
|
+
- [ ] All validation failures reported together (Zod `safeParse`, not sequential)
|
|
461
|
+
- [ ] Required secrets validated for minimum length (`z.string().min(32)`)
|
|
462
|
+
- [ ] `.env.example` committed; `.env` in `.gitignore`
|
|
463
|
+
- [ ] Every variable in `.env.example` has an explanatory comment
|
|
464
|
+
- [ ] No config values or `process.env` logged at any level
|
|
465
|
+
- [ ] Tests pass plain records to the loader; never mutate `process.env`
|
|
466
|
+
- [ ] Config factory in `tests/helpers/` for use-case and integration tests
|
|
467
|
+
- [ ] Loader tests cover: valid, each missing required var, invalid type
|
|
468
|
+
- [ ] React: `clientConfig` validated at module load; no secrets in client config
|
|
469
|
+
|
|
415
470
|
### `all`
|
|
416
|
-
All focus areas
|
|
471
|
+
All focus areas in sequence.
|
|
472
|
+
|
|
473
|
+
**Always active (cross-cutting — every file, every focus):**
|
|
474
|
+
`annotations`, `security`, `log`, and `config` are applied to every slice regardless of focus selection or file type.
|
|
475
|
+
|
|
476
|
+
**Auto-detected by file type / path:**
|
|
477
|
+
- `.tsx` files → include `react`
|
|
478
|
+
- `infrastructure/`, `dal/`, `repository/` paths → include `data`
|
|
479
|
+
- `*.test.ts` / `*.test.tsx` files → include `test`
|
|
480
|
+
- All files → include `prelude`
|
|
417
481
|
|
|
418
482
|
---
|
|
419
483
|
|
|
@@ -431,11 +495,17 @@ Example: `docs/audits/src-domain-prelude-20260517-1430.md` or `docs/audits/src-a
|
|
|
431
495
|
**Step 3 — Inspect slice by slice**
|
|
432
496
|
For each slice:
|
|
433
497
|
1. Read the file(s).
|
|
434
|
-
2.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
498
|
+
2. Determine which checklists apply:
|
|
499
|
+
- **Always:** base checklist including the `annotations` and `security` sections — every slice, every focus
|
|
500
|
+
- React checklist for `.tsx` files under `react` or `all` focus
|
|
501
|
+
- Data checklist for files in `infrastructure/`, `dal/`, `repository/` under `data` or `all` focus
|
|
502
|
+
- Test checklist for `*.test.ts` / `*.test.tsx` under `test` or `all` focus
|
|
503
|
+
- Prelude checklist for all files under `prelude` or `all` focus
|
|
504
|
+
3. Check every rule in the active focus set.
|
|
505
|
+
4. Record all findings (rule · line · severity · description).
|
|
506
|
+
5. Fill in the checklist.
|
|
507
|
+
6. Append the completed slice section to the report.
|
|
508
|
+
7. Update the slice status in the index table.
|
|
439
509
|
|
|
440
510
|
**Step 4 — Summarise**
|
|
441
511
|
After all slices: fill in the Summary table · set Status to ✅ Complete or ⚠️ Violations found · list the top 3 highest-priority issues.
|
|
@@ -152,6 +152,9 @@ Do not hand-roll what the prelude already provides.
|
|
|
152
152
|
| Key/value lookup | `intoMap`, `lookup`, `assoc`, `dissoc` |
|
|
153
153
|
| Set membership | `intoSet`, `conj`, `disj`, `member` |
|
|
154
154
|
| Exhaustive match | `absurd` |
|
|
155
|
+
| Application logging | `Logger` port — `import { type Logger } from '@tsfpp/prelude'`; inject as dependency; never `console.*` |
|
|
156
|
+
| Config access | Receive `Config` as a dependency; never read `process.env` directly |
|
|
157
|
+
| Config loading (entry point only) | `loadConfig` — `import { loadConfig } from '@tsfpp/boundary'` |
|
|
155
158
|
|
|
156
159
|
If you are about to write a `try/catch`, a `null` check, an `if (x === undefined)`,
|
|
157
160
|
a `x ?? fallback`, or a `.map()` that can fail — stop and use the prelude equivalent instead.
|
|
@@ -24,6 +24,8 @@ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
24
24
|
- `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
|
|
25
25
|
- `if (x === null)` `if (x !== null)` `if (x === undefined)` `if (x !== undefined)` `if (!x)` `x ?? y` — any nullability check in any form; use `fromNullable` → `Option<T>`, then `isSome` / `isNone` / `getOrElse`
|
|
26
26
|
- `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
|
|
27
|
+
- `console.log` `console.error` `console.warn` `console.info` — anywhere except `main.ts` / `server.ts` startup; use the injected `Logger` port from `@tsfpp/prelude`
|
|
28
|
+
- `process.env` outside the config loader — use the typed `Config` record injected as a dependency
|
|
27
29
|
|
|
28
30
|
## Always
|
|
29
31
|
|
|
@@ -85,6 +87,9 @@ const head = <A>(xs: ReadonlyArray<A>): Option<A> =>
|
|
|
85
87
|
|
|
86
88
|
All ADT constructors, combinators, and utilities come from `@tsfpp/prelude`. Never import from `ramda` directly.
|
|
87
89
|
|
|
90
|
+
Logger port: `import { type Logger, type LogEntry } from '@tsfpp/prelude'`
|
|
91
|
+
Config loader: `import { loadConfig, type ConfigError } from '@tsfpp/boundary'`
|
|
92
|
+
|
|
88
93
|
## Markers
|
|
89
94
|
|
|
90
95
|
```ts
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: config-standard
|
|
3
|
+
description: >
|
|
4
|
+
Normative TSF++ configuration management rules. Load when writing or reviewing
|
|
5
|
+
any code that loads environment variables, defines a Config type, calls
|
|
6
|
+
loadConfig, accesses process.env, implements a config loader, or writes
|
|
7
|
+
.env.example: loadConfig from @tsfpp/boundary, Config as typed readonly record,
|
|
8
|
+
Zod validation at the startup boundary, injection pattern, test factories.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# TSF++ config standard
|
|
12
|
+
|
|
13
|
+
Full standard: `node_modules/@tsfpp/standard/spec/CONFIG_CODING_STANDARD.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The pattern in one picture
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
process.env (string | undefined)
|
|
21
|
+
↓
|
|
22
|
+
loadConfig(schema, env) ← @tsfpp/boundary — validates all vars at once
|
|
23
|
+
↓
|
|
24
|
+
Result<Config, ConfigError>
|
|
25
|
+
↓ exit on Err at startup
|
|
26
|
+
Config (typed readonly record)
|
|
27
|
+
↓ injected into every module that needs it
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`process.env` is only touched at the entry point. Everything downstream receives a typed `Config`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Imports
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// In the config loader
|
|
38
|
+
import { loadConfig, type ConfigError } from '@tsfpp/boundary'
|
|
39
|
+
import { isErr, ok, type Result } from '@tsfpp/prelude'
|
|
40
|
+
|
|
41
|
+
// In application modules — inject Config as a dependency, never import process.env
|
|
42
|
+
import { type Config } from '../shared/config'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Config type — project-defined, not from a package
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/shared/config.ts
|
|
51
|
+
export type Config = {
|
|
52
|
+
readonly server: {
|
|
53
|
+
readonly port: number
|
|
54
|
+
readonly host: string
|
|
55
|
+
readonly logLevel: 'debug' | 'info' | 'warn' | 'error'
|
|
56
|
+
}
|
|
57
|
+
readonly database: {
|
|
58
|
+
readonly url: string
|
|
59
|
+
readonly poolMin: number
|
|
60
|
+
readonly poolMax: number
|
|
61
|
+
readonly queryTimeoutMs: number
|
|
62
|
+
}
|
|
63
|
+
readonly auth: {
|
|
64
|
+
readonly jwtSecret: string
|
|
65
|
+
readonly tokenTtlSeconds: number
|
|
66
|
+
}
|
|
67
|
+
readonly features: {
|
|
68
|
+
readonly maintenanceMode: boolean
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All fields are required. Optional config uses `Option<T>`, not `T | undefined`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Config loader
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/infrastructure/config-loader.ts
|
|
81
|
+
import { z } from 'zod'
|
|
82
|
+
import { loadConfig, type ConfigError } from '@tsfpp/boundary'
|
|
83
|
+
import { isErr, ok, type Result } from '@tsfpp/prelude'
|
|
84
|
+
import { type Config } from '../shared/config'
|
|
85
|
+
|
|
86
|
+
const schema = z.object({
|
|
87
|
+
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
|
|
88
|
+
HOST: z.string().default('0.0.0.0'),
|
|
89
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
90
|
+
DATABASE_URL: z.string().url(),
|
|
91
|
+
DATABASE_POOL_MIN: z.coerce.number().int().min(1).default(2),
|
|
92
|
+
DATABASE_POOL_MAX: z.coerce.number().int().min(1).default(10),
|
|
93
|
+
DATABASE_TIMEOUT_MS: z.coerce.number().int().min(100).default(5000),
|
|
94
|
+
JWT_SECRET: z.string().min(32),
|
|
95
|
+
TOKEN_TTL_SECONDS: z.coerce.number().int().min(60).default(3600),
|
|
96
|
+
MAINTENANCE_MODE: z.coerce.boolean().default(false),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const parseConfig = (
|
|
100
|
+
env: Record<string, string | undefined>
|
|
101
|
+
): Result<Config, ConfigError> => {
|
|
102
|
+
const raw = loadConfig(schema, env)
|
|
103
|
+
if (isErr(raw)) return raw
|
|
104
|
+
|
|
105
|
+
const e = raw.value
|
|
106
|
+
return ok({
|
|
107
|
+
server: { port: e.PORT, host: e.HOST, logLevel: e.LOG_LEVEL },
|
|
108
|
+
database: { url: e.DATABASE_URL, poolMin: e.DATABASE_POOL_MIN,
|
|
109
|
+
poolMax: e.DATABASE_POOL_MAX, queryTimeoutMs: e.DATABASE_TIMEOUT_MS },
|
|
110
|
+
auth: { jwtSecret: e.JWT_SECRET, tokenTtlSeconds: e.TOKEN_TTL_SECONDS },
|
|
111
|
+
features: { maintenanceMode: e.MAINTENANCE_MODE },
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Entry point — fail at startup
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/main.ts
|
|
122
|
+
import { parseConfig } from './infrastructure/config-loader'
|
|
123
|
+
|
|
124
|
+
const configResult = parseConfig(process.env)
|
|
125
|
+
if (isErr(configResult)) {
|
|
126
|
+
console.error(configResult.error.summary)
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
const config = configResult.value
|
|
130
|
+
// wire up the application with config
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Injection — never import process.env in modules
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// Good — Config injected as part of Deps
|
|
139
|
+
type Deps = {
|
|
140
|
+
readonly db: Database
|
|
141
|
+
readonly logger: Logger
|
|
142
|
+
readonly config: Pick<Config, 'auth'>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Bad — reads process.env inside a module
|
|
146
|
+
const ttl = parseInt(process.env.TOKEN_TTL_SECONDS ?? '3600', 10)
|
|
147
|
+
|
|
148
|
+
// Bad — imports a config singleton
|
|
149
|
+
import { config } from '../config'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Use `Pick<Config, 'auth'>` to declare precisely which slice of config a module needs.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Zod schema conventions
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// Required — no default; missing = startup failure
|
|
160
|
+
DATABASE_URL: z.string().url()
|
|
161
|
+
JWT_SECRET: z.string().min(32) // enforce minimum entropy for secrets
|
|
162
|
+
|
|
163
|
+
// Optional with sensible default
|
|
164
|
+
PORT: z.coerce.number().int().default(3000)
|
|
165
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
|
|
166
|
+
|
|
167
|
+
// Boolean — coerce from 'true' / 'false' string
|
|
168
|
+
MAINTENANCE_MODE: z.coerce.boolean().default(false)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
All coercion (`string → number`, `string → boolean`) happens in the schema. Never parse in application code.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Testing
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Never mutate process.env in tests
|
|
179
|
+
// Bad
|
|
180
|
+
process.env.DATABASE_URL = 'postgres://localhost/test'
|
|
181
|
+
|
|
182
|
+
// Good — pass a plain record to the loader
|
|
183
|
+
const result = parseConfig({ DATABASE_URL: 'postgres://localhost/test', JWT_SECRET: 'a'.repeat(32), ... })
|
|
184
|
+
|
|
185
|
+
// Config factory for use-case and integration tests
|
|
186
|
+
// tests/helpers/config.factory.ts
|
|
187
|
+
export const makeConfig = (overrides: Partial<Config> = {}): Config => ({
|
|
188
|
+
server: { port: 3000, host: '127.0.0.1', logLevel: 'error', ...overrides.server },
|
|
189
|
+
database: { url: 'postgres://localhost/test', poolMin: 1, poolMax: 2,
|
|
190
|
+
queryTimeoutMs: 1000, ...overrides.database },
|
|
191
|
+
auth: { jwtSecret: 'a'.repeat(32), tokenTtlSeconds: 3600, ...overrides.auth },
|
|
192
|
+
features: { maintenanceMode: false, ...overrides.features },
|
|
193
|
+
...overrides,
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Never
|
|
200
|
+
|
|
201
|
+
- Access `process.env` outside the config loader
|
|
202
|
+
- Import a config singleton in application modules
|
|
203
|
+
- Coerce types (parseInt, parseFloat) inside application code
|
|
204
|
+
- Log config values or `process.env` at any level
|
|
205
|
+
- Commit `.env` — only `.env.example` is committed
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: log-standard
|
|
3
|
+
description: >
|
|
4
|
+
Normative TSF++ logging rules. Load when writing or reviewing any code that
|
|
5
|
+
logs, uses the Logger port, implements a logger adapter, or calls tap/tapErr
|
|
6
|
+
for side effects: Logger port from @tsfpp/prelude, LogEntry field conventions,
|
|
7
|
+
log level semantics, structured message format, what never to log (PII,
|
|
8
|
+
secrets, stack traces), withRequestLog for HTTP, silentLogger for tests.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# TSF++ log standard
|
|
12
|
+
|
|
13
|
+
Full standard: `node_modules/@tsfpp/standard/spec/LOG_CODING_STANDARD.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { type Logger, type LogEntry, type LogLevel } from '@tsfpp/prelude'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Never import `pino`, `winston`, or `console` in core, use-case, or DAL code.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Logger port
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// Infrastructure adapter — inject this; never the library directly
|
|
31
|
+
import pino from 'pino'
|
|
32
|
+
import { type Logger } from '@tsfpp/prelude'
|
|
33
|
+
|
|
34
|
+
export const logger: Logger = {
|
|
35
|
+
debug: (entry) => pinoInstance.debug(entry, entry.message),
|
|
36
|
+
info: (entry) => pinoInstance.info(entry, entry.message),
|
|
37
|
+
warn: (entry) => pinoInstance.warn(entry, entry.message),
|
|
38
|
+
error: (entry) => pinoInstance.error(entry, entry.message),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Tests — always use silentLogger, never the production logger
|
|
42
|
+
export const silentLogger: Logger = {
|
|
43
|
+
debug: () => undefined,
|
|
44
|
+
info: () => undefined,
|
|
45
|
+
warn: () => undefined,
|
|
46
|
+
error: () => undefined,
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Log levels
|
|
53
|
+
|
|
54
|
+
| Level | Use when |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `debug` | Diagnostic detail — disabled in production by default. Never log PII even at debug. |
|
|
57
|
+
| `info` | A significant, expected business event occurred. One entry per meaningful outcome. |
|
|
58
|
+
| `warn` | Unexpected but recoverable — retry triggered, rate limit approached, deprecated path called. |
|
|
59
|
+
| `error` | Failure requiring attention — operation failed, dependency unreachable, unhandled `Err` at boundary. |
|
|
60
|
+
|
|
61
|
+
`info` is for **business events**, not execution steps. Never log "calling repository", "repository returned".
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## LogEntry field conventions
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// message — dot-separated event name, machine-readable
|
|
69
|
+
{ message: 'user.created' }
|
|
70
|
+
{ message: 'payment.charge.failed' }
|
|
71
|
+
{ message: 'session.expired' }
|
|
72
|
+
|
|
73
|
+
// Always include traceId in request-scoped logs
|
|
74
|
+
{ message: 'user.created', traceId: ctx.traceId, userId: user.id }
|
|
75
|
+
|
|
76
|
+
// Always include code on error-level logs
|
|
77
|
+
{ message: 'db.query.failed', code: 'db_timeout', traceId }
|
|
78
|
+
|
|
79
|
+
// duration for operations with performance budgets (milliseconds)
|
|
80
|
+
{ message: 'payment.charge.completed', duration: Date.now() - start, traceId }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Flat structure only — no nested objects. Log aggregators cannot query nested fields.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Logging in pipelines — tap / tapErr
|
|
88
|
+
|
|
89
|
+
Never break a `pipe` chain to log. Use `tap` / `tapErr`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// Good
|
|
93
|
+
pipe(
|
|
94
|
+
validateInput(input),
|
|
95
|
+
flatMap(createUser(deps.users)),
|
|
96
|
+
tap(user => deps.logger.info({ message: 'user.created', userId: user.id, traceId })),
|
|
97
|
+
tapErr(err => deps.logger.error({ message: 'user.create.failed', code: err.code, traceId })),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Bad — breaks the pipeline
|
|
101
|
+
const result = await createUser(deps.users)(input)
|
|
102
|
+
if (isOk(result)) deps.logger.info(...) // separate from the pipeline
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Log `cause` before discarding it
|
|
108
|
+
|
|
109
|
+
`dependency` and `internal` `ApiError` variants carry a `cause` that is stripped by `apiErrorToResponse`. Log it first:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
if (isErr(result) && result.error.kind === 'dependency') {
|
|
113
|
+
deps.logger.error({
|
|
114
|
+
message: 'payment.gateway.unreachable',
|
|
115
|
+
code: 'dependency_error',
|
|
116
|
+
error: String(result.error.cause),
|
|
117
|
+
traceId: ctx.traceId,
|
|
118
|
+
})
|
|
119
|
+
return apiErrorToResponse(result.error, ctx)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## HTTP request logging
|
|
126
|
+
|
|
127
|
+
Use `withRequestLog` from `@tsfpp/boundary` — never add manual logging to handler bodies.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const handler = pipe(
|
|
131
|
+
createUserHandler(deps),
|
|
132
|
+
withIdempotency(store),
|
|
133
|
+
withRequestLog(logger, '/v1/users'), // always outermost; routeTemplate not resolved URL
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`routeTemplate` must be the parameterised path (`/v1/users/:id`), never the resolved URL.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Never log
|
|
142
|
+
|
|
143
|
+
- PII — names, emails, phone numbers, addresses, national IDs
|
|
144
|
+
- Credentials — passwords, tokens, API keys, session IDs
|
|
145
|
+
- Full request or response bodies at `info` or above
|
|
146
|
+
- Stack traces in production — log `err.message`, not `err.stack`
|
|
147
|
+
- `process.env` or config values — they may contain secrets
|
|
148
|
+
- `console.*` anywhere except `main.ts` / `server.ts` startup boundary
|
package/init.mjs
CHANGED
|
@@ -66,6 +66,8 @@ const FILES = [
|
|
|
66
66
|
['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
|
|
67
67
|
['copilot/skills/test-standard/SKILL.md', '.github/skills/test-standard/SKILL.md'],
|
|
68
68
|
['copilot/skills/annotation-standard/SKILL.md', '.github/skills/annotation-standard/SKILL.md'],
|
|
69
|
+
['copilot/skills/log-standard/SKILL.md', '.github/skills/log-standard/SKILL.md'],
|
|
70
|
+
['copilot/skills/config-standard/SKILL.md', '.github/skills/config-standard/SKILL.md'],
|
|
69
71
|
|
|
70
72
|
// Claude Code
|
|
71
73
|
['claude/CLAUDE.md', '.claude/CLAUDE.md'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsfpp/agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tsfpp",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"LICENSE"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@tsfpp/standard":
|
|
41
|
-
"@tsfpp/prelude":
|
|
42
|
-
"@tsfpp/boundary":
|
|
43
|
-
"@tsfpp/eslint-config":">=1.0.0",
|
|
44
|
-
"@tsfpp/tsconfig":
|
|
40
|
+
"@tsfpp/standard": ">=1.0.0",
|
|
41
|
+
"@tsfpp/prelude": ">=1.0.0",
|
|
42
|
+
"@tsfpp/boundary": ">=1.0.0",
|
|
43
|
+
"@tsfpp/eslint-config": ">=1.0.0",
|
|
44
|
+
"@tsfpp/tsconfig": ">=1.0.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|