From 5854102f4bf7d1e76db9a9eac946d53df301b61c Mon Sep 17 00:00:00 2001 From: Wen Date: Sun, 11 Oct 2026 05:07:17 +0800 Subject: [PATCH] chore(check): allow Python at the edges instead of banning it The owner relaxed the outright ban. Python may again be used for SDKs, tooling and small scripts, as in DeepSeek's dsh harness, but never in the product (Server, Web, Desktop, Node and their shared packages, ADR-0050). Every .py file must still be run by CI, package.json, scripts or other code, so dead Python cannot pile up again. Co-Authored-By: Claude Opus 5.5 --- docs/research/repository-hygiene-guards.md | 6 ++- scripts/check-hygiene.test.ts | 35 +++++++++------- scripts/check-hygiene.ts | 46 ++++++++++++++++------ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/docs/research/repository-hygiene-guards.md b/docs/research/repository-hygiene-guards.md index 351cc4b1..de832686 100644 --- a/docs/research/repository-hygiene-guards.md +++ b/docs/research/repository-hygiene-guards.md @@ -46,8 +46,10 @@ uses no new dependency, and every failure message says how to fix it. It enforce `check-developer-entrypoints.ts`. 6. Every `experiments/*` directory is still run by code, scripts, `package.json` or CI. 7. Source files open with a comment saying what they are for. -8. No tracked `.py`, `requirements*.txt`, `pyproject.toml` or `.python-version` file, since P5 - retired Python (ADR-0050); an explicit allow-list in the script is empty by default. +8. Python is allowed at the edges (SDKs, tooling, small scripts), as in other TypeScript agent + harnesses such as DeepSeek's `dsh`, but never in the product (Server, Web, Desktop, Node and + their shared packages, ADR-0050), and every `.py` file must be run by CI, `package.json`, + scripts or other code. The owner relaxed the earlier outright ban on 2026-10-11. Rules 5 and 7 start from a recorded baseline in `scripts/hygiene-baseline.json` that may only go down; the check asks for the baseline to be lowered as soon as files improve. diff --git a/scripts/check-hygiene.test.ts b/scripts/check-hygiene.test.ts index 9752e0b4..b30fc599 100644 --- a/scripts/check-hygiene.test.ts +++ b/scripts/check-hygiene.test.ts @@ -133,20 +133,27 @@ test("new source files must say what they are for; the recorded gap only shrinks ); }); -test("Python files and packaging names cannot come back", () => { - for (const name of [ - "scripts/fix.py", - "experiments/probe/requirements-dev.txt", - "apps/tool/pyproject.toml", - ".python-version", - ]) - assert.match( - checkHygiene(input({ ...clean, [name]: "x\n" })).join("\n"), - new RegExp(`${name.replaceAll(".", "\\.")}: Python was retired in P5 \\(ADR-0050\\)`, "u"), - ); - // Similar names that are not Python stay allowed. - const similar = { ...clean, "docs/requirements.md": "x\n", "apps/web/src/copy.pyx.ts": "// x\n" }; - assert.deepEqual(checkHygiene(input(similar)), []); +test("Python is allowed at the edges while something runs it, never in the product", () => { + // A small script that CI or package.json runs is fine, with its tests and packaging files. + const tooling: Record = { + ...clean, + "scripts/encode_gif.py": "print(1)\n", + "scripts/tests/test_encode_gif.py": "import encode_gif\n", + "scripts/requirements.txt": "pillow==11.0.0\n", + "package.json": + '{"scripts":{"gif":"python3 scripts/encode_gif.py && pytest scripts/tests","probe":"python3 experiments/probe/run.py"}}\n', + }; + assert.deepEqual(checkHygiene(input(tooling)), []); + // A Python file nothing runs is dead code. + const dead = { ...tooling, "scripts/forgotten.py": "print(1)\n" }; + assert.match(checkHygiene(input(dead)).join("\n"), /scripts\/forgotten\.py: nothing in CI/u); + // The product stays TypeScript even when something would run the file. + const product: Record = { ...tooling, "apps/server/src/helper.py": "x = 1\n" }; + product["package.json"] = '{"scripts":{"h":"python3 apps/server/src/helper.py"}}\n'; + assert.match( + checkHygiene(input(product)).join("\n"), + /helper\.py: the product .* is TypeScript/u, + ); }); test("an opening comment may follow a shebang but not code", () => { diff --git a/scripts/check-hygiene.ts b/scripts/check-hygiene.ts index 4e027e50..6b75ff6d 100644 --- a/scripts/check-hygiene.ts +++ b/scripts/check-hygiene.ts @@ -24,15 +24,15 @@ const DATED_DOC = /(?:PLAN|ROADMAP|STATUS|WORKLOG|HANDOFF|CLEANUP|TODO)[^/]*\.md export const NESTED_RULES_MAX_LINES = 60; -/** Python sources and packaging files. P5 (ADR-0050) retired the last Python runtime. */ -const PYTHON_FILE = - /(?:^|\/)(?:[^/]+\.py|requirements[^/]*\.txt|pyproject\.toml|\.python-version)$/u; - /** - * Non-Python artifacts that must keep a Python-looking name. Empty by default; an entry needs a - * reason in the PR that adds it. + * Python is welcome at the edges (an SDK for Python users, sandbox or document tooling, small + * scripts), as in other TypeScript agent harnesses. The product itself stays one language: P5 + * (ADR-0050) moved the control plane, Worker and clients to TypeScript, and splitting them again + * is what made the repository incoherent. */ -export const PYTHON_NAME_ALLOWED: ReadonlySet = new Set(); +const PYTHON_SOURCE = /\.py$/u; +const TYPESCRIPT_ONLY = + /^(?:apps\/(?:server|web|desktop|node)|packages\/(?:protocol|domain|db|work))\//u; /** Paths written in backticks in living docs must exist. ADRs and research records are dated evidence. */ const PATH_ROOTS = "apps|packages|providers|plugins|scripts|docs|experiments|tests|deploy"; @@ -208,10 +208,34 @@ export function checkHygiene(input: HygieneInput): string[] { ); } - // 8. Python stays retired: the control plane, Worker and probes are TypeScript. - for (const file of files) - if (PYTHON_FILE.test(file) && !PYTHON_NAME_ALLOWED.has(file)) - failures.push(`${file}: Python was retired in P5 (ADR-0050); write this in TypeScript`); + // 8. Python only at the edges, and only while something still runs it. + const pythonRunners = files.filter( + // Evidence JSON only records hashes; package.json is the JSON that runs things. + (file) => + !file.endsWith(".md") && + (!file.endsWith(".json") || basename(file) === "package.json") && + !file.startsWith("docs/"), + ); + for (const file of files.filter((path) => PYTHON_SOURCE.test(path))) { + if (TYPESCRIPT_ONLY.test(file)) { + failures.push( + `${file}: the product (Server, Web, Desktop, Node and their shared packages) is TypeScript (ADR-0050); keep Python to SDKs, tooling and scripts`, + ); + continue; + } + const name = basename(file); + const folder = dirname(file); + const run = pythonRunners.some( + (runner) => + runner !== file && + (read(runner).includes(name) || + (name.startsWith("test_") && read(runner).includes(folder))), + ); + if (!run) + failures.push( + `${file}: nothing in CI, package.json, scripts or other code runs this Python file; wire it up or delete it`, + ); + } return failures; }