better-dsh 0.2.2-a → 0.2.2-b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/docs/50_test-reports/v0.2.4-ios-focus-zoom-suppression/345/256/236/346/265/213/346/212/245/345/221/212.md +158 -0
  2. package/docs/60_exploration-and-research/bun-compile-cordis-runtime-bootstrap-research.md +348 -0
  3. package/docs/60_exploration-and-research/dsh-mobile-spa-ios-input-experience-research.md +160 -0
  4. package/lib/index.d.ts +9 -1
  5. package/lib/index.js +253 -3
  6. package/package.json +1 -1
  7. package/docs/REPL-/345/267/245/345/205/267/350/260/203/347/224/250-/346/210/252/346/226/255/350/257/212/346/226/255.md +0 -110
  8. package/docs/adr/0001-bridge-tool-layer-not-service-layer.md +0 -14
  9. package/docs/adr/0002-masking-is-presentation-only.md +0 -15
  10. package/docs/plans/A2A-messaging-channel-test-archive.md +0 -256
  11. package/docs/plans/code-mode-vs-rlm-ipython-comparison.md +0 -137
  12. package/docs/plans/dashr-blueprint-review.md +0 -201
  13. package/docs/plans/dashr-blueprint.md +0 -561
  14. package/docs/plans/dashr-compaction-window-and-archive.md +0 -307
  15. package/docs/plans/dashr-profile-layer-feasibility.md +0 -367
  16. package/docs/plans/dashr-sandbox-escalation-semantics-gap.md +0 -171
  17. package/docs/plans/dashr-security-sandbox-analysis.md +0 -187
  18. package/docs/plans/dashr-surface-invariant-and-omp-imports.md +0 -97
  19. package/docs/plans/ipython-kernel-interactive-interface-test-report.md +0 -152
  20. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft.md +0 -146
  21. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v3.md +0 -50
  22. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v4.md +0 -79
  23. package/docs/plans/kernel-refactoring/Dash-vs-PrimeAgent-systemprompt-toolcatalog-comparison.md +0 -138
  24. package/docs/plans/kernel-refactoring/RLM-system-prompt-injection-gap-report.md +0 -161
  25. package/docs/plans/kernel-refactoring/V0.1.5-development-plan.md +0 -109
  26. package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_dsh.md +0 -50
  27. package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_prime.md +0 -113
  28. package/docs/plans/recallable-compaction.md +0 -147
  29. package/docs/plans/spike-tag-repro.mjs +0 -102
  30. package/docs/plans/upstream-analysis.md +0 -128
  31. package/docs/v0.1.8-improved-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -142
  32. package/docs/v0.1.8-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -193
  33. package/docs/v0.1.8b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -96
  34. package/docs/v0.1.8c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -127
  35. package/docs/v0.1.8d-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -150
  36. package/docs/v0.1.8d_artifacts/README.md +0 -138
  37. package/docs/v0.1.8d_artifacts/code-mode-repl-only.observation.md +0 -74
  38. package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.jsonl +0 -3890
  39. package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.w-sample-0435.jsonl +0 -544
  40. package/docs/v0.1.8d_artifacts/functions.json +0 -592
  41. package/docs/v0.1.8d_artifacts/skills-catalog.snapshot.md +0 -30
  42. package/docs/v0.1.8d_artifacts/tools-sdk.output-schemas.json +0 -1236
  43. package/docs/v0.1.8d_artifacts/tools-sdk.python.txt +0 -592
  44. package/docs/v0.1.8d_artifacts/tools-sdk.typescript.txt +0 -516
  45. package/docs/v0.1.8d_artifacts/wire-vs-transcription.diff.md +0 -54
  46. package/docs/v0.1.8e-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -224
  47. package/docs/v0.1.9a-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -168
  48. package/docs/v0.2.0b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -123
  49. package/docs/v0.2.0b_artifacts/f2probe/Cargo.lock +0 -7
  50. package/docs/v0.2.0b_artifacts/f2probe/Cargo.toml +0 -6
  51. package/docs/v0.2.0b_artifacts/f2probe/src/bin/messy.rs +0 -8
  52. package/docs/v0.2.0b_artifacts/f2probe/src/main.rs +0 -4
  53. package/docs/v0.2.0b_artifacts/hashline-probe.md +0 -5
  54. package/docs/v0.2.0b_artifacts/slowprobe/Cargo.lock +0 -7
  55. package/docs/v0.2.0b_artifacts/slowprobe/Cargo.toml +0 -7
  56. package/docs/v0.2.0b_artifacts/slowprobe/build.rs +0 -4
  57. package/docs/v0.2.0b_artifacts/slowprobe/src/main.rs +0 -13
  58. package/docs/v0.2.1-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -110
  59. package/docs/v0.2.1b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -86
  60. package/docs/v0.2.1c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -66
@@ -0,0 +1,158 @@
1
+ # v0.2.4 iOS Focus 放大抑制(zoomGuard)实测报告
2
+
3
+ - **Change**: `openspec/changes/2026-09-03-ios-focus-zoom-suppression`
4
+ - **版本**: better-dsh 0.2.2-a → **0.2.4**(canonical `dashr/package.json`;npm 已发布线 v0.2.3 顺延一档)
5
+ - **日期**: 2026-09-03
6
+ - **范围**: 纯 host 半(boot script 增量 + config 面 + payload 扩键);client 半零改动,无需 build-client
7
+ - **状态**: 单测/构建层自测完成;4999 实例验证(2.3)与真机观察(3.x)待做
8
+ - **状态**: 4999 实例验证(2.3)✅ 完成(含一轮 blocker 返修 commit `59870fc`);真机观察(3.x)待 user
9
+ ---
10
+
11
+ ## 一、实测范围与结果表
12
+
13
+ | # | 项目 | 层面 | 结果 | 证据 |
14
+ |---|------|------|------|------|
15
+ | 1 | vitest 全量(canonical) | 单测 | ✅ **460/460**(基线 428 + 新增 32;36 files) | `cd dashr && npm test` |
16
+ | 2 | tsc --noEmit(含新 spec) | 类型 | ✅ 0 错 | `npx tsc --noEmit` |
17
+ | 3 | schema:默认值 / off 透传 / font·bogus fail-loud | 单测 | ✅ | `test/zoom-guard.spec.ts` 'config schema' |
18
+ | 4 | 纯函数矩阵(UA × 视口 × config;幂等;还原;bare token;大小写) | 单测 | ✅ 32 tests | 同上 |
19
+ | 5 | 生成脚本形状(ES5、无 `</script`、断点派生 `(max-width:767.98px)`、payload 序列化) | 单测 | ✅ | 同上 'generated zoomGuard section' |
20
+ | 6 | **boot script 评估端到端**(stub DOM 执行真实脚本文本) | 单测 | ✅ 全矩阵 | 同上 'boot script evaluation'(12 场景) |
21
+ | 7 | 构建产物复核(lib/index.js 抽取嵌入函数源重组段 eval) | 构建 | ✅ ES5-ok + provisional/reconcile/还原/风暴全绿 | `.scratch/zg-built-check.ts` |
22
+ | 8 | monorepo 副本 rsync + tsdown(host 半) | 构建 | ✅ 9 files 528.31 kB | `pnpm --filter better-dsh exec tsdown` |
23
+ | 9 | dump-config 覆盖层渲染 `mobile.zoomGuard` | 集成 | ✅ `'off'` 逐字渲染于 dashr-repl 行 | `--patch .scratch/zg-overlay.yml` |
24
+ | 10 | 4999 实例(boot script 注入形状 + CDP 视口矩阵 + off 逃生门 + 桌面 user 复验) | 集成 | ✅ **9/9 矩阵 + 4 冒烟 + off PASS + 桌面 PASS**(经一轮 blocker 返修,见八) | §八;脚本 `.scratch/zg-verify/` |
25
+ | 11 | 真机 iOS PWA 观察(放大抑制/键盘自愈/回弹) | 真机 | ⏳ **待做**(user) | tasks 3.1–3.3 |
26
+
27
+ ## 二、实现要点(含 design.md 偏差说明)
28
+
29
+ 1. **纯函数 = 发布源**:`src/mobile/zoom-guard.ts` 的 `isIOSClassUA` / `mergeViewportTokens` / `shouldApplyZoomGuard` 以 ES5 自包含风格编写,boot script 经 `Function.prototype.toString()` 嵌入这三个函数 —— 单测覆盖的源码与页面执行的源码字节一致(构建后产物已复核该嵌入幸存于 tsdown bundle)。
30
+ 2. **schema enum 形式**:本 fork schemastery 无 `choice()`;采用 `z.union(['meta','off']).default('meta')`(字面量 union 即 enum,非法值 config load 即抛)。**'font' 预留值位不进 enum**(决策:未实现值若被 schema 接受则运行期静默 no-op,fail-loud 优于静默;预留以注释标注)。
31
+ 3. **⚠ 与 design D4 的重要发现级偏差(已按 D4 精神实现)**:webserver 的 head 注入 splice 点在 `<head>` 开标签**紧后**(`packages/host/webserver/src/injections.ts` `renderIndexInjections`),即 boot script 执行时上游 stock viewport meta(`apps/web/index.html:5`)**尚未解析**——"meta 缺失 → 创建"是常态路径而非边角。若照 D4 字面创建后不管,页面将永久双 meta,依赖引擎未定义的 multi-meta 合并语义,且 spec 的"还原 stock content"场景失义。实现为 **provisional meta + MutationObserver reconcile**:脚本执行时即建 provisional meta(抑制令牌先于一切 bundle 生效,时序要求满足)→ parser 插入 stock meta 时记录真 stock、token merge 改写 **stock meta 本体**、移除 provisional → 单 meta 文档;此后 resize 进出断口均以记录的 stock 为基线改写/还原(字节级还原)。无 MutationObserver 的引擎降级为 provisional 常驻(仍具抑制)。两路径(stock 先在场 = 直接改写;stock 后到 = reconcile)均有单测钉住。
32
+ 4. **payload 面**:`zoomGuard` 与 breakpoint 等键同构(配置了才进 payload,schema 默认保证生产必带 'meta');`zoomGuard:'off'` 时整段不生成(逃生门 = 零脚本段,非运行期判断)。
33
+ 5. **配置生命周期**:zoomGuard 随 mobile 腿(`mobile.enabled=false` 时整个 `__DASHR_MOBILE__` 不注入,zoomGuard 一并不生效)——design D5 "同路径同生命周期" 的字面执行。
34
+
35
+ ## 三、环境事实
36
+
37
+ - canonical:`/home/u1/workspaces/dashr/dashr`(node_modules 在场,vitest/tsc 直接跑)
38
+ - monorepo 副本:`upstream/deepseek-harness/packages/better-dsh/better-dsh`(rsync + tsdown host 半)
39
+ - Node v22.22.1 / vitest 4.x / tsdown 0.15.x
40
+ - dump-config:`DSH_HOME=.dsh-test npm run dsh -- web --dump-config --patch .scratch/zg-overlay.yml`(dump-config 只渲染 patch 层,schema 默认由单测钉住)
41
+
42
+ ## 四、boot script 评估测试明细(单测第 6 项)
43
+
44
+ stub DOM(El/Document/MutationObserver/live matchMedia/resize 记录),`new Function` 执行 `buildBootScript` 真实输出:
45
+
46
+ | 场景 | 断言 |
47
+ |------|------|
48
+ | iPhone ∧ 窄 | 脚本时 provisional meta(`maximum-scale=1, user-scalable=no`);stock 解析后单 meta、content = `width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no` |
49
+ | 非 viewport 解析插入 | observer 不误触发,继续守望 |
50
+ | 宽↔窄往返 + 5 连 resize 风暴 | 离带 = 字节级还原 stock;回带 = 合并;风暴零叠加 |
51
+ | stock 先在场(worker 形态) | 直接改写,无 provisional |
52
+ | 无 stock 页面 | provisional 常驻;离带移除自建 meta |
53
+ | iPad / iPadOS 桌面 UA+多点 | 改写生效;Mac 桌面(单/零触点)不生效 |
54
+ | Android ∧ 窄 / 桌面任意宽 | stock 原样、零监听器 |
55
+ | iPhone 宽载入后旋转入带 | 后补改写生效 |
56
+ | zoomGuard off | 无脚本段、零改写、零监听 |
57
+ | authorities + mobile 双腿 | `__DSH_TRANSPORT__` 与 zoomGuard 互不干扰 |
58
+ | 断点 900 | matchMedia 收到 `(max-width:899.98px)`,payload 带 `"breakpoint":900` |
59
+
60
+ ## 五、真机观察(user,PWA standalone 主用法)—— TODO
61
+
62
+ > 以下三章按 design D6 留给 user 真机执行(4999 或真实入口,iOS 版本号请记录)。
63
+
64
+ ### 5.1 放大抑制判定(tasks 3.1)— TODO
65
+
66
+ - [ ] Safari 态:focus composer / permission select / settings 各 input / 切会话自动聚焦 —— 无 115–120% 放大
67
+ - [ ] PWA standalone 态:同上全过
68
+ - [ ] 双指缩放仍可用(iOS 10+ 应忽略 user-scalable=no 对 pinch 的约束)
69
+ - [ ] `zoomGuard:'off'` 时放大复现(逃生门复验)
70
+
71
+ ### 5.2 键盘遮蔽自愈判定(tasks 3.2)— TODO
72
+
73
+ - [ ] PWA 态 unzoomed 下 focus composer:键盘弹出后 composer 是否被引擎 resize 抬到键盘上方(innerHeight/100dvh 收缩联动)
74
+ - [ ] 键盘关闭后 viewport 回弹(对照 dev.to 2026-07 记录的 iOS 17/18 standalone 卡死 bug)
75
+ - [ ] Safari 态对照记录
76
+
77
+ ### 5.3 观察结论回填(tasks 3.3)— TODO
78
+
79
+ - [ ] 研究文档 v2 §6 决策点 3/4 回填
80
+ - [ ] follow-up 取舍:visualViewport shim / display-flip 自愈 / focus gate 讨论输入
81
+
82
+ ## 六、发现的瑕疵与去向
83
+
84
+ - **发现(非瑕疵)**:head 注入 splice 位于 stock meta 之前 → 促成 provisional+reconcile 设计(见二.3);已进 upstream-alignment S7 查表第 8 条(viewport 行形状 + splice 次序两查点)。
85
+ - dump-config 只渲染 patch 层、不显 schema 默认 —— zoomGuard 默认值的可观测面在 payload(4999 shell 页)与单测,不在 dump-config;如需 dump 可见需显式配置(本报告第 9 项即此形态)。
86
+
87
+ ## 七、Open items
88
+ - ~~tasks 2.3(4999 实例验证)~~ ✅ 完成(见八;返修 `59870fc` 后 9/9)
89
+ - tasks 3.x 真机观察 —— user
90
+ - 发布/npm publish 与 prod 部署 —— 另按年龄门流程,不在本 change 内
91
+
92
+ ---
93
+
94
+ ## 八、4999 实测记录与 blocker 返修轮(lead,2026-09-03 晚)
95
+
96
+ ### 8.1 首轮矩阵与 blocker 发现
97
+
98
+ - 冒烟四断言全过:A)shell head 内联脚本含 zoomGuard 段;B)boot graph 含 `"id":"better-dsh"` 与 client URL;C)zoomGuard 段(HTML 偏移 25313)早于应用入口 module script(27056)——注:client-modules 的 combo 注册脚本(1286)先于我们执行,但纯工厂注册无 DOM/focus,行为层时序满足 S2;client.js 单包 URL 200 且与 `lib/client/index.js` 前缀字节一致(+84B sourceMappingURL,S7 惯例)。
99
+ - **CDP 视口矩阵首轮 7/9**:两个失败项均为**初始窄载入不 guard**(iOS+390、iPadOS 冒充+390);跨断口 resize 与事后 synthetic resize 均生效。
100
+
101
+ ### 8.2 根因定位(诊断脚本 `diag.mjs` / `isolate.mjs`)
102
+
103
+ - 事件时序实测:boot script 执行于 head 解析期,此时布局视口为 viewport-meta 未应用前的默认宽 **980**(`docStart innerWidth=980, mq.matches=false`)→ 首评不 guard;引擎随后应用 `width=device-width` 收窄到 390,但**该转换在载入期间不向页面派发 `resize` 事件**(探针隔离三变体一致;我们的 resize 监听在位且事后手动 dispatch 立即生效)。
104
+ - 关键后果:deferred module script(React 挂载 → unlock effect 自动聚焦)在 DOMContentLoaded 前执行——依赖 resize 事件的补评可能晚于首次 auto-focus。**初始窄载入 = iPhone 用户打开 DSH 的主路径,blocker 级**。
105
+
106
+ ### 8.3 返修(doer 第二轮,commit `59870fc`,tag `v0.2.4` 前移)
107
+
108
+ - **早期载入重评梯子**:首评未 applied 时启动 ~10ms poll 调 `re()`,三停条件 = applied / readyState complete / 200 tick(~2s);`apply(true)` 主动取消待发 tick,稳态零 timer。guard 不再依赖任何事件通知——事件缺失只影响"多快"发现,不影响"是否"发现;首次布局必然先于 deferred scripts,抑制仍落在任何输入可聚焦之前。
109
+ - **MQ change 监听兜底**(`mq.addEventListener('change', re)`,老引擎 `addListener` 形态)与 window resize 双通道。
110
+ - 测试 466/466(+6:晚翻无 resize、readyState 停止、非 iOS 零 setTimeout、MQ 通道、legacy addListener、梯子先于 stock 解析落 provisional);tsc 0。
111
+
112
+ ### 8.4 返修后终验(全绿)
113
+
114
+ - **CDP 矩阵 9/9**:iOS+390 → guarded(单 meta、两 token 在位);iOS+1280 → stock;iPadOS 桌面冒充+touch+390 → guarded;Android+390 / 桌面+390 / 桌面+1280 → stock(零改写);跨断口三段(宽载入 stock → 入带 guard → 出带字节级还原 stock)。
115
+ - **off 逃生门 live PASS**:home 层 `.dsh-test/cordis.patch.yml` 覆盖(注:CLI `web` 子命令不认 `--patch`,该路仅 dump-config 可用)→ shell **整段不发射**、payload `zoomGuard:'off'`、iOS+390 live = stock。验后已删覆盖文件并还原默认态。
116
+ - **默认态恢复复验 + 桌面 user 复验 PASS**:还原后 iOS+390 → guarded;桌面(无 UA 伪装)meta stock 单条、app 完整渲染(root/composer/`[data-composer-card]`/sidebar/slots 在位,标题与占位文案正常)。
117
+
118
+ ### 8.5 遗留观察(非阻塞)
119
+
120
+ - 真机 iOS Safari 上 8.2 竞态是否同样存在未证(无法本地复现 iOS 引擎);梯子设计使其无关化——真机观察(五)会给出实证。
121
+ - 探针实验中一次"全探针"运行曾捕捉到 resize 送达(时序非确定论),进一步佐证不能依赖事件通道。
122
+ - CDP 测试脚本存 `.scratch/zg-verify/`(matrix/diag/isolate/off/final 五件),可复跑。
123
+
124
+ ## 九、v0.2.5 standalone 分流(change `2026-09-03-zoomguard-standalone-font-floor`)—— 骨架,4999/真机结论待回填
125
+
126
+ > 背景:真机首批观察(§五)——浏览器态 meta 改写完美(放大抑制 + pinch 保留),但 **Add to Home Screen PWA standalone 态引擎尊重 `user-scalable=no` → pinch 失效**(Discourse 注释未覆盖的形态;spec v1 的 "pinch remains engine-controlled" 括号在 standalone 不成立)。user 裁决:standalone 态完全不碰 viewport meta,改注入 16px 字号地板 CSS;browser 态维持 v0.2.4。user 主用法即 PWA。
127
+
128
+ ### 9.1 实现摘要(doer,2026-09-03)
129
+
130
+ - 纯函数(`src/mobile/zoom-guard.ts`,ES5 自包含,toString 嵌入,`var ZD=`/`var ZF=` 与 ZI/ZM/ZS 同款):`isStandaloneDisplay(mqMatchesStandalone, navStandalone)` = 严格 `=== true` 双源 OR(MQ 主源 + `navigator.standalone` 老 iOS 兜底);`buildFontFloorCss(bp)` → `@media (max-width:{bp-0.02}px){ input,textarea,select,[contenteditable="true"]{ font-size:16px !important } }`(断口派生与 meta 腿一致:768→767.98、900→899.98)。
131
+ - boot script 分流(D1):iOS 判定后、任何 meta 逻辑前 —— `var sa=ZD((window.matchMedia&&window.matchMedia('(display-mode: standalone)').matches),(navigator.standalone===true));` standalone → `headEl()` 注入 `<style id="ios-zoom-font-floor" data-plugin="better-dsh" data-plugin-css="better-dsh/zoom-font-floor">`(textContent=ZF(rawBp))后 **return**:其后 provisional/reconcile/监听/梯子代码字面不可达,meta 字节不动。**style 注入不设宽度门**(宽度由 CSS media query 表达,宽屏 standalone 也注入、规则自然不匹配)。
132
+ - `'off'`:整段不发射(web-trust 门不变、零改动);`'meta'` 语义升级为自动双形态(enum 不动)。web-trust.ts / client 半零改动。
133
+
134
+ ### 9.2 单测(canonical,doer)
135
+
136
+ - **478/478 + tsc 0**(v0.2.4 基线 466,+12):双源判定矩阵(含 undefined 单源兜底)、CSS 文本钉扎(selector 集合 / `!important` / 768→767.98 与 900→899.98)、standalone 矩阵(MQ 源:style 在场 + 三认领属性 + meta 字节不动 + 零 addEventListener/setTimeout/MutationObserver 痕迹 + 零窄带 MQ 探针;宽屏仍注入;navigator.standalone 单源;无 matchMedia 兜底;断口 900;browser 态显式无 style;off 双零)。
137
+ - stub 基建:`runBootScript` 增 `standaloneAtScriptTime`/`navStandalone` 参数,matchMedia 按查询串分流(display-mode 探针不再误读窄带裁定);browser 全矩阵零回归(mediaQueries 断言更新为含 display-mode 前导探针)。
138
+
139
+ ### 9.3 4999 CDP 双形态(lead,2026-09-03 深夜)—— ✅ 8/8 全绿
140
+
141
+ **⚠ 测试方法学发现(重要)**:Chrome 的 `Emulation.setEmulatedMedia` **不支持 `display-mode` 特性**(实测 pre-nav/post-nav 设置后页面内 `matchMedia('(display-mode: standalone)').matches)` 恒 false —— Chrome 可仿真特性集不含它,DevTools Rendering 面板亦无此项)。改用 **matchMedia API 边界注入**:`Page.addScriptToEvaluateOnNewDocument` 在一切页面脚本(含 head 注入 boot script)之前包一层 `matchMedia`,仅对含 display-mode+standalone 的查询返回 `{matches:true}`,其余透传 —— boot script 全部分支逻辑照真运行(自检探针 truth=true 证实注入生效)。
142
+
143
+ | 场景 | 断言 | 结果 |
144
+ |---|---|---|
145
+ | standalone+iOS+390 | meta **字节不动**(`width=device-width, initial-scale=1` 原文)= pinch 恢复前提 | ✅ |
146
+ | standalone+iOS+390 | `#ios-zoom-font-floor` 在场,认领属性齐(data-plugin / data-plugin-css=better-dsh/zoom-font-floor),CSS 文本与 user 提案逐字一致 | ✅ |
147
+ | standalone+iOS+390 | composer computed font-size = **16px**(地板生效) | ✅ |
148
+ | browser+iOS+390 | meta guarded(两 token 在场)、无 font-floor | ✅ |
149
+ | browser+iOS+390 | **client 特性探针**:sidebar computed 首列 0px、认领样式 98 条(v0.2.1f 特性在位) | ✅ |
150
+ | standalone+iOS+1280 | floor 已注入(宽度无关)但 media query 不激活(composer 14px) | ✅ |
151
+ | standalone+Android+390 | iOS 门 → 无 floor、meta stock | ✅ |
152
+ | off | 双形态零发射(单测钉住;'off' 代码路径 v0.2.4 未动) | ✅ |
153
+
154
+ 脚本 `.scratch/zg-verify/dual-mode.mjs`(含 matchMedia 边界注入实现);重启后 boot graph rev 已随 v0.2.5 变化,tsdown+build-client 双步完成、lib/client 在场(md5 `a88850ec…`)。
155
+ ### 9.4 真机复验(user)—— TODO(tasks 3.1)
156
+
157
+ - [ ] PWA standalone:pinch 恢复 + focus 无放大(输入面 16px)。
158
+ - [ ] 浏览器态:维持 §五 结论(meta 改写 + pinch 保留)。
@@ -0,0 +1,348 @@
1
+ # Bun 编译自包含二进制 × Cordis 运行时自举 — Feasibility 研究
2
+
3
+ > 记录:2026-09-03 · 一手核验:upstream checkout `dsh-v0.1.2-alpha.5` vendored loader/include/hmr 源码 +
4
+ > `packages/boot/app-boot` 源码 + Bun 官方 executables 文档(2026-09 抓取)+ oven-sh/bun#11732(API 实查
5
+ > state=open)+ vercel/turborepo#11900 patch(2026-02 合并的生产级修复)+ azu/bun-build-dynamic-import repro。
6
+ > 本文是 [cordis-research.md](./cordis-research.md) §4.2 的深化:那一节给了"可行但有工程成本"的一行结论,
7
+ > 本文把"成本"拆到确切的机制、确切的失败面、和确切的缓解配方。
8
+
9
+ **范围声明(owner 已裁决,不再争论)**:node-pty 等 native addon 对 Bun 预编译的不兼容是已知项,开发具体
10
+ 工具时 exclude 或换 `Bun.Terminal`,本文不展开(见 cordis-research.md §4.2 的政策)。本文只回答一个问题:
11
+ **Cordis 的运行时自举——扫配置、解析包名、动态 import 插件——在 `bun build --compile` 的自包含二进制里
12
+ 会发生什么,能不能救,怎么救。**
13
+ **会发生什么,能不能救,怎么救。**
14
+ >
15
+ > **修订 v2(2026-09-03 同日,owner 边界重述)**:划分线不是"第一方/第三方插件",而是 **Bun 预编译界**——
16
+ > 编译期可枚举集(含 vendored 第三方,不问出身)vs 编译后运行时动态集。据此新增方案 E(Bun 静态核 +
17
+ > Node sidecar 双运行时)、§2 B10–B12(OpenClaw / better-sqlite3 / execa 行为级 divergence 实证)。
18
+ ---
19
+
20
+ ## 0. 结论速览
21
+
22
+ **Feasible,但"纯自包含单文件"和"保留全部运行时模块动态性"二者不可兼得——这不是 Bun 的 bug,是 bundler
23
+ 与 plugin-host 的本质张力(Deno/esbuild/webpack 同款)。正确的目标形态是"编译核心 + 分层插件解析策略":**
24
+
25
+ - **编译期已知集 → 构建期冻结**(codegen 静态注册表,全部 bundle 进二进制,字面量动态 import 保懒加载)。
26
+ "已知集"不问作者出身:第一方插件、工具自有组件、vendored 依赖、依赖树里的第三方包——凡 build 时已在
27
+ 模块图内者皆是。Cordis 的动态性分两层:**配置树动态性**(yml 行的 insert/patch/config/inject/disabled,
28
+ 运行时解释,完全保留)与**模块集动态性**(import 哪个包,冻结侧冻结)。这条边界同时是 **shim 可行性
29
+ 的边界**:已知集内任何 Bun 不兼容都是 build-time 工程项(alias/shim/exclude,图在我们手里);无界集内
30
+ 没有收敛解。对工具发行,冻结核恰是 reproducibility 收益。
31
+ - **运行时动态集(编译后才到达的插件,天然 TS/Node 形态)→ 承接路线三条**:B(Bun 运行时内磁盘解析 +
32
+ walker patch,Turborepo 生产配方)、E(Node sidecar 真实运行时承接,§4-E)、C(runtime bundling
33
+ 桥接)。用户侧只解包不做版本解析的原则不变。
34
+ "closed packaged runtimes" 概念、"Built bins need the Loader's native helper for bare plugin
35
+ specifiers" 的自述——upstream 作者已经在想打包形态,且所有动态 import 收敛在**我们自己 vendor 的两个
36
+ choke point** 上,不是黑盒。
37
+ - 真正阴险的不是"找不到包"(fail-loud,好修),而是**双实例身份问题**(磁盘插件 import `@deepseek-ai/*`
38
+ 解析到磁盘副本 → 两个 cordis/Fiber 并存)。有解(external 共享树 / virtual namespace 桥接),但必须作为
39
+ build 纪律 + boot 时断言来执行。
40
+
41
+ ---
42
+
43
+ ## 1. "自举"到底指什么 — Cordis 运行时加载面盘点(源码级)
44
+
45
+ 把"运行时扫描 + 动态 import"拆成六个具体机制,每个的 bundle 敏感性完全不同:
46
+
47
+ ### 1.1 Boot 链与 baseUrl 锚定
48
+
49
+ ```
50
+ bin.js(薄壳)→ boot() 【packages/boot/app-boot/src/index.ts:777】
51
+ ├─ new Context() # 框架基底
52
+ ├─ await ctx.plugin(Loader) # vendored loader 挂载
53
+ ├─ ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/' # index.ts:784
54
+ └─ mountRootInclude(ctx, configPath, patches, bareModuleBaseUrl) # index.ts:789
55
+ ```
56
+
57
+ `baseUrl` = **配置文件所在目录**(profile 目录)。相对路径插件条目(`./foo`)以它为锚 → 这是纯磁盘锚定,
58
+ 编译二进制下 fs 照常工作,**不受影响**。
59
+
60
+ ### 1.2 `tree.import` — 所有插件模块 import 的收敛点
61
+
62
+ `vendor/loader/src/config/tree.ts:145`,三分支:
63
+
64
+ ```ts
65
+ import(name) {
66
+ if (name.startsWith('cordis:')) {
67
+ return this.ctx.loader.builtins[name.slice(7)] // ① 内存表,零 fs
68
+ }
69
+ if (this.ctx.loader.internal) {
70
+ return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {}) // ② Node 内部 loader
71
+ } else if (name.startsWith('.')) {
72
+ return await import(new URL(name, this.ctx.baseUrl).href) // ③a 相对 → 绝对 file URL
73
+ } else {
74
+ return await import(name) // ③b 裸包名,不可分析
75
+ }
76
+ }
77
+ ```
78
+
79
+ - **① `cordis:` builtins**(include/group 等):静态注册的内存表,bundle 安全 ✅
80
+ - **② Node internals 深集成**(见 1.3):Bun 下拿不到 → 走兜底,**graceful 降级 ✅**
81
+ - **③a 相对名**:先展开成绝对 file URL 再 import → 磁盘锚定,Bun 运行时可 import 磁盘 JS/TS ✅
82
+ - **③b 裸包名**:`await import(name)`,specifier 来自 yml/patch 行,**完全不可静态分析** ⚠️ —— 用户说的
83
+ "最要命"就是这一行(以及 1.4 的同款)
84
+
85
+ ### 1.3 Node internals 深集成(loader 的 `internal`)与 HMR
86
+
87
+ `vendor/loader/src/internal.ts` 的 `ModuleLoader.fromInternal()`:
88
+
89
+ - 门槛:`process.versions.node` major ≥ 22,且要拿到 `internal/modules/esm/loader` 的
90
+ `getOrInitializeCascadedLoader()` —— 靠 `--expose-internals` execArgv **或**
91
+ `require('node-addon-require-builtin')`(一个 native addon)。
92
+ - 拿不到 → `fromInternal()` 返回 `undefined` → loader 全链走 ③ 兜底。**这是文档化的降级路径,不是崩**。
93
+ - 唯一硬依赖者是 HMR(`vendor/hmr/src/index.ts:121`:`--expose-internals is required for HMR service`,
94
+ 它直接操作 Node 内部 ESM `loadCache` 做模块驱逐)。**HMR 是 dev-only 面,dev 线继续用 Node 跑即可**;
95
+ 编译二进制(本来就是 ship 形态)丢 HMR 无损。
96
+
97
+ ### 1.4 `mountRootInclude` 的裸包名 seam —— upstream 已经在为打包形态留口
98
+
99
+ `packages/boot/app-boot/src/index.ts:501-518`:当传了 `bareModuleBaseUrl`,root include 换成
100
+ `HostResolvedRootInclude`,把裸包名的解析基点从"配置工程"改指"**installed-host base**":
101
+
102
+ ```ts
103
+ if (internal === undefined) return super.import(specifier, getOuterStack)
104
+ return internal.import(specifier, bareModuleBaseUrl, {})
105
+ ```
106
+
107
+ docstring(index.ts:745-757)原文值得照抄,因为它证明 upstream 对本研究的主题已有预设计:
108
+
109
+ > bare package names resolve there by default or against an explicit `bareModuleBaseUrl` **for closed
110
+ > packaged runtimes** … use it when the host, rather than the configuration project, **owns the complete
111
+ > plugin set**. … Built bins **need the Loader's native helper for bare plugin specifiers**; relative
112
+ > specifiers do not. … The package build **embeds Include while leaving Loader external**, so the built
113
+ > include tree and host **share one Loader peer**.
114
+
115
+ 三句话三个信号:(a) "闭包打包运行时"已是设计词汇;(b) built bin + 裸包名 = 已知难点,当前答案是 Node
116
+ native helper(Bun 下没有 → 正是我们要替换的 seam);(c) 构建已经用 "external peer 保单实例" 的纪律
117
+ (Include 内嵌、Loader 留 external 共享)——**双实例问题不是新问题,是这个纪律的推广**。
118
+
119
+ ### 1.5 配置层的其余机制(全部 bundle 友好)
120
+
121
+ - `!!js` 表达式 = `new Function('ctx','expr',…)`(`vendor/loader/src/config/utils.ts:5`),纯 JS,无
122
+ `node:vm`,Bun 支持 ✅
123
+ - YAML/patch 行读写 = fs + yaml,`--dump-config` 预览同理 ✅
124
+ - profile 目录 = `package.json`(out-of-tree 插件 deps + `dsh.profile.bundles` 列序)+
125
+ `cordis.patch.yml` + pnpm `node_modules`(`packages/boot/app-boot/src/profile.ts:5-19`)——纯磁盘数据
126
+ 结构,二进制照读 ✅
127
+
128
+ ### 1.6 盘点结论
129
+
130
+ **"Cordis 会运行时自举"作为 barrier,其实精确化为两个 choke point 上的裸包名动态 import**:
131
+ `Tree.import` ③b 和 `HostResolvedRootInclude` 的裸名分支。两者都在我们自己 vendor/维护的代码里
132
+ (vendor/loader + app-boot),可patch面极小。其余自举面(builtins、相对路径、yml/patch/!!js、baseUrl)
133
+ 在编译二进制下要么天然安全,要么走文档化降级。**barrier 真实存在,但它是"两个函数的裸包名分支",不是
134
+ "框架级黑盒"。**
135
+
136
+ ---
137
+
138
+ ## 2. Bun `--compile` 的模块解析事实(2026-09 现状)
139
+
140
+ 全部带来源;这些是本研究的硬地基:
141
+
142
+ | # | 事实 | 来源 |
143
+ |---|---|---|
144
+ | B1 | 编译产物 = 全部被 import 的模块(含字面量动态 import,配 `--splitting` 保懒加载 chunk)+ **完整 Bun 运行时**;built-in Bun/Node API 全支持 | [Bun executables docs](https://bun.sh/docs/bundler/executables)(splitting 示例本身就是 `await import("./lazy.ts")` 编译后免磁盘可用) |
145
+ | B2 | **非可分析动态 import 的裸包名**:运行时从导入者的虚拟位置 `/$bunfs/root/…` 向上找 node_modules → 必败:`Cannot find package "rambda" from "/$bunfs/root/bun-example"`(`import.meta.resolve` 同败) | [azu/bun-build-dynamic-import](https://github.com/azu/bun-build-dynamic-import) repro |
146
+ | B3 | 请求"用 flag 收编非可分析动态 import"的 issue **至今 open**(enhancement/bundler,7 评论,2024-06 开,2025-11 仍有活动,无里程碑);Deno 同问题已用 `--include <path>` 解决 | [oven-sh/bun#11732](https://github.com/oven-sh/bun/issues/11732) |
147
+ | B4 | 编译产物内 `createRequire().resolve()` **不可靠**(不沿 node_modules 祖先上溯)→ 生产级 workaround = 手写 node_modules 目录 walker,把**绝对路径**喂回去,Node builtin 标 `external: true` | [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900)(fix #11882,2026-02) |
148
+ | B5 | 相对路径若不在 bundle 内 → **从进程 cwd 读磁盘**,不存在则报错(Worker/SQLite 章节明示 cwd 锚定语义) | [Bun executables docs](https://bun.sh/docs/bundler/executables) |
149
+ | B6 | 内嵌运行时是完整的:编译产物可 `BUN_BE_BUN=1` 直接当 **bun CLI** 用(install/run/打包),即二进制自带转译器与包管理器——**用户侧可以完全不装 Node/pnpm** | 同上(v1.2.16+) |
150
+ | B7 | `Bun.build` 在编译产物内可用 → **运行时打包**模式成立:Turborepo 在编译产物里对用户磁盘上的 TS 配置现场 `Bun.build`,并用 onResolve/onLoad **virtual namespace 把二进制内置模块桥接给用户代码**(`BINARY_MODULES`),node builtin 走 external | [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900) |
151
+ | B8 | `--asset ./dir` 可整树内嵌,运行时经 `import.meta.dir`(虚拟根 `/$bunfs`)+ `node:fs`(readdirSync 等)可达;`with {type:"file"}` 内嵌文件同理;`Bun.isStandaloneExecutable` 可探测编译态;bytecode/sourcemap/minify/交叉编译齐备;`--compile` 不支持 `--outdir/--public-path/--no-bundle` | 同上 |
152
+ | B9 | `.env`/`bunfig.toml` 运行时自动加载(默认开),tsconfig/package.json 默认不加载(可开) | 同上 |
153
+ | B10 | **行为级 divergence(隐性、不可枚举)**:Bun 的 spawn 会消费 `encoding` 选项 → execa 的 `encoding:"buffer"` 组合直接抛 `ERR_UNKNOWN_ENCODING`(stable 1.3.14 仍复现);`bun install` 解不了 pnpm workspace 布局。这类不兼容**踩到才知道**,无静态清单可穷举 | [openclaw#114256](https://github.com/openclaw/openclaw/pull/114256)(2026-07 merged)引 [oven-sh/bun#36049](https://github.com/oven-sh/bun/issues/36049) |
154
+ | B11 | `node:sqlite`:Bun ≤1.3.x **不提供**;1.4.0 canary(**Rust 重写线**)起提供(`DatabaseSync` 实测可用)。388k★ 的 OpenClaw 接法 = `process.getBuiltinModule('node:sqlite')` **feature-probe,不做品牌/版本门** | [openclaw#114256](https://github.com/openclaw/openclaw/pull/114256) |
155
+ | B12 | `better-sqlite3`:可跑但**需重编译**;且有真实 N-API crash 服务启动失败案例,社区以 `bun:sqlite` 规避——native addon 在 Bun 下的"能用"是逐版本、逐包的灰色地带 | [oven-sh/bun#16050](https://github.com/oven-sh/bun/issues/16050)、[opencode-telegram-bridge#34](https://github.com/gabriel-trigo/opencode-telegram-bridge/issues/34)、[OmniRoute#11468](https://github.com/diegosouzapw/OmniRoute/pull/11468) |
156
+ 推论:**Bun 编译世界的模块解析 = "bundle 内虚拟根(`/$bunfs`)+ 磁盘(绝对路径/cwd 相对)"两界**。裸包名
157
+ 只在磁盘界有 node_modules 语义,而 bundled 模块住在虚拟界 → B2 必败。这就是 1.2 ③b 在编译产物下的死因。
158
+
159
+ ---
160
+
161
+ ## 3. 冲突矩阵:Cordis 自举面 × Bun compile
162
+
163
+ | Cordis 机制(§1) | Node 语义 | Bun compile 语义 | 差距判定 |
164
+ |---|---|---|---|
165
+ | `cordis:` builtins(include/group) | 内存表 | bundle 内 ✅ | 无 |
166
+ | 相对名插件(`./x` → baseUrl 绝对 URL) | 磁盘 import | 磁盘 import(运行时转译 TS 可用,B1/B6/B7) | 无 |
167
+ | **裸包名(`Tree.import` ③b / `HostResolvedRootInclude`)** | node_modules 分层上溯(①→④ 层) | **B2 必败**(虚拟根无 node_modules);`createRequire` 也不可靠(B4) | **核心差距,§4-B patch** |
168
+ | `internal`(Node cascaded loader) | 经 native addon/`--expose-internals` 取 internals | 取不到 → `fromInternal()`=undefined → **文档化降级**到兜底 import | 无阻断(丢深集成) |
169
+ | HMR(loadCache 驱逐) | 硬依赖 internals | 直接抛 `--expose-internals is required` | **放弃于编译形态**;dev 线保留 Node |
170
+ | `!!js` / yml / patch 行 / `--dump-config` | new Function + fs + yaml | 同左,全支持 | 无 |
171
+ | profile 目录(package.json/bundles/pnpm 树) | 磁盘数据结构 | 磁盘照读(B5/B9) | 无 |
172
+ | **磁盘插件 runtime-import `@deepseek-ai/*` peers**(14/14 实证,见 AGENTS.md §一) | host ②③ 层单一物理副本 → 单实例 | 解析到磁盘副本或失败 → **双实例/断裂风险** | **§5 专节,比 B2 阴险** |
173
+ | `dsh plugin add` → pnpm 子进程 | 用户侧需 Node+pnpm | 可改 `BUN_BE_BUN=1` 自身当包管理器(B6) | 运营面机会(§6) |
174
+
175
+ ---
176
+
177
+ ## 4. 缓解方案空间(按"自包含性 × 动态性"光谱)
178
+
179
+ ### 方案 A:Frozen composition — 构建期 codegen 静态插件注册表(编译期已知集)
180
+
181
+ - **机制**:构建期枚举已知集(第一方 bundle `dsh-base`/`dsh-web-app`、工具自有组件、vendored 与依赖树第三方——凡 build 时在图内者,不问出身),生成
182
+ `plugins.generated.ts`:`{ 'dsh-base': () => import('<literal path>'), … }`(**字面量** → 可分析 →
183
+ 被 bundle,`--splitting` 下每个插件是懒加载 chunk,B1)。patch `Tree.import`/root include:
184
+ **registry-first,磁盘-fallback**。二进制内建 `Loader` 身份天然单实例(bundler 去重)。
185
+ - **得到**:真·单文件;启动即 fail-loud 审计(`assertEntriesLoaded`)语义不变;`cordis.patch.yml` 整条
186
+ patch 线**原样可用**——patch 是配置层,id 覆盖/config/inject/`!!js` 全在运行时解释。Cordis 面向用户的
187
+ 组合动态性(配置树)一点没冻。
188
+ - **失去**:二进制内置插件不可热插拔(本来就无此需求);内置集升级 = 重新发二进制(对工具发行恰是
189
+ reproducibility 收益)。
190
+ - **成本**:codegen 脚本 + loader 一处 patch(registry 查询优先)。低。
191
+
192
+ ### 方案 B:Hybrid closed runtime — 二进制核 + 磁盘 profile 树(第三方插件)
193
+
194
+ - **机制**:二进制 = Bun 运行时 + harness 核 + vendored loader;`$DSH_HOME/profiles/<name>/` 磁盘树照旧
195
+ (package.json + node_modules)。**裸包名 patch**(在 `HostResolvedRootInclude.import` 的
196
+ `internal === undefined` 分支 + `Tree.import` ③b):手写 node_modules walker(从 `baseUrl` /
197
+ `bareModuleBaseUrl` 指向的 profile 目录上溯)→ 得绝对路径 → `pathToFileURL().href` → `import()`。
198
+ 这就是 Turborepo #11900 的生产配方(B4),一行不差地适用:他们的场景(编译产物加载用户磁盘配置、配置
199
+ import npm 包)与 Cordis loader 加载磁盘插件**同构**。
200
+ - **得到**:第三方插件生态全保留——用户 `dsh plugin add X` 装进 profile 目录,重启即挂载;相对名/裸名/
201
+ `cordis:` 全通。**用户侧无版本解析**:发布物是"预解析好的树"(CI 里 pnpm 已经算完 lockfile),或运行时
202
+ `BUN_BE_BUN=1` 自装(B6)。
203
+ - **失去**:不再是单文件(二进制 + DSH_HOME 树)——但"避免用户侧 Node 依赖/版本解析"的原始动机完整保住。
204
+ - **成本**:walker patch(~50 行,Turborepo patch 可参考)+ 解析锚点测试。中低。
205
+
206
+ ### 方案 C:Runtime bundling — Turborepo 全套(virtual namespace 桥接,B 方案进阶)
207
+
208
+ - **机制**(B7):磁盘插件不直接 `import()`,而是现场 `Bun.build`:onResolve 对裸包名先查磁盘 walker;
209
+ 查不到再查 `BINARY_MODULES`(二进制内嵌的 `@deepseek-ai/*` 等 host 包)→ 转向 virtual namespace,
210
+ onLoad 把内嵌模块**桥接**给插件代码。
211
+ - **得到**:磁盘插件可以直接 import host 包且**单实例**(§5 的最优解);比 B 多了对外部插件的完整身份控制。
212
+ - **失去**:桥接层是自维护面(CJS/ESM 边界、export 形状、加载延迟);复杂度显著高于 B。
213
+ - **定位**:B 跑通后的按需升级,不是首选项。
214
+
215
+ ### 方案 D:等上游 — `--include` flag(#11732)/ Deno 式收编
216
+
217
+ Deno 用 `--include` 解决了同构问题,Bun issue 两岁半仍 open(B3)。**不作为路径,只作观察项**:一旦落地,
218
+ A 的 codegen 可换成声明式 `--include` 清单,B 的磁盘集也可预收编。
219
+
220
+ ### 方案 E:Node sidecar — Bun 静态核 + IPC + 真 Node 运行时承接动态面(owner 提案,v2 新增)
221
+
222
+ - **动机**:动态集的无界性使"Bun 运行时内兼容一切 Node 插件"不收敛——B10 类**行为级 divergence**(连
223
+ `child_process` 的 option 组合都能翻车)没有静态清单可穷举,只有踩到才知道。唯一"完全兼容 Node 生态"
224
+ 的东西是 Node 本身 → 动态面整体路由给真实 Node sidecar,Bun 侧只保静态核。
225
+ - **先例校准(没有听起来那么"没人做过")**:家族先例 = VS Code Extension Host(主进程 + 扩展宿主进程 +
226
+ RPC 化 API 面)、Claude Code(bun 单文件 + MCP 子进程生态——动态面全在 bun 体外跑,即 E-seam 形态的
227
+ 大规模存在证明)、dsh 自己的 fd3 code-runtime(cordis-research.md §5 桥表)。**真正没人 ship 过的只有
228
+ 一块:跨进程 Cordis context bridge**(inject-epoch 反应式、fiber disposal 传播、事件全序跨边界)——
229
+ cordis-research.md §7.2(b) 已判"研究级、非免费午餐"。
230
+ - **三个子形态,成本差一个量级**:
231
+ - **E-seam**:动态组件经工具面接入(MCP/ACP/subprocess 工具),不进 context graph。现有原语直接可用,
232
+ 成本≈0;代价是动态组件不是"Cordis 公民"(无 inject/services/events)。
233
+ - **E-subtree**:动态插件挂 Node 侧真实 cordis+loader(磁盘树),整组作为一个 remote group 桥回核心
234
+ 图;服务/事件在**组边界**显式代理。桥面收窄为接口清单,工程可控——"野心方案"的可交付版本。
235
+ - **E-full**:双 context 全语义融合(任意插件可挂任意侧、inject 跨边界反应)。研究级,月级成本,不建议
236
+ 作首发目标。
237
+ - **架构红利**:双实例问题被**驯化**——两个 context 是设计而非事故(§5 身份风险在边界上显式化);
238
+ sidecar 用磁盘树真 cordis,身份天然一致。
239
+ - **成本/风险**:IPC 管道不贵(dsh 有 sdk-jsonrpc/acp/fd3 库存),贵在 Cordis 语义保真(E-full 的
240
+ inject-epoch 跨边界);artifact 变"Bun 核 + pinned node + 插件树"(约两份运行时体积);**若多数用户
241
+ 最终都要 sidecar,Bun 简洁性论证反转**——这是必须用真实插件集先测的决策变量;另 Bun 1.4 起 runtime
242
+ 本身在 Rust 重写线上(B11),把 C 类深度桥接押在其上要计入成熟度风险。
243
+ - **Node 从哪来**:随包 pinned node(免用户安装与版本解析,原始动机保全)或 tiered——默认单文件,检测到
244
+ 动态插件需求才要求/下载 sidecar。
245
+
246
+ ### 推荐(v2):**B 与 E 是升级关系,不是二选一**
247
+
248
+ loader 做**双运行时路由**——patch 行声明(或 probe)`runtime: bun|node`:纯 JS/TS 动态插件在 Bun 运行时
249
+ 内直接跑(B 的 walker patch 覆盖大多数);带 native / 踩 B10 类雷的插件路由到 Node sidecar(E-subtree
250
+ 起步)。无动态插件 = 单文件(tiered)。路由判定用 OpenClaw 验证过的 **feature-probe** 模式
251
+ (`process.getBuiltinModule` / 试载探测),不做品牌/版本门。C(runtime bundling 桥接)降级为 B 的可选
252
+ 增强;D(#11732)保持观察。
253
+
254
+ ---
255
+
256
+ ## 5. 双实例身份问题(比"找不到包"更阴险,单独一节)
257
+
258
+ **现象**:磁盘插件(B/C 世界)运行时 `import '@deepseek-ai/cordis'`(better-dsh 实证 14 个 host 包是
259
+ **运行期 import**,非 type-only,见 AGENTS.md §一)。Node 部署下 ②③ 层的单一物理副本保证插件与 host 拿到
260
+ **同一个模块实例**。编译世界里 host 的副本在 `/$bunfs` 内,磁盘插件的解析只能落在磁盘 → 两个 cordis 并存。
261
+
262
+ **为什么致命**:cordis 的 `RegistryService` 按 callback 身份键控、`ReflectService` 沿 fiber 祖先解析、
263
+ `ctx` proxy 与 `instanceof`/`symbols.isolate` 边界判定全部依赖**跨模块单实例**。双实例不一定立刻崩——
264
+ 更坏:半工作状态(事件两套、fiber 图断裂、卸载链丢失),恰好踩中 cordis-research.md §2 "两份 cordis 身份
265
+ 风险" 的老坑。
266
+
267
+ **解法(按纪律强度排序)**:
268
+
269
+ 1. **external 共享树**(B 的正统解):身份关键包(`@deepseek-ai/cordis`、vendored loader、
270
+ `cosmokit`、`schemastery`)在二进制 build 里标 **`--external`**,运行时从磁盘共享树加载——host 与磁盘
271
+ 插件解析到同一物理副本。app-boot 已有同构先例:"embeds Include while **leaving Loader external**, so
272
+ the built include tree and host **share one Loader peer**"(§1.4 引文)——把这个纪律从"Loader 一个包"
273
+ 推广到"身份关键包清单"即可。代价:这批包必须随 DSH_HOME 树一起 ship(树本来就要 ship,边际成本≈0)。
274
+ 2. **virtual namespace 桥接**(C):二进制内嵌包经 runtime bundling 桥给插件,单实例由 build plugin 保证。
275
+ 3. **boot 断言**(无论选哪条):启动时 probe 单实例(如 `ctx.loader` 与插件侧 `import` 得到的
276
+ `Loader`/`Context` 同源),fail-loud 拒绝双实例启动——把隐性半工作态变成显性启动错误,符合 dsh 的
277
+ fail-loud 哲学。
278
+ 4. **方案 E 的驯化红利**:若动态面走 Node sidecar,Node 侧用磁盘树里的真 cordis——双 context 是显式设计
279
+ 而非事故,身份一致性在边界两侧各自成立;本节三条纪律只适用于"Bun 进程内同时存在双侧副本"的 B/C 世界。
280
+ ---
281
+
282
+ ## 6. 运营面(简述)
283
+
284
+ - **用户安装面**:单文件(A)或"二进制+解包树"(B),均无 Node 版本协商、无 pnpm hoisting 分层、无
285
+ 用户侧供应链年龄门(`minimumReleaseAge` 是用户 install 相位的 pnpm 策略——版本解析已在 CI 完成即消解)。
286
+ 这正对 AGENTS.md 里记录的 0.2.2 发布日 install 被拦一类运营痛点。
287
+ - **`dsh plugin add`**:现走 pnpm 子进程(AGENTS.md §一)。Bun 世界两条路:`BUN_BE_BUN=1 ./dsh install`
288
+ (B6,二进制自身即包管理器,用户零依赖)或继续要求 pnpm(锁 pnpm 生态语义:年龄门、hoisted 模型)。
289
+ 注意 bun install ≠ pnpm(lockfile/hoist/策略引擎均不同)——切换是生态决策不是纯技术决策,**列为开放
290
+ 问题**。
291
+ - **dev/test 线不变**:4999 源码级实例、HMR、`--expose-internals` 全部留在 Node 轨道(§1.3);编译形态只
292
+ 是 ship 面的新成员。两轨道同源(cordis-research.md §4.2 的"195 包照旧 tsdown 构建,只在装配层用 Bun")。
293
+
294
+ ---
295
+
296
+ ## 7. 判决
297
+
298
+ 1. **Barrier 2(自举)是真的,但被高估为"框架级";实测是"两个 choke point 的裸包名分支"**,且 loader 是
299
+ 自家 vendor 代码、upstream 已有 `bareModuleBaseUrl`/"closed packaged runtime" 的设计预留。可patch面小、
300
+ 边界清晰。
301
+ 2. **Bun 侧的失败模式有生产级先例与配方**:B2/B4(azu repro、Turborepo #11900)不仅确诊了"编译产物内
302
+ 裸包名/`createRequire` 必败",还交付了被验证的 workaround(手写 walker + 绝对路径 + external builtin
303
+ + virtual namespace)。
304
+ 3. **A(冻结已知集)+ B(磁盘承接动态集)是基线形态**;**E(Node sidecar)按真实插件集的 Bun 通过率
305
+ 决定是首发件还是后备件**(§4-E 路由器模式:`runtime: bun|node` 双路由 + feature-probe)。Deno 的
306
+ `--include` 是该张力的生态级参照物;Bun 尚未跟进(#11732 open)。
307
+ 4. **必须带着 §5 的身份纪律上线**:external 共享树 + boot 单实例断言,否则双实例会把问题变成最难查的
308
+ 半工作态。
309
+ 5. **PoC 清单**(按依赖序,预计一天内可跑完判定):
310
+ a. 最小 cordis app(vendored core + loader + 一个静态插件 + 一行 yml)`bun build --compile` → 预期
311
+ boot 成功(core 零 native、`new Function`/fs/yaml 全通)。
312
+ b. 加一行裸包名条目 + 磁盘 node_modules → 复现 B2 报错 → 打 walker patch(Turborepo 配方)→ 复通。
313
+ c. 双实例 probe:磁盘插件 import `@deepseek-ai/cordis`,断言与 host 同实例 → 验证 external 共享树。
314
+ d. A 的 codegen registry(bund bundles 列表生成)→ registry-first → 懒加载 chunk + fail-loud 审计。
315
+ e. `BUN_BE_BUN=1 ./dsh install` 在 profile 目录装一个真插件(运营面冒烟)。
316
+ f. 双运行时路由率测定:拿 3–5 个真实目标动态插件在 Bun 运行时试载(`process.getBuiltinModule` probe
317
+ + 试 import + native 探测)→ 通过率决定 E-sidecar 是首发件还是后备件。
318
+ g. E-subtree 最小桥:Node 侧起真 loader 挂一个 group,服务/事件经组边界代理回 Bun 核——验证桥面接口
319
+ 清单是否收敛(§4-E 的可交付性判定)。
320
+
321
+ ---
322
+
323
+ ## 来源
324
+
325
+ - 本仓一手源码:`upstream/deepseek-harness`(`dsh-v0.1.2-alpha.5`)`vendor/loader/src/{internal.ts,
326
+ config/tree.ts, config/utils.ts, index.ts}`、`vendor/hmr/src/index.ts`、
327
+ `packages/boot/app-boot/src/{index.ts, profile.ts}`
328
+ - [Bun — Single-file executable(2026-09)](https://bun.sh/docs/bundler/executables):bundled modules +
329
+ 运行时、cwd 锚定(Worker/SQLite)、`/$bunfs` 内嵌文件与 `--asset` 目录树、`BUN_BE_BUN=1`、
330
+ `--splitting`、bytecode/sourcemap、不支持项清单
331
+ - [azu/bun-build-dynamic-import](https://github.com/azu/bun-build-dynamic-import):非可分析动态 import 在
332
+ 编译产物的 `Cannot find package … from "/$bunfs/root/…"` repro
333
+ - [oven-sh/bun#11732](https://github.com/oven-sh/bun/issues/11732):`--include` flag 请求,state=open
334
+ (API 实查 2026-09-03);issue 正文引 Deno `--include` 先例
335
+ - [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900)(fix #11882,2026-02):
336
+ 编译产物内 `createRequire().resolve()` 不可靠 → 手写 node_modules walker;运行时 `Bun.build` 用户配置 +
337
+ `BINARY_MODULES` virtual namespace 桥接 + node builtin `external: true`;含回归测试
338
+ - [openclaw/openclaw#114256](https://github.com/openclaw/openclaw/pull/114256)(2026-07 merged):388k★ TS
339
+ 工具加实验性 Bun 支持实录——node:sqlite 在 1.3.x 缺、1.4.0 canary(Rust 重写线)提供;execa
340
+ `encoding:"buffer"` 触发 #36049;接法 = `process.getBuiltinModule` feature-probe 而非品牌门
341
+ - [oven-sh/bun#36049](https://github.com/oven-sh/bun/issues/36049)(spawn `encoding` divergence,stable
342
+ 1.3.14 复现)、[oven-sh/bun#16050](https://github.com/oven-sh/bun/issues/16050)(better-sqlite3 需重编译)、
343
+ [opencode-telegram-bridge#34](https://github.com/gabriel-trigo/opencode-telegram-bridge/issues/34)
344
+ (better-sqlite3 N-API crash 实案)、[OmniRoute#11468](https://github.com/diegosouzapw/OmniRoute/pull/11468)
345
+ (以 bun:sqlite 规避 N-API crash)
346
+ - 本仓既有研究:[cordis-research.md](./cordis-research.md) §2(no privileged core / bootstrap 链)、§4.2
347
+ (Bun 编译首判:native addon 政策、Claude Code 先例、tsdown 线不动);AGENTS.md §一(①→④ 解析分层、
348
+ 14/14 运行期 host import、供应链年龄门运营史)