claude-toolkit 0.11.0 → 0.15.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 +9 -0
- package/README.md +2 -0
- package/core/skills/ct-code-style/SKILL.md +134 -0
- package/docs/README.md +2 -0
- package/docs/skills/code-style.md +26 -0
- package/docs/stacks/esnext-idioms.md +25 -0
- package/package.json +1 -1
- package/src/detect.ts +74 -0
- package/src/generator.ts +3 -1
- package/src/types.ts +1 -0
- package/stacks/esnext/skills/ct-esnext-idioms/SKILL.md +130 -0
- package/stacks/esnext/stack.json +37 -0
- package/stacks/solidjs/skills/ct-solidjs-patterns/SKILL.md +1 -1
- package/templates/configs/biome.base.json +8 -1
- package/templates/skill-rules.base.json +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 (2026-07-04)
|
|
4
|
+
|
|
5
|
+
Two new authoring-convention assets — an opt-in `esnext` stack and an always-on `ct-code-style` core skill — plus Biome enforcement of the mechanically-checkable style rules.
|
|
6
|
+
|
|
7
|
+
- feat: add the `esnext` stack (skill `ct-esnext-idioms`) — availability-aware guidance on modern ECMAScript runtime features: `Temporal` over `Date`, `using`/`await using` for deterministic cleanup, lazy iterator helpers, `structuredClone`, `Object.groupBy`/`Map.groupBy`, `Promise.withResolvers`, immutable array ops, and ESM hygiene. Detected on tsconfig `target`/`module`/`lib` = ESNext/ES2022+, package.json `"type":"module"`, `engines.node >= 20`, or a `.mjs` file.
|
|
8
|
+
- feat: add the `ct-code-style` core skill (always included) — universal code-structure conventions: guard clauses over nested `else`, object lookup tables over `if/else if` chains, single-level ternaries, full ES6 destructuring, and arrow-only functions with no `class`/`this`. Carve-outs defer to stack skills: never destructure reactive SolidJS `props`, and use `class`/`this` only where a platform mandates it (e.g. Cloudflare Durable Objects).
|
|
9
|
+
- feat: enforce the mechanically-checkable style rules in the scaffolded Biome config — `noUselessElse`, `noNestedTernary`, and `useArrowFunction`. `noMagicNumbers` is kept as skill guidance only (too noisy to enforce).
|
|
10
|
+
- docs: document both assets in the README, `docs/stacks/esnext-idioms.md`, `docs/skills/code-style.md`, and the docs index; cross-link the SolidJS props rule to `ct-code-style`.
|
|
11
|
+
|
|
3
12
|
## 0.11.0 (2026-07-04)
|
|
4
13
|
|
|
5
14
|
Remediation of the 2026-07-04 core audit (46 findings): the skill-eval hook is hardened, stack-owned commands are gated behind their stack, and the `ct-` agents, commands, skills, and reference docs are corrected and aligned.
|
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ Stacks in your config that are no longer detected are reported but left unchange
|
|
|
107
107
|
| `playwright` | Playwright E2E testing, Page Objects, fixtures, CI/CD |
|
|
108
108
|
| `storybook` | Storybook interaction testing, CSF 3, visual regression |
|
|
109
109
|
| `capacitor` | Capacitor 8 runtime, Capgo OTA, channels; webview UI & native feel |
|
|
110
|
+
| `esnext` | Modern ECMAScript idioms — Temporal, iterator helpers, `using`, structuredClone |
|
|
110
111
|
|
|
111
112
|
## Core Features (always included)
|
|
112
113
|
|
|
@@ -122,6 +123,7 @@ Stacks in your config that are no longer detected are reported but left unchange
|
|
|
122
123
|
- `ct-testing-patterns` — TDD workflow and patterns
|
|
123
124
|
- `ct-typescript-conventions` — TypeScript strict mode best practices
|
|
124
125
|
- `ct-verification-before-completion` — Evidence-based completion claims
|
|
126
|
+
- `ct-code-style` — Code structure & style: guard clauses, lookup tables, no magic values
|
|
125
127
|
|
|
126
128
|
### Core Commands
|
|
127
129
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ct-code-style
|
|
3
|
+
description: Code structure & style — guard clauses, lookup tables, destructuring, no classes, no magic values
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Style
|
|
7
|
+
|
|
8
|
+
Write small, flat, data-driven functions. These conventions apply to all JS/TS. Stack skills override where noted below.
|
|
9
|
+
|
|
10
|
+
## Guard clauses / early returns
|
|
11
|
+
|
|
12
|
+
Return early on edge cases; never nest the happy path inside `else`.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// Bad — nested, else-heavy
|
|
16
|
+
const price = (user) => {
|
|
17
|
+
if (user) {
|
|
18
|
+
if (user.active) {
|
|
19
|
+
return user.plan.price;
|
|
20
|
+
} else {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Good — guard clauses, flat
|
|
29
|
+
const price = (user) => {
|
|
30
|
+
if (!user) return 0;
|
|
31
|
+
if (!user.active) return 0;
|
|
32
|
+
return user.plan.price;
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Rule: no `else` / `else if` after a `return`.
|
|
37
|
+
|
|
38
|
+
## Lookup tables over chained conditionals
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// Bad
|
|
42
|
+
const getDiscount = (type) => {
|
|
43
|
+
if (type === "student") return 0.1;
|
|
44
|
+
else if (type === "senior") return 0.15;
|
|
45
|
+
else if (type === "employee") return 0.2;
|
|
46
|
+
else return 0;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Good
|
|
50
|
+
const getDiscount = (type) => {
|
|
51
|
+
const discounts = { student: 0.1, senior: 0.15, employee: 0.2 };
|
|
52
|
+
return discounts[type] ?? 0;
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For an exhaustive **typed** union where every case must be handled, prefer an exhaustive `switch` — see `ct-typescript-conventions`.
|
|
57
|
+
|
|
58
|
+
## Ternaries — one level, never nested
|
|
59
|
+
|
|
60
|
+
A ternary must not contain another ternary. Nesting → extract a lookup table or guard clauses.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Bad — nested ternary
|
|
64
|
+
const label = a ? "x" : b ? "y" : "z";
|
|
65
|
+
|
|
66
|
+
// Good — lookup or guards
|
|
67
|
+
const labels = { a: "x", b: "y" };
|
|
68
|
+
const label = labels[key] ?? "z";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Destructuring — use it fully
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const { id, name: title, role = "guest", address: { city } = {}, ...rest } = user;
|
|
75
|
+
// basic rename default nested (+ default) rest
|
|
76
|
+
const greet = ({ name, lang = "en" }) => `${GREETING[lang]} ${name}`; // in params
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**⚠️ Never destructure reactive framework props.** In SolidJS, `const { value } = props` breaks reactivity — read `props.value` at the point of use. This carve-out is authoritative: see `ct-solidjs-patterns`. Destructuring is for plain data objects.
|
|
80
|
+
|
|
81
|
+
## Functions — arrow-only, no `class`, no `this`
|
|
82
|
+
|
|
83
|
+
Model behavior as functions and closures over state. No `class`, no `this` in your own code.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// Bad — class + this
|
|
87
|
+
class Counter {
|
|
88
|
+
count = 0;
|
|
89
|
+
inc() {
|
|
90
|
+
this.count++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Good — closure
|
|
95
|
+
const makeCounter = () => {
|
|
96
|
+
let count = 0;
|
|
97
|
+
return { inc: () => ++count, value: () => count };
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**⚠️ Exception:** use `class` / `this` only when a third-party lib or platform leaves no functional alternative — e.g. Cloudflare Durable Objects / `WorkerEntrypoint` (`export class Room extends DurableObject`), or subclassing built-ins (`class AppError extends Error`). See `ct-cloudflare-d1-kv`.
|
|
102
|
+
|
|
103
|
+
## No magic numbers or strings
|
|
104
|
+
|
|
105
|
+
Name every literal that carries meaning. User-facing text → i18n keys, never inline strings.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// Bad
|
|
109
|
+
if (retries > 3) show("Trop de tentatives");
|
|
110
|
+
|
|
111
|
+
// Good
|
|
112
|
+
const MAX_RETRIES = 3;
|
|
113
|
+
if (retries > MAX_RETRIES) show(LL.errors.tooManyRetries()); // i18n key, not a literal
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
User-facing strings belong in the locale layer — see `ct-i18n-typesafe`.
|
|
117
|
+
|
|
118
|
+
## Anti-Patterns
|
|
119
|
+
|
|
120
|
+
| Bad | Good |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `else` after a `return` | guard clauses |
|
|
123
|
+
| `if/else if` chain on a value | object lookup table + `??` |
|
|
124
|
+
| nested ternary | lookup table or guards |
|
|
125
|
+
| `class` + `this` for your own logic | arrow functions + closures |
|
|
126
|
+
| destructuring SolidJS `props` | read `props.x` (see `ct-solidjs-patterns`) |
|
|
127
|
+
| magic number / inline UI string | named const / i18n key |
|
|
128
|
+
|
|
129
|
+
## See Also
|
|
130
|
+
|
|
131
|
+
- `ct-typescript-conventions` — exhaustive `switch` for typed unions; typing destructured params.
|
|
132
|
+
- `ct-esnext-idioms` — modern features these patterns pair with (`??`, `Object.groupBy`).
|
|
133
|
+
- `ct-solidjs-patterns` — props are reactive; do not destructure them.
|
|
134
|
+
- `ct-i18n-typesafe` — where user-facing strings live.
|
package/docs/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Skills that are always included regardless of stack configuration.
|
|
|
16
16
|
| [ct-testing-patterns](skills/testing-patterns.md) | Framework-agnostic TDD practices, mocking strategies, and test organization |
|
|
17
17
|
| [ct-typescript-conventions](skills/typescript-conventions.md) | Strict TypeScript patterns for type safety and maintainability |
|
|
18
18
|
| [ct-verification-before-completion](skills/verification-before-completion.md) | Evidence-based completion claims with structured verification checklist |
|
|
19
|
+
| [ct-code-style](skills/code-style.md) | Code structure & style: guard clauses, lookup tables, no magic values |
|
|
19
20
|
|
|
20
21
|
## Commands
|
|
21
22
|
|
|
@@ -55,3 +56,4 @@ Stack-specific skills activated based on your `claude-toolkit.config.ts` configu
|
|
|
55
56
|
| [ct-playwright-patterns](stacks/playwright-patterns.md) | `playwright` | E2E testing with Page Objects, fixtures, auth, network mocking, CI/CD |
|
|
56
57
|
| [ct-storybook-patterns](stacks/storybook-patterns.md) | `storybook` | Interaction testing, CSF 3, play functions, a11y, visual regression |
|
|
57
58
|
| [ct-capacitor-ota](stacks/capacitor-ota.md) | `capacitor` | Capacitor 8 native runtime and Capgo OTA live updates, channels, encryption |
|
|
59
|
+
| [ct-esnext-idioms](stacks/esnext-idioms.md) | `esnext` | Modern ECMAScript runtime idioms: Temporal, iterator helpers, using, structuredClone |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Code Style
|
|
2
|
+
|
|
3
|
+
> Code structure & style: guard clauses, lookup tables, destructuring, no classes, no magic values.
|
|
4
|
+
|
|
5
|
+
**Type:** Core Skill (always included)
|
|
6
|
+
**Source:** [`core/skills/ct-code-style/SKILL.md`](../../core/skills/ct-code-style/SKILL.md)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Universal, target-independent conventions for writing small, flat, data-driven functions. Always active — no stack required. It complements `ct-typescript-conventions` (types) and `ct-esnext-idioms` (modern features). Stack skills win where they overlap.
|
|
11
|
+
|
|
12
|
+
## Rules
|
|
13
|
+
|
|
14
|
+
- **Guard clauses / early returns** — no `else`/`else if` after a `return`.
|
|
15
|
+
- **Lookup tables** over `if/else if` chains keyed on a value (`discounts[type] ?? 0`).
|
|
16
|
+
- **Ternaries** — one level, never nested.
|
|
17
|
+
- **Full ES6 destructuring** — basic, renaming, defaults, nested, rest, in parameters.
|
|
18
|
+
- **Arrow-only, no `class`, no `this`** — model behavior as functions + closures.
|
|
19
|
+
- **No magic numbers/strings** — named constants; user-facing text → i18n keys.
|
|
20
|
+
|
|
21
|
+
## Carve-outs (stack skills win)
|
|
22
|
+
|
|
23
|
+
- **SolidJS props:** never destructure reactive `props` — it breaks reactivity. See [`ct-solidjs-patterns`](../best-practices/solidjs/props-patterns.md).
|
|
24
|
+
- **Platform classes:** `class`/`this` only where a third-party lib/platform requires it (Cloudflare Durable Objects, `class X extends Error`).
|
|
25
|
+
|
|
26
|
+
Three of these rules are additionally enforced by Biome in the scaffolded `biome.json` (`noUselessElse`, `noNestedTernary`, `useArrowFunction`). "No magic numbers" stays guidance-only — it is too noisy to enforce mechanically.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ESNext Idioms
|
|
2
|
+
|
|
3
|
+
> Modern ECMAScript runtime idioms — Temporal, iterator helpers, `using`, structuredClone.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `esnext` stack)
|
|
6
|
+
**Source:** [`stacks/esnext/skills/ct-esnext-idioms/SKILL.md`](../../stacks/esnext/skills/ct-esnext-idioms/SKILL.md)
|
|
7
|
+
**Detected by:** tsconfig `target`/`module`/`lib` = `ESNext`/`ES2022+`, package.json `"type":"module"`, `engines.node >= 20`, or a `.mjs` file
|
|
8
|
+
**File Extensions:** `.mjs`, `.cjs`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Guidance for preferring current ECMAScript **runtime features** over legacy patterns and hand-rolled polyfills. It is availability-aware: each newer feature notes the runtime/polyfill it needs, so the recommendation never generates code the target can't run. Types are covered by `ct-typescript-conventions`; general code structure by `ct-code-style`.
|
|
13
|
+
|
|
14
|
+
## Topics
|
|
15
|
+
|
|
16
|
+
- **Dates → `Temporal`** — immutable, timezone-correct calendar/clock math (polyfill until baseline).
|
|
17
|
+
- **Resource management → `using` / `await using`** — deterministic cleanup via `Symbol.dispose`/`asyncDispose`.
|
|
18
|
+
- **Iterator helpers** — lazy `map`/`filter`/`take`, `Iterator.concat`, `Array.fromAsync`.
|
|
19
|
+
- **Clone & group** — `structuredClone`, `Object.groupBy` / `Map.groupBy`.
|
|
20
|
+
- **Async ergonomics** — `Promise.withResolvers`, `AbortSignal.timeout`/`any`, top-level await.
|
|
21
|
+
- **Immutable array ops** — `at`, `findLast`, `toSorted`/`toReversed`/`with`.
|
|
22
|
+
- **Errors** — `Error({ cause })`, `AggregateError`.
|
|
23
|
+
- **ESM hygiene** — `import.meta.dirname` over `__dirname`, dynamic `import()`.
|
|
24
|
+
|
|
25
|
+
See the [skill source](../../stacks/esnext/skills/ct-esnext-idioms/SKILL.md) for the full patterns, availability notes, and anti-patterns table.
|
package/package.json
CHANGED
package/src/detect.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface PackageJson {
|
|
|
12
12
|
dependencies?: Record<string, string>;
|
|
13
13
|
devDependencies?: Record<string, string>;
|
|
14
14
|
workspaces?: string[] | { packages?: string[] };
|
|
15
|
+
type?: string;
|
|
16
|
+
engines?: { node?: string };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** A directory to inspect for stack markers, with its project-relative label */
|
|
@@ -54,6 +56,9 @@ const IGNORE_DIRS = new Set([
|
|
|
54
56
|
"test-results",
|
|
55
57
|
]);
|
|
56
58
|
|
|
59
|
+
/** Node major version at/above which modern ESNext runtime features are assumed available. */
|
|
60
|
+
const MIN_MODERN_NODE_MAJOR = 20;
|
|
61
|
+
|
|
57
62
|
function readPackageJson(dir: string): PackageJson | null {
|
|
58
63
|
const pkgPath = join(dir, "package.json");
|
|
59
64
|
if (!existsSync(pkgPath)) return null;
|
|
@@ -174,6 +179,57 @@ function findProtoFiles(ctx: DetectContext): string | null {
|
|
|
174
179
|
return null;
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
/** Read a file's text, or null if missing/unreadable (tolerates comments/trailing commas). */
|
|
183
|
+
function readTextFile(path: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
return readFileSync(path, "utf-8");
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* First scan root whose tsconfig.json sets target/module/lib to ESNext or ES2022+.
|
|
193
|
+
* Uses a lenient text scan because tsconfig files routinely carry comments and
|
|
194
|
+
* trailing commas that would break JSON.parse.
|
|
195
|
+
*/
|
|
196
|
+
function tsconfigTargetsEsnext(ctx: DetectContext): string | null {
|
|
197
|
+
const modern = /es(?:next|20(?:2[2-9]|[3-9]\d))/i; // ESNext or ES2022..ES2099
|
|
198
|
+
const keyValue = /"(?:target|module|lib)"\s*:\s*(?:"([^"]*)"|\[([^\]]*)\])/gi;
|
|
199
|
+
for (const { rel, dir } of ctx.scanRoots) {
|
|
200
|
+
const text = readTextFile(join(dir, "tsconfig.json"));
|
|
201
|
+
if (!text) continue;
|
|
202
|
+
for (const m of text.matchAll(keyValue)) {
|
|
203
|
+
if (modern.test(m[1] ?? m[2] ?? "")) return relPath(rel, "tsconfig.json");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Whether a package.json's engines.node floor is >= `major` (first integer in the range). */
|
|
210
|
+
function enginesNodeAtLeast(pkg: PackageJson, major: number): boolean {
|
|
211
|
+
const range = pkg.engines?.node;
|
|
212
|
+
if (!range) return false;
|
|
213
|
+
const m = range.match(/(\d+)/);
|
|
214
|
+
return m ? Number(m[1]) >= major : false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** First `.mjs` file in a bounded search: each scan root's top level + bin/, src/, scripts/. */
|
|
218
|
+
function findMjs(ctx: DetectContext): string | null {
|
|
219
|
+
const subs = ["", "bin", "src", "scripts"];
|
|
220
|
+
for (const { rel, dir } of ctx.scanRoots) {
|
|
221
|
+
for (const sub of subs) {
|
|
222
|
+
const searchDir = sub ? join(dir, sub) : dir;
|
|
223
|
+
if (!existsSync(searchDir)) continue;
|
|
224
|
+
const glob = new Bun.Glob("*.mjs"); // non-recursive: never descends into node_modules
|
|
225
|
+
for (const match of glob.scanSync({ cwd: searchDir, onlyFiles: true })) {
|
|
226
|
+
return relPath(rel, sub ? `${sub}/${match}` : match);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
177
233
|
const DETECTORS: StackDetector[] = [
|
|
178
234
|
{
|
|
179
235
|
name: "solidjs",
|
|
@@ -271,6 +327,24 @@ const DETECTORS: StackDetector[] = [
|
|
|
271
327
|
return config ? { name: "capacitor", reason: `found ${config}` } : null;
|
|
272
328
|
},
|
|
273
329
|
},
|
|
330
|
+
{
|
|
331
|
+
name: "esnext",
|
|
332
|
+
detect: (ctx) => {
|
|
333
|
+
const ts = tsconfigTargetsEsnext(ctx);
|
|
334
|
+
if (ts) return { name: "esnext", reason: `found ESNext target in ${ts}` };
|
|
335
|
+
const mod = ctx.packages.find(({ pkg }) => pkg.type === "module");
|
|
336
|
+
if (mod) return { name: "esnext", reason: `found "type":"module" in ${pkgLabel(mod.rel)}` };
|
|
337
|
+
const eng = ctx.packages.find(({ pkg }) => enginesNodeAtLeast(pkg, MIN_MODERN_NODE_MAJOR));
|
|
338
|
+
if (eng)
|
|
339
|
+
return {
|
|
340
|
+
name: "esnext",
|
|
341
|
+
reason: `found engines.node >=${MIN_MODERN_NODE_MAJOR} in ${pkgLabel(eng.rel)}`,
|
|
342
|
+
};
|
|
343
|
+
const mjs = findMjs(ctx);
|
|
344
|
+
if (mjs) return { name: "esnext", reason: `found ${mjs}` };
|
|
345
|
+
return null;
|
|
346
|
+
},
|
|
347
|
+
},
|
|
274
348
|
];
|
|
275
349
|
|
|
276
350
|
/** Scan a project directory (root + subdirs + workspace packages) and detect which stacks are present */
|
package/src/generator.ts
CHANGED
|
@@ -42,7 +42,7 @@ async function resolveConfig(config: ClaudeToolkitConfig): Promise<ResolvedConfi
|
|
|
42
42
|
"ct-testing-patterns",
|
|
43
43
|
"ct-typescript-conventions",
|
|
44
44
|
"ct-verification-before-completion",
|
|
45
|
-
|
|
45
|
+
"ct-code-style",
|
|
46
46
|
].filter((s) => !config.excludeSkills?.includes(s));
|
|
47
47
|
|
|
48
48
|
return { config, skills, directoryMappings: allMappings, hooks, stacks };
|
|
@@ -375,6 +375,8 @@ async function generateSkillsReadme(claudeDir: string, resolved: ResolvedConfig)
|
|
|
375
375
|
content += "| ct-testing-patterns | Test-driven development workflow and patterns |\n";
|
|
376
376
|
content += "| ct-typescript-conventions | TypeScript strict mode and conventions |\n";
|
|
377
377
|
content += "| ct-verification-before-completion | Evidence-based completion claims protocol |\n";
|
|
378
|
+
content +=
|
|
379
|
+
"| ct-code-style | Code structure & style: guard clauses, lookup tables, no magic values |\n";
|
|
378
380
|
content += "\n## Stack Skills\n\n";
|
|
379
381
|
content += "| Skill | Stack | Description |\n";
|
|
380
382
|
content += "|-------|-------|-------------|\n";
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ct-esnext-idioms
|
|
3
|
+
description: Modern ECMAScript runtime idioms — Temporal, iterator helpers, using, structuredClone
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modern ESNext Idioms
|
|
7
|
+
|
|
8
|
+
Prefer current ECMAScript runtime features over legacy patterns and hand-rolled polyfills. Types live in `ct-typescript-conventions`; code structure lives in `ct-code-style`; this skill is about *runtime features*.
|
|
9
|
+
|
|
10
|
+
> Some features below are newer than the Node/browser baseline. Each is tagged with what it needs. When a target can't guarantee it, use the noted polyfill/flag rather than dropping back to the legacy pattern.
|
|
11
|
+
|
|
12
|
+
## Dates & time → `Temporal`
|
|
13
|
+
|
|
14
|
+
`Date` is mutable, timezone-naive, and error-prone for math. Use `Temporal` for calendar/clock work.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const today = Temporal.Now.plainDateISO(); // no time, no tz confusion
|
|
18
|
+
const due = today.add({ days: 30 }); // immutable — returns a new value
|
|
19
|
+
const start = Temporal.ZonedDateTime.from("2026-07-04T09:00[Europe/Paris]");
|
|
20
|
+
const mins = start.until(Temporal.Now.zonedDateTimeISO()).total("minutes");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
_Availability: Stage 3 — not yet baseline in Node. Add `temporal-polyfill` (or `@js-temporal/polyfill`) until it ships._ Avoid `Date` mutation, manual `getTime()` ms math, and moment.js.
|
|
24
|
+
|
|
25
|
+
## Deterministic cleanup → `using` / `await using`
|
|
26
|
+
|
|
27
|
+
`Symbol.dispose` / `Symbol.asyncDispose` free resources when scope exits — no `try/finally` ladder.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const openFile = (path: string) => {
|
|
31
|
+
const handle = fs.openSync(path, "r");
|
|
32
|
+
return { handle, [Symbol.dispose]: () => fs.closeSync(handle) };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const readHead = (path: string) => {
|
|
36
|
+
using file = openFile(path); // closed automatically at end of block
|
|
37
|
+
return fs.readSync(file.handle, Buffer.alloc(16), 0, 16, 0);
|
|
38
|
+
}; // ← dispose runs here, even on throw
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`await using` awaits `Symbol.asyncDispose` (db connections, locks, spans). _Availability: TS 5.2+ (downlevels) or Node 24+._
|
|
42
|
+
|
|
43
|
+
## Iterator helpers → lazy, no intermediate arrays
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const firstThree = users.values()
|
|
47
|
+
.filter((u) => u.active)
|
|
48
|
+
.map((u) => u.name)
|
|
49
|
+
.take(3)
|
|
50
|
+
.toArray(); // only 3 names ever materialised
|
|
51
|
+
|
|
52
|
+
const merged = Iterator.concat(pageA.values(), pageB.values()); // Iterator Sequencing — Stage 4 (2025); polyfill on older runtimes
|
|
53
|
+
const rows = await Array.fromAsync(streamRows()); // async iterable → array
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
_Availability: standard iterator helpers (`.filter`/`.map`/`.take`) and `Array.fromAsync` are Node 22+ / modern browsers; `Iterator.concat` is newer (Iterator Sequencing, 2025) — polyfill until it lands._ Replaces `[...a, ...b].filter().map().slice()`, which allocates at every step.
|
|
57
|
+
|
|
58
|
+
## Clone & group
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const copy = structuredClone(state); // deep clone — Map/Set/Date/typed arrays
|
|
62
|
+
const byRole = Object.groupBy(users, (u) => u.role); // { admin: [...], guest: [...] }
|
|
63
|
+
const byId = Map.groupBy(users, (u) => u.id); // Map keys (any type)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Avoid `JSON.parse(JSON.stringify(x))` (drops `undefined`, turns `Date` into a string, throws on cycles) and manual `reduce` grouping.
|
|
67
|
+
|
|
68
|
+
## Promise & async ergonomics
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const { promise, resolve, reject } = Promise.withResolvers<Data>(); // no deferred-hack closure
|
|
72
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5_000) }); // auto-abort
|
|
73
|
+
const signal = AbortSignal.any([userAbort, AbortSignal.timeout(30_000)]);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Top-level `await` is available in ESM modules — no IIFE wrapper.
|
|
77
|
+
|
|
78
|
+
## Immutable array ops
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const last = items.at(-1); // negative index
|
|
82
|
+
const newest = orders.findLast((o) => o.settled);
|
|
83
|
+
const sorted = scores.toSorted((a, b) => b - a); // non-mutating; also toReversed/toSpliced/with
|
|
84
|
+
const patched = row.with(2, "x"); // copy with index 2 replaced
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Errors with cause
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
try {
|
|
91
|
+
await save();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error("Checkout failed", { cause: err }); // preserve the chain
|
|
94
|
+
}
|
|
95
|
+
throw new AggregateError(errors, "Some uploads failed"); // aggregate independent failures
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ESM hygiene
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const here = import.meta.dirname; // replaces __dirname (Node 20.11+/21.2+)
|
|
102
|
+
const url = import.meta.url;
|
|
103
|
+
const mod = await import("./plugin.js"); // dynamic import, not require()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Performance
|
|
107
|
+
|
|
108
|
+
> _Verified against ECMAScript 2024 / Node 22 (2026-07)._
|
|
109
|
+
|
|
110
|
+
- **Iterator helpers are lazy** — `take(3)` stops the pipeline after 3 items; the array form processes everything first. Use them for early-exit and large/infinite sequences.
|
|
111
|
+
- **`structuredClone` is a native deep copy** — faster and more correct than `JSON.parse(JSON.stringify())`, with no string round-trip.
|
|
112
|
+
- **`AbortSignal.timeout`** removes `setTimeout` bookkeeping and leaks; the runtime clears the timer when the signal settles.
|
|
113
|
+
|
|
114
|
+
## Anti-Patterns
|
|
115
|
+
|
|
116
|
+
| Legacy | Modern |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `new Date()` math / moment.js | `Temporal` (via polyfill) |
|
|
119
|
+
| `try { } finally { close() }` | `using` / `await using` |
|
|
120
|
+
| `[...a, ...b].filter().map().slice()` | iterator helpers + `Iterator.concat` |
|
|
121
|
+
| `JSON.parse(JSON.stringify(x))` | `structuredClone(x)` |
|
|
122
|
+
| manual `reduce` grouping | `Object.groupBy` / `Map.groupBy` |
|
|
123
|
+
| deferred `let resolve; new Promise(...)` | `Promise.withResolvers()` |
|
|
124
|
+
| `arr[arr.length - 1]` | `arr.at(-1)` |
|
|
125
|
+
| `__dirname` / `require()` | `import.meta.dirname` / `import()` |
|
|
126
|
+
|
|
127
|
+
## See Also
|
|
128
|
+
|
|
129
|
+
- `ct-typescript-conventions` — typing these values (`satisfies`, discriminated unions for async state).
|
|
130
|
+
- `ct-code-style` — structure around these features (guard clauses, no magic numbers, `??`).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ct-esnext-idioms",
|
|
3
|
+
"description": "Modern ECMAScript runtime idioms — Temporal, iterator helpers, using, structuredClone",
|
|
4
|
+
"defaultMappings": {},
|
|
5
|
+
"fileExtensions": ["mjs", "cjs"],
|
|
6
|
+
"skillRules": {
|
|
7
|
+
"ct-esnext-idioms": {
|
|
8
|
+
"description": "Modern ECMAScript runtime idioms and features",
|
|
9
|
+
"priority": 5,
|
|
10
|
+
"triggers": {
|
|
11
|
+
"keywords": [
|
|
12
|
+
"Temporal",
|
|
13
|
+
"structuredClone",
|
|
14
|
+
"iterator",
|
|
15
|
+
"using",
|
|
16
|
+
"dispose",
|
|
17
|
+
"groupBy",
|
|
18
|
+
"withResolvers",
|
|
19
|
+
"AbortSignal",
|
|
20
|
+
"fromAsync",
|
|
21
|
+
"esnext",
|
|
22
|
+
"moment",
|
|
23
|
+
"date"
|
|
24
|
+
],
|
|
25
|
+
"keywordPatterns": ["\\bTemporal\\b", "\\busing\\b", "\\bstructuredClone\\b"],
|
|
26
|
+
"contentPatterns": [
|
|
27
|
+
"new Date\\(",
|
|
28
|
+
"JSON\\.parse\\(JSON\\.stringify",
|
|
29
|
+
"__dirname",
|
|
30
|
+
"\\.groupBy\\("
|
|
31
|
+
],
|
|
32
|
+
"intentPatterns": ["(?:format|parse|manipulate).*date", "(?:clone|copy).*(?:object|deep)"]
|
|
33
|
+
},
|
|
34
|
+
"relatedSkills": ["ct-typescript-conventions", "ct-code-style"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -29,7 +29,7 @@ const [data] = createResource(userId, fetchUser); // async: data(), data.loading
|
|
|
29
29
|
|
|
30
30
|
## Props
|
|
31
31
|
|
|
32
|
-
**CRITICAL: Never destructure props.** Destructuring breaks reactivity.
|
|
32
|
+
**CRITICAL: Never destructure props.** Destructuring breaks reactivity. (This overrides the general "use destructuring" guidance in `ct-code-style` — destructure plain data, never reactive `props`.)
|
|
33
33
|
|
|
34
34
|
```tsx
|
|
35
35
|
// WRONG -- reads once, never updates
|
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
"linter": {
|
|
17
17
|
"enabled": true,
|
|
18
18
|
"rules": {
|
|
19
|
-
"recommended": true
|
|
19
|
+
"recommended": true,
|
|
20
|
+
"style": {
|
|
21
|
+
"noUselessElse": "error",
|
|
22
|
+
"noNestedTernary": "error"
|
|
23
|
+
},
|
|
24
|
+
"complexity": {
|
|
25
|
+
"useArrowFunction": "error"
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
},
|
|
22
29
|
"javascript": {
|
|
@@ -65,6 +65,29 @@
|
|
|
65
65
|
],
|
|
66
66
|
"contextPatterns": ["before claiming", "make sure", "double check"]
|
|
67
67
|
}
|
|
68
|
+
},
|
|
69
|
+
"ct-code-style": {
|
|
70
|
+
"description": "Code structure & style — guard clauses, lookup tables, destructuring, no magic values",
|
|
71
|
+
"priority": 6,
|
|
72
|
+
"triggers": {
|
|
73
|
+
"keywords": [
|
|
74
|
+
"refactor",
|
|
75
|
+
"clean up",
|
|
76
|
+
"guard clause",
|
|
77
|
+
"early return",
|
|
78
|
+
"destructure",
|
|
79
|
+
"ternary",
|
|
80
|
+
"magic number",
|
|
81
|
+
"magic string",
|
|
82
|
+
"lookup table",
|
|
83
|
+
"arrow function"
|
|
84
|
+
],
|
|
85
|
+
"contentPatterns": ["else if", "else\\s*\\{", "\\bclass\\b", "this\\.", "\\bfunction\\b"],
|
|
86
|
+
"intentPatterns": [
|
|
87
|
+
"(?:refactor|clean up|simplify|tidy).*(?:code|function|logic)",
|
|
88
|
+
"(?:extract|avoid).*(?:magic|constant|conditional)"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
93
|
}
|