Merge PR #3594 API simplification
This commit is contained in:
commit
27896dd532
15 files changed
+31
-42
No files matched your search
+2
-2
@@ -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: 70e2e1b41a5642e348fc710752861b89d1de66ba
|
||||
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: 58209202586c3939f929afd16f4d0a275ef3bb96
|
||||
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md: 0c83dc5e23515708d78747b1489847ac4f229705
|
||||
2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: badb759753229def9e29c8b706b1dab03a798bb5
|
||||
+2
@@ -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.
|
||||
|
||||
+2
@@ -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,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: 6cd6c45142f0a743e0c4a7b5104ec3b15fcf706a
|
||||
architecture.zh.md: 6add99a4086f7d3ec76e5e2cbbb9a1a2a3891176
|
||||
architecture.md: 8ac3ceb3e3daa298a394d05b1600355e62ed628a
|
||||
architecture.zh.md: c165ed29ed983e1c2d6f2c9f31a01ca38d96ae92
|
||||
@@ -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 in one contained group, failing rows are isolated and reported, and the `plugins` Remote changes profiles at runtime ([app-boot](../packages/boot/app-boot/README.md), [plugin-manager](../packages/host/plugin-manager/README.md)).
|
||||
Bundle rows retain declared ids and group hierarchy; enablement selects whole patch layers. Required entries must activate at startup; other row failures produce warnings ([app-boot](../packages/boot/app-boot/README.md)). The `plugins` Remote manages profile packages and rows ([plugin-manager](../packages/host/plugin-manager/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.
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。
|
||||
|
||||
已安装的组合包是外部的:它的行挂在一个受控组下,失败的行被隔离并报告,`plugins` Remote 在运行时修改 profile([app-boot](../packages/boot/app-boot/README.zh.md)、[plugin-manager](../packages/host/plugin-manager/README.zh.md))。
|
||||
组合包的行保留声明的 id 和组层级;启停选择整个 patch 层。启动时必需条目必须激活,其他行的失败产生警告([app-boot](../packages/boot/app-boot/README.zh.md))。`plugins` Remote 管理 profile 的插件包与插件行([plugin-manager](../packages/host/plugin-manager/README.zh.md))。
|
||||
|
||||
自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk`、`sdk-minimal` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。
|
||||
|
||||
|
||||
@@ -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: c03e33ef796f593f3cd260df238845f738d6beea
|
||||
README.zh.md: 62692813cc4c80f6b04e5a8280cd86110f268e73
|
||||
README.md: 94e46a03f5bd7603e8a5444e2663b937f028e65f
|
||||
README.zh.md: 516128a34ab8df8a96a4a9b7bfa5b12dba1b9112
|
||||
@@ -142,7 +142,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
|
||||
|
||||
|
||||
@@ -142,7 +142,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)。
|
||||
|
||||
### 源码地图
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,10 +52,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[] = [
|
||||
|
||||
Reference in new issue
Block a user