dsh-m 0.10.2 → 0.10.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.md DELETED
@@ -1,1347 +0,0 @@
1
- # 更新日志 · Changelog
2
-
3
- dsh-m 的完整版本历史,双语维护:**中文在前,英文在后**,同版本号对齐;各语言区块内按版本倒序排列。发版时请在两个区块各补一条目。
4
-
5
- The full release history of dsh-m, maintained bilingually: **Chinese first, English second**, aligned by version; entries within each section are newest-first. When releasing, prepend an entry to both sections.
6
-
7
- ---
8
-
9
- ## 中文
10
-
11
- ### 0.10.2 修复:D3 自升级 post-write 伪失败账实改判 + 静默自升级(0.10.0→0.10.1 dogfood 实证轮)
12
-
13
- - **需求(2026-10-18 凌晨,首次 D3 真机 dogfood——0.10.0→0.10.1 自升级事件)**:自升级报失败(`application=failed、error=already-installed`)但三面账(manifest/node_modules/lock)均已写入 0.10.1——宿主对自身 bundle 换装存在 **post-write 伪失败**:installBundle 写穿后 manifest 含该名,宿主完成路径自触守卫(自指操作伪影;守卫源码 `specPackageName` 对 tarball URL 同样可解析包名)。另:0.10.1 引入的自升级确认框破坏既有「点击即升级」体验(主人裁决撤除)。
14
- - **方案**:① **D3 终态失败一律账实复读改判**——实读 listInstalled:目标版已写入 = 升级事实成立,按成功改判(needsRestart=true 提示重启激活,output 留改判痕);未到目标 = 真失败原语义;profileDir 自 host-api/两处缝透传。② **静默自升级**——确认框整体撤除(受控态/Modal/文案键/SSR 钉),角标点击直接升级,结局由成功/失败横幅如实报告。ADR-0022 增补决议 7(spike 推断修正 + 改判 + UX 裁决入册)。
15
- - **验证**:typecheck 零错误;全量测试(新增 2 钉 T-D1 改判成功/T-D2 真失败双形态,静默结构钉替换确认框钉;18 例既有失败 028 本机环境清单零新增);真机事件本身即第一份 dogfood 数据:0.10.1 经「重启激活」到位,重启后守卫锚点干净。
16
-
17
- ### 0.10.1 修复:0.10.0 真机 E2E 实证缺陷三连——enable 竞速重试普适化 + 回装失败账实复读 + 守卫命中一律两步化
18
-
19
- - **需求(2026-10-17,0.10.0 装机当日 E2E 实证,真机 skip-auth@0.4.1 与卸载→重装两个事件)**:①插件树重载窗口的 enable 阶段激活竞速(`pending (waiting for service: webRuntime)`)会让两步升级的第二腿、兜底回装腿与 D3 自升级通道直接终态失败——0.9.94 时代的「enable 失败自动重试一次(重试即愈)」本地增强只覆盖了首腿;②回装腿报失败 ≠ 真缺席——pnpm 可能已写入账面(激活阶段失败),此时建议「手动重装旧版」会误导用户降级;③卸载后同会话重装:宿主活跃态/manifest 字面仍视其在装(守卫命中),而 previousVersion 探测口径为「未安装」,0.10.0 的 previousVersion 前置门使守卫命中原样透传 already-installed。
20
- - **方案**:① enable 竞速自动重试普适化——首腿、两步第二腿、兜底回装腿、D3 通道同享受限单次重试(`installOnceRetryingEnable`,退避与豁免形态不变);② 宣称缺席前实读账面(listInstalled)——已写入 → 「请重启 Desktop 完成激活」(details.installedVersion 如实透出),真缺席 → 重装指引,两条路径零误导性降级建议;③ install 路径守卫命中一律两步化(remove+add 即官方语义,去掉 previousVersion 前置门),无旧版可回装时失败走同款账实复读。附带:check-release 门禁行分割容忍 CRLF(本机工作树双语顶条恒找不到的修复)。
21
- - **验证**:typecheck 零错误;全量 1595 例(新增 8 钉:T-A1/A2/A3 enable 竞速三腿 + T-B1/B2 账实复读两形态 + T-C1/C2/C3 无旧版守卫两步化三态;18 例既有失败为 028 本机环境清单零新增);真机恢复实录:skip-auth 0.4.1 经「重启激活」恢复(零重装),重启后守卫锚点干净。
22
-
23
- ### 0.10.0 特性+修复:宿主 0.2.1 already-installed 适配——第三方两步升级 + 兜底回装降级恢复 + 自升级专用通道(D3)
24
-
25
- - **需求(2026-10-17,主人)**:新版 DSH(0.2.1-rc.1/rc.2)中 dsh-m 无法升级与自升级任何包。根因定谳:宿主 `installBundle()` 新增 already-installed 安装守卫——spec 解析出的包名已在 profile manifest 就先于 pnpm 拒绝,官方升级语义改为「先卸载再安装」;tarball/git 形态 spec 包名在 pnpm 前不可知、不受守卫约束(真机 spike 实证:URL 覆盖安装已装 dsh-m 成功、激活形态 restart-required)。影响面:产品内 desktop 升级/自升级全断;卸载、开关、全新安装、web 路径无恙。跳过 0.9.95+ 直接发 0.10.0。
26
- - **方案(ADR-0022,grill 决议 R1–R15)**:① **install-first 自适应两步**——不探测宿主版本,首腿收到 `already-installed` 错误码才转 remove+重试(至多一次、无循环);install 路径撞守卫同样静默两步化(已装态 install 即升级),全新安装零行为变化(removeBundle 恒零调用);release-age/build-blocked/cancelled 语义先于守卫判定。② **兜底回装**——两步化后安装腿终态失败自动回装旧版:回装成功 = **降级恢复**新结局(成功形态 + degraded/restoredTo;客户端 warn 投影不入「升级成功」臂、操作泵复用 warned 状态落账、设置页红点沿用既有 failed||warned 口径、× 删除照旧);回装再失败 = rollback-failed 缺席硬错误(回装腿自身 build-blocked 时 details.pendingBuilds 如实透出);cancelled 抛「已恢复旧版/未做升级」不冒充降级,build-blocked 回装兜住后仍抛审批请求。③ **通道分治**——第三方走官方两步;dsh-m 自升级独享 tarball 直覆盖专用通道(精确版本 → 生效 registry `dist.tarball` → intent 预写 → `installBundle(tarballUrl)` 直覆盖 → 就地收口),core 缝单点改道(SELF_PKG='dsh-m'),host-api/Agent 工具/客户端零特判,自升级绝不落入 remove+add;入口全面保留:头部角标新增确认框(不经过卸载、失败时当前版本不受影响、完成后需重启 Desktop 生效),needsRestart 忠实透传宿主 application(applied→无横幅、restart-required→提示重启)。④ **开机对账**——自升级 intent ledger(`$DSH_HOME/dshm/self-upgrade-intent.json`;ADR-0018 面板通道零外部日志不变量不破)作崩溃兜底网:正常路径就地收口,进程死于升级途中时由下一次 self-check 每进程一次比对实际版本收口。⑤ WRITE_GUIDANCE 自升级失配文案修复(不再让用户「回 DSH Web」,改指官方 Settings → Plugins remove + install 精确版本)。GLOSSARY 七词条入册(两步升级/宿主安装守卫/install-first 自适应/兜底回装/降级恢复/自升级专用通道/开机对账)。
27
- - **验证**:typecheck 零错误;全量 1585 测试(028 本机环境既有失败 18 例零新增——干净树对照为同一集合:symlink EPERM 家族 + client-lightbox ESM scheme + CRLF 源锚 + 网络 fetch);新增钉例覆盖计划 T1–T17(intent ledger 存储五态 / npmTarballUrl 三钉 / 两步化-兜底-降级边界九钉(含 remove-failed 短路、cancelled 文案、回装 build-blocked 透 pendingBuilds)/ D3 与 SELF_PKG 缝改道五钉 / host-api 开机对账与 upgrade(dsh-m) 改道端到端 / client 降级投影-两站点接线-确认框结构序-操作泵落账);ADR-0022 立项、分层 commit(feat core/client + docs adr/glossary + chore release)。
28
-
29
- ### 0.9.94 重构+修复:变更结局描述客户端单源化(op-outcome)+ 补丁映射 YAML 引号宽容与重复键根治
30
-
31
- - **需求(2026-10-17 架构巡查 + grill 九决议)**:变更结局的呈现无 owning module——守卫拦截文案装配 main.jsx 三处逐字重复(0.9.93 toWire 收敛了线格式、client 侧呈现是已知残尾,`repairBasis` 后加时的多点位手补复发已实证)、升级成功文案两处逐字、builds 后缀四处拼装、superseded 五处;另:事务补偿对 workspace `patchedDependencies` **非双引号在场形态**(pnpm 的单引号/无引号书写)逐字判重失明——逐字判重可自造重复键文件、复读检查可假 committed(同包链经 strip 先摘在生产不可达,本项属防御面收敛与 fail-closed 补洞)。
32
- - **方案**:新纯模块 `src/client/op-outcome.js`(GLOSSARY「变更结局描述」入册)——工厂 `createOpOutcome(lookup)` 出成品文案(`createMarkdown(h)` 同款注入先例),`guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed` 四函数;六处接线站点(五类投影)一行接线,流程特有分支(兼容弹窗/能力表拒绝/乐观提示)与 ToolCardRow 短版留守注记。D9:`verifyPatchMapping` workspace 支改 yaml 键成员判定(解析失败 fail-closed 报「复读失败」);`restorePatchMapping` 追加前解析去重(同键任意引号形态在场即跳过,解析失败退化逐字判重)。
33
- - **验证**:全量 1567 测试全绿(新增 client-op-outcome 参数化矩阵 32 例——guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 全交叉/mutationFailed × verb 五枚,期望串全手写字面量;事务补丁映射五钉例——单引号去重/无引号去重/模拟 pnpm 解析炸端到端/坏 YAML 假 committed→fail-closed/restore 退化不抛,夹具异包前提钉死)、typecheck 零错误;收敛锚 main.jsx `join(" | ")` 3→0、`mutationFailed` ≥ 6、`upgradeNotify` 解构随调用点清除;实施经计划评审四轮 15 项收敛 + 硬停点执行评审两轮(基线独立树复跑 1530 对照 + TDD 红→绿独立复演)后放行;实施计划见 `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`。
34
-
35
- ### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
36
-
37
- - **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
38
- - **方案**:产地方法 `toWire(): InstallGuardWire`(随类导出接口 + `GuardViolation` re-export)成为 6 字段共享线格式单一事实源;五处投影改一行展开(信封差异留在各端:host-api `ok/error`、tools `ok/guard/message`、cli 裸 JSON),`repairBasis` undefined 时键不存在的语义移入方法本体。加字段编辑点降为 2(类+toWire 一处、client 读取一处——浏览器边界不能 import core,防御性拼装留守,如实注记)。自觉差异注记:tools 信封键序 kind/message 交换(JSON 键序对消费方无语义,全 tests/ 无原始 JSON 键序钉)。
39
- - **验证**:全量 1530 测试全绿(新增 toWire 三钉例:六字段齐/repairBasis 键缺席/键序;新增 host-api dispatcher 409 钉例补 host-api 投影零测试覆盖的缺口;tools 两臂 execute 钉例零迁移)、typecheck 零错误;收敛锚 `repairBasis ? { repairBasis: … }` 三文件 5 → 0(兼证无第 6 处投影);实施计划经两轮独立评审四项收敛 + 执行评审两轮(含 A1 钉例独立成 describe)记录在 `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`。
40
-
41
- ### 0.9.92 修复+重构:三端市场查询文法统一(market-query 单一入口)——CLI 非法 category 吞错修复
42
-
43
- - **需求(2026-10-16 实测)**:`dshm search --category <非法>` 静默返回「没有匹配的收录条目」+ exit 0,而同输入 GUI 得 400、agent 工具显式报错——「合并市场」词条承诺的「三端同源同语义」被 CLI 打破;根因是分类 slug 正则、source/offset/limit 解析、精选标签表在三端各持一份拷贝(正则 ×3、解析 ×3、标签表 ×2、deadline 常量 ×2),CLI 的拷贝漏了 category 校验。
44
- - **方案**:新模块 `src/core/market-query.ts`(GLOSSARY「市场查询文法」)作为三端共享文法单一事实源——单一入口 `parseMarketQueryInput(raw, policy)` 吃下 category(精选五桶 ∪ 社区 slug,trim 后校验与透传)/source(三值枚举)/limit(floor 先行 + 各端 policy:GUI 96/32、tools/CLI 80/10)/offset,文法违规 throw `MarketQueryError`(消息逐字节沿用既有文案),各端一行信封映射(host-api 400 / tools throw 直飞 / CLI 经 dispatch 统一 catch `错误:` + exit 1);顺手单源化 slug 正则(含目录适配层第 3 份)、精选标签表(以 registry `CATEGORIES` 为键集基准 + 漂移守卫测试)、deadline 常量;CLI help 的 category 列表同步为现行策展五桶(旧值为 0.9.16 前残留)。自觉差异注记:① tools limit 的 (1,80) 区间小数今日由 core `clampLimit` 隐式 floor,改后在入口显式 floor(floor 位置前移,值域逐点相同,输出对象不含 limit 字段);② CLI category/source 改为 trim 后校验(对齐 host-api/tools,`' self-dev '` 今日匹配不到任何条目、`--source ' all '` 今日报非法,改后均按 trim 值正常判定);③ 双违例请求报错归属顺序统一 category → source(单违例逐字节不变)。
45
- - **验证**:全量 1526 测试全绿(新增 market-query 文法矩阵 18 例 + 标签键集 ≡ CATEGORIES 漂移守卫 + CLI 非法 category/非法 source 回归钉例;既有「非法分类/非法 source」锚零迁移)、typecheck 零错误;真机 `node lib/cli.js search --category '非法分类!!'` → stderr `错误:非法分类: 非法分类!!(需精选分类或 [a-z0-9-]{1,32} slug)` + exit 1(校验先于任何网络调用);实施计划与 grill 十决议、独立评审三轮 15 项收敛记录在 `docs/plans/2026-10-16-market-query-intake-implementation-plan.md`。
46
-
47
- ### 0.9.91 优化:设置页三卡行 4:7:3 → 4:7:4——「社区清单」与「关于」等宽
48
-
49
- - **需求(2026-10-16,主人真机反馈)**:宽屏/全屏下设置页三卡行左右不对称——「社区清单」(4fr)与「关于」(3fr)宽度不一致,视觉失衡。
50
- - **方案**:三列制 `grid-template-columns` 4fr 7fr 3fr → **4fr 7fr 4fr**,左右窄卡等宽、中间「精选清单」仍为视觉主体;两列常态(容器 <1100px)与 ≤760px 视口单列不变。
51
- - **验证**:typecheck 零错误、全量 1506 测试全绿(源锚断言同步更新至 4:7:4);主人真机验收通过(热部署预览定稿后随本版发版)。
52
-
53
- ### 0.9.90 性能:搜索归一化冷路径单遍化——世代冷搜索 ~330ms → 99ms p50(宿主事件循环最后一块 >100ms 停摆清除)+ CLI search 位置参数修复
54
-
55
- - **问题(2026-10-16 走查 + 评审实测)**:物化合并市场(ADR-0016)每个世代的**首次搜索**仍同步阻塞宿主事件循环 ~324–364ms——`normalizeSearchText` 逐字符双 regex 分类 + 收尾全量 regex replace 两遍扫描(全语料 256.8ms,NFKC 仅 13.6ms)+ `fieldText` 每字段三次数组分配;触发频率 = 每小时 registry SWR 换代(fetchedAt 无条件 bump)+ 每次宿主重启 + 每日目录换版后的第一次搜索,agent `dshm_search` 同路径。另:`dshm search 主题` 的位置参数被 parseArgs 静默丢弃(query=undefined → 无评分浏览整页)。
56
- - **方案**:① `normalizeSearchText` 激进单遍化——ASCII 查表 + CJK 稳定核四区间直判(4E00–9FA5 / 3400–4DB5 / F900–FA6D / FA70–FAD9,Unicode 赋值稳定性 → 与运行时版本无关)+ 非词符运行就地折叠(收尾全量 replace 退役)+ astral 词对透明规则;FA 段 40 个未赋值洞(U+FA6E/FA6F/FADA–FAFF)与版本敏感缘一律回退 WORD_RE/CJK_RE(同谓词同运行时,等价由构造保证);② `fieldText` 单元素快路(join([n])===n 恒等,免三次数组分配);③ CLI 首个位置参数等价 `--query`(flag 优先、`--query` 缺值宽容回退、help 同步)。
57
- - **等价红线**:golden 差分语料 3888 条(分层抽样 + mulberry32 种子 fuzz 含 astral/NFKC/FA 洞/lone surrogate 池 + 手写 quirk 钉 11 例)零 mismatch——𝕏 的 NFKC→ASCII 折叠、FA 段洞折叠、lone surrogate 折叠、astral 转角不触发等现行为**逐字节原样**(评审独立加固:839,295 例差分零 mismatch);mismatch 一律修实现、绝不改 fixture。生成器与计时探针入库(手动跑不进 CI;mulberry32 确定性 + 目录版本数值比较取最新,同 seed 再生同语料)。
58
- - **验证**:全量 1506 测试全绿、typecheck 零错误;实测(本机空闲):纯函数全语料 256.8→**44.5ms**(5.8×);世代冷搜索 324–364 → **99.3ms p50**(3.3×;首跑 JIT 冷 120.2ms 单列);E2E 重启场景 ~500 → **348.6ms**、小时场景 ~380 → **207.2ms**(builds delta=1,换代确实发生);评审独立复现 91.5–96.2 / 45.1–45.8ms。实施计划经独立评审 Agent 四轮对抗审核收敛(计划 R1–R3 + 执行 R4),四项验收口径全部达成。
59
-
60
- ### 0.9.89 优化:两行态头部视觉恒定——完整标题保留 + winctl 尺寸/行高不随断点跳变
61
-
62
- - **需求(2026-10-15,主人真机反馈)**:0.9.88 的 720 断点两行重排带来两处可感知跳变——标题在「插件市场 ↔ DeepSeek Harness 插件市场」间换字;右上角 winctl 按钮忽大忽小(28↔36px),且标题/按钮垂直位置跨断点漂移 1–2px,来回拖拽「一跳一跳」。
63
- - **方案**:① 两行态保留完整标题——seg 让行后首行空间充裕(640 宽实测 612px 内容区只需 ~300px),短标题切换退役(DOM 双 span 与基础隐藏规则保留,极端窄由 nowrap+ellipsis 兜底);② winctl 触控 36px 从容器断点迁 `pointer:coarse`(按钮大小跟随输入方式而非宽度:鼠标恒 28px、触屏恒 36px);③ winctl 盒钉 34px = seg 行高(按钮 28 + 边框 2 同源公式),单行/两行第一行高度归一。
64
- - **验证**:全量 1496 测试全绿、typecheck 零错误;CDP 实测跨断点标题/按钮垂直中心 delta 0.0px(修复前 -1~-2px);主人真机验收通过。
65
- - **杂项**:package-lock 根版本字段自 0.9.68 起历届发版漏改,本次补齐至当前版本并纳入发版流程。
66
-
67
- ### 0.9.88 特性:面板三区 resize——尺寸记忆 + 就地生长(ADR-0021)+ 验收三修
68
-
69
- - **需求(2026-10-15)**:市场面板「920px 浏览卡片太挤、全屏又太大」的真实档位空洞 + 桌面窗口本能手势(下意识移向边缘想 resize)。`/grill-with-docs` 两轮锁定设计:三区手柄(右/下/右下角)、就地生长锚定、尺寸记忆全套;实施计划经独立评审 Agent 三轮对抗审核(计划 11 条 + 执行 2 条全闭环,含两条方法论沉淀:`treeShaking:false` 下「bundle 标识符缺席」类预言必假、esbuild 默认 charset=ascii 下中文 bundle 锚必假)。
70
- - **方案(ADR-0021)**:① 纯逻辑 `window-resize.js`——`clampSize`(下限 640×460、视口−8px 边距、`.abs` 形态 origin 第二钳保右/下缘不破边距、视口小于下限退化)+ 尺寸记忆存取(与位置记忆逐字对称);② 手势状态机逐条镜像 ADR-0017(rAF 帧内写/手势帧零 setState/setPointerCapture/主键门卫/多指守卫/卸载清理);③ 热区 6/8px 边 + 14×14 角,cursor 即全部供性,全屏/窄视口/触摸四重豁免;④ reclamp 升维:先夹尺寸、后夹位置。
71
- - **主人实测三修(同版)**:头部两行重排断点 600→720(resize min 640 落入两行区,标题/seg 零折行);详情弹窗中段包进 `modalbody`(README 任意展开后 ✕ 与「安装」双端常驻);设置网格三列门控由 `.full` 改容器宽 ≥1100(还原态拉宽同样升三列,顺带修正小视口全屏强塞三列旧账)。
72
- - **实验记录**:tab 切换淡入当日实现当日被主人否决,整体回滚(字节级干净,评审复核在案)。
73
- - **验证**:全量 1496 测试(+24:纯逻辑 18 + 冒烟 3 + 三修源锚 3)、typecheck 零错误;CDP 量化矩阵 14 格全过(列数阈值 640→1/920→2/1140→3/1500→4、钳位下限 640×460 与上限、视口缩小双夹紧、720 断点、modalbody、coarse 豁免);Performance CDP 实测与 Windows 壳冒烟按计划延后(前者有代码级保证:手势帧零 setState)。验收记录(诚实口径):dsh-workspace `01_docs/research/2026-10-15-dshm-panel-resize-sweep-record.md`。
74
-
75
- ### 0.9.87 性能:热路径四连——已装枚举并行化 + patchRows 按需采集 + 收藏 stale 批量化 + 设置页社区 summary 接入 L2
76
-
77
- - **问题(2026-10-15 架构走查 + 独立复核)**:市场查询热路径上仍有三处重复支付——① `listInstalledPlugins` 逐依赖**串行**双文件读(package.json + cordis.patch.yml),每次查询/翻页/每次防抖搜索提交都全量重跑(14 依赖实测 ~11ms,占暖缓存查询近半预算);② 收藏分区 stale 检测对**每个收藏发一整条 market 查询**(≤8 并发扇出):回答一个布尔值却付全管线(枚举+社区+4445 条评分+排序+分页),20 收藏 ≈ 3 批 × 全管线;且旧路径存在 curatedFirst 挤出边界误判——primary 域同词命中 ≥8 条时 id-exact 社区条目被前置挤出前 8,**误判 stale**;③ 设置页 registry 方法背后的 `getCommunitySummary` 走无 memo 的全量 adapt+merge(~4400 条,每次打开 ~10-50ms CPU),而同身份的 L2 物化代(ADR-0016)近在咫尺。
78
- - **方案(热路径四连)**:① 枚举并行化——每依赖分类独立无跨依赖状态,`Promise.all` 保序折叠(输出仍按依赖键字典序),新增可选 `{ patchRows: false }`:市场热路径不再付每插件 ~0.55ms 的 YAML CST 解析(patchRows 仅 enablement/toggle 消费,已装页路径保持缺省采集;退出时**省略字段**而非伪造 `readable:false`);② 批量 id 存在性——core 新增 `existingMarketIds`(三不原则:不枚举已装/不探测 latest/不分页;merged 域原始 id Set 判定,与旧搜索扇出除 curatedFirst 边界外逐点一致,该边界上属**修复**)、host 新 method `market-ids-exist`(ids ≤500 校验 400)、客户端单发批量 + 失败保守全 stale;③ `getCommunitySummary` 改道 `mergedOutcome`(同身份免 adapt+merge,「summary 永不入代」保持;registry 传 `snap.loaded` 整体,namespace/profile 显式缺省与改道前缓存路径逐字节同路——保 parity 优先于共享代的自觉取舍);④ `toMarketItem` 逐页线性扫描改每查询一次已装索引(byPkg/byName/byGithub 三域 first-wins + min-ord,与 `matchInstalledByEntry` 逐点等价,随机 fuzz 640 条零不一致),`GITHUB_SPEC_RE` 导出为单一事实源。
79
- - **验证**:1472 项测试全绿(净增 7 例:枚举排序钉住/patchRows 退出对照/FP5 匹配等价四路径/existingMarketIds 四例含 L2 命中钉与 curatedFirst 边界钉/host method 正常+400/C3 源串锚迁移)、typecheck 零错误;实测(保守口径):枚举市场路径 10.55ms → 2.08ms(5.1×)、`listMarket` cache-only 端到端中位 ~28ms → 5-7ms、`existingMarketIds`(20 id) 2.52ms/次;两份实施计划均经独立评审 Agent 多轮对齐(FP1/FP5 两轮零保留通过;FP2/FP3+发版两轮 7+1 条意见全闭环)。
80
-
81
- ### 0.9.86 性能:packument 访问器——registry 元数据属主模块,登记网络腿出锁(钢人 C2 / ADR-0019)
82
-
83
- - **问题(钢人报告 C2 节)**:「registry 元数据」没有 owning module——登记(Register)只为读一个发布时戳,却在官方文件锁内发起 8MiB 级全量 packument 网络取(npmjs 首腿 5s 超时 + 生效源备腿至 12s),镜像机器装机的白等直接计入关键路径;B3 预热再经无缓存通道把同一全量取付第二次;三 adapter 各自取、各自缓存(或没有)。
84
- - **修复(ADR-0019)**:新属主模块 `src/core/packument.ts`——一次取回派生视图包 `{times, versions, distTags, readme, repo}`,按 (pkg, 生效源) 键缓存(TTL 5min / single-flight / cap=4 / 失败与 regex-invalid 负缓存同 TTL / onRouteSwitch 作废 + 生效源入键双保险)。登记 times **出锁**:发布时戳是不可变历史事实,锁外预取零 TOCTOU,锁体回归纯 fs(锁前存在性预读保「文件缺席零探测」钉子);预检/登记共享 single-flight;B3 预热改 `warmPackumentFor`(bypass 缓存 + 生效源单腿真发暖 CDN + 回填);readme 兜底腿同款单腿带缓存面(终末错误透传保全)。latest 小端点与 ADR-0006 制度不并入;四函数 + `extractGithubRepo` 迁入属主、versions.ts 原名 re-export(import 位零改动)。
85
- - **验证**:1462 项测试全绿(访问器 18 例新增:两腿超时收紧断言/single-flight 哨兵/负缓存/cap/路由作废/warm 单腿 bypass+回填/Active 面负缓存视 miss/readme 阶梯三态含 URL host 锚/锁内零网络时序断言)、typecheck 零错误;真机冒烟:访问器冷 143–269ms / 热 0ms / warm bypass 真发 29ms,登记真实 wire(临时 profile + 真实 young 版本)applied:true 111ms + 幂等 up-to-date 3ms;实施计划经独立评审三轮(13 条)+ 执行评审两轮(4 条建议全收口)。
86
-
87
- ### 0.9.85 优化:版本号迁「关于」链接行尾 + 头部自升级入本地操作泵(两则真机反馈)
88
-
89
- - **① 版本号迁位(主人定稿)**:0.9.84 把插件版本号挂在「关于」标题行 sub(弱化灰字),真机对照后主人拍板迁至**链接行尾**(GitHub · 问题反馈 · npm · v0.9.85)——与外向元数据同组更协调,标题行回归纯净。缺失(ping 失败 → null)时整段不渲染,不留悬空分隔点;新增 `.dshm-links-ver` 弱化样式(label-secondary + tabular-nums)。不放升级入口的裁决不变。
90
- - **② 头部自升级漏记根治(主人真机反馈:⬆ 升级在操作日志无记录)**:根因是渠道两不沾——host-api `self-upgrade` 显式 `channel: 'panel'`(ADR-0018「面板通道零外部日志」不变量,防漏标落 external),而头部 chip 的 `doUpgrade` 直接 `api("self-upgrade")` 绕过本地操作泵,两头都不记。修复:chip 接线 `ops`(面板操作账本)并入泵 `ops.runOp("upgrade", "dsh-m", …)`,执行器回传 `opMeta {from,to}` 落账 → 已结束列表渲染「升级 dsh-m vX→vY」;SSR/探针无 `ops` 时回退直连 api。`stillApplies` 语义安全:dsh-m 恒已装 → 恒 applies,不触发 superseded。全家族盘点:安装/升级/卸载/开关/清单源/社区开关/「全部更新」(逐条 runOp 入队)均无漏记,自升级是唯一缺口。
91
- - **验证**:1443 项测试全绿(0.9.84 基线 1440 + 新增源锚:about-version 迁位改写与 render-smoke 自升级入泵)、typecheck 零错误;F1 族就绪门连续三跑全量稳定(曾现一次 657/42 异常未再复现,归因共享机瞬时载荷,已留日志)。
92
-
93
- ### 0.9.84 优化:设置页「关于」标题行展示插件版本号
94
-
95
- - **问题(主人真机反馈)**:0.9.83 起窄容器下头部版本 chip 常态隐藏(共识 Q7——手机头部宽度让位给还原/关闭),插件自身版本号一度无处可查(设置页只有社区清单的「目录版本」)。
96
- - **方案(零视觉噪声)**:设置页「关于」卡标题行右侧既有 sub 槽(`.dshm-section-sub` 弱化灰字)挂裸版本号 `v{version}`——零新增行/块/CSS/i18n;`pluginVersion`(ping 单一事实源:挂载/回前台/重启确认后重取)以 prop 接入 SettingsTab,缺失(ping 失败 → null)时槽位自然为空(Section 兼容逻辑天然处理,与头部 chip「失败不留占位」同语义);不放升级入口——头部 ⬆ + 市场卡片/CLI 所有权不变(尊重「dsh-m 自身卡已移除」裁决)。
97
- - **验证**:1442 项测试全绿(0.9.83 基线 1440 + 新增源锚 2 组于 client-about-version.test.mjs)、typecheck 零错误。
98
-
99
- ### 0.9.83 功能:手机窄屏自适应——面板头部两行重排 + 窄视口默认全屏
100
-
101
- - **问题(主人手机实拍)**:`.dshm-head` 是单行 no-wrap flex,最小内容宽 ~440-470px,手机面板内容宽仅 330-365px → 行尾溢出被 `.dshm-panel` overflow:hidden 裁掉:还原/关闭按钮组不可见不可点(关面板只剩遮罩点击)、版本 chip 拦腰截断、标题逐词换行、seg 键逐字竖排。移动端问题第三次以「用户手机截图反馈」方式暴露(0.9.66 触屏滑动方向、0.9.67 字号失控之后)。
102
- - **方案(容器查询两行重排 + 窄视口默认全屏,grilling 共识 Q1-Q10 + 独立评审 4 轮 11 条修订 R1-R8/N1-N3)**:`.dshm-panel` 加 `container-type:inline-size`(面板宽高均公式派生不依赖内容,包含安全;灯箱 portal 挂 body、sticky 行在滚动容器内,均不受影响),`@container (max-width:600px)` 两行重排——行1 [短标题 ⋯spacer⋯ chip winctl]、行2 [seg 三键拉通 36px 触控];DOM 顺序零改动(纯 order+wrap,源锚/tab 序/读屏序稳定);标题双 span 切换(窄显「插件市场」,`title.panel` 现成 key);版本 chip 常态隐藏、warn 态缩为 ⬆ icon(tooltip/自升级语义不变),隐藏/收拢规则以专属类 `.dshm-verchip` 收窄——头部 profile 徽标(非 web profile)不在共识范围、不受影响;新规则对不支持容器查询的浏览器零生效(条件规则组等效兜底,老浏览器逐字节同旧版);窄视口(≤600px)初始挂载恒全屏(仅判定一次、不随 resize 跟随,FS_KEY 记忆窄视口下不生效——防「还原一次被永久记住挤压态」),宽视口行为逐字节同今日;overlay 窄视口 padding 8px。范围红线:ZoneChips/卡片网格/灯箱/设置页/遮罩契约/拖拽模型零触碰。
103
- - **验证**:1440 项测试全绿(0.9.82 基线 1435 + 新增源锚 5 组于 client-mobile-head.test.mjs;既有 render-smoke 两处断言同步改写——「warn 类名锚 + `⬆ v…` 文本锚」,0.9.82「一处既有用例标题改写」先例同款)、typecheck 零错误(`run-command-error-digest` F1 族时序用例为既有载荷敏感假红、与本改动无关(干净 HEAD 加压复现同红)——本版随执行评审 E5 以「就绪门」根治四用例(F1/T5 abort/timeout、F1-R/A/C):3 并发加压 9/9 全绿,修复前同法 6/6 红);`node scripts/verify-panel-mobile.mjs` 真浏览器 13 断言全过(393×852 窄 + 1280×800 宽 + FS_KEY 2×2 矩阵 + chip 双态:默认全屏/winctl 可达/头部零溢出/标题切换/seg 36px 触控/chip 隐藏与 warn 收拢/还原态 8px/宽视口 FS_KEY 回归);verify 脚本照 verify-lightbox 惯例手动运行不进 CI(本地 http 源 + /dshm 桩,零外网依赖);真机复核:主人手机。
104
-
105
- ### 0.9.82 功能:外部操作记录 failed 行单条移除——红点死锁解除(ADR-0018 修订)
106
-
107
- - **问题(红点死锁 + 操作层与展示层不对齐)**:外部记录(CLI / Agent 工具通道)失败后点亮设置 tab 红点,但外部行没有单条 ✕——0.9.79 的原裁决因服务端只有 list/clear 而不给出 ✕ 出口;唯一消除手段「清除已结束」连带清空全部终态记录(含成功历史),行为上等同永久亮灯,警报疲劳会让红点彻底失效。展示层(渠道徽标、按日分组、计数)0.9.79 起已与本地记录同构,唯独操作层掉队。
108
- - **方案(服务端 per-id 删除链路 + 客户端分流接线)**:core 新增 `removeExternalOperation(id)`(整文件读-改-写原子替换,幂等——id 不存在静默 no-op,保留记录原样透写不改写历史字段);host-api 新增 `external-operations-remove` method(body.id string 校验 400,鉴权同 list/clear 零新增特例);store 新增 `removeExternal(id)`(overlay 剔除 + **墓碑** `removedIds`——拦截在途 fetch 迟到响应把已删行并回,`clearExternal` 重置);`canRemoveRow` 对外部记录放行 `failed`(done 仍不给 ✕,与面板「问题态才有 ✕」语义精确对齐);`onOpsRemove` 按渠道分流——本地路径逐字节不变(R0 红线),外部路径**先 api 成功再动 overlay**(事务顺序强于「清除已结束」的 fire-and-forget),失败行保留 + err toast(`op.remove.failed` 新键,{err} 插值带出服务端错误线索,zh/en 同步)。单条 ✕ 无二次确认(与面板一致)。ADR-0018 文末追加修订注记,原裁决原文保留。
109
- - **验证**:1435 项测试全绿(0.9.80 基线 1425 + 净增 10:journal 存储 3、host-api method 4、store removeExternal+墓碑 2、canRemoveRow 改写与接线锚 2,含一处既有用例标题改写)、typecheck 零错误;实施计划经独立评审 Agent 三轮评审收敛(探针路径/链接期报错形态/墓碑兜底/断言保全/401 惯例/插值链路/回退口径 11 条意见全部闭环)。
110
-
111
- ### 0.9.80 修复:已装页开关抖动根治 + 社区开关定位恒定 + 「已实测」徽章退役(三项 UX 修复,client-only)
112
-
113
- - **① 已装页开关抖动根治(主人实拍:点开关整页抖、开关原地失踪、鼠标不再命中)**:CDP 实测每次点击开关发生 47px×2 卡片栅格位移 + 0.6–1.3s 命中丢失窗口(原点落到上方卡片 `.dshm-links` 外链行,误触即开浏览器标签)。根因三件:`ProgressLine`(install 专用 pnpm 进度条)对 toggle 也挂载(35px 行 + body 12px gap,插入/拔出即位移);toast/重启横幅是面板 column flex 的流内子元素(出现/消失压缩 body 39px×2);开关视觉翻转要等 5 个刷新请求落地(1.5–2.3s「点了没反应」真空期,且期间开关 `locked` 灰态)。修复:新增 `progressTarget`(仅 install/upgrade/self-upgrade 挂进度条;`activeMutateTarget` 语义不动,仍管逐行禁用);toast/横幅包进 `.dshm-banner-stack`(absolute 底部浮层,pointer-events 穿透);`pendingToggles` 乐观翻转(点击 42ms 即变,`finally` 摘除=成功收敛/失败回滚);toggle 只重取已装视图(installed+market×2+updates+external-operations → installed×1,`view-refresh.js` 未动、单测原样);`.dshm-switch::before` 热区 34×19 → 46×31(视觉不变)。CDP 回测四断言全过:位移 0、命中零丢失、翻转 42ms、body 底缘 0 位移。
114
- - **② 社区目录开关钉进标题行(主人实拍:开/关后开关位置高低不同)**:开关行原是卡片 body 末行,关态 5 行元信息消失即被上提约 5 行高。移入 `.dshm-section-title` 右侧(flex 现成 + 开关自带 `margin-left:auto`;与已装卡片右上角开关同一交互语言),CDP 实测 ON/OFF 两态开关 y 恒等(135.2px,漂移 0.0);冗余「社区目录开关」行退役,语义并入开关 tooltip。
115
- - **③ 「已实测」徽章退役(B 方案:byline 灰字承接)**:verified 仅覆盖精选 4/21 条且全为作者自家插件——徽章实为署名代理、区分度趋零,真载荷(实测过的 DSH 运行时清单)藏在 hover tooltip 里触屏不可达;按「卡片徽章只放例外信号(弃用/自用/可升级)」原则退役。市场卡/收藏快照卡/弹窗头三处渲染退役;市场卡 tag 行尾新增灰字 `实测 ≤{最新复验版本}`(tooltip 全量清单);详情弹窗「实测版本」行保留为唯一权威呈现;`badge.verified` 键按 0.9.76「版本无关徽章退役」先例退役;registry 数据、snapshotOf 透传与安装期 peer 兼容弹窗零触碰。
116
- - **验证**:1425 项测试全绿(0.9.79 基线 1423 + 退役断言 2;U12 收藏卡契约随 ③ 改写为新契约)、typecheck 零错误;三项均 client-only,刷新页面即生效;①② 经 CDP 全程量化回测(位移/命中/翻转延迟/几何恒定)。
117
-
118
- ### 0.9.79 功能:外部操作记录——非面板通道(CLI / Agent 工具)变更直落审计日志进面板「已结束」(ADR-0018)
119
-
120
- - **问题(审计面脱节实证)**:全局操作记录的口径 = 面板发起——quota-watch 0.1.28 经 `dshm_upgrade`(Agent 工具)升级装机成功后,面板「已结束」列表零记录,操作审计面与真实系统状态脱节。CLI 与 Agent 工具是独立进程,写不进浏览器 localStorage 的操作泵。
121
- - **方案(ADR-0018:机器本地直落日志,与操作泵分离)**:新增 `$DSH_HOME/dshm/external-operations.json`(`DSHM_EXTERNAL_JOURNAL_FILE` 可覆盖;机器本地、不跨机同步)——九个公共变更入口增 `channel` 参数(`'panel'|'cli'|'host-tool'`,缺省 `'external'` 兜底),非面板通道的操作终态后直落一条记录:**成功与失败都记**、滚动 200 条、tmp+rename 原子写、损坏文件改名隔离、append 任何失败被钩子吞掉(至多 stderr 一行,绝不反噬操作终态,R1-1);reason 截断 ≤300。desktop 口径(R1-2):desktopToggle 透传 togglePlugin(唯一嵌套对,单笔事务一条记录);desktopInstall/Uninstall/Upgrade 自记(call-graph audit 证实不经已插桩入口);服务缺席拒绝自记 `desktop-toggle` failed。
122
- - **面板消费(内存 overlay 并读,不写 localStorage)**:host-api 新增 `external-operations`(list)与 `external-operations-clear` 两 method(鉴权沿既有宿主信任检查);「已结束」列表 = 本地操作泵(localStorage,现状不动)+ 外部记录 overlay(服务端为唯一事实源,persist 路径零触碰——防服务端淘汰后被本地副本复活,R1-3);渠道徽标 **Agent 工具** / **命令行** / 兜底**外部**(Q2b/R1-4);失败记录点亮红点(Q4);外部行不渲染单条 ✕(R2-3);「清除已结束」连带清除服务端日志 + overlay 同步清空;fetch 时机 = 面板初始化 + 每次操作完成后 + 打开操作记录卡(不轮询,R1-6);「已结束」计数改用合并后全量,最近 50 条渲染截断照旧(R1-7)。
123
- - **验证**:1423 项测试全绿(0.9.78 基线 1389 + 新增 34:journal 存储 8(滚动/损坏容错/panel 拒绝/无凭据/路径上抛/截断)、通道插桩 8(panel 零记录/cli、host-tool 各一条/嵌套单条/失败 failed/append 注入抛错不反噬)、host-api 4(list/clear/鉴权/注入位)、client 模型+视图 14(overlay 升序并读/persist 纯净/幂等/徽标/✕ 出口/计数/接线锚))、typecheck 零错误;测试全程零触碰真实 `~/.dsh`(10 个既有测试文件补 journal 隔离,另新增 1 个通道插桩测试文件,沿用 cacheRoot 纪律)。实施计划经独立评审 Agent 三轮评审收敛;npm 发版与 dsh-m 自升级另行经用户确认。
124
-
125
- ### 0.9.78 性能:收藏 stale 检查零网络 + 市场页零增量提交守卫(钢人 C3)
126
-
127
- - **问题(钢人报告 C3 节)**:收藏 stale 判定只需要「id 还在不在目录」的成员布尔,却穿透富 market interface 的探测段——每次收藏区访问/切换(800ms debounce 合并后)仍是 N 条收藏 × full 查询,含每命中条 npm/github latest **网络扇出**且产物全部丢弃;叠加 toggle 后 refreshViews 四连响应无条件 `setData`——数据层精心维持的引用稳定性一次 setState 即蒸发。
128
- - **修复**:`lookupLive` 补 `probeMode:"cache-only"`(ADR-0013 第一段模式,零网络零探测;existence = items 成员判定,语义零变化);`fetchPage` 增设**零增量提交守卫**——新增 `marketResponseEquivalent` 纯函数(JSON 值语义递归比较,ZoneChips setGeom 同款纪律),内容等价则跳过 `setData`(dataRef/loading 语义不动)。报告可选项「客户端 stale TTL 缓存」明确不做:C1 物化后 cache-only 查询已是内存级。
129
- - **验证**:1389 项测试全绿(执行基线 1382 + 新增 7)、typecheck 零错误;真机差分冒烟 5 组用例(npm 主清单/自身/社区条目/不存在 id):cache-only 与 full 的 id 集合与 total 逐项一致(红点语义不变的充分条件),cache-only ~30ms vs full 冷跑 ~250ms(零探测指纹);实施计划经独立评审两轮闭环(F1-1 计划基线勘误)。
130
-
131
- ### 0.9.76 修复:三项展示层退役与吸顶跳动根治——「对话」徽章/「版本无关」徽章退役 + 分类行滚动锚定自激断路
132
-
133
- - **① 「对话」来源徽章退役(主人实拍:界面全点按钮却全员误标「对话」)**:`meta.session` 是 0.7.0 终审·新伤1 的**泵所有权标记**(restore 跳过 / replaceAll 不回卷 / persist 时剥离),GUI 的 `runOp` 与对话区镜像两路无条件同打同一标——它区分不了来源,且重载后徽章整体蒸发。修复:opRowVm 删 session 派生、行渲染删徽章、CSS 与 i18n 键退役;`meta.session` 本职不动(数据模型零变更),来源区分留待未来 `meta.origin` 承载。
134
- - **② 「版本无关」徽章退役(市场卡 + Modal 头)**:英文标签 Version-independent 19 字符把卡片最高价值资产——插件名——挤成 `D...`/`O...`;且同一事实在卡片描述文案(「版本无关,详见仓库。」)与 Modal Compat 行已双处存在,卡片徽章边际信息为零。设计原则落定:**卡片徽章行只放「别处拿不到且影响当下决策」的信息**(已实测/安装态/供给源),版本无关属详规层,由 Modal Compat 行(`modal.decoupled.*`)唯一承载;`snapshotOf` 数据字段保留(收藏/快照完整性);`badge.decoupled` 与 `badge.decoupled.tip` 两键退役。
135
- - **③ 市场 tab 分类行吸顶高频跳动根治(主人实拍:三 tab 来回切换偶发疯狂上下蹦跳)**:Chrome 滚动锚定把「吸顶 stuck 翻转的高度变化」翻译成 scrollTop 补偿,哨兵(sticky wrap 内 top:-5px)被来回送进/送出视口 → IO stuck 每帧翻转 → sticky bar 在钉住/流内两形态间高频瞬移。触发面:tab 条件渲染重挂(stuck=false + IO 重新武装)× `.dshm-body` 保留的 scrollTop 落在哨兵边界 ±30px 敏感带(翻页 `scrollIntoView` 会把 scrollTop 送到 bar 边缘,命中概率大增)。修复:`.dshm-body` `overflow-anchor:none` 一行断驱动器;吸顶/哨兵/+N 裁剪几何零变更。非本批回归——吸顶/IO 为 0.9.52-0.9.59 机制,长期潜伏的边界 bug。
136
- - **验证**:1379 项测试全绿(render-smoke 新增退役负向锚与锚定停用/吸顶几何契约锚)、typecheck 零错误;三项均为展示层修复,数据模型零迁移;client-only——刷新页面即生效。
137
-
138
- ### 0.9.75 功能:操作记录信息增强——行内时刻/按日分组/开关方向/耗时/版本变迁/来源标记(数据零迁移)
139
-
140
- - **背景(主人设置页实拍:开关记录读不出「开还是关」)**:操作记录行只有「状态 + 类型 + 包名」三要素——开关看不出方向(截图里连续两条 `开关 @iasiv5/dsh-quota-watch` 无法分辨先后开/关)、无任何时间、看不出耗时、看不出对话区发起还是界面点击、升级不知道版本从哪到哪。而这些信息**全部已在每条记录里**(`createdAt`/`updatedAt` 是 store 打点常态、`meta.on` 0.4.0 起就随开关入账、`meta.session` 是泵拾取标记)——纯展示层缺口,非数据缺失;浏览器 localStorage(`dshm-operations`)里的存量旧记录升级后立即可读,零迁移。
141
- - **方案(展示派生全部收敛纯函数模块)**:新增 `src/client/operations-view.js`(`fmtOpClock`/`fmtOpFull`/`opDayKey`/`fmtOpDuration`/`opUpgradeMeta`/`groupOpsByDay`/`opRowVm`,Node tests 直测,惯例同 toggle-view):① 行内 `HH:mm`(tabular-nums 对齐,title 完整到秒);② 已结束**按日分组**,组头 今天/昨天/YYYY-MM-DD,**新组在上、组内新→旧**——最新一条贴「已结束」标题可见,不再沉底滚动找;③ 开关/社区开关按 `meta.on` 渲染 `开`(绿)/`关`(灰)徽章——原「社区开关专属方向渲染」(0.9.71 评审 M1 `targetText`)退役为泛化实现;④ 终态耗时 `<1s/42s/1m02s/3h05m`(免翻译);⑤ 升级行版本变迁 `v0.9.71→v0.9.72`(github 源回退 sha 前 7 位,与 notify.upgraded 口径一致);⑥ `meta.session` 行加「对话」来源徽章;⑦ 「已结束(N)」计数(无计数旧键 `settings.ops.group.done` 退役)。字段缺席逐段省略,旧行形态优雅回退;字号守 0.9.67 十二 px 家族,徽章 `currentColor` 描边自适应主题。
142
- - **数据管道(唯一一处非展示层改动)**:执行泵成功路径支持执行器回传 `value.opMeta` 并入记录 meta——底座取**落账时实读**的当前 meta(入队快照会丢拾取阶段追加的 session 标记,破「陈旧快照不回卷」保证,评审自纠);失败路径不入账(结果不可信)。恢复路径(`dispatchRestored`)成功值统一接进成功值通道(`value = r`)——生产实现今日返回 `{ ok: true }`,对现有流零行为变化;两处 `doUpgrade` 调用点把 `fromVersion/version/sha` 管道为 `opMeta`。
143
- - **验证**:1375 项测试全绿(0.9.74 基线 1339 + 新增 36:`client-operations-view` 27 用例含格式化 TZ 无关派生/分组序/行 VM 回退/新病灶负向锚(`targetText` 与无计数旧键不得残留)+ 泵 opMeta 4 用例(done/warned 入账、失败与脏输入不入、恢复路径入账、session 保留))、typecheck 零错误;构建产物含新规则逐字锚。
144
-
145
- ### 0.9.74 功能:详情截图条滚轮横滚——裸滚轮 deltaY 安全映射 scrollLeft(钢人三红线)
146
-
147
- - **背景(主人验收 0.9.73 实拍反馈)**:截图条是横滚容器,鼠标滚轮在横滚容器上默认纵滚祖先(弹窗)——图片多时纯鼠标用户只能拖 ~700px 细滚动条(离散目标上的连续输入,精度成本极高);触控板双指横滑(deltaX)与 Shift+滚轮虽有原生路径,但属隐藏知识不可达。
148
- - **判定(新纯函数 `shotWheelAction`,src/client/wheel-strip.js,Node tests 直测)**:钢人三红线——①可滚才拦:scrollWidth ≤ clientWidth(+1 亚像素容差,0.9.72 同款口径) 零劫持,不满行的多数条目行为与今日一致,杜绝「静态劫持表达动态适用前提」的滚轮黑洞同构病复发;②到头放行:已到 deltaY 方向边界时零动作,纵滚还给弹窗(横向有得滚才借道、借完即还);③deltaX 不碰:|deltaX| ≥ |deltaY|(触控板横滑)不劫持不重复喂。非有限输入安全短路。
149
- - **接线**:DetailModal 手动 `addEventListener("wheel", …, {passive:false})`(React onWheel 走 document 级 passive 委托,preventDefault 无效);deps=visible.length 随截图条存在性(条件渲染/primary 异步到达/坏图剔除)重挂;布局入参逐事件现读,无缓存失效问题。灯箱、走链、键盘导航零变化。
150
- - **验证**:1339 项测试全绿(新增 client-wheel-strip 7 项:三红线逐条 + 容差边界 + 安全短路 + 接线源锚三件;img-chain ①锚放宽容 props 前缀)、typecheck 零错误。
151
-
152
- ### 0.9.73 修复:详情截图条统一瓷砖——「大小不一 + 预览图盖住左右邻图」(flex 收缩外溢根治)
153
-
154
- - **根因(主人详情弹窗实拍:缩略图大小参差,图片压在左右邻图上)**:两处机制缺陷叠加——① `.dsvm-shotbox` 无固定几何(盒 min 120×84 / img max 220×130),跟着图片固有比例走,16:9、4:3、竖图、小原图各渲各的,「大的大小的小」;②盒子显式 `min-width:120px` 顶掉了 flex 的 automatic minimum size——8 张截图自然总宽(~1.7k px)远超弹窗内容宽(~688px)触发 flex 收缩时,盒子被压到 120px 地板而盒内 `<img>`(替换元素)不受盒宽约束保持固有宽度,加上盒子 `overflow:visible`,图片居中外溢每侧盖住邻图 ~44px——且发生在滚动条出现之前,两症状同屏;加载完成瞬间行高还随最高图 84→130px 跳变(CLS)。
155
- - **修复(统一瓷砖)**:盒子 `flex:0 0 auto`(几何与图片数量/加载态无关,永不收缩)+ 固定 176×110 + `overflow:hidden`(外溢兜底双保险);img 改 `width/height:100%` + `object-fit:cover` + `object-position:top center`(顶锚定保住截图标题栏,裁切只发生在底部,竖图/方图露上半段,全图看灯箱);行加 `scroll-snap-type:x proximity` + 盒 `scroll-snap-align:start`(横滚翻页手感)。灯箱、走链、坏图剔除、键盘导航语义零变化。
156
- - **验证**:1332 项测试全绿(新增截图条瓷砖 CSS 源锚 2 用例:统一瓷砖形态 + 旧病灶负向锚)、typecheck 零错误;构建产物含新规则逐字锚。方案对比 mock(现状复刻 + cover/contain/网格三案实况)先行经主人选型(A 案 cover)。
157
-
158
- ### 0.9.72 修复:设置页滚轮黑洞——操作记录滚动区/横幅的 contain 改为「可滚才挂」(Chrome 144 规范对齐暴露)
159
-
160
- - **根因(主人设置页实拍:操作记录条数少、滚动条尚未出现时,鼠标悬停该区域滚轮无法翻页)**:`.dshm-ops-scroll` 恒挂 `overscroll-behavior:contain`(0.9.64 随 220px 滚动区引入)。`overflow-y:auto` 使元素无论内容多少都是滚动容器,而规范口径「没有可滚溢出的滚动容器恒在滚动边界」+ contain「到边界禁止向祖先链滚」→ 内容不满时整块区域吞掉滚轮。Chrome 144 起「Respect overscroll-behavior on non-scrollable scroll containers」落地(此前引擎仅真可滚才执行 contain,病灶被宽容掩盖),潜伏缺陷被引擎升级激活;错误/提示横幅 `.dshm-banner-text`(限高滚动时代引入的同款恒挂)同病。
161
- - **修复(意图精确化:有得滚才断链)**:新增 `useScrollableContain(dep)`——实测 `scrollHeight > clientHeight`(+1 容亚像素取整)才挂 `.is-scrollable`(= contain),容器与首子元素入 ResizeObserver,改宽折行/注记增删引起的可滚性翻转自动复检,SSR/Node(ResizeObserver 缺席)安全跳过;操作记录滚动区、重启横幅、toast 三处接线。钢人结论:0.9.64 每个局部决策(限高滚动区/50 条上限/清除钉底/contain 自身的嵌套滚动卫生)都成立,错的只是「静态 CSS 事实表达了动态适用前提」——日志不满限时滚轮还给页面,日志实滚时边界断链照旧。
162
- - **顺带**:滚动区补 `tabIndex=0` + aria-label(0.9.45 U8 键盘可达纪律延续,50 条记录此前键盘用户无法滚动);布局契约不动(通栏/220px/钉底照旧);布局 mock 同步(`.opscroll` 双规则 + 样本标注实滚类)。
163
- - **验证**:1330 项测试全绿(新增 `client-scroll-contain` 结构锚 4 用例:恒挂形态退役/条件规则在/hook 守卫/三处接线)、typecheck 零错误。
164
-
165
- ### 0.9.71 功能:设置类操作记录——社区目录开关与清单源变更入账(DESIGN §2.6 修订)
166
-
167
- - **背景**:设置页「社区目录开关」与「校验并应用 / 恢复默认」(清单源)的变更此前不入操作记录,事后无法回看「何时改的、从哪改到哪」——而 agent 供给面(`dshm_search` 可见目录规模 20 余精选 ↔ 4,000+ 社区)与信任根(自定义清单源)恰恰挂在这两个开关上。
168
- - **方案(DESIGN §2.6 修订)**:操作记录边界从包级事务扩至**供给面/信任边界设置变更**,新增两类**终态直落**记录(不入泵、创建即终态、不参与恢复重放;恢复原样保留 / 清除 / 红点语义天然兼容):`community-toggle` 成败皆记(失败 = 已接受的变更尝试中途回滚,有「以为关了其实没关」的误信风险;meta.on),`registry-url` 仅记成功(校验拒绝零状态变更且卡片红字就地呈现,高频试错入记录只会制造红点噪音;target = 新地址、恢复默认记 `(default)`,meta.from = 原地址)。记录行 target 展示派生:开关按 meta.on 显「开/关」,清单源保持 URL。
169
- - **口径**:面板发起的变更(CLI、`dsh plugin add`、DSH 设置表单直改不留痕,与包操作既有口径一致)。
170
- - **评审**:独立 subagent 盲评 0 blocker / 0 major,4 minor + 3 nit 全部随本版修掉(done 打点紧贴 api 成功消除刷新链吞错隐式依赖;测试计数桩钉死「终态不咨询恢复谓词」;`(default)` 收敛共享常量等)。
171
- - **验证**:1326 项测试全绿(新增设置类终态直落 describe 块 3 用例)、typecheck 零错误。
172
-
173
- ### 0.9.70 修复:拖拽观感与静止态一致——退役拖拽期实心固化(ADR-0017 v2)
174
-
175
- - **根因(用户浅色模式双态实拍)**:0.9.69 的拖拽期「计算值固化强制实心」在按住/松手瞬间造成实心↔玻璃的观感跳变,浅色模式下尤为突兀——防透字与观感连续两个目标里,实心方案牺牲了后者。
176
- - **修复(ADR-0017 决策 3 修订 v2)**:拖拽态与静止态**同玻璃同 blur,外观逐像素一致**——透字防护回归静止态既有不变量(毛玻璃模糊背景自持;透字病灶的成因是「关 blur 还留半透明」,两样都不动即无此病,openbmc 静止态可读性本就由 blur 兜住)。拖拽可供性只剩 grabbing 光标与投影加深;`solidifyColor`/`--dshm-drag-bg`/拖拽态 `!important` 全部退役(清单定长回归 4,`client-lightbox` 断言含「无残留」负向锚)。性能保底手段(pointer capture/手势期监听/rAF 帧内写/translate3d/will-change)全部保留;拖拽期恢复的每帧 backdrop 重滤镜由 GPU 合成承担,弱机若报告掉帧再评估。
177
- - **验证**:1323 项测试全绿、typecheck 零错误;ADR-0017 修订 v2(Considered Options 翻案记录在案)。
178
-
179
- ### 0.9.69 功能:还原态面板头部拖拽 + 位置记忆(ADR-0017)
180
-
181
- - **方案**:市场面板(`.dshm-panel`)还原态下按住头部(标题/tab 行)即可自由拖拽——头部全域为手柄,tab/全屏/关闭钮点击语义原样(`closest("button")` 门卫 + 多指重入守卫);双击头部非按钮区切换全屏;释放位置烘焙为视口绝对坐标存 `dshm-panel-pos`,下次打开还原,无记忆首开与旧版 flex 居中渲染逐字节一致。运动范围四周 8px;desktop profile 额外让出壳层标题拖拽带(quota-watch 契约:`data-windows-titlebar` + `--dsh-windows-titlebar-height`,web 无此变量恒 0)——胶囊时代「停进系统按钮区就再也抓不回」的坑由 clamp 机制性杜绝。挂载/全屏往返/resize 三触发源重夹紧,还原 commit 的重夹紧时序由 render 期 latest-ref 镜像保证(执行评审 E1-1 实证修复并以结构锚钉死)。
182
- - **皮肤免疫(拖拽期 = 最快渲染路径)**:拖拽背景取手势开始时 `getComputedStyle` 的最终色强制 α=1 写入 `--dshm-drag-bg` 并 `!important` 自持——openbmc/uefi 玻璃皮肤(bg-base α=0.55 病类,quota-watch 胶囊拖拽变半透明的同款病例)下按住拖动面板完全实心不透字;同时拖拽期关闭 backdrop-filter,每帧只剩 compositor 平移一层:pointer capture + 手势期才挂 document 监听 + pointermove 只存点、样式帧内写 + 拖拽帧绝不走 React state。静止态玻璃外观、0.9.27 防误关守卫、灯箱/详情 modal 语义全部不动。
183
- - **验证**:1326 项测试全绿、typecheck 零错误;构建产物含 `dshm-panel--dragging`/`dshm-panel-pos` 逐字锚;`!important` 清单定长 4→7(client-lightbox 断言登记)。实施计划与执行结果经独立评审 Agent 五轮对抗评审全闭环(16 条:13 条修于计划、3 条修于代码/如实申报)。
184
-
185
- ### 0.9.68 性能:合并市场物化——同身份免重付 5MB 解析 + 适配 + 合并(ADR-0016)
186
-
187
- - **问题(性能架构审查旗舰项,三轮评审闭环)**:领域概念「合并市场(Merged Market)」在 GLOSSARY 有名无主——装配逻辑内联在 `communityOutcome`,每个市场请求(缓存全命中的快路径也在内)都重付磁盘读 5.36MB 社区目录 + JSON.parse(实测 30–55ms)→ 目录适配层重建 4,400 条(30–90ms)→ 合并排序(5–35ms);且全部是**同步 JS、阻塞宿主事件循环**——市场每翻一页/每提交一次搜索,宿主的 SSE 对话流与工具调用停摆 65–400ms。搜索评分因归一化 WeakMap 跨请求永不命中再加 ~330ms(冷)。
188
- - **两级 memo,各持天然安全键(ADR-0016 决策 1)**:L1——community.ts `readCache` 按不可变 version-pin 文件名缓存已解析目录(cap=2,meta 每次实读保 checkedAt 新鲜,失败路径不入 memo);L2——新模块 `src/core/merged-market.ts` 物化适配→合并→匹配索引(cap=4),身份键 = (namespace, profile, registry 身份, 目录 version+pin+开关)。
189
- - **探测驱动换代 + 同步临界区(决策 3)**:每请求照常走廉价快路径(registry ~10KB + meta 141B ≈1–2ms)以返回身份字段查代;全部 await 位于键派生前、临界区全同步——并发同键天然只建一次,`builds` 计数断言哨兵。**summary 永不入代**每调用现算(status/checkedAt/errors 永远新鲜,SWR 与⑫同版本短路零漂移);deadline/unavailable/disabled/task=null 透传不落代;communityTask reject 透传(安装侧窄 catch 吞为 miss)。
190
- - **双索引保安装语义(R2-3)**:merged 域 `lookupInstalled` 服务读路径;全量域 `findById`/`lookupInstalledAll`([primary…, adapt 全量] 含 displaced 让位条目)服务安装/升级三处(resolveRegistryEntry / upgradePlugin / profile-ops desktop 升级)——id-让位条目今日可装可升,语义零变化;主清单优先短路保留(社区异常不阻断主清单命中)。
191
- - **解环 + score-once**:合并层六符号自 market.ts 迁入 merged-market.ts、23 条分类标签自 community.ts 随迁(原位 re-export 保 cli/tools 零改动)——market↔community import 环三方向断开;listMarket 评分段单遍化(类别短路先行、rank 查表),tie-break 与 0.9.67 逐点一致。
192
- - **行为零变化承诺**:ADR-0003/0006/0008/0012/0013 全部不动;golden 等价基线(七维度 capture,含分页/搜索/排序/探测徽标)全程逐字节绿。
193
- - **验证**:1305 项测试全绿、typecheck 零错误;实测本机 4,460 条真实目录首次 319ms → 同身份第二次 65ms,搜索评分冷 330ms → 同代热 8ms(40×)。实施经三轮独立评审闭环(计划评审 16 项 + 执行评审 7 项,全部「已修复并复核通过」;含一次修复脚本静默 no-op 的虚报,被评审 `git diff` 核对抓出并以真实 diff 补正)。实施计划与 ADR-0016 v3 在案(`docs/plans/`、`docs/adr/`)。
194
-
195
- ### 0.9.67 修复:操作记录卡移动端字号失控——显式入 12px 家族 + 折行自洽
196
-
197
- - **根因(用户手机截图反馈)**:操作记录行 `.dsvm-oprow` 从不声明 `font-size`——桌面宿主基础字号下无感,移动端浏览器基础字号(≈15-16px)下整行被放大近一倍,与同屏设置卡的 12px 家族(`dsvm-kv` 配置地址行、`dshm-hint`、`dsvm-fold`)明显脱节,观感突兀。
198
- - **修复(对齐周边卡片惯例)**:`.dsvm-oprow` 显式 `font-size:12px` 入族;状态字(完成/失败)保持 11px、错误/警告注记保持 11px 的次级层级不动;`flex-wrap:wrap` 让长目标名(如带 scope 的 npm 包名)与错误注记自然折行不挤压;单条移除 ✕ 钉行尾(`margin-left:auto`),与卡片行惯例一致。进行中/已结束分组标题(11px)与 220px 滚动区、空态 hint 均不受影响。
199
- - **验证**:1289 项测试全绿、typecheck 零错误;构建产物含新规则逐字锚。
200
-
201
- ### 0.9.66 修复:触屏滑动方向与移动端惯例相反(用户实机反馈)
202
-
203
- - **根因**:0.9.65 触屏滑动把映射锚在了键盘光标隐喻上(`ArrowRight=next` ⇒ 手指右拖=下一张),而触屏的正确隐喻是**内容拖拽**——手指拖着图像条走,**往左拖露出右边那张=下一张**(相册/微信/车龙全部如此)。`swipeDir` 返回的手指位移方向被原样喂给了 `lbStep`,致真机上「左滑出上一张、右滑出下一张」,与全部肌肉记忆相反。
204
- - **修复**:调用点取反映射 `lbStep(index, -dir, …)`;`swipeDir` 纯函数语义不变(仍报手指方向),文档注释明确「返回值是手指方向而非翻页方向,调用点必须取反」。键盘 ←→ 不动(桌面光标惯例:←=上一张、→=下一张,两者隐喻各自成立互不干扰)。
205
- - **探针同批修正**:`verify-lightbox.mjs` A31a/A31b 断言按正确惯例重写(修正后旧断言转红即方向确已翻转的实证);附带修正探针编号撞车——尾部既有 Primary 场景 A26–A29 保留,新特性场景编为 A30(预取命中)/A31(滑动)/A32(焦点圈闭);A19 重写为时序确定性版双断占位(当轮验证附带产出:疑似回归经消融+worktree 对比+确定性时序三重取证定谳为「预取预热了场景前提图」,非回归,取证记录见探针内注释)。
206
- - **验证**:1289 项测试全绿、typecheck 零错误;浏览器探针 50/50(A31 五断言含方向翻转实证 + A32 圈闭六断言 + A30 预取命中 + A19/A20–A25 既有锚全数保持)。
207
-
208
- ### 0.9.65 优化:灯箱邻图预取 + 触屏滑动切换 + 焦点圈闭
209
-
210
- - **邻图预取**:当前图加载完成(`useImgChain` settled)后静默预热左右邻——URL 用与灯箱真实请求**逐字节同形**的 `weservUrl(w=1600)`,翻页直接命中浏览器缓存近零等待。纯 best-effort:不碰赢家记忆/链状态,预取失败对真实链零影响(导航时照常走 weserv→原图双兜底);`prefetchedRef` Set 去重防来回翻页重复发起,Set 随灯箱挂载期生命周期(重开重新预热);单图由 `lbNeighbors` 空表自然短路。
211
- - **触屏滑动切换**:touch 指针横向滑过阈值即翻页(`swipeDir` 纯函数判向:|dx| ≥ 48px 且 |dx| ≥ 2|dy|;不设时长门槛,慢拖也算;竖滑/斜滑归零——纵向手势与 pinch 缩放可达性留给原生,`touch-action:pan-y pinch-zoom`)。只认 `pointerType==="touch"`——鼠标拖拽维持原生语义(图=原生拖拽、遮罩=按下+点击才关,0.9.27 防拖拽误关守卫不破);滑动终结后 500ms 内根上 capture 吞掉合成 click(双保险:即便滑动止于 ✕/遮罩也不触发其点击语义)。判向/邻图两枚纯函数入 `lightbox.js`(`swipeDir`/`lbNeighbors`,Node 单测覆盖)。
212
- - **焦点圈闭**:灯箱开着时 Tab/Shift+Tab 只在灯箱内可聚焦控件(按钮/链接:✕/‹›/失败态重试+打开原图)间回绕;焦点已逃出灯箱(如点击遮罩落在 body)时拉回首/尾。portal 挂 body 后 Tab 本会走到背景面板,此处收口;DetailModal 自身 U8 债(无圈闭)维持不变,Esc 仍只关最上层弹层。
213
- - **验证**:1289 项测试全绿、typecheck 零错误;`client-lightbox.test.mjs` 新增 6 项(`lbNeighbors`/`swipeDir` 纯逻辑单测 + 预取/滑动/圈闭/touch-action 结构锚),0.9.60/0.9.62 批次既有锚全数保持。
214
-
215
- ### 0.9.64 优化:设置页双清单并排重排 + 操作记录整体搬迁设置 tab
216
-
217
- - **设置页网格(mock 驱动终审)**:社区清单/精选清单两卡并排 **4:7**(社区卡只读五项 ~250px 足矣,宽度让给交互更多的精选卡);全屏态「关于」升入三卡行(`4fr 7fr 3fr`,不封顶——对齐市场「加列不加宽」哲学,设置页恰有三个卡片单元可用);视口 ≤760px 单列回落;关于卡常态通栏、操作记录恒通栏(`nth-child` 定位,与 `Section()` 签名解耦)。
218
- - **社区卡清爽列表**:标题行回归纯标题(去 `from` 前缀);状态徽章降为绿/黄/红**小圆点**;行间发丝分隔线 + 数值右对齐等宽数字;「社区目录开关」入列表为末行(开关是目录属性,钉底反而脱离语境);关态 = 提示行 + 开关行(重开入口不丢)。中途方案「统计带」(白盒大数字)经实评否决——卡片中的卡片是视觉噪音。
219
- - **精选卡减行**:说明行缩短(被删的「条目格式可参照下载的默认清单」与下载按钮 tooltip 重复,零信息损失);配置地址长 URL 单行省略 + `title` 悬浮全文。
220
- - **操作记录搬迁(第二批)**:三 tab 共用的底部常驻区退役;`OperationsPanel` → `OperationsCard` 落位设置 tab 末位通宽卡——进行中组滚动区外常驻(进度不被滚走),已结束组进 220px 滚动区(展示上限 50 条防 DOM 无界,store/恢复校验不动),「清除已结束」钉底,空态 hint 卡常驻导览。
221
- - **可见性补偿**:底部消失后失败/带警告终态无人主动发现 → 设置 tab 红点(复用 outdated 红点样式,清除/移除后随 syncOps 自灭);市场卡「进行中」徽章点击改道:切设置 tab + 滚动定位操作记录卡(文案「查看操作面板」→「查看操作记录」)。
222
- - **验证**:构建 verify 通过、typecheck 零错误、全量 1283 项测试连续 4 轮全绿(首轮 1 例与改动无关的瞬时 flake 未复现);布局终稿经交互式 HTML mock 三宽度 × 三布局 × 多状态现场评审定稿(`docs/settings-layout-mock.html` 入库作设计记录)。
223
-
224
- ### 0.9.63 修复:点在加载转圈上不再无动作——转圈显式接 onClick=onClose
225
-
226
- - **背景**:0.9.62 给灯箱加了走链转圈占位,但转圈是遮罩根的**子元素**,而 `backdropCloseHandlers`(0.9.27 防拖拽误关)只在 `mousedown` 与 `click` 都命中遮罩自身(`target === currentTarget`)时才关——点在 34px 转圈正中因此无动作。转圈显式接 `onClick: onClose`,与「点图即关」同语义:走链期间的任何一次点击都是有效的关闭逃生门。
227
- - **验证**:`client-lightbox.test.mjs` 转圈锚随签名更新(门控 + onClick 语义);全量测试/typecheck 照常。
228
-
229
- ### 0.9.62 修复:灯箱控件浅色模式下不可见——影院遮罩自持 + 控件深色玻璃芯片 + 加载转圈/淡入
230
-
231
- - **根因(2026-10-09 用户浅/深双模式实拍)**:dsh-skins ADR-0007 浮层毛玻璃并集按 `[role="dialog"][aria-modal="true"]` 命中灯箱根(灯箱根带 dialog 语义),浅色模式下 86% 暖白 tint + blur14 把影院式黑遮罩改成了浅色毛玻璃;而 ‹›/✕ 沿用基础 `.dsvm-btn` 白色半透配方(`rgba(255,255,255,.14)` 底 + 白字白边),浅底上整体隐形。深色模式走皮肤 `rgb(18,18,26)` 分支尚可读,故「浅色看不清、深色还行」。无皮肤的原生形态(黑遮罩恒在)无症状——这正是它漏网到 0.9.61 的原因。
232
- - **双侧修复**:① dsh-m 侧(本版):灯箱 `background` 加 `!important` 自持——「深色遮罩 + 浅色控件」是影院面的组件自有可读性不变量,不随颜色模式/皮肤翻转;marketplace 社区皮肤不可枚举,不变量必须组件自己兜底(`!important` 清单定长 4,回归门钉住,新增须显式登记)。② dsh-skins 侧:三皮肤 `[role="dialog"][aria-modal="true"]` 追加 `:not(.dsvm-lightbox)` 豁免(1.5.1,ADR-0007 amendment)——语义上「弹窗毛玻璃」本就不该染指图片影院。
233
- - **控件深色玻璃芯片**:‹›/✕ 从裸透明白改为与底部 pill 同族的深色芯片(slate-900 55% + 白描边 + blur8 + 投影)——深遮罩上靠描边/blur/投影出层次,浅遮罩上芯片本身就是对比面,任何颜色模式与皮肤下都可读;显式 `:hover` 提亮(特异性 0,3,0 压基础 `.dsvm-btn:hover` 0,2,0,免疫源顺序——评审 R1 同族教训)。
234
- - **加载指示与淡入(UX 补强)**:灯箱走链期间(weserv 慢速/8s 守卫换层/直连大图)此前是空黑黑洞,现转圈占位(`useImgChain` 暴露 `settled`);图片 onLoad 后 160ms 淡入换场;`prefers-reduced-motion` 下停用淡入、转圈降速。已知小瑕疵:转圈是遮罩的子元素,`backdropCloseHandlers` 防拖拽误关只认根元素——点在 34px 转圈正中无动作(点周边遮罩可关),0.9.63 补 `onClick` 收口。
235
- - **验证**:1283 项测试全绿、typecheck 零错误;`client-lightbox.test.mjs` 新增 6 项结构锚(遮罩 `!important` 定长清单/芯片规则与显式 hover/转圈门控/淡入类名/settled 暴露/reduced-motion),0.9.60 批次既有锚(portal/复合选择器/拖拽带/焦点还原/序号 key)全数保持。
236
-
237
- ### 0.9.61 修复:大陆浏览器拉不到插件截图/图标——图片加载链 weserv 优先双兜底
238
-
239
- - **根因(2026-10-09 实测)**:截图(raw.githubusercontent.com)与图标(github.com 头像)由用户浏览器直连拉取——服务器侧 200/0.2s,大陆浏览器路径时断时通,当日故障样本 dsh-wallpaper-engine 五图(22.5MB,含 10.4MB GIF)缩略图条与灯箱全空;0.9.60 已排除灯箱 v2 回归,图标「能显示」实为本地字母兜底。对标 dsh-market 1.66.14:缩略图无条件 weserv(其源码实测大陆 1.39s/23KB vs 原图 41KB)但灯箱直连零兜底——大陆点开大图同款黑洞。
240
- - **图片加载链(ADR-0014)**:三类消费方(缩略图 h=300 / 灯箱 w=1600 / 图标 h=96)共用 `useImgChain`——weserv 层(`fit=inside&we=1&output=webp&q=80` 服务端缩放转码)优先,失败或 8s 人工超时(仅此层;直连层零人工超时防误杀慢速合法下载)换原图直连,再败走终态:缩略图剔除(全败隐藏整条)、灯箱占位「⚠ 失败 + 重试 + 打开原图 ↗」、图标字母渐变兜底。赢家记忆按 raw/avatar 两桶记最近成功层,**链启动时快照次序**——同批多图竞态下兄弟图不因偏好翻转跳层(执行期实证:缩略图 1 经直连成功翻转偏好后 2/3 全灭的竞态);链内前进序感知(偏好为 direct 时 direct 败仍按序试 weserv,固定阶梯会跳过兜底直判死)。四处 shots 基准同步换 visible(整条门控/截图条 map/灯箱门控/键盘导航+effect 依赖),列表保持全量 map + `${i}:${src}` 稳定 key,灯箱 index 钳制防越界。
241
- - **配套**:纯逻辑入 `src/client/img-chain.js`(URL 构造/桶分类/序感知 tier 机/赢家记忆,`WESERV_BASE` 单常量——未来设置项只改一处);GLOSSARY 增「图片加载链(Image Chain)」一条;探针扩展 A16-A24(断直连/断 weserv/双断占位+重试/weserv 挂起 8s 守卫/赢家记忆/缩略图剔除/direct-first 换层 Probe 场景)+ `--live` 真网压缩实证;执行期探针 harness 三大确定性根基:**每场景全新 page(独立 context,消灭路由复挂竞态与跨场景解码复用)**、代次化 URL(凡需失败的 URL 全页生命周期唯一)、层序断言走 reqLog(page.on('request') 不依赖路由拦截)。
242
- - **验证**:1256 项测试全绿(基线 1236 + 新增 20)、typecheck 零错误;探针 31/31 三连跑稳定;`--live` 压缩实证 mascot-drawer.png 直连 2859KB vs weserv 151KB(≈18.9×)。评审吸收轮(完整多轮独立 subagent 评审):R1-1【阻断】8s 守卫在成功加载后不清除——成功图 8s 后被强制换层重载(大陆场景缩略图误剔除/图标字母化/灯箱变占位),修以 settled 语义(onLoad 置真、链启动/翻页重置、timer 四条件守卫含空 url),并新增探针 A25(成功驻留 >8.5s 行为钉子,旧代码必红)与计时门控源锚;R1-2 A22 补负向断言(导航后零 weserv 形态请求)、A24 断言首请求形态(赢家记忆翻转获得证伪力);R1-3 freshState 清 reqLog(场景封闭);R1-5 url 变化改渲染期 derived-state 重置(消除中间帧幽灵请求);R1-6 --live 双侧校验 r.ok + image/*(错误页不得假 PASS);R2-3 advance 防御性 settled 重置。第二轮终局裁决:共识 approve。遗留后续项:中间帧消除的行为级探针(现由源锚防回归)。
243
-
244
- ### 0.9.60 修复:截图灯箱窗口态无导航/无法退出——createPortal 逃出面板包含块 + 控件常驻
245
-
246
- - **根因**:灯箱 `position:fixed` 是 `.dshm-panel`(`backdrop-filter` + `overflow:hidden`)的后代,面板盒成了它的包含块——图片按视口单位放大(旧 94vw/80vh),在 ≤680px 高的窗口态面板里必然溢出,in-flow 的 ‹›/圆点被裁出屏外,又没有 ✕;全屏态面板恰为视口才「碰巧」可见(即「只有全屏才有导航按钮,窗口态图片占满小屏且无法退出」)。
247
- - **v2 修复(`b89af18`)**:① 灯箱 `createPortal` 挂 `document.body`——逃出包含块,窗口/全屏/窄屏任何状态都按真实视口定位(SSR/无 document 回退原树,冒烟测试照常渲染);② 控件全部绝对定位贴边,构造上不可能被图片挤出屏——右上 ✕ 常驻、两侧 ‹› 大热区、底部「计数+圆点+操作提示」pill,图片降为 74vh 给控件留位;③ 退出路径四条:点图即关(zoom-out 光标)、点遮罩关、Esc 关、✕ 关——键盘 ←→/Esc 语义不变(DetailModal 文档级监听不受 portal 影响);④ 单图不渲染 ‹›/计数/圆点(无死控件),圆点热区 `background-clip:content-box` 扩到 18px,开灯箱即聚焦 ✕(对齐 DetailModal U8 先例)。
248
- - **配套**:环形步进抽纯函数 `lbStep`(`lightbox.js`,组件与键盘共用,单测覆盖回绕/单图/非有限输入);新增 `client-lightbox.test.mjs` 结构守卫(portal/✕/点图关/单图收敛/CSS 绝对定位/内联取模退役),SSR 冒烟补灯箱多图/单图结构断言。
249
- - **评审吸收轮(独立 subagent 评审,发布前落码)**:R1 CSS 级联——`.dsvm-btn` 基础规则源顺序靠后,实测反杀 ✕/‹› 的 `padding:0/font-size:26px/radius:10px`(‹› 字形实渲 13px),改复合选择器 `.dsvm-btn.dsvm-lb*`(特异性 0,2,0,顺序免疫);R2 Windows Desktop 拖拽带——灯箱 `top:var(--dsh-windows-titlebar-height,0px)` 让带(0.9.4 先例,Web 无变量回落 0px 行为不变),✕ 不再落进壳顶 `-webkit-app-region:drag` 吞点击带;R3 焦点还原——关闭灯箱把焦点归还触发的截图缩略图(捕获 effect 声明于聚焦 ✕ 之前,`isConnected` 防已卸载元素);R4 灯箱圆点与缩略图条 key 改序号(URL 重复不撞车);R5 无头探针固化为 `scripts/verify-lightbox.mjs` 入库(手动跑,需 playwright-core),结构断言收敛为 includes 级锚(逐字正则红灯语义是「文本变了」非「行为变了」),SSR 冒烟补 `role=dialog/aria-modal` 与重复 URL 圆点断言。
250
- - **验证**:1236 项测试全绿、typecheck 零错误;`scripts/verify-lightbox.mjs` 复刻 `.dshm-overlay > .dshm-panel` 真实语境 19/19 断言——面板内 fixed 探针盒=面板盒(包含块实证)、灯箱盒=全视口、✕/‹›/pill 全程在视口内、**计算样式级联正确(R1 回归门)**、›/圆点/‹/←→ 导航与 Esc/点图关闭正确、**关闭后焦点还原缩略图(R3)**、**拖拽带 40px 避让(R2)**、全屏态回归、500px 矮窗口控件仍常驻(旧版此处必裁)。
251
-
252
- ### 0.9.59 优化:分类行左缘内缩 8px + 全屏下空转 ⌃ 收起钮按行数派生隐藏
253
-
254
- - **分类行内缩(`f42e5be`)**:分类 chips 行与搜索框/卡片左缘齐平,小颗粒胶囊贴着大元素左缘视觉憋仄;分类行整体右移 8px 留出呼吸空间。仅作用分类行(ZoneChips),上方分区 chips 行不随动。`offsetLeft` 与 absolute 定位同以 `.dshm-chips` 的 padding 盒为原点 → `+N` 跟随、右侧筛选钉位、`offsetTop` 裁剪几何均不受影响。
255
- - **⌃ 空转修复(`7cbdf11`)**:全屏等宽容器下分类 chips 可能排进 ≤ maxRows(2)行,展开态 `⌃` 收起钮点击后布局零变化(`hiddenCount=0` 连 `+N` 都不会出现),成死按钮。新增展开态行数测量 `expRows`(复用 `chipRows` 纯函数,measure 展开态分支只写 `expRows` 不动 `geom`),`⌃` 仅当展开行数 > maxRows 时渲染;ResizeObserver 在展开态也挂载——全屏↔窗口切换的宽度变化正是重测触发源,全屏下 `⌃` 自动隐藏、退回窗口自动恢复,行数含 `⌃` 自身故显隐收敛不振荡。
256
- - **验证**:1224 项测试全绿;真实 DSH Web 实测窗口/全屏两态 `⌃` 显隐与收起行为、分类行 8px 内缩生效。
257
-
258
- ### 0.9.58 优化:+N 动态紧跟末可见分类,并与其框高一致
259
-
260
- - **位置(`4ade54f`)**:在 0.9.57 的独立安全槽位中,根据末可见分类 chip 的实测右缘设置 `+N` 左缘 = 右缘 + 6px;槽位由 52px 扩为 60px,窄宽时仍不与分类或筛选重叠。精选区没有筛选时不额外预留筛选 gutter;展开态 `⌃` 仍位于列表尾部。
261
- - **框高(`aeed14c`)**:`+N` 直接采用末可见分类的实测 border-box 高度,并以 inline-flex 居中数字,不再依赖西文数字与中文标签各自的浏览器自动行高。只改 `+N`,不改变分区 chip 的尺寸。
262
- - **验证**:1224 项测试与 typecheck 全绿;用真实 DSH 目录数据在 880/700/520/420/320px 社区区测得两框同高 21px、顶边差 0、水平间距约 5–6px、相交面积 0;260px 精选区也无重叠,精选↔社区往返正常。
263
-
264
- ### 0.9.57 修复:精选切换后面板崩溃/无法重开 + 折叠筛选双框与 +N 重叠
265
-
266
- - **根因(0.9.56 真机复现)**:精选区的标签对象可能每次父级重渲染都重建;layout effect 依赖 `chips` 数组引用及自身更新的 `geom`,同步测量反复进入、最终触发 React `Maximum update depth exceeded`(#185)。React 清空面板树但留下 `#dshm-panel-root`,旧入口守卫只检查容器存在,导致点击入口永久无效。叠加折叠筛选定位壳和按钮的双边框、以及 +N 位置夹取/文档流试验在窄宽下压住相邻 chip。
267
- - **修复**:按实际渲染的分类 id/文案/计数生成稳定内容签名,layout effect 不再同步依赖自身 `geom`;预留槽变化改在上一轮状态提交后的 rAF 复测。回退 0.9.56 的 `display:none`/in-flow 反馈链,恢复分类全量占位 + 越界 `visibility:hidden`,以完整 `offsetTop` 按行纯派生隐藏数;+N 脱离文档流,独占右侧 52px 槽(筛选另占 96px),双类 CSS 选择器确保总预留不被 gutter 覆盖。末可见行顶按**去重行**而非第 N 颗 chip 计算。筛选外壳去边框仅保留定位;若崩溃留下无可见面板的空容器,再次点击时先移除再挂载。
268
- - **取舍**:+N 优先保证零重叠、正确计数和可重开;不再强求其贴着最后一颗分类。窄宽下边界分类自然少显示,`+N` 如实计入;展开态 `⌃` 仍在分类列表末尾。
269
- - **验证**:新增 3 个回归断言,累计 1224 测试全绿、typecheck 零错误;真实 DSH Web 加载候选客户端验证精选↔社区连续三次、关闭再开及崩溃残留空容器恢复;260–880px 六档 +N/chip/筛选相交面积恒 0,精选五类不出现错误 +N。
270
-
271
- ### 0.9.56 变更:ZoneChips +N 重叠根治——in-flow 文档流布局 + 折行减类(主人方案)
272
-
273
- - **缺陷(0.9.55 真机复现)**:折叠态 `+N` 采用「绝对定位 + contentW−48 夹取」,当末可见行剩余空间小于徽章宽时,徽章压住末可见 chip(真机:+9 压住「文档与渲染 61」右端计数)。
274
- - **根治(in-flow 文档流布局)**:`+N` 弃绝对定位,以 in-flow 插在「末可见分类」与「被裁分类」之间——flow 布局与 chip 的重叠在构造上不可能。
275
- - **折行减类(主人方案)**:被裁分类 `visibility:hidden` → **`display:none`**(不占位,折行减类生效前提);测量发现 `+N` 顶 > 末可见 chip 行顶(被折出)⇒ `hiddenCount +1`,一枚可见分类退位、`+N` 退回末可见行,同时 `max-height` 扩一行给 `+N` 独占;棘轮单向下行,宽度增大时保守不多显(+N 计数仍真实)。
276
- - **测试**:全量 1221 例零失败、typecheck 零错误;无头探针实证常宽 880/窄宽 420/展开/收起/Q1 回归全部 `chipOverlapPx = 0`、`+N` 全程可见、展开态 23+⌃+筛选全量、收起后 +N 回归蓝筐位。
277
-
278
- ### 0.9.55 变更:ZoneChips 折叠/展开切换钮内联化(对齐 dsh-market 参考设计)+ overlay 叠涂 scrim 强化对比度
279
-
280
- - **切换钮内联化(主人提出,dsh-market v1.66.11 参考设计)**:`+N` 从右侧 overlay 组改为**绝对定位内联**——钉在「末可见分类 chip 右侧 +6px」(即末可见行剩余空位),与展开态 `⌃`(列表末尾 in-flow)构成统一位置语义「紧跟当前列表末尾」,只翻方向不挪位;窄宽度放不下时贴内容右缘收口(不侵入筛选槽)。gutter 随 `+N` 移出由 132px 瘦身至 96px(仅承载钉住筛选),同宽度可多显示约 1 颗分类。overlay 组内 `+N` 移除后仅剩钉住筛选,第④类碰撞(R2-N1)自然消解。
281
- - **overlay 叠涂 scrim(主人提出:浅色主题对比度偏低)**:根因是 `--dsw-alias-bg-base` token 本身 0.55 半透明;overlay 底衬改同 token **三层叠涂**(background-color + 双 linear-gradient 层),有效不透明度 ≈ 1−0.45³ ≈ 91%,深浅主题自适应(深色 rgba(12,26,38,·) / 浅色 rgba(247,250,252,·) 均显著强化),不引入硬编码色值。
282
- - **测试**:全量 1221 例零失败、typecheck 零错误;无头探针实证:折叠态 `+N` 左缘 = 末可见 chip 右缘 +6px 同行对齐、scrim 双梯度、筛选单钮零碰撞、收起/展开往复位置稳定、Q1 回归(长文案 hidden>0);真机截图见发版记录。
283
-
284
- ### 0.9.54 变更:评审修复随版(ZoneChips 重测盲区/⌃ 弹回/overlay 居中)+ dsh-skip-browser-auth 收录文案对账
285
-
286
- - **ZoneChips 三处评审修复(执行结果评审 R1,`b2aab5f`)**:① 测量 deps 以 `chips` 引用替代 `chips.length` 并叠加 `active`——同长度内容变化(计数刷新/激活加粗变宽)确定性重测,消除 RO 因 max-height 钳制失聪导致的陈旧 geom 盲区(被裁 chip 保持可聚焦/激活分类越界不自动展开的失效窗口,D4 违背态);② `⌃` 手动收起无条件记录 autoRef——手动收起全路径被尊重,消除展开态 geom 陈旧时收起被决策 effect 弹回的边缘漂移;③ overlay 组按 rowHeight 垂直居中(bottom 补偿偏移)。
287
- - **收录文案对账(回应评审 Q5/Q7)**:dsh-skip-browser-auth 描述两轮更新随版生效(`ed6043a` 判别修复说明 + 缺陷警示、`23da0fd` 警示期结束瘦身保留保持最新版提示);自本版起发版前执行 tag↔CHANGELOG 对账,提交弃用 `git add -A` 改显式定点添加。
288
- - **测试**:全量 1221 例零失败、typecheck 零错误;定向探针实证 Q1(同长度文案变化 hidden 0→5 即时重算)与 Q6(⌃ 收起后 +500ms 不弹回);真机 e2e(V1 冷存储/V4 吸顶/V6 筛选/V7 深色)见 0.9.53 证据链。
289
-
290
- ### 0.9.53 变更:ZoneChips 方案A「隐身全量测量」——冷挂载分类行错误折叠根除 + 行容量即时重测
291
-
292
- - **动机(0.9.52 端到端验证发现)**:新浏览器 profile(无 localStorage 快照)首开面板时,市场数据未到 → `fit` 在空列表上测量 → 数据到达后重测面对的是按陈旧预算折叠的 DOM(只渲染 1 颗 + 「+N」)→ 棘轮式冻结在「3 颗 + +20」单行折叠。暖会话(快照命中)首挂载即全量数据、一次测准,故 0.7.0 以来从未暴露。姊妹问题:测量 effect 不监听宽度变化,窗口缩放后 `+N` 与实际容量脱节。完整根因链与无头实证见 `docs/plans/2026-10-08-zonechips-visual-clip-requirements.md`。
293
- - **方案(方案A「隐身全量测量」,经独立评审三轮 11 条意见收敛,评审记录见计划文档修订记录)**:废除 slice 预算,分类 chips **全量渲染**,折叠改为 `.dshm-chips` 上的 `max-height`(实测行高 × 行数)+ `overflow:hidden` 视觉裁剪;越界 chip 逐颗 `visibility:hidden`(与现状「未渲染不进 tab 序」对齐);`+N`/`⌃` 改右侧 overlay 组(水平并排、钉末可见行右端,gutter `--dshm-clip-gutter:132px` 几何保证与 chip 零相交);筛选触发器折叠态钉 overlay、展开态维持 in-flow;重测钩子 = deps(`chips.length/expanded/zone/stuck/rowHeight`)+ `ResizeObserver`(rAF 节流)+ `document.fonts.ready`,均 feature-detect 降级(先例 `:947`)。测量纯函数(`chipRows/countBeyondRows/clipTopOf/autoExpandDecision`)下沉 `market-state.js`,20 例 Node 单测直测。
294
- - **语义保持**:吸顶收一行/回滚两行(0.9.52 哨兵链路不动,裁剪宿主为 `.dshm-chips`、严禁 wrap 裁剪——wrap 裁剪会剪掉哨兵致 stuck 永久误判,评审 Q1 阻断项);`+N` 展开/`⌃` 收起;激活分类越界自动展开 + autoRef「手动收起后不弹回」(实现拆分为重置/决策两 effect,评审 Q3);0 计数降透明。行为改进一处:折叠态筛选由「行尾第 3 行 in-flow」改为「末可见行右端 overlay 常驻」。
295
- - **测试**:新增 `client-zonechips-clip.test.mjs` 20 例 + smoke 全量渲染断言 1 例,全量 1221 例零失败、typecheck 零错误;无头探针 V1–V6 矩阵实证(冷数据到达两行折叠正确 / 三档宽度即时重算 / 精选区不预留 gutter / 吸顶一行重算且回滚恢复 / 筛选与 +N 零遮挡均可点 / 自动展开与 autoRef 不弹回),探针不入库(0.9.52 惯例)。
296
-
297
- ### 0.9.52 变更:头部行距统一——吸顶哨兵移出 flex 流,消除搜索框与分类行间 25px 空带
298
-
299
- - **动机(主人截图发现)**:市场头部「搜索框行 → 分类 chips 行」间距实测 25px(12 gap + 1px 哨兵 + 12 gap),是「分区 chips → 搜索框行」12px 节奏的两倍余,社区/精选两区一致,视觉上是一条全宽无内容死带;搜索态下 ZoneChips 连带哨兵整体卸载、空带自动消失——证明是布局副作用而非设计节奏。
300
- - **根因**:0.7.0 Task 10 引入的吸顶检测哨兵(1px div,IntersectionObserver 观察其滚出视口 → chips 吸顶自动收一行)是 `.dshm-body`(flex column, gap:12px)的直接子项,多吃一份 gap。哨兵必须在流内才能服务吸顶:直接删除 / `display:contents` / 与 wrap 外包 wrapper 三种朴素方案分别杀死 IO、令收缩永久生效、锁死吸顶行程,均否决。
301
- - **改动(单点,ZoneChips 渲染尾)**:哨兵移入 `.dsvm-chipswrap` 内部绝对定位(`position:sticky` 本身即定位上下文),`top:-5px` 复刻旧几何——旧兄弟哨兵顶边恰在 wrap 上沿上方 5px,IO 出视口触发点逐像素等价。无头 Chromium 探针实证吸顶链路完整:pin 后 wrapTop=0、哨兵 −5px 出视口、IO `isIntersecting=false`(stuck 触发正常)。行距恢复 `zoneBar |12| searchRow |12| chips`:搜索框→分类行 24.36px→12px,头部净省 ~13px,浏览态与搜索态节奏一致。
302
- - **测试**:全量 1200 例零失败、typecheck 零错误;几何与吸顶行为以临时无头探针实证后移除(不入正式套件)。
303
-
304
- ### 0.9.51 变更:社区条目 github 字段补全(图标覆盖 45.6% → 100%)
305
-
306
- - **动机**:社区区 npm 源条目卡片恒为首字母色块——适配层 npm 分支只产出 `npm` 字段即定源,上游目录 `url` 里的 GitHub repo 映射只被降级进 homepage,`github` 字段恒缺,客户端 Icon 的 owner 头像兜底(`github.com/<owner>.png?size=64`)无从触发。实测 `dsh-plugin-catalog@2026.1007.4837`:4,226 条可收录条目中 npm 源 2,298 条 **100% 带 github.com url**,纯数据丢弃,非上游缺失。
307
- - **改动(单点)**:适配层 npm 分支补派生 `github = githubFromUrl(url)`(子包 `/tree/` 取 repo 根、原样大小写;派生失败不产出键、不跳过条目,条目层宽松 Q43)。`source` 语义不变;客户端零改动——Icon 兜底、详情行「GitHub · npm」、已装 `registryGithub`(README 基址三级兜底第一级)既有链路自动生效。
308
- - **语义激活(已在案接受)**:①合并层 github 碰撞分支开始作用于社区 npm 条目(同 repo 让位精选恒优先)——全量真实目录模拟新增让位 0 条(4,215 → 4,215);②GitHub 源手装的插件可被社区 npm 条目匹配,升级走 npm 源(与精选双源条目既有语义一致)。性能面零回归:宿主侧零新增网络请求、latest 探测路径与缓存键不变、Q46 社区 github 探测豁免与 GithubBudget 不触及。
309
- - **测试**:适配层翻转 1 断言 + 净增 4 例(github url 派生与原样大小写、非 github 域、无 url、非法形状),既有子包用例补 1 断言(取 repo 根);合并层净增 2 例(github 碰撞让位、GitHub 源手装匹配,均红→绿);全量与基线对照零新增失败、typecheck 零错误。
310
-
311
- ### 0.9.50 变更:README 兜底 /latest 两腿阶梯 + 未装文案 profile-aware + 兜底失败摘要
312
-
313
- - **动机(上海 Windows 桌面机实机实证)**:desktop profile(生效源 npmmirror)打开 billion-context(705k 周下载)详情 Modal 的 README 折叠页恒报「web profile 未安装该插件」——npmmirror packument 实测 8,586,604 字节,超过 npmPackumentReadme 的 8MB(8,388,608)上限被 readCapped 拒(`502 响应超过上限`),兜底腿必失败后按 0.9.45 语义如实重抛原始本地错误;同期 npmjs packument 8,195,089 字节恰低于帽、readme 为空,web 侧显示「没有 README」只是帽下侥幸——该包高频发版,npmjs 超帽只是时间问题。
314
- - **README 兜底两腿阶梯**:`npmPackumentReadme` 内部改两腿(对外签名与返回形状 `{readme, repo}` 不变)——腿1 `GET <生效源>/<pkg>/latest` 版本文档优先(KB 级小载荷、packument 超帽免疫;URL 拼法同 npmLatest 先例;**超时收紧 `Math.min(timeoutMs, 5_000)`**,同 npmLatest 首腿先例——生效源不可达时兜底最坏时延 40s→25s),readme 命中即短路;空/失败落腿2 packument(20s 满额,行为同 0.9.45)。任一腿成功即成功:readme 空渲染「没有 README」(含 latest 空白 readme + packument 失败的降级,不抛);两腿全败抛终末腿(packument)错误。两腿均在生效源(ADR-0012),**不跨源**;两腿 readme 判空同一 trim 规则;repo 随到达的腿取 repository(packument 到达时以 packument 为准)。
315
- - **未装文案 profile-aware**:`readInstalledPluginReadme` 新增第三参 profileName(缺省 'web'),host-api readme 分支传 `profile.name`——desktop profile 报「desktop profile 未安装该插件: X」(形态对齐 profile-ops 先例);market/toggle/profile-transaction 等 web 事务专用路径的「web profile」文案不变(desktop 走官方 pluginManager 委派,到不了那些代码)。
316
- - **兜底失败摘要(0.9.45「兜底失败如实抛原始错误」的语义修订)**:两腿全败时抛 `<本地未装错误>(npm README 兜底失败: <终末腿原因>)`——真实死因(如「响应超过上限 8388608 字节」)不再被吞,红字自解释;`cause` 保留原始本地错误。
317
- - **测试**:versions.test.mjs describe 重写为阶梯用例组(显式 registry 贯穿两腿、缺省生效源形态断言、latest 命中短路、空落 packument、latest 失败兜住、空白 readme 降级、终末腿错误、两腿都空、超时收紧 deps spy、repo 归一语义注记),红→绿全程断言(红集合 {2,3,4,6,8,9} 与计划推演逐条吻合);installed.test.mjs 增 profileName 用例;host-api.test.mjs readme 用例改断言 + 增 profile.name 传递与合成错误形态两用例。全量测试零新增失败(Windows 本机 symlink 族既有基线豁免);typecheck 零错误。
318
-
319
- ### 0.9.49 变更:已装 README 基址贯通 + 链接归一三件(0.9.48 深入分析跟进)
320
-
321
- - **已装视图基址贯通(P0,主缺口)**:0.9.48 已装详情挂载点误传不存在的 `it.github`,而已装插件走本地读取路径(`repo` 字段只在 npm 兜底路径附带)→ 已装 README 相对链接全部归 `#`。现改传 `vm.githubRepo`——installed-view 视图模型现成的三级兜底(registry 匹配 `registryGithub` → 已装包 package.json `repository` 解析 → 安装 spec `github:o/r` 解析),图标与 LinksRow 早就在用。装机浏览场景的相对链接/图片自此全部可锚定。
322
- - **链接归一(P1)**:①协议相对 `//host/x` 补 `https:`——此前有基址时被错剥成仓库内路径、无基址时跳 DSH 站内;②`git@github.com:o/r(.git)` 内联链接归一 https 形态(README 的 clone 指引段落);③有基址时 `#anchor` 锚 `github.com/<o>/<r>#anchor`(仓库首页 README 同名锚,裸 `#` 与图片 kind 不参与)。
323
- - **host 侧解析口径统一(P2)**:`githubRepoFromRepository` 尾部从 `$` 放宽到 `[/?#]`——`github.com/o/r/`(尾斜杠)与 `…/tree/main` 等更深路径此前解析不出;与 versions.ts `extractGithubRepo` 同口径。
324
- - **测试**:client-markdown 净增 4 例(协议相对两态、git@ 两形态、锚点四断言含 img kind 不锚)、installed 净增 1 例(repository 五形态含尾斜杠与子路径);锚点断言随新行为更新;全量 1185/1185、typecheck 零错误。纯客户端+host 只读字段透传变更,刷新页面即生效。
325
-
326
- ### 0.9.48 变更:README 实体解码 + 相对路径仓库基址锚定(0.9.47 实装反馈两连修)
327
-
328
- - **HTML 实体解码**:0.9.47 解析了标签但未解码实体——dsh-task-board 类 README 徽章间独立成行的 `&nbsp;` 按字面漏出。现命名实体(nbsp/amp/lt/gt/quot/copy/mdash/箭头/分数等 60+ 高频集)与数字实体(十进制/十六进制)均解码;文本段与 HTML 属性值都解(徽章 src 里 `&amp;labelColor` → `&labelColor`,shields 参数不再丢失);`code`/`pre` 内不解码;未知实体与裸 `&` 原样保留;单趟解码不回炉(`&amp;lt;` → `&lt;` 文本,与浏览器一致),解码结果只作为文本节点、绝不重新参与标签解析,无注入面。
329
- - **行内优先判定**:包裹标签(`<p align="center">` 等)剥壳后,若内部无空行分段、无 markdown 块结构、且行首标签均为行内/void 标签,则整块按行内 token 流渲染——徽章 + `&nbsp;` 连排在同一居中行(GitHub 语义),不再每行拆成独立段落;含块级结构(`tr/td/ul/li` 开头等)仍走 markdown 递归保结构。
330
- - **相对路径仓库基址锚定**:`[README.md](README.md)` 这类相对链接此前被 safeUrl 归 `#`(点击无反应——既有安全归化,非 0.9.47 回归)。现 README 预览可携带仓库基址:相对链接锚 `github.com/<o>/<r>/blob/HEAD/<path>`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/<path>`(`./`/`../` 前缀归一);绝对 URL 与页内锚点不受影响;无基址时保持归 `#` 旧行为。数据链路:npm packument 兜底时顺带提取 `repository` 归一为 `owner/repo`(仅 GitHub,git+https/git@/.git 形态归一,host-api 透传 `repo` 字段);客户端以卡片 `github` 字段兜底;渲染基址为 renderMarkdown 级同步设置、进出恢复,不跨渲染泄漏。
331
- - **测试**:client-markdown 净增 6 例(实体四类、独立 `&nbsp;` 徽章块、code 不解码与属性解码、基址锚定七断言、无基址回归与跨渲染泄漏);versions 净增 1 例(repository 两形态归一 + 非 GitHub 归空);全量 1181/1181、typecheck 零错误;真实数据端到端——dsh-TUI 25926 字符零泄漏零回归、task-board 真实徽章块三徽章一行居中 + `&amp;` 解码、相对链接正确锚 blob/HEAD。纯客户端变更,刷新页面即生效。
332
-
333
- ### 0.9.47 变更:README 预览内嵌 HTML 子集渲染(GitHub 风 README 不再满屏标签)
334
-
335
- - **问题**:市场/收藏/已装详情 Modal 的 README 折叠页,遇到 GitHub 风重度内嵌 HTML 的 README(`<p align="center">` 徽章墙、`<img>` Logo、`<details>` 折叠等——npm packument 兜底与本地读取都常见)会把标签按字面整屏显示,观感如「乱码」。根因是渲染端自研 markdown 渲染器此前完全不解析 HTML 标签(原始内容本身没有问题,GitHub 上渲染正常)。
336
- - **渲染子集(`src/client/markdown.js`)**:块级包裹标签(`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` 等)剥壳后按完整 markdown 递归渲染,内部标题/列表/表格/围栏结构全保留;行内标签(`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` 等)token 化构造元素;`align` 属性映射 `textAlign`,`<img width/height>` 映射显式尺寸并放开徽章 20px 高度帽(无 width 的徽章仍走小图帽);`<details>/<summary>` 原生折叠;文本含标签的普通段落/标题/表格单元格也走行内 token 流(`Press <kbd>Ctrl</kbd>` 类可渲染)。
337
- - **安全模型(不变式)**:绝不向 DOM 注入原始 HTML 字符串——只解析出白名单标签与白名单属性,全部经 `h()` 构造 React 元素,URL 一律过 `safeUrl` 闸门(`javascript:` 等归 `#`);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容整体丢弃;递归深度护栏(>8 层降级纯文本)。无 `dangerouslySetInnerHTML`,不存在 XSS 注入面。
338
- - **已知边界**:npm packument 兜底的 README 不携带仓库基址,相对路径图片(如 `docs/assets/logo.svg`)按 safeUrl 归 `#` 后由 onError 隐藏(徽章等外链图片不受影响);`<picture><source>` 的 source 分支丢弃、保 `img` 回退。
339
- - **测试**:`client-markdown.test.mjs` 净增 10 例(块级剥壳递归、徽章行连排、行内混排、script 丢弃与 javascript: 归化、未知标签剥壳、details/summary、整篇 div 包裹、HTML 表格、HTML 标题与行内元素、未闭合标签降级与深度护栏);真实 dsh-TUI README(25926 字符)渲染验证零标签泄漏、64 个徽章链接正常构造;全量测试 1175/1175,typecheck 零错误。纯客户端变更,刷新页面即生效。
340
-
341
- ### 0.9.46 变更:双语 README 精简重构+实拍 WebP 截图集(文档版)
342
-
343
- - **README×2 重构**:中文 17.5KB → 8.3KB、英文 18.1KB → 9.4KB——以用户任务动线重组(30 秒上手 → 浏览/搜索/安装 → 已装与设置 → Agent 工具与 CLI → 兼容与 FAQ);获取线路、缓存语义等实现细节不再展开,统一移交 [`docs/DESIGN.md`](docs/DESIGN.md) 与 ADR 承接;运行相位/生效时机表述与激活分桶语义对齐(不硬承诺「一定重启/一定刷新」,以操作返回为准)。
344
- - **截图集(`docs/images/`,8 张 WebP ≈248KB)**:市场首页、社区分类条、精选分类条、跨区搜索(精选置顶+社区分组)、插件详情(未装态安装入口)、已装管理、设置页全部换为当前实机实拍;官方安装对话框图沿用。已装图对宿主本机 profile 绝对路径做脱敏改写。
345
- - **新增 `scripts/capture-readme.mjs`(开发用,不入 npm 包——`assert-pack` 的 scripts/ 禁带清单继续兜底)**:playwright 只读驱动当前 Web 实例复现整套截图(`npm run capture:readme`);不执行安装/升级/开关/删除,仅浏览态与弹窗;每张截图前断言面板内无未脱敏的 profile 绝对路径。新增 devDependency `playwright-core@1.62.1`(锁官方 registry 解析)。
346
- - **验证**:typecheck 零错误;全量测试 1165/1165(首轮 1 项进程树终止时序断言在并发负载下偶发,单跑与全量重跑均通过,未改运行时代码);`npm pack --dry-run` + `assert-pack` 白名单通过;双语 README 各 8 处 WebP 引用与文件一一对应、无缺失无冗余。
347
-
348
- ### 0.9.45 变更:市场页两段加载 + 强制刷新穿透探测 + 精选页动线批
349
-
350
- - **市场页两段加载([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**:浏览态 market 查询拆两段——第一段 `probeMode:'cache-only'` 只回 TTL 内缓存命中、零网络瞬时回页,有缺口(`latestComplete=false`,豁免社区 github 条目——Q46 永久缺口不构成第二段理由)才自动发起第二段既有探测语义就地补徽标;重启后首开不再被全页重探阻塞(弱网 8–16s → 恒秒开 + 徽标异步补全)。`mergeLatestFields` 纯函数保证会话内切换筛选态时徽标不回退;快照只在终态写入。服务端 latest 缓存保持纯内存不动(ADR-0006 不翻案);tools/CLI 的 `withLatest:false` 通路零变化。
351
- - **force 探测穿透(重开 ADR-0006 暂缓裁决)**:设置页「强制刷新」新链路 onForceMarket → marketReloadAll(true) → core「peek 旧值兜底 + 全页重探」——严格落实 ADR-0006 在案约束「peek 不删除语义而非 ttlMin=0 先删后探」,重探失败保留旧值 + latestError,不产生空徽标窗口;强刷后精选清单与「有更新」徽标同时刷新。市场页 probeMode 只有 full/cache-only 两态(不设 'only')。
352
- - **详情 Modal 升级动线(U10)**:已装且可升级条目的 Modal 动作区新增「升级」主按钮(市场区与收藏区两挂载点;record.target=安装包名,走 ops 泵与 0.9.22 生效判定分流文案,guard/opSuperseded/普通失败三分支对齐已装页);新增 README 折叠页(展开才拉取,收起态零请求);「收录日期」走 fmtDate 本地化。**修复(实机验收发现)**:README 折叠页原直接复用已装页本地读取接口,未安装条目(市场浏览的主体)恒报「web profile 未安装该插件」——现本地未装时回源 npm packument 顶层 readme 字段兜底(HTTPS/8MB 上限/超时;无 readme 字段显示「没有 README」;兜底失败如实抛原始错误),已装条目仍走本地读取。
353
- - **错误恢复与空态(U9)**:错误行内联「重试」按钮(此前只能关开面板或绕道设置页);空态三分支——registry unavailable 主文案 / 分类空桶专用文案 / 通用文案。
354
- - **收藏身份(U12)**:收藏快照增补 verified/audience/decoupled 三字段(只存不标——§2.7 裁决⑤「收藏页不打标」不动);收藏区为精选条目补「精选」身份徽标与「已实测」质量徽标(此前精选条目进收藏区连「精选」标识都丢失)。
355
- - **可达性与杂项(U8/U3/U7)**:详情 Modal 补 role=dialog/aria-modal/aria-label 与打开时聚焦关闭钮,截图缩放支持键盘 Enter/Space(Tab 圈闭明确不做);精选区 0 计数桶显式渲染「0」+ 降透明;跨桶条目使分类计数之和大于总数时 chips 行容器给出 title 说明。
356
- - **测试**:market/host-api/client-market-state/client-render-smoke 净增用例与断言(probeMode 两态与 force peek 语义、缺口豁免、cache-only outdated、mergeLatestFields、两段与强刷接线、Modal 升级/README/可达性、收藏徽标、0 计数桶);全量测试 Windows 本机除既有 symlink 族基线 15 项外零新增失败;typecheck 零错误。
357
-
358
- ### 0.9.44 变更:插件卡片网格按容器宽度自适应列数(全屏显出更多卡片)
359
-
360
- - **自适应网格**:市场/已装/收藏三视图共用的卡片网格从固定两列改为按容器实际可用宽度自适应——`grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr))`。列数 ≈ `max(1, floor((W+8)/368))`(最小卡宽 360px、列距 8px,剩余宽度各列均分):浮动态(~920px 面板)保持 2 列零回归;全屏 1920 宽约 5 列、1280 宽 3 列,显著显出更多插件卡片。
361
- - **删视口断点**:移除 `@media (max-width:680px)` 的单列覆盖——视口宽度不代表面板实际可用宽度;极窄容器由 `min(100%,360px)` 兜底自动单列,不横向溢出。
362
- - **范围与边界**:搜索分组段头(`.dsvm-grouphead`)仍以 `grid-column:1/-1` 跨满整行;纯客户端变更,刷新页面即生效。
363
- - **测试**:新增 SSR 冒烟回归 1 例(锁定自适应网格规则与断点删除);typecheck 零错误。
364
-
365
- ### 0.9.33 变更:自研条目元数据 v1.1——`audience`/`decoupled` 两字段与三端标注
366
-
367
- - **schema**:registry 条目增补可选 `decoupled?: true`(解耦条目,实操口径判定;与 `verified` 互斥——同条目共存报 error)与 `audience?: 'public' | 'internal'`(缺省 public 不产生键;`'team'` 值集预留未开放)。决策共识与五项裁决记录见 DESIGN §2.7,术语见 GLOSSARY「解耦条目 / 自用条目」。
368
- - **三端标注**:GUI 卡片与 Modal 头新增「作者自用」「版本无关」徽章+Modal 受众/适配详情行;`dshm_search` 输出投影携带 `audience`/`decoupled`、条目行加 `[作者自用]` 标,工具描述写明推荐纪律(internal 勿向普通用户主动推荐,点名或内部推广场景除外);CLI `dshm search` 行内同标(新增 `tests/cli-search.test.mjs`)。已装视图与收藏页不打标——推荐发现链路才是纪律靶面。
369
- - **CI**:validate-registry 新增软警告——decoupled 条目 description 含「已适配」句式即 warn(copy-guide §4 第四句式「版本无关,详见仓库」,空间不足用紧凑形「版本无关。」)。
370
- - **registry 数据**:八个自研条目终态——surf / obmc-web / onetree-log 打 `decoupled`+`audience: internal`,quota-watch 打 `decoupled`;四条 `verified` 数组删除(历史归 git 与各仓库文档),兼容句换第四句式;四条 coupled 条目(dsh-m / dsh-skins / skip-browser-auth / copilot-auth)verified 原样保留。
371
- - **测试**:净增 9 例(registry 字段校验 5、tools-search 标注 2、cli-search 2);全量 1133 tests,本机 1108 pass / 15 fail——15 项全部为 Windows 平台既有 symlink 语义用例(与基线集合逐项一致,零回归);typecheck 零错误。
372
-
373
- ### 0.9.32 变更:npm registry 路由自适应——元数据预取换源/镜像自愈(ADR-0012)
374
-
375
- - **动机**:2026-10-04 上海 Windows 桌面机实证——元数据预取写死直连 registry.npmjs.org,间歇性超时使升级在委派 pnpm 之前夭折(`fetch failed` / `The operation was aborted due to timeout`,且 `.plugin-manager/logs` 无对应操作日志),而 npmmirror 镜像早已同步目标版本;同期首尔腾讯云机 npmjs 直连良好。两台机器需要零配置各自可用。
376
- - **读分类路由(L1/L3)**:检测读(`npmLatest` 升级探测 / `npmPackumentTimes` 发布时刻)恒以 npmjs 权威源优先、首腿超时收紧 ≤5s、失败降级生效源;履约读(`npmVersion` 精确版本元数据 / `npmPackument` 预热)走生效源优先。`npmPackument` 的 registry 参数此前被静默忽略,本版修复(显式参数开始生效,测试钉住)。
377
- - **L0 传输层**:httpx 换 undici 自带 fetch + 自建 dispatcher(`EnvHttpProxyAgent` 显式交接代理解析)——元数据预取开始感知 `HTTP(S)_PROXY` / `npm_config_*` 代理,并规避宿主 undici 全局 dispatcher 污染(dsh-market net.ts #742 同族);`fetchLimited` 新增 PUT 方法(仅供 sync 原语);网络失败错误附 `via` 上下文,失败摘要仅在代理路径渲染「(经 <掩码代理>)」。
378
- - **L2 镜像滞后自愈**:`NO_MATCHING_VERSION` 且 registry 字段指名 npmmirror(分类层结构化识别、完整文本上判定、上层零 regex)→ 按需同步镜像(`registry-direct.npmmirror.com/<pkg>/sync`,PUT)→ 既有退避重试(heal 记 `B3_NPMMIRROR_SYNC`);`npmVersion` 履约阶梯同步适用(404 → sync → 等待 10s → 同源重试 → npmjs 兜底);sync 受理后作废 latest 缓存。`DSHM_MIRROR_SYNC=0` 一键关闭。
379
- - **新模块 `src/core/npm-route.ts`**:`DSHM_NPM_REGISTRY` 源覆盖(最高优先,设了跳过探测)> [.npmrc registry, npmjs, npmmirror];probe-once(2.5s 共享预算、single-flight、胜者须完整响应带 version);决策持久化 `<cacheDir>/npm-route.json`(删文件即重探);全候选失败仅内存回退 npmjs(60s TTL 后重探、不落盘)。生效源切换自动作废 latest / packument 两级缓存。
380
- - **wire 测试缝隙迁移**:既有 mock `globalThis.fetch` 的测试缝隙迁至 `_setWireFetchForTests`(versions / npm-integrity / registry-check 三文件,断言与计数语义不变)。
381
- - **测试**:净增 41 例(L0 代理解析/wire 转发/via/PUT、npm-route 12 例、读分类路由、registry 分类字段、B3 sync、L4 摘要、kill-switch 阶梯/集成、尾斜杠归一);全量 1128 tests,本机 1103 pass / 15 fail——15 项全部为 Windows 平台既有 symlink 语义用例(与改动前基线集合逐项一致,零回归);typecheck 零错误。真机验收(上海机 scoped 自研包窗口内升级 + 首尔机 `dshm outdated`)随发版执行。
382
-
383
- ### 0.9.31 变更:doctor 支持 desktop profile(CLI 例外开口 + farmChecked 语义修订,ADR-0011)
384
-
385
- - **动机**:Windows desktop 机实机报告证实 core 引擎本就 profileDir 参数化无 web 硬编码(desktop 物化布局判 hoisted、farm=0 属常态),但 CLI 全命令一刀切拒绝 `--profile desktop` 把只读体检连坐;desktop-only 机器无 flag 体检还会扫不存在的 web 目录得全 0 报告。
386
- - **CLI 例外(仅 doctor)**:`dshm doctor --profile desktop` 允许并路由 `desktopProfileDir()`,输出首行加 `[web|desktop]` 标注;HELP 同步;**其余命令对 `--profile desktop` 的拒绝语义逐字不变**(回归钉子测试钉住),desktop 变更管理仍走官方 Desktop 插件页。
387
- - **空目录提示**:目标 profile 目录不存在时提示「desktop-only 机器请加 --profile desktop」——判据为目录不存在(存在但为空的合法 profile 不误伤)。
388
- - **宿主 method 零改动**:doctor case 本就 active-profile 无关,desktop 宿主免改生效(新增 desktop-kind 注入钉子测试);MCP 工具 `dshm_doctor` 维持 ADR-0010 决定 3 缓上;Windows Electron 宿主 runtimeVersion 可能仍降级(物化布局 farm=0,stale 无判定对象,实际影响为零,如实记录)。
389
- - **farmChecked 语义修订**:「0=遍历空转」判定仅适用于存在符号链农场的形态;物化布局 0 为常态(ADR-0011)。
390
- - **验证**:新增 8 例(desktop 路由/标注、他命令拒绝、非法值、空提示、正常不提示、显式 desktop 不出矛盾文案、显式 web 路由、method 钉子)+ 改写 1 例旧拒绝样本(净增 8,全量 1079→1087);全量 1087 pass / 0 fail / 0 skipped;typecheck 零错误。Windows 外机验收清单见实施计划(本机无法执行 Windows E2E)。
391
-
392
- ### 0.9.30 变更:修复 bin 符号链接静默 no-op(F2)+ 版本解析 realpath 第三源(F1)
393
-
394
- - **根因同源**:pnpm 生态里两类关键入口都是**符号链接**——`node_modules/.bin/dshm`(指向 lib/cli.js)与全局 shim `PNPM_HOME/dsh`(指向真实 bin.js)——而两处代码都在 realpath 之前做了路径身份判定。
395
- - **F2(自 v0.2.0 bin 入口引入以来潜在,本机 0.9.28 装机形态首次踩中并实证;🔴 bin 入口全命令静默 exit 0)**:cli.ts `invokedDirectly` 比较 `argv[1]`(链接路径)与 `import.meta.url`(node realpath 后的真实路径)永假,`dshm <任何命令>` 无输出直接退出。修复:比较前对 `argv[1]` 同样 realpath。新增 `tests/cli-bin-symlink.test.mjs`(经符号链接调用 --help 与 doctor --json 的复活验证,EPERM 环境照仓内先例 skip)。
396
- - **F1(0.9.29 装机验收实证)**:dsh-version.ts `readLauncherPackageVersion` 从 shim 位置直接向上三级找 package.json,落在 pnpm home 目录树上空走——宿主内纯 FS 版本解析在本机拓扑(`node PNPM_HOME/dsh web`)不可得,doctor 的 stale 判定降级、ping chip 被迫吃 spawn 回退。修复:判定 entry 形态后先 realpath 再上溯(本机实测解析得 0.2.0-rc.2)。新增 dsh-version.test.mjs shim 形态用例。
397
- - **验证**:专项 9/9(含两新用例);全量 1079 pass / 0 fail / 0 skipped;typecheck 零错误。装机后预期:`dshm doctor`(bin 入口)可用;宿主 method 通路 runtimeVersion=0.2.0-rc.2、stale 判定复活(装机时农场 dsh 链接 0.1.7-rc.2 为现成 stale 形态,heal 后归零)。**装机实测(2026-10-04)**:预期逐项兑现——`.bin/dshm doctor` 经符号链接复活出报告;宿主 method `runtimeVersion=0.2.0-rc.2 / stale=1(dsh→0.1.7-rc.2 现场点名)`;同日按 know-how 014 heal 该链接并清理 20 项残留后,双通路终验 `farm 236 / dangling 0 / stale 0 / residue 0 / errors 0`。
398
-
399
- ### 0.9.29 变更:profile 体检 Doctor——`dshm doctor` 上线(只读:农场测活 / 残留物清点 / 账实一致)
400
-
401
- - **动机**:dsh-m 至今没有诊断能力——know-how 014 的「每次 DSH 升级后重跑农场测活」是唯一现役周期必查项(曾 81 条悬空、230 行手工映射留档),know-how 023 实录「账实分裂」形态;本机实扫另发现 8 个空 scope 目录 + 12 个 `*.bak-*` 累积。学 dsh-market check.ts 的设计纪律(纯 FS 边界、三级严重度、unknown≠broken、误报记账、修复责任外移)落地 Day1 子集,选点裁决与对比详见 ADR-0010 与对比报告(2026-10-04)。
402
- - **新增**:`src/core/doctor.ts` 纯函数核心(无进程/无网络/无写入,任意时刻可安全调用)+ `/dshm` 新 method `doctor` + CLI `dshm doctor [--json]`(error 级发现 exit 1;HELP 同步)。三项检查:**农场测活**(`@deepseek-ai/*` 符号链祖先链遍历;悬空=error;dsh 伞包指向旧运行时 store=提示级——lockstep 店内非伞包版本不与 runtimeVersion 比较,防 cordis 等误报)、**残留物清点**(空 scope / 无 manifest 目录 / pnpm `*_tmp_*` / `*.bak-*`,全部零告警清单,「可见而非清理」)、**账实一致**(pin/实装/lock 三处核对,不一致=warning;lockfile 仅认 9.0 importers 形状,其余 unknown 不猜)。
403
- - **边界纪律(ADR-0010)**:runtimeVersion 仅用 `readLauncherPackageVersion` 纯 FS 通路(CLI 进程下为 null → stale 判定整体降级 unknown,绝不 spawn);密钥红线只禁含密钥**配置文件**内容(包元数据 version 字段可读);布局判定 workspace 声明优先(本机「hoisted 声明 + 仅 lock.yaml 的残留 .pnpm」并存形态实证);双市场并存(dsh-m+dshmarket 同装)信息级呈现;doctor 永不修复,建议以文字给出。
404
- - **验收结算(2026-10-04 装机实测)**:farmChecked=236(精确命中评审实测值)/ 悬空 0 / 8 空 scope + 12 bak 入清单 / 账实 13:0 / 双市场信息级 / errors=0 → exit=0 / `--json` 可解析。宿主 method 信封正确但 runtimeVersion=null——**发版日新发现**:本机宿主经 pnpm 全局 shim 启动(`node ~/.local/share/pnpm/dsh web`),`readLauncherPackageVersion` 从 shim 三级上溯落空,stale 判定双通路降级(底层事实人工核对成立:农场 dsh 链接 0.1.7-rc.2 vs 运行时 0.2.0-rc.2;版本第三源列下一批)。另录既有 bug:`.bin/dshm` 符号链接静默 no-op(后经考古为 v0.2.0 起潜在、本机 0.9.28 装机形态首次踩中,见 0.9.30 条目——`invokedDirectly` 比较 symlink 路径与 realpath 永假,bin 入口全命令静默 exit 0;修复列 0.9.30 候选,期间用 `node …/dsh-m/lib/cli.js` 直达)。
405
- - **验证**:新增 `tests/doctor.test.mjs`(36 例:布局冲突并存/祖先链反空转/两级 targetVersion/降级路径/零告警清单/023 形态/link 协议/lock peer 后缀/CLI 子进程)+ `tests/doctor-api.test.mjs`(2 例:method 信封真跑 + 空 profile);全量 1076 pass / 0 fail / 0 skipped。
406
-
407
- ### 0.9.28 变更:页大小档位去上游化——32/64/96 取代 24/48/96,默认 32
408
-
409
- - **动机**:24/48/96 是复刻 dsh-market 筛选面板(0.7.2)时带过来的上游血统数字。甄别后发现 96 早已被 dsh-m 内化为核心参数(`WITH_LATEST_MAX` 探测上限、精选区默认页、fast-open 快照判定基准),真正的上游痕迹只有 24——「砍 24、保 96」即去上游化与兼容性的交集。
410
- - **新档位**:`MARKET_PAGE_SIZES = [32, 64, 96]`(等差 +32,读序顺)、社区区默认 `DEFAULT_PAGE_SIZE = 32`(首页更满,仍低于 0.6.x 历史默认 50 的探测负载,Q46 预算姿态不变);精选区默认 96 与 core clamp 1..96 不动。
411
- - **联动**:host-api GUI 通道兜底默认 24 → 32(三端一致,不留暗默认);探测预算注释同步(market.ts / host-api.ts);README×2 与 DESIGN.md 措辞同步;GLOSSARY「精选区」词条按代码实态锐化(单页无分页 → 常态单页直出 + 超限降级分页);测试断言与快照 fixture 同步(偏离档位断言改用组内值 64,覆盖「档位内但非默认仍拒写/判假」)。
412
- - **已知一次性影响**:升级后社区区旧快照(limit=24)不再命中 fast-open 判定,首次打开市场多一次正常请求,快照按新默认重建后自愈;不加 legacy 兼容分支。
413
- - **验证**:`npm run build` 成功;全量 1030 pass / 0 fail(8 skipped)。
414
-
415
- ### 0.9.27 变更:修复遮罩拖选误关——面板内按下、拖出释放不再关面板
416
-
417
- - **问题(装机实测 2026-10-03)**:在市场搜索框内左键按下向左拖选(越过面板边界)释放,整个面板被关闭。非浏览器鼠标手势——面板遮罩是裸 `onClick: onClose`,而在面板内容里按下、拖到遮罩上释放时,浏览器把 `click` 派发到按下/释放目标的公共祖先(恰是遮罩),被误判为「点遮罩关面板」。详情 Modal、截图灯箱、兼容确认弹层同属该缺陷类。
418
- - **修复**:新增纯函数 `backdropCloseHandlers`(src/client/backdrop.js)——仅当 **mousedown 与 click 都落在遮罩自身**时才关闭(其余组合一律不关,click 后按位状态复位);四处遮罩(主面板/详情/灯箱/兼容弹层)统一切换。点遮罩关闭、Esc 关闭、✕ 按钮行为不变。
419
- - **验证**:新增 `tests/client-backdrop.test.mjs`(关闭/两类拖选不关/状态复位/畸形输入安全,5 组断言);全量 1030 pass / 0 fail。
420
-
421
- ### 0.9.26 变更:跨区搜索精选稳定前置——摘要行计数与首页所见一致
422
-
423
- - **问题(装机实测 2026-10-03)**:搜索「sidebar」摘要行报「⭐ 精选 3 · 社区 315」,首页精选段只见 1 条。非重复计算——`sourceCounts` 不读分类计数(`alsoCategories` 无涉),3 条为真实精选命中;错位根因是摘要行报全局命中数,而相关性排序叠加社区区默认 downloads 降序 tie-break 把弱命中精选压进后页。
424
- - **修复**:`listMarket` 新增 `curatedFirst`——`source='all'` 且 query 非空时精选命中**稳定前置**(稳定分区,分区内相关序不变,社区命中随后);仅 host-api GUI 通道携带,tools/CLI 不传,搜索排序三端同序不变;单分区/浏览态天然无效。跨页精选段头悬空与计数错位随之消失(精选命中 ≤ 页大小时全数落在首页段内)。
425
- - **验证**:新增测试 ⑮(无 flag 保持交织相关序 / 带 flag 精选前置 / 空 query 与单分区无效);全量 1025 pass / 0 fail。
426
-
427
- ### 0.9.25 变更:跨区搜索——浏览分区、搜索全局(社区 + 精选一并命中)
428
-
429
- - **跨区搜索**:任一分区(社区/精选)的搜索框升级为全局——`query` 非空时底层查询切 `source=all`,两分区条目统一相关性排序;清空关键词回到本区浏览态。分区制浏览(ADR-0004)与 `dshm_search`/CLI 三端同序不动;服务端唯一增量是 `MarketResult.sourceCounts` 分桶计数。
430
- - **搜索态呈现**:摘要行「⭐ 精选 N · 社区 M」精确计数(`!loading` 门控防旧数字闪现);精选命中页内置顶分组(段头各自仅在对应段非空时渲染,跨两列 grid);非社区卡补「精选」徽章;分类 chips 搜索态隐藏、「筛选」按钮随摘要行保留(搜索态只剩页大小组,排序被相关性优先覆盖);翻页回顶锚点随态切换。
431
- - **修复**:分区 tab 计数改从恒定字段推导(社区 = `acceptedCount - displaced`、精选 = `registryState.count`),不再随查询变化(此前读「最近一次查询的 total」,跨区搜索会污染计数);搜索提交时清空激活分类(chips 已隐藏,残留分类会成为不可见过滤)。
432
- - **降级**:`sourceCounts` 缺失/畸形时摘要行整体不渲染,列表行为不变;旧快照/旧宿主响应形状漂移免疫。
433
-
434
- ### 0.9.24 变更:排除条目代管——供应链等待期从「提前拒绝」到「治理 + 登记」(ADR-0009)
435
-
436
- - **问题(实机 2026-10-03 实证)**:0.9.19 的委派前预检把「目标版本未满 24h 等待期」预测成"官方管理器必拦"并提前拒绝——同目标 dsh-market 却能装上(copilot-auth@1.2.4 发布 8 分钟、quota-watch@0.1.21 两例)。pnpm 11.7 默认非严格策略对显式点名的新版本本来就放行并自动登记排除条目,预检的墙并不存在。
437
- - **三挂点代管**:① 委派前治理——desktop profile 排除块的坏形态(pnpm 自追加的死规则)合并为"每包一条、版本并集复合";② 装机成功后登记——等待期内目标并入排除块(scoped 精确单条 / 非 scoped 目标+上一版双选择器);③ 双码失败(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` / `ERR_PNPM_NO_MATURE_MATCHING_VERSION`)→ 治理 → 至多重试一次,仍败走既有失败翻译与账实分裂复读。web ladder 同套三挂点。
438
- - **边界与安全**:红线收窄出唯一例外(desktop 仅此块、仅委派前后挂点、原子写、解析失败即弃、留痕不落全文);治理/登记全程持官方同款锁并 fail-open——任何失败不阻塞委派,pnpm 仍是最终执行者;显式设置 age 或 strict 的 profile 仍前置拒绝并给可重试时刻;机理按"首条规则生效"口径改判(know-how 020 §2.4 形态论退役)。
439
- - **文档**:决策全文 `docs/adr/0009-release-age-exclude-governance.md`(含 9 项评估过不做的栅栏附录与观察预案);GLOSSARY 新词「首条规则/排除条目/治理/登记/等待期」。
440
-
441
- ### 0.9.23 变更:已装页两段加载——列表秒开 + 更新提示就地补 ⬆(ADR-0008)
442
-
443
- - **问题(实机 2026-10-03 实证)**:更新探测焊在已装列表接口里且带 60min 内存 TTL——发版后重开面板吃到陈旧缓存,升级提示迟迟不出现(quota-watch 新版发布 17 分钟后面板仍无提示,dshmarket 同刻已见)。
444
- - **两段并行(零新增按钮)**:第一段 `installed {probe:false}` 立即返回已装列表(registry 匹配与开关相位不受影响);第二段新方法 `installedUpdates`(`probeMode: 'only'`,**TTL=0 每次挂载真实探测**)完成后把 ⬆ 徽标 / tab 红点 / 「全部升级 (N)」就地补上;探测失败仍逐项走「检查未完成」,不冒充「没得更新」。
445
- - **边界与副作用(ADR-0008 如实留痕)**:范围仅已装页——市场浏览页探测、`dshm_list`/`dshm_outdated` 工具、CLI 行为不变(缺省 `probeMode: 'full'`);ttl=0 会先删 host namespace 共享缓存条目再重探,浏览页/工具的 TTL 内命中被刷新为更新值(数据只更新鲜、代码路径与自身 TTL 制度不变),GitHub 被动预算消耗速率上升(上限 25/req、50/h 滚动不变),超限走 latestError 优雅降级。
446
- - **文档**:决策全文 `docs/adr/0008-installed-two-phase-probe.md`;DESIGN「已装页/缓存语义」补句;GLOSSARY 新词「两段加载」。
447
-
448
- ### 0.9.22 变更:升级生效判定——tarball 差异三态分类 + 三端重启提示分流(ADR-0007)
449
-
450
- - **问题(0.9.21 实证)**:`@iasiv5/dsh-skins` 1.2.3→1.3.0 升级后未重启即已生效(客户端 bundle rev 热更),三端仍无条件提示「需要重启」——警报疲劳会侵蚀提示的权威性,重启本身也有真实成本(web 服务瞬断 / desktop 手动重开)。
451
- - **生效判定(GLOSSARY「生效判定/纯客户端更新」)**:npm 源升级成功点拉取新旧两版 tarball(并行、总 deadline 10s、单包 8MiB、无缓存),零依赖 ustar 只读解析(支持 pax 长名)+ 逐文件 sha256 diff,分类 `activation: 'client-only' | 'restart-required' | 'unknown'`。规则五条:client 集合 = `exports['./client']` 目标;`dsh.bundle.patch` 声明的补丁目标变更 → 宿主;`package.json` 忽略顶层 version 后语义比较(dependencies 等字段变化照常算宿主);其余差异按路径归属;client 指向变化保守判宿主。**fail-open**:一切异常 → unknown → 现状提示,绝不影响升级成功态。
452
- - **三端分流**:agent 工具消息 client-only 明示「刷新页面即可生效,不要询问 dshm_restart」、unknown 保守建议重启;GUI toast 后缀 + `needsRestart` 门(client-only 不亮重启横幅;纯函数 `upgradeNotify` 可单测);CLI 行文案换挡。`needsRestart` 源头放宽 boolean(TS2430 规避),`UpgradeResult`/desktop 升级结果新增 `activation` 字段;接线点 `upgradePluginLocked` + `desktopUpgradeLocked`,selfUpgrade/install/uninstall/github 源维持现状。
453
- - **测试与文档**:新增 ustar 解析 11 例、分类规则+fail-open 18 例、升级接线 7 例(npm/github/selfUpgrade/desktop)、renderUpgrade 4 例、upgradeNotify 3 例;既有升级替身统一补 `classifyActivation` 缝(防隐式出网)。决策与已知局限全文见 `docs/adr/0007-activation-classification.md`(client chunk require 图不追踪;docs 类随版差异保守判宿主侧)。
454
-
455
- ### 0.9.21 变更:设置页「强制刷新」显示连带修复 + registry.json 变更自动清 jsDelivr 缓存
456
-
457
- - **设置页强刷显示修复(实机 2026-10-03 实证)**:设置页「强制刷新」只重载 `registry` 接口(force 同步强刷全链路),但面板展示的 registryState **优先读 `registry-config` 的挂载时快照**——于是出现「toast 报已强制刷新、生效来源/更新时间/条目数纹丝不动」的假死,重开设置页才对齐。现在强刷成功后连带重载 `registry-config`(force 已更新 controller 内存快照,零成本取新值),显示即时跟随生效数据。
458
- - **jsDelivr 自动清缓存(`registry.yml` 新增 `purge-jsdelivr` job)**:默认链「线路粘性 + CDN 恒 200 即成功」会让 jsDelivr 的滞后快照**无限钉死**精选清单——实证:0.9.18 收录 DSH Market(18→19)后,粘性在 jsDelivr 的本机持续拉回 18 条旧版,raw 主线路永远轮不到,用户侧强制刷新也无解(唯有手动 `purge.jsdelivr.net`)。现在 push 到 main 且 `registry.json` 实际变更时(`github.event.before` diff 判定),`validate` 全绿后自动调 purge 接口清缓存并回读 cdn 验证条目数,结果写入 step summary;registry.json 未变的 push 与 PR 全部跳过。
459
-
460
- ### 0.9.20 变更:latest 探测缓存退回纯内存(重启即失效)+ mutation 定向失效(ADR-0006)
461
-
462
- - **事故复盘落地(2026-10-03 凌晨)**:0.9.14 起 latest 探测缓存 write-through 落盘、跨重启存活,失效通道只有 TTL 一条——00:22:58 缓存写入后,00:27–01:05 连发四版、01:07 重启 DSH、01:13 重开面板全部吃到陈旧值,卡片与 `dshm_outdated` 双双误报「已是最新」。本次推翻该设计,决策与取舍全文见 `docs/adr/0006-latest-cache-memory-only.md`。
463
- - **缓存退回纯内存**:latest 探测结果只存内存 Map + TTL(`cacheTtlMin`,默认 60 分钟),**重启即失效**——「发完版重启一下就能看到」重新成立。代价是重启后首轮受限重探(当前页条目、8 并发 + deadline 兜底,TTL 内只付一次);registry/社区**目录正文**的落盘缓存与 SWR 不受影响,页面骨架照旧秒开。0.9.14 遗留的 `<cacheRoot>/latest/<ns>.json` 惰性文件由探测段一次性 best-effort 清扫。
464
- - **mutation 定向失效**:install/upgrade/uninstall 事务**成功点**按 itemId 作废该条目全部 registryKey 缓存变体(浏览页键 / 已装页 matched 键 / npm-only 键)——升级后卡片不再出现「已装新版 / 最新旧版」自相矛盾;`dshm_outdated` 对刚升级包诚实。只失效不回写(故意装旧版时回写会伪造 latest=已装);事务回滚路径零失效;卸载对 github 源条目的 `gh:` 键尽力而为(pkg 名不可逆推 repo,交由 TTL 自然过期)。
465
- - **不做项留痕**:force 穿透探测缓存与浏览页 GitHub 预算对齐经主人拍板暂缓——前者残余盲区仅「不重启 + TTL 内」窗口,后者对现行全 npm 源精选清单收益为零(ADR-0006 §Considered Options)。
466
-
467
- ### 0.9.19 变更:供应链等待期「锁文件校验」实证修复——委派前预检 + 全量违规解析 + 账实分裂明示
468
-
469
- - **根因(本机 2026-10-03 00:15 实证)**:pnpm 11.7 对 desktop profile 做**锁文件级**供应链校验(`Verifying lockfile against supply-chain policies (180 entries)`),而它给每次成功安装自动追加的 `minimumReleaseAgeExclude` **非 scoped 独立精确条目不被这次校验认可**(`dsh-m@0.9.18` 即被拒;scoped 的 `'@iasiv5/dsh-quota-watch@0.1.13'` 则认可)——装过一个「太新」版本后,**等待期内任何官方包操作都会被这个旁包条目拦死**,与本次目标无关;且校验失败前目标包已被写入 node_modules、官方管理器只回滚 manifest/lockfile → 界面显示新版 active、操作记录却是失败(**「账实分裂」**),下次包操作还会把插件静默回退。
470
- - **委派前预检(`releaseAgePrecheck`,只读、零文件级红线不破、fail-open)**:npm 源安装/升级在委派官方管理器前,对目标版本与锁内「不被校验认可的独立精确排除条目」逐个核对 registry 发布时刻,任一未满等待期 → 结构化 `release-age-wait` 拒绝(含各自可重试时刻),不再产生半写状态;发布时刻不可得 / 策略不可读 / 命中有效排除条目(包名级、`||` 复合、scoped 独立精确)→ 放行,pnpm 仍是最终执行者。
471
- - **失败翻译换新(替代 0.9.10 单条解析)**:解析全部违规条目,按「本次目标 vs 锁内旁包」分述发布时刻与可重试时刻——旁包连坐不再被冒充成目标被拦;移除「改用 DSH Web 安装」的失配指引。`DesktopOpsError` 新增结构化 `details`(violations/targetViolating/splitState/blockers)。
472
- - **账实分裂复读**:等待期失败后复读实装状态,node_modules 已是目标版本而 manifest 仍旧版时,在错误信息中明示「下次包操作会回退到旧版,等待期满重新升级即可对齐」。
473
- - **接线**:desktop install / self-upgrade 调用点补穿 `profileDir`(升级路径此前已有)。
474
-
475
- ### 0.9.18 变更:精选收录 DSH Market + README 重构与变更日志外迁
476
-
477
- - **新收录 DSH Market**(npm `dshmarket`,精选 18→19):三方可视化插件市场——浏览、搜索社区插件并一键安装,主题一键热切换;主桶装机必备、次桶崔添翼精选(`alsoCategories` 次级归属)。文案按收录规范三句式,三方条目不设 `verified`。
478
- - **文档面重构**:README/README.en 重写为精简结构(亮点、TOC、环境要求、文档索引、支持矩阵 FAQ),推荐安装路径改为 DSH 官方插件管理界面(附截图与升级限制提示);本变更日志自 README 外迁(双语维护);Desktop FAQ 对齐 0.9.8 后能力面;DESIGN §3/§12 补实机核验与实测代际记录。
479
- - **附带**:registry 守卫测试条数 18→19;`package.json` files 新增 CHANGELOG.md 随包发布。
480
-
481
- ### 0.9.17 变更:策展桶次级归属(一插件跨桶)+「iasi自研」更名
482
-
483
- - **次级归属 `alsoCategories`**:收录条目可声明次级策展桶——chips 计数与桶过滤按「主桶 ∪ 次桶」计(跨桶条目在每个桶里都出现),详情页分类标签仍随主桶。首批双席位:**better-sidebar**(装机必备 ⊕ 崔添翼精选)、**dsh-m**(装机必备 ⊕ iasi自研);better-sidebar 原第 5 个「崔添翼精选」tag 由真实席位取代(tags 回归 ≤4 软规范)。
484
- - **更名**:策展桶「我的自研」→「**iasi自研**」(slug `self-dev` 不变,chips/工具/CLI/详情标签四端同步)。
485
- - **例行过渡**:schema 加字段后旧客户端校验拒收新 registry → 回落缓存/包内快照显示旧数据,升级即愈。
486
-
487
- ### 0.9.16 变更:精选策展五桶分类法 + 严筛收录(23→19)+ 新收录 DSH TUI
488
-
489
- - **策展分类法**:精选区分类从功能五分类(市场/工具/界面/搜索/其他)改为**策展五桶**——装机必备 / 崔添翼精选 / iasi自研 / 腾讯轻量云专区 / 观察区(chips 按此序,腾讯轻量云垫后)。分类语义从「插件是什么」转为「为什么值得进精选」;重叠归属按 装机必备 > 崔添翼精选 > 自研 > 腾讯轻量云 优先级归桶,功能属性转 tags 保留检索,旧分类值仍可作开放 slug 命中自定义源(ADR-0004 修订)。
490
- - **严筛收录 23→19**:通道五件套(lark/qqbot/weixin/wecom/dingtalk)退出精选——社区层(4,000+ 条)仍收录可装,只是退出策展位;新增收录 **DSH TUI**(崔添翼 9/26 X 推荐:终端 TUI 客户端,`dsh-tui`,走 DSH 客户端契约 ctx.remote);better-sidebar 因优先级归装机必备,以 tag「崔添翼精选」保留 9/27 推荐出处。
491
- - **同版本携带**(本地增强随发版收编):desktop 安装 enable 阶段失败自动重试一次(高频装卸/插件树重载竞速实证)+ 失败文案按 packageResult 精确化;市场页「缓存快照」横幅退役——stale 状态由设置页社区卡承接,浏览页不再提示临时缓存状态。
492
-
493
- ### 0.9.15 变更:安装进度/终态就地进详情 Modal
494
-
495
- - **场景**:从详情 Modal 点「安装」后,安装信息(pnpm 阶段进度行、「变更完成」横幅/toast)都渲染在面板底层,隔着 Modal 遮罩半透明透出——弹窗内只有按钮转圈,看不出装到哪一步(实机截图反馈 2026-10-02)。卸载无此问题:不弹 Modal,状态本来就挂在已装卡片上。
496
- - **变更**:安装进行中,Modal 内直接挂进度行(复用 host status 轮询:阶段/进度条/当前包);终态在 Modal 内就地显示结果行——成功附版本与构建脚本说明 + 重启提示(desktop 给官方应用生命周期指引),失败/守卫拦截附原因且按钮回到可重试,「已跳过」中性呈现;Modal 打开时底层同源进度行让位(关闭 Modal 后照常回归)。信息全部派生自全局操作记录与安装结果,「状态不挂卡片」的所有权模型不变(DESIGN §2.6)。
497
-
498
- ### 0.9.14 变更(市场秒开四件套 + 详情 Modal 安装命令显隐)
499
-
500
- - **市场秒开四件套**:针对「每次打开市场必现『加载收录清单中…』」的加速组合拳——
501
- - **SWR 先回缓存**:精选清单与社区目录的 TTL 过期不再同步等网络——磁盘缓存存在即**立即返回快照**(社区侧照常显示「缓存快照」横幅,绝不冒充最新),后台单飞自愈,下次打开即新;「强制刷新」按钮语义不变(始终同步强刷,社区卡不连坐)。
502
- - **线路粘性**:默认双线路(GitHub 原始文件 → jsDelivr 镜像)按缓存记录的**上次成功线路**排序——镜像成功过就先走镜像,不再每次白等主线路失败(大陆网络实测每次冷打开省 10-20s 等待)。
503
- - **探测缓存落盘**:页条目 npm/GitHub 版本探测缓存从纯内存改为磁盘信封(`latest/`,跨重启存活)——DSH 服务重启后首次打开市场不再重放探测。
504
- - **客户端快照**:默认首页响应存浏览器本地(10 分钟 TTL),打开面板先渲染上次数据再后台换新——加载 spinner 仅首次使用(无任何快照)出现。
505
- - **详情 Modal「安装命令」折叠行按上下文显隐**:
506
- - **场景**:折叠行命令推导写死 `dsh plugin --profile web add …`,社区条目的上游 install 原文也同为 --profile web 语义——两个来源都不看当前宿主 profile。desktop 上下文里照抄会把包装进 web profile(当前界面看不见);已安装条目还挂着命令纯属噪音(实机截图实证:desktop + dsh-m 自身条目,`已安装` 徽章与命令同屏)。
507
- - **变更**:desktop 上下文与已安装条目**整行隐藏**;web 未安装条目行为不变(CLI bootstrap 路径保留,「30 秒上手」同款命令)。desktop 装机正路是弹窗内「安装」按钮(官方 pluginManager 委派,ADR 0005 纪律)。不做「精简命令去掉 --profile」——显式 `--profile web` 是 0.9.0 拍板的设计(README「profile 目标」节),裸命令的默认 profile 语义含糊,精简反而更差。
508
-
509
- ### 0.9.13 变更:desktop 角标悬停文案更新(主人拍板)
510
-
511
- - 旧文案「当前 DSH profile:{name}(Desktop 首发仅支持只读市场、安装新包与开关)」在 0.9.8 开放升级/卸载/自升级后已过时;按主人拍板改为「**当前生效 Profile**」(en: Active profile)。
512
-
513
- ### 0.9.12 变更:0.9.11 的重发
514
-
515
- 0.9.11 在 npmjs 遭遇幽灵发布:OIDC 发布被受理并 **staged**(CLI exit 0、provenance 已上 Transparency Log),但从未 commit 进注册表——GET 404、同版本重发 409 `Cannot publish over previously staged version`。等待自愈无果后按标准解法换号重发。**内容与 0.9.11 完全一致:设置页「强制刷新」不再连坐社区清单卡。**
516
-
517
- ### 0.9.11 修复:设置页「强制刷新」不再连坐社区清单卡(force 语义只属精选链)
518
-
519
- - **场景**:弱网下点精选清单的「强制刷新」,社区清单卡跟着变「不可用 + 获取超时」——根因是 host 把 `force` 一路透传给社区目录 summary,强制重开获取 flight;弱网下 flight 3s 完不成,waiter 超时返回占位摘要(flight 本身在后台 30s hard cap 内继续,跑完即自愈)。
520
- - **修复**:`registry` 的 force 不再透传社区 summary——「强制刷新」语义只属精选链(registry 链);社区目录走自己的 TTL/共享 flight(首次打开等边界场景在极差网络下仍可能瞬时超时,但强刷不再触发)。
521
-
522
- ### 0.9.10 修复:Desktop 自升级撞上官方供应链等待期(minimumReleaseAge)→ 诚实指引发成可读
523
-
524
- - **场景**:点升级角标走官方管理器时,desktop profile 的 pnpm 供应链策略(`minimumReleaseAge`,发布满 24h 才可安装)拒绝了刚发布的版本——`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`。**这是策略在正确工作**(防供应链攻击的发布等待期),不是故障;但 dsh-m 此前把它当普通失败甩一屏 pnpm 原文。
525
- - **修复**:管理器结果判定识别该策略码,翻译成诚实指引——点名等待期内的条目与发布时刻、按策略推算「预计何时可重试」,并给出等待期内的替代路径(DSH Web 安装同版本)。绝不做策略绕过(等待期是防供应链攻击的红线,dsh-market #732 同态度)。
526
- - 附带:community 获取链 ⑨ 的挂起上限断言 2s→10s(全量套件并行定时器饥饿下两次闪断;契约「不永久挂起」不变)。
527
-
528
- ### 0.9.9 修复:备用线路接住后不再弹红色「主线路失败」提示(提示收敛)
529
-
530
- - **场景**:默认精选清单双线路(raw → jsDelivr)里主线路 `default-raw` 失败、备用线路成功接管时,设置页仍弹红色「远端提示:default-raw 失败:fetch failed…可稍后重试或检查网络后重试」——对着已经自愈的数据报警,还带着不成立的建议(大陆网络下 raw 间歇不可达是常态,这正是备用线路存在的原因)。
531
- - **修复**:后续线路成功 = 先行失败已自愈,`errors` 不再携带(设置页「生效来源」行已如实标注当前线路,如「GitHub 镜像(备用)」);全线路失败落 cache/bundled 时错误照常保留——那才是需要行动的信号。custom 链(自定义源失败回退缓存)行为不变,仍然提示。
532
- - 顺带补了 default 链的测试缝(`defaultRoutes` 覆写),双线路收敛与全挂保留各有回归门。
533
-
534
- ### 0.9.8 修复:Desktop 包操作全面接通官方管理器(安装恒 no-manager;升级/卸载/自升级开放)
535
-
536
- - **安装恒失败的根因**:Host API 的 desktop 安装分支调 `desktopInstall(id, cfg, opts)` 漏传第 4 参 deps——`getService` 根本没进适配器,desktop 安装恒报「官方 pluginManager 服务不可用(fail-closed)」(100% 必现,与时机无关)。工具面 `dshm_install` 同病。
537
- - **服务解析与 dsh-market 同源**(借鉴其 official-desktop 接线,本机两轮覆盖安装实证):探测改双上下文(webServer 注入回调的 hostCtx 优先)+ cordis inject 惰性拉起兜底(短超时)——官方 pluginManager 是惰性服务,未被拉起前一次性 get 恒 undefined;仍缺席才结构化拒绝(绝不文件级回退的红线不动)。
538
- - **能力表扩充(主人裁决,借鉴 dsh-market 策略)**:desktop 的 upgrade(installBundle 覆盖安装)/ uninstall(removeBundle)/ self-upgrade(installBundle('dsh-m@latest'))全部开放——dshmarket 正是这样完成 dsh-m 0.9.3→0.9.4/0.9.5 两轮升级的;判定纪律沿用(application/stage 为准、overridden 非失败、build-blocked 结构化回传 pendingBuilds、listBundles 复读不冒充成功)。restart 继续拒绝(Electron 生命周期归官方壳)。
539
- - GUI 与工具面(dshm_install / dshm_uninstall / dshm_upgrade)三入口同批接线。
540
-
541
- ### 0.9.7 修复:Desktop 下点升级角标弹红色「升级失败」(能力表拒绝应为指导而非报错)
542
-
543
- - **根因**:0.9.1 的升级角标点击后一律调 `self-upgrade`;Desktop 能力表按设计 409 结构化拒绝,但客户端把 409 当普通失败渲染成红色「升级失败」横幅——按能力表这根本不是失败,是「该走官方入口」的指引。
544
- - **修复**:`api()` 透传能力表拒绝的结构化字段(code/action/profile/guidance),角标点击收到 409 时改出**中性 info 横幅**展示官方入口指引(guidance 单一事实源仍在服务端 `active-profile.ts`,客户端零复制);info 横幅停留 12s。Web 端真实失败仍走红色 err 横幅,行为不变。
545
-
546
- ### 0.9.6 修复:「清除已完成」对失败记录无声 no-op
547
-
548
- - **根因**:0.7.0 评审共识把「失败/已跳过」排除在清除范围外(保留供回看),于是按钮对着一条失败记录点击毫无反应、也无任何反馈——Windows 实机被当成 bug 上报(2026-10-01)。显式点击清除不是「静默抹掉」,旧共识被推翻。
549
- - **修复**:「清除已结束」(原「清除已完成」)现在清除**全部终态**(done/warned/failed/superseded),在途态(queued/running/input)不受影响;没有可清终态时按钮置灰并带说明 tooltip,不再无声 no-op。单条 ✕ 照旧逐条删除。
550
- - **英文文案**同步改为 "Clear ended"。
551
-
552
- ### 0.9.5 修复:Desktop 下 GUI 读路径漏接 active profile(已装页恒显 web)
553
-
554
- - **根因**:0.9.0 双 profile 接线时,Host API 的 `installed` / `market` 两个读路径漏传 active profile——`listInstalledWithMeta` / `listMarket` 内部落回 `webProfileDir()`:Desktop 下已装页恒显「web profile 尚未安装任何插件」、市场「已安装」徽标恒空(agent 工具面 `dshm_list` 同链路已接线,故只有 GUI 错)。
555
- - **修复**:两处补齐 `profileDir: profile.dir` + `profile: profile.name`(与 tools 面同款,profileContext 单一事实源);registry/社区缓存随 `profile` 参数自动落到 desktop 段。
556
- - **文案**:已装空态/加载态与 profile 提示里的「web profile」硬编码改为 profile 中性表述(实际路径照旧展示)。
557
-
558
- ### 0.9.4 修复:Windows Desktop 全屏后头部不可点(tab / 还原键被系统标题栏吞掉)
559
-
560
- - **根因**:Desktop(Windows)以 `titleBarStyle:hidden + titleBarOverlay` 运行——窗口顶部 40px 是壳的全宽 `-webkit-app-region:drag` 拖拽带(按布局参与拖拽判定、无视 z-index 与绘制顺序),右上角另有系统绘制的 — □ ✕ 悬浮于一切内容之上。面板全屏后头部(tab、还原/关闭键)正好落进这条带:点击被窗口拖拽吞掉、还原键被系统键遮挡,全屏无法退出。
561
- - **修复**:与壳自家 overlay 同款对策——消费壳在 `html` 上设的 `--dsh-windows-titlebar-height` 让出该带(全屏态 `top:var(…)`;浮动态 `padding-top:max(24px,var(…))` 顺带修掉矮窗口下浮板顶边被带压住的边缘情况)。
562
- - **Web 零漂移**:DSH Web / 浏览器无此变量,回落 0px,行为与 0.9.3 完全一致。
563
-
564
- ### 0.9.3 修复:Windows 原生适配(Windows 宿主实机回归)
565
-
566
- - **registry 原子写**:临时文件名此前用 `split('/')` 从绝对路径取尾段,而 Windows 路径分隔符是 `\`——取到的是整条路径(内嵌 `\` 即目录分隔符),临时文件 open 必败且被吞,cache 与 accepted metadata 写入**静默全失败**;改用 `basename()`,三平台一致。
567
- - **本地文件清单地址解析**:`C:\…` 盘符与 `\\server\share` UNC 此前被误当 URL scheme(报「只允许 HTTPS」的误导性错误),`file://C:/…` 会归一成无盘符悬空路径必 ENOENT;现 POSIX 绝对 / Windows 盘符 / UNC 三形态均正确解析为 file kind。
568
- - **构建**:`node_modules/.bin/tsc` 是 POSIX sh shim,Windows 下 `spawnSync ENOENT`;改用当前 Node 直跑 TypeScript 的 JS 入口,`npm run build` 三平台一致。
569
- - **测试面**:Windows 实机全量 829 用例 41 败 → **0**(821 pass / 8 skipped);POSIX 进程组/SIGTERM 时序、symlink 权限、平台路径断言、固定 sleep 时序假设逐项标注 skip 或平台无关化/有界轮询,测试契约不变。
570
-
571
- ### 0.9.2 修复:跨服务重启后头部角标停留旧版本
572
-
573
- - **一键重启确认新进程后就地刷新**:boot id 确认 DSH Web 已恢复的瞬间,面板立即重取 ping——`dsh-m vX.Y.Z` 角标与 profile chip 同步到新进程数据,不再停留旧版本(0.9.1 实测:芯片升级 + 重启后仍显 v0.9.0)。
574
- - **页面回前台时重取**:面板常开、服务在后台被外部重启的场景,`visibilitychange` 回前台即重取 ping(零轮询成本)。
575
- - **角标版本护栏**:self-check 判定携带的版本与当前进程不符(旧进程残留判定)时一律静默,杜绝「v0.9.1 ⬆ v0.9.1」式误渲染。
576
-
577
- ### 0.9.1 新增:头部版本角标升级提示(有更新才点亮)
578
-
579
- - **静默口径**:面板头部 `dsh-m vX.Y.Z` 角标常态维持 0.7.5 起的静态展示——已是最新、检查失败、本地 dev 版领先 npm(ahead)一律不打扰(ahead 仅在悬停 title 里提示「本地开发版」)。
580
- - **仅 outdated 点亮**:`self-check`(npm latest vs 装机版本,只读)判定有新版本时,角标点亮为 warn 态并显示 `⬆ v<最新>`;点击即触发 `self-upgrade`(同一 mutation session + 装后守卫),成功后出「⚡ 一键重启」横幅。Desktop 下点击按能力表结构化拒绝(409,带官方生命周期指引)。
581
- - **TTL 缓存 + 版本护栏**:检查结果在浏览器 localStorage 缓存 30 分钟,开面板不重复打 npm registry;升级重启后缓存按版本号自动作废。检查失败静默,不弹任何错误。
582
-
583
- ### 0.9.0 新增:官方 Desktop(双 profile)支持
584
-
585
- - **同一包、两个 profile**:dsh-m 现在可装入官方 Desktop 的 `desktop` profile(`~/.dsh/profiles/desktop`),与 Web 的 `web` profile 并列;市场目录、面板与 agent 工具同一套,管理对象始终是宿主当前 profile(官方 `profileContext` 单一事实源)。
586
- - **入口信任检查改委派官方**:`/dshm` 全部 method(含 ping 与未知 method)在读取请求体之前委派官方 `connection.requestRejection()` 判定(trustedHosts / loopback / 跨站 / `Origin: null` 语义随宿主),被拒请求零 body 消耗、零业务调用;宿主缺该能力时 fail-closed 全拒。**行为变化**:旧版自制守卫「缺 Origin 一律 403」不再存在——Desktop 桥合法剥除 Origin 的请求按官方语义放行,无凭据的健康检查探针从「一律 403」变为「按宿主信任判定」。
587
- - **Desktop 首发能力表**:只读市场 + **安装新包**(委派官方 `pluginManager.installBundle`,完整性/锁/生效相位归官方)+ **开关**(委派官方管理器,服务缺席结构化拒绝、绝不文件级 fallback);**升级 / 卸载 / dsh-m 自更新 / 一键重启**在 Desktop 结构化拒绝(409,带官方入口指引——官方暂无 upgrade API,重启归 Electron 生命周期);构建脚本按官方 `pendingBuilds` 名单精确重试,绝不全量放行。
588
- - **读模型与缓存按 profile 隔离**:市场安装标注、已装列表、README 预览只读当前 profile;registry / 社区清单 / accepted-source 缓存按 profile 分段(web 沿用旧路径,零迁移零清空);收藏与操作记录按浏览器 origin 各自独立,Web 与 Desktop 不自动同步。
589
- - **CLI 恒作用于 web profile**:`--profile web` 显式声明;`--profile desktop` 明确拒绝并指引官方 Desktop 插件管理页。
590
- - **如实声明**:Desktop 实机(Win/macOS)E2E 未跑,`registry.json` verified 数组**不新增** Desktop 代际(实测后按收录纪律补录);Desktop 下不做 dsh-m 文件级装后守卫(app.asar 打包布局探测盲区),以官方结果判定 + `listBundles` 复读替代。
591
-
592
- ### 0.8.5 修复
593
-
594
- - **dshm_upgrade 守卫拦截假成功**:升级命中装后守卫拦截时(如 link/file 来源插件无法自动回退),文本输出误渲染为「✅ undefined 已升级(最新)」;现如实输出拦截原因、补偿终态与修复依据,与卡片标题(守卫拦截)一致。
595
-
596
- ### 0.8.4 变更:分类标签随界面语言双语化
597
-
598
- - **社区分类**:已知 23 个分类的英文名直接取上游目录 `categories.en`,英文界面下分类 chips 与详情/收藏 Modal 的分类行显示英文;缺英文名的上游新分类回退中文,仍按原样渲染进临时组,等发版收录。中文界面不变。
599
- - **精选分类**:五个分类 chip 改走双语字典(市场/工具/界面/搜索/其他 ⇄ Market/Tools/UI/Search/Other),与详情 Modal 口径一致。
600
- - **实现**:summary 新增 `categoryLabelsEn`(`communityOutcome` 从上游目录派生,缺 en 的 id 不进映射,不手养第二张表),客户端按界面语言合并取值。0.8.1–0.8.3 为本地迭代号,无独立变更面,不单列。
601
-
602
- ### 0.8.0 变更:设置页重做(对齐双清单分区定位)
603
-
604
- - **信息架构**:社区清单(from awesome-dsh-plugin)→ 精选清单(registry.json)→ dsh-m 自身 → 关于;「主清单」的 UI 可见名统一为「精选清单」。
605
- - **社区目录开关**:新增 GUI 开关(live 生效,即时切换无确认;关闭态整卡收为一行说明);新增 `set-community` API。
606
- - **精选清单瘦身**:状态字段 7 行收敛为 4 行(配置地址合并、删除写死的「缓存策略」行);「配置状态」仅异常时出现;按钮组精简为「强制刷新 / 校验并应用 / 恢复默认 / 下载默认 registry.json」,「检查条目可达性」从 GUI 移除(`registry-diagnose` API 保留)。
607
- - **文案修正**:自定义源说明明确「整体替换主清单、仅影响『精选』区」;删除与实际不符的 TTL 写死描述(`timeoutMs`/`cacheTtlMin` 仍走 config 配置)。
608
- - **dsh-m 自身**:本地 dev 版领先 npm 发布时改显「本地为开发版」而非误导性的旧「npm 最新」(新增 `ahead` 字段)。
609
- - **关于**:文案对齐当前定位,新增 GitHub 仓库与问题反馈链接。
610
-
611
- ### 0.7.10 美化
612
-
613
- - **头部三段分组**:标题与 tab 导航之间加发丝竖分隔线,「标题 | 导航 | 状态+窗口控制」边界清晰。
614
- - **版本角标同色**:v 版本号不再用主文字亮色,与 dsh-m 名称统一为次级灰。
615
- - **垂直节奏收敛**:窗口控制组按钮 26→28px 与 tab 按钮等高;版本角标微调至 24px 高;最大化/还原图标统一 12px;标题字重 700→600。
616
-
617
- ### 0.7.9 修复
618
-
619
- - **分类 chips 顺序恒定**:退役「激活分类置前」的换序逻辑(此前每点一个被折叠裁掉的分类,它就会跳到首位,顺序随点击不断变化)。替换为:激活分类落在收起态裁剪区时自动展开完整分类行——「当前激活的分类始终可见」目标不变,顺序从此稳定;同一激活分类下手动收起会被尊重,换选其他被裁掉的分类或点「全部」时重置。
620
-
621
- ### 0.7.8 变更
622
-
623
- - **修复「最大化没作用」的根因——CSS 热更自愈**:面板样式表此前只在首次注入(`#dshm-css` 存在即跳过),服务热更后旧 bundle 留下的样式表不含新类名规则(全屏/窗口控制组),导致新功能「点了没反应」、按钮裸奔成原生样式。现给注入的样式表带内容哈希(djb2)版本标记,bundle 更新后重新打开面板即自动替换旧样式,无需刷新页面。
624
- - **窗口控制组重新配色**:去掉浮起底色,改透明底 + 发丝外框(与搜索清除钮同一配色语言),单格加宽 34→44px 防误触;关闭悬停仍为红色警示。
625
-
626
- ### 0.7.7 变更
627
-
628
- - **移植 dsh-market 的全屏功能**:面板头部新增「最大化/还原」(连体窗口控制组设计:最大化 + 关闭等宽两格、发丝分隔线、统一 SVG 线条图标;关闭悬停红色警示)。全屏铺满视口、去圆角,状态 localStorage 记忆,Esc 关面板语义不变。
629
- - 0.7.6 补记:版本角标去粗体、去点击复制(改静态展示);吸顶分类行上沿镂空修复(sticky 锚点上移抵消容器 padding);关闭/清除按钮改框线平面风。
630
-
631
- ### 0.7.5 变更
632
-
633
- - **头部版本 chip 改显 dsh-m 自身版本**(`dsh-m v0.7.5`,点击复制;DSH 运行版本看设置页与 `dshm ping`)。
634
- - **× 关闭/清除按钮统一重绘**:搜索清除、详情 Modal 关闭、操作记录行移除三处改用 SVG 线条图标 + 悬停浅底的专用按钮样式;搜索清除钮悬浮于输入框右缘(胶囊内对齐)。
635
- - **修复 dsh-market 的 peer 告警**:`@deepseek-ai/dsh-tools` peer 由 `*`(semver 严格口径不匹配 rc 预发布版本)改为显式 range `^0.1.7-rc.2 || ^0.2.0-rc.1 || >=0.2.0`;未来更新的 rc 线(如 0.3.0-rc.x)需再追加。
636
-
637
- ### 0.7.4 变更
638
-
639
- - **面板 tab 回退旧版分段按钮风格**(0.7.2 误改下划线样式,按主人要求还原圆角按钮组 + 高亮态)。
640
- - **搜索框通长**:修复搜索容器缺 `display:flex` 导致输入框未拉伸的问题,恢复整行宽度。
641
- - **筛选按钮与页号跳转控件重新配色**:筛选按钮改浮起面板底色 + 方角与分类 chips 区分(激活态品牌色描边);页号输入改胶囊形细描边,「跳转」用品牌色文字钮。
642
- - **吸顶分类行彻底不透**:背景直接使用不透明底色 token(此前的 color-mix 半透明配方在深色主题下仍会透出下方卡片文字),保留底部分隔线。
643
-
644
- ### 0.7.3 变更(含 0.7.2)
645
-
646
- - **首页布局复刻 dsh-market**:市场页改为「分区 chips → 整宽搜索行 → 分类 chips + 行尾筛选弹层」结构;筛选弹层独立样式(方角矩形 + 前置 chevron),收纳排序字段(npm 下载量/Star 数/收录日期)、排列方向与每页条数(原排序下拉与分页器条数选择退役)。
647
- - **收藏卡可点开详情**:修复收藏区卡片点击无响应——收藏快照字段不全,打开时按 id 从两分区内存 → market API → 快照三级解析完整条目,详情 Modal 与安装链路(含兼容确认)全量复用。
648
- - **翻页页号跳转**:页码行新增页号输入框,输入有效页号回车或点「跳转」直达。
649
- - **吸顶分类行毛玻璃**:滚动时吸顶的分类行加 backdrop blur 与底部分隔线,下方卡片文字不再透出干扰。
650
- - **按需求移除**:「发现社区/申请收录」行(dsh-m 不支持收录功能)、「任务」按钮(操作记录面板恢复常驻)、「刷新」按钮(强制刷新在设置页)。
651
- - 说明:收录条目不携带宿主版本要求字段,dsh-market 的「宿主版本」筛选项无数据源,未复刻。
652
-
653
- ### 0.7.1 修复
654
-
655
- - **社区条目可安装**:修复 0.7.0 回归——社区区里的条目点安装报「registry 中没有该条目」(安装按收录 id 只查主清单,未查社区目录);现与升级路径同构,主清单 miss 时按 id 查社区目录再装。
656
- - **市场页第一行紧凑化**:搜索框、刷新、排序(社区区)并入分区 chips 行右侧;信息性来源横幅(「官方默认收录清单 / 自定义收录清单 / 来源为本地缓存」)退役——其「共 {count} 条」计数从未接线(恒显 0);错误态「收录清单不可用」与社区兜底/陈旧提示保留。
657
-
658
- ### 0.7.0 新增
659
-
660
- - **分区制市场**([ADR-0004](./docs/adr/0004-zoned-market-display.md)):数据层双清单合并不变,展示层按「社区(默认)/ 精选 / 收藏」三分区呈现;`dshm_search` 与 `dshm search` 改用 `--source community|primary|all` + `--offset` 真翻页(默认 10 条),`primary_only` 退役。
661
- - **搜索相关性**:NFKC 归一化 + 中西文边界 + 字段加权(name/npm > owner > 描述 > 分类 > tags),多词同字段全命中;id 整串精确匹配最高优先。
662
- - **操作记录 + 恢复执行器**、**本地收藏 + 下架清理**、**详情 Modal + 截图灯箱**、社区卡 byline/deprecated 徽章/目录版本快照兜底(不参与 outdated 判定)。
663
-
664
- ### 0.4.0 新增
665
-
666
- - **开关(Enablement Toggle)**:已装卡片一键启停,内部自动路由行覆盖(单行插件,即时生效)或 Bundle 选择(多行插件);写路径委派官方 `pluginManager` 服务、缺席时降级 loader 直操作([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md))。
667
- - **运行相位徽标**:loader fiber 状态投影,failed 一眼可见。
668
- - **保护名单**:`dsh-m` 自身与官方宿主命脉 16 项不可开关、不可卸载(升级不受影响)。
669
- - **精确构建放行**:needs-builds 拦截后按 pnpm 待决名单逐键放行,全量放行降为兜底并如实标注([ADR-0002](./docs/adr/0002-precise-build-approval.md))。
670
- - **peer 兼容预检**:安装/升级前校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(GitHub 源明示未检);不兼容时 GUI 弹确认、agent 回结构化结果、CLI `--force`。
671
- - **实测版本清单(verified)**:registry 条目可选 `verified` 数组记录实测过的 DSH 运行时版本——实测声明而非预测声明,只展示不拦截。
672
- - **bundle 身份验证**:装后检测无补丁层的包并警告「已装入为纯依赖」。
673
-
674
- ### 0.4.x 退役
675
-
676
- - **元数据源竞速**:0.4.0 曾引入 npmjs / npmmirror ping 竞速选择元数据读取源,现整体移除(含 `probeEnabled` / `probeTimeoutMs` / `probeCacheTtlMin` 三个设置项与设置页展示)。官方同款探测只服务于「安装对话框 registry 默认预选」,dsh-m 无此交互;实测宿主机 npmjs 稳定更快,探测恒等默认行为。元数据读取固定走 npmjs,与安装链路(profile `.npmrc` 默认源)一致。
677
-
678
- ---
679
-
680
- ## English
681
-
682
- ### Fixes in 0.10.2 — account-reality reconciliation for D3 self-upgrade post-write pseudo-failure + silent self-upgrade (first D3 dogfood round)
683
-
684
- - **Need (2026-10-18 early morning, first real-machine D3 dogfood — the 0.10.0→0.10.1 self-upgrade event)**: the self-upgrade reported failure (`application=failed、error=already-installed`) while all three ledgers (manifest/node_modules/lock) had already been written to 0.10.1 — the host has a **post-write pseudo-failure** for its own bundle swap: installBundle writes through, the manifest then contains the name, and the host's completion path trips its own guard (self-referential artifact; the guard's `specPackageName` resolves package names from tarball URLs too). Separately: the 0.10.1 self-upgrade confirmation dialog broke the existing "click to upgrade" experience (owner ordered its removal).
685
- - **Approach**: ① **reconcile on any D3 terminal failure** — re-read listInstalled: if the target version is already written, the upgrade factually happened and the result is re-judged as success (needsRestart=true prompting the activating restart, with the reconciliation noted in output); if the target is absent, the genuine-failure semantics stand; profileDir is threaded from host-api and both seams. ② **silent self-upgrade** — the confirmation dialog is removed wholesale (controlled state / modal / copy keys / SSR pin); the chip click upgrades directly and outcomes are still reported truthfully via banners. ADR-0022 decision 7 records the spike-inference correction, the reconciliation, and the UX ruling.
686
- - **Verification**: typecheck clean; full suite (2 new pins T-D1 reconciled-success / T-D2 genuine-failure, silent structure pins replacing the confirmation pins; 18 pre-existing failures are the 028 machine-environment set, zero new); the real-machine event itself is the first dogfood datum: 0.10.1 landed via "restart to activate", guard journal anchors clean after restart.
687
-
688
- ### Fixes in 0.10.1 — three defects proven by same-day real-machine E2E of 0.10.0: enable-race retry universalized + account-reality re-read on rollback failure + guard hit always two-steps
689
-
690
- - **Need (2026-10-17, proven during the same-day real-machine E2E of 0.10.0 — the skip-auth@0.4.1 upgrade event and the uninstall→reinstall event)**: ① the enable-stage activation race during plugin-tree reload windows (`pending (waiting for service: webRuntime)`) made the two-step upgrade's second leg, the rollback reinstall leg, and the D3 self-upgrade channel fail terminally — the 0.9.94-era "retry enable failures once (retry heals)" local enhancement covered only the first leg; ② a failed reinstall leg ≠ true absence — pnpm may have already written the package (activation failed afterwards), where advising "manually reinstall the old version" would mislead the user into a downgrade; ③ uninstall followed by a same-session reinstall: the host's live state / literal manifest still considers the package installed (guard fires) while the previousVersion probe reports "not installed", so 0.10.0's previousVersion precondition let the raw already-installed error surface.
691
- - **Approach**: ① universalize the enable-race auto-retry — the first leg, the two-step second leg, the rollback reinstall leg, and the D3 channel all share the limited single retry (`installOnceRetryingEnable`; backoff and exempted failure shapes unchanged); ② re-read the ledger (listInstalled) before claiming absence — if written → "restart Desktop to finish activation" (with `details.installedVersion` surfaced truthfully), if truly absent → reinstall guidance; neither path suggests a misleading downgrade; ③ on the install path a guard hit always two-steps (remove+add is the official semantics; the previousVersion precondition is removed), and a failure without an old version to roll back to goes through the same ledger re-read. Incidental: the check-release gate now splits lines CRLF-tolerantly (fixing the bilingual top-entry false negative on CRLF working trees).
692
- - **Verification**: typecheck clean; full suite 1595 tests (8 new pins: T-A1/A2/A3 enable-race on three legs + T-B1/B2 both ledger-re-read shapes + T-C1/C2/C3 three states of guard-hit two-step without an old version; 18 pre-existing failures are the 028 machine-environment set, zero new); real-machine recovery record: skip-auth 0.4.1 recovered via "restart to activate" (zero reinstall), guard journal anchors clean after restart.
693
-
694
- ### Feature + fix in 0.10.0 — host 0.2.1 already-installed adaptation: third-party two-step upgrade + rollback-reinstall degraded restore + dedicated self-upgrade channel (D3)
695
-
696
- - **Need (2026-10-17, the owner)**: on the new DSH (0.2.1-rc.1/rc.2) dsh-m could no longer upgrade or self-upgrade any package. Root cause: the host's `installBundle()` gained an already-installed guard — when the package name resolved from a registry spec already exists in the profile manifest it is rejected before pnpm runs, and the official upgrade semantics became "uninstall first, then install"; tarball/git specs are exempt because their package name is unknowable before pnpm (verified on a real machine: installing a tarball URL over the installed dsh-m succeeded, activation shape restart-required). Scope: in-product desktop upgrade/self-upgrade fully broken; uninstall, toggle, fresh install, and the web path unaffected. Version 0.9.95+ skipped straight to 0.10.0.
697
- - **Approach (ADR-0022, grilled decisions R1–R15)**: ① **install-first adaptive two-step** — no host version probing; only when the first leg returns `already-installed` does it convert to remove + retry (at most once, no loops); the install path silently two-steps on the guard too (installing an installed package is an upgrade), while fresh installs keep zero `removeBundle` calls; release-age/build-blocked/cancelled shapes take precedence over the guard check. ② **Rollback reinstall** — when the install leg terminates in failure the old version is reinstalled automatically: success yields the new **degraded restore** outcome (success shape + degraded/restoredTo; the client projects a warn that never enters the "upgraded vX→vY" arm, the operation pump reuses the existing `warned` state, the settings red dot keeps the existing failed||warned rule, and ✕ dismissal is unchanged); a failed reinstall is the hard `rollback-failed` absence error (with `details.pendingBuilds` surfaced truthfully when the reinstall leg itself is build-blocked); cancellation reports "restored the old version / no upgrade was made" and never masquerades as degraded; a build-blocked install leg still raises the approval request after the rollback has secured the old version. ③ **Channel separation** — third-party plugins use the official two-step; dsh-m's own self-upgrade uses the dedicated tarball direct-install channel (exact version → live-registry `dist.tarball` → intent pre-write → `installBundle(tarballUrl)` → in-place settle), rerouted at a single core seam (`SELF_PKG='dsh-m'`) with zero special-casing in host-api/agent tools/client, and never falling into remove+add; all entries preserved: the header chip gains a confirmation dialog (no uninstall involved, the current version stays intact on failure, a Desktop restart is needed afterwards), and `needsRestart` faithfully passes through the host's `application` (applied → no banner, restart-required → restart prompt). ④ **Boot reconciliation** — a self-upgrade intent ledger (`$DSH_HOME/dshm/self-upgrade-intent.json`; ADR-0018's panel-channel-zero-external-logging invariant intact) serves as a crash safety net: the normal path settles in-process, and only a process that died mid-upgrade is reconciled by the next self-check, once per process, against the actually installed version. ⑤ The mismatched self-upgrade WRITE_GUIDANCE copy no longer sends users "back to DSH Web", pointing to the official Settings → Plugins remove + install (exact version) instead. Seven GLOSSARY terms registered (two-step upgrade / already-installed guard / install-first adaptation / rollback reinstall / degraded restore / dedicated self-upgrade channel / boot reconciliation).
698
- - **Verification**: typecheck clean; full suite 1585 tests (18 pre-existing environment-bound failures per know-how 028, zero new — the clean-tree comparison yields the same set: symlink-EPERM family + client-lightbox ESM scheme + CRLF source anchor + network fetch); new pins cover plan T1–T17 (intent-ledger storage five states / npmTarballUrl three pins / two-step-rollback-degraded boundary nine pins including remove-failed short-circuit, cancelled copy, and reinstall build-blocked pendingBuilds passthrough / D3 and the SELF_PKG seam rerouting five pins / host-api boot reconciliation plus the upgrade(dsh-m) rerouting end-to-end / client degraded projection, two-site wiring, confirmation-dialog ordering, and pump accounting); ADR-0022 filed, layered commits (feat core/client + docs adr/glossary + chore release).
699
-
700
- ### Refactor + fix in 0.9.94 — client-side mutation-outcome copy single-sourced (op-outcome) + patch-mapping YAML quote tolerance and duplicate-key elimination
701
-
702
- - **Need (2026-10-17 architecture walk + nine grilled decisions)**: the presentation of mutation outcomes had no owning module — the guard-interception copy was assembled verbatim three times in main.jsx (0.9.93's toWire converged the wire format while the client-side presentation remained the known leftover; the multi-point hand-patching recurrence when `repairBasis` was added had already been demonstrated once), the upgrade-success copy twice verbatim, the builds suffix four times, superseded five times; separately: the transaction's compensation was blind to **non-double-quoted in-place forms** of workspace `patchedDependencies` (pnpm's single-quoted/unquoted spellings) via verbatim duplicate checks — the verbatim check could fabricate a duplicate-key file and the read-back check could false-pass (the same-package chain is unreachable in production because strip removes it first; this item is defensive-surface convergence and a fail-closed gap-fill).
703
- - **Approach**: a new pure module `src/client/op-outcome.js` (GLOSSARY term "Mutation Outcome Copy" registered) — a factory `createOpOutcome(lookup)` producing finished copy (the `createMarkdown(h)` injection precedent) with four functions `guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed`; six wiring sites (five projection families) become one-line calls, while flow-specific branches (compat dialog / capability-rejection banner / optimistic note) and the ToolCardRow short variant stay behind with an annotation. D9: the workspace leg of `verifyPatchMapping` now judges key membership via real yaml parsing (parse failure fails closed with "read-back failed"); `restorePatchMapping` parse-dedupes before appending (skip when the key exists in any quoting form; on parse failure it degrades to today's verbatim line check).
704
- - **Verification**: full suite 1567 green (new client-op-outcome parametrized matrix of 32 cases — guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 full cross/mutationFailed × five verbs, expected strings all hand-written literals; five transaction patch-mapping pins — single-quote dedupe / unquoted dedupe / simulated pnpm parse-explosion end-to-end / malformed-YAML false-committed → fail-closed / restore degradation does not throw, with the cross-package fixture premise pinned), typecheck clean; convergence anchors in main.jsx: `join(" | ")` 3→0, `mutationFailed` ≥ 6, and the `upgradeNotify` destructure removed along with its call sites; released after a four-round 15-finding plan review plus a hard-stopped two-round execution review (baseline independently re-run at 1530 for comparison + TDD red→green independently re-enacted); implementation plan in `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`.
705
-
706
- ### Refactor in 0.9.93 — post-install guard wire format converged onto InstallGuardError.toWire; adding a guard field now touches 8 → 2 points (pure refactor, zero wire-format semantic change)
707
-
708
- - **Need (2026-10-16 architecture walk, friction point 3, the walk's top pick)**: the `InstallGuardError` → wire-format projection was hand-written verbatim in 5 places (host-api 409 payload / tools install+upgrade arms / cli install+upgrade arms) plus one more assembly in the client — adding a guard field meant touching 8 points (class + 5 projections + client read + tests); the later addition of `repairBasis` had already demonstrated the recurrence once.
709
- - **Approach**: a source method `toWire(): InstallGuardWire` (interface exported alongside the class, plus a `GuardViolation` re-export) becomes the single source of the shared 6-field wire format; the five projections become one-line spreads (envelope differences stay per-end: host-api `ok/error`, tools `ok/guard/message`, cli bare JSON), and the "repairBasis key absent when undefined" semantics move into the method body. Adding a field now touches 2 points (class+toWire, and the client read — the browser bundle cannot import core modules, so its defensive assembly stays, honestly noted). Deliberate delta: the tools envelope swaps key order between kind/message (JSON key order carries no consumer semantics; no raw-JSON key-order pins exist across tests/).
710
- - **Verification**: full suite 1530 green (three new toWire pins: six fields / repairBasis key absence / key order; a new host-api dispatcher 409 pin closing the "host-api projection had zero test coverage" gap; tools-arm execute pins migrated zero), typecheck clean; the convergence anchor `repairBasis ? { repairBasis: … }` went 5 → 0 across the three files (also proving there is no 6th projection); plan review (two rounds, four findings) and execution review (two rounds, including A1 extracting the pin into its own describe) recorded in `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`.
711
-
712
- ### Fix + refactor in 0.9.92 — unified three-end market query intake (market-query single entry); CLI invalid-category silent swallow fixed
713
-
714
- - **Need (2026-10-16, reproduced live)**: `dshm search --category <invalid>` silently printed "no matching entries" and exited 0, while the same input gets a 400 from the panel API and an explicit error from the agent tool — breaking the "same source, same semantics across GUI / tools / CLI" promise. Root cause: the category slug regex, source/offset/limit parsing, and the curated label map each had per-end copies (regex ×3, parsing ×3, labels ×2, deadline constants ×2), and the CLI copy lacked category validation.
715
- - **Approach**: new module `src/core/market-query.ts` (GLOSSARY "Market Query Intake") is the single source of the shared grammar — one entry point `parseMarketQueryInput(raw, policy)` covers category (curated five ∪ community slug, validate-and-pass trimmed), source (three-value enum), limit (floor-first + per-end policy: GUI 96/32, tools/CLI 80/10), and offset; grammar violations throw `MarketQueryError` with byte-identical legacy messages, and each end maps to its own envelope (HTTP 400 / thrown tool error / CLI dispatch catch `错误:` + exit 1). Also single-sourced: the slug regex (including the catalog adapter's third copy), the curated label map (keyed off registry `CATEGORIES` with a drift-guard test), and the deadline constants; the CLI help category list now shows the current curated five (the old list was a pre-0.9.16 leftover). Deliberate deltas: (1) tools' in-range fractional limits were implicitly floored by core `clampLimit`; the floor now happens explicitly at intake (same values pointwise; output objects carry no limit field); (2) CLI category/source are now validated after trimming, matching host-api/tools (`' self-dev '` used to match nothing and `--source ' all '` used to error); (3) dual-violation requests now report category before source (single-violation messages byte-identical).
716
- - **Verification**: full suite 1526 green (18 new grammar-matrix cases + label-keys ≡ CATEGORIES drift guard + CLI invalid-category/invalid-source regression pins; existing "非法分类/非法 source" anchors migrated zero), typecheck clean; live `node lib/cli.js search --category '非法分类!!'` → stderr `错误:非法分类: 非法分类!!(需精选分类或 [a-z0-9]{1,32} slug)` + exit 1 (validation precedes any network call); plan, grill decisions, and the three-round 15-item independent review live in `docs/plans/2026-10-16-market-query-intake-implementation-plan.md`.
717
-
718
- ### Polish in 0.9.91 — settings three-card row 4:7:3 → 4:7:4: Community and About equal width
719
-
720
- - **Need (2026-10-16, live feedback)**: on wide/fullscreen the settings three-card row was asymmetric — Community (4fr) and About (3fr) had different widths.
721
- - **Approach**: three-column `grid-template-columns` 4fr 7fr 3fr → **4fr 7fr 4fr**; the two side cards are now equal width while Registry stays the visual anchor. The two-column default (container <1100px) and the ≤760px single column are unchanged.
722
- - **Verification**: typecheck clean, full suite 1506 green (source-anchor assertions updated to 4:7:4); accepted live (hot-deploy preview approved, shipped in this release).
723
-
724
- ### Performance in 0.9.90 — single-pass search normalization cold path: per-generation cold search ~330ms → 99ms p50 (last >100ms stall on the host event loop cleared) + CLI search positional fix
725
-
726
- - **Problem (2026-10-16 walk + review measurements)**: the first search of every materialized merged-market generation (ADR-0016) still blocked the DSH host event loop synchronously for ~324–364ms — `normalizeSearchText`'s per-char dual-regex classification plus a trailing full-scan regex replace (256.8ms over the full corpus; NFKC is only 13.6ms), plus three array allocations per field in `fieldText`. Triggered hourly by registry SWR generation churn (fetchedAt bumps unconditionally), after every host restart, and on the first search after each daily catalog bump — the agent `dshm_search` tool rides the same path. Also: `dshm search 主题` silently dropped the positional keyword (query=undefined → unranked browse page).
727
- - **Approach**: ① aggressive single-pass `normalizeSearchText` — ASCII lookup table + direct classification for four stable CJK core ranges (4E00–9FA5 / 3400–4DB5 / F900–FA6D / FA70–FAD9; Unicode assignment stability makes them independent of the runtime's Unicode version) + in-pass collapsing of non-word runs (the trailing full-scan replace retires) + a transparent astral word-pair rule; the 40 unassigned FA holes (U+FA6E/FA6F/FADA–FAFF) and version-sensitive edges always fall back to WORD_RE/CJK_RE (same predicate, same runtime — equivalence by construction); ② single-element fast path in `fieldText` (join([n])===n identity, no triple allocations); ③ CLI positional keywords now equal `--query` (flag wins, a valueless `--query` falls back leniently, help updated).
728
- - **Equivalence red line**: a 3,888-case golden differential corpus (stratified sampling + seeded fuzz with astral/NFKC/FA-hole/lone-surrogate pools + 11 handwritten quirk pins) shows **zero mismatch** — current behaviors such as 𝕏's NFKC→ASCII folding, FA-hole collapsing, lone-surrogate folding, and no-transition astral boundaries are preserved **byte-for-byte** (independently hardened by the reviewer: 839,295 differential cases, zero mismatch); mismatches are always fixed in the implementation, never in the fixture. The corpus generator and timing probe are committed (manual, not CI; mulberry32 determinism + numeric version comparison for catalog selection — same seed regenerates the same corpus).
729
- - **Verification**: full suite 1506 green, typecheck clean; measured (idle machine): pure function 256.8→**44.5ms** (5.8×); per-generation cold search 324–364 → **99.3ms p50** (3.3×; first JIT-cold run 120.2ms reported separately); E2E restart scenario ~500 → **348.6ms**, hourly scenario ~380 → **207.2ms** (builds delta=1); reviewer reproduction 91.5–96.2 / 45.1–45.8ms. The implementation plan converged through four adversarial review rounds (plan R1–R3 + execution R4); all four acceptance criteria passed.
730
-
731
- ### Polish in 0.9.89 — constant two-row head: full title kept, winctl size/row height independent of the breakpoint
732
-
733
- - **Need (2026-10-15, live feedback)**: the 720px two-row reflow shipped in 0.9.88 introduced two perceptible jumps — the title swapped between "插件市场" and "DeepSeek Harness 插件市场", and the winctl buttons changed size (28↔36px) while title/button vertical positions drifted 1–2px across the breakpoint.
734
- - **Approach**: ① the two-row state keeps the full title — with seg moved to its own row the first row has ample room (~300px used of 612px at 640 width), so the short-title swap retires (DOM spans and base hiding kept; nowrap+ellipsis covers extreme narrow); ② the winctl touch bump (36px) moved from the container breakpoint to `pointer:coarse` (button size follows input modality, not width: mouse always 28px, touch always 36px); ③ the winctl box pinned to 34px = seg row height (same 28+2-border formula), normalizing first-row height across both modes.
735
- - **Verification**: full suite 1496 green, typecheck clean; CDP measurement shows 0.0px vertical-center delta for title and buttons across the breakpoint (was -1~-2px); accepted on-device by the owner.
736
- - **Misc**: package-lock root version fields, unsynced since 0.9.68, now track the release version and join the release checklist.
737
-
738
- ### 0.9.88 — three-zone panel resize: size memory + grow-in-place (ADR-0021) + acceptance fixes
739
-
740
- - **Need (2026-10-15)**: a real gap between "cramped at 920px" and "overkill fullscreen", plus the instinctive desktop gesture of grabbing window edges. Design locked via two grill rounds: three handles (right/bottom/bottom-right), grow-in-place anchoring, full size memory; the implementation plan survived three independent adversarial review rounds (11 plan + 2 execution issues all closed, including two methodology findings: bundle-absence predictions are structurally false under `treeShaking:false`, and CJK bundle anchors are false under esbuild's default ascii charset).
741
- - **Approach (ADR-0021)**: ① DOM-free `window-resize.js` — `clampSize` (min 640×460, viewport−8px margins, an origin second clamp keeping right/bottom edges inside margins for the freely-positioned form, degraded bounds on tiny viewports) + size-memory storage symmetric to position memory; ② gesture state machine mirrors ADR-0017 (rAF-batched writes, zero setState in gesture frames, setPointerCapture, primary-button guard, multi-pointer guard, unmount cleanup); ③ hot zones 6/8px edges + 14×14 corner, cursor-only affordance, four exemptions (fullscreen / narrow viewport / touch / scrollbar trade-off); ④ reclamp upgraded to size-first-then-position.
742
- - **Acceptance fixes from live testing (same release)**: head two-row breakpoint 600→720 (min-width 640 no longer crushes the title); detail modal middle wrapped in `modalbody` (✕ and Install stay pinned however far the README expands); settings 3-column gate switched from `.full` to container ≥1100px (a widened restored panel now gets three columns too).
743
- - **Experiment record**: a tab-switch fade was implemented and rejected by the owner the same day, fully reverted (byte-identical, reviewer-verified).
744
- - **Verification**: 1496 tests green (+24), typecheck clean; CDP quantified matrix 14/14 cells passed (column thresholds 640→1/920→2/1140→3/1500→4, lower/upper clamping, dual reclamp on viewport shrink, 720 breakpoint, modalbody, coarse-pointer exemption); Performance CDP measurement and Windows shell smoke deferred per plan (the former backed by a code-level guarantee: zero setState in gesture frames). Honest acceptance record: dsh-workspace `01_docs/research/2026-10-15-dshm-panel-resize-sweep-record.md`.
745
-
746
- ### Performance in 0.9.87 — hot-path quartet: parallel installed enumeration, on-demand patchRows, batched favorites stale check, settings community summary via L2
747
-
748
- - **Problem (2026-10-15 architecture walk + independent audit)**: three recurring costs remained on the market-query hot path - (1) `listInstalledPlugins` read each dependency's two files (package.json + cordis.patch.yml) **strictly serially**, re-running in full on every query / page turn / debounced search commit (~11ms for 14 deps, nearly half the warm-cache query budget); (2) the favorites stale check fired **one full market query per favorite** (fan-out of 8): a single boolean answer paid the entire pipeline (enumeration + community + scoring 4,445 entries + sort + paging), 20 favorites ≈ 3 batches × full pipeline; the old path also had a curatedFirst squeeze-out false-stale edge - with ≥8 primary same-token hits the id-exact community entry got pushed past the page-8 window; (3) the settings-page registry method's `getCommunitySummary` ran an unmemoized full adapt+merge (~4,400 entries, ~10-50ms CPU per open) while the same-identity L2 materialized generation (ADR-0016) sat one function away.
749
- - **Fix (hot-path quartet)**: (1) parallel enumeration - per-dependency classification is independent, `Promise.all` folds in key order (output stays sorted by dependency key); new opt-out `{ patchRows: false }` spares the market path the ~0.55ms/plugin YAML CST parse (patchRows is only consumed by enablement/toggle, which keep the default; opting out **omits the field** instead of faking `readable:false`); (2) batch id-existence - new core `existingMarketIds` (three no's: no installed enumeration / no latest probing / no paging; raw-id Set membership over the merged domain, pointwise identical to the old search fan-out except the curatedFirst edge, where it is a **fix**), new host method `market-ids-exist` (ids ≤500, 400 on malformed), client sends one batch call and falls back to all-stale on error; (3) `getCommunitySummary` now routes through `mergedOutcome` (same identity skips adapt+merge; "summary never enters the generation" preserved; receives the whole `snap.loaded`, explicit namespace/profile defaults byte-identical to the pre-change cache paths - a deliberate parity-over-generation-sharing trade-off); (4) `toMarketItem`'s per-page linear scan became a once-per-query installed index (byPkg/byName/byGithub first-wins + min-ord, pointwise equal to `matchInstalledByEntry`, zero mismatches over a 640-entry random fuzz), with `GITHUB_SPEC_RE` exported as the single source of truth.
750
- - **Verification**: 1,472 tests green (+7: enumeration ordering pin / patchRows opt-out contrast / FP5 four-path match equivalence / four existingMarketIds cases incl. L2-hit and curatedFirst-edge pins / host method ok+400 / migrated C3 source anchor), typecheck clean; measured (conservative baseline): market-path enumeration 10.55ms → 2.08ms (5.1×), `listMarket` cache-only end-to-end median ~28ms → 5-7ms, `existingMarketIds` (20 ids) 2.52ms/call; both implementation plans aligned over multiple independent review rounds (FP1/FP5 two rounds, zero reservations; FP2/FP3+release two rounds, 7+1 findings all closed).
751
-
752
- ### Performance in 0.9.86 — packument accessor: registry metadata gets an owning module, register's network leg leaves the lock (steelman C2 / ADR-0019)
753
-
754
- - **Problem (steelman report, card C2)**: "registry metadata" had no owning module - Register fetched an 8MiB-scale full packument INSIDE the official file lock just to read one publish timestamp (npmjs first leg 5s timeout + effective-source leg up to 12s), putting mirror machines' white-wait directly on the install critical path; the B3 prewarm paid the same full fetch a second time through an uncached channel; three adapters each fetched, interpreted and cached (or not) independently.
755
- - **Fix (ADR-0019)**: new owning module `src/core/packument.ts` - one fetch derives a view bundle `{times, versions, distTags, readme, repo}`, cached under (pkg, effective registry) keys (TTL 5min / single-flight / cap=4 / failures and regex-invalids negative-cached for the same TTL / onRouteSwitch invalidation + registry-in-key double insurance). Register's timestamp fetch **leaves the lock**: publish timestamps are immutable historical facts (zero TOCTOU), the lock body is pure fs again (a cheap pre-lock existence read preserves the "local check first" pin). Precheck and register share single-flight; B3 prewarm uses `warmPackumentFor` (cache-bypassing + effective-source single-leg real request to warm the CDN + backfill); the readme fallback leg shares the same single-leg cached face (final-leg error passthrough preserved). The latest small endpoint and ADR-0006 stay out of scope; four functions + `extractGithubRepo` moved into the owner, versions.ts re-exports under the original names (zero import-site changes).
756
- - **Verification**: 1462 tests green (18 accessor cases added: two-leg timeout-tightening assertions / single-flight sentinel / negative cache / cap / route invalidation / warm single-leg bypass+backfill / Active-face negative-cache-as-miss / readme ladder three states with URL host anchors / in-lock zero-network timing assertion), typecheck clean; live smoke: accessor cold 143-269ms / warm 0ms / warm bypass real request 29ms, register real-wire (temp profile + genuinely young version) applied:true in 111ms + idempotent up-to-date in 3ms; the plan closed over three independent review rounds (13 findings) + two execution review rounds (4 suggestions).
757
-
758
- ### 0.9.85 Enhancement — version moves to the About links row + head self-upgrade enters the local ops pump (two real-device reports)
759
-
760
- - **① Version relocation (owner's call)**: 0.9.84 hung the plugin version on the "About" title-row sub (muted small text); after on-device comparison the owner decided to move it to the **links row tail** (GitHub · Issues · npm · v0.9.85) — grouped with the outward metadata, the title row stays clean. Missing version (ping failure → null) renders nothing, no dangling separator; new `.dshm-links-ver` muted style (label-secondary + tabular-nums). The no-upgrade-entry ruling stands.
761
- - **② Head self-upgrade log gap cured (owner's real-device report: ⬆ upgrades were absent from the operations log)**: the root cause fell between two ledgers — host-api `self-upgrade` explicitly passes `channel: 'panel'` (ADR-0018 invariant "panel channel writes zero external records", guarding against mislabelled external rows), while the head chip's `doUpgrade` called `api("self-upgrade")` directly, bypassing the local ops pump — so neither recorded it. Fix: the chip receives `ops` (the panel's ledger) and goes through `ops.runOp("upgrade", "dsh-m", …)`; the executor returns `opMeta {from,to}` so the ended list renders "升级 dsh-m vX→vY"; SSR/probes without `ops` fall back to the direct api call. `stillApplies` is semantically safe: dsh-m is always installed → always applies, no superseded. Family audit: install/upgrade/uninstall/toggle/registry-source/community-toggle and "Update all" (per-item runOp enqueue) all record correctly — self-upgrade was the only gap.
762
- - **Verification**: 1443 tests green (0.9.84 baseline 1440 + new source anchors: about-version relocation rewrite and render-smoke self-upgrade pump), typecheck clean; the F1-family readiness gate held stable across three consecutive full runs (one anomalous 657/42 run never reproduced — attributed to transient load on this shared box, logs archived).
763
-
764
- ### 0.9.84 Enhancement — plugin version shown on the Settings "About" title row
765
-
766
- - **Problem (owner's real-device feedback)**: since 0.9.83 the head version chip stays hidden in its idle state on narrow containers (consensus Q7 — phone head width goes to the restore/close buttons), leaving the plugin's own version nowhere to check (the settings page only had the community catalog's 目录版本).
767
- - **Fix (zero visual noise)**: the "About" card's title row reuses its existing `sub` slot (`.dshm-section-sub`, muted small text on the right) to show the bare `v{version}` — zero new rows/blocks/CSS/i18n; `pluginVersion` (ping single source of truth: mount / foreground-return / post-restart re-check) reaches SettingsTab as a prop, and a missing version (ping failure → null) leaves the slot naturally empty (Section's compat logic handles it — same "no placeholder on failure" semantics as the head chip); no upgrade entry here — the head ⬆ plus marketplace card/CLI keep that ownership (respecting the removed dsh-m self card ruling).
768
- - **Verification**: 1442 tests green (0.9.83 baseline 1440 + 2 new source-anchor groups in client-about-version.test.mjs), typecheck clean.
769
-
770
- ### 0.9.83 Feature — mobile adaptive layout: two-row panel head + fullscreen-by-default on narrow viewports
771
-
772
- - **Problem (user phone screenshot)**: `.dshm-head` is a single-line no-wrap flex whose minimum content width is ~440-470px, while a phone panel offers only 330-365px — the row-end overflow gets clipped by `.dshm-panel`'s `overflow:hidden`: the restore/close button group becomes invisible and untappable (closing the panel is left to backdrop taps only), the version chip is cut mid-glyph, the title wraps word-by-word into a tower, and the seg tabs stack their CJK labels one character per line. This is the third mobile issue surfaced via "user phone screenshot" (after 0.9.66 touch-swipe direction and 0.9.67 font blow-up).
773
- - **Fix (container-query two-row re-layout + fullscreen-by-default on narrow viewports, grilling consensus Q1-Q10 + 11 revisions from a 4-round independent review R1-R8/N1-N3)**: `.dshm-panel` gains `container-type:inline-size` (panel width/height are formula-derived and content-independent, so containment is safe; the lightbox portals to body and the sticky category row lives inside the scroll container — neither is affected), and `@container (max-width:600px)` re-lays the head into two rows — row 1 [short title ⋯spacer⋯ chip winctl], row 2 [three seg tabs stretched full-width at 36px touch height]; DOM order is untouched (pure order+wrap — source anchors, tab order and screen-reader order all stable); the title switches via dual spans (short 「插件市场」 on narrow, reusing the existing `title.panel` key); the version chip is hidden in its idle state and collapses to a ⬆ icon when outdated (tooltip and self-upgrade semantics intact), with hide/collapse rules narrowed by the dedicated `.dshm-verchip` class — the head's profile badge (non-web profiles) is outside the consensus and unaffected; new rules are inert on browsers without container-query support (conditional-rule-group equivalent fallback — byte-identical to the old layout); on viewports ≤600px the panel mounts fullscreen by default (checked once at mount, not followed on resize; the FS_KEY memory is ignored on narrow viewports — preventing "restore once, stay cramped forever"), while wide viewports stay byte-for-byte identical to today; overlay padding drops to 8px on narrow viewports. Scope red lines: ZoneChips, card grid, lightbox, settings tab, backdrop-close contract and the drag model are untouched.
774
- - **Verification**: 1440 tests green (0.9.82 baseline 1435 + 5 new source-anchor groups in client-mobile-head.test.mjs; two existing render-smoke assertions rewritten in sync — the warn class anchor and the `⬆ v…` text anchor, same precedent as 0.9.82's single retitled case), typecheck clean (`run-command-error-digest` F1-family timing cases are a pre-existing load-sensitive false-red, unrelated to this change — reproduced red under load on a clean HEAD too; this release also cures all four of them (F1/T5 abort/timeout, F1-R/A/C) with a readiness gate per execution-review E5: 3-instance concurrent stress 9/9 green, vs 6/6 red before); `node scripts/verify-panel-mobile.mjs` passes all 13 real-browser assertions (393×852 narrow + 1280×800 wide + the FS_KEY 2×2 matrix + both chip states: default fullscreen / winctl reachable / zero head overflow / title switching / 36px seg touch targets / chip hidden and warn collapsed / 8px restored overlay / wide-viewport FS_KEY regression); the verify script follows the verify-lightbox convention — manual, not in CI (local http origin + /dshm stub, zero external network); real-device recheck: the owner's phone.
775
-
776
- ### 0.9.82 Feature — per-row dismiss for failed external operation records: the red-dot deadlock is broken (ADR-0018 revision)
777
-
778
- - **Problem (red-dot deadlock + ops layer lagging behind the display layer)**: a failed external record (CLI / agent-tool channel) lights the settings-tab red dot but had no per-row dismiss — the 0.9.79 ruling withheld the ✕ because the server only exposed list/clear. The only way to clear the dot, "Clear ended", also wipes every finished record (successful history included) — behaviorally a permanent alert, and alert fatigue defeats the dot entirely. The display layer (channel badge, day grouping, counts) has been isomorphic with local records since 0.9.79; only the operations layer lagged.
779
- - **Fix (server per-id delete chain + client channel branching)**: core gains `removeExternalOperation(id)` (whole-file read-modify-write atomic replace, idempotent — silent no-op for missing ids, surviving records written back verbatim); host-api gains the `external-operations-remove` method (body.id string-validated with 400, auth identical to list/clear — zero new special cases); the store gains `removeExternal(id)` (overlay removal + a **tombstone** set `removedIds` that intercepts in-flight fetch responses from re-adding just-deleted rows, reset by `clearExternal`); `canRemoveRow` lets external `failed` rows through (done still has no ✕, precisely matching the panel's problem-status-only rule); `onOpsRemove` branches by channel — the local path is byte-for-byte unchanged (R0 red line), the external path **awaits the server delete before touching the overlay**, and on failure keeps the row and toasts with the new `op.remove.failed` key ({err} interpolation carries the server error, zh/en). No confirmation dialog (same as the panel ✕). ADR-0018 gains a revision note; the original ruling text is preserved.
780
- - **Verification**: 1435 tests green (0.9.80 baseline 1425 + 10 net new: journal storage 3, host-api method 4, store removeExternal+tombstone 2, canRemoveRow rewrite and wiring anchors 2, incl. one retitled existing case), typecheck clean; the implementation plan converged through three independent review-agent rounds (probe path / link-time error shape / tombstone backstop / assertion preservation / 401 convention / interpolation chain / rollback stance — 11 findings all closed).
781
-
782
- ### Fixes in 0.9.80 — installed-tab toggle jitter eliminated + community switch pinned to its title row + 已实测 badge retired (three UX fixes, client-only)
783
-
784
- - **① Installed-tab toggle jitter eliminated (user report: clicking the switch shook the whole page, the switch vanished in place, and the cursor no longer hit it)**: CDP measurement showed a 47px×2 card-grid shift per click plus a 0.6–1.3s hit-test loss window (the cursor's fixed point landed on the card above's `.dshm-links` external-links row — a mis-click opens a browser tab). Three root causes: `ProgressLine` (the install-only pnpm progress bar) mounted for toggles too (35px row + 12px body gap — inserting/unmounting it is the shift itself); the toast/restart banners were in-flow flex children of the panel column (mount/dismiss compressed the body by 39px×2); the switch visually flipped only after five refresh requests landed (a 1.5–2.3s "nothing happened" dead window, with the switch greyed to `locked`). Fixes: a new `progressTarget` gates ProgressLine to install/upgrade/self-upgrade (`activeMutateTarget` semantics untouched — it still drives per-row disabling); toasts/banners are wrapped in `.dshm-banner-stack` (absolute bottom overlay, pointer-events pass-through); optimistic `pendingToggles` flips the switch in 42ms (cleared in `finally` — converges on success, reverts to server state on failure); toggle refreshes the installed view only (installed+market×2+updates+external-operations → installed×1; `view-refresh.js` untouched, its unit tests unchanged); `.dshm-switch::before` grows the hit area 34×19 → 46×31 (visuals unchanged). CDP re-probe passes all four assertions: zero drift, zero hit-loss, 42ms flip, stable body bottom edge.
785
- - **② Community catalog switch pinned to its section title row (user report: the switch sat at different heights when on vs off)**: the switch row used to be the last child of the card body — toggling off dropped five metadata rows and yanked the switch up ~5 row heights. It now lives at the right edge of `.dshm-section-title` (flex already in place + the switch's own `margin-left:auto`; same interaction language as the installed-page card switches). CDP-measured identical Y in both states (135.2px, 0.0 drift); the redundant "社区目录开关" row is retired into the switch tooltip.
786
- - **③ 已实测 (verified) badge retired (plan B: byline gray text)**: verified covers only 4/21 curated entries — all the author's own plugins — making the badge an authorship proxy with near-zero discriminating power, while its real payload (the tested DSH runtime list) lived in a hover-only tooltip. Per the "card badges only carry exceptions (deprecated / internal / outdated)" principle it is retired from the market card, the favorite snapshot card, and the modal header; the market card tag line gains a low-emphasis `verified ≤{latest re-verified runtime}` suffix (full list on hover); the detail modal's "Verified runtimes" row remains the single authoritative presentation; the `badge.verified` i18n key is retired following the 0.9.76 version-independent-badge precedent. registry data, snapshotOf passthrough, and the install-time peer-compat dialog are untouched.
787
- - **Verification**: 1425 tests green (0.9.79 baseline 1423 + 2 retirement assertions; the U12 favorites-card contract rewritten for ③), typecheck clean; all three fixes are client-only — a page refresh applies them; ① and ② were quantified end-to-end with CDP probes (drift / hit-testing / flip latency / geometry stability).
788
-
789
- ### Feature in 0.9.79 — external operation records: non-panel channels (CLI / agent tools) land terminal-state audit entries into the panel's Ended list (ADR-0018)
790
-
791
- - **Problem (audit-surface disconnect, real incident)**: the global operation records only covered panel-initiated changes — the quota-watch 0.1.28 upgrade via `dshm_upgrade` (an agent tool) installed successfully yet left zero rows in the panel's Ended list. CLI and agent tools are separate processes and cannot write the browser-localStorage operations pump.
792
- - **Solution (ADR-0018: machine-local direct-write journal, decoupled from the pump)**: a new `$DSH_HOME/dshm/external-operations.json` (overridable via `DSHM_EXTERNAL_JOURNAL_FILE`; machine-local, never synced across machines) — the nine public mutation entries gain a `channel` parameter (`'panel'|'cli'|'host-tool'`, defaulting to `'external'` as the audit-safe fallback). Non-panel operations append one terminal-state record after completion: **both success and failure are recorded**, rolling cap of 200, tmp+rename atomic writes, corrupted files quarantined by rename, and any append failure swallowed by the hook (at most one stderr line — it never back-fires onto the operation result, R1-1); `reason` truncated to 300 chars. Desktop accounting (R1-2): desktopToggle passes through to togglePlugin (the only nested pair — one transaction, one record); desktopInstall/Uninstall/Upgrade self-record (call-graph audit confirms they bypass instrumented entries); the service-absent structured refusal self-records `desktop-toggle` failed.
793
- - **Panel consumption (in-memory overlay, never localStorage)**: host-api gains `external-operations` (list) and `external-operations-clear` methods (auth rides the existing host trust check); the Ended list = local operations pump (localStorage, untouched) + external-records overlay (the server side is the single source of truth, persist path untouched — prevents local resurrection after server-side eviction, R1-3); channel badges **Agent tool** / **CLI** / fallback **External** (Q2b/R1-4); failed external entries light the red dot (Q4); external rows render no per-row ✕ (R2-3); Clear ended also clears the server journal plus the overlay; fetch timing = panel init + after each operation + opening the operations card (no polling, R1-6); the Ended count now uses the merged total while the last-50 render cap stays (R1-7).
794
- - **Verification**: 1423 tests green (0.9.78 baseline 1389 + 34 added: journal storage 8 (rolling / corruption quarantine / panel rejection / no-credentials / path error propagation / truncation), channel wiring 8 (panel zero-record / cli and host-tool one each / nested single record / failure recorded as failed / injected append failure never back-fires), host-api 4 (list / clear / auth / override slots), client model+view 14 (overlay ascending merge / persist purity / idempotence / badge / ✕ exit / count / wiring anchors)), typecheck clean; tests never touch the real `~/.dsh` (10 existing test files gained journal isolation, plus 1 new channel-wiring test file, following the cacheRoot discipline). The implementation plan was closed over three independent review rounds; npm publish and dsh-m self-upgrade remain subject to separate user confirmation.
795
-
796
- ### Performance in 0.9.78 — favorites stale checks go cache-only: zero-network favorites access + zero-increment commit guard (steelman C3)
797
-
798
- - **Problem (steelman report, card C3)**: the favorites stale verdict only needs a "is this id still in the catalog" membership boolean, yet it pierces the rich market interface's probe segment - every favorites visit/toggle (after the 800ms debounce merge) still fires N favorites × full queries including a per-hit npm/github latest **network fan-out** whose products are entirely discarded. On top of that, the unconditional `setData` after refreshViews' four-in-a-row responses (the post-toggle norm) evaporates the reference stability the data layer carefully maintains.
799
- - **Fix**: `lookupLive` now passes `probeMode:"cache-only"` (ADR-0013 first-phase mode: zero network, zero probing; existence = items membership, semantics unchanged); `fetchPage` gains a **zero-increment commit guard** - a new `marketResponseEquivalent` pure function (recursive JSON-value comparison, ZoneChips setGeom discipline) skips `setData` when content is equivalent (dataRef/loading semantics untouched). The report's optional "client-side stale TTL cache" is explicitly not done: after C1 materialization, cache-only queries are already memory-grade.
800
- - **Verification**: 1389 tests green (execution baseline 1382 + 7 added), typecheck clean; live-instance differential smoke across 5 cases (npm primary / self / community entry / nonexistent id): id sets and totals identical between cache-only and full (the sufficient condition for unchanged red-dot semantics), cache-only ~30ms vs cold full ~250ms (the zero-probe fingerprint); the implementation plan was closed over two independent review rounds (F1-1 plan-baseline correction).
801
-
802
- ### Fix in 0.9.76 — three presentation-layer retirements and sticky-chips jitter fixed at the root: "Chat" badge / "Version-independent" badge retired + scroll-anchoring self-excitation cut on the category row
803
-
804
- - **① "Chat" source badge retired (owner screenshot: every row flagged despite all-UI clicks)**: `meta.session` is the **pump-ownership marker** from the 0.7.0 final review (restore skip / replaceAll no-rollback / stripped on persist) — both the GUI `runOp` path and the chat-mirror path set it unconditionally, so it cannot distinguish origins, and the badge vanishes entirely after a page reload. Fix: opRowVm drops the session derivation; the row badge, its CSS and i18n keys retire; `meta.session` keeps its day job (zero data-model change); a future `meta.origin` would be the honest carrier for source attribution.
805
- - **② "Version-independent" badge retired (market card + modal head)**: the 19-character English label squeezed the card's highest-value asset — the plugin name — into `D...`/`O...`, while the same fact already lived twice next to it (the card description text and the modal Compat row), leaving the card badge zero marginal information. Design rule settled: **card badge rows carry only information unavailable elsewhere and relevant to the decision at hand** (verified / installed / source); decoupled belongs to the detail layer, solely carried by the modal Compat row (`modal.decoupled.*`); `snapshotOf` keeps the data field (favorites/snapshot integrity); `badge.decoupled` and `badge.decoupled.tip` retire.
806
- - **③ Market-tab category-row high-frequency jitter fixed at the root (owner screenshot: wild bouncing after switching tabs back)**: Chrome scroll anchoring translated every stuck-toggle height change into a scrollTop compensation, shoving the sentinel (top:-5px inside the sticky wrap) in and out of the viewport → IO flipped stuck every frame → the sticky bar teleported between pinned and in-flow shapes. Trigger surface: conditional tab remount (stuck=false + re-armed IO) × `.dshm-body`'s preserved scrollTop landing in the ±30px sentinel boundary band (pagination `scrollIntoView` parks scrollTop right at the bar edge, raising the odds). Fix: one line — `.dshm-body` `overflow-anchor:none` — cuts the loop at its driver; sticky/sentinel/+N clip geometry untouched. Not a regression from this batch — the sticky/IO machinery dates to 0.9.52–0.9.59; a long-latent boundary bug.
807
- - **Verification**: 1379 tests all green (render-smoke gains retirement negative anchors plus anchoring-off / sticky-geometry contract anchors), typecheck clean; all three are presentation-layer fixes with zero data-model migration; client-only — a page refresh takes effect.
808
-
809
- ### Feature in 0.9.75 — operations log enrichment: inline clock/day groups/toggle direction/duration/upgrade versions/chat source (zero data migration)
810
-
811
- - **Background (owner screenshot: toggle records unreadable as on/off)**: each operations row carried only "status + kind + package" — a toggle showed no direction (two consecutive `toggle @iasiv5/dsh-quota-watch` rows in the screenshot were indistinguishable), no timestamp, no duration, no chat-vs-UI source, no upgrade from→to versions. Yet **all of this already exists per record** (`createdAt`/`updatedAt` are store bookkeeping, `meta.on` has been recorded with every toggle since 0.4.0, `meta.session` is the pump pickup marker) — a pure presentation gap, not missing data. Existing records in browser localStorage (`dshm-operations`) become readable right after upgrade, zero migration.
812
- - **Approach (all display derivation in one pure module)**: new `src/client/operations-view.js` (`fmtOpClock`/`fmtOpFull`/`opDayKey`/`fmtOpDuration`/`opUpgradeMeta`/`groupOpsByDay`/`opRowVm`, directly unit-tested, same convention as toggle-view): ① inline `HH:mm` (tabular-nums, full-to-second time in title); ② finished records **grouped by local day**, headers Today/Yesterday/YYYY-MM-DD, **newest group on top, newest-first within** — the latest row sits right under the "Ended" caption instead of sinking to the scroll bottom; ③ toggles render an `On` (green) / `Off` (grey) badge from `meta.on` — the 0.9.71 community-toggle-only direction rendering (`targetText`) retires into this generalized path; ④ terminal duration `<1s/42s/1m02s/3h05m` (translation-free); ⑤ upgrade rows show `v0.9.71→v0.9.72` (github source falls back to 7-char sha, consistent with notify.upgraded); ⑥ `meta.session` rows get a "Chat" source badge; ⑦ "Ended (N)" count (the count-less key `settings.ops.group.done` retires). Missing fields omit their segment — old records degrade to the previous row shape; 12px type-family discipline (0.9.67) kept, badges use `currentColor` borders to follow the theme.
813
- - **Data plumbing (the single non-presentation change)**: the pump's success path now merges an executor-returned `value.opMeta` into the record meta — the merge base is the **freshly read** meta at finalize time (the enqueue snapshot lacks the pickup-time session marker and would break the stale-snapshot no-rollback guarantee; caught in self-review); failures never merge (result untrustworthy). The restored path (`dispatchRestored`) now routes its success value through the same channel (`value = r`) — the production implementation returns `{ ok: true }` today, so existing flows are behavior-unchanged; both `doUpgrade` call sites pipe `fromVersion/version/sha` into `opMeta`.
814
- - **Verification**: 1375 tests all green (0.9.74 baseline 1339 + 36 new: `client-operations-view` 27 cases covering TZ-agnostic time formatting, group ordering, row-VM fallbacks, and negative anchors against the retired `targetText`/count-less key; 4 pump opMeta cases covering done/warned merge, failure & dirty-input rejection, restored-path merge, session preservation), typecheck clean; build artifact carries the new rules verbatim.
815
-
816
- ### Feature in 0.9.74 — screenshot-strip wheel hscroll: bare-wheel deltaY safely mapped to scrollLeft (steelman's three red lines)
817
-
818
- - **Background (owner feedback while verifying 0.9.73)**: the strip is a horizontal scroll container, and a mouse wheel over it scrolls the ancestor (the modal) by default — with many screenshots, mouse-only users must drag a ~700px thin scrollbar (continuous input on a discrete target); trackpad two-finger hscroll (deltaX) and Shift+wheel exist natively but are hidden knowledge.
819
- - **Decision (new pure predicate `shotWheelAction`, src/client/wheel-strip.js, Node-tested)**: steelman's three red lines — ① intercept only when scrollable: zero hijack while scrollWidth ≤ clientWidth (+1 sub-pixel tolerance, same rule as 0.9.72's useScrollableContain), so most entries (1-2 images) behave exactly as today — no relapse of the "static hijack expressing a dynamic precondition" wheel black-hole disease; ② release at the edge: zero action once the delta direction has no horizontal room left, handing vertical scroll back to the modal (borrow the scroll only while there is room); ③ never touch deltaX: |deltaX| ≥ |deltaY| (trackpad hscroll) passes through untouched. Non-finite inputs short-circuit safely.
820
- - **Wiring**: DetailModal attaches a manual `addEventListener("wheel", …, {passive:false})` (React's onWheel rides the document-level passive delegation where preventDefault is a no-op); deps=visible.length re-attach as the strip's existence changes (conditional render / async primary shots / broken-shot eviction); layout inputs are read per event — nothing cached. Lightbox, image chain, and keyboard navigation unchanged.
821
- - **Verification**: 1339 tests green (new client-wheel-strip, 7 cases: three red lines + tolerance edges + safe short-circuit + wiring anchors; the img-chain ① anchor widened to allow props prefixes), typecheck clean.
822
-
823
- ### Fix in 0.9.73 — detail screenshot strip unified tiles: inconsistent sizes and previews overlapping neighboring thumbnails (flex-shrink overflow fixed at the root)
824
-
825
- - **Root cause (owner screenshot: jagged thumbnails with images spilling over their left/right neighbors)**: two defects stacked — ① `.dsvm-shotbox` had no fixed geometry (box min 120×84 / img max 220×130) and followed each image's intrinsic aspect ratio, so 16:9, 4:3, portrait and small originals all rendered at their own size; ② the explicit `min-width:120px` overrode the flex automatic minimum size — with eight screenshots' natural total width (~1.7k px) far exceeding the modal content width (~688px), flex-shrink squeezed each box to its 120px floor while the inner `<img>` (a replaced element) kept its intrinsic width, and with `overflow:visible` on the box each image spilled out centered, covering ~44px of each neighbor — all before the scrollbar even appeared; the row height also jumped 84→130px when the tallest image loaded (CLS).
826
- - **Fix (unified tiles)**: boxes get `flex:0 0 auto` (geometry independent of image count/load state, never shrinks) + fixed 176×110 + `overflow:hidden` (belt-and-braces against spill); images switch to `width/height:100%` + `object-fit:cover` + `object-position:top center` (top-anchored so screenshot title bars survive; cropping only trims the bottom — portrait/square shots show their upper half, the full image lives in the lightbox); the row gains `scroll-snap-type:x proximity` and boxes `scroll-snap-align:start`. Lightbox, image chain, broken-shot eviction, and keyboard navigation semantics unchanged.
827
- - **Verification**: 1332 tests green (new screenshot-strip tile CSS source anchors, 2 cases: unified-tile shape + negative anchors on the retired defect forms), typecheck clean; the built bundle carries the new rules verbatim. A standalone comparison mock (current-bug reproduction + cover/contain/grid variants) was reviewed by the owner beforehand (variant A: cover chosen).
828
-
829
- ### Fix in 0.9.72 — settings-page wheel black hole: ops scroll region and banner now apply contain only when actually scrollable (exposed by Chrome 144 spec alignment)
830
-
831
- - **Root cause (owner screenshot: with few operation records and no scrollbar yet, wheeling over the scroll region does nothing)**: `.dshm-ops-scroll` carried a permanent `overscroll-behavior:contain` (introduced with the 220px region in 0.9.64). `overflow-y:auto` makes the element a scroll container no matter how little content it has, and per spec a scroll container without scrollable overflow is always at its scroll boundary — with contain forbidding chaining, the whole area swallows the wheel. Chrome 144's "Respect overscroll-behavior on non-scrollable scroll containers" made engines honor this unconditionally (previously contain only applied when actually scrollable), activating the latent defect; the banner text (`.dshm-banner-text`, same permanent contain from the capped-banner era) shares the disease.
832
- - **Fix (intent made precise: block chaining only when scrollable)**: new `useScrollableContain(dep)` — attaches `.is-scrollable` (= contain) only when `scrollHeight > clientHeight` (+1 for sub-pixel rounding), with the container and first child in a ResizeObserver so wrap/note changes re-check, and a safe skip when ResizeObserver is absent (SSR/Node); wired into the ops scroll region, restart banner, and toast. Steelman verdict: every 0.9.64 decision stands (capped region / 50-record cap / pinned clear button / contain's own nested-scroll hygiene); the only flaw was a static CSS fact expressing a dynamic precondition — short logs now hand the wheel back to the page, long logs keep boundary containment.
833
- - **Also**: the scroll region gains `tabIndex=0` + aria-label (keyboard-reachability discipline from 0.9.45 U8); the layout contract is untouched; the layout mock is synced.
834
- - **Verification**: 1330 tests green (new client-scroll-contain structural anchors, 4 cases), typecheck clean.
835
-
836
- ### Feature in 0.9.71 — settings-class operation records: community catalog toggle and registry source changes are now logged (DESIGN §2.6 revision)
837
-
838
- - **Background**: changes made via the settings page's community catalog toggle and the registry source actions (apply / restore default) never appeared in the operations panel, leaving no answer to "when was it changed, and from what" — even though the agent-facing supply surface (`dshm_search` scope: ~20 curated vs 4,000+ community entries) and the trust root (custom registry source) hang exactly on these two switches.
839
- - **Design (DESIGN §2.6 revision)**: the operations-log boundary expands from package-level transactions to **supply-scope / trust-boundary settings changes**, adding two **terminal-at-creation** record kinds (never enqueued, no queued lifecycle, no restore replay; restore-passthrough / clear / red-dot semantics work unchanged): `community-toggle` records both success and failure (failure = an accepted mutation attempt rolled back mid-flight — the "thought it was off but it wasn't" risk; meta.on), `registry-url` records success only (validation rejections change nothing and surface inline in the card; logging every typo would only manufacture red-dot noise; target = new address, restore default logged as `(default)`, meta.from = previous address). Row display derivation: the toggle shows On/Off from meta.on; the registry source keeps the URL.
840
- - **Scope**: panel-initiated changes only (CLI, `dsh plugin add`, and DSH settings-form direct writes leave no record — consistent with the existing package-operations scope).
841
- - **Review**: independent subagent blind review found 0 blockers / 0 majors; all 4 minors + 3 nits fixed in this release (done-record placement now hugs api success, removing an implicit reliance on the refresh chain's error swallowing; a counting stub pins "terminal records never consult the restore predicate"; `(default)` converged into a shared constant).
842
- - **Verification**: 1326 tests green (new terminal-at-creation describe block with 3 cases), typecheck clean.
843
-
844
- ### Fix in 0.9.70 — drag appearance matches the resting state: drag-time solidification retired (ADR-0017 v2)
845
-
846
- - **Root cause (user side-by-side capture in light mode)**: the 0.9.69 drag-time "computed-value solidification" made the panel jump between solid and glass at grab/release - jarring in light mode. Of the two goals (no see-through, visual continuity), the solid approach sacrificed continuity.
847
- - **Fix (ADR-0017 decision 3 revised to v2)**: the drag state now shares the resting state's exact glass and blur - pixel-identical appearance. See-through protection returns to the resting invariant (frosted blur holds the background; the disease class was "blur off while still translucent" - leave both untouched and it cannot occur, exactly how openbmc's resting readability is already held). Drag affordance reduces to the grabbing cursor and a deeper shadow; `solidifyColor` / `--dshm-drag-bg` / drag-time `!important` are all retired (inventory back to a pinned 4, with a no-residue negative anchor in `client-lightbox`). The performance toolbox (pointer capture / gesture-scoped listeners / rAF-frame writes / translate3d / will-change) is fully retained; the restored per-frame backdrop re-filter is carried by GPU compositing - revisit only if weak-GPU jank is reported.
848
- - **Verification**: 1323 tests green, typecheck clean; ADR-0017 revised to v2 (Considered Options overruling recorded).
849
-
850
- ### Feature in 0.9.69 — drag the marketplace panel by its header in restore state, with position memory (ADR-0017)
851
-
852
- - **Design**: hold the panel header (title/tab row) in restore state to drag the marketplace panel freely - the whole header is the handle while tab / fullscreen / close buttons keep their click semantics (`closest("button")` gate + multi-pointer re-entry guard); double-click on a non-button header area toggles fullscreen; on release the position is baked into absolute viewport coordinates and stored under `dshm-panel-pos`, restored on next open - with no stored position the first render stays byte-identical to the old flex-centered layout. The motion range is clamped 8px on all sides; the desktop profile additionally yields the shell titlebar drag band (quota-watch contract: `data-windows-titlebar` + `--dsh-windows-titlebar-height`; web without the variable is always 0) - the capsule-era trap of parking inside the system button zone and never being grabbable again is eliminated by construction. Mount, fullscreen round-trips and window resize all re-clamp; the restore-commit re-clamp is ordered by render-phase latest-ref mirrors (execution review E1-1, reproduced, fixed, and pinned by structural anchors).
853
- - **Skin immunity (drag = fastest render path)**: the drag background is captured at gesture start from `getComputedStyle` with alpha forced to 1 into `--dshm-drag-bg`, self-held by `!important` - on glass skins (openbmc/uefi, bg-base alpha 0.55 disease class - the same case that made the quota-watch capsule translucent while dragged) the panel stays fully opaque with zero see-through. backdrop-filter is disabled for the gesture, so each frame is a single compositor translation: pointer capture + document-level listeners only during the gesture + pointermove stores the point only (styles written inside rAF) + drag frames never touch React state. Resting glass looks, the 0.9.27 mis-close guard, lightbox and detail-modal semantics are all untouched.
854
- - **Verification**: 1326 tests green, typecheck clean; bundle carries literal anchors `dshm-panel--dragging` / `dshm-panel-pos`; the `!important` inventory is length-pinned 4→7 (registered in client-lightbox assertions). Plan and execution were adversarially reviewed by an independent reviewer agent across five rounds - 16 findings, all closed (13 fixed in the plan, 3 fixed in code / honestly declared).
855
-
856
- ### Performance in 0.9.68 — materialized merged market: skip the 5MB parse + adapt + merge for repeat identities (ADR-0016)
857
-
858
- - **Problem (flagship item of the performance architecture review, closed over three review rounds)**: the domain concept "Merged Market" existed in the GLOSSARY without an owning module - assembly logic lived inline in `communityOutcome`, so every market request (including fully cache-hit fast paths) re-paid a 5.36MB community catalog disk read + JSON.parse (measured 30-55ms) → adapter rebuild of 4,400 entries (30-90ms) → merge + sort (5-35ms). All of it is **synchronous JS blocking the host event loop** - every page turn / committed search froze the host's SSE streams and tool calls for 65-400ms. Search scoring added ~330ms cold because the normalization WeakMap never survived across requests.
859
- - **Two-level memo, each with a naturally safe key (ADR-0016 decision 1)**: L1 - community.ts `readCache` memoizes the parsed catalog keyed by the immutable version-pinned body filename (cap=2; meta re-read every time so `checkedAt` stays fresh; failure paths never enter the memo); L2 - new module `src/core/merged-market.ts` materializes adapt→merge→match-index (cap=4), keyed by identity = (namespace, profile, registry identity, catalog version+pin+flag).
860
- - **Probe-driven generations + synchronous critical section (decision 3)**: every request still walks the cheap fast path (registry ~10KB + meta 141B ≈1-2ms) and probes the store with the returned identity fields; all awaits happen before key derivation and the critical section (derive→check→adapt→merge→index→set) is fully synchronous - concurrent same-key callers naturally build once, asserted by a `builds` counter sentinel. **Summaries never enter the generation**: they are computed per call from the current state (status/checkedAt/errors always fresh; zero drift across SWR and the ⑫ same-version short-circuit). deadline/unavailable/disabled/task=null pass through without landing; communityTask rejections propagate (install-side narrow catches swallow them into a miss).
861
- - **Dual indexes preserve install semantics (R2-3)**: the merged-domain `lookupInstalled` serves read paths; the full-domain `findById`/`lookupInstalledAll` ([primary…, full adapt set] including displaced entries) serves the three install/upgrade sites (resolveRegistryEntry / upgradePlugin / profile-ops desktop upgrade) - id-displaced entries remain installable and upgradable, byte-for-byte semantics. Primary-first short-circuits preserved (community failures never block primary hits).
862
- - **Cycle dissolved + score-once**: six merge-layer symbols moved from market.ts into merged-market.ts, the 23 category labels moved from community.ts (re-exported in place so cli/tools stay untouched) - the market↔community import cycle is broken in all three directions; listMarket's scoring is single-pass (category short-circuit first, rank reads the table) with tie-breaks byte-identical to 0.9.67.
863
- - **Zero behavior change pledge**: ADR-0003/0006/0008/0012/0013 untouched; a golden equivalence baseline (seven capture dimensions incl. paging/search/sort/probe badges) stayed byte-identical throughout.
864
- - **Verification**: 1305 tests green, typecheck clean; measured on this machine against the real 4,460-entry catalog: first call 319ms → second same-identity call 65ms; search scoring 330ms cold → 8ms warm within a generation (40×). Implementation closed through three independent review rounds (16 plan-review + 7 execution-review findings, all "fixed and re-verified"; including one silent no-op fix caught by the reviewer's `git diff` discipline and corrected with a real diff). Implementation plan and ADR-0016 v3 on record (`docs/plans/`, `docs/adr/`).
865
-
866
- ### Fixed in 0.9.67 — operations card font blows up on mobile: join the 12px family + wrap self-consistency
867
-
868
- - **Root cause (user phone screenshot)**: the operations rows `.dsvm-oprow` never declared a `font-size` — invisible on the desktop host base size, but on mobile browsers (base ≈15-16px) the whole row rendered nearly twice the size of the settings cards around it, clashing with the 12px family (`dsvm-kv` config rows, `dshm-hint`, `dsvm-fold`) on the same screen.
869
- - **Fix (aligned with surrounding card conventions)**: `.dsvm-oprow` explicitly joins the family at `font-size:12px`; the status word (done/failed) keeps its 11px and error/warning notes keep 11px secondary hierarchy; `flex-wrap:wrap` lets long targets (scoped npm names) and error notes wrap naturally instead of squeezing; the per-row remove ✕ pins to the line end (`margin-left:auto`), matching row conventions elsewhere. Group captions (11px), the 220px scroll region and the empty-state hint are untouched.
870
- - **Verification**: 1289 tests green, typecheck clean; the built bundle carries the new rule verbatim.
871
-
872
- ### Fixed in 0.9.66 — touch swipe direction inverted vs mobile convention (user report from a real phone)
873
-
874
- - **Root cause**: 0.9.65 anchored the swipe mapping on the keyboard cursor metaphor (`ArrowRight=next` ⇒ finger-right = next image), while touch follows the **content-drag** metaphor - the finger drags the filmstrip, so **swiping left reveals the image on the right = next** (photo libraries, chat apps, every carousel). `swipeDir`'s finger-direction result was fed straight into `lbStep`, producing "swipe left → previous, swipe right → next" on real devices - inverted against all muscle memory.
875
- - **Fix**: the call site flips the mapping (`lbStep(index, -dir, …)`); `swipeDir` semantics unchanged (still reports the finger direction), now documented as "the return value is the finger direction, not the page direction - callers must negate". Keyboard ←→ untouched (desktop cursor convention: ←=previous, →=next - the two metaphors each hold in their own domain).
876
- - **Probe fixes in the same batch**: `verify-lightbox.mjs` A31a/A31b rewritten to the correct convention (the old assertion going red after the fix is direct proof the direction flipped); probe numbering collision fixed - the pre-existing Primary scenarios keep A26-A29, the new-feature scenarios become A30 (prefetch warm-hit) / A31 (swipe) / A32 (focus trap); A19 rewritten as a timing-deterministic double-fail placeholder scenario (byproduct of this verification round: the suspected regression was triply disproven - ablation probe, worktree bisect, deterministic timing - the prefetch had legitimately pre-warmed the scenario's premise image; forensics live in the probe comments).
877
- - **Verification**: 1289 tests green, typecheck clean; browser probe 50/50 (A31 five assertions incl. the flip proof + A32 six trap assertions + A30 prefetch warm-hit + A19/A20-A25 legacy anchors all held).
878
-
879
- ### Added in 0.9.65 — lightbox: neighbor prefetch + touch swipe navigation + focus trap
880
-
881
- - **Neighbor prefetch**: once the current image settles (`useImgChain` settled), the left/right neighbors are warmed silently - using a `weservUrl(w=1600)` URL **byte-identical** to the lightbox's real request, so a swipe/arrow navigation hits the browser cache with near-zero wait. Strictly best-effort: no winner-memory or chain-state mutation, a failed prefetch leaves the real chain untouched (navigation still runs the weserv→original dual fallback); a `prefetchedRef` Set dedupes repeated crossings (no re-issuing when bouncing back), the Set lives for the lightbox mount lifetime (a fresh open warms again); single-image boxes short-circuit naturally via `lbNeighbors` returning an empty list.
882
- - **Touch swipe navigation**: a horizontal touch drag past the threshold flips images (`swipeDir` pure predicate: |dx| ≥ 48px and |dx| ≥ 2|dy|; no time gate - slow deliberate drags count; vertical/diagonal drags return 0 - vertical gestures and pinch-zoom accessibility stay native, via `touch-action:pan-y pinch-zoom`). Only `pointerType==="touch"` is recognized - mouse drags keep their native semantics (image = native drag, backdrop = press+click to close, the 0.9.27 drag-misfire guard intact); for 500ms after a recognized swipe the root's capture phase swallows the synthetic click (double belt: a swipe ending on ✕ or the backdrop triggers neither).
883
- - **Focus trap**: while the lightbox is open, Tab/Shift+Tab cycles only among focusable controls inside it (buttons/links: ✕/‹›/failed-state retry + open-original); if focus has escaped (e.g. a backdrop click left it on body), the next Tab pulls it back to the first/last control. After the portal-to-body change, Tab used to walk into the background panel - this closes that gap; DetailModal's own U8 debt (no trap) is unchanged and Esc still closes only the topmost layer.
884
- - **Verification**: 1289 tests green, typecheck clean; `client-lightbox.test.mjs` gains 6 items (`lbNeighbors`/`swipeDir` pure-logic units + prefetch/swipe/trap/touch-action structural anchors); all 0.9.60/0.9.62-era anchors retained.
885
-
886
- ### Changed in 0.9.64 — settings page: dual-catalog side-by-side layout + operations history moved into the settings tab
887
-
888
- - **Settings grid (mock-driven final review)**: the Community and Curated cards now sit side by side at **4:7** (the read-only community card needs ~250px; width goes to the more interactive registry card); in fullscreen the About card joins a three-card row (`4fr 7fr 3fr`, uncapped — the same "add columns, don't stretch units" philosophy as the market tab); below a 760px viewport it falls back to a single column; About spans full width in normal mode, the operations card is always full width.
889
- - **Community card, clean list**: the title row is a pure title again (`from` prefix dropped); the status badge becomes a small green/amber/red dot; hairline row separators + right-aligned tabular figures; the catalog toggle joins the list as its last row (the toggle is an attribute of the catalog — pinning it to the card bottom detached it from context); off state = hint line + toggle row (the re-entry control is preserved). The intermediate "stat band" design (boxed big numbers) was rejected in live review — cards inside a card are visual noise.
890
- - **Curated card trim**: the note line is shortened (the removed clause duplicated the download button tooltip — zero information loss); long configured URLs render as a single ellipsized line with the full value on hover.
891
- - **Operations history migration (batch 2)**: the bottom strip shared by all three tabs is retired; `OperationsPanel` becomes `OperationsCard` at the bottom of the settings tab — active rows stay outside the scroll area (progress never scrolls away), finished records live in a 220px scroll region (display cap 50 to bound the DOM; store/restore validation untouched), "Clear finished" is pinned below, and an empty-state hint card doubles as wayfinding.
892
- - **Visibility compensation**: with the bottom strip gone, failed/warned records would go unnoticed — the settings tab now shows a red dot (reuses the outdated-dot style; clears on remove/clear); the market card's "running" badge now switches to the settings tab and scrolls to the operations card (label 查看操作面板 → 查看操作记录).
893
- - **Verification**: build verify passes, typecheck clean, full 1283-test suite green in 4 consecutive runs (one transient unrelated flake in the first run did not reproduce); the final layout was settled via an interactive HTML mock across three widths × three layouts × multiple states (`docs/settings-layout-mock.html` committed as a design record).
894
-
895
- ### Fixed in 0.9.63 — clicking dead-center on the loading spinner no longer no-ops: spinner gets an explicit onClick=onClose
896
-
897
- - **Background**: 0.9.62 added the chain-loading spinner, but the spinner is a **child** of the backdrop root, and `backdropCloseHandlers` (0.9.27 drag-misfire guard) closes only when both `mousedown` and `click` land on the backdrop itself (`target === currentTarget`) - so a click dead-center on the 34px spinner did nothing. The spinner now carries `onClick: onClose`, same semantics as click-image-to-close: during loading, any click is a valid escape hatch.
898
- - **Verification**: the spinner anchor in `client-lightbox.test.mjs` follows the new signature (gating + onClick semantics); full suite/typecheck as usual.
899
-
900
- ### Fixed in 0.9.62 — lightbox controls invisible in light color schemes: self-owned cinema backdrop + dark glass control chips + loading spinner/fade
901
-
902
- - **Root cause (user photos, both color schemes, 2026-10-09)**: the dsh-skins ADR-0007 overlay-frost union targets `[role="dialog"][aria-modal="true"]` - which matches the lightbox root (it carries dialog semantics). In light schemes the 86% warm-white tint + blur14 turned the cinema-black backdrop into light frosted glass, while ‹›/✕ reuse the base `.dsvm-btn` translucent-white recipe (`rgba(255,255,255,.14)` fill, white glyph/border) - invisible on a light surface. Dark schemes hit the skin's `rgb(18,18,26)` branch and stayed readable, hence "light mode unreadable, dark mode okay". Vanilla installs (backdrop always near-black) never showed it - which is how it slipped past 0.9.61.
903
- - **Two-sided fix**: ① dsh-m side (this release): the lightbox `background` is now `!important`-owned - "dark backdrop + light controls" is the component's own readability invariant for a cinema surface and must not flip with color schemes or skins; marketplace community skins are un-enumerable, so the component must defend itself (the `!important` inventory is pinned at a fixed length of 4 by the regression gate; additions must register explicitly). ② dsh-skins side: all three skins exempt `.dsvm-lightbox` via `:not(...)` (1.5.1, ADR-0007 amendment) - semantically a "dialog frost" never should have tinted an image theater.
904
- - **Dark glass control chips**: ‹›/✕ move from bare translucent white to a chip in the same family as the bottom pill (slate-900 55% + white border + blur8 + shadow) - on dark backdrops the border/blur/shadow provide edge definition, on light backdrops the chip itself is the contrast surface; readable under any scheme or skin. Explicit `:hover` brightening (specificity 0,3,0 beats base `.dsvm-btn:hover` 0,2,0 regardless of source order - the R1 family lesson).
905
- - **Loading indicator & fade (UX)**: while the image chain runs (slow weserv / 8s guard tier-swap / large direct original) the lightbox used to be a silent black void; it now shows a spinner (`useImgChain` exposes `settled`). The image fades in over 160ms on onLoad; under `prefers-reduced-motion` the fade is disabled and the spinner slows down. Known nit: the spinner is a child of the backdrop and `backdropCloseHandlers` only honors the root element (drag-misfire guard), so a click dead-center on the 34px spinner does nothing (the surrounding backdrop still closes) - `onClick` added in 0.9.63.
906
- - **Verification**: 1283 tests green, typecheck clean; `client-lightbox.test.mjs` gains 6 structural anchors (backdrop `!important` fixed-length inventory / chip rules with explicit hover / spinner gating / fade class / settled exposure / reduced-motion); all 0.9.60-era anchors (portal / compound selectors / titlebar offset / focus restore / index keys) retained.
907
-
908
- ### Fixed in 0.9.61 — screenshots/icons unreachable from mainland browsers: weserv-first dual-fallback image chain
909
-
910
- - **Root cause (measured 2026-10-09)**: screenshots (raw.githubusercontent.com) and icons (github.com avatars) load browser-direct - fine from the server (200/0.2s), intermittently dead from mainland browsers. That day's failure sample dsh-wallpaper-engine (5 images, 22.5MB incl. a 10.4MB GIF) rendered an empty strip and a black lightbox; 0.9.60 ruled out a lightbox-v2 regression, and "working" icons were the local letter fallback. Benchmark dsh-market 1.66.14: thumbnails unconditionally via weserv (their in-source mainland measurement: 1.39s/23KB vs 41KB original) but the lightbox loads direct with zero fallback - the same mainland black hole.
911
- - **Image chain (ADR-0014)**: three consumers (thumbnail h=300 / lightbox w=1600 / icon h=96) share `useImgChain` - weserv tier first (`fit=inside&we=1&output=webp&q=80` server-side resize/recode), on failure or an 8s guard (that tier only; the direct tier gets no artificial timeout so slow-but-legal downloads are never killed) fall back to the original URL, then to per-consumer finals: thumbnails dropped (whole strip hidden when all fail), lightbox placeholder "failed + retry + open original", icon letter fallback. Winner memory per service bucket (raw/avatar) remembers the last successful tier, and **each chain snapshots the order at start** - sibling images racing in the same batch never skip a tier because a preference flipped mid-flight (found in execution: thumbnail 1 succeeding via direct flipped the preference and killed thumbnails 2/3); advancement is order-aware (with direct preferred, a direct failure still tries weserv - a fixed ladder would skip the fallback entirely). All four shots references switch to visible (strip gating / strip map / lightbox gating / keyboard nav + effect deps), the strip keeps a full-list map with stable `${i}:${src}` keys, and the lightbox index is clamped against shrinkage.
912
- - **Supporting**: pure logic in `src/client/img-chain.js` (URL builder / bucket classifier / order-aware tier machine / winner memory; `WESERV_BASE` single constant - a future setting changes one line); one GLOSSARY term (Image Chain); probe extended A16-A24 (direct-blocked / weserv-blocked / dual-block placeholder+retry / weserv-hang 8s guard / winner memory / strip removal / direct-first Probe scenario) plus `--live` real-network compression; three probe-harness determinism pillars learned during execution: **a fresh page (fresh context) per scenario** (kills route-remount races and cross-scenario decoded-image reuse), generation URLs (`?g=N` - any URL that must fail is unique for the page lifetime), and tier-order assertions via reqLog (`page.on('request')` survives route interception quirks).
913
- - **Verification**: 1256 tests green (1236 baseline + 20 new), typecheck clean; probe 31/31 stable across three runs; `--live` compression: mascot-drawer.png direct 2859KB vs weserv 151KB (~18.9x). Review absorption round (full multi-round independent subagent review): R1-1 [blocker] the 8s guard survived a successful load - a displayed image was force-advanced 8s later (mainland: thumbnails wrongly dropped, icons degraded, lightbox mutating into the placeholder); fixed with settled semantics (set by onLoad, reset on chain start/navigation, four-condition timer guard incl. empty url), pinned by new probe A25 (dwell >8.5s; the old code cannot pass it) plus timer-gating source anchors. R1-2 A22 gains the negative assertion (zero weserv-form requests after navigation) and A24 asserts the first request's form (winner-memory flip now falsifiable). R1-3 freshState clears reqLog (scenario closure). R1-5 url change resets via render-phase derived state (the one-frame ghost request is gone). R1-6 --live validates r.ok + image/* on both fetches. R2-3 defensive settled reset in advance. Round-2 verdict: consensus approve. Deferred: a behavioral probe for the middle-frame elimination (currently pinned by source anchors).
914
-
915
- ### Fixed in 0.9.60 — screenshot lightbox unusable when windowed: portal out of the panel containing block + always-visible controls
916
-
917
- - **Root cause**: the lightbox was a `position:fixed` descendant of `.dshm-panel` (`backdrop-filter` + `overflow:hidden`), so the panel box became its containing block. The image sized in viewport units (old 94vw/80vh) inevitably overflows the ≤680px-tall windowed panel and the in-flow ‹›/dots row gets clipped off-screen, with no close button at all; fullscreen only worked because the panel then equals the viewport (the reported "nav buttons appear only in fullscreen; windowed fills the small screen and cannot exit").
918
- - **v2 fix (`b89af18`)**: ① portal the lightbox to `document.body` — it escapes the containing block and is positioned against the real viewport in every panel state (SSR/no-document falls back to the plain tree so the smoke test still renders it); ② every control is absolutely positioned to the edges and can never be pushed off-screen by an image — persistent top-right ✕, large side ‹ › arrows, a bottom counter+dots+hint pill, and the image capped at 74vh; ③ four ways out: click the image (zoom-out cursor), click the backdrop, Esc, or ✕ — keyboard ←→/Esc semantics unchanged (DetailModal document-level listeners are unaffected by the portal); ④ single-image shots hide ‹›/counter/dots (no dead controls), dot hit areas grow to 18px via `background-clip:content-box`, and the close button is focused on open (same pattern as DetailModal U8).
919
- - **Supporting changes**: the ring step is extracted as the pure `lbStep` helper (`lightbox.js`, shared by the component and the keyboard handler; unit-tested for wrap-around, single image and non-finite input); new `client-lightbox.test.mjs` structural guards (portal/✕/click-image-close/single-image/CSS absolutes/inline-modulo retired); SSR smoke extended with multi- and single-image lightbox assertions.
920
- - **Review absorption round (independent subagent review, landed pre-release)**: R1 CSS cascade — the base `.dsvm-btn` rule sits later in source order and measurably overrode the ✕/‹› `padding:0/font-size:26px/radius:10px` (arrows actually rendered a 13px glyph); switched to compound selectors `.dsvm-btn.dsvm-lb*` (specificity 0,2,0, order-immune). R2 Windows Desktop drag band — the lightbox now yields the top band via `top:var(--dsh-windows-titlebar-height,0px)` (the 0.9.4 precedent; web falls back to 0px with zero drift) so ✕ never lands in the shell's `-webkit-app-region:drag` strip that swallows clicks. R3 focus restore — closing the lightbox returns focus to the triggering screenshot thumbnail (capture effect declared before the focus-✕ effect; `isConnected` guard). R4 lightbox dots and the thumbnail strip now key by index (duplicate URLs no longer collide). R5 the headless probe is persisted as `scripts/verify-lightbox.mjs` (manual run, needs playwright-core); structural assertions converged to includes-level anchors (verbatim regex red lights mean "text changed", not "behavior changed"); SSR smoke gains `role=dialog/aria-modal` and duplicate-URL dots assertions.
921
- - **Verification**: 1236 tests pass and typecheck is clean; `scripts/verify-lightbox.mjs` reproducing the real `.dshm-overlay > .dshm-panel` context passed 19/19 assertions — a fixed probe inside the panel equals the panel box (containing block proven), the lightbox box equals the viewport, ✕/‹›/pill stay within the viewport at all times, **computed cascade styles correct (R1 regression gate)**, ›/dot/‹/←→ navigation plus Esc/click-image close behave correctly, **focus restored to the thumbnail on close (R3)**, **40px titlebar band avoided (R2)**, fullscreen regresses nothing, and controls remain present in a 500px-tall window (always clipped before).
922
-
923
- ### Improved in 0.9.59 — 8px category-row inset and a derived visibility for the no-op collapse toggle
924
-
925
- - **Category-row inset (`f42e5be`)**: the category chip row sat flush with the search box and card grid, and small pills hugging large elements felt cramped; the row now starts 8px further right for breathing room. Only the category row (ZoneChips) moves — the zone chip row above stays put. `offsetLeft` and absolute positioning share the `.dshm-chips` padding-box origin, so `+N` tracking, the pinned Filter trigger and `offsetTop`-based clip geometry are all unaffected.
926
- - **No-op `⌃` fix (`7cbdf11`)**: at fullscreen width all category chips can fit within maxRows (2) rows, where clicking the expanded-state `⌃` collapse toggle changed nothing visually (`hiddenCount=0`, not even a `+N` appears) — a dead button. Added an expanded-state row measurement `expRows` (reusing the pure `chipRows` helper; the expanded measure branch only writes `expRows`, never `geom`), and `⌃` renders only when expanded rows exceed maxRows. The ResizeObserver now also observes while expanded — the width change of fullscreen↔window switches is exactly the remeasure trigger — so `⌃` hides itself in fullscreen and returns back in the window; counting includes `⌃` itself, so visibility converges without oscillation.
927
- - **Verification**: 1224 tests pass; verified against real DSH Web for `⌃` visibility and collapse behavior in both window and fullscreen, and for the 8px category-row inset.
928
-
929
- ### Improved in 0.9.58 — the +N toggle follows the last visible category at matching height
930
-
931
- - **Position (`4ade54f`)**: within the 0.9.57 reserved, overlap-free slot, position `+N` from the measured right edge of the last visible category chip plus 6px. The slot grows from 52px to 60px to preserve clearance at narrow widths. Curated rows without a Filter trigger still reserve no unnecessary Filter gutter; expanded `⌃` remains at list end.
932
- - **Equal height (`aeed14c`)**: use the last visible chip’s measured border-box height for `+N` and center its text with inline-flex, rather than letting Latin-only text and CJK category labels acquire different automatic line heights. Other chips keep their existing dimensions.
933
- - **Verification**: 1224 tests and typecheck pass. Against the real DSH catalog, community layouts at 880/700/520/420/320px showed equal 21px boxes, 0px top-edge delta, roughly 5–6px horizontal separation, and zero overlap; the 260px curated layout had zero overlap and curated↔community switching remained stable.
934
-
935
- ### Fixed in 0.9.57 — curated-zone update loop, dead panel entry, and collapsed filter/overflow overlaps
936
-
937
- - **Root cause (reproduced on 0.9.56)**: curated labels may get a fresh object on every parent render; the layout effect depended on the `chips` array identity and on its own `geom` update. Synchronous remeasurement repeatedly entered the same commit, eventually throwing React `Maximum update depth exceeded` (#185). React emptied the panel tree while leaving `#dshm-panel-root`; the old entry guard checked only for a container and silently blocked every later open. The pinned Filter wrapper also added a second border around its bordered button, while the clamped/inline `+N` iterations overlapped neighboring chips at narrow widths.
938
- - **Fix**: depend on a stable signature of rendered category IDs, labels and counts; defer geometry-dependent remeasurement to an animation frame after state commits. Retire the 0.9.56 `display:none`/in-flow feedback path in favor of a full in-flow chip layout with `visibility:hidden` beyond the clip; derive the hidden count from all chip row offsets. Give out-of-flow `+N` its own 52px right slot, separate from Filter's 96px, with a more specific selector so the total reservation wins the cascade. Measure the last visible **distinct row** rather than the Nth chip. Strip the redundant Filter-wrapper border; discard an orphaned panel root on the next open.
939
- - **Tradeoff**: correctness, zero overlap and reopenability take precedence over keeping `+N` flush against the last chip. Its count remains truthful; expanded `⌃` remains at list end.
940
- - **Verification**: three additional regression assertions, 1224 tests passing and clean typecheck; real DSH Web with the candidate client survived three curated↔community round trips, close/reopen and stale-root recovery; six widths from 260–880px showed zero badge/chip/Filter intersection, and curated's five categories showed no spurious `+N`.
941
-
942
- ### Fixed in 0.9.56 — ZoneChips +N overlap eradicated: in-flow document layout + wrap-yield-a-category (owner's design)
943
-
944
- - **Defect (reproduced on 0.9.55)**: the collapsed `+N` used "absolute positioning + contentW−48 clamp"; when the last visible row's leftover was narrower than the badge, the badge covered the last visible chip (real panel: +9 covering the tail of 文档与渲染 61).
945
- - **Fix (in-flow document layout)**: `+N` abandons absolute positioning and flows inline between the last visible category and the clipped categories — chip/badge overlap is impossible by construction.
946
- - **Wrap-yield-a-category (owner's design)**: clipped categories switch `visibility:hidden` → **`display:none`** (no space — the prerequisite for yielding to work); when the measure finds the `+N` top beyond the last visible chip's row top (wrapped), `hiddenCount +1` — one visible category yields, `+N` pulls back into the last visible row, and `max-height` grows one row to give `+N` its own line; the ratchet is single-direction (down), staying conservative on width growth while the count stays truthful.
947
- - **Tests**: full suite 1221 cases zero failures, typecheck clean; headless probe verified 880px/420px/expanded/collapsed/Q1-regression all at `chipOverlapPx = 0` with `+N` visible throughout, expanded view showing 23+⌃+filter in full, and `+N` returning to its inline slot after collapse.
948
-
949
- ### Changed in 0.9.55 — ZoneChips expand/collapse toggle inline (aligned with the dsh-market reference design) + overlay stacked scrim for contrast
950
-
951
- - **Inline toggle (owner-requested, reference: dsh-market v1.66.11)**: `+N` moves out of the right-side overlay group into an **absolutely-positioned inline slot** pinned at "last visible category chip's right edge + 6px" (the leftover of the last visible row), sharing one position semantic with the expanded `⌃` (in-flow at list end): "hug the current end of the list", flipping direction without relocating; when the leftover is too narrow it clamps to the content right edge without entering the filter gutter. The gutter slims from 132px to 96px (hosting only the pinned filter), showing ~1 more category per width. With `+N` removed, the overlay group hosts only the pinned filter and the R2-N1 4th collision class dissolves naturally.
952
- - **Overlay stacked scrim (owner-requested: low contrast in light theme)**: root cause is that the `--dsw-alias-bg-base` token itself is 0.55-translucent; the overlay scrim now paints the same token in a **three-layer stack** (background-color + two linear-gradient layers), effective opacity ≈ 1−0.45³ ≈ 91%, theme-adaptive (dark rgba(12,26,38,·) / light rgba(247,250,252,·) both significantly strengthened) with zero hard-coded colors.
953
- - **Tests**: full suite 1221 cases zero failures, typecheck clean; headless probe verified collapsed-state `+N` left edge = last visible chip right edge + 6px on the same row, double scrim gradients, single-button zero-collision filter, stable toggle position across collapse/expand cycles, and the Q1 regression (long labels: hidden > 0); real-panel screenshots in the release record.
954
-
955
- ### Changed in 0.9.54 — review fixes ship (ZoneChips stale-geom / ⌃ bounce / overlay centering) + dsh-skip-browser-auth catalog copy reconciliation
956
-
957
- - **ZoneChips review fixes (execution-review R1, `b2aab5f`)**: ① measure deps use the `chips` reference instead of `chips.length` plus `active` — same-length content changes (count refresh / active bold reflow) now deterministically re-measure, closing the stale-geom blind window where RO is deafened by the max-height clamp (clipped-but-focusable chips / missed auto-expand, a D4 violation); ② manual `⌃` collapse unconditionally records autoRef — manual collapse is respected on every path, removing the edge drift where a stale-geom decision effect bounced the collapse back; ③ the overlay group is vertically centered per measured row-height (compensated bottom offset).
958
- - **Catalog copy reconciliation (review Q5/Q7)**: two rounds of dsh-skip-browser-auth description updates ship with this release (`ed6043a` fixed-branch note + defect warning, `23da0fd` warning-period ended, slimmed to keep-latest hint); from this release onward a tag↔CHANGELOG reconciliation runs before publishing, and commits use explicit `git add <files>` instead of `-A`.
959
- - **Tests**: full suite 1221 cases zero failures, typecheck clean; directed probes verified Q1 (same-length label change: hidden 0→5 live recompute) and Q6 (post-⌃ +500ms stays collapsed); real-panel e2e (V1 cold storage / V4 sticky / V6 filter / V7 dark theme) in the 0.9.53 evidence chain.
960
-
961
- ### Changed in 0.9.53 — ZoneChips Plan A "invisible full-list measurement": cold-mount mis-collapse eradicated + live row-capacity re-measurement
962
-
963
- - **Motivation (found during 0.9.52 end-to-end verification)**: with a fresh browser profile (no localStorage snapshot), the market data had not arrived when the panel first mounted, so the old `fit` measurement ran on an EMPTY list; when data arrived the re-measure faced a DOM already collapsed by the stale budget (1 chip + "+N") and ratcheted to a frozen "3 chips + +20" single row. Warm sessions hit the snapshot on first mount and measured correctly — which is why this 0.7.0-era defect never surfaced. Sister issue: the measure effect ignores width changes, so `+N` drifts from real capacity after window resizes. Full causal chain with headless evidence: `docs/plans/2026-10-08-zonechips-visual-clip-requirements.md`.
964
- - **Change (Plan A "invisible full-list measurement", converged through an independent 3-round review with 11 findings)**: the slice-budget is gone — category chips **always render in full**; collapsing is a `max-height` (measured row-height × rows) + `overflow:hidden` visual clip on `.dshm-chips`; chips beyond the clip get per-chip `visibility:hidden` (parity with today's "unrendered = not in tab order"); `+N`/`⌃` become a right-side overlay group (horizontal pair pinned to the last visible row, gutter `--dshm-clip-gutter:132px` geometrically guarantees zero chip intersection); the filter trigger is pinned as an overlay while collapsed and stays in-flow when expanded; re-measure hooks = deps (`chips.length/expanded/zone/stuck/rowHeight`) + `ResizeObserver` (rAF-throttled) + `document.fonts.ready`, all feature-detected with graceful degradation (precedent `:947`). Measurement pure functions (`chipRows/countBeyondRows/clipTopOf/autoExpandDecision`) moved to `market-state.js` with 20 direct Node unit tests.
965
- - **Semantics preserved**: sticky collapse to one row / restore on scroll-up (the 0.9.52 sentinel chain untouched; the clip host is `.dshm-chips` and clipping the wrap is forbidden — it would clip the sentinel and permanently mis-set stuck, review blocker Q1); `+N` expand / `⌃` collapse; auto-expand when the active category falls beyond the clip + autoRef "manual collapse is respected" (implemented as split reset/decision effects, review Q3); zero-count dimming. One intentional behavior improvement: while collapsed, the filter trigger moved from "third in-flow row end" to a persistent overlay at the last visible row's right end.
966
- - **Tests**: new `client-zonechips-clip.test.mjs` (20 cases) + 1 full-render smoke assertion; full suite 1221 cases zero failures, typecheck clean; headless probe matrix V1–V6 verified (cold data arrival collapses correctly at two rows / three-width live recompute / curated zone reserves no gutter / sticky one-row recompute and restore / filter & +N zero-overlap and clickable / auto-expand & autoRef no-bounce); probes not committed (0.9.52 convention).
967
-
968
- ### Changed in 0.9.52 — uniform header rhythm: sticky sentinel leaves the flex flow, killing the 25px dead band between search row and category chips
969
-
970
- - **Motivation (spotted in the owner's screenshots)**: the market header gap between the search row and the category chips row measures 25px (12 gap + 1px sentinel + 12 gap) — more than double the 12px rhythm between zone chips and the search row, identical in both community and curated zones, visually a full-width empty dead band; in search mode ZoneChips (and its sentinel) unmount entirely and the band disappears — proof it is a layout side effect, not designed rhythm.
971
- - **Root cause**: the sticky-detection sentinel added in 0.7.0 Task 10 (a 1px div watched by an IntersectionObserver — leaving the viewport means the chips row is pinned → auto-collapse to one row) is a direct flex child of `.dshm-body` (flex column, gap:12px) and consumes an extra gap. The sentinel must stay in-flow to serve stickiness: naive alternatives were all rejected — removing it / `display:contents` kills the observer (permanent one-row state), wrapping sentinel+wrap in a div locks the sticky travel.
972
- - **Change (single point, ZoneChips render tail)**: the sentinel moves inside `.dsvm-chipswrap` as an absolutely-positioned child (`position:sticky` is itself a positioning context), with `top:-5px` replicating the old geometry — the old sibling sentinel's top edge sat exactly 5px above the wrap's top edge, so the IO exit trigger is pixel-equivalent. A headless-Chromium probe verified the full stuck chain when pinned: wrapTop=0, sentinel at −5px out of viewport, IO `isIntersecting=false`. Spacing returns to `zoneBar |12| searchRow |12| chips`: search→chips 24.36px → 12px, header ~13px shorter, browse-mode rhythm now matches search mode.
973
- - **Tests**: full suite 1200 cases with zero failures and a clean typecheck; geometry and sticky behavior verified via temporary headless probes, then removed (not added to the formal suite).
974
-
975
- ### Added in 0.9.51 — community entries gain a derived github field (icon coverage 45.6% → 100%)
976
-
977
- - **Motivation**: community npm-source cards always fell back to the initial letter — the adapter's npm branch only produced the `npm` field and discarded the GitHub repo mapping inside the upstream `url` (demoted to `homepage` only), so `github` stayed unset and the client Icon owner-avatar fallback (`github.com/<owner>.png?size=64`) never fired. Measured against `dsh-plugin-catalog@2026.1007.4837`: 2,298 of 2,298 npm-source entries among 4,226 adaptable ones (**100%**) carry a github.com url — pure data discarding, not an upstream gap.
978
- - **Change (single point)**: the adapter npm branch now derives `github = githubFromUrl(url)` (`/tree/` subpaths resolve to the repo root; original casing preserved; derivation failure produces no key and never skips the entry — lenient entry semantics, Q43). `source` semantics unchanged; zero client changes — Icon fallback, the details row gaining "GitHub · npm", and installed-view `registryGithub` (first tier of the README base-URL fallback) all light up through existing pipelines.
979
- - **Activated semantics (accepted on record)**: ① the merge layer's github-collision branch now applies to community npm entries (same-repo entries yield to curated, curated always wins) — full real-catalog simulation shows zero new displacements (4,215 → 4,215); ② plugins installed from GitHub source can now match community npm entries, upgrading via the npm source (same semantics curated dual-source entries already had). Zero performance regression: no new host-side network requests, latest-probe path and cache keys unchanged, the Q46 community-github probe exemption and GithubBudget untouched.
980
- - **Tests**: adapter flips 1 assertion + nets 4 cases (github url derivation with original casing, non-github host, missing url, invalid shape), plus 1 added assertion in the existing subpath case (repo root); merge layer nets 2 cases (github-collision displacement, GitHub-source installed matching; both red → green); full suite shows zero new failures against the captured baseline and zero typecheck errors.
981
-
982
- ### Added in 0.9.50 — two-leg /latest README fallback ladder + profile-aware not-installed copy + fallback failure summary
983
-
984
- - **Motivation (verified on the Shanghai Windows desktop)**: on the desktop profile (effective registry npmmirror), the README fold for billion-context (705k weekly downloads) always failed with "web profile 未安装该插件" — npmmirror's packument measures 8,586,604 bytes, above dsh-m's 8MB (8,388,608) maxBytes cap (readCapped throws a 502 "response over limit"), so the fallback leg always failed and the original local error was rethrown per the 0.9.45 contract; npmjs's packument (8,195,089 bytes) merely happens to sit under the cap with an empty readme field — the web profile's "(no README)" was luck, and this fast-releasing package will outgrow the cap on npmjs too.
985
- - **Two-leg fallback ladder**: `npmPackumentReadme` now tries `/latest` internally (public signature and `{readme, repo}` shape unchanged) — leg 1 fetches the KB-sized version document (immune to oversized packuments; URL shape per the npmLatest precedent; **timeout tightened to `Math.min(timeoutMs, 5_000)`** like npmLatest's first leg — worst-case fallback latency drops from 40s to 25s) and short-circuits on a non-empty readme; empty/failed falls through to the packument leg (full 20s, unchanged 0.9.45 behavior). Any successful leg wins: an empty readme renders "no README" (including the degraded blank-latest + failed-packument case, no throw); both legs failing throws the terminal (packument) error. Both legs hit the effective registry (ADR-0012), never cross-source; both legs share the same trim-based emptiness rule; repo follows whichever leg was reached (packument wins when it responds).
986
- - **Profile-aware not-installed copy**: `readInstalledPluginReadme` gains a third parameter profileName (default 'web'), and the host-api readme branch passes `profile.name` — the desktop profile now reports "desktop profile 未安装该插件: X" (shape aligned with the profile-ops precedent); the "web profile" copy in web-transaction-only paths (market/toggle/profile-transaction) is intentionally unchanged (desktop delegates to the official pluginManager and never reaches them).
987
- - **Fallback failure summary (semantic revision of 0.9.45's "rethrow the original local error")**: when both legs fail, the error becomes `<local error>(npm README 兜底失败: <terminal leg reason>)` — the real cause (e.g. the over-cap 502) is no longer swallowed; the original local error is preserved as `cause`.
988
- - **Testing**: versions.test.mjs describe rewritten as a ladder suite (explicit registry across both legs, default-registry shape assertions, latest short-circuit, empty-readme fallthrough, latest-failure recovery, blank-readme degradation, terminal-leg error, both-empty, tightened-timeout deps spy, repo-normalization note), asserted red→green throughout (red set {2,3,4,6,8,9} matched the plan case by case); installed.test.mjs adds a profileName case; host-api.test.mjs extends the readme case and adds profile.name pass-through plus composed-error shape cases. Full suite shows zero new failures (existing Windows symlink-family baseline exempt); typecheck clean.
989
-
990
- ### Added in 0.9.49 — installed-view repo base + three link normalizations (follow-up to the 0.9.48 deep-dive)
991
-
992
- - **Installed-view base wired through (P0, the main gap)**: the 0.9.48 installed-detail mount passed a non-existent `it.github`, and installed plugins take the local-read path (the `repo` field only rides on the npm fallback) → every relative link in installed READMEs collapsed to `#`. Now passes `vm.githubRepo` — the ready three-level fallback in the installed view model (registry match `registryGithub` → installed package.json `repository` parse → install spec `github:o/r` parse), which the icon and LinksRow already consumed. Relative links and images now anchor in the installed-browsing scenario too.
993
- - **Link normalizations (P1)**: ① protocol-relative `//host/x` gains `https:` — previously mangled into a repo path with a base, or pointed at the DSH origin without; ② inline `git@github.com:o/r(.git)` links normalize to https form (clone-instruction paragraphs); ③ with a base, `#anchor` anchors to `github.com/<o>/<r>#anchor` (same-name anchor on the repo home README; bare `#` and the img kind do not participate).
994
- - **Host-side parse alignment (P2)**: `githubRepoFromRepository` tail relaxed from `$` to `[/?#]` — `github.com/o/r/` (trailing slash) and deeper paths like `…/tree/main` previously failed to parse; now aligned with versions.ts `extractGithubRepo`.
995
- - **Testing**: 4 new cases in client-markdown (protocol-relative both modes, git@ two shapes, four anchor assertions incl. img kind), 1 new installed case (five repository shapes incl. trailing slash and subpath); the anchor assertion updated to the new behavior; full suite 1185/1185, typecheck clean. Client-only plus a read-only host passthrough — a page refresh takes effect.
996
-
997
- ### Added in 0.9.48 — README entity decoding + repo-base anchoring for relative paths (two fixes from 0.9.47 field feedback)
998
-
999
- - **HTML entity decoding**: 0.9.47 parsed tags but left entities raw — standalone `&nbsp;` lines between badges (dsh-task-board-style READMEs) leaked literally. Named entities (nbsp/amp/lt/gt/quot/copy/mdash/arrows/fractions — 60+ common set) and numeric entities (decimal/hex) now decode; both text runs and HTML attribute values decode (badge src `&amp;labelColor` → `&labelColor`, shields params no longer dropped); `code`/`pre` content stays literal; unknown entities and bare `&` are preserved; single-pass decoding without re-processing (`&amp;lt;` → `&lt;` as text, matching browsers). Decoded output only ever becomes text nodes — never re-parsed as tags, no injection surface.
1000
- - **Inline-first heuristic**: after unwrapping a block wrapper (`<p align="center">` etc.), if the inner content has no blank-line paragraph breaks, no markdown block structures, and every line starts with an inline/void tag, the whole block renders through the inline token stream — badges + `&nbsp;` flow on one centered line (GitHub semantics) instead of one paragraph per line; content with block structures (lines starting with `tr/td/ul/li` etc.) still goes through the markdown recursion to preserve structure.
1001
- - **Repo-base anchoring for relative paths**: relative links like `[README.md](README.md)` previously collapsed to `#` via safeUrl (inert click — long-standing normalization, not a 0.9.47 regression). The README preview can now carry a repository base: relative links anchor to `github.com/<o>/<r>/blob/HEAD/<path>`, relative images anchor to `raw.githubusercontent.com/<o>/<r>/HEAD/<path>` (`./`/`../` prefixes normalized); absolute URLs and in-page anchors are untouched; without a base the old `#` behavior applies. Data flow: the npm packument fallback now also extracts `repository`, normalized to `owner/repo` (GitHub only; git+https/git@/.git forms normalized) and passed through host-api as `repo`; the client falls back to the card's `github` field; the render base is set per renderMarkdown call and restored on exit — no cross-render leakage.
1002
- - **Testing**: 6 new cases in client-markdown (four entity classes, standalone-`&nbsp;` badge block, code no-decode + attribute decoding, seven anchoring assertions, no-base regression + cross-render leakage); 1 new versions case (repository normalization in both shapes + non-GitHub → empty); full suite 1181/1181, typecheck clean; real-data end-to-end — dsh-TUI 25,926 chars zero leaks zero regression, the real task-board badge block renders three badges on one centered line with `&amp;` decoded, relative links anchor to blob/HEAD correctly. Client-only change — a page refresh takes effect.
1003
-
1004
- ### Added in 0.9.47 — embedded-HTML subset rendering for the README preview (GitHub-style READMEs no longer show raw tags)
1005
-
1006
- - **Problem**: the README fold in the detail Modal (market/curated/installed) showed GitHub-style HTML-heavy READMEs (`<p align="center">` badge walls, `<img>` logos, `<details>` blocks — common in both the npm packument fallback and local reads) with every tag rendered literally, looking like mojibake. Root cause: the in-house markdown renderer never parsed HTML tags at all. The source content itself was fine — GitHub renders it normally.
1007
- - **Rendering subset (`src/client/markdown.js`)**: block-level wrapper tags (`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` etc.) are unwrapped and their inner content re-rendered as full markdown, preserving headings/lists/tables/fences; inline tags (`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` etc.) are tokenized into React elements; `align` maps to `textAlign`; `<img width/height>` maps to explicit size and lifts the 20px badge height cap (badges without width keep the small-image cap); `<details>/<summary>` render as native disclosure; ordinary paragraphs/headings/table cells whose text contains tags also go through the inline token stream (`Press <kbd>Ctrl</kbd>` renders properly).
1008
- - **Security model (invariant)**: raw HTML is never injected into the DOM — only whitelisted tags with whitelisted attributes are parsed, everything is built via `h()` React elements, and URLs pass the `safeUrl` gate (`javascript:` etc. collapse to `#`); unknown tags are unwrapped keeping their text, `script/style/svg/iframe` containers are dropped together with their content, and a recursion depth guard (>8) degrades to plain text. No `dangerouslySetInnerHTML` — no XSS surface.
1009
- - **Known limits**: the npm packument fallback carries no repository base URL, so relative-path images (e.g. `docs/assets/logo.svg`) collapse to `#` via safeUrl and hide on error (externally hosted badge images are unaffected); inside `<picture><source>` the source branch is dropped and the `img` fallback kept.
1010
- - **Testing**: 10 new cases in `client-markdown.test.mjs` (block unwrap recursion, badge rows, inline mix, script dropping + `javascript:` neutralization, unknown-tag unwrap, details/summary, whole-document div wrapper, HTML tables, HTML headings + inline elements, unclosed-tag degradation + depth guard); the real dsh-TUI README (25,926 chars) renders with zero tag leaks and 64 badge links constructed; full suite 1175/1175, typecheck clean. Client-only change — a page refresh takes effect.
1011
-
1012
- ### Added in 0.9.46 — bilingual README restructure + live-captured WebP screenshot set (docs release)
1013
-
1014
- - **README×2 restructured**: zh-CN 17.5KB → 8.3KB, en 18.1KB → 9.4KB — reorganized around the user's task flow (quick start → browse/search/install → installed & settings → agent tools & CLI → compatibility & FAQ). Fetch-chain and cache-semantics details no longer live here; they are delegated to [`docs/DESIGN.md`](docs/DESIGN.md) and the ADRs. Runtime-phase / takes-effect wording now matches the activation-bucket semantics (no hard promises of "always restart" or "always refresh" — follow the operation result).
1015
- - **Screenshot set (`docs/images/`, 8 WebP ≈248KB)**: marketplace home, community category strip, curated category strip, cross-catalog search (curated hits first, community grouped), plugin detail (not-installed state with install action), installed management, and settings are all re-captured from the current live Web instance; the official install-dialog shot is retained. The installed shot redacts the host's absolute profile path.
1016
- - **New `scripts/capture-readme.mjs` (development-only, not shipped in the npm package — `assert-pack`'s scripts/ deny-list keeps enforcing that)**: drives the running Web instance read-only via playwright to reproduce the full set (`npm run capture:readme`); performs no install/upgrade/toggle/uninstall — browsing and modals only; every shot first asserts no unredacted absolute profile path inside the panel. New devDependency `playwright-core@1.62.1` (pinned to the official registry resolution).
1017
- - **Verification**: typecheck clean; full suite 1165/1165 (one process-tree termination timing assertion flaked once under concurrent load; passed both in isolation and on a full rerun, with no runtime code changes); `npm pack --dry-run` + `assert-pack` allowlist green; each README's 8 WebP references match files one-to-one — nothing missing, nothing unreferenced.
1018
-
1019
- ### Added in 0.9.45 — market two-phase loading + force probe passthrough + curated-zone motion batch
1020
-
1021
- - **Market two-phase loading ([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**: browsing market queries split into two phases — phase 1 sends `probeMode:'cache-only'` (TTL-fresh cache hits only, zero network, instant page), and only when gaps exist (`latestComplete=false`, community github entries exempt — a Q46 permanent gap is never a phase-2 reason) does phase 2 run the existing probe semantics to patch badges in place. First open after a DSH restart is no longer blocked by a full-page re-probe (8–16s on weak networks → always instant page + async badge completion). `mergeLatestFields` keeps badges from regressing during in-session filter switches; snapshots are written at terminal states only. The server-side latest cache stays memory-only (ADR-0006 untouched); the tools/CLI `withLatest:false` path is unchanged.
1022
- - **Force probe passthrough (reopens ADR-0006's deferred ruling)**: the settings-page "Force refresh" gains the onForceMarket → marketReloadAll(true) → core "peek old values as fallback + full-page re-probe" chain — strictly honoring ADR-0006's on-record constraint ("peek without delete, not ttlMin=0 delete-then-probe"); a failed re-probe keeps old values + latestError with no empty-badge window, and one click now refreshes both the curated list and the update badges. Market probeMode has exactly two values (full/cache-only) — no 'only'.
1023
- - **Detail Modal upgrade motion (U10)**: installed-and-outdated entries get a primary "Upgrade" button in the Modal actions (both the market and favorites mount points; record.target = installed package name, routed through the ops pump with 0.9.22 activation-based messaging; guard/opSuperseded/generic failure branches aligned with the Installed tab); a README fold (fetched on expand, zero requests while collapsed); "Added" date localized via fmtDate. **Fix (found in real-machine acceptance)**: the fold originally reused the Installed tab's local-read endpoint, so every not-installed entry (the majority of market browsing) errored with "web profile 未安装该插件" — not-installed entries now fall back to the npm packument top-level readme field (HTTPS/8MB cap/timeout; a missing readme field renders "no README"; a failed fallback surfaces the original error honestly), while installed entries keep the local read.
1024
- - **Error recovery & empty states (U9)**: inline "Retry" button on the error row (previously only panel reopen or a detour to Settings); three-way empty copy — registry-unavailable primary message / curated-bucket-specific message / generic.
1025
- - **Favorites identity (U12)**: favorite snapshots store verified/audience/decoupled (stored but not labeled — DESIGN §2.7 ruling ⑤ "favorites stay unmarked" untouched); curated entries in Favorites regain a "Curated" identity badge plus the "Verified" quality badge (previously they lost even the Curated marker).
1026
- - **Accessibility & misc (U8/U3/U7)**: Detail Modal gains role=dialog/aria-modal/aria-label and initial focus on the close button; screenshot lightboxes open via keyboard Enter/Space (focus trap explicitly out of scope); zero-count curated buckets render an explicit "0" at reduced opacity; the chips row gets a title explanation whenever cross-bucket double counting pushes Σchip counts above the total.
1027
- - **Tests**: new cases and assertions across market / host-api / client-market-state / client-render-smoke (two-state probeMode and force peek semantics, gap exemption, cache-only outdated, mergeLatestFields, two-phase and force-refresh wiring, Modal upgrade/README/a11y, favorites badges, zero-count chips); on this Windows machine the full suite has zero new failures beyond the 15 pre-existing symlink-family baseline cases; typecheck clean.
1028
-
1029
- ### Added in 0.9.44 — plugin card grid adapts column count to available width (more cards in fullscreen)
1030
-
1031
- - **Adaptive grid**: the card grid shared by the Market / Installed / Favorites views switches from a fixed two-column layout to container-width adaptation — `grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr))`. Column count ≈ `max(1, floor((W+8)/368))` (360px min card width, 8px gap, remaining width split evenly): the floating panel (~920px) keeps its 2 columns with zero regression; fullscreen at 1920px shows ~5 columns and 1280px shows 3 — noticeably more plugin cards on screen.
1032
- - **Viewport breakpoint removed**: the `@media (max-width:680px)` single-column override is gone — viewport width does not describe the width actually available to the panel; very narrow containers fall back to a single column via `min(100%,360px)` without horizontal overflow.
1033
- - **Scope and edges**: search group headers (`.dsvm-grouphead`) still span the full row via `grid-column:1/-1`; client-only change — refresh the page to take effect.
1034
- - **Tests**: 1 new SSR smoke regression case (locks the adaptive grid rule and the breakpoint removal); typecheck clean.
1035
-
1036
- ### Added in 0.9.33 — self-dev entry metadata v1.1: `audience`/`decoupled` fields with three-surface marking
1037
-
1038
- - **Schema**: registry entries gain optional `decoupled?: true` (decoupled entries, operational criterion; mutually exclusive with `verified` — co-occurrence is an error) and `audience?: 'public' | 'internal'` (defaults to public, no key emitted; `'team'` reserved, not accepted this release). The decision record and five rulings live in DESIGN §2.7; terms in GLOSSARY (Decoupled Entry / Internal Entry).
1039
- - **Three-surface marking**: GUI cards and the Modal head gain 「作者自用」 (Author's own) and 「版本无关」 (Version-independent) badges plus Modal audience/compat rows; `dshm_search` output projects `audience`/`decoupled` with an inline [作者自用] line marker, and the tool description states the recommendation discipline (never proactively recommend internal entries to general users unless named or for internal rollout); CLI `dshm search` marks lines identically (new `tests/cli-search.test.mjs`). Installed views and favorites stay unmarked — the discovery path is the discipline's target.
1040
- - **CI**: validate-registry gains a soft warning — a decoupled entry whose description contains the 「已适配」 pattern (copy-guide §4 fourth pattern 「版本无关,详见仓库」, compact form 「版本无关。」 when space is tight).
1041
- - **Registry data**: final state for the eight self-dev entries — surf / obmc-web / onetree-log gain `decoupled`+`audience: internal`, quota-watch gains `decoupled`; the four `verified` arrays are removed (history lives in git and per-repo docs) and compatibility sentences switch to the fourth pattern; the four coupled entries (dsh-m / dsh-skins / skip-browser-auth / copilot-auth) keep verified as-is.
1042
- - **Tests**: 9 net-new cases (registry validation 5, tools-search marking 2, cli-search 2); full suite 1133 tests, locally 1108 pass / 15 fail — all 15 are the pre-existing Windows symlink-semantics baseline (item-for-item identical, zero regression); typecheck clean.
1043
-
1044
- ### Added in 0.9.32 — npm registry route adaptation: metadata prefetch source switching / mirror self-heal (ADR-0012)
1045
-
1046
- - **Motivation**: on 2026-10-04 a Shanghai Windows desktop machine proved the metadata prefetch is hardcoded to registry.npmjs.org; intermittent timeouts aborted upgrades before pnpm was ever invoked (`fetch failed` / `The operation was aborted due to timeout`, with no operation log under `.plugin-manager/logs`), while npmmirror had already synced the target version. A Seoul (Tencent Cloud) box enjoys fast direct npmjs. Both machines must work with zero configuration.
1047
- - **Read-class routing (L1/L3)**: detection reads (`npmLatest` upgrade probe / `npmPackumentTimes` publish times) prefer the authoritative npmjs (first leg timeout tightened to ≤5s) and degrade to the effective source; fulfillment reads (`npmVersion` exact metadata / `npmPackument` warm) prefer the effective source. `npmPackument` previously ignored its registry parameter — fixed this release (explicit parameter now takes effect, pinned by tests).
1048
- - **L0 transport**: httpx now uses undici's own fetch with a self-built dispatcher (`EnvHttpProxyAgent` handed the resolved proxies explicitly) — metadata prefetch honours `HTTP(S)_PROXY` / `npm_config_*` proxies and is immune to host undici global-dispatcher pollution (dsh-market net.ts #742 family); `fetchLimited` gains a PUT method (sync primitive only); network failures carry a `via` context, and the failure digest renders 「(via <masked proxy>)」 only on proxy paths.
1049
- - **L2 mirror-lag self-heal**: on `NO_MATCHING_VERSION` whose registry field names npmmirror (structured recognition in the classification layer, judged on the full text — upper layers never regex) → on-demand mirror sync (`registry-direct.npmmirror.com/<pkg>/sync`, PUT) → the existing backoff retry (heal `B3_NPMMIRROR_SYNC`); the same ladder applies to `npmVersion` (404 → sync → wait 10s → same-source retry → npmjs fallback); latest cache is invalidated after a sync. `DSHM_MIRROR_SYNC=0` switches it all off.
1050
- - **New module `src/core/npm-route.ts`**: `DSHM_NPM_REGISTRY` override (highest priority, skips probing) > [.npmrc registry, npmjs, npmmirror]; probe-once (2.5s shared budget, single-flight, winner must return a complete body with version); decision persisted at `<cacheDir>/npm-route.json` (delete to re-probe); total failure falls back to npmjs in memory only (60s TTL then re-probe, never persisted). Effective-source switches invalidate both latest and packument caches.
1051
- - **Wire test seam migration**: the existing mock-`globalThis.fetch` seam moved to `_setWireFetchForTests` (versions / npm-integrity / registry-check; assertion and counting semantics unchanged).
1052
- - **Tests**: 41 new cases (L0 proxy parsing/wire forwarding/via/PUT, npm-route ×12, read-class routing, registry classification field, B3 sync, L4 digest, kill-switch ladder/integration, trailing-slash normalization); full suite 1128 tests — 1103 pass / 15 fail on this machine, all 15 being pre-existing Windows symlink-semantics cases (item-for-item identical to the pre-change baseline: zero regression); typecheck clean. Real-machine acceptance (in-window scoped self-published upgrade on the Shanghai box, `dshm outdated` on Seoul) ships with the release.
1053
-
1054
- ### Added in 0.9.31 — doctor desktop profile support (CLI exception + farmChecked semantics revision, ADR-0011)
1055
-
1056
- - **Motivation**: a Windows desktop machine's report confirmed the core engine is profileDir-parameterized with no web hardcoding (materialized desktop layout detects as hoisted with farm=0 — the norm, not a short-circuit), yet the CLI's blanket `--profile desktop` rejection swept up the read-only doctor; desktop-only machines scanning a nonexistent web dir got an all-zero report.
1057
- - **CLI exception (doctor only)**: `dshm doctor --profile desktop` is allowed and routed to `desktopProfileDir()`, with a `[web|desktop]` tag on the first output line; HELP synced; **every other command's rejection of `--profile desktop` stays byte-identical** (pinned by regression tests) — desktop mutation management remains with the official Desktop plugin page.
1058
- - **Missing-dir hint**: when the target profile dir does not exist, suggest `--profile desktop` (an existing-but-empty legit profile is not flagged).
1059
- - **Host method untouched**: the doctor case was already active-profile agnostic — desktop hosts get it for free (new desktop-kind injection pin test); the `dshm_doctor` MCP tool stays deferred per ADR-0010 decision 3; on Windows Electron hosts runtimeVersion may still degrade (materialized layout means farm=0, so stale has nothing to judge — zero practical impact, recorded honestly).
1060
- - **farmCount semantics revision**: "0 = walk short-circuited" applies only to shapes with a symlink farm; 0 is the norm for materialized layouts (ADR-0011).
1061
- - **Verification**: 8 new cases (desktop routing/tag, other-command rejection, invalid value, missing-dir hint, no-hint-on-normal, explicit-desktop no contradictory hint, explicit-web routing, method pin) + 1 rewritten legacy rejection sample (net +8, 1079→1087); full suite 1087 pass / 0 fail / 0 skipped; typecheck clean. The Windows on-machine acceptance checklist ships with the implementation plan (no Windows E2E possible on this machine).
1062
-
1063
- ### Fixed in 0.9.30 — bin symlink silent no-op (F2) + realpath third source for version resolution (F1)
1064
-
1065
- - **Shared root cause**: two critical pnpm-ecosystem entry points are **symlinks** — `node_modules/.bin/dshm` (→ lib/cli.js) and the global shim `PNPM_HOME/dsh` (→ the real bin.js) — while both code sites made path-identity decisions before realpath.
1066
- - **F2 (latent since the bin entry shipped in v0.2.0; first hit and proven on this machine's 0.9.28 install shape; 🔴 every command via the bin entry silently exits 0)**: cli.ts `invokedDirectly` compared `argv[1]` (link path) against `import.meta.url` (node's realpath) — never equal, so `dshm <any command>` printed nothing and exited. Fix: realpath `argv[1]` before comparing. New `tests/cli-bin-symlink.test.mjs` (revival via symlinked --help and doctor --json; EPERM environments skip per repo precedent).
1067
- - **F1 (proven at 0.9.29 install acceptance)**: dsh-version.ts `readLauncherPackageVersion` walked up three levels from the shim location, landing in the pnpm home tree — pure-FS version resolution was unreachable under this host topology (`node PNPM_HOME/dsh web`), degrading doctor's stale judgment and forcing the ping chip onto the spawn fallback. Fix: realpath the entry after the shape check, then walk (locally verified resolving 0.2.0-rc.2). New shim-shape case in dsh-version.test.mjs.
1068
- - **Verification**: targeted 9/9 (two new cases); full suite 1079 pass / 0 fail / 0 skipped; typecheck clean. Post-install expectations: `dshm doctor` works via the bin entry; host method path reports runtimeVersion=0.2.0-rc.2 with stale judgment live (the farm's dsh link at 0.1.7-rc.2 is a ready-made stale form until healed).
1069
-
1070
- ### Added in 0.9.29 — profile Doctor: `dshm doctor` (read-only: farm liveness / residue inventory / manifest-reality consistency)
1071
-
1072
- - **Motivation**: dsh-m had no diagnostics at all — know-how 014's "re-run farm liveness after every DSH upgrade" was the only standing periodic check (81 dangling links and a 230-line manual heal log), know-how 023 documented the manifest-reality split, and a live scan found 8 empty scope dirs + 12 `*.bak-*` files. Day1 subset adopting dsh-market check.ts's design disciplines (pure-FS boundary, three-tier severity, unknown≠broken, false-positive bookkeeping, repair responsibility shifted outward). See ADR-0010 and the comparison report (2026-10-04).
1073
- - **Added**: `src/core/doctor.ts` pure-function core (no processes, no network, no writes — safe to call any time) + new `/dshm` method `doctor` + CLI `dshm doctor [--json]` (exit 1 on error-tier findings; HELP synced). Three checks: **farm liveness** (ancestor-chain walk over `@deepseek-ai/*` symlinks; dangling = error; dsh umbrella pointing at an old runtime store = info-tier — non-umbrella versions are never compared against the runtime version, avoiding cordis-style false positives), **residue inventory** (empty scope dirs / no-manifest dirs / pnpm `*_tmp_*` / `*.bak-*`, all zero-alarm lists, "visible rather than cleaned"), **manifest-reality consistency** (pin vs installed vs lockfile; mismatch = warning; only lockfileVersion 9.0 importers shape is parsed, anything else stays unknown).
1074
- - **Boundary disciplines (ADR-0010)**: runtimeVersion comes only from the pure-FS `readLauncherPackageVersion` (null under the CLI → stale judgment degrades to unknown, never spawns); the secrets red line covers credential-bearing config files only (package metadata version fields are readable); layout detection is workspace-declaration-first (proven on the local "hoisted declaration + vestigial lock.yaml-only .pnpm" shape); dual-market coexistence (dsh-m + dshmarket installed together) surfaces as an informational fact; doctor never repairs — advice is text only.
1075
- - **Acceptance anchors (to verify after install)**: first local run expects farmChecked ≈236 (0 means the walk short-circuited), 0 dangling, 8 empty scopes + 12 bak files listed, dual market flagged.
1076
- - **Verification**: new `tests/doctor.test.mjs` (36 cases) + `tests/doctor-api.test.mjs` (2 cases); full suite 1076 pass / 0 fail / 0 skipped.
1077
-
1078
- ### Changed in 0.9.28 — page-size set de-upstreamed: 32/64/96 replaces 24/48/96, default 32
1079
-
1080
- - **Motivation**: 24/48/96 came bundled with the dsh-market filter-panel replica (0.7.2). On inspection, 96 has long been internalized as a dsh-m core parameter (`WITH_LATEST_MAX` probe cap, primary-zone default page, fast-open snapshot baseline); the only real upstream trace is 24 — "drop 24, keep 96" is exactly the intersection of de-upstreaming and compatibility.
1081
- - **New set**: `MARKET_PAGE_SIZES = [32, 64, 96]` (arithmetic +32), community default `DEFAULT_PAGE_SIZE = 32` (fuller first page, still below the 0.6.x default of 50 in probe load; Q46 posture unchanged); primary default 96 and the core clamp 1..96 untouched.
1082
- - **Ripple**: host-api GUI fallback default 24 → 32 (three ends consistent, no hidden default); probe-budget comments updated (market.ts / host-api.ts); README×2 and DESIGN.md wording synced; GLOSSARY "Primary Zone" entry sharpened to match the code (single-page-no-pager → normally single page, degraded to the community pager beyond capacity); test assertions and snapshot fixtures synced (off-default assertions now use in-set value 64, covering "in set but not default is still rejected/false").
1083
- - **Known one-time impact**: after upgrading, a community snapshot with limit=24 no longer matches the fast-open check — the first market open makes one extra normal request, then the snapshot rebuilds on the new default; no legacy-compat branch added.
1084
- - **Verification**: `npm run build` clean; full suite 1030 pass / 0 fail (8 skipped).
1085
-
1086
- ### Changed in 0.9.27 — fix accidental drag-out close: press inside, release on backdrop no longer closes the panel
1087
-
1088
- - **Problem (observed on install, 2026-10-03)**: pressing inside the market search box and dragging left past the panel edge closed the whole panel. Not a browser mouse gesture — the panel backdrop used a bare `onClick: onClose`; when a press inside the panel content drags onto the backdrop and releases, the browser dispatches `click` to the common ancestor of the press/release targets (the backdrop itself), misread as "clicked the backdrop". The detail modal, screenshot lightbox, and compat dialog shared the same defect class.
1089
- - **Fix**: new pure helper `backdropCloseHandlers` (src/client/backdrop.js) — close only when **both mousedown and click land on the backdrop itself** (all other combinations are ignored; the pressed state resets after each click); all four overlays (main panel/detail/lightbox/compat dialog) switched over. Backdrop-click close, Esc, and the ✕ button behave exactly as before.
1090
- - **Verification**: new `tests/client-backdrop.test.mjs` (close / two drag-out scenarios ignored / state reset / malformed input safe — 5 assertions); full suite 1030 pass / 0 fail.
1091
-
1092
- ### Changed in 0.9.26 — curated hits first in cross-zone search: summary counts now match page one
1093
-
1094
- - **Problem (observed on install, 2026-10-03)**: searching "sidebar" showed "⭐ Curated 3 · Community 315" but only one curated card on the first page. Not double counting — `sourceCounts` never reads category counts (`alsoCategories` is unrelated) and all 3 were genuine curated hits. The mismatch: the summary line reports global hit counts while the relevance ranking plus the community zone's default downloads-desc tie-break pushed weakly-matching curated entries onto later pages.
1095
- - **Fix**: `listMarket` gains `curatedFirst` — with `source='all'` and a non-empty query, curated hits are stably moved to the front (stable partition; relevance order preserved within each segment, community hits follow). Carried only by the host-api GUI channel — tools/CLI don't pass it, so the shared search ordering across the three surfaces is unchanged; single-zone and browse states are naturally unaffected. Dangling cross-page section headers and the count mismatch disappear (with curated hits ≤ page size, all of them land in the first-page section).
1096
- - **Verification**: new test ⑮ (no flag keeps the interleaved relevance order / flag moves curated first / empty query and single-zone are no-ops); full suite 1025 pass / 0 fail.
1097
-
1098
- ### Changed in 0.9.25 — cross-zone search: browsing stays zoned, searching goes global (community + curated together)
1099
-
1100
- - **Cross-zone search**: either zone's (Community/Curated) search box now searches globally — with a non-empty `query` the underlying query switches to `source=all` and both zones' entries are ranked by the shared relevance pipeline; clearing the keyword returns to the zone's own browse state. The zoned browsing model (ADR-0004) and the `dshm_search`/CLI shared ordering are untouched; the only server-side addition is the `MarketResult.sourceCounts` bucket count.
1101
- - **Search-mode presentation**: a summary line "⭐ Curated N · Community M" with exact per-zone counts (`!loading`-gated so stale numbers never flash); curated hits grouped on top within the page (section headers render only when their section is non-empty, spanning both grid columns); non-community cards gain a "Curated" badge; category chips hide during search while the Filter button moves onto the summary line (page-size group only — relevance outranks sort while searching); the pagination scroll anchor follows the mode.
1102
- - **Fixes**: zone tab counts now derive from stable fields (community = `acceptedCount - displaced`, curated = `registryState.count`) instead of "the last query's total" — cross-zone searches no longer pollute them; committing a search clears the active category (chips are hidden, so a leftover category would act as an invisible filter).
1103
- - **Degradation**: a missing/malformed `sourceCounts` hides the summary line entirely; list behavior unchanged; old snapshots/old host responses stay shape-drift immune.
1104
-
1105
- ### Changed in 0.9.24 — release-age exclusion governance: from pre-flight refusal to govern + register (ADR-0009)
1106
-
1107
- - **Problem (observed live 2026-10-03)**: the 0.9.19 pre-flight precheck predicted "the official manager will block a target inside the 24h release-age window" and refused up front — while dsh-market installed the very same targets (copilot-auth@1.2.4 published 8 minutes prior; quota-watch@0.1.21). pnpm 11.7's default non-strict policy admits explicitly named fresh releases and auto-registers their exclusion entries; the wall the precheck predicted did not exist.
1108
- - **Three governance hooks**: (1) pre-delegation govern — merge malformed exclusion shapes (pnpm's auto-appended dead rules) into "one rule per package, version union"; (2) post-success register — fold in-window targets into the exclusion block (scoped exact / non-scoped target+previous composite); (3) on dual-code failure (`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` / `ERR_PNPM_NO_MATURE_MATCHING_VERSION`) → govern → retry the command at most once, then fall through to the existing failure translation. The web ladder gets the same three hooks.
1109
- - **Boundary & safety**: the red line narrows by exactly one exception (desktop: this block only, delegation hooks only, atomic write, parse-failure bail-out, tracing without file dumps); govern/register hold the official same lock and fail open — any failure never blocks delegation, pnpm stays the final enforcer; profiles that explicitly set `minimumReleaseAge` or `minimumReleaseAgeStrict` still get refused up front with a retry time; the mechanism verdict follows "first rule wins" (know-how 020 §2.4 form-theory retired).
1110
- - **Docs**: full decision record in `docs/adr/0009-release-age-exclude-governance.md` (with a fence appendix of 9 evaluated-and-skipped items and the observation contingency); GLOSSARY terms "First Rule / Release-Age Exclusion / Govern / Register / Release Age".
1111
-
1112
- ### Changed in 0.9.23 — two-phase Installed tab: instant list + update hints filled in place (ADR-0008)
1113
-
1114
- - **Problem (observed live 2026-10-03)**: update probes were welded into the installed-list endpoint behind a 60-min in-memory TTL — after publishing a release, reopening the panel served the stale cache and update hints lagged (quota-watch's new version stayed invisible 17 minutes after publish while dsh-market already showed it).
1115
- - **Two parallel phases (zero new buttons)**: phase 1 `installed {probe:false}` returns the installed list immediately (registry matching and toggle phase untouched); phase 2 is a new `installedUpdates` method (`probeMode: 'only'`, **TTL=0 — a real probe on every panel mount**) that fills in ⬆ badges / the tab dot / "Upgrade all (N)" in place; failed checks still surface per item as "check incomplete" instead of pretending "up to date".
1116
- - **Scope & side effects (documented in ADR-0008)**: installed tab only — browse-page probes, `dshm_list`/`dshm_outdated` tools and CLI are unchanged (default `probeMode: 'full'`); ttl=0 evicts shared host-namespace cache entries before re-probing, so browse/tools TTL hits receive fresher write-backs (data only gets fresher; code paths and their own TTL regimes unchanged); GitHub passive budget consumption rate rises (limits unchanged: 25/req, 50/h rolling) with graceful `latestError` degradation.
1117
- - **Docs**: full decision record in `docs/adr/0008-installed-two-phase-probe.md`; DESIGN notes on the installed page / cache semantics; GLOSSARY term "Two-phase Load".
1118
-
1119
- ### Changed in 0.9.22 — upgrade activation classification (tarball diff, three states) + three-surface restart-hint split (ADR-0007)
1120
-
1121
- - **Problem (observed in 0.9.21)**: after upgrading `@iasiv5/dsh-skins` 1.2.3→1.3.0 the plugin was already live without a restart (client bundle rev hot-reload), yet all three surfaces still said "restart required" — the noise trains users to ignore the hint, and restarts have real costs (web service blip / manual desktop reopen).
1122
- - **Activation classification** (GLOSSARY「生效判定 / 纯客户端更新」): npm-source upgrades now fetch both tarballs at the success point (parallel, 10s total deadline, 8MiB per tarball, no cache), parse them with a zero-dependency read-only ustar reader (pax long names supported) and classify by per-file sha256 diff into `activation: 'client-only' | 'restart-required' | 'unknown'`. Five rules: client set = `exports['./client']` target; a changed `dsh.bundle.patch`-declared patch file → host; `package.json` compared semantically with the top-level `version` ignored (dependency changes still count as host); every other added/removed/changed file attributed by path; a changed client pointer conservatively counts as host. **Fail-open**: any error → unknown → current conservative hint, never affecting the upgrade's success.
1123
- - **Three-surface split**: the agent tool message for `client-only` says a page refresh suffices and explicitly forbids offering `dshm_restart`; the GUI toast gains a suffix and its restart banner gate follows `needsRestart` (pure helper `upgradeNotify`, unit-tested); the CLI line switches accordingly. `needsRestart` widened to boolean at the source (TS2430 avoidance); `UpgradeResult` and the desktop upgrade result carry the new `activation` field; wired at `upgradePluginLocked` + `desktopUpgradeLocked`; selfUpgrade/install/uninstall/github sources unchanged.
1124
- - **Tests & docs**: 11 new ustar parser cases, 18 classifier rule/fail-open cases, 7 upgrade-wiring cases (npm/github/selfUpgrade/desktop), 4 renderUpgrade cases, 3 upgradeNotify cases; existing upgrade fixtures gained a `classifyActivation` seam (no implicit network). Full decision record and known limitations in `docs/adr/0007-activation-classification.md` (no client chunk require-graph tracking; docs-only diffs conservatively count as host).
1125
-
1126
- ### Changed in 0.9.21 — Settings force-refresh display fix + automatic jsDelivr purge on registry.json changes
1127
-
1128
- - **Settings force-refresh display fix (observed live 2026-10-03)**: the Settings tab's "Force refresh" only reloaded the `registry` API (synchronous full-chain force fetch), but the panel renders the registryState **from the `registry-config` snapshot taken at mount** — so the toast said "Registry force-refreshed" while source / fetched-at / entry-count stayed frozen until the settings tab was remounted. A successful force refresh now also reloads `registry-config` (force already updated the controller's in-memory snapshot, so this costs nothing) and the display follows the live data immediately.
1129
- - **Automatic jsDelivr purge (new `purge-jsdelivr` job in `registry.yml`)**: the default chain's "sticky route + CDN always answers 200" combo lets a lagging jsDelivr snapshot **pin the curated list indefinitely** — observed: after 0.9.18 added DSH Market (18→19), a machine sticky to jsDelivr kept receiving the 18-entry snapshot; the raw primary route was never retried, and even the user-facing force refresh could not escape (only a manual `purge.jsdelivr.net` call fixed it). Now a push to main that actually changes `registry.json` (diffed against `github.event.before`) triggers a CDN purge **after** `validate` goes green, re-reads the CDN to verify the entry count, and writes both into the step summary; pushes without registry.json changes and PRs skip the job entirely.
1130
-
1131
- ### Changed in 0.9.20 — latest probe cache back to memory-only (restart invalidates) + mutation-scoped invalidation (ADR-0006)
1132
-
1133
- - **Post-mortem landed (night of 2026-10-03)**: since 0.9.14 the latest-version probe cache was written through to disk and survived restarts, with TTL as the only invalidation channel — after a cache write at 00:22:58, four releases (00:27–01:05), a DSH restart (01:07) and a panel reopen (01:13) all served the stale value; cards and `dshm_outdated` both wrongly reported "up to date". That design is overturned; the full decision record lives in `docs/adr/0006-latest-cache-memory-only.md`.
1134
- - **Memory-only cache**: probe results now live in an in-memory Map + TTL (`cacheTtlMin`, default 60) — **a DSH restart drops them**, so "publish, restart, see the new version" works again. The cost is one bounded re-probe wave on the first open after a restart (current page, 8 workers + deadline cap, paid once per TTL window); the registry/community **catalog body** disk cache and SWR are untouched, so the page skeleton still opens instantly. Leftover inert `latest/<ns>.json` envelopes from 0.9.14 are swept once, best-effort, before the probe segment.
1135
- - **Mutation-scoped invalidation**: on **success** of install/upgrade/uninstall, the entry's cached latest value is invalidated across all registryKey variants (browse key / installed matched key / npm-only key) by itemId — no more "installed new / latest old" self-contradicting cards; `dshm_outdated` is honest about freshly upgraded packages. Invalidate-only, no writeback (a deliberate old-version install would otherwise fabricate latest = installed); rollback paths never invalidate; uninstall invalidates npm keys on a best-effort basis (a `gh:` key cannot be reconstructed from a pkg name and expires via TTL).
1136
- - **Not done, on record**: force passthrough of the probe cache and browse-page GitHub budget alignment were deferred by the owner — the former's residual blind spot is only the "no restart, within TTL" window; the latter has zero benefit for today's all-npm curated list (ADR-0006 §Considered Options).
1137
-
1138
- ### Changed in 0.9.19 — supply-chain release-age: pre-delegation gate, full violation parsing, split-state disclosure
1139
-
1140
- - **Root cause (observed 2026-10-03 00:15 on this machine)**: pnpm 11.7 runs a **lockfile-wide** supply-chain verification on desktop installs (`Verifying lockfile against supply-chain policies (180 entries)`), and the standalone exact `minimumReleaseAgeExclude` entries it auto-appends after each successful install are **not honored by that verification when unscoped** (`dsh-m@0.9.18` flagged; scoped `'@iasiv5/dsh-quota-watch@0.1.13'` honored) — so one freshly installed version bricks every package operation for 24h, regardless of the current target. The target package gets written into node_modules before the verification fails while the official manager rolls back only manifest/lockfile → **"split state"**: the UI shows the new version active, the operation record says failed, and the next operation silently downgrades the plugin.
1141
- - **Pre-delegation gate (`releaseAgePrecheck`; read-only, fail-open)**: npm installs/upgrades now check the target version and untrusted lockfile exclude entries against registry publish times **before** delegating; anything inside its waiting window is refused up front as structured `release-age-wait` with per-entry ready times — no more half-written states. Missing publish data / unreadable policy / entries covered by effective selectors (package-level, `||` compound, scoped standalone exact) → proceed; pnpm remains the final enforcer.
1142
- - **Failure translation rewritten** (replacing the 0.9.10 single-match parser): all violations are parsed and reported as "target vs lockfile bystanders" with their own publish times and retry deadlines; the misleading "install via DSH Web instead" hint is gone. `DesktopOpsError` gained structured `details` (violations/targetViolating/splitState/blockers).
1143
- - **Split-state re-read**: after a release-age failure the installed state is re-read; when node_modules already holds the target version while the manifest still records the old one, the error explains the upcoming silent downgrade and how to converge.
1144
- - **Wiring**: desktop install / self-upgrade call sites now pass `profileDir` (upgrade already did).
1145
-
1146
- ### Changed in 0.9.18 — DSH Market added to Curated + README restructure, changelog extracted
1147
-
1148
- - **DSH Market added** (npm `dshmarket`, Curated 18→19): a third-party visual plugin market — browse, search community plugins, and one-click install; one-click live theme switching. Primary bucket Essentials, secondary Cui's Picks (via `alsoCategories`). Copy follows the registry guide; third-party entries carry no `verified` claims.
1149
- - **Docs restructure**: README/README.en rewritten into a lean shape (highlights, TOC, requirements, documentation index, support-matrix FAQ); the recommended install path is now DSH's official plugin manager (with a screenshot and the upgrade caveat); this changelog moved out of the READMEs (bilingual); the Desktop FAQ reflects the post-0.9.8 capability table; DESIGN §3/§12 gained live-verification and tested-generation records.
1150
- - **Also**: the registry guard test moved 18→19; CHANGELOG.md now ships in the npm package (package.json files).
1151
-
1152
- ### Changed in 0.9.17 — secondary curated-bucket membership (one plugin across buckets) + the "iasi自研" rename
1153
-
1154
- - **Secondary buckets `alsoCategories`**: curated entries can now declare a secondary curated bucket — chip counts and bucket filtering count "primary ∪ secondary" (a cross-bucket entry appears in every bucket it belongs to), while the detail page's category labels still follow the primary bucket. First dual seats: **better-sidebar** (Essentials ⊕ Cui's Picks) and **dsh-m** (Essentials ⊕ iasi In-house); better-sidebar's former fifth "崔添翼精选" tag is replaced by the real seat (tags return to the ≤4 soft rule).
1155
- - **Rename**: curated bucket "我的自研" (My in-house) → "**iasi自研**" (slug `self-dev` unchanged; synced across chips / tools / CLI / detail labels).
1156
- - **Routine transition**: once the schema gains a field, old clients reject the new registry on validation → they fall back to cache / bundled snapshot and show old data; upgrading fixes it.
1157
-
1158
- ### Changed in 0.9.16 — curated five-bucket taxonomy + stricter curation (23→19) + DSH TUI added
1159
-
1160
- - **Curated taxonomy**: the Curated zone's categories change from the functional five (Market/Tools/UI/Search/Other) to five curated buckets — Essentials / Cui's Picks / iasi In-house / Tencent Lighthouse / Watchlist (chips in this order, Tencent Lighthouse last). Category semantics shift from "what the plugin is" to "why it deserves curation"; overlapping membership is bucketed by priority Essentials > Cui's Picks > In-house > Tencent Lighthouse, functional attributes move into tags for search, and old category values still hit custom sources as open slugs (ADR-0004 revision).
1161
- - **Stricter curation 23→19**: the channel five (lark/qqbot/weixin/wecom/dingtalk) leave Curated — still listed and installable in the community layer (4,000+ entries), just off the curated stage; **DSH TUI** is added (Cui's 9/26 X recommendation: a terminal TUI client, `dsh-tui`, speaking the DSH client contract ctx.remote); better-sidebar is bucketed to Essentials by priority and keeps its 9/27 recommendation provenance via the "Cui's Picks" tag.
1162
- - **Same-version carry-over** (local enhancements folded into the release): desktop installs retry the enable stage once on failure (proven against high-frequency install/uninstall and plugin-tree reload races) + failure copy keyed precisely to packageResult; the market page's "cached snapshot" banner retires — stale status is now carried by the settings page's community card, and browse pages no longer show transient cache state.
1163
-
1164
- ### Changed in 0.9.15 — install progress/result now live inside the detail modal
1165
-
1166
- - **Scenario**: clicking Install in the detail modal rendered all install feedback (the pnpm phase line, the "changes applied" banner/toast) on the panel layer beneath, half-visible through the modal mask — inside the modal the button just spun, with no sense of progress (live screenshot feedback, 2026-10-02). Uninstall never had this problem: no modal is involved and status already lives on the installed card.
1167
- - **Change**: while an install runs, the modal hosts the progress line itself (same host-status polling: phase / bar / current package); terminal states render an inline result row — success with version + build-script notes and a restart hint (desktop gets the official app lifecycle guide), failure / guard block with the reason and a re-enabled retry button, "skipped" presented neutrally; while the modal is open the underlying twin line steps aside (and returns once the modal closes). Everything still derives from the global operation log and the install result — the "state lives off cards" ownership model is unchanged (DESIGN §2.6).
1168
-
1169
- ### Changed in 0.9.14 (instant-open quartet + context-aware "Install command" row)
1170
-
1171
- - **Instant-open quartet** — a fix combo for the marketplace spinner that used to show on every panel open:
1172
- - **SWR (serve-stale-while-revalidate)**: expired TTL no longer blocks on the network — the disk cache is returned immediately as a snapshot (the community zone keeps its honest "cached snapshot" banner; never pretending to be fresh), a background single-flight refreshes it for the next open. "Force refresh" semantics unchanged (always synchronous; community card stays decoupled).
1173
- - **Route stickiness**: the default two-route chain (GitHub raw → jsDelivr mirror) is ordered by the **last successful route** recorded in the cache — once the mirror has worked it goes first, instead of re-paying the raw timeout on every cold open (~10-20s saved per cold open on CN networks).
1174
- - **Persistent probe cache**: per-page npm/GitHub latest-version probes now persist to a disk envelope (`latest/`, survives restarts) — no probe replay on the first open after a DSH service restart.
1175
- - **Client snapshot**: the default first page is cached browser-locally (10-min TTL); opening the panel renders the last response first and refreshes in the background — the spinner now only appears on the very first use (no snapshot yet).
1176
- - **Detail modal "Install command" row is context-aware**:
1177
- - **Scenario**: the row's derived command hardcodes `dsh plugin --profile web add …`, and community entries' upstream `install` text carries the same web-profile semantics — neither source looks at the current host profile. Copying it on Desktop installs into the web profile (invisible in the current UI); for already-installed entries the command is pure noise (live screenshot evidence: desktop + the dsh-m self entry, `installed` badge and the command on screen together).
1178
- - **Change**: the row is hidden entirely on desktop and for installed entries; web + not-installed keeps today's behavior (the CLI bootstrap path, same command as Quick start). The sanctioned Desktop path is the modal's own Install button (official pluginManager delegation, ADR 0005 discipline). No "simplify by dropping --profile": the explicit `--profile web` flag is a settled 0.9.0 decision (see the "profile target" section), and a bare `dsh plugin add` has ambiguous default-profile semantics — not simpler, just worse.
1179
-
1180
- ### Changed in 0.9.13 — desktop chip hover copy updated (owner decision)
1181
-
1182
- - The old copy "当前 DSH profile:{name}(Desktop 首发仅支持只读市场、安装新包与开关)" was stale after 0.9.8 opened upgrades/uninstall/self-upgrade; per owner decision it now reads "当前生效 Profile" (Active profile).
1183
-
1184
- ### Changed in 0.9.12 — re-issue of 0.9.11
1185
-
1186
- 0.9.11 hit a npmjs ghost publish: the OIDC publish was accepted and **staged** (CLI exit 0, provenance published to the transparency log) but never committed into the registry — GET 404, and re-publishing the same version is rejected with 409 `Cannot publish over previously staged version`. After waiting out any propagation, the standard workaround applies: re-issue under a new number. **Content is identical to 0.9.11: settings force-refresh no longer takes down the community catalog card.**
1187
-
1188
- ### Fixed in 0.9.11 — Settings "force refresh" no longer takes down the community catalog card (force semantics belong to the curated chain only)
1189
-
1190
- - **Scenario**: on a weak network, clicking the curated registry card's force refresh also flipped the community catalog card to "unavailable / fetch timeout" — the host forwarded `force` into the community summary, restarting its fetch flight; under a weak network the flight couldn't finish within the 3s waiter, which returned the timeout placeholder (the flight itself keeps running under its 30s hard cap and self-heals).
1191
- - **Fix**: the `registry` force is no longer forwarded to the community summary — force semantics belong to the curated chain; the community catalog keeps its own TTL/shared-flight cadence (a first-ever load on a terrible network can still transiently time out, but force refresh no longer triggers it).
1192
-
1193
- ### Fixed in 0.9.10 — Desktop self-upgrade hitting the official supply-chain release-age policy now yields an honest wait guide
1194
-
1195
- - **Scenario**: clicking the upgrade chip routes through the official manager, whose pnpm supply-chain policy (`minimumReleaseAge`, releases must age 24h before installation) rejected the just-published version — `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`. **The policy is working as designed** (anti-supply-chain-attack release waiting period); dsh-m just dumped a screen of raw pnpm output as a generic failure.
1196
- - **Fix**: the manager-result judgment recognizes the policy code and translates it into an honest guide — naming the pending entry and its publish time, deriving when a retry will pass, and offering the interim path (install the same version from DSH Web). No policy bypass, ever (the waiting period is the anti-attack red line; same stance as dsh-market #732).
1197
- - Also: community chain ⑨ hang-cap assertion 2s→10s (timer starvation under the full parallel suite tripped it twice; the "never hangs forever" contract is unchanged).
1198
-
1199
- ### Fixed in 0.9.9 — No more red "primary route failed" notice once the mirror line took over (notice convergence)
1200
-
1201
- - **Scenario**: with the default dual-line registry (raw → jsDelivr), when the primary `default-raw` failed and the mirror succeeded, the settings page still showed a red "Remote notice: default-raw 失败:fetch failed…可稍后重试或检查网络后重试" — alarming against data that had already self-healed, with advice that didn't apply (intermittent raw.githubusercontent.com unreachability is exactly why the mirror line exists).
1202
- - **Fix**: a later line succeeding means the earlier failure self-healed — `errors` no longer carries it (the "effective source" row already states the live line, e.g. "GitHub 镜像(备用)"). When ALL lines fail into cache/bundled, the errors are still kept — that's the actionable signal. The custom chain (custom source failing into cache) keeps its warning, unchanged.
1203
- - Added a test seam for the default chain (`defaultRoutes` override); regression gates cover both convergence and the keep-on-total-failure cases.
1204
-
1205
- ### Fixed in 0.9.8 — Desktop package operations fully wired to the official manager (install always failed; upgrade/uninstall/self-upgrade opened)
1206
-
1207
- - **Why every install failed**: the Host API's desktop install branch called `desktopInstall(id, cfg, opts)` without the 4th `deps` argument — `getService` never reached the adapter, so desktop installs always failed with "official pluginManager service unavailable (fail-closed)" (100% reproducible, timing irrelevant). The `dshm_install` tool had the same defect.
1208
- - **Service resolution aligned with dsh-market** (borrowed from its official-desktop wiring, proven on this machine by two managed upgrades): the probe now checks both contexts (the hostCtx from the webServer inject callback first) and falls back to a short-timeout cordis `inject` to lazily wake the official service — `pluginManager` is lazy, so a one-shot `get` returns undefined until something used it. Only a genuinely absent service still refuses (the no-file-level-fallback red line is untouched).
1209
- - **Capability table widened (owner decision, following dsh-market strategy)**: desktop upgrade (installBundle over the installed bundle), uninstall (removeBundle) and self-upgrade (installBundle('dsh-m@latest')) are now delegated to the official manager — exactly how dshmarket performed the dsh-m 0.9.3→0.9.4/0.9.5 upgrades on this machine. Judgment discipline unchanged (application/stage first, overridden is not a failure, build-blocked returns structured pendingBuilds, listBundles re-read never fakes success). restart stays refused (Electron lifecycle belongs to the shell).
1210
- - All three entry points (GUI, installed-view actions, and the dshm_install / dshm_uninstall / dshm_upgrade tools) are wired in the same pass.
1211
-
1212
- ### Fixed in 0.9.7 — Upgrade chip on Desktop showed a red "upgrade failed" banner (capability refusal is guidance, not an error)
1213
-
1214
- - **Root cause**: the 0.9.1 upgrade chip always calls `self-upgrade`; the Desktop capability table refuses by design with a structured 409, but the client rendered that 409 like any other failure — a red "upgrade failed" banner. Per the capability table this is not a failure, it's a pointer to the official entry.
1215
- - **Fix**: `api()` now forwards the structured refusal fields (code/action/profile/guidance); on a 409 the chip click shows a **neutral info banner** with the official-entry guidance (the guidance text's single source stays server-side in `active-profile.ts`, zero duplication in the client); info banners stay for 12s. Real failures on Web still use the red error banner — unchanged.
1216
-
1217
- ### Fixed in 0.9.6 — "Clear finished" silently no-opped on failed records
1218
-
1219
- - **Root cause**: the 0.7.0 review consensus excluded failed/superseded records from clearing (kept for review), so clicking the button against a failed record did nothing with zero feedback — reported as a bug from the real Windows host (2026-10-01). An explicit clear click is not a "silent wipe"; the old consensus is overruled.
1220
- - **Fix**: "Clear ended" (formerly "Clear finished") now clears **all terminal states** (done/warned/failed/superseded); in-flight records (queued/running/input) are untouched. When there is nothing terminal to clear, the button is disabled with an explanatory tooltip instead of silently no-oping. The per-row ✕ still removes single records.
1221
- - English copy updated accordingly.
1222
-
1223
- ### Fixed in 0.9.5 — GUI read paths missed the active profile on Desktop (Installed tab always showed web)
1224
-
1225
- - **Root cause**: the 0.9.0 dual-profile wiring missed the `installed` / `market` read paths in the Host API — `listInstalledWithMeta` / `listMarket` fell back to `webProfileDir()`: on Desktop the Installed tab always said "no plugins installed in this web profile" and the market "installed" badges stayed empty (the agent-tool surface `dshm_list` was wired correctly, so only the GUI was wrong).
1226
- - **Fix**: both now pass `profileDir: profile.dir` + `profile: profile.name` (same as the tools surface; profileContext as the single source of truth); registry/community cache segments follow the `profile` parameter automatically.
1227
- - **Copy**: the hardcoded "web profile" in the installed empty/loading states and the profile hint is now profile-neutral (the actual path is still shown).
1228
-
1229
- ### Fixed in 0.9.4 — Fullscreen header eaten by the Windows Desktop titlebar (tabs / restore unreachable)
1230
-
1231
- - **Root cause**: on Windows Desktop the shell runs `titleBarStyle:hidden + titleBarOverlay` — the top 40px of the window is a full-width `-webkit-app-region:drag` band (drag hits are decided by layout and ignore z-index / paint order), and the OS-drawn — □ ✕ caption buttons float above everything. The maximized panel header landed inside that band: tab clicks were swallowed by window dragging and the restore/close buttons were covered by the system buttons — no way back.
1232
- - **Fix**: the same approach the shell uses for its own overlays — consume the `--dsh-windows-titlebar-height` variable the shell sets on `html` (fullscreen `top:var(…)`; floating `padding-top:max(24px,var(…))`, which also fixes the short-window edge case where the floating panel's top edge sat under the band).
1233
- - **Zero drift on Web**: browsers / DSH Web have no such variable — it falls back to 0px, byte-identical to 0.9.3.
1234
-
1235
- ### Fixed in 0.9.3 — Windows native support (verified on a real Windows host)
1236
-
1237
- - **Registry atomic writes**: temp file names were derived with `split('/')`, but Windows paths separate with `\` — the "basename" came out as the entire path, the temp-file open always failed silently, and every cache / accepted-metadata write silently failed; now uses `basename()`, identical on all three platforms.
1238
- - **Local-file registry address parsing**: `C:\…` drive paths and `\\server\share` UNC were misread as URL schemes (with a misleading "HTTPS only" error), and `file://C:/…` normalized to a drive-less dangling path that always failed with ENOENT; all three local forms (POSIX absolute / Windows drive / UNC) now parse correctly as file kind.
1239
- - **Build**: `node_modules/.bin/tsc` is a POSIX sh shim; on Windows `spawnSync` failed with ENOENT — the build now runs TypeScript's JS entry with the current Node, identical on all platforms.
1240
- - **Test surface**: on a real Windows host the full 829-case suite went from 41 failures to **0** (821 pass / 8 skipped); POSIX-only contracts (process-group/SIGTERM timing, symlink privileges, platform path assertions, fixed-sleep timing) are now labeled skips, platform-neutral, or poll-based — test contracts unchanged.
1241
-
1242
- ### Fixed in 0.9.2 — Header chip stuck on the old version after a service restart
1243
-
1244
- - **In-place refresh once the restart is confirmed**: the moment the boot id confirms DSH Web is back, the panel re-fetches ping — the `dsh-m vX.Y.Z` chip and the profile chip sync to the new process's data instead of lingering on the old version (observed on 0.9.1: chip upgrade + restart still showed v0.9.0).
1245
- - **Refetch when the page becomes visible again**: with the panel left open while the service is restarted externally, returning to the tab refetches ping via `visibilitychange` (zero polling cost).
1246
- - **Chip version guard**: a self-check verdict whose version doesn't match the current process (stale verdict from the old process) is treated as silent, preventing "v0.9.1 ⬆ v0.9.1"-style misrenders.
1247
-
1248
- ### New in 0.9.1 — Header version chip upgrade notice (lights up only when an update exists)
1249
-
1250
- - **Silent by default**: the panel-header `dsh-m vX.Y.Z` chip keeps the static look introduced in 0.7.5 — up to date, failed check, or a local dev build ahead of npm (ahead) never interrupt (ahead only shows a "local dev build" hint in the hover title).
1251
- - **Lights up only when outdated**: when `self-check` (npm latest vs the installed version, read-only) finds a newer version, the chip turns into a warn state showing `⬆ v<latest>`; clicking it runs `self-upgrade` (same mutation session + post-install guard) and a "⚡ Restart" banner follows on success. On Desktop the click is rejected structurally by the capability table (409, with official lifecycle guidance).
1252
- - **TTL cache + version guard**: check results are cached in browser localStorage for 30 minutes so opening the panel doesn't hit npm every time; after an upgrade + restart the cache auto-invalidates by version. Failed checks stay silent — no error is shown.
1253
-
1254
- ### New in 0.9.0 — Official Desktop (dual profile) support
1255
-
1256
- - **One package, two profiles**: dsh-m now installs into the official Desktop's `desktop` profile (`~/.dsh/profiles/desktop`) alongside the Web `web` profile; the catalog, panel, and agent tools are shared, and everything manages the host's current profile (official `profileContext` as the single source of truth).
1257
- - **Entrust admission to the host**: every `/dshm` method (including ping and unknown methods) delegates to the official `connection.requestRejection()` before any body is read (trustedHosts / loopback / cross-site / `Origin: null` semantics come from the host); rejected requests consume no body and call no business logic; hosts without the capability fail closed. **Behavior change**: the old home-grown "missing Origin → 403" guard is gone — Origin-less requests from the Desktop bridge are admitted per official semantics, and unauthenticated health probes now follow host trust instead of a blanket 403.
1258
- - **Desktop first-release capability table**: read-only market + **installing new packages** (delegated to the official `pluginManager.installBundle`; integrity, locking, and application phases stay with the official manager) + **toggles** (delegated; structured refusal when the service is absent, never a file-level fallback); **upgrade / uninstall / self-update / one-click restart** return structured 409 refusals with official-entry guidance (no official upgrade API exists; restarts belong to the Electron lifecycle); build scripts retry with the exact official `pendingBuilds` list — never an allow-all.
1259
- - **Per-profile read model and cache**: market installed-badges, the installed list, and README previews only read the current profile; registry / community / accepted-source caches are segmented per profile (web keeps its legacy paths — zero migration, zero clearing); favorites and operation logs stay per browser origin and do not sync between Web and Desktop.
1260
- - **CLI always targets the web profile**: `--profile web` is explicit; `--profile desktop` is rejected with a pointer to the official Desktop plugin management page.
1261
- - **Honest limitations**: Desktop on-device (Win/macOS) E2E has not been run; the `registry.json` verified arrays gain **no** Desktop generation (to be recorded after real testing); dsh-m's file-level post-install guard is skipped on Desktop (app.asar probing blind spot) and replaced by official result checks plus a `listBundles` re-read.
1262
-
1263
- ### Fixed in 0.8.5
1264
-
1265
- - **dshm_upgrade fake success on guard blocks**: when an upgrade hit the post-install guard (e.g. link/file-sourced plugins cannot be auto-rolled-back), the text output was mis-rendered as "✅ undefined 已升级(最新)"; it now reports the block reason, compensation status and repair basis honestly, consistent with the card title (Guard block).
1266
-
1267
- ### Changed in 0.8.4 — Category labels now follow the UI language
1268
-
1269
- - **Community categories**: English names for the 23 known categories come straight from the upstream catalog's `categories.en`; under the English UI, category chips and the category row in detail/favorite modals show English. Upstream categories without an English name fall back to Chinese and keep rendering as raw slugs in the temporary group until labels land in a release. The Chinese UI is unchanged.
1270
- - **Curated categories**: the five chips now go through the bilingual dictionary (Market/Tools/UI/Search/Other ⇄ 市场/工具/界面/搜索/其他), consistent with the detail modal.
1271
- - **Implementation**: the summary carries `categoryLabelsEn` (derived from the upstream catalog in `communityOutcome`; ids without English are omitted — no second hand-maintained table), and the client merges per UI language. 0.8.1–0.8.3 were local iteration numbers with no separate changelog surface.
1272
-
1273
- ### Changed in 0.8.0 — Settings page redo (aligned with the zoned dual-catalog positioning)
1274
-
1275
- - **Information architecture**: Community catalog (from awesome-dsh-plugin) → Curated registry (registry.json) → dsh-m itself → About; the primary registry's user-facing name is now unified as "Curated registry".
1276
- - **Community catalog toggle**: a new GUI switch (live effect, instant, no confirm; when off the whole card collapses to one line); new `set-community` API.
1277
- - **Curated registry slimmed**: status fields consolidated from 7 rows to 4 (merged address, removed the hard-coded "caching" row); config status only appears when abnormal; buttons reduced to "Force refresh / Validate & apply / Restore default / Download default registry"; "Check entries reachability" removed from the GUI (`registry-diagnose` API kept).
1278
- - **Copy fixes**: the custom-source note now says it replaces the whole primary registry and only affects the Curated zone; removed the hard-coded TTL description that didn't match reality (`timeoutMs`/`cacheTtlMin` remain config-file settings).
1279
- - **dsh-m itself**: when the local dev build is ahead of npm, show "local dev build" instead of the misleading old "npm latest" (new `ahead` field).
1280
- - **About**: copy aligned with the current positioning, plus GitHub repo and issue-tracker links.
1281
-
1282
- ### Polished in 0.7.10
1283
-
1284
- - **Head split into three groups**: a hairline divider between the title and tab navigation — "title | nav | status + window controls" reads clearly.
1285
- - **Version badge unified color**: the v-number no longer uses the bright primary text color; it matches the dsh-m name in secondary gray.
1286
- - **Vertical rhythm consolidated**: window-control cells 26→28px to match tab height; version badge adjusted to 24px; maximize/restore icons unified at 12px; title weight 700→600.
1287
-
1288
- ### Fixed in 0.7.9
1289
-
1290
- - **Category chips keep a stable order**: retired the "promote active to front" reordering (every click on a clipped category used to jump it to first place). Instead, when the active category falls inside the collapsed clip region, the row auto-expands so the active filter stays visible — same goal, stable order; a manual collapse under the same active category is respected, and picking another clipped category or "All" resets it.
1291
-
1292
- ### Changed in 0.7.8
1293
-
1294
- - **Root cause fix for "maximize does nothing" — CSS hot-update self-healing**: the panel stylesheet was only injected once (`#dshm-css` present → skip), so after a hot update the stale stylesheet lacked rules for new classes (fullscreen/window controls) — new features appeared dead and buttons rendered as unstyled natives. The injected stylesheet now carries a content hash (djb2); reopening the panel after a bundle update swaps in the fresh styles automatically, no page refresh needed.
1295
- - **Window-control group recolored**: dropped the raised surface for a transparent background + hairline outline (same language as the search clear button); cells widened 34→44px against mis-clicks; close keeps its red hover.
1296
-
1297
- ### Changed in 0.7.7
1298
-
1299
- - **Fullscreen ported from dsh-market**: a maximize/restore control joins the panel head as a unified window-control group (equal-width maximize + close cells, hairline divider, shared SVG line icons; close tints red on hover). Fullscreen fills the viewport without rounded corners, the state is remembered in localStorage, and Esc still closes the panel.
1300
- - 0.7.6 catch-up: version badge de-bolded and made static (no copy-on-click); sticky category row's top gap fixed (sticky anchor shifted to offset container padding); close/clear buttons moved to the outlined flat style.
1301
-
1302
- ### Changed in 0.7.5
1303
-
1304
- - **Header version chip now shows dsh-m's own version** (`dsh-m v0.7.5`, click to copy; the DSH runtime version lives in Settings and `dshm ping`).
1305
- - **× close/clear buttons redrawn**: search clear, detail-modal close, and operation-row remove now use an SVG line icon on a dedicated hover-tinted button; the search clear button floats inside the pill's right edge.
1306
- - **Fixed the dsh-market peer warning**: `@deepseek-ai/dsh-tools` peer changed from `*` (strict semver never matches rc prereleases) to the explicit range `^0.1.7-rc.2 || ^0.2.0-rc.1 || >=0.2.0`; newer rc lines (e.g. 0.3.0-rc.x) need another entry.
1307
-
1308
- ### Changed in 0.7.4
1309
-
1310
- - **Panel tabs reverted to the classic segmented style** (0.7.2 mistakenly introduced underline tabs; the rounded button group with a highlighted active tab is back).
1311
- - **Full-width search**: fixed the search wrapper missing `display:flex`, which kept the input from stretching to the row.
1312
- - **Filter button and page-jump control recolored**: the filter button now uses an elevated surface with squared corners to stand apart from the pill category chips (accent outline when open); the page input is a slim pill with a hairline border and an accent-colored "Go" text button.
1313
- - **Sticky category row fully opaque**: the background now uses the opaque base token directly (the previous color-mix translucency still let card text bleed through on the dark theme); the bottom divider stays.
1314
-
1315
- ### Changed in 0.7.3 (includes 0.7.2)
1316
-
1317
- - **Home layout modeled after dsh-market**: the market view becomes "zone chips → full-width search row → category chips with a trailing Filter popover"; the popover gets its own look (rounded rectangle + leading chevron) and holds sort field (npm downloads/stars/date added), direction, and page size (the old sort dropdown and pager page-size select are retired).
1318
- - **Favorites cards open the detail modal**: fixed favorites-zone cards not responding to clicks — favorite snapshots lack full fields, so opening resolves the complete entry by id (in-memory zones → market API → snapshot fallback), reusing the detail modal and the full install path (peer confirm included).
1319
- - **Page-number jump**: the pager gains a page input — type a valid page and hit Enter or "Go" to jump.
1320
- - **Frosted sticky category row**: the sticky row now blurs content beneath it (backdrop blur + divider), so card text no longer bleeds through.
1321
- - **Removed by request**: the "discover/request listing" line (dsh-m does not accept listings), the "Tasks" button (operations panel is always visible again), and the "Refresh" button (force refresh lives in Settings).
1322
- - Note: registry entries carry no host-version requirement field, so dsh-market's "host version" filter has no data source here and was not replicated.
1323
-
1324
- ### Fixed in 0.7.1
1325
-
1326
- - **Community entries are installable again**: fixes a 0.7.0 regression where installing a community listing failed with "registry 中没有该条目" (install-by-id only consulted the primary registry, never the community catalog); the path now mirrors upgrades — on a primary miss the catalog is searched by id.
1327
- - **Compact market header**: search, refresh, and sort (community zone) move into the zone chips row; informational source banners ("official default registry / custom registry / served from local cache") are retired — their "{count} listings" figure was never wired up (always 0); the "registry unavailable" error and community fallback/stale hints remain.
1328
-
1329
- ### New in 0.7.0
1330
-
1331
- - **Zoned market** ([ADR-0004](./docs/adr/0004-zoned-market-display.md)): the dual-catalog data merge stays, but the display splits into Community (default) / Curated / Favorites zones; `dshm_search` and `dshm search` switch to `--source community|primary|all` + `--offset` real pagination (default 10 cards) — `primary_only` is retired.
1332
- - **Relevance search**: NFKC normalization + CJK↔Latin boundaries + field weighting (name/npm > owner > description > category > tags), multi-term same-field matching; whole-id exact match takes top priority.
1333
- - **Operation log + resume executor**, **local favorites + delisted cleanup**, **detail modal + screenshot lightbox**, community byline/deprecated badges/catalog-snapshot version fallback (never used for outdated).
1334
-
1335
- ### New in 0.4.0
1336
-
1337
- - **Enablement toggle**: one-click on/off per installed card; internally routed to a row override (single-row plugins, applies live) or bundle selection (multi-row); the write path delegates to the official `pluginManager` service and falls back to direct loader operations when absent ([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md)).
1338
- - **Live phase badges**: a projection of loader fiber state — failed plugins are visible at a glance.
1339
- - **Protection roster**: `dsh-m` itself plus the 16 official host lifeline modules cannot be toggled or uninstalled (upgrades unaffected).
1340
- - **Precise build approval**: when pnpm blocks build scripts, only the pending list is approved key-by-key; the allow-everything fallback is labeled honestly ([ADR-0002](./docs/adr/0002-precise-build-approval.md)).
1341
- - **Peer compatibility precheck**: install/upgrade validates `@deepseek-ai/dsh(-*)` peers against the runtime version before touching the profile (GitHub sources state the check was skipped); on mismatch the GUI asks, the agent tool returns structured data, the CLI takes `--force`.
1342
- - **Verified runtimes**: an optional `verified` array per registry entry records DSH runtimes actually tested — a claim of record, not a prediction; display-only, never gates installs.
1343
- - **Bundle identity check**: post-install warning when a package lands without a patch layer ("installed as a plain dependency").
1344
-
1345
- ### Retired in 0.4.x
1346
-
1347
- - **Metadata source probe**: 0.4.0 introduced an npmjs / npmmirror ping race to pick the metadata read source; it has been removed entirely (including the `probeEnabled` / `probeTimeoutMs` / `probeCacheTtlMin` settings and the settings-page display). The official counterpart probe only pre-selects a registry in the interactive install dialog, which dsh-m does not have, and npmjs measured consistently faster from the host, so the probe always equaled the default. Metadata reads now always use npmjs, matching the install path (profile `.npmrc` default).