` system.
+
+## Files
+
+- **group.blade.php** -- Tab group container. Wraps tab triggers and manages selected state.
+- **panel.blade.php** -- Tab content panel. Shows/hides based on which tab is selected.
+
+## Usage
+
+```blade
+
+ General
+ Advanced
+
+General content
+Advanced content
+```
diff --git a/src/Core/Front/Components/View/Blade/table/CLAUDE.md b/src/Core/Front/Components/View/Blade/table/CLAUDE.md
new file mode 100644
index 0000000..2f475ac
--- /dev/null
+++ b/src/Core/Front/Components/View/Blade/table/CLAUDE.md
@@ -0,0 +1,28 @@
+# Blade/table
+
+Table sub-components for data tables.
+
+## Files
+
+- **cell.blade.php** -- Individual table cell (`| `)
+- **column.blade.php** -- Column header (` | `) with optional sorting
+- **columns.blade.php** -- Column header row container (``)
+- **row.blade.php** -- Table row (` `)
+- **rows.blade.php** -- Table body container (` | `)
+
+## Usage
+
+```blade
+
+
+ Name
+ Status
+
+
+
+ Item 1
+ Active
+
+
+
+```
diff --git a/src/Core/Front/Components/View/Blade/web/CLAUDE.md b/src/Core/Front/Components/View/Blade/web/CLAUDE.md
new file mode 100644
index 0000000..26715d9
--- /dev/null
+++ b/src/Core/Front/Components/View/Blade/web/CLAUDE.md
@@ -0,0 +1,11 @@
+# Blade/web
+
+Public-facing page templates for workspace websites. Registered under the `web::` namespace.
+
+## Files
+
+- **home.blade.php** -- Workspace homepage template
+- **page.blade.php** -- Generic content page template
+- **waitlist.blade.php** -- Pre-launch waitlist/signup page template
+
+These are rendered for workspace domains resolved by `FindDomainRecord` middleware. Used via `web::home`, `web::page`, `web::waitlist` view references.
diff --git a/src/Core/Front/Services/CLAUDE.md b/src/Core/Front/Services/CLAUDE.md
new file mode 100644
index 0000000..2b987d5
--- /dev/null
+++ b/src/Core/Front/Services/CLAUDE.md
@@ -0,0 +1,18 @@
+# Front/Services
+
+Shared services for the Front module.
+
+## Files
+
+- **DeviceDetectionService.php** -- User-Agent parser for device type, OS, browser, in-app browser, and bot detection. Detects 14 social platform in-app browsers (Instagram, Facebook, TikTok, Twitter, LinkedIn, Snapchat, Threads, Pinterest, Reddit, WeChat, LINE, Telegram, Discord, WhatsApp) plus generic WebView. Identifies strict content platforms (Meta, TikTok, Twitter, Snapchat, LinkedIn) and Meta-owned platforms. Platform-specific methods: `isInstagram()`, `isFacebook()`, `isTikTok()`, etc. Full parse returns `{device_type, os_name, browser_name, in_app_browser, is_in_app}`.
+
+## Usage
+
+```php
+$service = new DeviceDetectionService();
+$info = $service->parse($request->userAgent());
+
+if ($service->isStrictContentPlatform($ua)) {
+ // Hide restricted content for platform compliance
+}
+```
diff --git a/src/Core/Front/Stdio/CLAUDE.md b/src/Core/Front/Stdio/CLAUDE.md
new file mode 100644
index 0000000..dbe3975
--- /dev/null
+++ b/src/Core/Front/Stdio/CLAUDE.md
@@ -0,0 +1,7 @@
+# Front/Stdio
+
+Stdio frontage for terminal/pipe-based I/O.
+
+## Files
+
+- **Boot.php** -- ServiceProvider for stdin/stdout transport. Handles non-HTTP contexts: Artisan commands, MCP stdio transport (agents connecting via pipes), and interactive CLI tools. Currently a placeholder with empty command registration -- core CLI commands will be registered here. No HTTP middleware -- this is a different transport entirely.
diff --git a/src/Core/Front/Tests/Unit/CLAUDE.md b/src/Core/Front/Tests/Unit/CLAUDE.md
new file mode 100644
index 0000000..acf6a7e
--- /dev/null
+++ b/src/Core/Front/Tests/Unit/CLAUDE.md
@@ -0,0 +1,13 @@
+# Front/Tests/Unit
+
+Unit tests for the Front module.
+
+## Files
+
+- **DeviceDetectionServiceTest.php** -- Tests for `DeviceDetectionService`. Covers in-app browser detection (Instagram, Facebook, TikTok, Twitter, LinkedIn, Snapchat, Threads, Pinterest, Reddit, WeChat, WhatsApp), standard browser exclusion, platform-specific methods, strict content platform identification, Meta platform detection, display names, device type/OS/browser detection, bot detection, and full parse output. Uses PHPUnit DataProvider for UA string coverage.
+
+## Running
+
+```bash
+composer test -- --filter=DeviceDetectionServiceTest
+```
diff --git a/src/Core/Front/Web/Blade/components/CLAUDE.md b/src/Core/Front/Web/Blade/components/CLAUDE.md
new file mode 100644
index 0000000..988f95c
--- /dev/null
+++ b/src/Core/Front/Web/Blade/components/CLAUDE.md
@@ -0,0 +1,21 @@
+# Front/Web/Blade/components
+
+Anonymous Blade components for the public web frontage. Used via `` tag syntax.
+
+## Components
+
+- **nav-item.blade.php** -- Navigation list item with icon and active state highlighting.
+ - Props: `href` (required), `icon` (string|null), `active` (bool)
+ - Uses `` for icon rendering.
+
+- **page.blade.php** -- Simple page wrapper for public web pages with optional title and description.
+ - Props: `title` (string|null), `description` (string|null)
+ - Provides max-width container with responsive padding.
+
+## Usage
+
+```blade
+
+ Content here
+
+```
diff --git a/src/Core/Front/Web/Blade/layouts/CLAUDE.md b/src/Core/Front/Web/Blade/layouts/CLAUDE.md
new file mode 100644
index 0000000..147fac6
--- /dev/null
+++ b/src/Core/Front/Web/Blade/layouts/CLAUDE.md
@@ -0,0 +1,10 @@
+# Front/Web/Blade/layouts
+
+Layout templates for public web pages.
+
+## Files
+
+- **app.blade.php** -- Base HTML shell for public web pages. Includes dark mode support (cookie-based), CSRF meta tag, Vite assets (app.css + app.js), Flux appearance/scripts, and font loading via `layouts::partials.fonts`.
+ - Props: `title` (string, defaults to `core.app.name` config)
+ - Slots: `$head` (extra head content), `$scripts` (extra scripts before ``)
+ - Prevents white flash with inline critical CSS for dark mode.
diff --git a/src/Core/Front/Web/Boot.php b/src/Core/Front/Web/Boot.php
index 9de1177..f098906 100644
--- a/src/Core/Front/Web/Boot.php
+++ b/src/Core/Front/Web/Boot.php
@@ -15,9 +15,15 @@
use Core\Front\Web\Middleware\ResilientSession;
use Core\Headers\SecurityHeaders;
use Core\LifecycleEventProvider;
+use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
+use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Configuration\Middleware;
+use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
+use Illuminate\Routing\Middleware\SubstituteBindings;
+use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
+use Illuminate\View\Middleware\ShareErrorsFromSession;
/**
* Web frontage - public marketing stage.
@@ -33,13 +39,13 @@ class Boot extends ServiceProvider
public static function middleware(Middleware $middleware): void
{
$middleware->group('web', [
- \Illuminate\Cookie\Middleware\EncryptCookies::class,
- \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
- \Illuminate\Session\Middleware\StartSession::class,
+ EncryptCookies::class,
+ AddQueuedCookiesToResponse::class,
+ StartSession::class,
ResilientSession::class,
- \Illuminate\View\Middleware\ShareErrorsFromSession::class,
- \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
- \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ ShareErrorsFromSession::class,
+ ValidateCsrfToken::class,
+ SubstituteBindings::class,
SecurityHeaders::class,
FindDomainRecord::class,
]);
diff --git a/src/Core/Front/Web/CLAUDE.md b/src/Core/Front/Web/CLAUDE.md
new file mode 100644
index 0000000..bfb9267
--- /dev/null
+++ b/src/Core/Front/Web/CLAUDE.md
@@ -0,0 +1,21 @@
+# Front/Web
+
+Public-facing web frontage. Service provider that configures the `web` middleware group and registers `` Blade tag syntax.
+
+## Files
+
+- **Boot.php** -- ServiceProvider that configures the `web` middleware stack (cookies, session, CSRF, security headers, domain resolution), registers the `web::` Blade namespace, fires `WebRoutesRegistering` lifecycle event, and aliases `livewire` as `admin` for Flux compatibility.
+- **WebTagCompiler.php** -- Blade precompiler enabling `` tag syntax (like ``). Compiles to anonymous components in the `web::` namespace.
+
+## Middleware Stack
+
+The `web` group includes: EncryptCookies, AddQueuedCookiesToResponse, StartSession, ResilientSession, ShareErrorsFromSession, ValidateCsrfToken, SubstituteBindings, SecurityHeaders, FindDomainRecord.
+
+## Tag Syntax
+
+```blade
+Content here
+About
+```
+
+Resolves to anonymous components in `Web/Blade/components/` and `Components/View/Blade/web/`.
diff --git a/src/Core/Front/Web/Middleware/CLAUDE.md b/src/Core/Front/Web/Middleware/CLAUDE.md
new file mode 100644
index 0000000..8a0141f
--- /dev/null
+++ b/src/Core/Front/Web/Middleware/CLAUDE.md
@@ -0,0 +1,9 @@
+# Front/Web/Middleware
+
+HTTP middleware for public web requests.
+
+## Files
+
+- **FindDomainRecord.php** -- Resolves the current workspace from the incoming domain. Sets `workspace_model` and `workspace` attributes on the request. Core domains (base domain, www, localhost) pass through. Checks custom domains first, then subdomain slugs. Requires `Core\Tenant` module.
+- **RedirectIfAuthenticated.php** -- Redirects logged-in users away from guest-only pages (e.g., login). Redirects to `/` on the current domain instead of a global dashboard route.
+- **ResilientSession.php** -- Catches session corruption (decryption errors, DB failures) and recovers gracefully by clearing cookies and retrying. Prevents 503 errors from APP_KEY changes or session table issues. Returns JSON for AJAX requests.
diff --git a/src/Core/Front/Web/Middleware/FindDomainRecord.php b/src/Core/Front/Web/Middleware/FindDomainRecord.php
index b634723..6dfae3f 100644
--- a/src/Core/Front/Web/Middleware/FindDomainRecord.php
+++ b/src/Core/Front/Web/Middleware/FindDomainRecord.php
@@ -12,6 +12,7 @@
namespace Core\Front\Web\Middleware;
use Closure;
+use Core\Tenant\Models\Workspace;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -84,12 +85,12 @@ protected function isCoreDomain(string $host): bool
protected function resolveWorkspaceFromDomain(string $host): ?object
{
// Check if Tenant module is installed
- if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
+ if (! class_exists(Workspace::class)) {
return null;
}
// Check for custom domain first
- $workspace = \Core\Tenant\Models\Workspace::where('domain', $host)->first();
+ $workspace = Workspace::where('domain', $host)->first();
if ($workspace) {
return $workspace;
}
@@ -104,7 +105,7 @@ protected function resolveWorkspaceFromDomain(string $host): ?object
if (count($parts) >= 1) {
$workspaceSlug = $parts[0];
- return \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)
+ return Workspace::where('slug', $workspaceSlug)
->where('is_active', true)
->first();
}
diff --git a/src/Core/Front/Web/Middleware/ResilientSession.php b/src/Core/Front/Web/Middleware/ResilientSession.php
index 04c562a..6493bab 100644
--- a/src/Core/Front/Web/Middleware/ResilientSession.php
+++ b/src/Core/Front/Web/Middleware/ResilientSession.php
@@ -13,6 +13,7 @@
use Closure;
use Illuminate\Contracts\Encryption\DecryptException;
+use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
@@ -43,7 +44,7 @@ public function handle(Request $request, Closure $next): Response
return $next($request);
} catch (DecryptException $e) {
return $this->handleSessionError($request, $e, 'decrypt');
- } catch (\Illuminate\Database\QueryException $e) {
+ } catch (QueryException $e) {
// Only catch session-related query exceptions
if ($this->isSessionError($e)) {
return $this->handleSessionError($request, $e, 'database');
diff --git a/src/Core/Headers/CLAUDE.md b/src/Core/Headers/CLAUDE.md
new file mode 100644
index 0000000..6f79cab
--- /dev/null
+++ b/src/Core/Headers/CLAUDE.md
@@ -0,0 +1,72 @@
+# Headers
+
+HTTP security headers, CSP nonce generation, device detection, and GeoIP lookup.
+
+## What It Does
+
+Four concerns bundled as a single module:
+
+1. **SecurityHeaders** middleware -- Adds HSTS, CSP, Permissions-Policy, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy to all responses
+2. **CspNonceService** -- Per-request cryptographic nonce for inline scripts/styles, integrated with Vite
+3. **DetectDevice** -- User-Agent parsing for device type, OS, browser, in-app browser detection (14 platforms)
+4. **DetectLocation** -- GeoIP from CloudFlare headers, custom headers, or MaxMind database
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `Boot` | ServiceProvider: config, singletons, Blade directives (`@cspnonce`, `@cspnoncevalue`), `csp_nonce()` helper, Livewire `header-configuration-manager` component |
+| `SecurityHeaders` | Middleware. CSP built from: base directives -> env overrides -> nonces -> CDN sources -> external services -> dev WebSocket -> report URI. Supports report-only mode |
+| `CspNonceService` | Generates one nonce per request (128-bit, base64). Auto-registers with `Vite::useCspNonce()`. Methods: `getNonce()`, `getCspNonceDirective()`, `getNonceAttribute()` |
+| `DetectDevice` | `parse($ua)` returns `{device_type, os_name, browser_name, in_app_browser, is_in_app}`. Helpers: `isBot()`, `isInstagram()`, `isFacebook()`, `isTikTok()`, `isMetaPlatform()`, `isStrictContentPlatform()` |
+| `DetectLocation` | `lookup($ip, $request)` returns `{country_code, region, city}`. Checks CF headers first, then MaxMind DB. Cached 24h. Skips private IPs |
+| `HeaderConfigurationManager` | Livewire component for admin-panel header config editing |
+
+## Testing Support
+
+| Class | Purpose |
+|-------|---------|
+| `HeaderAssertions` | Test assertion helpers for security headers |
+| `SecurityHeaderTester` | Pre-built test scenarios |
+
+## CSP Configuration
+
+Config in `config/headers.php` (published via Boot):
+
+```php
+'csp' => [
+ 'enabled' => env('SECURITY_CSP_ENABLED', true),
+ 'report_only' => env('SECURITY_CSP_REPORT_ONLY', false),
+ 'nonce_enabled' => env('SECURITY_CSP_NONCE_ENABLED', true),
+ 'directives' => [...],
+ 'environment' => [
+ 'local' => ['script-src' => ["'unsafe-inline'", "'unsafe-eval'"]],
+ ],
+ 'nonce_skip_environments' => ['local', 'development'],
+ 'external' => [
+ 'jsdelivr' => ['enabled' => env('SECURITY_CSP_JSDELIVR', false)],
+ 'google_analytics' => [...],
+ ],
+],
+```
+
+## Blade Usage
+
+```blade
+
+
+
+```
+
+## In-App Browser Detection
+
+Detects 14 platforms: Instagram, Facebook, TikTok, Twitter/X, LinkedIn, Snapchat, Pinterest, Reddit, Threads, WeChat, LINE, Telegram, Discord, WhatsApp, plus generic WebView fallback.
+
+Key distinction: `isStrictContentPlatform()` returns true for platforms that enforce content policies (useful for adult content warnings).
+
+## Integration
+
+- `SecurityHeaders` middleware is included in both `web` and `admin` middleware groups (configured in `Front/Web/Boot` and `Front/Admin/Boot`)
+- Nonces auto-removed in `nonce_skip_environments` to not break HMR/dev tools
+- HSTS only added in production
+- Dev environments get WebSocket sources for Vite HMR (`localhost:8080`)
diff --git a/src/Core/Headers/Livewire/CLAUDE.md b/src/Core/Headers/Livewire/CLAUDE.md
new file mode 100644
index 0000000..899f20c
--- /dev/null
+++ b/src/Core/Headers/Livewire/CLAUDE.md
@@ -0,0 +1,7 @@
+# Headers/Livewire/ — Header Configuration UI
+
+## Components
+
+| Component | Purpose |
+|-----------|---------|
+| `HeaderConfigurationManager` | Livewire component for managing security header configuration in the admin panel. Provides UI for configuring CSP directives, HSTS settings, Permissions Policy features, and other security headers. |
diff --git a/src/Core/Headers/Livewire/HeaderConfigurationManager.php b/src/Core/Headers/Livewire/HeaderConfigurationManager.php
index 9af1c9b..5f493f7 100644
--- a/src/Core/Headers/Livewire/HeaderConfigurationManager.php
+++ b/src/Core/Headers/Livewire/HeaderConfigurationManager.php
@@ -11,6 +11,7 @@
namespace Core\Headers\Livewire;
+use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Config;
use Livewire\Component;
@@ -451,7 +452,7 @@ public function clearMessages(): void
/**
* Render the component.
*/
- public function render(): \Illuminate\Contracts\View\View
+ public function render(): View
{
return view('core::headers.livewire.header-configuration-manager');
}
diff --git a/src/Core/Headers/Testing/CLAUDE.md b/src/Core/Headers/Testing/CLAUDE.md
new file mode 100644
index 0000000..ef8889c
--- /dev/null
+++ b/src/Core/Headers/Testing/CLAUDE.md
@@ -0,0 +1,10 @@
+# Headers/Testing/ — Security Header Test Utilities
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `HeaderAssertions` | Trait with assertion methods for testing security headers in HTTP responses. Provides `assertHasSecurityHeaders()`, `assertCspContains()`, etc. |
+| `SecurityHeaderTester` | Standalone static helper for validating security headers. Can be used without traits for flexible testing scenarios. |
+
+Used by feature tests (`SecurityHeadersTest`) to verify CSP, HSTS, X-Frame-Options, and other security headers.
diff --git a/src/Core/Headers/Views/livewire/CLAUDE.md b/src/Core/Headers/Views/livewire/CLAUDE.md
new file mode 100644
index 0000000..f96d298
--- /dev/null
+++ b/src/Core/Headers/Views/livewire/CLAUDE.md
@@ -0,0 +1,7 @@
+# Headers/Views/livewire/ — Header Configuration Blade Template
+
+## Templates
+
+| File | Purpose |
+|------|---------|
+| `header-configuration-manager.blade.php` | Blade template for the `HeaderConfigurationManager` Livewire component. Renders the security header configuration form with CSP, HSTS, and Permissions Policy controls. |
diff --git a/src/Core/Helpers/CLAUDE.md b/src/Core/Helpers/CLAUDE.md
new file mode 100644
index 0000000..0242619
--- /dev/null
+++ b/src/Core/Helpers/CLAUDE.md
@@ -0,0 +1,39 @@
+# Core\Helpers
+
+Shared utility classes registered as singletons. The `Boot` service provider also registers backward-compat aliases from the old `App\Support\*` namespace.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `RecoveryCode` | Generates 2FA recovery codes (`XXXXX-XXXXX` format) |
+| `LoginRateLimiter` | Brute-force protection: 5 attempts / 60s per email+IP |
+| `RateLimit` | Generic sliding-window rate limiter (cache-backed) |
+| `PrivacyHelper` | GDPR IP anonymisation (truncation + daily-rotating SHA256 hashes) |
+| `HadesEncrypt` | Hybrid AES-256-GCM + RSA encryption of exceptions for error pages |
+| `File` | Base64-to-UploadedFile conversion, remote URL fetching, Content-Disposition parsing |
+| `Cdn` | CDN asset URL generation with optional `cdn.{domain}` subdomain and file-hash cache busting |
+| `UtmHelper` | UTM parameter extraction from Request, array, or URL string |
+| `TimezoneList` | Timezone list generator with GMT offset formatting, grouped by continent |
+| `HorizonStatus` | Laravel Horizon supervisor status check (inactive/paused/active) |
+| `SystemLogs` | Reads `storage/logs/*.log` files, caps at 3 MB per file |
+| `CommandResult` | Value object for remote command output (exitCode, output, error) |
+| `ServiceCollection` | Typed collection of service provider classes with group/filter/metadata methods |
+| `Log` | Social module logging facade, routes to configurable `social.log_channel` |
+
+## Sub-directory
+
+- `Rules/HexRule` -- Validation rule for hex colour codes (#fff or #ffffff). `forceFull` option rejects 3-digit codes.
+
+## Patterns
+
+- All classes are stateless or cache-backed -- no database models.
+- `PrivacyHelper` has two anonymisation levels: standard (last octet) and strong (last 2 octets). IPv6 is fully handled.
+- `HadesEncrypt` uses a hardcoded RSA public key (safe to commit) with env-var override. The HTML comment includes a whimsical "Hades" explanation for curious visitors.
+- `Boot::registerBackwardCompatAliases()` uses `class_alias` to map old `App\Support\*` names -- remove these once the migration is complete.
+
+## Integration
+
+- `PrivacyHelper` is used by Analytics Center and BioHost for consistent IP handling.
+- `RateLimit` cache keys are prefixed `social:` -- consider making this configurable.
+- `Cdn` reads from `config('cdn.enabled')` and `config('core.cdn.subdomain')`.
diff --git a/src/Core/Helpers/Rules/CLAUDE.md b/src/Core/Helpers/Rules/CLAUDE.md
new file mode 100644
index 0000000..7a897c6
--- /dev/null
+++ b/src/Core/Helpers/Rules/CLAUDE.md
@@ -0,0 +1,13 @@
+# Helpers/Rules/ — Custom Validation Rules
+
+## Rules
+
+| Rule | Purpose |
+|------|---------|
+| `HexRule` | Validates hexadecimal colour codes. Supports 3-digit (`#fff`) and 6-digit (`#ffffff`) formats. Hash symbol required. Constructor option `forceFull: true` rejects 3-digit codes. |
+
+Usage:
+```php
+'colour' => ['required', new HexRule()]
+'colour' => ['required', new HexRule(forceFull: true)]
+```
diff --git a/src/Core/Input/CLAUDE.md b/src/Core/Input/CLAUDE.md
new file mode 100644
index 0000000..46e09d2
--- /dev/null
+++ b/src/Core/Input/CLAUDE.md
@@ -0,0 +1,51 @@
+# Core\Input
+
+Pre-boot input sanitisation. Strips dangerous control characters from `$_GET` and `$_POST` before Laravel even creates the Request object.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `Input` | Static `capture()` method -- sanitises superglobals then delegates to `Request::capture()` |
+| `Sanitiser` | Configurable filter pipeline: Unicode NFC normalisation, control char stripping, HTML filtering, presets, max length, transformation hooks |
+
+## Sanitiser Pipeline
+
+Execution order per string value:
+
+1. Before hooks (global, then field-specific)
+2. Unicode NFC normalisation (via `intl` extension)
+3. Control character stripping (`FILTER_UNSAFE_RAW` + `FILTER_FLAG_STRIP_LOW`)
+4. HTML tag filtering (strip_tags with allowed tags)
+5. Preset application (email, url, phone, alpha, alphanumeric, numeric, slug)
+6. Additional schema-defined `filter_var` filters
+7. Max length enforcement (`mb_substr`)
+8. After hooks (global, then field-specific)
+9. Audit logging (if enabled and value changed)
+
+## Public API
+
+```php
+// Immutable builder pattern (returns cloned instance)
+$s = (new Sanitiser)
+ ->richText() // allow safe HTML tags
+ ->maxLength(1000) // truncate to 1000 chars
+ ->email('email_field') // apply email preset to specific field
+ ->slug('url_slug') // apply slug preset
+ ->beforeFilter(fn($v, $f) => trim($v))
+ ->transformField('username', fn($v) => strtolower($v));
+
+$clean = $s->filter(['email_field' => $raw, 'url_slug' => $raw2]);
+```
+
+## Conventions
+
+- **Sanitiser sanitises, Laravel validates.** This is explicitly called out in the class docblock.
+- Immutable: all `with*` / fluent methods return `clone $this`.
+- Presets are static and extensible via `Sanitiser::registerPreset()`.
+- The `*` wildcard key in schema applies to all fields as a default.
+- Field-specific schema merges over global (`*`) schema.
+
+## Tests
+
+Pest tests at `Tests/Unit/InputFilteringTest.php` cover: clean passthrough, control char stripping, full Unicode preservation (CJK, Arabic, Russian, emojis), and edge cases.
diff --git a/src/Core/Input/Tests/Unit/CLAUDE.md b/src/Core/Input/Tests/Unit/CLAUDE.md
new file mode 100644
index 0000000..cfadd77
--- /dev/null
+++ b/src/Core/Input/Tests/Unit/CLAUDE.md
@@ -0,0 +1,9 @@
+# Input/Tests/Unit/ — Input Filtering Tests
+
+## Test Files
+
+| File | Purpose |
+|------|---------|
+| `InputFilteringTest.php` | Pest unit tests for the `Sanitiser` class. Covers clean input passthrough (ASCII, punctuation, UTF-8), XSS prevention, SQL injection stripping, null byte removal, and edge cases. |
+
+Tests the WAF layer that `Core\Init` uses to sanitise `$_GET` and `$_POST` before Laravel processes the request.
diff --git a/src/Core/Lang/CLAUDE.md b/src/Core/Lang/CLAUDE.md
new file mode 100644
index 0000000..2997e7f
--- /dev/null
+++ b/src/Core/Lang/CLAUDE.md
@@ -0,0 +1,55 @@
+# Core\Lang
+
+Internationalisation subsystem with ICU message formatting, translation memory with fuzzy matching, TMX import/export, and translation coverage analysis.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `LangServiceProvider` | Auto-discovered provider: registers ICU formatter, TM, coverage, fallback chain, missing key validation |
+| `IcuMessageFormatter` | ICU MessageFormat support (plurals, select, number/date formatting) with intl fallback |
+| `Boot` | Empty marker class -- all work done in LangServiceProvider |
+
+### TranslationMemory/
+
+| Class | Purpose |
+|-------|---------|
+| `TranslationMemory` | Facade service: store, get, suggest (fuzzy), import/export TMX |
+| `FuzzyMatcher` | Multi-algorithm similarity: Levenshtein + token (Jaccard) + n-gram (Dice coefficient), configurable weights |
+| `TranslationMemoryEntry` | Immutable value object with quality scores (0.0-1.0), usage counts, metadata |
+| `JsonTranslationMemoryRepository` | File-backed repo: JSON files per locale pair, in-memory cache + dirty tracking |
+| `TranslationMemoryRepository` (contract) | Interface for storage backends (JSON, database) |
+| `TmxImporter` / `TmxExporter` | TMX 1.4b format import/export with locale normalisation and metadata preservation |
+
+### Coverage/
+
+| Class | Purpose |
+|-------|---------|
+| `TranslationCoverage` | Scans PHP/Blade/JS/Vue for translation keys, compares against lang files |
+| `TranslationCoverageReport` | Report object with missing/unused keys, per-locale stats, text/JSON output |
+
+### Console Commands
+
+- `lang:coverage` -- Find missing and unused translation keys
+- `lang:tm` -- Translation memory management (not fully read but registered)
+
+## Architecture
+
+- **Fallback chain**: `en_GB` -> `en` -> configured fallback. Built via `determineLocalesUsing()`.
+- **Missing key validation**: Only in local/dev/testing. Logs at configurable level, triggers `trigger_deprecation` in local.
+- **ICU formatter**: Caches compiled `MessageFormatter` instances (max 100, LRU eviction). Falls back to simple `{name}` placeholder replacement when intl is unavailable.
+- **Fuzzy matching**: Combined algorithm weights: Levenshtein 0.25, token 0.50, n-gram 0.25. Confidence = similarity * 0.7 + quality * 0.3.
+- **Translation Memory IDs**: Generated via `xxh128` hash of `sourceLocale:targetLocale:source`.
+
+## Configuration
+
+All under `config('core.lang.*')`:
+- `fallback_chain`, `validate_keys`, `log_missing_keys`, `missing_key_log_level`
+- `icu_enabled`
+- `translation_memory.enabled`, `translation_memory.driver` (json/database), `translation_memory.fuzzy.*`
+
+## Integration
+
+- Translations loaded under the `core` namespace: `__('core::core.brand.name')`
+- Override by publishing to `resources/lang/vendor/core/`
+- Translation files live in `en_GB/core.php` within this directory
diff --git a/src/Core/Lang/Console/Commands/CLAUDE.md b/src/Core/Lang/Console/Commands/CLAUDE.md
new file mode 100644
index 0000000..936503e
--- /dev/null
+++ b/src/Core/Lang/Console/Commands/CLAUDE.md
@@ -0,0 +1,10 @@
+# Lang/Console/Commands/ — Translation CLI Commands
+
+## Commands
+
+| Command | Purpose |
+|---------|---------|
+| `TranslationCoverageCommand` | Reports translation coverage across the codebase. Scans code for translation key usage and compares against translation files to identify missing and unused keys. |
+| `TranslationMemoryCommand` | CLI interface for managing translation memory — view statistics, browse entries, import/export TMX files, fuzzy match queries. |
+
+Part of the Lang subsystem's developer tooling.
diff --git a/src/Core/Lang/Console/Commands/TranslationCoverageCommand.php b/src/Core/Lang/Console/Commands/TranslationCoverageCommand.php
index 1cc95ed..ce20d14 100644
--- a/src/Core/Lang/Console/Commands/TranslationCoverageCommand.php
+++ b/src/Core/Lang/Console/Commands/TranslationCoverageCommand.php
@@ -12,7 +12,10 @@
namespace Core\Lang\Console\Commands;
use Core\Lang\Coverage\TranslationCoverage;
+use Core\Lang\Coverage\TranslationCoverageReport;
use Illuminate\Console\Command;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
/**
* Report translation coverage across the codebase.
@@ -164,7 +167,7 @@ protected function displaySummary(array $summary): void
/**
* Display per-locale statistics.
*
- * @param \Core\Lang\Coverage\TranslationCoverageReport $report
+ * @param TranslationCoverageReport $report
*/
protected function displayLocaleStats($report): void
{
@@ -201,7 +204,7 @@ protected function displayLocaleStats($report): void
/**
* Display missing keys.
*
- * @param \Core\Lang\Coverage\TranslationCoverageReport $report
+ * @param TranslationCoverageReport $report
*/
protected function displayMissingKeys($report, bool $verbose): void
{
@@ -241,7 +244,7 @@ protected function displayMissingKeys($report, bool $verbose): void
/**
* Display unused keys.
*
- * @param \Core\Lang\Coverage\TranslationCoverageReport $report
+ * @param TranslationCoverageReport $report
*/
protected function displayUnusedKeys($report, bool $verbose): void
{
@@ -291,8 +294,8 @@ protected function shortenPath(string $path): string
* Get shell completion suggestions.
*/
public function complete(
- \Symfony\Component\Console\Completion\CompletionInput $input,
- \Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
+ CompletionInput $input,
+ CompletionSuggestions $suggestions
): void {
if ($input->mustSuggestOptionValuesFor('locale')) {
// Suggest available locales
diff --git a/src/Core/Lang/Console/Commands/TranslationMemoryCommand.php b/src/Core/Lang/Console/Commands/TranslationMemoryCommand.php
index 9958769..5eeadac 100644
--- a/src/Core/Lang/Console/Commands/TranslationMemoryCommand.php
+++ b/src/Core/Lang/Console/Commands/TranslationMemoryCommand.php
@@ -11,8 +11,11 @@
namespace Core\Lang\Console\Commands;
+use Core\Lang\TranslationMemory\FuzzyMatcher;
use Core\Lang\TranslationMemory\TranslationMemory;
use Illuminate\Console\Command;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
/**
* Translation Memory management command.
@@ -388,7 +391,7 @@ protected function handleSuggest(TranslationMemory $tm): int
$confidence = $suggestion['confidence'];
$similarityColor = $similarity >= 0.9 ? 'green' : ($similarity >= 0.75 ? 'yellow' : 'red');
- $category = \Core\Lang\TranslationMemory\FuzzyMatcher::categorizeSimilarity($similarity);
+ $category = FuzzyMatcher::categorizeSimilarity($similarity);
$this->line(" ".sprintf('%.0f%%', $similarity * 100).'> match ('.$category.')');
$this->line(" Source:> {$entry->getSource()}");
@@ -566,8 +569,8 @@ protected function formatBytes(int $bytes): string
* Get shell completion suggestions.
*/
public function complete(
- \Symfony\Component\Console\Completion\CompletionInput $input,
- \Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
+ CompletionInput $input,
+ CompletionSuggestions $suggestions
): void {
if ($input->mustSuggestArgumentValuesFor('action')) {
$suggestions->suggestValues([
diff --git a/src/Core/Lang/Coverage/CLAUDE.md b/src/Core/Lang/Coverage/CLAUDE.md
new file mode 100644
index 0000000..a566f93
--- /dev/null
+++ b/src/Core/Lang/Coverage/CLAUDE.md
@@ -0,0 +1,10 @@
+# Lang/Coverage/ — Translation Coverage Analysis
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `TranslationCoverage` | Scans PHP, Blade, and JS/Vue files for translation key usage (`__()`, `trans()`, `@lang`, etc.) and compares against translation files. Reports missing keys (used but undefined) and unused keys (defined but unused). |
+| `TranslationCoverageReport` | DTO containing analysis results — missing keys, unused keys, coverage statistics per locale, and usage locations. Implements `Arrayable`. |
+
+Used by the `lang:coverage` Artisan command for translation quality assurance.
diff --git a/src/Core/Lang/TranslationMemory/CLAUDE.md b/src/Core/Lang/TranslationMemory/CLAUDE.md
new file mode 100644
index 0000000..0e77121
--- /dev/null
+++ b/src/Core/Lang/TranslationMemory/CLAUDE.md
@@ -0,0 +1,14 @@
+# Lang/TranslationMemory/ — Translation Memory System
+
+Stores and retrieves previous translations for reuse and consistency.
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `TranslationMemory` | Unified service — store, retrieve, suggest translations. Supports exact and fuzzy matching with confidence scoring. |
+| `TranslationMemoryEntry` | DTO for a single translation unit — source text, target text, metadata, quality score (0.0-1.0). |
+| `FuzzyMatcher` | Finds similar translations via Levenshtein distance, token/word matching, and n-gram matching. Combines with quality score for confidence rating. |
+| `JsonTranslationMemoryRepository` | File-based storage backend — JSON files organised by locale pairs. |
+| `TmxExporter` | Exports to TMX (Translation Memory eXchange) standard XML format. |
+| `TmxImporter` | Imports from TMX files for interoperability with other translation tools. |
diff --git a/src/Core/Lang/TranslationMemory/Contracts/CLAUDE.md b/src/Core/Lang/TranslationMemory/Contracts/CLAUDE.md
new file mode 100644
index 0000000..ef180cf
--- /dev/null
+++ b/src/Core/Lang/TranslationMemory/Contracts/CLAUDE.md
@@ -0,0 +1,9 @@
+# Lang/TranslationMemory/Contracts/ — TM Repository Interface
+
+## Interfaces
+
+| Interface | Purpose |
+|-----------|---------|
+| `TranslationMemoryRepository` | Storage backend contract. Methods: `store(entry)`, `find(source, locale)`, `search(query, locale)`, `all(locale)`, `delete(id)`. Implementations may use JSON files, databases, or external services. |
+
+The default implementation is `JsonTranslationMemoryRepository`.
diff --git a/src/Core/Lang/en_GB/CLAUDE.md b/src/Core/Lang/en_GB/CLAUDE.md
new file mode 100644
index 0000000..da004f1
--- /dev/null
+++ b/src/Core/Lang/en_GB/CLAUDE.md
@@ -0,0 +1,9 @@
+# Lang/en_GB/ — British English Translations
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `core.php` | British English translation strings for the Core framework. Returns a keyed array of translatable strings used throughout the UI, emails, and error messages. |
+
+This is the primary locale — the framework uses UK English as its base language (colour, organisation, centre).
diff --git a/src/Core/LifecycleEventProvider.php b/src/Core/LifecycleEventProvider.php
index 8ff9ee9..af6160c 100644
--- a/src/Core/LifecycleEventProvider.php
+++ b/src/Core/LifecycleEventProvider.php
@@ -20,6 +20,8 @@
use Core\Events\McpToolsRegistering;
use Core\Events\QueueWorkerBooting;
use Core\Events\WebRoutesRegistering;
+use Core\Front\Mcp\Contracts\McpToolHandler;
+use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
@@ -252,7 +254,7 @@ public function boot(): void
*/
protected static function processMiddleware(Events\LifecycleEvent $event): void
{
- /** @var \Illuminate\Routing\Router $router */
+ /** @var Router $router */
$router = app('router');
foreach ($event->middlewareRequests() as [$alias, $class]) {
@@ -484,7 +486,7 @@ public static function fireMcpRoutes(): void
*
* @return array Fully qualified class names of McpToolHandler implementations
*
- * @see \Core\Front\Mcp\Contracts\McpToolHandler (in php-mcp package)
+ * @see McpToolHandler (in php-mcp package)
*/
public static function fireMcpTools(): array
{
diff --git a/src/Core/Mail/CLAUDE.md b/src/Core/Mail/CLAUDE.md
new file mode 100644
index 0000000..50c4370
--- /dev/null
+++ b/src/Core/Mail/CLAUDE.md
@@ -0,0 +1,49 @@
+# Core\Mail
+
+Email validation and disposable domain blocking service with statistics tracking.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `EmailShield` | Main service: validate emails, block disposable domains, MX lookup, normalisation, async validation |
+| `EmailShieldStat` | Eloquent model for daily validation stats (valid/invalid/disposable counts) |
+| `EmailValidationResult` | Value object with named constructors: `valid()`, `invalid()`, `disposable()` |
+| `Rules\ValidatedEmail` | Laravel validation rule wrapping EmailShield |
+| `Boot` | Service provider: registers EmailShield singleton + backward-compat aliases |
+
+## EmailShield Features
+
+- **Disposable domain blocking**: 100k+ domains from community-maintained GitHub list, cached 24h, stored at `storage/app/email-shield/disposable-domains.txt`
+- **MX record validation**: Cached 1h per domain, suppresses DNS warnings
+- **Validation caching**: Results cached 5 min to avoid repeated checks
+- **Email normalisation**: Gmail dot-stripping, plus-addressing removal, googlemail.com -> gmail.com
+- **Async validation**: Immediate format+disposable check, queues MX lookup for background processing
+- **Statistics**: Atomic daily counters via `insertOrIgnore` + `increment`
+
+## Public API
+
+```php
+$shield = app(EmailShield::class);
+
+// Validate
+$result = $shield->validate('user@example.com'); // EmailValidationResult
+$result->passes(); // bool
+$result->isDisposable; // bool
+
+// Normalise
+$shield->normalize('J.O.H.N+spam@gmail.com'); // 'john@gmail.com'
+$shield->isSameMailbox('john@gmail.com', 'j.o.h.n@googlemail.com'); // true
+
+// Update blocklist
+$shield->updateDisposableDomainsList();
+
+// In validation rules
+'email' => ['required', new ValidatedEmail(blockDisposable: true)]
+```
+
+## Conventions
+
+- Disposable domain list update validates minimum 100 domains to prevent corrupted lists.
+- `EmailShieldStat::pruneOldRecords(90)` for cleanup (called by Console PruneEmailShieldStatsCommand).
+- Backward-compat aliases map `App\Services\Email\*` to `Core\Mail\*`.
diff --git a/src/Core/Mail/Rules/CLAUDE.md b/src/Core/Mail/Rules/CLAUDE.md
new file mode 100644
index 0000000..0a7afee
--- /dev/null
+++ b/src/Core/Mail/Rules/CLAUDE.md
@@ -0,0 +1,14 @@
+# Mail/Rules/ — Email Validation Rules
+
+## Rules
+
+| Rule | Purpose |
+|------|---------|
+| `ValidatedEmail` | Laravel validation rule using the `EmailShield` service. Validates email format and optionally blocks disposable email domains. Constructor takes `blockDisposable: true` (default). |
+
+Usage:
+```php
+'email' => ['required', new ValidatedEmail(blockDisposable: true)]
+```
+
+Part of the Mail subsystem's input validation layer.
diff --git a/src/Core/Media/Abstracts/CLAUDE.md b/src/Core/Media/Abstracts/CLAUDE.md
new file mode 100644
index 0000000..d033935
--- /dev/null
+++ b/src/Core/Media/Abstracts/CLAUDE.md
@@ -0,0 +1,10 @@
+# Media/Abstracts/ — Media Base Classes
+
+## Abstract Classes
+
+| Class | Purpose |
+|-------|---------|
+| `Image` | Base class for image handling operations. |
+| `MediaConversion` | Abstract base for all media conversions (image resizing, thumbnail generation, video processing). Provides common functionality: queueing for large files (threshold configurable via `media.queue_threshold_mb`), progress reporting via `ConversionProgress` event, and storage abstraction. |
+
+Concrete implementations in `Core\Media\Conversions\`.
diff --git a/src/Core/Media/Abstracts/MediaConversion.php b/src/Core/Media/Abstracts/MediaConversion.php
index 9e4fe2e..f0a779a 100644
--- a/src/Core/Media/Abstracts/MediaConversion.php
+++ b/src/Core/Media/Abstracts/MediaConversion.php
@@ -15,6 +15,7 @@
use Core\Media\Jobs\ProcessMediaConversion;
use Core\Media\Support\ConversionProgressReporter;
use Core\Media\Support\MediaConversionData;
+use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@@ -463,7 +464,7 @@ protected function buildQueueOptions(): array
/**
* Get a filesystem instance for the given disk.
*/
- protected function filesystem(string $disk): \Illuminate\Contracts\Filesystem\Filesystem
+ protected function filesystem(string $disk): Filesystem
{
return Storage::disk($disk);
}
diff --git a/src/Core/Media/CLAUDE.md b/src/Core/Media/CLAUDE.md
new file mode 100644
index 0000000..caecb3a
--- /dev/null
+++ b/src/Core/Media/CLAUDE.md
@@ -0,0 +1,39 @@
+# Core\Media
+
+Image processing, media conversion pipeline, and on-demand thumbnail generation.
+
+## Directory Structure
+
+```
+Media/
+ Abstracts/ MediaConversion base class, Image abstract
+ Conversions/ MediaImageResizerConversion, MediaVideoThumbConversion
+ Events/ ConversionProgress event
+ Image/ ImageOptimization, ImageOptimizer, ExifStripper, ModernFormatSupport, OptimizationResult
+ Jobs/ GenerateThumbnail, ProcessMediaConversion
+ Routes/ web.php (thumbnail routes)
+ Support/ ConversionProgressReporter, TemporaryFile, TemporaryDirectory, ImageResizer, MediaConversionData
+ Thumbnail/ ThumbnailController, LazyThumbnail, helpers.php
+ Boot.php Service provider
+ config.php Media configuration
+```
+
+## Key Concepts
+
+- **Media Conversions**: Abstract pipeline (`MediaConversion`) for processing uploaded media. Concrete implementations handle image resizing and video thumbnail extraction.
+- **Thumbnail System**: On-demand thumbnail generation via `ThumbnailController` with `LazyThumbnail` for deferred processing. Routes registered in `Routes/web.php`.
+- **Image Optimization**: `ImageOptimizer` handles format conversion, EXIF stripping, and compression. `ModernFormatSupport` detects WebP/AVIF browser support.
+- **Progress Reporting**: `ConversionProgressReporter` + `ConversionProgress` event for tracking long-running media conversions.
+- **Temporary Files**: `TemporaryFile` and `TemporaryDirectory` helpers for safe cleanup of processing artifacts.
+
+## Jobs
+
+- `GenerateThumbnail` -- Queued job for thumbnail generation
+- `ProcessMediaConversion` -- Queued job for media conversion pipeline
+
+## Integration
+
+- Boot provider registers config from `config.php`
+- Thumbnail routes are web routes (not API)
+- Conversion data flows through `MediaConversionData` DTO
+- `ImageResizer` in Support handles the actual resize operations
diff --git a/src/Core/Media/Conversions/CLAUDE.md b/src/Core/Media/Conversions/CLAUDE.md
new file mode 100644
index 0000000..630e15e
--- /dev/null
+++ b/src/Core/Media/Conversions/CLAUDE.md
@@ -0,0 +1,12 @@
+# Media/Conversions/ — Media Conversion Implementations
+
+Concrete implementations of `MediaConversion` for specific processing tasks.
+
+## Conversions
+
+| Class | Purpose |
+|-------|---------|
+| `MediaImageResizerConversion` | Resizes images to specified dimensions while maintaining aspect ratio. Prevents upscaling. Skips GIF files to preserve animation. Uses `ImageResizer`. |
+| `MediaVideoThumbConversion` | Extracts thumbnail frames from video files for preview display. |
+
+Both extend `Core\Media\Abstracts\MediaConversion` and can be queued via `ProcessMediaConversion` job.
diff --git a/src/Core/Media/Events/CLAUDE.md b/src/Core/Media/Events/CLAUDE.md
new file mode 100644
index 0000000..5642b47
--- /dev/null
+++ b/src/Core/Media/Events/CLAUDE.md
@@ -0,0 +1,7 @@
+# Media/Events/ — Media System Events
+
+## Events
+
+| Event | Fired When | Purpose |
+|-------|-----------|---------|
+| `ConversionProgress` | During media conversion processing | Reports progress stages for UI progress bars, logging, and monitoring. |
diff --git a/src/Core/Media/Image/CLAUDE.md b/src/Core/Media/Image/CLAUDE.md
new file mode 100644
index 0000000..15a79ef
--- /dev/null
+++ b/src/Core/Media/Image/CLAUDE.md
@@ -0,0 +1,13 @@
+# Media/Image/ — Image Processing
+
+Image optimisation and processing utilities.
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `ImageOptimizer` | Main optimiser — resizes, compresses, and converts images. Includes memory safety checks (GD needs 5-6x image size). |
+| `ImageOptimization` | Optimisation configuration and pipeline orchestration. |
+| `ExifStripper` | Removes EXIF metadata from images for privacy (GPS coordinates, camera info). |
+| `ModernFormatSupport` | Detects and enables WebP/AVIF support based on server capabilities. |
+| `OptimizationResult` | DTO containing optimisation results — original size, optimised size, savings percentage, format used. |
diff --git a/src/Core/Media/Image/ImageOptimization.php b/src/Core/Media/Image/ImageOptimization.php
index e30c242..831941f 100644
--- a/src/Core/Media/Image/ImageOptimization.php
+++ b/src/Core/Media/Image/ImageOptimization.php
@@ -15,6 +15,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
+use Illuminate\Support\Carbon;
/**
* Image optimisation record.
@@ -30,8 +31,8 @@
* @property int|null $workspace_id
* @property string|null $optimizable_type
* @property int|null $optimizable_id
- * @property \Illuminate\Support\Carbon|null $created_at
- * @property \Illuminate\Support\Carbon|null $updated_at
+ * @property Carbon|null $created_at
+ * @property Carbon|null $updated_at
*/
class ImageOptimization extends Model
{
diff --git a/src/Core/Media/Jobs/CLAUDE.md b/src/Core/Media/Jobs/CLAUDE.md
new file mode 100644
index 0000000..1b4ecc1
--- /dev/null
+++ b/src/Core/Media/Jobs/CLAUDE.md
@@ -0,0 +1,8 @@
+# Media/Jobs/ — Media Background Jobs
+
+## Jobs
+
+| Job | Purpose |
+|-----|---------|
+| `ProcessMediaConversion` | Queued job for running media conversions asynchronously. Dispatches `ConversionProgress` events during processing. Logs failures. |
+| `GenerateThumbnail` | Queued job for generating thumbnails on demand. Called by `LazyThumbnail` when a thumbnail doesn't exist yet. |
diff --git a/src/Core/Media/Routes/CLAUDE.md b/src/Core/Media/Routes/CLAUDE.md
new file mode 100644
index 0000000..824dfce
--- /dev/null
+++ b/src/Core/Media/Routes/CLAUDE.md
@@ -0,0 +1,7 @@
+# Media/Routes/ — Media Web Routes
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `web.php` | Web route definitions for media serving — thumbnail endpoints, image serving routes. Registered via the `WebRoutesRegistering` lifecycle event. |
diff --git a/src/Core/Media/Support/CLAUDE.md b/src/Core/Media/Support/CLAUDE.md
new file mode 100644
index 0000000..6b7aac4
--- /dev/null
+++ b/src/Core/Media/Support/CLAUDE.md
@@ -0,0 +1,11 @@
+# Media/Support/ — Media Processing Utilities
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `ImageResizer` | Image resizing with aspect ratio preservation and memory safety checks. Prevents upscaling and OOM crashes. |
+| `ConversionProgressReporter` | Reports media conversion progress to listeners via `ConversionProgress` event. |
+| `MediaConversionData` | DTO carrying conversion parameters — source path, output path, dimensions, format, quality. |
+| `TemporaryDirectory` | Manages temporary directories for media processing with automatic cleanup. |
+| `TemporaryFile` | Manages temporary files for media processing with automatic cleanup. |
diff --git a/src/Core/Media/Thumbnail/CLAUDE.md b/src/Core/Media/Thumbnail/CLAUDE.md
new file mode 100644
index 0000000..14a6760
--- /dev/null
+++ b/src/Core/Media/Thumbnail/CLAUDE.md
@@ -0,0 +1,11 @@
+# Media/Thumbnail/ — Lazy Thumbnail Generation
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `LazyThumbnail` | On-demand thumbnail generation service. Generates thumbnails when first requested rather than eagerly on upload. Caches generated thumbnails. Dispatches `GenerateThumbnail` job for async processing. |
+| `ThumbnailController` | HTTP controller serving thumbnail requests. Generates on-the-fly if not cached. |
+| `helpers.php` | Helper functions for thumbnail URL generation in Blade templates. |
+
+Thumbnails are generated lazily to avoid processing overhead on upload and to only create sizes that are actually requested.
diff --git a/src/Core/Rules/CLAUDE.md b/src/Core/Rules/CLAUDE.md
new file mode 100644
index 0000000..5a5099b
--- /dev/null
+++ b/src/Core/Rules/CLAUDE.md
@@ -0,0 +1,51 @@
+# Core\Rules
+
+Security-focused Laravel validation rules. No service provider -- use directly in validation arrays.
+
+## Rules
+
+### SafeWebhookUrl
+
+SSRF protection for webhook delivery URLs.
+
+**Blocks:**
+- Localhost and loopback (127.0.0.0/8, ::1)
+- Private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
+- Link-local, reserved ranges, special-use addresses
+- Local domain names (.local, .localhost, .internal)
+- Decimal IP encoding (2130706433 = 127.0.0.1)
+- IPv4-mapped IPv6 (::ffff:127.0.0.1)
+- Non-HTTPS schemes
+
+**Service mode:** Optionally restrict to known webhook domains (Discord, Slack, Telegram). Known service domains skip SSRF checks.
+
+```php
+'url' => [new SafeWebhookUrl] // any HTTPS, no SSRF
+'url' => [new SafeWebhookUrl('discord')] // discord.com/discordapp.com only
+```
+
+### SafeJsonPayload
+
+Protects against malicious JSON payloads stored in the database.
+
+**Validates:**
+- Maximum total size (default 10 KB)
+- Maximum nesting depth (default 3)
+- Maximum total keys across all levels (default 50)
+- Maximum string value length (default 1000 chars)
+
+**Factory methods:**
+- `SafeJsonPayload::default()` -- 10 KB, depth 3, 50 keys
+- `SafeJsonPayload::small()` -- 2 KB, depth 2, 20 keys
+- `SafeJsonPayload::large()` -- 100 KB, depth 5, 200 keys
+- `SafeJsonPayload::metadata()` -- 5 KB, depth 2, 30 keys, 256 char strings
+
+```php
+'payload' => ['array', SafeJsonPayload::metadata()]
+```
+
+## Conventions
+
+- Both rules implement `Illuminate\Contracts\Validation\ValidationRule`.
+- `SafeWebhookUrl` resolves hostnames and checks ALL returned IPs against blocklists.
+- These are standalone -- no Boot provider, no config. Import and use directly.
diff --git a/src/Core/Search/Analytics/CLAUDE.md b/src/Core/Search/Analytics/CLAUDE.md
new file mode 100644
index 0000000..0435491
--- /dev/null
+++ b/src/Core/Search/Analytics/CLAUDE.md
@@ -0,0 +1,15 @@
+# Search/Analytics/ — Search Analytics
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `SearchAnalytics` | Tracks search queries, results, and user interactions. Features: query tracking with timestamps and result counts, click-through tracking, zero-result query tracking for content gap analysis, popular search trending. |
+
+Part of the Search subsystem. Data feeds into `SearchSuggestions` for autocomplete.
+
+## Migrations
+
+| File | Purpose |
+|------|---------|
+| `migrations/2024_01_01_000001_create_search_analytics_tables.php` | Creates tables for search query tracking, result counts, and click-through data. |
diff --git a/src/Core/Search/Analytics/SearchAnalytics.php b/src/Core/Search/Analytics/SearchAnalytics.php
index 03a248c..3582211 100644
--- a/src/Core/Search/Analytics/SearchAnalytics.php
+++ b/src/Core/Search/Analytics/SearchAnalytics.php
@@ -11,6 +11,7 @@
namespace Core\Search\Analytics;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -210,9 +211,9 @@ public function trackClick(
*
* @param int $limit Maximum number of queries to return
* @param int $days Number of days to look back
- * @return \Illuminate\Support\Collection
+ * @return Collection
*/
- public function getPopularQueries(int $limit = 10, int $days = 7): \Illuminate\Support\Collection
+ public function getPopularQueries(int $limit = 10, int $days = 7): Collection
{
if (! $this->isEnabled()) {
return collect();
@@ -232,9 +233,9 @@ public function getPopularQueries(int $limit = 10, int $days = 7): \Illuminate\S
*
* @param int $limit Maximum number of queries to return
* @param int $days Number of days to look back
- * @return \Illuminate\Support\Collection
+ * @return Collection
*/
- public function getZeroResultQueries(int $limit = 20, int $days = 30): \Illuminate\Support\Collection
+ public function getZeroResultQueries(int $limit = 20, int $days = 30): Collection
{
if (! $this->isEnabled()) {
return collect();
@@ -254,9 +255,9 @@ public function getZeroResultQueries(int $limit = 20, int $days = 30): \Illumina
* Get search trend over time.
*
* @param int $days Number of days to look back
- * @return \Illuminate\Support\Collection
+ * @return Collection
*/
- public function getTrend(int $days = 30): \Illuminate\Support\Collection
+ public function getTrend(int $days = 30): Collection
{
if (! $this->isEnabled()) {
return collect();
diff --git a/src/Core/Search/Analytics/migrations/CLAUDE.md b/src/Core/Search/Analytics/migrations/CLAUDE.md
new file mode 100644
index 0000000..757dd6a
--- /dev/null
+++ b/src/Core/Search/Analytics/migrations/CLAUDE.md
@@ -0,0 +1,7 @@
+# Search/Analytics/migrations/ — Search Analytics Schema
+
+## Migrations
+
+| File | Purpose |
+|------|---------|
+| `2024_01_01_000001_create_search_analytics_tables.php` | Creates tables for search query tracking, result counts, and click-through data used by the `SearchAnalytics` service. |
diff --git a/src/Core/Search/Boot.php b/src/Core/Search/Boot.php
index f67ade4..be8042e 100644
--- a/src/Core/Search/Boot.php
+++ b/src/Core/Search/Boot.php
@@ -11,6 +11,7 @@
namespace Core\Search;
+use App\Services\Search\UnifiedSearchService;
use Core\Search\Analytics\SearchAnalytics;
use Core\Search\Suggestions\SearchSuggestions;
use Illuminate\Support\ServiceProvider;
@@ -72,8 +73,8 @@ protected function publishConfig(): void
*/
protected function registerBackwardCompatAliases(): void
{
- if (! class_exists(\App\Services\Search\UnifiedSearchService::class)) {
- class_alias(Unified::class, \App\Services\Search\UnifiedSearchService::class);
+ if (! class_exists(UnifiedSearchService::class)) {
+ class_alias(Unified::class, UnifiedSearchService::class);
}
}
}
diff --git a/src/Core/Search/CLAUDE.md b/src/Core/Search/CLAUDE.md
new file mode 100644
index 0000000..9525e99
--- /dev/null
+++ b/src/Core/Search/CLAUDE.md
@@ -0,0 +1,62 @@
+# Core\Search
+
+Unified search across system components with analytics tracking, autocomplete suggestions, and result highlighting.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `Unified` | Main search service: searches MCP tools/resources, API endpoints, patterns, assets, todos, agent plans |
+| `Analytics\SearchAnalytics` | Tracks queries, clicks, zero-result queries, trends. Privacy-aware (daily-rotating IP hashes, sensitive pattern exclusion) |
+| `Suggestions\SearchSuggestions` | Autocomplete from popular queries, recent searches, and content. Logarithmic popularity scoring |
+| `Support\SearchHighlighter` | Highlights matched terms in results with configurable HTML wrapper, snippet extraction with context |
+| `Boot` | Service provider: merges config, registers singletons, loads migrations |
+
+## Search Sources (Unified)
+
+| Type | Source | Model/Data |
+|------|--------|------------|
+| `mcp_tool` | YAML files | `resource_path('mcp/servers/*.yaml')` |
+| `mcp_resource` | YAML files | Same |
+| `api_endpoint` | Config | `config('core.search.api_endpoints')` |
+| `pattern` | Database | `Core\Mod\Uptelligence\Models\Pattern` |
+| `asset` | Database | `Core\Mod\Uptelligence\Models\Asset` |
+| `todo` | Database | `Core\Mod\Uptelligence\Models\UpstreamTodo` |
+| `plan` | Database | `Core\Mod\Agentic\Models\AgentPlan` |
+
+## Scoring Algorithm
+
+Configurable weights via `config/search.php`:
+- Exact match: 20 (30 if field equals query exactly)
+- Starts-with: 15
+- Word match: 5 (7.5 for exact word)
+- Position factor: earlier fields in the array score higher
+- Fuzzy: Levenshtein distance, 0.5x score multiplier, min 4 char query
+
+## Analytics
+
+- Queries stored in `search_analytics` table with `query_hash` (xxh3) for grouping
+- Click tracking in `search_analytics_clicks`
+- Sensitive patterns excluded: password, secret, token, key, credit, ssn
+- IP hashed with daily-rotating salt
+- `prune()` respects `retention_days` config (default 90)
+
+## Suggestions
+
+Three sources (configurable priority): `popular`, `recent`, `content`
+- Popular: From analytics, log-scale scoring, queries with results only
+- Recent: Per-user cache (30 days), session fallback for guests
+- Content: Prefix matching on model names
+- Trending: Growth comparison between recent and earlier period
+
+## Configuration
+
+All in `config/search.php` (publishable):
+- `scoring.*` -- match weights
+- `fuzzy.*` -- Levenshtein settings
+- `suggestions.*` -- autocomplete settings
+- `analytics.*` -- tracking and retention
+
+## LIKE injection protection
+
+`escapeLikeQuery()` escapes `%` and `_` wildcards. If more than 3 wildcards, strips them entirely to prevent DoS.
diff --git a/src/Core/Search/Suggestions/CLAUDE.md b/src/Core/Search/Suggestions/CLAUDE.md
new file mode 100644
index 0000000..5b47673
--- /dev/null
+++ b/src/Core/Search/Suggestions/CLAUDE.md
@@ -0,0 +1,9 @@
+# Search/Suggestions/ — Search Autocomplete
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `SearchSuggestions` | Type-ahead suggestion service. Sources: popular queries from SearchAnalytics, recent searches (per user/session), prefix matching, content-based suggestions from searchable items. Cached for performance. |
+
+Part of the Search subsystem. Provides autocomplete data for search UIs.
diff --git a/src/Core/Search/Suggestions/SearchSuggestions.php b/src/Core/Search/Suggestions/SearchSuggestions.php
index 7a2aacd..4c33d4d 100644
--- a/src/Core/Search/Suggestions/SearchSuggestions.php
+++ b/src/Core/Search/Suggestions/SearchSuggestions.php
@@ -11,6 +11,9 @@
namespace Core\Search\Suggestions;
+use Core\Mod\Uptelligence\Models\Asset;
+use Core\Mod\Uptelligence\Models\Pattern;
+use Core\Search\Analytics\SearchAnalytics;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@@ -43,7 +46,7 @@
* 'sources' => ['popular', 'recent', 'content'],
* ]
*
- * @see \Core\Search\Analytics\SearchAnalytics For search tracking integration
+ * @see SearchAnalytics For search tracking integration
*/
class SearchSuggestions
{
@@ -259,9 +262,9 @@ public function getContentSuggestions(string $prefix, int $limit = 10): Collecti
$escaped = $this->escapeLikeQuery($prefix);
// Search patterns if available
- if (class_exists(\Core\Mod\Uptelligence\Models\Pattern::class)) {
+ if (class_exists(Pattern::class)) {
try {
- $patterns = \Core\Mod\Uptelligence\Models\Pattern::where('name', 'like', "{$escaped}%")
+ $patterns = Pattern::where('name', 'like', "{$escaped}%")
->limit($limit)
->pluck('name')
->map(fn ($name) => [
@@ -278,9 +281,9 @@ public function getContentSuggestions(string $prefix, int $limit = 10): Collecti
}
// Search assets if available
- if (class_exists(\Core\Mod\Uptelligence\Models\Asset::class)) {
+ if (class_exists(Asset::class)) {
try {
- $assets = \Core\Mod\Uptelligence\Models\Asset::where('name', 'like', "{$escaped}%")
+ $assets = Asset::where('name', 'like', "{$escaped}%")
->limit($limit)
->pluck('name')
->map(fn ($name) => [
diff --git a/src/Core/Search/Support/CLAUDE.md b/src/Core/Search/Support/CLAUDE.md
new file mode 100644
index 0000000..10797df
--- /dev/null
+++ b/src/Core/Search/Support/CLAUDE.md
@@ -0,0 +1,9 @@
+# Search/Support/ — Search Utilities
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `SearchHighlighter` | Highlights search query terms within result text. Multi-word support, case-insensitive matching with original case preserved, HTML-safe output, context extraction (snippets around matches), configurable highlight styles. |
+
+Used by search result rendering to show users why a result matched their query.
diff --git a/src/Core/Search/Unified.php b/src/Core/Search/Unified.php
index c76550b..671908e 100644
--- a/src/Core/Search/Unified.php
+++ b/src/Core/Search/Unified.php
@@ -11,6 +11,10 @@
namespace Core\Search;
+use Core\Mod\Agentic\Models\AgentPlan;
+use Core\Mod\Uptelligence\Models\Asset;
+use Core\Mod\Uptelligence\Models\Pattern;
+use Core\Mod\Uptelligence\Models\UpstreamTodo;
use Core\Search\Analytics\SearchAnalytics;
use Core\Search\Suggestions\SearchSuggestions;
use Illuminate\Support\Collection;
@@ -410,14 +414,14 @@ protected function searchApiEndpoints(string $query): Collection
*/
protected function searchPatterns(string $query): Collection
{
- if (! class_exists(\Core\Mod\Uptelligence\Models\Pattern::class)) {
+ if (! class_exists(Pattern::class)) {
return collect();
}
$escaped = $this->escapeLikeQuery($query);
try {
- return \Core\Mod\Uptelligence\Models\Pattern::where('name', 'like', "%{$escaped}%")
+ return Pattern::where('name', 'like', "%{$escaped}%")
->orWhere('description', 'like', "%{$escaped}%")
->orWhere('category', 'like', "%{$escaped}%")
->limit(20)
@@ -448,14 +452,14 @@ protected function searchPatterns(string $query): Collection
*/
protected function searchAssets(string $query): Collection
{
- if (! class_exists(\Core\Mod\Uptelligence\Models\Asset::class)) {
+ if (! class_exists(Asset::class)) {
return collect();
}
$escaped = $this->escapeLikeQuery($query);
try {
- return \Core\Mod\Uptelligence\Models\Asset::where('name', 'like', "%{$escaped}%")
+ return Asset::where('name', 'like', "%{$escaped}%")
->orWhere('slug', 'like', "%{$escaped}%")
->orWhere('description', 'like', "%{$escaped}%")
->limit(20)
@@ -487,14 +491,14 @@ protected function searchAssets(string $query): Collection
*/
protected function searchTodos(string $query): Collection
{
- if (! class_exists(\Core\Mod\Uptelligence\Models\UpstreamTodo::class)) {
+ if (! class_exists(UpstreamTodo::class)) {
return collect();
}
$escaped = $this->escapeLikeQuery($query);
try {
- return \Core\Mod\Uptelligence\Models\UpstreamTodo::where('title', 'like', "%{$escaped}%")
+ return UpstreamTodo::where('title', 'like', "%{$escaped}%")
->orWhere('description', 'like', "%{$escaped}%")
->limit(20)
->get()
@@ -525,14 +529,14 @@ protected function searchTodos(string $query): Collection
*/
protected function searchPlans(string $query): Collection
{
- if (! class_exists(\Core\Mod\Agentic\Models\AgentPlan::class)) {
+ if (! class_exists(AgentPlan::class)) {
return collect();
}
$escaped = $this->escapeLikeQuery($query);
try {
- return \Core\Mod\Agentic\Models\AgentPlan::where('title', 'like', "%{$escaped}%")
+ return AgentPlan::where('title', 'like', "%{$escaped}%")
->orWhere('slug', 'like', "%{$escaped}%")
->orWhere('description', 'like', "%{$escaped}%")
->limit(20)
diff --git a/src/Core/Seo/Analytics/CLAUDE.md b/src/Core/Seo/Analytics/CLAUDE.md
new file mode 100644
index 0000000..c8cb584
--- /dev/null
+++ b/src/Core/Seo/Analytics/CLAUDE.md
@@ -0,0 +1,7 @@
+# Seo/Analytics/ — SEO Score Tracking
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `SeoScoreTrend` | Tracks SEO scores over time to show improvement or regression trends. Reads from `SeoScoreHistory` model. Provides daily, weekly, and monthly aggregation. |
diff --git a/src/Core/Seo/Boot.php b/src/Core/Seo/Boot.php
index 875b930..321f590 100644
--- a/src/Core/Seo/Boot.php
+++ b/src/Core/Seo/Boot.php
@@ -11,6 +11,7 @@
namespace Core\Seo;
+use App\Services\Content\SchemaService;
use Core\Seo\Analytics\SeoScoreTrend;
use Core\Seo\Console\Commands\AuditCanonicalUrls;
use Core\Seo\Console\Commands\RecordSeoScores;
@@ -105,8 +106,8 @@ protected function registerCommands(): void
protected function registerBackwardCompatAliases(): void
{
// Schema (high-level JSON-LD generator)
- if (! class_exists(\App\Services\Content\SchemaService::class)) {
- class_alias(Schema::class, \App\Services\Content\SchemaService::class);
+ if (! class_exists(SchemaService::class)) {
+ class_alias(Schema::class, SchemaService::class);
}
// Services
diff --git a/src/Core/Seo/CLAUDE.md b/src/Core/Seo/CLAUDE.md
new file mode 100644
index 0000000..4df8c86
--- /dev/null
+++ b/src/Core/Seo/CLAUDE.md
@@ -0,0 +1,73 @@
+# Core\Seo
+
+SEO metadata management, JSON-LD schema generation, structured data validation, OG image handling, and score trend tracking.
+
+## Key Classes
+
+| Class | Purpose |
+|-------|---------|
+| `Schema` | High-level JSON-LD generator: auto-detects Article, HowTo, FAQ, Breadcrumb from content |
+| `SeoMetadata` | Eloquent model (polymorphic): title, description, canonical, OG, Twitter, schema markup, score, issues |
+| `HasSeoMetadata` | Trait for models: `seoMetadata()` morphOne, `updateSeo()`, `getSeoHeadTagsAttribute()` |
+| `Boot` | Service provider: registers singletons + artisan commands |
+
+### Services/
+
+| Class | Purpose |
+|-------|---------|
+| `SchemaBuilderService` | Lower-level schema building blocks |
+| `ServiceOgImageService` | OG image generation for service pages |
+
+### Validation/
+
+| Class | Purpose |
+|-------|---------|
+| `SchemaValidator` | Validates schema against schema.org specifications |
+| `StructuredDataTester` | Tests structured data, checks rich results eligibility, generates reports |
+| `CanonicalUrlValidator` | Validates URL format and detects conflicts between records |
+| `OgImageValidator` | Validates OG image dimensions and requirements |
+
+### Analytics/
+
+| Class | Purpose |
+|-------|---------|
+| `SeoScoreTrend` | Daily/weekly score trend tracking |
+| `Models\SeoScoreHistory` | Historical score records |
+
+## Schema Generation
+
+`Schema::generateSchema($item)` builds a `@graph` containing:
+1. Organisation schema (always, from config)
+2. Article schema (TechArticle by default)
+3. Breadcrumb schema
+4. HowTo schema (if content has numbered steps)
+5. FAQ schema (if content has FAQ section with Q&A pairs)
+
+Content detection uses regex on `display_content`. Steps extracted from JSON blocks or numbered lists. FAQs from `## FAQ` sections.
+
+## SeoMetadata Model
+
+- **Lazy schema_markup**: Custom accessor/mutator defers JSON parsing until accessed
+- **Meta tag generation**: `meta_tags` attribute produces complete HTML ``, ``, ``, OG, Twitter tags
+- **JSON-LD output**: `json_ld` attribute wraps schema in `