From 3e6c1044455e342a4e6439495a7ab2367072e7de Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Mon, 28 Sep 2026 15:58:23 +0800 Subject: [PATCH 1/2] perf(settings): reuse composition for unoverridden entries --- packages/boot/config-editor/README.i18n.yaml | 4 +- packages/boot/config-editor/README.md | 2 + packages/boot/config-editor/README.zh.md | 2 + packages/boot/config-editor/src/index.ts | 10 ++- .../tests/configuration-inheritance.spec.ts | 89 +++++++++++++++++++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/settings/settings/tests/configuration-inheritance.spec.ts diff --git a/packages/boot/config-editor/README.i18n.yaml b/packages/boot/config-editor/README.i18n.yaml index ecc1d50e34..5e26f64cec 100644 --- a/packages/boot/config-editor/README.i18n.yaml +++ b/packages/boot/config-editor/README.i18n.yaml @@ -18,8 +18,8 @@ en: 1eedaa5d72b48177 zh: 39abc1d927a692d2 /deepseek-ai-dsh-config-editor/understand-the-implementation: - en: 972ac006df48ad85 - zh: df8adddc40817877 + en: b6ecd942daccc797 + zh: 2f4637d725034f96 /deepseek-ai-dsh-config-editor/further-exploration: en: 0c9e1d1401a865cc zh: 6130d52636e84676 diff --git a/packages/boot/config-editor/README.md b/packages/boot/config-editor/README.md index b2dd0fe5bc..a7ae05067a 100644 --- a/packages/boot/config-editor/README.md +++ b/packages/boot/config-editor/README.md @@ -40,6 +40,8 @@ Use [settings](../../settings/settings/README.md) for forms restricted to live f The [editor](src/index.ts) reconciles external changes before deriving a candidate, locks the profile manifest alongside other profile operations, and atomically replaces its config override. It preserves YAML comments and `!!js` expressions outside replaced values. A failed reconciliation restores the prior document and reloads the prior patches. +Configuration reads share one composition for entries without profile config overrides. Overridden entries are composed separately with only their own override removed; profile inserts and other entries' overrides remain effective. Returned configs are detached, and compositions are not cached between reads. + No invariant companion is published: the editor keeps no independent configuration projection. Loader and the persisted profile patch own configuration state. diff --git a/packages/boot/config-editor/README.zh.md b/packages/boot/config-editor/README.zh.md index 35823f52ea..a5bfe0faed 100644 --- a/packages/boot/config-editor/README.zh.md +++ b/packages/boot/config-editor/README.zh.md @@ -40,6 +40,8 @@ kind: "package-reference" [编辑器](src/index.ts) 在派生候选配置前应用外部更改,与其他 profile 操作共同锁定 profile manifest,并原子替换配置覆盖项。它保留替换值以外的 YAML 注释和 `!!js` 表达式。应用失败时恢复之前的文档并重新加载之前的 patch。 +读取配置时,没有 profile config 覆盖项的条目共用一次组合结果。有覆盖项的条目分别组合,仅移除自身的覆盖项;profile 插入的条目和其他条目的覆盖项仍然生效。返回的配置为独立副本,组合结果不跨读取缓存。 + 此包不发布 invariant companion:编辑器不维护独立配置投影。Loader 和持久化的 profile patch 拥有配置状态。 diff --git a/packages/boot/config-editor/src/index.ts b/packages/boot/config-editor/src/index.ts index 8caad69562..498d64f24f 100644 --- a/packages/boot/config-editor/src/index.ts +++ b/packages/boot/config-editor/src/index.ts @@ -49,8 +49,16 @@ export class ConfigEditor extends Service { configuration(): Array<{ entry: Entry; inherited: Record; override: Record }> { const profile = this.ownerContext.profileContext const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor) + const overridden = new Set(loaded.patches.filter(patch => patch.insert === undefined && Object.hasOwn(patch, 'config')).map(patch => patch.id)) + const composed = new Map() + for (const row of flatten(composeEntries([...loaded.layers.map(layer => layer.patches), loaded.patches]))) { + if (!composed.has(row.id)) composed.set(row.id, row) + } return this.entries().map(entry => ({ - entry, inherited: this.inherited(entry, loaded), + entry, + inherited: overridden.has(entry.options.id) + ? this.inherited(entry, loaded) + : structuredClone((composed.get(entry.options.id)?.config ?? {}) as Record), override: structuredClone((loaded.patches.findLast( row => row.id === entry.options.id && row.config !== undefined, )?.config ?? {}) as Record), diff --git a/packages/settings/settings/tests/configuration-inheritance.spec.ts b/packages/settings/settings/tests/configuration-inheritance.spec.ts new file mode 100644 index 0000000000..410d285eeb --- /dev/null +++ b/packages/settings/settings/tests/configuration-inheritance.spec.ts @@ -0,0 +1,89 @@ +/** Shared profile composition preserves per-entry inheritance and detached results. */ +import { readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect, it, vi } from 'vitest' +import * as appBoot from '@deepseek-ai/dsh-app-boot' +import { configurationFixture } from './configuration-fixture.ts' + +it('shares composition for entries without config overrides while retaining group and insert inheritance', async () => { + const { ctx, profile, start } = await configurationFixture({ hmr: false }) + await ctx.fiber.dispose() + writeFileSync(profile.patchPath, JSON.stringify([ + { id: 'first', config: { ordinary: 'fixed', count: 8 } }, + { id: 'first', config: { ordinary: 'fixed', count: 9 } }, + { id: 'second', disabled: false }, + { insert: [ + { id: 'inserted', name: 'cordis:probe', config: { ordinary: 'inserted', count: 3 } }, + { id: 'group', name: 'cordis:group', group: true, config: [ + { id: 'nested', name: 'cordis:probe', config: { ordinary: 'original', count: 4 } }, + ] }, + ] }, + { id: 'inserted', config: { ordinary: 'inserted', count: 7 } }, + { id: 'group', config: [ + { id: 'nested', name: 'cordis:probe', config: { ordinary: 'replacement', count: 6 } }, + ] }, + ])) + const restored = await start() + const compose = vi.spyOn(appBoot, 'composeEntries') + try { + const rows = restored.configEditor.configuration() + const inherited = Object.fromEntries(rows.map(row => [row.entry.options.id, row.inherited])) + expect(inherited).toMatchObject({ + first: { ordinary: 'fixed', token: 'private' }, + second: { ordinary: 'second' }, + inserted: { ordinary: 'inserted', count: 3 }, + nested: { ordinary: 'replacement', count: 6 }, + group: [{ id: 'nested', name: 'cordis:probe', config: { ordinary: 'original', count: 4 } }], + }) + expect(inherited['first']).not.toHaveProperty('count') + expect(compose).toHaveBeenCalledTimes(4) + const nested = rows.find(row => row.entry.options.id === 'nested')! + nested.inherited['ordinary'] = 'mutated' + expect(nested.entry.options.config).toMatchObject({ ordinary: 'replacement' }) + expect(restored.configEditor.configuration().find(row => row.entry.options.id === 'nested')!.inherited) + .toEqual({ ordinary: 'replacement', count: 6 }) + } finally { + compose.mockRestore() + } +}) + +it('recomposes unoverridden entries after bundle changes and returns empty configs when absent', async () => { + const { ctx, profile } = await configurationFixture({ hmr: false }) + const read = () => ctx.configEditor.configuration() + const before = read() + expect(before.find(row => row.entry.options.id === 'config-editor')!.inherited).toEqual({}) + const bundlePath = join(profile.dir, 'node_modules', 'test-bundle', 'cordis.patch.yml') + const patches = JSON.parse(readFileSync(bundlePath, 'utf8')) as Array<{ insert: Array<{ id: string; config?: object }> }> + patches[0]!.insert.find(row => row.id === 'second')!.config = { ordinary: 'new bundle value' } + writeFileSync(bundlePath, JSON.stringify(patches)) + expect(read().find(row => row.entry.options.id === 'second')!.inherited).toEqual({ ordinary: 'new bundle value' }) + expect(before.find(row => row.entry.options.id === 'second')!.inherited).toEqual({ ordinary: 'second' }) + patches[0]!.insert.push({ id: 'second', config: { ordinary: 'duplicate' } }) + writeFileSync(bundlePath, JSON.stringify(patches)) + expect(read().find(row => row.entry.options.id === 'second')!.inherited).toEqual({ ordinary: 'new bundle value' }) +}) + +it.each([0, 13])('bounds composition work for 190 entries with %i config overrides', async (overrideCount) => { + const { ctx, profile, start } = await configurationFixture({ hmr: false }) + await ctx.fiber.dispose() + const bundlePath = join(profile.dir, 'node_modules', 'test-bundle', 'cordis.patch.yml') + const patches = JSON.parse(readFileSync(bundlePath, 'utf8')) as Array<{ insert: object[] }> + const entries = Array.from({ length: 185 }, (_, index) => ({ + id: `generated-${index}`, name: 'cordis:probe', config: { ordinary: `value-${index}` }, + })) + patches[0]!.insert.push(...entries) + writeFileSync(bundlePath, JSON.stringify(patches)) + writeFileSync(profile.patchPath, JSON.stringify(entries.slice(0, overrideCount).map(entry => ({ + id: entry.id, config: { ...entry.config, count: 7 }, + })))) + const restored = await start() + const compose = vi.spyOn(appBoot, 'composeEntries') + try { + const rows = restored.configEditor.configuration() + expect(rows).toHaveLength(190) + expect(rows.find(row => row.entry.options.id === 'generated-0')!.inherited).toEqual({ ordinary: 'value-0' }) + expect(compose).toHaveBeenCalledTimes(overrideCount + 1) + } finally { + compose.mockRestore() + } +}) From f2af03539bf0554e302307f019e27d13e7460d54 Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Mon, 28 Sep 2026 16:12:42 +0800 Subject: [PATCH 2/2] perf(settings): skip unused shared compositions --- packages/boot/config-editor/src/index.ts | 10 +++++++--- .../tests/configuration-inheritance.spec.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/boot/config-editor/src/index.ts b/packages/boot/config-editor/src/index.ts index 498d64f24f..ffec879f40 100644 --- a/packages/boot/config-editor/src/index.ts +++ b/packages/boot/config-editor/src/index.ts @@ -49,12 +49,16 @@ export class ConfigEditor extends Service { configuration(): Array<{ entry: Entry; inherited: Record; override: Record }> { const profile = this.ownerContext.profileContext const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor) + const entries = this.entries() + // An own config key can replace inherited config even when its value is undefined. const overridden = new Set(loaded.patches.filter(patch => patch.insert === undefined && Object.hasOwn(patch, 'config')).map(patch => patch.id)) const composed = new Map() - for (const row of flatten(composeEntries([...loaded.layers.map(layer => layer.patches), loaded.patches]))) { - if (!composed.has(row.id)) composed.set(row.id, row) + if (entries.some(entry => !overridden.has(entry.options.id))) { + for (const row of flatten(composeEntries([...loaded.layers.map(layer => layer.patches), loaded.patches]))) { + if (!composed.has(row.id)) composed.set(row.id, row) + } } - return this.entries().map(entry => ({ + return entries.map(entry => ({ entry, inherited: overridden.has(entry.options.id) ? this.inherited(entry, loaded) diff --git a/packages/settings/settings/tests/configuration-inheritance.spec.ts b/packages/settings/settings/tests/configuration-inheritance.spec.ts index 410d285eeb..b55c1aad6d 100644 --- a/packages/settings/settings/tests/configuration-inheritance.spec.ts +++ b/packages/settings/settings/tests/configuration-inheritance.spec.ts @@ -1,4 +1,6 @@ -/** Shared profile composition preserves per-entry inheritance and detached results. */ +/** Shared profile composition preserves per-entry inheritance and detached results. + * Work-count spies require Vite-transformed source exports; native ESM namespaces are immutable. + */ import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { expect, it, vi } from 'vitest' @@ -63,18 +65,18 @@ it('recomposes unoverridden entries after bundle changes and returns empty confi expect(read().find(row => row.entry.options.id === 'second')!.inherited).toEqual({ ordinary: 'new bundle value' }) }) -it.each([0, 13])('bounds composition work for 190 entries with %i config overrides', async (overrideCount) => { +it.each([0, 13, 190])('bounds composition work for 190 entries with %i config overrides', async (overrideCount) => { const { ctx, profile, start } = await configurationFixture({ hmr: false }) await ctx.fiber.dispose() const bundlePath = join(profile.dir, 'node_modules', 'test-bundle', 'cordis.patch.yml') - const patches = JSON.parse(readFileSync(bundlePath, 'utf8')) as Array<{ insert: object[] }> + const patches = JSON.parse(readFileSync(bundlePath, 'utf8')) as Array<{ insert: Array<{ id: string; config?: object }> }> const entries = Array.from({ length: 185 }, (_, index) => ({ id: `generated-${index}`, name: 'cordis:probe', config: { ordinary: `value-${index}` }, })) patches[0]!.insert.push(...entries) writeFileSync(bundlePath, JSON.stringify(patches)) - writeFileSync(profile.patchPath, JSON.stringify(entries.slice(0, overrideCount).map(entry => ({ - id: entry.id, config: { ...entry.config, count: 7 }, + writeFileSync(profile.patchPath, JSON.stringify(patches[0]!.insert.slice(0, overrideCount).map(entry => ({ + id: entry.id, config: entry.config ?? {}, })))) const restored = await start() const compose = vi.spyOn(appBoot, 'composeEntries') @@ -82,7 +84,7 @@ it.each([0, 13])('bounds composition work for 190 entries with %i config overrid const rows = restored.configEditor.configuration() expect(rows).toHaveLength(190) expect(rows.find(row => row.entry.options.id === 'generated-0')!.inherited).toEqual({ ordinary: 'value-0' }) - expect(compose).toHaveBeenCalledTimes(overrideCount + 1) + expect(compose).toHaveBeenCalledTimes(overrideCount + Number(overrideCount < rows.length)) } finally { compose.mockRestore() }