Skip to content

Releases: SanderMuller/richter

0.17.0

Choose a tag to compare

@SanderMuller SanderMuller released this 01 Aug 06:47
Immutable release. Only release title and notes can be modified.

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-constant edge 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 via self::/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

  • GraphCache format 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

Choose a tag to compare

@SanderMuller SanderMuller released this 31 Jul 21:58
Immutable release. Only release title and notes can be modified.

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 override edge. 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 and richter: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. The UNRESOLVED status token itself is unchanged — only the surrounding prose — and the --json contract is untouched.

Internal

  • The graph cache format version was bumped: the new override edges 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

Choose a tag to compare

@SanderMuller SanderMuller released this 31 Jul 18:48
Immutable release. Only release title and notes can be modified.

Internal

  • HTML report escaping hardened — no output change. The --html report was already safe; this closes the fragilities that would let a future edit reintroduce a hole. Added adversarial coverage for the editor-link href at 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 editor href is kept URL-safe by rawurlencode plus 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-perfect dev 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 maintained tomasvotruba/type-coverage stays. 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

Choose a tag to compare

@SanderMuller SanderMuller released this 25 Jul 09:48
Immutable release. Only release title and notes can be modified.

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, or richter:affected-tests, only the report's findings list. Tunable via the payload_parity config (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 artisan process — 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 --profile forces serial so the phase split stays measurable. Controlled by the new parallel config key (default on).

Fixed

  • richter:affected-tests now 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 MethodTracer draws 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

Choose a tag to compare

@SanderMuller SanderMuller released this 24 Jul 09:24
Immutable release. Only release title and notes can be modified.
f281c2c

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, or casts(), 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 feed risk, --fail-on, or affected-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) and App\Transformers only when nothing is wired.
    • Deliberately no-guess: the default mirror_threshold of 1.0 fires only on an exact mirror, and any resource whose toArray() 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.
  • Configuration: payload_parity.{enabled, mirror_threshold, ignore} in config/richter.php. On by default. ignore suppresses a specific field (App\Models\Post::internal_flag) or a whole resource (its FQCN).
  • --no-payload-parity on richter:detect-changes disables the lane for a single run.
  • expect_finding on benchmark cases (and a --expect-finding option on richter: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

Choose a tag to compare

@SanderMuller SanderMuller released this 22 Jul 15:03
Immutable release. Only release title and notes can be modified.

Fixed

  • A dispatched command's handler is no longer a hidden caller. The dispatch tracer drew a dispatcher → handler edge only when the dispatched or instantiated target looked like a queued job — namespaced under Jobs\, or implementing ShouldQueue. A resolved dispatch(new SomeCommand()) whose target is a Dispatchable command, or a plain self-handling command (a handle()/__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 from affected-tests selection — but only when the graph contained no other unfollowable dispatch — and richter:impact under-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 ShouldQueue job 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:impact and detect-changes may report more reach — and occasionally a higher risk level — for code that dispatches commands. This release only ever adds edges to the graph, so affected-tests can select the same tests or more, never fewer, and impact reports become more complete. If a change dispatches Dispatchable or 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-tests determinability 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 Dispatchable command 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

Choose a tag to compare

@SanderMuller SanderMuller released this 22 Jul 14:03
Immutable release. Only release title and notes can be modified.

Fixed

  • affected-tests is 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, a Dispatchable command, or a plain self-handling handle()/__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::map to 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

Choose a tag to compare

@SanderMuller SanderMuller released this 21 Jul 19:17
Immutable release. Only release title and notes can be modified.
cc1cabb

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 from file:// 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). --open launches 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: --html replaces 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; --json remains the semver-governed machine output.
  • Clickable editor links in the report. Every file:line opens your editor at that line. richter.editor reuses debugbar's / Ignition's env chain (CODE_EDITOR, then DEBUGBAR_EDITOR, then IGNITION_EDITOR) and, like debugbar, defaults to phpstorm, 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. Set richter.editor to null to 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: --json and 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

Choose a tag to compare

@SanderMuller SanderMuller released this 20 Jul 21:49
Immutable release. Only release title and notes can be modified.

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 the Laravel\Pennant\Feature facade or the @feature Blade directive. Register your project's Enum\Class::method wrappers and a change behind one is annotated as flag-gated. Annotation only — it never feeds risk, the --fail-on gate, or affected-tests selection.
  • 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 covers route, fetch, axios, useFetch, $http, $ (jQuery), window, and page / cy (Playwright / Cypress spec navigation); register custom HTTP wrappers through config. This removes false route seeds from unrelated calls such as translate('/settings') or console.log('/…') that previously inflated the touched-route surface.

Changed

  • HEAD-mode diffs now analyse the working tree. detect-changes and affected-tests compare 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-HEAD ref still replays that ref's committed tree, so historical and benchmark replays are unchanged, and CI (which checks out clean) is unaffected.

Fixed

  • affected-tests no longer silently under-selects around a file it cannot diff. An untracked (never git add-ed) file under app/, resources/views/, or a configured frontend root is invisible to git diff; the selection now fails closed (exit 2 — "run the full suite") instead of emitting a narrowed set that omits it. git add the file to include it. detect-changes keeps its advisory stderr note.
  • Hunk/source desync in HEAD mode. 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

Choose a tag to compare

@SanderMuller SanderMuller released this 20 Jul 07:09
Immutable release. Only release title and notes can be modified.

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 new entryPointTestReferences JSON/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 to risk, a --fail-on gate, or richter:affected-tests selection.
  • entryPointTestReferences in --json and 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 --json and --markdown stdout 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 expect edge cases), the profile phase-event sequence, and the --profile output-contract coexistence with --json.
  • Build phase timings ride the existing onProgress callback (richter:phase events), zero-cost when no listener is attached.

Full Changelog: v0.8.0...v0.9.0