返回 CodeWhale
composition-inventory.ts
根目录 / crates / tui / extension-host / src / dsh / upstream / agent-presets / composition-inventory.ts
1 /**
2 * Structured composition reads for plugin-listing surfaces: the plugin rows
3 * each preset names, with each row's effective enablement. A preset with a
4 * live standing mount answers from that mount's Loader entries — evaluated
5 * `disabled`, real root-fiber states; a preset no session has composed since
6 * boot answers from its composition file, with `!!js` disabled expressions
7 * evaluated through the caller-supplied Loader evaluator so the file answer
8 * matches the decision a mount on this host would make. A row whose
9 * expression the evaluator refuses stays `'conditional'`.
10 * @module @deepseek-ai/dsh-agent-presets/composition-inventory
11 */
12
13 import { load } from 'js-yaml'
14 import type { FiberState } from '@deepseek-ai/cordis'
15 import { isJsExpr, type EntryTree } from '../loader/src/index.ts'
16 import { entryListSchema } from '../include/src/index.ts'
17 import { entryListProblem } from './discovery.ts'
18 import type { PresetTrust } from './preset.ts'
19
20 /**
21 * Effective enablement of one composition row: a literal or evaluated
22 * boolean, or `'conditional'` when a `!!js` disabled expression could not be
23 * evaluated outside a mount.
24 */
25 export type CompositionRowEnablement = boolean | 'conditional'
26
27 /**
28 * Evaluate one `!!js` disabled expression the way the Loader would at a mount
29 * decision. Throwing refuses the answer: the row is reported `'conditional'`
30 * rather than guessed.
31 */
32 export type DisabledExpressionEvaluator = (expression: string) => unknown
33
34 /** One plugin row a preset composition names. */
35 export interface AgentPresetCompositionRow {
36 /**
37 * The Loader-tree entry id when read from a live mount, else the id the
38 * composition file declares; null when the file row declares none.
39 */
40 readonly entryId: string | null
41 /** Module specifier the row names. */
42 readonly moduleName: string
43 /** Effective enablement, including disabled ancestor groups. */
44 readonly enabled: CompositionRowEnablement
45 /** The row's own `!!js` disabled expression, when it carries one. */
46 readonly condition?: string
47 /** Root-fiber state, present only when read from a live mount. */
48 readonly fiberState?: FiberState
49 }
50
51 /** One preset's roster identity beside its composition rows. */
52 export interface AgentPresetComposition {
53 /** Stable preset id. */
54 readonly id: string
55 /** Whether the deployment ships the preset or the user owns it. */
56 readonly trust: PresetTrust
57 /** Display name the preset published. */
58 readonly name?: string
59 /** Whether a session naming no preset composes this one. */
60 readonly isDefault: boolean
61 /** Why this preset's rows cannot be read; absent when {@link rows} answers. */
62 readonly broken?: string
63 /** Composition rows in composition order; empty when the preset is broken. */
64 readonly rows: readonly AgentPresetCompositionRow[]
65 }
66
67 /**
68 * One `disabled` node's contribution to effective enablement, mirroring the
69 * Loader's own reading: a `!!js` expression is asked of the evaluator — a
70 * refusal (throw) leaves the decision to a mount — and anything else disables
71 * exactly when `Boolean(value)` does.
72 * @param value - the raw `disabled` node of one composition row.
73 * @param evaluateExpression - the Loader-context evaluator for `!!js` nodes.
74 * @returns true (disabled), false (enabled), or `'conditional'`.
75 */
76 function disabledContribution(
77 value: unknown,
78 evaluateExpression: DisabledExpressionEvaluator,
79 ): boolean | 'conditional' {
80 if (isJsExpr(value)) {
81 try {
82 return Boolean(evaluateExpression(value.__jsExpr))
83 } catch {
84 // The evaluator refused (a malformed or context-dependent expression);
85 // only a real mount decision can answer, so the row stays conditional.
86 return 'conditional'
87 }
88 }
89 return Boolean(value)
90 }
91
92 /**
93 * Combine an ancestor group's disabled state with a row's own, the way the
94 * Loader walks owning groups: any literal true disables, otherwise any
95 * expression leaves the decision to a mount.
96 * @param outer - the combined ancestor contribution.
97 * @param own - this row's contribution.
98 * @returns the row's effective disabled state.
99 */
100 function combineDisabled(
101 outer: boolean | 'conditional',
102 own: boolean | 'conditional',
103 ): boolean | 'conditional' {
104 if (outer === true || own === true) return true
105 if (outer === 'conditional' || own === 'conditional') return 'conditional'
106 return false
107 }
108
109 /** A parsed composition row after {@link entryListProblem} accepted the list. */
110 interface RawRow {
111 readonly id?: unknown
112 readonly name: string
113 readonly group?: unknown
114 readonly config?: unknown
115 readonly disabled?: unknown
116 }
117
118 /**
119 * Flatten one parsed row list into plugin rows. Group rows are structural —
120 * the Loader reports a group entry as always enabled and lets children
121 * inherit its `disabled` — so only their children are emitted.
122 * @param rows - the parsed rows, shape-checked by the caller.
123 * @param outerDisabled - the combined ancestor-group disabled state.
124 * @param evaluateExpression - the Loader-context evaluator for `!!js` nodes.
125 * @param found - the accumulator receiving flattened rows.
126 */
127 function flattenRows(
128 rows: readonly unknown[],
129 outerDisabled: boolean | 'conditional',
130 evaluateExpression: DisabledExpressionEvaluator,
131 found: AgentPresetCompositionRow[],
132 ): void {
133 for (const value of rows) {
134 const row = value as RawRow
135 const disabled = combineDisabled(outerDisabled, disabledContribution(row.disabled, evaluateExpression))
136 if (row.group === true) {
137 flattenRows(row.config as readonly unknown[], disabled, evaluateExpression, found)
138 continue
139 }
140 found.push({
141 entryId: typeof row.id === 'string' && row.id !== '' ? row.id : null,
142 moduleName: row.name,
143 enabled: disabled === true ? false : disabled === 'conditional' ? 'conditional' : true,
144 ...isJsExpr(row.disabled) ? { condition: row.disabled.__jsExpr } : {},
145 })
146 }
147 }
148
149 /**
150 * Plugin rows of one composition file, for a preset with no live mount.
151 *
152 * Parsed with the Loader's own dialect ({@link entryListSchema}), so the rows
153 * reported are the rows a mount would start from. A file that stopped reading
154 * as a composition — discovery judged the preset healthy moments earlier, so
155 * only an edit racing this read gets here — answers as broken with the raced
156 * reason rather than dropping the rows silently.
157 * @param path - absolute path of the composition file.
158 * @param evaluateExpression - the Loader-context evaluator for `!!js` nodes.
159 * @returns flattened rows in composition order, or why they cannot be read.
160 */
161 export async function fileComposition(
162 path: string,
163 evaluateExpression: DisabledExpressionEvaluator,
164 readFile: (path: string, encoding: string) => Promise<string>,
165 ): Promise<{ rows: AgentPresetCompositionRow[] } | { broken: string }> {
166 let rows: unknown
167 try {
168 rows = load(await readFile(path, 'utf8'), { schema: entryListSchema })
169 } catch (error) {
170 /* v8 ignore next -- fs and js-yaml throw Errors for every failure here; the fallback keeps a hostile value readable */
171 return { broken: error instanceof Error ? error.message : String(error) }
172 }
173 const problem = entryListProblem(rows)
174 if (problem !== undefined) return { broken: problem }
175 const found: AgentPresetCompositionRow[] = []
176 flattenRows(rows as readonly unknown[], false, evaluateExpression, found)
177 return { rows: found }
178 }
179
180 /**
181 * Plugin rows of one live standing composition, in Loader-entry order.
182 * @param tree - the standing mount's entry tree.
183 * @returns rows with the Loader's evaluated enablement and root-fiber states.
184 */
185 export function mountedCompositionRows(tree: EntryTree): AgentPresetCompositionRow[] {
186 const found: AgentPresetCompositionRow[] = []
187 for (const entry of tree.entries()) {
188 if (entry.options.group) continue
189 found.push({
190 entryId: entry.id,
191 moduleName: entry.options.name,
192 enabled: !entry.disabled,
193 ...isJsExpr(entry.options.disabled) ? { condition: entry.options.disabled.__jsExpr } : {},
194 ...entry.fiber === undefined ? {} : { fiberState: entry.fiber.state },
195 })
196 }
197 return found
198 }
199
199 lines TYPESCRIPT