DSH / Atlas
2026-08-10implementedprocess

Rescope vendored Cordis into @deepseek-ai

把 vendored Cordis 重命名进 @deepseek-ai scope

The nine packages under `vendor/` kept their upstream npm names (`cordis`, `cosmokit`, `schemastery`, `@cordisjs/plugin-*`). That premise does not survive publication: every harness package declares `cordis` as a peer dependency, so a consumer installing `@deepseek-ai/dsh-*` must resolve it from the registry, which means publishing the harness publishes this framework layer too. Publishing it under the upstream names

English

Problem

The nine packages under vendor/ kept their upstream npm names (cordis, cosmokit, schemastery, @cordisjs/plugin-*). That premise does not survive publication: every harness package declares cordis as a peer dependency, so a consumer installing @deepseek-ai/dsh-* must resolve it from the registry, which means publishing the harness publishes this framework layer too. Publishing it under the upstream names squats them on the registry, and where that registry proxies npmjs, the same-name entries shadow the real upstream packages and install the wrong framework into unrelated projects.

Decision

All nine packages move into the @deepseek-ai scope. Directory names, upstream version numbers, and dependency ranges stay untouched, so the vendor/README.md manifest still reads as an upstream snapshot. docs/rescope.md restates this mapping for consumers.

Directorynpm nameUpstream name
cordis/@deepseek-ai/cordiscordis
cosmokit/@deepseek-ai/cosmokitcosmokit
schemastery/@deepseek-ai/schemasteryschemastery
loader/@deepseek-ai/cordis-plugin-loader@cordisjs/plugin-loader
include/@deepseek-ai/cordis-plugin-include@cordisjs/plugin-include
group/@deepseek-ai/cordis-plugin-group@cordisjs/plugin-group
timer/@deepseek-ai/cordis-plugin-timer@cordisjs/plugin-timer
hmr/@deepseek-ai/cordis-plugin-hmr@cordisjs/plugin-hmr
logger-console/@deepseek-ai/cordis-plugin-logger-console@cordisjs/plugin-logger-console

The rewrite touches only delimited, complete package-name tokens: quoted or backticked specifiers (optionally with a /subpath), package.json names and dependency keys, cordis.yml name: values, and tsconfig.base.json paths keys. Identically spelled strings that are not package names therefore stayed as they were: the cordis.yml config-file family, the Loader's literal cordis: builtin prefix (cordis:include, cordis:group — see vendor/loader/src/config/tree.ts), kind strings like cordis-config-entry, @deepseek-ai/dsh-tool-cordis, Schemastery's upstream Symbol.for('schemastery') and vendor: metadata field, the packages/<group>/ directory names in GROUP_ORDER (scripts/gen-module-graph.ts, scripts/gen-doc-graphs.ts), and the upstream install instructions in vendor/*/README.md.

Two classes are invisible to a token rule and were renamed site by site. First, property access — manifest.peerDependencies?.cordis — where TypeScript cannot catch a stale Record<string, string> key. Second, constants that carry the name as data: the vendored set in check-workspace-constraints.ts, the group/include names in verify-cordis-config.ts, the declare module target strings in cordis-walk.ts, gen-scoped-events.ts, and typert's analyzer.ts, and alwaysBundle in app-boot/tsdown.config.ts.

Markdown splits along what a reader does with it. Every fence follows the rename regardless of its info string, because a fence is code they copy or configuration they mount — the yaml fences naming Loader plugins and the ts ignore-check fences beside compiled ones included. Prose follows it under docs/, where a tutorial sentence quoting a name teaches something this repository no longer resolves. Prose elsewhere — vendor/*/README.md, package READMEs, and .agents/notes/ — keeps the names it was written with, both because it records what was true then and because the same spelling can mean something else: the Python SDK's cordis option, the unvendored @cordisjs/plugin-http, or an agent-preset id.

Consequences

  • No upstream name remains in the publication set. publish-npm-baseline.ts now requires every published package to be @deepseek-ai/* with no vendored exemption, so regressing the rename fails before packing.
  • The vendor/README.md manifest table gains an upstream-name column; gen-third-party-notices parses six columns and renders that name into THIRD_PARTY_NOTICES.md, keeping MIT attribution pointed at each fork's origin rather than our scope.
  • pnpm-workspace.yaml drops the cordis and @cordisjs/plugin-loader minimumReleaseAgeExclude entries, which can no longer be fetched from a registry, and knip.json drops the @cordisjs/.+ ignore pattern that @deepseek-ai/.+ already covers.
  • Upstream sync follows the procedure in vendor/README.md with one added obligation in step 3: re-apply the rename over the copied sources with pnpm run rescope-vendor --apply, whose mapping and the table's two name columns must agree.
  • Returning to the official upstream packages means applying that mapping in reverse — pnpm run rescope-vendor --apply --reverse — then restoring the two minimumReleaseAgeExclude entries and relaxing the publication-set assertion. It spans roughly 1300 files, so replay it with the script rather than by hand.

scripts/rescope-vendor.ts owns the rename: the mapping, the delimited-token rule, the per-file exemptions where a name is a directory instead of a package, the exact edits above, and a --check mode asserting no residue, every exact edit landed, and idempotency, which the hygiene gate runs on every CI pass. A rebase replays it instead of resolving a 1300-file conflict, and an upstream change to one of the pinned sites fails the run loudly instead of being silently skipped.

Alternatives considered

Keep the upstream names and exclude vendor/ from publication. Rejected because every harness package declares cordis as a peer dependency, so an installed @deepseek-ai/dsh-* would have no resolvable framework.

Rename only at pack time. Rejected because the published names would disagree with the source tree, every module specifier would have to be rewritten inside the publish path, and no local run could reproduce what was published.

Rename the vendor/ directories and unify versions on the repository base version too. Rejected because directory names are not publication identity — renaming them drags in project references, tsdown globs, and documentation paths for no gain — and a 0.0.1 version would no longer satisfy the preserved ^4.0.0-rc.7 ranges, so pnpm would look for a registry copy and verify-vendored-links would fail.

Rewrite prose outside docs/ and historical Agent Notes as well. Rejected because those record what was true when written, and a bare cordis there is as likely to be an SDK option name or a preset id as a package; docs/rescope.md carries the mapping for readers instead.

中文

问题

vendor/ 下的九个包此前保留上游 npm 名(cordiscosmokitschemastery@cordisjs/plugin-*)。这个前提在发布时不成立:每个 harness 包都把 cordis 声明成 peer dependency,装了 @deepseek-ai/dsh-* 的消费者必须能从 registry 解析到它,所以发布 harness 必然连带发布这一层框架。用上游名发布就是在 registry 上占用别人的名字;若该 registry 对 npmjs 做上游代理,本名条目还会遮蔽真正的上游包,把错误的框架装进无关项目。

决定

九个包统一改名进 @deepseek-ai scope。目录名、上游版本号、依赖 range 一律不动,所以 vendor/README.md 的清单仍然读作一份上游快照。面向使用者的映射表见 docs/rescope.md

目录npm 名上游名
cordis/@deepseek-ai/cordiscordis
cosmokit/@deepseek-ai/cosmokitcosmokit
schemastery/@deepseek-ai/schemasteryschemastery
loader/@deepseek-ai/cordis-plugin-loader@cordisjs/plugin-loader
include/@deepseek-ai/cordis-plugin-include@cordisjs/plugin-include
group/@deepseek-ai/cordis-plugin-group@cordisjs/plugin-group
timer/@deepseek-ai/cordis-plugin-timer@cordisjs/plugin-timer
hmr/@deepseek-ai/cordis-plugin-hmr@cordisjs/plugin-hmr
logger-console/@deepseek-ai/cordis-plugin-logger-console@cordisjs/plugin-logger-console

改写只落在带定界符的完整包名 token 上:引号或反引号包裹的 specifier(可带 /子路径)、package.jsonname 与依赖键、cordis.ymlname: 值、tsconfig.base.jsonpaths 键。因此以下同形串一律未改,它们不是包名:cordis.yml 及其家族文件名、Loader 的 cordis: 内建前缀(cordis:includecordis:group,见 vendor/loader/src/config/tree.ts)、cordis-config-entry 这类 kind 串、@deepseek-ai/dsh-tool-cordis、Schemastery 上游的 Symbol.for('schemastery')vendor: 元数据、scripts/gen-module-graph.tsgen-doc-graphs.tsGROUP_ORDERpackages/<group>/ 目录名,以及 vendor/*/README.md 里的上游安装指引。

Token 规则看不见两类点位,它们按名字逐处改:一是属性访问 manifest.peerDependencies?.cordis——TypeScript 抓不到过期的 Record<string, string> 键;二是把名字当数据的常量(check-workspace-constraints.ts 的 vendored 集合、verify-cordis-config.ts 的 group/include 名、cordis-walk.tsgen-scoped-events.ts 与 typert analyzer.ts 里识别 declare module 目标的字符串、app-boot/tsdown.config.tsalwaysBundle)。

Markdown 按「读者拿它做什么」一分为二。围栏一律跟着改,不看 info string——围栏里是读者要照抄的代码或要挂载的配置,包括写着 Loader 插件名的 yaml 围栏和紧邻编译围栏的 ts ignore-check 围栏。散文只在 docs/ 下跟着改:教程里引用某个名字的句子,教的是本仓已不解析的东西。docs/ 之外的散文——vendor/*/README.md、各包 README、.agents/notes/——保留写作当时的名字:既因为它记录的是当时的事实,也因为同一个拼写可能指别的东西,比如 Python SDK 的 cordis 选项、我们没 vendor 的 @cordisjs/plugin-http,或某个 agent-preset 的 id。

影响

  • 发布集里不再有任何上游名:publish-npm-baseline.ts 现在无条件要求每个待发包都是 @deepseek-ai/*,vendored 包不再豁免,改名一旦回退就会在打包前失败。
  • vendor/README.md 的清单表新增「上游名」列,gen-third-party-notices 随之解析六列并把上游名渲进 THIRD_PARTY_NOTICES.md;MIT 归属指向 fork 的来源,而不是我们的 scope。
  • pnpm-workspace.yamlminimumReleaseAgeExclude 删去 cordis@cordisjs/plugin-loader 两条:改名后这两个名字永远不从 registry 取。knip.json@cordisjs/.+ 忽略模式同理删除,已被 @deepseek-ai/.+ 覆盖。
  • 上游 sync 照 vendor/README.md 的流程走,第 3 步多一项:对拷进来的源码重跑 pnpm run rescope-vendor --apply,脚本里的映射与清单表两列名字必须一致。
  • 要回到官方上游包时反着跑这份映射——pnpm run rescope-vendor --apply --reverse——再补回 minimumReleaseAgeExclude 两条、放开发布集对 @deepseek-ai/* 的断言。改写量约 1300 个文件,用脚本重放而不是手改。

改名这件事由 scripts/rescope-vendor.ts 承载:映射、带定界符的 token 规则、名字其实是目录而非包时的逐文件豁免、上面那批精确改写,以及一个断言「零残留、每条精确改写都落上、幂等」的 --check 模式——它由 hygiene 门在每次 CI 上执行。rebase 时重放它,而不是去解一个 1300 文件的冲突;上游动了任一被钉住的点位,脚本会响亮失败而不是静默漏改。

考虑过的替代方案

保留上游名,把 vendor/ 排除在发布集之外。 否决:每个 harness 包都声明 cordis 为 peer dependency,装好的 @deepseek-ai/dsh-* 会解析不到框架。

只在打包时改名。 否决:发出去的名字与源码树不一致,所有模块 specifier 得在发布路径里现改,本地也没有任何一次运行能复现发布出去的东西。

目录名与版本号一并改。 否决:目录名不是发布标识,改它会连带项目引用、tsdown glob 与文档路径,收益为零;版本号并入 0.0.1 后不再满足保留下来的 ^4.0.0-rc.7 range,pnpm 会转去 registry 找副本,verify-vendored-links 直接红。

docs/ 之外的散文与历史 Agent Note 一起改。 否决:它们记录的是写作当时的事实,而且那里的裸 cordis 同样可能是 SDK 选项名或某个 preset id,未必是包;面向读者的映射由 docs/rescope.md 承载。