Merge pull request #948 from deepseek-harness/docs/third-party-notices
docs: add THIRD_PARTY_NOTICES.md disclosing third-party dependencies
This commit is contained in:
16 files changed
+1148
-10
No files matched your search
@@ -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-07-30-generated-third-party-notices.md
|
||||
2026-07-30-generated-third-party-notices.md: e480954d29d5dc09ef8ecd4069059a1f0c8b1043
|
||||
2026-07-30-generated-third-party-notices.zh.md: 78ba7250e797c57048078d1b4f62b7a9a5d9d561
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent Note: Generated third-party notices
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-30-generated-third-party-notices.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Open-sourcing this repository requires disclosing the third-party software it depends on, with each project's license. The disclosure has to be complete, has to stay true as dependencies change, and has to say something a reader can act on — which of these packages end up on a user's machine, and which only build and test the repository.
|
||||
|
||||
A hand-written inventory answers none of those durably. Roughly a hundred rows of names and license strings derived from manifests drift silently the moment a package is added, removed, or relicensed, and nothing would notice.
|
||||
|
||||
## Decision
|
||||
|
||||
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, and `pnpm-workspace.yaml`. The root README pair links the file from its License section.
|
||||
|
||||
**Freshness is maintained, not merely enforced.** A pre-commit job regenerates the file and stages it whenever a generator input is staged — any manifest, a workspace declaration, the root lock file, `vendor/README.md`, a `pyproject.toml`, the generator itself, or the script holding the build-time pin — so an unrelated dependency edit never has to come back and rerun a generator. The committed bytes are then asserted inside [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts), which the test lane already runs — the check adds no gate process, no scheduler slot, and no separate CI step. `pnpm run verify-third-party-notices` remains available for a standalone check.
|
||||
|
||||
One trigger gap is accepted rather than worked around: lefthook inspects only files present on disk, so **deleting** a manifest runs no job, and removing a package reaches the assertion in the test lane instead. Reconstructing the staged file list to include deletions was tried and does not work — lefthook filters the list against the working tree either way. The assertion is the backstop for exactly this case.
|
||||
|
||||
The file discloses **direct** dependencies only. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy.
|
||||
|
||||
**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the `bin/dsh` launcher execs through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly).
|
||||
|
||||
The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. `scripts/install.sh` installs the repository itself, so a user's `cordis.yml` can mount any plugin package; `@modelcontextprotocol/sdk` and the OpenTelemetry packages reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice.
|
||||
|
||||
The manifest set is derived from the `packages:` members each `pnpm-workspace.yaml` declares — the root one and the nested Landlock workspace's — so a new member area is read the day it is declared rather than the day someone remembers to extend a list. License and repository metadata come from the installed pnpm stores, both the root one and the Landlock workspace's, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. `OVERRIDES` carries the packages whose published manifest cannot answer — Rust-built npm bins that omit `license`, and the `modelcontextprotocol/servers` packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. A runtime dependency whose license is not on the permissive list is a hard error: shipping copyleft is a distribution decision, not something a regenerated table may absorb silently. Vendored packages are cross-checked against `vendor/README.md` and rejected if any is not MIT, and `pnpm-workspace.yaml`'s `patchedDependencies` are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of `@earendil-works/pi-tui` and `node-pty`, and the patch files are the record of what changed.
|
||||
|
||||
## Testing
|
||||
|
||||
The same spec that asserts freshness pins the tiering rule against fixture manifests — including the two cases that motivate it, a `dependencies` entry of a test-support package and a plugin package no app mounts. It also pins the parsers against the shapes that would otherwise drop a package without a word: a `vendor/README.md` table that stops covering a vendored directory, a requirement array holding extras (`"httpx[http2]"`), a requirement with no version at all, an author-named `[dependency-groups]` table, and a workspace member area absent from any hardcoded list. Each of those is a silent-omission path, which is the failure mode a disclosure file cannot afford.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the hand-written file and review it at release time.** Reviewing a hundred derived rows by eye is exactly the work a generator does correctly, and the file's own claim — that it lists every direct dependency — would be unverified between releases.
|
||||
|
||||
**Verify through a dedicated `doc-sync` gate.** That is how every other generated artifact here is checked, and it was the first shape of this change. It costs a gate process and a scheduler slot in a matrix that is already long, and — worse — its only failure mode is telling a contributor, minutes after they pushed an unrelated dependency bump, to go rerun a generator. Regenerating at commit time removes the interruption, and the assertion inside a spec the test lane already runs keeps the guarantee at no additional CI cost.
|
||||
|
||||
**Enumerate the full transitive closure.** The closure is thousands of packages, already recorded in the lock files with exact versions, and would bury the direct dependencies that a reader actually evaluates. The file points at the lock files and the `pnpm licenses list` renderer instead.
|
||||
|
||||
**Tier by manifest section (`dependencies` vs `devDependencies`).** Mechanically simple and wrong on real data in both directions, as the tiering paragraph above records.
|
||||
|
||||
**Tier by reachability from the shipped assemblies only** (`apps/*` plus `python/sdk-runtime`). This produces a tighter runtime tier, but classifies the MCP client and the OpenTelemetry exporter as development-only even though a user running the installed repository can mount them. It understates the disclosure, which is the wrong direction to err for a legal notice.
|
||||
|
||||
**Emit the notices as a bilingual pair.** Every other root document is paired, but the file is a table of upstream package names, SPDX identifiers, and URLs; the translatable surface is a handful of section blurbs. `scripts/translation-pairing.ts` scopes discovery to `README*`, `.agents/notes/**`, `docs/**`, and `python/**`, so a root non-README file is outside the bilingual corpus by construction, and the README pair carries the bilingual entry points into it.
|
||||
|
||||
## Consequences
|
||||
|
||||
A dependency edit now carries a regenerated notices file into the same commit. Contributors pay one generator run — about a second — on commits that touch a manifest, and nothing on any other commit. Committing with hooks disabled defers the cost to a test-lane failure that names the command.
|
||||
|
||||
The generator needs an installed tree, which makes it heavier than a pure-source generator, and a new package with unusable published metadata needs an `OVERRIDES` entry rather than silently rendering a blank license. Both failures are loud and name the remedy.
|
||||
|
||||
The tiering rule is a policy encoded in one constant. Adding a workspace area that never ships — a second test-infrastructure tier, another site — requires extending `DEV_ONLY_AREAS`, or its dependencies will be disclosed as runtime.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent Note: Generated third-party notices
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-30-generated-third-party-notices.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
本仓库开源需要披露所依赖的第三方软件及各自的许可证。这份披露必须完整,必须随依赖变化保持为真,还必须给出读者用得上的信息:哪些包最终会进到用户机器上,哪些只用于构建和测试。
|
||||
|
||||
手写清单无法长期满足其中任何一条。约一百行从各清单文件推导出来的包名与许可证标识,只要有依赖新增、移除或换用许可证就会悄悄失真,而没有任何检查会察觉。
|
||||
|
||||
## Decision
|
||||
|
||||
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区清单、`vendor/README.md`、`pyproject.toml` 与 `pnpm-workspace.yaml` 生成。根 README 双语两侧都从「许可证」一节链到该文件。
|
||||
|
||||
**新鲜度靠维护而非拦截。** 只要暂存了生成器的任一输入——任何清单文件、工作区声明、根锁文件、`vendor/README.md`、某个 `pyproject.toml`、生成器自身,或持有构建期 pin 的脚本——pre-commit 任务就会重新生成并一并入库,改依赖的人不必事后再折返跑一次生成器。已提交的字节随后由 [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) 断言,而测试 lane 本就会跑这个文件——这项校验不增加门禁进程、不占调度位、也不新增 CI 步骤。需要单独校验时,`pnpm run verify-third-party-notices` 仍然可用。
|
||||
|
||||
有一处触发缺口是接受而非绕过的:lefthook 只检视磁盘上存在的文件,因此**删除**清单文件不会触发任何任务,移除一个包会落到测试 lane 的断言上。重构暂存文件列表以纳入删除的做法试过,不成立——无论怎么给列表,lefthook 都会拿工作树过滤一遍。这个场景正由断言兜底。
|
||||
|
||||
文件只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。
|
||||
|
||||
**分层依据是声明方所在区域,而非清单字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一清单——即根清单、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而 `bin/dsh` 启动器 exec 经过的 `tsx`,根本没有任何清单把它声明为运行时依赖,只能由生成器显式标记。
|
||||
|
||||
运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。`scripts/install.sh` 安装的就是仓库本身,用户的 `cordis.yml` 可以挂载任何插件包;`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。
|
||||
|
||||
清单集合由两个 `pnpm-workspace.yaml`——根工作区与嵌套的 Landlock 工作区——各自声明的 `packages:` 成员派生,因此新增成员区域在声明当天就会被读取,而不必等谁想起来去补一份列表。许可证与仓库地址取自已安装的 pnpm store,根 store 与 Landlock 工作区的 store 都会查;某个包两处都解析不到时直接失败,而不是留下空单元格。`OVERRIDES` 收录已发布清单答不上来的包:用 Rust 构建、发布时省略 `license` 字段的 npm 可执行包,以及 `modelcontextprotocol/servers` 系列——该仓库正处在 MIT 向 Apache-2.0 的重新许可过程中,实际条款按贡献逐条而定。运行时依赖的许可证若不在宽松清单内即为硬失败:交付 copyleft 是一项分发决策,不该被一次重新生成悄悄吸收。被源码收编的包会与 `vendor/README.md` 交叉核对,出现非 MIT 即报错;`pnpm-workspace.yaml` 的 `patchedDependencies` 列在运行时表格之后,因为 pnpm 在安装期就会打上这些补丁——交付产物携带的是改动过的 `@earendil-works/pi-tui` 与 `node-pty`,补丁文件本身就是改动的完整记录。
|
||||
|
||||
## Testing
|
||||
|
||||
断言新鲜度的同一个 spec 也用夹具清单钉住分层规则,覆盖促成该规则的两个场景:测试支撑包的 `dependencies` 条目,以及没有任何应用挂载的插件包。它还把各解析器钉在那些原本会让某个包无声消失的形态上:不再覆盖全部收编目录的 `vendor/README.md` 表、含 extras 的依赖数组(`"httpx[http2]"`)、完全不带版本的依赖、作者自取名字的 `[dependency-groups]` 表,以及任何硬编码列表都不含的工作区成员区域。这些都是静默漏报路径——正是披露文件最担不起的失败方式。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留手写文件,发版时人工过一遍。** 用肉眼审阅上百行推导数据,恰恰是生成器能做对的活;而且在两次发版之间,文件自称「列出全部直接依赖」这句话无人验证。
|
||||
|
||||
**用专门的 `doc-sync` 门禁校验。** 仓库里其他生成产物都是这么把关的,本次改动最初也是这个形态。但它要在本已冗长的矩阵里再占一个门禁进程和一个调度位;更糟的是,它唯一的失败方式,就是在别人推完一个无关的依赖升级几分钟后,通知对方回去重跑一次生成器。改为提交时重新生成消除了这次打断,而把断言放进测试 lane 本就会跑的 spec 里,则以零额外 CI 成本保住了这项保证。
|
||||
|
||||
**列出完整传递闭包。** 闭包有数千个包,锁文件里已带精确版本,铺开只会淹没读者真正要评估的直接依赖。文件转而指向锁文件与 `pnpm licenses list`。
|
||||
|
||||
**按清单字段分层(`dependencies` 与 `devDependencies`)。** 机械上最省事,但在真实数据上两个方向都会出错,理由见上文分层段落。
|
||||
|
||||
**只按已交付装配的可达性分层**(`apps/*` 加 `python/sdk-runtime`)。这样得到的运行时层更紧凑,但会把 MCP 客户端与 OpenTelemetry 导出器判为仅开发用途——而运行已安装仓库的用户完全可以挂载它们。这会低估披露,对法务通告来说错在了更危险的一侧。
|
||||
|
||||
**把披露文件做成双语对。** 其他根文档都是成对的,但这份文件是上游包名、SPDX 标识与网址构成的表格,可翻译的只有寥寥几段章节导语。`scripts/translation-pairing.ts` 的发现范围限定在 `README*`、`.agents/notes/**`、`docs/**` 与 `python/**`,根目录下的非 README 文件在构造上就不属于双语语料;双语入口由 README 对承担。
|
||||
|
||||
## Consequences
|
||||
|
||||
此后改动依赖时,重新生成的披露文件会随同一个提交入库。触及清单文件的提交多付一次生成器运行——约一秒;其余提交不受影响。若禁用钩子提交,代价推迟为一次测试 lane 失败,其报错会指明补救命令。
|
||||
|
||||
生成器需要已安装的工作树,因此比纯源码生成器更重;发布元数据不可用的新包需要补一条 `OVERRIDES`,而不是默默渲染出空白许可证。这两类失败都会明确报错并指出补救方式。
|
||||
|
||||
分层规则是编码在一个常量里的政策。若新增了不参与交付的工作区区域——第二层测试基础设施、另一个站点——就要同步扩展 `DEV_ONLY_AREAS`,否则其依赖会被当作运行时依赖披露出去。
|
||||
+2
-2
@@ -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 README.md
|
||||
README.md: 08ae0b3d5d2d7ad8e7cb62bd5e8b3242426735dc
|
||||
README.zh.md: 9587215b6b17250504877b8930fdf431fd24b2ac
|
||||
README.md: b447c9634189353854e8be9d0bf597a8b0c7e371
|
||||
README.zh.md: f8bbbc36bc670403c0b9a40977f32f598e77ee46
|
||||
@@ -94,3 +94,5 @@ DeepSeek Harness is currently in internal testing.
|
||||
## License
|
||||
|
||||
[BSD 3-Clause](LICENSE)
|
||||
|
||||
Third-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
@@ -98,3 +98,5 @@ DeepSeek Harness 目前处于内测阶段。
|
||||
## 许可证
|
||||
|
||||
[BSD 3-Clause](LICENSE)
|
||||
|
||||
第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。
|
||||
@@ -0,0 +1,165 @@
|
||||
<!-- Generated by scripts/gen-third-party-notices.ts — do not edit by hand.
|
||||
Run `pnpm run gen-third-party-notices` to regenerate. -->
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
|
||||
The complete npm transitive closure, with exact pinned versions, is recorded in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python closure is recorded in [`python/sdk/uv.lock`](python/sdk/uv.lock), and the Landlock launcher workspace keeps its own in [`native/landlock-run/pnpm-lock.yaml`](native/landlock-run/pnpm-lock.yaml).
|
||||
|
||||
## Vendored source (`vendor/`)
|
||||
|
||||
The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed; each directory preserves its upstream `LICENSE` file. Exact upstream commits and local modifications are recorded in [`vendor/README.md`](vendor/README.md).
|
||||
|
||||
| Package | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
| `cosmokit` | [github.com/deepseek-harness/cosmokit](https://github.com/deepseek-harness/cosmokit) | MIT |
|
||||
| `schemastery` | [github.com/deepseek-harness/schemastery](https://github.com/deepseek-harness/schemastery) | MIT |
|
||||
| `cordis` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
|
||||
| `@cordisjs/plugin-loader` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
|
||||
| `@cordisjs/plugin-include` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
|
||||
| `@cordisjs/plugin-group` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
|
||||
| `@cordisjs/plugin-timer` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
|
||||
| `@cordisjs/plugin-hmr` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
|
||||
| `@cordisjs/plugin-logger-console` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
|
||||
|
||||
## Runtime npm dependencies
|
||||
|
||||
External packages that a workspace package resolves at runtime. `scripts/install.sh` installs this repository itself, so the tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI/TUI, the Web UI, and the Python SDK runtime load by default.
|
||||
|
||||
| Package | License |
|
||||
| --- | --- |
|
||||
| [`@agentclientprotocol/sdk`](https://github.com/agentclientprotocol/typescript-sdk) | Apache-2.0 |
|
||||
| [`@babel/code-frame`](https://github.com/babel/babel) | MIT |
|
||||
| [`@clack/core`](https://github.com/bombshell-dev/clack) | MIT |
|
||||
| [`@clack/prompts`](https://github.com/bombshell-dev/clack) | MIT |
|
||||
| [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT |
|
||||
| [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi) | MIT |
|
||||
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
|
||||
| [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
|
||||
| [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/otlp-exporter-base`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/resources`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/sdk-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
|
||||
| [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
|
||||
| [`anser`](https://github.com/IonicaBizau/anser) | MIT |
|
||||
| [`chokidar`](https://github.com/paulmillr/chokidar) | MIT |
|
||||
| [`clsx`](https://github.com/lukeed/clsx) | MIT |
|
||||
| [`commander`](https://github.com/tj/commander.js) | MIT |
|
||||
| [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause |
|
||||
| [`dotenv`](https://github.com/motdotla/dotenv) | BSD-2-Clause |
|
||||
| [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
|
||||
| [`handlebars`](https://github.com/handlebars-lang/handlebars.js) | MIT |
|
||||
| [`immer`](https://github.com/immerjs/immer) | MIT |
|
||||
| [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT |
|
||||
| [`jsonc-parser`](https://github.com/microsoft/node-jsonc-parser) | MIT |
|
||||
| [`koffi`](https://github.com/Koromix/koffi) | MIT |
|
||||
| [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT |
|
||||
| [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT |
|
||||
| [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT |
|
||||
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
|
||||
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
|
||||
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
|
||||
| [`react`](https://github.com/facebook/react) | MIT |
|
||||
| [`react-dom`](https://github.com/facebook/react) | MIT |
|
||||
| [`react-markdown`](https://github.com/remarkjs/react-markdown) | MIT |
|
||||
| [`remark-gfm`](https://github.com/remarkjs/remark-gfm) | MIT |
|
||||
| [`saxes`](https://github.com/lddubeau/saxes) | ISC |
|
||||
| [`shiki`](https://github.com/shikijs/shiki) | MIT |
|
||||
| [`supports-color`](https://github.com/chalk/supports-color) | MIT |
|
||||
| [`tsx`](https://github.com/privatenumber/tsx) | MIT |
|
||||
| [`turndown`](https://github.com/mixmark-io/turndown) | MIT |
|
||||
| [`typescript`](https://github.com/microsoft/TypeScript) | Apache-2.0 |
|
||||
| [`use-sync-external-store`](https://github.com/facebook/react) | MIT |
|
||||
| [`yaml`](https://github.com/eemeli/yaml) | ISC |
|
||||
| [`zod`](https://github.com/colinhacks/zod) | MIT |
|
||||
| [`zustand`](https://github.com/pmndrs/zustand) | MIT |
|
||||
|
||||
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
|
||||
|
||||
- `@earendil-works/pi-tui@0.80.7` — [`patches/@earendil-works__pi-tui@0.80.7.patch`](patches/@earendil-works__pi-tui@0.80.7.patch)
|
||||
- `node-pty@1.1.0` — [`patches/node-pty@1.1.0.patch`](patches/node-pty@1.1.0.patch)
|
||||
|
||||
## Development-only npm dependencies
|
||||
|
||||
External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
|
||||
|
||||
| Package | License |
|
||||
| --- | --- |
|
||||
| [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
|
||||
| [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT |
|
||||
| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
|
||||
| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
|
||||
| [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/react-dom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/spdx-expression-parse`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/turndown`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint) | MIT |
|
||||
| [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react) | MIT |
|
||||
| [`@vitest/coverage-v8`](https://github.com/vitest-dev/vitest) | MIT |
|
||||
| [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT |
|
||||
| [`@yarnpkg/cli-dist`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
|
||||
| [`cytoscape`](https://github.com/cytoscape/cytoscape.js) | MIT |
|
||||
| [`cytoscape-cose-bilkent`](https://github.com/cytoscape/cytoscape.js-cose-bilkent) | MIT |
|
||||
| [`dayjs`](https://github.com/iamkun/dayjs) | MIT |
|
||||
| [`debug`](https://github.com/debug-js/debug) | MIT |
|
||||
| [`esbuild`](https://github.com/evanw/esbuild) | MIT |
|
||||
| [`eslint`](https://github.com/eslint/eslint) | MIT |
|
||||
| [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only |
|
||||
| [`execa`](https://github.com/sindresorhus/execa) | MIT |
|
||||
| [`fast-check`](https://github.com/dubzzz/fast-check) | MIT |
|
||||
| [`jscpd`](https://github.com/kucherenko/jscpd) | MIT |
|
||||
| [`jsdom`](https://github.com/jsdom/jsdom) | MIT |
|
||||
| [`knip`](https://github.com/webpro-nl/knip) | ISC |
|
||||
| [`lefthook`](https://github.com/evilmartians/lefthook) | MIT |
|
||||
| [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 |
|
||||
| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
|
||||
| [`oxlint`](https://github.com/oxc-project/oxc) | MIT |
|
||||
| [`oxlint-tsgolint`](https://github.com/oxc-project/tsgolint) | MIT |
|
||||
| [`playwright`](https://github.com/microsoft/playwright) | Apache-2.0 |
|
||||
| [`publint`](https://github.com/publint/publint) | MIT |
|
||||
| [`smol-toml`](https://github.com/squirrelchat/smol-toml) | BSD-3-Clause |
|
||||
| [`spdx-expression-parse`](https://github.com/jslicense/spdx-expression-parse.js) | MIT |
|
||||
| [`tsdown`](https://github.com/rolldown/tsdown) | MIT |
|
||||
| [`typescript-language-server`](https://github.com/typescript-language-server/typescript-language-server) | Apache-2.0 |
|
||||
| [`vite`](https://github.com/vitejs/vite) | MIT |
|
||||
| [`vite-tsconfig-paths`](https://github.com/aleclarson/vite-tsconfig-paths) | MIT |
|
||||
| [`vitepress`](https://github.com/vuejs/vitepress) | MIT |
|
||||
| [`vitepress-plugin-mermaid`](https://github.com/emersonbottero/vitepress-plugin-mermaid) | MIT |
|
||||
| [`vitest`](https://github.com/vitest-dev/vitest) | MIT |
|
||||
|
||||
`eslint-plugin-sonarjs` (LGPL-3.0-only) and `lightningcss` (MPL-2.0) run only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact.
|
||||
|
||||
## Python SDK dependencies (`python/`)
|
||||
|
||||
Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the development workflow tool.
|
||||
|
||||
| Package | License | Role |
|
||||
| --- | --- | --- |
|
||||
| [`hatchling`](https://github.com/pypa/hatch) | MIT | build backend |
|
||||
| [`pydantic`](https://github.com/pydantic/pydantic) | MIT | runtime dependency of `deepseek-harness` |
|
||||
| [`pytest`](https://github.com/pytest-dev/pytest) | MIT | test-only |
|
||||
| [`uv`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
|
||||
|
||||
## Fetched at build time
|
||||
|
||||
| Package | License | Role |
|
||||
| --- | --- | --- |
|
||||
| [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg) | MIT | invoked by `scripts/build-exe-for-python-sdk.ts` to assemble the single-file SDK runtime executable |
|
||||
|
||||
## First-party sibling releases
|
||||
|
||||
`node-addon-landlock-run` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
|
||||
@@ -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 docs/development.md
|
||||
development.md: f58cad7d361def14667fa66017cb003b74d70749
|
||||
development.zh.md: 88ddd8483c234bdf1c1fd0bcda9df3ca02ea6fa4
|
||||
development.md: 22eb7915f621883a84688d70e2ccad2fee2dbbba
|
||||
development.zh.md: 480cd323d4d2325974f472c734edab9ce7459e89
|
||||
+1
-1
@@ -83,7 +83,7 @@ DEEPSEEK_BASE_URL=https://... # optional
|
||||
|
||||
lefthook is configured in `lefthook.yml` as a fast local checkpoint:
|
||||
|
||||
- `pre-commit` applies formatting-only ESLint fixes, validates the staged files with Oxlint and applies its native fixes, checks the staged diff for whitespace errors, and runs the vendor manifest guard.
|
||||
- `pre-commit` applies formatting-only ESLint fixes, validates the staged files with Oxlint and applies its native fixes, regenerates `THIRD_PARTY_NOTICES.md` when a staged file is one of its inputs, checks the staged diff for whitespace errors, and runs the vendor manifest guard.
|
||||
- `pre-push` runs only the incremental repository typecheck (`tsc -b` over the root solution, covering both the host and client aggregates).
|
||||
|
||||
The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.
|
||||
|
||||
@@ -83,7 +83,7 @@ DEEPSEEK_BASE_URL=https://... # optional
|
||||
|
||||
lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:
|
||||
|
||||
- `pre-commit` 应用仅用于格式化的 ESLint 修复,使用 Oxlint 验证暂存文件并应用其原生修复,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;
|
||||
- `pre-commit` 应用仅用于格式化的 ESLint 修复,使用 Oxlint 验证暂存文件并应用其原生修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;
|
||||
- `pre-push` 只运行仓库增量类型检查(对根 solution 执行 `tsc -b`,覆盖 host 与 client 两个聚合)。
|
||||
|
||||
vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。
|
||||
|
||||
@@ -18,6 +18,16 @@ pre-commit:
|
||||
run: node_modules/.bin/tsx scripts/run-oxlint.ts --fix --no-error-on-unmatched-pattern {staged_files}
|
||||
stage_fixed: true
|
||||
|
||||
# Regenerate rather than reject: a dependency edit that forgot the notices
|
||||
# would otherwise fail the test lane long after the commit. The glob matches
|
||||
# every input the generator reads, including the generator itself and the
|
||||
# build-time pin source. Deleting a manifest cannot trigger this job —
|
||||
# lefthook only inspects files present on disk — so that one case still
|
||||
# falls through to the freshness assertion in the test lane.
|
||||
- name: third-party notices (staged)
|
||||
glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/build-exe-for-python-sdk.ts}'
|
||||
run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md
|
||||
|
||||
- name: whitespace (staged)
|
||||
run: git diff --cached --check
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
"verify-doc-graphs": "tsx scripts/gen-doc-graphs.ts --check",
|
||||
"gen-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts",
|
||||
"verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check",
|
||||
"gen-third-party-notices": "tsx scripts/gen-third-party-notices.ts",
|
||||
"verify-third-party-notices": "tsx scripts/gen-third-party-notices.ts --check",
|
||||
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
|
||||
"gen-scoped-events": "tsx scripts/gen-scoped-events.ts",
|
||||
"verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check",
|
||||
@@ -120,6 +122,7 @@
|
||||
"@types/jsdom": "^28.0.3",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.20.0",
|
||||
"@types/spdx-expression-parse": "^4.0.0",
|
||||
"@typescript-eslint/parser": "8.61.0",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"@yarnpkg/cli-dist": "4.17.1",
|
||||
@@ -140,6 +143,8 @@
|
||||
"oxlint": "1.76.0",
|
||||
"oxlint-tsgolint": "7.0.2001",
|
||||
"publint": "^0.3.21",
|
||||
"smol-toml": "^1.7.1",
|
||||
"spdx-expression-parse": "^5.0.0",
|
||||
"tsdown": "^0.22.2",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
Generated
+38
@@ -39,6 +39,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.20.0
|
||||
version: 22.20.0
|
||||
'@types/spdx-expression-parse':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
@@ -99,6 +102,12 @@ importers:
|
||||
publint:
|
||||
specifier: ^0.3.21
|
||||
version: 0.3.21
|
||||
smol-toml:
|
||||
specifier: ^1.7.1
|
||||
version: 1.7.1
|
||||
spdx-expression-parse:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
tsdown:
|
||||
specifier: ^0.22.2
|
||||
version: 0.22.2(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3)
|
||||
@@ -8804,6 +8813,9 @@ packages:
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
'@types/spdx-expression-parse@4.0.0':
|
||||
resolution: {integrity: sha512-odQzy87phelGS4inXOzjmusx4hoCVD0IbxUANxHzVkmTzMRTNnUPoq1urIl7S1qf09KcDWKLFIftPmLtgbsAHA==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
@@ -11151,6 +11163,10 @@ packages:
|
||||
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
smol-toml@1.7.1:
|
||||
resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -11162,6 +11178,15 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
spdx-exceptions@2.5.0:
|
||||
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
|
||||
|
||||
spdx-expression-parse@5.0.0:
|
||||
resolution: {integrity: sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==}
|
||||
|
||||
spdx-license-ids@3.0.23:
|
||||
resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -13788,6 +13813,8 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/spdx-expression-parse@4.0.0': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
@@ -16654,12 +16681,23 @@ snapshots:
|
||||
|
||||
smol-toml@1.6.1: {}
|
||||
|
||||
smol-toml@1.7.1: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
spdx-exceptions@2.5.0: {}
|
||||
|
||||
spdx-expression-parse@5.0.0:
|
||||
dependencies:
|
||||
spdx-exceptions: 2.5.0
|
||||
spdx-license-ids: 3.0.23
|
||||
|
||||
spdx-license-ids@3.0.23: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps } from './gen-third-party-notices.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
describe('THIRD_PARTY_NOTICES.md', () => {
|
||||
// Freshness lives here rather than in its own doc-sync gate: this spec file
|
||||
// already runs in the test lane, so the check costs no extra CI process.
|
||||
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
|
||||
// this assertion means the notices were committed without that hook.
|
||||
it('matches what the generator produces from the current manifests', () => {
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(render())
|
||||
})
|
||||
})
|
||||
|
||||
/** Build the (manifests, names) pair `tierExternalDeps` consumes. */
|
||||
function workspace(entries: Record<string, Manifest>): { manifests: Map<string, Manifest>; names: Set<string> } {
|
||||
const manifests = new Map(Object.entries(entries))
|
||||
const names = new Set<string>()
|
||||
for (const manifest of manifests.values()) {
|
||||
if (manifest.name !== undefined) names.add(manifest.name)
|
||||
}
|
||||
return { manifests, names }
|
||||
}
|
||||
|
||||
describe('tierExternalDeps', () => {
|
||||
it('tiers by declaring area, not by the declaring section name', () => {
|
||||
const { manifests, names } = workspace({
|
||||
// Root tooling and test infrastructure never ship, whichever section declares them.
|
||||
'package.json': { dependencies: { 'root-runtime-looking': '^1' }, devDependencies: { 'lint-tool': '^1' } },
|
||||
'packages/support/loader-smoke/package.json': { name: '@deepseek-ai/dsh-loader-smoke', dependencies: { 'smoke-helper': '^1' } },
|
||||
'packages/client/test-runtime/package.json': { name: '@deepseek-ai/dsh-client-test-runtime', dependencies: { 'test-lib': '^1' } },
|
||||
'website/package.json': { devDependencies: { 'site-tool': '^1' } },
|
||||
// A plugin package's runtime dependency ships even when no app mounts it by default.
|
||||
'packages/mcp/mcp-client/package.json': { name: '@deepseek-ai/dsh-mcp-client', dependencies: { 'protocol-sdk': '^1' }, devDependencies: { 'protocol-fixture-server': '^1' } },
|
||||
'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli', dependencies: { 'cli-lib': '^1', '@deepseek-ai/dsh-mcp-client': 'workspace:^' } },
|
||||
})
|
||||
|
||||
expect(tierExternalDeps(manifests, names)).toEqual(new Map([
|
||||
['tsx', true],
|
||||
['root-runtime-looking', false],
|
||||
['lint-tool', false],
|
||||
['smoke-helper', false],
|
||||
['test-lib', false],
|
||||
['site-tool', false],
|
||||
['protocol-sdk', true],
|
||||
['protocol-fixture-server', false],
|
||||
['cli-lib', true],
|
||||
]))
|
||||
})
|
||||
|
||||
it('keeps a package runtime when any shipping area declares it, and excludes workspace links', () => {
|
||||
const { manifests, names } = workspace({
|
||||
'package.json': { devDependencies: { shared: '^1' } },
|
||||
'packages/ui/tui/package.json': { name: '@deepseek-ai/dsh-tui', dependencies: { shared: '^1', '@deepseek-ai/dsh-cli': 'workspace:^' } },
|
||||
'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli' },
|
||||
})
|
||||
|
||||
expect(tierExternalDeps(manifests, names).get('shared')).toBe(true)
|
||||
expect(tierExternalDeps(manifests, names).has('@deepseek-ai/dsh-cli')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseVendoredRows', () => {
|
||||
it('reads the committed vendor manifest table', () => {
|
||||
const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))
|
||||
|
||||
expect(rows.length).toBeGreaterThan(0)
|
||||
expect(rows).toContainEqual({ npmName: 'cordis', upstream: 'https://github.com/cordiverse/cordis' })
|
||||
// The upstream column carries a trailing package path for some rows; it is not part of the URL.
|
||||
expect(rows.every(row => /^https:\/\/\S+$/.test(row.upstream))).toBe(true)
|
||||
})
|
||||
|
||||
it('yields nothing when the table shape changes, so the generator fails loud', () => {
|
||||
expect(parseVendoredRows('| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([])
|
||||
})
|
||||
|
||||
it('covers every vendored directory, so no package can drop out of the notices', () => {
|
||||
const parsed = new Set(parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8')).map(row => row.npmName))
|
||||
const onDisk = readdirSync(resolve(root, 'vendor'), { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => (JSON.parse(readFileSync(resolve(root, 'vendor', entry.name, 'package.json'), 'utf8')) as Manifest).name)
|
||||
|
||||
expect([...onDisk].sort()).toEqual([...parsed].sort())
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePyprojectRequirements', () => {
|
||||
it('reads the committed manifests', () => {
|
||||
expect(parsePyprojectRequirements(readFileSync(resolve(root, 'python/sdk/pyproject.toml'), 'utf8'))).toContain('pydantic')
|
||||
})
|
||||
|
||||
it('locates requirement arrays by TOML table, so author-named groups are not missed', () => {
|
||||
expect(parsePyprojectRequirements([
|
||||
'[build-system]',
|
||||
'requires = ["hatchling>=1.24.0"]',
|
||||
'',
|
||||
'[project]',
|
||||
'name = "not-a-requirement"',
|
||||
'dependencies = ["pydantic>=2.12"]',
|
||||
'',
|
||||
'[project.optional-dependencies]',
|
||||
'cli = ["click"]',
|
||||
'',
|
||||
'[dependency-groups]',
|
||||
'docs = ["sphinx>=7"]',
|
||||
'',
|
||||
'[tool.hatch.build.targets.wheel]',
|
||||
'packages = ["src/deepseek_harness"]',
|
||||
'',
|
||||
'[tool.pytest.ini_options]',
|
||||
'testpaths = ["tests"]',
|
||||
].join('\n'))).toEqual(['hatchling', 'pydantic', 'click', 'sphinx'])
|
||||
})
|
||||
|
||||
it('does not truncate an array at a bracket inside extras', () => {
|
||||
expect(parsePyprojectRequirements('[project]\ndependencies = ["httpx[http2]", "requests"]\n'))
|
||||
.toEqual(['httpx', 'requests'])
|
||||
})
|
||||
|
||||
it('reads names whether or not requirements carry versions, extras, or markers', () => {
|
||||
expect(parsePyprojectRequirements("[project]\ndependencies = [\"pydantic>=2.12\", \"requests\", \"httpx[http2]\", \"tomli ; python_version < '3.11'\", \"hatchling >= 1.24.0\"]\n"))
|
||||
.toEqual(['pydantic', 'requests', 'httpx', 'tomli', 'hatchling'])
|
||||
})
|
||||
|
||||
it('reads single-quoted TOML literals and rejects an unreadable requirement', () => {
|
||||
expect(parsePyprojectRequirements("[project]\ndependencies = ['requests', \"pydantic>=2\"]\n")).toEqual(['requests', 'pydantic'])
|
||||
expect(() => parsePyprojectRequirements('[project]\ndependencies = ["!!broken"]\n')).toThrow(/cannot read a distribution name/)
|
||||
})
|
||||
|
||||
it('reads a multi-line array', () => {
|
||||
expect(parsePyprojectRequirements('[project]\ndependencies = [\n "pydantic>=2.12",\n "typing-extensions",\n]\n'))
|
||||
.toEqual(['pydantic', 'typing-extensions'])
|
||||
})
|
||||
|
||||
it('obeys TOML comments, quoted keys, and escaped strings', () => {
|
||||
expect(parsePyprojectRequirements([
|
||||
'[project] # a legal header comment',
|
||||
'dependencies = [',
|
||||
' "pydantic", # ] does not close the array',
|
||||
' # "old-package" is not a dependency',
|
||||
' "tomli; python_version < \'3.11\'",',
|
||||
']',
|
||||
'',
|
||||
'[dependency-groups]',
|
||||
'"test.docs" = ["pytest"]',
|
||||
].join('\n'))).toEqual(['pydantic', 'tomli', 'pytest'])
|
||||
})
|
||||
|
||||
it('accepts dependency-group includes and rejects unsupported requirement shapes', () => {
|
||||
expect(parsePyprojectRequirements('[dependency-groups]\nbase = ["pytest"]\nall = [{ include-group = "base" }]\n'))
|
||||
.toEqual(['pytest'])
|
||||
expect(() => parsePyprojectRequirements('[project]\ndependencies = "pytest"\n')).toThrow(/must be an array/)
|
||||
expect(() => parsePyprojectRequirements('[dependency-groups]\ntest = [{ unknown = "pytest" }]\n')).toThrow(/unsupported requirement entry/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collectPythonDependencies', () => {
|
||||
it('excludes normalized local project names without exempting a third-party prefix', () => {
|
||||
const pyprojects = [
|
||||
'[project]\nname = "deepseek-harness-runtime-bin"\ndependencies = ["pydantic"]\n',
|
||||
'[project]\nname = "deepseek-harness"\ndependencies = ["DeepSeek.Harness_Runtime-Bin", "deepseek-unrelated"]\n',
|
||||
]
|
||||
expect(() => collectPythonDependencies(pyprojects)).toThrow(
|
||||
'python dependency deepseek-unrelated is missing from PYTHON_METADATA',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPermissive', () => {
|
||||
it('accepts the licenses this project ships and rejects copyleft or unknown ones', () => {
|
||||
expect(['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0', 'MIT / Apache-2.0', '(MIT OR CC0-1.0)'].every(isPermissive)).toBe(true)
|
||||
expect(['LGPL-3.0-only', 'MPL-2.0', 'GPL-3.0-or-later', 'SEE LICENSE IN LICENSE'].some(isPermissive)).toBe(false)
|
||||
})
|
||||
|
||||
it('requires every operand of an AND, so a copyleft conjunct cannot ride along', () => {
|
||||
expect(isPermissive('(MIT OR Apache-2.0) AND GPL-3.0-only')).toBe(false)
|
||||
expect(isPermissive('MIT AND ISC')).toBe(true)
|
||||
// An exception clause is not a recognized identifier, so it fails closed.
|
||||
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
|
||||
})
|
||||
|
||||
it('honors grouping and SPDX precedence', () => {
|
||||
expect(isPermissive('MIT OR (GPL-3.0-only AND GPL-2.0-only)')).toBe(true)
|
||||
expect(isPermissive('(MIT OR Apache-2.0) AND ISC')).toBe(true)
|
||||
})
|
||||
|
||||
it('fails closed for malformed expressions, additions, and exceptions', () => {
|
||||
expect(['MIT)', '((MIT', '(MIT OR GPL-3.0-only', 'MIT OR OR GPL-3.0-only'].some(isPermissive)).toBe(false)
|
||||
expect(isPermissive('MIT+')).toBe(false)
|
||||
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('manifestPatterns', () => {
|
||||
it('derives globs from the declared members, so a new member area is read', () => {
|
||||
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([
|
||||
'package.json',
|
||||
'packages/*/*/package.json',
|
||||
'tools/*/package.json',
|
||||
'examples/*/package.json',
|
||||
'native/landlock-run/package.json',
|
||||
'native/landlock-run/packages/*/package.json',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,596 @@
|
||||
/**
|
||||
* Generate `THIRD_PARTY_NOTICES.md` from the workspace manifests: every
|
||||
* external dependency named by a workspace `package.json`, the vendored-package
|
||||
* manifest in `vendor/README.md`, the Python `pyproject.toml` files, and the
|
||||
* pnpm patch list. License and repository metadata come from the installed
|
||||
* store, so the tree must be installed. `--check` verifies the committed
|
||||
* artifact. Tier policy and ownership live in
|
||||
* `.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md`.
|
||||
*/
|
||||
|
||||
import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
|
||||
import parseSpdx from 'spdx-expression-parse'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'THIRD_PARTY_NOTICES.md'
|
||||
|
||||
/** Dependency-declaration kinds a consumer resolves at runtime. */
|
||||
const RUNTIME_KINDS = ['dependencies', 'optionalDependencies'] as const
|
||||
/** All manifest sections that name an external package this file must disclose. */
|
||||
const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
|
||||
/**
|
||||
* Workspace areas that never reach a user: repository tooling and gates (the
|
||||
* root manifest), test infrastructure, the documentation site, the runnable
|
||||
* demo leaves, and the native launcher's build workspace. A runtime
|
||||
* declaration by anything outside these areas is a disclosure-relevant
|
||||
* runtime dependency, because `scripts/install.sh` installs the repository
|
||||
* itself and any plugin package can be mounted from a user's `cordis.yml`.
|
||||
*/
|
||||
const DEV_ONLY_AREAS = [
|
||||
'package.json',
|
||||
'packages/support/',
|
||||
'packages/client/test-runtime/',
|
||||
'website/',
|
||||
'examples/',
|
||||
'native/',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* First-party packages released from sibling repositories under the project's
|
||||
* own license: reachable from workspace manifests but not third-party.
|
||||
*/
|
||||
const FIRST_PARTY = new Set([
|
||||
'node-addon-landlock-run',
|
||||
'node-addon-landlock-run-linux-arm64',
|
||||
'node-addon-landlock-run-linux-x64',
|
||||
])
|
||||
|
||||
/**
|
||||
* Metadata overrides where the installed manifest is wrong or unreachable.
|
||||
* Each entry documents why the store cannot answer.
|
||||
*/
|
||||
const OVERRIDES: Record<string, { license?: string; repo?: string }> = {
|
||||
// Rust workspaces publishing npm bins without `license` in package.json.
|
||||
'oxlint': { license: 'MIT', repo: 'https://github.com/oxc-project/oxc' },
|
||||
'oxlint-tsgolint': { license: 'MIT', repo: 'https://github.com/oxc-project/tsgolint' },
|
||||
// `license: SEE LICENSE IN LICENSE`: the servers repo is mid MIT→Apache-2.0
|
||||
// relicensing, so the effective terms are per-contribution.
|
||||
'@modelcontextprotocol/server-everything': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' },
|
||||
'@modelcontextprotocol/server-filesystem': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' },
|
||||
// No repository field in the published manifest.
|
||||
'node-addon-require-builtin': { repo: 'https://www.npmjs.com/package/node-addon-require-builtin' },
|
||||
}
|
||||
|
||||
/**
|
||||
* Python dependencies are few and named directly in `pyproject.toml` files
|
||||
* without installed metadata to harvest, so license/repo are recorded here and
|
||||
* the generator fails when a manifest names a package this map misses.
|
||||
*/
|
||||
const PYTHON_METADATA: Record<string, { license: string; repo: string; role: string }> = {
|
||||
pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness`' },
|
||||
hatchling: { license: 'MIT', repo: 'https://github.com/pypa/hatch', role: 'build backend' },
|
||||
pytest: { license: 'MIT', repo: 'https://github.com/pytest-dev/pytest', role: 'test-only' },
|
||||
}
|
||||
|
||||
type PythonMetadata = typeof PYTHON_METADATA
|
||||
|
||||
/** Tools fetched by scripts at build time, keyed by the pin the script owns. */
|
||||
const BUILD_TIME_TOOLS = [
|
||||
{
|
||||
name: '@yao-pkg/pkg',
|
||||
license: 'MIT',
|
||||
repo: 'https://github.com/yao-pkg/pkg',
|
||||
role: 'invoked by `scripts/build-exe-for-python-sdk.ts` to assemble the single-file SDK runtime executable',
|
||||
pinSource: 'scripts/build-exe-for-python-sdk.ts',
|
||||
},
|
||||
]
|
||||
|
||||
/** The `package.json` fields this generator reads. */
|
||||
export interface Manifest {
|
||||
name?: string
|
||||
private?: boolean
|
||||
license?: string
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
/** One disclosed external npm dependency. */
|
||||
interface ExternalDep {
|
||||
name: string
|
||||
license: string
|
||||
repo: string
|
||||
/** True when some shipped workspace consumer reaches it through runtime dependency edges. */
|
||||
runtime: boolean
|
||||
}
|
||||
|
||||
/** Read and parse a workspace-relative `package.json`. */
|
||||
function readManifest(rel: string): Manifest {
|
||||
return JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as Manifest
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest globs, derived from the workspace declarations rather than listed
|
||||
* here, so a new member area (`tools/*`) is read the day it is declared.
|
||||
* @returns one glob per manifest-bearing location, repository-relative.
|
||||
*/
|
||||
export function manifestPatterns(rootMembers: readonly string[], nativeMembers: readonly string[]): string[] {
|
||||
return [
|
||||
'package.json',
|
||||
...rootMembers.map(member => `${member}/package.json`),
|
||||
// The demo leaves join the workspace through `examples/package.json`, so
|
||||
// their own manifests are members of nothing and no glob above reaches them.
|
||||
'examples/*/package.json',
|
||||
// `native/landlock-run` is a nested workspace with its own lock file.
|
||||
'native/landlock-run/package.json',
|
||||
...nativeMembers.map(member => `native/landlock-run/${member}/package.json`),
|
||||
]
|
||||
}
|
||||
|
||||
/** The `packages:` member globs declared by one pnpm workspace file. */
|
||||
function workspaceMembers(rel: string): string[] {
|
||||
const declared = (yaml.load(readFileSync(resolve(root, rel), 'utf8')) as { packages?: unknown }).packages
|
||||
if (!Array.isArray(declared) || declared.length === 0) {
|
||||
throw new Error(`gen-third-party-notices: ${rel} declares no workspace members; the manifest set cannot be derived.`)
|
||||
}
|
||||
return declared.map(member => String(member))
|
||||
}
|
||||
|
||||
/** Every workspace manifest, keyed by path, plus the set of workspace package names. */
|
||||
function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Set<string> } {
|
||||
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'), workspaceMembers('native/landlock-run/pnpm-workspace.yaml'))
|
||||
const manifests = new Map<string, Manifest>()
|
||||
const names = new Set<string>()
|
||||
for (const pattern of patterns) {
|
||||
for (const path of globSync(pattern, { cwd: root })) {
|
||||
const manifest = readManifest(path)
|
||||
manifests.set(path, manifest)
|
||||
if (manifest.name !== undefined) names.add(manifest.name)
|
||||
}
|
||||
}
|
||||
if (manifests.size < 100) throw new Error(`gen-third-party-notices: only ${manifests.size} workspace manifests found; the glob set is stale.`)
|
||||
return { manifests, names }
|
||||
}
|
||||
|
||||
/** License and repository URL for an installed external package, from the pnpm store. */
|
||||
function installedMetadata(name: string): { license: string; repo: string } {
|
||||
const override = OVERRIDES[name]
|
||||
let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined
|
||||
// The nested Landlock workspace installs into its own store, so a package
|
||||
// only that workspace depends on is unreachable from the root one.
|
||||
for (const store of ['node_modules', 'native/landlock-run/node_modules']) {
|
||||
const direct = resolve(root, store, name, 'package.json')
|
||||
if (existsSync(direct)) {
|
||||
manifest = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest
|
||||
break
|
||||
}
|
||||
const virtual = resolve(root, store, '.pnpm')
|
||||
if (!existsSync(virtual)) continue
|
||||
const prefix = `${name.replace('/', '+')}@`
|
||||
const entry = readdirSync(virtual).find(dir => dir.startsWith(prefix))
|
||||
if (entry === undefined) continue
|
||||
manifest = JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as typeof manifest
|
||||
break
|
||||
}
|
||||
const license = override?.license ?? manifest?.license
|
||||
const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage
|
||||
const repo = override?.repo ?? normalizeRepo(rawRepo)
|
||||
if (license === undefined || repo === undefined) {
|
||||
throw new Error(`gen-third-party-notices: cannot resolve ${license === undefined ? 'license' : 'repository'} for ${name}; run \`pnpm install\` (or, for a Landlock-only dependency, \`pnpm --dir native/landlock-run install\`), or add an OVERRIDES entry.`)
|
||||
}
|
||||
return { license, repo }
|
||||
}
|
||||
|
||||
/** Normalize a manifest repository/homepage value to a browsable https URL. */
|
||||
function normalizeRepo(raw: string | undefined): string | undefined {
|
||||
if (raw === undefined || raw === '') return undefined
|
||||
let url = raw
|
||||
.replace(/^git\+ssh:\/\/git@/, 'https://')
|
||||
.replace(/^git\+/, '')
|
||||
.replace(/^git:\/\//, 'https://')
|
||||
.replace(/^github:/, 'https://github.com/')
|
||||
.replace(/\.git$/, '')
|
||||
if (!url.startsWith('http')) url = `https://github.com/${url}`
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* External npm dependencies, tiered by which workspace area declares them at
|
||||
* runtime: a package is runtime when any manifest outside `DEV_ONLY_AREAS`
|
||||
* names it in `dependencies`/`optionalDependencies`. A package declared only
|
||||
* by tooling, test infrastructure, the website, or the demo leaves — whatever
|
||||
* the declaring section is called — is development-only.
|
||||
*/
|
||||
function collectNpmDeps(): ExternalDep[] {
|
||||
const { manifests, names } = loadWorkspaceManifests()
|
||||
return [...tierExternalDeps(manifests, names)]
|
||||
.filter(([name]) => !FIRST_PARTY.has(name))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([name, runtime]) => ({ name, ...installedMetadata(name), runtime }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier every external dependency the workspace declares.
|
||||
* @param manifests - workspace manifests keyed by repository-relative path.
|
||||
* @param names - every workspace package name, which never counts as external.
|
||||
* @returns each external package mapped to whether it is a runtime dependency.
|
||||
*/
|
||||
export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<string>): Map<string, boolean> {
|
||||
const tiers = new Map<string, boolean>()
|
||||
// `tsx` is runtime by fiat: `bin/dsh` execs the CLI through its ESM hook.
|
||||
tiers.set('tsx', true)
|
||||
for (const [path, manifest] of manifests) {
|
||||
const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area))
|
||||
for (const kind of ALL_KINDS) {
|
||||
for (const [dep, range] of Object.entries(manifest[kind] ?? {})) {
|
||||
if (names.has(dep) || range.startsWith('workspace:')) continue
|
||||
const runtime = !devOnly && (RUNTIME_KINDS as readonly string[]).includes(kind)
|
||||
tiers.set(dep, (tiers.get(dep) ?? false) || runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tiers
|
||||
}
|
||||
|
||||
/** A vendored package row parsed out of the `vendor/README.md` manifest table. */
|
||||
export interface VendoredRow {
|
||||
npmName: string
|
||||
upstream: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the vendored-package manifest table out of `vendor/README.md`.
|
||||
* @param text - the complete `vendor/README.md` contents.
|
||||
* @returns one row per manifest-table entry, in table order.
|
||||
*/
|
||||
export function parseVendoredRows(text: string): VendoredRow[] {
|
||||
const rows: VendoredRow[] = []
|
||||
for (const line of text.split('\n')) {
|
||||
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 })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the vendored manifest table and confirm it accounts for every vendored
|
||||
* directory. The `vendor/` tree — not the table — is the set that must be
|
||||
* disclosed, so a row that stops matching the table format is a hard error
|
||||
* rather than a package that quietly vanishes from the notices.
|
||||
*/
|
||||
function collectVendored(): VendoredRow[] {
|
||||
const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))
|
||||
const onDisk = new Map<string, string>()
|
||||
for (const entry of readdirSync(resolve(root, 'vendor'), { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue
|
||||
const manifest = readManifest(`vendor/${entry.name}/package.json`)
|
||||
if (manifest.name !== undefined) onDisk.set(manifest.name, entry.name)
|
||||
}
|
||||
|
||||
const parsed = new Set(rows.map(row => row.npmName))
|
||||
const missing = [...onDisk.keys()].filter(name => !parsed.has(name))
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`gen-third-party-notices: vendor/README.md has no manifest-table row for ${missing.join(', ')}; its table format changed or the sync is incomplete.`)
|
||||
}
|
||||
for (const row of rows) {
|
||||
const dir = onDisk.get(row.npmName)
|
||||
if (dir === undefined) throw new Error(`gen-third-party-notices: vendored package ${row.npmName} from vendor/README.md has no vendor/ directory.`)
|
||||
const license = readManifest(`vendor/${dir}/package.json`).license
|
||||
if (license !== 'MIT') {
|
||||
throw new Error(`gen-third-party-notices: vendored ${row.npmName} declares license ${JSON.stringify(license)}; the vendored section assumes MIT throughout.`)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/** Whether a parsed TOML value is a table rather than an array or scalar. */
|
||||
function isTomlTable(value: TomlValueWithoutBigInt | undefined): value is TomlTableWithoutBigInt {
|
||||
return value !== undefined && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/** Parse one PEP 508 requirement string into its distribution name. */
|
||||
function parsePythonRequirement(requirement: string): string {
|
||||
const name = /^\s*([a-zA-Z][a-zA-Z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(?:[<>=!~;@].*)?$/.exec(requirement)?.[1]
|
||||
if (name === undefined) {
|
||||
throw new Error(`gen-third-party-notices: cannot read a distribution name from the requirement ${JSON.stringify(requirement)}.`)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
/** Add the string requirements from one parsed TOML array. */
|
||||
function collectPythonRequirementArray(
|
||||
names: string[],
|
||||
value: TomlValueWithoutBigInt | undefined,
|
||||
location: string,
|
||||
allowGroupIncludes = false,
|
||||
): void {
|
||||
if (value === undefined) return
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`gen-third-party-notices: ${location} must be an array.`)
|
||||
}
|
||||
for (const item of value) {
|
||||
if (typeof item === 'string') {
|
||||
names.push(parsePythonRequirement(item))
|
||||
continue
|
||||
}
|
||||
if (allowGroupIncludes && isTomlTable(item) && typeof item['include-group'] === 'string' && Object.keys(item).length === 1) {
|
||||
continue
|
||||
}
|
||||
throw new Error(`gen-third-party-notices: ${location} contains an unsupported requirement entry.`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Read an optional TOML table and reject a present value of another shape. */
|
||||
function optionalTomlTable(value: TomlValueWithoutBigInt | undefined, location: string): TomlTableWithoutBigInt | undefined {
|
||||
if (value === undefined || isTomlTable(value)) return value
|
||||
throw new Error(`gen-third-party-notices: ${location} must be a table.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `pyproject.toml` project identity and every requirement it declares:
|
||||
* `requires` under
|
||||
* `[build-system]`, `dependencies` under `[project]`, and every key under
|
||||
* `[project.optional-dependencies]` and `[dependency-groups]`. A TOML parser
|
||||
* owns comments, quoted keys, escapes, and array boundaries; unsupported
|
||||
* requirement shapes fail instead of disappearing from the notices.
|
||||
* @param text - the complete `pyproject.toml` contents.
|
||||
* @returns the local project name and declared requirement names.
|
||||
*/
|
||||
function parsePyproject(text: string): { projectName?: string; requirements: string[] } {
|
||||
const names: string[] = []
|
||||
const document = parseToml(text, { integersAsBigInt: false })
|
||||
const buildSystem = optionalTomlTable(document['build-system'], '[build-system]')
|
||||
const project = optionalTomlTable(document.project, '[project]')
|
||||
const projectName = project?.name
|
||||
if (projectName !== undefined && typeof projectName !== 'string') {
|
||||
throw new Error('gen-third-party-notices: [project].name must be a string.')
|
||||
}
|
||||
collectPythonRequirementArray(names, buildSystem?.requires, '[build-system].requires')
|
||||
collectPythonRequirementArray(names, project?.dependencies, '[project].dependencies')
|
||||
|
||||
const optional = optionalTomlTable(project?.['optional-dependencies'], '[project.optional-dependencies]')
|
||||
for (const [group, requirements] of Object.entries(optional ?? {})) {
|
||||
collectPythonRequirementArray(names, requirements, `[project.optional-dependencies].${group}`)
|
||||
}
|
||||
|
||||
const groups = optionalTomlTable(document['dependency-groups'], '[dependency-groups]')
|
||||
for (const [group, requirements] of Object.entries(groups ?? {})) {
|
||||
collectPythonRequirementArray(names, requirements, `[dependency-groups].${group}`, true)
|
||||
}
|
||||
return projectName === undefined
|
||||
? { requirements: names }
|
||||
: { projectName, requirements: names }
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every requirement name declared by one `pyproject.toml`.
|
||||
* @param text - the complete `pyproject.toml` contents.
|
||||
* @returns each declared requirement's distribution name, in file order.
|
||||
*/
|
||||
export function parsePyprojectRequirements(text: string): string[] {
|
||||
return parsePyproject(text).requirements
|
||||
}
|
||||
|
||||
/** Normalize a Python distribution name according to the packaging name rule. */
|
||||
function normalizePythonDistributionName(name: string): string {
|
||||
return name.toLowerCase().replace(/[-_.]+/g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve external Python dependencies after excluding local project names.
|
||||
* @param pyprojects - complete local `pyproject.toml` contents.
|
||||
* @param metadata - disclosure metadata for every external dependency.
|
||||
* @returns disclosed dependencies in normalized name order.
|
||||
*/
|
||||
export function collectPythonDependencies(
|
||||
pyprojects: string[],
|
||||
metadata: PythonMetadata = PYTHON_METADATA,
|
||||
): { name: string; license: string; repo: string; role: string }[] {
|
||||
const parsed = pyprojects.map(parsePyproject)
|
||||
const firstParty = new Set(parsed.flatMap(({ projectName }) => (
|
||||
projectName === undefined ? [] : [normalizePythonDistributionName(projectName)]
|
||||
)))
|
||||
const found = new Set(parsed
|
||||
.flatMap(({ requirements }) => requirements.map(normalizePythonDistributionName))
|
||||
.filter(name => !firstParty.has(name)))
|
||||
return [...found].sort((a, b) => a.localeCompare(b)).map((name) => {
|
||||
const entry = metadata[name]
|
||||
if (entry === undefined) throw new Error(`gen-third-party-notices: python dependency ${name} is missing from PYTHON_METADATA.`)
|
||||
return { name, ...entry }
|
||||
})
|
||||
}
|
||||
|
||||
/** Direct Python dependencies named by the `pyproject.toml` manifests under `python/`. */
|
||||
function collectPython(): { name: string; license: string; repo: string; role: string }[] {
|
||||
const manifests = globSync('python/*/pyproject.toml', { cwd: root })
|
||||
if (manifests.length === 0) throw new Error('gen-third-party-notices: no python/*/pyproject.toml found; the Python tree moved.')
|
||||
return collectPythonDependencies(manifests.map(path => readFileSync(resolve(root, path), 'utf8')))
|
||||
}
|
||||
|
||||
/** pnpm-patched external packages, from `pnpm-workspace.yaml`. */
|
||||
function collectPatched(): { spec: string; patch: string }[] {
|
||||
const workspace = yaml.load(readFileSync(resolve(root, 'pnpm-workspace.yaml'), 'utf8')) as { patchedDependencies?: Record<string, string> }
|
||||
return Object.entries(workspace.patchedDependencies ?? {}).map(([spec, patch]) => ({ spec, patch }))
|
||||
}
|
||||
|
||||
/** Verify each build-time tool pin still appears in its owning script. */
|
||||
function verifyBuildTimePins(): void {
|
||||
for (const tool of BUILD_TIME_TOOLS) {
|
||||
const text = readFileSync(resolve(root, tool.pinSource), 'utf8')
|
||||
if (!text.includes(tool.name)) {
|
||||
throw new Error(`gen-third-party-notices: ${tool.pinSource} no longer references ${tool.name}; update BUILD_TIME_TOOLS.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** SPDX identifiers this project may ship without further review. */
|
||||
const PERMISSIVE_LICENSES = new Set(['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'BlueOak-1.0.0', 'Python-2.0'])
|
||||
|
||||
/** Evaluate a parsed SPDX expression under the repository's license policy. */
|
||||
function isPermissiveSpdx(expression: ReturnType<typeof parseSpdx>): boolean {
|
||||
if ('conjunction' in expression) {
|
||||
return expression.conjunction === 'and'
|
||||
? isPermissiveSpdx(expression.left) && isPermissiveSpdx(expression.right)
|
||||
: isPermissiveSpdx(expression.left) || isPermissiveSpdx(expression.right)
|
||||
}
|
||||
return expression.plus !== true
|
||||
&& expression.exception === undefined
|
||||
&& PERMISSIVE_LICENSES.has(expression.license)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an SPDX expression grants terms this project may ship under.
|
||||
* `OR` needs one permissive alternative, because the consumer chooses; `AND`
|
||||
* needs all of them, because every obligation applies. Anything that is not a
|
||||
* recognized permissive identifier — copyleft, an exception clause, or a
|
||||
* license this list has never seen — evaluates to false, so an unfamiliar
|
||||
* expression fails closed rather than passing on a partial match.
|
||||
* @param license - the SPDX expression from the package manifest.
|
||||
* @returns true when the expression's obligations are all permissive.
|
||||
*/
|
||||
export function isPermissive(license: string): boolean {
|
||||
// Some npm manifests use a slash for a choice despite SPDX requiring `OR`.
|
||||
const normalized = license.replace(/\s*\/\s*/g, ' OR ').trim()
|
||||
try {
|
||||
return isPermissiveSpdx(parseSpdx(normalized))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the sentence that isolates non-permissive development tooling, or
|
||||
* nothing at all when every development dependency is permissive.
|
||||
* @param deps - development dependencies whose license is not permissive.
|
||||
* @returns the paragraph to place after the development table.
|
||||
*/
|
||||
function renderNonPermissiveNote(deps: ExternalDep[]): string {
|
||||
if (deps.length === 0) return ''
|
||||
const named = deps.map(dep => `\`${dep.name}\` (${dep.license})`)
|
||||
const subject = named.length === 1 ? named[0] : `${named.slice(0, -1).join(', ')} and ${named.at(-1)}`
|
||||
return `\n${subject} ${named.length === 1 ? 'runs' : 'run'} only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact.\n`
|
||||
}
|
||||
|
||||
/** Render one npm dependency table. */
|
||||
function renderNpmTable(deps: ExternalDep[]): string {
|
||||
const lines = ['| Package | License |', '| --- | --- |']
|
||||
for (const dep of deps) lines.push(`| [\`${dep.name}\`](${dep.repo}) | ${dep.license} |`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the complete notices document.
|
||||
* @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
|
||||
*/
|
||||
export function render(): string {
|
||||
verifyBuildTimePins()
|
||||
const npm = collectNpmDeps()
|
||||
const runtimeDeps = npm.filter(dep => dep.runtime)
|
||||
const devDeps = npm.filter(dep => !dep.runtime)
|
||||
const vendored = collectVendored()
|
||||
const python = collectPython()
|
||||
const patched = collectPatched()
|
||||
|
||||
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
|
||||
// A copyleft license reaching a shipped surface is a distribution decision,
|
||||
// not a rendering detail; the notices cannot quietly absorb it.
|
||||
const nonPermissiveRuntime = runtimeDeps.filter(dep => !isPermissive(dep.license))
|
||||
if (nonPermissiveRuntime.length > 0) {
|
||||
throw new Error(`gen-third-party-notices: runtime ${nonPermissiveRuntime.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
|
||||
}
|
||||
const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`)
|
||||
|
||||
return `<!-- Generated by scripts/gen-third-party-notices.ts — do not edit by hand.
|
||||
Run \`pnpm run gen-third-party-notices\` to regenerate. -->
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
|
||||
|
||||
The complete npm transitive closure, with exact pinned versions, is recorded in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded in [\`python/sdk/uv.lock\`](python/sdk/uv.lock), and the Landlock launcher workspace keeps its own in [\`native/landlock-run/pnpm-lock.yaml\`](native/landlock-run/pnpm-lock.yaml).
|
||||
|
||||
## Vendored source (\`vendor/\`)
|
||||
|
||||
The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed; each directory preserves its upstream \`LICENSE\` file. Exact upstream commits and local modifications are recorded in [\`vendor/README.md\`](vendor/README.md).
|
||||
|
||||
| Package | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://', '')}](${row.upstream}) | MIT |`).join('\n')}
|
||||
|
||||
## Runtime npm dependencies
|
||||
|
||||
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI/TUI, the Web UI, and the Python SDK runtime load by default.
|
||||
|
||||
${renderNpmTable(runtimeDeps)}
|
||||
|
||||
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
|
||||
|
||||
${patchedLines.join('\n')}
|
||||
|
||||
## Development-only npm dependencies
|
||||
|
||||
External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — \`pnpm-lock.yaml\` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
|
||||
|
||||
${renderNpmTable(devDeps)}
|
||||
${renderNonPermissiveNote(nonPermissiveDev)}
|
||||
## Python SDK dependencies (\`python/\`)
|
||||
|
||||
Direct dependencies of the \`pyproject.toml\` manifests, plus \`uv\` as the development workflow tool.
|
||||
|
||||
| Package | License | Role |
|
||||
| --- | --- | --- |
|
||||
${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.role} |`).join('\n')}
|
||||
| [\`uv\`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
|
||||
|
||||
## Fetched at build time
|
||||
|
||||
| Package | License | Role |
|
||||
| --- | --- | --- |
|
||||
${BUILD_TIME_TOOLS.map(tool => `| [\`${tool.name}\`](${tool.repo}) | ${tool.license} | ${tool.role} |`).join('\n')}
|
||||
|
||||
## First-party sibling releases
|
||||
|
||||
\`node-addon-landlock-run\` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
|
||||
`
|
||||
}
|
||||
|
||||
/** CLI entry: default writes the notices, `--check` fails if the committed copy
|
||||
* is stale. Guarded behind an entry-point check so importing this module for
|
||||
* tests neither regenerates the committed file nor calls process.exit. */
|
||||
function main(): void {
|
||||
const content = render()
|
||||
if (process.argv.includes('--check')) {
|
||||
let committed: string | null = null
|
||||
try {
|
||||
committed = readFileSync(resolve(root, OUT), 'utf8')
|
||||
} catch {
|
||||
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
|
||||
// file is not a state this repo produces, and the remedy is the same.
|
||||
committed = null
|
||||
}
|
||||
if (committed === content) {
|
||||
console.log(`gen-third-party-notices: ${OUT} is up to date.`)
|
||||
process.exit(0)
|
||||
}
|
||||
console.error(`gen-third-party-notices: ${OUT} is stale. Run \`pnpm run gen-third-party-notices\` and commit ${OUT}.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
writeFileSync(resolve(root, OUT), content)
|
||||
console.log(`gen-third-party-notices: wrote ${OUT}.`)
|
||||
}
|
||||
|
||||
// Run only when invoked as a script, not when imported by a test.
|
||||
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
|
||||
main()
|
||||
}
|
||||
File diff suppressed because one or more lines are too long.
Reference in New Issue
Block a user