@study-lenses/create-package 0.1.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/LICENSE +21 -0
- package/README.md +86 -0
- package/package.json +79 -0
- package/templates/.editorconfig +15 -0
- package/templates/.github/ISSUE_TEMPLATE/bug-report.yml +91 -0
- package/templates/.github/ISSUE_TEMPLATE/feature-request.yml +68 -0
- package/templates/.github/PULL_REQUEST_TEMPLATE.md +27 -0
- package/templates/.github/dependabot.yml +9 -0
- package/templates/.github/workflows/ci.yml +19 -0
- package/templates/.github/workflows/docs.yml +25 -0
- package/templates/.github/workflows/publish.yml +23 -0
- package/templates/.husky/pre-commit +1 -0
- package/templates/.prettierrc.json +15 -0
- package/templates/.vscode/extensions.json +11 -0
- package/templates/.vscode/launch.json +34 -0
- package/templates/.vscode/settings.json +28 -0
- package/templates/AGENTS.md +575 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/CODE-OF-CONDUCT.md +39 -0
- package/templates/CONTRIBUTING.md +53 -0
- package/templates/DEV.md +1200 -0
- package/templates/DOCS.md +19 -0
- package/templates/LICENSE +21 -0
- package/templates/README.md +89 -0
- package/templates/eslint.config.js +308 -0
- package/templates/gitignore +70 -0
- package/templates/package.json +78 -0
- package/templates/src/DOCS.md +16 -0
- package/templates/src/README.md +15 -0
- package/templates/src/index.ts +1 -0
- package/templates/tsconfig.json +57 -0
- package/templates/tsconfig.lint.json +5 -0
- package/templates/typedoc.json +15 -0
- package/templates/vitest.config.ts +8 -0
package/templates/DEV.md
ADDED
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
# Developer Guide
|
|
2
|
+
|
|
3
|
+
Internal architecture, conventions, and implementation details for contributors.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Architecture Overview](#architecture-overview)
|
|
8
|
+
- [Codebase Conventions](#codebase-conventions)
|
|
9
|
+
- [Directory Structure](#directory-structure)
|
|
10
|
+
- [Development Workflow](#development-workflow)
|
|
11
|
+
- [Testing Strategy](#testing-strategy)
|
|
12
|
+
- [Incremental Development Workflow](#incremental-development-workflow)
|
|
13
|
+
- [Linting Conventions](#linting-conventions)
|
|
14
|
+
- [Module Boundaries](#module-boundaries)
|
|
15
|
+
- [Code Quality Anti-Patterns](#code-quality-anti-patterns)
|
|
16
|
+
- [VS Code Setup](#vs-code-setup)
|
|
17
|
+
|
|
18
|
+
## Architecture Overview
|
|
19
|
+
|
|
20
|
+
See [README.md § Architecture](./README.md#architecture) for an overview.
|
|
21
|
+
Detailed conventions and module boundaries are documented in sections below.
|
|
22
|
+
|
|
23
|
+
## Codebase Conventions
|
|
24
|
+
|
|
25
|
+
> This codebase is designed to be accessible for first-time contributors and less experienced
|
|
26
|
+
> developers. Conventions prioritize learnability, debuggability, and consistency over brevity
|
|
27
|
+
> or "idiomatic JS."
|
|
28
|
+
|
|
29
|
+
### Conventions Summary
|
|
30
|
+
|
|
31
|
+
| Situation | Convention |
|
|
32
|
+
| ------------------------------ | ----------------------------------------------------------------- |
|
|
33
|
+
| Non-trivial function | Named `function` declaration |
|
|
34
|
+
| Inline callback (trivial) | Arrow OK: `user => user.id`, `n => n > 0` |
|
|
35
|
+
| Arrow assigned to variable | **Not allowed** — use named `function` declaration |
|
|
36
|
+
| Arrow with body block `{}` | **Not allowed** — use named `function` declaration |
|
|
37
|
+
| Callback (non-trivial) | Extract as named `function`, pass by name |
|
|
38
|
+
| Hoisting below call site | Encouraged for readability |
|
|
39
|
+
| `this` keyword | **Banned** (functional codebase) |
|
|
40
|
+
| Classes | **Banned** (exception: error classes in `/errors`) |
|
|
41
|
+
| Error handling | Use base error class for catch-all |
|
|
42
|
+
| Mutable closures | **Banned** |
|
|
43
|
+
| Immutable closures | OK (e.g. currying over cached config) |
|
|
44
|
+
| Method shorthand in objects | Allowed (`{ process() {} }`) |
|
|
45
|
+
| Variable bindings | Prefer `const`; `let` only when reassignment needed |
|
|
46
|
+
| Export | Define first, `export default` at bottom |
|
|
47
|
+
| Import paths | Always include `.js` extension |
|
|
48
|
+
| Multiple things from one file | Split into separate files |
|
|
49
|
+
| Destructured object params | Default empty object: `{ ... } = {}` |
|
|
50
|
+
| Boolean functions | Prefix with `is`/`has`/`can`/`should` |
|
|
51
|
+
| Return values (objects/arrays) | Deep freeze (clone+freeze for external, freeze-in-place for own) |
|
|
52
|
+
|
|
53
|
+
### 1. Export Conventions
|
|
54
|
+
|
|
55
|
+
**CRITICAL**: All internal files use default-only exports with named-then-export pattern.
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// ✅ CORRECT - Named function, then export at bottom
|
|
59
|
+
function myFunction() { ... }
|
|
60
|
+
|
|
61
|
+
export default myFunction;
|
|
62
|
+
|
|
63
|
+
// ✅ CORRECT - Constants follow same pattern
|
|
64
|
+
const MY_CONSTANT = Symbol('description');
|
|
65
|
+
|
|
66
|
+
export default MY_CONSTANT;
|
|
67
|
+
|
|
68
|
+
// ❌ WRONG - Inline default export (poor tooling support)
|
|
69
|
+
export default function myFunction() { ... }
|
|
70
|
+
|
|
71
|
+
// ❌ WRONG - Named exports in internal files
|
|
72
|
+
export function myFunction() { ... }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**NO BARREL FILES**: Import directly from the source file. No internal `index.ts` re-exports.
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// ✅ CORRECT - Direct imports
|
|
79
|
+
import createConfig from './configuring/create.js';
|
|
80
|
+
import applyPreset from './configuring/apply-preset.js';
|
|
81
|
+
|
|
82
|
+
// ❌ WRONG - Barrel imports
|
|
83
|
+
import { createConfig, applyPreset } from './configuring/index.js';
|
|
84
|
+
|
|
85
|
+
// ✅ EXCEPTION - Public API only
|
|
86
|
+
import { doThing } from '@study-lenses/this-package';
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Rationale**:
|
|
90
|
+
|
|
91
|
+
- Explicit dependency graph (no indirection)
|
|
92
|
+
- Better tree-shaking
|
|
93
|
+
- No circular dependency traps
|
|
94
|
+
- IDE "go to definition" works directly
|
|
95
|
+
- Tooling gets function names from declarations
|
|
96
|
+
- Simpler mental model for contributors
|
|
97
|
+
|
|
98
|
+
### 2. Type Location Convention
|
|
99
|
+
|
|
100
|
+
Types live **with their module**, not in a centralized location.
|
|
101
|
+
|
|
102
|
+
| Location | Purpose |
|
|
103
|
+
| ----------------------- | ------------------------------------- |
|
|
104
|
+
| `src/<module>/types.ts` | Types for that module |
|
|
105
|
+
| `src/index.ts` | Re-exports consumer-facing types flat |
|
|
106
|
+
|
|
107
|
+
**Rules:**
|
|
108
|
+
|
|
109
|
+
1. Each module has its own `types.ts` (if needed)
|
|
110
|
+
2. Types stay with the code they document (transparency, portability)
|
|
111
|
+
3. Internal code imports directly from module's `types.ts`
|
|
112
|
+
4. `/src/index.ts` re-exports consumer-facing types (flat, no namespace)
|
|
113
|
+
|
|
114
|
+
**Rationale:**
|
|
115
|
+
|
|
116
|
+
- Transparency: Types are discoverable where they're used
|
|
117
|
+
- Portability: Renaming/moving folders doesn't break unrelated code
|
|
118
|
+
- Consistency: Parallels `/src/index.ts` as entry point for code
|
|
119
|
+
|
|
120
|
+
### 2.5. When `any` is OK
|
|
121
|
+
|
|
122
|
+
The `@typescript-eslint/no-explicit-any` rule is set to **warn** (not error) because `any` has
|
|
123
|
+
legitimate uses. All `any` usage MUST be justified during code review.
|
|
124
|
+
|
|
125
|
+
**Acceptable uses:**
|
|
126
|
+
|
|
127
|
+
1. **Dynamic runtime values** — data parsed from JSON, user input, or eval results
|
|
128
|
+
2. **Untyped library boundaries** — wrapping third-party libraries without type definitions
|
|
129
|
+
3. **Generic utilities** — functions operating on arbitrary structures
|
|
130
|
+
4. **Test fixtures** — intentionally breaking types to test error handling
|
|
131
|
+
5. **Stub implementations** — temporary mock data during TDD cycles
|
|
132
|
+
|
|
133
|
+
**Unacceptable uses:**
|
|
134
|
+
|
|
135
|
+
- Business logic with known types (use proper interfaces)
|
|
136
|
+
- Public API parameters (force callers to use correct types)
|
|
137
|
+
- Return values from internal functions (be explicit)
|
|
138
|
+
- Lazy typing ("I don't know the type so I'll use `any`")
|
|
139
|
+
|
|
140
|
+
**Code review requirement:** Every `any` type must have a comment explaining WHY it's necessary.
|
|
141
|
+
|
|
142
|
+
### 2.6. Using `eslint-disable` Comments
|
|
143
|
+
|
|
144
|
+
`eslint-disable` comments are a code review tool, NOT a development shortcut.
|
|
145
|
+
|
|
146
|
+
**Rules:**
|
|
147
|
+
|
|
148
|
+
1. **Never add `eslint-disable` in initial implementation** — fix the violation instead
|
|
149
|
+
2. **Only add during code review** — after discussing with reviewer
|
|
150
|
+
3. **Require justification comment** — explain WHY the rule doesn't apply
|
|
151
|
+
|
|
152
|
+
**Format:**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// eslint-disable-next-line rule-name -- Justification for disabling
|
|
156
|
+
const problematicCode = ...;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3. Object-Threading Pattern
|
|
160
|
+
|
|
161
|
+
Functions accept and return objects with predetermined keys:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// Input object with known keys
|
|
165
|
+
const input = { code: 'let x = 5', config: expandedConfig };
|
|
166
|
+
|
|
167
|
+
// Function adds new keys while preserving input
|
|
168
|
+
const output = process(input);
|
|
169
|
+
// Returns: { code, config, result }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Benefits**:
|
|
173
|
+
|
|
174
|
+
- Explicit data flow
|
|
175
|
+
- Easy debugging (inspect objects between stages)
|
|
176
|
+
- Composable pipeline stages
|
|
177
|
+
- No hidden state
|
|
178
|
+
|
|
179
|
+
### 4. Pure Functional Approach
|
|
180
|
+
|
|
181
|
+
- No mutations — always return new objects
|
|
182
|
+
- No side effects in core functions
|
|
183
|
+
- State passed explicitly through parameters
|
|
184
|
+
- Deterministic behavior for testing
|
|
185
|
+
- Prefer `const`; use `let` only when reassignment is genuinely needed (loop counters,
|
|
186
|
+
accumulators)
|
|
187
|
+
|
|
188
|
+
### 5. Error Handling Strategy
|
|
189
|
+
|
|
190
|
+
**Error Classes**: Library errors extend a base error class. This enables catch-all handling
|
|
191
|
+
while preserving specific error discrimination via `instanceof`.
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Catch-all for any library error
|
|
195
|
+
try {
|
|
196
|
+
const result = await doThing(input);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof BaseError) {
|
|
199
|
+
showUserError(error.message); // Library error - handle gracefully
|
|
200
|
+
} else {
|
|
201
|
+
throw error; // Not ours - propagate
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**General Patterns**:
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// Graceful degradation for config errors
|
|
210
|
+
if (invalidConfig) {
|
|
211
|
+
console.warn('Invalid config value, using default');
|
|
212
|
+
return defaultValue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Fail fast for critical errors (use specific error classes)
|
|
216
|
+
if (!input) {
|
|
217
|
+
throw new ArgumentInvalidError('input', 'Input is required');
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 6. Function Conventions
|
|
222
|
+
|
|
223
|
+
Use **named `function` declarations** by default. Arrow functions (`=>`) are
|
|
224
|
+
allowed only as short, single-expression forms with implicit return.
|
|
225
|
+
|
|
226
|
+
#### Arrow Functions: When They're Fine
|
|
227
|
+
|
|
228
|
+
Arrow functions are allowed **only** as anonymous inline callbacks when **all** of these hold:
|
|
229
|
+
|
|
230
|
+
1. **Single expression** with implicit return (no `{` body block)
|
|
231
|
+
2. **At a glance** — you can read it without slowing down
|
|
232
|
+
3. **Inline as a callback** — not assigned to a variable
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
// ✅ — trivial transforms and predicates, inline
|
|
236
|
+
users.map((user) => user.id);
|
|
237
|
+
items.filter((item) => item.enabled);
|
|
238
|
+
values.some((v) => v === null);
|
|
239
|
+
amounts.reduce((sum, n) => sum + n, 0);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// ❌ — assigned to a variable: use a named function declaration
|
|
244
|
+
const extractId = (user) => user.id;
|
|
245
|
+
|
|
246
|
+
// ❌ — has a body block: use a named function declaration
|
|
247
|
+
const process = (config) => {
|
|
248
|
+
const expanded = expandShorthand(config);
|
|
249
|
+
return applyPreset(expanded);
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Named `function` Declarations: Everything Else
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
// ✅ — named function declaration
|
|
257
|
+
function processConfig(config) {
|
|
258
|
+
const expanded = expandShorthand(config);
|
|
259
|
+
return applyPreset(expanded);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Callbacks Longer Than a Quick Expression
|
|
264
|
+
|
|
265
|
+
When a callback grows beyond a simple expression, **extract it** as a named `function`
|
|
266
|
+
declaration and pass the name into the chain.
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
// ✅ — extracted named functions, passed by name
|
|
270
|
+
const results = users.filter(isActiveAdmin).map(formatUserSummary);
|
|
271
|
+
|
|
272
|
+
function isActiveAdmin(user) {
|
|
273
|
+
return user.status === 'active' && user.role === 'admin' && !user.suspended;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatUserSummary(user) {
|
|
277
|
+
return {
|
|
278
|
+
id: user.id,
|
|
279
|
+
display: `${user.firstName} ${user.lastName}`,
|
|
280
|
+
since: user.createdAt.toISOString(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Why?**
|
|
286
|
+
|
|
287
|
+
- `users.filter(isActiveAdmin)` reads like English
|
|
288
|
+
- Named functions show in stack traces
|
|
289
|
+
- Extracted functions are independently testable
|
|
290
|
+
- Forces naming, which clarifies intent
|
|
291
|
+
|
|
292
|
+
#### Hoisting for Readability
|
|
293
|
+
|
|
294
|
+
Defining a `function` below where its name is first used is encouraged when it improves
|
|
295
|
+
readability — high-level flow at the top, implementation details below.
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
// ✅ — main flow reads top-down, details defined below
|
|
299
|
+
const pipeline = buildPipeline(config);
|
|
300
|
+
const result = executePipeline(pipeline, code);
|
|
301
|
+
return formatOutput(result);
|
|
302
|
+
|
|
303
|
+
function buildPipeline(config) { ... }
|
|
304
|
+
function executePipeline(pipeline, code) { ... }
|
|
305
|
+
function formatOutput(result) { ... }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 7. No `this` Keyword
|
|
309
|
+
|
|
310
|
+
This is a functional codebase. The `this` keyword is banned.
|
|
311
|
+
|
|
312
|
+
**Exception**: Low-level code may use `this` when interfacing with libraries that require it.
|
|
313
|
+
These modules should be clearly marked.
|
|
314
|
+
|
|
315
|
+
### 8. No Mutable Closures
|
|
316
|
+
|
|
317
|
+
Closures over **mutable** variables (`let`, reassigned bindings) are banned in core code.
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
// ✅ OK - closure over immutable values
|
|
321
|
+
function embodyWithClosedConfig({ code }) {
|
|
322
|
+
// cachedConfig was set once and never changes
|
|
323
|
+
return trace({ code, config: cachedConfig });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ❌ BANNED - closure over mutable state
|
|
327
|
+
function createCounter(initialCount = 0) {
|
|
328
|
+
let count = initialCount;
|
|
329
|
+
return {
|
|
330
|
+
increment() {
|
|
331
|
+
count++;
|
|
332
|
+
return count;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Exception**: Low-level code may use mutable closures when interfacing with libraries that
|
|
339
|
+
require stateful patterns. Same boundary as the `this` exception.
|
|
340
|
+
|
|
341
|
+
### 9. Method Shorthand, Default Empty Object, const
|
|
342
|
+
|
|
343
|
+
**Method shorthand**: Use method shorthand syntax in object literals.
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
// ✅ CORRECT
|
|
347
|
+
const pipeline = {
|
|
348
|
+
process() { ... },
|
|
349
|
+
validate() { ... },
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// ❌ AVOID
|
|
353
|
+
const pipeline = {
|
|
354
|
+
process: function process() { ... },
|
|
355
|
+
};
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Default empty object**: All functions that destructure object parameters should provide a
|
|
359
|
+
default empty object.
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
// ✅ CORRECT
|
|
363
|
+
function processConfig({ preset = 'detailed', variables = true } = {}) {}
|
|
364
|
+
|
|
365
|
+
// ❌ AVOID - no default
|
|
366
|
+
function processConfig({ preset = 'detailed', variables = true }) {}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Prefer `const`**: Use `let` only when reassignment is genuinely needed.
|
|
370
|
+
|
|
371
|
+
### 10. Naming
|
|
372
|
+
|
|
373
|
+
**Functions: verb first**
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
376
|
+
// ✅ CORRECT
|
|
377
|
+
function extractId(user) {}
|
|
378
|
+
function isActive(item) {}
|
|
379
|
+
function hasPermission(user, action) {}
|
|
380
|
+
function createConfig(options) {}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Predicates**: Boolean-returning functions start with `is`, `has`, `can`, `should`.
|
|
384
|
+
|
|
385
|
+
**Callbacks: describe the transform** (`extractId` not `mapUser`, `isEnabled` not `filterItem`).
|
|
386
|
+
|
|
387
|
+
### 11. Imports, Types, Comments
|
|
388
|
+
|
|
389
|
+
**Imports**: Always include `.js` extension. Group and order:
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
// 1. External dependencies (node_modules)
|
|
393
|
+
import { describe, it } from 'vitest';
|
|
394
|
+
|
|
395
|
+
// 2. Internal modules (relative paths)
|
|
396
|
+
import processConfig from './process-config.js';
|
|
397
|
+
import validateInput from '../helpers/validate-input.js';
|
|
398
|
+
|
|
399
|
+
// 3. Type imports (last)
|
|
400
|
+
import type { Config } from './types.js';
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Types**: Prefer `type` over `interface`. Each module can have a `types.ts` file.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// ✅ PREFERRED
|
|
407
|
+
type Config = {
|
|
408
|
+
preset: string;
|
|
409
|
+
variables: boolean;
|
|
410
|
+
};
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Comments**: JSDoc/TSDoc for public functions. Use `@remarks` for consumer-facing "why" context
|
|
414
|
+
that should appear in generated API documentation. Inline comments explain **why**, not what.
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
// ❌ WRONG - says what (obvious from code)
|
|
418
|
+
// Loop through users
|
|
419
|
+
for (const user of users) {
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ✅ CORRECT - says why (not obvious)
|
|
423
|
+
// Skip inactive users to avoid rate limiting on the API
|
|
424
|
+
for (const user of users.filter(isActive)) {
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### 12. Readability Patterns
|
|
429
|
+
|
|
430
|
+
These patterns shape how code reads, not just what it does. The goal: a reader should be
|
|
431
|
+
able to follow a function without holding the whole thing in their head.
|
|
432
|
+
|
|
433
|
+
#### Guard-first, happy-path-last
|
|
434
|
+
|
|
435
|
+
Screen out bad/edge cases with early returns at the top. The happy path stays visible and
|
|
436
|
+
uncluttered at the bottom. This also works with the linter: deep nesting triggers a
|
|
437
|
+
`cognitive-complexity` violation, early returns avoid it.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// ✅ — guards up top, happy path at the end
|
|
441
|
+
function isPlainObject(thing: unknown): thing is Record<string, unknown> {
|
|
442
|
+
if (typeof thing !== 'object') return false; // screen: primitives
|
|
443
|
+
if (thing === null) return false; // screen: null
|
|
444
|
+
if (Array.isArray(thing)) return false; // screen: arrays
|
|
445
|
+
|
|
446
|
+
const proto = Object.getPrototypeOf(thing); // happy path: one clear check
|
|
447
|
+
return proto === Object.prototype;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### Named intermediate variables
|
|
452
|
+
|
|
453
|
+
When a sub-expression has a clear identity, capture it in a `const`. Name the thing, then
|
|
454
|
+
use the name. Avoids repeating the same lookup expression (error-prone) and makes the intent
|
|
455
|
+
visible at both the declaration and the use site.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// ✅ — named at declaration; reader sees the type at a glance
|
|
459
|
+
const tracerModule = tracers[tracer];
|
|
460
|
+
if (!tracerModule) throw new TracerUnknownError(tracer, ...);
|
|
461
|
+
const options = tracerModule.optionsSchema ? prepareConfig(...) : {};
|
|
462
|
+
|
|
463
|
+
// ❌ — reader must parse tracers[tracer] twice; easy to introduce subtle bugs
|
|
464
|
+
if (!tracers[tracer]) throw new TracerUnknownError(tracer, ...);
|
|
465
|
+
const options = tracers[tracer].optionsSchema ? prepareConfig(...) : {};
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Real example: `src/api/trace.ts` lines 39–40.
|
|
469
|
+
|
|
470
|
+
#### Ternary: transparent value selection only
|
|
471
|
+
|
|
472
|
+
OK when both branches compute "the same kind of thing" — a variable name can capture the
|
|
473
|
+
identity regardless of which path executes. Not OK when branches do structurally different
|
|
474
|
+
things; use `if-else` for those.
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// ✅ — both branches produce a [key, value] pair (same shape)
|
|
478
|
+
const entry = condition ? [key, expandBoolean(value, schema)] : [key, value];
|
|
479
|
+
|
|
480
|
+
// ❌ — branches do different things; ternary hides the divergence
|
|
481
|
+
const result = condition ? executeSomething() : returnEarlyWithFallback();
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Real example: `src/configuring/expand-shorthand.ts` `.map()` callback.
|
|
485
|
+
|
|
486
|
+
#### Within-file helpers for readability; separate file for reuse
|
|
487
|
+
|
|
488
|
+
**Within-file helper** (file-private, possibly single-use): extract when the main function
|
|
489
|
+
reads more clearly after the extraction. The caller says WHAT without explaining HOW inline.
|
|
490
|
+
Single use is fine. Define below (hoisting) for subordinate helpers; above for substantial
|
|
491
|
+
ones.
|
|
492
|
+
|
|
493
|
+
**Separate file**: only when the logic is used in 2+ places.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// ✅ — shouldExpand() and expandBoolean() are single-use but they name the concepts
|
|
497
|
+
// expandShorthand() now reads like English prose
|
|
498
|
+
|
|
499
|
+
function expandShorthand(options, schema) {
|
|
500
|
+
...
|
|
501
|
+
return entries.map(([key, value]) =>
|
|
502
|
+
typeof value === 'boolean' && shouldExpand(schemaProperties[key])
|
|
503
|
+
? [key, expandBoolean(value, schemaProperties[key])]
|
|
504
|
+
: [key, value],
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Helpers defined below (hoisting) — details after the main function
|
|
509
|
+
function shouldExpand(fieldSchema) { ... }
|
|
510
|
+
function expandBoolean(value, fieldSchema) { ... }
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// ✅ — executeTrace() called from both embody() AND closure() → separate function justified
|
|
515
|
+
function embody(input = {}) {
|
|
516
|
+
...
|
|
517
|
+
if (allPresent) return executeTrace(tracer, code, config); // call site 1
|
|
518
|
+
return createClosure(...);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function createClosure(state) {
|
|
522
|
+
function closure(remaining = {}) {
|
|
523
|
+
...
|
|
524
|
+
if (allPresent) return executeTrace(tracer, code, config); // call site 2
|
|
525
|
+
return createClosure(...);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Real examples: `src/configuring/expand-shorthand.ts`, `src/api/embody.ts`.
|
|
531
|
+
|
|
532
|
+
#### Numbered step comments for multi-phase functions
|
|
533
|
+
|
|
534
|
+
When a function has distinct phases that aren't self-evident from the code, number them.
|
|
535
|
+
Makes long functions skimmable — a reader can jump to the step they care about. Write the
|
|
536
|
+
number and a short label; optionally add a key constraint in parens.
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// 1. Validate tracer type (sync)
|
|
540
|
+
if (typeof tracer !== 'string' ...) throw ...;
|
|
541
|
+
|
|
542
|
+
// 2. Check tracer exists (sync)
|
|
543
|
+
const tracerModule = tracers[tracer];
|
|
544
|
+
if (!tracerModule) throw ...;
|
|
545
|
+
|
|
546
|
+
// 3. Prepare config (sync)
|
|
547
|
+
const meta = prepareConfig(...);
|
|
548
|
+
|
|
549
|
+
// 4. Record (async) — returns steps directly
|
|
550
|
+
return tracerModule.record(code, { meta, options });
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Real example: `src/api/trace.ts` (8 numbered steps).
|
|
554
|
+
|
|
555
|
+
#### WHY comments for non-obvious JS semantics
|
|
556
|
+
|
|
557
|
+
When code relies on language mechanics that aren't universally known, add a short comment
|
|
558
|
+
explaining WHY this approach is required — not WHAT the code does (the code already shows
|
|
559
|
+
that).
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// typeof null === 'object' in JS — must explicitly exclude null after the typeof check
|
|
563
|
+
if (thing === null) return false;
|
|
564
|
+
|
|
565
|
+
// Object.getPrototypeOf(null) throws — the null check above is a prerequisite
|
|
566
|
+
const proto = Object.getPrototypeOf(thing);
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Candidates: prototype chain operations, `typeof null`, coercion edge cases, WeakMap/WeakSet
|
|
570
|
+
patterns, async ordering constraints.
|
|
571
|
+
|
|
572
|
+
#### Blank lines as paragraph breaks
|
|
573
|
+
|
|
574
|
+
Separate distinct phases of logic with a blank line. One blank line = end of one thought,
|
|
575
|
+
start of the next. Group related statements; don't break every line individually.
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// ✅ — guards form one paragraph; result forms another
|
|
579
|
+
if (typeof thing !== 'object') return false;
|
|
580
|
+
if (thing === null) return false;
|
|
581
|
+
if (Array.isArray(thing)) return false;
|
|
582
|
+
|
|
583
|
+
const proto = Object.getPrototypeOf(thing);
|
|
584
|
+
return proto === Object.prototype;
|
|
585
|
+
|
|
586
|
+
// ❌ — no visual structure; every line isolated
|
|
587
|
+
if (typeof thing !== 'object') return false;
|
|
588
|
+
|
|
589
|
+
if (thing === null) return false;
|
|
590
|
+
|
|
591
|
+
if (Array.isArray(thing)) return false;
|
|
592
|
+
|
|
593
|
+
const proto = Object.getPrototypeOf(thing);
|
|
594
|
+
|
|
595
|
+
return proto === Object.prototype;
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Linting connections
|
|
599
|
+
|
|
600
|
+
Some patterns are partially enforced; others are code-review only.
|
|
601
|
+
|
|
602
|
+
- **Guard-first** — `sonarjs/cognitive-complexity` (warn) penalizes deep nesting; early
|
|
603
|
+
returns keep the score down. `sonarjs/nested-control-flow` (error) flags nested loops
|
|
604
|
+
and conditions directly.
|
|
605
|
+
- **Named intermediates** — `prefer-const` (error) ensures named values stay immutable;
|
|
606
|
+
the discipline of naming is manual but the linter enforces the `const`.
|
|
607
|
+
- **Ternary** — `arrow-body-style: never` (error) requires implicit returns in arrow
|
|
608
|
+
callbacks, which signals "pure value calculation" — same intent as the ternary rule.
|
|
609
|
+
- **Within-file helpers** — `sonarjs/cognitive-complexity` flags overly long functions
|
|
610
|
+
(extract to reduce); `sonarjs/no-identical-functions` (error) catches duplicate logic
|
|
611
|
+
across call sites.
|
|
612
|
+
- **WHY comments** — `spaced-comment` (error) enforces comment formatting; comment
|
|
613
|
+
_content_ quality is a code-review concern only.
|
|
614
|
+
- **Blank lines** — Prettier handles structural whitespace; semantic phase breaks
|
|
615
|
+
(paragraph rhythm) are a manual judgment call.
|
|
616
|
+
|
|
617
|
+
### 13. Deep Freeze Return Values
|
|
618
|
+
|
|
619
|
+
Objects and arrays returned from functions must be deep frozen before leaving the function
|
|
620
|
+
boundary. These libraries are consumed by LLMs — freezing catches accidental mutation at the
|
|
621
|
+
return boundary rather than producing silent bugs downstream.
|
|
622
|
+
|
|
623
|
+
**Two operations, one ownership rule:**
|
|
624
|
+
|
|
625
|
+
| Operation | When to use | Behavior |
|
|
626
|
+
| --------------- | ------------------------------------------------ | ----------------------------- |
|
|
627
|
+
| Clone + freeze | Objects we don't own (caller-provided, external) | Clones first, returns new ref |
|
|
628
|
+
| Freeze in place | Objects we just built (fresh results, wrappers) | Freezes in place, same ref |
|
|
629
|
+
|
|
630
|
+
The distinction is about **ownership**: if you just constructed the object (e.g., a spread
|
|
631
|
+
result, a new config wrapper), freeze it in place — there's no reason to clone something
|
|
632
|
+
nobody else has a reference to. If the object came from outside (a parameter, imported data),
|
|
633
|
+
clone-then-freeze to avoid mutating the caller's data.
|
|
634
|
+
|
|
635
|
+
Use a deep-freeze utility from your package's dependencies for both operations.
|
|
636
|
+
|
|
637
|
+
**What to freeze:**
|
|
638
|
+
|
|
639
|
+
- All function return values that are objects or arrays
|
|
640
|
+
- Config objects and resolved options
|
|
641
|
+
- Constants and shared defaults
|
|
642
|
+
- Module-level data structures
|
|
643
|
+
|
|
644
|
+
**Exception:** Performance-critical hot paths where profiling shows freeze overhead is
|
|
645
|
+
unacceptable. Document with a `// perf: skip freeze — [reason]` comment.
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// ✅ — freshly built result, freeze in place
|
|
649
|
+
function createResult(steps, meta) {
|
|
650
|
+
const result = { ok: true, steps, meta };
|
|
651
|
+
return freezeInPlace(result); // your deep-freeze utility
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ✅ — caller-provided config, clone + freeze
|
|
655
|
+
function resolveConfig(userConfig) {
|
|
656
|
+
const resolved = merge(defaults, userConfig); // your deep-merge utility
|
|
657
|
+
return cloneAndFreeze(resolved); // your deep-freeze utility
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ❌ — returned object is mutable; LLM consumer can accidentally mutate
|
|
661
|
+
function createResult(steps, meta) {
|
|
662
|
+
return { ok: true, steps, meta };
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
## Directory Structure
|
|
667
|
+
|
|
668
|
+
**Convention**: One concept per file, named after its default export. `kebab-case`
|
|
669
|
+
for all files and directories. Match filename to export: `createConfig` → `create.ts`.
|
|
670
|
+
|
|
671
|
+
### Directory Documentation Convention
|
|
672
|
+
|
|
673
|
+
Every source directory under `src/` has a `README.md`. Directories with non-obvious
|
|
674
|
+
architecture or key design decisions also have a `DOCS.md`:
|
|
675
|
+
|
|
676
|
+
| Content | Where | Audience |
|
|
677
|
+
| --------------------------------------------------- | -------------------------- | ------------ |
|
|
678
|
+
| API reference (signatures, params, returns, throws) | JSDoc/TSDoc → `docs/` | Consumers |
|
|
679
|
+
| Consumer-facing "why" context | TSDoc `@remarks` → `docs/` | Consumers |
|
|
680
|
+
| What this module does, how to navigate it | `README.md` per directory | Contributors |
|
|
681
|
+
| Architecture, design decisions, why this approach | `DOCS.md` per directory | Developers |
|
|
682
|
+
| Non-obvious implementation detail | Inline `//` comment | Code readers |
|
|
683
|
+
|
|
684
|
+
**Rules:**
|
|
685
|
+
|
|
686
|
+
- Every directory has a `README.md` (brief — 5–15 lines is typical)
|
|
687
|
+
- Directories with non-obvious architecture or key design decisions also have a `DOCS.md`
|
|
688
|
+
- `DOCS.md` captures the "why" — tradeoffs, alternatives considered, constraints. Keep it short.
|
|
689
|
+
It is NOT an API reference — JSDoc handles that. Hand-maintained: fix it or delete it if it goes stale.
|
|
690
|
+
- Tests directories (`tests/`) are exempt from needing `README.md`
|
|
691
|
+
- `README.md` is cross-referenced: parent links down, child links up, siblings link to each other
|
|
692
|
+
- Public functions have JSDoc/TSDoc in source; TypeDoc generates `docs/` (gitignored, CI-only)
|
|
693
|
+
|
|
694
|
+
**Public function documentation:**
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
/**
|
|
698
|
+
* Creates a config from user input, applying defaults and preset expansion.
|
|
699
|
+
*
|
|
700
|
+
* @param options - User-provided config options
|
|
701
|
+
* @returns Fully resolved config with all defaults applied
|
|
702
|
+
* @throws {ArgumentInvalidError} If options is not an object
|
|
703
|
+
*
|
|
704
|
+
* @remarks
|
|
705
|
+
* The config goes through three stages: shorthand expansion (booleans to full
|
|
706
|
+
* objects), default-filling (missing keys get defaults), and schema validation.
|
|
707
|
+
* The `@remarks` tag is for consumer-facing "why" context that belongs in the
|
|
708
|
+
* generated API docs alongside the signature.
|
|
709
|
+
*/
|
|
710
|
+
function createConfig(options: UserOptions = {}): ResolvedConfig { ... }
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Test Organization
|
|
714
|
+
|
|
715
|
+
Unit tests live in a `tests/` subdirectory at the same level as the files they test:
|
|
716
|
+
|
|
717
|
+
```text
|
|
718
|
+
src/
|
|
719
|
+
module/
|
|
720
|
+
tests/
|
|
721
|
+
feature.test.ts
|
|
722
|
+
feature.ts
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- Directory name: `tests/` (plural, always)
|
|
726
|
+
- File suffix: `.test.ts` (never `.spec.ts`)
|
|
727
|
+
- Root `/tests/` directory: integration test fixtures (not unit tests)
|
|
728
|
+
|
|
729
|
+
## Development Workflow
|
|
730
|
+
|
|
731
|
+
### 1. Setup
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
npm install
|
|
735
|
+
npm run test:watch # Run tests in watch mode
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### 2. Making Changes
|
|
739
|
+
|
|
740
|
+
1. Create feature branch
|
|
741
|
+
2. Update relevant default export function
|
|
742
|
+
3. Add/update tests
|
|
743
|
+
4. Update `README.md` in affected directories
|
|
744
|
+
5. Run quality checks:
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
npm run validate # lint + type-check + test
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### 3. Conventions Checklist
|
|
751
|
+
|
|
752
|
+
- [ ] Named function/const, then `export default` at bottom
|
|
753
|
+
- [ ] Direct imports from source files (no barrels), always with `.js` extension
|
|
754
|
+
- [ ] Named `function` declarations (arrows only for inline callbacks)
|
|
755
|
+
- [ ] No `this` keyword, no mutable closures
|
|
756
|
+
- [ ] Default empty object `= {}` on all destructured parameters
|
|
757
|
+
- [ ] Verb-first naming; predicates prefixed with `is`/`has`/`can`/`should`
|
|
758
|
+
- [ ] Types added to module's `types.ts`; prefer `type` over `interface`
|
|
759
|
+
- [ ] Tests in `tests/` subdirectory (not alongside source files), `.test.ts` suffix
|
|
760
|
+
- [ ] Tests cover happy path and edge cases
|
|
761
|
+
- [ ] No mutations of input data
|
|
762
|
+
- [ ] Returned objects/arrays are deep frozen (clone-then-freeze for external, freeze-in-place for own)
|
|
763
|
+
- [ ] Errors handled gracefully
|
|
764
|
+
- [ ] `README.md` exists in every modified directory
|
|
765
|
+
- [ ] JSDoc/TSDoc on public functions; `@remarks` for consumer-facing "why"
|
|
766
|
+
|
|
767
|
+
## Testing Strategy
|
|
768
|
+
|
|
769
|
+
### Test Organization Convention
|
|
770
|
+
|
|
771
|
+
All unit tests live in a `tests/` subdirectory co-located with the source they test.
|
|
772
|
+
|
|
773
|
+
### Unit Tests
|
|
774
|
+
|
|
775
|
+
Each exported function has a dedicated test file in the nearest `tests/` subdirectory:
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
import { expect, test } from 'vitest';
|
|
779
|
+
|
|
780
|
+
import parseJSON from '../parse-json.js';
|
|
781
|
+
|
|
782
|
+
test('parses valid JSON string', () => {
|
|
783
|
+
const result = parseJSON('{"a":1}');
|
|
784
|
+
expect(result).toEqual({ a: 1 });
|
|
785
|
+
});
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Testing Conventions
|
|
789
|
+
|
|
790
|
+
#### Test Naming
|
|
791
|
+
|
|
792
|
+
Use direct description. Implicit arrows (`→`) for compactness when input/output is clear.
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
// Standard — describes what happens
|
|
796
|
+
it('returns expanded config with all defaults', () => {...});
|
|
797
|
+
|
|
798
|
+
// Compact with arrow — input → output
|
|
799
|
+
it('string input → parsed object', () => {...});
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
#### Describe Block Structure
|
|
803
|
+
|
|
804
|
+
Top-level `describe` = function name. Nest freely for clarity.
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
describe('createConfig', () => {
|
|
808
|
+
describe('preset application', () => {
|
|
809
|
+
describe('overview preset', () => {
|
|
810
|
+
it('sets variables.read to false', () => {...});
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
describe('boolean shorthand expansion', () => {
|
|
815
|
+
describe('happy path', () => {...});
|
|
816
|
+
describe('edge cases', () => {...});
|
|
817
|
+
describe('errors', () => {...});
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
#### Test Ordering
|
|
823
|
+
|
|
824
|
+
Within each describe block: **feature/behavior → happy path → edge cases → errors → performance**
|
|
825
|
+
|
|
826
|
+
#### One Assertion Per Test
|
|
827
|
+
|
|
828
|
+
Use nested `describe` blocks instead of multiple assertions in one `it`.
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// ❌ WRONG — multiple assertions hide which failed
|
|
832
|
+
it('returns complete config', () => {
|
|
833
|
+
expect(result.preset).toBe('detailed');
|
|
834
|
+
expect(result.variables).toBe(true);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// ✅ CORRECT — one assertion, grouped by describe
|
|
838
|
+
describe('returns complete config', () => {
|
|
839
|
+
it('preset = "detailed"', () => {
|
|
840
|
+
expect(result.preset).toBe('detailed');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('variables = true', () => {
|
|
844
|
+
expect(result.variables).toBe(true);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
#### Error Testing
|
|
850
|
+
|
|
851
|
+
Always use `.toThrow()`. Never use try-catch in tests.
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
// ✅ Basic
|
|
855
|
+
it('throws on invalid input', () => {
|
|
856
|
+
expect(() => parseJSON('{bad}')).toThrow();
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// ✅ With message substring
|
|
860
|
+
it('error mentions function name', () => {
|
|
861
|
+
expect(() => processConfig()).toThrow('processConfig');
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
#### Test Data
|
|
866
|
+
|
|
867
|
+
Inline only. No shared fixtures. Each test is self-contained and independently understandable.
|
|
868
|
+
|
|
869
|
+
#### Minimal Logic in Tests
|
|
870
|
+
|
|
871
|
+
Tests should contain only the function being tested and bare minimum data setup (inline).
|
|
872
|
+
No `if`, no loops, no try-catch. For multiple values, use `it.each`:
|
|
873
|
+
|
|
874
|
+
```typescript
|
|
875
|
+
// ✅ CORRECT
|
|
876
|
+
it.each([
|
|
877
|
+
[false, false],
|
|
878
|
+
[0, false],
|
|
879
|
+
['', false],
|
|
880
|
+
[null, false],
|
|
881
|
+
])('%p → Boolean coercion = %p', (value, expected) => {
|
|
882
|
+
expect(Boolean(value)).toBe(expected);
|
|
883
|
+
});
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
#### No Comments in Tests
|
|
887
|
+
|
|
888
|
+
Test names and describe blocks are executable documentation.
|
|
889
|
+
|
|
890
|
+
#### Complete Example
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
import { describe, expect, it } from 'vitest';
|
|
894
|
+
|
|
895
|
+
import parseJSON from '../parse-json.js';
|
|
896
|
+
|
|
897
|
+
describe('parseJSON', () => {
|
|
898
|
+
describe('valid JSON string', () => {
|
|
899
|
+
describe('happy path', () => {
|
|
900
|
+
it('object string → parsed object', () => {
|
|
901
|
+
expect(parseJSON('{"a":1}')).toEqual({ a: 1 });
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
describe('edge cases', () => {
|
|
906
|
+
it('empty object string → empty object', () => {
|
|
907
|
+
expect(parseJSON('{}')).toEqual({});
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
describe('invalid input', () => {
|
|
913
|
+
describe('errors', () => {
|
|
914
|
+
it('malformed JSON → throws', () => {
|
|
915
|
+
expect(() => parseJSON('{bad}')).toThrow();
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
## Incremental Development Workflow
|
|
923
|
+
|
|
924
|
+
All development uses TDD with atomic increments. One unit test = one increment of work.
|
|
925
|
+
|
|
926
|
+
### Phase 0: Documentation Specification (before any code)
|
|
927
|
+
|
|
928
|
+
Documentation-driven development ensures clarity BEFORE code exists.
|
|
929
|
+
|
|
930
|
+
**0.1. Update README.md** — What does this module do? Where does it fit?
|
|
931
|
+
|
|
932
|
+
**0.2. Adversarial Design Challenge (AR-1)** — Spawn a separate reviewer agent to
|
|
933
|
+
challenge the README spec before types lock the contract. See AGENTS.md §
|
|
934
|
+
Adversarial Review Protocol for prompt structure and verdict definitions.
|
|
935
|
+
|
|
936
|
+
**0.3. Update types.ts** — Type signatures are executable documentation
|
|
937
|
+
|
|
938
|
+
- Update type definitions to reflect the new contract (incorporating AR-1 feedback)
|
|
939
|
+
- Type errors after this step become the TODO list for implementation
|
|
940
|
+
|
|
941
|
+
**0.4. Review & Resolve** — Confirm understanding before writing code
|
|
942
|
+
|
|
943
|
+
### Phase 1: TDD Implementation
|
|
944
|
+
|
|
945
|
+
For each behavioral increment:
|
|
946
|
+
|
|
947
|
+
1. **JSDoc/TSDoc** — document the behavioral contract (with `@remarks` for consumer-facing "why")
|
|
948
|
+
2. **Stub function** — create function with stub body
|
|
949
|
+
3. **Placeholder types** — `any`/`unknown` to unblock; tighten later
|
|
950
|
+
4. **Lint checkpoint 1** — `npm run lint <new-file>`. Fix violations.
|
|
951
|
+
5. **Unit test** — write ONE failing test for the behavior
|
|
952
|
+
5b. **Adversarial Test Challenge (AR-2)** — Spawn a separate reviewer agent to
|
|
953
|
+
challenge test strategy. See AGENTS.md § AR-2 for focus areas.
|
|
954
|
+
6. **Lint checkpoint 2** — `npm run lint <test-file>`. Fix violations.
|
|
955
|
+
7. **Implement** — minimal code to pass the test (Red → Green)
|
|
956
|
+
8. **Lint checkpoint 3** — `npm run lint <impl-file>`. Fix violations.
|
|
957
|
+
9. **Refactor** — clean up while tests stay green
|
|
958
|
+
10. **Lint checkpoint 4** — final lint on modified files. Should be clean.
|
|
959
|
+
11. **Update types** — finalize based on actual implementation
|
|
960
|
+
12. **Self-review** — simplest solution? only what requested? junior-maintainable?
|
|
961
|
+
12b. **Adversarial Implementation Audit (AR-3)** — Spawn a separate reviewer agent
|
|
962
|
+
to audit the implementation. See AGENTS.md § AR-3 for focus areas.
|
|
963
|
+
13. **Quality checks** — `npm test && npm run lint && npm run type-check`
|
|
964
|
+
14. **Verify docs match implementation** — update README.md if behavior changed during TDD
|
|
965
|
+
15. **Atomic commit** — one behavior per commit
|
|
966
|
+
|
|
967
|
+
Use linter feedback as refactoring guide:
|
|
968
|
+
|
|
969
|
+
- `cognitive-complexity` error? Break into smaller functions
|
|
970
|
+
- `no-duplicate-string`? Extract constant
|
|
971
|
+
|
|
972
|
+
### Phase 2: Pre-Merge Review
|
|
973
|
+
|
|
974
|
+
After all increments are complete, before prompting the human to commit:
|
|
975
|
+
|
|
976
|
+
1. **Run full quality checks** — `npm test && npm run lint && npm run type-check`
|
|
977
|
+
2. **Adversarial Pre-Merge Review (AR-4)** — Spawn a separate reviewer agent to
|
|
978
|
+
review the full changeset. See AGENTS.md § AR-4 for focus areas. Provide the
|
|
979
|
+
full diff, modified files list, and the original task description.
|
|
980
|
+
3. **Address PAUSE/CONSIDER items** — resolve concerns per AGENTS.md § Resolution Rules
|
|
981
|
+
4. **Prompt human for atomic commit** — ready to commit only after AR-4 clears
|
|
982
|
+
|
|
983
|
+
### Session Handoff
|
|
984
|
+
|
|
985
|
+
Before ending a work session:
|
|
986
|
+
|
|
987
|
+
1. Update plan file with current state, what's done, what's left
|
|
988
|
+
2. Commit all completed increments
|
|
989
|
+
3. Note any blockers or open questions
|
|
990
|
+
|
|
991
|
+
### Atomic Commits
|
|
992
|
+
|
|
993
|
+
Each passing TDD cycle = one atomic commit:
|
|
994
|
+
|
|
995
|
+
- One behavior per commit
|
|
996
|
+
- Descriptive message: `add: createConfig expands boolean shorthand`
|
|
997
|
+
- Feature branch for planned work batches
|
|
998
|
+
|
|
999
|
+
### What NOT to Do
|
|
1000
|
+
|
|
1001
|
+
- No implementing multiple behaviors before testing
|
|
1002
|
+
- No skipping the refactor step
|
|
1003
|
+
- No skipping doc updates ("I'll do it at the end")
|
|
1004
|
+
- Each edit should do exactly one thing
|
|
1005
|
+
- **No full implementations in plans** — plans describe BEHAVIOR and INTENT, not code
|
|
1006
|
+
|
|
1007
|
+
## Linting Conventions
|
|
1008
|
+
|
|
1009
|
+
This codebase uses a three-tool pipeline for code quality:
|
|
1010
|
+
|
|
1011
|
+
- **ESLint** — enforces logic patterns and code style
|
|
1012
|
+
- **Prettier** — handles formatting (spaces, quotes, line length)
|
|
1013
|
+
- **TypeScript** — validates types via `tsc` compiler
|
|
1014
|
+
|
|
1015
|
+
### Running the Tools
|
|
1016
|
+
|
|
1017
|
+
```bash
|
|
1018
|
+
# Check for violations
|
|
1019
|
+
npm run lint # ESLint
|
|
1020
|
+
npm run format:check # Prettier
|
|
1021
|
+
npm run type-check # TypeScript
|
|
1022
|
+
|
|
1023
|
+
# Auto-fix what's fixable
|
|
1024
|
+
npm run lint:fix # ESLint auto-fix
|
|
1025
|
+
npm run format # Prettier auto-format
|
|
1026
|
+
|
|
1027
|
+
# Run all checks at once
|
|
1028
|
+
npm run validate # lint + type-check + test
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### Pre-commit Hooks
|
|
1032
|
+
|
|
1033
|
+
Husky + lint-staged run automatically before each commit:
|
|
1034
|
+
|
|
1035
|
+
- `npm run lint:fix` on staged `.ts`/`.js` files
|
|
1036
|
+
- `npm run format` on staged `.ts`/`.js`/`.json`/`.md`/`.yml`/`.yaml` files
|
|
1037
|
+
|
|
1038
|
+
Most violations get fixed automatically before you even see them.
|
|
1039
|
+
|
|
1040
|
+
### Enforced Conventions
|
|
1041
|
+
|
|
1042
|
+
See [eslint.config.js](./eslint.config.js) for full configuration.
|
|
1043
|
+
|
|
1044
|
+
#### Functional Programming Core
|
|
1045
|
+
|
|
1046
|
+
- No `this` keyword (use closures over parameters)
|
|
1047
|
+
- No classes (use factory functions)
|
|
1048
|
+
- No parameter reassignment (create new bindings)
|
|
1049
|
+
- Immutable data encouraged (warn on mutations)
|
|
1050
|
+
|
|
1051
|
+
#### Functions and Naming
|
|
1052
|
+
|
|
1053
|
+
- All functions must have names (`func-names: error`)
|
|
1054
|
+
- Arrow functions must use implicit returns — no body blocks (`arrow-body-style: never`)
|
|
1055
|
+
- `for-of` loops for side effects, `.map()`/`.filter()` for transformations
|
|
1056
|
+
|
|
1057
|
+
#### Imports and Exports
|
|
1058
|
+
|
|
1059
|
+
- Always include `.js` extension in imports
|
|
1060
|
+
- No named exports (except `src/index.ts` and `types.ts`)
|
|
1061
|
+
- Imports ordered: builtin → external → internal, alphabetized within groups
|
|
1062
|
+
|
|
1063
|
+
#### Style
|
|
1064
|
+
|
|
1065
|
+
- `kebab-case` filenames (`unicorn/filename-case`)
|
|
1066
|
+
- `const` by default; `let` only when reassigned
|
|
1067
|
+
- Template literals for string concatenation (`prefer-template`)
|
|
1068
|
+
- `type` over `interface` (`@typescript-eslint/consistent-type-definitions`)
|
|
1069
|
+
|
|
1070
|
+
### TypeScript Strict Mode
|
|
1071
|
+
|
|
1072
|
+
All TypeScript strict checks are enabled. Run `npm run type-check` to verify.
|
|
1073
|
+
|
|
1074
|
+
### Manual Review Conventions
|
|
1075
|
+
|
|
1076
|
+
These conventions can't be automated and must be checked during code review:
|
|
1077
|
+
|
|
1078
|
+
- Default empty object for destructured parameters
|
|
1079
|
+
- Verb-first function naming
|
|
1080
|
+
- One concept per file
|
|
1081
|
+
- Comments explain "why" not "what"
|
|
1082
|
+
- No mutable closures
|
|
1083
|
+
- `README.md` updated in every modified directory
|
|
1084
|
+
|
|
1085
|
+
### Teaching Moments for Linting Errors
|
|
1086
|
+
|
|
1087
|
+
When linting errors occur, treat them as teaching opportunities — explain WHAT and WHY, not just
|
|
1088
|
+
how to fix.
|
|
1089
|
+
|
|
1090
|
+
| Rule | Concept to Teach |
|
|
1091
|
+
| -------------------------------- | ------------------------------------------------------------------------------- |
|
|
1092
|
+
| `unicorn/no-array-for-each` | Imperative vs functional: use `for-of` for side effects, methods for transforms |
|
|
1093
|
+
| `prefer-template` | Keep `+` for math only. Template literals prevent type coercion bugs. |
|
|
1094
|
+
| `arrow-body-style` | Implicit returns signal "pure transform"; braces signal "does more." |
|
|
1095
|
+
| `func-names` | Named functions improve stack traces and enable hoisting. |
|
|
1096
|
+
| `functional/no-this-expressions` | `this` binding changes based on call-site. Closures are explicit. |
|
|
1097
|
+
| `sonarjs/cognitive-complexity` | Too many nested conditions/loops. Break into smaller named functions. |
|
|
1098
|
+
| `sonarjs/no-duplicate-string` | Magic strings → named constants for searchability and refactoring. |
|
|
1099
|
+
|
|
1100
|
+
## Module Boundaries
|
|
1101
|
+
|
|
1102
|
+
Import boundaries are enforced via `eslint-plugin-boundaries`. This catches architectural
|
|
1103
|
+
violations at lint time.
|
|
1104
|
+
|
|
1105
|
+
### Template: Single Layer (`src`)
|
|
1106
|
+
|
|
1107
|
+
The template ships with one layer: all source files under `src/**` can import from each
|
|
1108
|
+
other. As your package grows, add more specific layers to enforce architectural boundaries.
|
|
1109
|
+
|
|
1110
|
+
```javascript
|
|
1111
|
+
// eslint.config.js — current template setup
|
|
1112
|
+
'boundaries/elements': [
|
|
1113
|
+
{ type: 'src', pattern: 'src/**', mode: 'file' },
|
|
1114
|
+
],
|
|
1115
|
+
'boundaries/element-types': [
|
|
1116
|
+
'error',
|
|
1117
|
+
{
|
|
1118
|
+
default: 'disallow',
|
|
1119
|
+
rules: [
|
|
1120
|
+
{ from: 'src', allow: ['src'] },
|
|
1121
|
+
],
|
|
1122
|
+
},
|
|
1123
|
+
],
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Expanding for Your Package
|
|
1127
|
+
|
|
1128
|
+
When your package grows internal layers (e.g., `api/`, `configuring/`, `errors/`), add
|
|
1129
|
+
elements with more specific patterns (listed before the `src` catch-all) and matching
|
|
1130
|
+
`element-types` rules:
|
|
1131
|
+
|
|
1132
|
+
```javascript
|
|
1133
|
+
// Example: multi-layer package
|
|
1134
|
+
'boundaries/elements': [
|
|
1135
|
+
{ type: 'entry', pattern: 'src/index.ts', mode: 'file' },
|
|
1136
|
+
{ type: 'core', pattern: 'src/core/*', mode: 'file' },
|
|
1137
|
+
{ type: 'error', pattern: 'src/errors/*', mode: 'file' },
|
|
1138
|
+
],
|
|
1139
|
+
// In rules:
|
|
1140
|
+
'boundaries/element-types': ['error', {
|
|
1141
|
+
default: 'disallow',
|
|
1142
|
+
rules: [
|
|
1143
|
+
{ from: 'entry', allow: ['core'] },
|
|
1144
|
+
{ from: 'core', allow: ['error'] },
|
|
1145
|
+
{ from: 'error', allow: [] },
|
|
1146
|
+
],
|
|
1147
|
+
}],
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
More specific patterns are listed first so they match before the broader catch-all.
|
|
1151
|
+
See embody's `eslint.config.js` for a full multi-layer example.
|
|
1152
|
+
|
|
1153
|
+
### Updating Boundaries
|
|
1154
|
+
|
|
1155
|
+
When the architecture evolves:
|
|
1156
|
+
|
|
1157
|
+
1. Update `boundaries/elements` patterns in `eslint.config.js`
|
|
1158
|
+
2. Update `boundaries/element-types` rules for new allowed imports
|
|
1159
|
+
3. Update this section of DEV.md
|
|
1160
|
+
4. Run `npm run lint` to verify no violations
|
|
1161
|
+
|
|
1162
|
+
## Code Quality Anti-Patterns
|
|
1163
|
+
|
|
1164
|
+
Common patterns to avoid:
|
|
1165
|
+
|
|
1166
|
+
| Anti-Pattern | Rule | Example Fix |
|
|
1167
|
+
| ------------------------- | ----------------------------- | ----------------------------------------------- |
|
|
1168
|
+
| **Over-engineering** | Helper used once? Inline it | `const x = getX(o)` → `const x = o.x` |
|
|
1169
|
+
| **Class addiction** | Prefer functions over classes | `class X` → `function createX()` |
|
|
1170
|
+
| **Future-proofing** | Don't add unused flexibility | `options = {}` with unused fields → direct impl |
|
|
1171
|
+
| **Defensive over-coding** | Validate at boundaries only | Remove internal re-validation |
|
|
1172
|
+
| **Verbose docs** | Name + types self-document? | Only document WHY or non-obvious contracts |
|
|
1173
|
+
|
|
1174
|
+
### Pre-Commit Checklist
|
|
1175
|
+
|
|
1176
|
+
Before proposing code, answer YES to ALL:
|
|
1177
|
+
|
|
1178
|
+
- [ ] **Simplest solution?** Not most "elegant" or "extensible"
|
|
1179
|
+
- [ ] **Only what requested?** No future-proofing, no "nice-to-haves"
|
|
1180
|
+
- [ ] **Helpers used >1x?** If used once, inline it
|
|
1181
|
+
- [ ] **Validate at boundaries only?** No re-validating internal calls
|
|
1182
|
+
- [ ] **Junior-maintainable?** Understandable without explanation
|
|
1183
|
+
|
|
1184
|
+
## VS Code Setup
|
|
1185
|
+
|
|
1186
|
+
The `.vscode/` directory provides workspace configuration for consistent development:
|
|
1187
|
+
|
|
1188
|
+
- **settings.json** — Format-on-save, ESLint auto-fix, word wrap at 100 chars, `.js` import
|
|
1189
|
+
extensions
|
|
1190
|
+
- **extensions.json** — Recommended extensions (ESLint, Prettier, EditorConfig, Vitest, spell
|
|
1191
|
+
checker, pretty TS errors)
|
|
1192
|
+
- **launch.json** — Debug configurations for tests and scripts
|
|
1193
|
+
|
|
1194
|
+
Open VS Code → install recommended extensions when prompted → editor is configured.
|
|
1195
|
+
|
|
1196
|
+
**Debug configurations:**
|
|
1197
|
+
|
|
1198
|
+
- **Debug Current Test File** — open a `.test.ts` file, press F5
|
|
1199
|
+
- **Debug All Tests** — run full suite with breakpoints
|
|
1200
|
+
- **Debug Current Script** — debug any `.ts`/`.js` file directly
|