Files
deepseek-harness/packages/boot/app-boot/src/patch-rows.ts
T
Yichen Jiang aaf58eb5bf fix(boot): own the rows a config override sets, queue recompositions, and let records follow the configuration
Second review round on the contained-group composition:

- Ownership scanned only `insert` rows, so an external bundle could set a
  built-in row's id as a child through `{ id: own, config: [...] }` and the
  Loader re-parented that row under the bundle while `originOf` still named
  the built-in layer. The rows an id-targeted patch sets as a group's config
  now count as the layer's own: an external bundle that sets an id another
  layer owns is left out whole, a built-in layer that takes another layer's
  id fails loud, and a layer restating its own child is not a repeat.
- `recompose` read the committed profile before awaiting the update, so a
  watcher firing while a bundle was being enabled composed the tree before
  the enable and applied it after; the plugin vanished while `layers` kept
  listing it. Recompositions now queue, each reading what the previous one
  committed, and a rejection stays its own caller's outcome.
- A patch that replaced a built-in target's config removed the wrapper the
  bundle had put there, yet later inserts still targeted the detached
  wrapper and vanished. The composition forgets the wrapper on such an
  override and creates a new one under the same id.
- Generated wrapper ids bypassed the duplicate check, so a bundle declaring
  a row spelled like a wrapper produced two nodes with one id. Declared and
  generated ids now go through one registration.
- Failure records outlived their rows when a bundle update dropped a failed
  row without unmounting the group, or when a pending row sat in a plain
  nested group whose id the record named. A contained group now drops the
  records of rows its configuration no longer names after each update, and
  the audit records every row under the contained group that isolates it.
- `applyEntryPatches` assigned override values by reference, so applying a
  patch list carried a later insert's rows back into the patch object; the
  vendored include now clones object-valued overrides as it does inserts.
2026-09-07 11:28:19 +08:00

52 lines
2.1 KiB
TypeScript

/**
* The one walk over the rows a patch list introduces. Composition, ownership,
* the package probe, and patch loading all read those rows and their group
* children; sharing the walk keeps them reading the same tree.
* @module @deepseek-ai/dsh-app-boot/patch-rows
*/
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
/** How a patch introduced a row: inserted it, or set it as a group's `config`. */
export type PatchRowSource = 'insert' | 'config'
/** Whether a config item is a row: a plain object naming a module. */
function isRowLike(value: unknown): value is EntryOptions {
return typeof value === 'object' && value !== null && !Array.isArray(value) && typeof (value as { name?: unknown }).name === 'string'
}
/**
* Visit one row and, for a group with a list config, each of its children,
* parents before children, in written order.
* @param row - the row.
* @param visit - called once per row.
*/
export function visitRowTree(row: EntryOptions, visit: (row: EntryOptions) => void): void {
visit(row)
if (row.group && Array.isArray(row.config)) {
for (const child of row.config as EntryOptions[]) visitRowTree(child, visit)
}
}
/**
* Visit every row a patch list introduces, at any depth, in written order:
* the rows a patch inserts, and the rows an id-targeted patch sets as a
* group's `config`, which mount as that group's children the same way an
* insert's do.
* @param patches - the patch list.
* @param visit - called once per row, with how the patch introduced it.
*/
export function visitPatchRows(patches: readonly PatchOptions[], visit: (row: EntryOptions, source: PatchRowSource) => void): void {
for (const patch of patches) {
if (patch.insert !== undefined) {
for (const row of patch.insert) visitRowTree(row, (entry) => { visit(entry, 'insert') })
continue
}
if (patch.id === undefined || !Array.isArray(patch.config)) continue
for (const item of patch.config as unknown[]) {
if (isRowLike(item)) visitRowTree(item, (entry) => { visit(entry, 'config') })
}
}
}