diff --git a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml index d6114b546a..3afa76fb0c 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md index 70e2e1b41a..0c83dc5e23 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md +++ b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md index 5820920258..badb759753 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md @@ -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 视为运行隔离。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index dead4ef8ce..ea413c1838 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md index 6cd6c45142..8ac3ceb3e3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 6add99a408..c165ed29ed 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -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 应用拥有工作之后,替换其依赖会破坏该生命周期。 diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 265a051769..ced0039529 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -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 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index c03e33ef79..94e46a03f5 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -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 diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 62692813cc..516128a34a 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -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)。 ### 源码地图 diff --git a/packages/boot/app-boot/src/compose-stack.ts b/packages/boot/app-boot/src/compose-stack.ts index 711f682d93..25f49d6b7c 100644 --- a/packages/boot/app-boot/src/compose-stack.ts +++ b/packages/boot/app-boot/src/compose-stack.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 } -/** 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 /** The conflicts of each bundle left out, by package name. */ readonly skipped: Map - /** The analysis of each bundle layer that owns its ids, by package name; rendered once and mounted as is. */ - readonly composed: Map } /** 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() const skipped = new Map() - const composed = new Map() 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 } } /** diff --git a/packages/boot/app-boot/src/external-bundles.ts b/packages/boot/app-boot/src/external-bundles.ts index 9ce7ffacd1..450e2b9b0f 100644 --- a/packages/boot/app-boot/src/external-bundles.ts +++ b/packages/boot/app-boot/src/external-bundles.ts @@ -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 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() @@ -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 } } /** diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index d8785e928a..3af0b8d1f1 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -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, diff --git a/packages/boot/app-boot/tests/compose-stack.spec.ts b/packages/boot/app-boot/tests/compose-stack.spec.ts index a34a9c6704..29b1bbd6ec 100644 --- a/packages/boot/app-boot/tests/compose-stack.spec.ts +++ b/packages/boot/app-boot/tests/compose-stack.spec.ts @@ -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', () => { diff --git a/packages/boot/app-boot/tests/entry-issues.spec.ts b/packages/boot/app-boot/tests/entry-issues.spec.ts index 413dceb8f6..8b4a12edb7 100644 --- a/packages/boot/app-boot/tests/entry-issues.spec.ts +++ b/packages/boot/app-boot/tests/entry-issues.spec.ts @@ -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[] = [] diff --git a/packages/boot/app-boot/tests/external-bundles.spec.ts b/packages/boot/app-boot/tests/external-bundles.spec.ts index 29d0fbfc7d..8f2c96c105 100644 --- a/packages/boot/app-boot/tests/external-bundles.spec.ts +++ b/packages/boot/app-boot/tests/external-bundles.spec.ts @@ -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[] = [