Prechádzať zdrojové kódy

docs: evaluate SPDX conjunctions and read every Python manifest

Reject a copyleft conjunct in an expression like '(MIT OR Apache-2.0)
AND GPL-3.0-only', which a permissive-alternative search accepted.
Discover python/*/pyproject.toml by glob, accept single-quoted TOML
literals, and fail on a requirement whose name cannot be read. Say that
the development tier records who declares a package rather than what a
build bundles, since a runtime dependency can pull one in transitively,
and sync the contributor guide's pre-commit list.
ZiyaZhang 2 mesiacov pred
rodič
commit
421594472a

+ 2 - 3
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.

+ 2 - 2
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

+ 1 - 1
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.

+ 1 - 1
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`。

+ 12 - 0
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', () => {

+ 34 - 16
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<string>()
-  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.