From 765b7749d31675d3cbf541b146ead1aafd6d6ae9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 21 Jul 2026 02:54:18 +0900 Subject: feat(plugin-class-classifier): add deterministic plugin-boundary classifier 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 --- scripts/plugin-class-classifier/src/Report.php | 103 +++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 scripts/plugin-class-classifier/src/Report.php (limited to 'scripts/plugin-class-classifier/src/Report.php') diff --git a/scripts/plugin-class-classifier/src/Report.php b/scripts/plugin-class-classifier/src/Report.php new file mode 100644 index 00000000..e24363fe --- /dev/null +++ b/scripts/plugin-class-classifier/src/Report.php @@ -0,0 +1,103 @@ +classifier->closure->vendorReachable as $fqcn => $bits) { + $info = VendorPackages::lookup($fqcn); + $vendor[] = [ + 'fqcn' => $fqcn, + 'package' => $info['package'], + 'category' => $info['category'], + 'direction' => $bits === 3 ? 'both' : ($bits === 1 ? 'provided' : 'consumed'), + ]; + } + usort($vendor, static fn (array $a, array $b) => strcmp($a['fqcn'], $b['fqcn'])); + + $builtins = array_keys($this->classifier->closure->builtinReachable); + sort($builtins); + + $vendorPackages = []; + foreach (VendorPackages::PREFIXES as $prefix => $info) { + $vendorPackages[$info['package']] = $info['category']; + } + ksort($vendorPackages); + + $data = [ + 'categories' => $this->countByCategory(), + 'classes' => array_values($this->classifier->rows), + 'vendorPackages' => $vendorPackages, + 'vendorTypes' => $vendor, + 'reachableBuiltins' => $builtins, + 'violations' => $this->classifier->violations, + ]; + + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if (file_put_contents($path, $json . "\n") === false) { + throw new \RuntimeException("cannot write $path"); + } + } + + public function printSummary(): void + { + $counts = $this->countByCategory(); + echo "# Plugin boundary classification\n\n"; + echo "| category | classes |\n|---|---|\n"; + foreach ($counts as $category => $count) { + echo "| $category | $count |\n"; + } + + echo "\n## Reachable surface\n\n"; + foreach (['rust-proxy', 'rust-snapshot', 'contract'] as $category) { + $names = []; + foreach ($this->classifier->rows as $row) { + if ($row['category'] === $category && $row['reachable']) { + $names[] = $row['fqcn'] . ($row['direction'] === 'both' ? ' (both)' : ''); + } + } + echo '### ' . $category . ' (' . count($names) . ")\n\n"; + foreach ($names as $name) { + echo "- $name\n"; + } + echo "\n"; + } + + $overridden = array_filter($this->classifier->rows, static fn (array $r) => isset($r['computedCategory'])); + if ($overridden !== []) { + echo "## Overridden\n\n"; + foreach ($overridden as $row) { + echo "- {$row['fqcn']}: {$row['computedCategory']} -> {$row['category']}\n"; + } + echo "\n"; + } + + if ($this->classifier->violations !== []) { + echo "## Violations\n\n"; + foreach ($this->classifier->violations as $violation) { + echo "- $violation\n"; + } + echo "\n"; + } + } + + /** @return array */ + private function countByCategory(): array + { + $counts = array_fill_keys(Classifier::CATEGORIES, 0); + foreach ($this->classifier->rows as $row) { + $counts[$row['category']]++; + } + + return $counts; + } +} -- cgit v1.3.1