From 9298810fbdb2c71b13d0d842e2cb8273eb813214 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:10:45 +0000 Subject: [PATCH 01/16] Fix Tinker one-shot execution lifecycle Treat every non-null --execute value as one-shot code, including zero and the empty string. Preserve PsySH exit codes, contain ordinary execution failures, and stop mutating Symfony Console's shared exception policy. Use a small execute-only shell that omits PsySH's interactive signal listener while keeping the normal interactive shell unchanged. Tighten configured command and alias inputs, keep loader cleanup exception-safe, and cover falsey code, signals, exit behavior, disabled commands, coroutine dispatch, and the disposable application subprocess path. --- src/tinker/src/Console/TinkerCommand.php | 33 +++-- src/tinker/src/ExecuteShell.php | 22 ++++ tests/Tinker/TinkerCommandTest.php | 153 +++++++++++++++++++++-- 3 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 src/tinker/src/ExecuteShell.php diff --git a/src/tinker/src/Console/TinkerCommand.php b/src/tinker/src/Console/TinkerCommand.php index a7e69cbfc6..0325013ced 100644 --- a/src/tinker/src/Console/TinkerCommand.php +++ b/src/tinker/src/Console/TinkerCommand.php @@ -7,7 +7,9 @@ use Hypervel\Console\Command; use Hypervel\Support\Env; use Hypervel\Tinker\ClassAliasAutoloader; +use Hypervel\Tinker\ExecuteShell; use Psy\Configuration; +use Psy\Exception\BreakException; use Psy\Shell; use Psy\VersionUpdater\Checker; use Symfony\Component\Console\Attribute\AsCommand; @@ -40,8 +42,6 @@ class TinkerCommand extends Command */ public function handle(): int { - $this->getApplication()->setCatchExceptions(false); - $config = Configuration::fromInput($this->input); $config->setUpdateCheck(Checker::NEVER); @@ -57,11 +57,17 @@ public function handle(): int $this->getCasters() ); - if ($this->option('execute')) { + /** @var ?string $code */ + $code = $this->option('execute'); + + if ($code !== null) { $config->setRawOutput(true); } - $shell = new Shell($config); + $shell = $code !== null + ? new ExecuteShell($config) + : new Shell($config); + $shell->addCommands($this->getCommands()); $shell->setIncludes($this->argument('include')); @@ -72,14 +78,17 @@ public function handle(): int $loader = ClassAliasAutoloader::register( $shell, $path, - $appConfig->get('tinker.alias', []), - $appConfig->get('tinker.dont_alias', []) + $appConfig->array('tinker.alias', []), + $appConfig->array('tinker.dont_alias', []) ); - if ($code = $this->option('execute')) { + if ($code !== null) { try { $shell->setOutput($this->output); + $shell->boot(); $shell->execute($code, true); + } catch (BreakException $e) { + return $e->getCode(); } catch (Throwable $e) { $shell->writeException($e); @@ -114,9 +123,11 @@ protected function getCommands(): array $config = $this->getHypervel()->make('config'); foreach ($config->array('tinker.commands', []) as $command) { - $commands[] = $this->getApplication()->addCommand( + if (($command = $this->getApplication()->addCommand( $this->getHypervel()->make($command) - ); + )) !== null) { + $commands[] = $command; + } } return $commands; @@ -141,9 +152,7 @@ protected function getCasters(): array $casters['Hypervel\Process\ProcessResult'] = 'Hypervel\Tinker\TinkerCaster::castProcessResult'; } - if (class_exists('Hypervel\Foundation\Application')) { - $casters['Hypervel\Foundation\Application'] = 'Hypervel\Tinker\TinkerCaster::castApplication'; - } + $casters['Hypervel\Foundation\Application'] = 'Hypervel\Tinker\TinkerCaster::castApplication'; $config = $this->getHypervel()->make('config'); diff --git a/src/tinker/src/ExecuteShell.php b/src/tinker/src/ExecuteShell.php new file mode 100644 index 0000000000..387356b45b --- /dev/null +++ b/src/tinker/src/ExecuteShell.php @@ -0,0 +1,22 @@ + ! $listener instanceof SignalHandler, + ); + } +} diff --git a/tests/Tinker/TinkerCommandTest.php b/tests/Tinker/TinkerCommandTest.php index 224c96897f..7ef20885c0 100644 --- a/tests/Tinker/TinkerCommandTest.php +++ b/tests/Tinker/TinkerCommandTest.php @@ -4,10 +4,21 @@ namespace Hypervel\Tests\Tinker; +use Hypervel\Console\Application as ConsoleApplication; +use Hypervel\Console\Command; +use Hypervel\Contracts\Console\Kernel as KernelContract; use Hypervel\Contracts\Foundation\Application; +use Hypervel\Filesystem\Filesystem; use Hypervel\Support\Env; +use Hypervel\Support\ServiceProvider; use Hypervel\Testbench\TestCase; +use Hypervel\Testing\ParallelTesting; use Hypervel\Tinker\TinkerServiceProvider; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\InputStream; +use Symfony\Component\Process\Process; class TinkerCommandTest extends TestCase { @@ -22,32 +33,158 @@ protected function defineEnvironment(Application $app): void Env::getRepository()->set('COMPOSER_VENDOR_DIR', dirname(__DIR__, 2) . '/vendor'); } - public function testExecuteSuccess() + public function testExecuteSuccess(): void { $this->artisan('tinker', ['--execute' => 'echo "hello";']) ->assertExitCode(0); } - public function testExecuteFailure() + public function testExecuteFailure(): void { $this->artisan('tinker', ['--execute' => 'throw new \Exception("fail");']) ->assertExitCode(1); } - public function testExecuteRunsInsideCoroutine() + public function testExecuteReturnsRequestedExitCodeWithoutRenderingAnError(): void { - $file = tempnam(sys_get_temp_dir(), 'tinker_coroutine_'); + $this->artisan('tinker', ['--execute' => 'exit(3);']) + ->doesntExpectOutput() + ->assertExitCode(3); + } + + #[DataProvider('falseyExecuteCodeProvider')] + public function testFalseyExecuteValuesUseDirectExecution(string $code): void + { + $providersPath = BASE_PATH . '/bootstrap/providers.php'; + $originalProviders = file_get_contents($providersPath); + + if (! is_string($originalProviders)) { + $this->fail('Unable to read the Testbench provider file.'); + } + + $input = new InputStream; + $process = new Process( + command: [PHP_BINARY, BASE_PATH . '/artisan', 'tinker', '--execute=' . $code], + cwd: dirname(__DIR__, 2), + env: array_merge($_ENV, [ + 'COMPOSER_VENDOR_DIR' => (string) Env::get('COMPOSER_VENDOR_DIR'), + 'HYPERVEL_AUTOLOAD_PATH' => dirname(__DIR__, 2) . '/vendor/autoload.php', + ]), + timeout: 10, + ); + $process->setInput($input); + + try { + $this->assertTrue(ServiceProvider::addProviderToBootstrapFile( + TinkerServiceProvider::class, + $providersPath, + )); + + $process->run(); + } catch (ProcessTimedOutException) { + $this->fail('Falsey execute input started the interactive shell.'); + } finally { + $input->close(); + $process->stop(); + file_put_contents($providersPath, $originalProviders); + } + + $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput()); + } + + #[DataProvider('directExecutionOutcomeProvider')] + #[RequiresPhpExtension('pcntl')] + #[RequiresPhpExtension('posix')] + public function testDirectExecutionPreservesTheSigintHandler(string $code, int $exitCode): void + { + $originalHandler = pcntl_signal_get_handler(SIGINT); + $sentinelHandler = static function (): void { + }; + + pcntl_signal(SIGINT, $sentinelHandler); + + try { + $this->artisan('tinker', ['--execute' => $code]) + ->assertExitCode($exitCode); + + $this->assertSame($sentinelHandler, pcntl_signal_get_handler(SIGINT)); + } finally { + pcntl_signal(SIGINT, $originalHandler); + } + } + + public function testExecuteDoesNotChangeTheConsoleExceptionPolicy(): void + { + $application = $this->app->make(KernelContract::class)->getArtisan(); + + $this->assertInstanceOf(ConsoleApplication::class, $application); + + $application->setCatchExceptions(true); + + $this->artisan('tinker', ['--execute' => 'echo "hello";']) + ->assertExitCode(0); + + $this->assertTrue($application->areExceptionsCaught()); + } + + public function testDisabledConfiguredCommandsAreIgnored(): void + { + config()->set('tinker.commands', [DisabledTinkerCommand::class]); + + $this->artisan('tinker', ['--execute' => 'echo "hello";']) + ->assertExitCode(0); + } + + public function testExecuteRunsInsideCoroutine(): void + { + $filesystem = new Filesystem; + $directory = ParallelTesting::tempDir('TinkerCommandTest'); + $file = $directory . '/coroutine'; + + $filesystem->deleteDirectory($directory); + $filesystem->ensureDirectoryExists($directory); $code = sprintf( "file_put_contents('%s', \\Hypervel\\Coroutine\\Coroutine::inCoroutine() ? 'true' : 'false');", addslashes($file) ); - $this->artisan('tinker', ['--execute' => $code]) - ->assertExitCode(0); + try { + $this->artisan('tinker', ['--execute' => $code]) + ->assertExitCode(0); - $this->assertSame('true', file_get_contents($file)); + $this->assertSame('true', file_get_contents($file)); + } finally { + $filesystem->deleteDirectory($directory); + } + } + + public static function falseyExecuteCodeProvider(): array + { + return [ + ['0'], + [''], + ]; + } - unlink($file); + public static function directExecutionOutcomeProvider(): array + { + return [ + ['echo "hello";', 0], + ['throw new \Exception("fail");', 1], + ]; + } +} + +class DisabledTinkerCommand extends Command +{ + protected ?string $name = 'tinker:disabled'; + + /** + * Determine whether the command is enabled. + */ + public function isEnabled(): bool + { + return false; } } From 7ea7cdf8c31ec29b8488704f74d5ca4d8901422c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:10:57 +0000 Subject: [PATCH 02/16] Correct Tinker alias boundaries Normalize configured class and namespace names once, then match only exact classes or real namespace descendants. This prevents a prefix such as App\Nova from also admitting App\NovaThing. Apply the same boundary rule to exclusions and require vendor paths to be actual children of the configured vendor directory. Add coverage for exact matches, descendants, prefix siblings, trailing separators, exclusions, vendor children, and vendor-prefix siblings. --- src/tinker/src/ClassAliasAutoloader.php | 28 +++++++---- tests/Tinker/ClassAliasAutoloaderTest.php | 57 +++++++++++++++++++---- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/tinker/src/ClassAliasAutoloader.php b/src/tinker/src/ClassAliasAutoloader.php index c84e2d51ac..26137a149c 100644 --- a/src/tinker/src/ClassAliasAutoloader.php +++ b/src/tinker/src/ClassAliasAutoloader.php @@ -50,8 +50,10 @@ public function __construct( array $excludedAliases = [], ) { $this->vendorPath = dirname(dirname($classMapPath)); - $this->includedAliases = collect($includedAliases); - $this->excludedAliases = collect($excludedAliases); + $this->includedAliases = collect($includedAliases) + ->map(static fn (string $alias): string => trim($alias, '\\')); + $this->excludedAliases = collect($excludedAliases) + ->map(static fn (string $alias): string => trim($alias, '\\')); $classes = require $classMapPath; @@ -111,22 +113,30 @@ public function isAliasable(string $class, string $path): bool return false; } - if ($this->includedAliases->contains(function ($alias) use ($class) { - return Str::startsWith($class, $alias); - })) { + if ($this->includedAliases->contains( + static fn (string $alias): bool => self::matchesAlias($class, $alias) + )) { return true; } - if (Str::startsWith($path, $this->vendorPath)) { + if (Str::startsWith($path, $this->vendorPath . DIRECTORY_SEPARATOR)) { return false; } - if ($this->excludedAliases->contains(function ($alias) use ($class) { - return Str::startsWith($class, $alias); - })) { + if ($this->excludedAliases->contains( + static fn (string $alias): bool => self::matchesAlias($class, $alias) + )) { return false; } return true; } + + /** + * Determine whether a class matches an alias boundary. + */ + private static function matchesAlias(string $class, string $alias): bool + { + return $class === $alias || Str::startsWith($class, $alias . '\\'); + } } diff --git a/tests/Tinker/ClassAliasAutoloaderTest.php b/tests/Tinker/ClassAliasAutoloaderTest.php index d543aa3e4f..119615c66a 100644 --- a/tests/Tinker/ClassAliasAutoloaderTest.php +++ b/tests/Tinker/ClassAliasAutoloaderTest.php @@ -15,7 +15,7 @@ class ClassAliasAutoloaderTest extends TestCase { protected string $classmapPath; - protected ClassAliasAutoloader $loader; + protected ?ClassAliasAutoloader $loader = null; protected function setUp(): void { @@ -26,12 +26,14 @@ protected function setUp(): void protected function tearDown(): void { - $this->loader->unregister(); - - parent::tearDown(); + try { + $this->loader?->unregister(); + } finally { + parent::tearDown(); + } } - public function testCanAliasClasses() + public function testCanAliasClasses(): void { $this->loader = ClassAliasAutoloader::register( $shell = m::mock(Shell::class), @@ -46,7 +48,7 @@ public function testCanAliasClasses() $this->assertInstanceOf(TinkerBar::class, new \TinkerBar); } - public function testCanExcludeNamespacesFromAliasing() + public function testCanExcludeNamespacesFromAliasing(): void { $this->loader = ClassAliasAutoloader::register( $shell = m::mock(Shell::class), @@ -60,7 +62,7 @@ public function testCanExcludeNamespacesFromAliasing() $this->assertFalse(class_exists('TinkerQux')); } - public function testVendorClassesAreExcluded() + public function testVendorClassesAreExcluded(): void { $this->loader = ClassAliasAutoloader::register( $shell = m::mock(Shell::class), @@ -72,7 +74,7 @@ public function testVendorClassesAreExcluded() $this->assertFalse(class_exists('TinkerThree')); } - public function testVendorClassesCanBeWhitelisted() + public function testVendorClassesCanBeWhitelisted(): void { $this->loader = ClassAliasAutoloader::register( $shell = m::mock(Shell::class), @@ -87,4 +89,43 @@ public function testVendorClassesCanBeWhitelisted() $this->assertTrue(class_exists('TinkerThree')); $this->assertInstanceOf(TinkerThree::class, new \TinkerThree); } + + public function testIncludedAliasesMatchClassAndNamespaceBoundaries(): void + { + $loader = new ClassAliasAutoloader( + m::mock(Shell::class), + $this->classmapPath, + ['Acme\Package\Thing\\'], + ); + $vendorPath = dirname($this->classmapPath, 2); + + $this->assertTrue($loader->isAliasable('Acme\Package\Thing', $vendorPath . '/Thing.php')); + $this->assertTrue($loader->isAliasable('Acme\Package\Thing\Child', $vendorPath . '/Child.php')); + $this->assertFalse($loader->isAliasable('Acme\Package\ThingElse', $vendorPath . '/ThingElse.php')); + } + + public function testExcludedAliasesMatchClassAndNamespaceBoundaries(): void + { + $loader = new ClassAliasAutoloader( + m::mock(Shell::class), + $this->classmapPath, + [], + ['App\Nova\\'], + ); + $applicationPath = dirname($this->classmapPath, 3) . '/App'; + + $this->assertFalse($loader->isAliasable('App\Nova', $applicationPath . '/Nova.php')); + $this->assertFalse($loader->isAliasable('App\Nova\Resource', $applicationPath . '/Resource.php')); + $this->assertTrue($loader->isAliasable('App\NovaThing', $applicationPath . '/NovaThing.php')); + } + + public function testVendorPathsMatchDirectoryBoundaries(): void + { + $loader = new ClassAliasAutoloader(m::mock(Shell::class), $this->classmapPath); + $vendorPath = dirname($this->classmapPath, 2); + + $this->assertFalse($loader->isAliasable('Vendor\Package\Thing', $vendorPath . '/Package/Thing.php')); + $this->assertTrue($loader->isAliasable('App\VendorThing', $vendorPath . '-local/VendorThing.php')); + $this->assertFalse($loader->isAliasable('VendorThing', $vendorPath . '-local/VendorThing.php')); + } } From c038e9f4b8f752f2c70b35c34112180f84f408ac Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:11:09 +0000 Subject: [PATCH 03/16] Contain Tinker application caster failures Keep application presentation best-effort when one optional getter throws an Error or TypeError. Each property is resolved independently, so a failing value is omitted without hiding the remaining useful application details. Retain null filtering and caster output order, and add a regression that proves later virtual properties are still rendered after an earlier getter fails. --- src/tinker/src/TinkerCaster.php | 11 ++++++----- tests/Tinker/TinkerCasterTest.php | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/tinker/src/TinkerCaster.php b/src/tinker/src/TinkerCaster.php index 6a3d4cb155..a20c398bf6 100644 --- a/src/tinker/src/TinkerCaster.php +++ b/src/tinker/src/TinkerCaster.php @@ -4,7 +4,6 @@ namespace Hypervel\Tinker; -use Exception; use Hypervel\Database\Eloquent\Model; use Hypervel\Foundation\Application; use Hypervel\Process\ProcessResult; @@ -12,6 +11,7 @@ use Hypervel\Support\HtmlString; use Hypervel\Support\Stringable; use Symfony\Component\VarDumper\Caster\Caster; +use Throwable; class TinkerCaster { @@ -45,12 +45,13 @@ public static function castApplication(Application $app): array foreach (self::$appProperties as $property) { try { - $val = $app->{$property}(); + $value = $app->{$property}(); - if (! is_null($val)) { - $results[Caster::PREFIX_VIRTUAL . $property] = $val; + if ($value !== null) { + $results[Caster::PREFIX_VIRTUAL . $property] = $value; } - } catch (Exception $e) { + } catch (Throwable) { + // An unavailable presentation value should not hide the remaining properties. } } diff --git a/tests/Tinker/TinkerCasterTest.php b/tests/Tinker/TinkerCasterTest.php index 1491df12fc..51c86a4bae 100644 --- a/tests/Tinker/TinkerCasterTest.php +++ b/tests/Tinker/TinkerCasterTest.php @@ -4,16 +4,31 @@ namespace Hypervel\Tests\Tinker; +use Error; +use Hypervel\Foundation\Application; use Hypervel\Support\Collection; use Hypervel\Tests\TestCase; use Hypervel\Tinker\TinkerCaster; +use Mockery as m; +use Symfony\Component\VarDumper\Caster\Caster; class TinkerCasterTest extends TestCase { - public function testCanCastCollection() + public function testCanCastCollection(): void { $result = TinkerCaster::castCollection(new Collection(['foo', 'bar'])); $this->assertSame([['foo', 'bar']], array_values($result)); } + + public function testApplicationPropertyErrorsDoNotSuppressLaterValues(): void + { + $application = m::mock(Application::class); + $application->shouldReceive('configurationIsCached')->once()->andThrow(new Error('Unavailable')); + $application->shouldReceive('version')->once()->andReturn('1.0.0'); + + $result = TinkerCaster::castApplication($application); + + $this->assertSame('1.0.0', $result[Caster::PREFIX_VIRTUAL . 'version']); + } } From 9eecc0f666bdf3177bc431415e38c3d2e3bac530 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:11:23 +0000 Subject: [PATCH 04/16] Correct Tinker package metadata Remove the unused Contracts dependency and align the split package's direct external constraints with the monorepo root. Keep Database as the one optional package that enables documented Tinker behavior and preserve provider discovery metadata. Add focused metadata coverage so dependency constraints, the Database suggestion, and automatic provider discovery cannot drift. Complete return types in the neighboring provider tests while keeping their behavior unchanged. --- src/tinker/composer.json | 1 - tests/Tinker/PackageMetadataTest.php | 59 ++++++++++++++++++++++ tests/Tinker/TinkerServiceProviderTest.php | 4 +- 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 tests/Tinker/PackageMetadataTest.php diff --git a/src/tinker/composer.json b/src/tinker/composer.json index 7b8fcae928..99ca4d0acf 100644 --- a/src/tinker/composer.json +++ b/src/tinker/composer.json @@ -34,7 +34,6 @@ "php": "^8.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", - "hypervel/contracts": "^0.4", "hypervel/foundation": "^0.4", "hypervel/support": "^0.4", "psy/psysh": "^0.12.22", diff --git a/tests/Tinker/PackageMetadataTest.php b/tests/Tinker/PackageMetadataTest.php new file mode 100644 index 0000000000..d2799b23ac --- /dev/null +++ b/tests/Tinker/PackageMetadataTest.php @@ -0,0 +1,59 @@ +assertArrayHasKey($dependency, $composer['require']); + } + + foreach (['psy/psysh', 'symfony/console', 'symfony/var-dumper'] as $dependency) { + $this->assertSame($rootComposer['require'][$dependency], $composer['require'][$dependency]); + } + + $this->assertArrayNotHasKey('hypervel/contracts', $composer['require']); + $this->assertSame([ + 'hypervel/database' => 'Required for Eloquent model casting in Tinker (^0.4).', + ], $composer['suggest']); + $this->assertSame([ + TinkerServiceProvider::class, + ], $composer['extra']['hypervel']['providers']); + $this->assertContains( + TinkerServiceProvider::class, + $rootComposer['extra']['hypervel']['providers'], + ); + } +} diff --git a/tests/Tinker/TinkerServiceProviderTest.php b/tests/Tinker/TinkerServiceProviderTest.php index fb48faeea1..5cd197b151 100644 --- a/tests/Tinker/TinkerServiceProviderTest.php +++ b/tests/Tinker/TinkerServiceProviderTest.php @@ -16,14 +16,14 @@ protected function getPackageProviders(Application $app): array return [TinkerServiceProvider::class]; } - public function testTinkerCommandIsRegistered() + public function testTinkerCommandIsRegistered(): void { $command = $this->app->make('command.tinker'); $this->assertInstanceOf(TinkerCommand::class, $command); } - public function testTinkerConfigIsMerged() + public function testTinkerConfigIsMerged(): void { $config = $this->app->make('config'); From b6125701b4a5615afca5ecd63f98a112dcde443d Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:11:30 +0000 Subject: [PATCH 05/16] Document Tinker upstream provenance Link the split package to Laravel Tinker, which remains its upstream source. Keep the README deliberately small so the Boost guide remains the single user-documentation surface. --- src/tinker/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tinker/README.md b/src/tinker/README.md index 6a7894d78b..57073bc63b 100644 --- a/src/tinker/README.md +++ b/src/tinker/README.md @@ -2,3 +2,5 @@ Tinker for Hypervel === [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/tinker) + +Ported from: https://github.com/laravel/tinker From e42b87c97b47a19c3dd92c05bcb682ea47e9728c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:11:40 +0000 Subject: [PATCH 06/16] Expand the Tinker Artisan guide Document one-shot execution and exit codes, positional includes, alias controls, custom casters, project trust, and Hypervel's process-forking limit in the same plain style as the surrounding Artisan guide. Keep the section focused on supported user behavior. It avoids internal listener details and does not document direct include execution until the required public PsySH lifecycle ships in a stable release. --- src/boost/docs/artisan.md | 40 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/boost/docs/artisan.md b/src/boost/docs/artisan.md index 0e52a45413..17cd1af3c1 100644 --- a/src/boost/docs/artisan.md +++ b/src/boost/docs/artisan.md @@ -65,6 +65,14 @@ Tinker allows you to interact with your entire Hypervel application on the comma php artisan tinker ``` +You may also execute code without opening the interactive shell using the `--execute` option: + +```shell +php artisan tinker --execute='echo App\Models\User::count();' +``` + +The command returns an exit status of zero when the code completes successfully. If the code calls `exit`, Artisan returns the requested exit status. Uncaught exceptions return an exit status of one. + You can publish Tinker's configuration file using the `vendor:publish` command and Tinker's publish tag: ```shell @@ -81,7 +89,7 @@ php artisan vendor:publish --provider="Hypervel\Tinker\TinkerServiceProvider" > The `dispatch` helper function and `dispatch` method on the `Dispatchable` class depend on garbage collection to place the job on the queue. Therefore, when using Tinker, you should use `Bus::dispatch` or `Queue::push` to dispatch jobs. > [!NOTE] -> Hypervel Tinker disables PsySH's pcntl support because `pcntl_fork` is incompatible with Swoole's coroutine scheduler. +> Hypervel Tinker disables PsySH's process forking because `pcntl_fork` is incompatible with Swoole's coroutine scheduler. #### Command Allow List @@ -95,9 +103,17 @@ Tinker utilizes an "allow" list to determine which Artisan commands are allowed ``` -#### Classes That Should Not Be Aliased +#### Class Aliases + +Tinker does not automatically alias classes from your application's dependencies. To allow a specific vendor class or namespace, add its fully qualified name to the `alias` array of your `tinker.php` configuration file: + +```php +'alias' => [ + 'Vendor\Package', +], +``` -Typically, Tinker automatically aliases classes as you interact with them in Tinker. However, you may wish to never alias some classes. You may accomplish this by listing the classes in the `dont_alias` array of your `tinker.php` configuration file: +You may also prevent application classes from being aliased by adding them to the `dont_alias` array: ```php 'dont_alias' => [ @@ -105,6 +121,24 @@ Typically, Tinker automatically aliases classes as you interact with them in Tin ], ``` + +#### Custom Casters + +Tinker uses Symfony VarDumper casters to present objects in the shell. You may register custom casters in your `tinker.php` configuration file: + +```php +'casters' => [ + App\Money::class => App\Tinker\MoneyCaster::class . '::cast', +], +``` + +Application casters take precedence over Tinker's default casters. + + +#### Trusting Project Configuration + +PsySH may load project-specific configuration from a local `.psysh.php` file. Hypervel trusts this configuration by default. To ask before loading it or to reject it, change the `trust_project` option in your `tinker.php` configuration file or set the `TINKER_TRUST_PROJECT` environment variable to `prompt` or `never`. + ## Writing Commands From 2e5d5d728612f86977baf31540dfddece932709a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:12:45 +0000 Subject: [PATCH 07/16] Record the Tinker lifecycle audit Record the verified Tinker command, alias, caster, metadata, and documentation findings together with their final designs, performance limits, rejected alternatives, and regression coverage. Route the core audit and ledger to this work unit while keeping Tinker open for the one remaining release dependency: a stable PsySH version containing the public, exception-safe include lifecycle. Record the related upstream signal and full-run corrections without making them Hypervel completion gates. --- ...amework-coroutine-state-lifecycle-audit.md | 6 +- ...-coroutine-state-lifecycle-audit-ledger.md | 22 ++ ...ess-psysh-lifecycles-and-current-parity.md | 357 ++++++++++++++++++ 3 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md diff --git a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md index 716fe0c804..4ec9cf0050 100644 --- a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md +++ b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md @@ -990,9 +990,9 @@ An exceptionally large shared work unit may receive its own linked detail plan w This compact index routes the completed-work history that must be consulted with the full plan after compaction. Detailed history remains in the [companion ledger](2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md). -- **Active package or work unit:** `sentry`; the complete correctness, coroutine-ownership, parity, and performance audit is recorded under `Complete Sentry correctness, coroutine ownership, and performance`; detail plan `2026-08-08-1711-sentry-correctness-coroutine-ownership-and-performance.md`. -- **Ledger entries required for the active work:** `Complete Sentry correctness, coroutine ownership, and performance`; `Make coroutine creation and copied context failure-safe`; `Harden Core lifecycle callbacks and stdout logging`; `Isolate object-pool maintenance and remove false dependencies`; `Harden filesystem I/O, streaming, and response teardown`; `Complete Cache parity, cleanup, permanence, and tagged ownership`; `Complete Notifications correctness, Slack parity, and reentrant failure ownership`; `Complete Queue pooling, payload durability, and current Laravel parity`; `Correct AOP proxy generation and publication`; `Complete Redis pooling, subscriber transport, topology, parity, and lifecycle safety`; and `Harden Server startup, reload, and process lifecycles`. -- **Pending revalidation carried into the active work:** Telescope must retain captured fork values under `coroutine-08`; Sentry and every other named consumer are revalidated in this work unit. +- **Active package or work unit:** `tinker`; the in-progress correctness and PsySH lifecycle audit is recorded under `Advance Tinker correctness and PsySH lifecycles`; detail plan `2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`. +- **Ledger entries required for the active work:** `Advance Tinker correctness and PsySH lifecycles` and `Complete Console command, scheduling, and generator lifecycles`. +- **Pending revalidation carried into the active work:** None. Tinker completion waits only for a stable PsySH release containing public exception-safe include loading for `tinker-03` / `tinker-04` in PsySH #951, followed by direct integration and the final gate. Update these three lines when a package starts, completes, or gains a cross-package dependency. Name exact work-unit headings or shared finding IDs from the companion ledger; never use “see recent entries” or require a full-ledger reread. diff --git a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md index 5c8224c87f..aa9ca656de 100644 --- a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md +++ b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md @@ -2227,3 +2227,25 @@ Append package entries in checklist order. Keep each entry compact but complete - **Performance and complexity:** Disabled Telescope now installs less instrumentation. Request byte checks are cheaper, exception aggregation reduces writes, monitored tags retain one bulk insert, ordered deletes use indexed columns, event prefixes use one direct check, EventWatcher no longer resolves its dispatcher per recorded event, and schedule reconciliation mutates the pending object without an extra query. Enabled view reflection occurs only for recorded views with no per-view container lookup. DumpWatcher performs one cache read only on explicit non-`always` dumps. Other guards are constant-time cold telemetry checks; no request path gains a lock, retry, poll, network round trip, unbounded allocation, or meaningful performance regression. - **Validation and review:** Changed focused suites, complete Telescope coverage, affected sibling tests, frontend install/audit/build review, generated-asset provenance, formatting, both PHPStan configurations, the full parallel suite, Testbench package mode, dogfood, and stale scans passed at the applicable checkpoints. `git diff --check` reports only the intentional trailing whitespace in the generated Highlight.js PHP grammar at `src/telescope/dist/app.js:59`; it is load-bearing regex-class content and must not be normalized. Post-gate corrections passed targeted and complete affected coverage. Independent review verified final behavior, API/performance boundaries, counterfactual tests, and records with no remaining finding. - **Assessment:** Telescope is coroutine-safe, worker-lifecycle-safe, query- and storage-correct, failure-truthful, current at the audited Laravel surface, production-suitable when enabled, and cheaper when disabled. Every accepted finding is fixed at its lowest owner without a workaround, speculative abstraction, stale path, meaningful hot-path regression, unintended Laravel API break, or unresolved defect. + +### Advance Tinker correctness and PsySH lifecycles + +- **Status and inspected surface:** Independent Hypervel implementation is complete and focused coverage is green. The audit covered every reported Tinker finding, all Tinker source/tests/configuration/metadata/documentation, current Laravel Tinker, installed and current PsySH execution/include lifecycles, and connected Console programmatic execution. Completion remains blocked only on a stable PsySH release containing [PsySH #951](https://github.com/bobthecow/psysh/pull/951), followed by the include integration, final gate, self-review, and code review. The detailed design is recorded in [`2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`](2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md). + +| Findings | Final decision | +|---|---| +| `tinker-01` | Use a stateless one-shot Shell subclass that omits only PsySH's unbalanced direct `SignalHandler` until a released PsySH lifecycle makes it unnecessary. | +| `tinker-02`, `tinker-05`, `tinker-11` | Select direct execution for every non-null value, preserve requested exit codes without error rendering, and stop mutating the shared Console application's exception policy. | +| `tinker-03`, `tinker-04` | Consume #951's public exception-safe include lifecycle after its first stable release; do not copy, reflect, generate, or conditionally emulate dependency internals. | +| `tinker-06` | Normalize configured aliases once and match exact classes, namespace descendants, and vendor directory children on semantic boundaries. | +| `tinker-07` | Contain each Application presentation getter's `Throwable` independently so later virtual properties remain visible. | +| `tinker-08` | Omit disabled configured commands when Symfony returns `null`. | +| `tinker-09`, `tinker-10` | Remove the unused Contracts split dependency, pin root/split metadata and provider discovery, add upstream provenance, and document direct execution, process-forking policy, aliases, casters, and project trust. The PsySH floor and direct-include guidance wait for #951's release. | +| `psysh-01` | Track the owner-level signal/listener correction in [PsySH #952](https://github.com/bobthecow/psysh/pull/952) and full-run/ProcessForker settlement in stacked [PsySH #953](https://github.com/bobthecow/psysh/pull/953); neither gates Hypervel completion. | + +- **Architecture and ownership:** Each command invocation owns one shell and alias loader. One-shot execution filters only the process-global listener whose lifecycle PsySH 0.12.24 does not settle; interactive execution retains Ctrl-C handling. No static state, coroutine context, registry, cache, lock, retry, background task, or retained worker allocation was added. +- **Console revalidation:** Programmatic Console execution already bypasses Symfony's process-global wrapper through `Application::runProgrammatically()`, while root CLI execution remains owned by Kernel/Symfony. Removing Tinker's redundant `setCatchExceptions(false)` preserves both paths and the caller's configured policy. +- **Important rejected concerns:** No reflected private method, copied include loop, generated `require_once` code, compatibility branch, shell factory, signal snapshot around yielding Hypervel code, PTY harness, alias index, path canonicalization, classmap cache, or process isolation was added. `TESTBENCH_BASE_PATH` is intentionally absent from the falsey-execute child harness because the disposable clone's own `artisan` defines `BASE_PATH` directly. +- **Regression coverage:** Focused tests cover falsey direct values through the real disposable child application with open stdin, requested exit status, ordinary failures, SIGINT preservation, exception-policy preservation, disabled commands, coroutine execution, exact alias/vendor boundaries, per-property native errors, split metadata, provider discovery, and existing registration/configuration behavior. Include ordering, scope, failure continuation, error-handler restoration, and exit-status interaction remain the exact post-release regressions required for #951. +- **Performance and compatibility:** Changes run only while starting or using the developer command. Matching and filtering are bounded in-memory work over configured aliases/listeners/commands; no application request, queue, database, network, or worker hot path changes. Laravel-facing Tinker options and configuration remain compatible, and Hypervel's no-fork Swoole adaptation remains intact. +- **Assessment:** Every independent accepted finding is corrected directly without a workaround or speculative mechanism. Tinker remains open only for the released PsySH include owner, its direct integration/docs/tests, and the final validation/review workflow. diff --git a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md new file mode 100644 index 0000000000..05c5f8afab --- /dev/null +++ b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md @@ -0,0 +1,357 @@ +# Tinker Correctness, PsySH Lifecycles, and Current Parity + +## Status + +Independent Hypervel implementation and focused validation are complete. PsySH PRs [#951](https://github.com/bobthecow/psysh/pull/951), [#952](https://github.com/bobthecow/psysh/pull/952), and [#953](https://github.com/bobthecow/psysh/pull/953) contain the accepted upstream corrections. Tinker is complete only after a stable release containing #951 is consumed, the direct include integration is implemented, and the full validation and review workflow passes. #952 and #953 do not gate Hypervel completion. + +## Scope + +Correct the verified Tinker findings without turning this targeted maintenance unit into a second package-wide audit. Preserve Hypervel's coroutine-aware Console execution, prohibition on PsySH process forking, upstream Tinker APIs and configuration, optional Database/Process presentation, and operation-local shell/alias-loader ownership. + +References checked: + +- Hypervel Components `59442418c2e7cdf7dac9f532f34bf170580ae2d2`, including all Tinker source/tests and connected Console behavior; +- Laravel Tinker `a1fd59c74a05f93a8343d1ff002972aebc6aaa5e` (`3.x`); +- Laravel documentation `9c5a062c14069bab9054b558829e282f9593a065`; +- installed PsySH 0.12.24 (`ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1`); +- PsySH main `cd98f04e0e8d8611e4c619334e85e74f3096b24e`. + +This plan is the post-compaction implementation reference. It reproduces the core plan's "What this audit is not" section and principles 7–10 verbatim below. + +## What this audit is not + +This audit is not permission to add defensive machinery for every imaginable failure. Do not add an abstraction, state machine, retry loop, configurable timeout, registry, mutex, context slot, cache, or compatibility API merely because it sounds robust. + +Complexity must pay for itself with at least one of: + +- a demonstrated failure; +- a complete source trace proving a realistic vulnerable schedule; +- a clear general capability with real consumers and owner approval; +- deletion of greater or riskier complexity elsewhere. + +Typical Laravel lifecycle semantics define the supported contract. A package that intentionally relies on model events, middleware, listeners, transactions, or another documented mechanism is not defective merely because userland can explicitly bypass that mechanism. Do not build a parallel enforcement path for `withoutEvents()`, raw database writes, disabled middleware, direct transport access, or comparable deliberate bypasses unless the public contract explicitly promises behavior through that bypass. + +Underengineering is equally a failure. Fix every verified defect completely at its lowest owning boundary, never with a partial fix or a local patch over a broken shared contract, and always surface meaningful evidence-backed improvements rather than dropping them to avoid effort. Restraint applies to speculative machinery and cosmetic change, not to complete fixes or worthwhile opportunities. + +Do not treat an upstream difference as a bug without tracing it. Do not treat upstream parity as proof of correctness. A real Hypervel defect remains a defect when Laravel, Hyperf, Symfony, or an SDK has the same hole. + +The audit categories are discovery lenses, not boundaries around what may be corrected. Any genuine issue discovered while auditing, implementing, testing, or reviewing must be investigated, assigned to its lowest owning boundary, and taken through the applicable consensus, implementation, validation, review, and approval workflow—even when it is outside the current package, initial taxonomy, or changed diff. Do not dismiss a verified issue as unrelated or defer it merely to preserve package order. This rule applies only after the evidence threshold is met; it does not turn speculative concerns, deliberate bypasses, unsupported use, or contract violations into work. + +### 7. Preserve hot-path quality + +For every fix, inspect: + +- additional allocations; +- container or facade resolutions; +- locking and atomics; +- hashing and serialization; +- new yields or sleeps; +- retries and polling; +- logging or exception construction; +- retained worker memory; +- cache invalidation and eviction. + +A correctness guard on a cold failure path has a different cost from a new lock or resolver on every request. State the difference explicitly. + +Any proposed change with a measured or source-proven hot-path regression requires explicit owner approval before implementation, even when it fixes a defect. Present the expected frequency and magnitude, the evidence, and the viable alternatives. Do not hide an unavoidable tradeoff inside a general correctness claim. + +Performance improvements must provide a meaningful practical benefit after accounting for code complexity and divergence from upstream. Measure representative behavior where practical. Always surface an evidence-backed opportunity to the owner, but do not implement it without approval; a micro-optimization within measurement noise is neither a reason to diverge nor an actionable finding. + +### 8. Remove superseded design completely + +When a fix changes the owning model, delete obsolete helpers, callbacks, properties, config keys, comments, tests, and documentation. Do not leave a compatibility path or comment describing behavior that no longer exists. Preserve intentional upstream comments unless the new design makes them incorrect. + +### 9. Treat remediation patterns as candidates + +The established patterns later in this plan are a vocabulary, not a lookup table. Choose among per-call parameters, immutable values, scoped bindings, cloning, CoroutineContext, factories, explicit ownership, static reset, or resource teardown only after proving the real lifetime and owner. + +### 10. Reject speculative complexity + +Record low-confidence concerns under rejected or unresolved analysis. Do not implement them. Surface every evidence-backed, meaningful non-defect improvement to the owner with its benefit, cost, and alternatives, then stop for explicit approval. This requirement exists to keep worthwhile opportunities visible, not to discourage finding them. + +## Contracts and performance budget + +- Keep `--execute`, positional `include`, `commands`, `alias`, `dont_alias`, `casters`, and `trust_project` Laravel-shaped. +- Keep PsySH process forking disabled before shell construction. Local `.psysh.php` configuration cannot re-enable `ProcessForker`: listeners are constructed before local config is loaded and are never rebuilt. +- Interactive Tinker retains Ctrl-C handling. One-shot execution must not leave process-global signal or error-handler state behind. +- User casters keep overriding defaults. Database model and Process result casters remain optional; Foundation Application is a hard package dependency. +- No HTTP/request path changes. All added comparisons, filtering, and loading occur only while starting or running the developer command. There is no lock, yield, retry, cache, static registry, coroutine context, retained worker allocation, or repeated filesystem I/O beyond includes explicitly requested by the caller. + +## Final findings + +| ID | Defect | Final treatment | +|---|---|---| +| `tinker-01` | Direct execution invokes PsySH's `SignalHandler::onExecute()` without its balancing loop lifecycle, replacing process-global SIGINT state in surviving programmatic/ParaTest processes. | Use an execute-only shell without that listener unless a released PsySH source fully fixes direct execution settlement. | +| `tinker-02` | Truthy option checks send valid `--execute=0` and `--execute=''` values to the REPL branch. | Treat every non-null `--execute` value as direct execution. | +| `tinker-03` | `setIncludes()` configures files, but direct `Shell::execute()` never loads them. | Expose PsySH's real include lifecycle, consume its first stable release, and call it before direct execution. | +| `tinker-04` | PsySH catches only `Exception` while loading includes and restores its error handler only normally, so `ParseError` aborts later includes and leaves PsySH's process-global handler installed. | Restore the handler in the installing method's `finally` and contain `Throwable` per include. | +| `tinker-05` | `execute($code, true)` rethrows `BreakException`; Tinker's broad catch renders `exit(3)` as an error and returns 1. | Return the embedded exit code without error rendering. | +| `tinker-06` | Raw prefixes make `App\Nova` also match `App\NovaThing` and make `/app/vendor-local/...` look like `/app/vendor/...`. | Match normalized aliases and vendor directories on semantic boundaries. | +| `tinker-07` | One Application presentation getter throwing `Error` or `TypeError` escapes the per-property `Exception` boundary and aborts the dump. | Contain `Throwable` from each getter. | +| `tinker-08` | Symfony returns `null` for a disabled configured command, which PsySH forwards to its `callable|Command` parameter and rejects with `TypeError`. | Omit disabled command results. | +| `tinker-09` | Split metadata declares unused Contracts, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | +| `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Boost guide in Laravel-docs prose. | +| `tinker-11` | Tinker redundantly writes the Kernel-cached Console application's exception policy and can leave a caller's explicit setting changed. | Remove the mutation. | +| `psysh-01` | Public direct/piped execution calls Shell/listener acquisition hooks without the balancing `afterLoop()` lifecycle. | Submit the non-gating owner correction, record its reference, and consume it opportunistically when its released source is complete. | + +## Implementation + +### 1. Fix and release PsySH include ownership + +PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) makes `Shell::loadIncludes()` public. Preserve the scope-carrying closure and `include_once` semantics; widen only the failure boundary and make the installer own restoration: + +```php +public function loadIncludes(): void +{ + $load = function (self $__psysh__): void { + \set_error_handler([$__psysh__, 'handleError']); + + try { + foreach ($__psysh__->getIncludes() as $__psysh_include__) { + try { + include_once $__psysh_include__; + } catch (\Throwable $_e) { + $__psysh__->writeException($_e); + } + } + } finally { + \restore_error_handler(); + } + + unset($__psysh_include__); + + // Override any new local variables with pre-defined scope variables + \extract($__psysh__->getScopeVariables(false)); + + // ... then add the whole mess of variables back. + $__psysh__->setScopeVariables(\get_defined_vars()); + }; + + $load($this); +} +``` + +The `Throwable` change restores the original intent lost when PsySH commit `6d3d2177` removed the separate `Error` catch without widening the remaining `Exception` catch. Add upstream tests proving a `ParseError` is reported, later includes still load, scope variables survive, and the caller's error handler is restored on success/failure. + +Wait for a stable PsySH release containing the public method. Then update `psy/psysh` in both root `composer.json` and `src/tinker/composer.json` to that first release and run Composer update. Never call the method while the split constraint can resolve 0.12.22–0.12.24, where it is private. If upstream stalls or rejects the correction, stop and return the decision to the owner; do not add reflection, generated includes, copied dependency code, or a compatibility branch. + +### 2. Make one-shot execution exact + +Resolve the option once and select the shell once: + +```php +/** @var ?string $code */ +$code = $this->option('execute'); + +if ($code !== null) { + $config->setRawOutput(true); +} + +$shell = $code !== null + ? new ExecuteShell($config) + : new Shell($config); +``` + +`ExecuteShell` has no constructor or state. It overrides `getDefaultLoopListeners()`, calls the parent, and filters only `SignalHandler`: + +```php +class ExecuteShell extends Shell +{ + protected function getDefaultLoopListeners(): array + { + return array_filter( + parent::getDefaultLoopListeners(), + static fn (object $listener): bool => ! $listener instanceof SignalHandler, + ); + } +} +``` + +Do not filter `ProcessForker`: `setUsePcntl(false)` has already made it impossible at listener construction, including after local config loads. + +Before adding `ExecuteShell`, inspect the released PsySH source. Omit the class and use `Shell` when—and only when—the release contains all three direct-execution corrections: complete `ExecutionClosure`/`afterLoop()` pairing, both terminal-signal mutations gated by an active run, and exact prior SIGINT/async-mode restoration. A partial fix does not supersede the filter. Never retain both a redundant filter and the complete upstream lifecycle. + +The direct branch becomes: + +```php +if ($code !== null) { + try { + $shell->setOutput($this->output); + $shell->boot(); + $shell->loadIncludes(); + $shell->execute($code, true); + } catch (BreakException $e) { + return $e->getCode(); + } catch (Throwable $e) { + $shell->writeException($e); + + return 1; + } finally { + $loader->unregister(); + } + + return 0; +} +``` + +Keep the existing `$shell->setIncludes($this->argument('include'))` call before alias-loader registration and before either execution branch. `boot()` must precede `loadIncludes()` because project `.psysh.php` is loaded during boot and may contribute `defaultIncludes`; `execute()` then observes the already-booted shell. + +PsySH reports include failures per file and continues loading. Preserve that contract: a malformed include does not itself change Tinker's exit status; the status reflects the subsequently executed code. Do not reintroduce the rejected abort-on-first-failure divergence. + +Delete `$this->getApplication()->setCatchExceptions(false)`: Console already sets this policy, programmatic dispatch bypasses Symfony's wrapper, and Tinker must not mutate shared application state. Keep loader cleanup in both paths. + +In `handle()`, keep scalar/null trust configuration on `get()` and use typed array retrieval for alias configuration: + +```php +$config->setTrustProject($appConfig->get('tinker.trust_project')); + +$loader = ClassAliasAutoloader::register( + $shell, + $path, + $appConfig->array('tinker.alias', []), + $appConfig->array('tinker.dont_alias', []), +); +``` + +In `getCommands()`, the local `$config` is Hypervel's Config Repository. Use its typed array accessor and omit disabled configured commands: + +```php +$config = $this->getHypervel()->make('config'); + +foreach ($config->array('tinker.commands', []) as $command) { + if (($command = $this->getApplication()->addCommand( + $this->getHypervel()->make($command), + )) !== null) { + $commands[] = $command; + } +} +``` + +Do not extract a shell factory or command registry. The branch and null check are the whole required policy. + +### 3. Correct alias boundaries + +Normalize configured class/namespace names once in the constructor. Preserve Collection matching and explicit-include precedence: + +```php +$this->includedAliases = collect($includedAliases) + ->map(static fn (string $alias): string => trim($alias, '\\')); +$this->excludedAliases = collect($excludedAliases) + ->map(static fn (string $alias): string => trim($alias, '\\')); +``` + +Match exact classes or namespace descendants only: + +```php +private static function matchesAlias(string $class, string $alias): bool +{ + return $class === $alias || Str::startsWith($class, $alias . '\\'); +} +``` + +Use the matcher for both included and excluded aliases. A Composer classmap value is a file path, so vendor exclusion needs only the directory-child boundary: + +```php +if (Str::startsWith($path, $this->vendorPath . DIRECTORY_SEPARATOR)) { + return false; +} +``` + +Do not canonicalize paths, inspect the filesystem per class, cache the classmap across invocations, or add an alias index. + +### 4. Keep presentation failures local + +In `TinkerCaster::castApplication()`, import and catch `Throwable` around each getter, without an unused catch variable, so one failing optional virtual property does not suppress later ones: + +```php +foreach (self::$appProperties as $property) { + try { + $value = $app->{$property}(); + + if ($value !== null) { + $results[Caster::PREFIX_VIRTUAL . $property] = $value; + } + } catch (Throwable) { + } +} +``` + +Register the Foundation Application caster unconditionally because `hypervel/foundation` is a direct hard dependency. Keep Database and Process class guards. + +### 5. Correct metadata, provenance, and documentation + +In `src/tinker/composer.json`: + +- remove unused `hypervel/contracts`; +- keep root-consistent `symfony/console:^8.1` and `symfony/var-dumper:^8.1`; +- apply the released PsySH floor from section 1; +- retain only the Database suggestion. Do not add a Process suggestion solely for symmetry. + +Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency, the Database suggestion, and provider discovery. Add `Ported from: https://github.com/laravel/tinker` to the README. + +Update only the Tinker section of `src/boost/docs/artisan.md`, following the surrounding Laravel-docs prose. Document: + +- `--execute` and its zero/non-zero exit-status behavior, including that a reported include failure does not alter the status produced by the executed code; +- positional includes before direct execution; +- that Hypervel disables process forking, not all PCNTL support; +- `tinker.alias` vendor opt-in and `dont_alias` exclusions; +- custom `tinker.casters`; +- `trust_project`. + +Keep the guide concise: no exhaustive config reference, internal listener discussion, or default-caster listing. + +### 6. Track the separate PsySH execution-settlement correction + +This upstream defect does not gate Tinker completion because every Hypervel path where it both installs and survives is closed either by `ExecuteShell` or the complete released upstream correction; the piped CLI path exits immediately, and programmatic Tinker cannot obtain piped code from Symfony input. PsySH PR [#952](https://github.com/bobthecow/psysh/pull/952) contains the signal/listener correction, and stacked PR [#953](https://github.com/bobthecow/psysh/pull/953) contains the full-run and ProcessForker settlement correction. + +The upstream design is: + +1. Wrap the complete `ExecutionClosure` body in a new outer `try/finally` and call `Shell::afterLoop()` only after output-buffer completion and scope persistence, matching `ExecutionLoopClosure` ordering. +2. Add one Shell-owned run-active flag around the selected `doRun()` branch, acquired after boot/pending-code/autoloader setup and cleared in `finally`; expose the minimal query required by built-in listeners. +3. Gate both Shell-level and `SignalHandler` `stty isig` / `stty -isig` mutations on that active run. Direct `Shell::execute()` does not own terminal flags and performs neither mutation. +4. Snapshot and restore the exact prior SIGINT handler and `pcntl_async_signals()` mode in `SignalHandler`; add `pcntl_signal_get_handler` to its capability list. +5. Cover direct success/failure, piped noninteractive SignalHandler settlement, pcntl-enabled piped ProcessForker settlement, and unchanged interactive per-loop settlement. + +ProcessForker's child gains a benign `afterLoop()` call and still hardcodes `SIG_DFL`. The `throw-up` path's terminal restoration is covered by #953 rather than separate machinery. If the full signal fix reaches the PsySH release consumed by Tinker, remove `ExecuteShell` as described in section 2. Doing so changes long-running `--execute` Ctrl-C from the ordinary one-shot default exit (130) to PsySH's rendered interruption/failure (1); either is valid, and the complete upstream lifecycle makes the filter otherwise needless. + +### 7. Update durable records + +Add one compact Tinker ledger section covering `tinker-01` through `tinker-11`, the PsySH include release/constraint, `psysh-01` and its upstream reference, Console revalidation, final API/performance result, and rejected designs. Route the core Tinker line to this work unit. Check the core package checklist only after the blocking PsySH include release is consumed and implementation, validation, self-review, and code review are complete. + +## Tests and validation + +Run changed test files after each coherent source slice. Touch test methods with `: void`; make `ClassAliasAutoloaderTest::$loader` nullable and conditionally unregister it so setup failures remain primary. Use `ParallelTesting::tempDir()` and exception-safe cleanup instead of global `tempnam()`. + +Required Hypervel regressions: + +1. Successful and failing direct execution preserve a sentinel SIGINT handler; test cleanup restores the sentinel even after assertion failure. Do not assert async-signal mode at the Hypervel boundary because Symfony Console owns additional signal state. +2. A bounded subprocess runs the disposable runtime clone's own `artisan` at `BASE_PATH` to prove `--execute=0` and `--execute=''` select direct execution. The clone does not discover the root package, so temporarily add `TinkerServiceProvider` to its `bootstrap/providers.php` through the existing provider-file API and restore the original file in `finally`. Pass `COMPOSER_VENDOR_DIR` and `HYPERVEL_AUTOLOAD_PATH` to the child; `TESTBENCH_BASE_PATH` is not involved because the clone's entry point already owns `BASE_PATH`. Give the child an open stdin pipe that is deliberately not closed while awaiting it: the wrong REPL branch sees piped input and blocks in `getInput(false)`, while the direct branch returns immediately. Use a ten-second failure budget, treat timeout as test failure, and close every pipe in `finally`; do not require a PTY, invent another bootstrap, or add a production shell factory. +3. Positional and project-configured default includes share variables with evaluated code; malformed includes are reported, later includes still load, the prior error handler remains installed, and a successful executed expression still returns 0 after the reported include failure. +4. `exit(3)` returns 3 without evaluation-error output; ordinary throwables still return 1. +5. A disabled configured command is omitted while enabled commands retain order. +6. The public `isAliasable()` matrix covers exact class, namespace child, common-prefix sibling, trailing separator, exclusion, real vendor child, and vendor-prefix sibling without creating irreversible class aliases. +7. An Application getter throwing `Error` is omitted while later virtual properties remain. +8. Metadata/provenance and existing coroutine execution remain correct. + +Validation order: + +1. Run each changed Tinker test file, then the complete `tests/Tinker` group. +2. Validate both Composer manifests and the installed PsySH floor. +3. Run `composer fix` once after implementation. +4. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. +5. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. + +## Rejected designs and non-findings + +- No generated `require_once` source, private-method reflection, copied include loop, switch to PsySH's noninteractive runner, or temporary compatibility API. +- No signal/error-handler snapshot around yielding Hypervel code, process isolation, lock, listener registry, mode router, or coroutine context. +- No removal of interactive signal handling and no `ProcessForker` filter beyond the existing `setUsePcntl(false)` invariant. +- No class-alias registry, unalias attempt, path canonicalization, classmap cache, or concurrency machinery. PHP has no coroutine-local class table, and concurrent REPLs in one worker are unsupported. +- Keep `ClassAliasAutoloader::__destruct()`: while registered, the autoload callback retains the object; normal `finally` cleanup unregisters it first, and destruction remains an idempotent fallback. +- Keep configured commands on the invocation-local shell and existing caster precedence. No mutable worker state is introduced. +- Keep the null guard around dynamic Application getter results; only its failure boundary widens. +- Do not add default caster config, exhaustive docs, Process metadata for symmetry, or tests that merely mirror trivial mappings. + +## Expected result + +Tinker preserves its Laravel-facing API and Hypervel's coroutine/no-fork adaptations while direct execution becomes exact for falsey code, includes, exit status, disabled commands, and process-global cleanup. Alias discovery respects semantic boundaries; presentation degrades per property; metadata and docs describe the real package. All work remains cold developer-console work, with no application hot-path or high-scale footprint. No accepted defect, workaround, stale branch, compatibility shim, TODO, or speculative machinery remains in the completed Hypervel package. From a1dba213393d59d30427540873a2c7da97d5ee9c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:36:42 +0000 Subject: [PATCH 08/16] Update Tinker plan for PsySH include and execution lifecycles --- ...ess-psysh-lifecycles-and-current-parity.md | 66 ++++++------------- 1 file changed, 20 insertions(+), 46 deletions(-) diff --git a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md index 05c5f8afab..b9885102e9 100644 --- a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md +++ b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md @@ -2,7 +2,7 @@ ## Status -Independent Hypervel implementation and focused validation are complete. PsySH PRs [#951](https://github.com/bobthecow/psysh/pull/951), [#952](https://github.com/bobthecow/psysh/pull/952), and [#953](https://github.com/bobthecow/psysh/pull/953) contain the accepted upstream corrections. Tinker is complete only after a stable release containing #951 is consumed, the direct include integration is implemented, and the full validation and review workflow passes. #952 and #953 do not gate Hypervel completion. +Independent Hypervel implementation and focused validation are complete. Merged PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) contains the include-failure correction; merged PsySH PR [#954](https://github.com/bobthecow/psysh/pull/954) makes outermost direct execution honor configured includes without exposing the loader. PsySH PRs [#952](https://github.com/bobthecow/psysh/pull/952) and [#953](https://github.com/bobthecow/psysh/pull/953) propose the non-gating execution-lifecycle corrections. Tinker is complete only after a stable release containing #951 and #954 is consumed, the direct include integration is implemented, and the full validation and review workflow passes. ## Scope @@ -83,7 +83,7 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp |---|---|---| | `tinker-01` | Direct execution invokes PsySH's `SignalHandler::onExecute()` without its balancing loop lifecycle, replacing process-global SIGINT state in surviving programmatic/ParaTest processes. | Use an execute-only shell without that listener unless a released PsySH source fully fixes direct execution settlement. | | `tinker-02` | Truthy option checks send valid `--execute=0` and `--execute=''` values to the REPL branch. | Treat every non-null `--execute` value as direct execution. | -| `tinker-03` | `setIncludes()` configures files, but direct `Shell::execute()` never loads them. | Expose PsySH's real include lifecycle, consume its first stable release, and call it before direct execution. | +| `tinker-03` | `setIncludes()` configures files, but direct `Shell::execute()` never loads them. | Make PsySH load configured includes at the outermost `run()` or `execute()` boundary, consume its first stable release, and keep the loader private. | | `tinker-04` | PsySH catches only `Exception` while loading includes and restores its error handler only normally, so `ParseError` aborts later includes and leaves PsySH's process-global handler installed. | Restore the handler in the installing method's `finally` and contain `Throwable` per include. | | `tinker-05` | `execute($code, true)` rethrows `BreakException`; Tinker's broad catch renders `exit(3)` as an error and returns 1. | Return the embedded exit code without error rendering. | | `tinker-06` | Raw prefixes make `App\Nova` also match `App\NovaThing` and make `/app/vendor-local/...` look like `/app/vendor/...`. | Match normalized aliases and vendor directories on semantic boundaries. | @@ -92,48 +92,23 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp | `tinker-09` | Split metadata declares unused Contracts, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | | `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Boost guide in Laravel-docs prose. | | `tinker-11` | Tinker redundantly writes the Kernel-cached Console application's exception policy and can leave a caller's explicit setting changed. | Remove the mutation. | -| `psysh-01` | Public direct/piped execution calls Shell/listener acquisition hooks without the balancing `afterLoop()` lifecycle. | Submit the non-gating owner correction, record its reference, and consume it opportunistically when its released source is complete. | +| `psysh-01` | Public execution calls listener `onExecute()` hooks without a matching execution-completion hook; using `afterLoop()` for cleanup creates unpaired callbacks and misses nested command execution boundaries. | Add a paired `afterExecute()` lifecycle, keep loop cleanup on `afterLoop()`, record the upstream reference, and consume it opportunistically when its released source is complete. | ## Implementation ### 1. Fix and release PsySH include ownership -PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) makes `Shell::loadIncludes()` public. Preserve the scope-carrying closure and `include_once` semantics; widen only the failure boundary and make the installer own restoration: +PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) preserves the private include lifecycle while containing each `Throwable` and making the error-handler installer own restoration. PsySH PR [#954](https://github.com/bobthecow/psysh/pull/954) keeps that loader private and makes configured includes part of every outermost shell execution. ```php -public function loadIncludes(): void -{ - $load = function (self $__psysh__): void { - \set_error_handler([$__psysh__, 'handleError']); - - try { - foreach ($__psysh__->getIncludes() as $__psysh_include__) { - try { - include_once $__psysh_include__; - } catch (\Throwable $_e) { - $__psysh__->writeException($_e); - } - } - } finally { - \restore_error_handler(); - } - - unset($__psysh_include__); - - // Override any new local variables with pre-defined scope variables - \extract($__psysh__->getScopeVariables(false)); - - // ... then add the whole mess of variables back. - $__psysh__->setScopeVariables(\get_defined_vars()); - }; - - $load($this); -} +private int $executionDepth = 0; ``` -The `Throwable` change restores the original intent lost when PsySH commit `6d3d2177` removed the separate `Error` catch without widening the remaining `Exception` catch. Add upstream tests proving a `ParseError` is reported, later includes still load, scope variables survive, and the caller's error handler is restored on success/failure. +`doRun()` increments this counter around its existing mode dispatch and decrements it in `finally`. The interactive and non-interactive branches keep their current `beforeRun()` → `loadIncludes()` ordering but call the loader only at depth one. `execute()` boots first, increments the same counter, loads includes at depth one before `setCode()`, and decrements in `finally`. Loading before `setCode()` is required because reporting an include failure clears pending code. Nested calls from `timeit`, reflection/config commands, another `execute()`, or a re-entered `run()` do not reload includes. + +The `Throwable` change restores the original intent lost when PsySH commit `6d3d2177` removed the separate `Error` catch without widening the remaining `Exception` catch. #951 tests parse-error containment, continued loading, scope precedence, and caller-owned error-handler restoration. #954 covers direct loading plus nested direct execution, a command executing inside `run()`, and `run()` re-entry. Keep the output precondition documented; do not add a default output, public/protected loader, empty-includes fast path, helper abstraction, or command-specific branches. -Wait for a stable PsySH release containing the public method. Then update `psy/psysh` in both root `composer.json` and `src/tinker/composer.json` to that first release and run Composer update. Never call the method while the split constraint can resolve 0.12.22–0.12.24, where it is private. If upstream stalls or rejects the correction, stop and return the decision to the owner; do not add reflection, generated includes, copied dependency code, or a compatibility branch. +Wait for a stable PsySH release containing both #951 and #954. Then update `psy/psysh` in both root `composer.json` and `src/tinker/composer.json` to that first release and run Composer update. If upstream stalls or rejects the execution contract, stop and return the decision to the owner; do not add reflection, generated includes, copied dependency code, or a compatibility branch. ### 2. Make one-shot execution exact @@ -152,7 +127,7 @@ $shell = $code !== null : new Shell($config); ``` -`ExecuteShell` has no constructor or state. It overrides `getDefaultLoopListeners()`, calls the parent, and filters only `SignalHandler`: +`ExecuteShell` has no constructor or state. Its only job is to filter `SignalHandler` from the parent's default listeners: ```php class ExecuteShell extends Shell @@ -169,7 +144,7 @@ class ExecuteShell extends Shell Do not filter `ProcessForker`: `setUsePcntl(false)` has already made it impossible at listener construction, including after local config loads. -Before adding `ExecuteShell`, inspect the released PsySH source. Omit the class and use `Shell` when—and only when—the release contains all three direct-execution corrections: complete `ExecutionClosure`/`afterLoop()` pairing, both terminal-signal mutations gated by an active run, and exact prior SIGINT/async-mode restoration. A partial fix does not supersede the filter. Never retain both a redundant filter and the complete upstream lifecycle. +Before adding `ExecuteShell`, inspect the released PsySH source. Omit the class and use `Shell` when—and only when—the release contains all three direct-execution corrections: paired `onExecute()`/`afterExecute()` callbacks, both terminal-signal mutations gated by an active run, and exact prior SIGINT/async-mode restoration. A partial fix does not supersede the filter. Never retain both a redundant filter and the complete upstream lifecycle. The direct branch becomes: @@ -177,8 +152,6 @@ The direct branch becomes: if ($code !== null) { try { $shell->setOutput($this->output); - $shell->boot(); - $shell->loadIncludes(); $shell->execute($code, true); } catch (BreakException $e) { return $e->getCode(); @@ -194,7 +167,7 @@ if ($code !== null) { } ``` -Keep the existing `$shell->setIncludes($this->argument('include'))` call before alias-loader registration and before either execution branch. `boot()` must precede `loadIncludes()` because project `.psysh.php` is loaded during boot and may contribute `defaultIncludes`; `execute()` then observes the already-booted shell. +Keep the existing `$shell->setIncludes($this->argument('include'))` call before alias-loader registration and before either execution branch. PsySH's `execute()` boots before loading includes because project `.psysh.php` may contribute `defaultIncludes`. Keep `setOutput()` before execution because include failures use the configured output. PsySH reports include failures per file and continues loading. Preserve that contract: a malformed include does not itself change Tinker's exit status; the status reflects the subsequently executed code. Do not reintroduce the rejected abort-on-first-failure divergence. @@ -306,13 +279,14 @@ This upstream defect does not gate Tinker completion because every Hypervel path The upstream design is: -1. Wrap the complete `ExecutionClosure` body in a new outer `try/finally` and call `Shell::afterLoop()` only after output-buffer completion and scope persistence, matching `ExecutionLoopClosure` ordering. -2. Add one Shell-owned run-active flag around the selected `doRun()` branch, acquired after boot/pending-code/autoloader setup and cleared in `finally`; expose the minimal query required by built-in listeners. -3. Gate both Shell-level and `SignalHandler` `stty isig` / `stty -isig` mutations on that active run. Direct `Shell::execute()` does not own terminal flags and performs neither mutation. -4. Snapshot and restore the exact prior SIGINT handler and `pcntl_async_signals()` mode in `SignalHandler`; add `pcntl_signal_get_handler` to its capability list. -5. Cover direct success/failure, piped noninteractive SignalHandler settlement, pcntl-enabled piped ProcessForker settlement, and unchanged interactive per-loop settlement. +1. Add `afterExecute(Shell $shell)` directly to PsySH's listener contract and no-op base listener. `Shell::afterExecute()` reverse-dispatches it once for every `onExecute()` call; it does not own loop output tracking or Shell terminal-mode cleanup. +2. Call `afterExecute()` from `ExecutionClosure` in its existing outer `finally`, after output-buffer cleanup and scope persistence. In `ExecutionLoopClosure`, keep `getInput()` outside the execution span and call `afterExecute()` after result rendering so Ctrl-C remains handled during long dumps. `afterLoop()` returns to its original loop-only contract and call sites. +3. Give `SignalHandler` an execution-depth counter. The outer `onExecute()` snapshots the prior SIGINT handler and async-signal mode and installs PsySH's handler; nested calls share that state. Each matching `afterExecute()` decrements the counter, and only the outer completion restores the prior process and terminal state. +4. Replace Shell's boolean run-active flag with a nesting counter around `doRun()`. `isRunActive()` and Shell's interactive signal-character gate read `runDepth > 0`, so re-entered runs cannot clear the outer run's ownership. +5. Keep the private execution-depth counter from #954 solely for outermost include loading. Do not use it to suppress listener callbacks; that would leave nested `onExecute()` calls unpaired. +6. Cover direct success/failure with zero loop callbacks, nested direct and `timeit` execution with one completion per execution callback, nested run ownership, piped noninteractive settlement, and unchanged interactive loop cleanup. -ProcessForker's child gains a benign `afterLoop()` call and still hardcodes `SIG_DFL`. The `throw-up` path's terminal restoration is covered by #953 rather than separate machinery. If the full signal fix reaches the PsySH release consumed by Tinker, remove `ExecuteShell` as described in section 2. Doing so changes long-running `--execute` Ctrl-C from the ordinary one-shot default exit (130) to PsySH's rendered interruption/failure (1); either is valid, and the complete upstream lifecycle makes the filter otherwise needless. +ProcessForker's matching execution-cleanup gap remains owned by #953, together with its full-run and `throw-up` lifecycle corrections. If the complete SignalHandler fix reaches the PsySH release consumed by Tinker, remove `ExecuteShell` as described in section 2. Doing so changes long-running `--execute` Ctrl-C from the ordinary one-shot default exit (130) to PsySH's rendered interruption/failure (1); either is valid, and the complete upstream lifecycle makes the filter otherwise needless. ### 7. Update durable records @@ -343,7 +317,7 @@ Validation order: ## Rejected designs and non-findings -- No generated `require_once` source, private-method reflection, copied include loop, switch to PsySH's noninteractive runner, or temporary compatibility API. +- No public/protected include loader, nested include reloading, generated `require_once` source, private-method reflection, copied include loop, switch to PsySH's noninteractive runner, or temporary compatibility API. - No signal/error-handler snapshot around yielding Hypervel code, process isolation, lock, listener registry, mode router, or coroutine context. - No removal of interactive signal handling and no `ProcessForker` filter beyond the existing `setUsePcntl(false)` invariant. - No class-alias registry, unalias attempt, path canonicalization, classmap cache, or concurrency machinery. PHP has no coroutine-local class table, and concurrent REPLs in one worker are unsupported. From f0025b27a74aca720496cf7015d5269e50ed0dc9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:23:31 +0000 Subject: [PATCH 09/16] Update Tinker plan for current PsySH main Use PsySH dev-main while Hypervel 0.4 is under development and remove the obsolete local shell subclass from the final design. Drop completed upstream PR history, preserve the newer Tinker work already on 0.4, point the guide at its current location, and keep only the dependency behavior needed to finish implementation and validation. --- ...ess-psysh-lifecycles-and-current-parity.md | 88 ++++++------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md index b9885102e9..3f720dc411 100644 --- a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md +++ b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md @@ -2,7 +2,7 @@ ## Status -Independent Hypervel implementation and focused validation are complete. Merged PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) contains the include-failure correction; merged PsySH PR [#954](https://github.com/bobthecow/psysh/pull/954) makes outermost direct execution honor configured includes without exposing the loader. PsySH PRs [#952](https://github.com/bobthecow/psysh/pull/952) and [#953](https://github.com/bobthecow/psysh/pull/953) propose the non-gating execution-lifecycle corrections. Tinker is complete only after a stable release containing #951 and #954 is consumed, the direct include integration is implemented, and the full validation and review workflow passes. +The implementation and focused validation are complete against the branch's original base. Merge current `0.4`, preserve its newer Tinker behavior, consume PsySH `dev-main`, remove the obsolete local shell subclass, and repeat the full validation and review workflow. Replace `dev-main` with the first compatible stable PsySH release containing the required behavior before Hypervel 0.4 is released. ## Scope @@ -10,11 +10,9 @@ Correct the verified Tinker findings without turning this targeted maintenance u References checked: -- Hypervel Components `59442418c2e7cdf7dac9f532f34bf170580ae2d2`, including all Tinker source/tests and connected Console behavior; -- Laravel Tinker `a1fd59c74a05f93a8343d1ff002972aebc6aaa5e` (`3.x`); -- Laravel documentation `9c5a062c14069bab9054b558829e282f9593a065`; -- installed PsySH 0.12.24 (`ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1`); -- PsySH main `cd98f04e0e8d8611e4c619334e85e74f3096b24e`. +- current Hypervel Components `0.4`, including all Tinker source/tests and connected Console behavior; +- current Laravel Tinker `3.x` and Laravel documentation; +- current PsySH `main`. This plan is the post-compaction implementation reference. It reproduces the core plan's "What this audit is not" section and principles 7–10 verbatim below. @@ -73,6 +71,7 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp - Keep `--execute`, positional `include`, `commands`, `alias`, `dont_alias`, `casters`, and `trust_project` Laravel-shaped. - Keep PsySH process forking disabled before shell construction. Local `.psysh.php` configuration cannot re-enable `ProcessForker`: listeners are constructed before local config is loaded and are never rebuilt. +- Use PsySH's normal `Shell`; current `dev-main` owns include loading and direct-execution signal cleanup without a Hypervel subclass. - Interactive Tinker retains Ctrl-C handling. One-shot execution must not leave process-global signal or error-handler state behind. - User casters keep overriding defaults. Database model and Process result casters remain optional; Foundation Application is a hard package dependency. - No HTTP/request path changes. All added comparisons, filtering, and loading occur only while starting or running the developer command. There is no lock, yield, retry, cache, static registry, coroutine context, retained worker allocation, or repeated filesystem I/O beyond includes explicitly requested by the caller. @@ -81,34 +80,31 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp | ID | Defect | Final treatment | |---|---|---| -| `tinker-01` | Direct execution invokes PsySH's `SignalHandler::onExecute()` without its balancing loop lifecycle, replacing process-global SIGINT state in surviving programmatic/ParaTest processes. | Use an execute-only shell without that listener unless a released PsySH source fully fixes direct execution settlement. | +| `tinker-01` | Direct execution invoked PsySH's `SignalHandler::onExecute()` without matching cleanup, replacing process-global SIGINT state in surviving programmatic/ParaTest processes. | Use PsySH's paired execution cleanup through its normal `Shell`; do not retain a local listener filter. | | `tinker-02` | Truthy option checks send valid `--execute=0` and `--execute=''` values to the REPL branch. | Treat every non-null `--execute` value as direct execution. | -| `tinker-03` | `setIncludes()` configures files, but direct `Shell::execute()` never loads them. | Make PsySH load configured includes at the outermost `run()` or `execute()` boundary, consume its first stable release, and keep the loader private. | -| `tinker-04` | PsySH catches only `Exception` while loading includes and restores its error handler only normally, so `ParseError` aborts later includes and leaves PsySH's process-global handler installed. | Restore the handler in the installing method's `finally` and contain `Throwable` per include. | +| `tinker-03` | `setIncludes()` configured files, but direct `Shell::execute()` did not load them. | Depend temporarily on PsySH `dev-main`, which loads configured includes at the outermost `run()` or `execute()` boundary while keeping the loader private. | +| `tinker-04` | PsySH caught only `Exception` while loading includes and restored its error handler only normally, so `ParseError` aborted later includes and left PsySH's process-global handler installed. | Use the corrected `dev-main` include lifecycle, which restores the handler in `finally` and contains each `Throwable`. | | `tinker-05` | `execute($code, true)` rethrows `BreakException`; Tinker's broad catch renders `exit(3)` as an error and returns 1. | Return the embedded exit code without error rendering. | | `tinker-06` | Raw prefixes make `App\Nova` also match `App\NovaThing` and make `/app/vendor-local/...` look like `/app/vendor/...`. | Match normalized aliases and vendor directories on semantic boundaries. | | `tinker-07` | One Application presentation getter throwing `Error` or `TypeError` escapes the per-property `Exception` boundary and aborts the dump. | Contain `Throwable` from each getter. | | `tinker-08` | Symfony returns `null` for a disabled configured command, which PsySH forwards to its `callable|Command` parameter and rejects with `TypeError`. | Omit disabled command results. | | `tinker-09` | Split metadata declares unused Contracts, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | -| `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Boost guide in Laravel-docs prose. | +| `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Tinker guide in Laravel-docs prose. | | `tinker-11` | Tinker redundantly writes the Kernel-cached Console application's exception policy and can leave a caller's explicit setting changed. | Remove the mutation. | -| `psysh-01` | Public execution calls listener `onExecute()` hooks without a matching execution-completion hook; using `afterLoop()` for cleanup creates unpaired callbacks and misses nested command execution boundaries. | Add a paired `afterExecute()` lifecycle, keep loop cleanup on `afterLoop()`, record the upstream reference, and consume it opportunistically when its released source is complete. | ## Implementation -### 1. Fix and release PsySH include ownership +### 1. Merge current `0.4` and consume PsySH `dev-main` -PsySH PR [#951](https://github.com/bobthecow/psysh/pull/951) preserves the private include lifecycle while containing each `Throwable` and making the error-handler installer own restoration. PsySH PR [#954](https://github.com/bobthecow/psysh/pull/954) keeps that loader private and makes configured includes part of every outermost shell execution. +Merge current `0.4` into this branch before further source changes. Resolve overlaps by preserving all newer Tinker behavior from `0.4`, including lazy command resolution, optional command and alias lists, the configured caster map, model appends through `getAppends()`, and documentation at `src/docs/artisan.md`. Combine those changes with the audit fixes; do not restore the old Boost documentation path, eager command resolution, direct model-property access, or narrower caster failure boundary. -```php -private int $executionDepth = 0; -``` +Use Composer to change the root `psy/psysh` requirement to `dev-main`, set the split package requirement in `src/tinker/composer.json` to the same constraint, and update the installed dependency. Current PsySH `main` provides all behavior Hypervel needs: -`doRun()` increments this counter around its existing mode dispatch and decrements it in `finally`. The interactive and non-interactive branches keep their current `beforeRun()` → `loadIncludes()` ordering but call the loader only at depth one. `execute()` boots first, increments the same counter, loads includes at depth one before `setCode()`, and decrements in `finally`. Loading before `setCode()` is required because reporting an include failure clears pending code. Nested calls from `timeit`, reflection/config commands, another `execute()`, or a re-entered `run()` do not reload includes. +- configured includes load once at the outermost `run()` or `execute()` boundary; +- each include `Throwable` is reported without stopping later includes, and the caller's error handler is restored; +- execution callbacks are paired, and `SignalHandler` restores the exact SIGINT handler and async-signal setting it replaced. -The `Throwable` change restores the original intent lost when PsySH commit `6d3d2177` removed the separate `Error` catch without widening the remaining `Exception` catch. #951 tests parse-error containment, continued loading, scope precedence, and caller-owned error-handler restoration. #954 covers direct loading plus nested direct execution, a command executing inside `run()`, and `run()` re-entry. Keep the output precondition documented; do not add a default output, public/protected loader, empty-includes fast path, helper abstraction, or command-specific branches. - -Wait for a stable PsySH release containing both #951 and #954. Then update `psy/psysh` in both root `composer.json` and `src/tinker/composer.json` to that first release and run Composer update. If upstream stalls or rejects the execution contract, stop and return the decision to the owner; do not add reflection, generated includes, copied dependency code, or a compatibility branch. +Replace `dev-main` with the first compatible stable release containing these behaviors before Hypervel 0.4 is released. Do not copy or reflect into PsySH internals, expose its include loader, add a version branch, or keep a local shell subclass. ### 2. Make one-shot execution exact @@ -122,29 +118,10 @@ if ($code !== null) { $config->setRawOutput(true); } -$shell = $code !== null - ? new ExecuteShell($config) - : new Shell($config); -``` - -`ExecuteShell` has no constructor or state. Its only job is to filter `SignalHandler` from the parent's default listeners: - -```php -class ExecuteShell extends Shell -{ - protected function getDefaultLoopListeners(): array - { - return array_filter( - parent::getDefaultLoopListeners(), - static fn (object $listener): bool => ! $listener instanceof SignalHandler, - ); - } -} +$shell = new Shell($config); ``` -Do not filter `ProcessForker`: `setUsePcntl(false)` has already made it impossible at listener construction, including after local config loads. - -Before adding `ExecuteShell`, inspect the released PsySH source. Omit the class and use `Shell` when—and only when—the release contains all three direct-execution corrections: paired `onExecute()`/`afterExecute()` callbacks, both terminal-signal mutations gated by an active run, and exact prior SIGINT/async-mode restoration. A partial fix does not supersede the filter. Never retain both a redundant filter and the complete upstream lifecycle. +Delete `ExecuteShell` and use the same PsySH `Shell` for direct and interactive execution. Current PsySH `main` owns direct-execution signal cleanup. Keep `setUsePcntl(false)` before shell construction because `ProcessForker` remains incompatible with Swoole; do not add another listener filter. The direct branch becomes: @@ -257,12 +234,12 @@ In `src/tinker/composer.json`: - remove unused `hypervel/contracts`; - keep root-consistent `symfony/console:^8.1` and `symfony/var-dumper:^8.1`; -- apply the released PsySH floor from section 1; +- require PsySH `dev-main` as described in section 1; - retain only the Database suggestion. Do not add a Process suggestion solely for symmetry. Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency, the Database suggestion, and provider discovery. Add `Ported from: https://github.com/laravel/tinker` to the README. -Update only the Tinker section of `src/boost/docs/artisan.md`, following the surrounding Laravel-docs prose. Document: +Update only the Tinker section of `src/docs/artisan.md`, following the surrounding Laravel-docs prose. Document: - `--execute` and its zero/non-zero exit-status behavior, including that a reported include failure does not alter the status produced by the executed code; - positional includes before direct execution; @@ -273,24 +250,9 @@ Update only the Tinker section of `src/boost/docs/artisan.md`, following the sur Keep the guide concise: no exhaustive config reference, internal listener discussion, or default-caster listing. -### 6. Track the separate PsySH execution-settlement correction - -This upstream defect does not gate Tinker completion because every Hypervel path where it both installs and survives is closed either by `ExecuteShell` or the complete released upstream correction; the piped CLI path exits immediately, and programmatic Tinker cannot obtain piped code from Symfony input. PsySH PR [#952](https://github.com/bobthecow/psysh/pull/952) contains the signal/listener correction, and stacked PR [#953](https://github.com/bobthecow/psysh/pull/953) contains the full-run and ProcessForker settlement correction. - -The upstream design is: - -1. Add `afterExecute(Shell $shell)` directly to PsySH's listener contract and no-op base listener. `Shell::afterExecute()` reverse-dispatches it once for every `onExecute()` call; it does not own loop output tracking or Shell terminal-mode cleanup. -2. Call `afterExecute()` from `ExecutionClosure` in its existing outer `finally`, after output-buffer cleanup and scope persistence. In `ExecutionLoopClosure`, keep `getInput()` outside the execution span and call `afterExecute()` after result rendering so Ctrl-C remains handled during long dumps. `afterLoop()` returns to its original loop-only contract and call sites. -3. Give `SignalHandler` an execution-depth counter. The outer `onExecute()` snapshots the prior SIGINT handler and async-signal mode and installs PsySH's handler; nested calls share that state. Each matching `afterExecute()` decrements the counter, and only the outer completion restores the prior process and terminal state. -4. Replace Shell's boolean run-active flag with a nesting counter around `doRun()`. `isRunActive()` and Shell's interactive signal-character gate read `runDepth > 0`, so re-entered runs cannot clear the outer run's ownership. -5. Keep the private execution-depth counter from #954 solely for outermost include loading. Do not use it to suppress listener callbacks; that would leave nested `onExecute()` calls unpaired. -6. Cover direct success/failure with zero loop callbacks, nested direct and `timeit` execution with one completion per execution callback, nested run ownership, piped noninteractive settlement, and unchanged interactive loop cleanup. - -ProcessForker's matching execution-cleanup gap remains owned by #953, together with its full-run and `throw-up` lifecycle corrections. If the complete SignalHandler fix reaches the PsySH release consumed by Tinker, remove `ExecuteShell` as described in section 2. Doing so changes long-running `--execute` Ctrl-C from the ordinary one-shot default exit (130) to PsySH's rendered interruption/failure (1); either is valid, and the complete upstream lifecycle makes the filter otherwise needless. - -### 7. Update durable records +### 6. Update durable records -Add one compact Tinker ledger section covering `tinker-01` through `tinker-11`, the PsySH include release/constraint, `psysh-01` and its upstream reference, Console revalidation, final API/performance result, and rejected designs. Route the core Tinker line to this work unit. Check the core package checklist only after the blocking PsySH include release is consumed and implementation, validation, self-review, and code review are complete. +Add one compact Tinker ledger section covering `tinker-01` through `tinker-11`, the temporary PsySH `dev-main` constraint, Console revalidation, final API/performance result, and rejected designs. Route the core Tinker line to this work unit. Preserve every newer `0.4` record while resolving the audit-plan and ledger conflicts. Check the core package checklist only after current `0.4` is merged, `dev-main` is installed, and implementation, validation, self-review, and code review are complete. ## Tests and validation @@ -310,16 +272,16 @@ Required Hypervel regressions: Validation order: 1. Run each changed Tinker test file, then the complete `tests/Tinker` group. -2. Validate both Composer manifests and the installed PsySH floor. +2. Validate both Composer manifests and confirm the installed PsySH source is current `dev-main`. 3. Run `composer fix` once after implementation. 4. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. 5. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. ## Rejected designs and non-findings -- No public/protected include loader, nested include reloading, generated `require_once` source, private-method reflection, copied include loop, switch to PsySH's noninteractive runner, or temporary compatibility API. +- No local shell subclass, listener filter, public/protected include loader, nested include reloading, generated `require_once` source, private-method reflection, copied include loop, switch to PsySH's noninteractive runner, or version-specific compatibility path. - No signal/error-handler snapshot around yielding Hypervel code, process isolation, lock, listener registry, mode router, or coroutine context. -- No removal of interactive signal handling and no `ProcessForker` filter beyond the existing `setUsePcntl(false)` invariant. +- No removal of interactive signal handling. Keep the existing `setUsePcntl(false)` invariant; do not add a `ProcessForker` listener filter. - No class-alias registry, unalias attempt, path canonicalization, classmap cache, or concurrency machinery. PHP has no coroutine-local class table, and concurrent REPLs in one worker are unsupported. - Keep `ClassAliasAutoloader::__destruct()`: while registered, the autoload callback retains the object; normal `finally` cleanup unregisters it first, and destruction remains an idempotent fallback. - Keep configured commands on the invocation-local shell and existing caster precedence. No mutable worker state is introduced. From 11310e3d0568f55794db10ee4763b638587f747c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:25:06 +0000 Subject: [PATCH 10/16] Finish Tinker execution on current PsySH Use PsySH's normal Shell for both direct and interactive execution now that the dependency owns include loading and paired signal cleanup. Remove the temporary local shell subclass, preserve exact exit codes, keep caller-owned Console policy unchanged, and retain disabled-command filtering. Point both manifests at PsySH dev-main until the required behavior has a stable release. Remove metadata that no longer reflects the dependency graph, register built-in casters without dead class checks, and cover includes, process state, nullable trust, command selection, and split-package metadata. --- composer.json | 2 +- src/tinker/composer.json | 5 +- src/tinker/src/Console/TinkerCommand.php | 19 +-- src/tinker/src/ExecuteShell.php | 22 ---- tests/Tinker/PackageMetadataTest.php | 4 +- tests/Tinker/TinkerCommandTest.php | 145 ++++++++++++++++++--- tests/Tinker/TinkerServiceProviderTest.php | 2 +- 7 files changed, 133 insertions(+), 66 deletions(-) delete mode 100644 src/tinker/src/ExecuteShell.php diff --git a/composer.json b/composer.json index 9542532547..44f4cae62f 100644 --- a/composer.json +++ b/composer.json @@ -198,7 +198,7 @@ "psr/http-message": "^2.0", "psr/log": "^3.0", "psr/simple-cache": "^3.0", - "psy/psysh": "^0.12.22", + "psy/psysh": "dev-main", "sentry/sentry": "dev-master", "spomky-labs/otphp": "^11.0", "symfony/console": "^8.1", diff --git a/src/tinker/composer.json b/src/tinker/composer.json index 99ca4d0acf..fb7dcd1772 100644 --- a/src/tinker/composer.json +++ b/src/tinker/composer.json @@ -36,13 +36,10 @@ "hypervel/console": "^0.4", "hypervel/foundation": "^0.4", "hypervel/support": "^0.4", - "psy/psysh": "^0.12.22", + "psy/psysh": "dev-main", "symfony/console": "^8.1", "symfony/var-dumper": "^8.1" }, - "suggest": { - "hypervel/database": "Required for Eloquent model casting in Tinker (^0.4)." - }, "config": { "sort-packages": true }, diff --git a/src/tinker/src/Console/TinkerCommand.php b/src/tinker/src/Console/TinkerCommand.php index 0325013ced..1c469c9c59 100644 --- a/src/tinker/src/Console/TinkerCommand.php +++ b/src/tinker/src/Console/TinkerCommand.php @@ -7,7 +7,6 @@ use Hypervel\Console\Command; use Hypervel\Support\Env; use Hypervel\Tinker\ClassAliasAutoloader; -use Hypervel\Tinker\ExecuteShell; use Psy\Configuration; use Psy\Exception\BreakException; use Psy\Shell; @@ -64,9 +63,7 @@ public function handle(): int $config->setRawOutput(true); } - $shell = $code !== null - ? new ExecuteShell($config) - : new Shell($config); + $shell = new Shell($config); $shell->addCommands($this->getCommands()); $shell->setIncludes($this->argument('include')); @@ -85,7 +82,6 @@ public function handle(): int if ($code !== null) { try { $shell->setOutput($this->output); - $shell->boot(); $shell->execute($code, true); } catch (BreakException $e) { return $e->getCode(); @@ -142,18 +138,11 @@ protected function getCasters(): array 'Hypervel\Support\Collection' => 'Hypervel\Tinker\TinkerCaster::castCollection', 'Hypervel\Support\HtmlString' => 'Hypervel\Tinker\TinkerCaster::castHtmlString', 'Hypervel\Support\Stringable' => 'Hypervel\Tinker\TinkerCaster::castStringable', + 'Hypervel\Database\Eloquent\Model' => 'Hypervel\Tinker\TinkerCaster::castModel', + 'Hypervel\Process\ProcessResult' => 'Hypervel\Tinker\TinkerCaster::castProcessResult', + 'Hypervel\Foundation\Application' => 'Hypervel\Tinker\TinkerCaster::castApplication', ]; - if (class_exists('Hypervel\Database\Eloquent\Model')) { - $casters['Hypervel\Database\Eloquent\Model'] = 'Hypervel\Tinker\TinkerCaster::castModel'; - } - - if (class_exists('Hypervel\Process\ProcessResult')) { - $casters['Hypervel\Process\ProcessResult'] = 'Hypervel\Tinker\TinkerCaster::castProcessResult'; - } - - $casters['Hypervel\Foundation\Application'] = 'Hypervel\Tinker\TinkerCaster::castApplication'; - $config = $this->getHypervel()->make('config'); return array_merge($casters, $config->array('tinker.casters', [])); diff --git a/src/tinker/src/ExecuteShell.php b/src/tinker/src/ExecuteShell.php deleted file mode 100644 index 387356b45b..0000000000 --- a/src/tinker/src/ExecuteShell.php +++ /dev/null @@ -1,22 +0,0 @@ - ! $listener instanceof SignalHandler, - ); - } -} diff --git a/tests/Tinker/PackageMetadataTest.php b/tests/Tinker/PackageMetadataTest.php index d2799b23ac..878c3400e1 100644 --- a/tests/Tinker/PackageMetadataTest.php +++ b/tests/Tinker/PackageMetadataTest.php @@ -45,9 +45,7 @@ public function testPackageMetadataMatchesTheRootPackage(): void } $this->assertArrayNotHasKey('hypervel/contracts', $composer['require']); - $this->assertSame([ - 'hypervel/database' => 'Required for Eloquent model casting in Tinker (^0.4).', - ], $composer['suggest']); + $this->assertArrayNotHasKey('suggest', $composer); $this->assertSame([ TinkerServiceProvider::class, ], $composer['extra']['hypervel']['providers']); diff --git a/tests/Tinker/TinkerCommandTest.php b/tests/Tinker/TinkerCommandTest.php index 0549377c34..a1ad75396e 100644 --- a/tests/Tinker/TinkerCommandTest.php +++ b/tests/Tinker/TinkerCommandTest.php @@ -20,6 +20,7 @@ use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Psy\Configuration; use Psy\VarDumper\Presenter; +use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; @@ -100,6 +101,14 @@ public function testOptionalCommandAndAliasListsMayBeOmitted(): void ->assertExitCode(0); } + public function testNullProjectTrustIsAccepted(): void + { + config()->set('tinker.trust_project', null); + + $this->artisan('tinker', ['--execute' => 'echo "hello";']) + ->assertExitCode(0); + } + public function testExecuteFailure(): void { $this->artisan('tinker', ['--execute' => 'throw new \Exception("fail");']) @@ -113,6 +122,82 @@ public function testExecuteReturnsRequestedExitCodeWithoutRenderingAnError(): vo ->assertExitCode(3); } + public function testExecuteLoadsPositionalAndProjectIncludes(): void + { + $workingDirectory = getcwd(); + $positionalInclude = $this->temporaryDirectory . '/scope-positional.php'; + $projectInclude = $this->temporaryDirectory . '/scope-project.php'; + $result = $this->temporaryDirectory . '/scope-result.txt'; + + file_put_contents($positionalInclude, 'temporaryDirectory . '/.psysh.php', + ' [' . var_export($projectInclude, true) . ']];', + ); + + $this->assertTrue(chdir($this->temporaryDirectory)); + + try { + $this->artisan('tinker', [ + 'include' => [$positionalInclude], + '--execute' => sprintf( + "file_put_contents('%s', \$positionalValue . ':' . \$projectValue);", + addslashes($result), + ), + ])->assertExitCode(0); + } finally { + chdir($workingDirectory); + } + + $this->assertSame('positional:project', file_get_contents($result)); + } + + public function testExecuteContinuesAfterMalformedIncludeAndRestoresErrorHandler(): void + { + $invalidInclude = $this->temporaryDirectory . '/failure-invalid.php'; + $validInclude = $this->temporaryDirectory . '/failure-valid.php'; + $result = $this->temporaryDirectory . '/failure-result.txt'; + + file_put_contents($invalidInclude, 'withoutMockingConsoleOutput(); + + $exitCode = $this->artisan('tinker', [ + 'include' => [$invalidInclude, $validInclude], + '--execute' => sprintf( + "file_put_contents('%s', \$includedValue);", + addslashes($result), + ), + ]); + } finally { + $observedHandler = set_error_handler(static function (): bool { + return true; + }); + restore_error_handler(); + + if ($observedHandler !== $handler) { + restore_error_handler(); + } + + restore_error_handler(); + } + + $output = $this->app->make(KernelContract::class)->output(); + + $this->assertSame(0, $exitCode); + $this->assertStringContainsString('ParseError', $output); + $this->assertSame('included', file_get_contents($result)); + $this->assertSame($handler, $observedHandler); + } + #[DataProvider('falseyExecuteCodeProvider')] public function testFalseyExecuteValuesUseDirectExecution(string $code): void { @@ -153,6 +238,14 @@ public function testFalseyExecuteValuesUseDirectExecution(string $code): void $this->assertSame(0, $process->getExitCode(), $process->getErrorOutput()); } + public static function falseyExecuteCodeProvider(): array + { + return [ + ['0'], + [''], + ]; + } + #[DataProvider('directExecutionOutcomeProvider')] #[RequiresPhpExtension('pcntl')] #[RequiresPhpExtension('posix')] @@ -174,6 +267,14 @@ public function testDirectExecutionPreservesTheSigintHandler(string $code, int $ } } + public static function directExecutionOutcomeProvider(): array + { + return [ + ['echo "hello";', 0], + ['throw new \Exception("fail");', 1], + ]; + } + public function testExecuteDoesNotChangeTheConsoleExceptionPolicy(): void { $application = $this->app->make(KernelContract::class)->getArtisan(); @@ -188,12 +289,27 @@ public function testExecuteDoesNotChangeTheConsoleExceptionPolicy(): void $this->assertTrue($application->areExceptionsCaught()); } - public function testDisabledConfiguredCommandsAreIgnored(): void + public function testConfiguredCommandsIncludeEnabledCommandsInOrderAndIgnoreDisabledCommands(): void { - config()->set('tinker.commands', [DisabledTinkerCommand::class]); + config()->set('tinker.commands', [ + EnabledTinkerCommand::class, + DisabledTinkerCommand::class, + ]); - $this->artisan('tinker', ['--execute' => 'echo "hello";']) - ->assertExitCode(0); + /** @var TinkerCommand $command */ + $command = $this->app->make(TinkerCommand::class); + $command->setHypervel($this->app); + $command->setApplication($this->app->make(KernelContract::class)->getArtisan()); + + $commands = (new ClassInvoker($command))->getCommands(); + $names = array_map( + static fn (SymfonyCommand $command): ?string => $command->getName(), + $commands, + ); + + $this->assertContains('env', $names); + $this->assertSame('tinker:enabled', $names[array_key_last($names)]); + $this->assertNotContains('tinker:disabled', $names); } public function testExecuteRunsInsideCoroutine(): void @@ -233,22 +349,6 @@ public function testConfiguredCasterIsAppliedByTheTinkerPresenter(): void $this->assertStringContainsString('configured caster', $output); } - - public static function falseyExecuteCodeProvider(): array - { - return [ - ['0'], - [''], - ]; - } - - public static function directExecutionOutcomeProvider(): array - { - return [ - ['echo "hello";', 0], - ['throw new \Exception("fail");', 1], - ]; - } } class DisabledTinkerCommand extends Command @@ -264,6 +364,11 @@ public function isEnabled(): bool } } +class EnabledTinkerCommand extends Command +{ + protected ?string $name = 'tinker:enabled'; +} + class TinkerCommandTestValue { } diff --git a/tests/Tinker/TinkerServiceProviderTest.php b/tests/Tinker/TinkerServiceProviderTest.php index 58610c2446..b65e087671 100644 --- a/tests/Tinker/TinkerServiceProviderTest.php +++ b/tests/Tinker/TinkerServiceProviderTest.php @@ -56,7 +56,7 @@ public function testTinkerConfigIsMerged(): void $this->assertIsArray($config->get('tinker.commands')); $this->assertIsArray($config->get('tinker.alias')); $this->assertIsArray($config->get('tinker.dont_alias')); - $this->assertNotNull($config->get('tinker.trust_project')); + $this->assertSame('always', $config->get('tinker.trust_project')); } public function testPublishedConfigDoesNotExcludeApplicationNamespacesByDefault(): void From 3adb0a7ef87c789e203a90f8c5329e63e95932b3 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:25:16 +0000 Subject: [PATCH 11/16] Keep Tinker alias tests order independent Exercise vendor exclusion through the loader without creating a permanent PHP class alias. Remove the unused classmap fixture so the test remains isolated in normal, reverse, and randomized execution order. --- tests/Tinker/ClassAliasAutoloaderTest.php | 6 ++++-- tests/Tinker/Fixtures/Vendor/composer/autoload_classmap.php | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Tinker/ClassAliasAutoloaderTest.php b/tests/Tinker/ClassAliasAutoloaderTest.php index 119615c66a..22120a001d 100644 --- a/tests/Tinker/ClassAliasAutoloaderTest.php +++ b/tests/Tinker/ClassAliasAutoloaderTest.php @@ -64,14 +64,16 @@ public function testCanExcludeNamespacesFromAliasing(): void public function testVendorClassesAreExcluded(): void { - $this->loader = ClassAliasAutoloader::register( + $loader = new ClassAliasAutoloader( $shell = m::mock(Shell::class), $this->classmapPath ); $shell->shouldNotReceive('writeStdout'); - $this->assertFalse(class_exists('TinkerThree')); + // PHP class aliases are permanent, so call the loader directly and let the + // Mockery expectation prove this vendor class was excluded. + $loader->aliasClass('TinkerThree'); } public function testVendorClassesCanBeWhitelisted(): void diff --git a/tests/Tinker/Fixtures/Vendor/composer/autoload_classmap.php b/tests/Tinker/Fixtures/Vendor/composer/autoload_classmap.php index 3803afe911..3393484009 100644 --- a/tests/Tinker/Fixtures/Vendor/composer/autoload_classmap.php +++ b/tests/Tinker/Fixtures/Vendor/composer/autoload_classmap.php @@ -8,5 +8,4 @@ 'Hypervel\Tests\Tinker\Fixtures\App\Foo\TinkerBar' => $baseDir . '/App/Foo/TinkerBar.php', 'Hypervel\Tests\Tinker\Fixtures\App\Baz\TinkerQux' => $baseDir . '/App/Baz/TinkerQux.php', 'Hypervel\Tests\Tinker\Fixtures\Vendor\One\Two\TinkerThree' => $vendorDir . '/One/Two/TinkerThree.php', - 'Four\Five\Six' => $vendorDir . '/Four/Five/Six.php', ]; From 458628a914152fa7bbd72cf5f07e238ba59ef94c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:25:23 +0000 Subject: [PATCH 12/16] Document Tinker execution and configuration Explain one-shot execution, exit statuses, positional includes, alias controls, custom casters, and project trust in the Artisan guide. Record the user-visible no-fork difference in the package README while keeping detailed usage in the main documentation. --- src/docs/artisan.md | 8 ++++++++ src/tinker/README.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/docs/artisan.md b/src/docs/artisan.md index a7930301a3..e849d1457f 100644 --- a/src/docs/artisan.md +++ b/src/docs/artisan.md @@ -73,6 +73,14 @@ php artisan tinker --execute='echo App\Models\User::count();' The command returns an exit status of zero when the code completes successfully. If the code calls `exit`, Artisan returns the requested exit status. Uncaught exceptions return an exit status of one. +You may pass one or more PHP files to load before Tinker executes your code: + +```shell +php artisan tinker bootstrap.php --execute='echo $message;' +``` + +If an included file cannot be loaded, Tinker reports the error and continues. The command's exit status still reflects the executed code. + You can publish Tinker's configuration file using the `vendor:publish` command and Tinker's publish tag: ```shell diff --git a/src/tinker/README.md b/src/tinker/README.md index 57073bc63b..0f16e0cbc0 100644 --- a/src/tinker/README.md +++ b/src/tinker/README.md @@ -3,4 +3,8 @@ Tinker for Hypervel [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/tinker) +## Differences From Laravel + +Hypervel disables PsySH process forking because it is incompatible with Swoole. A fatal error ends the Tinker session instead of only ending the current evaluation. + Ported from: https://github.com/laravel/tinker From 1674a40ad7673a3b2f6d124c88902ad7d139dc6e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:25:33 +0000 Subject: [PATCH 13/16] Complete the Tinker lifecycle audit records Record the final execution, include, alias, caster, metadata, and documentation design together with its focused regression coverage and performance limits. Mark the Tinker work complete after validation and review, while keeping the stable PsySH release requirement explicit as a Hypervel 0.4 release gate. --- ...amework-coroutine-state-lifecycle-audit.md | 6 +-- ...-coroutine-state-lifecycle-audit-ledger.md | 13 ++++--- ...ess-psysh-lifecycles-and-current-parity.md | 37 ++++++++++--------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md index e42029fb17..dd158ade37 100644 --- a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md +++ b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md @@ -990,9 +990,9 @@ An exceptionally large shared work unit may receive its own linked detail plan w This compact index routes the completed-work history that must be consulted with the full plan after compaction. Detailed history remains in the [companion ledger](2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md). -- **Active package or work unit:** `tinker`; the in-progress correctness and PsySH lifecycle audit is recorded under `Advance Tinker correctness and PsySH lifecycles`; detail plan `2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`. -- **Ledger entries required for the active work:** `Advance Tinker correctness and PsySH lifecycles` and `Complete Console command, scheduling, and generator lifecycles`. -- **Pending revalidation carried into the active work:** None. The remaining Tinker work is package-local: merge current `0.4`, consume PsySH `dev-main`, remove the obsolete shell subclass, and run the final gate. +- **Active package or work unit:** None; Tinker is complete under `Complete Tinker correctness and PsySH lifecycles`; detail plan `2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`. +- **Ledger entries required for the active work:** None. +- **Pending revalidation carried into the active work:** None. Update these three lines when a package starts, completes, or gains a cross-package dependency. Name exact work-unit headings or shared finding IDs from the companion ledger; never use “see recent entries” or require a full-ledger reread. diff --git a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md index e927999901..88d58d51d7 100644 --- a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md +++ b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md @@ -2305,9 +2305,9 @@ Append package entries in checklist order. Keep each entry compact but complete - **Validation and review:** Every changed file was exercised through its focused suite; affected Testing, Console, Foundation, Mail, Testbench, and metadata coverage is green. Canonical assertion, view, console-test, and Testing-package documentation was checked against the final public surfaces. Root/split Composer validation, formatting, both PHPStan configurations, the complete parallel components suite, Testbench package mode, dogfood, stale-symbol scans, and `git diff --check` passed. Fresh caller/callee, token/application ownership, cleanup, API, fixture, retained-state, hot-path, dead-code, and overengineering review is complete, and independent code review signed off with no remaining implementation finding. - **Assessment:** Testing now has exact parallel/resource ownership, current assertion and response parity, failure-truthful diagnostics, bounded metadata, and exception-safe fixture cleanup. Every accepted finding is fixed at its lowest owner without a workaround, speculative abstraction, stale path, production hot-path regression, unintended Laravel API break, or unresolved accepted defect. -### Advance Tinker correctness and PsySH lifecycles +### Complete Tinker correctness and PsySH lifecycles -- **Status and inspected surface:** The implementation and focused validation are complete against the branch's original base. The audit covered every reported Tinker finding, all Tinker source/tests/configuration/metadata/documentation, current Laravel Tinker, current PsySH execution/include lifecycles, and connected Console programmatic execution. Remaining work is to preserve current `0.4` behavior while merging, consume PsySH `dev-main`, remove the obsolete local shell subclass, and repeat the full validation and review workflow. The detailed design is recorded in [`2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`](2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md). +- **Status and inspected surface:** Complete; implementation, the current `0.4` merge, PsySH `dev-main` consumption, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Publishing Hypervel 0.4 is blocked until `dev-main` is replaced with the first compatible stable PsySH release containing the required behavior. The audit covered every reported Tinker finding, all Tinker source/tests/configuration/metadata/documentation, current Laravel Tinker, current PsySH execution/include lifecycles, and connected Console programmatic execution. The detailed design is recorded in [`2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`](2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md). | Findings | Final decision | |---|---| @@ -2317,14 +2317,15 @@ Append package entries in checklist order. Keep each entry compact but complete | `tinker-06` | Normalize configured aliases once and match exact classes, namespace descendants, and vendor directory children on semantic boundaries. | | `tinker-07` | Contain each Application presentation getter's `Throwable` independently so later virtual properties remain visible. | | `tinker-08` | Omit disabled configured commands when Symfony returns `null`. | -| `tinker-09`, `tinker-10` | Remove the unused Contracts split dependency, pin root/split metadata and provider discovery, add upstream provenance, and document direct execution, process-forking policy, aliases, casters, and project trust. | +| `tinker-09`, `tinker-10` | Remove the unused Contracts dependency and misleading Database suggestion, pin root/split metadata and provider discovery, add upstream provenance and the no-fork difference, and document direct execution, process-forking policy, aliases, casters, and project trust. | -- **Architecture and ownership:** Each command invocation owns one normal PsySH shell and alias loader. PsySH owns include loading and execution cleanup, interactive execution retains Ctrl-C handling, and Hypervel continues to disable process forking before shell construction. No static state, coroutine context, registry, cache, lock, retry, background task, or retained worker allocation was added. +- **Architecture and ownership:** Each command invocation owns one normal PsySH shell and alias loader. PsySH owns include loading and execution cleanup, interactive execution retains Ctrl-C handling, and Hypervel continues to disable process forking before shell construction. The shell command set is invocation-local while configured command registration on the Kernel-cached Console application follows upstream Tinker behavior. No static state, coroutine context, registry, cache, lock, retry, background task, or retained worker allocation was added. - **Console revalidation:** Programmatic Console execution already bypasses Symfony's process-global wrapper through `Application::runProgrammatically()`, while root CLI execution remains owned by Kernel/Symfony. Removing Tinker's redundant `setCatchExceptions(false)` preserves both paths and the caller's configured policy. - **Important rejected concerns:** No local shell subclass, reflected private method, copied include loop, generated `require_once` code, compatibility branch, shell factory, signal snapshot around yielding Hypervel code, PTY harness, alias index, path canonicalization, classmap cache, or process isolation is retained. `TESTBENCH_BASE_PATH` is intentionally absent from the falsey-execute child harness because the disposable clone's own `artisan` defines `BASE_PATH` directly. -- **Regression coverage:** Focused tests cover falsey direct values through the real disposable child application with open stdin, requested exit status, ordinary failures, SIGINT preservation, exception-policy preservation, disabled commands, coroutine execution, exact alias/vendor boundaries, per-property native errors, split metadata, provider discovery, existing registration/configuration behavior, include ordering and scope, failure continuation, error-handler restoration, and exit-status interaction. +- **Regression coverage:** Focused tests cover falsey direct values through the real disposable child application with open stdin, requested exit status, ordinary failures, SIGINT preservation, exception-policy preservation, enabled-command placement and disabled-command omission, coroutine execution, exact alias/vendor boundaries, per-property native errors, split metadata, provider discovery, nullable project trust, existing registration/configuration behavior, positional and trusted local include scope, malformed-include reporting, later-include continuation, error-handler restoration, and success status after a reported include failure. - **Performance and compatibility:** Changes run only while starting or using the developer command. Matching and filtering are bounded in-memory work over configured aliases/listeners/commands; no application request, queue, database, network, or worker hot path changes. Laravel-facing Tinker options and configuration remain compatible, and Hypervel's no-fork Swoole adaptation remains intact. -- **Assessment:** Every accepted finding is corrected directly without a workaround or speculative mechanism. Tinker remains open only for the current `0.4` merge, PsySH `dev-main` consumption, local subclass removal, and the final validation/review workflow. +- **Validation:** Both Composer manifests are valid and aligned, the installed PsySH source is the required `dev-main`, formatting, both PHPStan configurations, and the complete Tinker suite in normal and random order pass, and both include tests fail under their targeted source counterfactuals. Fresh caller/callee, process-global state, public API, cold-path performance, retained-memory, stale-code, and overengineering review is complete. +- **Assessment:** Every accepted finding is corrected directly without a workaround or speculative mechanism, and independent code review signed off with no remaining implementation finding. ### Complete Testbench correctness, parallel ownership, and current parity diff --git a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md index 3f720dc411..5b76eb71c9 100644 --- a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md +++ b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md @@ -2,11 +2,11 @@ ## Status -The implementation and focused validation are complete against the branch's original base. Merge current `0.4`, preserve its newer Tinker behavior, consume PsySH `dev-main`, remove the obsolete local shell subclass, and repeat the full validation and review workflow. Replace `dev-main` with the first compatible stable PsySH release containing the required behavior before Hypervel 0.4 is released. +Complete. The implementation, current `0.4` merge, PsySH `dev-main` update, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Replace `dev-main` with the first compatible stable PsySH release containing the required behavior before Hypervel 0.4 is released. ## Scope -Correct the verified Tinker findings without turning this targeted maintenance unit into a second package-wide audit. Preserve Hypervel's coroutine-aware Console execution, prohibition on PsySH process forking, upstream Tinker APIs and configuration, optional Database/Process presentation, and operation-local shell/alias-loader ownership. +Correct the verified Tinker findings without turning this targeted maintenance unit into a second package-wide audit. Preserve Hypervel's coroutine-aware Console execution, prohibition on PsySH process forking, upstream Tinker APIs and configuration, Database/Process presentation, and operation-local shell/alias-loader ownership. References checked: @@ -73,7 +73,7 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp - Keep PsySH process forking disabled before shell construction. Local `.psysh.php` configuration cannot re-enable `ProcessForker`: listeners are constructed before local config is loaded and are never rebuilt. - Use PsySH's normal `Shell`; current `dev-main` owns include loading and direct-execution signal cleanup without a Hypervel subclass. - Interactive Tinker retains Ctrl-C handling. One-shot execution must not leave process-global signal or error-handler state behind. -- User casters keep overriding defaults. Database model and Process result casters remain optional; Foundation Application is a hard package dependency. +- User casters keep overriding defaults. Database model, Process result, and Foundation Application casters remain built in; Foundation brings Database and Process as hard transitive dependencies. - No HTTP/request path changes. All added comparisons, filtering, and loading occur only while starting or running the developer command. There is no lock, yield, retry, cache, static registry, coroutine context, retained worker allocation, or repeated filesystem I/O beyond includes explicitly requested by the caller. ## Final findings @@ -88,7 +88,7 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp | `tinker-06` | Raw prefixes make `App\Nova` also match `App\NovaThing` and make `/app/vendor-local/...` look like `/app/vendor/...`. | Match normalized aliases and vendor directories on semantic boundaries. | | `tinker-07` | One Application presentation getter throwing `Error` or `TypeError` escapes the per-property `Exception` boundary and aborts the dump. | Contain `Throwable` from each getter. | | `tinker-08` | Symfony returns `null` for a disabled configured command, which PsySH forwards to its `callable|Command` parameter and rejects with `TypeError`. | Omit disabled command results. | -| `tinker-09` | Split metadata declares unused Contracts, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | +| `tinker-09` | Split metadata declares unused Contracts and a misleading Database suggestion, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | | `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Tinker guide in Laravel-docs prose. | | `tinker-11` | Tinker redundantly writes the Kernel-cached Console application's exception policy and can leave a caller's explicit setting changed. | Remove the mutation. | @@ -121,7 +121,7 @@ if ($code !== null) { $shell = new Shell($config); ``` -Delete `ExecuteShell` and use the same PsySH `Shell` for direct and interactive execution. Current PsySH `main` owns direct-execution signal cleanup. Keep `setUsePcntl(false)` before shell construction because `ProcessForker` remains incompatible with Swoole; do not add another listener filter. +Use the same PsySH `Shell` for direct and interactive execution. Current PsySH `main` owns direct-execution signal cleanup. Keep `setUsePcntl(false)` before shell construction because `ProcessForker` remains incompatible with Swoole; do not add a local shell subclass or listener filter. The direct branch becomes: @@ -226,7 +226,7 @@ foreach (self::$appProperties as $property) { } ``` -Register the Foundation Application caster unconditionally because `hypervel/foundation` is a direct hard dependency. Keep Database and Process class guards. +Register the Foundation Application, Database model, and Process result casters unconditionally. `hypervel/foundation` is a direct hard dependency, Foundation directly requires Database, and Foundation's Concurrency dependency requires Process. Symfony stores caster class-string keys without resolving them, so conditional registration would not protect a runtime boundary even if a class were absent. ### 5. Correct metadata, provenance, and documentation @@ -235,9 +235,9 @@ In `src/tinker/composer.json`: - remove unused `hypervel/contracts`; - keep root-consistent `symfony/console:^8.1` and `symfony/var-dumper:^8.1`; - require PsySH `dev-main` as described in section 1; -- retain only the Database suggestion. Do not add a Process suggestion solely for symmetry. +- omit `suggest`: Database and Process are already hard transitive dependencies. -Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency, the Database suggestion, and provider discovery. Add `Ported from: https://github.com/laravel/tinker` to the README. +Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency and `suggest` section, and provider discovery. Add upstream provenance and a one-line `Differences From Laravel` note about the user-visible no-fork behavior to the README. Update only the Tinker section of `src/docs/artisan.md`, following the surrounding Laravel-docs prose. Document: @@ -262,20 +262,21 @@ Required Hypervel regressions: 1. Successful and failing direct execution preserve a sentinel SIGINT handler; test cleanup restores the sentinel even after assertion failure. Do not assert async-signal mode at the Hypervel boundary because Symfony Console owns additional signal state. 2. A bounded subprocess runs the disposable runtime clone's own `artisan` at `BASE_PATH` to prove `--execute=0` and `--execute=''` select direct execution. The clone does not discover the root package, so temporarily add `TinkerServiceProvider` to its `bootstrap/providers.php` through the existing provider-file API and restore the original file in `finally`. Pass `COMPOSER_VENDOR_DIR` and `HYPERVEL_AUTOLOAD_PATH` to the child; `TESTBENCH_BASE_PATH` is not involved because the clone's entry point already owns `BASE_PATH`. Give the child an open stdin pipe that is deliberately not closed while awaiting it: the wrong REPL branch sees piped input and blocks in `getInput(false)`, while the direct branch returns immediately. Use a ten-second failure budget, treat timeout as test failure, and close every pipe in `finally`; do not require a PTY, invent another bootstrap, or add a production shell factory. -3. Positional and project-configured default includes share variables with evaluated code; malformed includes are reported, later includes still load, the prior error handler remains installed, and a successful executed expression still returns 0 after the reported include failure. +3. One integration test changes into an isolated temporary project and proves that a positional include and the shipped trusted-by-default local `.psysh.php` include both share variables with directly executed code. A second uses unique include paths, disables mocked console output, and proves that a malformed positional include reports `ParseError`, a later include still loads, the prior error handler remains installed, and successful executed code still returns 0. Inspect and rebalance the handler stack before any assertion so a regression cannot contaminate later tests. 4. `exit(3)` returns 3 without evaluation-error output; ordinary throwables still return 1. -5. A disabled configured command is omitted while enabled commands retain order. -6. The public `isAliasable()` matrix covers exact class, namespace child, common-prefix sibling, trailing separator, exclusion, real vendor child, and vendor-prefix sibling without creating irreversible class aliases. +5. Direct `getCommands()` coverage proves that an enabled configured command is retained after the whitelist while a disabled configured command is omitted. +6. The public `isAliasable()` matrix covers exact class, namespace child, common-prefix sibling, trailing separator, exclusion, real vendor child, and vendor-prefix sibling. The loader exclusion test invokes `aliasClass()` directly and relies on its shell mock because PHP class aliases are permanent and make `class_exists()` order-dependent. 7. An Application getter throwing `Error` is omitted while later virtual properties remain. -8. Metadata/provenance and existing coroutine execution remain correct. +8. Metadata/provenance, nullable project-trust configuration, and existing coroutine execution remain correct. Validation order: -1. Run each changed Tinker test file, then the complete `tests/Tinker` group. +1. Run each changed Tinker test file, the alias-loader file in reverse order, then the complete `tests/Tinker` group. 2. Validate both Composer manifests and confirm the installed PsySH source is current `dev-main`. -3. Run `composer fix` once after implementation. -4. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. -5. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. +3. Confirm the include tests are load-bearing by temporarily removing the positional `setIncludes()` call and moving direct-execution `setOutput()` after `execute()`, running the matching test after each change, and reverting immediately. +4. Run `composer lint:fix`, `composer analyse`, and the complete `tests/Tinker` group in that order. +5. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. +6. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. ## Rejected designs and non-findings @@ -284,9 +285,9 @@ Validation order: - No removal of interactive signal handling. Keep the existing `setUsePcntl(false)` invariant; do not add a `ProcessForker` listener filter. - No class-alias registry, unalias attempt, path canonicalization, classmap cache, or concurrency machinery. PHP has no coroutine-local class table, and concurrent REPLs in one worker are unsupported. - Keep `ClassAliasAutoloader::__destruct()`: while registered, the autoload callback retains the object; normal `finally` cleanup unregisters it first, and destruction remains an idempotent fallback. -- Keep configured commands on the invocation-local shell and existing caster precedence. No mutable worker state is introduced. +- Keep the shell's command set invocation-local and preserve existing caster precedence. Configured command registration on the Kernel-cached Console application follows upstream Tinker behavior. - Keep the null guard around dynamic Application getter results; only its failure boundary widens. -- Do not add default caster config, exhaustive docs, Process metadata for symmetry, or tests that merely mirror trivial mappings. +- Do not add default caster config, exhaustive docs, suggestions for packages already required transitively, or tests that merely mirror trivial mappings. ## Expected result From 140a3788e53c525243cfa800f1a83177f57d44e0 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:32:45 +0000 Subject: [PATCH 14/16] Default Tinker to safe project trust Use PsySH's prompt mode by default so Tinker does not silently execute .psysh.php from an unfamiliar working directory. The existing trust_project setting and all supported values remain available for applications that want a different policy. Keep the trusted include test explicit about its policy, add a regression proving non-interactive execution skips untrusted project configuration, and pin the shipped default in the provider test. --- src/tinker/config/tinker.php | 7 +++--- tests/Tinker/TinkerCommandTest.php | 25 ++++++++++++++++++++++ tests/Tinker/TinkerServiceProviderTest.php | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/tinker/config/tinker.php b/src/tinker/config/tinker.php index 36116ed871..8067974b54 100644 --- a/src/tinker/config/tinker.php +++ b/src/tinker/config/tinker.php @@ -63,11 +63,12 @@ | Project Trust Mode |-------------------------------------------------------------------------- | - | PsySH restricts local project features unless your project is trusted. - | Set this to "always" to avoid untrusted project warnings in Tinker. + | PsySH asks before loading local features from an unfamiliar project. + | Non-interactive runs skip untrusted project configuration instead. + | You may use "always" when Tinker only runs from trusted directories. | Accepted values: "prompt", "always", "never", true, false, null. | */ - 'trust_project' => env('TINKER_TRUST_PROJECT', 'always'), + 'trust_project' => env('TINKER_TRUST_PROJECT', 'prompt'), ]; diff --git a/tests/Tinker/TinkerCommandTest.php b/tests/Tinker/TinkerCommandTest.php index a1ad75396e..9775333d9e 100644 --- a/tests/Tinker/TinkerCommandTest.php +++ b/tests/Tinker/TinkerCommandTest.php @@ -124,6 +124,9 @@ public function testExecuteReturnsRequestedExitCodeWithoutRenderingAnError(): vo public function testExecuteLoadsPositionalAndProjectIncludes(): void { + // This test covers include scope, so trust its isolated project explicitly. + config()->set('tinker.trust_project', 'always'); + $workingDirectory = getcwd(); $positionalInclude = $this->temporaryDirectory . '/scope-positional.php'; $projectInclude = $this->temporaryDirectory . '/scope-project.php'; @@ -153,6 +156,28 @@ public function testExecuteLoadsPositionalAndProjectIncludes(): void $this->assertSame('positional:project', file_get_contents($result)); } + public function testExecuteDoesNotLoadUntrustedProjectConfigurationByDefault(): void + { + $workingDirectory = getcwd(); + $sentinel = $this->temporaryDirectory . '/untrusted-project.txt'; + + file_put_contents( + $this->temporaryDirectory . '/.psysh.php', + 'assertTrue(chdir($this->temporaryDirectory)); + + try { + $this->artisan('tinker', ['--execute' => 'echo "hello";']) + ->assertExitCode(0); + } finally { + chdir($workingDirectory); + } + + $this->assertFileDoesNotExist($sentinel); + } + public function testExecuteContinuesAfterMalformedIncludeAndRestoresErrorHandler(): void { $invalidInclude = $this->temporaryDirectory . '/failure-invalid.php'; diff --git a/tests/Tinker/TinkerServiceProviderTest.php b/tests/Tinker/TinkerServiceProviderTest.php index b65e087671..fabb0892ea 100644 --- a/tests/Tinker/TinkerServiceProviderTest.php +++ b/tests/Tinker/TinkerServiceProviderTest.php @@ -56,7 +56,7 @@ public function testTinkerConfigIsMerged(): void $this->assertIsArray($config->get('tinker.commands')); $this->assertIsArray($config->get('tinker.alias')); $this->assertIsArray($config->get('tinker.dont_alias')); - $this->assertSame('always', $config->get('tinker.trust_project')); + $this->assertSame('prompt', $config->get('tinker.trust_project')); } public function testPublishedConfigDoesNotExcludeApplicationNamespacesByDefault(): void From 997d6330704c2c86f84890dda59b7d78f2d30ec8 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:33:05 +0000 Subject: [PATCH 15/16] Document Tinker project trust Explain how Tinker handles unfamiliar project configuration in interactive and non-interactive sessions. Show the supported environment override for trusted automation and the never mode for disabling local configuration. Record the intentional Laravel default difference in the package README and porting guide so applications that rely on .psysh.php know when to opt into always. --- src/docs/artisan.md | 10 +++++++++- src/docs/porting-from-laravel.md | 6 ++++++ src/tinker/README.md | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/docs/artisan.md b/src/docs/artisan.md index e849d1457f..987fb86aa3 100644 --- a/src/docs/artisan.md +++ b/src/docs/artisan.md @@ -145,7 +145,15 @@ Application casters take precedence over Tinker's default casters. #### Trusting Project Configuration -PsySH may load project-specific configuration from a local `.psysh.php` file. Hypervel trusts this configuration by default. To ask before loading it or to reject it, change the `trust_project` option in your `tinker.php` configuration file or set the `TINKER_TRUST_PROJECT` environment variable to `prompt` or `never`. +PsySH may load project-specific configuration from a local `.psysh.php` file. By default, Tinker asks you to trust an unfamiliar project before loading this file. During non-interactive execution, untrusted project configuration is skipped. If PsySH suggests the `--trust-project` option, use the environment variable below instead; Artisan does not expose this option. + +If Tinker only runs from a trusted working directory, you may set the `trust_project` option in your `tinker.php` configuration file to `always`. You may also trust the project for a single command using the `TINKER_TRUST_PROJECT` environment variable: + +```shell +TINKER_TRUST_PROJECT=always php artisan tinker --execute='echo App\Models\User::count();' +``` + +To prevent Tinker from loading local project configuration, set `trust_project` to `never`. ## Writing Commands diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index a86cbf65d6..2dc4397875 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -32,6 +32,7 @@ - [Dates](#dates) - [UUIDs](#uuids) - [Filesystem](#filesystem) + - [Tinker](#tinker) - [Database, Cache, Sessions, and Queues](#database-cache-sessions-and-queues) - [Database](#database) - [Redis](#redis) @@ -554,6 +555,11 @@ Hypervel's `Filesystem::hash()` method uses `xxh128` by default. Pass `md5` expl Unlike Laravel, Hypervel honors `read-only` on scoped disk records. Remove that option from any scoped disk that must accept writes. + +### Tinker + +Hypervel uses PsySH's prompt project-trust mode by default, while Laravel Tinker trusts `.psysh.php` configuration automatically. Interactive sessions ask before loading an unfamiliar project, and non-interactive sessions skip its configuration. Applications that rely on loading this file without confirmation should set `trust_project` or `TINKER_TRUST_PROJECT` to `always` when Tinker runs from a trusted working directory. See the [Tinker documentation](/docs/{{version}}/artisan#trusting-project-configuration) for more information. + ## Database, Cache, Sessions, and Queues diff --git a/src/tinker/README.md b/src/tinker/README.md index 0f16e0cbc0..dd3ddd2bdb 100644 --- a/src/tinker/README.md +++ b/src/tinker/README.md @@ -7,4 +7,6 @@ Tinker for Hypervel Hypervel disables PsySH process forking because it is incompatible with Swoole. A fatal error ends the Tinker session instead of only ending the current evaluation. +Hypervel uses PsySH's prompt project-trust mode by default, while Laravel Tinker trusts `.psysh.php` configuration automatically. Interactive sessions ask before loading an unfamiliar project, and non-interactive sessions skip its configuration. + Ported from: https://github.com/laravel/tinker From 28b9e0bccfd8d695520163ef397fdb04612f1e71 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:33:22 +0000 Subject: [PATCH 16/16] Record the Tinker trust correction Add the approved project-trust finding and final treatment to the Tinker plan and audit ledger. Record the compatibility boundary, rejected alternatives, focused regression, and load-bearing old-default counterfactual. Also repair the malformed findings table cell and close the work unit after validation, self-review, and final review. --- ...-coroutine-state-lifecycle-audit-ledger.md | 11 ++--- ...ess-psysh-lifecycles-and-current-parity.md | 41 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md index 88d58d51d7..e7e55c8c61 100644 --- a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md +++ b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md @@ -2307,7 +2307,7 @@ Append package entries in checklist order. Keep each entry compact but complete ### Complete Tinker correctness and PsySH lifecycles -- **Status and inspected surface:** Complete; implementation, the current `0.4` merge, PsySH `dev-main` consumption, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Publishing Hypervel 0.4 is blocked until `dev-main` is replaced with the first compatible stable PsySH release containing the required behavior. The audit covered every reported Tinker finding, all Tinker source/tests/configuration/metadata/documentation, current Laravel Tinker, current PsySH execution/include lifecycles, and connected Console programmatic execution. The detailed design is recorded in [`2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`](2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md). +- **Status and inspected surface:** Complete; implementation, the current `0.4` merge, PsySH `dev-main` consumption, the approved project-trust correction, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Publishing Hypervel 0.4 is blocked until `dev-main` is replaced with the first compatible stable PsySH release containing the required behavior. The audit covered every reported Tinker finding, all Tinker source/tests/configuration/metadata/documentation, current Laravel Tinker, current PsySH execution/include/trust lifecycles, and connected Console programmatic execution. The detailed design is recorded in [`2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md`](2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md). | Findings | Final decision | |---|---| @@ -2318,13 +2318,14 @@ Append package entries in checklist order. Keep each entry compact but complete | `tinker-07` | Contain each Application presentation getter's `Throwable` independently so later virtual properties remain visible. | | `tinker-08` | Omit disabled configured commands when Symfony returns `null`. | | `tinker-09`, `tinker-10` | Remove the unused Contracts dependency and misleading Database suggestion, pin root/split metadata and provider discovery, add upstream provenance and the no-fork difference, and document direct execution, process-forking policy, aliases, casters, and project trust. | +| `tinker-12` | Default project trust to PsySH's native `prompt` mode so an unfamiliar working directory cannot silently execute `.psysh.php`; preserve explicit `always`, `never`, boolean, and null configuration. | - **Architecture and ownership:** Each command invocation owns one normal PsySH shell and alias loader. PsySH owns include loading and execution cleanup, interactive execution retains Ctrl-C handling, and Hypervel continues to disable process forking before shell construction. The shell command set is invocation-local while configured command registration on the Kernel-cached Console application follows upstream Tinker behavior. No static state, coroutine context, registry, cache, lock, retry, background task, or retained worker allocation was added. - **Console revalidation:** Programmatic Console execution already bypasses Symfony's process-global wrapper through `Application::runProgrammatically()`, while root CLI execution remains owned by Kernel/Symfony. Removing Tinker's redundant `setCatchExceptions(false)` preserves both paths and the caller's configured policy. -- **Important rejected concerns:** No local shell subclass, reflected private method, copied include loop, generated `require_once` code, compatibility branch, shell factory, signal snapshot around yielding Hypervel code, PTY harness, alias index, path canonicalization, classmap cache, or process isolation is retained. `TESTBENCH_BASE_PATH` is intentionally absent from the falsey-execute child harness because the disposable clone's own `artisan` defines `BASE_PATH` directly. -- **Regression coverage:** Focused tests cover falsey direct values through the real disposable child application with open stdin, requested exit status, ordinary failures, SIGINT preservation, exception-policy preservation, enabled-command placement and disabled-command omission, coroutine execution, exact alias/vendor boundaries, per-property native errors, split metadata, provider discovery, nullable project trust, existing registration/configuration behavior, positional and trusted local include scope, malformed-include reporting, later-include continuation, error-handler restoration, and success status after a reported include failure. -- **Performance and compatibility:** Changes run only while starting or using the developer command. Matching and filtering are bounded in-memory work over configured aliases/listeners/commands; no application request, queue, database, network, or worker hot path changes. Laravel-facing Tinker options and configuration remain compatible, and Hypervel's no-fork Swoole adaptation remains intact. -- **Validation:** Both Composer manifests are valid and aligned, the installed PsySH source is the required `dev-main`, formatting, both PHPStan configurations, and the complete Tinker suite in normal and random order pass, and both include tests fail under their targeted source counterfactuals. Fresh caller/callee, process-global state, public API, cold-path performance, retained-memory, stale-code, and overengineering review is complete. +- **Important rejected concerns:** No local shell subclass, reflected private method, copied include loop, generated `require_once` code, compatibility branch, shell factory, signal snapshot around yielding Hypervel code, PTY harness, alias index, path canonicalization, classmap cache, process isolation, project-trust layer, path allowlist, or new trust command option is retained. `TESTBENCH_BASE_PATH` is intentionally absent from the falsey-execute child harness because the disposable clone's own `artisan` defines `BASE_PATH` directly. PsySH-derived trust flags are overridden by application config but unreachable because neither Laravel nor Hypervel defines them on the Tinker command. +- **Regression coverage:** Focused tests cover falsey direct values through the real disposable child application with open stdin, requested exit status, ordinary failures, SIGINT preservation, exception-policy preservation, enabled-command placement and disabled-command omission, coroutine execution, exact alias/vendor boundaries, per-property native errors, split metadata, provider discovery, nullable and default project trust, rejection of untrusted local project configuration, existing registration/configuration behavior, positional and explicitly trusted local include scope, malformed-include reporting, later-include continuation, error-handler restoration, and success status after a reported include failure. +- **Performance and compatibility:** Changes run only while starting or using the developer command. Matching and filtering are bounded in-memory work over configured aliases/listeners/commands; no application request, queue, database, network, or worker hot path changes. Laravel-facing Tinker options, project-trust values, and configuration keys remain compatible; Hypervel deliberately uses PsySH's safer `prompt` default rather than Laravel Tinker's `always`, and the no-fork Swoole adaptation remains intact. +- **Validation:** Both Composer manifests are valid and aligned, the installed PsySH source is the required `dev-main`, formatting, both PHPStan configurations, and the complete Tinker suite in normal and random order pass. Both include tests fail under their targeted source counterfactuals; selecting the old `always` trust default makes the untrusted-project and default-pinning regressions fail while the explicitly trusted include test passes. Fresh caller/callee, process-global state, public API, cold-path performance, retained-memory, stale-code, and overengineering review is complete. - **Assessment:** Every accepted finding is corrected directly without a workaround or speculative mechanism, and independent code review signed off with no remaining implementation finding. ### Complete Testbench correctness, parallel ownership, and current parity diff --git a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md index 5b76eb71c9..d44665b04f 100644 --- a/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md +++ b/docs/plans/2026-08-09-0219-tinker-correctness-psysh-lifecycles-and-current-parity.md @@ -2,7 +2,7 @@ ## Status -Complete. The implementation, current `0.4` merge, PsySH `dev-main` update, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Replace `dev-main` with the first compatible stable PsySH release containing the required behavior before Hypervel 0.4 is released. +Complete. The implementation, current `0.4` merge, PsySH `dev-main` update, project-trust correction, focused validation, load-bearing counterfactuals, final self-review, and independent code review are complete. Replace `dev-main` with the first compatible stable PsySH release containing the required behavior before Hypervel 0.4 is released. ## Scope @@ -69,7 +69,7 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp ## Contracts and performance budget -- Keep `--execute`, positional `include`, `commands`, `alias`, `dont_alias`, `casters`, and `trust_project` Laravel-shaped. +- Keep `--execute`, positional `include`, `commands`, `alias`, `dont_alias`, and `casters` Laravel-shaped. Keep the `trust_project` name, accepted values, and semantics while defaulting to PsySH's safer `prompt` mode instead of Laravel Tinker's `always`. - Keep PsySH process forking disabled before shell construction. Local `.psysh.php` configuration cannot re-enable `ProcessForker`: listeners are constructed before local config is loaded and are never rebuilt. - Use PsySH's normal `Shell`; current `dev-main` owns include loading and direct-execution signal cleanup without a Hypervel subclass. - Interactive Tinker retains Ctrl-C handling. One-shot execution must not leave process-global signal or error-handler state behind. @@ -87,10 +87,11 @@ Record low-confidence concerns under rejected or unresolved analysis. Do not imp | `tinker-05` | `execute($code, true)` rethrows `BreakException`; Tinker's broad catch renders `exit(3)` as an error and returns 1. | Return the embedded exit code without error rendering. | | `tinker-06` | Raw prefixes make `App\Nova` also match `App\NovaThing` and make `/app/vendor-local/...` look like `/app/vendor/...`. | Match normalized aliases and vendor directories on semantic boundaries. | | `tinker-07` | One Application presentation getter throwing `Error` or `TypeError` escapes the per-property `Exception` boundary and aborts the dump. | Contain `Throwable` from each getter. | -| `tinker-08` | Symfony returns `null` for a disabled configured command, which PsySH forwards to its `callable|Command` parameter and rejects with `TypeError`. | Omit disabled command results. | +| `tinker-08` | Symfony returns `null` for a disabled configured command, which PsySH forwards to its `callable\|Command` parameter and rejects with `TypeError`. | Omit disabled command results. | | `tinker-09` | Split metadata declares unused Contracts and a misleading Database suggestion, lacks durable dependency coverage, and omits upstream provenance. | Correct dependencies/provenance and add focused metadata coverage. | | `tinker-10` | Public guidance omits execute/alias/caster/trust behavior and incorrectly says all PCNTL support is disabled. | Complete the concise Tinker guide in Laravel-docs prose. | | `tinker-11` | Tinker redundantly writes the Kernel-cached Console application's exception policy and can leave a caller's explicit setting changed. | Remove the mutation. | +| `tinker-12` | Laravel Tinker's `always` project-trust default silently executes `.psysh.php` from the current working directory, opting out of PsySH's protection against untrusted project configuration. | Use PsySH's native `prompt` mode by default; retain explicit `always` and `never` configuration. | ## Implementation @@ -237,7 +238,9 @@ In `src/tinker/composer.json`: - require PsySH `dev-main` as described in section 1; - omit `suggest`: Database and Process are already hard transitive dependencies. -Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency and `suggest` section, and provider discovery. Add upstream provenance and a one-line `Differences From Laravel` note about the user-visible no-fork behavior to the README. +Add `tests/Tinker/PackageMetadataTest.php` to pin direct dependency/root-constraint agreement, the absent Contracts dependency and `suggest` section, and provider discovery. Add upstream provenance and concise `Differences From Laravel` notes about the user-visible no-fork behavior and safer project-trust default to the README. + +Default `trust_project` to `prompt`, using PsySH's existing trust implementation. Interactive Tinker asks before loading an unfamiliar local `.psysh.php`; noninteractive execution skips untrusted project configuration without blocking. Keep `always`, `never`, boolean, and null values available. Do not add path allowlists, change working directories, or expose PsySH's `--trust-project` options: Hypervel's command does not define those options, and the existing environment variable is sufficient for one-run automation. Update only the Tinker section of `src/docs/artisan.md`, following the surrounding Laravel-docs prose. Document: @@ -246,13 +249,15 @@ Update only the Tinker section of `src/docs/artisan.md`, following the surroundi - that Hypervel disables process forking, not all PCNTL support; - `tinker.alias` vendor opt-in and `dont_alias` exclusions; - custom `tinker.casters`; -- `trust_project`. +- the `prompt` project-trust default, interactive confirmation, noninteractive skip, and working remedies: answer the prompt, configure `trust_project`, or set `TINKER_TRUST_PROJECT=always` for one trusted run. Keep the guide concise: no exhaustive config reference, internal listener discussion, or default-caster listing. +Add one concise porting-guide entry explaining that Laravel applications which rely on implicit `.psysh.php` loading must explicitly select `always` in a trusted environment. + ### 6. Update durable records -Add one compact Tinker ledger section covering `tinker-01` through `tinker-11`, the temporary PsySH `dev-main` constraint, Console revalidation, final API/performance result, and rejected designs. Route the core Tinker line to this work unit. Preserve every newer `0.4` record while resolving the audit-plan and ledger conflicts. Check the core package checklist only after current `0.4` is merged, `dev-main` is installed, and implementation, validation, self-review, and code review are complete. +Add one compact Tinker ledger section covering `tinker-01` through `tinker-12`, the temporary PsySH `dev-main` constraint, Console revalidation, final API/performance result, and rejected designs. Route the core Tinker line to this work unit. Preserve every newer `0.4` record while resolving the audit-plan and ledger conflicts. Check the core package checklist only after current `0.4` is merged, `dev-main` is installed, and implementation, validation, self-review, and code review are complete. ## Tests and validation @@ -262,21 +267,23 @@ Required Hypervel regressions: 1. Successful and failing direct execution preserve a sentinel SIGINT handler; test cleanup restores the sentinel even after assertion failure. Do not assert async-signal mode at the Hypervel boundary because Symfony Console owns additional signal state. 2. A bounded subprocess runs the disposable runtime clone's own `artisan` at `BASE_PATH` to prove `--execute=0` and `--execute=''` select direct execution. The clone does not discover the root package, so temporarily add `TinkerServiceProvider` to its `bootstrap/providers.php` through the existing provider-file API and restore the original file in `finally`. Pass `COMPOSER_VENDOR_DIR` and `HYPERVEL_AUTOLOAD_PATH` to the child; `TESTBENCH_BASE_PATH` is not involved because the clone's entry point already owns `BASE_PATH`. Give the child an open stdin pipe that is deliberately not closed while awaiting it: the wrong REPL branch sees piped input and blocks in `getInput(false)`, while the direct branch returns immediately. Use a ten-second failure budget, treat timeout as test failure, and close every pipe in `finally`; do not require a PTY, invent another bootstrap, or add a production shell factory. -3. One integration test changes into an isolated temporary project and proves that a positional include and the shipped trusted-by-default local `.psysh.php` include both share variables with directly executed code. A second uses unique include paths, disables mocked console output, and proves that a malformed positional include reports `ParseError`, a later include still loads, the prior error handler remains installed, and successful executed code still returns 0. Inspect and rebalance the handler stack before any assertion so a regression cannot contaminate later tests. -4. `exit(3)` returns 3 without evaluation-error output; ordinary throwables still return 1. -5. Direct `getCommands()` coverage proves that an enabled configured command is retained after the whitelist while a disabled configured command is omitted. -6. The public `isAliasable()` matrix covers exact class, namespace child, common-prefix sibling, trailing separator, exclusion, real vendor child, and vendor-prefix sibling. The loader exclusion test invokes `aliasClass()` directly and relies on its shell mock because PHP class aliases are permanent and make `class_exists()` order-dependent. -7. An Application getter throwing `Error` is omitted while later virtual properties remain. -8. Metadata/provenance, nullable project-trust configuration, and existing coroutine execution remain correct. +3. One integration test changes into an isolated temporary project, explicitly selects `always`, and proves that a positional include and trusted local `.psysh.php` include both share variables with directly executed code. A second uses unique include paths, disables mocked console output, and proves that a malformed positional include reports `ParseError`, a later include still loads, the prior error handler remains installed, and successful executed code still returns 0. Inspect and rebalance the handler stack before any assertion so a regression cannot contaminate later tests. +4. A direct-execution test changes into an isolated temporary project under the shipped `prompt` default and proves that an untrusted `.psysh.php` cannot create its sentinel file. Do not assert PsySH's warning text. +5. `exit(3)` returns 3 without evaluation-error output; ordinary throwables still return 1. +6. Direct `getCommands()` coverage proves that an enabled configured command is retained after the whitelist while a disabled configured command is omitted. +7. The public `isAliasable()` matrix covers exact class, namespace child, common-prefix sibling, trailing separator, exclusion, real vendor child, and vendor-prefix sibling. The loader exclusion test invokes `aliasClass()` directly and relies on its shell mock because PHP class aliases are permanent and make `class_exists()` order-dependent. +8. An Application getter throwing `Error` is omitted while later virtual properties remain. +9. Metadata/provenance, nullable project-trust configuration, and existing coroutine execution remain correct. Validation order: 1. Run each changed Tinker test file, the alias-loader file in reverse order, then the complete `tests/Tinker` group. 2. Validate both Composer manifests and confirm the installed PsySH source is current `dev-main`. 3. Confirm the include tests are load-bearing by temporarily removing the positional `setIncludes()` call and moving direct-execution `setOutput()` after `execute()`, running the matching test after each change, and reverting immediately. -4. Run `composer lint:fix`, `composer analyse`, and the complete `tests/Tinker` group in that order. -5. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. -6. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. +4. Confirm the project-trust regression is load-bearing by running the negative trust regression, positive include test, and default-pinning config test with `TINKER_TRUST_PROJECT=always`: the negative and default-pinning tests must fail while the positive include test still passes through its explicit `always` setting. +5. Run `composer lint:fix`, `composer analyse`, and the complete `tests/Tinker` group in that order. +6. Perform a fresh caller/callee, process-global state, terminal/signal, public API, cold-path performance, retained-memory, stale-code, and overengineering review. +7. Apply review corrections, rerun affected focused tests, and repeat the complete gate when changes warrant it. ## Rejected designs and non-findings @@ -288,7 +295,9 @@ Validation order: - Keep the shell's command set invocation-local and preserve existing caster precedence. Configured command registration on the Kernel-cached Console application follows upstream Tinker behavior. - Keep the null guard around dynamic Application getter results; only its failure boundary widens. - Do not add default caster config, exhaustive docs, suggestions for packages already required transitively, or tests that merely mirror trivial mappings. +- Do not add `--trust-project` or `--no-trust-project` to Tinker. Supporting them would add two options and reorder trust configuration to duplicate the existing environment-variable control. +- `setTrustProject()` would override trust flags parsed by PsySH, but neither Laravel nor Hypervel defines those flags on the Tinker command. This unreachable shared behavior is not a defect. ## Expected result -Tinker preserves its Laravel-facing API and Hypervel's coroutine/no-fork adaptations while direct execution becomes exact for falsey code, includes, exit status, disabled commands, and process-global cleanup. Alias discovery respects semantic boundaries; presentation degrades per property; metadata and docs describe the real package. All work remains cold developer-console work, with no application hot-path or high-scale footprint. No accepted defect, workaround, stale branch, compatibility shim, TODO, or speculative machinery remains in the completed Hypervel package. +Tinker preserves its Laravel-facing API and Hypervel's coroutine/no-fork adaptations while direct execution becomes exact for falsey code, includes, exit status, disabled commands, process-global cleanup, and untrusted project configuration. Alias discovery respects semantic boundaries; presentation degrades per property; metadata and docs describe the real package. All work remains cold developer-console work, with no application hot-path or high-scale footprint. No accepted defect, workaround, stale branch, compatibility shim, TODO, or speculative machinery remains in the completed Hypervel package.