@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.
@@ -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