Merge pull request #5357 from deepseek-harness/turtle/config-inherited-compose
perf(settings): reuse composition for unoverridden entries
This commit is contained in:
commit
3366973eea
5 files changed
+111
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -40,6 +40,8 @@ kind: "package-reference"
|
||||
|
||||
[编辑器](src/index.ts) 在派生候选配置前应用外部更改,与其他 profile 操作共同锁定 profile manifest,并原子替换配置覆盖项。它保留替换值以外的 YAML 注释和 `!!js` 表达式。应用失败时恢复之前的文档并重新加载之前的 patch。
|
||||
|
||||
读取配置时,没有 profile config 覆盖项的条目共用一次组合结果。有覆盖项的条目分别组合,仅移除自身的覆盖项;profile 插入的条目和其他条目的覆盖项仍然生效。返回的配置为独立副本,组合结果不跨读取缓存。
|
||||
|
||||
此包不发布 invariant companion:编辑器不维护独立配置投影。Loader 和持久化的 profile patch 拥有配置状态。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -49,8 +49,20 @@ export class ConfigEditor extends Service {
|
||||
configuration(): Array<{ entry: Entry; inherited: Record<string, unknown>; override: Record<string, unknown> }> {
|
||||
const profile = this.ownerContext.profileContext
|
||||
const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor)
|
||||
return this.entries().map(entry => ({
|
||||
entry, inherited: this.inherited(entry, loaded),
|
||||
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<string, EntryOptions>()
|
||||
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 entries.map(entry => ({
|
||||
entry,
|
||||
inherited: overridden.has(entry.options.id)
|
||||
? this.inherited(entry, loaded)
|
||||
: structuredClone((composed.get(entry.options.id)?.config ?? {}) as Record<string, unknown>),
|
||||
override: structuredClone((loaded.patches.findLast(
|
||||
row => row.id === entry.options.id && row.config !== undefined,
|
||||
)?.config ?? {}) as Record<string, unknown>),
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/** 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'
|
||||
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, 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: 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(patches[0]!.insert.slice(0, overrideCount).map(entry => ({
|
||||
id: entry.id, config: entry.config ?? {},
|
||||
}))))
|
||||
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 + Number(overrideCount < rows.length))
|
||||
} finally {
|
||||
compose.mockRestore()
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user