@nklisch/pi-agile-workflow 0.15.3
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/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +28 -0
- package/CHANGELOG.md +591 -0
- package/README.md +107 -0
- package/docs/ARCHITECTURE.md +716 -0
- package/docs/MIGRATION.md +419 -0
- package/docs/PRINCIPLES.md +464 -0
- package/docs/ROADMAP.md +95 -0
- package/docs/SPEC.md +855 -0
- package/docs/VISION.md +106 -0
- package/extensions/agile-workflow.test.ts +419 -0
- package/extensions/agile-workflow.ts +467 -0
- package/hooks/hooks.json +52 -0
- package/hooks/scripts/prompt-context.py +628 -0
- package/hooks/scripts/substrate-maintainer.py +359 -0
- package/hooks/scripts/test_prompt_context.py +757 -0
- package/package.json +30 -0
- package/scripts/install-work-view.sh +165 -0
- package/scripts/tests/agent-metadata.test.sh +75 -0
- package/scripts/tests/bump-version.test.sh +552 -0
- package/scripts/tests/channel-parity.test.sh +117 -0
- package/scripts/tests/convert-content-integrity.test.sh +246 -0
- package/scripts/tests/convert-install-routing.test.sh +159 -0
- package/scripts/tests/install-work-view.test.sh +411 -0
- package/scripts/tests/pi-package-metadata.test.sh +119 -0
- package/scripts/tests/work-board-shim.test.sh +239 -0
- package/scripts/tests/work-view-dist-version.test.sh +105 -0
- package/scripts/work-board.sh +64 -0
- package/scripts/work-view.sh +423 -0
- package/skills/autopilot/SKILL.md +330 -0
- package/skills/autopilot/agents/openai.yaml +6 -0
- package/skills/board/SKILL.md +62 -0
- package/skills/board/agents/openai.yaml +6 -0
- package/skills/bold-refactor/SKILL.md +244 -0
- package/skills/bold-refactor/agents/openai.yaml +6 -0
- package/skills/bug-scan/SKILL.md +378 -0
- package/skills/bug-scan/agents/openai.yaml +6 -0
- package/skills/bug-scan/references/async-promises.md +119 -0
- package/skills/bug-scan/references/concurrency-races.md +148 -0
- package/skills/bug-scan/references/data-layer.md +167 -0
- package/skills/bug-scan/references/error-handling.md +197 -0
- package/skills/bug-scan/references/gate-item-template.md +94 -0
- package/skills/bug-scan/references/language-footguns.md +219 -0
- package/skills/bug-scan/references/parked-item-template.md +103 -0
- package/skills/bug-scan/references/report-template.md +115 -0
- package/skills/bug-scan/references/resource-leaks.md +199 -0
- package/skills/bug-scan/references/state-closures.md +148 -0
- package/skills/bug-scan/references/time-numbers.md +158 -0
- package/skills/convert/SKILL.md +1463 -0
- package/skills/convert/agents/openai.yaml +6 -0
- package/skills/convert/references/legacy-overlap-migration.md +179 -0
- package/skills/deep-code-scan/SKILL.md +378 -0
- package/skills/deep-code-scan/agents/openai.yaml +6 -0
- package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
- package/skills/deep-code-scan/references/consolidation.md +91 -0
- package/skills/deep-code-scan/references/decomposition.md +127 -0
- package/skills/deep-code-scan/references/item-templates.md +263 -0
- package/skills/deep-code-scan/references/lane-catalog.md +129 -0
- package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
- package/skills/deep-code-scan/references/scanner-brief.md +152 -0
- package/skills/e2e-test-design/SKILL.md +517 -0
- package/skills/e2e-test-design/references/anti-tautology.md +261 -0
- package/skills/e2e-test-design/references/service-mocks.md +234 -0
- package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
- package/skills/epic-design/SKILL.md +476 -0
- package/skills/epicize/SKILL.md +210 -0
- package/skills/epicize/agents/openai.yaml +6 -0
- package/skills/feature-design/SKILL.md +472 -0
- package/skills/fix/SKILL.md +173 -0
- package/skills/gate-cruft/SKILL.md +258 -0
- package/skills/gate-docs/SKILL.md +278 -0
- package/skills/gate-patterns/SKILL.md +389 -0
- package/skills/gate-refactor/SKILL.md +408 -0
- package/skills/gate-security/SKILL.md +275 -0
- package/skills/gate-tests/SKILL.md +367 -0
- package/skills/groom/SKILL.md +162 -0
- package/skills/groom/agents/openai.yaml +6 -0
- package/skills/ideate/SKILL.md +185 -0
- package/skills/ideate/agents/openai.yaml +6 -0
- package/skills/implement/SKILL.md +288 -0
- package/skills/implement-orchestrator/SKILL.md +604 -0
- package/skills/park/SKILL.md +111 -0
- package/skills/perf-design/SKILL.md +427 -0
- package/skills/perf-scout/SKILL.md +404 -0
- package/skills/perf-scout/agents/openai.yaml +6 -0
- package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
- package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
- package/skills/perf-scout/references/caching-and-memoization.md +123 -0
- package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
- package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
- package/skills/perf-scout/references/distributed-systems.md +150 -0
- package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
- package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
- package/skills/perf-scout/references/idea-ranking.md +75 -0
- package/skills/perf-scout/references/io-and-batching.md +135 -0
- package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
- package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
- package/skills/perf-scout/references/parked-item-template.md +117 -0
- package/skills/perf-scout/references/peer-review-pass.md +150 -0
- package/skills/perf-scout/references/report-template.md +148 -0
- package/skills/principles/SKILL.md +678 -0
- package/skills/principles/references/models.md +191 -0
- package/skills/principles/references/subagents.md +158 -0
- package/skills/prose-author/SKILL.md +185 -0
- package/skills/refactor-conventions-creator/SKILL.md +359 -0
- package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
- package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
- package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
- package/skills/refactor-design/SKILL.md +384 -0
- package/skills/release-deploy/SKILL.md +667 -0
- package/skills/release-deploy/agents/openai.yaml +6 -0
- package/skills/research/SKILL.md +166 -0
- package/skills/research/agents/openai.yaml +6 -0
- package/skills/review/SKILL.md +267 -0
- package/skills/review/references/deep-review.md +98 -0
- package/skills/review/references/review-lenses.md +68 -0
- package/skills/review/references/substrate-side-effects.md +161 -0
- package/skills/review/references/target-resolution.md +52 -0
- package/skills/scope/SKILL.md +486 -0
- package/work-view/Cargo.lock +486 -0
- package/work-view/Cargo.toml +9 -0
- package/work-view/crates/cli/.work-view-version +1 -0
- package/work-view/crates/cli/Cargo.toml +18 -0
- package/work-view/crates/cli/src/actionable.rs +500 -0
- package/work-view/crates/cli/src/args.rs +792 -0
- package/work-view/crates/cli/src/board/assets/board.css +855 -0
- package/work-view/crates/cli/src/board/assets/board.js +259 -0
- package/work-view/crates/cli/src/board/assets/card.js +139 -0
- package/work-view/crates/cli/src/board/assets/components.css +290 -0
- package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
- package/work-view/crates/cli/src/board/assets/detail.js +256 -0
- package/work-view/crates/cli/src/board/assets/filters.js +389 -0
- package/work-view/crates/cli/src/board/assets/index.html +77 -0
- package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
- package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
- package/work-view/crates/cli/src/board/assets/motion.css +131 -0
- package/work-view/crates/cli/src/board/assets/state.js +245 -0
- package/work-view/crates/cli/src/board/assets/table.js +304 -0
- package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
- package/work-view/crates/cli/src/board/assets/views.js +41 -0
- package/work-view/crates/cli/src/board/assets.rs +162 -0
- package/work-view/crates/cli/src/board/feed.rs +204 -0
- package/work-view/crates/cli/src/board/mod.rs +246 -0
- package/work-view/crates/cli/src/board/open.rs +144 -0
- package/work-view/crates/cli/src/board/server.rs +407 -0
- package/work-view/crates/cli/src/main.rs +195 -0
- package/work-view/crates/cli/src/render.rs +354 -0
- package/work-view/crates/cli/src/scope.rs +157 -0
- package/work-view/crates/cli/src/stale.rs +581 -0
- package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
- package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
- package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
- package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
- package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
- package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
- package/work-view/crates/cli/tests/integration.rs +2833 -0
- package/work-view/crates/core/Cargo.toml +13 -0
- package/work-view/crates/core/src/error.rs +61 -0
- package/work-view/crates/core/src/filter.rs +804 -0
- package/work-view/crates/core/src/graph.rs +324 -0
- package/work-view/crates/core/src/index.rs +590 -0
- package/work-view/crates/core/src/lib.rs +54 -0
- package/work-view/crates/core/src/model.rs +202 -0
- package/work-view/crates/core/src/parse.rs +494 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
- package/work-view/crates/core/tests/integration.rs +541 -0
- package/work-view/dist/.gitattributes +1 -0
- package/work-view/dist/README.md +42 -0
- package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
- package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
- package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
- package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# State Management & Closure — Bug Reference
|
|
2
|
+
|
|
3
|
+
> **When to load this reference**: when scanning code that uses closures, callbacks captured for
|
|
4
|
+
> later execution, framework reactivity primitives (useState/useEffect, ref/reactive, $state,
|
|
5
|
+
> signals, stores), or any "value captured at one time, read at another" pattern.
|
|
6
|
+
|
|
7
|
+
## Detection signals
|
|
8
|
+
- grep heuristics: `useEffect\(`, `useCallback\(`, `useMemo\(`, `useRef\(`, `useState\(`,
|
|
9
|
+
`\.bind\(`, `for\s*\(\s*var\s`, `for\s*\(.*\)\s*\{[^}]*function`, `setTimeout\([^,]+,`,
|
|
10
|
+
`addEventListener\(`, `ref\(`, `reactive\(`, `\$state\b`, `\$derived\b`, `\$effect\b`,
|
|
11
|
+
`createSignal`, `createEffect`, `watch\(`, `watchEffect\(`, `useStore\(`, `useSelector\(`,
|
|
12
|
+
`queryKey:\s*\[`, `enabled:\s*!!`, `defineProps\(`, `\.value` after destructure,
|
|
13
|
+
async arrow inside JSX, module-scope `let` or `const` arrays/maps in server code.
|
|
14
|
+
- Read-time vs write-time mismatch: any place the value was captured before the read.
|
|
15
|
+
|
|
16
|
+
## Patterns
|
|
17
|
+
|
|
18
|
+
### 1. Loop variable captured by closure
|
|
19
|
+
- **Signature**: `for` / iteration creates closures that all read the same loop variable; all callbacks end up using the final value.
|
|
20
|
+
- **Why hard to find**: Looks correct line-by-line; the bug only appears when callbacks fire.
|
|
21
|
+
- **Where to look**: `setTimeout`/`setInterval` inside loops, `forEach` with async work scheduled, event handlers bound in loops, goroutines started in loops, Python `lambda` in list comprehensions.
|
|
22
|
+
- **Example**: `for (var i = 0; i < n; i++) setTimeout(() => log(i), 0)` logs `n` n times.
|
|
23
|
+
- **Fix direction**: Bind a per-iteration copy: JS `let` instead of `var`; Python `lambda i=i: ...`; Go 1.22+ loop-var scoping is per-iteration (older Go: copy to inner var); Rust prefer `move` closures over borrows of loop locals.
|
|
24
|
+
- **Language/framework variants**: JS `var` (broken) vs `let`/`const` (fine); Go pre-1.22 vs 1.22+; Python lambdas always capture by name; Rust borrow checker often catches this at compile.
|
|
25
|
+
|
|
26
|
+
### 2. React: stale closure in useEffect reading outside-dep state
|
|
27
|
+
- **Signature**: Effect body reads `state` or `props` that aren't in the dep array; subscriber/handler keeps seeing the initial value.
|
|
28
|
+
- **Why hard to find**: First render works; bug surfaces only after a state change.
|
|
29
|
+
- **Where to look**: `useEffect(() => { ws.onmessage = e => setX(state + e.data) }, [])` — `state` is frozen at mount.
|
|
30
|
+
- **Example**: Counter increments only show `1` because handler closes over `count = 0`.
|
|
31
|
+
- **Fix direction**: Add to deps, use functional setter `setX(prev => ...)`, move logic into `useEffectEvent` (React 19.2+) for non-reactive reads, or stash latest via `useRef`.
|
|
32
|
+
- **Variants**: Same bug in `useCallback`/`useMemo` with stale deps; React 19's `useEffectEvent` is now the idiomatic fix and ESLint ignores it in dep arrays.
|
|
33
|
+
|
|
34
|
+
### 3. React: missing or wrong dep array → infinite re-render / stale read
|
|
35
|
+
- **Signature**: Object/array/function literal in deps that's reconstructed every render; or no deps when there should be.
|
|
36
|
+
- **Why hard to find**: Symptoms range from network storms to no symptom until data shape shifts.
|
|
37
|
+
- **Where to look**: `useEffect(() => fetch(...), [{ id }])` — object literal is new each render. Or `useEffect(fn)` with no deps array (runs every render).
|
|
38
|
+
- **Fix direction**: Depend on primitives, not objects. Memoize with `useMemo`/`useCallback`. Use the React ESLint plugin (`react-hooks/exhaustive-deps`).
|
|
39
|
+
- **Variants**: Same shape in Preact's `useEffect`; SolidJS `createEffect` tracks automatically so this class is rarer there.
|
|
40
|
+
|
|
41
|
+
### 4. React: setState during render (incl. derived-state anti-pattern)
|
|
42
|
+
- **Signature**: Calling `setState` synchronously inside the render function or inside `useMemo`; or syncing prop → state with `useEffect(() => setX(prop), [prop])`.
|
|
43
|
+
- **Why hard to find**: "Too many re-renders" error or one-frame-stale UI; tests that don't await commit miss it.
|
|
44
|
+
- **Where to look**: Component bodies that conditionally call `setX(...)`; `useEffect` blocks whose only job is to mirror a prop into state.
|
|
45
|
+
- **Fix direction**: Derive during render (`const derived = compute(prop)`), use `key` to reset child state, or call `setState` in an event handler — never just to mirror props.
|
|
46
|
+
- **Variants**: React 18+ strict mode double-invokes render, exposing more of these.
|
|
47
|
+
|
|
48
|
+
### 5. React: ref read in render path (no rerender on mutation)
|
|
49
|
+
- **Signature**: Rendering `ref.current` directly in JSX, then expecting UI to update when ref changes.
|
|
50
|
+
- **Why hard to find**: It works on first render; subsequent updates silently lag because mutating `.current` does not schedule render.
|
|
51
|
+
- **Where to look**: `<div>{countRef.current}</div>`, `if (someRef.current) ...` in JSX.
|
|
52
|
+
- **Fix direction**: Use `useState` if the value drives UI; refs are for imperative handles, DOM nodes, or values needed across renders without triggering one.
|
|
53
|
+
|
|
54
|
+
### 6. React: race in async effect (setState after unmount / out-of-order responses)
|
|
55
|
+
- **Signature**: `useEffect(async () => { const r = await fetch(...); setX(r) }, [id])` — when `id` changes fast, late responses overwrite newer ones.
|
|
56
|
+
- **Why hard to find**: Only shows under latency variance; tests with mocked instant fetches miss it.
|
|
57
|
+
- **Where to look**: Any async work in an effect without a cancellation flag or `AbortController`.
|
|
58
|
+
- **Fix direction**: `let cancelled = false; ... if (!cancelled) setX(r); return () => { cancelled = true }`. Or use `AbortController` + check `signal.aborted`. Better: use TanStack Query.
|
|
59
|
+
|
|
60
|
+
### 7. Vue 3: destructuring `reactive()` loses reactivity
|
|
61
|
+
- **Signature**: `const { x } = reactive({ x: 1 })` — `x` is a plain primitive, not reactive.
|
|
62
|
+
- **Why hard to find**: Code reads naturally; template updates simply don't fire.
|
|
63
|
+
- **Where to look**: Anywhere a `reactive` object is unpacked into locals, passed across composable boundaries, or spread.
|
|
64
|
+
- **Fix direction**: Use `toRefs(state)` then destructure, prefer `ref()` for primitives, or access via `state.x`. In Vue 3.5+ `defineProps()` destructure is compiler-magic and stays reactive — **only** there.
|
|
65
|
+
- **Variants**: `reactive` arrays lose reactivity on full reassignment (`state.list = newArr` works only if `state` itself is reactive and `list` is a property, not on a destructured local).
|
|
66
|
+
|
|
67
|
+
### 8. Vue 3: `ref` / `.value` mistakes across boundaries
|
|
68
|
+
- **Signature**: Forgetting `.value` in `<script>`, or accidentally using `.value` in template (unwrap is automatic only at top level of template).
|
|
69
|
+
- **Why hard to find**: TS may not flag it if types collapse; runtime just reads `undefined` or never updates.
|
|
70
|
+
- **Where to look**: Composables returning refs that get used as values; nested refs in objects (no auto-unwrap inside a plain object passed to template).
|
|
71
|
+
- **Fix direction**: Consistent ref convention; rely on Volar/Vetur warnings; prefer returning `toRefs`-style records from composables.
|
|
72
|
+
|
|
73
|
+
### 9. Vue 3: `watch` / `watchEffect` stop handle leaked
|
|
74
|
+
- **Signature**: `watch(...)` called outside a component setup (e.g., in a module, in a long-lived store) and never `stop()`-ed.
|
|
75
|
+
- **Why hard to find**: Memory grows slowly; same callback fires N times after N route changes.
|
|
76
|
+
- **Where to look**: Pinia stores that register watchers in actions; composables that watch without `onScopeDispose`.
|
|
77
|
+
- **Fix direction**: Capture the returned stop handle and call it in `onUnmounted`/`onScopeDispose`, or use `effectScope`.
|
|
78
|
+
|
|
79
|
+
### 10. Svelte 5: replacing $state object loses reactivity on properties
|
|
80
|
+
- **Signature**: Re-exporting/re-assigning a `$state` object reference from a module or destructuring it loses the proxy; mutating `obj.field` no longer triggers updates.
|
|
81
|
+
- **Why hard to find**: Looks identical to working code; only the binding mechanism differs.
|
|
82
|
+
- **Where to look**: Modules exporting `$state` directly (`export const s = $state({...})` then `import { s } from ...`; destructuring at import-site breaks), components doing `let local = sharedState` then mutating `local.x`.
|
|
83
|
+
- **Fix direction**: Export an accessor/getter or wrap in a class with `$state` fields. Never destructure a `$state` proxy. For immutable replacement, use `$state.raw` (only full reassignment triggers).
|
|
84
|
+
- **Variants**: `$state.raw` is opposite trap — mutating fields does NOT trigger; must reassign whole value.
|
|
85
|
+
|
|
86
|
+
### 11. Svelte 5: `$derived` stale via `untrack` or non-reactive read
|
|
87
|
+
- **Signature**: `$derived` body reads from a plain (non-rune) variable, or wraps reads in `untrack()`, then never updates.
|
|
88
|
+
- **Why hard to find**: Compiler doesn't error; first paint correct.
|
|
89
|
+
- **Where to look**: `$derived` reading from module-scope `let`, or from a destructured rune (see #10), or `$derived.by(() => untrack(() => state.x))`.
|
|
90
|
+
- **Fix direction**: Read directly from `$state` runes inside `$derived`; reserve `untrack` for known one-shot reads (e.g., initialization values).
|
|
91
|
+
|
|
92
|
+
### 12. SolidJS: signal accessor not called
|
|
93
|
+
- **Signature**: `<div>{count}</div>` instead of `<div>{count()}</div>` — passing the function instead of invoking it.
|
|
94
|
+
- **Why hard to find**: Renders something (often `function count()` source string or `[object Function]`); no reactivity.
|
|
95
|
+
- **Where to look**: All JSX expressions where a signal is referenced; props passed to children as `prop={signal}` vs `prop={signal()}`.
|
|
96
|
+
- **Fix direction**: Call accessors at the point of use. Pass signal itself (uncalled) only when the child needs lazy reactive access; document the convention.
|
|
97
|
+
- **Variants**: `createMemo` returns an accessor too; same bug. SolidStart SSR amplifies symptom because hydration mismatches.
|
|
98
|
+
|
|
99
|
+
### 13. Zustand / Pinia: mutating state outside the setter / action
|
|
100
|
+
- **Signature**: Direct `store.items.push(x)` without going through `set(state => ...)` or a Pinia action.
|
|
101
|
+
- **Why hard to find**: In Pinia (Proxy-based) it may "work" until devtools/time-travel break; in Zustand it silently fails to notify subscribers.
|
|
102
|
+
- **Where to look**: Components grabbing the whole store and mutating; reducers using `state.x = y` outside Immer/Redux Toolkit.
|
|
103
|
+
- **Fix direction**: Always go through `set`/actions. Use Immer for ergonomic updates. Lint with `no-param-reassign` on reducers.
|
|
104
|
+
- **Variants**: Redux without Toolkit: mutation breaks `===` checks and selectors. Redux Toolkit reducers run in Immer so mutation is fine *only inside* `createSlice` reducers.
|
|
105
|
+
|
|
106
|
+
### 14. Selector returning new object each call → infinite re-render
|
|
107
|
+
- **Signature**: `useSelector(s => ({ a: s.a, b: s.b }))` or Zustand `useStore(s => s.list.filter(...))` returns a fresh reference every render.
|
|
108
|
+
- **Why hard to find**: Test passes; production hits React's render-loop bailout or just thrashes.
|
|
109
|
+
- **Where to look**: Any selector that constructs an object/array/derived list inline.
|
|
110
|
+
- **Fix direction**: Select primitives, use `shallow` (`useStore(sel, shallow)` in Zustand), `createSelector` (reselect) in Redux, or `useShallow` (Zustand v4.4+).
|
|
111
|
+
|
|
112
|
+
### 15. TanStack Query v5: queryKey identity unstable
|
|
113
|
+
- **Signature**: `queryKey: [{ id, filters }]` or `queryKey: ['x', new Date()]` — fresh reference each render means cache misses and refetch storms.
|
|
114
|
+
- **Why hard to find**: Network tab shows constant requests; logic works.
|
|
115
|
+
- **Where to look**: Object literals, `Date.now()`, `Math.random()`, function references in queryKeys; queryKeys built inside render without memoization.
|
|
116
|
+
- **Fix direction**: Keys should be serializable and stable: `['user', id, { sort, filter }]` — TanStack Query v5 hashes deterministically, so object literals are fine *if their content is stable*. The bug is when content changes identity (e.g., `filters` rebuilt each render). Memoize the input or flatten to primitives.
|
|
117
|
+
- **Variants**: `enabled: !!user && user.role === 'admin'` toggling causes refetch; `placeholderData`/`select` returning new refs causes re-render churn — wrap with `useCallback`.
|
|
118
|
+
|
|
119
|
+
### 16. Global module-level mutable state in SSR / serverless
|
|
120
|
+
- **Signature**: `const cache = new Map()` at module scope in a Next.js / Remix / Nuxt server module; first request populates, later requests for other users see leaked data.
|
|
121
|
+
- **Why hard to find**: Works locally with one user; appears as cross-tenant data leak in prod.
|
|
122
|
+
- **Where to look**: Top-level `let`/`const` collections in server files; singletons holding request-specific context; React Server Components storing per-request data in module scope.
|
|
123
|
+
- **Fix direction**: Use request-scoped storage (`AsyncLocalStorage` in Node, request context in framework), pass context explicitly, or use per-request providers. Treat module scope as multi-tenant shared.
|
|
124
|
+
- **Variants**: Same bug with global Prisma / DB clients holding per-request transaction state, or globally cached auth tokens.
|
|
125
|
+
|
|
126
|
+
### 17. Event listener captured stale `this` (or stale closure)
|
|
127
|
+
- **Signature**: `el.addEventListener('click', this.handle)` without `.bind(this)` or arrow, then `this` inside is `undefined`/the element; or handler captures stale local.
|
|
128
|
+
- **Why hard to find**: Throws only on dispatch path that may be rare.
|
|
129
|
+
- **Where to look**: Class components, vanilla JS modules, custom elements, third-party widget integrations.
|
|
130
|
+
- **Fix direction**: Class fields with arrow (`handle = () => {...}`), explicit `.bind(this)` once and store the bound ref so `removeEventListener` works, or use functional patterns.
|
|
131
|
+
|
|
132
|
+
### 18. Memoization key includes object identity (always different)
|
|
133
|
+
- **Signature**: `useMemo(() => compute(opts), [opts])` where `opts` is a fresh object each render; or `_.memoize(fn)` keyed on object args.
|
|
134
|
+
- **Why hard to find**: Looks memoized; profiler shows cache never hits.
|
|
135
|
+
- **Where to look**: Any memo whose deps are objects/arrays/functions not stabilized upstream.
|
|
136
|
+
- **Fix direction**: Depend on primitives or memoize the upstream object too. For caches, key on a serialized form (`JSON.stringify` for shallow data, structural hash for deep).
|
|
137
|
+
|
|
138
|
+
### 19. Reactive computation reading from non-reactive source
|
|
139
|
+
- **Signature**: Vue `computed`, Svelte `$derived`, Solid `createMemo` reading a plain variable, DOM property, `Date.now()`, or globalThis — no dependency is tracked, so never re-runs.
|
|
140
|
+
- **Why hard to find**: Initial value correct; never updates.
|
|
141
|
+
- **Where to look**: Bridges between framework state and "outside" data (localStorage, window size before `ResizeObserver` is wired, native events).
|
|
142
|
+
- **Fix direction**: Wrap external sources in framework primitives (`ref` + listener that writes it, `createSignal` + subscription, `$state` + `$effect`). Many frameworks ship adapters (`useSyncExternalStore` in React, `useExternalStore`-style composables in Vue).
|
|
143
|
+
|
|
144
|
+
### 20. Async race with React 18+ concurrent rendering / Suspense
|
|
145
|
+
- **Signature**: Effect reads `state` then awaits, but a transition/`useDeferredValue` already rendered with newer state by the time the await resolves.
|
|
146
|
+
- **Why hard to find**: Only under heavy interaction or `startTransition` boundaries.
|
|
147
|
+
- **Where to look**: Code mixing `useTransition`, `useDeferredValue`, and manual async work in effects.
|
|
148
|
+
- **Fix direction**: Prefer Suspense-aware data libs (TanStack Query, Relay, RSC). For manual effects, version-tag the request (`const reqId = ++ref.current; if (reqId !== ref.current) return`).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Time, Date & Numeric Precision — Bug Reference
|
|
2
|
+
|
|
3
|
+
> **When to load this reference**: when scanning code that handles dates, timestamps, durations,
|
|
4
|
+
> calendars, scheduling, billing periods — or any numeric work involving money, percentages,
|
|
5
|
+
> floating-point comparisons, integer overflow risk, or cross-language number boundaries.
|
|
6
|
+
|
|
7
|
+
These bugs share the "value looks right, isn't" property: tests pass on the dev's machine, in
|
|
8
|
+
their locale, on Tuesday afternoon, with their sample data — and break elsewhere.
|
|
9
|
+
|
|
10
|
+
## Detection signals
|
|
11
|
+
|
|
12
|
+
**Time/date grep heuristics:**
|
|
13
|
+
- `new Date(`, `Date.now`, `\.toISOString`, `\.getTime`, `\.getHours` (local!) vs `\.getUTCHours`
|
|
14
|
+
- `moment(`, `dayjs(`, `date-fns`, `luxon`, `Temporal\.`
|
|
15
|
+
- `datetime\.now\(\)`, `datetime\.utcnow`, `datetime\.today`, `strftime`, `strptime`, `time\.time\(\)`
|
|
16
|
+
- `time\.Now\(\)`, `time\.Parse`, `chrono::`, `SystemTime::now`, `Instant::now`
|
|
17
|
+
- `setTimeout.*\*.*1000`, `\* 86400`, `\* 3600`, magic seconds-in-X constants
|
|
18
|
+
- `TIMESTAMP`, `DATETIME`, `TIMESTAMPTZ` in schemas; `WITHOUT TIME ZONE`
|
|
19
|
+
- String date sorts: `\.sort\(.*date`, ISO vs `MM/DD/YYYY` mixing
|
|
20
|
+
|
|
21
|
+
**Numeric grep heuristics:**
|
|
22
|
+
- `parseFloat`, `parseInt`(without radix), `Number\(`, `\+\+` on string-typed value
|
|
23
|
+
- `\.toFixed`, `Math\.round`, `Math\.floor` on money-shaped names (`price`, `total`, `amount`, `cents`)
|
|
24
|
+
- `==` or `===` with floats; `=== NaN`, `isNaN(` (vs `Number\.isNaN`)
|
|
25
|
+
- `BigInt\(`, `n` suffix mixed with regular `\d`; `Decimal\(`, `decimal\.Decimal`
|
|
26
|
+
- `i32`, `u32`, `as i64`, `as u32`, casts that narrow
|
|
27
|
+
- `\$\{.*\}`/string-templating numbers without `Intl\.NumberFormat` / `toLocaleString`
|
|
28
|
+
|
|
29
|
+
## Time / Date / TZ Patterns
|
|
30
|
+
|
|
31
|
+
### 1. Naive datetime treated as UTC (or vice versa)
|
|
32
|
+
- **Signature**: `datetime.utcnow()`, `datetime.now()` without tz, `new Date('2025-01-01')`, naive `LocalDateTime`.
|
|
33
|
+
- **Why hard to find**: works perfectly when server TZ == UTC. Breaks at deploy to a non-UTC host or when devs run locally.
|
|
34
|
+
- **Where to look**: API boundaries (request parsing, DB writes), cron jobs, log timestamps, JWT `exp`/`iat`.
|
|
35
|
+
- **Example**: Python `datetime.utcnow()` returns a *naive* datetime (no tzinfo) — comparing it to an aware `datetime.now(tz=...)` raises `TypeError`, and serializing it omits the offset so the consumer guesses. Deprecated in Python 3.12; removal targeted for a future version.
|
|
36
|
+
- **Fix direction**: Always construct aware datetimes: `datetime.now(timezone.utc)` (or `datetime.UTC` on 3.11+). In JS, store as ISO-8601 with explicit `Z` or offset. In Postgres, prefer `TIMESTAMPTZ`.
|
|
37
|
+
- **Language variants**: Java `LocalDateTime` vs `ZonedDateTime`/`Instant`; Go `time.Time` always carries location but `time.Parse` without layout-with-zone yields UTC.
|
|
38
|
+
|
|
39
|
+
### 2. DST transitions: missing and duplicate hours
|
|
40
|
+
- **Signature**: scheduling, recurring jobs, "every day at 2:30am", date arithmetic across spring/fall.
|
|
41
|
+
- **Why hard to find**: only fires twice a year, in some timezones. CI in UTC never sees it.
|
|
42
|
+
- **Where to look**: cron-like schedulers, billing-period rollovers, alarm clocks, calendar invites.
|
|
43
|
+
- **Example**: `2025-03-09 02:30` doesn't exist in `America/New_York` (skipped forward); `2025-11-02 01:30` happens twice (fall back).
|
|
44
|
+
- **Fix direction**: Schedule in UTC or use a library with explicit DST disambiguation (`fold` in Python, `Temporal.ZonedDateTime` with `disambiguation: 'earlier'|'later'|'reject'`). Never compute "tomorrow at X" by adding 86400 seconds.
|
|
45
|
+
- **Language variants**: JS `Date` silently shifts; Luxon and Temporal expose `isAmbiguous`/`isValid`.
|
|
46
|
+
|
|
47
|
+
### 3. Date arithmetic crossing month / year / DST
|
|
48
|
+
- **Signature**: `date + 30 days`, `addMonths(d, 1)`, "last day of month", anniversary billing.
|
|
49
|
+
- **Why hard to find**: tests use mid-month dates that never trigger edge cases.
|
|
50
|
+
- **Where to look**: subscriptions, trial expirations, statement periods, leap-year-sensitive logic (`Feb 29 + 1 year`).
|
|
51
|
+
- **Example**: `addMonths(new Date('2025-01-31'), 1)` — Feb has no 31st. Different libraries return Feb 28, Mar 3, or throw.
|
|
52
|
+
- **Fix direction**: Use a calendar-aware library (`date-fns`, Luxon, Temporal). For anniversaries, snap to last-day-of-month explicitly. For Feb 29 birthdays on non-leap years, define policy (Feb 28 or Mar 1).
|
|
53
|
+
- **Language variants**: Java `Period` vs `Duration` — `Period` is calendar-aware, `Duration` is fixed seconds.
|
|
54
|
+
|
|
55
|
+
### 4. Monotonic clock vs wall clock for durations
|
|
56
|
+
- **Signature**: `Date.now() - start`, `time.time() - start`, `SystemTime::now()` deltas.
|
|
57
|
+
- **Why hard to find**: NTP slews and leap seconds make wall clock jump backward; only matters for sub-second timing or long-running timers.
|
|
58
|
+
- **Where to look**: performance metrics, timeouts, retry backoff, rate limits, "time since last X".
|
|
59
|
+
- **Example**: NTP adjustment moves `Date.now()` backward by 200ms → negative duration → infinite retry loop.
|
|
60
|
+
- **Fix direction**: Use monotonic clocks for durations: `performance.now()` (JS), `time.monotonic()` (Python), `Instant::now()` (Rust), `System.nanoTime()` (Java), `time.Since` with `time.Now()` is wall in Go — use `time.Monotonic` reading where available.
|
|
61
|
+
|
|
62
|
+
### 5. Unix timestamp unit confusion (s / ms / µs / ns)
|
|
63
|
+
- **Signature**: integer timestamp passed to a function expecting a different unit. `1700000000` vs `1700000000000`.
|
|
64
|
+
- **Why hard to find**: dates near 1970 (small ms read as seconds) or far future (seconds read as ms) — usually wrong by ~1000x but might just look like "a wrong date."
|
|
65
|
+
- **Where to look**: cross-language boundaries (Python→JS, Go→browser), Redis/Kafka, log aggregation.
|
|
66
|
+
- **Example**: `new Date(timestamp)` expects ms; `time.time()` returns float seconds; Go `time.Unix(sec, nsec)`; Rust's chrono `from_timestamp` is seconds, `from_timestamp_millis` is ms.
|
|
67
|
+
- **Fix direction**: Name variables with units (`expires_at_ms`, `expires_at_s`). Validate range at boundaries (anything before 2001 or after 2100 in seconds is suspect).
|
|
68
|
+
|
|
69
|
+
### 6. ISO 8601 parsing ambiguity (date-only strings)
|
|
70
|
+
- **Signature**: `new Date('2025-01-01')`, `Date.parse('2025-01-01')`.
|
|
71
|
+
- **Why hard to find**: same string parses to different instants depending on tool/version.
|
|
72
|
+
- **Where to look**: date pickers, query params, CSV imports.
|
|
73
|
+
- **Example**: `new Date('2025-01-01')` is parsed as UTC midnight; `new Date('2025-01-01T00:00:00')` is parsed as *local* midnight. In `America/Los_Angeles` these differ by 8 hours and land on different *dates*.
|
|
74
|
+
- **Fix direction**: Always include explicit offset (`Z` or `+00:00`). For calendar-only data use `Temporal.PlainDate` or store as `DATE` not `TIMESTAMP`.
|
|
75
|
+
|
|
76
|
+
### 7. Locale-dependent date parsing (`MM/DD` vs `DD/MM`)
|
|
77
|
+
- **Signature**: `new Date('03/04/2025')`, `strptime` without explicit format, Excel/CSV imports.
|
|
78
|
+
- **Why hard to find**: ambiguous dates (day ≤ 12) silently parse to wrong date; clearly invalid ones throw.
|
|
79
|
+
- **Where to look**: file imports, user-typed forms, third-party feeds.
|
|
80
|
+
- **Fix direction**: Require ISO-8601 at boundaries. If accepting locale formats, ask for locale explicitly and reject ambiguous input.
|
|
81
|
+
|
|
82
|
+
### 8. Sorting mixed date string formats
|
|
83
|
+
- **Signature**: `array.sort()` on strings like `'1/2/2025'`, `'12/31/2024'`, `'2025-01-02'`.
|
|
84
|
+
- **Why hard to find**: ISO-8601 sorts lexically as it does chronologically; other formats don't. Mixing them silently misorders.
|
|
85
|
+
- **Where to look**: report generation, log file processing, filename sorts.
|
|
86
|
+
- **Fix direction**: Parse to instants/PlainDates and sort those, or normalize to ISO-8601 first.
|
|
87
|
+
|
|
88
|
+
### 9. Year 2038 problem (32-bit unix epoch)
|
|
89
|
+
- **Signature**: `int32`/`time_t`/`i32` storing seconds since 1970; embedded systems, legacy DBs, C structs over the wire.
|
|
90
|
+
- **Why hard to find**: works today, will fail at `2038-01-19T03:14:07Z`. Code paths computing "1 year from now" already trigger near 2037.
|
|
91
|
+
- **Where to look**: MySQL `INT UNSIGNED` for timestamps, old C/C++ binaries, MQTT/binary protocols, JWT `exp` rendered as `int32`.
|
|
92
|
+
- **Fix direction**: Use 64-bit timestamp types end-to-end. Postgres `TIMESTAMPTZ` is already 64-bit; MySQL `DATETIME` (not `TIMESTAMP`) avoids the 2038 cliff.
|
|
93
|
+
|
|
94
|
+
## Numeric Precision Patterns
|
|
95
|
+
|
|
96
|
+
### 10. Floating-point equality and `0.1 + 0.2 != 0.3`
|
|
97
|
+
- **Signature**: `===`/`==` on floats, `if (total === expected)`, fixture comparisons.
|
|
98
|
+
- **Why hard to find**: passes for "round" values, fails when inputs accumulate from arithmetic.
|
|
99
|
+
- **Where to look**: tax/discount/percentage math, geometric calculations, currency conversion, test assertions.
|
|
100
|
+
- **Example**: `(0.1 + 0.2) === 0.3` → `false`. `0.3 - 0.1 - 0.1 - 0.1` → `-2.78e-17`.
|
|
101
|
+
- **Fix direction**: Compare with epsilon: `Math.abs(a - b) < EPSILON`. For currency, use integer minor units (cents) or a Decimal library. Use language-provided `isClose` (Python `math.isclose`, Rust `approx::abs_diff_eq!`).
|
|
102
|
+
|
|
103
|
+
### 11. Money in floating point
|
|
104
|
+
- **Signature**: `price: number`, `amount: f64`, `total *= 1.08`, JSON dollars-as-float.
|
|
105
|
+
- **Why hard to find**: small invoices appear correct; rounding errors compound at scale, in batch jobs, or under FX conversion.
|
|
106
|
+
- **Where to look**: invoicing, ledger entries, tax math, refunds, exchange rates, anywhere `*=` or `+=` accumulates a price.
|
|
107
|
+
- **Fix direction**: Store money as integer minor units (cents) **or** use Decimal: `decimal.Decimal` (Python), `BigDecimal` (Java/Scala), `decimal.js` / `dinero.js` (JS), `rust_decimal` (Rust). Never `parseFloat` a money string and put it back in a DB.
|
|
108
|
+
|
|
109
|
+
### 12. NaN propagation and `NaN === NaN` is false
|
|
110
|
+
- **Signature**: `parseFloat`/`parseInt` on bad input, `0/0`, `Math.sqrt(-1)`, `Number(undefined)`.
|
|
111
|
+
- **Why hard to find**: NaN poisons every downstream calculation silently (no exception). `if (x === NaN)` never fires.
|
|
112
|
+
- **Where to look**: form input parsing, JSON without schema, division, aggregations over partially-null data.
|
|
113
|
+
- **Example**: `parseFloat('--')` → `NaN` → stored, summed, displayed as `NaN`. `[1, 2, NaN].reduce((a,b)=>a+b)` → `NaN`.
|
|
114
|
+
- **Fix direction**: Use `Number.isNaN(x)` (not the global `isNaN` — that coerces strings). Validate at boundaries; reject NaN early. Python `float('nan')` and `math.isnan` analogues.
|
|
115
|
+
|
|
116
|
+
### 13. Integer overflow (i32 / i64 / JS safe range)
|
|
117
|
+
- **Signature**: `i32`/`u32` arithmetic, `as i32` narrowing casts, JS numbers beyond `2^53`.
|
|
118
|
+
- **Why hard to find**: Rust panics in debug but **wraps silently** in `--release` for primitive `+`/`-`/`*`. JS silently loses precision past `Number.MAX_SAFE_INTEGER` (`2^53 - 1`).
|
|
119
|
+
- **Where to look**: ID generation, counters, byte-size math, multiplication of "reasonable" numbers (`1_000_000 * 1_000_000` overflows `i32`).
|
|
120
|
+
- **Example**: Rust `let x: i32 = 1_000_000 * 3_000;` panics in debug, wraps to a negative in release. JS `9007199254740993 === 9007199254740992` → `true`.
|
|
121
|
+
- **Fix direction**: Rust — use `checked_*`, `wrapping_*`, `saturating_*` explicitly; or enable `overflow-checks = true` in release profile. JS — use `BigInt` for 64-bit IDs (Twitter-style snowflake IDs from APIs). Serialize 64-bit ints as strings over JSON.
|
|
122
|
+
|
|
123
|
+
### 14. BigInt / Number mixing in JavaScript
|
|
124
|
+
- **Signature**: `bigIntValue + 1`, `Math.floor(bigIntValue)`, `JSON.stringify(bigIntValue)`.
|
|
125
|
+
- **Why hard to find**: TypeError at runtime (`Cannot mix BigInt and other types`); `JSON.stringify` throws on BigInt; silent precision loss when forced via `Number(big)`.
|
|
126
|
+
- **Where to look**: ID handling from APIs, high-precision counters, crypto/blockchain code, anywhere BigInt enters from one source and meets `+ 1` somewhere else.
|
|
127
|
+
- **Fix direction**: Pick one type at the boundary and convert explicitly: `Number(big)` (lossy past 2^53) or `BigInt(num)` (throws on fractions). Custom `JSON.stringify` replacer to render BigInts as strings.
|
|
128
|
+
|
|
129
|
+
### 15. Implicit type coercion (`"10" + 5`)
|
|
130
|
+
- **Signature**: numeric op on a value sourced from `req.query`, `process.env`, form input, CSV.
|
|
131
|
+
- **Why hard to find**: JS `+` is string concat for any string operand; `-`/`*`/`/` coerce both sides. `"10" + 5 === "105"` but `"10" - 5 === 5`.
|
|
132
|
+
- **Where to look**: query-string parsing, env-var-as-number, JSON with quoted numbers, Python `input()` (always str).
|
|
133
|
+
- **Fix direction**: Parse and validate at boundaries (`Number(x)`, `parseInt(x, 10)`, `int(x)`, zod/pydantic). Use strict TypeScript and lint rules against arithmetic on `any`.
|
|
134
|
+
|
|
135
|
+
### 16. Division rounding surprises
|
|
136
|
+
- **Signature**: integer `/` in Python 2 / Go / Rust / C; `Math.round` and `toFixed` for display.
|
|
137
|
+
- **Why hard to find**: Go/Rust `5/2 == 2` (integer truncation toward zero). JS `Math.round(0.5) === 1` but `Math.round(-0.5) === 0` (rounds half-up). Python 3 `5/2 == 2.5`, `5//2 == 2`. Banker's rounding (Python `round`, `decimal.ROUND_HALF_EVEN`) differs from `toFixed` (which rounds half-to-even-ish but is browser-inconsistent).
|
|
138
|
+
- **Where to look**: pagination, distributing remainders, percentage splits, tax allocation.
|
|
139
|
+
- **Example**: `(0.1 + 0.2).toFixed(2)` may render `"0.30"` while sum-of-rounded ≠ rounded-sum.
|
|
140
|
+
- **Fix direction**: For display, use `Intl.NumberFormat` (locale-aware, deterministic). For money allocation, use a "largest remainder" or "banker's" distribution algorithm — don't trust naive `total / n`.
|
|
141
|
+
|
|
142
|
+
### 17. Sum of large + small floats loses the small
|
|
143
|
+
- **Signature**: accumulating many small floats into a running large total, telemetry aggregation, financial running totals.
|
|
144
|
+
- **Why hard to find**: result is plausible but wrong by a percent or two; depends on iteration order.
|
|
145
|
+
- **Example**: `1e16 + 1.0 - 1e16` → `0` (the `1.0` falls below the precision of `1e16`).
|
|
146
|
+
- **Fix direction**: Use Kahan / Neumaier compensated summation, or sort small-to-large before summing. For exact totals, use Decimal/integer cents.
|
|
147
|
+
|
|
148
|
+
### 18. Negative zero edge cases
|
|
149
|
+
- **Signature**: `Math.sign`, `1/x` (gives `-Infinity` vs `Infinity`), `Object.is(0, -0)`, sort keys.
|
|
150
|
+
- **Why hard to find**: `-0 === 0` is `true`, but `Object.is(-0, 0)` is `false` and `1/-0 === -Infinity`. Sorts and `Map` keys behave inconsistently.
|
|
151
|
+
- **Fix direction**: Normalize to `+0` when sign is irrelevant: `x === 0 ? 0 : x` or `x + 0`. Be explicit when sign matters.
|
|
152
|
+
|
|
153
|
+
## Cross-cutting fix directions
|
|
154
|
+
- Pick canonical representations at the **system boundary** (UTC instant + IANA tz name; integer minor units for money; ISO-8601 for dates; explicit unit in field name for timestamps).
|
|
155
|
+
- Validate with schemas (zod, pydantic, JSON Schema) — don't trust upstream types.
|
|
156
|
+
- For JS in 2026, prefer the **Temporal API** (Stage 4, ES2026, shipping in Chrome 144 / Firefox 139+; polyfill `@js-temporal/polyfill` elsewhere) over legacy `Date`.
|
|
157
|
+
- For Python, use **timezone-aware** datetimes everywhere (`datetime.now(timezone.utc)`); `utcnow()` is deprecated in 3.12+.
|
|
158
|
+
- For Rust, prefer `checked_*` / `saturating_*` over raw arithmetic in any code that handles untrusted input or runs in release with `overflow-checks = false`.
|