diff --git a/.agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml similarity index 87% rename from .agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml rename to .agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml index 92b67825c5..6d1fc5cde6 100644 --- a/.agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.md +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md 2026-08-11-loader-entry-disabled-interpolation.md: 63be14ac5b25a3189f97e0283c36c29bfbcb5cec 2026-08-11-loader-entry-disabled-interpolation.zh.md: 74f14443221bbae5b3d602c3819d611783b59670 diff --git a/.agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.md b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md similarity index 100% rename from .agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.md rename to .agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md diff --git a/.agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.zh.md b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md similarity index 100% rename from .agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.zh.md rename to .agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml index 4ec2811550..231768828d 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md -2026-07-20-dsh-cli-personal-config.md: c910d28e2c616b1348e95d0a8731a9b47fc03edf -2026-07-20-dsh-cli-personal-config.zh.md: 161169af11c2c054ac5b1ae3df74fe963e57aec5 +2026-07-20-dsh-cli-personal-config.md: bc2aff322de01bb9c6beebb1679b2ff9909d1fe3 +2026-07-20-dsh-cli-personal-config.zh.md: 507a7188a4a77d904e3204499290f3ed22abab2c diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md index c910d28e2c..bc2aff322d 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md @@ -6,7 +6,7 @@ English | [中文](2026-07-20-dsh-cli-personal-config.zh.md) ## Problem -A developer's own preferences — which provider and model the TUI uses, personal credentials, a private adapter route — had nowhere to live except edits to committed files. Pointing the TUI demo at a personal Anthropic-proxy Opus route meant patching `examples/tui-agent/cordis.yml` and `.env` in the working tree, which risks committing secrets and repeats per checkout. There was also no installable command: running the agent in an arbitrary project directory required invoking the repo's demo script from the repo root. Loader metadata is static except the entry `disabled` field (see the [loader `disabled` interpolation decision](../process/2026-08-11-loader-entry-disabled-interpolation.md)), so "conditional composition uses overlays" (AGENTS.md) — but overlays only existed as committed sibling files, not as a machine-level layer. +A developer's own preferences — which provider and model the TUI uses, personal credentials, a private adapter route — had nowhere to live except edits to committed files. Pointing the TUI demo at a personal Anthropic-proxy Opus route meant patching `examples/tui-agent/cordis.yml` and `.env` in the working tree, which risks committing secrets and repeats per checkout. There was also no installable command: running the agent in an arbitrary project directory required invoking the repo's demo script from the repo root. Loader metadata is static except the entry `disabled` field (see the [loader `disabled` interpolation decision](../architecture/2026-08-11-loader-entry-disabled-interpolation.md)), so "conditional composition uses overlays" (AGENTS.md) — but overlays only existed as committed sibling files, not as a machine-level layer. ## Decision diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md index 161169af11..507a7188a4 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -开发者自己的偏好——TUI 使用哪个提供方和模型、个人凭证、私有的适配器路由——除了改动已提交的文件之外无处安放。要把 TUI 示例指向个人的 Anthropic 代理 Opus 路由,只能在工作区里改 `examples/tui-agent/cordis.yml` 和 `.env`,既有提交密钥的风险,又要在每个 checkout 里重复一遍。也没有可安装的命令:想在任意项目目录里运行这个 agent,必须回到仓库根目录调用示例脚本。Loader 元数据是静态的——条目 `disabled` 字段除外(见 [loader `disabled` 插值决策](../process/2026-08-11-loader-entry-disabled-interpolation.md))——所以「条件组合使用 overlay」(AGENTS.md);但 overlay 此前只以已提交的同级文件形式存在,没有机器级的层。 +开发者自己的偏好——TUI 使用哪个提供方和模型、个人凭证、私有的适配器路由——除了改动已提交的文件之外无处安放。要把 TUI 示例指向个人的 Anthropic 代理 Opus 路由,只能在工作区里改 `examples/tui-agent/cordis.yml` 和 `.env`,既有提交密钥的风险,又要在每个 checkout 里重复一遍。也没有可安装的命令:想在任意项目目录里运行这个 agent,必须回到仓库根目录调用示例脚本。Loader 元数据是静态的——条目 `disabled` 字段除外(见 [loader `disabled` 插值决策](../architecture/2026-08-11-loader-entry-disabled-interpolation.md))——所以「条件组合使用 overlay」(AGENTS.md);但 overlay 此前只以已提交的同级文件形式存在,没有机器级的层。 ## Decision diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml index 49952e51cf..6e4ca08c86 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md -2026-07-26-code-dispatch-ui-foundation.md: f98e919c2459327c8a42f31bea1fd115ce78b361 -2026-07-26-code-dispatch-ui-foundation.zh.md: a35864b3e2e2e2facb8d464d175a1e8f4d160ec1 +2026-07-26-code-dispatch-ui-foundation.md: 94316e774f231a2f2d5e9bcc8d1a30fd4a2ec733 +2026-07-26-code-dispatch-ui-foundation.zh.md: 2c9ee5b93e20b5c09950e2896a888f90863b8bd0 diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md index f98e919c24..94316e774f 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.md @@ -16,7 +16,7 @@ Three changes, one per obstacle: 1. **`run_code` gains a required `description` parameter** (bash's exact contract: active voice, 5-10 words, shown in the UI; whitespace-only rejected at execute). `presentCall` now titles the card with the description and moves the program to `rawInput`. The prompt-side cost is a few tokens per call; the return is that every surface — TUI card, ACP title, web row — gets a human-readable label without parsing TypeScript. 2. **`tool/code-dispatch` logs the sub-call's complete model-facing outcome** — `content: ContentBlock[]` + `isError`, the `tool/result` vocabulary — replacing `resultSummary` and deleting the summarize/cwd-normalization machinery outright. A UI renders a sub-call through the identical code path as a native result, including error text and non-text blocks. The event stays log-only (`deriveMessages()` ignores it): nothing about model context changes. -3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata was static when this shipped — no conditional row existed; the later [`disabled` interpolation decision](../process/2026-08-11-loader-entry-disabled-interpolation.md) makes one possible but changes nothing here — a native boot only registers the service, workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands. +3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata was static when this shipped — no conditional row existed; the later [`disabled` interpolation decision](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) makes one possible but changes nothing here — a native boot only registers the service, workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.zh.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.zh.md index a35864b3e2..2c9ee5b93e 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.zh.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.zh.md @@ -16,7 +16,7 @@ Status: implemented 1. **`run_code` 新增必填的 `description` 参数**(与 bash 完全相同的约定:主动语态、5-10 个词、展示在 UI 中;仅含空白的取值在执行时被拒绝)。`presentCall` 现在以该 description 作为卡片标题,并把程序文本移入 `rawInput`。提示词侧的成本是每次调用多出几个 token;换来的是每个表面——TUI 卡片、ACP(Agent Client Protocol)标题、Web 行——都无需解析 TypeScript 就能获得可供人阅读的标签。 2. **`tool/code-dispatch` 记录子调用面向模型的完整结果**(`content: ContentBlock[]` 加 `isError`,即 `tool/result` 的词汇),取代 `resultSummary`,并把摘要与 cwd 归一化机制彻底删除。UI 渲染子调用走的代码路径与渲染原生结果完全相同,包括错误文本和非文本块。该事件保持仅日志(`deriveMessages()` 忽略它):模型上下文没有任何变化。 -3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(本项交付时 loader 元数据仍是静态的,因此不存在条件行;后来的 [`disabled` 插值决策](../process/2026-08-11-loader-entry-disabled-interpolation.md) 让条件行成为可能,但此处不变——native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。 +3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(本项交付时 loader 元数据仍是静态的,因此不存在条件行;后来的 [`disabled` 插值决策](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) 让条件行成为可能,但此处不变——native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index d3fa7fd534..2aedf6e59f 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: 1ca6974424d6ce0b1ac86ca3eb84a8bf6807d9bf -2026-08-01-windows-pwsh-default.zh.md: 8affc28ac2509bec8a7a2f651bbf9518ca71fdb7 +2026-08-01-windows-pwsh-default.md: a749cf4d80f9f52fda23d57fd43433d6c9f4f710 +2026-08-01-windows-pwsh-default.zh.md: f9a1ff2c959268312a3907667ccecd00b32dd380 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index 1ca6974424..a749cf4d80 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -12,7 +12,7 @@ The harness's shipped execution profile is bash-first on every platform. Windows Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged. -- **The base patch gates both shell stacks on its own rows** (the [loader `disabled` interpolation](../process/2026-08-11-loader-entry-disabled-interpolation.md) note records the mechanism and the platform-layer fold): `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount only on win32 with the inverted expression — one shared patch file, exactly one shell stack per host. The confined pwsh stack runs over the ACL restricted-token runner, and the permission surface stays exactly as on POSIX (the [Windows ACL restricted-token sandbox](2026-08-08-windows-acl-restricted-token-sandbox.md) note owns that roster). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack or an unconfined pwsh executor overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel. The separate `windows.cordis.patch.yml` layer and the launcher's `apps/cli/src/windows-shell.ts` injection are deleted; the layer existed only because entry metadata was static. +- **The base patch gates both shell stacks on its own rows** (the [loader `disabled` interpolation](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note records the mechanism and the platform-layer fold): `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount only on win32 with the inverted expression — one shared patch file, exactly one shell stack per host. The confined pwsh stack runs over the ACL restricted-token runner, and the permission surface stays exactly as on POSIX (the [Windows ACL restricted-token sandbox](2026-08-08-windows-acl-restricted-token-sandbox.md) note owns that roster). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack or an unconfined pwsh executor overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel. The separate `windows.cordis.patch.yml` layer and the launcher's `apps/cli/src/windows-shell.ts` injection are deleted; the layer existed only because entry metadata was static. - **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the pwsh rows. `apps/cli` and `dsh-base` declare `dsh-pwsh-sandbox`/`dsh-tool-pwsh`, and the executor's dependency chain supplies `dsh-pwsh-local`; the base bundle lists every row plugin as a dependency by house style. The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior. diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index 8affc28ac2..f9a1ff2c95 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -12,7 +12,7 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。 -- **base patch 在自身行上按平台门控两个 shell 栈**([loader `disabled` 插值](../process/2026-08-11-loader-entry-disabled-interpolation.md) note 记录了该机制与平台层折叠):`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。受限 pwsh 栈运行在 ACL 受限令牌 runner 之上,权限面与 POSIX 完全一致([Windows ACL 受限令牌沙箱](2026-08-08-windows-acl-restricted-token-sandbox.md) note 拥有该清单)。覆盖交付默认是组合决策:偏好 bash 栈或不限权 pwsh 执行器的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。独立的 `windows.cordis.patch.yml` 层与启动器的 `apps/cli/src/windows-shell.ts` 注入已删除;该层只因条目元数据是静态的而存在。 +- **base patch 在自身行上按平台门控两个 shell 栈**([loader `disabled` 插值](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note 记录了该机制与平台层折叠):`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。受限 pwsh 栈运行在 ACL 受限令牌 runner 之上,权限面与 POSIX 完全一致([Windows ACL 受限令牌沙箱](2026-08-08-windows-acl-restricted-token-sandbox.md) note 拥有该清单)。覆盖交付默认是组合决策:偏好 bash 栈或不限权 pwsh 执行器的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。独立的 `windows.cordis.patch.yml` 层与启动器的 `apps/cli/src/windows-shell.ts` 注入已删除;该层只因条目元数据是静态的而存在。 - **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到 pwsh 行。`apps/cli` 与 `dsh-base` 声明 `dsh-pwsh-sandbox`/`dsh-tool-pwsh`,执行器的依赖链提供 `dsh-pwsh-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。 pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。 diff --git a/docs/cordis-tutorial/05-config.i18n.yaml b/docs/cordis-tutorial/05-config.i18n.yaml index 4e953918dd..603af93954 100644 --- a/docs/cordis-tutorial/05-config.i18n.yaml +++ b/docs/cordis-tutorial/05-config.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cordis-tutorial/05-config.md -05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4 -05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33 +05-config.md: 17cccce2ec43be65477ce527800ee6a636ee5d96 +05-config.zh.md: 87f1cb465d5f3e52a6f8e449cfb29818ba86dcb8 diff --git a/docs/cordis-tutorial/05-config.md b/docs/cordis-tutorial/05-config.md index 2357f66313..17cccce2ec 100644 --- a/docs/cordis-tutorial/05-config.md +++ b/docs/cordis-tutorial/05-config.md @@ -77,7 +77,7 @@ The loader used in this repo supports a `!!js` tag for config values that must b greeting: !!js process.env.DEMO_GREETING ?? 'Hello' ``` -`!!js` works **only inside `config`**. Entry metadata (`name`, `id`, `disabled`, `inject`, ...) is static; `disabled: !!js ...` produces a truthy expression object that always disables the entry. See [loader configuration](../cordis-primer.md#loader-configuration). +`!!js` works only inside `config` and in an entry's `disabled` field. `disabled: !!js ...` evaluates against the loader context at every mount decision (this repo's extension), so a row can gate itself on platform or environment; the other metadata (`name`, `id`, `inject`, ...) stays static, where an expression is ordinary truthy data. See [loader configuration](../cordis-primer.md#loader-configuration). Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application. diff --git a/docs/cordis-tutorial/05-config.zh.md b/docs/cordis-tutorial/05-config.zh.md index fbd94d1794..87f1cb465d 100644 --- a/docs/cordis-tutorial/05-config.zh.md +++ b/docs/cordis-tutorial/05-config.zh.md @@ -77,7 +77,7 @@ ValidationError: invalid config: greeting: !!js process.env.DEMO_GREETING ?? 'Hello' ``` -`!!js` **仅在 `config` 内有效**。Cordis 配置项的元数据(`name`、`id`、`disabled`、`inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该 Cordis 配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。 +`!!js` 仅在 `config` 与条目 `disabled` 字段内有效。`disabled: !!js ...` 在每次挂载决策时基于 loader 上下文求值(本仓库的扩展),可以按平台或环境门控一行;其余元数据(`name`、`id`、`inject` 等)保持静态,其中的表达式是普通真值数据。详见 [loader 配置](../cordis-primer.md#loader-configuration)。 下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。 diff --git a/scripts/verify-cordis-config.spec.ts b/scripts/verify-cordis-config.spec.ts index bfc4f10169..6c1304e16a 100644 --- a/scripts/verify-cordis-config.spec.ts +++ b/scripts/verify-cordis-config.spec.ts @@ -20,4 +20,20 @@ describe('verify-cordis-config metadata expressions', () => { const problems = metadataExpressionErrors({ id: { __jsExpr: 'process.platform' }, name: 'pkg' }, '[0]') expect(problems).toContain('[0].id: !!js is not interpolated here') }) + + it('rejects an expression nested below disabled (only the field itself interpolates)', () => { + const problems = metadataExpressionErrors( + { id: 'tool-bash', name: 'pkg', disabled: { when: { __jsExpr: 'process.platform' } } }, + '[0]', + ) + expect(problems).toContain('[0].disabled.when: !!js is not interpolated here') + }) + + it('rejects a disabled expression that does not parse (the loader would fail the boot)', () => { + const problems = metadataExpressionErrors( + { id: 'tool-bash', name: 'pkg', disabled: { __jsExpr: 'process.platform ===' } }, + '[0]', + ) + expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true) + }) }) diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index 0eb2c3571a..3ecd22bd99 100644 --- a/scripts/verify-cordis-config.ts +++ b/scripts/verify-cordis-config.ts @@ -1,13 +1,13 @@ /** * Validate Cordis Loader entry metadata and package resolution. * - * The Loader interpolates a plugin entry's `config` and the entry `disabled` - * field (both evaluate against the loader context; `disabled` at tree build). - * Every other entry metadata field stays static, so an expression there - * remains truthy data and silently changes composition. Example configs and - * the dsh Web composition resolve named plugins from their owning workspace - * manifests. Local example packages must also be in the root TypeScript - * project graph. + * The Loader interpolates a plugin entry's `config` (after declared injections + * activate, against that plugin context) and the entry `disabled` field (at + * every mount decision, against the loader context). Every other entry + * metadata field stays static, so an expression there remains truthy data and + * silently changes composition. Example configs and the dsh Web composition + * resolve named plugins from their owning workspace manifests. Local example + * packages must also be in the root TypeScript project graph. */ import { globSync, readFileSync } from 'node:fs' @@ -387,7 +387,9 @@ function validateMetadata(entry: Record, file: string, path: st /** * Expression-node diagnostics for one entry. `disabled` is the single - * interpolated metadata field; every other metadata field must stay static. + * interpolated metadata field: its own `!!js` expression node is allowed and + * must parse, while expressions nested below it stay truthy data; every other + * metadata field must stay fully static. * @param entry - one loader entry (or patch row). * @param path - the entry's diagnostic path prefix. * @returns one diagnostic per offending expression. @@ -400,9 +402,41 @@ export function metadataExpressionErrors(entry: Record, path: s collectExpressionPaths(entry[field], `${path}.${field}`, expressionPaths) for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`) } + const disabled = entry.disabled + if (disabled !== undefined) { + if (isJsExpr(disabled)) { + const detail = disabledExpressionProblem(disabled.__jsExpr) + if (detail !== undefined) problems.push(`${path}.disabled${detail}`) + } else { + // A non-expression value gates on Boolean() at mount; an expression + // nested anywhere below it never evaluates, so it must stay literal. + const expressionPaths: string[] = [] + collectExpressionPaths(disabled, `${path}.disabled`, expressionPaths) + for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`) + } + } return problems } +/** + * Parse-only validation of a `disabled` expression: the Loader evaluates it + * at every mount decision, and a syntax error would fail the boot — rejecting + * it here moves that failure to the earliest resolvable point. + * @param expression - the `!!js` expression text. + * @returns the diagnostic suffix, or `undefined` when the expression parses. + */ +function disabledExpressionProblem(expression: string): string | undefined { + try { + // Compilation only — the constructor never executes the body. + // eslint-disable-next-line no-new-func + new Function(`return (${expression})`) + return undefined + } catch (error) { + const detail = error instanceof Error ? error.message : String(error) + return `: disabled expression does not parse: ${detail}` + } +} + function collectExpressionPaths(value: unknown, path: string, output: string[]): void { if (isJsExpr(value)) { output.push(path)