dsh-brief-sidebar 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +178 -0
  2. package/cordis.patch.yml +18 -0
  3. package/dsh.plugin.json +16 -0
  4. package/lib/client.js +960 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.mjs +5620 -0
  7. package/lib/index.mjs.map +1 -0
  8. package/lib/types/client/BriefIcon.d.ts +13 -0
  9. package/lib/types/client/SummaryTab.d.ts +18 -0
  10. package/lib/types/client/TodoBoardTab.d.ts +31 -0
  11. package/lib/types/client/brief/board.d.ts +50 -0
  12. package/lib/types/client/brief/dock-shadow.d.ts +49 -0
  13. package/lib/types/client/brief/use-todos.d.ts +13 -0
  14. package/lib/types/client/file-open.d.ts +50 -0
  15. package/lib/types/client/index.d.ts +23 -0
  16. package/lib/types/client/locales.d.ts +53 -0
  17. package/lib/types/client/summary/DeliverablesSection.d.ts +18 -0
  18. package/lib/types/client/summary/deliverables.d.ts +40 -0
  19. package/lib/types/client/use-projection.d.ts +54 -0
  20. package/lib/types/index.d.ts +27 -0
  21. package/lib/types/projection/deliverables-fold.d.ts +85 -0
  22. package/lib/types/projection/keys.d.ts +25 -0
  23. package/lib/types/projection/register.d.ts +9 -0
  24. package/package.json +94 -0
  25. package/src/client/BriefIcon.tsx +56 -0
  26. package/src/client/SummaryTab.tsx +73 -0
  27. package/src/client/TodoBoardTab.tsx +188 -0
  28. package/src/client/brief/board.ts +104 -0
  29. package/src/client/brief/dock-shadow.tsx +74 -0
  30. package/src/client/brief/use-todos.ts +27 -0
  31. package/src/client/file-open.ts +108 -0
  32. package/src/client/index.tsx +144 -0
  33. package/src/client/locales.ts +78 -0
  34. package/src/client/summary/DeliverablesSection.tsx +192 -0
  35. package/src/client/summary/deliverables.ts +93 -0
  36. package/src/client/use-projection.ts +117 -0
  37. package/src/index.ts +32 -0
  38. package/src/projection/deliverables-fold.ts +206 -0
  39. package/src/projection/keys.ts +27 -0
  40. package/src/projection/register.ts +99 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Reading a live session projection from the client Session Controller.
3
+ *
4
+ * better-sidebar tab bodies are NOT rendered inside a DSH slot, so the
5
+ * framework's `useProjection` standard prop is not available to them. The
6
+ * equivalent face is reachable directly: the client Session Controller is a
7
+ * cordis service on the root context, and each session binding exposes its
8
+ * projections store (`SessionFace.projections.faceOf(key)`), which is the very
9
+ * observable `useProjection` resolves. Since it is a bare
10
+ * `{ getSnapshot, subscribe }` source, `useSyncExternalStore` binds it as-is —
11
+ * no local mirror, no folding, and the value reference only changes when the
12
+ * Host pushes a frame.
13
+ *
14
+ * Everything here is resolved defensively: a host without the Session
15
+ * Controller, or a session that is not open any more, degrades to "capability
16
+ * absent" (undefined) rather than throwing inside a render.
17
+ *
18
+ * This module is key-agnostic; the per-projection knowledge (which key, how to
19
+ * narrow the raw value) lives with the feature modules that call it.
20
+ */
21
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+
24
+ /** Structural view of a projection face (`ObservableSnapshot<unknown>`). */
25
+ export interface ProjectionFace {
26
+ getSnapshot(): unknown
27
+ subscribe(listener: () => void): () => void
28
+ }
29
+
30
+ /** Structural view of one session binding's projection store. */
31
+ export interface ProjectionsLike {
32
+ faceOf(key: string): ProjectionFace
33
+ }
34
+
35
+ /** Structural view of the client Session Controller face this plugin uses. */
36
+ export interface SessionsLike {
37
+ binding(id: string): { readonly session: { readonly projections: ProjectionsLike } } | undefined
38
+ }
39
+
40
+ /** A no-op unsubscribe, kept as one identity so effects never thrash. */
41
+ const NO_SUBSCRIBE = (): void => {}
42
+
43
+ /** A source that never has a value — the absent-capability face. */
44
+ const ABSENT: ProjectionFace = { getSnapshot: () => undefined, subscribe: () => NO_SUBSCRIBE }
45
+
46
+ /**
47
+ * Whether a value really is a projection face.
48
+ *
49
+ * The store contract promises an identity-stable face for every key, so this
50
+ * never fires against a healthy host. It exists because the fallback is worse
51
+ * than the check: `useSyncExternalStore` would throw on a face-less key, and a
52
+ * thrown render is a blank sidebar rather than a diagnosable "unavailable".
53
+ */
54
+ function isFace(value: unknown): value is ProjectionFace {
55
+ if (value === null || typeof value !== 'object') return false
56
+ const face = value as Partial<ProjectionFace>
57
+ return typeof face.getSnapshot === 'function' && typeof face.subscribe === 'function'
58
+ }
59
+
60
+ /**
61
+ * Resolve one projection face for one session.
62
+ *
63
+ * `ctx.get('sessions')` is the safe accessor (a direct `ctx.sessions` read can
64
+ * throw on a context that does not carry the service yet).
65
+ *
66
+ * @param ctx - the client root context handed to the tab body.
67
+ * @param sessionId - the tab's session scope; undefined means "no session".
68
+ * @param key - the projection key to resolve.
69
+ * @returns the projection face, or {@link ABSENT} when unreachable.
70
+ */
71
+ export function resolveFace(ctx: Context | undefined, sessionId: string | undefined, key: string): ProjectionFace {
72
+ if (ctx === undefined || sessionId === undefined) return ABSENT
73
+ const sessions = ctx.get('sessions') as SessionsLike | undefined
74
+ if (sessions === undefined || typeof sessions.binding !== 'function') return ABSENT
75
+
76
+ const binding = sessions.binding(sessionId)
77
+ const projections = binding?.session?.projections
78
+ if (projections === undefined || typeof projections.faceOf !== 'function') return ABSENT
79
+
80
+ const face: unknown = projections.faceOf(key)
81
+ return isFace(face) ? face : ABSENT
82
+ }
83
+
84
+ /**
85
+ * Read one frame of a projection without subscribing.
86
+ *
87
+ * The one-shot sibling of {@link useProjectionValue}, for consumers that are
88
+ * not React components — the tab badge is a plain function called on every
89
+ * tab-bar render, so it must neither hold a subscription nor allocate one.
90
+ *
91
+ * @param ctx - the client root context.
92
+ * @param sessionId - the session whose projection is read.
93
+ * @param key - the projection key to read.
94
+ * @returns the raw snapshot: `undefined` while the capability is absent; the
95
+ * semantics of any other value belong to the projection's own contract.
96
+ */
97
+ export function projectionSnapshot(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
98
+ return resolveFace(ctx, sessionId, key).getSnapshot()
99
+ }
100
+
101
+ /**
102
+ * Subscribe to one session projection by key.
103
+ *
104
+ * @param ctx - the client root context.
105
+ * @param sessionId - the session whose projection is shown.
106
+ * @param key - the projection key to subscribe to.
107
+ * @returns the raw snapshot value, identity-stable across frames the host did
108
+ * not change. Narrowing is the caller's job.
109
+ */
110
+ export function useProjectionValue(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
111
+ const face = useMemo(() => resolveFace(ctx, sessionId, key), [ctx, sessionId, key])
112
+
113
+ const subscribe = useCallback((listener: () => void) => face.subscribe(listener), [face])
114
+ const getSnapshot = useCallback(() => face.getSnapshot(), [face])
115
+
116
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
117
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Node half of the brief sidebar plugin.
3
+ *
4
+ * The Cordis loader needs an entry point for the profile row; the browser half
5
+ * (`./client`, served by `dsh-client-modules`) renders the summary tab.
6
+ *
7
+ * Since the summary upgrade, this half owns ONE behaviour: contributing the
8
+ * `dshSummaryDeliverables` projection unit (see `./projection/register`), which
9
+ * folds successful mutation calls into a live produced-file feed. The
10
+ * contribution is optional — `registerDeliverablesProjection` waits for the
11
+ * `sessionProjections` service via `ctx.inject`, so a composition without the
12
+ * registry still loads this profile row and simply hides the deliverables
13
+ * section client-side.
14
+ *
15
+ * In particular this half still never touches the filesystem at load time: a
16
+ * profile row is instantiated on the boot path, and a synchronous read there
17
+ * would delay the host's startup. The fold is event-driven and stores paths
18
+ * only.
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis'
21
+ import { registerDeliverablesProjection } from './projection/register'
22
+
23
+ /** Profile row identity. */
24
+ export const name = 'dsh-brief-sidebar'
25
+
26
+ /**
27
+ * Node-face apply.
28
+ * @param ctx - the host context the loader hands this profile row.
29
+ */
30
+ export function apply(ctx: Context): void {
31
+ registerDeliverablesProjection(ctx)
32
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Host-side fold of produced-file facts — the projection unit state for
3
+ * `dshSummaryDeliverables`.
4
+ *
5
+ * The vocabulary is the one the harness's own deliverables experience uses
6
+ * (`ui-deliverables/turn-deliverables.ts`): the arguments of successful
7
+ * first-party mutation calls (`write`, `edit`, and the mutating
8
+ * `str_replace_editor` commands), never presentation data or prose. Reads,
9
+ * deletes, unsupported tools, malformed calls, and failed results contribute
10
+ * nothing; a file written and then edited in the same turn is one entry, in
11
+ * first-seen order.
12
+ *
13
+ * This module is deliberately PURE — no cordis, no zod, no IO — so the fold
14
+ * can be tested directly against hand-written event fixtures. The projection
15
+ * registry drives it once per committed `session/event`, which is what makes
16
+ * the produced list update mid-turn, the gap the official turn-tail row has.
17
+ */
18
+
19
+ /** One bucket: the paths produced so far in one turn, first-seen order. */
20
+ export interface DeliverablesTurnBucket {
21
+ readonly turn: number
22
+ readonly paths: readonly string[]
23
+ }
24
+
25
+ /** Fold state. Plain JSON, so the projection cache can persist it. */
26
+ export interface DeliverablesState {
27
+ /** Turn number of the open (or latest seen) turn; 0 before the first turn. */
28
+ readonly turn: number
29
+ /** Open turn's `callId → mutation path` map (`null` = not a mutation call). */
30
+ readonly calls: Readonly<Record<string, { readonly turn: number; readonly path: string | null }>>
31
+ /** Paths accumulated in the latest turn so far (`null` before the first turn). */
32
+ readonly latest: DeliverablesTurnBucket | null
33
+ /** Every distinct produced path, most-recent-first, capped (see CAPS). */
34
+ readonly sessionPaths: readonly string[]
35
+ /** Distinct produced paths over the whole session, uncapped. */
36
+ readonly sessionTotal: number
37
+ }
38
+
39
+ /**
40
+ * Wire-view capacity bounds. The projection rides every event frame, so the
41
+ * lists are capped; the uncapped `sessionTotal` keeps the summary truthful
42
+ * beyond the cap.
43
+ */
44
+ export const CAPS = {
45
+ /** Latest-turn paths kept in the wire view. */
46
+ latest: 50,
47
+ /** Session-wide paths kept in the wire view (most recent win). */
48
+ session: 100,
49
+ } as const
50
+
51
+ /** The fold's initial state: nothing has happened yet. */
52
+ export function initDeliverablesState(): DeliverablesState {
53
+ return { turn: 0, calls: {}, latest: null, sessionPaths: [], sessionTotal: 0 }
54
+ }
55
+
56
+ /**
57
+ * Structural view of one committed session event. The registry hands the
58
+ * host's discriminated union; the fold only needs `type` and `data`, and the
59
+ * tool-event shapes mirror `dsh-session` types: `tool/call` carries
60
+ * `{ turn, step, callId, name, arguments }`, `tool/result` carries
61
+ * `{ turn, step, message: { content: [{ isError? }], source: { callId } } }`,
62
+ * `turn/start` carries `{ turn }`.
63
+ */
64
+ export interface FoldEvent {
65
+ readonly type: string
66
+ readonly data: unknown
67
+ }
68
+
69
+ /**
70
+ * The next state after one committed event. Events the fold does not own
71
+ * return the SAME reference — an unchanged reference produces zero downstream
72
+ * work in the registry's `Object.is` drive.
73
+ *
74
+ * @param state - the state covering all prior events.
75
+ * @param event - the next committed session event.
76
+ * @returns the next state (same reference when nothing the fold tracks moved).
77
+ */
78
+ export function applyDeliverablesEvent(state: DeliverablesState, event: FoldEvent): DeliverablesState {
79
+ if (event.type === 'turn/start') {
80
+ const turn = event.data as { turn?: unknown }
81
+ if (typeof turn?.turn !== 'number') return state
82
+ // Keep the previous turn's call records until its results settled; only
83
+ // older turns' unsettled entries are pruned (bounded-state hygiene).
84
+ const calls: Record<string, { readonly turn: number; readonly path: string | null }> = {}
85
+ for (const [id, entry] of Object.entries(state.calls)) {
86
+ if (entry.turn >= turn.turn - 1) calls[id] = entry
87
+ }
88
+ return { ...state, turn: turn.turn, calls, latest: { turn: turn.turn, paths: [] } }
89
+ }
90
+
91
+ if (event.type === 'tool/call') {
92
+ const call = event.data as { callId?: unknown; name?: unknown; arguments?: unknown }
93
+ if (typeof call?.callId !== 'string') return state
94
+ const path = typeof call.name === 'string' && typeof call.arguments === 'string'
95
+ ? mutationPath(call.name, call.arguments)
96
+ : null
97
+ return { ...state, calls: { ...state.calls, [call.callId]: { turn: state.turn, path } } }
98
+ }
99
+
100
+ if (event.type !== 'tool/result') return state
101
+
102
+ const result = event.data as {
103
+ message?: { content?: readonly { isError?: unknown }[]; source?: { callId?: unknown } }
104
+ }
105
+ const callId = result?.message?.source?.callId
106
+ if (typeof callId !== 'string') return state
107
+ if (result?.message?.content?.[0]?.isError === true) return state
108
+
109
+ const recorded = state.calls[callId]
110
+ const path = recorded?.path
111
+ if (path === undefined || path === null) return state
112
+
113
+ // A path written and edited again in the same turn is one bucket entry. A
114
+ // stray result that settles after a LATER turn opened never rewrites the
115
+ // newer bucket — its path still counts session-wide below.
116
+ const latest = state.latest
117
+ const sameTurn = latest !== null && latest.turn === recorded.turn
118
+ const nextLatest: DeliverablesTurnBucket | null = sameTurn && !latest.paths.includes(path)
119
+ ? { turn: latest.turn, paths: [...latest.paths, path].slice(-CAPS.latest) }
120
+ : latest
121
+
122
+ // Session-wide: distinct paths, most-recent-first, capped. The filter is
123
+ // non-empty-preserving, so a length change means the path was NEW here.
124
+ const withoutDuplicate = state.sessionPaths.filter(existing => existing !== path)
125
+ const sessionPaths = [path, ...withoutDuplicate].slice(0, CAPS.session)
126
+ const sessionTotal = withoutDuplicate.length === state.sessionPaths.length
127
+ ? state.sessionTotal + 1
128
+ : state.sessionTotal
129
+
130
+ return { ...state, latest: nextLatest, sessionPaths, sessionTotal }
131
+ }
132
+
133
+ /**
134
+ * Extract the path from a supported first-party mutation call.
135
+ *
136
+ * Ported from the harness deliverables fold so both carriers agree on what a
137
+ * "produced file" is. Session `tool/call` events are root calls; PTC dispatch
138
+ * children do not enter this fold independently.
139
+ *
140
+ * @param name - wire tool name.
141
+ * @param argsRaw - model-produced JSON arguments.
142
+ * @returns the mutation path, or null when the call is not a supported mutation.
143
+ */
144
+ export function mutationPath(name: string, argsRaw: string): string | null {
145
+ let args: unknown
146
+ try {
147
+ args = JSON.parse(argsRaw) as unknown
148
+ } catch {
149
+ return null
150
+ }
151
+ if (!isRecord(args)) return null
152
+ switch (name) {
153
+ case 'write':
154
+ return typeof args.content === 'string' ? pathValue(args.file_path) : null
155
+ case 'edit':
156
+ return validEditArgs(args) ? pathValue(args.file_path) : null
157
+ case 'str_replace_editor':
158
+ return editorMutationPath(args)
159
+ default:
160
+ return null
161
+ }
162
+ }
163
+
164
+ /** Validate the fields that an `edit` execution requires. */
165
+ function validEditArgs(args: Readonly<Record<string, unknown>>): boolean {
166
+ return typeof args.old_string === 'string'
167
+ && args.old_string.length > 0
168
+ && typeof args.new_string === 'string'
169
+ && args.old_string !== args.new_string
170
+ && (args.replace_all === undefined || typeof args.replace_all === 'boolean')
171
+ }
172
+
173
+ /** Extract a path only from a complete mutating editor command. */
174
+ function editorMutationPath(args: Readonly<Record<string, unknown>>): string | null {
175
+ const path = pathValue(args.path)
176
+ if (path === null) return null
177
+ switch (args.command) {
178
+ case 'create':
179
+ return typeof args.file_text === 'string' ? path : null
180
+ case 'str_replace':
181
+ return typeof args.old_str === 'string'
182
+ && args.old_str.length > 0
183
+ && (args.new_str === undefined || typeof args.new_str === 'string')
184
+ ? path
185
+ : null
186
+ case 'insert':
187
+ return typeof args.insert_line === 'number'
188
+ && Number.isInteger(args.insert_line)
189
+ && args.insert_line >= 0
190
+ && typeof args.new_str === 'string'
191
+ ? path
192
+ : null
193
+ default:
194
+ return null
195
+ }
196
+ }
197
+
198
+ /** A non-blank path preserves the exact spelling supplied to the tool. */
199
+ function pathValue(value: unknown): string | null {
200
+ return typeof value === 'string' && value.trim().length > 0 ? value : null
201
+ }
202
+
203
+ /** Narrow parsed JSON to an argument object. */
204
+ function isRecord(value: unknown): value is Record<string, unknown> {
205
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
206
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The projection key this plugin contributes to the session-projection
3
+ * registry, and its wire view.
4
+ *
5
+ * The key is namespaced with the plugin name on purpose: the registry refuses
6
+ * to share a key across differing `stateVersion`s, so a collision with a
7
+ * future host-side deliverables unit would be a hard load error, not a silent
8
+ * shadow. The view type is declared structurally (the plugin has no compile
9
+ * dependency on `@deepseek-ai/dsh-session-projection`); the registry accepts
10
+ * any string key at runtime.
11
+ */
12
+
13
+ /** The projection key registered by this plugin's host half. */
14
+ export const DELIVERABLES_KEY = 'dshSummaryDeliverables'
15
+
16
+ /** Client view of the produced-file fold (JSON-safe). */
17
+ export interface DeliverablesView {
18
+ /** Paths produced in the latest turn, first-seen order (capped). */
19
+ readonly latest: {
20
+ readonly turn: number
21
+ readonly paths: readonly string[]
22
+ } | null
23
+ /** Distinct produced paths session-wide, most-recent-first (capped). */
24
+ readonly sessionPaths: readonly string[]
25
+ /** Distinct produced paths session-wide, uncapped — the truthful total. */
26
+ readonly sessionTotal: number
27
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Registration of the `dshSummaryDeliverables` projection unit.
3
+ *
4
+ * The unit is an OPTIONAL contribution: a composition without the
5
+ * session-projection registry (or an older host) simply never registers it,
6
+ * and the client reads the key as capability absence — the deliverables
7
+ * section stays hidden. That is why this module routes through
8
+ * `ctx.inject(['sessionProjections'], …)` (wait for the service, register only
9
+ * when it appears) instead of declaring the service in the plugin's own
10
+ * `inject` list, which would make the whole profile row fail to load.
11
+ *
12
+ * The registry drives every registered unit once per committed session event
13
+ * and lazily replays the in-memory log for late units, so a session's history
14
+ * is folded even when this plugin mounts after work started.
15
+ */
16
+ import { z } from 'zod'
17
+ import type { Context } from '@deepseek-ai/cordis'
18
+ import { applyDeliverablesEvent, CAPS, initDeliverablesState } from './deliverables-fold'
19
+ import type { DeliverablesState } from './deliverables-fold'
20
+ import { DELIVERABLES_KEY } from './keys'
21
+ import type { DeliverablesView } from './keys'
22
+
23
+ /**
24
+ * Structural view of the registry this plugin needs. Declared locally on
25
+ * purpose: the plugin has no compile dependency on the session-projection
26
+ * package, and `register` accepts any string key at runtime.
27
+ */
28
+ interface ProjectionRegistryLike {
29
+ register(definition: {
30
+ key: string
31
+ stateSchema: { parse(value: unknown): unknown }
32
+ init(): DeliverablesState
33
+ apply(state: DeliverablesState, event: { type: string; data: unknown }): DeliverablesState
34
+ wire: { viewSchema: { parse(value: unknown): unknown }; view(state: DeliverablesState): DeliverablesView }
35
+ stateVersion: number
36
+ }): () => void
37
+ }
38
+
39
+ /** Zod schema of the fold state — validates persisted states before replay. */
40
+ const stateSchema = z.object({
41
+ turn: z.number(),
42
+ calls: z.record(z.string(), z.object({ turn: z.number(), path: z.string().nullable() })),
43
+ latest: z.object({ turn: z.number(), paths: z.array(z.string()) }).nullable(),
44
+ sessionPaths: z.array(z.string()),
45
+ sessionTotal: z.number(),
46
+ })
47
+
48
+ /** Zod schema of the wire view — validates the payload before it leaves the host. */
49
+ const viewSchema = z.object({
50
+ latest: z.object({ turn: z.number(), paths: z.array(z.string()) }).nullable(),
51
+ sessionPaths: z.array(z.string()),
52
+ sessionTotal: z.number(),
53
+ })
54
+
55
+ /**
56
+ * The view memo. The registry compares consecutive `view` results by
57
+ * `Object.is`, so a fresh object per event would republish the key on every
58
+ * unrelated frame. States are immutable references, so a WeakMap keyed by
59
+ * state reuses the view reference until the state really changes.
60
+ */
61
+ const viewMemo = new WeakMap<DeliverablesState, DeliverablesView>()
62
+
63
+ /** State → wire view, reference-stable across unchanged states. */
64
+ function view(state: DeliverablesState): DeliverablesView {
65
+ const memoized = viewMemo.get(state)
66
+ if (memoized !== undefined) return memoized
67
+ const next: DeliverablesView = {
68
+ latest: state.latest === null
69
+ ? null
70
+ : { turn: state.latest.turn, paths: state.latest.paths.slice(-CAPS.latest) },
71
+ sessionPaths: state.sessionPaths.slice(0, CAPS.session),
72
+ sessionTotal: state.sessionTotal,
73
+ }
74
+ viewMemo.set(state, next)
75
+ return next
76
+ }
77
+
78
+ /**
79
+ * Register the deliverables projection as an optional contribution.
80
+ *
81
+ * @param ctx - the host context the plugin's `apply` receives.
82
+ * @returns nothing; the registration's disposer rides the injected fiber, so
83
+ * unloading the plugin removes the key (clients then read capability absence).
84
+ */
85
+ export function registerDeliverablesProjection(ctx: Context): void {
86
+ ctx.inject(['sessionProjections'], (injected) => {
87
+ const registry = (injected as unknown as { sessionProjections?: ProjectionRegistryLike })
88
+ .sessionProjections
89
+ if (registry === undefined || typeof registry.register !== 'function') return
90
+ registry.register({
91
+ key: DELIVERABLES_KEY,
92
+ stateSchema,
93
+ init: () => initDeliverablesState(),
94
+ apply: applyDeliverablesEvent,
95
+ wire: { viewSchema, view },
96
+ stateVersion: 0,
97
+ })
98
+ })
99
+ }