PHPackages                             hellpat/watson - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [CLI &amp; Console](/categories/cli)
4. /
5. hellpat/watson

ActiveLibrary[CLI &amp; Console](/categories/cli)

hellpat/watson
==============

PR blast-radius analyser for PHP. Standalone dev-only CLI that introspects Symfony / Laravel apps from the outside. Reports which routes / commands / jobs / listeners / tests a git diff reaches.

v0.7.0(4w ago)1324—7.4%[1 issues](https://github.com/HellPat/watson/issues)[2 PRs](https://github.com/HellPat/watson/pulls)MITPHPPHP ^8.4CI passing

Since May 2Pushed 2w agoCompare

[ Source](https://github.com/HellPat/watson)[ Packagist](https://packagist.org/packages/hellpat/watson)[ Docs](https://github.com/HellPat/watson)[ RSS](/packages/hellpat-watson/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (8)Versions (17)Used By (0)

watson
======

[](#watson)

> PR blast-radius analyzer for PHP. Standalone dev-only CLI that introspects Symfony / Laravel apps from the outside. Reports which routes, commands, jobs, message handlers, and tests a diff actually reaches.

[![ci](https://github.com/HellPat/watson/actions/workflows/ci.yml/badge.svg)](https://github.com/HellPat/watson/actions/workflows/ci.yml)[![packagist](https://camo.githubusercontent.com/9d697ae2da93eb3d1be539a575501dfd103c7b896613dc47be2d8e05fbf243ae/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f68656c6c7061742f776174736f6e2e737667)](https://packagist.org/packages/hellpat/watson)[![license](https://camo.githubusercontent.com/d380f4b58e8c1f9fb8e0d6eb56b11f37919b34cd7edbf6ac394f7fcf1fa91bbc/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f48656c6c5061742f776174736f6e2e737667)](LICENSE)

```
composer require --dev hellpat/watson
git diff -W -U99999 origin/main...HEAD | vendor/bin/watson blastradius --format=tok

```

watson does not shell out to git. You pipe a unified diff in, watson tells you which framework entry points are reached — at **method** granularity, with comment-only and whitespace-only edits dropped at the AST layer. `-W` keeps each changed method whole inside the hunk; `-U99999` makes the hunk carry the full file so watson can AST-diff the two halves in memory.

---

Output formats
--------------

[](#output-formats)

Same data, four shapes. Pick the one that fits the consumer.

### `--format=text` (default — human terminal)

[](#--formattext-default--human-terminal)

```
=====================================================================
watson php symfony (root: /abs/path)
=====================================================================

[list-entrypoints]

  3 entry point(s):
    - symfony.route            home              App\Controller\HelloController::home
    - symfony.command          app:ping          App\Command\PingCommand::execute
    - symfony.message_handler  App\Message\Ping  App\MessageHandler\PingHandler::__invoke

```

### `--format=md` (markdown for PR descriptions / LLM prompts)

[](#--formatmd-markdown-for-pr-descriptions--llm-prompts)

```
# watson — php symfony

_tool watson v0.3.0_

Root: `/abs/path`

## list-entrypoints
_v0.3.0_

**3 entry points**

| kind | name | handler |
|---|---|---|
| `symfony.route` | `home` | `App\Controller\HelloController::home` (`src/Controller/HelloController.php:12`) |
| `symfony.command` | `app:ping` | `App\Command\PingCommand::execute` (`src/Command/PingCommand.php:15`) |
| `symfony.message_handler` | `App\Message\PingMessage` | `App\MessageHandler\PingHandler::__invoke` (`src/MessageHandler/PingHandler.php:13`) |

```

### `--format=json` (machine contract)

[](#--formatjson-machine-contract)

```
{
  "tool": "watson",
  "version": "0.3.0",
  "language": "php",
  "framework": "symfony",
  "context": {"root": "/abs/path"},
  "analyses": [
    {
      "name": "list-entrypoints",
      "version": "0.3.0",
      "ok": true,
      "result": {
        "entry_points": [
          {
            "kind": "symfony.route",
            "name": "home",
            "handler_fqn": "App\\Controller\\HelloController::home",
            "handler_path": "/abs/path/src/Controller/HelloController.php",
            "handler_line": 12,
            "source": "runtime",
            "extra": {"path": "/", "methods": ["GET"]}
          }
        ]
      }
    }
  ]
}

```

### `--format=tok` (token-optimized for LLM pipes)

[](#--formattok-token-optimized-for-llm-pipes)

Tab-separated, no JSON keys, no whitespace padding. Header lines start with `#`. Per-row layout: `kind \t name \t handler_fqn \t relative/path:line \t extra` (extra is HTTP-method + path for routes, message FQN for handlers, empty otherwise).

```
# watson 0.3.0 list-entrypoints php/symfony root=/abs/path
# entrypoints=3
# kinds: sc=symfony.command smh=symfony.message_handler sr=symfony.route
# fields: kind\tname\thandler\tpath:line\textra
sr	home	App\Controller\HelloController::home	src/Controller/HelloController.php:12	GET /
sc	app:ping	App\Command\PingCommand::execute	src/Command/PingCommand.php:15
smh	App\Message\PingMessage	App\MessageHandler\PingHandler::__invoke	src/MessageHandler/PingHandler.php:13

```

Roughly half the token cost of pretty-printed JSON.

---

Recipes
-------

[](#recipes)

Each block below is a description followed by the command. All examples assume `composer require --dev hellpat/watson` is done.

### Pre-merge — review prompts piped to an LLM

[](#pre-merge--review-prompts-piped-to-an-llm)

```
# 1. Auto-review focused only on what changed
#    LLM is told the affected entry points; flags risky areas.
git diff -W -U99999 origin/main...HEAD | vendor/bin/watson blastradius --format=tok | llm \
  --system "Review this PR. Focus only on the affected entry points listed below.
Flag anything risky around auth, money handling, or user-visible behaviour."

# 2. Generate a manual testing guide
#    Turns the blast radius into a concrete click-through checklist for QA.
git diff -W -U99999 origin/main...HEAD | vendor/bin/watson blastradius --format=tok | llm \
  --system "You are a senior dev. Given these affected entry points, write a
concise manual testing guide: list the scenarios a reviewer must click through,
the edge cases most likely to break, and any data shape that needs verifying."

# 3. Coverage gap check — is the change covered by e2e / feature tests?
#    `--scope=all` includes phpunit.test entries so the LLM can cross-reference.
git diff -W -U99999 origin/main...HEAD | vendor/bin/watson blastradius --scope=all --format=tok | llm \
  --system "The tok output lists affected entry points (routes / commands / jobs /
message handlers) AND every phpunit.test in the repo. Cross-reference: which affected
entry points have at least one test that exercises them, and which don't?
Output a markdown table; flag gaps as 'NEEDS COVERAGE'."

```

> For a plain list of affected routes / commands / risk surface, the `--format=tok` (or `text` / `md`) output is the answer on its own — no LLM needed. Pipe to an LLM only when you want something the raw output can't give you: subjective judgement (risk, regression severity), cross-referencing with an external source (tests, coverage, observability), or written prose (testing guides).

### Post-release — observability MCP correlation

[](#post-release--observability-mcp-correlation)

After a deploy, pipe the **just-shipped** entry points into an LLM that has an observability MCP server wired up — e.g. [Better Stack MCP](https://betterstack.com/docs/getting-started/integrations/mcp/) (`claude mcp add betterstack --transport http https://mcp.betterstack.com`). The LLM gets the surface that changed *and* live metrics — it can correlate the two without you copy-pasting route names into a dashboard.

```
# 1. Latency regression on routes that shipped in the last release
#    Diffs two release tags so you only ask about routes that actually changed.
git diff -W -U99999 v1.4.0..v1.5.0 | vendor/bin/watson blastradius --scope=routes --format=tok \
  --base=v1.4.0 --head=v1.5.0 | llm \
  --system "These routes shipped in v1.5.0. Use Better Stack MCP:
for each route, query p50 / p95 latency since the deploy timestamp
and compare to the previous 24h baseline. Flag any route whose p95
grew >20% or whose error rate doubled."

# 2. Error / exception regression after deploy
#    Wider scope so jobs and message handlers are also checked for new exceptions.
git diff -W -U99999 v1.4.0..v1.5.0 | vendor/bin/watson blastradius --format=tok \
  --base=v1.4.0 --head=v1.5.0 | llm \
  --system "These entry points (routes / commands / jobs / message handlers)
shipped in v1.5.0. Use Better Stack MCP error tracking to:
- list new exception classes seen on any affected handler since deploy,
- count occurrences vs the prior 24h,
- group by handler FQN and rank by impact."

# 3. Open incidents touching the changed surface
#    Uses list-entrypoints (the full registry) since incidents may not align
#    with a specific release window.
vendor/bin/watson list-entrypoints --scope=routes --format=tok | llm \
  --system "Use Better Stack MCP to list all currently-open incidents.
For each incident, identify which (if any) of the entry points below
is involved. Output a markdown table mapping incident → affected
entry point with a one-line summary."

```

---

Install
-------

[](#install)

```
composer require --dev hellpat/watson

```

No bundle, no service provider, no `config/bundles.php` entry. watson auto-detects Symfony vs Laravel by walking up from CWD looking for `bin/console` or `artisan`.

**Requirements:** PHP 8.4+. Symfony 6.4 / 7.x / 8.x or Laravel 10 / 11 / 12. `git` is *not* a watson dependency — watson only reads the diff you pipe in. If your diff source is git, you'll have it for that reason.

---

Commands
--------

[](#commands)

### `watson blastradius`

[](#watson-blastradius)

Reads a unified diff from stdin and reports which entry points reach the changed methods. watson does not run git; the caller picks the diff source. There is **one** input shape — looser modes (name-only, explicit file lists) were removed so the engine has a single contract to support.

```
git diff -W -U99999 origin/main...HEAD | watson blastradius --format=tok

```

`-W` expands every hunk to the whole function it touches; `-U99999` pads context to ∞ so the hunk carries the full file. watson reconstructs old + new file content in-memory from the diff, AST-parses both halves, and hashes each `Class::method` body (docblocks + comments stripped, whitespace normalised). Only methods with a different hash become `ChangedSymbol`s — comment-only and whitespace-only edits never reach the reach engine.

Variants of the same pipe:

- **Staged-only:** `git diff --cached -W -U99999 | watson blastradius --format=tok`
- **Two-tag delta:** `git diff -W -U99999 v1.4.0..v1.5.0 | watson blastradius --format=tok`

When run with no pipe (interactive shell), watson exits with a usage hint instead of silently producing zero results.

Each affected entry point comes back with an **`affected by changed`** column listing the trigger `Class::method` symbols. Reach kind is one of:

- `🎯 direct` — the entry point's own handler file holds a changed symbol.
- `🔗 indirect` — the handler reaches a changed file through its imports, `new`, static calls, or type hints. The reverse-BFS is unbounded by default (the call graph saturates on its own); set `--max-depth=N` (`N >= 1`) to truncate.

#### Flags

[](#flags)

flagdefaultwhat it does`--format=text|md|json|tok``text`Output shape. `md` is tuned for PR descriptions / LLM prompts; `json` is the machine contract; `tok` is tab-separated for token-cheap LLM piping.`--scope=routes|all``all``routes` skips commands / jobs / message handlers / tests. Faster startup, smaller signal.`--max-depth=N``0` (unbounded)Hops the reverse-BFS walks from each entry-point handler before stopping. `0` = let the call graph saturate; set `N >= 1` to tighten the signal.`--app-env=ENV``dev`Value passed to `bin/console` / `artisan` when collecting routes.`--project=PATH`walks up from `cwd`Force the project root rather than autodetecting.`--base=REF` / `--head=REF`noneCosmetic labels shown in the rendered envelope so consumers can correlate output to a diff range.### `watson list-entrypoints`

[](#watson-list-entrypoints)

Snapshot every entry point the framework has registered: routes, commands, message handlers, jobs (Laravel), tests. Same flags as `blastradius`, minus the diff-input one.

### `watson  --help`

[](#watson-cmd---help)

```
$ watson blastradius --help

Description:
  Report which routes, commands, jobs, and listeners are reached by the
  unified diff piped on stdin.

Usage:
  blastradius [options]

Options:
      --base=BASE          Cosmetic label shown as the diff base in rendered output.
      --head=HEAD          Cosmetic label shown as the diff head in rendered output.
      --project=PROJECT    Project root (defaults to walking up from CWD).
      --format=FORMAT      text | md | json | tok [default: "text"]
      --scope=SCOPE        routes | all [default: "all"]
      --app-env=APP-ENV    APP_ENV passed to bin/console / artisan [default: "dev"]
      --max-depth=MAX-DEPTH  BFS hops the indirect-reach pass walks. `0` = unbounded [default: 0]

```

```
$ watson list-entrypoints --help

Description:
  Snapshot every route, command, job, message handler, and test the framework
  has registered.

Usage:
  list-entrypoints [options]

Options:
      --project=PROJECT  Project root (defaults to walking up from CWD)
      --format=FORMAT    text (human terminal) | md (PRs/LLMs) | json (machine)
                         | tok (token-optimized for LLM pipes) [default: "text"]
      --scope=SCOPE      routes (cheapest) | all (+ commands / jobs / message
                         handlers / tests) [default: "all"]
      --app-env=APP-ENV  APP_ENV passed to bin/console / artisan [default: "dev"]

```

---

How watson reads your app
-------------------------

[](#how-watson-reads-your-app)

kindsource`symfony.route``bin/console debug:router --format=json``symfony.command``bin/console debug:container --tag=console.command --format=json` (vendor filtered)`symfony.message_handler``bin/console debug:container --tag=messenger.message_handler --format=json` (vendor filtered, message inferred via reflection on the handler's first param when the tag's `handles` is null)`laravel.route``php artisan route:list --json``laravel.command`inline `php -r` runner that boots Laravel and dumps `Artisan::all()` (vendor filtered)`laravel.job`AST scan of `app/Jobs/` for `ShouldQueue` implementers`phpunit.test`AST scan of the project's `autoload-dev.psr-4` roots for `PHPUnit\Framework\TestCase` subclasseswatson is a CLI binary, not a bundle/provider. AST scans go through [`nikic/php-parser`](https://github.com/nikic/PHP-Parser) — watson never `require_once`s your app's source. (Earlier versions used [`roave/better-reflection`](https://github.com/Roave/BetterReflection) for per-class inheritance walks; that turned out to be a perf trap on large Laravel apps, so discovery is now hash-map BFS over a single-pass AST class index.)

---

Pipeline
--------

[](#pipeline)

 ```
flowchart TD
  GD["git diff -W -U99999"] --> CFR["ChangedFilesReader(reads stdin)"]
  CFR --> ADM["AstDiffMapperold AST vs new AST per method(comments / whitespace ignored)"]
  ADM --> CS["changed symbolsClass::method (or Class::*)"]

  APP["target app"] --> ASK["ask framework(debug:router, debug:container,route:list, Laravel boot)"]
  ASK --> EP["entry pointsClass::method"]
  EP --> AST["AST → handler file:line(Composer ClassLoader + Better Reflection)"]

  AST --> RX(("reach BFS(file-graph, method-aware in Phase B)"))
  CS --> RX
  RX --> AEP["affected entry points+ trigger Class::method"]
  AEP --> R["rendertext / md / json / tok('affected by changed' column,🔗 indirect badge)"]
```

      Loading ---

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance75

Regular maintenance activity

Popularity19

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 90.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

12

Last Release

28d ago

PHP version history (2 changes)v0.2.0PHP &gt;=8.2

v0.3.1PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/115f225c58170ec218bb8a1bad6ec87895d484cd3b4d1b5fbb30083288b16090?d=identicon)[HellPat](/maintainers/HellPat)

---

Top Contributors

[![ph-khero](https://avatars.githubusercontent.com/u/241262139?v=4)](https://github.com/ph-khero "ph-khero (80 commits)")[![renovate[bot]](https://avatars.githubusercontent.com/in/2740?v=4)](https://github.com/renovate[bot] "renovate[bot] (5 commits)")[![HellPat](https://avatars.githubusercontent.com/u/1016798?v=4)](https://github.com/HellPat "HellPat (3 commits)")

---

Tags

code reviewconsolediffsymfonylaravelartisanreviewprblast-radiusai-review

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/hellpat-watson/health.svg)

```
[![Health](https://phpackages.com/badges/hellpat-watson/health.svg)](https://phpackages.com/packages/hellpat-watson)
```

###  Alternatives

[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.6k38.2k](/packages/matomo-matomo)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

21764.8M1.6k](/packages/drupal-core)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M195](/packages/sulu-sulu)[rector/rector-src

Instant Upgrade and Automated Refactoring of any PHP code

136400.8k14](/packages/rector-rector-src)[jolicode/castor

A lightweight and modern task runner. Automate everything. In PHP.

54642.4k4](/packages/jolicode-castor)[forumify/forumify-platform

132.0k12](/packages/forumify-forumify-platform)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
