fix(boot): answer user-disabled entries from the profile runtime
The inventory keeps dsh-app-boot an optional, type-only peer, so the walk over an entry's owning groups moves behind ProfileRuntime.userDisables(): a consumer with the runtime asks it, one without reports composition as before. The module graph and the cordis catalog follow.
This commit is contained in:
1 parent
d6f5160591
commit
c73d88e4e5
13 files changed
+66
-30
No files matched your search
@@ -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/module-graph.md
|
||||
module-graph.md: e435185d0d184d66c7d29df8a31ec2946ed8fdfc
|
||||
module-graph.zh.md: adf406a333885adaa980f93d4f4d6ecfd63bace7
|
||||
module-graph.md: e427d893e3b0a3cd1b2f9881a27366ccdcbe3949
|
||||
module-graph.zh.md: e94742d107f76554bbc3d6cb5f54b502cb868970
|
||||
@@ -392,6 +392,7 @@ flowchart TD
|
||||
pkg_storage_json --> pkg_storage
|
||||
pkg_storage_sqlite --> pkg_storage
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_subprocess --> pkg_launch_environment
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_session --> pkg_scope
|
||||
pkg_system_prompt --> pkg_invariants
|
||||
@@ -1259,7 +1260,7 @@ flowchart TD
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
|
||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
|
||||
@@ -394,6 +394,7 @@ flowchart TD
|
||||
pkg_storage_json --> pkg_storage
|
||||
pkg_storage_sqlite --> pkg_storage
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_subprocess --> pkg_launch_environment
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_session --> pkg_scope
|
||||
pkg_system_prompt --> pkg_invariants
|
||||
@@ -1261,7 +1262,7 @@ flowchart TD
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
|
||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
|
||||
@@ -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/subsystems/core.md
|
||||
core.md: 4028b19620e72a3c39164444453bb5fc1f818d7f
|
||||
core.zh.md: 2c75c867db13173c6db34b8a83f47c3f10f28972
|
||||
core.md: 275b95ac088260e01dacae0d24a3e0acdb935494
|
||||
core.zh.md: 23895d409d2f71021c8e38e98c423114c82ed23f
|
||||
@@ -861,6 +861,16 @@ originOf(rowId: string): RowOrigin | undefined
|
||||
*/
|
||||
userDisabledRowIds(): ReadonlySet<string>
|
||||
|
||||
/**
|
||||
* Whether the user patch layers disable an entry: its own row id, or the
|
||||
* id of a group holding it, is among {@link userDisabledRowIds}. The Loader
|
||||
* disables every descendant of a disabled group, so a child's own id alone
|
||||
* does not say who switched it off.
|
||||
* @param entry - the Loader entry.
|
||||
* @returns true when the user's patches disable the entry or one of the groups holding it.
|
||||
*/
|
||||
userDisables(entry: Entry): boolean
|
||||
|
||||
/**
|
||||
* Recompose the host tree from the profile's layers and the user patch files
|
||||
* as they stand now. The root Include re-applies the stack transactionally:
|
||||
|
||||
@@ -871,6 +871,16 @@ originOf(rowId: string): RowOrigin | undefined
|
||||
*/
|
||||
userDisabledRowIds(): ReadonlySet<string>
|
||||
|
||||
/**
|
||||
* Whether the user patch layers disable an entry: its own row id, or the
|
||||
* id of a group holding it, is among {@link userDisabledRowIds}. The Loader
|
||||
* disables every descendant of a disabled group, so a child's own id alone
|
||||
* does not say who switched it off.
|
||||
* @param entry - the Loader entry.
|
||||
* @returns true when the user's patches disable the entry or one of the groups holding it.
|
||||
*/
|
||||
userDisables(entry: Entry): boolean
|
||||
|
||||
/**
|
||||
* Recompose the host tree from the profile's layers and the user patch files
|
||||
* as they stand now. The root Include re-applies the stack transactionally:
|
||||
|
||||
@@ -65,7 +65,6 @@ export {
|
||||
} from './compose-stack.ts'
|
||||
export {
|
||||
ProfileRuntime, type ProfileRuntimeOptions, type RowOrigin,
|
||||
userDisablesEntry,
|
||||
} from './profile-runtime.ts'
|
||||
export {
|
||||
PLUGIN_PROBE_DIR, PLUGIN_PROBE_FORMAT, probePackage, readProbeCache, writeProbeCache,
|
||||
|
||||
@@ -131,6 +131,18 @@ export class ProfileRuntime extends Service {
|
||||
return this.committed.stack.userDisabledRowIds
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user patch layers disable an entry: its own row id, or the
|
||||
* id of a group holding it, is among {@link userDisabledRowIds}. The Loader
|
||||
* disables every descendant of a disabled group, so a child's own id alone
|
||||
* does not say who switched it off.
|
||||
* @param entry - the Loader entry.
|
||||
* @returns true when the user's patches disable the entry or one of the groups holding it.
|
||||
*/
|
||||
userDisables(entry: Entry): boolean {
|
||||
return userDisablesEntry(entry, this.committed.stack.userDisabledRowIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompose the host tree from the profile's layers and the user patch files
|
||||
* as they stand now. The root Include re-applies the stack transactionally:
|
||||
@@ -169,16 +181,8 @@ export class ProfileRuntime extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user patch layers disable an entry: its own row id, or the id
|
||||
* of a group holding it, is in the set. The Loader disables every descendant
|
||||
* of a disabled group, so a child's own id alone does not say who switched
|
||||
* it off.
|
||||
* @param entry - the Loader entry.
|
||||
* @param userDisabled - the ids the user layers disable, from `userDisabledRowIds()`.
|
||||
* @returns true when the user's patches disable the entry or one of the groups holding it.
|
||||
*/
|
||||
export function userDisablesEntry(entry: Entry, userDisabled: ReadonlySet<string>): boolean {
|
||||
/** The walk behind {@link ProfileRuntime.userDisables}: the entry, then each group holding it, outward. */
|
||||
function userDisablesEntry(entry: Entry, userDisabled: ReadonlySet<string>): boolean {
|
||||
for (let current: Entry | undefined = entry; current !== undefined; current = current.parent.ctx.fiber.entry) {
|
||||
if (typeof current.options.id === 'string' && userDisabled.has(current.options.id)) return true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { Entry, EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { claimLayerIds, ProfileRuntime, userDisablesEntry, type ComposedStack, type Profile, type ProfileLayer } from '../src/index.ts'
|
||||
import { claimLayerIds, ProfileRuntime, type ComposedStack, type Profile, type ProfileLayer } from '../src/index.ts'
|
||||
|
||||
const contexts: Context[] = []
|
||||
afterEach(async () => {
|
||||
@@ -100,13 +100,15 @@ describe('ProfileRuntime', () => {
|
||||
expect([...runtime.userDisabledRowIds()]).toEqual(['booted-off'])
|
||||
})
|
||||
|
||||
it('tells a row the user disabled through a group holding it from one the composition gates', () => {
|
||||
it('tells a row the user disabled through a group holding it from one the composition gates', async () => {
|
||||
const { runtime } = await harness([layer('a', 'builtin', [])])
|
||||
const chain = (ids: string[]): Entry => ids.reduceRight<Entry | undefined>(
|
||||
(parent, id) => ({ options: { id }, parent: { ctx: { fiber: { entry: parent } } } } as unknown as Entry), undefined,
|
||||
) as Entry
|
||||
expect(userDisablesEntry(chain(['kid', 'grp', 'root']), new Set(['grp']))).toBe(true)
|
||||
expect(userDisablesEntry(chain(['kid', 'grp']), new Set(['kid']))).toBe(true)
|
||||
expect(userDisablesEntry(chain(['kid', 'grp']), new Set(['other']))).toBe(false)
|
||||
// The booted composition disables 'booted-off'.
|
||||
expect(runtime.userDisables(chain(['kid', 'booted-off', 'root']))).toBe(true)
|
||||
expect(runtime.userDisables(chain(['booted-off', 'grp']))).toBe(true)
|
||||
expect(runtime.userDisables(chain(['kid', 'grp']))).toBe(false)
|
||||
})
|
||||
|
||||
it('recomposes through the root include, optionally re-reading the profile first, and commits on acceptance', async () => {
|
||||
|
||||
@@ -1403,6 +1403,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
parameters: [],
|
||||
returns: 'the ids, from the committed composition.',
|
||||
},
|
||||
{
|
||||
signature: 'userDisables(entry: Entry): boolean',
|
||||
description: 'Whether the user patch layers disable an entry: its own row id, or the id of a group holding it, is among userDisabledRowIds. The Loader disables every descendant of a disabled group, so a child\'s own id alone does not say who switched it off.',
|
||||
parameters: [{ name: 'entry', description: 'the Loader entry.' }],
|
||||
returns: 'true when the user\'s patches disable the entry or one of the groups holding it.',
|
||||
},
|
||||
{
|
||||
signature: 'async recompose(options: { reloadBundles?: boolean } = {}): Promise<void>',
|
||||
description: 'Recompose the host tree from the profile\'s layers and the user patch files as they stand now. The root Include re-applies the stack transactionally: a row whose options changed is updated in place, a row that appeared is created, a row that vanished is disposed, and a failure rolls the whole update back with the previous tree still running. The candidate profile, its ownership, and its conflicts become the committed composition only once the update holds; until then, and after a rejection, `current`, `layers`, `originOf`, and `conflicts` keep describing the running tree. Calls queue: one that arrives while another is in flight starts after it settled and reads what it committed. A rejection is that call\'s outcome alone and does not stop the ones behind it.',
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {} from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
// Type-only: the optional profile runtime and contained-failure registry the
|
||||
// boot glue provides, both resolved through `ctx.get`.
|
||||
import { userDisablesEntry } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-app-boot'
|
||||
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
|
||||
// Typert-generated ./typert and ./remote artifacts import Zod at runtime.
|
||||
import type {} from 'zod'
|
||||
@@ -79,7 +79,6 @@ export class PluginInventoryGateway extends TypertRemoteService {
|
||||
const entries: PluginInventoryEntry[] = []
|
||||
const runtime = this.ctx.get('profileRuntime')
|
||||
const failures = this.ctx.get('pluginFailures')
|
||||
const userDisabled = runtime?.userDisabledRowIds() ?? new Set<string>()
|
||||
const listed = new Set<string>()
|
||||
for (const entry of this.ctx.loader.entries()) {
|
||||
if (entry.options.group) continue
|
||||
@@ -96,7 +95,7 @@ export class PluginInventoryGateway extends TypertRemoteService {
|
||||
fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
|
||||
trust: origin?.trust ?? 'builtin',
|
||||
...origin === undefined ? {} : { package: packageRef(origin) },
|
||||
...enabled ? {} : { disabledBy: userDisablesEntry(entry, userDisabled) ? 'user' as const : 'composition' as const },
|
||||
...enabled ? {} : { disabledBy: runtime?.userDisables(entry) === true ? 'user' as const : 'composition' as const },
|
||||
...failure === undefined ? {} : { failure: { stage: failure.stage, message: failure.message } },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context, FiberState, type Plugin } from '@deepseek-ai/cordis'
|
||||
import Loader, { Group } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { AgentPresets } from '@deepseek-ai/dsh-agent-presets'
|
||||
import { ensurePluginFailures, type ProfileRuntime, type RowOrigin } from '@deepseek-ai/dsh-app-boot'
|
||||
import { ensurePluginFailures, ProfileRuntime, type ComposedStack, type Profile, type RowOrigin } from '@deepseek-ai/dsh-app-boot'
|
||||
import PluginInventoryGateway from '../src/index.ts'
|
||||
|
||||
const contexts: Context[] = []
|
||||
@@ -111,7 +111,7 @@ describe('PluginInventoryGateway', () => {
|
||||
])
|
||||
ctx.provide('profileRuntime', {
|
||||
originOf: (rowId: string) => origins.get(rowId),
|
||||
userDisabledRowIds: () => new Set([off]),
|
||||
userDisables: (entry: { id: string }) => entry.id === off,
|
||||
layers: [{ packageName: 'late', version: '9.9.9' }],
|
||||
conflicts: [
|
||||
{ rowId: 'tool', moduleName: 'late', layer: 'late', packageName: 'late', declaredBy: 'ext', message: 'row "tool" is already declared by ext' },
|
||||
@@ -160,9 +160,12 @@ describe('PluginInventoryGateway', () => {
|
||||
const groupId = await ctx.loader.create({ name: 'cordis:group', group: true, config: [{ name: 'cordis:active' }] })
|
||||
const child = [...ctx.loader.entries()].find(entry => !entry.options.group && entry.parent.ctx.fiber.entry?.options.id === groupId)
|
||||
expect(child).toBeDefined()
|
||||
ctx.provide('profileRuntime', {
|
||||
originOf: () => undefined, userDisabledRowIds: () => new Set([groupId]), layers: [], conflicts: [],
|
||||
} as unknown as ProfileRuntime)
|
||||
// The real runtime over a composition whose user layer disabled the group.
|
||||
const profile: Profile = { name: 'web', dir: '/p', layers: [], patchPath: '/p/cordis.patch.yml', patches: [], patchReload: 'live' }
|
||||
const stack: ComposedStack = {
|
||||
patches: [], layers: [], owners: new Map(), conflicts: [], skippedBundles: [], userDisabledRowIds: new Set([groupId]),
|
||||
}
|
||||
await ctx.plugin(ProfileRuntime, { profile, stack, loadProfile: () => profile, compose: () => stack, rootEntry: () => undefined })
|
||||
await ctx.loader.update(groupId, { disabled: true })
|
||||
const listed = (await inventory.list()).entries.find(entry => entry.entryId === child?.id)
|
||||
expect(listed).toMatchObject({ enabled: false, disabledBy: 'user' })
|
||||
|
||||
@@ -661,6 +661,7 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
|
||||
'AsyncIterable',
|
||||
'Context',
|
||||
'Error',
|
||||
'Entry',
|
||||
'EntryTree',
|
||||
'Exclude',
|
||||
'Extract',
|
||||
|
||||
Reference in new issue
Block a user