dsh-structured 0.8.2 → 0.9.1

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/src/index.js CHANGED
@@ -52,16 +52,20 @@ const k = (x) => (typeof x.volatile === 'function' ? x.volatile() : x)
52
52
  /** volatile 字段在 host 侧是可读信号;普通字段原样返回。 */
53
53
  const readConfig = (x) => (x && typeof x.get === 'function' ? x.get() : x)
54
54
 
55
+ /** 主题白名单:host 与 client 共用同一组值。 */
56
+ export const THEMES = ['deepseek', 'codex', 'anthropic']
57
+
55
58
  /**
56
59
  * DSH 插件配置 schema:设置表单据此渲染,并持久化到 active profile 的 Cordis patch。
57
- * 不把 visual 写进 state.json,避免 structured-test 与 desktop 共享同一状态文件。
60
+ * 不把主题/视觉写进 state.json,避免 structured-test 与 desktop 共享同一状态文件。
58
61
  */
59
62
  export const Config = Schema.object({
60
- visual: k(Schema.boolean().default(true)),
61
- defaultEnabled: k(Schema.boolean().default(true)),
62
- tailReminder: k(Schema.boolean().default(true)),
63
- includeSubagents: k(Schema.boolean().default(false)),
64
- genui: k(Schema.union(['auto', 'on', 'off']).default('auto').loose()),
63
+ theme: k(Schema.union(THEMES).default('deepseek').description('主题 / Theme: deepseek | codex | anthropic').loose()),
64
+ visual: k(Schema.boolean().default(true).description('视觉层 / Visual layer: 关闭后不加载回复区样式')),
65
+ defaultEnabled: k(Schema.boolean().default(true).description('新会话默认开启 / Default on for new sessions')),
66
+ tailReminder: k(Schema.boolean().default(true).description('尾部提醒 / Tail reminder: 每轮追加一句结构化提醒')),
67
+ includeSubagents: k(Schema.boolean().default(false).description('子代理注入 / Include subagents: 默认关闭')),
68
+ genui: k(Schema.union(['auto', 'on', 'off']).default('auto').description('GenUI 联动 / GenUI routing: auto | on | off').loose()),
65
69
  })
66
70
 
67
71
  /**
@@ -80,16 +84,17 @@ export const BUILTIN_STYLE_TEXT = [
80
84
  '4. **清晰优先,不设硬性篇幅上限**:组件是帮读者快速定位结论的工具,不是加信息的借口;短答不套卡,长答按信息量组织;判断标准是读者只看首段标题 + 表格首列/列表首句,就能知道结论和下一步;细节写文件,回复保留结论与路径。',
81
85
  '5. **支撑信息带证据**:每条支撑尽量带数字、路径或结果;没有证据的判断明确标成推断。',
82
86
  '6. **可复制内容用代码块**:提示词、命令、配置、JSON、SQL 等需要复制的文本,用带语言标签的代码块包裹(如 `text` / `powershell`),不要用引用块或普通段落;代码块右上角有复制按钮。',
87
+ '7. **代码/补丁例外**:当本轮主要是代码、代码 diff、文件修改或补丁时,代码块或 diff 卡可以直接作为第一块,不强制标题卡;不要为了结构化加标题卡、表格或「下一步」卡;仍不写过程叙事。',
83
88
  ].join('\n')
84
89
 
85
90
  /** GenUI 已安装时才追加的路由规则:交互/图表走 dsh-ui,普通卡片仍走 markdown。 */
86
91
  export const GENUI_ROUTING_TEXT = [
87
- '**GenUI 已安装**:图表、可排序表格、表单、超过 8 行的大表用 ` ```dsh-ui ` fence;结论/判断/风险/下一步仍用上面的 markdown 卡片,不要为每条回复生成 JSON,也不要重复渲染同一内容。',
92
+ '**GenUI 已安装**:只有图表、可排序表格、表单、超过 8 行的大表才用 ` ```dsh-ui ` fence;`dsh-ui` 只能出现在 markdown 标题卡与正文之后,**不得用它的 title 充当首卡,不得把结论/判断/风险/下一步做成 dsh-ui 组件**;宿主默认要出 UI 时以本节为准,不要为每条回复生成 JSON,也不要重复渲染同一内容。',
88
93
  ].join('\n')
89
94
 
90
95
  /** 尾部提醒:一句话重复契约中最容易衰减的四件事,长度控制在几十个 token。 */
91
96
  export const TAIL_REMINDER_TEXT = [
92
- '提醒:本轮最终回复第一段用无标签标题卡:加粗标题(≤60 字)/ 正文两行;表格前给 `**表格结论:**`;状态用 `✅ 完成` / `⚠️ 待做`;全文最多两个强调块;表格优先 ≤5 行;不设硬性篇幅上限;清晰优先;可复制内容用代码块。',
97
+ '提醒:本轮最终回复第一段必须是 markdown 无标签标题卡(代码/diff 回复例外:代码块或 diff 卡可直接作首块,不强制标题卡):加粗标题(≤60 字)/ 正文两行(即使后文要用组件也一样);组件只能排在标题卡之后,不得用组件充当首卡;表格前给 `**表格结论:**`;状态用 `✅ 完成` / `⚠️ 待做`;全文最多两个强调块;表格优先 ≤5 行;不设硬性篇幅上限;清晰优先;可复制内容用代码块。',
93
98
  ].join('\n')
94
99
 
95
100
  /** DSH home:优先 DSH_HOME,缺省 ~/.dsh。 */
@@ -303,16 +308,18 @@ export function setEnabledFor(sessions, defaultEnabled, sessionId, enabled) {
303
308
 
304
309
  /**
305
310
  * 生成 /structured 的状态文本(不含副作用)。
306
- * @param {{ sessionId: string | null, sessions: Record<string, { enabled: boolean, at: number }>, defaultEnabled: boolean, genui?: boolean, visual?: boolean }} input
311
+ * @param {{ sessionId: string | null, sessions: Record<string, { enabled: boolean, at: number }>, defaultEnabled: boolean, genui?: boolean, visual?: boolean, theme?: string }} input
307
312
  * @returns {string}
308
313
  */
309
314
  export function statusText(input) {
310
315
  const style = activeStyle()
311
316
  const enabled = isEnabledFor(input.sessions, input.defaultEnabled, input.sessionId)
317
+ const theme = THEMES.includes(input.theme) ? input.theme : 'deepseek'
312
318
  return [
313
319
  '结构化提示词:' + (enabled ? '已开启' : '已关闭') + '(本会话)',
314
320
  '新会话默认:' + (input.defaultEnabled ? '开启' : '关闭'),
315
321
  '规则来源:' + (style.source === 'custom' ? '自定义 ' + styleFile() : '内置(无标签标题卡 + 结构化组件)'),
322
+ '主题:' + theme + '(由插件设置 theme 控制)',
316
323
  '视觉层:' + (input.visual === false ? '已关闭' : '已开启') + '(由插件设置 visual 控制;/structured 只控制提示词)',
317
324
  'GenUI 联动:' + (input.genui ? '已检测(图表/交互走 dsh-ui,普通卡片走 fallback)' : '未检测(纯 markdown fallback)'),
318
325
  '状态文件:' + stateFile(),
@@ -330,7 +337,7 @@ export function handleCommand(invocation, api) {
330
337
  const sessionId = sessionIdOf(invocation)
331
338
  const arg = String(invocation?.rawInput ?? '').trim().toLowerCase()
332
339
  if (arg === '' || arg === 'status') {
333
- return { kind: 'success', text: statusText({ sessionId, sessions: api.sessions, defaultEnabled: api.defaultEnabled, genui: api.genui, visual: api.visual }) }
340
+ return { kind: 'success', text: statusText({ sessionId, sessions: api.sessions, defaultEnabled: api.defaultEnabled, genui: api.genui, visual: api.visual, theme: api.theme }) }
334
341
  }
335
342
  if (arg !== 'on' && arg !== 'off') {
336
343
  return { kind: 'error', text: '未知参数:' + arg + '\n用法:/structured on | off | status' }
@@ -352,10 +359,14 @@ export function handleCommand(invocation, api) {
352
359
  /**
353
360
  * 挂载结构化输出:提示词 section + 尾部提醒 + 会话开关 + /structured 命令。
354
361
  * @param {import('@deepseek-ai/cordis').Context} ctx host 根上下文。
355
- * @param {{ defaultEnabled?: boolean, tailReminder?: boolean, includeSubagents?: boolean, genui?: 'auto' | 'on' | 'off', visual?: boolean }} [config]
362
+ * @param {{ defaultEnabled?: boolean, tailReminder?: boolean, includeSubagents?: boolean, genui?: 'auto' | 'on' | 'off', visual?: boolean, theme?: 'deepseek' | 'codex' | 'anthropic' }} [config]
356
363
  */
357
364
  export function apply(ctx, config = {}) {
358
365
  const defaultEnabled = readConfig(config?.defaultEnabled) !== false
366
+ const themeNow = () => {
367
+ const theme = readConfig(config?.theme)
368
+ return THEMES.includes(theme) ? theme : 'deepseek'
369
+ }
359
370
  const visualNow = () => readConfig(config?.visual) !== false
360
371
  const tailReminder = readConfig(config?.tailReminder) !== false
361
372
  const includeSubagents = readConfig(config?.includeSubagents) === true
@@ -482,6 +493,7 @@ export function apply(ctx, config = {}) {
482
493
  defaultEnabled,
483
494
  genui: genuiNow(),
484
495
  visual: visualNow(),
496
+ theme: themeNow(),
485
497
  onChange: (sessionId, enabled, result) => {
486
498
  warn(
487
499
  'session ' + sessionId + ' structured ' + (enabled ? 'on' : 'off') + (result.ok ? '' : '(持久化失败)'),
@@ -116,7 +116,7 @@ function makeContext(form) {
116
116
  test('client:visual=false 停用视觉层,visual=true 恢复', () => {
117
117
  const dom = makeDom()
118
118
  const form = {
119
- value: { visual: false },
119
+ value: { visual: false, theme: 'codex' },
120
120
  listeners: new Set(),
121
121
  getSnapshot() {
122
122
  return { value: this.value }
@@ -131,16 +131,27 @@ test('client:visual=false 停用视觉层,visual=true 恢复', () => {
131
131
 
132
132
  assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
133
133
  assert.equal(dom.attrs.has('data-dsh-structured'), false)
134
+ assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
134
135
 
135
- form.value = { visual: true }
136
+ form.value = { visual: true, theme: 'codex' }
136
137
  for (const listener of form.listeners) listener()
137
138
  assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
138
139
  assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
140
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'codex')
139
141
 
140
- form.value = { visual: false }
142
+ form.value = { visual: true, theme: 'anthropic' }
143
+ for (const listener of form.listeners) listener()
144
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'anthropic')
145
+
146
+ form.value = { visual: true, theme: 'bogus' }
147
+ for (const listener of form.listeners) listener()
148
+ assert.equal(dom.document.body['data-dsh-structured-theme'], 'deepseek')
149
+
150
+ form.value = { visual: false, theme: 'anthropic' }
141
151
  for (const listener of form.listeners) listener()
142
152
  assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
143
153
  assert.equal(dom.attrs.has('data-dsh-structured'), false)
154
+ assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
144
155
 
145
156
  dispose()
146
157
  })
@@ -161,3 +172,119 @@ test('client:configForms 不可用时视觉层保持默认开启', () => {
161
172
  assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
162
173
  dispose()
163
174
  })
175
+
176
+ function attrsElement(extra = {}) {
177
+ const attrs = new Map()
178
+ return {
179
+ attrs,
180
+ setAttribute(name, value) {
181
+ attrs.set(name, value)
182
+ },
183
+ removeAttribute(name) {
184
+ attrs.delete(name)
185
+ },
186
+ getAttribute(name) {
187
+ return attrs.get(name)
188
+ },
189
+ ...extra,
190
+ }
191
+ }
192
+
193
+ test('client:diff 代码块不套插件 chrome,普通代码块才套', () => {
194
+ const dom = makeDom()
195
+ const module = loadClient(dom)
196
+ const diffCode = attrsElement({
197
+ querySelector: () => ({ textContent: 'diff' }),
198
+ closest: () => null,
199
+ })
200
+ const jsCode = attrsElement({
201
+ querySelector: () => ({ textContent: 'js' }),
202
+ closest: () => null,
203
+ })
204
+ const diffCopyCode = attrsElement({
205
+ querySelector: () => ({ textContent: 'diff复制' }),
206
+ closest: () => null,
207
+ })
208
+ assert.equal(module.__test.isDiffCode(diffCopyCode), true)
209
+ const root = {
210
+ querySelectorAll(selector) {
211
+ if (selector.includes('.md-code-block')) return [diffCode, jsCode, diffCopyCode]
212
+ return []
213
+ },
214
+ }
215
+ module.__test.decorate(root)
216
+ assert.equal(diffCode.attrs.get('data-dsh-structured-skip'), 'code')
217
+ assert.equal(diffCode.attrs.get('data-dsh-structured-code'), undefined)
218
+ assert.equal(diffCode.attrs.get('data-dsh-structured-lang'), undefined)
219
+ assert.equal(diffCopyCode.attrs.get('data-dsh-structured-skip'), 'code')
220
+ assert.equal(jsCode.attrs.get('data-dsh-structured-code'), '1')
221
+ assert.equal(jsCode.attrs.get('data-dsh-structured-lang'), 'js')
222
+ })
223
+
224
+ test('client:普通状态表仍套插件表格样式并标状态行', () => {
225
+ const dom = makeDom()
226
+ const module = loadClient(dom)
227
+ const row = attrsElement({
228
+ textContent: '✅ 完成',
229
+ cells: [{ textContent: '✅ 完成' }],
230
+ closest: () => null,
231
+ })
232
+ const table = attrsElement({
233
+ closest: () => null,
234
+ querySelectorAll: (selector) => (selector === 'tbody tr' ? [row] : []),
235
+ })
236
+ const root = {
237
+ querySelectorAll(selector) {
238
+ if (selector === '[class*="_markdown_"] table') return [table]
239
+ if (selector === '[class*="_markdown_"] tbody tr') return [row]
240
+ if (selector === '[class*="_markdown_"] table tr') return [row]
241
+ return []
242
+ },
243
+ }
244
+ module.__test.decorate(root)
245
+ assert.equal(table.attrs.get('data-dsh-structured-status-table'), '1')
246
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
247
+ assert.equal(row.attrs.get('data-dsh-structured-row'), 'success')
248
+ })
249
+
250
+ test('client:GenUI 区域不写入 skip,保持旧行为', () => {
251
+ const dom = makeDom()
252
+ const module = loadClient(dom)
253
+ const code = attrsElement({
254
+ querySelector: () => ({ textContent: 'js' }),
255
+ closest: (selector) => (selector.includes('genui') ? {} : null),
256
+ })
257
+ const table = attrsElement({
258
+ closest: (selector) => (selector.includes('genui') ? {} : null),
259
+ querySelectorAll: () => [],
260
+ })
261
+ const root = {
262
+ querySelectorAll(selector) {
263
+ if (selector.includes('.md-code-block')) return [code]
264
+ if (selector === '[class*="_markdown_"] table') return [table]
265
+ return []
266
+ },
267
+ }
268
+ module.__test.decorate(root)
269
+ assert.equal(code.attrs.get('data-dsh-structured-skip'), undefined)
270
+ assert.equal(code.attrs.get('data-dsh-structured-code'), undefined)
271
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
272
+ })
273
+
274
+ test('client:diff 表格不套插件表格样式', () => {
275
+ const dom = makeDom()
276
+ const module = loadClient(dom)
277
+ const table = attrsElement({
278
+ closest: (selector) => (selector.includes('[data-diff]') ? {} : null),
279
+ querySelectorAll: () => [],
280
+ })
281
+ const root = {
282
+ querySelectorAll(selector) {
283
+ if (selector === '[class*="_markdown_"] table') return [table]
284
+ return []
285
+ },
286
+ }
287
+ module.__test.decorate(root)
288
+ assert.equal(table.attrs.get('data-dsh-structured-skip'), 'table')
289
+ assert.equal(table.attrs.get('data-dsh-structured-status-table'), undefined)
290
+ })
@@ -0,0 +1,9 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { firstScreenStructured } from '../scripts/measure-prose.mjs'
4
+
5
+ test('代码/diff 优先回复:首屏 fenced code 计为结构化', () => {
6
+ assert.equal(firstScreenStructured('```js\nconst answer = 42\n```\n\n说明。'), true)
7
+ assert.equal(firstScreenStructured('```diff\n- old\n+ new\n```\n\n说明。'), true)
8
+ assert.equal(firstScreenStructured('第一段是普通散文。\n\n第二段也是普通散文。'), false)
9
+ })
@@ -87,6 +87,9 @@ test('内置规则覆盖首句结论、组件、清晰度三件事', () => {
87
87
  assert.match(BUILTIN_STYLE_TEXT, /过程叙事/)
88
88
  assert.match(BUILTIN_STYLE_TEXT, /推断/)
89
89
  assert.match(BUILTIN_STYLE_TEXT, /短答不套卡/)
90
+ assert.match(BUILTIN_STYLE_TEXT, /代码\/补丁例外/)
91
+ assert.match(BUILTIN_STYLE_TEXT, /不强制标题卡/)
92
+ assert.match(TAIL_REMINDER_TEXT, /代码\/diff 回复例外/)
90
93
  assert.doesNotMatch(BUILTIN_STYLE_TEXT, /500\s*字|≤\s*500|字数上限/)
91
94
  assert.match(BUILTIN_STYLE_TEXT, /可复制内容用代码块/)
92
95
  assert.match(TAIL_REMINDER_TEXT, /加粗标题/)
@@ -98,6 +101,10 @@ test('内置规则覆盖首句结论、组件、清晰度三件事', () => {
98
101
  assert.match(BUILTIN_STYLE_TEXT, /下一步/)
99
102
  assert.match(BUILTIN_STYLE_TEXT, /✅ 完成/)
100
103
  assert.match(GENUI_ROUTING_TEXT, /dsh-ui/)
104
+ assert.match(TAIL_REMINDER_TEXT, /即使后文要用组件也一样/)
105
+ assert.match(TAIL_REMINDER_TEXT, /不得用组件充当首卡/)
106
+ assert.match(GENUI_ROUTING_TEXT, /不得用它的 title 充当首卡/)
107
+ assert.match(GENUI_ROUTING_TEXT, /以本节为准/)
101
108
  })
102
109
 
103
110
  test('detectGenui:profile 清单不再作为 auto 依据,config 可强制', () => {
@@ -528,7 +535,7 @@ test('client:视觉层可由 configForms 的 visual 设置关闭', () => {
528
535
  const client = readFileSync('src/client.js', 'utf8')
529
536
  assert.match(client, /configForms/)
530
537
  assert.match(client, /getSnapshot/)
531
- assert.match(client, /visual !== false/)
538
+ assert.match(client, /readValue\(value\.visual\) !== false/)
532
539
  assert.match(client, /applyVisual/)
533
540
  assert.doesNotMatch(client, /state\.json/)
534
541
  })
@@ -0,0 +1,121 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readFileSync } from 'node:fs'
3
+ import test from 'node:test'
4
+ import { Config, statusText, THEMES } from '../src/index.js'
5
+ import { checkThemeContrast, THEME_TOKENS } from '../scripts/theme-contrast.mjs'
6
+
7
+ const value = (field) => (field && typeof field.get === 'function' ? field.get() : field)
8
+
9
+ test('0.9.0 Config:theme 默认 deepseek,三值可选', () => {
10
+ assert.deepEqual(THEMES, ['deepseek', 'codex', 'anthropic'])
11
+ assert.equal(value(Config({}).theme), 'deepseek')
12
+ assert.equal(value(Config({ theme: 'codex' }).theme), 'codex')
13
+ assert.equal(value(Config({ theme: 'anthropic' }).theme), 'anthropic')
14
+ assert.equal(value(Config({ defaultEnabled: false }).defaultEnabled), false)
15
+ })
16
+
17
+ test('0.9.1 Config:六个字段的默认值与显式值', () => {
18
+ const defaults = Config({})
19
+ assert.equal(value(defaults.theme), 'deepseek')
20
+ assert.equal(value(defaults.visual), true)
21
+ assert.equal(value(defaults.defaultEnabled), true)
22
+ assert.equal(value(defaults.tailReminder), true)
23
+ assert.equal(value(defaults.includeSubagents), false)
24
+ assert.equal(value(defaults.genui), 'auto')
25
+ const custom = Config({
26
+ theme: 'codex',
27
+ visual: false,
28
+ defaultEnabled: false,
29
+ tailReminder: false,
30
+ includeSubagents: true,
31
+ genui: 'on',
32
+ })
33
+ assert.equal(value(custom.theme), 'codex')
34
+ assert.equal(value(custom.visual), false)
35
+ assert.equal(value(custom.defaultEnabled), false)
36
+ assert.equal(value(custom.tailReminder), false)
37
+ assert.equal(value(custom.includeSubagents), true)
38
+ assert.equal(value(custom.genui), 'on')
39
+ })
40
+
41
+ test('0.9.1 Config:六个字段全部 volatile,且带中英 description', () => {
42
+ for (const field of ['theme', 'visual', 'defaultEnabled', 'tailReminder', 'includeSubagents', 'genui']) {
43
+ const meta = Config.dict[field].meta
44
+ assert.ok(meta, `${field} meta`)
45
+ assert.equal(meta.volatile, true, `${field} volatile=true`)
46
+ assert.ok(String(meta.description ?? '').length > 8, `${field} description`)
47
+ }
48
+ })
49
+
50
+ test('0.9.1 client:token 语义色/代码块实际使用', () => {
51
+ const client = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
52
+ assert.match(client, /THEMES\.includes\(theme\)/)
53
+ assert.match(client, /body\[data-dsh-structured-theme="codex"\]/)
54
+ assert.match(client, /body\[data-ds-dark-theme\]\[data-dsh-structured-theme="anthropic"\]/)
55
+ assert.match(client, /\.md-code-block\[\$\{CODE_ATTR\}="1"\]\{[^}]*background:var\(--ds-code-bg\)/s)
56
+ assert.match(client, /\.md-code-block\[\$\{CODE_ATTR\}="1"\] pre\{[^}]*color:var\(--ds-code-text\)/s)
57
+ assert.match(client, /var\(--ds-success\)/)
58
+ assert.match(client, /var\(--ds-warn\)/)
59
+ assert.match(client, /var\(--ds-error\)/)
60
+ })
61
+
62
+ test('0.9.0 statusText:显示 theme 与 visual', () => {
63
+ const text = statusText({
64
+ sessionId: 'session-a',
65
+ sessions: {},
66
+ defaultEnabled: true,
67
+ theme: 'anthropic',
68
+ visual: false,
69
+ })
70
+ assert.match(text, /主题:anthropic/)
71
+ assert.match(text, /视觉层:已关闭/)
72
+ })
73
+
74
+ test('0.9.0 client:主题 token/暗色 selector/code token 存在,旧 selector 已删', () => {
75
+ const client = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
76
+ assert.match(client, /body\[data-ds-dark-theme\]/)
77
+ assert.match(client, /data-dsh-structured-theme/)
78
+ assert.match(client, /--ds-code-bg/)
79
+ assert.match(client, /--ds-code-text/)
80
+ assert.match(client, /--ds-accent-text/)
81
+ assert.doesNotMatch(client, /\[data-theme="dark"\]/)
82
+ assert.doesNotMatch(client, /--dsw-alias-state-/)
83
+ assert.match(client, /--ds-accent:#2563EB/)
84
+ for (const theme of ['codex', 'anthropic']) assert.match(client, new RegExp(`data-dsh-structured-theme="${theme}"`))
85
+ assert.match(client, /a\{color:var\(--ds-accent-text\)/)
86
+ assert.match(client, /li::marker\{color:var\(--ds-accent-text\)/)
87
+ assert.match(client, /p\[\$\{LEAD_ATTR\}="table"\] strong\{color:var\(--ds-accent-text\)/)
88
+ assert.match(client, /color:var\(--ds-accent-text\);overflow-wrap:anywhere/)
89
+ assert.match(client, /blockquote\[\$\{TONE_ATTR\}\]>p:first-child>strong:first-child\{color:var\(--ds-accent-text\)/)
90
+ const narrow = client.slice(client.indexOf('@media (max-width:720px)'), client.indexOf('@media (prefers-reduced-motion'))
91
+ assert.match(narrow, /table:not\(\[\$\{SKIP_ATTR\}\]\)/)
92
+ assert.doesNotMatch(narrow, /\[class\*="_markdown_"\] table\{/)
93
+ })
94
+
95
+ test('0.9.1 主题 v2:三套 token 精确值与 accentText', () => {
96
+ assert.equal(THEME_TOKENS.deepseek.light.accent, '#2563EB')
97
+ assert.equal(THEME_TOKENS.deepseek.light.accentText, '#2563EB')
98
+ assert.equal(THEME_TOKENS.deepseek.dark.accentText, '#60A5FA')
99
+ assert.equal(THEME_TOKENS.codex.light.accent, '#10A37F')
100
+ assert.equal(THEME_TOKENS.codex.light.accentText, '#0B6B4F')
101
+ assert.equal(THEME_TOKENS.codex.light.surface2, '#F0F7F4')
102
+ assert.equal(THEME_TOKENS.codex.dark.accent, '#34D399')
103
+ assert.equal(THEME_TOKENS.codex.dark.accentText, '#6EE7B7')
104
+ assert.equal(THEME_TOKENS.codex.dark.surface, '#0B1512')
105
+ assert.equal(THEME_TOKENS.anthropic.light.accent, '#CC785C')
106
+ assert.equal(THEME_TOKENS.anthropic.light.accentText, '#A14A2E')
107
+ assert.equal(THEME_TOKENS.anthropic.light.surface, '#FAF9F5')
108
+ assert.equal(THEME_TOKENS.anthropic.light.text, '#141413')
109
+ assert.equal(THEME_TOKENS.anthropic.dark.accent, '#E8A087')
110
+ const client = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
111
+ assert.match(client, /--ds-accent:#10A37F;--ds-accent-text:#0B6B4F/)
112
+ assert.match(client, /--ds-accent:#CC785C;--ds-accent-text:#A14A2E/)
113
+ assert.match(client, /blockquote\[\$\{TONE_ATTR\}="warn"\]\{--ds-accent:var\(--ds-warn\);--ds-accent-text:var\(--ds-warn\)\}/)
114
+ assert.match(client, /blockquote\[\$\{TONE_ATTR\}="success"\]\{--ds-accent:var\(--ds-success\);--ds-accent-text:var\(--ds-success\)\}/)
115
+ })
116
+
117
+ test('0.9.0 对比度:3×2 全组 60 项通过', () => {
118
+ const { results, failures } = checkThemeContrast()
119
+ assert.equal(failures.length, 0, JSON.stringify(failures, null, 2))
120
+ assert.equal(results.length, 60)
121
+ })