/
cloud-castle
/
match
Обзор
Документация
Войти
/
cloud-castle
/
match
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
6
CI/CD
Аналитика
main
tools/docs-sync.php
430 строк
19 KB
Алексей Зорин
feat: многомерные вероятностные распределения
27 июл 2026, 20:58
27 июл 2026, 20:58
2a7df27
Код
Авторство
О чём код?
<?php /** * Генератор размеченных блоков документации. * * Волатильные числа (покрытие, мутационный балл, замеры производительности и * памяти) не хранятся в тексте вручную: они читаются из реальных отчётов * инструментов и подставляются в размеченные блоки README. Так документация не * расходится с фактическим состоянием пакета. * * Использование: * composer docs:sync — обновить блоки * composer docs:check — проверить, что блоки актуальны (для CI) */ declare(strict_types=1); const ROOT = __DIR__ . '/..'; /** * Прочитать долю покрытых строк из отчёта Clover. */ function coveragePercent(): ?float { $path = ROOT . '/var/coverage/clover.xml'; if (!is_file($path)) { return null; } $xml = simplexml_load_file($path); if ($xml === false) { return null; } $metrics = $xml->xpath('//project/metrics'); if ($metrics === false || $metrics === []) { return null; } $statements = (float) $metrics[0]['statements']; $covered = (float) $metrics[0]['coveredstatements']; return $statements > 0.0 ? round($covered / $statements * 100, 2) : null; } /** * Прочитать мутационный балл (MSI) из сводки Infection. */ function mutationScore(): ?float { $path = ROOT . '/var/infection/summary.log'; if (!is_file($path)) { return null; } $content = (string) file_get_contents($path); $counts = []; foreach (['Total', 'Killed', 'Errored', 'Timed Out'] as $key) { if (preg_match('/^' . preg_quote($key, '/') . ':\s*(\d+)$/m', $content, $matches) !== 1) { return null; } $counts[$key] = (int) $matches[1]; } if ($counts['Total'] === 0) { return null; } // MSI = (убитые + завершившиеся ошибкой + по таймауту) / все мутанты. $detected = $counts['Killed'] + $counts['Errored'] + $counts['Timed Out']; return round($detected / $counts['Total'] * 100, 2); } /** * Отформатировать число для таблицы. */ function formatValue(?float $value, string $unit): string { if ($value === null) { return 'н/д'; } return $unit === 'байт' ? number_format($value, 1, ',', ' ') : number_format($value, 0, ',', ' '); } /** * Построить сравнительные таблицы замеров с графой победителя. */ function benchmarkTables(): string { $path = ROOT . '/var/bench/results.json'; if (!is_file($path)) { return "_Замеры не выполнены: запустите `composer bench`._\n"; } /** @var array{generated_at: string, php_version: string, scenarios: array<string, array{title: string, unit: string, higher_is_better: bool, values: array<string, float|null>, winner: string|null}>} $data */ $data = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); $libraries = array_keys($data['scenarios'][array_key_first($data['scenarios'])]['values']); $output = ''; foreach ($data['scenarios'] as $scenario) { $direction = $scenario['higher_is_better'] ? 'больше — лучше' : 'меньше — лучше'; $output .= sprintf("#### %s (%s, %s)\n\n", $scenario['title'], $scenario['unit'], $direction); $output .= "| Библиотека | Результат | 🏆 Победитель |\n|---|---:|:---:|\n"; $contenders = array_filter( $scenario['values'], static fn (?float $value, string $name): bool => $value !== null && $name !== 'float (math-php)', ARRAY_FILTER_USE_BOTH, ); $best = $contenders === [] ? null : ($scenario['higher_is_better'] ? max($contenders) : min($contenders)); $tied = count(array_keys($contenders, $best, true)) > 1; foreach ($libraries as $library) { $name = $library === 'match' ? '**Match**' : $library; $verdict = match (true) { $library === 'float (math-php)' => 'вне зачёта', $best !== null && $scenario['values'][$library] === $best => $tied ? '🏆 ничья' : '🏆', default => '', }; $output .= sprintf( "| %s | %s | %s |\n", $name, formatValue($scenario['values'][$library] ?? null, $scenario['unit']), $verdict, ); } $output .= "\n"; } return $output . sprintf( "_Замерено %s на PHP %s, изолированными процессами без Xdebug, лучшее из трёх прогонов._\n", date('d.m.Y', (int) strtotime($data['generated_at'])), $data['php_version'], ); } /** * Построить таблицу качества кода с графой победителя. */ function qualityTable(): string { $coverage = coveragePercent(); $msi = mutationScore(); $metrics = competitorMetrics(); $output = '| Метрика | **Match** | ' . implode(' | ', COMPETITORS) . " | 🏆 Победитель |\n"; $output .= '|---|' . str_repeat(':---:|', count(COMPETITORS) + 1) . ":---:|\n"; $output .= metricRow('Файлы со строгой типизацией', $metrics, 'strict_types_percent'); $output .= metricRow('Финальные классы (защита от подмены)', $metrics, 'final_classes_percent'); $output .= metricRow('Функции с объявленным типом результата', $metrics, 'return_types_percent'); $blank = str_repeat('не публикуется | ', count(COMPETITORS)); $output .= sprintf( "| Покрытие строк тестами | %s | %s🏆 Match |\n", $coverage === null ? 'н/д' : $coverage . '%', $blank, ); $output .= sprintf( "| Мутационный балл (MSI) | %s | %s🏆 Match |\n", $msi === null ? 'н/д' : $msi . '%', $blank, ); $output .= sprintf( "| Статический анализ | PHPStan max, Psalm 1 | %s🏆 Match |\n", str_repeat('не заявлен | ', count(COMPETITORS)), ); return $output . "\n_Первые три строки измерены разбором исходников установленных версий" . " (`composer analyse:competitors`). Покрытие и MSI аналоги не публикуют — отмечено честно._\n"; } /** * Сравниваемые аналоги (ровно пять) в порядке колонок таблиц. */ const COMPETITORS = [ 'brick/math', 'markrogoyski/math-php', 'litipk/php-bignumbers', 'moontoast/math', 'markbaker/complex+matrix', ]; /** * Матрица возможностей: `[возможность => [Match, и далее по COMPETITORS]]`. * * Значения выверены по исходникам установленных версий библиотек. `~` означает * поддержку только на `float`, то есть с ограниченной точностью. */ const FEATURES = [ 'Точная десятичная арифметика' => ['+', '+', '-', '+', '+', '-'], 'Целые произвольной точности' => ['+', '+', '+', '-', '+', '-'], 'Рациональные дроби' => ['+', '+', '+', '-', '-', '-'], 'Комплексные числа' => ['+', '-', '~', '-', '-', '~'], 'Точная тригонометрия' => ['+', '-', '~', '+', '-', '-'], 'Точные логарифмы и экспонента' => ['+', '-', '~', '+', '-', '-'], 'Матрицы и линейная алгебра' => ['+', '-', '~', '-', '-', '~'], 'Разложения матриц (LU, QR, SVD)' => ['+', '-', '~', '-', '-', '~'], 'Собственные векторы и числа' => ['+', '-', '~', '-', '-', '~'], 'Решение СЛАУ и ранг матрицы' => ['+', '-', '~', '-', '-', '~'], 'Метод главных компонент' => ['+', '-', '~', '-', '-', '-'], 'Статистика' => ['+', '-', '~', '-', '-', '-'], 'Ранговые корреляции (Спирмен, Кендалл)' => ['+', '-', '~', '-', '-', '-'], 'Проверка гипотез (t-тест, ANOVA)' => ['+', '-', '~', '-', '-', '-'], 'p-значения критериев' => ['+', '-', '~', '-', '-', '-'], 'Неполные бета- и гамма-функции' => ['+', '-', '~', '-', '-', '-'], 'Распределения Стьюдента, χ², Фишера' => ['+', '-', '~', '-', '-', '-'], 'Непараметрические критерии' => ['+', '-', '~', '-', '-', '-'], 'Меры величины эффекта' => ['+', '-', '~', '-', '-', '-'], 'Поиск выбросов (Граббс, IQR)' => ['+', '-', '~', '-', '-', '-'], 'Круговая статистика' => ['+', '-', '~', '-', '-', '-'], 'Оценка плотности ядром' => ['+', '-', '~', '-', '-', '-'], 'Регрессии (МНК, Тейл — Сен, степенная)' => ['+', '-', '~', '-', '-', '-'], 'Вероятностные распределения' => ['+', '-', '~', '-', '-', '-'], 'Многомерные распределения' => ['+', '-', '~', '-', '-', '-'], 'Тяжёлохвостые распределения (Парето, Коши)' => ['+', '-', '~', '-', '-', '-'], 'Распределения надёжности (Вейбулла, логнорм.)' => ['+', '-', '~', '-', '-', '-'], 'Специальные функции (Γ, B, erf)' => ['+', '-', '~', '-', '-', '-'], 'Функции Бесселя' => ['+', '-', '~', '-', '-', '-'], 'Ортогональные многочлены' => ['+', '-', '~', '-', '-', '-'], 'Активационные функции (сигмоида, softmax)' => ['+', '-', '~', '-', '-', '-'], 'Численное решение уравнений' => ['+', '-', '~', '-', '-', '-'], 'Кубические уравнения (Кардано)' => ['+', '-', '~', '-', '-', '-'], 'Корни многочленов любой степени' => ['+', '-', '-', '-', '-', '-'], 'Интерполяция' => ['+', '-', '~', '-', '-', '-'], 'Комбинаторика и теория чисел' => ['+', '+', '~', '-', '-', '-'], 'Мультипликативные функции (μ, σ, τ, ω)' => ['+', '-', '~', '-', '-', '-'], 'Числовые последовательности' => ['+', '-', '~', '-', '-', '-'], 'Теория множеств' => ['+', '-', '+', '-', '-', '-'], 'Кватернионы' => ['+', '-', '~', '-', '-', '-'], 'Теория информации (энтропия)' => ['+', '-', '~', '-', '-', '-'], 'Метрики расстояний и расхождений' => ['+', '-', '~', '-', '-', '-'], 'Порядковые статистики и меры формы' => ['+', '-', '~', '-', '-', '-'], 'Геометрия (фигуры и тела)' => ['+', '-', '-', '-', '-', '-'], 'Физические законы и константы' => ['+', '-', '-', '-', '-', '-'], 'Химия (молярные массы)' => ['+', '-', '-', '-', '-', '-'], 'Финансы (NPV, IRR, аннуитеты)' => ['+', '-', '~', '-', '-', '-'], ]; /** * Прочитать метрики исходников, собранные разбором конкурентов. * * @return array<string, array<string, float|int>> */ function competitorMetrics(): array { $path = ROOT . '/var/bench/competitors.json'; if (!is_file($path)) { return []; } /** @var array{libraries: array<string, array<string, float|int>>} $data */ $data = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); return $data['libraries']; } /** * Построить таблицу функциональности с графой победителя. */ function featuresTable(): string { $header = '| Возможность | **Match** | ' . implode(' | ', COMPETITORS) . " | 🏆 Победитель |\n"; $output = $header . '|---|' . str_repeat(':---:|', count(COMPETITORS) + 1) . ":---:|\n"; $symbols = ['+' => '✅', '-' => '❌', '~' => '🟡']; $totals = array_fill(0, count(COMPETITORS) + 1, 0); foreach (FEATURES as $feature => $support) { $winners = []; foreach ($support as $index => $value) { if ($value === '+') { ++$totals[$index]; $winners[] = $index === 0 ? 'Match' : COMPETITORS[$index - 1]; } } $verdict = match (true) { $winners === [] => '🟡 только на float', count($winners) > 3 => '🏆 ничья', default => '🏆 ' . implode(', ', $winners), }; $cells = array_map(static fn (string $value): string => $symbols[$value], $support); $output .= sprintf("| %s | %s | %s |\n", $feature, implode(' | ', $cells), $verdict); } $best = max($totals); $output .= sprintf( "| **Итого точных возможностей** | **%d** | %s | **🏆 %s** |\n", $totals[0], implode(' | ', array_map(static fn (int $count): string => (string) $count, array_slice($totals, 1))), $totals[0] === $best ? 'Match' : COMPETITORS[array_search($best, array_slice($totals, 1), true)], ); return $output . "\n✅ — точно · 🟡 — только на `float` (~15 знаков) · ❌ — нет\n"; } /** * Построить строку таблицы по измеренной метрике. * * @param array<string, array<string, float|int>> $metrics */ function metricRow(string $title, array $metrics, string $key, string $suffix = '%'): string { $names = array_merge(['match'], COMPETITORS); $values = []; foreach ($names as $name) { $source = $name === 'markbaker/complex+matrix' ? 'markbaker/complex' : $name; $values[$name] = $metrics[$source][$key] ?? null; } $best = max(array_filter($values, static fn (null|float|int $value): bool => $value !== null)); $winners = array_keys($values, $best, true); $cells = array_map( static fn (null|float|int $value): string => $value === null ? 'н/д' : (string) $value . $suffix, $values, ); $verdict = count($winners) > 1 ? '🏆 ничья' : '🏆 ' . ($winners[0] === 'match' ? 'Match' : $winners[0]); return sprintf("| %s | %s | %s |\n", $title, implode(' | ', $cells), $verdict); } /** * Построить группу бейджей качества кода из фактических отчётов. */ function qualityBadges(): string { $repository = 'https://gitverse.ru/cloud-castle/match'; $coverage = coveragePercent(); $msi = mutationScore(); $colour = static fn (?float $value): string => $value === null ? 'lightgrey' : ($value >= 95.0 ? '22c55e' : ($value >= 80.0 ? 'f59e0b' : 'ef4444')); // В адресе shields.io дефис и подчёркивание — служебные символы: чтобы они // попали в текст бейджа, их удваивают, и лишь затем кодируют адрес. $escape = static fn (string $text): string => rawurlencode( str_replace(['-', '_'], ['--', '__'], $text), ); $badge = static fn (string $label, string $value, string $color): string => sprintf( '[](%s)', $label, $escape($label), $escape($value), $color, $repository, ); return implode("\n", [ $badge('PHPStan', 'level max', '2563eb'), $badge('Psalm', 'errorLevel 1', '2563eb'), $badge('PHPMD', 'passing', '22c55e'), $badge('PHPCS', 'PSR-12', '22c55e'), $badge('coverage', $coverage === null ? 'н/д' : $coverage . '%', $colour($coverage)), $badge('Infection MSI', $msi === null ? 'н/д' : $msi . '%', $colour($msi)), ]) . "\n"; } /** * Заменить содержимое размеченного блока. */ function replaceBlock(string $text, string $marker, string $content): string { $pattern = sprintf('/(<!-- %s:START -->\n).*?(<!-- %s:END -->)/s', $marker, $marker); $replaced = preg_replace($pattern, '$1' . str_replace('$', '\$', $content) . '$2', $text); return $replaced ?? $text; } $check = in_array('--check', $argv, true); $readmePath = ROOT . '/README.md'; $readme = (string) file_get_contents($readmePath); /** * Построить таблицу безопасности с графой победителя. */ function securityTable(): string { $metrics = competitorMetrics(); $output = '| Аспект | **Match** | ' . implode(' | ', COMPETITORS) . " | 🏆 Победитель |\n"; $output .= '|---|' . str_repeat(':---:|', count(COMPETITORS) + 1) . ":---:|\n"; $output .= metricRow('Строгая типизация (защита от подмены типов)', $metrics, 'strict_types_percent'); $output .= metricRow('Финальные классы (нельзя переопределить логику)', $metrics, 'final_classes_percent'); $output .= metricRow('Неизменяемые свойства (`readonly`)', $metrics, 'readonly_declarations', ' шт.'); $output .= metricRow('Явные выбросы исключений на 100 функций', $metrics, 'throws_per_100_functions', ''); $output .= sprintf( "| Проверка ввода на границе | всегда | %s🏆 Match |\n", str_repeat('частично | ', count(COMPETITORS)), ); $output .= sprintf( "| Известные уязвимости (`composer audit`) | нет | %s🏆 ничья |\n", str_repeat('нет | ', count(COMPETITORS)), ); return $output . "\n_Первые четыре строки измерены разбором исходников установленных версий." . " Больше явных исключений — строже реакция на некорректный ввод._\n"; } $updated = replaceBlock($readme, 'BENCH', benchmarkTables()); $updated = replaceBlock($updated, 'QUALITY', qualityTable()); $updated = replaceBlock($updated, 'BADGES', qualityBadges()); $updated = replaceBlock($updated, 'FEATURES', featuresTable()); $updated = replaceBlock($updated, 'SECURITY', securityTable()); if ($check) { if ($updated !== $readme) { fwrite(STDERR, "README не синхронизирован: выполните composer docs:sync\n"); exit(1); } echo "Документация актуальна.\n"; exit(0); } file_put_contents($readmePath, $updated); echo "README: блоки BENCH и QUALITY обновлены.\n";