fix(experimental): reject the experimental prefix in new package names

Freeze the existing prefixed directories in
LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES and reject the prefix
elsewhere. Record the naming decision in a new Agent Note and keep the
plugin identity note's facts current.

Refs #7841
This commit is contained in:
Turtle
2026-11-04 04:59:30 +08:00
parent 22f0226e49
commit cfa6f7359e
14 changed files with 193 additions and 42 deletions
@@ -9,14 +9,14 @@
en: 8112c1c059d1cf10
zh: 852d23c56a93a555
/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/decision:
en: 90d74300f045bf6e
zh: 088e88944478ca2a
en: 3cf1a8f986cea871
zh: 3105cf2608108ecd
/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/deferred-naming-cleanup:
en: 5b0cef3219fdb774
zh: f479a48a83ec5f07
en: 7f59c9d14a36e133
zh: 2e7829c59b4fcd24
/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/alternatives-considered:
en: 65af55b5775c9bc5
zh: 2ea75b3fd33ac0c2
/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/consequences:
en: e8e96b0354f1a3c6
zh: ad1bfe3600e14ff1
en: 08613c24e7c552ab
zh: 6f2509d3e61cb2c7
@@ -10,7 +10,7 @@ Moving a capability into the experimental directory changes its ownership and su
## Decision
The moved packages retain the published names declared by the [experimental naming policy](../../../../scripts/experimental-package-policy.ts). That typed directory-to-name map is the sole maintained inventory of naming exceptions. Package manifests, consumers, installation checks, and bundler checks agree with it; a missing, moved, or duplicated exception is rejected. [Experimental status](../../../../packages/experimental/README.md#status) remains independent of spelling, Official discovery, and explicit selection.
The moved packages retain the published names declared by the [experimental naming policy](../../../../scripts/experimental-package-policy.ts). That typed directory-to-name map is the sole maintained inventory of unprefixed experimental names; under the [unprefixed naming decision](2026-11-04-unprefixed-experimental-package-names.md), every new experimental package joins it. Package manifests, consumers, installation checks, and bundler checks agree with it; a missing, moved, or duplicated entry is rejected. [Experimental status](../../../../packages/experimental/README.md#status) remains independent of spelling, Official discovery, and explicit selection.
Existing badge, Ralph, and native delegation row IDs remain valid Host row selectors. New isolation groups and newly introduced capabilities retain their new IDs. Later user layers still replace configuration or disable rows. Selecting an optional bundle is still explicit: a patch targeting an absent row does not select its bundle.
@@ -20,10 +20,9 @@ Compatibility protection covers Web and Desktop profiles, including existing nat
| Retained identity | Unconstrained cleanup | Why it is deferred |
|---|---|---|
| Published names with the legacy `@deepseek-ai/dsh-experimental-` prefix | Drop the prefix so that every experimental package keeps its name on promotion | Existing dependencies, imports, module declarations, and profile files use the established names |
| Existing badge, Ralph, and native-tool row IDs | Give optional contributions consistently distinct `optional-*` selectors | Existing user overrides use those IDs; the ownership move does not require a new selector |
These naming changes are deferred, not prerequisites for experimental classification. A later cleanup requires an explicit release and migration decision, an inventory of affected Web/Desktop configurations, and installed-profile upgrade evidence. Directory moves or pre-stable API status alone do not retire these exceptions. The package list stays in the typed policy rather than being copied into this note or package READMEs.
These naming changes are deferred, not prerequisites for experimental classification. A later cleanup requires an explicit release and migration decision, an inventory of affected Web/Desktop configurations, and installed-profile upgrade evidence. Directory moves or pre-stable API status alone do not retire these row IDs. The package list stays in the typed policy rather than being copied into this note or package READMEs.
## Alternatives considered
@@ -35,6 +34,6 @@ These naming changes are deferred, not prerequisites for experimental classifica
## Consequences
The repository carries a bounded naming-exception map and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides and disabled state. No general alias or automatic profile migration mechanism is introduced.
The repository carries a typed map of unprefixed experimental names and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides and disabled state. No general alias or automatic profile migration mechanism is introduced.
The [classification decision](../process/2026-10-05-product-package-classification.md), [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md), and [on-demand discovery decision](2026-10-05-official-on-demand-bundles.md) retain their separate responsibilities. Released Session generations and persisted hook event names remain protected independently of this profile compatibility scope.
@@ -10,7 +10,7 @@ Status: implemented
## 决策
迁移的包保留[实验包命名策略](../../../../scripts/experimental-package-policy.ts)声明的已发布名称。这个带类型的目录到包名映射是命名例外唯一手工维护的清单。包清单、消费者、安装检查和打包检查与其保持一致;缺失、迁移到其他位置或重复归属的例外会被拒绝。[实验状态](../../../../packages/experimental/README.zh.md#status)仍独立于名称拼写、Official 发现和显式选择。
迁移的包保留[实验包命名策略](../../../../scripts/experimental-package-policy.ts)声明的已发布名称。这个带类型的目录到包名映射是不带前缀的实验包名唯一手工维护的清单;按照[不带前缀命名决策](2026-11-04-unprefixed-experimental-package-names.zh.md),每个新实验包都加入其中。包清单、消费者、安装检查和打包检查与其保持一致;缺失、迁移到其他位置或重复归属的条目会被拒绝。[实验状态](../../../../packages/experimental/README.zh.md#status)仍独立于名称拼写、Official 发现和显式选择。
既有徽章、Ralph 和原生委派行 ID 作为 Host 行选择器保持有效。新增隔离组和新引入的能力保留各自的新 ID。后续用户层仍可替换配置或禁用行。可选 bundle 仍须显式选择:指向缺失行的 patch 不会自动选择其 bundle。
@@ -20,10 +20,9 @@ Status: implemented
| 保留的身份 | 不受兼容性约束时的清理 | 暂缓原因 |
|---|---|---|
| 带旧 `@deepseek-ai/dsh-experimental-` 前缀的已发布名称 | 去掉前缀,使每个实验包晋升时保留名称 | 既有依赖、导入、模块声明和 profile 文件使用原有名称 |
| 既有徽章、Ralph 和原生工具行 ID | 为可选贡献统一使用独立的 `optional-*` 选择器 | 既有用户覆盖使用这些 ID;归属迁移不要求新的选择器 |
这些命名变更被暂缓,不是实验分类的前提。后续清理需要明确的发布与迁移决策、受影响 Web/Desktop 配置清单,以及已安装 profile 的升级验证。仅目录迁移或公共 API 处于稳定前阶段,不足以删除这些例外。包清单保留在带类型的策略中,不复制到本记录或各包 README。
这些命名变更被暂缓,不是实验分类的前提。后续清理需要明确的发布与迁移决策、受影响 Web/Desktop 配置清单,以及已安装 profile 的升级验证。仅目录迁移或公共 API 处于稳定前阶段,不足以删除这些行 ID。包清单保留在带类型的策略中,不复制到本记录或各包 README。
## 考虑过的替代方案
@@ -35,6 +34,6 @@ Status: implemented
## 后果
仓库维护范围明确的命名例外映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置和禁用状态的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。
仓库维护带类型的不带前缀实验包名映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置和禁用状态的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。
[分类决策](../process/2026-10-05-product-package-classification.zh.md)、[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)和[按需发现决策](2026-10-05-official-on-demand-bundles.zh.md)继续承担各自独立的职责。已发布 Session 代际和持久化 hook 事件名的保护独立于此 profile 兼容范围。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-11-04-unprefixed-experimental-package-names.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-11-04-unprefixed-experimental-package-names.md
/agent-note-name-new-experimental-packages-without-the-experimental-prefix:
en: b84bbfa015f35105
zh: dff2c32694314996
/agent-note-name-new-experimental-packages-without-the-experimental-prefix/problem:
en: 84638e34c7b1d291
zh: 0c3928b3424db016
/agent-note-name-new-experimental-packages-without-the-experimental-prefix/decision:
en: 0f13619da3b05747
zh: fac563c2a8b61270
/agent-note-name-new-experimental-packages-without-the-experimental-prefix/alternatives-considered:
en: 1c6a08a813dcb3a7
zh: 16bf31184377c80b
/agent-note-name-new-experimental-packages-without-the-experimental-prefix/consequences:
en: 2b536c42672e8850
zh: f6a551964855308e
@@ -0,0 +1,29 @@
# Agent Note: Name new experimental packages without the experimental prefix
Status: implemented
English | [中文](2026-11-04-unprefixed-experimental-package-names.zh.md)
## Problem
An experimental package named `@deepseek-ai/dsh-experimental-<name>` must be renamed when it moves to a product group. The rename changes every import, dependency entry, module declaration, and profile file that names the package, so promotion carries a migration that is unrelated to the capability's behavior.
## Decision
New experimental packages use `@deepseek-ai/dsh-<name>` and never the `experimental` prefix. Each one is declared in `EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS` in the [experimental package policy](../../../../scripts/experimental-package-policy.ts), because isolation checks and plugin-record ownership recognize an unprefixed experimental name only through that map. Promotion moves the package and removes its map entry; its npm name stays the same.
Existing packages with the prefix are listed in `LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES`. `pnpm run constraints` rejects the prefix in any other directory and rejects a listed directory that no longer holds a prefixed package, so the list can only shrink. A legacy package is renamed when it is promoted; renaming it earlier needs the release and migration evidence that the [plugin identity decision](2026-10-06-web-desktop-plugin-identity-compatibility.md) requires.
This decision replaces the deferred cleanup that the plugin identity decision recorded, which gave every experimental package the prefix. The [package rules](../../../../packages/experimental/AGENTS.md) state the naming rule for authors.
## Alternatives considered
**Keep the prefix as the default.** The name shows experimental status, but each promotion then renames the package and migrates its consumers and installed profiles.
**Rename the legacy packages now.** One rename removes the legacy list, but it changes published names that existing dependencies and profiles use, without a promotion that needs the change.
**Document the rule without a gate.** A new package copied from a legacy one would keep the prefix with every check passing.
## Consequences
The unprefixed-name map grows with every new experimental package; the legacy list only shrinks. Directory placement and the map, not the npm name, identify an unprefixed package as experimental, and an unprefixed name does not grant admission to the default product.
@@ -0,0 +1,29 @@
# Agent Note: 新实验包命名不使用 experimental 前缀
Status: implemented
[English](2026-11-04-unprefixed-experimental-package-names.md) | 中文
## 问题
名为 `@deepseek-ai/dsh-experimental-<name>` 的实验包迁入产品分组时必须重命名。重命名会改变引用该包名的每个导入、依赖项、模块声明和 profile 文件,因此晋升会附带一次与能力行为无关的迁移。
## 决策
新实验包使用 `@deepseek-ai/dsh-<name>`,不使用 `experimental` 前缀。每个新实验包都在[实验包命名策略](../../../../scripts/experimental-package-policy.ts)的 `EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS` 中声明,因为隔离检查和插件记录归属只通过这个映射识别不带前缀的实验包名。晋升时移动该包并删除其映射条目;其 npm 名称保持不变。
带前缀的既有包列在 `LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES` 中。`pnpm run constraints` 拒绝其他目录中出现该前缀,也拒绝已列出但不再包含带前缀包的目录,因此该清单只能缩减。遗留包在晋升时重命名;提前重命名需要[插件身份决策](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)要求的发布与迁移依据。
本决策取代插件身份决策记录的暂缓清理,该清理为每个实验包加上前缀。[包规则](../../../../packages/experimental/AGENTS.md)向作者说明命名规则。
## 考虑过的替代方案
**默认保留前缀。** 名称能体现实验状态,但每次晋升都要重命名该包,并迁移其消费方和已安装 profile。
**现在重命名遗留包。** 一次重命名即可删除遗留清单,但会在没有晋升需要的情况下改变既有依赖和 profile 使用的已发布名称。
**只记录规则,不设门禁。** 从遗留包复制的新包会保留前缀,而所有检查仍会通过。
## 后果
不带前缀包名映射随每个新实验包增长;遗留清单只会缩减。识别不带前缀的包是否为实验包依据的是目录位置和该映射,而不是 npm 名称;不带前缀的名称也不因此获准进入默认产品。
+2 -2
View File
@@ -27,8 +27,8 @@
en: c4bf3103b9762b6c
zh: 369c734bb20de2ea
/deepseek-ai-dsh-session/use-this-package/write-experimental-plugin-records:
en: c5d4328a0700f64b
zh: e3bb55cf8ffaba43
en: 002b8240caa0c57c
zh: e0954dd62913a944
/deepseek-ai-dsh-session/use-this-package/fork-a-session:
en: 1c40d17a786059d3
zh: 81320ce118977c65
+1 -1
View File
@@ -71,7 +71,7 @@ Session log positions use two numeric types. `SessionSeq` identifies an existing
`appendPluginRecord(session, type, data)` appends one plugin record for a package under `packages/experimental/`; the `verify-plugin-record-callers` check rejects any other production caller in this repository. Plugins outside this repository must not call it either, although no repository check can inspect them.
Declare each record in `PluginRecordMap` through `@deepseek-ai/dsh-session/types`, with a description and an explicit payload type annotation in the owning experimental package's `src/`. Names use `plugin:<owner>/<record>`, where `owner` is the package name without `@deepseek-ai/dsh-`, or without `@deepseek-ai/dsh-experimental-` for a legacy prefixed name; for example, `plugin:pi-extensions/entry`. TypeScript checks each name and payload against its declaration; the writer's runtime JSON snapshot rejects values that cannot be preserved losslessly. Put extension-defined or other dynamic names inside the payload of a declared record.
Declare each record in `PluginRecordMap` through `@deepseek-ai/dsh-session/types`, with a description and an explicit payload type annotation in the owning experimental package's `src/`. Names use `plugin:<owner>/<record>`, where `owner` is the package name after removing the legacy `@deepseek-ai/dsh-experimental-` prefix when present, otherwise `@deepseek-ai/dsh-`; for example, `plugin:pi-extensions/entry`. TypeScript checks each name and payload against its declaration; the writer's runtime JSON snapshot rejects values that cannot be preserved losslessly. Put extension-defined or other dynamic names inside the payload of a declared record.
The [experimental persistence catalog](../../../docs/experimental-persistence-catalog.md) lists current plugin-record declarations with their owners, descriptions, payload type annotations, and source files. These declarations remain separate from `SessionEventMap`, expanded persistence schemas, released type history, and `KNOWN_SESSION_EVENT_TYPES`. Records carry `ignorable: true`: a build that does not recognize one retains and skips it on read. Records never enter the model-visible surface. Resume and fork carry them with the rest of the log; Session format migration keeps them on a best-effort basis.
+1 -1
View File
@@ -71,7 +71,7 @@ session.deriveMessages() // the derived model history
`appendPluginRecord(session, type, data)` 为 `packages/experimental/` 下的包追加一条插件记录;`verify-plugin-record-callers` 检查拒绝本仓库中任何其他生产调用方。本仓库之外的插件同样不得调用它,尽管仓库检查无法检查它们。
在所属实验性包的 `src/` 中,通过 `@deepseek-ai/dsh-session/types` 的 `PluginRecordMap` 为每条记录声明描述与显式 payload 类型注解。名称使用 `plugin:<owner>/<record>`,其中 `owner` 是包名去掉 `@deepseek-ai/dsh-` 后的部分,带旧前缀的包名则去掉 `@deepseek-ai/dsh-experimental-`;例如 `plugin:pi-extensions/entry`。TypeScript 按声明检查每个名称与 payload;写入方运行时的 JSON 快照拒绝无法无损保留的值。扩展自定义名称或其他动态名称放在已声明记录的 payload 中。
在所属实验性包的 `src/` 中,通过 `@deepseek-ai/dsh-session/types` 的 `PluginRecordMap` 为每条记录声明描述与显式 payload 类型注解。名称使用 `plugin:<owner>/<record>`,其中 `owner` 是包名去掉前缀后的部分:带旧前缀时去掉 `@deepseek-ai/dsh-experimental-`,否则去掉 `@deepseek-ai/dsh-`;例如 `plugin:pi-extensions/entry`。TypeScript 按声明检查每个名称与 payload;写入方运行时的 JSON 快照拒绝无法无损保留的值。扩展自定义名称或其他动态名称放在已声明记录的 payload 中。
[实验性持久化目录](../../../docs/experimental-persistence-catalog.zh.md)列出当前插件记录声明及其所属包、描述、payload 类型注解与源文件。这些声明与 `SessionEventMap`、展开的持久化 schema、已发布类型历史及 `KNOWN_SESSION_EVENT_TYPES` 分开。记录带有 `ignorable: true`:不认识某条记录的构建在读取时保留并跳过它。记录从不进入模型可见的 surface。resume 与 fork 随日志其余部分一起携带它们;Session 格式迁移以尽力而为的方式保留它们。
+1 -1
View File
@@ -3,7 +3,7 @@
These rules supplement the [package rules](../AGENTS.md). The [experimental reference](README.md#status) defines experimental, optional, and Official status; the rules below define dependency isolation and promotion.
- 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.
- Name a new package `@deepseek-ai/dsh-<name>`, never `@deepseek-ai/dsh-experimental-<name>`, and add its directory to `EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS` in [the experimental naming policy](../../scripts/experimental-package-policy.ts). A name with the `experimental` prefix must change when the package moves to a product group, which breaks every import, dependency entry, and configuration that names it.
- Name a new package `@deepseek-ai/dsh-<name>`, never `@deepseek-ai/dsh-experimental-<name>`, and add its directory to `EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS` in [the experimental naming policy](../../scripts/experimental-package-policy.ts); isolation checks and plugin-record ownership recognize an unprefixed experimental name only through that map. A name with the `experimental` prefix must change when the package moves to a product group, which breaks every import, dependency entry, and configuration that names it. `pnpm run constraints` rejects the prefix outside `LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES`, which must not grow.
- Packages join 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`. [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, or snapshot requirements.
+26 -7
View File
@@ -8,6 +8,7 @@ import {
isPublicExperimentalPackageDirectory,
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES,
} from './experimental-package-policy.ts'
import {
checkDshFamilyVersion,
@@ -22,8 +23,11 @@ import {
type WorkspaceManifest,
} from './check-workspace-constraints.ts'
const legacyManifests = LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES
.map(dir => ({ dir, manifest: { name: dir.replace('packages/experimental/', '@deepseek-ai/dsh-experimental-') } }))
const experimental = {
dir: 'packages/experimental/prototype',
dir: LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES[0]!,
manifest: {
name: '@deepseek-ai/dsh-experimental-prototype',
publishConfig: { access: 'public' },
@@ -133,21 +137,36 @@ describe('experimental workspace constraints', () => {
.toEqual([expect.stringContaining('declared for this directory')])
})
it('rejects a missing manifest name even when the directory has no exception', () => {
expect(checkExperimentalManifest({ ...experimental, manifest: { publishConfig: { access: 'public' } } }))
.toEqual([expect.stringContaining('experimental package name must be declared')])
it('rejects the legacy prefix outside the legacy directory list', () => {
expect(checkExperimentalManifest({ ...experimental, dir: 'packages/experimental/prototype' })).toEqual([
'@deepseek-ai/dsh-experimental-prototype: new experimental packages must not use the "@deepseek-ai/dsh-experimental-" prefix; name it @deepseek-ai/dsh-<name> and declare it in EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS (scripts/experimental-package-policy.ts)',
])
})
it('rejects stale, moved and duplicated retained-name declarations', () => {
it('rejects a missing manifest name', () => {
expect(checkExperimentalManifest({ dir: 'packages/experimental/prototype', manifest: { publishConfig: { access: 'public' } } }))
.toEqual(['packages/experimental/prototype: experimental package must declare a name'])
})
it('rejects stale, moved and duplicated unprefixed-name declarations', () => {
const manifests = Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS).map(([dir, name]) => ({ dir, manifest: { name } }))
expect(checkExperimentalNameExceptions(manifests)).toEqual([])
expect(checkExperimentalNameExceptions([...manifests, ...legacyManifests])).toEqual([])
const [first, ...rest] = manifests
for (const invalid of [
rest,
[{ ...first!, manifest: { name: '@deepseek-ai/dsh-experimental-replacement' } }, ...rest],
[{ ...first!, dir: 'packages/core/replacement' }, ...rest],
[...manifests, { ...first!, dir: 'packages/experimental/duplicate' }],
]) expect(checkExperimentalNameExceptions(invalid)).toEqual([expect.stringContaining(first!.manifest.name)])
]) expect(checkExperimentalNameExceptions([...invalid, ...legacyManifests])).toEqual([expect.stringContaining(first!.manifest.name)])
})
it('rejects a legacy directory without a prefixed package', () => {
const unprefixed = Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS).map(([dir, name]) => ({ dir, manifest: { name } }))
const others = legacyManifests.filter(entry => entry.dir !== experimental.dir)
for (const invalid of [others, [...others, { dir: experimental.dir, manifest: { name: '@deepseek-ai/dsh-renamed' } }]]) {
expect(checkExperimentalNameExceptions([...unprefixed, ...invalid]))
.toEqual([`${experimental.dir}: remove this directory from LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES; no package with the legacy prefix lives here`])
}
})
it.each(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('rejects retained runtime names and aliases: %s', (name) => {
+15 -4
View File
@@ -15,6 +15,7 @@ import {
hasExperimentalPackageReference,
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
EXPERIMENTAL_PACKAGE_NAME_PREFIX,
LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES,
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
} from './experimental-package-policy.ts'
import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
@@ -423,8 +424,13 @@ export function checkExperimentalManifest(
if (!experimentalPackageDirectory.test(dir)) return []
const label = manifest.name ?? dir
const errors: string[] = []
if (manifest.name === undefined || !manifest.name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX)
&& manifest.name !== EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS[dir]) {
if (manifest.name === undefined) {
errors.push(`${label}: experimental package must declare a name`)
} else if (manifest.name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX)) {
if (!LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES.includes(dir)) {
errors.push(`${label}: new experimental packages must not use the ${JSON.stringify(EXPERIMENTAL_PACKAGE_NAME_PREFIX)} prefix; name it @deepseek-ai/dsh-<name> and declare it in EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS (scripts/experimental-package-policy.ts)`)
}
} else if (manifest.name !== EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS[dir]) {
errors.push(`${label}: experimental package name must be declared for this directory in EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS (scripts/experimental-package-policy.ts)`)
}
if (isPublicExperimentalPackageDirectory(dir, privateDirectories)) {
@@ -440,7 +446,7 @@ export function checkExperimentalManifest(
}
/**
* Reject missing, renamed, moved, or duplicated retained-name exceptions.
* Reject missing, renamed, moved, or duplicated unprefixed-name declarations and stale legacy prefixed directories.
* @param manifests - every workspace manifest, with repository-relative directories.
* @returns Violations of the exact directory/name ownership declarations.
*/
@@ -449,7 +455,12 @@ export function checkExperimentalNameExceptions(manifests: readonly WorkspaceMan
for (const [dir, name] of Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS)) {
const owners = manifests.filter(entry => entry.manifest.name === name)
if (owners.length !== 1 || owners[0]?.dir !== dir) {
errors.push(`${dir}: retained experimental name ${name} must have exactly one workspace owner at this directory`)
errors.push(`${dir}: unprefixed experimental name ${name} must have exactly one workspace owner at this directory`)
}
}
for (const dir of LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES) {
if (!manifests.some(entry => entry.dir === dir && entry.manifest.name?.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) === true)) {
errors.push(`${dir}: remove this directory from LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES; no package with the legacy prefix lives here`)
}
}
return errors
+4 -4
View File
@@ -1,4 +1,4 @@
/** Retained package names keep experimental isolation and persisted record namespaces. */
/** Unprefixed and legacy prefixed package names keep experimental isolation and persisted record namespaces. */
import { describe, expect, it } from 'vitest'
import {
@@ -19,7 +19,7 @@ describe('experimental package identities', () => {
}
})
it.each(names)('does not extend the retained-name exception %s to another package', (name) => {
it.each(names)('does not extend the unprefixed name %s to another package', (name) => {
for (const reference of [`${name}-unrelated`, `other-${name}`, name.replace('@deepseek-ai/', '@other/')]) {
expect(isExperimentalPackageName(reference)).toBe(false)
expect(hasExperimentalPackageReference(reference)).toBe(false)
@@ -27,12 +27,12 @@ describe('experimental package identities', () => {
}
})
it('keeps new experimental packages under their prefixed namespaces', () => {
it('derives the record namespace from a legacy prefixed name', () => {
expect(experimentalPackageRecordNamespace('@deepseek-ai/dsh-experimental-bridge')).toBe('bridge')
expect(hasExperimentalPackageReference('@deepseek-ai/dsh-core')).toBe(false)
})
it.each(Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('keeps %s record names independent of the retained npm name', (directory, name) => {
it.each(Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('keeps %s record names independent of the unprefixed npm name', (directory, name) => {
expect(experimentalPackageRecordNamespace(name)).toBe(directory.split('/').at(-1))
})
})
+54 -8
View File
@@ -1,9 +1,9 @@
/** Experimental package identities, publication policy, and retained npm names. */
/** Experimental package identities, publication policy, and npm names. */
/**
* Experimental packages whose npm names omit the experimental prefix, by directory: identities retained across
* experimental classification and package-role moves, and packages named without the prefix so that leaving
* experimental status keeps their names.
* Experimental packages whose npm names omit the experimental prefix, by directory. Every new experimental package
* belongs here so that promotion keeps its name; isolation checks and plugin-record ownership recognize an unprefixed
* experimental name only through this map.
*/
export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, string>> = {
'packages/experimental/cua': '@deepseek-ai/dsh-cua',
@@ -27,17 +27,63 @@ export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, strin
'packages/experimental/personalized-recommendations': '@deepseek-ai/dsh-personalized-recommendations',
} satisfies Record<`packages/experimental/${string}`, `@deepseek-ai/dsh-${string}`>
/** Legacy npm namespace kept by existing experimental packages; new packages must not use it. */
/** Legacy npm namespace, allowed only in `LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES`. */
export const EXPERIMENTAL_PACKAGE_NAME_PREFIX = '@deepseek-ai/dsh-experimental-'
const retainedNames = new Set(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))
/** Existing experimental packages that keep the legacy prefix until a rename; this list must not grow. */
export const LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES: readonly string[] = [
'packages/experimental/api-speech-to-text',
'packages/experimental/auto-review',
'packages/experimental/badge-skill-bundle',
'packages/experimental/browser-use-chrome-devtools-mcp',
'packages/experimental/browser-use-playwright-mcp',
'packages/experimental/browser-use-runtime',
'packages/experimental/browser-use-stagehand-native',
'packages/experimental/claude-code-mods',
'packages/experimental/client-ui-claude-code-mods',
'packages/experimental/client-ui-cot-translation',
'packages/experimental/client-ui-forge',
'packages/experimental/client-ui-stats',
'packages/experimental/client-ui-voice-input',
'packages/experimental/computer-use-cua-driver-mcp',
'packages/experimental/computer-use-cua-driver-native',
'packages/experimental/cot-translation-bundle',
'packages/experimental/cua-macos',
'packages/experimental/forge',
'packages/experimental/forge-bundle',
'packages/experimental/forge-github',
'packages/experimental/forge-session',
'packages/experimental/inspector',
'packages/experimental/inspector-profile',
'packages/experimental/liveshare',
'packages/experimental/liveshare-protocol',
'packages/experimental/plugin-market-bundle',
'packages/experimental/ptc-runtime-python',
'packages/experimental/ralph-bundle',
'packages/experimental/session-inspector',
'packages/experimental/session-search',
'packages/experimental/session-titles-bundle',
'packages/experimental/speech-to-text',
'packages/experimental/speech-to-text-sensevoice',
'packages/experimental/subagent-oar',
'packages/experimental/terminal-bundle',
'packages/experimental/tool-subagent-oar',
'packages/experimental/tool-worktree',
'packages/experimental/translator',
'packages/experimental/voice-input-bundle',
'packages/experimental/webworker-packer',
'packages/experimental/webworker-runtime',
'packages/experimental/worktree',
]
const unprefixedNames = new Set(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))
/**
* Identify an experimental npm package independently of its installed directory.
* @param name - complete npm package name, without a version or subpath.
* @returns Whether the name uses the experimental prefix or a retained name.
* @returns Whether the name uses the legacy prefix or appears in `EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS`.
*/
export function isExperimentalPackageName(name: string): boolean {
return name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) || retainedNames.has(name)
return name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) || unprefixedNames.has(name)
}
/**