dsh-fonttune 0.2.2 → 0.2.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.
- package/CHANGELOG.en.md +44 -0
- package/CHANGELOG.md +44 -0
- package/README.en.md +20 -5
- package/README.md +20 -5
- package/lib/client.js +913 -312
- package/lib/index.js +95 -11
- package/lib/shared.cjs +285 -145
- package/package.json +11 -6
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.2.4] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **One package installs on both host lines**: `0.1.5-rc.1` – `rc.3` (the host's `settings.installSection` plus the client's `settingsScope`) and `0.1.7-alpha.1` – `alpha.2` (the host's `settings.configure({auto:true}, ctx.fiber)` plus the client's `configForms`). The plugin looks at which dialect the running host offers, and **when neither is there it only applies its styles and registers no card** rather than parking the whole package. `package.json` now declares `engines.dsh` as the semver prerelease range `>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0` (a plain `>=0.1.5-rc.1` never matches `0.1.7-alpha.1`), and `dsh.compatibility.dshReleases` lists the five host lines that were actually exercised.
|
|
9
|
+
- **The card can appear in any of the three seats the two lines declare**, and the host picks which to render: the rc line's `settings.plugin.item` (claimed by settings namespace), the alpha's `plugins.item` (an entry on the Plugins page — `id: fonttune`, label from the dictionary's `card.title`, summary from `card.description`, the whole card only on its detail page, which is **how DSH's own shell / agent-loop / subagent / web-search register themselves**), and `plugins.bundle.config` for an installed profile package (claimed by the npm name `dsh-fonttune`, and rendered on that package's detail page).
|
|
10
|
+
- **Claiming the settings namespace now waits, then matches by signature**: the alpha's `configForms.describe()` is asynchronous, and the old code guessed a candidate by name before it answered, so the guess could land on somebody else's namespace and every write was refused by the host (on a real machine: the card opens, and the first edit reports "The setting was not saved"). With no candidate the scope is now `null` and a pending stand-in (the whole `getSnapshot`/`subscribe`/`set`/`unset` surface) is adopted once the describe view arrives, recognized by a **schema that carries both `uiFollowsDialog` and `stackDialog`** (real log: `served namespaces []` → `matched by served schema: fonttune`).
|
|
11
|
+
- **The host side unwraps volatile references**: on the alpha every field of `ctx.fiber.config` is a cosmokit volatile reference (`{ get(), [Symbol.for("cosmokit.volatile.write")](v) }`, frozen), and reading it as a plain object yields nothing — on a real machine the card's **first render was a column of empty controls and the injected stylesheet was 0 bytes**. `plainConfigValue()` now unwraps it deeply, so `styleRow()` sees an ordinary object (measured: a 973 B rule set with `Georgia` / `520`).
|
|
12
|
+
- **`inject` narrowed to the services both lines have** (`slots` / `locale`): declaring one that a line does not have (say the rc-only `settingsScope`) **parks the whole package** (`pending (waiting for service: …)`) and the web never boots. The settings scope is looked up at runtime with `ctx.get()`, and a missing host layer degrades to browser-local storage.
|
|
13
|
+
- **Both shapes of a write result are understood**: the rc line's `mutate` resolves `undefined` even when it failed, while the alpha's `set`/`unset` resolve a boolean (`false` = the host refused). `followWrite` now reports a synchronous `false`, a thenable resolving `false`, and a rejection in that same note beside the preset row.
|
|
14
|
+
- **`test/host-line-probe.mjs`, a live probe that is independent of the host line.** It asserts only what must hold on every line — the page boots without errors, the plugin owns exactly **one** stylesheet element and it is connected, the settings really reach the document (with `DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` set it also asserts the computed values), the configuration card is reachable from the settings sheet and renders (`.dfp-card`, whichever seat that line declares) and a control on it really writes through to the host. It joined the read-only group of `npm run verify:browser`. `DFP_PROBE=1` makes the plugin log its diagnostics (which namespaces are served, which one was claimed, what state the seat is in); `DFP_DUMP=1` makes the probe print the Plugins page grouped as the host renders it.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **No more `require` of host internals**: the alpha exports `IconPlusOutlineRegular` / `IconPlusOutlineMedium` and not the rc's `IconPlusOutline16`, so the `primitives` require is gone and the plus icon needed here is an inline SVG.
|
|
18
|
+
- **What rc.3 actually changed, measured** (so a later release can decide whether it needs a branch): hashing all 728 js/css files under `lib/` + `dist/` across rc.2 and rc.3 leaves **exactly 2 files different** — `@deepseek-ai/cosmokit/lib/index.js` (13336 → 17160 B, gaining the volatile primitives `createVolatile` / `isVolatile` / `volatileEntries` / `updateVolatile` and `Symbol.for("cosmokit.volatile.write")`) and `dsh-client-ui-sidebar/lib/client.js` (a build stamp only, `0.1.5-rc.2-449af19` → `0.1.5-rc.3-a5380f3`). The internal `@deepseek-ai/*` dependencies moved to `^0.1.5-rc.3`, and `@deepseek-ai/schemastery` / `@deepseek-ai/cordis` went from `^` to exact pins (3.18.2 / 4.0.2). **No typography, theme or settings-API change**, so the rc behaviour needs no branch for rc.3. The rc line's own schemastery 3.18.2 has **no** `.volatile()` (the alpha's 3.18.4 does), so the volatile mark on the root schema has to be **added when supported and skipped when not**.
|
|
19
|
+
- The compatibility table gained 0.2.4, and both READMEs describe how the runtime handles the two dialects.
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
- `test/run.mjs` (91 checks): the card's slot allow-list gained `plugins.item` / `plugins.bundle.config`, and the case is now "registers the card in every seat a supported host may declare", asserting the three seats plus the `plugins.item` id, label and component shape (summary from `card.description`, the page returning an object).
|
|
23
|
+
- `test/render-card.mjs` (23 checks): rendering resolves its seat as `settings.plugin.item` and falls back to `plugins.item`, so both lines' shapes are exercised.
|
|
24
|
+
- Live (managed instance + headless Edge): `host-line-probe` passes on `0.1.5-rc.2`, `0.1.5-rc.3` and `0.1.7-alpha.2` (insert-style loading), and once more on alpha.2 loaded as a bundle. On the alpha the `codeLigatures: 2` driven from the card lands in `profiles/dfp/cordis.patch.yml`; on the rc line it lands in `settings.yaml`.
|
|
25
|
+
- **CI caught a case the local dialect cannot show**: three schema cases in `test/run.mjs` read fields straight off `schema({…}).sans`. The release CI installs the newest schemastery (3.18.4, which has `.volatile()`), and a resolved field is then a **reference** rather than a value — all three were green locally (3.18.2) and red in CI. The host half now exports `plainConfigValue`, those cases read the config **the way the plugin reads it**, and each dialect's shape is asserted: with the modifier a field is a reference that has to be unwrapped, without it the field is the value. The devDependency moved to `^3.18.4` so local and CI exercise the same dialect (the rc line's unmarked shape is covered live on rc.2 / rc.3 by the probe).
|
|
26
|
+
|
|
27
|
+
## [0.2.3] - 2026-09-22
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **A refused settings write is no longer swallowed**: `scope.set` / `scope.unset` failures used to hang off an empty `catch` — the control snapped back to the stored value and the user believed it saved. Both a synchronous throw and a rejected promise now print "The setting was not saved: <reason>" (clipped at 160 characters, full text on hover) on the right of the preset row, using the same row that fades itself out after 2.6 s, so nothing else moves.
|
|
31
|
+
- **An export the clipboard refuses no longer hands over truncated JSON**: the old fallback cut the JSON to 120/200 characters and printed it in that one-line status row, which the user could not copy from. Three layers now: (1) the async Clipboard API; (2) outside a secure context (the remote HTTP origin) `navigator.clipboard` does not exist at all, so a hidden textarea plus `document.execCommand("copy")` — the legacy path still works there; (3) only when both fail does the **whole** JSON land in an auto-focused, fully selected, read-only box that reuses the field surface, where one copy shortcut is all that is left (it puts itself away on `onCopy`). The status row says just "Clipboard unavailable — press Ctrl+C to copy the selected text."
|
|
32
|
+
- **Three problems in the font picker**: (1) the four curated groups (monospace / CJK / Latin / generic) used to vanish whenever the local enumeration *worked* and a search was running, so a curated family this machine does not have could not be found by name at all; they are now always listed, filtered by the search text. (2) The local group no longer repeats what a curated heading already offers — a family is listed once per panel, and the curated heading wins, because on Windows nearly the whole Latin group is installed and letting the local list win would empty the short list. (3) A refused enumeration was cached **forever**, so granting the font permission in the browser changed nothing; a refusal is now trusted for 20 seconds and the next panel open retries — a still-blocked permission is rejected instantly without prompting again, so the retry costs nothing.
|
|
33
|
+
- **The token refresh polls cheaply**: the size axis has to re-read its base values after DSH writes its runtime tokens, and the old loop walked every property of every rule of every stylesheet every four seconds. Each tick now compares three cheap signals first (body's `style` attribute, the root class, the sheet count) and returns untouched when none moved; every eighth tick still does a full sweep, which is what catches an in-place rewrite inside the CSS-in-JS sheet, and `theme/change` still forces one. An offline counting stub proves it: no CSS read while the environment is unchanged, the very next cheap tick follows a moved inline token, and the eighth tick sweeps anyway.
|
|
34
|
+
- **The per-theme "code font inside the dialog" rule missed its theme prefix** (dormant: per-theme is off today). `pushScopedCodeFamily` emitted its scoped rule without `body[data-ds-dark-theme]`, so with per-theme on the dark set would re-assert the **light** code family inside a dark dialog. It is prefixed like every other rule now, and a case asserts that every per-theme rule starts with the theme attribute.
|
|
35
|
+
- **`test/split-verify.mjs` was measuring a retired axis**: it still wrote `sizeOffset` (the interface size axis retired in 0.2.0) and expected the conversation chain to follow, so it rightly went red on a real machine. It now writes `sizeOffsetDialog` + `sizeOffsetCode`, computes its ratios from the measured baseline (the conversation offset rides DSH's own size, the code axis scales over the 16px reference — the two expectations differ), and adds a live check that the retired interface size axis moves nothing.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **A smaller stylesheet**: the cascaded `:not(a):not(b):not(c)…` chain collapsed into one `:not(a,b,c…)`. That chain was copied into both dialog selectors and the interface rule, and each copy saves roughly 120 bytes: a real user's injected CSS went **2735 → 2440 B** with `:not(` occurrences **62 → 3**. Specificity really does drop (`:not(a):not(b)` sums its arguments, `:not(a,b)` takes the most specific one), so the risk was measured first: the entire DSH 0.1.5-rc.2 frontend holds exactly one `!important` typography declaration (`font-size` on inline markdown `code`, and this plugin never emits a `font-size`), no installed plugin adds another `!important` weight/family rule, every rule here is `!important` (which already beats the theme's non-important class rules) and the rules that could collide are held apart by these very exclusions rather than by their weight. A live re-run of WEIGHT OK passes all 17 checks (the interface moves 137 text elements, the markdown subtree 0). The per-theme prefix helper now splits a selector list on **top-level** commas only, so the commas inside `:not(a,b)` cannot cut a selector in half.
|
|
39
|
+
- **The retired size and line-height axes left the value surface**: `sizeOffset` and `lineHeight` (the interface's size and line height, which have emitted nothing since 0.2.0) stay in the schema and the document — old configurations still parse and `normalizeConfig` still carries them — but they are no longer part of a preset snapshot and no longer decide whether the dark set is a second set, so a light/dark pair that differs only there emits one stylesheet instead of two. "Reset every axis" **still clears them**, so a value nothing shows cannot hide in a document forever.
|
|
40
|
+
- **Dead code removed**: 31 dictionary keys nobody renders (both locales — the retired interface size/line-height copy, the per-theme editor, the old preset button row, the fixed preview label), one dead CSS rule (`.dfp-presetForm`) and seven dead exports (`MARKER`, `isShorthandToken`, `bodyTokenNames`, `uiLineTokenNames`, `deltaTokenNames`, `codeTokenNames`, `uiShorthandTokens`, `scaleFontShorthand`). `VALUE_FIELDS` / `RETIRED_FIELDS` / `DURABLE_FIELDS` now name the three ideas — value axes, stored-but-unrendered, every durable field — separately.
|
|
41
|
+
- **One entry point for the browser checks**: `npm run verify:browser` (`test/browser-verify.mjs`) runs the read-only `browser-probe` / `ui-walk` / `style-verify` by default, adds the four scripts that write the settings namespace under `--writers` (each snapshots the user layer and restores it before exiting), takes `--only a,b` for a subset, and prints a per-script time and exit-code summary. Two walk scripts were rewritten for the 0.2.x card: `slider-walk` expands the section it needs first (the card opens fully collapsed) and asserts "a drag writes nothing, a release writes, each axis lands on its own field"; `split-walk` writes a three-entry stack and asserts the simple-mode slots and remainder note, that a mode round-trip leaves the stack alone, and that picking a Western family in the real panel replaces exactly that slot. **A missing control is now a failure** — the old scripts printed "not found" and exited zero, which is a false green.
|
|
42
|
+
- Compatibility table gained 0.2.3.
|
|
43
|
+
|
|
44
|
+
### Tests
|
|
45
|
+
- `test/run.mjs` (86 checks): the picker's grouping rules (curated groups always present, no family twice, the local cap and sanitizing, the refusal-retry contract) and the polling gate with a counting stub (negative control included).
|
|
46
|
+
- `test/render-card.mjs` (21 checks): a second render after a state change keeping the hook order, both shapes of a refused write being reported, the export fallback handing over the whole JSON selected, reset clearing the retired axes, and a retired axis not being a value axis.
|
|
47
|
+
- Live (managed instance + headless Edge): WEIGHT OK (17 checks), SPLIT OK (9), SLIDER OK (9), SPLIT WALK OK (14), browser-probe and ui-walk green.
|
|
48
|
+
|
|
5
49
|
## [0.2.2] - 2026-09-16
|
|
6
50
|
|
|
7
51
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.2.4] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **一个包同时装上两条宿主线**:`0.1.5-rc.1` – `rc.3`(宿主 `settings.installSection` + 客户端 `settingsScope` 那一套)与 `0.1.7-alpha.1` – `alpha.2`(宿主 `settings.configure({auto:true}, ctx.fiber)` + 客户端 `configForms` 那一套)。插件在运行时看宿主到底给了哪一套,**两条都不在就只装样式、不注册卡片**,不会把整包 park 住。`package.json` 的 `engines.dsh` 写成语义化预发布区间 `>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`(写 `>=0.1.5-rc.1` 是匹配不到 `0.1.7-alpha.1` 的),`dsh.compatibility.dshReleases` 逐条列出五条实测过的宿主线。
|
|
9
|
+
- **卡片在两条线的三个座位上都能出现**,由宿主自己决定渲染哪一个:rc 的 `settings.plugin.item`(按设置命名空间认领)、alpha 的 `plugins.item`(插件页的插件条目,`id: fonttune`、标签取字典里的 `card.title`、摘要用 `card.description`、详情页才渲染整张卡——**DSH 自己的 shell / agent-loop / subagent / web-search 也是这么登记的**)、以及给已装 profile 包用的 `plugins.bundle.config`(按 npm 包名 `dsh-fonttune` 认领,落在该包的详情页)。
|
|
10
|
+
- **认领命名空间改成「先等、再按签名认」**:alpha 的 `configForms.describe()` 是异步的,以前抢在它应答前按名字猜候选,猜错就会落在别的命名空间上、每次写入都被宿主拒绝(真机表现:卡片能开、一改就报「设置没有保存成功」)。现在候选为空就返回 `null`,并挂一个「待定作用域」占位(`getSnapshot`/`subscribe`/`set`/`unset` 全套),等 describe 的视图到达后按 **schema 里同时含 `uiFollowsDialog` 与 `stackDialog`** 认出自己那一个再接管(真机日志:`served namespaces []` → `matched by served schema: fonttune`)。
|
|
11
|
+
- **宿主侧拆掉 volatile 引用**:alpha 的 `ctx.fiber.config` 里每个字段都是 cosmokit 的 volatile 引用(`{ get(), [Symbol.for("cosmokit.volatile.write")](v) }`,且被冻结),当普通对象读只会拿到空值——真机表现是**卡片第一次渲染是一整排空控件、注入的样式表 0 字节**。现在统一走 `plainConfigValue()` 深拆,`styleRow()` 拿到的是普通对象(实测样式行 973 B、`Georgia` / `520`)。
|
|
12
|
+
- **`inject` 收窄到两条线都有的服务**(`slots` / `locale`):声明一个某条线上不存在的服务(例如 rc 才有的 `settingsScope`)会把**整包 park 住**(`pending (waiting for service: …)`),Web 直接起不来。设置作用域改为运行时用 `ctx.get()` 免声明查找,找不到就按「没有宿主层」降级。
|
|
13
|
+
- **写设置的结果两种形状都认**:rc 的 `mutate` 即使失败也 resolve `undefined`,alpha 的 `set`/`unset` resolve 布尔(`false` = 宿主拒绝)。`followWrite` 现在把同步 `false`、resolve 成 `false` 的 thenable、以及 rejection 三种都报到预设行右侧那句提示里。
|
|
14
|
+
- **`test/host-line-probe.mjs`:与宿主线无关的真机探针**。只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表且已接入文档、设置真的作用到文档(设了 `DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进宿主。已并入 `npm run verify:browser` 的只读组。设 `DFP_PROBE=1` 时插件会往控制台打诊断行(服务了哪些命名空间、认领了哪个、座位当前状态),`DFP_DUMP=1` 时探针额外按分组打印插件页的条目。
|
|
15
|
+
|
|
16
|
+
### 变更
|
|
17
|
+
- **不再 require 宿主内部原语**:alpha 只导出 `IconPlusOutlineRegular` / `IconPlusOutlineMedium`(没有 rc 的 `IconPlusOutline16`),所以 `primitives` 的 require 整个去掉,用到的加号改成内联 SVG。
|
|
18
|
+
- **实测清楚 rc.3 到底改了什么**(供后续判断要不要为它开分支):把 rc.2 与 rc.3 的 `lib/` + `dist/` 共 728 个 js/css 逐个哈希比对,**只有 2 个文件不同**——`@deepseek-ai/cosmokit/lib/index.js`(13336 → 17160 B,新增 volatile 原语 `createVolatile` / `isVolatile` / `volatileEntries` / `updateVolatile` 与 `Symbol.for("cosmokit.volatile.write")`)和 `dsh-client-ui-sidebar/lib/client.js`(只差构建戳 `0.1.5-rc.2-449af19` → `0.1.5-rc.3-a5380f3`);依赖上内部包抬到 `^0.1.5-rc.3`,`@deepseek-ai/schemastery` / `@deepseek-ai/cordis` 从 `^` 改成精确钉版(3.18.2 / 4.0.2)。**排版、主题、设置 API 一处没动**,面向 rc 的行为在 rc.3 上不需要新分支。rc 线自带的 schemastery 3.18.2 **没有** `.volatile()`(alpha 的 3.18.4 才有),所以根 schema 上的 volatile 标记必须**有则加、无则跳**。
|
|
19
|
+
- 兼容表补 0.2.4(中英两份 README 都改了运行时双方言的处理说明)。
|
|
20
|
+
|
|
21
|
+
### 测试
|
|
22
|
+
- `test/run.mjs`(91 项):卡片槽位白名单加入 `plugins.item` / `plugins.bundle.config`,那条用例改成「每个受支持宿主可能派发的座位都注册」,断言三个座位与 `plugins.item` 的 id / 标签 / 组件形态(摘要取 `card.description`,页面交出一个对象)。
|
|
23
|
+
- `test/render-card.mjs`(23 项):渲染取值改为「`settings.plugin.item` 不在就取 `plugins.item`」,两条线的座位各跑一遍。
|
|
24
|
+
- 真机(受管实例 + 无头 Edge):`0.1.5-rc.2`、`0.1.5-rc.3`、`0.1.7-alpha.2` 各跑一次 `host-line-probe` 通过(插入式装载);alpha.2 另跑一次 bundle 式装载通过。alpha 上确认从卡片改的 `codeLigatures: 2` 真的写进 `profiles/dfp/cordis.patch.yml`,rc 上确认落在 `settings.yaml`。
|
|
25
|
+
- **CI 抓到一条本地测不出来的用例**:`test/run.mjs` 里三条 schema 用例直接读 `schema({…}).sans` 这类字段。发布 CI 装的是最新的 schemastery(3.18.4,带 `.volatile()`),于是解析出来的字段是**引用**而不是值,三条在本地(3.18.2)全绿、在 CI 全红。现在宿主半把 `plainConfigValue` 导出,这三条按**插件读配置的同一方式**取值,并断言两种方言各自的形态:有修饰符时字段是引用、拆开后才是值;没有修饰符时字段本身就是值。devDependency 同时抬到 `^3.18.4`,让本地与 CI 跑同一个方言(rc 线那套无修饰符的形态由真机探针在 rc.2 / rc.3 上覆盖)。
|
|
26
|
+
|
|
27
|
+
## [0.2.3] - 2026-09-22
|
|
28
|
+
|
|
29
|
+
### 修复
|
|
30
|
+
- **写设置失败不再被吞掉**:以前 `scope.set` / `scope.unset` 的失败只挂一个空的 `catch`——控件弹回旧值、用户以为改成功了。现在同步抛出的错误和 Promise 拒绝都会在预设行右侧显示一句「设置没有保存成功:<原因>」(超长截断到 160 字,悬停可看全文),走的是同一个 2.6 秒自动淡出的行,不占额外版面。
|
|
31
|
+
- **剪贴板不可用时,导出不再只给一段被截断的 JSON**:以前导出失败会把 JSON 截到 120/200 字塞进那一行提示里,用户根本拿不到完整内容。现在三级兜底:① 异步 Clipboard API;② 非安全上下文(远程 HTTP 入口)里 `navigator.clipboard` 压根不存在,退回隐藏 textarea + `document.execCommand("copy")` 这条老路(在非安全上下文里照样可用);③ 两条都失败才把**完整 JSON** 放进一个自动聚焦、已全选、只读的文本框(复用输入框样式),按一次复制快捷键即可,`onCopy` 之后自动收起。状态行只说一句「剪贴板不可用,按 Ctrl+C 复制下方已选中的内容」。
|
|
32
|
+
- **选字体面板的三个问题**:① 内置四组(等宽 / 中文 / 拉丁 / 通用)以前在「本机字体枚举成功**并且**正在搜索」时整组消失,于是本机没装的预设字体**搜名字都搜不到**;现在四组任何情况下都在(搜索时按关键词过滤)。② 本机分组不再重复预设组已经列出的字体,同一个字体在一个面板里只会出现一次(预设标题优先,因为它才是那张短名单);实测 Windows 上拉丁预设组几乎全部已安装,若让本机分组优先,预设组会被掏空。③ 枚举被拒(`queryLocalFonts` 拒绝)以前会**永久缓存**,用户去浏览器里授予字体权限后也不会再变;现在拒绝结果只信任 20 秒,之后再次打开面板就会重试——还是被拒时浏览器是**立即**拒绝、不会重新弹窗,所以重试没有代价。
|
|
33
|
+
- **令牌轮询的省电闸门**:界面字号轴需要在 DSH 运行时写入令牌后重新读取基准值,原来每 4 秒无条件把每个样式表的每条规则的每个属性扫一遍。现在每 tick 只比较三个廉价信号(`body` 的 `style` 属性字符串、`html` 的 class、样式表数量),三者都没动就直接返回;每第 8 次 tick 仍做一次全量扫描,接住 CSS-in-JS 样式表**就地改写**规则的情况;`theme/change` 事件照旧强制全量。离线用计数桩证明:环境不变时一次 CSS 都不读,内联令牌一变下一个廉价 tick 就立刻跟上,第 8 次 tick 照常兜底。
|
|
34
|
+
- **逐主题的「对话框内代码字体」规则漏了主题前缀**(休眠缺陷,逐主题当前关闭):`pushScopedCodeFamily` 生成的作用域规则没带 `body[data-ds-dark-theme]`,一旦开启逐主题,深色那一套会用**浅色**的代码字体覆盖深色对话框。现在与其他规则一样带前缀,并加了「每一条逐主题规则都必须以主题属性开头」的用例。
|
|
35
|
+
- **`test/split-verify.mjs` 测的是早已退役的轴**:脚本还在写 `sizeOffset`(0.2.0 起退役的界面字号轴)并期望对话链路跟着动,真机自然红。现在它写 `sizeOffsetDialog` + `sizeOffsetCode`,比值按**实测基准**算(对话轴加在 DSH 自己的字号上、代码轴按 16px 基准缩放,两者不同),并新增一条「退役的界面字号轴什么都不动」的真机断言。
|
|
36
|
+
|
|
37
|
+
### 变更
|
|
38
|
+
- **样式表瘦身**:把级联的 `:not(a):not(b):not(c)…` 收成一个 `:not(a,b,c…)`,那条排除链原本会被复制进对话框规则的两个选择器与界面规则各一份,每份省约 120 字节。用户真实配置的注入样式 **2735 → 2440 B**,`:not(` 出现次数 **62 → 3**。**特异性确实被降低了**(`:not(a):not(b)` 累加参数特异性,`:not(a,b)` 只取最高的那个),所以先量了风险:DSH 0.1.5-rc.2 全部前端资产里只有一条 `!important` 排版声明(markdown 行内 `code` 的 `font-size`,而本插件从不写 `font-size`),已装插件里也没有第二条 `!important` 的字重/字体规则;本插件的规则全部带 `!important`(本来就赢得过主题那些非 important 的 class 规则),而且彼此靠这些排除列表**互不重叠**。真机复测 WEIGHT OK 17 项全过(界面移动 137 个文本元素、markdown 子树 0 个)。顺带把逐主题用的前缀函数改成只按**顶层逗号**切分选择器列表(`:not(a,b)` 里的逗号不再被切断)。
|
|
39
|
+
- **退役的字号/行高轴退出「值表面」**:`sizeOffset` 与 `lineHeight`(界面的字号与行高,0.2.0 起不产生任何规则)仍然留在 schema 与文档字段里(老配置照常解析、`normalizeConfig` 照常带出来),但不再进预设快照、不再参与明暗对比——只在这两个字段上有差别的明暗配置不再生成第二份样式表;卡片上的「全部重置」**仍然会清掉它们**,免得一个永远不显示的值烂在文档里。
|
|
40
|
+
- **清理死代码**:删掉 31 个不再渲染的词典键(中英各 31 个,含退役的界面字号/行高、逐主题编辑器、旧预设按钮组与固定预览的文案)、1 条死 CSS(`.dfp-presetForm`)与 7 个死导出(`MARKER`、`isShorthandToken`、`bodyTokenNames`、`uiLineTokenNames`、`deltaTokenNames`、`codeTokenNames`、`uiShorthandTokens`、`scaleFontShorthand`)。`VALUE_FIELDS` / `RETIRED_FIELDS` / `DURABLE_FIELDS` 三个清单把「值轴」「仍存储但不渲染」「全部持久字段」分开写清楚了。
|
|
41
|
+
- **`npm run verify:browser` 统一入口**(`test/browser-verify.mjs`):默认跑只读的 `browser-probe` / `ui-walk` / `style-verify`,`--writers` 再加会写设置命名空间的四支(各自快照并在退出前还原),`--only a,b` 挑子集,逐支打印耗时与退出码并汇总。两支走查脚本按 0.2.x 的卡片重写:`slider-walk` 先展开分区再拖(卡片默认全部收起),断言「拖动不写、松手才写、每条轴落到自己的字段」;`split-walk` 先写入三档字体栈,断言简单模式两格与剩余提示、模式往返不改动栈、真实面板里选一个西文字体只替换西文槽。**找不到控件现在是失败**(以前打印一句「not found」然后退出 0,等于假绿)。
|
|
42
|
+
- 兼容表补 0.2.3。
|
|
43
|
+
|
|
44
|
+
### 测试
|
|
45
|
+
- `test/run.mjs`(86 项):新增面板分组规则(预设四组恒在、同字体不重复、本机分组上限与消毒、拒绝后的重试契约)与轮询闸门的计数用例(含负对照:去掉闸门后该用例红)。
|
|
46
|
+
- `test/render-card.mjs`(21 项):新增「第二次渲染(状态变化后 hook 顺序不变)」「拒绝写入的两种形态都上报」「导出兜底交出完整 JSON 并被选中」「重置清掉退役轴」「退役轴不再是值轴」。
|
|
47
|
+
- 真机(受管实例 + 无头 Edge):WEIGHT OK(17 项)、SPLIT OK(9 项)、SLIDER OK(9 项)、SPLIT WALK OK(14 项)、browser-probe / ui-walk 通过。
|
|
48
|
+
|
|
5
49
|
## [0.2.2] - 2026-09-16
|
|
6
50
|
|
|
7
51
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -14,21 +14,33 @@
|
|
|
14
14
|
- **Code ligatures** — a three-mode switch (default / on / off; browsers enable them by default, turning them off restores `=>` and `!=` as plain characters), plus an advanced `font-feature-settings` value field
|
|
15
15
|
- **Refuse synthetic styles** — stops the browser from faking italic and bold for CJK faces, which have neither, so marked text stays upright
|
|
16
16
|
- **The interface-follows-conversation switch** — a "Follows the conversation" switch at the top of the interface section (on by default): on, the interface takes the conversation's **font and weight** and its own two controls stay hidden; off, it uses its own font and weight, independent of the conversation. Size and line height belong to the conversation only (the interface has no hooks for them)
|
|
17
|
-
- **Presets** — a dropdown select right under the edit mode: five built-in presets out of the box, pick one to apply it; with a preset selected every change auto-saves into it, and "Rename / Export / Import" manage the list (export as JSON, import-merge a paste)
|
|
17
|
+
- **Presets** — a dropdown select right under the edit mode: five built-in presets out of the box, pick one to apply it; with a preset selected every change auto-saves into it, and "Rename / Export / Import" manage the list (export as JSON, import-merge a paste). When the clipboard is unavailable (a non-secure context, or no clipboard permission) the export does not print a truncated message: the whole JSON goes into a selected, read-only box, so the copy shortcut works, and the box puts itself away once copied. A settings write the document **refuses** is reported on the same row instead of leaving the control quietly showing the old value
|
|
18
18
|
- **West / CJK split (simple mode)** — an edit-mode switch: simple mode gives just a Western and a CJK single-pick slot per set, advanced mode is the full stack editor; both views share one stack, so toggling never changes the order you set up
|
|
19
|
-
- **Font picker panel** — four
|
|
19
|
+
- **Font picker panel** — four curated groups (monospace / CJK / Latin / generic), always listed — while searching and even when enumeration fails — plus an "installed on this machine" group on Chromium that lists only what the curated groups do not, so no family is ever offered twice. A name that is not in the list can be created with "use xxx". A refused font permission is retried after a while, so granting it in the browser and reopening the panel fills the list in
|
|
20
20
|
- **Drag to reorder** — picked fonts are draggable chips with earlier/later buttons too (keyboard and touch friendly)
|
|
21
21
|
- **Per-section previews** — expanding the conversation / interface / code section ends with that section's own preview box, rendering just that part with the current configuration
|
|
22
22
|
|
|
23
23
|
## Compatibility
|
|
24
24
|
|
|
25
|
+
**One package serves both host lines, 0.1.5-rc.x and 0.1.7-alpha.x**, and the install is the same on either: both were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
|
|
26
|
+
|
|
25
27
|
| Plugin version | Supported DSH versions |
|
|
26
28
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
29
|
+
| **0.2.4** (latest) | 0.1.5-rc.1 / rc.2 / rc.3, 0.1.7-alpha.1 / alpha.2 |
|
|
30
|
+
| 0.2.3 | 0.1.5-rc.2 |
|
|
31
|
+
| 0.2.2 | 0.1.5-rc.2 |
|
|
28
32
|
| 0.2.1 | 0.1.5-rc.2 |
|
|
29
33
|
| 0.2.0 | 0.1.5-rc.2 |
|
|
30
34
|
|
|
31
|
-
`engines.dsh`
|
|
35
|
+
`engines.dsh` is declared as **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`** (a prerelease needs its own branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.1` never matches `0.1.7-alpha.1`). Every difference between the lines is settled at runtime rather than by version number:
|
|
36
|
+
|
|
37
|
+
- **The settings service**: up to 0.1.5-rc.x it is `settingsScope.bind({namespace})` (by namespace); from 0.1.7-alpha.x it is `configForms.get(<profile entry id>)` (by entry id, which the installing profile's patch decides — so the plugin claims the form whose served schema names its own fields). Both expose the same `getSnapshot`/`subscribe`/`set`/`unset` face, so one implementation covers them.
|
|
38
|
+
- **The configuration card's seat**: the rc line has a keyed cell under Settings → Plugins → Plugin configuration (`settings.plugin.item`); the alpha line dropped that slot and instead contributes an entry to the Plugins page's official list (`plugins.item`, which is how the built-in settings pages are added there), plus a per-package page for a profile that installed this plugin as a bundle (`plugins.bundle.config`). All three are registered; the one a host never declares is inert.
|
|
39
|
+
- **The host half's config values**: once the alpha marks the schema `.volatile()`, the fields the host half receives are cosmokit volatile references (`{get(),[write]}`) rather than plain values, which left the first-frame stylesheet row empty; the references are unwrapped before use.
|
|
40
|
+
- **`inject` declares only services both lines have** (`slots`, `locale`): declaring one a host does not provide parks the whole package — on the alpha a declared `settingsScope` kept the entire Web UI from booting. The settings service is looked up with `ctx.get(name)` instead.
|
|
41
|
+
- **The alpha's form is addressed by entry id**: while the host's `describe()` view has not arrived, the plugin does not guess a name (a wrong guess means every write is refused) — it stands in with a pending scope, adopts the real form as soon as the view is served, and re-reads immediately.
|
|
42
|
+
|
|
43
|
+
The market entry also states these requirements (`engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`), so the host a version needs can be checked before installing.
|
|
32
44
|
|
|
33
45
|
## Install
|
|
34
46
|
|
|
@@ -72,7 +84,10 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
|
|
|
72
84
|
- `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships a **stamped** `<style>` into the served index through `webserver/index-inject`, so the first paint already uses the saved values; the browser half then **adopts and keeps that element** (there is only ever one copy of the stylesheet, so rules can be added *and* removed)
|
|
73
85
|
- `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
|
|
74
86
|
- `build.mjs` — zero-dependency build: inlines the shared core, wraps the bundle in the `window.__ModuleLoader__.load({id, factory})` shell, copies the host half, and enforces that the client bundle only requires shell-held modules
|
|
75
|
-
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class)
|
|
87
|
+
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity, the picker's grouping rules, the token-refresh power gate); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three). The devDependency is the **newest** schemastery (`^3.18.4`) — the alpha's dialect, whose `.volatile()` makes a resolved field a **reference** rather than a value — so these suites read values through the `plainConfigValue` the host half exports, the same path the plugin takes; at runtime the **host's own copy** is used (3.18.2 on rc.2 / rc.3, with no such modifier, a shape the live probe covers)
|
|
88
|
+
- Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
|
|
89
|
+
- `test/host-line-probe.mjs` is the **host-line-independent** check: it asserts only what must hold on every supported line — the page boots without errors, the plugin owns exactly ONE stylesheet element, the settings really reach the document (`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` also assert the computed values when set), the configuration card is reachable from the settings sheet and renders (`.dfp-card`, whichever seat that line declares), and changing a control there really writes through. Run it first after switching host lines (rc ↔ alpha); set `DFP_PROBE=1` when debugging "the card's writes do not stick on the alpha" — the plugin then logs one `[dfp-probe]` line (which namespaces are served, which one was claimed, and what state the seat is in)
|
|
90
|
+
- `test/weight-verify.mjs` measures all three weight axes (interface / conversation / code) in a live page, including that there is only one stylesheet element and that switching following off drops the followed weight without a reload; `test/split-verify.mjs` measures the conversation and code size axes plus the **retired interface size axis moving nothing**; `test/slider-walk.mjs` and `test/split-walk.mjs` drive the real card (a drag writes nothing, a release does; the simple-mode slots and the whole stack round-trip); the market-entry tooling is `test/market-pr.mjs` (status / update / refresh / reopen / open / about) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
|
|
76
91
|
- `docs/` — the 0.2.0 [research](docs/research-0.2.0.md) and [specification](docs/spec-0.2.0.md) records (written during design, with the trade-offs of that moment; where they differ from what shipped, README/CHANGELOG win — see the note at the top of each). **Not part of the npm `files` list, so they are not published**
|
|
77
92
|
- The client bundle may only require modules from the shell's static table (`react`, `react/jsx-runtime`, `react-dom`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`…); `dsh.client.inject` declares load order, not require permission
|
|
78
93
|
- Build before refreshing: `node build.mjs` (or `npm run watch`) turns `src/` into `lib/`, and the browser loads `lib/` — editing `src/client.js` without building leaves the page on the previous bundle. `npm test` only checks behaviour; `node test/artifacts.mjs` (part of `npm run verify`) proves the committed `lib/` is byte-for-byte what the current `src/` produces. Editing the host half (`src/index.mjs`) or `cordis.patch.yml` needs a DSH restart
|
package/README.md
CHANGED
|
@@ -14,21 +14,33 @@
|
|
|
14
14
|
- **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
|
|
15
15
|
- **拒绝合成样式** —— 禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰)
|
|
16
16
|
- **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
|
|
17
|
-
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON
|
|
17
|
+
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)。剪贴板不可用时(非安全上下文、或没有剪贴板权限)导出不会只给一段被截断的提示,而是把完整 JSON 放进一个已选中、只读的文本框,按复制快捷键即可,复制后自动收起;任何**写设置失败**(宿主拒绝、只读部署等)都会在当前行显示出来,而不是让控件悄悄弹回旧值
|
|
18
18
|
- **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
|
|
19
|
-
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium
|
|
19
|
+
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;四组预设**任何情况下都在**(包括搜索时,也包括枚举本机字体失败时),本机分组只列出预设分组没有的字体,所以同一个字体不会出现两次;搜索不到的名字可以直接「使用 xxx」新建。字体枚举被浏览器拒绝时会在一段时间后自动重试(用户在浏览器里授予字体权限后,重新打开面板即可看到本机字体)
|
|
20
20
|
- **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
|
|
21
21
|
- **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
|
|
22
22
|
|
|
23
23
|
## 兼容性
|
|
24
24
|
|
|
25
|
+
**一份包同时支持 0.1.5-rc.x 与 0.1.7-alpha.x 两条宿主线**,安装方式与宿主线无关:两条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
|
|
26
|
+
|
|
25
27
|
| 插件版本 | 支持的 DSH 版本 |
|
|
26
28
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
29
|
+
| **0.2.4**(最新) | 0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2 |
|
|
30
|
+
| 0.2.3 | 0.1.5-rc.2 |
|
|
31
|
+
| 0.2.2 | 0.1.5-rc.2 |
|
|
28
32
|
| 0.2.1 | 0.1.5-rc.2 |
|
|
29
33
|
| 0.2.0 | 0.1.5-rc.2 |
|
|
30
34
|
|
|
31
|
-
`engines.dsh`
|
|
35
|
+
`engines.dsh` 声明为 **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行,写成 `>=0.1.5-rc.1` 匹配不到 `0.1.7-alpha.1`)。两条线的差别**全部在运行时二选一,不看版本号**:
|
|
36
|
+
|
|
37
|
+
- **设置服务**:0.1.5-rc.x 是 `settingsScope.bind({namespace})`(按命名空间),0.1.7-alpha.x 换成了 `configForms.get(<profile 条目 id>)`(按条目 id,id 由安装方的 profile patch 决定,所以插件靠「宿主服务的那份 schema 里有没有本插件自己的字段」来认领)。两者都是 `getSnapshot/subscribe/set/unset` 同一张脸,插件只写一套。
|
|
38
|
+
- **配置卡片座位**:rc 线是 设置 → 插件 → 插件配置 里的一个 keyed 单元(`settings.plugin.item`);alpha 线删掉了这个槽位,改成在「插件」页的官方插件列表里贡献一个条目(`plugins.item`,内置那些设置页就是这么挂的),并另外提供「已安装成 bundle 时」的每包页面(`plugins.bundle.config`)。三个座位都注册,宿主不声明的那个是惰性的。
|
|
39
|
+
- **宿主半的配置来源**:alpha 把 schema 标成 `.volatile()` 之后,宿主半拿到的配置字段是 cosmokit 的 volatile 引用(`{get(),[write]}`)而不是普通值,首帧样式行会因此变空;插件先把引用解出来再用。
|
|
40
|
+
- **`inject` 只声明两条线都有的服务**(`slots`、`locale`):声明一个宿主没有的服务会把整包 park 住——alpha 上声明 `settingsScope` 会让整个 Web 界面起不来。设置服务一律用 `ctx.get(name)` 现查。
|
|
41
|
+
- **alpha 的表单是「条目 id + 描述视图」**:宿主 `describe()` 还没答话时,插件不会去猜一个条目名(猜错的话每次写入都会被告主拒绝),而是先用一个等待态座位顶住,等宿主把视图发出来再接管并立刻重读。
|
|
42
|
+
|
|
43
|
+
市场条目同时声明这些兼容信息(`package.json` 的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies`),安装前可据此判断这一版需要的宿主版本。
|
|
32
44
|
|
|
33
45
|
## 安装
|
|
34
46
|
|
|
@@ -72,7 +84,10 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
72
84
|
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
|
|
73
85
|
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
|
|
74
86
|
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
|
|
75
|
-
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS
|
|
87
|
+
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)。devDependency 钉的是**最新的** schemastery(`^3.18.4`),也就是 alpha 那套方言——它带 `.volatile()`,解析出来的字段是**引用**而不是值,所以这几套检查都通过宿主半导出的 `plainConfigValue` 取值(和插件读配置同一条路);运行时用的是**宿主自带的那份**(rc.2/rc.3 是 3.18.2,没有该修饰符,那条形态由真机探针覆盖)
|
|
88
|
+
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
|
|
89
|
+
- `test/host-line-probe.mjs` 是**与宿主线无关**的那一支:只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表、设置真的作用到文档(`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 给了就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进去。换宿主线(rc ↔ alpha)后先跑它;调试「alpha 上卡片写不进去」这类问题时设 `DFP_PROBE=1`,插件会往控制台打一行 `[dfp-probe]`(服务了哪些命名空间、认领了哪个、座位当前是什么状态)
|
|
90
|
+
- `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」;`test/split-verify.mjs` 量对话/代码两条字号轴的独立性,外加**退役的界面字号轴什么都不动**;`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
|
|
76
91
|
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
|
|
77
92
|
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
|
|
78
93
|
- 改完源码要**先构建再刷新**:`node build.mjs`(或 `npm run watch`)把 `src/` 生成到 `lib/`,浏览器加载的是 `lib/`,只改 `src/client.js` 不构建的话页面看到的还是旧包;`npm test` 只检查功能,`node test/artifacts.mjs`(在 `npm run verify` 里)专门校验 `lib/` 与当前 `src/` 逐字节一致。改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
|