Found while closing a Codex review finding on #752. Two halves of one defect; the second is the one that reaches CI.
1. The generator does not delete outputs it no longer produces
python/scripts/generate_services.py writes the current modules and rewrites __init__.py, and stops there. The other three generators all sweep first:
| generator |
stale handling |
typescript/scripts/generate-services.ts:1736-1741 |
fs.unlinkSync any .ts not in the generated set |
ruby/scripts/generate-services.rb:344-353 |
deletes any *_service.rb carrying @generated and not regenerated |
kotlin/generator/.../Main.kt:65 |
wipes the services dir before writing |
python/scripts/generate_services.py |
nothing |
So removing or renaming a service in the Python mapping leaves the old module on disk, and make py-generate-services will never clear it.
2. The Python CI drift step is blind to the leftover
The local script and the CI step are not the same check.
scripts/check-python-service-drift.sh regenerates into a tmpdir and diff -rqs against a copy of the committed tree. That reports Only in <committed>: fanfares.py and fails. Verified:
$ printf '# @generated stale module\n' > python/src/basecamp/generated/services/fanfares.py
$ make py-check-drift
ERROR: Generated services are out of date. Run 'make py-generate-services'
Only in /var/folders/.../services_committed: fanfares.py
REAL_EXIT=2
The Python CI job (.github/workflows/test.yml, "Check generated code drift") does not run that script. It regenerates in place and tests git status --porcelain -- src/basecamp/generated/. A stale file that is already committed produces no diff — nothing rewrites it, nothing deletes it — so there is nothing for git status to report. Verified in a throwaway repo reproducing the sequence:
PORCELAIN_LINES=0 <-- CI passes
stale file still present: fanfares.py keep.py
A Python mapping removal therefore reaches main with green CI. Only a developer running the full local make would see it.
This also trips the house rule that drift gates must regenerate out-of-tree rather than in-place (an in-place regeneration mutates the working tree it is judging).
Remedy
- Give
generate_services.py the sweep the other three have. It must skip _base.py and _async_base.py, which are hand-written infrastructure living under generated/ by exception (AGENTS.md Hard Rule 1) — Ruby's @generated-marker guard is the safest pattern to copy.
- Point the CI step at
scripts/check-python-service-drift.sh so CI and make run the same check, instead of a weaker in-place variant.
Already worked around, but only for one reader
scripts/check-service-inventory-parity (#752) reads Python from its generated __init__.py barrel rather than the directory, precisely because the barrel is rewritten whole and cannot name a corpse. That is a workaround in one gate, not a fix.
python/tests/test_client.py::TestGroupedClientAccessorInventory (#748) still enumerates the directory — but it fails safe: a stale module makes it demand an accessor for a service that no longer exists, which is red, not green. Worth leaving as-is until the generator is fixed.
Found while closing a Codex review finding on #752. Two halves of one defect; the second is the one that reaches CI.
1. The generator does not delete outputs it no longer produces
python/scripts/generate_services.pywrites the current modules and rewrites__init__.py, and stops there. The other three generators all sweep first:typescript/scripts/generate-services.ts:1736-1741fs.unlinkSyncany.tsnot in the generated setruby/scripts/generate-services.rb:344-353*_service.rbcarrying@generatedand not regeneratedkotlin/generator/.../Main.kt:65python/scripts/generate_services.pySo removing or renaming a service in the Python mapping leaves the old module on disk, and
make py-generate-serviceswill never clear it.2. The Python CI drift step is blind to the leftover
The local script and the CI step are not the same check.
scripts/check-python-service-drift.shregenerates into a tmpdir anddiff -rqs against a copy of the committed tree. That reportsOnly in <committed>: fanfares.pyand fails. Verified:The Python CI job (
.github/workflows/test.yml, "Check generated code drift") does not run that script. It regenerates in place and testsgit status --porcelain -- src/basecamp/generated/. A stale file that is already committed produces no diff — nothing rewrites it, nothing deletes it — so there is nothing forgit statusto report. Verified in a throwaway repo reproducing the sequence:A Python mapping removal therefore reaches
mainwith green CI. Only a developer running the full localmakewould see it.This also trips the house rule that drift gates must regenerate out-of-tree rather than in-place (an in-place regeneration mutates the working tree it is judging).
Remedy
generate_services.pythe sweep the other three have. It must skip_base.pyand_async_base.py, which are hand-written infrastructure living undergenerated/by exception (AGENTS.md Hard Rule 1) — Ruby's@generated-marker guard is the safest pattern to copy.scripts/check-python-service-drift.shso CI andmakerun the same check, instead of a weaker in-place variant.Already worked around, but only for one reader
scripts/check-service-inventory-parity(#752) reads Python from its generated__init__.pybarrel rather than the directory, precisely because the barrel is rewritten whole and cannot name a corpse. That is a workaround in one gate, not a fix.python/tests/test_client.py::TestGroupedClientAccessorInventory(#748) still enumerates the directory — but it fails safe: a stale module makes it demand an accessor for a service that no longer exists, which is red, not green. Worth leaving as-is until the generator is fixed.