diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 841e16d605..a60ad3f0ad 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,7 +5,7 @@ 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 manifest changes, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Run `pnpm run verify-third-party-notices` for the standalone check. +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). @@ -87,7 +87,7 @@ pnpm applies local patches to the following packages at install time, so shipped ## Development-only npm dependencies -External packages declared only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. They are not part of any shipped runtime artifact. +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 | | --- | --- | @@ -140,7 +140,6 @@ External packages declared only by repository tooling, test infrastructure, the `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. diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index c456a02a31..7d71b529fa 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -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 diff --git a/docs/development.md b/docs/development.md index f58cad7d36..22eb7915f6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. diff --git a/docs/development.zh.md b/docs/development.zh.md index 88ddd8483c..480cd323d4 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -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`。 diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 29db79e3ff..93d615b667 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -127,6 +127,11 @@ describe('parsePyprojectRequirements', () => { .toEqual(['httpx', 'requests']) }) + 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']) @@ -138,6 +143,13 @@ describe('isPermissive', () => { 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) + }) }) describe('manifestPatterns', () => { diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index 134a939662..86dde7cf84 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -177,7 +177,7 @@ function installedMetadata(name: string): { license: string; repo: string } { 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}; install the tree or add an OVERRIDES entry.`) + 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 } } @@ -296,9 +296,15 @@ function collectVendored(): VendoredRow[] { */ export function parsePythonRequirements(block: string): string[] { const names: string[] = [] - for (const match of block.matchAll(/"\s*([a-zA-Z][a-zA-Z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(?:[<>=!~;@].*?)?"/g)) { - const name = match[1] - if (name !== undefined) names.push(name) + // TOML strings are single- or double-quoted; every item must yield a name, so + // an unrecognized requirement fails loud instead of dropping a package. + for (const item of block.matchAll(/"([^"]*)"|'([^']*)'/g)) { + const requirement = item[1] ?? item[2] ?? '' + 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)}.`) + } + names.push(name) } return names } @@ -365,7 +371,9 @@ export function parsePyprojectRequirements(text: string): string[] { /** Direct Python dependencies named by the `pyproject.toml` manifests under `python/`. */ function collectPython(): { name: string; license: string; repo: string; role: string }[] { const found = new Set() - for (const path of ['python/sdk/pyproject.toml', 'python/sdk-runtime/pyproject.toml']) { + 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.') + for (const path of manifests) { for (const name of parsePyprojectRequirements(readFileSync(resolve(root, path), 'utf8'))) { if (name.startsWith('deepseek')) continue found.add(name) @@ -394,18 +402,29 @@ function verifyBuildTimePins(): void { } } +/** 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']) + /** - * Whether an SPDX expression is a permissive license this project may ship. - * Anything outside the list — copyleft or unrecognized — is reported rather - * than silently rendered, because the tier tables assert what may be linked. + * 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 every alternative in the expression is permissive. + * @returns true when the expression's obligations are all permissive. */ export function isPermissive(license: string): boolean { - const permissive = 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']) - return license.split('/').map(part => part.trim().replace(/^\(|\)$/g, '')) - .flatMap(part => part.split(' OR ').map(alternative => alternative.trim())) - .some(alternative => permissive.has(alternative)) + // npm's legacy dual-license notation predates SPDX `OR`. + const normalized = license.replace(/\s*\/\s*/g, ' OR ').replace(/[()]/g, ' ').trim() + if (normalized.includes(' AND ')) { + return normalized.split(' AND ').every(operand => isPermissive(operand)) + } + if (normalized.includes(' OR ')) { + return normalized.split(' OR ').some(operand => isPermissive(operand)) + } + return PERMISSIVE_LICENSES.has(normalized.trim()) } /** @@ -457,7 +476,7 @@ export function render(): string { 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 manifest changes, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Run \`pnpm run verify-third-party-notices\` for the standalone check. +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). @@ -481,11 +500,10 @@ ${patchedLines.join('\n')} ## Development-only npm dependencies -External packages declared only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. They are not part of any shipped runtime artifact. +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.