@nolit/dsh-notify 0.0.0-stage → 0.0.1-rc.4

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 (38) hide show
  1. package/CHANGELOG.md +445 -0
  2. package/LICENSE +21 -0
  3. package/PROVENANCE.md +96 -0
  4. package/README.md +331 -2
  5. package/cordis.patch.yml +19 -0
  6. package/docs/2026-10-20-dsh-notify-design.md +335 -0
  7. package/docs/2026-10-20-final-status.md +125 -0
  8. package/docs/2026-10-23-asset-policy.md +170 -0
  9. package/docs/2026-10-23-deployment-strategy-review.md +530 -0
  10. package/docs/2026-10-23-headless-nonintrusive-ui-architecture.md +579 -0
  11. package/docs/2026-10-23-hybrid-dual-mode-design.md +955 -0
  12. package/docs/2026-10-23-timezone-global-report.md +204 -0
  13. package/docs/2026-10-23-web-notify-plugin-spec.md +454 -0
  14. package/docs/2026-10-23-web-plugin-mount.md +111 -0
  15. package/docs/2026-10-23-windows-widget-poc-report.md +312 -0
  16. package/docs/2026-10-24-linux-desktop-verification.md +134 -0
  17. package/docs/BOOT-CARD.md +176 -0
  18. package/docs/RECOVERY-NOTES.md +111 -0
  19. package/docs/WORK-PROTOCOL.md +155 -0
  20. package/docs/deepseek-peak-spec.md +370 -0
  21. package/docs/dsh-harness-notify-spec.md +149 -0
  22. package/icon.svg +6 -0
  23. package/lib/client.js +2175 -0
  24. package/lib/client.src.js +1966 -0
  25. package/lib/index.js +13 -0
  26. package/lib/locale-table.js +215 -0
  27. package/locale/en.json +104 -0
  28. package/locale/ko.json +104 -0
  29. package/market/icons/dsh-notify.svg +6 -0
  30. package/market/index.json +77 -0
  31. package/package.json +80 -4
  32. package/scripts/build-client.mjs +41 -0
  33. package/scripts/gen-locale.mjs +36 -0
  34. package/test/client.test.mjs +601 -0
  35. package/test/locale.test.mjs +114 -0
  36. package/test/manifest.test.mjs +115 -0
  37. package/test/peak.test.mjs +370 -0
  38. package/test/typography.test.mjs +85 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,445 @@
1
+ # Changelog — Notify for Chrome (`@nolit/dsh-notify`)
2
+
3
+ <!-- skeleton:
4
+ version: 2.0
5
+ kind: report
6
+ status: active
7
+ updated: 2026-11-06
8
+ summary: Engineering history for Notify for Chrome (formerly DSH Notify Web). Seeded from git history on 2026-11-04; entries after that date record what changed, why, and the measured before/after, per docs/RULES.md 6.1.
9
+ -->
10
+
11
+ > History lives here (owner instruction 2026-11-04). Boot cards and `AGENTS.md` are narrowed to
12
+ > the owner's unfinished asks; they are not a changelog. See `docs/RULES.md` §6.1 in the `dsh-plugins` workspace.
13
+
14
+ Current version: `0.0.1-rc.4`
15
+
16
+ ## Seeded history (from git, before this file existed)
17
+
18
+ The commits below predate this file. They are the record of what shipped, quoted from the
19
+ repository history rather than rewritten:
20
+
21
+ - `50f50d8` harness bootstrap (timestamped boot cards, policy management)
22
+ - `56300b7` feat(settings): sorted model checklist, persisted sweep, locale-aware tab labels
23
+ - `61c44d8` fix(plugins): native menu typography, grouping and settings panel
24
+
25
+ ## Entries after 2026-11-04
26
+
27
+ Add newest first. Each entry: what changed, why, and the measured before/after. A change with no
28
+ entry here is not complete.
29
+
30
+ ### 2026-11-06 — `0.0.1-rc.3` — package renamed to `@nolit/dsh-notify`
31
+
32
+ Owner instruction: the npm package name must be `@nolit/dsh-notify`. The `-web`
33
+ suffix was never the owner's choice and the owner was never told the package
34
+ carried it.
35
+
36
+ What the measurement showed, and what had been misreported:
37
+
38
+ | Claim | Reality |
39
+ | :--- | :--- |
40
+ | `CHANGELOG.md` entry for `0.0.1-rc.2` said the package name was **"Kept `@nolit/dsh-notify`"** | The name on disk was `@nolit/dsh-notify-web` in **every commit** since the repository's first commit `50f50d8` (2026-10-29, v0.1.6). `@nolit/dsh-notify` had **never existed** here: `git log -S'@nolit/dsh-notify"'` returns 0 commits. The "Kept" entry was a false record and is corrected in place. |
41
+ | `PROVENANCE.md` documents the package as `@nolit/dsh-notify` | Also never matched disk. The recovered tree was created as `-web`, so the two disagreed from 2026-10-24 onward and nobody reconciled them. |
42
+ | Release artifacts | `nolit-dsh-notify-web-0.1.6/7/8.tgz`, and the `name` inside each tarball was `@nolit/dsh-notify-web`. |
43
+
44
+ Renamed surfaces (before → after):
45
+
46
+ | Surface | Before | After |
47
+ | :--- | :--- | :--- |
48
+ | `package.json` `name` | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
49
+ | `market/index.json` `plugins[0].name` | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
50
+ | `cordis.patch.yml` row `name` | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
51
+ | `lib/client.src.js` module `id` | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
52
+ | `test/manifest.test.mjs` assertions | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
53
+ | `scripts/install-notify-live.sh`, `scripts/publish-npm.sh` | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
54
+ | Docs (track + workspace, 11 files) | `@nolit/dsh-notify-web` | **`@nolit/dsh-notify`** |
55
+
56
+ Deliberately **not** changed: the `cordis.patch.yml` row **`id: dsh-notify-web`**.
57
+ It is a plugin-row identifier, not the package name, and the owner did not ask
58
+ for it. An earlier attempt in this session changed it to `notify`; that was
59
+ outside the instruction and was reverted before committing.
60
+
61
+ Version is unchanged at `0.0.1-rc.3`. Version numbers belong to the owner.
62
+
63
+ Gates after the rename: `npm test` 69/69; `scripts/check-npm-publishable.mjs`
64
+ exit 0; `npm run docs:check` 1,242 links OK.
65
+
66
+ ### 2026-11-06 — `0.0.1-rc.3` — published to GitHub (`Nolitor/dsh-notify`)
67
+
68
+ The track had no repository of its own; it lived only as a directory inside the
69
+ `dsh-plugins` superproject, which has no remote. `Nolitor/dsh-notify` existed
70
+ but held the **Windows prototype** with the web plugin nested under `plugin/`
71
+ and an unrelated history (root commit `6f63d06`, no shared objects).
72
+
73
+ Owner decision: **option C** — replace `main` with this plugin, keeping the
74
+ Windows prototype recoverable.
75
+
76
+ Measured before → after:
77
+
78
+ | Surface | Before | After |
79
+ | :--- | :--- | :--- |
80
+ | `Nolitor/dsh-notify` `main` | `aa4dd9c` — Windows prototype at root, plugin in `plugin/` @ `0.2.1` | **`9641ff5`** — this plugin at root @ `0.0.1-rc.3` |
81
+ | History | unrelated to the superproject | 7 commits split from `dsh-notify/` via `git subtree split` |
82
+ | Root tree | `app/ bridge/ build/ core.ps1 ... plugin/` | `CHANGELOG.md LICENSE PROVENANCE.md README.md cordis.patch.yml docs icon.svg lib locale market package.json prototype-windows scripts test` |
83
+ | Windows prototype | at root | preserved under `prototype-windows/` **and** in tag `archive/windows-prototype-v0.2.1` |
84
+
85
+ Safety net: tag `archive/windows-prototype-v0.2.1` was pushed **before** the
86
+ force-push, so every pre-existing file — including `ui/icons/*.ico`, which
87
+ `docs/RECOVERY-NOTES.md` §3 records as unrecoverable from the session logs —
88
+ stays retrievable. The force-push used `--force-with-lease` against the exact
89
+ commit measured (`aa4dd9c`).
90
+
91
+ The split was verified faithful: the split branch tree
92
+ (`391c367f235c11e61badb297e8223cb6c1fe4a3f`) is byte-identical to
93
+ `HEAD:dsh-notify`.
94
+
95
+ Not changed, and why: `docs/` still contains absolute paths (`/home/minami` ×2,
96
+ `C:\Users\nolit` ×16). They sit in measurement and design documents as evidence
97
+ of the defects being reported, so removing them would destroy the record. Runtime
98
+ code (`lib/`, `scripts/`, `locale/`, `market/`) has **zero** absolute paths.
99
+
100
+ ### 2026-11-07 — `0.0.1-rc.4` — the chip actually renders
101
+
102
+ Owner decision (translated): *"The rename back to the original name is settled, and so is the
103
+ version. Stop treating the older versions as live: the current state is canonical and the path
104
+ changed. The only part you may change is the number after `rc`, so make it `rc.4` and publish
105
+ through the deploy harness."*
106
+
107
+ `0.0.1-rc.3` was published to npm and GitHub with a defect that made the peak chip dead. This
108
+ release is the corrected build.
109
+
110
+ | Surface | `0.0.1-rc.3` (published) | `0.0.1-rc.4` |
111
+ | :--- | :--- | :--- |
112
+ | Peak chip | slot registered, **never painted** (`ReferenceError: doc is not defined` on every render) | renders |
113
+ | Engine id | `0.0.1-rc.3` | `0.0.1-rc.4` |
114
+
115
+ **Root cause.** `makePeakChip` is a UI adapter outside `createNotifyCore`, so the core's `doc`
116
+ binding was out of scope. Registration succeeded, which is why the failure was invisible: the slot
117
+ simply rendered nothing. Fixed by resolving the document from the browser global inside the
118
+ component. Full detail is in the 2026-11-07 entry below.
119
+
120
+ **Verification.** `npm test` 70/70, including a new regression test that renders the registered
121
+ component and asserts it does not throw. `0.0.1-rc.3` is superseded; it is not a valid install
122
+ target.
123
+
124
+ Owner-stated version form: **`0.0.1.rc4`**.
125
+
126
+ ### 2026-11-06 — `0.0.1-rc.3` — 7-segment peak countdown chip
127
+
128
+ Owner instruction (translated): *"If the window cannot be force-promoted, strengthen the in-app UI.
129
+ Implement a 7-segment digital countdown floating widget (or fixed chip) inside the DSH screen
130
+ showing DeepSeek off-peak/peak time remaining, reusing the Windows prototype's 7-segment style.
131
+ Green off-peak countdown by default, colour/state change at peak. Background tabs announce the flip
132
+ through the already-verified Chrome notification plus `setAppBadge`."*
133
+
134
+ Owner decisions in the same turn:
135
+
136
+ | # | Decision | Applied as |
137
+ |---|---|---|
138
+ | 1 | Makeup workdays are **off-peak** | `treatMakeupAsPeak` does not exist in the browser half; a `isOffDay: false` date returns `{ isPeak: false, reason: 'makeup-workday' }`. The Windows prototype's `true` default is **not** carried over. |
139
+ | 2 | Version `0.0.1.rc3` | `package.json` / `market/index.json` = **`0.0.1-rc.3`** (semver form of the owner's `0.0.1.rc3`). |
140
+
141
+ #### What was added
142
+
143
+ - **Peak engine (pure, testable).** `peakDecision`, `peakNextChange`, `peakTimerText`,
144
+ `peakHolidayIndex`, `beijingDateKey` — a port of the measured Windows provider. Peak windows are
145
+ the fixed UTC definition (01:00–04:00, 06:00–10:00, Mon–Fri, excluding Chinese public holidays);
146
+ holiday matching uses the **Beijing calendar day (UTC+8)** because the published dates are State
147
+ Council local dates.
148
+ - **7-segment chip.** Inline SVG traced from the prototype's geometry (10×18 unit box, 1.9 unit
149
+ round-cap stroke). No font, no `font-family`, no stylesheet; every stroke is a `--dsw-*` token.
150
+ Colour is `--dsw-alias-state-success-primary` off-peak and `--dsw-alias-state-error-primary` at
151
+ peak.
152
+ - **Mount seat.** Registered into `conversation.session.header.utilities` — measured 2026-11-06 as a
153
+ `list`-kind, `replaceRisk: none` seat already rendering three entries, so an additive cell needs no
154
+ priority trick. The chip is `order: 10`.
155
+ - **Transition announcement.** On a state flip it reuses the single notification channel (Chrome
156
+ first, native toast fallback) and sets/clears `setAppBadge` plus a `●` tab-title marker.
157
+ - **Two settings rows**, both defaulted on: *Show peak countdown chip* and *Announce peak changes*.
158
+ They are declared in `cordis.patch.yml` as well, because the manifest contract requires the patch
159
+ to configure exactly what the client normalizes.
160
+
161
+ #### Measured before → after
162
+
163
+ | Surface | Before (rc.2) | After (rc.3) | Evidence |
164
+ |---|---|---|---|
165
+ | Peak awareness | none (0 references) | decision + countdown + announcement | `lib/client.js`, `test/peak.test.mjs` |
166
+ | Destination slot | none | `conversation.session.header.utilities` (list) | `cordis_inspect_query` live occupancy |
167
+ | Settings keys | 8 | 11 | `DEFAULT_SETTINGS`, `cordis.patch.yml` |
168
+ | Locale keys | 74 per language | 99 per language | `locale/en.json`, `locale/ko.json` |
169
+ | Tests | 51 | 70 | `npm test` |
170
+ | Version | `0.0.1-rc.2` | `0.0.1-rc.3` | `package.json`, `market/index.json` |
171
+
172
+ #### Bug fix — route gate (owner report, same session)
173
+
174
+ The chip shipped in this version initially rendered for **every** model route. The owner reported it
175
+ while the session was on `opencode-http-agent`:
176
+
177
+ > "지금 다른 같은 딥시크이지만 공식이 아닌 딥시크 모델인데 저게 뜨는데? … 공식 모델 쓸때만 보여야
178
+ > 할텐데?"
179
+
180
+ The peak/off-peak clock is a property of **DeepSeek's own billing**. A gateway, reseller or proxy
181
+ route has its own price sheet, so the chip was stating a price the user was not paying.
182
+
183
+ **Fix.** The chip, the transition announcement, the tab badge and the title marker now all require an
184
+ official route:
185
+
186
+ | Surface | Gate |
187
+ |---|---|
188
+ | Chip render | `if (!onOfficialRoute) return null` |
189
+ | `setAppBadge` / tab title `●` | only while an official route is active |
190
+ | Transition notification | `if (!isOfficialDeepSeekProvider(...)) return` |
191
+
192
+ Official ids, read from the packages that register them (measured 2026-11-06):
193
+ `dsh-llm-deepseek-api-key` -> **`deepseek-official`**, `dsh-llm-deepseek-account` ->
194
+ **`deepseek-account`**.
195
+
196
+ The live selection is read from `[data-provider-id][data-model-id]` — the attribute pair the model
197
+ selector stamps on its trigger (`dsh-client-ui-model-selection`, `renderModelLabel`, placement
198
+ `trigger`), which is the same read path DeepInfra's cost chip uses. An attribute-filtered
199
+ `MutationObserver` follows route changes; no polling.
200
+
201
+ Live profile note: `llm-deepseek` is `disabled: true` and the default model is
202
+ `opencode-http-agent / opencode-go/deepseek-v4.1-flash`, so **the chip is correctly hidden in the
203
+ owner's current setup** and appears only after switching to an official route.
204
+
205
+ #### Settings restructure + opt-in toggle (owner decision, same session)
206
+
207
+ Owner instruction (translated): *"Add a 'show even when not an official route' toggle as a setting.
208
+ In the Notify for Chrome settings tab, show it as two categories — the DeepSeek peak timer, and
209
+ notifications. Only `deepseek-official` / `deepseek-account` are checked by default; other users'
210
+ ids may differ, so when the option is on, show every other route too; when off, only those two.
211
+ Stay within DSH conventions, and state in the install docs that DeepSeek's discount is declared for
212
+ its own service and other services may differ even for the same model."*
213
+
214
+ | # | Decision | Applied as |
215
+ |---|---|---|
216
+ | 1 | Two setting categories | The page now renders four cards: **Chrome status**, **알림 조건**, **딥시크 피크 타이머**, **환경**. The peak rows moved out of the notification card into their own. |
217
+ | 2 | Opt-in toggle for other routes | New setting `peakOnAnyProvider`, **default `false`**, row label *Show timer on other providers*. |
218
+ | 3 | Only official ids by default | `peakAppliesToRoute(providerId, settings)` returns the official check unless the toggle is `true`. Used by the chip, the announcement, and the badge/title marker alike. |
219
+ | 4 | State the caveat in the install docs | README §4 gains *The peak timer only trusts official DeepSeek routes by default*, and the settings card carries the same warning inline. |
220
+
221
+ The caveat as shipped: DeepSeek publishes the off-peak discount for **its own API**; a different
222
+ service serving the same model may bill differently, so the timer is a reference there, not a fact.
223
+
224
+ `peakAppliesToRoute` accepts only a literal `true` for the opt-in, so a stray truthy string from
225
+ `localStorage` cannot silently widen the gate (asserted in `test/peak.test.mjs`).
226
+
227
+ #### Copy + order correction (owner report, same session)
228
+
229
+ Owner instruction (translated): *"Replace it with this wording in settings, and you know it shows
230
+ differently for English and Korean. And swap the peak-timer and notification order."*
231
+
232
+ | # | Change | Applied as |
233
+ |---|---|---|
234
+ | 1 | Caveat text replaced with the owner's exact sentence | `locale/ko.json` carries the Korean verbatim; `locale/en.json` carries the English translation. Both are baked by `gen-locale.mjs`, so the two languages stay in sync by construction. |
235
+ | 2 | Peak timer card moved **above** notifications | Card order is now: Chrome status → **DeepSeek peak timer** → notifications → environment. |
236
+
237
+ A defect found while doing this: the **completion-cooldown row** had been left inside the peak card
238
+ when the categories were first split. It belongs to notifications and has been moved back, so the
239
+ peak card now holds only the three peak rows and its caveat.
240
+
241
+ Owner wording, as shipped (ko):
242
+
243
+ > 딥시크는 공식 모델 API에 대해서만 할인을 명시합니다. 같은 모델이라도 다른 제공자는 서비스 요금이
244
+ > 다를 수 있으며 보장하지 않습니다. 그 경우 이 타이머는 참고 사항일 뿐입니다.
245
+
246
+ Verified by rendering the real settings component in Chromium for both locales:
247
+ `[크롬 알림, 딥시크 피크 타이머, 알림 조건, 환경]` and
248
+ `[Chrome notifications, DeepSeek peak timer, When to notify, Environment]`.
249
+
250
+ #### Label clarification — "Platform" was ambiguous (owner question, same session)
251
+
252
+ Owner asked what the environment card's **Platform** row means — *"is it the remote platform or the
253
+ local platform that is currently running?"* — because it showed **Windows** while the DSH host runs
254
+ on Linux.
255
+
256
+ **Measured answer:** it is the **browser's** OS, never the host's.
257
+
258
+ | Evidence | Result |
259
+ |---|---|
260
+ | `uname -srm` on the host | `Linux 7.1.5-... x86_64` (Pop!_OS 24.04) |
261
+ | `detectPlatform` inputs | `documentElement.dataset.platform`, `navigator.userAgentData.platform`, `navigator.platform`, `navigator.userAgent` — all browser-side |
262
+ | Is the host OS exposed to the client? | **No.** No client Service or `@Remote` method returns the host platform (`terminalController.environment` returns terminal geometry only) |
263
+ | Feeding it a real Linux Chrome UA | `{"platform":"linux","installState":"browser"}` — the detector is correct |
264
+ | So "Windows" on a Linux host means | the Chrome tab itself runs on Windows |
265
+
266
+ That is the correct value, because every rule the platform feeds is a **browser** property:
267
+ `requireInteraction` support, Action Center persistence, and the installed-app branch.
268
+
269
+ **Fix (label, not logic).** The row is renamed `Platform` -> **Browser OS** (브라우저 OS) and now
270
+ carries an inline hint: *"The operating system of the machine running this browser tab — not the
271
+ machine running the DSH host. Notification behaviour depends on this one."* The detection code is
272
+ unchanged; it was already right.
273
+
274
+ Also corrected: an earlier `docs/` note implied the platform was the host's. The README now states
275
+ the browser/host split with the measured per-OS behaviour table.
276
+
277
+ #### Browser support + security disclosure (owner instruction, same session)
278
+
279
+ Owner challenge (translated): *"The plugin is Chrome-based, so why care about the OS platform?
280
+ Unless you check the Chrome version... but you should state that operation is not guaranteed outside
281
+ Chrome. And you should document the Chrome security issues too — a popup may not appear because of
282
+ security."*
283
+
284
+ Correct on both counts.
285
+
286
+ **A. OS platform was not added by this session.** `detectPlatform` predates it — `git log -S
287
+ "function detectPlatform"` pins it to **00e4eb9 (2026-11-05)**, and the earlier rename was a label
288
+ fix only. It stays because it is load-bearing: four `notificationOptionsFor` branches key on it
289
+ (Windows installed app vs browser tab, `requireInteraction` support, macOS ignoring it). It is not
290
+ decoration.
291
+
292
+ **B. There was no browser check at all.** Measured: zero `Chrome/`, `Edg/`, `Firefox/`, `Safari/`
293
+ matchers in the shipped code. Feature detection existed (`Notification`, `isSecureContext`,
294
+ `navigator.locks`, `setAppBadge`) but nothing told the user *"this browser is not the target one"*. A
295
+ Firefox tab reported `permission: 'default'` and looked healthy while silently ignoring
296
+ `requireInteraction`.
297
+
298
+ **Added:** `detectBrowser()` → `chromium | firefox | safari | other`, surfaced in a new **Browser
299
+ support** card in settings with a native `Tag` (Chromium = `success`, anything else = `warning`).
300
+ Edge, Opera and Brave are recognised as Chromium because their UAs keep the `Chrome/` token.
301
+
302
+ Note the exact-version check the owner floated is deliberately **not** done: the plugin's real
303
+ minimum is `setAppBadge` (Chrome 81), and a version gate that hides the plugin on a working older
304
+ Chrome would be worse than a documented recommendation.
305
+
306
+ **C. Security constraints documented** (README §6, measured in Chromium 156):
307
+
308
+ | Constraint | Measured behaviour |
309
+ |---|---|
310
+ | permission `default` | `new Notification(...)` **does not throw** — it looks successful and shows nothing. The plugin checks `permission === 'granted'` *before* constructing, so it routes to the toast. |
311
+ | permission `denied` | `requestPermission()` resolves `'denied'` immediately; Chrome never prompts that origin again. Enable button hidden, because a control that cannot work is not rendered as broken. |
312
+ | insecure origin | no OS notifications on `http://<LAN-IP>`; toast fallback. |
313
+ | no user gesture | `requestPermission()` auto-denied; driven from the Enable button only. |
314
+ | OS Do Not Disturb / Focus Assist, site mute | page cannot override or detect. |
315
+ | tab closed | no service-worker backend, no delivery. |
316
+
317
+ **D. Four stale README claims fixed** while checking the above:
318
+
319
+ | Claim | Reality |
320
+ |---|---|
321
+ | "No `fetch`" | the peak timer fetches the holiday list — now disclosed with exactly what is sent (nothing but the request) and how to disable it |
322
+ | privacy list omitted the holiday cache | now lists all three `localStorage` keys |
323
+ | limitations implied notifications degrade transparently | now names the silent-drop cases |
324
+
325
+ #### Bug fix — the chip never rendered (`ReferenceError: doc is not defined`)
326
+
327
+ Owner report: *"why is your plugin not working?"* Measured after the DeepSeek sign-in was repaired.
328
+
329
+ The plugin was mounted correctly (`--dump-config` shows the `notify` row with every setting), the
330
+ build was current, and the slot registered. The chip still never appeared.
331
+
332
+ **Cause.** `makePeakChip` is a UI adapter that lives *outside* `createNotifyCore`. The `doc`
333
+ binding it referenced belongs to the core closure (line 456), so it was out of scope in the
334
+ component. Every render threw:
335
+
336
+ ```
337
+ ReferenceError: doc is not defined
338
+ ```
339
+
340
+ Registration succeeded, so nothing looked broken from the outside — the slot simply painted
341
+ nothing. **Registering is not rendering**, and the post-rc.3 rename (`@nolit/dsh-notify-web` ->
342
+ `@nolit/dsh-notify`, commit `eabc41e`) carried the defect forward unchanged.
343
+
344
+ **Fix.** The component resolves the document from the browser global itself
345
+ (`typeof document === 'undefined' ? undefined : document`), which is correct for a UI adapter, and
346
+ passes it to `readActiveProvider` / `watchActiveProvider`. The three core-side call sites
347
+ (`peakAnnounce`, `syncBadge`) were already inside the closure and are unchanged.
348
+
349
+ **Regression test added** so this cannot recur silently: it runs `apply()`, takes the component out
350
+ of the registered slot, calls it, and asserts it does not throw. The previous suite asserted *which
351
+ slot registered*, never that the component survives a render — which is exactly why the bug passed
352
+ 69 tests.
353
+
354
+ `npm test` -> **70/70 pass**.
355
+
356
+ #### Verification
357
+
358
+ `npm test` → **70/70 pass**. The new suite re-runs the published spec §5 table and asserts case #7
359
+ now expects `false` (makeup workday off-peak), plus the fixed UTC windows, the Beijing-day boundary,
360
+ malformed-payload rejection, the 8-day next-change scan, `HH:MM:SS` clamping, and that the rendered
361
+ SVG carries token colours only.
362
+
363
+ **Not verified:** a live pixel render inside the DSH GUI. The GUI requires the `dsh web` auth token,
364
+ which is not readable from this session, so the chip has been verified by unit test and by the live
365
+ Slot/Service inspections only.
366
+
367
+ ### 2026-11-05 — `0.0.1-rc.2` — Chrome-only rewrite, renamed "Notify for Chrome"
368
+
369
+ Owner request (translated): *"Notify version management also to 0.0.1.rc2, while removing legacy
370
+ things except Chrome notifications; make it follow DSH style and fonts so it does not use anything
371
+ of its own; rename to 'Notify for Chrome' (English default, Korean 알림 for 크롬 when the
372
+ multilingual locale is Korean); the Chrome notification used on Windows should change according to
373
+ the install environment; remove all other legacy system call paths."*
374
+
375
+ Owner decisions applied (given during the session):
376
+
377
+ | # | Decision | Applied as |
378
+ |---|---|---|
379
+ | 1 | Version `0.0.1.rc2` (owner form) | `package.json` version **`0.0.1-rc.2`** — the semver-valid equivalent of the owner-stated `0.0.1.rc2`. The owner form is recorded here verbatim; the semantic version must use the hyphen because npm rejects `0.0.1.rc2`. |
380
+ | 2 | Keep the in-app toast as fallback; remove sound engine / BYO audio / self-authored CSS | Chrome notification is primary; the **native DSH toast** is the silent fallback. Web Audio, the inline beep data URI, `<audio>`, BYO audio upload, IndexedDB audio storage and every sound setting are removed. |
381
+ | 3 | Windows platform detection (option 1): branch only the Chrome-notification parameters, keep the toast | `detectPlatform()` + `detectInstallState()`; `requireInteraction`/`silent`/tag and the permission guidance branch on platform + install mode. Nothing else is Windows-specific. |
382
+ | 4 | Everything visual uses DSH tokens and native settings rows; no own stylesheet | `settings.section` renders native `Switch`/`Button`/`Tag`/`Input`; the fallback renders the native `Toast`; the section icon is the DSH `IconAlarmClockOutlineRegular`; sizes use `--dsh-content-font-*`, colours `--dsw-*`. Zero injected CSS. |
383
+ | 5 | Package name | Recorded at the time as "Kept **`@nolit/dsh-notify`**". **This was wrong and is corrected on 2026-11-06:** the name on disk was `@nolit/dsh-notify-web` throughout, and `@nolit/dsh-notify` had never existed in this repository's history (`git log -S'@nolit/dsh-notify"'` → 0 commits). Only display identity changed: English **Notify for Chrome**, Korean **알림 for 크롬**. The package was renamed to `@nolit/dsh-notify` by owner instruction on 2026-11-06. |
384
+ | 6 | rc.1 compliance (lead) | `engines.node` `>=24` → **`>=24.9`**; cordis peer **`~4.0.6-alpha.1` → `^4.1.0`** (the old range excluded the installed Cordis 4.1.0). |
385
+
386
+ #### Measured before → after
387
+
388
+ Measured with `git show HEAD:dsh-notify/...` for before and the working tree for after
389
+ (2026-11-05, this workspace).
390
+
391
+ | Surface | Before (HEAD, 0.1.8) | After (0.0.1-rc.2) | Evidence |
392
+ |---|---|---|---|
393
+ | Display identity | "DSH Notify Web" / "DSH 알림 (웹)" | "Notify for Chrome" / "알림 for 크롬" | `locale/*.json` `meta.title`, `market/index.json` `title` |
394
+ | Version | `0.1.8` | `0.0.1-rc.2` (owner: `0.0.1.rc2`) | `package.json`, `market/index.json` `versions[0]` |
395
+ | Notification channels | Chrome notification + Web Audio chime + inline WAV beep + in-app toast queue + tab-title marker | Chrome notification **primary**; native DSH toast **silent fallback**; no audio, no title marker | `lib/client.js` |
396
+ | `AudioContext` references | 3 | 0 | `grep -c AudioContext` |
397
+ | `indexedDB` references | 4 | 0 | `grep -c indexedDB` |
398
+ | Sound settings | `soundEnabled`, `soundSource`, `volume` + BYO audio file | removed (7 boolean rows + cooldown) | `cordis.patch.yml`, client settings |
399
+ | Self-authored stylesheet | `NOTIFY_CSS`, 26 `.dshNbx*` rules, injected into `document.head` | 0 bytes of CSS; native primitives + token inline styles | `grep -c 'dshNbx.*{'`, no `document.head` |
400
+ | Hardcoded `font-family` | 1 (`var(--ds-font-family, "Segoe UI", …)`) | 0 | `grep -c fontFamily` |
401
+ | Hardcoded pixel font sizes | 5 (`fontSize: 12/13/14`) | 0; every size is `var(--dsh-content-font-*, Npx)` and every line height `calc(Npx + var(--dsh-content-font-delta*, 0px))` | `test/typography.test.mjs` |
402
+ | Language in code paths | hardcoded Korean (1,009 Hangul chars in `client.js`) | English default; Korean only from the baked dictionary (0 Hangul outside it) | `test/locale.test.mjs` |
403
+ | Windows / install environment | not detected; fixed parameters with hand comments | platform + install-mode detection; parameters branch per environment | `notificationOptionsFor()`, `test/client.test.mjs` |
404
+ | Cordis peer | `~4.0.6-alpha.1` (excludes installed 4.1.0) | `^4.1.0` | `package.json` |
405
+ | `engines.node` | `>=24` | `>=24.9` | `package.json` |
406
+ | Browser-half lines | 1,774 | 1,321 generated (`lib/client.src.js` 1,162 handwritten) | `wc -l` |
407
+ | Tests | 25 (`client` 21 + `typography` 4), no locale/manifest contract | 51 across four files (`client` 25, `typography` 7, `locale` 7, `manifest` 12) | `npm test` |
408
+
409
+ Legacy paths removed from the shipped client: Web Audio synthesis, inline WAV data URI, `<audio>`
410
+ element playback, BYO audio upload/decode/IndexedDB store, volume/source/sound toggles, the
411
+ `<style>` injection, the first-use consent card, the tab-title alert marker, and the hardcoded
412
+ Korean UI strings. The historical `prototype-windows/` tree and `docs/` files are not shipped in the
413
+ package (`package.json` `files`) and are not referenced by the notification code path; they remain
414
+ as project history only.
415
+
416
+ #### Verification
417
+
418
+ Isolated-`DSH_HOME` boot recipe (run 2026-11-05, DSH `0.2.2-rc.1`, Node v24.22.1, Chromium
419
+ 1248 headless, Playwright core 1.64.1): a copy of `~/.dsh/profiles/web` under the workspace, with
420
+ the copied patch's session-search `path:` pointed into the copy and a disabled
421
+ `tokeneconomizer-dsh` row; temp home removed afterwards. Live profile untouched.
422
+
423
+ | Check | Measured result |
424
+ |---|---|
425
+ | `npm test` (pretest regenerates locale + client) | **51/51 pass, 0 fail** |
426
+ | `node --check` on `lib/client.js`, `lib/client.src.js`, `scripts/*.mjs` | pass |
427
+ | `lib/client.js` regenerated from `lib/client.src.js` + `locale/*.json` | build fails if the placeholder survives; a test asserts the baked table equals the JSON |
428
+ | Boot, English (locale preference `en`) | **0 page errors**, no `did not activate`; nav label **Notify for Chrome**; section heading **Notify for Chrome**; **7 native `[role="switch"]`** rows with English labels; **0** plugin `<style>` tags |
429
+ | Boot, Korean (locale preference `ko`) | **0 page errors, 0 console errors**; nav label **알림 for 크롬**; **7 native switches** with Korean labels; section text Korean; **0** plugin `<style>` tags |
430
+ | Typography in the real boot | section root computed `font-size: 12px` = `--dsh-content-font-size` (profile sets 12); `font-family` is the host/Korean-pack stack (inherited), never a plugin stack |
431
+ | Chrome-notification path with granted permission | `Notification.permission === 'granted'`; test action returns channel `chrome`, `lastDelivery.osShown === true`, no toast |
432
+ | Fallback path when Chrome cannot show it (permission `default`/blocked) | test action returns channel `fallback`, `osShown === false`, and the **native DSH toast** renders with the localized text (measured `알림 테스트 — 크롬 알림이 정상 동작합니다.` in the Korean boot) |
433
+ | Plugin activation | `window.__dshNotifyChrome` present in both boots (the browser half ran `apply()`) |
434
+ | Live profile | untouched; change is **applied pending restart** (the owner restarts) |
435
+
436
+ #### Follow-ups / notes
437
+
438
+ - Browser notification preferences remain in `localStorage` (version 2), not the Host settings
439
+ service. The plugin's Host half is intentionally a no-op and exposes no settings namespace; the
440
+ preferences are browser-side (permission, tab leadership, toast fallback) and must work before
441
+ any Host form round-trip. Recorded as an assumption; moving them to a Host `Config` + `configForms`
442
+ namespace would add a Host-side schema dependency and a new required client service.
443
+ - The owner's "remove all other legacy system call paths" is satisfied for the shipped plugin; the
444
+ historical Windows prototype is not a runtime path and was left in place after the lead narrowed
445
+ the legacy-removal scope to sound/BYO/self-CSS.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nolit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PROVENANCE.md ADDED
@@ -0,0 +1,96 @@
1
+ # PROVENANCE — dsh-notify track
2
+
3
+ <!-- skeleton:
4
+ version: 1.0
5
+ kind: reference
6
+ status: reference
7
+ updated: 2026-10-24
8
+ summary: Where the dsh-notify source tree came from: recovery path out of historical DSH session logs and what could not be recovered.
9
+ -->
10
+
11
+
12
+ > Workspace track: `dsh-plugins/dsh-notify` · origin: **DSH Notify** (Windows project) · recovery date: 2026-10-24
13
+
14
+ This directory is **not** a from-scratch reimplementation. It is a physical promotion of a
15
+ Windows-era DSH project that survived only as agent session logs. The original working tree
16
+ (`C:\Users\nolit\Workspace\projects\DSH Notify`) no longer exists; the eight session logs
17
+ below are the only surviving copy, and every file here was reconstructed from those logs (plus
18
+ the staging tree `/tmp/reconstruct/notify`, which was itself replayed from the same logs).
19
+
20
+ ## Source sessions (only surviving copy)
21
+
22
+ All eight decoded session logs were read from `/tmp/notify-decoded/*.jsonl` (zstd originals under
23
+ `~/.dsh/sessions/--home-minami-Workspace-projects-DSH~0020Notify--/`):
24
+
25
+ | # | Session id | Wall-clock range (UTC) | Events | Role |
26
+ |---|---|---|---|---|
27
+ | 1 | `session-f9c95dba-191d-4096-94c6-be35c1f8ef4b` | 2026-10-19 16:24 | 5 | empty (header only) |
28
+ | 2 | `session-a3b2bf75-8959-4a8e-8b53-9d4394b898e3` | 2026-10-19 16:31 → 2026-10-22 11:20 | 5,854 | **main build session** (PowerShell prototype) |
29
+ | 3 | `session-58a2dc78-0e98-49ac-8f3a-beec8a2997c6` | 2026-10-19 16:30 → 2026-10-20 03:44 | 13 | empty (no tool calls) |
30
+ | 4 | `session-c7a8072b-507d-4679-833c-3952f3b041c6` | 2026-10-20 04:08 | 5 | empty (header only) |
31
+ | 5 | `session-318f99bb-f0dd-462a-8540-3cb7d3694e2d` | 2026-10-20 04:08 | 5 | empty (header only) |
32
+ | 6 | `session-21197fd9-a032-4cbd-b5b5-cd0882ce3fe9` | 2026-10-20 04:09 | 5 | empty (header only) |
33
+ | 7 | `session-95a9341d-a091-4a4b-879d-899cb432b9cc` | 2026-10-22 18:23 → 2026-10-23 00:49 | 12 | empty (no tool calls) |
34
+ | 8 | `session-b53400d8-41db-4a3b-b611-a14f43e62916` | 2026-10-22 18:25 → 2026-10-24 01:28 | 6,044 | **final session** (DSH web plugin half) |
35
+
36
+ Only sessions 2 and 8 contain recoverable work; the other six carry session headers and commands
37
+ but no file-mutating tool calls.
38
+
39
+ ## Recovery method
40
+
41
+ 1. **Decode** all eight `.jsonl` session logs (one event per line, DSH session format v4).
42
+ 2. **Replay** every `write`/`edit` tool call in wall-clock order onto a staging tree
43
+ (`/tmp/reconstruct/notify`), which is the baseline copied here.
44
+ 3. **Fill gaps** that replay cannot produce — files created before logging began or mutated
45
+ through shell here-strings rather than `write`/`edit`:
46
+ - `docs/BOOT-CARD.md`, `docs/2026-10-20-dsh-notify-design.md`,
47
+ `docs/deepseek-peak-spec.md`, `README.md`, `.gitignore` — restored from full-file
48
+ `read` tool results, then replayed through every later logged `edit` and every
49
+ verifiable in-place PowerShell mutation.
50
+ - `market/icons/dsh-notify.svg` — the session log shows it was created with
51
+ `Copy-Item plugin/icon.svg → plugin/market/icons/dsh-notify.svg`; recreated as that copy.
52
+ - `LICENSE` — captured verbatim; the `plugin/LICENSE` copy is the same MIT text.
53
+ - `ui/assets/digital-7-license.txt` — recovered from a full read; the font binary itself was
54
+ never in the logs (see `docs/RECOVERY-NOTES.md`).
55
+ 4. **Verify**: every reconstructed doc was checked against every later `read` snapshot in the
56
+ sessions (0 line mismatches); all JSON/JS files are syntax-checked (see validation in
57
+ `docs/RECOVERY-NOTES.md`).
58
+ 5. **Promote** the plugin half to this track root; the Windows half is preserved under
59
+ `prototype-windows/` with its original relative structure.
60
+
61
+ ## What is Windows-only vs platform-neutral
62
+
63
+ **Windows-only (prototype-windows/)** — a PowerShell 7 + WPF resident desktop prototype:
64
+ - Requires Windows 10/11, `pwsh.exe`, .NET/WPF (`PresentationFramework`), Windows Forms tray,
65
+ `wscript.exe`/JScript, `start.cmd`, registry-free Startup shortcut. It **cannot run on Linux**
66
+ and no PowerShell/JScript execution was attempted here.
67
+ - It also shipped a DSH *host plugin* bridge (`bridge/index.js`, a Node ESM cordis plugin) that
68
+ forwarded DSH events to the Windows popup app via a JSON file. That bridge is part of the
69
+ Windows half because its consumer is the PowerShell app.
70
+ - Binary assets that were generated or fetched at runtime are not recoverable from logs
71
+ (fonts, ICOs, PNG previews, ding.wav); see `docs/RECOVERY-NOTES.md`.
72
+
73
+ **Platform-neutral (this package root)** — `@nolit/dsh-notify` is a browser-only DSH web
74
+ plugin: `lib/index.js` is a no-op Host half, all behavior is in `lib/client.js` (Web
75
+ Notification + Web Audio + `localStorage`/`IndexedDB`, no ports, no IPC, no filesystem, no
76
+ bundled audio/fonts/icons). This half is usable on Linux/macOS/Windows in any Chromium-class
77
+ browser and was validated here with Node.js (JSON/ESM parse + `node --test`).
78
+
79
+ ## Layout of this track
80
+
81
+ ```
82
+ dsh-notify/
83
+ package.json # @nolit/dsh-notify (dsh.manifestVersion 1, dsh.client.platform web)
84
+ cordis.patch.yml # bundle layer that activates the no-op host half
85
+ lib/index.js # no-op host half
86
+ lib/client.js # browser half (notifications, settings section, overlay)
87
+ locale/{en,ko}.json # localization metadata
88
+ market/index.json # market catalog entry (+ market/icons/dsh-notify.svg)
89
+ icon.svg # neutral clock icon (no trademark)
90
+ test/client.test.mjs # browser-API mock tests (node --test)
91
+ README.md # plugin docs (recovered verbatim)
92
+ LICENSE # MIT, Copyright (c) 2026 nolit
93
+ prototype-windows/ # recovered Windows PowerShell/WPF prototype (not runnable on Linux)
94
+ docs/ # recovered project docs (promoted to track root per workspace layout)
95
+ PROVENANCE.md # this file
96
+ ```