Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/dsh-candidate-source.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: dsh-candidate-source

on:
pull_request:
paths:
- compatibility-candidates.json
- compatibility.json
- packages/dsh/**
- packages/cli/**
- packages/model-knowledge/**
- scripts/qa-dsh-candidate-source.sh
- .github/workflows/dsh-candidate-source.yml
push:
branches:
- main
paths:
- compatibility-candidates.json
- compatibility.json
- packages/dsh/**
- packages/cli/**
- packages/model-knowledge/**
- scripts/qa-dsh-candidate-source.sh
- .github/workflows/dsh-candidate-source.yml
workflow_dispatch:

concurrency:
group: dsh-candidate-source-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
candidate-source-graph:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6.1.0
with:
version: 11.7.0
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- name: DSH candidate source-graph qualification
run: bash scripts/qa-dsh-candidate-source.sh
6 changes: 5 additions & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ async function probeRuntime(): Promise<{ rows: Row[]; failed: boolean }> {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
// Empty config is valid in both the verified rc.7 SystemPrompt generation
// (`persona?`) and current DSH (`personaPrefix?` / `personaSuffix?`). The
// doctor probe registers its scoped prompt section separately, so it does
// not need a deployment persona here.
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
Expand Down
79 changes: 55 additions & 24 deletions packages/dsh/src/config-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
*
* Precedence (tested): defaults → user → project → request → runtime
* validation. The user layer comes from `ctx.settings` when a settings
* provider is composed (official settings-namespace seam); the project layer
* is the committed file; request layers come from the resolve call.
* provider is composed; the project layer is the committed file; request
* layers come from the resolve call.
*/
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { loadPolicyLayers, type PolicyDocument, type PolicyLayerValue } from '@dshelm/core'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'

export const DSHELM_CONFIG_DIR = '.dshelm'
export const DSHELM_CONFIG_FILE = 'config.jsonc'
export const DSHELM_SETTINGS_NAMESPACE = settingsNamespace('dshelm')
// rc.7 exported settingsNamespace(); current DSH validates the literal inside
// SettingsProvider.register(). The branded literal works with both type surfaces
// without depending on the removed value export.
export const DSHELM_SETTINGS_NAMESPACE = 'dshelm' as SettingsNamespace

/** Schema-backed user-level override document (optional fields). */
export interface DSHelmUserSettings {
Expand All @@ -23,6 +26,32 @@ export interface DSHelmUserSettings {
readonly categories?: Record<string, unknown>
}

/**
* Minimal callable schema accepted by both settings generations. The settings
* service has already merged its composition/user layers before invoking this
* function; full DSHelm policy validation remains owned by @dshelm/core.
* `toJSON` is the descriptor surface used by settings UIs.
*/
const dshelmUserSettingsSchema = Object.assign(
(value: unknown): DSHelmUserSettings => {
if (value === undefined) return {}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError('dshelm settings section must be an object')
}
return value as DSHelmUserSettings
},
{
toJSON: () => ({
type: 'object',
properties: {
profiles: { type: 'object' },
agents: { type: 'object' },
categories: { type: 'object' },
},
}),
},
)

/**
* Load the project layer from `<cwd>/.dshelm/config.jsonc` when present.
* A missing file yields `undefined` (no project override); malformed
Expand All @@ -39,32 +68,34 @@ export async function loadProjectPolicyLayer(cwd: string): Promise<PolicyLayerVa
}

/**
* Install the `dshelm` user-settings namespace (official
* `@deepseek-ai/dsh-settings` seam). Returns the live reader; the section
* is fiber-owned and unwinds on disposal.
* Install the optional `dshelm` user-settings namespace through the stable
* `ctx.settings.register()` service seam shared by rc.7 and current DSH.
*
* `installSettingsSection(ctx, ns, schema, entry, hooks)` signature verified
* against `@deepseek-ai/dsh-settings` rc.6: `hooks.setSource` receives a
* THUNK returning the currently authoritative value, not the value itself.
* Older DSH exported an `installSettingsSection()` helper while 0.1.5 moved
* that convenience operation onto SettingsProvider. Depending on either helper
* would make this source generation-specific, so DSHelm composes the common
* primitive directly. The returned reader stays live because it calls
* `scope.get()` instead of snapshotting the value at registration time.
*/
export function installDSHelmSettings(
ctx: Context,
base: DSHelmUserSettings,
): () => DSHelmUserSettings | undefined {
let source: DSHelmUserSettings = base
installSettingsSection(
ctx,
DSHELM_SETTINGS_NAMESPACE,
// Minimal schema: free-form policy sections layered on the base document.
// Full schema validation happens in @dshelm/core at load time.
{ profiles: { type: 'object' }, agents: { type: 'object' }, categories: { type: 'object' } } as never,
base,
{
setSource: (current) => { source = current() },
onChange: () => {},
},
)
return () => source
let current: () => DSHelmUserSettings = () => base

ctx.inject(['settings'], (settingsCtx) => {
const scope = settingsCtx.settings.register(
DSHELM_SETTINGS_NAMESPACE,
dshelmUserSettingsSchema as never,
{ base },
)
current = () => scope.get() as DSHelmUserSettings
settingsCtx.effect(() => () => {
current = () => base
})
})

return () => current()
}

/**
Expand Down
21 changes: 18 additions & 3 deletions packages/dsh/src/projection-types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import type {} from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-session-projection/types'
import type { ControlPlaneProjectionValue } from './session-events.ts'

/**
* Host-only augmentation for the merge-extensible DSH projection map.
* Host-only augmentation for the merge-extensible DSH projection maps.
*
* Kept out of `session-events.ts` because that shared wire-types module is also
* compiled into the browser client declaration graph. Both rc.7 and 0.1.5
* publish this augmentation target from `@deepseek-ai/dsh-session-projection/types`.
* compiled into the browser client declaration graph. rc.7 uses only
* `SessionProjectionMap`; current DSH separates internal fold state into
* `SessionProjectionStateMap`. Declaring both keeps one DSHelm projection key
* valid across the two generations without leaking host augmentation into the
* browser declaration graph.
*
* The explicit type-only imports are load-bearing: the root import activates
* DSH's `Context.sessionProjections` augmentation, while the `/types` import
* makes this declaration an augmentation of the published module rather than
* a new ambient module after `projection.ts` stopped importing the
* generation-specific `ProjectionDefinition` type.
*/
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
'dshelm.controlPlane': ControlPlaneProjectionValue
}

interface SessionProjectionStateMap {
'dshelm.controlPlane': ControlPlaneProjectionValue | undefined
}
}

export {}
71 changes: 51 additions & 20 deletions packages/dsh/src/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
*
* The host publishes whole-value `dshelm/control-plane` events; this unit
* folds them into the `dshelm.controlPlane` projection key, which the
* official `session/projection` wire + client projection face deliver
* to the browser. Registered through `ctx.sessionProjections.register`.
* official session-projection wire + client projection face deliver to the
* browser. Registered through `ctx.sessionProjections.register`.
*/
import { z } from 'zod'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import './projection-types.ts'
import type { ControlPlaneProjectionValue } from './session-events.ts'
Expand All @@ -18,7 +17,7 @@ import type { ControlPlaneProjectionValue } from './session-events.ts'
* The annotation is `z.ZodType<ControlPlaneProjectionValue>`: zod's inferred
* output spells optional fields as `T | undefined` while the interfaces use
* exact optional properties, so the annotation is a variance-only cast — the
* runtime `parse` (the projection drive's only runtime consumer) is unchanged.
* runtime `parse` is unchanged.
*/
export const controlPlaneSchema: z.ZodType<ControlPlaneProjectionValue> = z.object({
version: z.literal(1),
Expand Down Expand Up @@ -60,23 +59,55 @@ export const controlPlaneSchema: z.ZodType<ControlPlaneProjectionValue> = z.obje
/** Internal fold state: the latest whole snapshot. */
type ControlPlaneState = ControlPlaneProjectionValue | undefined

export const dshelmControlPlaneProjection: ProjectionDefinition<'dshelm.controlPlane', ControlPlaneState> = {
key: 'dshelm.controlPlane',
schema: controlPlaneSchema,
init: () => undefined,
apply: (state, event: SessionEvent) =>
event.type === 'dshelm/control-plane' ? event.data : state,
view: (state) => {
if (state === undefined) {
return {
version: 1,
request: { category: '' },
roles: [],
inspector: { request: 'no-delegation', trace: { version: 1, request: { category: '' }, category: '', agent: '', profile: '', candidates: [], fields: [] } },
source: 'projection:empty',
}
const controlPlaneStateSchema = controlPlaneSchema.optional() as unknown as z.ZodType<ControlPlaneState>

function viewControlPlane(state: ControlPlaneState): ControlPlaneProjectionValue {
if (state === undefined) {
return {
version: 1,
request: { category: '' },
roles: [],
inspector: {
request: 'no-delegation',
trace: {
version: 1,
request: { category: '' },
category: '',
agent: '',
profile: '',
candidates: [],
fields: [],
},
},
source: 'projection:empty',
}
return state
}
return state
}

/**
* Dual-generation projection definition.
*
* rc.7 reads the top-level `schema` + `view` fields. Current DSH separates the
* persisted fold state (`stateSchema`) from the client wire surface (`wire`).
* Keeping both structural contracts on one object lets each registry consume
* only the fields it owns while the actual fold/view semantics stay identical.
*/
export const dshelmControlPlaneProjection = {
key: 'dshelm.controlPlane' as const,
// rc.7 projection contract.
schema: controlPlaneSchema,
view: viewControlPlane,
// Current projection contract.
stateSchema: controlPlaneStateSchema,
wire: {
viewSchema: controlPlaneSchema,
view: viewControlPlane,
},
// A no-argument initializer remains assignable to the current initializer,
// whose SessionHeader/inheritedEventCount inputs are not needed here.
init: (): ControlPlaneState => undefined,
apply: (state: ControlPlaneState, event: SessionEvent): ControlPlaneState =>
event.type === 'dshelm/control-plane' ? event.data : state,
stateVersion: 1,
}
Loading
Loading