Implements Spec 110 Ops‑UX Enforcement and applies the repo‑wide “enterprise” standard for operation start + dedup surfaces. Key points - Start surfaces: only ephemeral queued toast (no DB notifications for started/queued/running). - Dedup paths: canonical “already queued” toast. - Progress refresh: dispatch run-enqueued browser event so the global widget updates immediately. - Completion: exactly-once terminal DB notification on completion (per Ops‑UX contract). Tests & formatting - Full suite: 1738 passed, 8 skipped (8477 assertions). - Pint: `vendor/bin/sail bin pint --dirty --format agent` (pass). Notable change - Removed legacy `RunStatusChangedNotification` (replaced by the terminal-only completion notification policy). Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #134
104 lines
2.7 KiB
PHP
104 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Support\OpsUx;
|
|
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use SplFileInfo;
|
|
|
|
final class SourceFileScanner
|
|
{
|
|
/**
|
|
* @param list<string> $roots
|
|
* @param list<string> $excludedAbsolutePaths
|
|
* @return list<string>
|
|
*/
|
|
public static function phpFiles(array $roots, array $excludedAbsolutePaths = []): array
|
|
{
|
|
$files = [];
|
|
$excluded = array_fill_keys(array_map(self::normalizePath(...), $excludedAbsolutePaths), true);
|
|
|
|
foreach ($roots as $root) {
|
|
$root = self::normalizePath($root);
|
|
|
|
if (! is_dir($root)) {
|
|
continue;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
|
|
/** @var SplFileInfo $file */
|
|
foreach ($iterator as $file) {
|
|
if (! $file->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
$path = self::normalizePath($file->getPathname());
|
|
|
|
if (pathinfo($path, PATHINFO_EXTENSION) !== 'php') {
|
|
continue;
|
|
}
|
|
|
|
if (isset($excluded[$path])) {
|
|
continue;
|
|
}
|
|
|
|
$files[] = $path;
|
|
}
|
|
}
|
|
|
|
sort($files);
|
|
|
|
return array_values(array_unique($files));
|
|
}
|
|
|
|
public static function projectRoot(): string
|
|
{
|
|
return self::normalizePath(dirname(__DIR__, 3));
|
|
}
|
|
|
|
public static function relativePath(string $absolutePath): string
|
|
{
|
|
$absolutePath = self::normalizePath($absolutePath);
|
|
$root = self::projectRoot();
|
|
|
|
if (str_starts_with($absolutePath, $root.'/')) {
|
|
return substr($absolutePath, strlen($root) + 1);
|
|
}
|
|
|
|
return $absolutePath;
|
|
}
|
|
|
|
public static function read(string $path): string
|
|
{
|
|
return (string) file_get_contents($path);
|
|
}
|
|
|
|
public static function snippet(string $source, int $line, int $contextLines = 2): string
|
|
{
|
|
$allLines = preg_split('/\R/', $source) ?: [];
|
|
$line = max(1, $line);
|
|
|
|
$start = max(1, $line - $contextLines);
|
|
$end = min(count($allLines), $line + $contextLines);
|
|
|
|
$snippet = [];
|
|
|
|
for ($index = $start; $index <= $end; $index++) {
|
|
$prefix = $index === $line ? '>' : ' ';
|
|
$snippet[] = sprintf('%s%4d | %s', $prefix, $index, $allLines[$index - 1] ?? '');
|
|
}
|
|
|
|
return implode("\n", $snippet);
|
|
}
|
|
|
|
private static function normalizePath(string $path): string
|
|
{
|
|
return str_replace('\\', '/', $path);
|
|
}
|
|
}
|