@deepseek-ai/dsh-app-boot 0.1.6-alpha.2 → 0.1.7-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
5
- README.md: 507445c13953d5b38fcc780f0c0e9adaac7a2d9d
6
- README.zh.md: 11ab4b9df5df5ca16addda7d58513fdd7869a1b0
5
+ README.md: 5bbe75d641eeeaf3b87c31221b3491ad1408934e
6
+ README.zh.md: b3ac89c1cb082880622175b494d540888b19e804
package/README.md CHANGED
@@ -47,7 +47,7 @@ With that entry point, startup keeps every plugin that can activate. An enabled
47
47
 
48
48
  Import profile and bundle declaration types from [`@deepseek-ai/dsh-package-manifest`](../../util/package-manifest/README.md). App-boot adapts `DshPackageManifest` to `ProfileManifest` with optional package identity because local profiles need no published version. App-boot owns profile loading, JSON validation, and resolved runtime data.
49
49
 
50
- A profile is how one dsh installation ships different app surfaces: `web`, `headless`, `acp`, `sdk`, and `sdk-minimal` start distinct compositions from the same launcher. A profile lives at `$DSH_HOME/profiles/<name>` and combines installable bundles with its own `cordis.patch.yml`. The YAML composition enables or disables HMR. The shipped `web` template uses live reload, while the other shipped templates apply patches only at startup. `sdk-minimal` names only its standalone bundle; the other templates retain base-plus-mode stacks. `dsh --profile <name> --from-default-profile <template>` creates a custom profile at a new non-shipped name from one shipped template, while `dsh plugin` initializes a base-backed profile and manages its installed bundles. A missing bundle or one without a patch declaration fails startup loudly. Application-owned npm projects, such as Electron's reserved Desktop profile, use `loadProfileDirectory` to load an already initialized directory without exposing it through CLI profile lookup.
50
+ A profile is how one dsh installation ships different app surfaces: `web`, `headless`, `acp`, `sdk`, and `sdk-minimal` start distinct compositions from the same launcher. A profile lives at `$DSH_HOME/profiles/<name>` and combines installable bundles with its own `cordis.patch.yml`. A bundle's `dsh.bundle.patch` names one patch file or an ordered list of files; `bundlePatchFiles` validates the declaration and `bundlePatchPaths` resolves it to absolute paths; the layer concatenates their patch lists in that order. The YAML composition enables or disables HMR. The shipped `web` template uses live reload, while the other shipped templates apply patches only at startup. `sdk-minimal` names only its standalone bundle; the other templates retain base-plus-mode stacks. `dsh --profile <name> --from-default-profile <template>` creates a custom profile at a new non-shipped name from one shipped template, while `dsh plugin` initializes a base-backed profile and manages its installed bundles. Bundle resolution, manifest, and patch-loading failures print a diagnostic and skip that bundle without changing its selection. Remaining bundles keep their order; profile and user-patch errors still fail startup. Skipping a bundle does not guarantee that the remaining composition can provide the required services. Application-owned npm projects, such as Electron's reserved Desktop profile, use `loadProfileDirectory` to load an already initialized directory without exposing it through CLI profile lookup.
51
51
 
52
52
  Your machine-local preferences also live in the Harness home:
53
53
 
@@ -58,7 +58,7 @@ The enabled `dsh-hmr` plugin watches the profile manifest and both user patch fi
58
58
 
59
59
  Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative `./` or `../` paths to file URLs within `insert` rows and their nested groups; existing-entry name assertions and replacement `config` values remain literal.
60
60
 
61
- Before mounting profile rows, the `dsh` launcher computes one immutable package-resolution generation from the installation and ordered bundle dependency graphs. Runtime mode is the default: it installs the generation through Node's ESM and CommonJS resolvers without creating fallback links. Plain Node callers of `runProfile` may explicitly select link mode to materialize the generation, dual mode to materialize and verify it, or runtime mode. Packaged executables and the Electron Host always use runtime mode.
61
+ Before mounting profile rows, the `dsh` launcher computes one immutable runtime resolution from the installation and ordered bundle dependency graphs. Every profile launcher uses runtime resolution, including plain Node, packaged executables, and the Electron Host. It installs the runtime resolution through Node's ESM and CommonJS resolvers without creating fallback links.
62
62
 
63
63
  `sanitizeProfile(binName, profileDir, bundles)` provides filesystem recovery without loading plugins or parsing patches. Desktop uses it for native fatal recovery. Call it only after stopping the profile and excluding concurrent profile writes. It renames the profile’s `cordis.patch.yml` to a unique `.bak-<timestamp>` sibling and restores the supplied bundle list, preserving installed packages and other manifest fields. The timestamp is Unix time in milliseconds; collisions append an ordinal (`-1`, `-2`, …) without changing it. It returns the backup path, or `undefined` when no patch exists; missing profiles remain absent. Profile initialization recreates an empty patch on the next launch. The home-level patch is unchanged. Invalid profile JSON fails before mutation; later errors propagate and retain completed changes for retry.
64
64
 
@@ -66,10 +66,22 @@ Before mounting profile rows, the `dsh` launcher computes one immutable package-
66
66
 
67
67
  Before you boot, you can print the exact configuration the app will mount: the dump shows the composed entry list with `!!js` expressions verbatim, grouped under comments naming each source file and the patch layers that changed it, as one loadable YAML document. Patches that match no row are reported with their layer label; a missing, unparsable, or invalid config fails the dump.
68
68
 
69
+ ### Inspecting plugin configuration schemas
70
+
71
+ `generateConfigSchema` takes a diagnostic bin name, a prepared on-disk profile, ordered patch lists, and an installation anchor, and returns `ConfigSchemaDump`. App-boot owns composition, runtime resolution, and collection diagnostics. The caller owns profile preparation, home/argv layer selection, process streams, and exit policy. `createConfigProjector`, `isNativeConfigSchema`, and `LOADER_EXPRESSION_SCHEMA` are exported for callers that project one live plugin Config without profile collection; projected value positions reference `#/$defs/loaderExpression`, so the enclosing document must define it.
72
+
73
+ The generated JSON Schema 2020-12 describes the composed entry list, with `$defs.patchList` for root-tree overlays and shared definitions projected from plugin Config graphs. It includes disabled entries, native groups, and literal YAML/JSON includes; builtin and canonical native package exports are matched using each tree's module-resolution base, including profile-local copies. Custom carriers are not inferred from their config fields. A missing include with literal `initial` entries is expanded in memory without writes. Discovery and projection diagnostics remain in `x-cordis`, including unknown Configs and partial constraints. The [CLI schema-dump reference](../../../apps/cli/reference/README.md#config-schema-dump) owns the output fields and editing semantics.
74
+
75
+ The projector preserves native omission behavior by checking literal defaults against generated schemas with Ajv, without executing native validators or transform callbacks. Regex compatibility checks and unsupported or recursive-default cases produce explicit limitations. Opaque input adaptations and lazy metadata effects widen validation rather than replaying native mutation. Non-JSON default/presentation annotations are omitted with limitations without losing the structural schema; an unrepresentable default leaves omission acceptance unknown unless the field is required. These dependencies load only when collection runs. Imports, Config getters, and lazy builders still execute trusted code; collection is not a sandbox. Do not overlap profile-resolution interceptions. The collector releases its interception before returning, while Node retains imported modules; runtime-created plugins and Agent preset instances remain outside discovery.
76
+
77
+ ### Reading plugin display metadata
78
+
79
+ Use `readPluginMeta(specifier, parentURL)` or `ctx.pluginPackages.metaOf(specifier, parentURL)` to read installed package display text without importing or activating the plugin. Lookup uses the complete package specifier and the caller's resolution base, respecting Node exports. File paths and file URLs return no metadata without resolving resources. Missing locale fields fall back to the accessible `package.json` at that address; malformed metadata returns an `error` diagnostic. Results retain translations for Client-side language selection. The reader also loads `package.json.icon` as an image data URL, even when locale text is complete; an icon error preserves valid text alongside the diagnostic. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format.
80
+
69
81
  <a id="startup-and-reload-failures"></a>
70
82
  ### Startup and reload failures
71
83
 
72
- Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation.
84
+ Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation emits `app-boot/config-reload` after lifecycle settlement and diagnostic checks, including programmatic updates without HMR. The event carries no diff or parsed config. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update.
73
85
 
74
86
  After the Loader settles, app-boot warns when only optional entries are inactive. If an enabled required entry cannot activate, `boot()` rejects with `StartupError` after disposal. An independently owned logger exporter retains warning and error records through asynchronous disposal and is released before `boot()` settles. Its message groups all failed plugins and pending services, marks required entries, and retains original stacks, nested causes, and aggregate members. The CLI prints that message once and saves [full startup diagnostics](../../../apps/cli/reference/README.md#startup-diagnostics) before exiting with code 1; unrelated exceptions retain their normal stack output. In the table, stopping startup means disposing any mounted plugins and exiting nonzero without reporting readiness; continuing keeps successful plugins running. Later configuration HMR does not repeat the required-startup audit and does not roll back the whole update.
75
87
 
@@ -110,12 +122,15 @@ This section explains how the outcomes above are realized and points at the code
110
122
  ### Design notes
111
123
 
112
124
  - **Profile launch data.** `ctx.profileContext` contains only profile locations, startup bundle names, parsed invocation overlays and the telemetry opt-out value. `readProfilePatches()` composes the supplied startup profile or reads current files at those locations; callers schedule and apply the result.
113
- - **Process-local module resolution.** Runtime and dual modes install one generation on Node's internal ESM and CommonJS resolvers before profile rows mount; link mode leaves both resolvers unchanged. Node still owns exports, conditions, subpaths, module caches, and error codes; routed ESM failures report the original importer instead of the internal lookup anchor. `ctx.pluginPackages` exposes package metadata from the same generation without recording Entry imports; an installed generation is authoritative even for a miss, while low-level embedders that install the service without one retain native lookup.
125
+ - **Process-local module resolution.** The launcher installs the runtime resolution on Node's internal ESM and CommonJS resolvers before profile rows mount. Node still owns exports, conditions, subpaths, module caches, and error codes; routed ESM failures report the original importer. Explicit CommonJS `paths` always retain native lookup, including paths inside profiles.
126
+ - **Linked directories.** A profile link to an external directory admits its importers to peer-aware ancestor lookup, even without its own `package.json`. At each `D/node_modules` position, current `D/package.json` peer names present in the runtime table use the runtime package; other names use the physical candidate. A nearer physical package precedes a later peer declaration, and a peer position needs no physical `node_modules`. Installation-scope package directories are excluded from linked interception; overlapping roots do not change the importer's lookup order ([rule](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md)).
127
+ - **Package metadata.** `ctx.pluginPackages.packageOf` locates the owning package without loading code or requiring an exported `package.json`; a subpath selects its package without validating that file. The installed runtime resolution owns selection even for a miss. Low-level embedders that install the service without one retain native lookup. Display metadata uses the separate entry-aware reader described above.
114
128
  - **Two Loader builtins.** `mountRootInclude` registers `cordis:include` and `cordis:group` as Loader builtins: a group row gives one `isolate` realm to a provider and its consumers together, and an agent preset outside this workspace cannot resolve `@deepseek-ai/cordis-plugin-group` by name. Both load through the ambient module pipeline rather than the included tree's own specifier resolution.
115
- - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot applies the global required-entry policy after initial settlement; agent presets and dynamic multi-entry compositions own and dispose their separate generation when they require all-or-nothing setup. App-boot reads failed fibers to report their recorded errors and coalesces duplicate Loader rejection notifications through one process checkpoint.
116
- - **One fallback generation.** The installation-first and ordered-bundle breadth-first traversal produces both the runtime table and the retained disk materializer. Runtime mode creates no resolution links and ignores stale projections at their former lookup positions. External bare targets selected by package `imports` use the same package order, while Node retains mapping, conditions, and exact target resolution. Link mode materializes the same table; dual mode also compares Node's disk result with the table. A complete successor may add package names atomically, while changing or removing an existing mapping requires restart.
117
- - **Application-owned profiles.** Link mode projects missing installation and bundle packages inside the profile without writing a shared Harness-home fallback. Runtime mode supplies the same installation and bundle generation without creating links. Package operations remove only profile links owned by dsh; pnpm-managed entries remain untouched.
118
- - **Owned Workers.** Worker build banners import `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` before bundled business code. Each Worker installs the structured-cloned generation in its own isolate. The bootstrap bundle has no static package imports. Source Worker entries retain their self-contained dependency closure, and third-party Workers receive no injection.
129
+ - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot applies the global required-entry policy after initial settlement; agent presets and dynamic multi-entry compositions own and dispose their separate Loader subtree when they require all-or-nothing setup. App-boot reads failed fibers to report their recorded errors and coalesces duplicate Loader rejection notifications through one process checkpoint.
130
+ - **One runtime resolution.** The installation-first and ordered-bundle breadth-first traversal produces the runtime table. Runtime resolution creates no links; runtime resolution entries occupy their package names at `$DSH_HOME/profiles/node_modules`, and every other name sees that directory as an ordinary ancestor. Profile load removes the `.dsh-module-fallback` projections that link-backend releases wrote into a profile; pnpm-installed packages stay. External bare targets selected by package `imports` use the same package order, while Node retains mapping, conditions, and exact target resolution. A complete successor may add package names and update linked-root membership atomically within the existing package-mapping and local-name constraints.
131
+ - **Removing linked interception.** A successor may remove a linked root without restart. Directories outside all remaining roots use native lookup for subsequent requests, which may find a development copy or report a missing package. Existing module references and Node caches stay unchanged. Re-adding the same link name and target is allowed; pointing a previously published name at a different target is rejected even after removal ([generation rules](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md#immutable-generations)).
132
+ - **Application-owned profiles.** Application-owned profiles use the same runtime resolution. Links within the active profile directory, including pnpm store links, are not external roots even when the profile is outside the shared profiles tree. Resolution does not modify their `node_modules`; pnpm owns installed packages.
133
+ - **Owned Workers.** Worker build banners import `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` before bundled business code. Each Worker installs the structured-cloned runtime resolution in its own isolate. The bootstrap bundle has no static package imports. Source Worker entries retain their self-contained dependency closure, and third-party Workers receive no injection.
119
134
  - **Update completion.** App boot observes restart failures through the `internal/update` waterfall. Live patch reloads wait for the tree's fibers before auditing activation; `Fiber.update()` and `Entry.update()` alone do not establish restart success.
120
135
  - **One rejection checkpoint.** `inactiveEntries` keeps the exact reasons it folds into the boot diagnostic visible through the next process rejection checkpoint, so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal.
121
136
  - **Two-stage failure labels.** Outside startup audit failures, `boot()` distinguishes `host preparation failed` — `prepare` threw before any config-tree entry mounted — from `plugin tree failed to load`, and appends the deepest plugin error's stack. Plugin diagnostics retain nested causes and aggregate member failures; cyclic causes stop traversal without replacing the original error.
@@ -131,11 +146,12 @@ The exports each own one stage of the boot: config resolution and snapshot repla
131
146
  | File | Role |
132
147
  |---|---|
133
148
  | [`src/index.ts`](src/index.ts) | Boot helpers: config resolution, environment loading, fail-loud guard, activation audit, patch parsing, config dump, harness-source section |
134
- | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution, module fallback |
149
+ | [`src/profile.ts`](src/profile.ts) | Profile discovery, initialization, bundle resolution, runtime resolution construction |
135
150
  | [`src/profile-plugins.ts`](src/profile-plugins.ts) | Installed dependencies, bundle activation policy, and manifest updates |
136
151
  | [`src/profile-sanitize.ts`](src/profile-sanitize.ts) | Profile patch backup and recovery bundle activation |
152
+ | [`src/config-schema/`](src/config-schema/) | Profile schema generation, discovery, native projection, and result types |
137
153
  | [`src/profile-resolution/`](src/profile-resolution/) | Runtime resolver, package-metadata service, and built Worker bootstrap |
138
- | — | No runtime invariant companion is published; one registration owns each resolver generation, and dual mode compares the independently materialized result at resolution time. |
154
+ | — | No runtime invariant companion is published; one interception owns each runtime resolution. |
139
155
 
140
156
  </details>
141
157
 
@@ -173,7 +189,10 @@ Boot itself changes no request prefix. `addHarnessSourceSection` places its sour
173
189
 
174
190
  These limits describe when this boot library is a poor fit or needs special care. They are current package constraints, not a task backlog.
175
191
 
176
- - **Runtime resolution depends on Node internals** — supported Node versions require the native builtin-access addon and executable compatibility coverage. Only built Harness-owned Workers receive the generation bootstrap; third-party Workers and custom `vm` linkers keep native resolution.
192
+ - **Runtime resolution depends on Node internals** — supported Node versions require the native builtin-access addon and executable compatibility coverage. Only built Harness-owned Workers receive the runtime resolution bootstrap; third-party Workers and custom `vm` linkers keep native resolution.
193
+ - **Relinking a profile package needs a restart** — Node caches real paths, so changing the target of a profile link or a dependency link requires a process restart.
194
+ - **Linked scope follows recorded real directories** — a hoisted dependency outside every linked root uses native Node. Current peer reads do not invalidate Node caches, watch files, or validate peer version ranges.
195
+ - **Source launches use an ESM-only hook** — CommonJS requests still need the JavaScript files selected by package exports; resolution does not supply missing build outputs.
177
196
  - **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection.
178
197
  - **Environment discovery is launch-scoped** — `loadLayeredEnv` reads only the invocation directory and Harness home once; it does not search parents or follow a workspace selected later. `loadEnv` remains the one-directory helper for non-product bins.
179
198
  - **A user patch replaces the whole matched config** — an id-targeted patch does not deep-merge, so a profile override restates the bundle fields it keeps.
@@ -186,8 +205,8 @@ These limits describe when this boot library is a poor fit or needs special care
186
205
 
187
206
  This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
188
207
 
189
- #### Open: config dump stability
208
+ #### Open: YAML config dump stability
190
209
 
191
- `renderConfigDump` output is a loadable YAML document whose `# ==` source comments and `!!js`-verbatim rendering serve the `--dump-config` diagnostic. Nothing promises byte stability across package versions; decide whether the dump becomes a serialization contract before anything consumes it programmatically.
210
+ `renderConfigDump` output is a loadable YAML document whose `# ==` source comments and `!!js`-verbatim rendering serve the `--dump-config` diagnostic. Nothing promises byte stability across package versions; decide whether the dump becomes a serialization contract before anything consumes it programmatically. JSON Schema output follows the separate [pre-stable compatibility policy](../../../apps/cli/reference/README.md#config-schema-dump).
192
211
 
193
212
  </details>
package/README.zh.md CHANGED
@@ -47,7 +47,7 @@ const ctx = await boot('dsh', resolveConfigPath(argv[2], process.env.DSH_SNAPSHO
47
47
 
48
48
  Profile 与组合包的声明类型从 [`@deepseek-ai/dsh-package-manifest`](../../util/package-manifest/README.zh.md) 导入。App-boot 将 `DshPackageManifest` 适配为包身份可选的 `ProfileManifest`,因为本地 profile 无需发布版本。App-boot 负责 profile 加载、JSON 校验和解析后的运行时数据。
49
49
 
50
- profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`headless`、`acp`、`sdk` 与 `sdk-minimal` 从同一 launcher 启动不同组合。profile 位于 `$DSH_HOME/profiles/<name>`,由可安装组合包和自身 `cordis.patch.yml` 组成。YAML 组合决定是否启用 HMR。随产品交付的 `web` 模板实时重载,其他随附模板只在启动时应用 patch。`sdk-minimal` 只列出自身的独立组合包,其他模板保留 base 加模式的组合包栈。`dsh --profile <name> --from-default-profile <template>` 从一个随附模板,在新的非内置名称处创建自定义 profile;`dsh plugin` 则初始化以 base 为基础的 profile,并管理其中安装的组合包。缺失组合包或未声明 patch 的组合包会让启动明确失败。由应用持有的 npm 项目(例如 Electron 保留的 Desktop profile)通过 `loadProfileDirectory` 加载已经初始化的目录,而不会将它暴露给 CLI profile 查找。
50
+ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`headless`、`acp`、`sdk` 与 `sdk-minimal` 从同一 launcher 启动不同组合。profile 位于 `$DSH_HOME/profiles/<name>`,由可安装组合包和自身 `cordis.patch.yml` 组成。组合包的 `dsh.bundle.patch` 指定一个 patch 文件或一个有序的文件列表;`bundlePatchFiles` 校验该声明,`bundlePatchPaths` 把它解析为绝对路径;该层按此顺序拼接各文件的 patch 列表。YAML 组合决定是否启用 HMR。随产品交付的 `web` 模板实时重载,其他随附模板只在启动时应用 patch。`sdk-minimal` 只列出自身的独立组合包,其他模板保留 base 加模式的组合包栈。`dsh --profile <name> --from-default-profile <template>` 从一个随附模板,在新的非内置名称处创建自定义 profile;`dsh plugin` 则初始化以 base 为基础的 profile,并管理其中安装的组合包。组合包解析、manifest 读取或 patch 加载失败时会输出诊断并跳过该组合包,不改变其选择状态。其余组合包保持原顺序;profile 和用户 patch 错误仍会导致启动失败。跳过组合包不保证剩余组合能够提供所需服务。由应用持有的 npm 项目(例如 Electron 保留的 Desktop profile)通过 `loadProfileDirectory` 加载已经初始化的目录,而不会将它暴露给 CLI profile 查找。
51
51
 
52
52
  你的机器本地偏好同样位于 harness home 中:
53
53
 
@@ -58,7 +58,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
58
58
 
59
59
  插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把 `insert` 条目及其嵌套分组中的绝对路径以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL;对已有条目名称的断言及替换用的 `config` 值保持原样。
60
60
 
61
- 挂载 profile 条目前,`dsh` launcher 会从安装依赖图与有序 bundle 依赖图计算一份不可变的 package resolution generation。默认使用 runtime 模式,将 generation 安装到 Node 的 ESM 与 CommonJS 解析器中,不创建 fallback 链接。普通 Node 中的 `runProfile` 调用方可以显式选择 link 模式以物化 generation,选择 dual 模式以物化并校验它,或选择 runtime 模式。打包可执行文件和 Electron Host 始终使用 runtime 模式。
61
+ 挂载 profile 条目前,`dsh` launcher 会从安装依赖图与有序 bundle 依赖图计算一份不可变的 runtime resolution。普通 Node、打包可执行文件与 Electron Host 等所有 profile 启动器都使用 runtime 解析,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 解析器中,不创建 fallback 链接。
62
62
 
63
63
  `sanitizeProfile(binName, profileDir, bundles)` 提供文件恢复,无需加载插件或解析 patch。Desktop 在原生致命错误恢复中调用它。调用前必须停止 profile 并排除并发 profile 写入。它将 profile 的 `cordis.patch.yml` 重命名为带唯一 `.bak-<timestamp>` 后缀的同目录备份,并恢复调用方指定的 bundle 列表,保留已安装包和其他 manifest 字段。时间戳为 Unix 毫秒数;同名备份已存在时追加序号(`-1`、`-2`、……),时间戳保持不变。返回值为备份路径;patch 不存在时返回 `undefined`,缺失的 profile 不会被创建。下次启动的 profile 初始化会重新创建空 patch。home 级 patch 不变。无效 profile JSON 在修改前报错;后续错误向调用方抛出,保留已完成的修改供重试。
64
64
 
@@ -66,10 +66,22 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
66
66
 
67
67
  启动前,你可以打印应用将挂载的确切配置:dump 会以 `!!js` 表达式原样展示组合后的条目列表,并按注释分组标明每个源文件及其 patch 层,输出是一份可加载的 YAML 文档。未匹配到任何行的 patch 会连同其层标签一起报告;配置缺失、无法解析或字段无效都会使 dump 失败。
68
68
 
69
+ ### 检查插件配置 schema
70
+
71
+ `generateConfigSchema` 接收用于诊断的 bin 名称、已准备好的磁盘 profile、有序 patch 列表和安装锚点,返回 `ConfigSchemaDump`。App-boot 负责组合、运行时解析和收集诊断。调用方负责 profile 准备、home/argv 层选择、进程流及退出策略。`createConfigProjector`、`isNativeConfigSchema` 与 `LOADER_EXPRESSION_SCHEMA` 供不经 profile 收集、只投影单个运行中插件 Config 的调用方使用;投影后的取值位置会引用 `#/$defs/loaderExpression`,外层文档必须定义它。
72
+
73
+ 生成的 JSON Schema 2020-12 描述组合后的 entry list,以 `$defs.patchList` 描述根树 overlay,并从插件 Config 图投影共享定义。它包含禁用项、原生 group 和字面量 YAML/JSON include;内置与规范原生包导出按每棵树的模块解析基准匹配,包括 profile 本地副本。不会根据 config 字段猜测自定义承载插件。include 缺失但有字面量 `initial` 条目时,只在内存中展开,不写文件。发现和投影诊断保留在 `x-cordis` 中,包括未知 Config 和部分约束。[CLI schema dump 参考](../../../apps/cli/reference/README.zh.md#config-schema-dump)负责说明输出字段和编辑语义。
74
+
75
+ 投影器使用 Ajv 根据生成的 schema 检查字面量默认值,以保留原生省略行为,不执行原生验证器或 transform 回调。正则兼容性检查、不支持的情况及递归默认值分析会产生显式限制说明。不透明的输入转换和 lazy 元数据副作用会放宽验证,而不是重放原生修改。非 JSON 默认值或展示注释会被省略并附上限制说明,不丢弃结构 schema;无法表示的默认值使省略接受性保持未知,必填字段除外。这些依赖仅在收集运行时加载。导入、Config getter 和 lazy builder 仍会执行可信代码;收集不是沙箱。profile 解析拦截不得重叠。收集器返回前释放自己的拦截,而 Node 仍缓存导入的模块;运行时创建的插件和 Agent preset 实例不在发现范围内。
76
+
77
+ ### 读取插件展示元信息
78
+
79
+ 使用 `readPluginMeta(specifier, parentURL)` 或 `ctx.pluginPackages.metaOf(specifier, parentURL)` 读取已安装包的展示文本,无需导入或激活插件。查询使用完整包标识与调用方的解析基准,并遵循 Node exports。文件路径与文件 URL 不解析资源,直接返回无元信息。缺失的 locale 字段回退到该地址下可访问的 `package.json`;格式错误的元信息返回 `error` 诊断。结果保留翻译,由 Client 选择语言。即使 locale 文本完整,读取器也会将 `package.json.icon` 加载为图片 data URL;图标出错时,保留有效文本并附上诊断。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。
80
+
69
81
  <a id="startup-and-reload-failures"></a>
70
82
  ### 启动与重载失败
71
83
 
72
- profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。
84
+ profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。成功重载在生命周期结束及诊断检查通过后发出 `app-boot/config-reload`,包括未启用 HMR 时的程序化更新。事件不携带 diff 或解析后的配置。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。
73
85
 
74
86
  Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如果已启用的 required 条目无法激活,`boot()` 会在释放资源后以 `StartupError` 拒绝。独立管理生命周期的 logger exporter 会保留异步资源释放期间的警告和错误记录,并在 `boot()` 结算前释放。其消息分组列出所有失败插件和等待的服务,标记 required 条目,并保留原始堆栈、嵌套原因和聚合错误成员。CLI 仅输出该消息一次,并在保存[完整启动诊断](../../../apps/cli/reference/README.zh.md#startup-diagnostics)后以退出码 1 结束;其他异常保留正常堆栈输出。表中的“终止启动”指释放已挂载插件并以非零码退出,不报告就绪;“继续”指保留成功运行的插件。后续配置 HMR 不会再次执行 required 启动审计,也不会回滚整个更新。
75
87
 
@@ -110,12 +122,15 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
110
122
  ### 设计说明
111
123
 
112
124
  - **Profile 启动数据。** `ctx.profileContext` 只包含 profile 位置、启动时组合包名称、已解析的调用级 overlay 与遥测退出值。`readProfilePatches()` 组合传入的启动 profile,或读取这些位置上的当前文件;调用方负责调度和应用结果。
113
- - **进程内模块解析。** runtime 和 dual 模式会在挂载 profile 条目前,将一份 generation 安装到 Node 的 ESM 与 CommonJS 内部 resolver;link 模式不修改这两个 resolver。exports、conditions、subpath、模块缓存和错误码仍由 Node 负责;路由后的 ESM 失败会报告原始 importer,而不是内部查找锚点。`ctx.pluginPackages` 从同一 generation 提供 package metadata,不记录 Entry import;安装 generation 后,即使查询未命中也以 generation 为准,仅安装服务而未提供 generation 的底层嵌入方仍使用 Node 原生查找。
125
+ - **进程内模块解析。** launcher 在挂载 profile 条目前,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 内部 resolver。exports、conditions、子路径、模块缓存和错误码仍由 Node 负责;路由后的 ESM 失败报告原始 importer。显式 CommonJS `paths` 始终保留原生查询,包括指向 profile 内的路径。
126
+ - **链接目录。** profile 链接到树外目录时,其下的 importer 参与逐层 peer 查询,即使目标没有自身的 `package.json`。在每个 `D/node_modules` 位置,当前 `D/package.json` 的 peer 包名若存在于运行时表,就使用运行时包;其他包名查询物理候选。更近的物理包先于后续 peer 声明,peer 位置无需物理 `node_modules`。installation 作用域包目录不参与 linked 拦截,重叠 root 不改变 importer 的查询顺序([规则](../../../.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md))。
127
+ - **包元数据。** `ctx.pluginPackages.packageOf` 定位所属包,不加载代码,也不要求导出 `package.json`;子路径选择其所属包,不校验该文件。安装 runtime resolution 后,即使查询未命中也以其选包规则为准。仅安装服务而不提供 runtime resolution 的底层嵌入方保留原生查询。展示元数据使用上文另述的入口感知读取器。
114
128
  - **两个 Loader builtin。** `mountRootInclude` 把 `cordis:include` 与 `cordis:group` 注册为 Loader builtin:group 行能把一个提供方与它的消费方放进同一个 `isolate` realm,而位于本工作区之外的 agent preset 无法按名称解析 `@deepseek-ai/cordis-plugin-group`。两者都通过宿主的模块管线加载,而非被包含树自身的说明符解析。
115
- - **由 consumer 持有严格语义。** 普通 Loader group 保留成功 sibling。App-boot 在首次结算后应用全局 required-entry policy;agent preset 与动态多 entry 组合在需要 all-or-nothing setup 时,持有并拆卸各自的独立 generation。App-boot 读取 failed fiber 来报告已记录的错误,并在一个进程检查点内合并 Loader 重复的 rejection 通知。
116
- - **唯一 fallback generation。** 安装优先、有序 bundle 逐根 breadth-first 遍历同时生成运行时表和保留的磁盘 materializer。runtime 模式不创建解析链接,并在旧链接原来的查找位置忽略陈旧投影。package `imports` 选中的外部 bare target 使用相同的选包顺序,映射、conditions 和精确 target 解析仍由 Node 负责。link 模式物化同一张表;dual 模式还会比较 Node 的磁盘结果与表。完整后继 generation 可以原子增加 package name,修改或删除既有映射则要求重启。
117
- - **应用自有 profile。** link 模式在 profile 内投影缺失的安装包及 bundle 包,不写共享的 Harness-home 后备目录。runtime 模式提供相同的安装包及 bundle generation,不创建链接。包操作仅移除 dsh 所有的 profile 链接;pnpm 管理的条目保持不变。
118
- - **自有 Worker。** Worker 构建 banner 会在业务 bundle 前导入 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。每个 Worker 在自己的 isolate 中安装结构化克隆的 generation。bootstrap bundle 不静态导入任何包。源码 Worker 入口保留自包含依赖,第三方 Worker 不接受注入。
129
+ - **由 consumer 持有严格语义。** 普通 Loader group 保留成功 sibling。App-boot 在首次结算后应用全局 required-entry policy;agent preset 与动态多 entry 组合在需要 all-or-nothing setup 时,持有并拆卸各自的独立 Loader 子树。App-boot 读取 failed fiber 来报告已记录的错误,并在一个进程检查点内合并 Loader 重复的 rejection 通知。
130
+ - **唯一 runtime resolution。** 安装优先、有序 bundle 逐根 breadth-first 遍历生成运行时表。runtime 解析不创建链接;runtime resolution 条目占据 `$DSH_HOME/profiles/node_modules` 上各自的包名位置,其余包名把该目录当作普通祖先。profile 加载时删除 Link 后端发布版写进 profile 的 `.dsh-module-fallback` 投影;pnpm 安装的包保留。package `imports` 选中的外部 bare target 使用相同的选包顺序,映射、conditions 和精确 target 解析仍由 Node 负责。完整后继 runtime resolution 可以在既有包映射和本地包名约束内原子增加 package name、更新 linked root 集合。
131
+ - **移除链接拦截。** 后继 generation 可以移除 linked root,无需重启。目录不再被任何剩余 root 覆盖时,后续请求使用原生查询,可能找到开发副本,也可能报告缺包。已有模块引用和 Node 缓存保持不变。同名、同目标可以重新加入;曾发布的名称改指向不同目标时,即使中间移除过也会被拒绝([generation 规则](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md#immutable-generations))。
132
+ - **应用自有 profile。** 应用自有 profile 使用相同的 runtime resolution。目标位于当前 profile 目录内的链接(包括 pnpm store 链接)不算外部 root,即使 profile 位于共享 profiles 树外。解析过程不修改其 `node_modules`;已安装包由 pnpm 管理。
133
+ - **自有 Worker。** Worker 构建 banner 会在业务 bundle 前导入 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。每个 Worker 在自己的 isolate 中安装结构化克隆的 runtime resolution。bootstrap bundle 不静态导入任何包。源码 Worker 入口保留自包含依赖,第三方 Worker 不接受注入。
119
134
  - **更新完成。** App boot 通过 `internal/update` waterfall 观察重启失败。实时 patch 重载在检查激活状态前等待配置树中的 fiber;单独调用 `Fiber.update()` 或 `Entry.update()` 不能确定重启成功。
120
135
  - **单一 rejection 检查点。** `inactiveEntries` 把折入启动诊断的确切原因保持到下一个进程级 rejection 检查点可见,使 `installFailLoud` 能合并 Loader 的重复通知,而所有无关的未处理 rejection 仍然致命。
121
136
  - **两阶段失败标签。** 除启动审计失败外,`boot()` 区分 `host preparation failed`(`prepare` 在任何配置树条目挂载前抛出)与 `plugin tree failed to load`,并追加最深层插件错误的堆栈。插件诊断保留嵌套原因和聚合错误中的各项失败;原因链出现循环时会停止遍历,但不会替换原始错误。
@@ -131,11 +146,12 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
131
146
  | 文件 | 职责 |
132
147
  |---|---|
133
148
  | [`src/index.ts`](src/index.ts) | 启动 helper:配置解析、环境加载、会明确报错的保护机制、激活审计、patch 解析、配置 dump、harness 源码段落 |
134
- | [`src/profile.ts`](src/profile.ts) | profile 发现、初始化、组合包解析、模块后备机制 |
149
+ | [`src/profile.ts`](src/profile.ts) | profile 发现、初始化、组合包解析、runtime resolution 构造 |
135
150
  | [`src/profile-plugins.ts`](src/profile-plugins.ts) | 已安装依赖、bundle 启用策略与 manifest 更新 |
136
151
  | [`src/profile-sanitize.ts`](src/profile-sanitize.ts) | profile patch 备份与恢复 bundle 启用状态 |
152
+ | [`src/config-schema/`](src/config-schema/) | Profile schema 生成、发现、原生投影与结果类型 |
137
153
  | [`src/profile-resolution/`](src/profile-resolution/) | 运行时 resolver、package metadata 服务与构建后 Worker bootstrap |
138
- | — | 不发布运行时不变式伴生入口;每个 resolver generation 只有一个 registration 所有,dual 模式在解析时比较独立物化的结果。 |
154
+ | — | 不发布运行时不变式伴生入口;每个 runtime resolution 只有一个拦截所有。 |
139
155
 
140
156
  </details>
141
157
 
@@ -173,7 +189,10 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
173
189
 
174
190
  这些限制说明此启动库在何时不合适,或何时需要特别注意。它们是当前包约束,不是任务积压。
175
191
 
176
- - **运行时解析依赖 Node 内部机制**——受支持的 Node 版本需要 native builtin access addon 和可执行兼容验证。只有构建后的 Harness 自有 Worker 接收 generation bootstrap;第三方 Worker 与自定义 `vm` linker 保持原生解析。
192
+ - **运行时解析依赖 Node 内部机制**——受支持的 Node 版本需要 native builtin access addon 和可执行兼容验证。只有构建后的 Harness 自有 Worker 接收 runtime resolution bootstrap;第三方 Worker 与自定义 `vm` linker 保持原生解析。
193
+ - **重新链接 profile 包需要重启**——Node 缓存真实路径,因此改变 profile 链接或依赖链接的目标需要重启进程。
194
+ - **链接作用域以记录的真实目录为准**——提升后的依赖若在所有 linked root 之外,就使用原生 Node。实时读取 peer 不会使 Node 缓存失效、监视文件或校验 peer 版本范围。
195
+ - **源码启动只安装 ESM 钩子**——CommonJS 请求仍需要 package exports 选中的 JavaScript 文件,解析器不会补出缺失的构建产物。
177
196
  - **快照回放替换仅识别特定 basename**——只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
178
197
  - **环境发现以启动为界**——`loadLayeredEnv` 只读取一次调用目录与 harness home 中的 `.env`;它不搜索父目录,也不跟随之后选择的 workspace。`loadEnv` 仍是非产品 bin 使用的单目录 helper。
179
198
  - **用户 patch 会替换匹配到的整个配置**——按 id 定位的 patch 不做深度合并,因此 profile 覆盖必须重述需要保留的组合包字段。
@@ -186,8 +205,8 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如
186
205
 
187
206
  本开发备注是维护者的工作上下文:开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
188
207
 
189
- #### 待定:配置 dump 稳定性
208
+ #### 待定:YAML 配置 dump 稳定性
190
209
 
191
- `renderConfigDump` 的输出是一份可加载的 YAML 文档,其 `# ==` 来源注释与 `!!js` 原样渲染服务于 `--dump-config` 诊断。任何内容都不承诺跨包版本的字节稳定性;在程序化消费该输出之前,请决定 dump 是否成为序列化约定。
210
+ `renderConfigDump` 的输出是一份可加载的 YAML 文档,其 `# ==` 来源注释与 `!!js` 原样渲染服务于 `--dump-config` 诊断。任何内容都不承诺跨包版本的字节稳定性;在程序化消费该输出之前,请决定 dump 是否成为序列化约定。JSON Schema 输出遵循单独的 [pre-stable 兼容性规则](../../../apps/cli/reference/README.zh.md#config-schema-dump)。
192
211
 
193
212
  </details>