Skip to content
Open
321 changes: 278 additions & 43 deletions .dev-loop/INGEST_REPORT.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions log.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ Append-only. Format: `## [YYYY-MM-DD] <ingest|revise|lint|gap|contradiction|drif
## [2026-08-06] revise | Union-merged amendments from the same 15 PRs into 24 existing pages (largest: tests-that-cannot-fail — 7 PRs: per-assertion mutation granularity, restore-mechanism-by-commit-state, suite-total-drop detection; portable-shell-scripts — 8 PRs: POSIX inline set-- reordering, ${VAR:-} vs ${VAR-} colon semantics, quote-by-what-the-text-is; test-data-and-isolation — 6 PRs: env-derived write paths, absent-variable cases, exec-bit fixtures, leak attribution; change-impact/call-site-enumeration — 8 unique additions from 7 path-variants: rename-in-place, same-type reorder, *args forwarding, two-searches-agree fallacy, codemod, coverage completion check). All non-canonical cross-page ids remapped to canonical pages; every added line traced to its source PR hunk (fabricated worker output was detected by orphan-line verification and replaced with true PR content).
## [2026-08-06] dedup | Cross-PR duplicate clusters collapsed: client-side rate limiting (8 PR versions at 3 paths → 1), call-site enumeration (7 versions at 7 paths → additions folded into the already-merged canonical page), stderr/exit-0 diagnostics (4 versions at 4 paths → 1), macOS sysroot (2→1), env-var off switch (2→1), completion predicates (2→1), robots.txt (2→1), harness-mediated results (2→1), leaked test artifacts (2→1), orchestration category naming (orchestration vs agent-orchestration → agent-orchestration). Kept distinct after trigger comparison: differential-testing vs differential-run-agreement (setup vs interpretation), unset-versus-empty-parameters vs env-var-off-switches (semantics vs design, cross-linked), import-time-side-effects vs test-level-choice edge (tactics vs level choice, cross-linked). Merged-main near-dup scan (Jaccard over title+trigger, 141 pages): 0 pairs — no duplication among previously merged content.
## [2026-08-06] ingest | Reconciled post-consolidation flush PRs #42–#43 (created against pre-#44 main). Folded unique content: platforms/filesystems/permissions-and-exec-bits (+`sh "$SCRIPT"` interpreter-invocation rows — stubs need no exec bit, avoids EDR chmod+x heuristics), platforms/shells/command-text-inspected-before-execution (+gate-AUTHOR step 8: parse all three POSIX quoting forms, expand only ~/$HOME/${HOME}; red-then-green bats evidence), testing/quality/checks-that-cannot-pass (+vacuously-green pre-implementation test row), platforms/processes/tool-diagnostics-without-a-failing-exit-code (+repeat-suppression edge, +clang reproduction source), infrastructure/ci-cd/secrets-handling (+independent credential channels row: gh token vs git SSH vs API token, git push --dry-run / author:@me probes), testing/mocking/what-to-mock (+ESM read-only-bindings DI rows, +PATH-emptied tripwire negative-proof). Dropped as duplicates of the #44 state: #42's rate-limit row (already in timeouts-and-retries + client-side-rate-limiting), #42's warning-only-diagnostics page (same trigger as tool-diagnostics-without-a-failing-exit-code), #42's leak-attribution row (already in test-data-and-isolation).
## [2026-08-07] ingest | knowledge-flush of 4 queued insights — 4 new pages, 7 existing pages cross-linked. New: testing/quality/surviving-mutant-equivalence-triage (classify a live mutant as missing-test / equivalent / no-coverage before writing anything; when equivalent, delete the redundant branch and correct the justification comment that the equivalence just disproved — Stryker's documented remedy is "try to rewrite the code so it won't occur"), testing/quality/source-text-wiring-assertions (a `>= n` / `toHaveLength(n)` count assertion over a call appearing at several sites stays green when the one site the guard was written for is deleted — enumerate the sites and bind each to a bounded order anchor that occurs exactly once in the file, or to a function-body slice; prove each by deleting only its own site, and run a reformat control. Measured: greedy vs lazy quantifiers give identical verdicts, so the bound and the anchor's uniqueness are what constrain the match), frontend/data-fetching/query-state-vs-fetch-state (a `data | undefined` component prop collapses TanStack Query's two orthogonal axes; a disabled or offline-paused query is `status: pending` with `isLoading === false` and `isError === false`, so "undefined means loading" renders a spinner no fetch will resolve — pass status+fetchStatus or an explicit union and test one case per cell), backend/python/language/default-encoding-in-text-io (a byte round-trip cannot prove an `encoding=` fix on a UTF-8 locale — run the real entry point under `-X warn_default_encoding -W always::EncodingWarning` and assert zero warning lines naming that file). Merged into existing: tests-that-cannot-fail (whole-suite edge row now routes surviving mutants through classification instead of reading them all as missing tests), harness-reverse-controls, behavior-not-implementation, guard-shape-vs-consequence, async-ui-states (+disabled/paused edge row), bytecode-cache-staleness, timezone-and-locale — related links both ways. All cited URLs opened this session; two local reproductions (CPython 3.14.6 EncodingWarning discriminator vs byte-identical round-trip; `@tanstack/query-core@5.100.14` queryObserver.js:308-332 `isLoading = isPending && isFetching`).
## [2026-08-07] revise | testing/quality/guard-shape-vs-consequence — corrected a misattributed citation found by the pre-PR adversarial pass: the sentence "you cannot safely refactor code if you know you need to adapt the tests afterwards to get them passing again" was presented as the Google Testing Blog article's own, but re-fetching the page shows it is a reader comment (2015-02-04) with different wording ("refactor stuff", "know for sure"). The article body was not retrievable in full, so the bullet now cites the URL for the change-detector category without quoting it, and states the correction inline. The same quote had been copied into a new page in this flush before verification — the lesson being that a citation already present in the wiki is not a verified citation.

## [2026-08-10] ingest | Folded one queued insight into this PR's page rather than opening a sibling: testing/quality/source-text-wiring-assertions +step 2 (make the assertion's subject the comment-stripped file, taking comment ranges from the language's tokenizer — ESLint: "While comments are not technically part of the AST", so a rule reaches them via `sourceCode.getAllComments()` instead of node traversal), +4 edge-case rows (comment-only change reddens the guard; negative `not.toMatch` and count assertions are the shapes comments flip; no parser available; the code under test is itself about comments), +3 Instead-of rows, and a step-6 note that with step 2 the comment control is green by construction so a red one indicts the stripper. Verification corrected the queued directive: measured 2026-08-10 in Node, `src.replace(/\/\*[\s\S]*?\*\//g,'').replace(/\/\/.*$/gm,'')` truncates `"https://api.example.com//v2/items"` to `"https:` and a string-aware variant still truncates `/a//b/` to `/a` — so the page prescribes the tokenizer and records the regex forms only as Instead-of rows with that measurement. Field basis: three guards in one rtb-unified PR reddened on correct code from a JSDoc, a `Number.isFinite` comment, and a `SOURCE.slice(...)` comment. Independent adversarial review then corrected the fold itself: `ts.createSourceFile` yields `comments === undefined` and no comment nodes (so it strips nothing — step 2 now names `getLeadingCommentRanges`/`espree`/`@babel/parser`), the step-6 "green by construction" claim covered only the comment half (a reformat still reddens the bound), the no-parser fallback had to cut each line to EOL because filtering whole comment lines misses trailing comments, and N must be measured on the stripped subject.
## [2026-08-11] revise | Folded one queued insight into this PR's page rather than opening a sibling: backend/python/language/bytecode-cache-staleness — the `-B`/`PYTHONDONTWRITEBYTECODE` edge row is corrected (both options govern *writing* only, per docs.python.org/3/using/cmdline.html, so a `.pyc` from an earlier run is still read and the failure survives; purge `__pycache__` once before the run and keep `-B` for the rest), and a new row covers the fresh-spec import path (`spec_from_file_location` + `module_from_spec` + `exec_module` bypasses `sys.modules`, not the on-disk cache; `exec(compile(text, path, "exec"), ns)` consults no cache). Measured 2026-08-11 on Python 3.14.6/macOS: a same-length rewrite under a re-pinned mtime returned the stale value through the fresh-spec loader, and again under `python3 -B` with the cache present.
1 change: 1 addition & 0 deletions wiki/backend/python/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ Match your situation to a "load when" line; load only matching pages.
|------|-----------|
| [mutable-state-traps](language/mutable-state-traps.md) | State persists or leaks across calls/requests in a long-lived Python process — one user's data appears for another, values "remembered" between calls; loop-built callbacks all use the last value; reviewing function signatures (mutable defaults), class bodies (class attributes), or module-level objects for hidden sharing; choosing contextvars vs thread-locals for request context |
| [bytecode-cache-staleness](language/bytecode-cache-staleness.md) | A script or harness rewrites `.py` files and re-runs them in a loop (mutation testing, edit/test/revert, codegen check, bisect) and the result stops tracking what is on disk — a revert that `git diff` reports clean still fails, or an injected change has no effect; choosing between clearing `__pycache__`, refreshing mtime, and hash-based `.pyc` (PEP 552); designing byte-length-preserving mutations |
| [default-encoding-in-text-io](language/default-encoding-in-text-io.md) | Python opens a text file without `encoding=` (`open`, `Path.read_text`, `subprocess` text mode) and you are adding the argument or writing the regression test that keeps it there; a file-writing bug reproduces on Windows, a `LANG=C` container, or a cp949/cp932 desktop but not on your machine; choosing a test discriminator that does not depend on the runner's locale |
14 changes: 11 additions & 3 deletions wiki/backend/python/language/bytecode-cache-staleness.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ sources:
- https://docs.python.org/3/reference/import.html
- https://peps.python.org/pep-0552/
- https://docs.python.org/3/library/py_compile.html
last_verified: 2026-08-04
related: [testing-quality-harness-reverse-controls, testing-quality-tests-that-cannot-fail, backend-python-language-mutable-state-traps]
- https://docs.python.org/3/library/shutil.html
- https://docs.python.org/3/using/cmdline.html
last_verified: 2026-08-11
related: [backend-python-language-default-encoding-in-text-io, testing-quality-harness-reverse-controls, testing-quality-tests-that-cannot-fail, backend-python-language-mutable-state-traps]
---

# Edited Python Source the Interpreter Keeps Ignoring
Expand Down Expand Up @@ -55,7 +57,9 @@ no effect at all.
| The mutation is the thing that vanished (injected change has no effect) and the revert looks fine | Same mechanism, opposite direction: the cache predates both writes. Clear `__pycache__` and re-inject, then confirm the mutation *does* change behavior before scoring it as "caught" ([testing-quality-harness-reverse-controls]) |
| The harness reports every mutant caught | Verify one mutation reaches the interpreter by hand first — a stale cache that pins the *original* bytecode makes every mutant look survived, and one that pins a *mutant* makes every later case look caught |
| Writes are driven by a tool that preserves mtime (`rsync -t`, archive extraction, `git checkout` of an unchanged blob, `touch -t` in a script) | The second-granularity race becomes a certainty rather than a race; use hash-based `.pyc` or clear the cache unconditionally |
| The tree is read-only or `PYTHONDONTWRITEBYTECODE` is set | No `.pyc` is written, so this failure cannot occur — and the harness pays a recompile per run |
| The harness backs the file up and restores it with `shutil.copy2` | `copy2` "also attempts to preserve file metadata" via `copystat`, so the restore stamps the *original* mtime back — the same certainty as the row above, reached through the idiomatic backup/restore call. Apply that row's remedy (hash-based `.pyc`, or clear `__pycache__` unconditionally), and switch the restore to `shutil.copyfile` ("no metadata") or `shutil.copy` ("the file's creation and modification times, is not preserved") so the write at least stops re-stamping the old timestamp. A bare `os.utime(path, None)` after the restore is not sufficient on its own: it sets mtime to *now*, which collides again whenever the cached compile happened in the same second |
| The tree is read-only, or the harness runs under `-B` / `PYTHONDONTWRITEBYTECODE` | Both settings govern writing only — Python "won't try to write `.pyc` files on the import of source modules" — so a `.pyc` left on disk by an earlier run is still validated and reused, and this failure still occurs. Purge `__pycache__` once before the run and keep `-B` set for the rest of it: with nothing cached and nothing written, every iteration recompiles from source |
| The harness re-imports the mutated file through a fresh `importlib.util.spec_from_file_location` + `module_from_spec` + `exec_module` each iteration | A fresh spec and module object bypass `sys.modules`, not the on-disk cache: the source loader still validates `__pycache__` and reuses it. Purge the cache (or compile with hash-based invalidation) between iterations, or read the file yourself and `exec(compile(text, path, "exec"), ns)`, which consults no cache |
| The stale module was already imported in a long-lived process | Clearing `__pycache__` does not help; the module object is in `sys.modules` and only a fresh process (or an explicit reload) picks the change up |
| An installed package ships `.pyc` files without sources | The unchecked-hash variant is assumed valid whenever it exists; edits to a co-located source are never consulted |

Expand All @@ -73,4 +77,8 @@ no effect at all.
- https://docs.python.org/3/reference/import.html — "By default, Python does this by storing the source's last-modified timestamp and size in the cache file when writing it"; "At runtime, the import system then validates the cache file by checking the stored metadata in the cache file against the source's metadata"; hash-based `.pyc` files store "a hash of the source file's contents rather than its metadata", in checked and unchecked variants, overridable with `--check-hash-based-pycs`
- https://peps.python.org/pep-0552/ — hash-based `.pyc` invalidation, added in Python 3.7, as the deterministic alternative to timestamp+size
- https://docs.python.org/3/library/py_compile.html — `PycInvalidationMode` selects timestamp, checked-hash, or unchecked-hash invalidation when compiling
- https://docs.python.org/3/using/cmdline.html — `-B`: "If given, Python won't try to write `.pyc` files on the import of source modules"; `PYTHONDONTWRITEBYTECODE` "is equivalent to specifying the `-B` option" — both describe writing only, so neither stops an existing `.pyc` from being read
- https://docs.python.org/3/library/shutil.html — `copyfile` copies "the contents (no metadata)"; `copy` copies data and permission mode and "Other metadata, like the file's creation and modification times, is not preserved"; `copy2` is "Identical to `copy()` except that `copy2()` also attempts to preserve file metadata" and "uses `copystat()` to copy the file metadata" — so `copy2` is the mtime-restoring member of the family
- Field reproduction 2026-08-11 (batch mutation harness, one process, byte-length-preserving mutation of a numeric literal, restore via `shutil.copy2`): three consecutive mutants scored GREEN in the batch and the third scored RED when run alone; printing the mutated constant from a fresh subprocess showed all three runs loading the *first* mutant's value. Purging `__pycache__` and calling `os.utime(path, None)` between iterations flipped the third to RED while a no-op control mutation stayed GREEN
- Field reproduction 2026-08-11 (Python 3.14.6, macOS, fresh-spec import path): `mod.py` holding `VERSION = "3.1.0"` was pinned to a fixed mtime and loaded via `spec_from_file_location` + `module_from_spec` + `exec_module`, which wrote `__pycache__/mod.cpython-314.pyc`. Rewriting the file to the same-length `"3.1.1"` and re-pinning the same mtime, the identical loader printed `3.1.0`; re-running under `python3 -B` with that `.pyc` still present also printed `3.1.0`. A fresh spec and `-B` each leave stale bytecode in play
- Field reproduction 2026-08-04 (Python 3.14.6, macOS): with `mod.py` pinned to a fixed mtime via `touch -t` and every revision exactly 18 bytes, compiling `VERSION = "3.1.1"` and then reverting the file to `VERSION = "3.1.0"` left `import mod` reporting `3.1.1` — reverted source, mutant bytecode. Deleting `__pycache__` returned `3.1.0`; a bare `touch mod.py` (mtime bumped, cache left in place) also returned `3.1.0`. The `.pyc` header decoded to `flags=0` (timestamp invalidation) with the source's exact mtime and `size=18`
Loading
Loading