feat: add app and database modules

This commit is contained in:
2026-05-21 16:05:11 +07:00
parent 37b7e783f5
commit fad70d096b
212 changed files with 23901 additions and 0 deletions
+311
View File
@@ -0,0 +1,311 @@
<?php
namespace App\Jobs;
use App\Models\AiHealingLog;
use App\Services\AI\AiService;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
class AiHealerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $logId;
public $faultyFile;
public $lineNum;
public function __construct($logId, $faultyFile = null, $lineNum = null)
{
$this->logId = $logId;
$this->faultyFile = $faultyFile;
$this->lineNum = $lineNum;
}
public function handle(AiService $aiService, SystemConfigService $cfg)
{
$healingLog = AiHealingLog::find($this->logId);
if (!$healingLog) {
return;
}
$healingLog->update(['status' => 'diagnosing']);
// ── Capability gates ────────────────────────────────────────────────
// These honour the toggles on the AI Self-Healing config panel.
$allowCache = (bool) $cfg->get('ai_healing_allow_cache', true);
$allowQueue = (bool) $cfg->get('ai_healing_allow_queue', true);
$allowMaintenance = (bool) $cfg->get('ai_healing_allow_maintenance', false);
$allowDb = (bool) $cfg->get('ai_healing_allow_db', false);
$commandCapabilityMap = [
'cache:clear' => $allowCache,
'view:clear' => $allowCache,
'config:clear' => $allowCache,
'route:clear' => $allowCache,
'optimize:clear' => $allowCache,
'queue:restart' => $allowQueue,
'down' => $allowMaintenance,
'up' => $allowMaintenance,
'migrate' => $allowDb,
'migrate:fresh' => false, // never permitted — destructive
];
$stackTrace = $healingLog->stack_trace;
$faultyFile = $this->faultyFile;
$lineNum = (int) $this->lineNum;
if (!$faultyFile || str_contains($faultyFile, 'vendor/')) {
if (preg_match('/(\/var\/www\/html\/(?!vendor\/)[^\s:]+\.php):(\d+)/', $stackTrace, $matches)) {
$faultyFile = $matches[1];
$lineNum = (int) $matches[2];
}
}
if ($faultyFile && str_contains($faultyFile, 'storage/framework/views')) {
$compiledContent = @file_get_contents($faultyFile);
if ($compiledContent && preg_match('/PATH (.*?) ENDPATH/', $compiledContent, $m)) {
$faultyFile = trim($m[1]);
}
}
$fileContext = '';
if ($faultyFile && file_exists($faultyFile)) {
$lines = file($faultyFile);
$start = max(0, $lineNum - 25);
$end = min(count($lines), $lineNum + 25);
$fileContext = implode('', array_slice($lines, $start, $end - $start));
}
$enabledCommands = array_keys(array_filter($commandCapabilityMap, fn ($v) => $v === true));
$enabledList = $enabledCommands ? implode(', ', $enabledCommands) : '(none — only code edits are permitted)';
$isBlade = $faultyFile && str_ends_with($faultyFile, '.blade.php');
$isView = $isBlade
|| str_contains($healingLog->error_type, 'View')
|| str_contains($healingLog->error_message, 'storage/framework/views');
$prompt = "You are an autonomous Laravel 11 self-healing agent. The user has explicitly authorised you to apply fixes WITHOUT human confirmation when the error is unambiguous. A timestamped .bak backup is written before every code edit, so the user has a rollback path.\n\n";
$prompt .= "EXCEPTION DETAILS:\n";
$prompt .= 'Error Type: ' . $healingLog->error_type . "\n";
$prompt .= 'Error Message: ' . $healingLog->error_message . "\n";
if ($fileContext) {
$prompt .= "The error originated in file: {$faultyFile}\n";
$prompt .= "Here is the code snippet around the error (±25 lines):\n```php\n{$fileContext}\n```\n";
} else {
$prompt .= 'Stack Trace (first 1200 chars): ' . substr($stackTrace, 0, 1200) . "\n\n";
}
if ($isView) {
$prompt .= "\nBLADE-SPECIFIC HINTS:\n";
$prompt .= "- Literal `@directive` in HTML output (e.g. documentation that mentions @can, @if, @foreach) MUST be escaped as `@@directive` so Blade outputs it as text. Example: change `<code>@can('view users')</code>` to `<code>@@can('view users')</code>`.\n";
$prompt .= "- Literal `{{ }}` in HTML must be escaped as `@{{ }}`. Literal `{!! !!}` as `@{!! !!}`.\n";
$prompt .= "- 'unexpected end of file, expecting elseif/else/endif' usually means an unescaped `@can` / `@if` / `@auth` directive in plain text — Blade thinks it must close it. Find the directive that has no matching `@endX` and either escape it (`@@X`) or add the missing `@endX`.\n";
$prompt .= "- Unmatched `@foreach`/`@endforeach`, `@if`/`@endif`, `@can`/`@endcan` pairs cause the same EOF error.\n";
$prompt .= "- When the error's file is in `storage/framework/views/` (compiled cache), the real bug is in the source `.blade.php` shown above.\n\n";
}
$prompt .= "===== DECISION POLICY =====\n";
$prompt .= "Classify this error into one of three tiers and act accordingly:\n\n";
$prompt .= "TIER 1 — AUTO-FIX MANDATORY (confidence ≥ 0.8). You MUST return a code_edit or action_command. Examples:\n";
$prompt .= " • Typo in variable / method / class / route name visible in the snippet\n";
$prompt .= " • Missing or wrong `use` import statement\n";
$prompt .= " • Unescaped Blade directive in plain text (apply @@ escape)\n";
$prompt .= " • Unmatched `@if`/`@endif`, `@foreach`/`@endforeach`, `@can`/`@endcan` (add the missing closer)\n";
$prompt .= " • `Undefined array key` where a safe default is obvious → use `?? default`\n";
$prompt .= " • `Trying to access property of null` → apply `?->` null-safe operator\n";
$prompt .= " • `Class \"X\" not found` after a clear rename / typo → fix the FQCN\n";
$prompt .= " • Stale Blade / route / config cache → return action_command (view:clear, route:clear, optimize:clear)\n";
$prompt .= " • Failed worker after code deploy → return action_command queue:restart (if allowed)\n";
$prompt .= " • Wrong helper signature with obvious right form (Cache::set → Cache::put)\n";
$prompt .= "\nTIER 2 — BEST-EFFORT FIX (confidence 0.50.79). Attempt the most likely fix if you can identify a concrete change. Examples:\n";
$prompt .= " • Type mismatch where a cast solves it (e.g. `(int) \$id`)\n";
$prompt .= " • SQL function not available in this driver — replace with portable PHP equivalent (e.g. Postgres lacks DATE_FORMAT; use Carbon bucketing in PHP)\n";
$prompt .= " • Missing model attribute that has a safe fallback\n";
$prompt .= "\nTIER 3 — DO NOT AUTO-FIX (confidence < 0.5). Set both code_edit and action_command to null, write a precise diagnosis. Examples:\n";
$prompt .= " • Business logic / data semantics (you cannot infer intent)\n";
$prompt .= " • Multi-file refactor (you can only edit one file per fix)\n";
$prompt .= " • Race conditions / concurrency issues\n";
$prompt .= " • Anything in vendor/ (never touch)\n";
$prompt .= " • The fix would require deleting / re-creating significant code blocks\n";
$prompt .= " • The exception is itself coming from the healing pipeline (avoid loops)\n";
$prompt .= "\nGENERAL RULES:\n";
$prompt .= "- Bias toward decisive ACTION for Tier 1 and Tier 2. The user has authorised auto-fix and there is a backup.\n";
$prompt .= "- For code_edit, `target_content` MUST be an EXACT substring that exists in the file (preserve all whitespace and indentation).\n";
$prompt .= "- `replacement_content` must be the minimal change that fixes the bug. Do NOT refactor surrounding code.\n";
$prompt .= "- Only use commands from the allow-list: {$enabledList}\n";
$prompt .= "- If you return code_edit, action_command MUST be null (apply exactly one remedy).\n";
$prompt .= "- Diagnosis should be ≤ 2 sentences, plain language, explaining what was wrong AND what you changed.\n";
$prompt .= "\nRESPOND ONLY WITH VALID JSON (no markdown fences, no prose around it):\n";
$prompt .= "{\n";
$prompt .= " \"confidence\": 0.0-1.0,\n";
$prompt .= " \"tier\": 1|2|3,\n";
$prompt .= " \"diagnosis\": \"What was wrong + what you changed (or why you can't fix it).\",\n";
$prompt .= " \"action_command\": \"cache:clear\" | null,\n";
$prompt .= " \"code_edit\": {\"file_path\":\"...\",\"target_content\":\"...\",\"replacement_content\":\"...\"} | null\n";
$prompt .= "}";
try {
$aiResult = $aiService->provider()->generate($prompt);
if (!$aiResult['success']) {
throw new \Exception($aiResult['error'] ?? 'Unknown AI Provider error');
}
$response = $aiResult['response'];
$jsonStr = trim(preg_replace('/```json|```/', '', $response));
$result = json_decode($jsonStr, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$healingLog->update([
'status' => 'failed',
'ai_diagnosis' => 'AI response was not valid JSON: ' . $response,
'action_taken' => 'Invalid JSON from provider',
]);
return;
}
$diagnosis = $result['diagnosis'] ?? 'AI could not provide a diagnosis.';
$action = $result['action_command'] ?? null;
$codeEdit = $result['code_edit'] ?? null;
$confidence = isset($result['confidence']) ? (float) $result['confidence'] : null;
$tier = isset($result['tier']) ? (int) $result['tier'] : null;
if ($confidence !== null || $tier !== null) {
$tag = '';
if ($confidence !== null) {
$tag .= '[conf=' . number_format($confidence, 2) . ']';
}
if ($tier !== null) {
$tag .= '[tier=' . $tier . ']';
}
$diagnosis = trim($tag . ' ' . $diagnosis);
}
// ── Try code edit first ─────────────────────────────────────────
if (is_array($codeEdit) && isset($codeEdit['file_path'], $codeEdit['target_content'], $codeEdit['replacement_content'])) {
$applied = $this->applyCodeEdit($healingLog, $codeEdit, $diagnosis);
if ($applied !== null) {
return; // resolved or hard-failed
}
// If $applied is null, the edit couldn't be applied (e.g., file missing) — fall through to command attempt.
}
// ── Try Artisan command ────────────────────────────────────────
if (is_string($action) && $action !== '') {
if (!array_key_exists($action, $commandCapabilityMap)) {
$healingLog->update([
'status' => 'failed',
'action_taken' => "Refused: command '{$action}' is not in security policy",
'ai_diagnosis' => $diagnosis,
]);
return;
}
if (!$commandCapabilityMap[$action]) {
$healingLog->update([
'status' => 'failed',
'action_taken' => "Refused: command '{$action}' is currently disabled by capability toggle",
'ai_diagnosis' => $diagnosis,
]);
return;
}
try {
Artisan::call($action);
$healingLog->update([
'status' => 'resolved',
'action_taken' => 'Executed: ' . $action,
'ai_diagnosis' => $diagnosis,
]);
Log::info("AI Healer executed Artisan command: {$action}");
} catch (\Throwable $e) {
$healingLog->update([
'status' => 'failed',
'action_taken' => "Artisan '{$action}' threw: " . $e->getMessage(),
'ai_diagnosis' => $diagnosis,
]);
}
return;
}
// ── Nothing actionable ─────────────────────────────────────────
$healingLog->update([
'status' => 'failed',
'ai_diagnosis' => $diagnosis,
'action_taken' => 'No fix applied (AI provided no actionable remedy)',
]);
} catch (\Throwable $e) {
Log::error('AI Healer failed: ' . $e->getMessage());
$healingLog->update([
'status' => 'failed',
'ai_diagnosis' => 'Internal Error: ' . $e->getMessage(),
'action_taken' => 'Healer pipeline crashed',
]);
}
}
/**
* Apply a code edit suggested by the AI.
* Returns true on resolved, false on hard fail, null if it should fall through to command attempt.
*/
private function applyCodeEdit(AiHealingLog $log, array $edit, string $diagnosis): ?bool
{
$filePath = $edit['file_path'];
if (!file_exists($filePath) || !is_writable($filePath)) {
// Fall through — let the action_command branch try instead.
return null;
}
$content = file_get_contents($filePath);
if (!str_contains($content, $edit['target_content'])) {
$log->update([
'status' => 'failed',
'action_taken' => 'Code edit failed: target snippet not found in ' . basename($filePath),
'ai_diagnosis' => $diagnosis,
]);
return false;
}
$newContent = str_replace($edit['target_content'], $edit['replacement_content'], $content);
if (trim($newContent) === '') {
$log->update([
'status' => 'failed',
'action_taken' => 'Code edit aborted: replacement would empty the file',
'ai_diagnosis' => $diagnosis,
]);
return false;
}
// Write a .bak before editing — gives the user a rollback path.
$backupPath = $filePath . '.bak.' . date('YmdHis');
@file_put_contents($backupPath, $content);
file_put_contents($filePath, $newContent);
Artisan::call('optimize:clear');
$log->update([
'status' => 'resolved',
'action_taken' => 'CODE_EDIT|' . $filePath . '|' . $backupPath,
'ai_diagnosis' => $diagnosis,
'original_code' => $content,
'fixed_code' => $newContent,
]);
return true;
}
}