| 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 |