From 25eae07649dd47aad27cfebf8804911162963642 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:20:30 +0000 Subject: [PATCH 1/6] Report exceptions thrown from `ResultCacheMetaExtension` during result cache restore as internal errors - Wrap `ResultCacheMetaExtension::getKey()`/`getHash()` calls in `ResultCacheManager::getMetaFromPhpStanExtensions()` and rethrow any `Throwable` as a new `ResultCacheMetaExtensionException` carrying the extension class and original exception. - Catch `ResultCacheMetaExtensionException` around `restore()` in `AnalyseApplication::analyse()` and convert it into an `InternalError` so the run fails with a non-zero exit code instead of the exception escaping as a fatal that a global `set_exception_handler()` can swallow into exit 0. - In debug mode the original exception is rethrown, matching the previous `--debug` behaviour. - Guard both `getKey()` and `getHash()` (sibling extension methods) since either can throw the same way. - `process()` reuses the meta already computed during `restore()` and does not call `getHash()` again, so no guard is needed there. --- .../ResultCache/ResultCacheManager.php | 13 +++- .../ResultCacheMetaExtensionException.php | 33 +++++++++++ src/Command/AnalyseApplication.php | 43 +++++++++++++- .../ResultCacheMetaExtensionTest.php | 59 +++++++++++++++++++ .../data/ThrowingResultCacheMetaExtension.php | 21 +++++++ .../ResultCache/data/file-with-error.php | 8 +++ .../ResultCache/throwing-meta-extension.neon | 5 ++ 7 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 src/Analyser/ResultCache/ResultCacheMetaExtensionException.php create mode 100644 tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php create mode 100644 tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php create mode 100644 tests/PHPStan/Analyser/ResultCache/data/file-with-error.php create mode 100644 tests/PHPStan/Analyser/ResultCache/throwing-meta-extension.neon diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 0bc1e756e5b..667ba765fd0 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -1345,14 +1345,21 @@ private function getMetaFromPhpStanExtensions(): array /** @var ResultCacheMetaExtension $extension */ foreach ($this->container->getServicesByTag(ResultCacheMetaExtension::EXTENSION_TAG) as $extension) { - if (array_key_exists($extension->getKey(), $meta)) { + try { + $key = $extension->getKey(); + $hash = $extension->getHash(); + } catch (Throwable $e) { + throw new ResultCacheMetaExtensionException($extension::class, $e); + } + + if (array_key_exists($key, $meta)) { throw new ShouldNotHappenException(sprintf( 'Duplicate ResultCacheMetaExtension with key "%s" found.', - $extension->getKey(), + $key, )); } - $meta[$extension->getKey()] = $extension->getHash(); + $meta[$key] = $hash; } ksort($meta); diff --git a/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php b/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php new file mode 100644 index 00000000000..257aeb2d5af --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php @@ -0,0 +1,33 @@ +getMessage()), + previous: $previous, + ); + } + + public function getExtensionClass(): string + { + return $this->extensionClass; + } + +} diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index e78212f492c..6421bbf09f2 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -7,7 +7,9 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\Analyser\ResultCache\ResultCacheMetaExtensionException; use PHPStan\Collectors\CollectedData; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\BytesHelper; @@ -86,7 +88,15 @@ public function analyse( } $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { - $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); + try { + $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); + } catch (ResultCacheMetaExtensionException $e) { + if ($debug) { + throw $e->getPrevious() ?? $e; + } + + return $this->createResultCacheMetaExtensionErrorResult($e, $defaultLevelUsed, $projectConfigFile); + } $intermediateAnalyserResult = $this->runAnalyser( $resultCache->getFilesToAnalyse(), $files, @@ -192,6 +202,37 @@ public function analyse( ); } + private function createResultCacheMetaExtensionErrorResult( + ResultCacheMetaExtensionException $e, + bool $defaultLevelUsed, + ?string $projectConfigFile, + ): AnalysisResult + { + $previous = $e->getPrevious() ?? $e; + $internalError = new InternalError( + $previous->getMessage(), + sprintf('computing result cache metadata from %s', $e->getExtensionClass()), + InternalError::prepareTrace($previous), + $previous->getTraceAsString(), + shouldReportBug: false, + ); + + return new AnalysisResult( + [], + [], + [$internalError], + [], + [], + $defaultLevelUsed, + $projectConfigFile, + false, + memory_get_peak_usage(true), + false, + [], + [], + ); + } + /** * @param CollectorData $collectedData * diff --git a/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php b/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php new file mode 100644 index 00000000000..414482b5eb3 --- /dev/null +++ b/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php @@ -0,0 +1,59 @@ +getByType(AnalyseApplication::class); + $resource = fopen('php://memory', 'w', false); + if ($resource === false) { + throw new ShouldNotHappenException(); + } + $output = new StreamOutput($resource); + $symfonyOutput = new SymfonyOutput( + $output, + new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createStub(InputInterface::class), $output)), + ); + + $analysisResult = $analyserApplication->analyse( + [__DIR__ . '/data/file-with-error.php'], + true, + $symfonyOutput, + $symfonyOutput, + false, + false, + null, + null, + null, + null, + $this->createStub(InputInterface::class), + ); + + $this->assertTrue($analysisResult->hasInternalErrors()); + $internalErrors = $analysisResult->getInternalErrorObjects(); + $this->assertCount(1, $internalErrors); + $this->assertStringContainsString('boom from getHash', $internalErrors[0]->getMessage()); + $this->assertStringContainsString( + 'computing result cache metadata', + $internalErrors[0]->getContextDescription(), + ); + } + +} diff --git a/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php b/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php new file mode 100644 index 00000000000..71f0654f3a8 --- /dev/null +++ b/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php @@ -0,0 +1,21 @@ + Date: Fri, 12 Jun 2026 08:34:15 +0000 Subject: [PATCH 2/6] Remove PHPUnit-based ResultCacheMetaExtension test in favor of e2e test Co-Authored-By: Claude Opus 4.8 --- .../ResultCacheMetaExtensionTest.php | 59 ------------------- .../data/ThrowingResultCacheMetaExtension.php | 21 ------- .../ResultCache/data/file-with-error.php | 8 --- .../ResultCache/throwing-meta-extension.neon | 5 -- 4 files changed, 93 deletions(-) delete mode 100644 tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php delete mode 100644 tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php delete mode 100644 tests/PHPStan/Analyser/ResultCache/data/file-with-error.php delete mode 100644 tests/PHPStan/Analyser/ResultCache/throwing-meta-extension.neon diff --git a/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php b/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php deleted file mode 100644 index 414482b5eb3..00000000000 --- a/tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php +++ /dev/null @@ -1,59 +0,0 @@ -getByType(AnalyseApplication::class); - $resource = fopen('php://memory', 'w', false); - if ($resource === false) { - throw new ShouldNotHappenException(); - } - $output = new StreamOutput($resource); - $symfonyOutput = new SymfonyOutput( - $output, - new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createStub(InputInterface::class), $output)), - ); - - $analysisResult = $analyserApplication->analyse( - [__DIR__ . '/data/file-with-error.php'], - true, - $symfonyOutput, - $symfonyOutput, - false, - false, - null, - null, - null, - null, - $this->createStub(InputInterface::class), - ); - - $this->assertTrue($analysisResult->hasInternalErrors()); - $internalErrors = $analysisResult->getInternalErrorObjects(); - $this->assertCount(1, $internalErrors); - $this->assertStringContainsString('boom from getHash', $internalErrors[0]->getMessage()); - $this->assertStringContainsString( - 'computing result cache metadata', - $internalErrors[0]->getContextDescription(), - ); - } - -} diff --git a/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php b/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php deleted file mode 100644 index 71f0654f3a8..00000000000 --- a/tests/PHPStan/Analyser/ResultCache/data/ThrowingResultCacheMetaExtension.php +++ /dev/null @@ -1,21 +0,0 @@ - Date: Fri, 12 Jun 2026 08:34:22 +0000 Subject: [PATCH 3/6] Add e2e test asserting non-zero exit when ResultCacheMetaExtension throws Reproduces phpstan/phpstan#14805 end-to-end: a ResultCacheMetaExtension whose getHash() throws during result cache restore, combined with a bootstrap file that installs a global set_exception_handler (as a framework like Laravel/larastan would). Without the fix the handler swallows the uncaught exception and the process exits 0, silently skipping analysis; with the fix the throw is reported as an internal error and the run exits non-zero. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/e2e-tests.yml | 11 ++++++++++ .../.gitignore | 1 + .../bootstrap.php | 13 ++++++++++++ .../composer.json | 5 +++++ .../composer.lock | 18 ++++++++++++++++ .../phpstan.neon | 12 +++++++++++ .../src/Foo.php | 10 +++++++++ .../src/ThrowingResultCacheMetaExtension.php | 21 +++++++++++++++++++ 8 files changed, 91 insertions(+) create mode 100644 e2e/result-cache-meta-extension-throw/.gitignore create mode 100644 e2e/result-cache-meta-extension-throw/bootstrap.php create mode 100644 e2e/result-cache-meta-extension-throw/composer.json create mode 100644 e2e/result-cache-meta-extension-throw/composer.lock create mode 100644 e2e/result-cache-meta-extension-throw/phpstan.neon create mode 100644 e2e/result-cache-meta-extension-throw/src/Foo.php create mode 100644 e2e/result-cache-meta-extension-throw/src/ThrowingResultCacheMetaExtension.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d7574c772a2..fa60dc05f3a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -336,6 +336,17 @@ jobs: echo "$OUTPUT" ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" ../bashunit -a contains 'Result cache not used because the metadata do not match: metaExtensions' "$OUTPUT" + - script: | + cd e2e/result-cache-meta-extension-throw + composer install + # https://github.com/phpstan/phpstan/issues/14805 + # An exception thrown from ResultCacheMetaExtension::getHash() during result cache + # restore must fail the run with a non-zero exit code - even when a bootstrap file + # installs a global exception handler that would otherwise swallow it into exit 0. + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + echo "$OUTPUT" + ../bashunit -a contains 'Internal error: boom from getHash' "$OUTPUT" + ../bashunit -a contains 'computing result cache metadata from ResultCacheE2E\MetaExtensionThrow\ThrowingResultCacheMetaExtension' "$OUTPUT" - script: | cd e2e/result-cache-restore-without-reflection composer install diff --git a/e2e/result-cache-meta-extension-throw/.gitignore b/e2e/result-cache-meta-extension-throw/.gitignore new file mode 100644 index 00000000000..61ead86667c --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/result-cache-meta-extension-throw/bootstrap.php b/e2e/result-cache-meta-extension-throw/bootstrap.php new file mode 100644 index 00000000000..126f6991a0f --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/bootstrap.php @@ -0,0 +1,13 @@ +getMessage() . "\n"); + exit(0); +}); diff --git a/e2e/result-cache-meta-extension-throw/composer.json b/e2e/result-cache-meta-extension-throw/composer.json new file mode 100644 index 00000000000..a072011fe86 --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/composer.json @@ -0,0 +1,5 @@ +{ + "autoload-dev": { + "classmap": ["src/"] + } +} diff --git a/e2e/result-cache-meta-extension-throw/composer.lock b/e2e/result-cache-meta-extension-throw/composer.lock new file mode 100644 index 00000000000..b383d88ac57 --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/e2e/result-cache-meta-extension-throw/phpstan.neon b/e2e/result-cache-meta-extension-throw/phpstan.neon new file mode 100644 index 00000000000..26d4bced9ff --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: 8 + paths: + - src + bootstrapFiles: + - bootstrap.php + +services: + - + class: ResultCacheE2E\MetaExtensionThrow\ThrowingResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/e2e/result-cache-meta-extension-throw/src/Foo.php b/e2e/result-cache-meta-extension-throw/src/Foo.php new file mode 100644 index 00000000000..12d0afae07c --- /dev/null +++ b/e2e/result-cache-meta-extension-throw/src/Foo.php @@ -0,0 +1,10 @@ + Date: Fri, 12 Jun 2026 08:58:54 +0000 Subject: [PATCH 4/6] Revert internal ResultCacheMetaExtension exception handling Reverts the in-PHPStan handling of exceptions thrown from ResultCacheMetaExtension::getKey()/getHash() during result cache restore. This will be replaced by catching \Throwable in the Symfony Console Application, which fixes the swallowed-exit-0 problem generally. Co-Authored-By: Claude Opus 4.8 --- .../ResultCache/ResultCacheManager.php | 13 ++---- .../ResultCacheMetaExtensionException.php | 33 -------------- src/Command/AnalyseApplication.php | 43 +------------------ 3 files changed, 4 insertions(+), 85 deletions(-) delete mode 100644 src/Analyser/ResultCache/ResultCacheMetaExtensionException.php diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 667ba765fd0..0bc1e756e5b 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -1345,21 +1345,14 @@ private function getMetaFromPhpStanExtensions(): array /** @var ResultCacheMetaExtension $extension */ foreach ($this->container->getServicesByTag(ResultCacheMetaExtension::EXTENSION_TAG) as $extension) { - try { - $key = $extension->getKey(); - $hash = $extension->getHash(); - } catch (Throwable $e) { - throw new ResultCacheMetaExtensionException($extension::class, $e); - } - - if (array_key_exists($key, $meta)) { + if (array_key_exists($extension->getKey(), $meta)) { throw new ShouldNotHappenException(sprintf( 'Duplicate ResultCacheMetaExtension with key "%s" found.', - $key, + $extension->getKey(), )); } - $meta[$key] = $hash; + $meta[$extension->getKey()] = $extension->getHash(); } ksort($meta); diff --git a/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php b/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php deleted file mode 100644 index 257aeb2d5af..00000000000 --- a/src/Analyser/ResultCache/ResultCacheMetaExtensionException.php +++ /dev/null @@ -1,33 +0,0 @@ -getMessage()), - previous: $previous, - ); - } - - public function getExtensionClass(): string - { - return $this->extensionClass; - } - -} diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 6421bbf09f2..e78212f492c 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -7,9 +7,7 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; -use PHPStan\Analyser\InternalError; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; -use PHPStan\Analyser\ResultCache\ResultCacheMetaExtensionException; use PHPStan\Collectors\CollectedData; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\BytesHelper; @@ -88,15 +86,7 @@ public function analyse( } $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { - try { - $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); - } catch (ResultCacheMetaExtensionException $e) { - if ($debug) { - throw $e->getPrevious() ?? $e; - } - - return $this->createResultCacheMetaExtensionErrorResult($e, $defaultLevelUsed, $projectConfigFile); - } + $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( $resultCache->getFilesToAnalyse(), $files, @@ -202,37 +192,6 @@ public function analyse( ); } - private function createResultCacheMetaExtensionErrorResult( - ResultCacheMetaExtensionException $e, - bool $defaultLevelUsed, - ?string $projectConfigFile, - ): AnalysisResult - { - $previous = $e->getPrevious() ?? $e; - $internalError = new InternalError( - $previous->getMessage(), - sprintf('computing result cache metadata from %s', $e->getExtensionClass()), - InternalError::prepareTrace($previous), - $previous->getTraceAsString(), - shouldReportBug: false, - ); - - return new AnalysisResult( - [], - [], - [$internalError], - [], - [], - $defaultLevelUsed, - $projectConfigFile, - false, - memory_get_peak_usage(true), - false, - [], - [], - ); - } - /** * @param CollectorData $collectedData * From 50a4aa2a649ae2e5f1208b4d1dd9df61021b2ab6 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 12 Jun 2026 09:00:03 +0000 Subject: [PATCH 5/6] Catch \Throwable in Symfony Console Application via patch An uncaught \Error (e.g. thrown from a ResultCacheMetaExtension during result cache restore) escaped Symfony Console's `catch (\Exception $e)` in Application::run() and propagated to a user-installed global set_exception_handler() (e.g. a framework bootstrap), which could swallow it into a clean exit 0 - silently skipping analysis with green CI. Patch the catch to \Throwable (PHP 7.x compatible) so any uncaught Error/Exception is rendered and produces a non-zero exit code, the same way `--debug` (setCatchExceptions(false)) already rethrows it. Update the e2e test to assert the throw surfaces (exit 1, 'boom from getHash') and is not swallowed by the global handler. Closes https://github.com/phpstan/phpstan/issues/14805 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/e2e-tests.yml | 4 ++-- composer.json | 3 ++- patches/Application.patch | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 patches/Application.patch diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fa60dc05f3a..6753593ba47 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -345,8 +345,8 @@ jobs: # installs a global exception handler that would otherwise swallow it into exit 0. OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") echo "$OUTPUT" - ../bashunit -a contains 'Internal error: boom from getHash' "$OUTPUT" - ../bashunit -a contains 'computing result cache metadata from ResultCacheE2E\MetaExtensionThrow\ThrowingResultCacheMetaExtension' "$OUTPUT" + ../bashunit -a contains 'boom from getHash' "$OUTPUT" + ../bashunit -a not_contains 'Swallowed by global exception handler' "$OUTPUT" - script: | cd e2e/result-cache-restore-without-reflection composer install diff --git a/composer.json b/composer.json index 7219ef39d73..ce1237cd846 100644 --- a/composer.json +++ b/composer.json @@ -144,7 +144,8 @@ "patches/Resolver.patch" ], "symfony/console": [ - "patches/OutputFormatter.patch" + "patches/OutputFormatter.patch", + "patches/Application.patch" ] } }, diff --git a/patches/Application.patch b/patches/Application.patch new file mode 100644 index 00000000000..9eca4a7c00a --- /dev/null +++ b/patches/Application.patch @@ -0,0 +1,11 @@ +--- Application.php ++++ Application.php +@@ -169,7 +169,7 @@ class Application implements ResetInterface + $this->configureIO($input, $output); + + $exitCode = $this->doRun($input, $output); +- } catch (\Exception $e) { ++ } catch (\Throwable $e) { + if (!$this->catchExceptions) { + throw $e; + } From 2723cfdb86502dd1ccfdb45696c90a79c7cb3567 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 12 Jun 2026 11:29:34 +0200 Subject: [PATCH 6/6] Update composer.lock --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index d9f2158470a..50f10be61fb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "018f18ace63a6268bd90ea37daca0d80", + "content-hash": "13e4d9e979c07e7c27e8d479ffa5b359", "packages": [ { "name": "clue/ndjson-react",