aboutsummaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-07-24fix(locker): return stability-flags as int, not stringnsfisis
Locker::get_stability_flags returned IndexMap<String, String>, converting each value via PhpMixed::as_string(), which only matches the String variant. composer.lock's "stability-flags" values are always JSON integers (BasePackage::STABILITIES), so every flag silently decoded to "" and installer.rs's downstream .parse::<i64>() defaulted it to 0 (stable). This made any locked package pinned via a non-stable stability-flags entry look "unacceptable" during `composer install`, silently dropping it from the solver's pool instead of fixing/requiring it — turning a real dependency conflict into a spurious "lock file needs changes" result. Return i64 directly, matching RootPackageInterface::get_stability_flags and set_lock_data's existing convention for this same PHP array shape.
2026-07-24fix(installer-test): match Locker content-hash encoding to lock hashnsfisis
do_test_integration built the Locker's composer.json content string with serde_json::to_string, which doesn't escape slashes, while the fixture's auto-computed lock "hash" field used JsonFile::encode_with_options with slashes escaped (mirroring PHP's json_encode default). Since virtually every package name contains a "/", this made Locker::isFresh() spuriously report every such lock as stale, unignoring 12 fixtures that depended on the lock being recognized as fresh during `install`.
2026-07-24test(installer): split integration tests into one #[test] per fixturensfisis
Replace the three loop-based test functions (each iterating all fixtures with a shared SKIP_INSTALLER_* skip list) with macro-generated per-fixture test functions, so a single failing or panicking fixture no longer hides the pass/fail status of the rest. Known-failing fixtures keep their TODO(phase-d) reasons via #[ignore] on their own test instead of a shared skip list. All generated tests are #[serial] since they mutate global env vars (COMPOSER_POOL_OPTIMIZER, COMPOSER_FUND) that would otherwise race across parallel test threads. This is an interim structure for triaging the ~131 ignored fixtures one at a time; it should be reverted to a single loop-based test per group (matching Composer's directory-scanned fixtures) once they are resolved.
2026-07-24refactor(filesystem-repository): drop eval() shim, defer installed.php ↵nsfisis
reload to PHP runtime Rust has no PHP interpreter, so eval() can never be ported faithfully. safely_load_installed_versions()'s job of priming Composer\InstalledVersions before plugins run only matters within a single shared PHP process, which the RPC-based plugin architecture does not have; the PHP runtime process can call InstalledVersions::reload() itself instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24docs(symfony-filesystem): mark unported Filesystem gaps with TODO(phase-c)nsfisis
Audited every remaining method in the Symfony Filesystem port against composer/vendor/symfony/filesystem/Filesystem.php and tagged each divergence with a searchable TODO(phase-c): the missing self::$lastError propagation, copy()'s collapsed fopen-failure messages and skipped mtime preservation, exists()'s missing PHP_MAXPATHLEN guard, do_remove()'s unported rename/rollback safety trick and its Unix short-circuit gap, symlink()'s unported Windows path-normalization/copy_on_windows fallback, link_exception()'s unported error-code-1314 message, read_link()'s missing canonicalize=true overload and its Windows PHP<7.4 quirk, and mirror()'s missing getRealPath()/filesCreatedWhileMirroring dedup. Windows-only branches were previously left with plain comments claiming they "never run on Unix" instead of the required TODO marker, which understates them as permanently out of scope rather than unported work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24refactor(symfony-filesystem): remove unused Filesystem API surfacensfisis
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24refactor(symfony-style): remove unused StyleInterface impl on OutputStylensfisis
Real symfony/console OutputStyle is abstract and never defines these methods itself (title/section/table/ask/... stay abstract, deferred to SymfonyStyle). The Rust impl block was a porting artifact never invoked anywhere: OutputStyle is only used as SymfonyStyle's concrete `inner` field, and every StyleInterface call site goes through SymfonyStyle's own full implementation. new_line, which SymfonyStyle::new_line does delegate to, moves to an inherent method to keep that call working.
2026-07-24refactor(symfony-process): remove unused Process API surfacensfisis
Since Process is php-native with no Rust-fidelity obligation (see plugin-class-classification.md), this port only needs to cover what Rust-ported Composer code actually calls. Made the pipes/process_utils modules pub(crate) (nothing outside symfony/process used them) and rebuilt with `--force-warn dead_code` (normally allowed workspace-wide) to find genuinely unreachable methods: Process lost 17 methods, 5 constants, and a private clone helper; ExecutableFinder lost two unused suffix setters; AbstractPipes lost handle_error, whose only caller (a stream_select error-handler registration) was never wired up. Removing several of those setters (set_pty, set_idle_timeout, disable_output/enable_output, set_options) then left the fields they used to write with no remaining writer, so they hold one constant value on every reachable path: pty always false, idle_timeout always None, output_disabled always false, options always {suppress_errors, bypass_shell}. Audited by value (not just call-graph reachability) and removed everything that depended on the now-constant value: - pty: is_pty(), is_pty_supported(), the PTY descriptor branch in UnixPipes::get_descriptors(), and the now-unconstructed Descriptor::Pty variant in shirabe-php-shim (plus its proc_open match arm). - idle_timeout: get_idle_timeout() and check_timeout()'s idle branch; ProcessTimedOutException collapses to the single reachable timeout type (dropped timeout_type/TYPE_GENERAL/TYPE_IDLE/is_general_timeout/ is_idle_timeout/get_exceeded_timeout). - output_disabled: is_output_disabled(), build_callback()'s disabled variant, get_descriptors()'s output_disabled term, and the always-false guard in read_pipes_for_output()/ProcessFailedException (its output section is now unconditional). - options: Drop::drop()'s create_new_console branch can never fire (that key can no longer exist), so it always just stops the process. - has_callback/last_output_time: left write-only once their only readers (the branches above) were gone. - have_read_support: constant true once output_disabled collapsed, so removed from PipesInterface, UnixPipes (incl. its /dev/null null-stream branch), WindowsPipes, and Process::wait()'s dead guard. No behavior change: every removed item/branch had zero callers, or was constant on every reachable call site.
2026-07-23feat(process-executor): stub plugin-facing execute_async_php pathnsfisis
A plugin can reach ProcessExecutor::executeAsync() through the rust-proxy stub, which resolves with a real Symfony Process instance that can't be reconstructed on the Rust side (its state is tied to whichever process calls proc_open(), and it refuses serialization). Add execute_async_php() as a todo!() stub, documented as a dual- instantiation split in plugin-class-classification.md: Rust-internal callers keep using execute_async(), while the plugin path must forward spawning to the PHP child once the RPC channel exists.
2026-07-23fix(plugin-class-classifier): reach docblock-only generic payload typesnsfisis
The reachability closure only consults @phpstan-return/@param/@var docblocks when the native type is array/iterable/mixed/object/absent. For a concrete wrapper class whose payload is expressed only via a phpstan generic (PromiseInterface<Process>), the payload type was silently dropped: Symfony\Component\Process\Process never appeared as reached even though ProcessExecutor::executeAsync() hands one to plugin callbacks. Treat known generic wrapper types the same as array/iterable/mixed/object so their docblock payload is folded into the closure.
2026-07-23feat(plugin-class-classifier): add query helper for single-class lookupsnsfisis
Answers how a given class is treated at the plugin boundary, accepting a PHP source file, a Rust source file, or a (short or fully qualified) class name. Reads report.json and generates it first when missing. Rust paths resolve by normalized segment matching because the snake_case mapping is not reversible for acronyms (io_interface.rs -> IOInterface). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23feat(plugin-class-classifier): add deterministic plugin-boundary classifiernsfisis
Decides, for every composer/composer class, how it is treated at the plugin boundary (rust-proxy / rust-snapshot / contract / two-world / php-native / unsupported) so that upstream updates re-classify new or rewritten classes without re-deriving the design by hand. Rules and category definitions live in docs/dev/plugin-class-classification.md; the tool (PHP + nikic/PHP-Parser) implements them as a reachability closure with direction marks, per-method pure/mutator analysis, and a leaf-first fixed point for unreachable classes, with three small versioned exception lists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23fix(bench): pin composer binary and work around HTTP/3 slowdownnsfisis
Benchmarks were comparing against whatever composer happened to be on PATH instead of the pinned submodule version, and paid the HTTP/3 fallback penalty from composer/composer#12987 on every packagist request.
2026-07-21refactor(lint): rewrite structural linters from Ruby to PHPnsfisis
Fold scripts/lint and scripts/linters/*.rb into a standalone Composer project under scripts/linters/, matching the scripts/plugin-class-classifier/ convention. Uses no external packages, only PHP + Composer autoloading. Verified byte-for-byte identical output against the original Ruby implementation, both on the current repo (all linters pass) and on a synthetic fixture exercising every violation type. Entry point moves from `scripts/lint` to `scripts/linters/lint`.
2026-07-21chore: update READMEnsfisis
2026-07-21chore: add logonsfisis
2026-07-20perf(composer-repository): avoid redundant clones when checking security ↵nsfisis
advisories get_security_advisories cloned each package's full metadata blob (hundreds of versions for popular packages) just to peek at its "security-advisories" field. Same pattern already fixed in load_async_packages by 2862d2da; move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: response processing in the security-advisories metadata branch drops ~80% (236ms -> 48ms), and the whole run_security_advisory_filter phase drops from ~420-500ms to ~250-270ms. End-to-end, shirabe now matches or edges out upstream Composer (6.57s vs 6.85s in this run) instead of trailing by ~1.2s.
2026-07-20perf(composer-repository): avoid redundant clones when loading async package ↵nsfisis
metadata load_async_packages cloned each response's version metadata up to three times (spec_list/response, response_arr, versions_mixed, then every per-version field) before building the versions Vec. Move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: per-batch response processing time in load_async_packages drops ~39% (1.30s -> 0.80s cumulative), narrowing the E2E gap vs upstream Composer from 1.37s to 1.14s on average.
2026-07-20feat(php-rpc): attach worker process state to request I/O errorsnsfisis
A dead worker and a live one hitting a framing bug both surface as a raw socket I/O error (e.g. "Broken pipe"), which doesn't say whether the child crashed, was signaled, or is still running. Query the child's exit status via try_wait() and attach it as anyhow::Context so the panic message shows the root cause directly.
2026-07-20fix(json-file): forward detected indent into write's encode optionsnsfisis
JsonFile::write_with_options() used the caller-supplied JsonEncodeOptions verbatim, ignoring self.indent (set by read()'s detect_indenting), unlike PHP's write() which always passes $this->indent to encode() regardless of the $options argument. Un-ignore test_preserve_indentation_after_read.
2026-07-20fix(search-command): avoid usize overflow panic when truncating descriptionnsfisis
The negated remaining-width subtraction was cast straight to usize, overflowing whenever it went negative (e.g. an abandoned-package warning ate the available width) and panicking on slice indexing. Route through the shim's substr(), which mirrors PHP's negative-length semantics and clamps instead of overflowing.
2026-07-20fix(pool-builder): fix three build_pool bugs found by un-ignoring ↵nsfisis
test_pool_builder build_pool never populated skipped_load for locked packages skipped due to the update allow list (nor their replace targets), so load_package's skipped_load-driven transitive-unlock branch never fired. load_packages_marked_for_loading never recorded loaded packages into loaded_per_repo, so repositories had no way to dedupe already-loaded versions when a constraint got re-expanded, producing duplicate pool entries. unlock_package looked up a locked package's removal index via the position in a Vec snapshot of self.packages.values() instead of its actual IndexMap key, so after earlier removals the wrong entry (or none) got removed, leaving stale locked packages in the pool. Fixing all three lets test_pool_builder run un-ignored.
2026-07-20fix(no-proxy-pattern): stop chr() corrupting IP bytes as lossy UTF-8nsfisis
shirabe_php_shim::chr() returned a Rust String, which lossily re-encodes bytes >= 0x80 as UTF-8 replacement characters. ip_get_mask, ip_get_network, and ip_map_to_6 relied on chr() to build raw in_addr and netmask byte arrays, corrupting IPv4-in-IPv6 mappings and CIDR netmasks. Build the Vec<u8> byte arrays directly instead of round-tripping through String, and un-ignore test_ip_address and test_ip_range now that the underlying bug is fixed. chr()'s only other caller (http_downloader.rs, an ASCII ESC byte in a regex pattern) didn't need the indirection either, so remove the shim function entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20test(proxy-manager): un-ignore tests by serializing env-var accessnsfisis
The ignored tests raced on the process-wide HTTP_PROXY/etc env vars and the ProxyManager::INSTANCE mutex, causing spurious PoisonError panics when run in parallel. Adding #[serial_test::serial], the same annotation test_instantiation already used, fixes the race with no other changes needed.
2026-07-20fix(php-shim): use PhpMixed::to_bool() for PHP truthy castsnsfisis
Several call sites coerced PhpMixed to bool via `.as_bool()` (which only matches a literal Bool variant) where the corresponding PHP code does a plain `(bool)` cast or truthy check (isset()/array_key_exists() + implicit bool conversion). This silently dropped truthy non-bool values (e.g. String("true"), String("1"), Int(1)) to their unwrap_or default instead of PHP's actual truthy result. Switched these sites to PhpMixed::to_bool(), which implements PHP's full truthy-cast rules.
2026-07-20test(ignore): document root causes for unannotated #[ignore] testsnsfisis
74 tests carried a bare #[ignore] with no explanation. Re-ran each: 25 now pass and had the attribute removed; the remaining 49 got a concise reason (todo!() stubs, regex-crate PCRE gaps, PhpMixed type mismatches, config bool-coercion bugs, missing skipped_load wiring in PoolBuilder, etc.) so future work can find and fix them by grep. No production code or test logic/assertions were changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20fix(advisory): treat int-keyed ignore entries as plain IDsnsfisis
parseIgnoreWithApply distinguishes ['CVE-123' => 'reason'] from [0 => 'CVE-123'] by checking is_int($key) in PHP. AuditConfig only matched on the value's type, so a canonical-int-keyed string value was mistaken for an id => reason pair. Expose canonical_int_key from the php-shim to replicate PHP's key-canonicalization rule and unignore test_mixed_formats.
2026-07-20fix(diagnose-command): stop holding a Config borrow across http callsnsfisis
execute() held &config.borrow() across check_http/check_composer_repo/ check_composer_audit, which reach HttpDownloader -> CurlDownloader:: download; that method does self.config.borrow_mut() on the same Config RefCell, panicking with "RefCell already borrowed" once the phpinfo panic that previously masked this was fixed. Switch those three helpers to take the Rc<RefCell<Config>> handle (matching check_version's existing pattern) and borrow only where a field is actually read, so no borrow spans the downstream network call. Un-ignore the now-passing run_diagnose smoke test and test_cmd_fail; test_cmd_success stays ignored, now for two separate reasons: it needs real network access (as the PHP original does), and shirabe_php_shim::OPENSSL_VERSION_NUMBER is a hardcoded stub (0) that always trips check_platform's TLSv1.1/1.2 support check regardless of the real linked OpenSSL, forcing a non-zero exit code.
2026-07-20feat(php-rpc): implement phpinfo() capture over RPCnsfisis
DiagnoseCommand::check_platform needed phpinfo() output but shirabe-php-shim's ob_start()/phpinfo()/ob_get_clean() are unmodeled todo!()s. Add a phpinfo dispatch entry to the PHP worker (capturing output the same way extension_info already does) and a get_phpinfo() wrapper, then switch check_platform to call it directly. This unblocks the phpinfo-related panic in diagnose; the ignored tests now hit a separate RefCell double-borrow bug in check_http, so their ignore reasons are updated to point at that instead.
2026-07-20test(fund-command): match PHP tester options for deterministic displaynsfisis
capture_stderr_separately routed the tester through ConsoleOutput, which detects color/hyperlink support from the real process STDOUT before the stream is swapped to the memory buffer. When STDOUT is a real tty, decoration leaks into the captured output. The PHP original never passes this option and get_error_output() was unused here, so drop it to match and keep the assertion deterministic.
2026-07-20test(show-command): ignore test_self over UTC-only date() timezone gapnsfisis
The shim date() renders in UTC only (no timezone database) while PHP's date() uses the system default timezone, so get_relative_time misses the "today" match and prints "this week" whenever the local date differs from the UTC date (daily 00:00-09:00 JST on this machine). Verified by running the test with and without TZ=UTC at 07:40 JST. Mark the gap with a TODO(phase-c) in the shim; fixing it needs a timezone database (a new crate), which is a user decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(cli-tests): detail run_diagnose ignore reasonnsfisis
Measured the actual panic site: DiagnoseCommand::check_platform reaches the todo!() ob_start()/ob_get_clean() shims while capturing phpinfo(), the same root cause already recorded in tests/command/diagnose_command_test.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(require-command): drop input borrows before determine_requirementsnsfisis
The Ref temporaries created by input.borrow() inside the argument expressions of the determine_requirements call lived until the end of the whole call statement, so ConsoleIO::ask_question's borrow_mut() on the same shared input RefCell panicked with "RefCell already borrowed" when the command prompted for packages. Hoist the argument computations into locals so no borrow is held across the call, and un-ignore the run_require CLI test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(zip): detail why the zip-extension test stays ignorednsfisis
PHP only runs this test when the zip extension is not loaded; the Rust port has unconditional zip support, so that precondition cannot exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(repository-manager): un-ignore test_repo_creationnsfisis
The ignore reason was stale: create_repository_by_class now dispatches every repository class this test registers, and the test passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(github-driver): defer get_branches until tags search missesnsfisis
PHP's `?:` chain in getComposerInformation short-circuits, so getBranches() only runs when the tags search is falsy. The eager port issued an extra git/refs/heads API request that PHP never makes. Un-ignore test_public_repository_archived, which this fixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(platform-repository): name real test_library_information blockersnsfisis
The previous ignore reason blamed unmodeled extension info; the actual blockers are the TODO(plugin) stubs resource_bundle_get (returns Null, dropping lib-icu-cldr) and imagick_get_version_string (returns "", dropping lib-imagick-imagemagick), both pending dynamic method dispatch on PHP objects via the plugin RPC mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(composer-repository): un-ignore test_what_providesnsfisis
The ignore reason went stale: AliasPackage::get_source_type is implemented (delegates to alias_of) and the test passes as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(semver): stop classical pattern matching 6+ digit versionsnsfisis
The regex crate parses PCRE's possessive \d{1,5}+ as a stacked repetition (?:\d{1,5})+, i.e. \d+, so date versions like 20121020 matched the classical pattern and normalized to 20121020.0.0.0 instead of falling through to the date(time) pattern like PHP. The plain \d{1,5} is equivalent to the possessive form here per the regex-porting rules. Un-ignore test_find_recommended_require_version which this had blocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(config): drop insecure git protocol under secure-httpnsfisis
Config::get("github-protocols") ported PHP's array_search over the protocol list via a string-keyed map, but array_search_mixed returns the matched index as PhpMixed::Int, which the as_string() read never matched, so the git protocol was never removed (Config.php:447-449 removes it whenever secure-http is on). Search the list directly and read the index as an Int. Un-ignore test_update_throws_runtime_exception_if_git_command_fails which this had blocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(config): restore PHP (int) cast for ttl and timeout valuesnsfisis
PHP's Config::get() applies an (int) cast to cache-files-ttl, cache-ttl, and the process-timeout env override (Config.php:326,367,398), so string values like '99999999' become integers. The strict PhpMixed::as_int returned None for strings, collapsing them to 0. Use the intval shim, which implements the PHP cast, and un-ignore test_cache_garbage_collection_is_called which this had blocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(update-command): un-ignore test_interactive_tmp via assoc select choicesnsfisis
PHP's UpdateCommand::getPackagesInteractively passes $autocompleterValues keyed by package name to $io->select, so the selection resolves to package names. The port passed only the keys as a list, making select resolve to a numeric index that the update then treated as an unknown package. The old ignore reason (non-interactive terminal error) no longer applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(run-script-command): pass script choices as assoc array in interactnsfisis
PHP's RunScriptCommand::interact passes $options keyed by script name, so select resolves the entered value to the script name and sets it as the script argument. The port passed only the keys as a list, which made select resolve to a numeric index instead of the script name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(io): widen IOInterface::select choices to PhpMixed for assoc arraysnsfisis
PHP's IOInterface::select accepts an associative choices array whose keys are the selectable values, but the port narrowed it to Vec<String>, making key-based selection unrepresentable. Accept PhpMixed (List or Array) like PHP's array $choices; ConsoleIO already branched on both shapes internally. Also mirror PHP in the single-select array_search fallback for numeric-keyed arrays. All call sites keep their previous list-based behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(path-repository): un-ignore test_url_remains_relativensfisis
The PHP test implicitly requires the process cwd to be an ancestor of the Fixtures dir (phpunit runs inside the composer checkout); cargo runs tests from the crate manifest dir, which is not. Replicate the phpunit precondition with a drop-restoring CwdGuard chdir'ing to the __DIR__ equivalent, and serialize it (together with the only other cwd-mutating test in the repository binary, test_repository_writes_installed_php) via #[serial] so the process-global cwd cannot race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(installation-manager): un-ignore test_add_remove_installernsfisis
Add the __add_installer test seam that registers an installer as a pre-built Rc handle, so the test can reproduce PHP's object-identity semantics (assertSame / removeInstaller) via Rc::ptr_eq; add_installer cannot serve because Rc::from(Box) reallocates, losing the caller's pointer identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(run-script-command): detail dev-mode test ignore blockersnsfisis
Re-verified the reason: besides the missing as_any seam on EventInterface (a cross-cutting trait change over every event type), the mocked dispatchScript call expectation is also inexpressible since dispatch_script is a concrete method with no call-recording seam. Record both blockers in the ignore string and TODO(phase-d) comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(console-io): un-ignore test_write_error via setErrorOutputnsfisis
The ignore reason had rotted: ConsoleOutputInterface::set_error_output is already ported, so a BufferedOutput error sink can be injected into a real ConsoleOutput and the error-routed write read back, matching the file's convention of replacing PHPUnit mock expectations with BufferedOutput assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(event-dispatcher): un-ignore test_dispatcher_support_for_additional_argsnsfisis
The only missing seam was the PHP test's ReflectionMethod(getPhpExecCommand) access; add the test-only __get_php_exec_command wrapper and port the test on the existing get_listeners override and process-executor mock infrastructure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(auditor): restore instanceof ConsoleIO semantics for BufferIOnsfisis
PHP's BufferIO extends ConsoleIO, so $io instanceof ConsoleIO matches it; the port models that inheritance as composition, making the plain ConsoleIO downcast reject BufferIO and throw where PHP renders tables. Also try a BufferIO downcast and unwrap its inner ConsoleIO, which unblocks the two FORMAT_TABLE cases and un-ignores test_audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>