@krtclcdy/tersio 2.25.6 → 2.25.7

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 (188) hide show
  1. package/README.md +30 -7
  2. package/dist/bench/code-out/pfull-0/debounce.js +13 -0
  3. package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
  4. package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
  5. package/dist/bench/code-out/pfull-1/debounce.js +13 -0
  6. package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
  7. package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
  8. package/dist/bench/code-out/pfull-2/debounce.js +13 -0
  9. package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
  10. package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
  11. package/dist/bench/code-out/pfull-3/debounce.js +12 -0
  12. package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
  13. package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
  14. package/dist/bench/code-out/pfull-4/debounce.js +10 -0
  15. package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
  16. package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
  17. package/dist/bench/code-out/plite-0/debounce.js +13 -0
  18. package/dist/bench/code-out/plite-0/deepClone.js +18 -0
  19. package/dist/bench/code-out/plite-0/groupBy.js +15 -0
  20. package/dist/bench/code-out/plite-1/debounce.js +13 -0
  21. package/dist/bench/code-out/plite-1/deepClone.js +17 -0
  22. package/dist/bench/code-out/plite-1/groupBy.js +11 -0
  23. package/dist/bench/code-out/plite-2/debounce.js +12 -0
  24. package/dist/bench/code-out/plite-2/deepClone.js +17 -0
  25. package/dist/bench/code-out/plite-2/groupBy.js +14 -0
  26. package/dist/bench/code-out/plite-3/debounce.js +12 -0
  27. package/dist/bench/code-out/plite-3/deepClone.js +17 -0
  28. package/dist/bench/code-out/plite-3/groupBy.js +14 -0
  29. package/dist/bench/code-out/plite-4/debounce.js +12 -0
  30. package/dist/bench/code-out/plite-4/deepClone.js +17 -0
  31. package/dist/bench/code-out/plite-4/groupBy.js +14 -0
  32. package/dist/bench/code-out/pultra-0/debounce.js +12 -0
  33. package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
  34. package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
  35. package/dist/bench/code-out/pultra-1/debounce.js +10 -0
  36. package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
  37. package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
  38. package/dist/bench/code-out/pultra-2/debounce.js +13 -0
  39. package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
  40. package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
  41. package/dist/bench/code-out/pultra-3/debounce.js +13 -0
  42. package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
  43. package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
  44. package/dist/bench/code-out/pultra-4/debounce.js +13 -0
  45. package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
  46. package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
  47. package/dist/bench/code-out/run-0/debounce.js +12 -0
  48. package/dist/bench/code-out/run-0/deepClone.js +17 -0
  49. package/dist/bench/code-out/run-0/groupBy.js +14 -0
  50. package/dist/bench/code-out/run-1/debounce.js +13 -0
  51. package/dist/bench/code-out/run-1/deepClone.js +15 -0
  52. package/dist/bench/code-out/run-1/groupBy.js +13 -0
  53. package/dist/bench/code-out/run-2/debounce.js +12 -0
  54. package/dist/bench/code-out/run-2/deepClone.js +18 -0
  55. package/dist/bench/code-out/run-2/groupBy.js +13 -0
  56. package/dist/bench/code-out/run-3/debounce.js +12 -0
  57. package/dist/bench/code-out/run-3/deepClone.js +18 -0
  58. package/dist/bench/code-out/run-3/groupBy.js +14 -0
  59. package/dist/bench/code-out/run-4/debounce.js +12 -0
  60. package/dist/bench/code-out/run-4/deepClone.js +16 -0
  61. package/dist/bench/code-out/run-4/groupBy.js +13 -0
  62. package/dist/cli/banner.js +1 -2
  63. package/dist/cli/common.js +1 -1
  64. package/dist/cli/doctor.js +48 -12
  65. package/dist/cli/hosts.js +1 -2
  66. package/dist/cli/install.js +7 -2
  67. package/dist/cli/manifest.js +25 -5
  68. package/dist/cli/uninstall.js +1 -1
  69. package/dist/extensions/caveman/index.js +6 -2
  70. package/dist/extensions/caveman/rule-lite.md +77 -0
  71. package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
  72. package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
  73. package/dist/extensions/caveman/rule.md +1 -1
  74. package/dist/extensions/combo/index.js +26 -7
  75. package/dist/extensions/pstack/index.js +3 -1
  76. package/dist/extensions/pstack/rule.md +88 -12
  77. package/dist/extensions/rtk/session.js +18 -10
  78. package/dist/extensions/shared/rtk-eligible.js +84 -0
  79. package/dist/extensions/shared/rtk-gain.js +1 -1
  80. package/dist/extensions/shared/sqlite.js +7 -13
  81. package/dist/extensions/shared/status.js +1 -2
  82. package/dist/extensions/shared/usage-ledger.js +102 -20
  83. package/extensions/caveman/index.ts +6 -2
  84. package/extensions/caveman/rule-lite.md +77 -0
  85. package/extensions/caveman/rule-megacave-lite.md +59 -0
  86. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  87. package/extensions/caveman/rule.md +1 -1
  88. package/extensions/combo/index.ts +27 -7
  89. package/extensions/pstack/index.ts +3 -1
  90. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  91. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  92. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  93. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  94. package/extensions/pstack/playbooks/babysit.md +23 -0
  95. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  96. package/extensions/pstack/playbooks/eval.md +25 -0
  97. package/extensions/pstack/playbooks/feature.md +21 -0
  98. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  99. package/extensions/pstack/playbooks/investigation.md +14 -0
  100. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  101. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  102. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  103. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  104. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  105. package/extensions/pstack/playbooks/prototype.md +14 -0
  106. package/extensions/pstack/playbooks/refactoring.md +16 -0
  107. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  108. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  109. package/extensions/pstack/playbooks/shipping.md +17 -0
  110. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  111. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  112. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  113. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  114. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  115. package/extensions/pstack/principles/build-the-lever.md +22 -0
  116. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  117. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  118. package/extensions/pstack/principles/experience-first.md +18 -0
  119. package/extensions/pstack/principles/explain-the-number.md +16 -0
  120. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  121. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  122. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  123. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  124. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  125. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  126. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  127. package/extensions/pstack/principles/model-the-domain.md +23 -0
  128. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  129. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  130. package/extensions/pstack/principles/prove-it-works.md +21 -0
  131. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  132. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  133. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  134. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  135. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  136. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  137. package/extensions/pstack/references/bugbot-triage.md +142 -0
  138. package/extensions/pstack/rule.md +88 -12
  139. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  140. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  141. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  142. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  143. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  144. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  145. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  146. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  147. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  148. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  149. package/extensions/pstack/skills/how/SKILL.md +35 -0
  150. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  151. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  152. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  153. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  154. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  155. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  156. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  157. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  158. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  159. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  160. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  161. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  162. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  163. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  164. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  165. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  166. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  167. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  168. package/extensions/pstack/skills/why/SKILL.md +97 -0
  169. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  170. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  171. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  172. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  173. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  174. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  175. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  176. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  177. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  178. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  179. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  180. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  181. package/extensions/rtk/session.ts +17 -9
  182. package/extensions/shared/rtk-eligible.ts +83 -0
  183. package/extensions/shared/rtk-gain.ts +1 -1
  184. package/extensions/shared/sqlite.ts +7 -13
  185. package/extensions/shared/status.ts +1 -2
  186. package/extensions/shared/usage-ledger.ts +97 -19
  187. package/extensions/timing/index.ts +1 -2
  188. package/package.json +7 -4
package/README.md CHANGED
@@ -4,14 +4,36 @@
4
4
 
5
5
  [![npm version](https://shieldcn.dev/npm/@krtclcdy%2Ftersio.svg?variant=branded&size=xs&logo=npm)](https://www.npmjs.com/package/@krtclcdy/tersio)
6
6
  [![node](https://shieldcn.dev/badge/node-%3E%3D20.12-22c55e.svg?variant=branded&size=xs&logo=nodedotjs)](https://nodejs.org)
7
- [![bun](https://shieldcn.dev/badge/bun-%3E%3D1.4.2-22c55e.svg?variant=branded&size=xs&logo=bun)](https://bun.sh)
8
7
  [![npm downloads](https://shieldcn.dev/npm/dm/@krtclcdy%2Ftersio.svg?variant=branded&size=xs&logo=npm)](https://www.npmjs.com/package/@krtclcdy/tersio)
9
8
  [![Build](https://shieldcn.dev/github/ci/KurutoDenzeru/tersio.svg?variant=branded&size=xs&logo=githubactions&label=Build)](https://github.com/KurutoDenzeru/tersio/actions)
10
9
  [![MIT](https://shieldcn.dev/badge/license-MIT-2563eb.svg?variant=branded&size=xs&logo=opensourceinitiative)](./LICENSE)
11
10
 
12
- Token saver & usage tracker for coding agents: terse caveman replies, compact RTK shell output, lean Ponytail code calls, a pstack rigor mode, and a gain dashboard — one-command combo presets over one shared extension tree.
11
+ Token saver & usage tracker for coding agents: terse caveman replies, compact RTK shell output, lean Ponytail code, pstack rigor, and a gain dashboard — one-command combos over one shared extension tree.
13
12
 
14
- Measured savings per mode, the prompt overhead each one adds, and the rerun protocol: [docs/BENCHMARK.md](./docs/BENCHMARK.md).
13
+ Measured savings per mode, the prompt overhead each one adds, and the rerun protocol: [docs/BENCHMARK_GUIDE.md](./docs/BENCHMARK_GUIDE.md).
14
+
15
+ ## 🎯 What this is, and why you would use it
16
+
17
+ Coding agents bill you per token. Three of the costs you never asked for:
18
+
19
+ - **Replies carry filler.** The same three sentences, plus a greeting, a hedge, a recap, and a "hope this helps".
20
+ - **Shell output is noise.** A failing test prints hundreds of lines to reach the four that matter.
21
+ - **The rules repeat every turn.** Your agent re-reads its instructions on every single turn.
22
+
23
+ Tersio installs into Oh My Pi, pi, or OpenCode, and cuts those costs:
24
+
25
+ | Mode | What it does for you |
26
+ |---|---|
27
+ | 🪨 **Caveman** | Shorter replies. Answer first, filler gone, and code, commands, paths, and errors stay exact. |
28
+ | ⚡ **RTK** | Condensed shell output, every signal kept. A failing test still names the test, the assertion, and the line. |
29
+ | 🦥 **Ponytail** | Leaner code. YAGNI: no feature, option, or abstraction nobody asked for, and the shortest diff that still works. |
30
+ | 🥔 **pstack** | Rigor mode. It applies a design principle when the moment fits, and owns the approach, the check, and the design. |
31
+ | 🧩 **Combo** | One command sets all of the above, at three strengths. |
32
+ | 📊 **Dashboard** | What you spent, per model and per day. |
33
+
34
+ Every mode is a text block you can read in the repo. Measured savings and prompt overhead are published per mode in [docs/BENCHMARK_GUIDE.md](./docs/BENCHMARK_GUIDE.md). No total-bill claim: the tests measure reply tokens, prompt overhead, and command output only.
35
+
36
+ Use an agent daily and pay per token? This is for you. Use one a few times a week? It is not worth installing.
15
37
 
16
38
  ## ⚡ Getting Started
17
39
 
@@ -53,8 +75,9 @@ npm exec --yes --prefer-online --package=@krtclcdy/tersio@latest -- tersio insta
53
75
  ### Requirements
54
76
 
55
77
  - [OMP](https://github.com/can1357/oh-my-pi), [pi](https://github.com/earendil-works/pi), or [OpenCode](https://opencode.ai) — all three are supported; pick one with `tersio install --host <omp|pi|opencode>`
56
- - Node.js 20.12+ with npm, or Bun — the CLI runs under either, and the installer prefers Bun when it is present
57
- - SQLite runs in process where the runtime provides it: Bun always, Node 22.5+ through `node:sqlite`. An older Node uses the `sqlite3` binary instead, and both answer identically
78
+ - **Node.js 20.12+ with npm.** That is the requirement. The extensions load inside your host, the CLI runs under Node, and the package declares no Bun entry point
79
+ - **Bun is optional.** The installer prefers it when it is on PATH and falls back to npm. `npm i -g` alone installs everything. Bun also supplies SQLite in process, which is its only effect on the code
80
+ - SQLite comes from the runtime where it can: Bun, or Node 22.5+ through `node:sqlite`. An older Node needs the `sqlite3` binary on PATH, and every engine answers identically
58
81
  - macOS or Linux, native or WSL
59
82
 
60
83
  On Windows, use WSL. There is no native Windows path: the installer, the extensions, and the RTK wiring are tested on macOS and Linux only. WSL has its own home directory, so install from inside WSL — `command -v npm` must resolve to a Linux path, not `/mnt/c/`.
@@ -88,7 +111,7 @@ It is display-only — it never enters the model context, so it costs the sessio
88
111
  | Command | Purpose |
89
112
  |---|---|
90
113
  | `/combo off\|medium\|balanced\|max\|status` | One preset for all four modes (start here): medium = caveman lite · rtk on · ponytail lite · pstack on, balanced = full/full/on/on, max = ultra/ultra/on/on, and off disables every mode; per-mode tweaks drop to `custom`. |
91
- | `/caveman lite\|full\|ultra\|megacave-lite\|megacave-full\|megacave-ultra\|off\|status` | Terse replies with upstream-aligned Caveman levels. The legacy `wenyan` spelling still resolves, onto `megacave-full`. |
114
+ | `/caveman lite\|full\|ultra\|megacave-lite\|megacave-full\|megacave-ultra\|off\|status` | Terse replies with upstream-aligned Caveman levels. `full`, `ultra`, and `megacave-full` load upstream's own bodies byte for byte; `lite`, `megacave-lite`, and `megacave-ultra` are Tersio's own bodies for registers upstream defines but no longer ships, and each one names its upstream row in the file. The legacy `wenyan` spellings still resolve: `wenyan` → `megacave-full`, `wenyan-lite` → `megacave-lite`, `wenyan-full` → `megacave-full`, `wenyan-ultra` → `megacave-ultra`. |
92
115
  | `/rtk on\|off\|status` | Controls RTK session guidance, `rtk_run`, and the automatic hook through shared `RTK_DISABLED` state. Exact bytes bypass RTK. |
93
116
  | `/ponytail off\|lite\|full\|ultra\|status` | Minimal code using upstream Ponytail modes. Review runs separately through `/ponytail-review`. |
94
117
  | `/pstack on\|off\|status` | Rigor mode, forked from [pstack](https://github.com/cursor/plugins/tree/main/pstack) (MIT). The medium, balanced, and max presets turn it on; `normal mode` clears it. |
@@ -138,7 +161,7 @@ An OMP install also registers the package in `~/.omp/plugins`, so it shows under
138
161
 
139
162
  **RTK missing or not executable:** run `tersio install`, then `tersio doctor`. On Linux/macOS: `chmod +x ~/.bun/bin/rtk`.
140
163
 
141
- **RTK commands not metered in the Dashboard:** only bash calls routed through rtk's OMP wiring (`~/.omp/agent/extensions/rtk.ts`, written by `rtk init -g --agent omp`) are metered. Missing: run that command (needs rtk ≥ 0.49) or `tersio doctor --fix rtk`, then restart OMP. Native tool calls (`read`/`edit`/`eval`) stay unmetered.
164
+ **RTK savings and adoption:** the Gain numbers read rtk's own `history.db`, which the binary writes on every host, so pi and OpenCode runs are already in them. The adoption row counts executed Bash calls from all three stores: the omp and pi session transcripts, and OpenCode's v2 sqlite or pre-v2 JSON message tree. The eligible command list comes from `rtk rewrite` itself, cached per binary version. Native tool calls (`read`/`edit`/`eval`) stay out of RTK by design, and exact bytes bypass RTK.
142
165
 
143
166
  **Checksum warning or failure:** the installer aborts on RTK checksum mismatch but warns and continues when checksum metadata is unavailable; `/tersio update rtk` aborts when metadata is missing.
144
167
 
@@ -0,0 +1,13 @@
1
+ // Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,13 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ // The closure rebinds per call, so only the last call's arguments fire.
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const grouped = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const bucket = grouped[key];
6
+ if (bucket === undefined) {
7
+ grouped[key] = [item];
8
+ }
9
+ else {
10
+ bucket.push(item);
11
+ }
12
+ }
13
+ return grouped;
14
+ }
@@ -0,0 +1,13 @@
1
+ // debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,7 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ (groups[keyFn(item)] ??= []).push(item);
5
+ }
6
+ return groups;
7
+ }
@@ -0,0 +1,10 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ clearTimeout(timer);
5
+ timer = setTimeout(() => {
6
+ timer = undefined;
7
+ fn(...args);
8
+ }, ms);
9
+ };
10
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const bucket = groups[key];
6
+ if (bucket === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ bucket.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,13 @@
1
+ // Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,18 @@
1
+ // deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
2
+ export function deepClone(value) {
3
+ if (value === null || typeof value !== 'object') {
4
+ return value;
5
+ }
6
+ if (value instanceof Date) {
7
+ return new Date(value.getTime());
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => deepClone(item));
11
+ }
12
+ const source = value;
13
+ const clone = {};
14
+ for (const key of Object.keys(source)) {
15
+ clone[key] = deepClone(source[key]);
16
+ }
17
+ return clone;
18
+ }
@@ -0,0 +1,15 @@
1
+ // groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
2
+ export function groupBy(items, keyFn) {
3
+ const groups = {};
4
+ for (const item of items) {
5
+ const key = keyFn(item);
6
+ const bucket = groups[key];
7
+ if (bucket === undefined) {
8
+ groups[key] = [item];
9
+ }
10
+ else {
11
+ bucket.push(item);
12
+ }
13
+ }
14
+ return groups;
15
+ }
@@ -0,0 +1,13 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ // Close over the latest arguments so the trailing call forwards them.
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const result = {};
13
+ for (const key of Object.keys(source)) {
14
+ result[key] = deepClone(source[key]);
15
+ }
16
+ return result;
17
+ }
@@ -0,0 +1,11 @@
1
+ export function groupBy(items, keyFn) {
2
+ const result = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ if (result[key] === undefined) {
6
+ result[key] = [];
7
+ }
8
+ result[key].push(item);
9
+ }
10
+ return result;
11
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const existing = groups[key];
6
+ if (existing === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ existing.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const existing = groups[key];
6
+ if (existing === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ existing.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const existing = groups[key];
6
+ if (existing === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ existing.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,12 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ if (timer !== undefined) {
5
+ clearTimeout(timer);
6
+ }
7
+ timer = setTimeout(() => {
8
+ timer = undefined;
9
+ fn(...args);
10
+ }, ms);
11
+ };
12
+ }
@@ -0,0 +1,20 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== "object") {
3
+ return value;
4
+ }
5
+ if (Array.isArray(value)) {
6
+ // SAFETY: every element is cloned recursively, so the result has the same shape as the input array.
7
+ return value.map((item) => deepClone(item));
8
+ }
9
+ if (value instanceof Date) {
10
+ // SAFETY: getTime() is the Date's full state, so a new Date is equal but unshared.
11
+ return new Date(value.getTime());
12
+ }
13
+ const source = value;
14
+ const clone = {};
15
+ for (const key of Object.keys(source)) {
16
+ clone[key] = deepClone(source[key]);
17
+ }
18
+ // SAFETY: only the input's own keys are rebuilt, so the clone is structurally the same T.
19
+ return clone;
20
+ }
@@ -0,0 +1,14 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ const key = keyFn(item);
5
+ const bucket = groups[key];
6
+ if (bucket === undefined) {
7
+ groups[key] = [item];
8
+ }
9
+ else {
10
+ bucket.push(item);
11
+ }
12
+ }
13
+ return groups;
14
+ }
@@ -0,0 +1,10 @@
1
+ export function debounce(fn, ms) {
2
+ let timer;
3
+ return (...args) => {
4
+ clearTimeout(timer);
5
+ timer = setTimeout(() => {
6
+ timer = undefined;
7
+ fn(...args);
8
+ }, ms);
9
+ };
10
+ }
@@ -0,0 +1,17 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value;
4
+ }
5
+ if (value instanceof Date) {
6
+ return new Date(value.getTime());
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((item) => deepClone(item));
10
+ }
11
+ const source = value;
12
+ const clone = {};
13
+ for (const key of Object.keys(source)) {
14
+ clone[key] = deepClone(source[key]);
15
+ }
16
+ return clone;
17
+ }
@@ -0,0 +1,7 @@
1
+ export function groupBy(items, keyFn) {
2
+ const groups = {};
3
+ for (const item of items) {
4
+ (groups[keyFn(item)] ??= []).push(item);
5
+ }
6
+ return groups;
7
+ }
@@ -0,0 +1,13 @@
1
+ // debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
2
+ export function debounce(fn, ms) {
3
+ let timer;
4
+ return (...args) => {
5
+ if (timer !== undefined) {
6
+ clearTimeout(timer);
7
+ }
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, ms);
12
+ };
13
+ }