qstd 0.3.57 → 0.3.58

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.
Files changed (2) hide show
  1. package/ATOMIC_MODULES.md +1026 -0
  2. package/package.json +3 -2
@@ -0,0 +1,1026 @@
1
+ # Atomic Modules
2
+
3
+ A compact module structure that keeps concerns clear and greppable. Each module is "atomic": small, focused, and split into predictable files.
4
+
5
+ **Core principle:** Simplicity through consistency. Complexity is managed by predictable patterns, not eliminated by clever abstractions.
6
+
7
+ > **Updating this document:** Blend new guidance into existing sections when possible. Only create new sections when content truly doesn't fit elsewhere. Enhance rather than expand.
8
+
9
+ ## Module anatomy
10
+
11
+ **Only `index.ts` is required.** All other files are optional and added as needed:
12
+
13
+ - `index.ts`: **[REQUIRED]** orchestrates and exports the module's public API
14
+ - `types.ts`: **[OPTIONAL]** shared types and interfaces
15
+ - `literals.ts`: **[OPTIONAL]** constants and tunables
16
+ - `fns.ts`: **[OPTIONAL]** pure helper functions/utilities
17
+ - `domain.ts`: **[OPTIONAL]** business/domain logic orchestrator; wraps specialized modules with instrumentation, error handling, and business rules
18
+ - **Specialized modules**: **[OPTIONAL]** `ast.ts`, `parser.ts`, `compiler.ts`, `tokenize.ts`, etc. - focused implementations for complex features
19
+
20
+ **For small modules:** Just put everything in `index.ts` (or `index.tsx` for components). The atomic structure is useful for complex features, not simple utilities.
21
+
22
+ **When to split into specialized modules:** When you have a complex feature (like a parser, compiler, or state machine) with 300+ lines of logic:
23
+
24
+ - Split into focused modules: `ast.ts`, `parse-blocks.ts`, `parse-inline.ts`, `tokenize.ts`, etc.
25
+ - Use `domain.ts` as the orchestrator (adds debugging, error handling, business rules)
26
+ - **Skip `fns.ts`** - it's redundant when you have specialized modules
27
+ - Each specialized module should be self-contained and focused on one responsibility
28
+
29
+ **When to use fns.ts:** Only when you have a moderate amount of logic (< 300 lines) that fits in one file. If your logic is complex enough to need multiple specialized modules, skip `fns.ts` and use `domain.ts` or `index.ts` as the orchestrator.
30
+
31
+ ### Deciding between fns.ts and domain.ts
32
+
33
+ When a module grows past ~300 lines, split into `fns.ts` and `domain.ts` based on cognitive load, not just line count. Categorize functions by their nature:
34
+
35
+ **fns.ts — Pure utilities with no business meaning:**
36
+
37
+ - Data transformations (reshape data structures)
38
+ - Formatters (convert to display strings)
39
+ - Simple accessors (extract values from structures)
40
+ - String/template builders
41
+
42
+ **domain.ts — Business logic with semantic meaning:**
43
+
44
+ - Math/algorithms that encode business rules (ELO calculations, scoring formulas)
45
+ - Generators that implement business constraints (matchup generation, judge assignment)
46
+ - State machines and workflow logic
47
+ - Functions that could have different implementations in different business contexts
48
+
49
+ **The litmus test:** If changing a function requires understanding the business domain (not just the data shape), it belongs in `domain.ts`. If it's a generic transformation that could work in any context, it belongs in `fns.ts`.
50
+
51
+ ```typescript
52
+ // fns.ts - Pure utilities, no business knowledge needed
53
+ export function formatEloWithDelta(current: number, baseline: number): string { ... }
54
+ export function buildScoringPrompt(query: string, drafts: Draft[]): string { ... }
55
+
56
+ // domain.ts - Business logic, requires domain understanding
57
+ export function updateElo(ratingA: number, ratingB: number, winner: Winner): [number, number] { ... }
58
+ export function assignJudges(modelIds: string[], matchups: Matchup[]): Assignment[] { ... }
59
+ ```
60
+
61
+ ### Keep things where they belong
62
+
63
+ **If a file already exists, use it.** Don't define types in `fns.ts` if `types.ts` exists. Don't put constants in `domain.ts` if `literals.ts` exists.
64
+
65
+ ```typescript
66
+ // ❌ BAD: Defining types in fns.ts when types.ts exists
67
+ // fns.ts
68
+ export interface InsertionPoint { ... } // Why is this here?
69
+ export function insertMediaBlock(...) { ... }
70
+
71
+ // ✅ GOOD: Types in types.ts, functions in fns.ts
72
+ // types.ts
73
+ export interface InsertionPoint { ... }
74
+
75
+ // fns.ts
76
+ import * as _t from "./types";
77
+ export function insertMediaBlock(content: _t.ContentBlock[], point: _t.InsertionPoint, ...) { ... }
78
+ ```
79
+
80
+ **The rule:** Once you've split a module into separate files, respect that structure. Each file has a purpose:
81
+
82
+ - Types/interfaces → `types.ts`
83
+ - Constants/config → `literals.ts`
84
+ - Pure functions → `fns.ts`
85
+ - Business logic → `domain.ts`
86
+
87
+ **Exceptions:**
88
+
89
+ - Small modules can keep everything in `index.ts` until they grow
90
+ - Inline function argument types are fine (see Function arguments section)
91
+
92
+ ### Component & Hook Exports
93
+
94
+ - **Components:** Export as default function statements.
95
+ ```tsx
96
+ export default function Recorder() { ... }
97
+ ```
98
+ - **Hooks:** Export as default function statements.
99
+ ```ts
100
+ export default function useAudioRecorder() { ... }
101
+ ```
102
+ - **Filenames:** Always lowercase kebab-case, matching the export where possible.
103
+ - Import example: `import Recorder from "components/molecules/recorder"`
104
+
105
+ ### Internal module imports
106
+
107
+ **Use namespace imports with underscore prefixes** for internal modules:
108
+
109
+ ```ts
110
+ // Standard module imports (always these prefixes)
111
+ import * as _d from "./domain";
112
+ import * as _l from "./literals";
113
+ import * as _t from "./types";
114
+ import * as _f from "./fns";
115
+
116
+ // Specialized module imports (use descriptive underscore prefix)
117
+ import * as _ast from "./ast";
118
+ import * as _parser from "./parser";
119
+ ```
120
+
121
+ **Why namespace imports with underscores:**
122
+
123
+ - **Greppable**: Easy to find all uses of a module (search for `_ast.`)
124
+ - **Refactor-safe**: Change internal structure without breaking imports
125
+ - **No naming conflicts**: `_ast` never conflicts with other imports
126
+ - **Clear boundaries**: Easy to see what's from which module
127
+
128
+ **Rules:**
129
+
130
+ - ❌ **NEVER destructure imports from internal modules**: `import { parse } from "./parser"`
131
+ - ✅ **ALWAYS use namespace imports**: `import * as _parser from "./parser"`
132
+ - Standard prefixes: `_t` (types), `_l` (literals), `_f` (fns), `_d` (domain)
133
+
134
+ ### Re-exports in index.ts
135
+
136
+ **Default to bulk exports.** Use `export * from` unless you need to hide internals:
137
+
138
+ ```ts
139
+ // ✅ GOOD: Bulk exports (default for most modules)
140
+ export type * from "./types";
141
+ export * from "./literals";
142
+ export * from "./fns";
143
+
144
+ // ✅ GOOD: Selective exports (when some things are private)
145
+ export type { Token, AST } from "./types"; // Only public types
146
+ export { parse, compile } from "./fns"; // Only public functions
147
+ ```
148
+
149
+ **When to use selective exports:**
150
+
151
+ - Module has internal-only types/functions not meant for consumers
152
+ - You're intentionally limiting the public API surface
153
+ - Performance concerns (tree-shaking complex modules)
154
+
155
+ **When to use bulk exports:**
156
+
157
+ - All types/functions are meant for public consumption
158
+ - You want flexibility to add exports without updating index.ts
159
+ - Early development when the API is still evolving
160
+
161
+ ## Path aliases: Always use them
162
+
163
+ **Never use relative paths with `../`.** Always use path aliases configured in tsconfig.json.
164
+
165
+ ```typescript
166
+ // ✅ GOOD: Path aliases
167
+ import * as Entry from "entities/entry";
168
+ import useMediaUpload from "hooks/use-media-upload";
169
+ import * as Recording from "storage/recording";
170
+
171
+ // ❌ AVOID: Relative paths
172
+ import * as Entry from "../../entities/entry";
173
+ import useMediaUpload from "../use-media-upload";
174
+ import * as Recording from "../../../storage/recording";
175
+ ```
176
+
177
+ **Why path aliases?**
178
+
179
+ - **Refactor-safe**: Move files without updating imports
180
+ - **Readable**: Clear where things come from
181
+ - **Consistent**: Same import path from anywhere in the codebase
182
+ - **Shorter**: No counting `../` levels
183
+
184
+ ### Two-segment import rule
185
+
186
+ Keep imports to two segments max. Express complexity through dot notation, not deeper paths.
187
+
188
+ ```typescript
189
+ // ✅ GOOD: Two-segment imports
190
+ import * as md from "features/markdown";
191
+ import * as Entry from "entities/entry";
192
+
193
+ // ❌ AVOID: Three+ segment imports
194
+ import * as compilers from "features/markdown/compilers";
195
+ ```
196
+
197
+ ## Consuming modules
198
+
199
+ ### Entities and features: Namespace imports
200
+
201
+ Always use `* as` namespace imports for entities and feature modules:
202
+
203
+ ```typescript
204
+ // ✅ GOOD: Namespace imports
205
+ import * as Entry from "entities/entry";
206
+ import * as User from "entities/user";
207
+ import * as Recording from "storage/recording";
208
+
209
+ // Usage
210
+ Entry.insertMediaBlock(content, point, galleryId, mediaIds);
211
+ User.getCurrentUser();
212
+ Recording.save(data);
213
+ ```
214
+
215
+ ```typescript
216
+ // ❌ AVOID: Destructuring
217
+ import { insertMediaBlock } from "entities/entry";
218
+ import { getCurrentUser } from "entities/user";
219
+ ```
220
+
221
+ **Why?**
222
+
223
+ - **Origin is clear**: `Entry.insertMediaBlock()` vs orphaned `insertMediaBlock()`
224
+ - **No naming conflicts**: Multiple modules can have similar function names
225
+ - **Discoverable**: Type `Entry.` to see all available functions
226
+
227
+ ### Hooks: Namespace the return value
228
+
229
+ When consuming hooks, assign to a namespace variable (the "noun" of the hook). **Never destructure.**
230
+
231
+ ```typescript
232
+ // ✅ GOOD: Namespace variable
233
+ const recorder = useAudioRecorder();
234
+ const uploader = useMediaUpload();
235
+
236
+ // Usage
237
+ if (recorder.isRecording) { ... }
238
+ recorder.start();
239
+ uploader.uploadFiles(files, params);
240
+ ```
241
+
242
+ ```typescript
243
+ // ❌ AVOID: Destructuring
244
+ const { isRecording, start } = useAudioRecorder();
245
+ const { uploadFiles, status } = useMediaUpload();
246
+ ```
247
+
248
+ **Why?**
249
+
250
+ - **Clarity at call site**: `recorder.start()` is more explicit than `start()`
251
+ - **Avoids naming conflicts**: Multiple hooks with similar methods
252
+ - **Grouping**: Related functionality stays visibly grouped
253
+ - **Discovery**: Typing `recorder.` triggers autocomplete
254
+
255
+ ## Writing hooks
256
+
257
+ ### Event listener cleanup: Use AbortController
258
+
259
+ **Always use `AbortController` for event listener cleanup in effects.** It's cleaner than manual `removeEventListener` and handles multiple listeners automatically.
260
+
261
+ ```typescript
262
+ // ✅ GOOD: AbortController for clean cleanup
263
+ React.useEffect(() => {
264
+ const container = containerRef.current;
265
+ if (!container) return;
266
+
267
+ const controller = new AbortController();
268
+ const signal = controller.signal;
269
+
270
+ const handleScroll = () => { ... };
271
+ const handleResize = () => { ... };
272
+
273
+ container.addEventListener("scroll", handleScroll, { passive: true, signal });
274
+ window.addEventListener("resize", handleResize, { signal });
275
+
276
+ return () => controller.abort();
277
+ }, [deps]);
278
+ ```
279
+
280
+ ```typescript
281
+ // ❌ AVOID: Manual removeEventListener
282
+ React.useEffect(() => {
283
+ const container = containerRef.current;
284
+ if (!container) return;
285
+
286
+ const handleScroll = () => { ... };
287
+ const handleResize = () => { ... };
288
+
289
+ container.addEventListener("scroll", handleScroll, { passive: true });
290
+ window.addEventListener("resize", handleResize);
291
+
292
+ return () => {
293
+ container.removeEventListener("scroll", handleScroll);
294
+ window.removeEventListener("resize", handleResize);
295
+ };
296
+ }, [deps]);
297
+ ```
298
+
299
+ **Why AbortController?**
300
+
301
+ - **Single cleanup call**: `controller.abort()` removes all listeners at once
302
+ - **Less error-prone**: Can't forget to remove a listener or mismatch handler references
303
+ - **Works everywhere**: Same pattern for window, document, elements, and even fetch requests
304
+ - **Composable**: Pass the signal to multiple listeners or async operations
305
+
306
+ **With timeouts:** AbortController handles listeners, but timeouts still need manual cleanup:
307
+
308
+ ```typescript
309
+ React.useEffect(() => {
310
+ const controller = new AbortController();
311
+ const signal = controller.signal;
312
+ let timeout: ReturnType<typeof setTimeout> | null = null;
313
+
314
+ window.addEventListener(
315
+ "scroll",
316
+ () => {
317
+ if (timeout) clearTimeout(timeout);
318
+ timeout = setTimeout(handleSnapDecision, 150);
319
+ },
320
+ { signal }
321
+ );
322
+
323
+ return () => {
324
+ controller.abort();
325
+ if (timeout) clearTimeout(timeout);
326
+ };
327
+ }, [deps]);
328
+ ```
329
+
330
+ ### React state naming
331
+
332
+ Prefer `store`/`setStore` over `state`/`setState` for local React state:
333
+
334
+ ```typescript
335
+ // ✅ GOOD: Clear naming
336
+ const [store, setStore] = React.useState<_t.Upload>({ ... });
337
+
338
+ // ❌ AVOID: Generic "state"
339
+ const [state, setState] = React.useState<_t.Upload>({ ... });
340
+ ```
341
+
342
+ ### Ref comments: Explain the purpose
343
+
344
+ **Always add a comment above each ref explaining its purpose in the app.** Refs often point to invisible DOM elements (sentinels, markers) or serve non-obvious roles. A brief comment saves readers from tracing through the code to understand what each ref is for.
345
+
346
+ ```tsx
347
+ // ✅ GOOD: Each ref has a purpose comment
348
+ // Container ref for auto-scroll to follow highlighted word during playback
349
+ const entryRef = React.useRef<HTMLDivElement>(null);
350
+ // Zero-height sentinel at top of entry; when it scrolls out, the header is sticky
351
+ const headerSentinelRef = React.useRef<HTMLDivElement | null>(null);
352
+ // Marker at bottom of content; used to calculate dynamic fade padding for sticky footer
353
+ const contentEndRef = React.useRef<HTMLDivElement | null>(null);
354
+ ```
355
+
356
+ ```tsx
357
+ // ❌ BAD: No context for what these refs do
358
+ const entryRef = React.useRef<HTMLDivElement>(null);
359
+ const headerSentinelRef = React.useRef<HTMLDivElement | null>(null);
360
+ const contentEndRef = React.useRef<HTMLDivElement | null>(null);
361
+ ```
362
+
363
+ **Why comment refs?**
364
+
365
+ - **Invisible elements**: Sentinel divs have `height: 0`—comments explain their role
366
+ - **Non-obvious connections**: Refs often connect to IntersectionObservers or scroll handlers defined elsewhere
367
+ - **Quick orientation**: New readers can understand the component's DOM strategy at a glance
368
+
369
+ ### Hook return values: Spread state
370
+
371
+ **Spread state properties directly** in the return. Don't wrap in a `state` object—consumers shouldn't need `hook.state.isLoading`.
372
+
373
+ ```typescript
374
+ // ✅ GOOD: Spread state for flat access
375
+ export default function useMediaUpload() {
376
+ const [store, setStore] = React.useState<_t.Upload>({
377
+ status: "idle",
378
+ progress: { loaded: 0, total: 0, percent: 0 },
379
+ uploadedMedia: [],
380
+ });
381
+
382
+ const uploadFiles = async (...) => { ... };
383
+ const reset = () => { ... };
384
+
385
+ return { ...store, uploadFiles, reset };
386
+ }
387
+
388
+ // Consumer gets flat access:
389
+ const uploader = useMediaUpload();
390
+ uploader.status; // ✅ Direct
391
+ uploader.progress; // ✅ Direct
392
+ uploader.uploadFiles(); // ✅ Direct
393
+ ```
394
+
395
+ ```typescript
396
+ // ❌ AVOID: Nested state object
397
+ return { state, uploadFiles, reset };
398
+
399
+ // Consumer needs extra nesting:
400
+ uploader.state.status; // ❌ Extra layer
401
+ uploader.state.progress; // ❌ Extra layer
402
+ ```
403
+
404
+ **Why spread?**
405
+
406
+ - **Simpler consumer API**: `uploader.status` not `uploader.state.status`
407
+ - **Consistent access pattern**: All properties at same level
408
+ - **Better autocomplete**: Flat object shows all options immediately
409
+
410
+ ## Naming conventions
411
+
412
+ ### Clarity over brevity
413
+
414
+ Choose names that reveal intent:
415
+
416
+ ```typescript
417
+ // ❌ Too generic
418
+ md.process(data);
419
+ md.handle(thing);
420
+
421
+ // ✅ Clear intent
422
+ md.ast.parse(markdown);
423
+ md.compilers.compile(ast);
424
+ ```
425
+
426
+ ### Avoid redundant suffixes
427
+
428
+ **Don't use "State", "Type", "Data", "Info", "Object"** when avoidable:
429
+
430
+ ```typescript
431
+ // ❌ AVOID: Redundant suffixes
432
+ interface PlayerState { ... }
433
+ type RecordingType = "audio" | "video";
434
+ const configObject = { ... };
435
+
436
+ // ✅ PREFER: Clean names
437
+ interface Player { ... }
438
+ type Recording = "audio" | "video";
439
+ const config = { ... };
440
+ ```
441
+
442
+ **When suffixes ARE appropriate:**
443
+
444
+ ```typescript
445
+ // ✅ OK: Distinguishing related concepts
446
+ interface PlayerConfig { ... } // Configuration
447
+ interface PlayerControls { ... } // Control methods
448
+
449
+ // ✅ OK: Unit disambiguation
450
+ type DurationMs = number;
451
+ type TokenId = string;
452
+ ```
453
+
454
+ ### Document type fields with JSDoc
455
+
456
+ **Add JSDoc to fields when the name is generic but the value has specific meaning.** Especially `id` fields with particular formats, foreign key references, or `string`/`number` fields with constraints.
457
+
458
+ ```typescript
459
+ // ❌ BAD: What format is id? What do draftA/draftB contain?
460
+ export type PairwiseMatchup = {
461
+ draftA: string;
462
+ draftB: string;
463
+ id: string;
464
+ };
465
+
466
+ // ✅ GOOD: Hover reveals meaning
467
+ export type PairwiseMatchup = {
468
+ /** Model ID of the first draft (shown as "A" to judges) */
469
+ draftA: string;
470
+ /** Model ID of the second draft (shown as "B" to judges) */
471
+ draftB: string;
472
+ /** Unique matchup identifier, e.g. "claude-3-5-sonnet-vs-gpt-4o" */
473
+ id: string;
474
+ };
475
+ ```
476
+
477
+ ### File naming: Lowercase kebab-case
478
+
479
+ ```
480
+ ✅ GOOD
481
+ use-audio-player.tsx
482
+ audio-recorder.tsx
483
+ sync-recordings.ts
484
+
485
+ ❌ AVOID
486
+ useAudioPlayer.tsx # camelCase
487
+ AudioRecorder.tsx # PascalCase
488
+ ```
489
+
490
+ ## Coding style
491
+
492
+ ### Never use IIFEs
493
+
494
+ **Never use Immediately Invoked Function Expressions.** They're ugly, hard to read, and always avoidable.
495
+
496
+ ```typescript
497
+ // ❌ NEVER: IIFE in useEffect
498
+ React.useEffect(() => {
499
+ void (async () => {
500
+ const data = await fetchData();
501
+ setData(data);
502
+ })();
503
+ }, []);
504
+
505
+ // ✅ GOOD: Promise chain
506
+ React.useEffect(() => {
507
+ fetchData().then(setData).catch(console.error);
508
+ }, []);
509
+
510
+ // ✅ GOOD: Extract to named function if complex
511
+ React.useEffect(() => {
512
+ const loadData = async () => {
513
+ const data = await fetchData();
514
+ const processed = transform(data);
515
+ setData(processed);
516
+ };
517
+ loadData().catch(console.error);
518
+ }, []);
519
+ ```
520
+
521
+ **Why IIFEs are banned:**
522
+
523
+ - **Visual noise**: Extra parentheses and invocation clutter the code
524
+ - **Confusing intent**: `void (async () => { ... })()` is cryptic to newcomers
525
+ - **Always avoidable**: Promise chains or extracted functions are clearer
526
+ - **Anti-pattern**: If you need an IIFE, your code structure is wrong
527
+
528
+ **The rule:** If you're reaching for an IIFE, refactor instead. Extract a named function or use promise chains.
529
+
530
+ ### Prefer Maps for key-based lookups
531
+
532
+ When aggregating or deduplicating items by a key, use `Map` instead of array methods like `findIndex`. Maps provide O(1) lookups vs O(n) scans.
533
+
534
+ **Initialize Maps with mapped tuples** to leverage TypeScript inference:
535
+
536
+ ```typescript
537
+ // ❌ AVOID: Manual loop + explicit types
538
+ const claimMap = new Map<string, Claim>();
539
+ for (const claim of existingClaims) {
540
+ claimMap.set(claim.text.toLowerCase(), claim);
541
+ }
542
+
543
+ // ✅ GOOD: Tuple initialization, types inferred
544
+ const claimMap = new Map(
545
+ existingClaims.map((claim) => [claim.text.toLowerCase(), claim])
546
+ );
547
+ ```
548
+
549
+ **Why tuple initialization?**
550
+
551
+ - TypeScript infers `Map<string, Claim>` from the tuple shape
552
+ - More concise—one expression instead of loop + set
553
+ - Declarative style fits functional patterns
554
+
555
+ **Full pattern for merging/deduplicating:**
556
+
557
+ ```typescript
558
+ export function mergeClaims(existing: Claim[], incoming: Claim[]): Claim[] {
559
+ const claimMap = new Map(existing.map((c) => [c.text.toLowerCase(), c]));
560
+
561
+ for (const claim of incoming) {
562
+ const key = claim.text.toLowerCase();
563
+ const found = claimMap.get(key);
564
+ if (found) {
565
+ claimMap.set(key, {
566
+ ...found,
567
+ foundBy: [...found.foundBy, ...claim.foundBy],
568
+ });
569
+ } else {
570
+ claimMap.set(key, claim);
571
+ }
572
+ }
573
+
574
+ return Array.from(claimMap.values());
575
+ }
576
+ ```
577
+
578
+ ### Prefer toSorted over spread + sort
579
+
580
+ For browser-only code, use `toSorted()` instead of `[...array].sort()`. It's cleaner and doesn't require the spread.
581
+
582
+ ```typescript
583
+ // ❌ AVOID: Spread then sort
584
+ const shuffled = [...matchups].sort(() => Math.random() - 0.5);
585
+ const ranked = [...drafts].sort((a, b) => b.score - a.score);
586
+
587
+ // ✅ GOOD: toSorted (ES2023, supported in all modern browsers)
588
+ const shuffled = matchups.toSorted(() => Math.random() - 0.5);
589
+ const ranked = drafts.toSorted((a, b) => b.score - a.score);
590
+ ```
591
+
592
+ **Why toSorted?**
593
+
594
+ - Cleaner syntax—no spread ceremony
595
+ - Intent is clear: "give me a sorted copy"
596
+ - Same performance, fewer characters
597
+
598
+ **Note:** `toSorted()` is ES2023. For Node.js < 20 or older browsers, stick with spread + sort.
599
+
600
+ ## Component props: Pass the object, not its fields
601
+
602
+ **When multiple props come from the same object, pass the object directly.**
603
+
604
+ ```tsx
605
+ // ❌ BAD: Prop explosion - extracting fields from the same object
606
+ <DrawerForMediaPicker
607
+ open={showMediaPicker}
608
+ onClose={handleClose}
609
+ entryId={entry.id}
610
+ entryCreatedAt={entry.createdAt}
611
+ currentContent={entry.content || []}
612
+ currentRevisedContent={entry.revisedContent}
613
+ insertionPoint={insertionPoint}
614
+ />
615
+
616
+ // ✅ GOOD: Pass the object directly
617
+ <DrawerForMediaPicker
618
+ open={showMediaPicker}
619
+ onClose={handleClose}
620
+ entry={entry}
621
+ insertionPoint={insertionPoint}
622
+ />
623
+ ```
624
+
625
+ **Why this is better:**
626
+
627
+ - **Cleaner call sites** - Less visual noise, easier to read
628
+ - **Flexible** - Need another field later? No prop changes needed
629
+ - **Less maintenance** - One prop instead of many to thread through
630
+ - **Type-safe** - TypeScript still catches misuse inside the component
631
+
632
+ **"But what about re-renders when unrelated fields change?"**
633
+
634
+ With React 19 compiler, this is handled automatically. Even without it, the re-render cost is negligible compared to the cognitive overhead of managing many props. Don't optimize for imaginary performance problems.
635
+
636
+ **The rule:** If you're passing 3+ props from the same object, just pass the object.
637
+
638
+ ## Function arguments: The 1-2-3 Rule
639
+
640
+ **Self-documenting arguments eliminate inline comments.**
641
+
642
+ ```typescript
643
+ // ❌ BAD: Requires comments
644
+ findActiveToken(ttsTokens, audio.currentTime, 0.12); // 0.12 = tolerance
645
+
646
+ // ✅ GOOD: Self-documenting
647
+ findActiveToken(audio.currentTime, {
648
+ tokens: ttsTokens,
649
+ toleranceInSecs: 0.12,
650
+ });
651
+ ```
652
+
653
+ **The pattern:**
654
+
655
+ 1. **First arg** = Primary value (id, time, insertionPoint, etc.)
656
+ 2. **Second arg** = Named `props` object (self-documenting)
657
+ 3. **Third arg** = Optional config (rarely needed)
658
+
659
+ **Naming:** Always use `props` for the named object argument.
660
+
661
+ **Inline types:** For simple, one-off argument types, define them inline:
662
+
663
+ ```typescript
664
+ // ✅ GOOD: Simple props, inline type
665
+ export function insertMediaBlock(
666
+ insertionPoint: _t.InsertionPoint,
667
+ props: {
668
+ content: _t.ContentBlock[];
669
+ galleryId: string;
670
+ mediaIds: string[];
671
+ }
672
+ ): _t.ContentBlock[] { ... }
673
+ ```
674
+
675
+ **When to extract to a separate type instead:**
676
+
677
+ - Type is reused by multiple functions
678
+ - Complex types (unions, generics, conditional types)
679
+ - Function overloads that share a type
680
+ - Type needs JSDoc documentation
681
+
682
+ **Exceptions:**
683
+
684
+ - Universal patterns: `map(array, fn)`, `filter(array, predicate)`
685
+ - Math operations: `clamp(value, min, max)`
686
+ - Single argument: `parseId(id)`, `normalize(text)`
687
+ - Two clear arguments: `compileNode(type, node)`
688
+
689
+ ## Function documentation: Explain the why
690
+
691
+ **Every function should have a JSDoc comment that explains the business purpose—not just what it does, but why the app needs it.**
692
+
693
+ A function name like `contentToText` tells you it converts content to text, but that's obvious from the signature. What's not obvious: _why does the app need to flatten blocks into a string?_
694
+
695
+ ````typescript
696
+ // ❌ BAD: Describes what (redundant with signature)
697
+ /**
698
+ * Converts content blocks to text.
699
+ */
700
+ export const contentToText = (blocks: ContentBlock[]): string => { ... }
701
+
702
+ // ✅ GOOD: Explains why + shows how
703
+ /**
704
+ * Extracts plain text from content blocks for TTS generation.
705
+ *
706
+ * Journal entries are stored as structured blocks (text, diarized-text, media)
707
+ * to support rich editing and inline media. However, TTS services like
708
+ * ElevenLabs require plain text input. This function flattens the block
709
+ * structure into a single string, skipping media blocks since they can't
710
+ * be narrated.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * const blocks: ContentBlock[] = [
715
+ * { type: "text", value: "First paragraph." },
716
+ * { type: "media", galleryId: "g1", mediaIds: ["m1"] },
717
+ * { type: "diarized-text", segments: [
718
+ * { speaker: 0, text: "Hello", wordTimings: [] },
719
+ * { speaker: 1, text: "Hi there", wordTimings: [] }
720
+ * ]}
721
+ * ];
722
+ *
723
+ * contentToText(blocks);
724
+ * // Returns: "First paragraph.\n\nHello Hi there"
725
+ * ```
726
+ */
727
+ export const contentToText = (blocks: ContentBlock[]): string => { ... }
728
+ ````
729
+
730
+ **The pattern:**
731
+
732
+ 1. **First line:** One sentence explaining the business purpose (what problem it solves)
733
+ 2. **Context paragraph:** Why this function exists—what system constraint or requirement makes it necessary
734
+ 3. **@example block:** Show concrete input/output so readers can verify their understanding
735
+
736
+ **Keep comments in sync with code.** When you change a function's logic, update the JSDoc. Stale comments are worse than no comments—they actively mislead.
737
+
738
+ **When to document:**
739
+
740
+ - ✅ Domain/business functions in `fns.ts`, `domain.ts`
741
+ - ✅ Complex transformations or algorithms
742
+ - ✅ Functions with non-obvious behavior or edge cases
743
+ - ⚠️ Simple utilities can have shorter docs (one-liner is fine)
744
+ - ❌ Skip for trivial getters/setters or obvious wrappers
745
+
746
+ **Questions good JSDoc answers:**
747
+
748
+ - Why does this function exist? What problem does it solve?
749
+ - What system or service requires this transformation?
750
+ - What happens to different input types? (shown via example)
751
+ - Are there edge cases or gotchas?
752
+
753
+ ## Module organization: Flat > Nested
754
+
755
+ **Prefer flat structure.** Avoid nested sub-modules unless absolutely necessary.
756
+
757
+ ```
758
+ ✅ GOOD: Flat structure
759
+ tts/
760
+ types.ts
761
+ literals.ts
762
+ fns.ts
763
+ domain.ts
764
+ index.ts
765
+
766
+ ⚠️ RARELY needed: Nested only when truly independent
767
+ payments/
768
+ stripe/
769
+ index.ts
770
+ paypal/
771
+ index.ts
772
+ index.ts
773
+ ```
774
+
775
+ **Rule of thumb:** If you're questioning whether to nest, flatten instead.
776
+
777
+ ## Contracts: The shared kernel
778
+
779
+ When client and server need identical types, define them once in `packages/contracts`. This is the "shared kernel"—a small, stable set of types that both environments agree on.
780
+
781
+ ### Why contracts?
782
+
783
+ In a monorepo with client and server, the same data structures often appear in both:
784
+
785
+ ```typescript
786
+ // Without contracts: duplicated types that drift apart
787
+ // packages/client/src/entities/entry/types.ts
788
+ interface WordTiming {
789
+ start: number;
790
+ end: number;
791
+ word: string;
792
+ }
793
+
794
+ // packages/server/src/entities/entry/types.ts
795
+ interface WordTiming {
796
+ start: number;
797
+ end: number;
798
+ word: string;
799
+ speaker?: number;
800
+ }
801
+ // ^ Oops, server added speaker field. Client breaks when it receives it.
802
+ ```
803
+
804
+ Contracts eliminate this drift by defining shared types in one place:
805
+
806
+ ```typescript
807
+ // packages/contracts/src/entry/types.ts
808
+ export interface WordTiming {
809
+ speaker?: number;
810
+ start: number;
811
+ end: number;
812
+ word: string;
813
+ }
814
+ ```
815
+
816
+ ### The pattern
817
+
818
+ **Contracts are pure types.** No business logic, no I/O, no environment-specific code. Just the data shapes that cross the client-server boundary.
819
+
820
+ ```
821
+ packages/
822
+ contracts/
823
+ src/
824
+ entry/
825
+ types.ts # Shared types: ContentBlock, WordTiming, etc.
826
+ index.ts # export type * from "./types"
827
+ user/
828
+ index.ts # Schema + types for user entity
829
+ index.ts # export * as Entry from "./entry"
830
+ ```
831
+
832
+ **Local entities import and extend:**
833
+
834
+ ```typescript
835
+ // packages/server/src/entities/entry/types.ts
836
+ export type * from "contracts/entry"; // Re-export shared types
837
+
838
+ // Server-specific: DDB record shape
839
+ export interface Record {
840
+ pk: string;
841
+ sk: string;
842
+ audioS3Key: string; // Server stores S3 keys
843
+ content: ContentBlock[];
844
+ // ...
845
+ }
846
+ ```
847
+
848
+ ```typescript
849
+ // packages/client/src/entities/entry/types.ts
850
+ export type * from "contracts/entry"; // Re-export shared types
851
+
852
+ // Client-specific: UI presentation model
853
+ export interface UnifiedEntry {
854
+ audioUrl?: string; // Client uses presigned URLs
855
+ isLocal: boolean; // Client tracks local state
856
+ content?: ContentBlock[];
857
+ // ...
858
+ }
859
+ ```
860
+
861
+ ### What belongs in contracts?
862
+
863
+ **Include:**
864
+
865
+ - Types that appear identically in both client and server
866
+ - API request/response shapes
867
+ - Enum-like discriminated unions (`ContentBlock`, `SpeakerSegment`)
868
+ - Validation schemas (arktype/zod) when runtime validation is needed
869
+
870
+ **Don't include:**
871
+
872
+ - Environment-specific extensions (DDB keys, presigned URLs)
873
+ - UI-only types (`InsertionPoint`, `UnifiedEntry`)
874
+ - Server-only types (`Record` with `pk`/`sk`)
875
+
876
+ ### Consuming contracts
877
+
878
+ **Never import contracts directly in application code.** Always go through local entities:
879
+
880
+ ```typescript
881
+ // ✅ GOOD: Import through local entity
882
+ import * as Entry from "entities/entry";
883
+ Entry.ContentBlock; // Shared type (from contracts)
884
+ Entry.Record; // Server-specific type
885
+
886
+ // ❌ AVOID: Bypassing the local entity layer
887
+ import type { ContentBlock } from "contracts/entry";
888
+ ```
889
+
890
+ **Why?** The local entity is the API surface. It can add, remove, or alias types without breaking consumers. If you import contracts directly, you're coupled to the shared kernel's internal structure.
891
+
892
+ ### Constraints
893
+
894
+ **Contracts must be leaves.** They can't import from server or client packages—only external libraries and other contracts. This prevents circular dependencies.
895
+
896
+ **Keep contracts minimal.** Only share what's truly identical. When in doubt, keep types local and share later when the need is proven.
897
+
898
+ **Deploy together.** When contracts change, both client and server must deploy. In a monorepo this happens naturally, but be aware that independent versioning isn't possible.
899
+
900
+ ### Import path setup
901
+
902
+ Configure path aliases in each package's tsconfig.json:
903
+
904
+ ```json
905
+ // packages/server/tsconfig.json
906
+ {
907
+ "compilerOptions": {
908
+ "paths": {
909
+ "contracts/*": ["../contracts/src/*"],
910
+ "entities/*": ["src/entities/*"]
911
+ }
912
+ }
913
+ }
914
+
915
+ // packages/client/tsconfig.json
916
+ {
917
+ "compilerOptions": {
918
+ "paths": {
919
+ "contracts/*": ["../contracts/src/*"],
920
+ "entities/*": ["./src/entities/*"]
921
+ }
922
+ }
923
+ }
924
+ ```
925
+
926
+ ### When to create a contract
927
+
928
+ 1. **Same type in both environments** - If you're copying a type from server to client (or vice versa), it belongs in contracts.
929
+ 2. **API boundary types** - Request/response shapes that define the contract between client and server.
930
+ 3. **Shared validation** - When you need the same runtime validation on both sides.
931
+
932
+ **Not every shared concept needs a contract.** Simple primitives (`string`, `number`) or well-known library types don't need to be in contracts. Focus on domain-specific types that carry meaning in your application.
933
+
934
+ ## API design principles
935
+
936
+ **Consistency** - Predictable patterns reduce cognitive load
937
+ **Clarity** - Purpose obvious from the path
938
+ **Reduction** - Simplify without dumbing down
939
+ **Accessibility** - Common things easy, complex things possible
940
+
941
+ ### Case study: Data over structure
942
+
943
+ **The problem:** We have table, code, list compilers. How do we expose them?
944
+
945
+ **Attempt 1: Deep nesting**
946
+
947
+ ```typescript
948
+ // ❌ Four dots, verbose, hard to refactor
949
+ md.compilers.table.compile(node);
950
+ md.compilers.code.compile(node);
951
+ md.compilers.list.compile(node);
952
+ ```
953
+
954
+ **Attempt 2: Flat namespace**
955
+
956
+ ```typescript
957
+ // ❌ Naming conflicts, loses organization
958
+ md.compileTable(node);
959
+ md.compileCode(node);
960
+ md.compileList(node);
961
+ ```
962
+
963
+ **The insight:** We're repeating "compile" with different data. The node type should be a parameter, not a namespace.
964
+
965
+ **Solution: Discriminated dispatch**
966
+
967
+ ```typescript
968
+ // ✅ Three dots, type-safe, extensible
969
+ md.compilers.compileNode("table", node);
970
+ md.compilers.compileNode("code", node);
971
+ md.compilers.compileNode("list", node);
972
+ ```
973
+
974
+ **Why this works:**
975
+
976
+ - Unified API surface (one function instead of many)
977
+ - Type-safe through string literal unions
978
+ - Data-driven (easy to loop over, test, dynamically dispatch)
979
+ - Stays at three-dot notation
980
+
981
+ ### Getting unstuck: The creative process
982
+
983
+ When you hit a constraint that feels wrong:
984
+
985
+ 1. **Identify the pattern** — What's being repeated? (Same operation on different data)
986
+ 2. **Question the structure** — Am I organizing by _what it does_ or _what it operates on_?
987
+ 3. **Try data over structure** — Can the variant become a parameter instead of a namespace?
988
+ 4. **Validate the feel** — Does `compileNode("table", node)` read naturally? Does it autocomplete well?
989
+
990
+ **Signals you need to rethink:**
991
+
992
+ - Four+ dot notation feels awkward
993
+ - Many similar functions with slight variations
994
+ - Nested namespaces that mirror each other
995
+ - Comments needed to explain what's what
996
+
997
+ **The unlock:** Constraints breed creativity. The two-segment import rule _forces_ better API design. When you can't nest deeper, you're pushed to find more elegant patterns.
998
+
999
+ ## Alternative names for domain.ts
1000
+
1001
+ If `domain` doesn't fit your use case:
1002
+
1003
+ - `services.ts` (\_s): orchestrates flows across helpers/IO
1004
+ - `business.ts` (\_b): explicit business label
1005
+ - `routines.ts` (\_r): stepwise procedures
1006
+ - `operations.ts` (\_o): imperative operations
1007
+ - `engine.ts` (\_e): core driving module
1008
+
1009
+ ## Summary
1010
+
1011
+ **Simplicity is not the absence of complexity—it's the management of it.**
1012
+
1013
+ Atomic modules provide:
1014
+
1015
+ - **Predictability** - You always know where to look
1016
+ - **Greppability** - Namespace imports make refactoring safer
1017
+ - **Scalability** - Pattern works for small and large modules
1018
+ - **Elegance** - Consistency creates a sense of rightness
1019
+
1020
+ **Remember:** When you hit complexity, ask:
1021
+
1022
+ - Can I use data instead of structure?
1023
+ - Can I unify similar operations?
1024
+ - Does this feel natural to type and read?
1025
+
1026
+ The answer often reveals a more elegant path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qstd",
3
- "version": "0.3.57",
3
+ "version": "0.3.58",
4
4
  "description": "Standard Block component and utilities library with Panda CSS",
5
5
  "author": "malin1",
6
6
  "license": "MIT",
@@ -37,7 +37,8 @@
37
37
  "styled-system",
38
38
  "panda.config.ts",
39
39
  "README.md",
40
- "CHANGELOG.md"
40
+ "CHANGELOG.md",
41
+ "ATOMIC_MODULES.md"
41
42
  ],
42
43
  "sideEffects": [
43
44
  "dist/react/index.css"