feat: classify Host dependency exports

This commit is contained in:
imccyu 2026-08-26 19:11:24 +08:00
parent c55beac34a
commit 943a544899
4 changed files with 98 additions and 8 deletions

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-26-published-dependency-faces.md
2026-08-26-published-dependency-faces.md: 04b02afe372b2f3d90729f17e837b40ff1e0e6fa
2026-08-26-published-dependency-faces.zh.md: 468fb443e2e94bf23f8c61201f43c2a82f7f509b
2026-08-26-published-dependency-faces.md: a4c745e8bc811027791317b334d285274f24dd20
2026-08-26-published-dependency-faces.zh.md: 73b8dd28f40a63ea2a13544be535be7832138ba2

View file

@ -30,11 +30,40 @@ Workspace imports used by the Client bundle, type-only imports, module augmentat
The verifier reads source manifests and source files, so it runs on a clean tree without built `lib/`. An unclassified Host runtime export is a policy violation that blocks all `--fix` writes; a maintainer must review the export and classify it, change the source relationship, or change the package selection. Once source safety passes, `--fix` performs only the section and range changes implied by the classification and removes stale peer metadata.
### Maintainer workflow
Run the verifier without `--fix` for a read-only check of package selection, export classifications, dependency sections, workspace ranges, and peer metadata. An unclassified runtime import reports one clickable `path:line:column` diagnostic per imported export.
```sh
pnpm run verify-package-dependencies
```
Classify each new Host runtime export in [`package-dependency-policy.ts`](../../../../scripts/package-dependency-policy.ts) before generating manifests. `safeHostDependencyExports` permits an ordinary dependency; `peerRequiredHostExports` keeps the whole provider package edge in matching peer and development sections. An export may appear in exactly one table. After refactoring a peer-required export so duplicate package copies are safe, move that exact specifier and export to the safe table; an edge becomes an ordinary dependency only after none of its imported exports remain peer-required.
Generate managed manifest sections, refresh the lockfile separately, and review both results. `--fix` writes nothing while a policy violation exists; after success it prints the ordinary-dependency and peer-required edge lists, but does not update the lockfile.
```sh
pnpm run verify-package-dependencies -- --fix
pnpm install --lockfile-only
git diff -- packages pnpm-lock.yaml
```
Measure the working-tree graph and a Git ref through the local metadata-only registry. Each run creates a fresh consumer and npm cache, executes `npm install --package-lock-only`, rejects archive downloads, and leaves the repository unchanged. `--runs` controls repetitions, `--timeout-ms` bounds each run, and optional `--max-ms` makes the command fail when the slowest run exceeds a threshold.
```sh
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
pnpm run benchmark:npm-resolution -- --ref=origin/master --runs=5 --timeout-ms=300000
```
Rank the next Host package by applying the current policy in memory, measuring a baseline, trying each reachable unconfigured package, and serially retesting the fastest coarse candidates. Positive `gainSeconds` is `baseline median - candidate median`; `--candidates` limits the roster, `--jobs` controls coarse concurrency, and neither phase writes manifests. A selected candidate still requires export classification before it joins `hostPackages`.
```sh
pnpm run benchmark:npm-resolution:next -- --runs=1 --finalist-runs=5 --finalists=5 --jobs=8 --timeout-ms=120000
```
### Performance verification
[`benchmark-next-package-dependency`](../../../../scripts/benchmark-next-package-dependency.ts) applies the current policy to an in-memory local registry, measures the current CLI graph, and tries every reachable unconfigured Host package one at a time. Concurrent runs provide a coarse shortlist; finalists run serially because npm's peer-placement search can take different paths when metadata request completion order changes.
The benchmark is manual rather than a CI gate. It performs metadata-only installs in fresh consumers, so its relative results identify peer relays without measuring registry latency or archive downloads.
[`benchmark-npm-resolution`](../../../../scripts/benchmark-npm-resolution.ts) and [`benchmark-next-package-dependency`](../../../../scripts/benchmark-next-package-dependency.ts) remain manual rather than CI gates because resolver time varies with machine load and metadata completion order. Their fresh-consumer, metadata-only runs isolate npm's dependency-tree calculation from registry latency and archive downloads, so relative results identify peer relays without creating a release-time performance promise.
## Alternatives considered

View file

@ -30,11 +30,40 @@ Client bundle 使用的 workspace import、纯类型 import、模块扩充、`ds
验证器读取源码 manifest 和源码文件,因此可以在没有已构建 `lib/` 的干净工作树上运行。未分类的 Host 运行期导出属于策略违规,会阻止 `--fix` 的全部写入;维护者必须审查该导出,并选择分类该导出、修改源码关系或修改选包范围。源码安全检查通过后,`--fix` 只执行分类所确定的区段与范围变更,并删除失效的 peer 元数据。
### 维护流程
不带 `--fix` 运行验证器,会以只读方式检查选包范围、导出分类、依赖区段、workspace range 与 peer metadata。未分类的运行期 import 会按每个导出分别报告可点击的 `path:line:column` 诊断。
```sh
pnpm run verify-package-dependencies
```
生成 manifest 前,在 [`package-dependency-policy.ts`](../../../../scripts/package-dependency-policy.ts) 中分类每个新增 Host 运行期导出。`safeHostDependencyExports` 允许普通 dependency;`peerRequiredHostExports` 让整个提供包依赖边保留在范围一致的 peer 与开发区段。一个导出只能出现在一个表中。把 peer-required 导出重构到重复安装安全后,将该精确 specifier 与导出移入 safe 表;只有当一条依赖边的所有 import 都不再使用 peer-required 导出时,它才会成为普通 dependency。
生成受管 manifest 区段后,单独刷新 lockfile,并审查两部分结果。存在策略违规时,`--fix` 不写任何文件;成功后,它会分别打印普通 dependency 与 peer-required 依赖边,但不会更新 lockfile。
```sh
pnpm run verify-package-dependencies -- --fix
pnpm install --lockfile-only
git diff -- packages pnpm-lock.yaml
```
通过仅 metadata 的本地 registry 测量工作树依赖图与 Git ref。每轮都会创建全新 consumer 与 npm cache,执行 `npm install --package-lock-only`,拒绝下载包归档,并保持仓库不变。`--runs` 控制重复次数,`--timeout-ms` 限制单轮耗时,可选 `--max-ms` 会在最慢一轮超过阈值时让命令失败。
```sh
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
pnpm run benchmark:npm-resolution -- --ref=origin/master --runs=5 --timeout-ms=300000
```
计算下一项 Host 包时,命令会在内存中应用当前策略、测量 baseline、逐个尝试可达且未配置的包,并串行复测粗筛中最快的候选。正数 `gainSeconds` 等于 `baseline median - candidate median`;`--candidates` 限定名册,`--jobs` 控制粗筛并发度,两个阶段都不写 manifest。选中的候选仍需先完成导出分类,才能加入 `hostPackages`。
```sh
pnpm run benchmark:npm-resolution:next -- --runs=1 --finalist-runs=5 --finalists=5 --jobs=8 --timeout-ms=120000
```
### 性能验证
[`benchmark-next-package-dependency`](../../../../scripts/benchmark-next-package-dependency.ts) 把当前策略应用到内存中的本地 registry,测量当前 CLI 依赖图,并逐个尝试每个可达且未配置的 Host 包。并发运行用于得到粗筛名单;由于 metadata 请求的完成顺序会让 npm 的 peer 放置搜索走不同路径,最终候选会串行复测。
Benchmark 是手动诊断工具而非 CI 门禁。它在全新 consumer 中执行仅 metadata 的安装,因此相对结果可以定位 peer 中继,但不测量 registry 延迟或包归档下载。
[`benchmark-npm-resolution`](../../../../scripts/benchmark-npm-resolution.ts) 与 [`benchmark-next-package-dependency`](../../../../scripts/benchmark-next-package-dependency.ts) 保持为手动工具而非 CI 门禁,因为 resolver 耗时会随机器负载和 metadata 完成顺序变化。它们通过全新 consumer 和仅 metadata 的运行,把 npm 依赖树计算与 registry 延迟、包归档下载分离,因此相对结果可以定位 peer 中继,但不构成发布时性能承诺。
## 考虑过的替代方案

View file

@ -6,6 +6,7 @@ const CLIENT_FACE_INCLUDE: readonly string[] = []
/** Packages exempted from automatic Client/Host treatment despite declaring `dsh.client`. */
const CLIENT_FACE_EXCLUDE: readonly string[] = [
'@deepseek-ai/dsh-api-session-controller',
'@deepseek-ai/dsh-api-workspace-controller',
]
/** Host-only packages whose peer relays are deliberately flattened. */
@ -14,11 +15,40 @@ const HOST_DEPENDENCY_PACKAGES: readonly string[] = [
'@deepseek-ai/dsh-session',
]
/**
* Runtime exports whose values remain valid when npm installs another package copy.
*/
const SAFE_HOST_DEPENDENCY_EXPORTS = {
'@deepseek-ai/dsh-api-session-controller/remote-events': ['SESSION_CONTROLLER_REMOTE_EVENTS'],
'@deepseek-ai/dsh-credentials': ['credentialKey'],
'@deepseek-ai/dsh-host-apiproxy': ['toFetchHandler'],
'@deepseek-ai/dsh-host-apiproxy/api': ['RpcId', 'clientRequestSchema'],
'@deepseek-ai/dsh-llm': ['MessageId', 'callConfigEquals', 'deepFreeze', 'freezeMessage'],
'@deepseek-ai/dsh-llm/brand': ['ToolCallId'],
'@deepseek-ai/dsh-session': ['isJsonValue'],
'@deepseek-ai/dsh-settings': ['settingsNamespace'],
'@deepseek-ai/dsh-system-prompt': ['FIRST_PARTY_SECTION_ORDER'],
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
'@deepseek-ai/dsh-util-crypto': ['randomUUID'],
'@deepseek-ai/schemastery': ['default'],
} as const satisfies HostDependencyExports
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
const PEER_REQUIRED_HOST_EXPORTS = {
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'],
'@deepseek-ai/dsh-typert-protocol': ['TypertLookupFailure', 'TypertRemoteFailure', 'remoteMethods'],
} as const satisfies HostDependencyExports
/** Exact import specifier to reviewed runtime exports. */
type HostDependencyExports = Readonly<Record<string, readonly string[]>>
/** Complete configurable input to package dependency classification. */
export interface PackageDependencyPolicy {
readonly clientFaceInclude: readonly string[]
readonly clientFaceExclude: readonly string[]
readonly hostPackages: readonly string[]
readonly safeHostDependencyExports: HostDependencyExports
readonly peerRequiredHostExports: HostDependencyExports
}
/** Repository dependency policy consumed by verification and benchmarking. */
@ -26,6 +56,8 @@ export const PACKAGE_DEPENDENCY_POLICY: PackageDependencyPolicy = {
clientFaceInclude: CLIENT_FACE_INCLUDE,
clientFaceExclude: CLIENT_FACE_EXCLUDE,
hostPackages: HOST_DEPENDENCY_PACKAGES,
safeHostDependencyExports: SAFE_HOST_DEPENDENCY_EXPORTS,
peerRequiredHostExports: PEER_REQUIRED_HOST_EXPORTS,
}
function isRecord(value: unknown): value is Record<string, unknown> {