@chenmiao8563/dsh-token-ledger 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,31 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
9
  Nothing yet.
10
10
 
11
+ ## [0.2.0] - 2026-09-10
12
+
13
+ ### Added
14
+
15
+ - **A settings page.** The browser half registers a `settings.section`, so the
16
+ ledger appears in the settings sidebar beside every other section. It shows:
17
+ - range totals for **this month**, **this year** and the **last 7 days**, each
18
+ with total tokens, cache hit rate and call count, plus the bucket breakdown;
19
+ - **today**, live, which moves as steps complete;
20
+ - a **usage calendar** switchable between year, month and week, where the week
21
+ view is a bar chart rather than a heatmap;
22
+ - the per-model breakdown with its own cache hit rates.
23
+ - `lib/overview.js`, a pure function from a ledger snapshot to the payload the
24
+ page renders, and `lib/route.js`, the read-only
25
+ `GET /api/token-ledger/summary` route that serves it.
26
+ - The browser half is hand-written against the client's module loader and
27
+ requires nothing but `react`, so the package still installs with no build step.
28
+
29
+ ### Changed
30
+
31
+ - The overview is served over an HTTP route rather than a settings namespace.
32
+ A namespace would need a schema — a real dependency — and would rewrite
33
+ `settings.yaml` on every debounce with data that is derived and reproducible.
34
+ The ledger file remains the only store of record.
35
+
11
36
  ## [0.1.1] - 2026-09-10
12
37
 
13
38
  Both fixes come from mounting the plugin in a real DSH host, which is the only
@@ -63,6 +88,7 @@ so both now have regression tests.
63
88
  - Zero runtime dependencies, zero peer dependencies and no install scripts, so
64
89
  the package installs without a build step.
65
90
 
66
- [Unreleased]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.1.1...HEAD
91
+ [Unreleased]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.2.0...HEAD
92
+ [0.2.0]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.1.1...v0.2.0
67
93
  [0.1.1]: https://github.com/chenmiao8563/dsh-token-ledger/compare/v0.1.0...v0.1.1
68
94
  [0.1.0]: https://github.com/chenmiao8563/dsh-token-ledger/releases/tag/v0.1.0
package/README.md CHANGED
@@ -144,6 +144,33 @@ audit useless. The ledger's own `updatedAt` settles it:
144
144
  (1 calls, 5100 tokens not yet flushed)
145
145
  ```
146
146
 
147
+ ## Settings page
148
+
149
+ The browser half adds a **Token ledger** section to the settings sidebar:
150
+
151
+ - **Range totals** for this month, this year and the last 7 days — total tokens,
152
+ cache hit rate, call count, and the bucket breakdown behind them. The cache hit
153
+ rate is `cacheRead / (cacheRead + uncachedInput)`: the share of input the prompt
154
+ cache absorbed, so a route with no caching reads 0% rather than undefined.
155
+ - **Today, live** — today's tokens, hit rate and calls, which move as steps
156
+ complete.
157
+ - **A usage calendar** switchable between year, month and week. Year and month are
158
+ heatmaps; the week view is a bar chart. Heat levels are relative to the busiest
159
+ day in the window and square-rooted, so one huge day cannot flatten the rest.
160
+ - **Per model** totals, each with its own hit rate.
161
+
162
+ The page reads one read-only route, `GET /api/token-ledger/summary`, and polls it
163
+ every 15 seconds. The route refuses a non-loopback peer, so it stays private even
164
+ if the web server is bound to `0.0.0.0`.
165
+
166
+ It needs a profile with a web server (the `web` or `desktop` profile). Without one,
167
+ `/tokens` and the CLI still work, and the section says so instead of failing.
168
+
169
+ The overview is deliberately **not** published through a settings namespace. That
170
+ would need a schema — a real dependency, and this package has none — and would
171
+ rewrite `settings.yaml` on every debounce with data that is derived and
172
+ reproducible. The ledger file stays the only store of record.
173
+
147
174
  ## Configuration
148
175
 
149
176
  Override the composition entry by its `id`:
@@ -186,7 +213,8 @@ The ledger file is left alone on purpose — delete
186
213
  ## Development
187
214
 
188
215
  ```bash
189
- npm test # 49 tests, no dependencies to install
216
+ npm install # devDependencies only: react, react-dom, and the slot registry DSH runs
217
+ npm test # 106 tests
190
218
  npm run verify # packaging invariants (dependency-free, no install scripts, no bare imports)
191
219
  ```
192
220
 
@@ -194,6 +222,15 @@ npm run verify # packaging invariants (dependency-free, no install scripts,
194
222
  spawning a child process per test file is blocked, use
195
223
  `npm run test:single-process`.
196
224
 
225
+ The devDependencies are **test-only**. They are never installed for a consumer:
226
+ the package ships with no dependencies, no peer dependencies and no install
227
+ scripts, and `pnpm` does not install a dependency's devDependencies. They exist
228
+ so the browser half can be checked against the real thing — React renders it, so
229
+ a hook-order or DOM-prop mistake fails instead of passing silently, and
230
+ `@deepseek-ai/dsh-client-ui-slots` validates the registration against the very
231
+ registry that DSH runs. Without them installed the affected tests skip with a
232
+ stated reason, so `npm test` still works from a fresh clone with no network.
233
+
197
234
  See [docs/VERIFICATION.md](docs/VERIFICATION.md) for what was actually verified
198
235
  and how, including the evidence behind the fork rule.
199
236
 
package/README.zh.md CHANGED
@@ -136,6 +136,29 @@ dsh-token-ledger export [选项] 导出 CSV 与 JSON
136
136
  (1 calls, 5100 tokens not yet flushed)
137
137
  ```
138
138
 
139
+ ## 设置页
140
+
141
+ 浏览器端会在设置侧边栏注册一个 **用量账本** 分区:
142
+
143
+ - **区间总计**:本月 / 本年 / 近 7 天三档可切换,每档显示 Token 总计、缓存命中率、
144
+ 调用次数,以及背后的四桶明细。缓存命中率定义为
145
+ `缓存读入 / (缓存读入 + 未命中输入)`——即输入中被缓存吸收的比例,因此完全不缓存的
146
+ 路由读数是 0%,而不是空白。
147
+ - **今日实时**:今天的 Token、命中率与调用次数,随每一步完成而变动。
148
+ - **用量热力图**:年 / 月 / 一周三档可切换;年和月是热力图,切到一周改为柱状图。
149
+ 热力档位相对窗口内最忙的一天取平方根,避免某一天特别大把其余全部压成最淡档。
150
+ - **按模型**:各模型的用量与各自命中率。
151
+
152
+ 页面只读一个路由 `GET /api/token-ledger/summary`,每 15 秒轮询一次。
153
+ 该路由会拒绝非回环来源,因此即使 web 服务器绑定到 `0.0.0.0` 也不会外泄。
154
+
155
+ 它需要带 web 服务器的 profile(`web` 或 `desktop`)。没有的话 `/tokens` 与 CLI
156
+ 照常可用,分区会明确说明而不是直接失败。
157
+
158
+ 概览**刻意不**通过设置命名空间下发:那需要 schema(一个真实依赖,而本包零依赖),
159
+ 而且每次防抖都会用可推导、可重放的数据重写一遍 `settings.yaml`。
160
+ 账本文件始终是唯一的记录来源。
161
+
139
162
  ## 配置
140
163
 
141
164
  按 `id` 覆盖组合条目:
@@ -172,13 +195,20 @@ dsh plugin --profile web remove @chenmiao8563/dsh-token-ledger
172
195
  ## 开发
173
196
 
174
197
  ```bash
175
- npm test # 49 个测试,无需安装任何依赖
198
+ npm install # 两个 devDependency:react 与 react-dom,供渲染测试使用
199
+ npm test # 96 个测试
176
200
  npm run verify # 打包不变式(零依赖、无安装脚本、无裸模块说明符)
177
201
  ```
178
202
 
179
203
  `npm test` 使用 Node 内置测试运行器。在禁止逐文件 spawn 子进程的受限环境里,
180
204
  改用 `npm run test:single-process`。
181
205
 
206
+ React **只是 devDependency**,消费者永远不会安装它:本包不带任何依赖、任何 peer
207
+ 依赖、任何安装脚本,而 pnpm 不会为依赖安装其 devDependencies。它在这里的作用是让
208
+ 浏览器端能用**真库**渲染并断言——这能抓到替身抓不到的东西:hook 顺序违规与非法
209
+ DOM 属性,React 会报出来,而手写的 `createElement` 会默默接受。没装它时这些渲染
210
+ 测试会带明确原因**跳过而不是失败**,所以全新克隆、无网络也能跑 `npm test`。
211
+
182
212
  实际验证了什么、怎么验证的(包括 fork 规则背后的证据)见
183
213
  [docs/VERIFICATION.md](docs/VERIFICATION.md)。
184
214
 
@@ -220,6 +220,83 @@ sessions against the 72 logs carrying a `session/end-seed` marker and the 29
220
220
  declaring a parent. The 61 sessions outside both sets turned out to be
221
221
  four-record logs with no usage at all, which is why they correctly have no entry.
222
222
 
223
+ ## Browser half (0.2.0)
224
+
225
+ What is checked here and what is not, because the difference is large.
226
+
227
+ **Checked by test.** `lib/overview.js` and `lib/route.js` are pure or near-pure
228
+ and are tested directly: range boundaries, cache-hit-rate arithmetic, gap-filled
229
+ series, the loopback and origin guard, method handling, and the 500-on-fault
230
+ path. The browser half is loaded through a stand-in module loader with a
231
+ stand-in React, which exercises the module wrapper, the registration contract,
232
+ and the whole render tree — formatting, heat levels, week-bar slicing, view
233
+ switching, and the empty, malformed and stale-data paths.
234
+ `scripts/verify-package.mjs` additionally fails the build if the browser half
235
+ requires anything the loader does not provide, if its module id stops matching
236
+ the package name, if it stops compiling as a classic script, or if a declared
237
+ client dependency is outside the client-half namespace.
238
+
239
+ **Checked by reading DSH's own source, not by inference.** The contracts the
240
+ browser half depends on were read out of the shipped bundles rather than copied
241
+ from a third-party plugin:
242
+
243
+ | Contract | Where it was read | Result |
244
+ | --- | --- | --- |
245
+ | The `settings.section` registration shape | the sections DSH ships — `dsh-client-ui-settings-models` (order 10), `-settings-plugins` (15) and `-ui-agent-preset` (20) — plus the shell in `-settings-general` that renders them via `renderSlot("settings.section", …, { only: active })` | identical field set; `locale` is optional, proven by `-settings-models` omitting it; the sidebar rows come from `id`, `order` and `label`, all of which this plugin supplies |
246
+ | The client module format | every `@deepseek-ai/dsh-client-*/lib/client.js` | byte-identical wrapper: `window.__ModuleLoader__.load({ id, factory })` with `var module = { exports: {} }` and `exports.apply`/`exports.inject` |
247
+ | When the factory runs | `dsh-client-modules` module docs | only registration happens at script execution; body side effects, styles included, run at materialization — which is why the stylesheet is installed inside `apply()` |
248
+ | The `dsh.client` declaration | `parseDshClient` in `dsh-client-modules/lib/index.js` | `platform` must be a string and only `web` is loaded; `inject` and `external` are optional string arrays; `external` — not `inject` — is what orders the module graph |
249
+ | What `dsh.client.inject` **contains** | the five `dsh.client` declarations DSH ships, two of them non-empty: `dsh-client-locale` and `dsh-client-ui-settings-general` | **package/entry ids, not service names.** A module's own exported `inject` is the separate service list. This plugin lists the two packages whose services it uses, which is the same convention, and an empty list is also legal (`dsh-client-connection` uses one) |
250
+ | What `external` contains | the module-graph walk in `dsh-client-modules/lib/client.js`, which runs every entry through `stripClientSuffix` before resolving it | module specifiers for *other rows*. This plugin requires only `react`, which the loader provides and no row owns, so it declares none |
251
+ | How `exports["./client"]` is located | `clientExportOf` in `dsh-client-modules/lib/index.js` | a plain string or an object with a string `default`; anything else throws. A plain string is used here, and `verify-package.mjs` pins it |
252
+ | Whether a **scoped** package id survives | `stripClientSuffix` in `dsh-client-modules/lib/client.js` | it removes only a trailing `/client`, so `@chenmiao8563/dsh-token-ledger` passes through intact. DSH's own client packages are scoped too, so the transport already carries ids containing `/` |
253
+
254
+ That reading corrected two real mistakes in the first draft of this half, both of
255
+ which would have been a silent failure rather than an error:
256
+
257
+ - `@deepseek-ai/dsh-client-ui-slots` was listed as a client dependency. It is a
258
+ pure core library (its manifest exports only `.`, and it declares no
259
+ `dsh.client`), so it is never a client module row.
260
+ - The settings shell package was listed as `-ui-settings`, which holds the
261
+ namespace-scope service; the section slot is declared and rendered by
262
+ `-ui-settings-general`.
263
+
264
+ **Checked under the real React.** `test/client-render.test.mjs` renders the
265
+ presentation component with React 18.3.1 — the same major version DSH bundles —
266
+ through `react-dom/server`, and asserts the actual markup: the range tabs and
267
+ their active state, the totals, hit rate and call count for each range, today's
268
+ block surviving a year-range selection, the year heat grid (one cell per day plus
269
+ five legend swatches), the month grid (every day of March), the week bars (seven,
270
+ with inline heights), bounded heat levels drawn from a five-entry palette,
271
+ proportional model bars, and the loading, error, stale and empty states. It also
272
+ captures `console.error` and `console.warn` and **fails if React complained at
273
+ all**, which is what catches an invalid DOM prop or a hook-order violation — the
274
+ class of defect a hand-rolled `createElement` accepts silently.
275
+
276
+ React is a devDependency, so this runs in CI and under `prepublishOnly`. Without
277
+ it installed the file skips with a stated reason rather than failing, which keeps
278
+ `npm test` working from a fresh clone with no network.
279
+
280
+ **Checked against the real slot registry.** `test/slot-registration.test.mjs`
281
+ feeds the registration `apply` actually builds into
282
+ `@deepseek-ai/dsh-client-ui-slots` — the registry DSH itself runs, pinned as a
283
+ devDependency to `0.1.2-rc.1`, the exact version this DSH bundles (`latest` on
284
+ npm is an older `0.0.1-rc.1`, so the version is pinned rather than ranged). It
285
+ asserts that the registry accepts the entry, that `resolveSlotLabel` resolves the
286
+ label thunk to a non-empty string — a label it could not resolve would render as
287
+ a blank sidebar row — that the entry id does not collide with the three sections
288
+ DSH ships, that a duplicate id is refused, that disposing retires the entry, and
289
+ that registering into an undeclared slot is refused, which is why the plugin uses
290
+ `slots.inject` rather than registering blind.
291
+
292
+ **Not checked.** Whether the client module loader accepts the file, whether the
293
+ shell renders the section, and how any of it looks. Nothing here executes
294
+ `window.__ModuleLoader__` or mounts the component into a DOM: the module's shape
295
+ is asserted to match every bundle DSH ships, and the registration is validated by
296
+ the real registry, but no single process loads the plugin the way the browser
297
+ does. The remaining risk is integration and visual — a first-run surprise, not a
298
+ contractual one.
299
+
223
300
  ## Not verified
224
301
 
225
302
  Stated plainly, because a verification file that only lists successes is not