Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/research/repository-hygiene-guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 21 additions & 14 deletions scripts/check-hygiene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
...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<string, string> = { ...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", () => {
Expand Down
46 changes: 35 additions & 11 deletions scripts/check-hygiene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new Set<string>();
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";
Expand Down Expand Up @@ -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;
}
Expand Down
Loading