Files
SkinbaseNova/app/Console/Commands/ValidateAiBiographyCommand.php
2026-04-18 17:02:56 +02:00

139 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\CreatorAiBiography;
use App\Models\User;
use App\Services\AiBiography\AiBiographyValidator;
use Illuminate\Console\Command;
/**
* Validate stored AI biographies against the current validator rules.
*
* Useful after hardening the validator (e.g. v1.1 upgrade) to identify bios
* generated under older, looser rules that no longer pass.
*
* Flagged biographies have needs_review=true set but are NOT hidden or deleted.
*
* Usage:
* php artisan ai-biography:validate
* php artisan ai-biography:validate {user_id}
* php artisan ai-biography:validate --dry-run
*/
final class ValidateAiBiographyCommand extends Command
{
protected $signature = 'ai-biography:validate
{user_id? : Validate biography for a single creator}
{--dry-run : Report failures without updating records}
{--limit=500 : Maximum number of records to check}';
protected $description = 'Validate stored AI biographies against current validator rules';
public function handle(AiBiographyValidator $validator): int
{
$userId = $this->argument('user_id');
$dryRun = (bool) $this->option('dry-run');
$limit = max(1, (int) $this->option('limit'));
if ($userId !== null) {
return $this->handleSingle((int) $userId, $validator, $dryRun);
}
return $this->handleBatch($validator, $dryRun, $limit);
}
private function handleSingle(int $userId, AiBiographyValidator $validator, bool $dryRun): int
{
$user = User::query()->where('id', $userId)->whereNull('deleted_at')->first();
if ($user === null) {
$this->error("User #{$userId} not found.");
return self::FAILURE;
}
$record = CreatorAiBiography::query()
->where('user_id', $userId)
->where('is_active', true)
->latest()
->first();
if ($record === null) {
$this->line("User #{$userId} ({$user->username}): no active biography.");
return self::SUCCESS;
}
if ($record->is_user_edited) {
$this->line("User #{$userId} ({$user->username}): biography is user-edited — skipping.");
return self::SUCCESS;
}
$errors = $validator->validate(
(string) $record->text,
$record->input_quality_tier ?? 'rich',
);
if ($errors === []) {
$this->info("User #{$userId} ({$user->username}): ✓ valid");
} else {
$this->warn("User #{$userId} ({$user->username}): ✗ " . implode('; ', $errors));
if (! $dryRun) {
$record->update(['needs_review' => true]);
}
}
return self::SUCCESS;
}
private function handleBatch(AiBiographyValidator $validator, bool $dryRun, int $limit): int
{
$this->info('Validating stored AI biographies against current rules...');
$checked = 0;
$passed = 0;
$failed = 0;
$skipped = 0;
CreatorAiBiography::query()
->where('is_active', true)
->whereNotNull('text')
->where('is_user_edited', false)
->whereIn('status', [
CreatorAiBiography::STATUS_GENERATED,
CreatorAiBiography::STATUS_APPROVED,
])
->limit($limit)
->chunkById(100, function ($records) use ($validator, $dryRun, &$checked, &$passed, &$failed, &$skipped): void {
foreach ($records as $record) {
$checked++;
$errors = $validator->validate(
(string) $record->text,
$record->input_quality_tier ?? 'rich',
);
if ($errors === []) {
$passed++;
} else {
$failed++;
$this->warn(" [user:{$record->user_id}] id:{$record->id}" . implode('; ', $errors));
if (! $dryRun) {
$record->update(['needs_review' => true]);
}
}
}
});
$dryTag = $dryRun ? ' [dry-run — no records updated]' : '';
$this->info("Done. checked={$checked} passed={$passed} failed/flagged={$failed}{$dryTag}");
return self::SUCCESS;
}
}