Releases: SanderMuller/richter
Release list
0.17.0
More precise change impact: a change to a class constant or enum case now pins to the code that reads it, instead of coarsely flagging the whole class.
Added
-
Class-constant and enum-case member references. A changed class constant or enum case could not be pinned to a member node — only methods could — so the impact walk seeded the whole class and reported "low confidence: a coarse class-level estimate". richter now gives constants and enum cases their own graph nodes and draws a
references-constantedge from every method that reads one, so a change to a constant or case pins to its actual readers and drops the low-confidence flag. Reads resolve to the constant's declaring class through the hierarchy, so a constant read viaself::/static::in a subclass still connects to the ancestor that declares it. The edge feeds the blast radius,richter:affected-tests, and the risk level (a constant read is a real value dependency). Documented under "Coverage beyond Brain".Scope, kept honest: a constant read whose owner can't be resolved to a scanned class (a dynamic
$var::CONST, a vendor constant) reads UNRESOLVED, never "no impact"; a trait constant, a property ($fillable/$casts), and a class-level modifier still resolve coarsely to the class.
Internal
GraphCacheformat version bumped: constants and enum cases are new graph nodes/edges, so warm caches invalidate rather than serving a pre-feature graph.- Suite: 831 tests / 1,942 assertions, including declaring-class resolution (inherited/interface constants), parameter-default and nested-anonymous-class reads, and a reachability check that a constant change pins to its readers without low confidence.
Full Changelog: v0.16.0...v0.17.0
0.16.0
Wider graph coverage: the change graph now follows polymorphic dispatch, so a concrete override reached only through an abstract class or interface is no longer invisible. Plus a plainer report.
Added
- Class-hierarchy analysis. A call that resolves to an abstract-class or interface method now also reaches the concrete overrides in every subclass/implementor, drawn as a new
overrideedge. This connects handlers chosen at runtime — a driver registered in a config array, a factory,app()->make($runtimeClass), constructor-injected polymorphism — that a static call graph otherwise leaves orphaned, so a change to one of their overrides no longer reads as "unreachable" or "no entry point". Reachability only: the edge feeds the blast radius andrichter:affected-tests, never the risk level (it is an over-approximated association, not a direct call). It reads only the class hierarchy — no configuration to add. Documented under "Coverage beyond Brain".
Changed
- Plainer report copy. The detect-changes report drops the em-dash asides and double negatives (
(advisory — not a gate),UNRESOLVED — not graphed, never "no impact") in favour of plain wording. TheUNRESOLVEDstatus token itself is unchanged — only the surrounding prose — and the--jsoncontract is untouched.
Internal
- The graph cache format version was bumped: the new
overrideedges change the graph for identical file inputs, so warm caches invalidate automatically rather than serving a pre-CHA graph. - Suite: 812 tests / 1,900 assertions, including class-hierarchy unit coverage, a graph-build reachability test, and a reachability-not-risk pin for the new edge.
Full Changelog: v0.15.1...v0.16.0
0.15.1
Internal
- HTML report escaping hardened — no output change. The
--htmlreport was already safe; this closes the fragilities that would let a future edit reintroduce a hole. Added adversarial coverage for the editor-linkhrefat both the unit and integration levels; routed every remaining interpolation in the blast-radius SVG through the central escape helper so the renderer keeps no silent exception to its own "escape everything" rule; and corrected the docblocks to state that the editorhrefis kept URL-safe byrawurlencodeplus a fixed scheme allow-list, with HTML-escaping only the attribute layer — not a substitute for encoding the URL. The rendered report is byte-for-byte identical. - Dropped the abandoned
rector/type-perfectdev dependency. Abandoned upstream, it had begun colliding with current PHPStan and broke the static-analysis job on every push. Removed it along with its two rules (null_over_false,narrow_return); the maintainedtomasvotruba/type-coveragestays. Dev-only — nothing a consumer installs changes. - The CHANGELOG decorator now sanitises the release body before prepending it, so internal markers no longer leak into
CHANGELOG.md. - Suite: 798 tests / 1,864 assertions.
Full Changelog: v0.15.0...v0.15.1
0.15.0
A new advisory payload-parity check plus a set of graph-build performance improvements. No breaking changes — every new behaviour is advisory or output-invariant, and the new config keys default to sensible values.
Added
- Payload-parity detection. A new advisory lane flags a model field added to
$fillable/$casts/casts()that is not mirrored into a resource which already mirrors the model's other fields — the exact shape behind a payload field silently going missing from an API response. It is advisory only: it never feeds the risk level,--fail-on, orrichter:affected-tests, only the report's findings list. Tunable via thepayload_parityconfig (enabled,mirror_threshold,ignore) and suppressible for one run with--no-payload-parity. - Parallel graph build. Every command that builds the graph now runs Brain's route-anchored analysis and richter's own source-tracers concurrently — the tracers run in a child
artisanprocess — instead of sequentially, shortening a cold build on a multi-core machine. The merged graph is identical to the serial build (edge order included); any child-process failure transparently falls back to the serial build, and--profileforces serial so the phase split stays measurable. Controlled by the newparallelconfig key (default on).
Fixed
richter:affected-testsnow fails closed on git-quoted untracked paths. An untracked file whose pathname git quotes (non-ASCII or special characters) is unquoted before the fail-closed check, so it can no longer slip past and yield a falsely-narrow test selection.
Internal
- Faster repeated fingerprinting. The graph cache's content fingerprint reuses a file's hash across repeated builds within one process (e.g. a long-lived MCP session re-checking a mostly-unchanged tree) when the file's stat signature — inode, size, mtime, and ctime — is unchanged and not racily-recent, skipping the re-read. The fingerprint value stays byte-identical to hashing every file: staleness is still designed out, not heuristically hoped out.
- Fewer entry-point traces. The entry-point tracer skips a method whose body contains none of the AST nodes Brain's
MethodTracerdraws an edge from, cutting redundant per-method traces with no change to the graph. - Suite: 796 tests / 1,858 assertions, including a byte-identical parallel-vs-serial build gate, fingerprint transparency + change-detection tests, and adversarial coverage of the worker payload validation.
Full Changelog: v0.14.0...v0.15.0
0.14.0
v0.14.0
Adds a payload-parity findings lane: richter now notices when a model field is added but never reaches the API Resource that renders it — the shape behind a setting that saves fine yet silently reverts on the client. Sourced from production dogfood, where this was the single most-reproduced defect class. Advisory only; nothing about the risk level, the --fail-on gate, or affected-tests changes.
Added
- Payload-parity detection. When a diff adds a name to a model's
$fillable,$casts, orcasts(), richter checks the resources that render that model and reports — under Findings — any that mirror the model's other fields but omit the newly added one. Findings are advisory strings only; they never feedrisk,--fail-on, oraffected-tests.- Candidate resources are matched by graph wiring first (the controllers/actions that touch the model and the resources they return), falling back to conventional names (
App\Http\Resources\PostResource,PostCollection, or the model name as a namespace segment) andApp\Transformersonly when nothing is wired. - Deliberately no-guess: the default
mirror_thresholdof1.0fires only on an exact mirror, and any resource whosetoArray()the parser cannot statically enumerate — a spread,array_merge,mergeWhen,parent::toArray(),only(), or a dynamic key — is skipped rather than guessed at. Constant-based field names and keys (Post::TITLE) are resolved on both sides.
- Candidate resources are matched by graph wiring first (the controllers/actions that touch the model and the resources they return), falling back to conventional names (
- Configuration:
payload_parity.{enabled, mirror_threshold, ignore}inconfig/richter.php. On by default.ignoresuppresses a specific field (App\Models\Post::internal_flag) or a whole resource (its FQCN). --no-payload-parityonrichter:detect-changesdisables the lane for a single run.expect_findingon benchmark cases (and a--expect-findingoption onrichter:benchmark:add) asserts that a replayed case surfaces a finding containing a given substring — scoring a checker's identification, not just the blast radius it elevates.
Compatibility
No breaking changes. The lane is purely additive: the --json contract is unchanged (findings remains a list<string>), and existing reports read identically with the lane disabled. A project that wants it off sets payload_parity.enabled to false or passes --no-payload-parity.
Full Changelog: v0.13.0...v0.14.0
0.12.0
Fixed
-
A dispatched command's handler is no longer a hidden caller. The dispatch tracer drew a
dispatcher → handleredge only when the dispatched or instantiated target looked like a queued job — namespaced underJobs\, or implementingShouldQueue. A resolveddispatch(new SomeCommand())whose target is aDispatchablecommand, or a plain self-handling command (ahandle()/__invoke()class with no queue trait, which Laravel still runs synchronously through the bus), drew no edge. A change to such a handler could then drop the dispatching action's test fromaffected-testsselection — but only when the graph contained no other unfollowable dispatch — andrichter:impactunder-reported the change's blast radius. Both now recognise the command handler as a real caller. -
A queued job with an unloadable ancestor no longer silently disappears from the graph. The previous job check swallowed autoload failures and concluded "not a job", so a
ShouldQueuejob whose parent class or trait could not be loaded drew no dispatch edge at all. The shared dispatch-target predicate now resolves that uncertainty toward drawing the edge, so an unclassifiable target is over-approximated (a caller is shown) rather than dropped.
Changed
richter:impactanddetect-changesmay report more reach — and occasionally a higher risk level — for code that dispatches commands. This release only ever adds edges to the graph, soaffected-testscan select the same tests or more, never fewer, and impact reports become more complete. If a change dispatchesDispatchableor self-handling commands, expect its reported reach to grow to include those handlers. This is the intended, more-honest behavior; it lands as a minor version because the output shifts for a real class of applications.
Internal
- The dispatch tracer and the
affected-testsdeterminability blocker now share one definition of "dispatch target", so edge-drawing and change-scoping recognise exactly the same set of shapes. - No cache-format bump was needed: the richter package version is part of the graph fingerprint, so upgrading invalidates any cached graph and rebuilds it once, automatically, on first use — the wider edge set appears immediately, no action required.
- Suite grows to 717 tests / 1,649 assertions, including new coverage that a resolved dispatch of a self-handling command and of a
Dispatchablecommand each draw the handler edge, and that a genuinely non-dispatchable class still draws none.
Full Changelog: v0.11.1...v0.12.0
0.11.1
Fixed
-
affected-testsis usable on real applications again. An unfollowable job/command dispatch anywhere in the graph previously made every change undeterminable ("run the full suite"), because the "unfollowable dispatch" signal was graph-global. It is now change-scoped: an unfollowable dispatch only blocks a change that could actually be reached through it — i.e. when a possible dispatch target (a queued job, aDispatchablecommand, or a plain self-handlinghandle()/__invoke()command) sits in the change's caller closure, or is the changed class itself. A change with no dispatch target upstream — a read-only controller path, a model method, a Livewire component — now narrows to the tests it can reach.This never trades away safety. The signal that a file could not be parsed at all is kept as a separate, global block (an unreadable file could hide anything), and the scoped-dispatch rule fails toward blocking on any uncertainty. The one narrow, documented gap — a command wired through
Bus::mapto a separate handler with none of the recognised markers — only affects a codebase with no unfollowable dispatch at all, and is a pre-existing analysis limitation, not a regression.
Internal
- The graph's on-disk cache format is bumped, so a consumer's cached graph rebuilds once, automatically, on first use after upgrading — no action required.
- Suite grows to 715 tests / 1,647 assertions, including five adversarial guards that pin every path where the scoped selection must still block (a changed job, a job/command reached by the change, an unclassifiable caller, an unparseable file, and the unlock case where it correctly narrows).
Full Changelog: v0.11.0...v0.11.1
0.11.0
Added
richter:detect-changes --html=<path>. Writes the report as one self-contained HTML file — every style and script inline, nothing fetched — so it opens offline fromfile://and travels as a CI artifact you can link from a pull request. Five tabs: Overview (a Files / Impacted / Depth / Risk stat row, the reached entry points, and what to focus on), Graph (the blast radius drawn as concentric rings, one per BFS depth from the change, with hover/focus tooltips and connected-edge highlighting), Paths (how each entry point reaches the change), Changes (the member-level diff, naming the member that drove a low-confidence verdict), and Advisory (findings, route security issues, test references, and the gate).--openlaunches it in the default browser afterwards; a failing opener is a warning, never a failed run. The diagram caps at 300 nodes and says so when it does — the counts above it are never capped. It composes with--fail-on:--htmlreplaces the text report on stdout but never touches the gate or the exit code. The HTML is a rendering surface, not a contract — its markup may change in any release;--jsonremains the semver-governed machine output.- Clickable editor links in the report. Every
file:lineopens your editor at that line.richter.editorreuses debugbar's / Ignition's env chain (CODE_EDITOR, thenDEBUGBAR_EDITOR, thenIGNITION_EDITOR) and, like debugbar, defaults tophpstorm, so an existing setup needs no new variable. PhpStorm, the VS Code family (vscode,vscode-insiders,vscode-remote,vscodium), Sublime, TextMate, Emacs, MacVim, Atom, Nova, NetBeans, and Xdebug are supported. Setrichter.editortonullto keep the file references plain text — worth doing for a shared CI artifact, where a link would point every reader at an absolute path only present on the machine that generated the report.
Internal
- The blast-radius diagram is laid out in PHP — depth is the radius — so it is deterministic and snapshot-testable, and the package still ships no JavaScript build step. The graph the report draws is carried alongside the report only:
--jsonand the MCP output schema are byte-unchanged. - Suite grows to 700 tests / 1,613 assertions.
Full Changelog: v0.10.0...v0.11.0
0.10.0
A precision-and-completeness release sourced from a full-feature dogfood of 0.9.0 against a large production Laravel application. richter:affected-tests now reflects your working tree and never silently narrows past a change it cannot see; the frontend bridge only treats genuine HTTP/route calls as route references; and Pennant feature flags are recognised behind the common enum-wrapper convention. All changes are additive or behaviour-refining — there are no breaking changes.
Added
richter.feature_gate_methods. Recognise feature-flag checks written as an enum wrapper (FeatureToggle::SomeFlag->isActive()), not only theLaravel\Pennant\Featurefacade or the@featureBlade directive. Register your project'sEnum\Class::methodwrappers and a change behind one is annotated as flag-gated. Annotation only — it never feedsrisk, the--fail-ongate, oraffected-testsselection.richter.frontend.http_callees. A frontend string literal counts as a backend route reference only when it is the first argument of an HTTP/route callee. The built-in allowlist coversroute,fetch,axios,useFetch,$http,$(jQuery),window, andpage/cy(Playwright / Cypress spec navigation); register custom HTTP wrappers through config. This removes false route seeds from unrelated calls such astranslate('/settings')orconsole.log('/…')that previously inflated the touched-route surface.
Changed
HEAD-mode diffs now analyse the working tree.detect-changesandaffected-testscompare the working tree against the merge-base with--base, so uncommitted and staged edits are included — running either before you commit now sees your actual changes rather than only what is committed. Passing an explicit non-HEADref still replays that ref's committed tree, so historical and benchmark replays are unchanged, and CI (which checks out clean) is unaffected.
Fixed
affected-testsno longer silently under-selects around a file it cannot diff. An untracked (nevergit add-ed) file underapp/,resources/views/, or a configured frontend root is invisible togit diff; the selection now fails closed (exit 2 — "run the full suite") instead of emitting a narrowed set that omits it.git addthe file to include it.detect-changeskeeps its advisory stderr note.- Hunk/source desync in
HEADmode. With uncommitted edits stacked on committed ones, added/removed line numbers and member spans now come from a single tree, so a hunk can no longer map to the wrong member.
Documentation
- Exposure classification (
[public]/[authed]/[admin]) is route-only. A Livewire, Filament, or queue entry point carrying no exposure tag means "not classified," never "public" or "unauthenticated" — its real exposure comes from mount-time authorization, middleware, or route placement the graph does not model.
Internal
- Test fixtures were migrated to a neutral, synthetic domain, and a guideline was added to keep fixtures, documentation examples, and specs free of any consumer's product vocabulary. Development scaffolding (
plans/,specs/) is now excluded from the Composer dist archive. - Suite grows to 608 tests / 1,351 assertions.
Full Changelog: v0.9.0...v0.10.0
0.9.0
Added
- Assertion-graded test references. A reached entry point that a test references but whose referencing tests contain no behavioural assertion the scan recognises is now tagged
[test-referenced — no behavioural assertion found](text),🟡 test-referenced, no behavioural assertion found(markdown), and carries"referenced-no-behavioural-assertion"in the newentryPointTestReferencesJSON/MCP map. The grade is per file and certainty-gated: a file counts as assertion-weak only when every assert-ish call in it is a provable smoke form (assertOk,assertSuccessful,assertStatus(200),assertTrue(true)) or it has none — any behavioural or unrecognised assertion, or a status check that carries meaning (assertStatus(403),assertForbidden, an authorization test's own claim), leaves it plain[test-referenced]. Uncertainty always collapses to the weaker claim, never to the sub-tag: a false "proves nothing" would wrongly discredit a real test. It is advisory annotation only — never an input torisk, a--fail-ongate, orrichter:affected-testsselection. entryPointTestReferencesin--jsonand MCP structured content. Per reached entry point,"referenced"/"referenced-no-behavioural-assertion"/"unreferenced"; an entry point whose reference state cannot be determined is omitted from the map.richter:detect-changes --profile. Forces a fresh build and prints a phase-by-phase timing split (Brain analysis, canonicalisation, the consolidated tracer pass, entry-point tracing, Blade tracers, rewrites) to stderr, so--jsonand--markdownstdout stay a single clean document. It answers where a build's wall-clock actually goes on a given codebase.
Internal
- Suite grows from 562 to 580 tests: per-file assertion grading (smoke-form vs behavioural, authorization-status and Pest
expectedge cases), the profile phase-event sequence, and the--profileoutput-contract coexistence with--json. - Build phase timings ride the existing
onProgresscallback (richter:phaseevents), zero-cost when no listener is attached.
Full Changelog: v0.8.0...v0.9.0