mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
build(vendor): add the @deepseek-ai rescope codemod, its mapping doc, and its Agent Note
Every harness package declares cordis as a peer dependency, so publishing the harness publishes the vendored framework layer too; under the upstream names that publication would squat them on the registry. scripts/rescope-vendor.ts owns the rename: the nine-package mapping, a delimited-token rule that leaves cordis.yml, the Loader's cordis: builtins and vendor directory names alone, per-file exemptions where a name is a directory or an upstream runtime identifier, and the exact edits for sites a token rule cannot express — dot-notation lookups, unquoted manifest keys, a regex literal whose failure would make every Context-merge scan silently find nothing, the vendored-manifest table, and the contracts that told readers vendored packages keep their upstream names. Markdown follows the rename inside every fence, because a fence is code a reader copies or configuration they mount, and in `docs/` prose as well, where a sentence quoting a name teaches something this repository no longer resolves. Prose elsewhere records what was true when it was written, and the same spelling can mean something else: the Python SDK's `cordis` option, or the unvendored `@cordisjs/plugin-http`. `docs/rescope.md` states both names on purpose and is exempt. exactEditState() classifies every exact edit as pending, applied, or invalid. An insertion keeps its anchor and a deletion keeps its remainder, so each side counts the form that survives: a duplicated insertion, a half-applied replacement, and a deletion whose remainder moved are all invalid. The run classifies every edit before writing anything and aborts on the first invalid one, so a disagreement between the mapping and the tree cannot leave a half-rescoped checkout; each write re-reads its file, because two edits can target one. rescope-vendor.spec.ts pins those rejections, and --check asserts the whole post-state from the hygiene gate, so CI owns the invariant. --reverse restores the upstream names, verified as a round trip: reverse, then apply, reproduces this tree byte for byte. docs/rescope.md is the consumer-facing reference: the old-name/new-name table with each package's role, what the rename deliberately leaves alone, the sites callers must change, and the commands to apply, verify, and revert. The Agent Note carries the decision and its consequences. The rename itself lands in the next commit, produced by running the script.
This commit is contained in:
@@ -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-08-10-vendor-package-rescope.md
|
||||
2026-08-10-vendor-package-rescope.md: f2a142cec4e3c28fae54af8063cd730931fa738b
|
||||
2026-08-10-vendor-package-rescope.zh.md: 994064fc869e1ccd95609ec67010eae794ed1c48
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Rescope vendored Cordis into @deepseek-ai
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-10-vendor-package-rescope.zh.md)
|
||||
|
||||
## 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](../../../../docs/rescope.md) restates this mapping for consumers.
|
||||
|
||||
| Directory | npm name | Upstream name |
|
||||
|---|---|---|
|
||||
| `cordis/` | `@deepseek-ai/cordis` | `cordis` |
|
||||
| `cosmokit/` | `@deepseek-ai/cosmokit` | `cosmokit` |
|
||||
| `schemastery/` | `@deepseek-ai/schemastery` | `schemastery` |
|
||||
| `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 and unquoted object keys — `manifest.peerDependencies?.cordis`, and the manifest keys the scaffold generates in `npm-dependency-policy.ts` and `local-plugin-blueprint.ts` — 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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: 把 vendored Cordis 重命名进 @deepseek-ai scope
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-10-vendor-package-rescope.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`vendor/` 下的九个包此前保留上游 npm 名(`cordis`、`cosmokit`、`schemastery`、`@cordisjs/plugin-*`)。这个前提在发布时不成立:每个 harness 包都把 `cordis` 声明成 peer dependency,装了 `@deepseek-ai/dsh-*` 的消费者必须能从 registry 解析到它,所以发布 harness 必然连带发布这一层框架。用上游名发布就是在 registry 上占用别人的名字;若该 registry 对 npmjs 做上游代理,本名条目还会遮蔽真正的上游包,把错误的框架装进无关项目。
|
||||
|
||||
## 决定
|
||||
|
||||
九个包统一改名进 `@deepseek-ai` scope。目录名、上游版本号、依赖 range 一律不动,所以 `vendor/README.md` 的清单仍然读作一份上游快照。面向使用者的映射表见 [docs/rescope.md](../../../../docs/rescope.md)。
|
||||
|
||||
| 目录 | npm 名 | 上游名 |
|
||||
|---|---|---|
|
||||
| `cordis/` | `@deepseek-ai/cordis` | `cordis` |
|
||||
| `cosmokit/` | `@deepseek-ai/cosmokit` | `cosmokit` |
|
||||
| `schemastery/` | `@deepseek-ai/schemastery` | `schemastery` |
|
||||
| `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.json` 的 `name` 与依赖键、`cordis.yml` 的 `name:` 值、`tsconfig.base.json` 的 `paths` 键。因此以下同形串一律未改,它们不是包名:`cordis.yml` 及其家族文件名、Loader 的 `cordis:` 内建前缀(`cordis:include`、`cordis: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.ts` 与 `gen-doc-graphs.ts` 里 `GROUP_ORDER` 的 `packages/<group>/` 目录名,以及 `vendor/*/README.md` 里的上游安装指引。
|
||||
|
||||
Token 规则看不见两类点位,它们按名字逐处改:一是属性访问与未加引号的对象键(`manifest.peerDependencies?.cordis`、脚手架 `npm-dependency-policy.ts` 与 `local-plugin-blueprint.ts` 生成的清单键)——TypeScript 抓不到过期的 `Record<string, string>` 键;二是把名字当数据的常量(`check-workspace-constraints.ts` 的 vendored 集合、`verify-cordis-config.ts` 的 group/include 名、`cordis-walk.ts` 与 `gen-scoped-events.ts` 与 typert `analyzer.ts` 里识别 `declare module` 目标的字符串、`app-boot/tsdown.config.ts` 的 `alwaysBundle`)。
|
||||
|
||||
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.yaml` 的 `minimumReleaseAgeExclude` 删去 `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` 承载。
|
||||
6
docs/rescope.i18n.yaml
Normal file
6
docs/rescope.i18n.yaml
Normal file
@@ -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 docs/rescope.md
|
||||
rescope.md: 3dde39875021e7a4161e1ae66550e9dedf5eb4fa
|
||||
rescope.zh.md: a7f355cf651fb063bf2d4c3cefe18babd4a57401
|
||||
53
docs/rescope.md
Normal file
53
docs/rescope.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Vendored package rescope
|
||||
|
||||
English | [中文](rescope.zh.md)
|
||||
|
||||
The Cordis framework and its foundation libraries are vendored under [`vendor/`](../vendor/README.md) and published under the `@deepseek-ai` scope, because every harness package declares the framework as a peer dependency: publishing the harness publishes this layer with it, and under the upstream names that publication would squat them on the registry. This page is the name mapping; the decision and its consequences live in the [rescope Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md), and the upstream commits in [`vendor/README.md`](../vendor/README.md).
|
||||
|
||||
## Name mapping
|
||||
|
||||
| Directory | Upstream name | Published name | Version | Role |
|
||||
|---|---|---|---|---|
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | Framework core: `Context`, `Service`, `Fiber`, events |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | Disposal-aware timers on `ctx` |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
|
||||
| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | Console logger exporter |
|
||||
|
||||
Subpath exports keep their path: `@cordisjs/plugin-loader/repository` becomes `@deepseek-ai/cordis-plugin-loader/repository`.
|
||||
|
||||
## What the rename does not touch
|
||||
|
||||
- **Directory names and versions.** `vendor/hmr/` stays `vendor/hmr/`, and every package keeps the upstream version its manifest table row records, so the vendored tree still reads as an upstream snapshot.
|
||||
- **Dependency ranges.** A dependency entry changes its key, never its range: `"cordis": "^4.0.0-rc.7"` becomes `"@deepseek-ai/cordis": "^4.0.0-rc.7"`. `linkWorkspacePackages` resolves those preserved ranges to the pinned workspaces.
|
||||
- **The Loader's `cordis:` builtin prefix.** `cordis:include` and `cordis:group` are a protocol prefix, not a package name.
|
||||
- **The `cordis.yml` configuration family**, including `*.cordis.yml`, `*.cordis.snapshot.yml`, and `cordis.patch.yml`.
|
||||
- **Harness packages whose own names contain the word**, such as `@deepseek-ai/dsh-tool-cordis`.
|
||||
- **Upstream runtime identifiers**, such as Schemastery's `Symbol.for('schemastery')` and its `vendor:` metadata field.
|
||||
- **Prose outside `docs/`.** `vendor/*/README.md`, package READMEs, and Agent Notes keep the names they were written with; a bare `cordis` there can also be the Python SDK's option name or an agent-preset id. Inside `docs/`, prose and every Markdown fence follow the rename.
|
||||
|
||||
## What your code has to change
|
||||
|
||||
| Site | Before | After |
|
||||
|---|---|---|
|
||||
| Module import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
|
||||
| Typed-event merge | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
|
||||
| `package.json` dependency key | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
|
||||
| `cordis.yml` plugin entry | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
|
||||
|
||||
## Applying, verifying, and reverting
|
||||
|
||||
[`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) owns the mapping above and performs the rename, so no reference is renamed by hand:
|
||||
|
||||
```sh
|
||||
pnpm run rescope-vendor # report what would change
|
||||
pnpm run rescope-vendor --apply # rewrite every reference
|
||||
pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
|
||||
pnpm run rescope-vendor --apply --reverse # return to the upstream names
|
||||
```
|
||||
|
||||
Re-apply it after an upstream sync ([procedure](../vendor/README.md)), and follow it with the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, and `pnpm run verify-translation-pairing --write` for the bilingual pairs it touched.
|
||||
53
docs/rescope.zh.md
Normal file
53
docs/rescope.zh.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Vendored 包改名
|
||||
|
||||
[English](rescope.md) | 中文
|
||||
|
||||
Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/README.md) 下,并以 `@deepseek-ai` scope 发布:每个 harness 包都把框架声明为 peer dependency,发布 harness 就会连带发布这一层,用上游名发布等于在 registry 上占用别人的名字。本页是名字映射表;决策与影响见 [改名 Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),上游 commit 见 [`vendor/README.md`](../vendor/README.md)。
|
||||
|
||||
## 名字映射
|
||||
|
||||
| 目录 | 上游名 | 发布名 | 版本 | 角色 |
|
||||
|---|---|---|---|---|
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | `ctx` 上随 disposal 回收的定时器 |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
|
||||
| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 控制台日志导出 |
|
||||
|
||||
子路径导出保持原路径:`@cordisjs/plugin-loader/repository` 变成 `@deepseek-ai/cordis-plugin-loader/repository`。
|
||||
|
||||
## 改名不碰什么
|
||||
|
||||
- **目录名与版本号。** `vendor/hmr/` 仍是 `vendor/hmr/`,每个包保留清单表那行记录的上游版本,所以 vendored 树依旧读作一份上游快照。
|
||||
- **依赖 range。** 依赖条目只换键、不换范围:`"cordis": "^4.0.0-rc.7"` 变成 `"@deepseek-ai/cordis": "^4.0.0-rc.7"`;`linkWorkspacePackages` 靠这些保留下来的范围把它们解析到固定的 workspace。
|
||||
- **Loader 的 `cordis:` 内建前缀。** `cordis:include`、`cordis:group` 是协议前缀,不是包名。
|
||||
- **`cordis.yml` 配置文件家族**,包括 `*.cordis.yml`、`*.cordis.snapshot.yml`、`cordis.patch.yml`。
|
||||
- **名字里带这个词的 harness 包**,例如 `@deepseek-ai/dsh-tool-cordis`。
|
||||
- **上游运行时标识符**,例如 Schemastery 的 `Symbol.for('schemastery')` 及其 `vendor:` 元数据字段。
|
||||
- **`docs/` 之外的散文。** `vendor/*/README.md`、各包 README 与 Agent Note 保留写作当时的名字;那里的裸 `cordis` 也可能是 Python SDK 的选项名或某个 agent-preset 的 id。`docs/` 之内,散文与所有 Markdown 围栏都跟着改。
|
||||
|
||||
## 你的代码要改什么
|
||||
|
||||
| 位置 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| 模块 import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
|
||||
| 类型事件声明合并 | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
|
||||
| `package.json` 依赖键 | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
|
||||
| `cordis.yml` 插件条目 | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
|
||||
|
||||
## 施加、核验与回退
|
||||
|
||||
上面这份映射由 [`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) 承载并执行改名,任何引用都不靠手改:
|
||||
|
||||
```sh
|
||||
pnpm run rescope-vendor # report what would change
|
||||
pnpm run rescope-vendor --apply # rewrite every reference
|
||||
pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
|
||||
pnpm run rescope-vendor --apply --reverse # return to the upstream names
|
||||
```
|
||||
|
||||
上游 sync 之后重跑它([流程](../vendor/README.md)),并接上它打印的重生成:`pnpm install` 重生成 lockfile、`pnpm run gen-third-party-notices`、以及对它触及的双语对跑 `pnpm run verify-translation-pairing --write`。
|
||||
@@ -95,6 +95,8 @@
|
||||
"verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
|
||||
"verify-vendored-links": "tsx scripts/verify-vendored-links.ts",
|
||||
"verify-cordis-config": "tsx scripts/verify-cordis-config.ts",
|
||||
"rescope-vendor": "tsx scripts/rescope-vendor.ts",
|
||||
"rescope-vendor:check": "tsx scripts/rescope-vendor.ts --check",
|
||||
"verify-client-domain-graph": "tsx scripts/verify-client-domain-graph.ts",
|
||||
"gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
|
||||
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
|
||||
@@ -117,7 +119,7 @@
|
||||
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||
"doc-sync": "tsx scripts/run-gates.ts doc-sync",
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure && pnpm run verify-vendored-links",
|
||||
"hygiene": "pnpm run rescope-vendor:check && pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure && pnpm run verify-vendored-links",
|
||||
"publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts",
|
||||
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
|
||||
"demo:headless": "node --import tsx/esm apps/cli/src/bin.ts run",
|
||||
|
||||
41
scripts/rescope-vendor.spec.ts
Normal file
41
scripts/rescope-vendor.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Acceptance-path coverage for the rescope codemod's exact-edit classifier: a
|
||||
* duplicated insertion — what a non-idempotent apply produces — must be
|
||||
* rejected rather than applied again.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { exactEditState } from './rescope-vendor.ts'
|
||||
|
||||
const ANCHOR = '\n## Sync procedure'
|
||||
const INSERTED = `\n15. **rescope**: one log entry.\n${ANCHOR}`
|
||||
|
||||
describe('exactEditState', () => {
|
||||
it('classifies an insertion by its target form, so a duplicate is invalid', () => {
|
||||
expect(exactEditState(`log\n${ANCHOR}\n`, ANCHOR, INSERTED, 1)).toBe('pending')
|
||||
expect(exactEditState(`log${INSERTED}\n`, ANCHOR, INSERTED, 1)).toBe('applied')
|
||||
// The anchor survives an insertion, so counting the source form would have
|
||||
// called this pending and inserted the entry a second time.
|
||||
expect(exactEditState(`log${INSERTED}${INSERTED}\n`, ANCHOR, INSERTED, 1)).toBe('invalid')
|
||||
expect(exactEditState('log\n', ANCHOR, INSERTED, 1)).toBe('invalid')
|
||||
})
|
||||
|
||||
it('classifies a deletion by its source form, and requires its remainder to survive', () => {
|
||||
const remainder = 'exclude:\n'
|
||||
const withEntries = 'exclude:\n - cordis@4\n'
|
||||
expect(exactEditState(withEntries, withEntries, remainder, 1)).toBe('pending')
|
||||
expect(exactEditState(remainder, withEntries, remainder, 1)).toBe('applied')
|
||||
// Upstream dropped the whole field: the source form is gone, but so is the
|
||||
// remainder, so this is a moved site rather than a completed deletion.
|
||||
expect(exactEditState('unrelated:\n', withEntries, remainder, 1)).toBe('invalid')
|
||||
})
|
||||
|
||||
it('requires a replacement to leave no source form and the exact target count', () => {
|
||||
expect(exactEditState('a = 1\n', 'a = 1', 'b = 2', 1)).toBe('pending')
|
||||
expect(exactEditState('b = 2\n', 'a = 1', 'b = 2', 1)).toBe('applied')
|
||||
expect(exactEditState('b = 2\nb = 2\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
|
||||
// A moved or partially applied site: neither state is complete.
|
||||
expect(exactEditState('a = 1\nb = 2\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
|
||||
expect(exactEditState('x\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
|
||||
})
|
||||
})
|
||||
771
scripts/rescope-vendor.ts
Normal file
771
scripts/rescope-vendor.ts
Normal file
@@ -0,0 +1,771 @@
|
||||
/**
|
||||
* Rescope the vendored Cordis packages into the `@deepseek-ai` scope, and undo
|
||||
* that rescope with `--reverse`. Every harness package declares `cordis` as a
|
||||
* peer dependency, so publication carries this framework layer too; publishing
|
||||
* it under the upstream names would squat them on the registry
|
||||
* ([rationale](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),
|
||||
* [name mapping](../docs/rescope.md)).
|
||||
*
|
||||
* The generic pass rewrites ONLY delimited, complete package-name tokens:
|
||||
* `'old'` / `"old"` / `` `old` `` / `'old/subpath'`, plus a YAML `name: old`
|
||||
* scalar. A match needs a quote (or `name: `) immediately left and the matching
|
||||
* quote — optionally after a `/subpath` — immediately right, which excludes
|
||||
* `cordis.yml`, the Loader's `cordis:` builtin prefix, `cordis-config-entry`,
|
||||
* `@deepseek-ai/dsh-tool-cordis`, and `cordiverse/cordis`, and makes the
|
||||
* rewrite idempotent because the scoped name's `cordis` is preceded by `/`.
|
||||
* Markdown follows the rename inside every fence, and in `docs/` prose too:
|
||||
* a tutorial that teaches an unresolvable name is wrong, while prose elsewhere
|
||||
* records what was true when it was written.
|
||||
*
|
||||
* Sites the token rule cannot express (dot-notation access, unquoted object
|
||||
* keys, regex literals, the vendored-manifest table) are listed in
|
||||
* {@link EXACT_EDITS} with an exact hit count, so an upstream change to one of
|
||||
* them fails loudly instead of being silently skipped.
|
||||
*
|
||||
* Usage: `pnpm run rescope-vendor [--apply|--check] [--reverse]`. Without a
|
||||
* mode it reports what would change. `--check` asserts the post-state: no
|
||||
* residue, every exact edit landed, every postcondition holds, and a second
|
||||
* `--apply` would be a no-op.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** One vendored package's directory, upstream npm name, and rescoped name. */
|
||||
interface Rename {
|
||||
readonly directory: string
|
||||
readonly upstream: string
|
||||
readonly scoped: string
|
||||
}
|
||||
|
||||
/** The mapping this codemod applies; `vendor/README.md` carries the same table. */
|
||||
const RENAMES: readonly Rename[] = [
|
||||
{ directory: 'cordis', upstream: 'cordis', scoped: '@deepseek-ai/cordis' },
|
||||
{ directory: 'cosmokit', upstream: 'cosmokit', scoped: '@deepseek-ai/cosmokit' },
|
||||
{ directory: 'schemastery', upstream: 'schemastery', scoped: '@deepseek-ai/schemastery' },
|
||||
{ directory: 'loader', upstream: '@cordisjs/plugin-loader', scoped: '@deepseek-ai/cordis-plugin-loader' },
|
||||
{ directory: 'include', upstream: '@cordisjs/plugin-include', scoped: '@deepseek-ai/cordis-plugin-include' },
|
||||
{ directory: 'group', upstream: '@cordisjs/plugin-group', scoped: '@deepseek-ai/cordis-plugin-group' },
|
||||
{ directory: 'timer', upstream: '@cordisjs/plugin-timer', scoped: '@deepseek-ai/cordis-plugin-timer' },
|
||||
{ directory: 'hmr', upstream: '@cordisjs/plugin-hmr', scoped: '@deepseek-ai/cordis-plugin-hmr' },
|
||||
{ directory: 'logger-console', upstream: '@cordisjs/plugin-logger-console', scoped: '@deepseek-ai/cordis-plugin-logger-console' },
|
||||
]
|
||||
|
||||
const EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.tpl', '.json', '.yml', '.yaml', '.md'] as const
|
||||
|
||||
/** An exact-string edit the token rule cannot express, with its required hit count. */
|
||||
interface ExactEdit {
|
||||
readonly id: string
|
||||
readonly file: string
|
||||
readonly find: string
|
||||
readonly replace: string
|
||||
readonly expect: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A file where an upstream name also appears as a vendor DIRECTORY name or an
|
||||
* upstream runtime identifier: the generic pass is disabled for the listed
|
||||
* names and {@link EXACT_EDITS} renames the real package-name occurrences.
|
||||
*/
|
||||
interface GenericSkip {
|
||||
readonly file: string
|
||||
readonly upstream: readonly string[]
|
||||
}
|
||||
|
||||
const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
// `vendorPackages` lists vendor/ directory names, joined with 'vendor' below it.
|
||||
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', upstream: ['cordis', 'cosmokit', 'schemastery'] },
|
||||
// Mixes join(root, 'vendor', 'cordis') paths with real manifest names.
|
||||
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', upstream: ['cordis'] },
|
||||
// `Symbol.for('schemastery')` and the `vendor:` metadata field are upstream identifiers.
|
||||
{ file: 'vendor/schemastery/src/index.ts', upstream: ['schemastery'] },
|
||||
// Asserts the vendored-manifest table, which gains an upstream-name column.
|
||||
{ file: 'scripts/gen-third-party-notices.spec.ts', upstream: RENAMES.map(rename => rename.upstream) },
|
||||
// `cordis` is also an agent-preset id — the directory name under
|
||||
// apps/cli/config/agent-presets/ — so in these files the bare name is
|
||||
// product data, not a package reference. Renaming it changed which preset
|
||||
// the creator flow stages and which id the roster reports.
|
||||
{ file: 'packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/src/client/index.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/apply.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/client/ui-agent-preset/tests/section.spec.tsx', upstream: ['cordis'] },
|
||||
{ file: 'apps/cli/tests/web-agent-presets.e2e.ts', upstream: ['cordis'] },
|
||||
{ file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/preset/agent-presets/tests/session.spec.ts', upstream: ['cordis'] },
|
||||
// The preset's own composition: its header comment and its system prompt name
|
||||
// the preset a model mounts, so the scoped name would send the model after an
|
||||
// id no roster reports.
|
||||
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
|
||||
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
|
||||
{ file: 'scripts/gen-module-graph.ts', upstream: ['cordis'] },
|
||||
{ file: 'scripts/gen-doc-graphs.ts', upstream: ['cordis'] },
|
||||
]
|
||||
|
||||
/** A string that must appear exactly `count` times once the rescope has run. */
|
||||
interface PostCondition {
|
||||
readonly file: string
|
||||
readonly text: string
|
||||
readonly count: number
|
||||
}
|
||||
|
||||
const POSTCONDITIONS: readonly PostCondition[] = [
|
||||
{ file: 'vendor/cordis/package.json', text: '"name": "@deepseek-ai/cordis"', count: 1 },
|
||||
{ file: 'vendor/hmr/package.json', text: '"name": "@deepseek-ai/cordis-plugin-hmr"', count: 1 },
|
||||
{ file: 'scripts/cordis-walk.ts', text: '@deepseek-ai\\/cordis', count: 1 },
|
||||
{ file: 'scripts/cordis-walk.ts', text: '!== \'@deepseek-ai/cordis\'', count: 1 },
|
||||
{ file: 'scripts/gen-scoped-events.ts', text: '=== \'@deepseek-ai/cordis\'', count: 1 },
|
||||
{ file: 'packages/typert/generator/src/analyzer.ts', text: '!== \'@deepseek-ai/cordis\'', count: 2 },
|
||||
{ file: 'scripts/check-workspace-constraints.ts', text: '?.[\'@deepseek-ai/cordis\']', count: 2 },
|
||||
{ file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts', text: '\'@deepseek-ai/cordis\': \'^4.0.0-rc.7\'', count: 1 },
|
||||
{ file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts', text: '\'@deepseek-ai/cordis\': cordisSpec', count: 2 },
|
||||
{ file: 'packages/boot/app-boot/tsdown.config.ts', text: '[\'@deepseek-ai/cordis-plugin-include\']', count: 1 },
|
||||
{ file: 'tsconfig.base.json', text: '"@deepseek-ai/cordis-plugin-loader": ["./vendor/loader/src"]', count: 1 },
|
||||
// One insertion, once: a duplicated log entry is what a non-idempotent apply produced.
|
||||
{ file: 'vendor/README.md', text: '15. **`@deepseek-ai` rescope**', count: 1 },
|
||||
{ file: 'knip.json', text: '@cordisjs', count: 0 },
|
||||
{ file: 'pnpm-workspace.yaml', text: 'cordis@4.0.0-rc.7', count: 0 },
|
||||
// The preset ids in this table are product data, not package names.
|
||||
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', text: '[\'cordis\', \'presetCordisName\'', count: 1 },
|
||||
// The preset id the shipped composition documents to its own model.
|
||||
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
|
||||
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
|
||||
// The vendor-directory paths in these fixtures must survive the rename.
|
||||
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', text: 'join(root, \'vendor\', \'cordis\')', count: 2 },
|
||||
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', text: '\'cordis\', \'loader\', \'include\', \'timer\', \'hmr\', \'logger-console\',', count: 1 },
|
||||
]
|
||||
|
||||
/**
|
||||
* Every exact edit, in application order. Each `find` is written against the
|
||||
* PRE-rename text because these run before the generic pass, so no `find` may
|
||||
* quote a neighbouring line the generic pass would rewrite.
|
||||
*/
|
||||
const EXACT_EDITS: readonly ExactEdit[] = [
|
||||
{
|
||||
id: 'cordis-walk-merge-head',
|
||||
file: 'scripts/cordis-walk.ts',
|
||||
find: 'const MERGE_HEAD = /declare module [\'"](?:cordis|\\.\\/context\\.ts)[\'"]/',
|
||||
replace: 'const MERGE_HEAD = /declare module [\'"](?:@deepseek-ai\\/cordis|\\.\\/context\\.ts)[\'"]/',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'constraints-manifest-lookup',
|
||||
file: 'scripts/check-workspace-constraints.ts',
|
||||
find: ` const peer = manifest.peerDependencies?.cordis
|
||||
const dev = manifest.devDependencies?.cordis
|
||||
|
||||
if (!peer) errors.push(\`\${label}: cordis must be a peerDependency\`)
|
||||
if (!dev) errors.push(\`\${label}: cordis must also be a devDependency\`)
|
||||
if (peer && dev && peer !== dev) {
|
||||
errors.push(\`\${label}: cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
|
||||
replace: ` const peer = manifest.peerDependencies?.['@deepseek-ai/cordis']
|
||||
const dev = manifest.devDependencies?.['@deepseek-ai/cordis']
|
||||
|
||||
if (!peer) errors.push(\`\${label}: @deepseek-ai/cordis must be a peerDependency\`)
|
||||
if (!dev) errors.push(\`\${label}: @deepseek-ai/cordis must also be a devDependency\`)
|
||||
if (peer && dev && peer !== dev) {
|
||||
errors.push(\`\${label}: @deepseek-ai/cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'scaffold-dependency-policy',
|
||||
file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts',
|
||||
find: ' cordis: \'^4.0.0-rc.7\',',
|
||||
replace: ' \'@deepseek-ai/cordis\': \'^4.0.0-rc.7\',',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'scaffold-plugin-blueprint',
|
||||
file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts',
|
||||
find: ` cordis: cordisSpec,
|
||||
},
|
||||
devDependencies: {
|
||||
cordis: cordisSpec,
|
||||
},`,
|
||||
replace: ` '@deepseek-ai/cordis': cordisSpec,
|
||||
},
|
||||
devDependencies: {
|
||||
'@deepseek-ai/cordis': cordisSpec,
|
||||
},`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'scaffold-link-workspace-lookup',
|
||||
file: 'packages/scaffold/create-sdk/tests/link-workspace.e2e.ts',
|
||||
find: 'manifest.dependencies.cordis',
|
||||
replace: 'manifest.dependencies[\'@deepseek-ai/cordis\']',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents-spec-manifest-name',
|
||||
file: 'packages/scaffold/helper/tests/documents.spec.ts',
|
||||
find: 'JSON.stringify({ name: \'cordis\' })',
|
||||
replace: 'JSON.stringify({ name: \'@deepseek-ai/cordis\' })',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents-spec-peer-key',
|
||||
file: 'packages/scaffold/helper/tests/documents.spec.ts',
|
||||
find: 'peerDependencies: { cordis: \'^4\' },',
|
||||
replace: 'peerDependencies: { \'@deepseek-ai/cordis\': \'^4\' },',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents-spec-closure-order',
|
||||
file: 'packages/scaffold/helper/tests/documents.spec.ts',
|
||||
find: ' \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\', \'cordis\',',
|
||||
replace: ' \'@deepseek-ai/cordis\', \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\',',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents-spec-lookups',
|
||||
file: 'packages/scaffold/helper/tests/documents.spec.ts',
|
||||
find: ` expect(manifest.npmDependency('cordis')?.spec).toMatch(/^link:/)
|
||||
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
|
||||
expect(workspace.packageDirectory('cordis')).toBe(join(root, 'vendor', 'cordis'))
|
||||
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('cordis')`,
|
||||
replace: ` expect(manifest.npmDependency('@deepseek-ai/cordis')?.spec).toMatch(/^link:/)
|
||||
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
|
||||
expect(workspace.packageDirectory('@deepseek-ai/cordis')).toBe(join(root, 'vendor', 'cordis'))
|
||||
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('@deepseek-ai/cordis')`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents-spec-policy-lookup',
|
||||
file: 'packages/scaffold/helper/tests/documents.spec.ts',
|
||||
find: ' expect(resolveNpmDependency(\'cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
|
||||
replace: ' expect(resolveNpmDependency(\'@deepseek-ai/cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The rescoped name is already covered by the `@deepseek-ai/.+` pattern beside it.
|
||||
id: 'knip-logger-console',
|
||||
file: 'knip.json',
|
||||
find: ` "ignoreDependencies": [
|
||||
"@cordisjs/plugin-logger-console",
|
||||
"@deepseek-ai/.+"
|
||||
]
|
||||
},
|
||||
"packages/util/home": {`,
|
||||
replace: ` "ignoreDependencies": [
|
||||
"@deepseek-ai/.+"
|
||||
]
|
||||
},
|
||||
"packages/util/home": {`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'knip-bundle-base',
|
||||
file: 'knip.json',
|
||||
find: ` "packages/bundle/base": {
|
||||
"ignoreDependencies": [
|
||||
"@deepseek-ai/.+",
|
||||
"@cordisjs/.+"
|
||||
]`,
|
||||
replace: ` "packages/bundle/base": {
|
||||
"ignoreDependencies": [
|
||||
"@deepseek-ai/.+"
|
||||
]`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// Rescoped packages are never fetched from a registry, so the exclusion is dead config.
|
||||
id: 'pnpm-release-age',
|
||||
file: 'pnpm-workspace.yaml',
|
||||
find: `minimumReleaseAgeExclude:
|
||||
# Cordis release candidates are source-vendored and pinned in vendor/README.md
|
||||
# during the same-day sync that updates package manifests and the lockfile.
|
||||
- '@cordisjs/plugin-loader@1.0.0-rc.5'
|
||||
- cordis@4.0.0-rc.7
|
||||
`,
|
||||
replace: 'minimumReleaseAgeExclude:\n',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'publication-set-scope-assertion',
|
||||
file: 'scripts/publish-npm-baseline.ts',
|
||||
find: ' if (!isVendored && !name.startsWith(\'@deepseek-ai/\')) {',
|
||||
replace: ` // Vendored packages are rescoped too (vendor/README.md), so publication
|
||||
// never carries an upstream name that would squat it on the registry.
|
||||
if (!name.startsWith('@deepseek-ai/')) {`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendor-readme-preamble',
|
||||
file: 'vendor/README.md',
|
||||
find: 'All vendored packages keep their **original npm names** and are marked `private: true` — they are never published from this repo. `pnpm-workspace.yaml#linkWorkspacePackages` makes matching upstream semver ranges resolve these pinned workspaces, including imports from built `lib/`; disabling it substitutes npm copies behind the same names.',
|
||||
replace: 'All vendored packages are **renamed into the `@deepseek-ai` scope** (`cordis` → `@deepseek-ai/cordis`, `@cordisjs/plugin-<x>` → `@deepseek-ai/cordis-plugin-<x>`): every harness package declares `cordis` as a peer dependency, so publishing the harness publishes this framework layer too, and a publication under the upstream names would squat them on the registry. Directory names and upstream version numbers are deliberately unchanged, so the manifest below still reads as an upstream snapshot. `pnpm-workspace.yaml#linkWorkspacePackages` makes those preserved semver ranges resolve these pinned workspaces, including imports from built `lib/`.',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendor-readme-schemastery-note',
|
||||
file: 'vendor/README.md',
|
||||
find: 'whose lazy `require(\'cosmokit\')` can race',
|
||||
replace: 'whose lazy `require(\'@deepseek-ai/cosmokit\')` can race',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendor-readme-table-head',
|
||||
file: 'vendor/README.md',
|
||||
find: '| Directory | npm name | Version | Upstream repo | Commit |\n|---|---|---|---|---|',
|
||||
replace: '| Directory | npm name | Upstream name | Version | Upstream repo | Commit |\n|---|---|---|---|---|---|',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendor-readme-local-modification-log',
|
||||
file: 'vendor/README.md',
|
||||
find: '\n## Sync procedure',
|
||||
replace: '15. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table\'s `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for(\'schemastery\')` and Schemastery\'s `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table\'s two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).\n\n## Sync procedure',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// A plain fence listing the bundle's mounted tree: a bare token, no quotes.
|
||||
id: 'agent-spine-demo-mounted-tree',
|
||||
file: 'packages/examples/agent-spine-demo/README.md',
|
||||
find: '@cordisjs/plugin-timer timer service',
|
||||
replace: '@deepseek-ai/cordis-plugin-timer timer service',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'agent-spine-demo-mounted-tree-zh',
|
||||
file: 'packages/examples/agent-spine-demo/README.zh.md',
|
||||
find: '@cordisjs/plugin-timer timer service',
|
||||
replace: '@deepseek-ai/cordis-plugin-timer timer service',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The root contract claimed vendored packages keep their upstream names.
|
||||
id: 'root-agents-vendored-name-contract',
|
||||
file: 'AGENTS.md',
|
||||
find: 'vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package.',
|
||||
replace: 'vendored packages are rescoped ([mapping](docs/rescope.md)) and `private: true`. `@deepseek-ai/cordis` is a peerDependency (+ dev) of every harness package.',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The client purity gate reads `@deepseek-ai/` as "another plugin package".
|
||||
// The rescope moves the vendored framework and its libraries into that
|
||||
// namespace, where the gate would reject the library imports client
|
||||
// bundles have always inlined, so it needs their names.
|
||||
id: 'client-purity-vendored-libraries',
|
||||
file: 'packages/client/tsdown.client.ts',
|
||||
find: '/** Generated descriptor/codec contribution with no shared runtime identity. */',
|
||||
replace: `/**
|
||||
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
|
||||
* would read them as plugin packages. They carry no cross-plugin runtime
|
||||
* identity to share — the framework itself is a platform module (external),
|
||||
* while these are ordinary libraries a browser bundle inlines.
|
||||
*/
|
||||
const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
|
||||
|
||||
/** Generated descriptor/codec contribution with no shared runtime identity. */`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'client-purity-vendored-libraries-predicate',
|
||||
file: 'packages/client/tsdown.client.ts',
|
||||
find: ' if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point',
|
||||
replace: ` if (VENDORED_LIBRARY.test(source)) return null // vendored library: inline, no shared identity
|
||||
if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The step-1 file tree told the reader to keep the upstream name, one
|
||||
// paragraph above the invariant that says to rescope it.
|
||||
id: 'vendoring-cookbook-tree-comment',
|
||||
file: 'docs/cookbook/adding-a-vendored-package.md',
|
||||
find: ' package.json # from upstream; set "private": true, keep name/exports/type',
|
||||
replace: ' package.json # from upstream; set "private": true, rescope the name, keep exports/type',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendoring-cookbook-tree-comment-zh',
|
||||
file: 'docs/cookbook/adding-a-vendored-package.zh.md',
|
||||
find: ' package.json # from upstream; set "private": true, keep name/exports/type',
|
||||
replace: ' package.json # from upstream; set "private": true, rescope the name, keep exports/type',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The checklist told the next vendoring to keep upstream's name.
|
||||
id: 'vendoring-cookbook-name-invariant',
|
||||
file: 'docs/cookbook/adding-a-vendored-package.md',
|
||||
find: "keep upstream's `name`/`version`/`exports`/`type`",
|
||||
replace: "rescope the `name` ([mapping](../rescope.md)) while keeping upstream's `version`/`exports`/`type`",
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'vendoring-cookbook-name-invariant-zh',
|
||||
file: 'docs/cookbook/adding-a-vendored-package.zh.md',
|
||||
find: '保留上游的 `name`/`version`/`exports`/`type`',
|
||||
replace: '改写 `name` 的 scope([映射](../rescope.md)),保留上游的 `version`/`exports`/`type`',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The real package references in files whose other `cordis` strings are preset ids.
|
||||
id: 'agent-preset-spec-framework-import',
|
||||
file: 'packages/client/ui-agent-preset/tests/apply.spec.ts',
|
||||
find: "import { Context } from 'cordis'",
|
||||
replace: "import { Context } from '@deepseek-ai/cordis'",
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'web-agent-presets-e2e-framework-import',
|
||||
file: 'apps/cli/tests/web-agent-presets.e2e.ts',
|
||||
find: "import { Context } from 'cordis'",
|
||||
replace: "import { Context } from '@deepseek-ai/cordis'",
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-vendored-row-type',
|
||||
file: 'scripts/gen-third-party-notices.ts',
|
||||
find: `export interface VendoredRow {
|
||||
npmName: string
|
||||
upstream: string
|
||||
}`,
|
||||
replace: `export interface VendoredRow {
|
||||
npmName: string
|
||||
/** The name this package carries upstream; MIT attribution names the fork's origin, not our scope. */
|
||||
upstreamName: string
|
||||
upstream: string
|
||||
}`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-vendored-row-parse',
|
||||
file: 'scripts/gen-third-party-notices.ts',
|
||||
find: ` const match = /^\\| \\x60\\S+\\/\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\S+ \\| (https:\\/\\/\\S+?)(?: \\([^)]*\\))? \\| \\x60[0-9a-f]+\\x60 \\|$/.exec(line)
|
||||
if (match === null) continue
|
||||
const [, npmName, upstream] = match
|
||||
if (npmName === undefined || upstream === undefined) continue
|
||||
rows.push({ npmName, upstream })`,
|
||||
replace: ` const match = new RegExp(String.raw\`^\\| \\x60\\S+\\/\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\S+ \\| \`
|
||||
+ String.raw\`(https:\\/\\/\\S+?)(?: \\([^)]*\\))? \\| \\x60[0-9a-f]+\\x60 \\|$\`).exec(line)
|
||||
if (match === null) continue
|
||||
const [, npmName, upstreamName, upstream] = match
|
||||
if (npmName === undefined || upstreamName === undefined || upstream === undefined) continue
|
||||
rows.push({ npmName, upstreamName, upstream })`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-vendored-section',
|
||||
file: 'scripts/gen-third-party-notices.ts',
|
||||
find: 'The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed',
|
||||
replace: 'The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm, and republished under the \\`@deepseek-ai\\` scope. All are MIT-licensed',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-vendored-table',
|
||||
file: 'scripts/gen-third-party-notices.ts',
|
||||
find: `| Package | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
\${vendored.map(row => \`| \\\`\${row.npmName}\\\` | [\${row.upstream.replace('https://', '')}](\${row.upstream}) | MIT |\`).join('\\n')}`,
|
||||
replace: `| Package | Upstream name | Upstream | License |
|
||||
| --- | --- | --- | --- |
|
||||
\${vendored.map(row => \`| \\\`\${row.npmName}\\\` | \\\`\${row.upstreamName}\\\` | [\${row.upstream.replace('https://', '')}](\${row.upstream}) | MIT |\`).join('\\n')}`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-spec-row-fixture',
|
||||
file: 'scripts/gen-third-party-notices.spec.ts',
|
||||
find: ' expect(rows).toContainEqual({ npmName: \'cordis\', upstream: \'https://github.com/cordiverse/cordis\' })',
|
||||
replace: ` expect(rows).toContainEqual({
|
||||
npmName: '@deepseek-ai/cordis',
|
||||
upstreamName: 'cordis',
|
||||
upstream: 'https://github.com/cordiverse/cordis',
|
||||
})`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'notices-spec-shape-fixture',
|
||||
file: 'scripts/gen-third-party-notices.spec.ts',
|
||||
find: 'parseVendoredRows(\'| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')',
|
||||
replace: 'parseVendoredRows(\'| `cordis/` | `@deepseek-ai/cordis` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')',
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
// The framework peer is no longer a registry name, so the rehearsal must install this
|
||||
// repository's vendored copies; cosmokit comes along as cordis's own dependency.
|
||||
id: 'packed-install-vendored-peer',
|
||||
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
|
||||
find: ` 'packages/support/invariants',
|
||||
]`,
|
||||
replace: ` 'packages/support/invariants',
|
||||
// The framework and the vendored packages the closure declares outright:
|
||||
// rescoped into @deepseek-ai, so the consumer installs this repository's
|
||||
// copies. Schemastery is a hard dependency of three members above, not a
|
||||
// peer, so npm resolves it while installing them.
|
||||
'vendor/cordis',
|
||||
'vendor/cosmokit',
|
||||
'vendor/schemastery',
|
||||
]`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'packed-install-registry-spec',
|
||||
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
|
||||
find: ` // Peer ranges resolve to the tarballs; Cordis is pinned to their peer range. Do not omit optional
|
||||
// dependencies because the launcher selects its OS/CPU package through one.
|
||||
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
|
||||
const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.7'], {`,
|
||||
replace: ` // Peer ranges resolve to the tarballs, the framework peer included. Do not omit optional
|
||||
// dependencies because the launcher selects its OS/CPU package through one.
|
||||
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
|
||||
const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs], {`,
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
id: 'packed-install-module-doc',
|
||||
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
|
||||
find: ` * Keyless publish-path rehearsal. It packs the provider, its workspace peers, and the current
|
||||
* repository's Landlock entry/platform packages, then installs those exact tarballs in an external
|
||||
* plain-Node consumer. The host launcher comes from the exact local tarballs, so no registry copy,
|
||||
* tsx, path mapping, or workspace resolution can hide missing files, dependency errors, or lost
|
||||
* executable modes.`,
|
||||
replace: ` * Keyless publish-path rehearsal. It packs the provider, its workspace peers, the vendored framework
|
||||
* peer, and the current repository's Landlock entry/platform packages, then installs those exact
|
||||
* tarballs in an external plain-Node consumer. The host launcher comes from the exact local tarballs,
|
||||
* so no registry copy, tsx, path mapping, or workspace resolution can hide missing files, dependency
|
||||
* errors, or lost executable modes.`,
|
||||
expect: 1,
|
||||
},
|
||||
// The manifest table's name column plus the new upstream-name column, one edit per row.
|
||||
...RENAMES.map(rename => ({
|
||||
id: `vendor-readme-row-${rename.directory}`,
|
||||
file: 'vendor/README.md',
|
||||
find: `| \`${rename.directory}/\` | \`${rename.upstream}\` | `,
|
||||
replace: `| \`${rename.directory}/\` | \`${rename.scoped}\` | \`${rename.upstream}\` | `,
|
||||
expect: 1,
|
||||
})),
|
||||
]
|
||||
|
||||
/** Files the rescope must never rewrite. */
|
||||
function excluded(file: string): boolean {
|
||||
if (file === 'scripts/rescope-vendor.ts') return true // the mapping itself
|
||||
if (file.startsWith('.agents/notes/')) return true // notes record what was true when written
|
||||
// Recorded model payloads quote documentation verbatim, so they must mirror the
|
||||
// sources on disk — including the notes this rescope leaves alone.
|
||||
if (file.startsWith('scripts/snapshots/')) return true
|
||||
// The mapping documents state both names on purpose.
|
||||
if (file === 'docs/rescope.md' || file === 'docs/rescope.zh.md') return true
|
||||
if (file.endsWith('.i18n.yaml')) return true // blob-hash records, re-recorded by the pairing gate
|
||||
if (file === 'pnpm-lock.yaml') return true // regenerated by pnpm install
|
||||
if (/^vendor\/[^/]+\/(README\.md|LICENSE)$/.test(file)) return true // upstream files kept verbatim
|
||||
return !EXTENSIONS.some(extension => file.endsWith(extension))
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/** One name's rewrite, precompiled for both delimited forms. */
|
||||
interface Pattern {
|
||||
readonly upstream: string
|
||||
readonly from: string
|
||||
readonly to: string
|
||||
readonly token: RegExp
|
||||
readonly yamlName: RegExp
|
||||
}
|
||||
|
||||
function patterns(reverse: boolean): Pattern[] {
|
||||
return RENAMES
|
||||
.map(rename => ({
|
||||
upstream: rename.upstream,
|
||||
from: reverse ? rename.scoped : rename.upstream,
|
||||
to: reverse ? rename.upstream : rename.scoped,
|
||||
}))
|
||||
.sort((left, right) => right.from.length - left.from.length)
|
||||
.map(rename => ({
|
||||
...rename,
|
||||
token: new RegExp(`(['"\`])${escapeRegExp(rename.from)}((?:/[^'"\`\\s]*)?)\\1`, 'g'),
|
||||
yamlName: new RegExp(`^(\\s*(?:-\\s*)?name:[ \\t]+)${escapeRegExp(rename.from)}([ \\t]*(?:#.*)?)$`, 'gm'),
|
||||
}))
|
||||
}
|
||||
|
||||
function skipped(file: string, pattern: Pattern): boolean {
|
||||
return GENERIC_SKIPS.some(skip => skip.file === file && skip.upstream.includes(pattern.upstream))
|
||||
}
|
||||
|
||||
function rewriteLine(line: string, file: string, all: readonly Pattern[]): string {
|
||||
let out = line
|
||||
for (const pattern of all) {
|
||||
if (skipped(file, pattern)) continue
|
||||
out = out.replace(pattern.token, (_match, quote: string, subpath: string) => `${quote}${pattern.to}${subpath}${quote}`)
|
||||
out = out.replace(pattern.yamlName, (_match, prefix: string, suffix: string) => `${prefix}${pattern.to}${suffix}`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a file's eligible lines.
|
||||
*
|
||||
* Markdown splits in two. Every fence is code a reader copies or a
|
||||
* configuration they mount, so every fence follows the rename regardless of its
|
||||
* info string. Prose follows it only under `docs/`, where a sentence quoting
|
||||
* `` `cordis` `` teaches a name this repository no longer resolves; elsewhere
|
||||
* prose is a record of what was true when it was written, and the same spelling
|
||||
* can mean something else entirely — the Python SDK's `cordis` option, or the
|
||||
* unvendored `@cordisjs/plugin-http`.
|
||||
*/
|
||||
function rewrite(text: string, file: string, all: readonly Pattern[]): { text: string; lines: number } {
|
||||
const markdown = file.endsWith('.md')
|
||||
const prose = markdown && file.startsWith('docs/')
|
||||
let insideFence = false
|
||||
let lines = 0
|
||||
const out = text.split('\n').map((line) => {
|
||||
if (markdown) {
|
||||
if (/^\s*```/.test(line)) {
|
||||
insideFence = !insideFence
|
||||
return line
|
||||
}
|
||||
if (!insideFence && !prose) return line
|
||||
}
|
||||
const next = rewriteLine(line, file, all)
|
||||
if (next !== line) lines += 1
|
||||
return next
|
||||
})
|
||||
return { text: out.join('\n'), lines }
|
||||
}
|
||||
|
||||
function classify(file: string): string {
|
||||
if (/^vendor\/[^/]+\/package\.json$/.test(file)) return 'vendor manifest name'
|
||||
if (file.endsWith('package.json')) return 'package.json dependencies'
|
||||
if (/\.(ts|tsx|js|mjs|cjs|tpl)$/.test(file)) return 'code specifiers'
|
||||
if (/\.(yml|yaml)$/.test(file)) return 'YAML plugin names'
|
||||
if (file.endsWith('.json')) return 'JSON configuration'
|
||||
return 'Markdown fences and docs prose'
|
||||
}
|
||||
|
||||
/**
|
||||
* One exact edit's state in the text it targets. `pending` means the source
|
||||
* form is present and the target form absent; `applied` means the reverse;
|
||||
* anything else — a partial application, a moved site, or a DUPLICATED
|
||||
* insertion — is `invalid`, so it fails the run instead of being applied again.
|
||||
*/
|
||||
export type ExactEditState = 'pending' | 'applied' | 'invalid'
|
||||
|
||||
/**
|
||||
* Classify one exact edit against its target text.
|
||||
*
|
||||
* An insertion keeps its anchor (`replace` contains `find`) and a deletion
|
||||
* keeps its remainder (`find` contains `replace`), so neither can be judged by
|
||||
* the source form alone: the surviving side counts the target form instead.
|
||||
* @param text - the complete current text of the edited file.
|
||||
* @param find - the source form, already oriented for the running direction.
|
||||
* @param replace - the target form, already oriented for the running direction.
|
||||
* @param expect - how many occurrences one complete application produces.
|
||||
* @returns Whether the edit is pending, already applied, or invalid.
|
||||
*/
|
||||
export function exactEditState(text: string, find: string, replace: string, expect: number): ExactEditState {
|
||||
const hits = text.split(find).length - 1
|
||||
const landed = text.split(replace).length - 1
|
||||
if (replace.includes(find)) {
|
||||
if (landed === expect) return 'applied'
|
||||
return landed === 0 && hits === expect ? 'pending' : 'invalid'
|
||||
}
|
||||
if (find.includes(replace)) {
|
||||
if (hits === 0) return landed === expect ? 'applied' : 'invalid'
|
||||
return hits === expect ? 'pending' : 'invalid'
|
||||
}
|
||||
if (hits === 0 && landed === expect) return 'applied'
|
||||
return hits === expect && landed === 0 ? 'pending' : 'invalid'
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = process.argv.slice(2)
|
||||
const mode = args.includes('--apply') ? 'apply' : args.includes('--check') ? 'check' : 'dry'
|
||||
const reverse = args.includes('--reverse')
|
||||
const all = patterns(reverse)
|
||||
const files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' })
|
||||
.split('\0')
|
||||
.filter(file => file !== '' && !excluded(file))
|
||||
|
||||
const counts = new Map<string, { files: number; lines: number }>()
|
||||
const failures: string[] = []
|
||||
const outstanding: string[] = []
|
||||
|
||||
// Classify every exact edit before writing anything: a single invalid site
|
||||
// means the mapping and the tree disagree, and a half-applied tree is worse
|
||||
// than an untouched one.
|
||||
const planned: { edit: ExactEdit; path: string; find: string; replace: string }[] = []
|
||||
for (const edit of EXACT_EDITS) {
|
||||
const path = resolve(root, edit.file)
|
||||
const before = readFileSync(path, 'utf8')
|
||||
const find = reverse ? edit.replace : edit.find
|
||||
const replace = reverse ? edit.find : edit.replace
|
||||
const state = exactEditState(before, find, replace, edit.expect)
|
||||
if (state === 'invalid') {
|
||||
failures.push(`exact edit ${edit.id}: ${edit.file} is neither pending nor cleanly applied (duplicated, partial, or moved)`)
|
||||
continue
|
||||
}
|
||||
if (mode === 'check') {
|
||||
if (state !== 'applied') failures.push(`exact edit ${edit.id} did not land in ${edit.file}`)
|
||||
continue
|
||||
}
|
||||
if (state === 'pending') planned.push({ edit, path, find, replace })
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
for (const failure of failures) console.error(`rescope-vendor: ${failure}`)
|
||||
console.error(`rescope-vendor: ${String(failures.length)} problem(s); nothing was written.`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (mode === 'apply') {
|
||||
// Re-read per edit: two edits can target one file, and a stale snapshot
|
||||
// would let the second write discard the first.
|
||||
for (const { path, find, replace } of planned) {
|
||||
writeFileSync(path, readFileSync(path, 'utf8').split(find).join(replace))
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const path = resolve(root, file)
|
||||
const before = readFileSync(path, 'utf8')
|
||||
const { text: after, lines } = rewrite(before, file, all)
|
||||
if (after === before) continue
|
||||
outstanding.push(file)
|
||||
const kind = classify(file)
|
||||
const current = counts.get(kind) ?? { files: 0, lines: 0 }
|
||||
counts.set(kind, { files: current.files + 1, lines: current.lines + lines })
|
||||
if (mode === 'apply') writeFileSync(path, after)
|
||||
}
|
||||
|
||||
console.log(`rescope-vendor: ${mode}${reverse ? ' --reverse' : ''} over ${String(files.length)} tracked files`)
|
||||
for (const kind of [...counts.keys()].sort()) {
|
||||
const { files: count, lines } = counts.get(kind) ?? { files: 0, lines: 0 }
|
||||
console.log(` ${kind.padEnd(24)} ${String(count).padStart(4)} file(s), ${String(lines)} line(s)`)
|
||||
}
|
||||
|
||||
if (mode !== 'dry') {
|
||||
for (const check of POSTCONDITIONS) {
|
||||
if (reverse) break
|
||||
const path = resolve(root, check.file)
|
||||
const hits = existsSync(path) ? readFileSync(path, 'utf8').split(check.text).length - 1 : -1
|
||||
if (hits !== check.count) {
|
||||
failures.push(`postcondition: ${check.file} has ${String(hits)} occurrence(s) of ${JSON.stringify(check.text)}, expected ${String(check.count)}`)
|
||||
}
|
||||
}
|
||||
// The generic pass above already told us which files would still change,
|
||||
// which in check mode is exactly the residue-and-idempotency signal.
|
||||
if (mode === 'check') {
|
||||
for (const file of outstanding) failures.push(`residue: ${file} still carries a pre-rescope name token`)
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
for (const failure of failures) console.error(`rescope-vendor: ${failure}`)
|
||||
console.error(`rescope-vendor: ${String(failures.length)} problem(s); the mapping or an upstream site moved.`)
|
||||
process.exitCode = 1
|
||||
} else if (mode === 'check') {
|
||||
console.log('rescope-vendor: post-state verified — no residue, every exact edit landed, idempotent.')
|
||||
} else if (mode === 'apply') {
|
||||
console.log('rescope-vendor: applied. Run `pnpm install`, `pnpm run gen-third-party-notices`, and re-record the touched bilingual pairs.')
|
||||
}
|
||||
}
|
||||
|
||||
// Importing this module for its exported classifier must not run the codemod.
|
||||
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
||||
main()
|
||||
}
|
||||
@@ -553,6 +553,7 @@ function flagEnabled(envName: string): boolean {
|
||||
function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
const artifactOptions = options.artifactNeeds === undefined ? {} : { needs: options.artifactNeeds }
|
||||
return [
|
||||
pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('publint', 'publint', artifactOptions),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
|
||||
Reference in New Issue
Block a user