Skip to content

Stop validator cache from freezing transient engine failures + add TTL#272

Merged
fdaviddpt merged 1 commit into
masterfrom
fix/validator-cache-poisoning-ttl
Jun 1, 2026
Merged

Stop validator cache from freezing transient engine failures + add TTL#272
fdaviddpt merged 1 commit into
masterfrom
fix/validator-cache-poisoning-ttl

Conversation

@fdaviddpt
Copy link
Copy Markdown
Contributor

Context

validate:...Test.php:rector was persistently reporting System error: "ClassReflection must be resolved for class XTest" — on Kevin and locally — always with an identical frozen duration.

Root cause (proven): mcp-rector-warm's long-lived daemon intermittently trips rector's own known reflection bug on test classes. It's warm-process-state dependent, not file dependent — a clean re-run and plain rector CLI reflect the same file fine. But the failed result got written to the validator cache (keyed on file-content hash, with its duration_ms), so every later run on the unchanged file replayed the stale failure. 2100 poisoned entries were found across a test suite. The identical frozen duration was the tell — live warm runs are ~200–500ms, never the cached number.

Changes

Fix (two layers):

  • validators/rector-mcp/rector-mcp.py — drop rector System error: results at the source. An engine/reflection glitch is not a code finding.
  • supertool.py _validator_result_is_cacheable() — never cache non-deterministic engine failures (codes mcp / orchestrator / rector.exit, or System error: messages). Real findings (PHPStan types, rector.refactor) stay cached.

Defense in depth (TTL):

  • supertool.py _validator_cache_read() — entries expire validator_cache_ttl_hours after write (default 24; 0 disables). The cache key only hashes file content, so an entry can outlive changes it can't see — an updated adapter, a changed rector.php, or a transient failure. Expiry is on access (stale → miss → re-run → rewrite); no cron. Applies to all validators — the read path is the single chokepoint.

Tests + docs:

  • tests/test_security_hardening_150.pyTestValidatorCacheTtl (fresh hit / expired miss / ttl=0 disables) + TestValidatorResultIsCacheable (ok & real findings cacheable; rector System error, mcp, exit not).
  • docs/validators.md, README.md, .supertool.example.json, CHANGELOG.md.

Test plan

  • Full suite: 3093 passed, 34 skipped, coverage 87.57% (≥ 86% gate).
  • Manual: validate:...Test.php:rector returns ok with cache on; plain rector CLI confirmed clean on the same file; purged the 2100 stale entries locally.

🤖 Generated by Max — turns out the bug was the cache remembering a lie.

rector-mcp's warm daemon intermittently trips rector's own
"System error: ClassReflection must be resolved for class XTest" reflection
bug on test classes — warm-process-state dependent, not file dependent (a
clean re-run and plain rector CLI pass the same file). That failure was
cached keyed on file content and replayed on every later run, frozen
duration and all; 2100 entries were poisoned this way across a test suite.

- rector-mcp.py: drop rector `System error:` results at the source — an
  engine glitch is not a code finding.
- supertool.py `_validator_result_is_cacheable()`: never cache
  non-deterministic engine failures (mcp/orchestrator/rector.exit codes,
  `System error:` messages). Real findings stay cached.
- supertool.py `_validator_cache_read()`: TTL on entries
  (`validator_cache_ttl_hours`, default 24, 0=off). The key only hashes file
  content, so an entry can outlive an updated adapter/rector.php or a
  transient failure. Expire on access, self-healing.
- Tests for both behaviors; docs + CHANGELOG + example config.

Co-Authored-By: Max <noreply>
@fdaviddpt fdaviddpt merged commit cf54766 into master Jun 1, 2026
12 checks passed
@fdaviddpt fdaviddpt deleted the fix/validator-cache-poisoning-ttl branch June 1, 2026 06:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant