inti project
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Architecture\ArchitectureScanner;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class ArchitectureScanCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:architecture-scan {--format=console : console or json} {--fail-on=error : error, warning, or none}';
|
||||
protected $description = 'Scan the codebase for modular architecture boundary violations.';
|
||||
|
||||
public function handle(ArchitectureScanner $scanner): int
|
||||
{
|
||||
$violations = $scanner->scan();
|
||||
|
||||
if ($this->option('format') === 'json') {
|
||||
$this->line(json_encode(array_map(fn ($v) => $v->toArray(), $violations), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
if ($violations === []) {
|
||||
$this->info('No architecture violations found. Suspiciously civilized.');
|
||||
}
|
||||
foreach ($violations as $violation) {
|
||||
$this->line(sprintf(
|
||||
'[%s] %s %s:%d %s',
|
||||
strtoupper($violation->severity),
|
||||
$violation->ruleId,
|
||||
$violation->file,
|
||||
$violation->line,
|
||||
$violation->message
|
||||
));
|
||||
if ($violation->suggestedFix) {
|
||||
$this->line(' Fix: '.$violation->suggestedFix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$failOn = (string) $this->option('fail-on');
|
||||
if ($failOn === 'none') {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
if ($violation->severity === 'error' || ($failOn === 'warning' && $violation->severity === 'warning')) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Architecture\DependencyMapGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
final class DependencyMapCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:dependency-map {--markdown : Also write docs/architecture/dependency-map.md}';
|
||||
protected $description = 'Generate a module dependency map for governance review.';
|
||||
|
||||
public function handle(DependencyMapGenerator $generator): int
|
||||
{
|
||||
$map = $generator->generate();
|
||||
Storage::disk('local')->put('generated/dependency-map.json', json_encode($map, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if ($this->option('markdown')) {
|
||||
$lines = ['# Dependency Map', '', 'Generated from PHP imports. Treat this as a smoke alarm, not a philosophical essay.', ''];
|
||||
foreach ($map['files'] as $file => $imports) {
|
||||
$lines[] = '## `'.$file.'`';
|
||||
$lines[] = '';
|
||||
foreach ($imports as $import) {
|
||||
$lines[] = '- `'.$import.'`';
|
||||
}
|
||||
$lines[] = '';
|
||||
}
|
||||
@mkdir(base_path('docs/architecture'), 0777, true);
|
||||
file_put_contents(base_path('docs/architecture/dependency-map.md'), implode(PHP_EOL, $lines).PHP_EOL);
|
||||
}
|
||||
|
||||
$this->info('Dependency map generated. Now the imports have fewer places to hide.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class DocumentationCoverageCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:docs-coverage {--inventory=storage/app/generated/api-route-inventory.json} {--openapi=docs/openapi.phase8.yaml}';
|
||||
protected $description = 'Check that canonical routes and deprecated routes are represented in documentation artifacts.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$inventoryPath = base_path((string) $this->option('inventory'));
|
||||
$openApiPath = base_path((string) $this->option('openapi'));
|
||||
$deprecationsPath = base_path('docs/deprecations.md');
|
||||
|
||||
if (! is_file($inventoryPath) || ! is_file($openApiPath)) {
|
||||
$this->error('Inventory or OpenAPI file is missing. Documentation cannot be trusted by vibes alone.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$rows = json_decode(file_get_contents($inventoryPath) ?: '[]', true) ?: [];
|
||||
$openApi = file_get_contents($openApiPath) ?: '';
|
||||
$deprecations = is_file($deprecationsPath) ? (file_get_contents($deprecationsPath) ?: '') : '';
|
||||
$failures = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$uri = '/'.ltrim((string) ($row['uri'] ?? ''), '/');
|
||||
$status = (string) ($row['canonical_status'] ?? 'unknown');
|
||||
if ($status === 'canonical' && ! str_contains($openApi, $uri)) {
|
||||
$failures[] = $uri.' is canonical but missing from OpenAPI.';
|
||||
}
|
||||
if ($status === 'deprecated' && ! str_contains($deprecations, $uri)) {
|
||||
$failures[] = $uri.' is deprecated but missing from docs/deprecations.md.';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($failures as $failure) {
|
||||
$this->error($failure);
|
||||
}
|
||||
|
||||
return $failures === [] ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
final class GenerateApiRouteInventoryCommand extends Command
|
||||
{
|
||||
protected $signature = 'api:route-inventory {--markdown : Also write docs/api-route-inventory.md}';
|
||||
protected $description = 'Generate API route inventory for controller cleanup and OpenAPI coverage.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$classification = config('api_controller_classification.controllers', []);
|
||||
$rows = collect(RouteFacade::getRoutes())
|
||||
->filter(fn (Route $route) => str_starts_with($route->uri(), 'api/'))
|
||||
->map(function (Route $route) use ($classification): array {
|
||||
$action = $route->getActionName();
|
||||
[$controller, $method] = str_contains($action, '@') ? explode('@', $action, 2) : [$action, '__invoke'];
|
||||
$meta = $classification[$controller] ?? [];
|
||||
|
||||
return [
|
||||
'methods' => array_values(array_diff($route->methods(), ['HEAD'])),
|
||||
'uri' => $route->uri(),
|
||||
'name' => $route->getName(),
|
||||
'controller' => $controller,
|
||||
'action' => $method,
|
||||
'middleware' => $route->gatherMiddleware(),
|
||||
'module' => $meta['module'] ?? 'unknown',
|
||||
'risk' => $meta['risk'] ?? 'unknown',
|
||||
'canonical_status' => $meta['status'] ?? config('api_controller_classification.default_status', 'unknown'),
|
||||
'contract' => $meta['contract'] ?? null,
|
||||
'auth_required' => collect($route->gatherMiddleware())->contains(fn ($m) => str_starts_with((string) $m, 'auth')),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
Storage::disk('local')->put('generated/api-route-inventory.json', json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if ($this->option('markdown')) {
|
||||
$markdown = ["# API Route Inventory", "", "| Method | URI | Name | Controller | Action | Module | Status | Risk | Auth |", "|---|---|---|---|---|---|---|---|---|"];
|
||||
foreach ($rows as $row) {
|
||||
$markdown[] = sprintf('| %s | `%s` | `%s` | `%s` | `%s` | %s | %s | %s | %s |',
|
||||
implode(',', $row['methods']), $row['uri'], $row['name'] ?? '', $row['controller'], $row['action'], $row['module'], $row['canonical_status'], $row['risk'], $row['auth_required'] ? 'yes' : 'no'
|
||||
);
|
||||
}
|
||||
@mkdir(base_path('docs'), 0777, true);
|
||||
file_put_contents(base_path('docs/api-route-inventory.md'), implode(PHP_EOL, $markdown).PHP_EOL);
|
||||
}
|
||||
|
||||
$this->info('API route inventory written. Unknown routes are now visible, the bare minimum for civilization.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
final class LegacyUnsafeRouteAuditCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:legacy-unsafe-route-audit';
|
||||
protected $description = 'Audit known unsafe legacy routes that must be disabled or removed before final rollout.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$items = config('release_readiness.unsafe_legacy_routes', []);
|
||||
$report = [
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'items' => array_map(fn (string $key): array => [
|
||||
'key' => $key,
|
||||
'status' => 'requires_project_mapping',
|
||||
'note' => 'Map this key to actual legacy route names in the real app and disable/remove before production readiness signoff.',
|
||||
], $items),
|
||||
];
|
||||
|
||||
$path = storage_path('app/generated/legacy-unsafe-route-audit.json');
|
||||
File::ensureDirectoryExists(dirname($path));
|
||||
File::put($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
$this->line(json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Release\FeatureFlagRegistry;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
final class ReleaseFeatureFlagsCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:release-feature-flags {--format=json}';
|
||||
protected $description = 'Export modular rollout feature flag map.';
|
||||
|
||||
public function handle(FeatureFlagRegistry $registry): int
|
||||
{
|
||||
$data = ['flags' => $registry->export()];
|
||||
$path = storage_path('app/generated/release-feature-flags.json');
|
||||
File::ensureDirectoryExists(dirname($path));
|
||||
File::put($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$this->info('Feature flag rollout map written to '.$path);
|
||||
$this->line(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Release\MigrationValidationRunner;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
final class ReleaseMigrationValidateCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:release-migration-validate {--module=all}';
|
||||
protected $description = 'Run modular migration validation checks.';
|
||||
|
||||
public function handle(MigrationValidationRunner $runner): int
|
||||
{
|
||||
$results = array_map(fn ($result): array => $result->toArray(), $runner->runAll());
|
||||
$summary = [
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'results' => $results,
|
||||
'status' => collect($results)->every(fn ($result): bool => $result['status'] === 'pass') ? 'pass' : 'fail',
|
||||
];
|
||||
|
||||
$path = storage_path('app/generated/release-validation/validation-summary.json');
|
||||
File::ensureDirectoryExists(dirname($path));
|
||||
File::put($path, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$this->info('Migration validation summary written to '.$path);
|
||||
$this->line(json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $summary['status'] === 'pass' ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Release\ReleaseReadinessGate;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class ReleaseReadinessGateCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:release-readiness-gate {--warning-mode}';
|
||||
protected $description = 'Evaluate modular release readiness artifacts and gates.';
|
||||
|
||||
public function handle(ReleaseReadinessGate $gate): int
|
||||
{
|
||||
$result = $gate->evaluate();
|
||||
$this->line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if ($result['status'] !== 'pass') {
|
||||
$this->warn('Release readiness gate failed. Fix artifacts or run in warning mode only for early rollout rehearsals.');
|
||||
return $this->option('warning-mode') ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Release readiness gate passed. Disturbingly competent.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\Release\ShadowComparisonRunner;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
final class ReleaseShadowCompareCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:release-shadow-compare {--module=all}';
|
||||
protected $description = 'Run shadow comparisons between legacy and modular outputs.';
|
||||
|
||||
public function handle(ShadowComparisonRunner $runner): int
|
||||
{
|
||||
$results = array_map(fn ($result): array => $result->toArray(), $runner->runAll());
|
||||
$summary = [
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'results' => $results,
|
||||
'status' => collect($results)->contains(fn ($result): bool => $result['status'] === 'fail') ? 'fail' : 'warning',
|
||||
];
|
||||
|
||||
$path = storage_path('app/generated/shadow-comparisons/shadow-summary.json');
|
||||
File::ensureDirectoryExists(dirname($path));
|
||||
File::put($path, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$this->warn('Shadow comparisons are scaffolded. Wire legacy comparisons before production cutover.');
|
||||
$this->line(json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class RouteInventoryCheckCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:route-inventory-check {--path=storage/app/generated/api-route-inventory.json}';
|
||||
protected $description = 'Check generated route inventory for governance metadata gaps.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = base_path((string) $this->option('path'));
|
||||
if (! is_file($path)) {
|
||||
$this->error('Route inventory missing. Run php artisan api:route-inventory first.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$rows = json_decode(file_get_contents($path) ?: '[]', true) ?: [];
|
||||
$failures = [];
|
||||
foreach ($rows as $row) {
|
||||
$uri = (string) ($row['uri'] ?? '');
|
||||
$methods = $row['methods'] ?? [];
|
||||
$mutation = count(array_intersect($methods, ['POST', 'PUT', 'PATCH', 'DELETE'])) > 0;
|
||||
if (($row['module'] ?? 'unknown') === 'unknown') {
|
||||
$failures[] = $uri.' is missing module classification.';
|
||||
}
|
||||
if ($mutation && empty($row['auth_required'])) {
|
||||
$failures[] = $uri.' is a mutation route without auth metadata.';
|
||||
}
|
||||
if (str_contains($uri, 'islamic-sunday-school') && ! str_contains(json_encode($row['middleware'] ?? []), 'domain.profile')) {
|
||||
$failures[] = $uri.' is an extension route without domain profile middleware.';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($failures as $failure) {
|
||||
$this->error($failure);
|
||||
}
|
||||
|
||||
return $failures === [] ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user