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 76b4bac8a2..1598f77317 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: 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 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 986d8588fe..364e79d290 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 5b44878a1a..80a22e1839 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 19c4ee66cd..7e19c1eba5 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: 5249930e288da5b13145eff92d67ff310ac63b3d -architecture.zh.md: b9020585b0c436a314042ee19e82a9a544a74d43 +architecture.md: d88429227fbdae3a49f0bdf1a7c1abb005b27195 +architecture.zh.md: c3bcd1b8ce318421f12f1b03c8f798761413d887 diff --git a/docs/architecture.md b/docs/architecture.md index 5249930e28..d88429227f 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 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. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index b9020585b0..c3bcd1b8ce 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,或插入新条目。 -已安装的组合包是外部的:它的行挂在一个受控组下,启动失败的行被隔离并报告,而内置行仍然让启动失败([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 应用拥有工作之后,替换其依赖会破坏该生命周期。 diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 0de33fe65d..de3c0cda45 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: cabe6a1fd3df6e1c079450dcbf0f0ecfa77c2f42 -README.zh.md: 57aeb255ec0de21416bad381534106c20003d3c0 +README.md: 7d7d612def6e9261fd64097fa1381761a33e087e +README.zh.md: 3f76119dfbed4ec47c3cb6ecd435899ed8d813ad diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index cabe6a1fd3..7d7d612def 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -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 diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 57aeb255ec..3f76119dfb 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -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)。 ### 源码地图 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 328aecaa9d..011dfccb5d 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -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, 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 2ce439d15d..98809fa9c6 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[] = [