refactor(boot): keep bundle analysis internal

This commit is contained in:
Yichen Jiang committed 2026-09-14 13:36:26 +08:00
1 parent 94419d809b
commit 8fc785de3a
15 files changed
+31 -42

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md: 986d8588fe5849f53c38f956d10937d7c56c2741
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: 5b44878a1a6483568adc6a6c898ca1f2129b0a18
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md: 364e79d29095754e0297dfaf552d9783cc187a4c
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: 80a22e1839e0a288b298f6b8b2fda70ecc76c448
@@ -22,6 +22,8 @@ Third-party plugin failures must leave the application’s management endpoints
**Unhandled process failures stay fatal.** `installFailLoud` remains until shutdown. Only duplicate rejections already observed by an entry audit are coalesced through its process checkpoint. Unrelated detached rejections retain master’s teardown-and-exit policy. No in-process group protects against `process.exit`, a blocked event loop, native crashes or OOM. Preset generation owners retain their own strict mount-and-cleanup behavior.
**Public helpers serve launcher and management operations.** Composition, conflict checks, diagnostics and package management expose their inputs and results. Per-row diagnostic helpers and intermediate bundle analysis stay package-private so callers depend on operation results rather than analysis steps. Ownership analysis returns row owners and conflicts without retaining unused per-bundle results.
## Alternatives considered
**Keep contained groups and child probes.** Contained groups protected against transactional Loader rollback and supplied failure records for removed rows. Native entries retain both siblings and failed rows, so wrapping adds identities without supplying bundle-wide enablement that layer composition lacks. Child probes bounded discovery-time execution and discovered undeclared exports and schemas. Static declarations avoid that execution; automatic main-export detection and pre-mount schemas are given up. A future execution sandbox needs its own process ownership and teardown design, not a discovery probe presented as runtime isolation.
@@ -22,6 +22,8 @@ Status: implemented
**未处理的进程级失败仍然致命。** `installFailLoud` 保持到应用关闭。只有条目审计已观测到的重复 rejection 会在其进程检查点内合并。无关的脱离管理 rejection 保留 master 的清理退出策略。进程内的组无法防护 `process.exit`、事件循环阻塞、原生崩溃或 OOM。预设代际的拥有者保留独立的严格挂载与清理行为。
**公开 helper 服务于启动与管理操作。** 组合、冲突检查、诊断和包管理公开其输入与结果。单行诊断 helper 和组合包分析中间数据保留在包内,使调用方依赖操作结果而非分析步骤。归属分析返回行归属与冲突,不保留无人使用的逐包分析结果。
## Alternatives considered
**保留 contained group 和子进程 probe。** contained group 防止事务式 Loader 回滚,并为已移除行提供失败记录。原生条目会同时保留成功行与失败行,包装只增加身份,并未提供整层组合所缺少的整包启停能力。子进程 probe 为发现阶段的执行设定边界,并发现未声明导出与 schema。静态声明避免这次执行,同时放弃自动主入口识别和挂载前 schema。未来的执行沙箱需要独立设计进程归属与清理,不能把发现 probe 视为运行隔离。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
architecture.md: 5249930e288da5b13145eff92d67ff310ac63b3d
architecture.zh.md: b9020585b0c436a314042ee19e82a9a544a74d43
architecture.md: d88429227fbdae3a49f0bdf1a7c1abb005b27195
architecture.zh.md: c3bcd1b8ce318421f12f1b03c8f798761413d887
+1 -1
View File
@@ -26,7 +26,7 @@ Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile
Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows.
An installed bundle is external: its rows mount under one contained group, a failing row is isolated and reported, and built-in rows keep failing the boot ([app-boot](../packages/boot/app-boot/README.md)).
Bundle rows retain their declared ids and group hierarchy. Enabling a bundle applies its whole patch layer; startup requires the application’s required entries to activate and reports other row failures as warnings ([app-boot](../packages/boot/app-boot/README.md)).
Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, `sdk-minimal`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle.
+1 -1
View File
@@ -26,7 +26,7 @@
各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。
已安装的组合包是外部的:它的行挂在一个受控组下,启动失败的行被隔离并报告,而内置行仍然让启动失败([app-boot](../packages/boot/app-boot/README.zh.md))。
组合包的行保留声明的 id 和组层级。启用组合包会应用它的整个 patch 层;启动要求应用的必需条目激活,其他行的失败则作为警告报告([app-boot](../packages/boot/app-boot/README.zh.md))。
自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk`、`sdk-minimal` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
README.md: cabe6a1fd3df6e1c079450dcbf0f0ecfa77c2f42
README.zh.md: 57aeb255ec0de21416bad381534106c20003d3c0
README.md: 7d7d612def6e9261fd64097fa1381761a33e087e
README.zh.md: 3f76119dfbed4ec47c3cb6ecd435899ed8d813ad
+1 -1
View File
@@ -116,7 +116,7 @@ This section explains how the outcomes above are realized and points at the code
### Helper behavior
The exports each own one stage of the boot: config resolution and snapshot replay, layered environment loading, fail-loud reporting, activation auditing, patch parsing, root-include mounting, config dump rendering, live patch watching, profile composition, and the harness-source section. Per-export contracts live in the code, not this README — see [`src/index.ts`](src/index.ts) and [`src/profile.ts`](src/profile.ts).
The package root exports boot, composition, diagnostics and package-management operations with their input and result types. Per-row diagnostic helpers and intermediate bundle analysis stay internal. Per-export contracts live in [`src/index.ts`](src/index.ts) and [`src/profile.ts`](src/profile.ts).
### Source map
+1 -1
View File
@@ -116,7 +116,7 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
### Helper 行为
每个导出各负责启动的一个阶段:配置解析与快照回放、分层环境加载、明确报错的保护机制、激活审计、patch 解析、根 include 挂载、配置 dump 渲染、活动 patch 监视、profile 组合,以及 harness 源码段落。各导出的约定在代码中,不在本 README——见 [`src/index.ts`](src/index.ts) 与 [`src/profile.ts`](src/profile.ts)。
包根导出启动、组合、诊断和包管理操作及其输入与结果类型。单行诊断 helper 和组合包分析中间数据保留在包内。各导出的约定见 [`src/index.ts`](src/index.ts) 与 [`src/profile.ts`](src/profile.ts)。
### 源码地图
+4 -8
View File
@@ -7,7 +7,7 @@
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { analyzeBundleLayer, type AnalyzedBundleLayer } from './external-bundles.ts'
import { analyzeBundleLayer } from './external-bundles.ts'
import { visitPatchRows, visitRowTree } from './patch-rows.ts'
import type { ProfileLayer } from './profile.ts'
@@ -58,14 +58,12 @@ export interface ComposedStack {
readonly userDisabledRowIds: ReadonlySet<string>
}
/** Row-id ownership across the bundle layers: who owns each id, which bundles lost, and how the rest mount. */
/** Row-id ownership and conflicts across the bundle layers. */
export interface LayerOwnership {
/** The layer that owns each id a bundle layer introduces. */
readonly owners: Map<string, ProfileLayer>
/** The conflicts of each bundle left out, by package name. */
readonly skipped: Map<string, RowConflict[]>
/** The analysis of each bundle layer that owns its ids, by package name; rendered once and mounted as is. */
readonly composed: Map<string, AnalyzedBundleLayer>
}
/** One conflict with its message: the id's other declarer, or the losing layer itself declaring it twice. */
@@ -91,12 +89,11 @@ function rowIds(row: EntryOptions): string[] {
* an earlier layer's id is omitted whole. Restating a child in the same group
* preserves its ownership; moving it to another group is a duplicate.
* @param layers - the profile's bundle layers, in manifest order.
* @returns row owners, rejected-bundle conflicts, and each accepted layer's analysis.
* @returns row owners and rejected-bundle conflicts.
*/
export function claimLayerIds(layers: readonly ProfileLayer[]): LayerOwnership {
const owners = new Map<string, ProfileLayer>()
const skipped = new Map<string, RowConflict[]>()
const composed = new Map<string, AnalyzedBundleLayer>()
for (const layer of layers) {
const { packageName } = layer
const composition = analyzeBundleLayer(layer)
@@ -114,9 +111,8 @@ export function claimLayerIds(layers: readonly ProfileLayer[]): LayerOwnership {
continue
}
for (const id of composition.rows.keys()) owners.set(id, layer)
composed.set(packageName, composition)
}
return { owners, skipped, composed }
return { owners, skipped }
}
/**
+5 -11
View File
@@ -1,7 +1,6 @@
/** Bundle patch ownership and the profile manifest's installed/enabled layer lists. */
import { join } from 'node:path'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import type { DshProfileManifest } from '@deepseek-ai/dsh-package-manifest'
import { visitIdentifiedRows } from './patch-rows.ts'
import {
@@ -9,23 +8,21 @@ import {
} from './profile.ts'
/** One row declared twice within a bundle layer. */
export interface DuplicateRow {
interface DuplicateRow {
readonly rowId: string
readonly moduleName: string
}
/** Static row ownership and overrides of one unmodified bundle patch list. */
export interface AnalyzedBundleLayer {
readonly patches: PatchOptions[]
/** Declared row ids and duplicates within one bundle patch list. */
interface AnalyzedBundleLayer {
readonly rows: Map<string, string>
readonly duplicates: DuplicateRow[]
readonly overrides: string[]
}
/**
* Inspect the rows a layer introduces without changing their ids or parents.
* @param layer - the bundle patch list.
* @returns its declared rows, duplicates, and external override targets.
* @returns its declared rows and duplicates.
*/
export function analyzeBundleLayer(layer: ProfileLayer): AnalyzedBundleLayer {
const rows = new Map<string, string>()
@@ -41,10 +38,7 @@ export function analyzeBundleLayer(layer: ProfileLayer): AnalyzedBundleLayer {
declaredUnder.set(id, place.target)
}
})
const overrides = layer.patches.flatMap(patch => (
patch.insert === undefined && patch.id !== undefined && !rows.has(patch.id) ? [patch.id] : []
))
return { patches: layer.patches, rows, duplicates, overrides }
return { rows, duplicates }
}
/**
+2 -2
View File
@@ -51,10 +51,10 @@ export {
type ProfileModuleFallbackOptions,
type ProfileTemplate,
} from './profile.ts'
export { entryIssue, inspectEntryIssues, type EntryIssue } from './entry-issues.ts'
export { inspectEntryIssues, type EntryIssue } from './entry-issues.ts'
export {
disableBundle, enableBundle, reconcileInstalledBundles,
type BundleReconciliation, type AnalyzedBundleLayer, type DuplicateRow,
type BundleReconciliation,
} from './external-bundles.ts'
export {
claimLayerIds, composeProfileStack, formatRowConflict,
@@ -110,22 +110,19 @@ describe('claimLayerIds', () => {
{ insert: [{ id: 'self-group', name: 'cordis:group', group: true, config: [{ id: 'row', name: 'self/row' }] }] },
{ id: 'self-group', config: [{ id: 'row', name: 'self/row' }, { id: 'row', name: 'self/row-again' }] },
])
const { skipped, composed } = claimLayerIds([base, self])
const { skipped } = claimLayerIds([base, self])
expect(skipped.get('self')?.map(conflict => conflict.message)).toEqual(['row "row" is declared twice by self'])
expect(composed.has('self')).toBe(false)
})
it('leaves out a bundle that declares one of its own ids twice and composes each mounted bundle once', () => {
it('leaves out a bundle that declares one of its own ids twice while accepting other bundles', () => {
const stutter = layer('stutter', [{ insert: [{ id: 'x', name: 'stutter/a' }, { id: 'x', name: 'stutter/b' }] }])
const clean = layer('clean', [{ insert: [{ id: 'y', name: 'clean' }] }])
const { owners, skipped, composed } = claimLayerIds([base, stutter, clean])
const { owners, skipped } = claimLayerIds([base, stutter, clean])
expect(skipped.get('stutter')).toEqual([
{ rowId: 'x', moduleName: 'stutter/b', layer: 'stutter', packageName: 'stutter', declaredBy: 'stutter', message: 'row "x" is declared twice by stutter' },
])
expect(owners.has('x')).toBe(false)
expect(owners.get('y')?.packageName).toBe('clean')
expect([...composed.keys()]).toEqual(['@deepseek-ai/dsh-base', 'clean'])
expect(composed.get('clean')?.patches[0]).toEqual({ insert: [{ id: 'y', name: 'clean' }] })
})
it('gives the earlier external bundle the id and leaves the later one out whole', () => {
@@ -7,7 +7,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context, type Plugin } from '@deepseek-ai/cordis'
import Loader, { type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { boot, composeProfileStack, entryIssue, inspectEntryIssues, ProfileRuntime, rootIncludeEntry, warnNestedFiberFailures, type Profile, type ProfileLayer } from '../src/index.ts'
import { boot, composeProfileStack, inspectEntryIssues, ProfileRuntime, rootIncludeEntry, warnNestedFiberFailures, type Profile, type ProfileLayer } from '../src/index.ts'
import { entryIssue } from '../src/entry-issues.ts'
const roots: string[] = []
const contexts: Context[] = []
@@ -18,7 +18,7 @@ function layer(patches: PatchOptions[]): ProfileLayer {
return { packageName: 'ext', version: undefined, packageDir: '/nowhere', patchPath: '/nowhere/patch.yml', patches }
}
describe('analyzeBundleLayer', () => {
it('preserves parents, explicit ids and overrides across several groups', () => {
it('identifies declared rows across several groups without claiming override targets', () => {
const patches: PatchOptions[] = [
{ insert: [{ id: 'own', name: 'cordis:group', group: true, config: [{ id: 'child', name: 'ext/child' }] }] },
{ id: 'tools', insert: [{ id: 'tool', name: 'ext/tool' }] },
@@ -27,16 +27,13 @@ describe('analyzeBundleLayer', () => {
{ id: 'child', disabled: true },
]
const result = analyzeBundleLayer(layer(patches))
expect(result.patches).toBe(patches)
expect([...result.rows.keys()]).toEqual(['own', 'child', 'tool', 'panel'])
expect(result.overrides).toEqual(['webserver'])
expect(result.duplicates).toEqual([])
})
it('keeps array-valued plugin configuration out of the row inventory', () => {
const result = analyzeBundleLayer(layer([{ id: 'provider', config: ['value', { value: 42 }, null] }]))
expect([...result.rows]).toEqual([])
expect(result.duplicates).toEqual([])
expect(result.overrides).toEqual(['provider'])
})
it('rejects repeated insert ids but permits restating children in the same group', () => {
const patches: PatchOptions[] = [