fix(cordis): accept colon-containing entry ids used by generated bundles

This commit is contained in:
Shigma
2026-10-29 01:50:09 +08:00
parent 8866ed40bb
commit 9a26b453c3
7 changed files with 28 additions and 30 deletions
@@ -3,14 +3,14 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-alpha.2/cordis-loader-entry-trees/guide.md
/:
en: 4b668dd74d0315b9
zh: a1885d2bbb4ef33e
en: 02dfa517303f3206
zh: abf9a22603299cc7
/loader-entry-tree-reports-and-include-persistence:
en: dc27815908c90cd9
zh: e9cc077d4d3fdbec
/loader-entry-tree-reports-and-include-persistence/change:
en: 2436912b9554b6dd
zh: ff4582ebc4672662
en: 6ada19e725d5c04e
zh: efbc6183cfd094e1
/loader-entry-tree-reports-and-include-persistence/migration:
en: cd7db13e17bfd339
zh: be43c51c78e55439
en: 574f30ad53da1e66
zh: 1b9914c4ead412f7
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
description: "Loader entry trees report mutations through commit(change), reject ids containing a colon, and Include persists them through a journal."
description: "Loader entry trees report mutations through commit(change), and Include persists them through a journal."
---
# Loader entry tree reports and Include persistence
@@ -12,14 +12,12 @@ English | [中文](guide.zh.md)
`@deepseek-ai/cordis-plugin-loader` follows Loader 1.0.0-rc.8, and `@deepseek-ai/cordis-plugin-include` follows Include 1.1.1. This affects `cordis.yml` and patch files, code that subclasses `EntryTree`, and code that calls `applyEntryPatches` or relies on Include writes.
- `EntryTree.write()` is removed. Subclasses implement `commit(change: EntryChange)`, which receives each create, update, move, removal and explicit Fiber save synchronously. In-memory trees implement a no-op.
- An entry `id` containing `:` is rejected, because `:` separates nested tree paths. A configured row such as `id: foo:bar` previously mounted; it is now logged as an error and skipped, and a programmatic `create()` throws.
- `applyEntryPatches(data, patches, warn)` always returns cloned entries, including without patches, and clones inserted rows. Callers can no longer mutate the input through the result.
- Include persists runtime changes through a journal reconciled with the latest file. File edits win conflicting fields and are logged as conflicts. Changes to patch-owned fields and inserted rows update the parent Include's patches. `flush()` waits for pending writes and rejects while changes remain unsaved; `refresh()` and `stop()` return the same result.
## Migration
1. Rename entry ids that contain `:` in `cordis.yml`, profile patches and `--patch` overlays, for example `foo:bar` to `foo-bar`, and update patches that address them.
2. In `EntryTree` subclasses, replace `write()` with `commit(change)`. Persist from `change.options` and `change.legacy`: a missing `legacy` is a creation and a missing `options` is a removal.
3. Treat the `applyEntryPatches` result as the only copy to modify.
4. Where code needs a runtime change on disk, await `include.flush()`; for patch-owned changes, flush the child Include and then its parent.
5. Start the application and confirm that no `entry id must not contain ":"` errors are logged and that runtime edits appear in the configuration file after `flush()`.
1. In `EntryTree` subclasses, replace `write()` with `commit(change)`. Persist from `change.options` and `change.legacy`: a missing `legacy` is a creation and a missing `options` is a removal.
2. Treat the `applyEntryPatches` result as the only copy to modify.
3. Where code needs a runtime change on disk, await `include.flush()`; for patch-owned changes, flush the child Include and then its parent.
4. Start the application and confirm that runtime edits appear in the configuration file after `flush()`.
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
description: "Loader 条目树通过 commit(change) 报告变更并拒绝含冒号的 id,Include 通过 journal 持久化这些变更。"
description: "Loader 条目树通过 commit(change) 报告变更,Include 通过 journal 持久化这些变更。"
---
# Loader 条目树变更报告与 Include 持久化
@@ -12,14 +12,12 @@ description: "Loader 条目树通过 commit(change) 报告变更并拒绝含冒
`@deepseek-ai/cordis-plugin-loader` 跟随 Loader 1.0.0-rc.8,`@deepseek-ai/cordis-plugin-include` 跟随 Include 1.1.1。受影响的是 `cordis.yml` 与 patch 文件、继承 `EntryTree` 的代码,以及调用 `applyEntryPatches` 或依赖 Include 写盘的代码。
- 移除了 `EntryTree.write()`。子类改为实现 `commit(change: EntryChange)`,它会同步收到每一次创建、更新、移动、删除和显式 Fiber 保存。内存树实现为空操作。
- 含 `:` 的条目 `id` 会被拒绝,因为 `:` 用于分隔嵌套树路径。此前 `id: foo:bar` 这样的配置行可以挂载;现在会记录一条错误并跳过该行,程序化调用 `create()` 会抛出异常。
- `applyEntryPatches(data, patches, warn)` 总是返回克隆后的条目,没有 patch 时也一样,插入行同样会被克隆。调用方不能再通过结果修改输入。
- Include 通过与最新文件协调的 journal 持久化运行时变更。文件编辑在字段冲突时优先,并记录为冲突。对 patch 所拥有字段和插入行的修改会写入父 Include 的 patch。`flush()` 等待待处理的写入,并在仍有未保存变更时拒绝;`refresh()` 和 `stop()` 返回相同的结果。
## 迁移
1. 在 `cordis.yml`、profile patch 和 `--patch` 覆盖层中重命名含 `:` 的条目 id,例如把 `foo:bar` 改为 `foo-bar`,并同步更新引用它们的 patch。
2. 在 `EntryTree` 子类中把 `write()` 换成 `commit(change)`。根据 `change.options` 和 `change.legacy` 持久化:缺少 `legacy` 表示创建,缺少 `options` 表示删除。
3. 只修改 `applyEntryPatches` 返回的结果。
4. 需要运行时变更落盘时,await `include.flush()`;对于 patch 所拥有的变更,先 flush 子 Include,再 flush 其父 Include。
5. 启动应用,确认没有记录 `entry id must not contain ":"` 错误,并且 `flush()` 之后运行时编辑已出现在配置文件中。
1. 在 `EntryTree` 子类中把 `write()` 换成 `commit(change)`。根据 `change.options` 和 `change.legacy` 持久化:缺少 `legacy` 表示创建,缺少 `options` 表示删除。
2. 只修改 `applyEntryPatches` 返回的结果。
3. 需要运行时变更落盘时,await `include.flush()`;对于 patch 所拥有的变更,先 flush 子 Include,再 flush 其父 Include。
4. 启动应用,确认 `flush()` 之后运行时编辑已出现在配置文件中。
+6 -4
View File
@@ -85,12 +85,14 @@ describe('Loader tree mutation reports', () => {
expect(changes).toEqual([])
})
it('rejects separator-containing ids before changing a tree', async () => {
it('mounts explicit ids containing the separator without nested-path resolution', async () => {
const { loader, changes } = await fixture()
const options: EntryOptions = { id: 'a:b', name: 'cordis:example' }
await expect(loader.create(options)).rejects.toThrow('entry id must not contain')
expect(loader.root.data).toEqual([])
expect(changes).toEqual([])
await expect(loader.create(options)).resolves.toBe('a:b')
await loader.await()
expect(loader.store['a:b']!.fiber!.state).toBe(FiberState.ACTIVE)
expect(changes).toEqual([{ id: 'a:b', group: loader.root, options }])
expect(() => loader.resolve('a:b')).toThrow('cannot resolve entry a:b')
})
})
+2 -1
View File
@@ -30,7 +30,7 @@ Intentionally **not** vendored (verified unused by this set): `reggol`, `@cordis
## Local modifications
Keep this log exhaustive — every divergence from upstream must be listed. Each entry has a stable ID: remove an entry when its divergence no longer exists, and never renumber or reuse IDs. The next new entry is V27.
Keep this log exhaustive — every divergence from upstream must be listed. Each entry has a stable ID: remove an entry when its divergence no longer exists, and never renumber or reuse IDs. The next new entry is V28.
- **V1 · `hmr/src/index.ts`**: removed the `./locales/en-US.yml` / `./locales/zh-CN.yml` imports, the `.i18n({...})` call on the `Config` schema, and the `src/locales/` directory. Rationale: those imports require a runtime YAML loader hook (`@cordisjs/unyaml`) that we do not vendor; the i18n texts only localize config descriptions.
- **V2 · All `package.json` files**: regenerated for Harness releases with scoped names, release versions, publication metadata, precise bundled-runtime and `lib/types/**/*.d.ts` / `.d.ts.map` file entries, source exports where applicable, and declaration metadata pointing at `lib/types`. References within the vendored set use `workspace:~`. HMR declares `esbuild` as a direct dev dependency for its imported `BuildFailure` type, and Loader requires `node-addon-require-builtin@^0.1.4` to match published app runtimes.
- **V3 · All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json`, emit TypeScript intermediates to `lib/types`, and declare project references.
@@ -50,6 +50,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. Each
- **V24 · `timer/locale/{en,zh}.json` display metadata**: local English and Chinese titles and descriptions for the Settings plugin inventory. `timer/package.json` exports and publishes these locale resources.
- **V25 · Loader lifecycle adaptations**: before a combined context/config update, Loader refreshes dependencies locally, which prevents activation against stale isolation bindings; disabled-entry updates await disposal. Failed fibers require an explicit update after correcting their input or dependencies. Core retains the effect hardening in V6; rc.11 owns dependency teardown ordering and superseded-activation handling.
- **V26 · `loader/src/config/tree.ts` cross-tree move reports**: `EntryTree.update()` reports a move into another tree as a removal to the source tree and a creation, without `legacy` or `from`, to the destination tree. Upstream reports the destination as an update with `legacy`, which the Include journal records as a partial row for an id that its file does not contain. Covered by `scripts/cordis-entry-commit.spec.ts`.
- **V27 · `loader/src/config/tree.ts` explicit ids with `:`**: `ensureId()` accepts explicit ids containing `EntryTree.sep`; upstream rejects them. Generated bundles from `@deepseek-ai/dsh-creation-recommendations` prefix their row ids with `<package name>:`, and installed profiles already contain such rows. `resolve()` still splits ids on `:`, so these entries cannot be addressed by nested path. Covered by `scripts/cordis-entry-commit.spec.ts` and `packages/experimental/creation-recommendations/tests/loader-composition.spec.ts`.
### Resolver adaptations
+1 -1
View File
@@ -68,4 +68,4 @@ Entry updates await `loader/patch-context`, the Fiber update waterfall and disab
## Tree mutation reports
`EntryTree.commit(change)` receives each mutation made through `EntryTree` methods synchronously after options and group membership change; file-driven `EntryGroup` reconciliation does not report. A missing `legacy` means creation; missing `options` means removal. Updates carry both, with `from` naming the previous group after a move within the tree; a move into another tree reports a removal to the source tree and a creation to the destination tree. Explicit Fiber saves report their simplified configuration. File-driven reconciliation, parent teardown and Loader removal do not produce spurious self-disable reports. In-memory trees implement a no-op; Include journals the reports and reconciles them with file edits. Entry ids cannot contain `:`, which separates nested tree paths.
`EntryTree.commit(change)` receives each mutation made through `EntryTree` methods synchronously after options and group membership change; file-driven `EntryGroup` reconciliation does not report. A missing `legacy` means creation; missing `options` means removal. Updates carry both, with `from` naming the previous group after a move within the tree; a move into another tree reports a removal to the source tree and a creation to the destination tree. Explicit Fiber saves report their simplified configuration. File-driven reconciliation, parent teardown and Loader removal do not produce spurious self-disable reports. In-memory trees implement a no-op; Include journals the reports and reconciles them with file edits. Explicit entry ids may contain `:`; `resolve()` cannot address such entries by nested path, because `:` separates nested tree paths.
+3 -4
View File
@@ -73,18 +73,17 @@ export abstract class EntryTree {
}
/**
* Assign a random tree-local id when `options.id` is absent.
* Assign a random tree-local id when `options.id` is absent. Explicit ids
* may contain `EntryTree.sep`; `resolve()` and `resolveGroup()` cannot
* address such entries by nested path.
* @param options - Entry options; a generated id is written to `options.id`.
* @returns The existing or generated id.
* @throws When an explicit id contains `EntryTree.sep`, which separates nested tree paths.
*/
ensureId(options: Partial<EntryOptions>) {
if (!options.id) {
do {
options.id = Math.random().toString(16).slice(2, 10)
} while (this.store[options.id])
} else if (options.id.includes(EntryTree.sep)) {
throw new Error(`entry id must not contain "${EntryTree.sep}": ${options.id}`)
}
return options.id!
}