소스 검색

ci(release): keep experimental packages out of the default product

Tianyi Cui 1 주 전
부모
커밋
f34dd86285

+ 6 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/process/2026-09-12-default-product-experimental-isolation.md
+2026-09-12-default-product-experimental-isolation.md: 66341e95297267b97be9c41f3edce81bbd5f9c8c
+2026-09-12-default-product-experimental-isolation.zh.md: 207d7f5c05d96665df98f1a9875db68315b9bf03

+ 33 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md

@@ -0,0 +1,33 @@
+# Agent Note: Keep experimental packages outside the default product
+
+Status: implemented
+
+English | [中文](2026-09-12-default-product-experimental-isolation.zh.md)
+
+## Problem
+
+Public npm availability does not make an experimental package part of the default product. Direct manifest checks miss dependency aliases, transitive installation paths, runtime imports declared only for development, and plugins loaded by configuration. The release smoke installs every tarball together, so the presence of experimental packages in its consumer directory does not identify what the default product requires.
+
+## Decision
+
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) runs in static CI and package hygiene. It follows runtime dependencies, optional dependencies, and peers from every app and the Python runtime, resolves workspace and npm aliases, and identifies experimental packages by their npm prefix or repository directory. Publication denylist membership has no effect on this classification.
+
+The source check also reads runtime imports in the selected packages, installation-owned profile bundle lists, bundle patches, shipped agent presets, and declared configuration trees. Loader groups, inserts, Include patches, and disabled plugin rows remain checked. Ordinary plugin configuration data is not interpreted as another Loader entry list. Missing default roots fail the check.
+
+The default Web source graph starts at the module scripts in its actual HTML entry, including inline modules and locally referenced Worker entries. The separate experimental preview can exist without joining this graph. Importing it from the default entry fails the check. Source references to Cordis configuration files include the Desktop patch in the same proof.
+
+[`verify-packed-install`](../../../../scripts/release/verify-packed-install.ts) follows the installed dependency graph from `@deepseek-ai/dsh`, using resolved manifest names to detect aliases and external transitive dependencies. Development dependencies and unrelated tarballs installed beside the product are excluded. Missing required dependencies fail; omitted optional dependencies remain allowed unless they name an experimental package.
+
+This check enforces the existing [experimental dependency isolation rules](../architecture/2026-08-18-experimental-agent-teams-packages.md). The [publication policy](2026-09-12-experimental-publication-denylist.md) independently determines which experimental packages explicit consumers may install.
+
+## Alternatives considered
+
+**Check only direct dependency names.** Aliases, runtime source imports, and configuration-loaded plugins can bypass that check.
+
+**Reject every installed experimental tarball.** The release smoke intentionally installs the whole release family, including opt-in packages. Only the default entry's dependency graph answers whether those packages ship as product requirements.
+
+**Scan every Web source as a default entry.** This would reject the separate experimental preview even when the default HTML and runtime imports never reach it.
+
+## Consequences
+
+Experimental packages may publish without joining default installations or compositions. The source proof covers declared and literal runtime references; the installed check additionally validates the package-manager-resolved dependency graph. Arbitrary runtime-generated module names remain subject to composition review and runtime tests rather than static evaluation.

+ 33 - 0
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 将实验包隔离在默认产品之外
+
+Status: implemented
+
+[English](2026-09-12-default-product-experimental-isolation.md) | 中文
+
+## Problem
+
+在 npm 上公开可用不意味着实验包属于默认产品。直接 manifest 检查会漏掉依赖别名、传递安装路径、仅声明为开发依赖的运行时导入,以及由配置加载的插件。发布 smoke 会一起安装所有 tarball,因此消费者目录中存在实验包并不能说明默认产品需要它。
+
+## Decision
+
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) 在静态 CI 和包 hygiene 中运行。它从所有应用与 Python runtime 出发,遍历运行时依赖、可选依赖和 peer,解析 workspace 与 npm 别名,并按 npm 前缀或仓库目录识别实验包。发布 denylist 的成员关系不影响此分类。
+
+源码检查还读取所选包的运行时导入、安装自带的 profile bundle 列表、bundle patch、随产品提供的 Agent preset,以及声明的配置树。Loader group、insert、Include patch 和禁用的插件行均纳入检查。普通插件配置数据不会被解释为另一个 Loader entry 列表。默认入口缺失会使检查失败。
+
+默认 Web 源码图从实际 HTML 入口中的 module script 出发,包括内联模块和本地引用的 Worker 入口。独立的实验预览可以存在而不加入此图;默认入口导入它时检查会失败。源码中的 Cordis 配置文件引用会将 Desktop patch 纳入同一证明。
+
+[`verify-packed-install`](../../../../scripts/release/verify-packed-install.ts) 从 `@deepseek-ai/dsh` 遍历已安装依赖图,通过解析后的 manifest 名称识别别名和外部传递依赖。开发依赖以及安装在产品旁边的不相关 tarball 不参与遍历。缺失必需依赖会失败;允许省略可选依赖,但其名称不得指向实验包。
+
+此检查执行现有的[实验包依赖隔离规则](../architecture/2026-08-18-experimental-agent-teams-packages.zh.md)。[发布策略](2026-09-12-experimental-publication-denylist.zh.md) 独立决定显式消费者可以安装哪些实验包。
+
+## Alternatives considered
+
+**只检查直接依赖名称。** 别名、运行时源码导入和配置加载的插件可以绕过该检查。
+
+**拒绝任何已安装的实验 tarball。** 发布 smoke 有意安装整个发布族,包括可选启用的包。只有默认入口的依赖图能够回答这些包是否成为产品依赖。
+
+**将全部 Web 源码视为默认入口。** 即使默认 HTML 和运行时导入从未到达独立的实验预览,这也会错误拒绝它。
+
+## Consequences
+
+实验包可以发布而不加入默认安装或组合。源码证明覆盖声明和字面量运行时引用;安装检查进一步验证包管理器解析出的依赖图。任意运行时生成的模块名称仍由组合评审和运行时测试约束,不做静态求值。

+ 1 - 0
package.json

@@ -130,6 +130,7 @@
     "verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
     "verify-optional-dependency-imports": "tsx scripts/verify-optional-dependency-imports.ts",
     "verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
+    "verify-default-product-isolation": "tsx scripts/verify-default-product-isolation.ts",
     "verify-application-entrypoints": "tsx scripts/verify-application-entrypoints.ts",
     "verify-package-dependencies": "tsx scripts/verify-package-dependencies.ts",
     "verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts",

+ 1 - 1
packages/experimental/AGENTS.md

@@ -4,6 +4,6 @@ These rules supplement the [package rules](../AGENTS.md). The [experimental publ
 
 - A package belongs here only when its complete public contract is experimental or internal-only. An experimental option inside a release package stays with its owning product role.
 - Every package here uses the `@deepseek-ai/dsh-experimental-*` npm prefix and joins the dsh release family by default: omit `private` and set `publishConfig.access: public`. Only directories in `PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES` in [the publication policy](../../scripts/experimental-package-policy.ts) stay private: set `private: true` and omit `publishConfig`.
-- Release packages and apps outside this group must not name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`. Experimental packages may depend on release packages and each other. Tests may use experimental packages through `devDependencies`; examples may load them explicitly.
+- Release packages and apps outside this group must not name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`. [Default-product isolation](../../.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md) also checks transitive installations, runtime imports, and shipped compositions in CI. Experimental packages may depend on release packages and each other. Tests may use experimental packages through `devDependencies`; examples may load them explicitly.
 - Experimental status does not relax engineering, security, documentation, lifecycle, testing, invariant, or snapshot requirements.
 - Publishing an experimental package does not promote it or add a stability promise. Promotion moves a package to its product-role group and removes `experimental-` from its npm name; update every import and configuration row atomically, then review its public contract, limitations, test evidence, release payload, runtime dependents, and named stable owner.

+ 93 - 0
scripts/release/installed-product-isolation.spec.ts

@@ -0,0 +1,93 @@
+/** Installed-product isolation uses resolved package identities and runtime edges. */
+
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { verifyInstalledProductIsolation } from './installed-product-isolation.ts'
+
+const roots: string[] = []
+const experimental = '@deepseek-ai/dsh-experimental-example'
+
+function fixture(): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh-installed-isolation-'))
+  roots.push(root)
+  return root
+}
+
+function writePackage(root: string, key: string, manifest: Record<string, unknown>): string {
+  const directory = join(root, 'node_modules', key)
+  mkdirSync(directory, { recursive: true })
+  writeFileSync(join(directory, 'package.json'), `${JSON.stringify({ name: key, ...manifest })}\n`)
+  return directory
+}
+
+afterEach(() => {
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+describe('installed default-product isolation', () => {
+  it('ignores development dependencies and unrelated installed experimental packages', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      dependencies: { core: '1.0.0' },
+      devDependencies: { [experimental]: '1.0.0' },
+    })
+    writePackage(root, 'core', { peerDependencies: { '@deepseek-ai/dsh': '1.0.0' } })
+    writePackage(root, experimental, {})
+
+    expect(verifyInstalledProductIsolation(entry)).toBe(2)
+  })
+
+  it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
+    'rejects a transitive experimental %s',
+    (section) => {
+      const root = fixture()
+      const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { thirdParty: '1.0.0' } })
+      writePackage(root, 'thirdParty', { [section]: { [experimental]: '1.0.0' } })
+
+      expect(() => verifyInstalledProductIsolation(entry)).toThrow(
+        `@deepseek-ai/dsh -> thirdParty -> ${experimental}`,
+      )
+    },
+  )
+
+  it('rejects experimental identities hidden behind an installed alias', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { safeName: 'file:../prototype' } })
+    writePackage(root, 'safeName', { name: experimental })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('rejects experimental npm aliases even when an optional package is omitted', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      optionalDependencies: { safeName: `npm:${experimental}@1.0.0` },
+    })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('follows nested installations rather than an unrelated hoisted package', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { core: '1.0.0' } })
+    writePackage(root, 'core', {})
+    writePackage(entry, 'core', { dependencies: { [experimental]: '1.0.0' } })
+
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
+  })
+
+  it('permits missing optional packages and optional peers but rejects missing dependencies', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      dependencies: { optional: '1.0.0' },
+      optionalDependencies: { optional: '1.0.0' },
+      peerDependencies: { peer: '1.0.0' },
+      peerDependenciesMeta: { peer: { optional: true } },
+    })
+    expect(verifyInstalledProductIsolation(entry)).toBe(1)
+    writePackage(root, '@deepseek-ai/dsh', { dependencies: { missing: '1.0.0' } })
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow('dependency is missing')
+  })
+})

+ 65 - 0
scripts/release/installed-product-isolation.ts

@@ -0,0 +1,65 @@
+/** Experimental-package isolation for the installed default product's dependency graph. */
+
+import { existsSync, readFileSync, realpathSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { join } from 'node:path'
+
+interface InstalledManifest {
+  name: string
+  dependencies?: Record<string, string>
+  optionalDependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+  peerDependenciesMeta?: Record<string, { optional?: boolean }>
+}
+
+/**
+ * Reject experimental dependencies reachable from one installed product entry.
+ * Other packages installed beside the entry and development dependencies do not join its graph.
+ * @param directory - installed entry package directory.
+ * @returns number of distinct installed packages visited.
+ */
+export function verifyInstalledProductIsolation(directory: string): number {
+  const visited = new Set<string>()
+  const queue = [{ directory, chain: [] as string[] }]
+  for (const item of queue) {
+    const canonical = realpathSync(item.directory)
+    if (visited.has(canonical)) continue
+    visited.add(canonical)
+    const manifest = JSON.parse(readFileSync(join(canonical, 'package.json'), 'utf8')) as InstalledManifest
+    const chain = [...item.chain, manifest.name]
+    rejectExperimental(manifest.name, chain)
+    for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
+      for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        rejectExperimental(name, [...chain, name])
+        if (range.startsWith('npm:')) {
+          rejectExperimental(range.slice(4), [...chain, `${name} (${range})`])
+        }
+        const dependency = installedPackage(canonical, name)
+        if (dependency === undefined) {
+          if (section === 'optionalDependencies'
+            || Object.hasOwn(manifest.optionalDependencies ?? {}, name)
+            || section === 'peerDependencies' && manifest.peerDependenciesMeta?.[name]?.optional === true) continue
+          throw new Error(`default product dependency is missing: ${[...chain, name].join(' -> ')}`)
+        }
+        queue.push({ directory: dependency, chain })
+      }
+    }
+  }
+  return visited.size
+}
+
+function rejectExperimental(name: string, chain: readonly string[]): void {
+  if (name.startsWith('@deepseek-ai/dsh-experimental-')) {
+    throw new Error(`default product includes an experimental package: ${chain.join(' -> ')}`)
+  }
+}
+
+/** Resolve a dependency directory through the installed package's ancestor node_modules. */
+function installedPackage(from: string, name: string): string | undefined {
+  const resolver = createRequire(join(from, 'package.json'))
+  for (const directory of resolver.resolve.paths(name) ?? []) {
+    const candidate = join(directory, name)
+    if (existsSync(join(candidate, 'package.json'))) return candidate
+  }
+  return undefined
+}

+ 4 - 0
scripts/release/verify-packed-install.ts

@@ -24,6 +24,7 @@ import { parseArgs } from 'node:util'
 import { releaseFamily } from './families.ts'
 import { capture, isEntry } from './process.ts'
 import { packedIdentity } from './tarball.ts'
+import { verifyInstalledProductIsolation } from './installed-product-isolation.ts'
 
 /**
  * Environment for the installed artifact: no host Node hooks, no host DeepSeek
@@ -107,6 +108,9 @@ function main(): void {
     capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional', '--loglevel=http'],
       { cwd: consumerRoot, env: environment })
 
+    const installedEntry = join(consumerRoot, 'node_modules', entry.packageName)
+    const packageCount = verifyInstalledProductIsolation(installedEntry)
+    console.log(`release verify-packed-install: ${String(packageCount)} default-product packages exclude experimental packages`)
     const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)
     const version = capture(process.execPath, [bin, '--version'], { cwd: consumerRoot, env: environment })
     if (version !== expected.version) {

+ 12 - 1
scripts/run-gates.spec.ts

@@ -245,7 +245,7 @@ describe('gate graph validation', () => {
     const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
 
     expect(ids).toEqual([
-      'rescope-vendor', 'publint', 'constraints', 'package-dependencies', 'application-entrypoints',
+      'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'package-dependencies', 'application-entrypoints',
       'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
       'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'no-bare-dispatcher', 'cordis-config',
       'runtime-closure',
@@ -293,6 +293,17 @@ describe('gate graph validation', () => {
     },
   )
 
+  it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
+    'executes default-product experimental isolation in %s',
+    (mode) => {
+      const gate = withPnpmEntrypoint(() => gatesForMode(mode)
+        .find(subject => subject.id === 'default-product-isolation'))
+
+      expect(gate?.args).toContain('verify-default-product-isolation')
+      expect(gate?.allowFailure).not.toBe(true)
+    },
+  )
+
   it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
     'keeps the client dependency policy in %s',
     (mode) => {

+ 2 - 0
scripts/run-gates.ts

@@ -297,6 +297,7 @@ export function gatesForMode(selected: Mode): Gate[] {
 function ciSharedStaticGates(): Gate[] {
   return [
     pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
+    pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
     pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
     pnpmScript('constraints', 'constraints'),
     pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
@@ -693,6 +694,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
     pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
     pnpmScript('publint', 'publint', artifactOptions),
     pnpmScript('constraints', 'constraints'),
+    pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
     pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
     pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
     pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),

+ 204 - 0
scripts/verify-default-product-isolation.spec.ts

@@ -0,0 +1,204 @@
+/** Source, installation, and composition regressions for default-product isolation. */
+
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { verifyDefaultProductIsolation } from './verify-default-product-isolation.ts'
+
+const roots: string[] = []
+const experimental = '@deepseek-ai/dsh-experimental-prototype'
+const core = '@deepseek-ai/dsh-core'
+const base = '@deepseek-ai/dsh-base'
+const profile = 'packages/boot/app-boot/src/profile.ts'
+const preset = 'packages/preset/agent-presets/presets/standard/agent.cordis.yml'
+const patch = 'packages/bundle/base/cordis.patch.yml'
+
+function write(root: string, path: string, value: unknown): void {
+  const target = join(root, path)
+  mkdirSync(dirname(target), { recursive: true })
+  writeFileSync(target, typeof value === 'string' ? value : `${JSON.stringify(value)}\n`)
+}
+
+function manifest(root: string, path: string, fields: Record<string, unknown>): void {
+  const existing = JSON.parse(readFileSync(join(root, path), 'utf8')) as Record<string, unknown>
+  write(root, path, { ...existing, ...fields })
+}
+
+function fixture(): string {
+  const root = mkdtempSync(join(tmpdir(), 'dsh-default-isolation-'))
+  roots.push(root)
+  write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh', dependencies: { [core]: 'workspace:^' } })
+  write(root, 'apps/cli/src/bin.ts', 'export {}\n')
+  write(root, 'apps/web/package.json', { name: '@deepseek-ai/dsh-web-frontend' })
+  write(root, 'apps/web/index.html', '<script type="module" src="/src/main.ts"></script>')
+  write(root, 'apps/web/src/main.ts', 'export {}\n')
+  write(root, 'python/sdk-runtime/package.json', { name: '@deepseek-ai/dsh-python-runtime' })
+  write(root, 'packages/core/core/package.json', { name: core })
+  write(root, 'packages/core/core/src/index.ts', 'export {}\n')
+  write(root, 'packages/bundle/base/package.json', { name: base, dsh: { bundle: { patch: './cordis.patch.yml' } } })
+  write(root, patch, [{ insert: [{ name: core }] }])
+  write(root, preset, [{ name: core }])
+  write(root, profile, `export const PROFILE_TEMPLATES = { headless: { bundles: ['${base}'], patchReload: 'startup' } }\n`
+    + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+  write(root, 'packages/experimental/prototype/package.json', { name: experimental })
+  return root
+}
+
+afterEach(() => {
+  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+describe('default product isolation', () => {
+  it('allows development dependencies, type imports, and separate experimental preview entries', () => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { devDependencies: { [experimental]: 'workspace:^' } })
+    write(root, 'apps/cli/src/bin.ts', `import type { Options } from '${experimental}'\nexport type { Options }\n`)
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+    write(root, 'packages/experimental/prototype/cordis.patch.yml', [{ name: experimental }])
+
+    expect(verifyDefaultProductIsolation(root)).toMatchObject({ failures: [], packageCount: 5, configCount: 2 })
+  })
+
+  it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
+    'rejects transitive experimental %s',
+    (section) => {
+      const root = fixture()
+      manifest(root, 'packages/core/core/package.json', { [section]: { [experimental]: 'workspace:^' } })
+
+      expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`${core} ${section} -> ${experimental}`)
+    },
+  )
+
+  it.each([
+    `npm:${experimental}@1.0.0`, `workspace:${experimental}@*`,
+    'file:../../experimental/prototype', 'link:../../experimental/prototype',
+    'workspace:../../experimental/prototype',
+  ])('rejects a safe-looking dependency alias targeting %s', (range) => {
+    const root = fixture()
+    manifest(root, 'packages/core/core/package.json', { dependencies: { safe: range } })
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('rejects experimental names absent from the inventory and paths with another package name', () => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { dependencies: { '@deepseek-ai/dsh-experimental-missing': '*' } })
+    manifest(root, 'packages/experimental/prototype/package.json', { name: '@fixture/innocent' })
+    manifest(root, 'python/sdk-runtime/package.json', { dependencies: { '@fixture/innocent': '*' } })
+
+    const failures = verifyDefaultProductIsolation(root).failures.join('\n')
+    expect(failures).toContain('@deepseek-ai/dsh-experimental-missing')
+    expect(failures).toContain('@fixture/innocent')
+  })
+
+  it('follows private application intermediaries and terminates cycles', () => {
+    const root = fixture()
+    write(root, 'apps/desktop/package.json', { name: '@fixture/desktop', private: true,
+      dependencies: { [core]: '*', [experimental]: '*' } })
+    manifest(root, 'packages/core/core/package.json', { peerDependencies: { '@fixture/desktop': '*' } })
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    `import '${experimental}'`,
+    `export * from '${experimental}'`,
+    `await import('${experimental}/client')`,
+    `const x = require('${experimental}')`,
+    'import \'./../../../packages/experimental/prototype/src/index.ts\'',
+  ])('rejects runtime source reference %s', (source) => {
+    const root = fixture()
+    write(root, 'apps/cli/src/bin.ts', source)
+    write(root, 'packages/experimental/prototype/src/index.ts', 'export {}\n')
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    "import './preview.ts'", "import './preview.ts?worker'", "new Worker(new URL('./preview.ts', import.meta.url))",
+  ])('rejects default Web entry reaching preview via %s', (source) => {
+    const root = fixture()
+    write(root, 'apps/web/src/main.ts', source)
+    write(root, 'apps/web/src/preview.ts', `import '${experimental}'\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each([
+    [{ name: experimental, disabled: true }],
+    [{ group: true, config: [{ name: experimental }] }],
+    [{ insert: [{ name: experimental }] }],
+    [{ name: '@deepseek-ai/cordis-plugin-group', config: [{ name: experimental }] }],
+    [{ name: '@deepseek-ai/cordis-plugin-include', config: { patches: [{ insert: [{ name: experimental }] }] } }],
+  ].map(entries => ({ entries })))('rejects experimental plugin rows in $entries', ({ entries }) => {
+    const root = fixture()
+    write(root, patch, entries)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('follows Include files while ignoring ordinary plugin config data', () => {
+    const root = fixture()
+    write(root, patch, [{ name: core, config: { name: experimental, insert: [{ name: experimental }] } }])
+    expect(verifyDefaultProductIsolation(root).failures).toEqual([])
+    write(root, patch, [{ name: '@deepseek-ai/cordis-plugin-include', config: { path: './nested.yml' } }])
+    write(root, 'packages/bundle/base/nested.yml', [{ name: experimental }])
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('checks default profile bundle names and declared config trees', () => {
+    const root = fixture()
+    write(root, profile, `export const PROFILE_TEMPLATES = { test: { bundles: ['${experimental}'] } }\n`
+      + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+    manifest(root, 'apps/cli/package.json', { dsh: { configTrees: [{ path: './config' }] } })
+    write(root, 'apps/cli/config/extra.cordis.yml', [{ name: experimental }])
+    const failures = verifyDefaultProductIsolation(root).failures.join('\n')
+    expect(failures).toContain(`${profile} -> ${experimental}`)
+    expect(failures).toContain('apps/cli/config/extra.cordis.yml')
+  })
+
+  it('checks desktop configuration reached through a source URL', () => {
+    const root = fixture()
+    write(root, 'apps/desktop-host/package.json', { name: '@fixture/desktop-host', private: true })
+    write(root, 'apps/desktop-host/src/index.ts', "new URL('../config/desktop.cordis.patch.yml', import.meta.url)")
+    write(root, 'apps/desktop-host/config/desktop.cordis.patch.yml', [{ insert: [{ name: experimental }] }])
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('checks installation-owned profile tuples as well as new-profile defaults', () => {
+    const root = fixture()
+    const source = readFileSync(join(root, profile), 'utf8')
+    write(root, profile, `${source}\nconst INSTALLATION_OWNED_PROFILE_TUPLES = { headless: ['${experimental}'] }\n`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it('checks inline modules alongside the default Web script', () => {
+    const root = fixture()
+    write(root, 'apps/web/index.html', '<script type="module" src="/src/main.ts"></script>'
+      + `<script type="module">import '${experimental}'</script>`)
+
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(experimental)
+  })
+
+  it.each(['apps/cli/package.json', 'apps/web/index.html', 'python/sdk-runtime/package.json', preset])(
+    'fails closed when required input %s is missing',
+    (path) => {
+      const root = fixture()
+      rmSync(join(root, path))
+      expect(verifyDefaultProductIsolation(root).failures.length).toBeGreaterThan(0)
+    },
+  )
+
+  it.each([
+    '',
+    'export const PROFILE_TEMPLATES = {}; export const DEFAULT_PROFILE_BUNDLES = []',
+    'export const PROFILE_TEMPLATES = computedProfiles(); export const DEFAULT_PROFILE_BUNDLES = []',
+  ])('rejects absent, empty, or uninspectable default profile definitions', (source) => {
+    const root = fixture()
+    write(root, profile, source)
+    expect(() => verifyDefaultProductIsolation(root)).toThrow('profile')
+  })
+})

+ 307 - 0
scripts/verify-default-product-isolation.ts

@@ -0,0 +1,307 @@
+/** Keep experimental packages outside default installations, runtime imports, and shipped compositions. */
+
+import { existsSync, globSync, readFileSync, statSync } from 'node:fs'
+import { dirname, relative, resolve } from 'node:path'
+import { JSDOM } from 'jsdom'
+import ts from 'typescript'
+import { isCordisGroupEntry, loadCordisYaml } from './cordis-yaml.ts'
+import {
+  collectRuntimeLocalSourceSpecifiers,
+  collectRuntimeSourceSpecifiers,
+} from './verify-client-packages.ts'
+
+const EXPERIMENTAL_PREFIX = '@deepseek-ai/dsh-experimental-'
+const PROFILE_SOURCE = 'packages/boot/app-boot/src/profile.ts'
+const PRESET_PATTERN = 'packages/preset/agent-presets/presets/*/agent.cordis.yml'
+const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
+
+interface Manifest {
+  name: string
+  dependencies?: Record<string, string>
+  optionalDependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+  devDependencies?: Record<string, string>
+  dsh?: { bundle?: { patch?: string }; configTrees?: Array<{ path: string }> }
+}
+
+interface Package {
+  directory: string
+  manifest: Manifest
+}
+
+/** Counts and violations from the default product's source and configuration inputs. */
+export interface ProductIsolationResult {
+  failures: string[]
+  packageCount: number
+  sourceCount: number
+  configCount: number
+}
+
+/**
+ * Check default app installations and their authored runtime/configuration inputs.
+ * Experimental opt-in packages and separate Web preview entries are outside these roots.
+ * @param root - repository root; no build outputs or installed workspace links are read.
+ * @returns violations and the sizes of the checked package, source, and configuration sets.
+ */
+export function verifyDefaultProductIsolation(root: string): ProductIsolationResult {
+  const failures: string[] = []
+  const packages = new Map<string, Package>()
+  const directories = new Map<string, Package>()
+  const display = (path: string): string => relative(root, path).replaceAll('\\', '/')
+  for (const path of globSync([
+    'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json',
+    'native/system/packages/*/package.json', 'python/sdk-runtime/package.json',
+  ], { cwd: root }).sort()) {
+    const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as Manifest
+    if (typeof manifest.name !== 'string' || manifest.name === '') throw new Error(`${path}: missing package name`)
+    if (packages.has(manifest.name)) throw new Error(`${path}: duplicate package name ${manifest.name}`)
+    const pkg = { directory: dirname(resolve(root, path)), manifest }
+    packages.set(manifest.name, pkg)
+    directories.set(pkg.directory, pkg)
+  }
+  for (const path of ['apps/cli/package.json', 'apps/web/package.json', 'python/sdk-runtime/package.json']) {
+    if (!existsSync(resolve(root, path))) failures.push(`missing default product root ${path}`)
+  }
+  if (directories.get(resolve(root, 'apps/cli'))?.manifest.name !== '@deepseek-ai/dsh') {
+    failures.push('apps/cli/package.json must identify @deepseek-ai/dsh')
+  }
+
+  const queue: Package[] = []
+  const visited = new Set<string>()
+  const sources = new Set<string>()
+  const configs = new Set<string>()
+  const isExperimental = (pkg: Package): boolean => pkg.manifest.name.startsWith(EXPERIMENTAL_PREFIX)
+    || display(pkg.directory).startsWith('packages/experimental/')
+  const add = (pkg: Package, origin: string): void => {
+    if (isExperimental(pkg)) {
+      failures.push(`${origin} -> ${pkg.manifest.name}: default product must not include experimental packages`)
+    } else if (!visited.has(pkg.directory)) {
+      visited.add(pkg.directory)
+      queue.push(pkg)
+    }
+  }
+  const ownerOf = (path: string): Package | undefined => {
+    let directory = dirname(path)
+    for (;;) {
+      const pkg = directories.get(directory)
+      if (pkg !== undefined) return pkg
+      const parent = dirname(directory)
+      if (parent === directory) return undefined
+      directory = parent
+    }
+  }
+  const reference = (name: string, origin: string, owner?: Package): void => {
+    if (name.startsWith('.') || name.startsWith('/') || /^(?:file|link):/.test(name)) {
+      const target = resolve(owner?.directory ?? root, name.replace(/^(?:file|link):/, ''))
+      const pkg = directories.get(target) ?? ownerOf(target)
+      if (pkg !== undefined) add(pkg, origin)
+      return
+    }
+    const packageName = barePackageName(name)
+    if (packageName.startsWith(EXPERIMENTAL_PREFIX)) {
+      failures.push(`${origin} -> ${name}: default product must not include experimental packages`)
+      return
+    }
+    const pkg = packages.get(packageName)
+    if (pkg !== undefined) add(pkg, origin)
+    else if (packageName.startsWith('@deepseek-ai/')) failures.push(`${origin}: unknown workspace package ${name}`)
+  }
+  const dependency = (name: string, range: string, owner: Package, origin: string): void => {
+    reference(name, origin, owner)
+    if (/^(?:file:|link:|workspace:\.)/.test(range)) {
+      reference(range.replace(/^workspace:/, ''), origin, owner)
+    } else if (/^(?:npm:|workspace:)(?:@|[a-zA-Z])/.test(range)) {
+      reference(range.replace(/^(?:npm:|workspace:)/, ''), origin, owner)
+    }
+  }
+  const sourceReference = (specifier: string, path: string): void => {
+    const owner = ownerOf(path)
+    if (owner === undefined) return
+    reference(specifier, display(path), owner)
+    const name = barePackageName(specifier)
+    const range = [...RUNTIME_SECTIONS, 'devDependencies' as const]
+      .map(section => owner.manifest[section]?.[name]).find(value => value !== undefined)
+    if (range !== undefined) dependency(name, range, owner, display(path))
+  }
+  const scanSource = (path: string, followLocal = false, inlineSource?: string): void => {
+    if (sources.has(path)) return
+    sources.add(path)
+    const source = inlineSource ?? readFileSync(path, 'utf8')
+    for (const specifier of collectRuntimeSourceSpecifiers(path, source)) sourceReference(specifier, path)
+    for (const specifier of runtimeLocalSpecifiers(path, source)) {
+      const target = resolveSource(path, specifier)
+      const resolved = target ?? resolve(dirname(path), specifier)
+      const owner = ownerOf(resolved)
+      if (owner !== undefined && isExperimental(owner)) add(owner, display(path))
+      if (/[^/]*cordis[^/]*\.ya?ml$/.test(resolved)) scanConfig(resolved)
+      if (followLocal && target !== undefined) scanSource(target, true)
+    }
+  }
+  const scanConfig = (path: string): void => {
+    if (configs.has(path)) return
+    configs.add(path)
+    const document = loadCordisYaml(readFileSync(path, 'utf8'))
+    if (!Array.isArray(document)) {
+      failures.push(`${display(path)}: shipped composition must contain an entry array`)
+      return
+    }
+    const visit = (entry: unknown): void => {
+      if (!isRecord(entry)) return
+      if (typeof entry.name === 'string') {
+        const owner = ownerOf(path)
+        if (entry.name.startsWith('.')) {
+          const target = resolve(dirname(path), entry.name)
+          const targetOwner = ownerOf(target)
+          if (targetOwner !== undefined) add(targetOwner, display(path))
+        } else reference(entry.name, display(path), owner)
+      }
+      if (isCordisGroupEntry(entry)) entry.config.forEach(visit)
+      if (Array.isArray(entry.insert)) entry.insert.forEach(visit)
+      if (entry.name === '@deepseek-ai/cordis-plugin-include' && isRecord(entry.config)) {
+        if (Array.isArray(entry.config.patches)) entry.config.patches.forEach(visit)
+        const paths = typeof entry.config.path === 'string' ? [entry.config.path] : entry.config.path
+        if (Array.isArray(paths)) {
+          for (const included of paths) {
+            if (typeof included !== 'string') continue
+            scanConfig(resolve(dirname(path), included))
+          }
+        }
+      }
+    }
+    document.forEach(visit)
+  }
+
+  for (const pkg of packages.values()) {
+    const path = display(pkg.directory)
+    if (path.startsWith('apps/') || path === 'python/sdk-runtime') add(pkg, path)
+  }
+  const profilePath = resolve(root, PROFILE_SOURCE)
+  if (existsSync(profilePath)) {
+    for (const name of profilePackages(readFileSync(profilePath, 'utf8'))) {
+      reference(name, PROFILE_SOURCE)
+      if (packages.get(name)?.manifest.dsh?.bundle?.patch === undefined) {
+        failures.push(`${PROFILE_SOURCE}: default bundle ${name} must declare dsh.bundle.patch`)
+      }
+    }
+  } else failures.push(`missing default profile source ${PROFILE_SOURCE}`)
+  const presets = globSync(PRESET_PATTERN, { cwd: root }).sort()
+  if (presets.length === 0) failures.push(`no shipped presets matched ${PRESET_PATTERN}`)
+  for (const path of presets) scanConfig(resolve(root, path))
+
+  const html = resolve(root, 'apps/web/index.html')
+  if (existsSync(html)) {
+    const dom = new JSDOM(readFileSync(html, 'utf8'))
+    try {
+      const entries = [...dom.window.document.querySelectorAll('script[type="module"]')]
+      if (entries.length === 0) failures.push('apps/web/index.html: missing default module entry')
+      for (const [index, entry] of entries.entries()) {
+        const src = entry.getAttribute('src')
+        if (src === null) {
+          scanSource(`${html}.inline-${String(index)}.js`, true, entry.textContent)
+          continue
+        }
+        if (/^(?:https?:)?\/\//.test(src)) {
+          failures.push(`apps/web/index.html: external default module entry cannot be checked: ${src}`)
+          continue
+        }
+        scanSource(resolve(root, 'apps/web', src.replace(/^\//, '')), true)
+      }
+    } finally {
+      dom.window.close()
+    }
+  } else failures.push('missing default Web entry apps/web/index.html')
+
+  for (const pkg of queue) {
+    const { manifest } = pkg
+    for (const section of RUNTIME_SECTIONS) {
+      for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        dependency(name, range, pkg, `${manifest.name} ${section}`)
+      }
+    }
+    if (manifest.dsh?.bundle?.patch !== undefined) scanConfig(resolve(pkg.directory, manifest.dsh.bundle.patch))
+    for (const tree of manifest.dsh?.configTrees ?? []) {
+      for (const path of globSync('**/*.{yml,yaml}', { cwd: resolve(pkg.directory, tree.path),
+        exclude: ['**/*.i18n.yaml', '**/preset.yml'] })) scanConfig(resolve(pkg.directory, tree.path, path))
+    }
+    if (display(pkg.directory) === 'apps/web') continue
+    const files = globSync('src/**/*.{ts,tsx,mts,cts,js,mjs,cjs}', { cwd: pkg.directory,
+      exclude: ['**/*.spec.*', '**/*.test.*', '**/*.d.ts', '**/tests/**', '**/__tests__/**'] })
+    if (display(pkg.directory) === 'apps/cli' && files.length === 0) failures.push('apps/cli: no default runtime sources')
+    for (const path of files) scanSource(resolve(pkg.directory, path))
+  }
+  return { failures: [...new Set(failures)], packageCount: visited.size,
+    sourceCount: sources.size, configCount: configs.size }
+}
+
+function barePackageName(specifier: string): string {
+  return /^(?:@[^/]+\/)?[^/@]+/.exec(specifier)?.[0] ?? specifier
+}
+
+function resolveSource(from: string, specifier: string): string | undefined {
+  const path = resolve(dirname(from), specifier.replace(/[?#].*$/, ''))
+  if (!/\.(?:[cm]?[jt]sx?)$/.test(path) && /\.[^/]+$/.test(path)) return undefined
+  const base = path.replace(/\.[cm]?js$/, '')
+  return [path, `${base}.ts`, `${base}.tsx`, `${path}/index.ts`, `${path}/index.tsx`]
+    .find(candidate => existsSync(candidate) && statSync(candidate).isFile())
+}
+
+function runtimeLocalSpecifiers(path: string, source: string): Set<string> {
+  const specifiers = collectRuntimeLocalSourceSpecifiers(path, source)
+  const file = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
+  const visit = (node: ts.Node): void => {
+    if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'URL') {
+      const first = node.arguments?.[0]
+      if (first !== undefined && ts.isStringLiteralLike(first) && first.text.startsWith('.')) specifiers.add(first.text)
+    }
+    ts.forEachChild(node, visit)
+  }
+  visit(file)
+  return specifiers
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+/** Read the literal package lists that define installation-owned profile defaults. */
+function profilePackages(source: string): string[] {
+  const file = ts.createSourceFile(PROFILE_SOURCE, source, ts.ScriptTarget.Latest, true)
+  const required = new Set(['PROFILE_TEMPLATES', 'DEFAULT_PROFILE_BUNDLES'])
+  const found = new Set<string>()
+  const packages: string[] = []
+  const literals = (node: ts.Node): void => {
+    if (ts.isStringLiteralLike(node)) {
+      if (node.text.startsWith('@')) packages.push(node.text)
+    } else if (ts.isArrayLiteralExpression(node)) node.elements.forEach(literals)
+    else if (ts.isObjectLiteralExpression(node)) node.properties.forEach(literals)
+    else if (ts.isPropertyAssignment(node)) literals(node.initializer)
+    else if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
+      literals(node.expression)
+    } else throw new Error(`${PROFILE_SOURCE}: default profile packages must use static literal lists`)
+  }
+  for (const statement of file.statements) {
+    if (!ts.isVariableStatement(statement)) continue
+    for (const declaration of statement.declarationList.declarations) {
+      if (!ts.isIdentifier(declaration.name)
+        || !required.has(declaration.name.text) && declaration.name.text !== 'INSTALLATION_OWNED_PROFILE_TUPLES') continue
+      if (declaration.initializer === undefined) continue
+      if (required.has(declaration.name.text)) found.add(declaration.name.text)
+      const before = packages.length
+      literals(declaration.initializer)
+      if (packages.length === before) throw new Error(`${PROFILE_SOURCE}: ${declaration.name.text} has no default bundles`)
+    }
+  }
+  if (found.size !== required.size) throw new Error(`${PROFILE_SOURCE}: missing default profile declarations`)
+  return packages
+}
+
+if (import.meta.main) {
+  const result = verifyDefaultProductIsolation(resolve(import.meta.dirname, '..'))
+  if (result.failures.length > 0) {
+    for (const failure of result.failures) console.error(`verify-default-product-isolation: ${failure}`)
+    process.exitCode = 1
+  } else {
+    console.log(`verify-default-product-isolation: ${String(result.packageCount)} packages, `
+      + `${String(result.sourceCount)} runtime sources, ${String(result.configCount)} configurations exclude experimental packages.`)
+  }
+}