139 lines
4.4 KiB
PHP
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;
|
|
}
|
|
}
|