diff --git a/.ai/AGENTS.md b/.ai/AGENTS.md
new file mode 100644
index 000000000..172ca5e69
--- /dev/null
+++ b/.ai/AGENTS.md
@@ -0,0 +1,154 @@
+# Goravel Agent Reference
+
+Go-first framework. Not Laravel. Do not port PHP patterns directly.
+
+---
+
+## Setup
+
+Knowledge files are installed per-facade via the framework CLI:
+
+```bash
+# Install knowledge for specific facades you are using
+./artisan agents:install --facade=Orm,Route,Auth
+
+# Install all knowledge files
+./artisan agents:install --all
+
+# Update already-installed files to latest
+./artisan agents:install --update
+```
+
+Files are downloaded from the docs repo and placed in `.ai/knowledge/`.
+The manifest at `.ai/agents.json` maps facade names to file URLs.
+
+---
+
+## Facade Import
+
+```go
+// WRONG: import "github.com/goravel/framework/facades" (does not exist)
+// RIGHT: import path from go.mod module name
+import "yourmodule/app/facades"
+```
+
+Your installed facades are in `app/facades/`. Check which ones exist before using them.
+Not all facades are installed in every project.
+
+---
+
+## Wrong vs Right Patterns
+
+| Pattern | Wrong | Right |
+| --------------------- | ----------------------------- | ------------------------------------------------------------------- |
+| Handler return | `func(ctx http.Context)` | `func(ctx http.Context) http.Response` |
+| Find missing record | check `err != nil` | `FindOrFail` -- `Find` returns nil on miss |
+| Struct zero update | `Update(User{Age: 0})` | `Update(map[string]any{"age": 0})` |
+| GlobalScopes return | `[]func(Query) Query` | `map[string]func(Query) Query` |
+| Sum/Avg/Max/Min | `sum, err := .Sum("col")` | `err := .Sum("col", &dest)` |
+| Validation Make | `Make(input, rules)` | `Make(ctx, input, rules)` |
+| Custom Rule Passes | `Passes(data, val)` | `Passes(ctx context.Context, data Data, val any, opts ...any) bool` |
+| Custom Rule Message | `Message() string` | `Message(ctx context.Context) string` |
+| Custom Filter Handle | `Handle() any` | `Handle(ctx context.Context) any` |
+| Log driver Handle | `Handle(ch) (Hook, error)` | `Handle(ch string) (Handler, error)` |
+| Log With | `With("key", val)` | `With(map[string]any{"key": val})` |
+| Log Request | `Request(*http.Request)` | `Request(http.ContextRequest)` |
+| Auth User/ID | call any time | call `Parse(token)` first on same ctx |
+| gRPC client | `Grpc().Client()` | `Grpc().Connect("name")` |
+| Http client Bind | `request.Bind(&dest)` | `response.Bind(&dest)` |
+| Http client body | `Post(uri, string)` | `Post(uri, io.Reader)` |
+| Middleware type | struct with Handle | `func() http.Middleware` returning closure |
+| Middleware abort | bare `return` | `.Abort(); return` |
+| Session chain | methods return void | all mutating methods return `Session` |
+| Session Regenerate | returns `bool` | returns `error` |
+| Cache Increment | returns `(int, error)` | returns `(int64, error)` |
+| Validation Errors.Get | returns `[]string` | returns `map[string]string` |
+| Validation Errors.All | returns `map[string][]string` | returns `map[string]map[string]string` |
+| UserProvider typo | `RetrieveByID` | `RetriveByID` (matches contract exactly) |
+| Queue Jobs alias | `Jobs{}` | `ChainJob{}` (Jobs is deprecated) |
+| Queue Machinery | use Machinery driver | removed; use `sync`/`database`/`redis` |
+| ORM Delete args | `Delete(value, conds...)` | `Delete(value ...any)` |
+| ORM Update return | `error` | `(*db.Result, error)` |
+
+---
+
+## Available Facades
+
+```
+App Artisan Auth(ctx) Cache Config Crypt DB Event Gate Grpc
+Hash Http Lang(ctx) Log Mail Orm Process Queue RateLimiter
+Route Schedule Schema Seeder Session Storage Validation View
+```
+
+`Auth(ctx)` and `Lang(ctx)` take `http.Context`. All others: `facades.Name()`.
+
+For the latest interface of any facade, the authoritative source is the framework contracts:
+`https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/`
+
+---
+
+## Bootstrap Entry Point
+
+The application entry is `bootstrap/app.go`. For the full `ApplicationBuilder` interface:
+`https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application_builder.go`
+
+Boot order: **Config** -> **Register providers** -> **Boot providers** -> **WithCallback** -> **Runners**
+
+`WithCallback` is the only safe place to call facades during startup (gates, observers, rate limiters).
+
+---
+
+## Section-Based Loading
+
+Every knowledge file has this structure. Load only what the sub-task requires.
+
+| Section | Contains | Load when |
+| --------------------------- | ------------------------------------------- | -------------------------------------------------------- |
+| `## Core Imports` | Exact import paths | Writing import blocks |
+| `## Contracts` | Raw GitHub URLs to framework contract files | Need exact type/interface definition; fetch the URL live |
+| `## Available Methods` | Concise method list with return types | Looking up a method name or return type |
+| `## Implementation Example` | One complete working code block | Starting an implementation from scratch |
+| `## Rules` | Constraints, gotchas, deprecated notices | Debugging; something is not working |
+
+**Interface and struct definitions are not duplicated in these files.**
+Fetch the contract URL(s) from `## Contracts` when you need exact signatures.
+The framework contracts are always current and authoritative.
+
+- Skip `## Implementation Example` when you only need a method signature.
+- Skip `## Core Imports` when imports are already established in the file being edited.
+- Read `## Rules` last -- only needed when the straightforward approach is failing.
+
+---
+
+## Knowledge Files
+
+Install and load the relevant file for your task. Check `.ai/agents.json` for the full facade-to-file map.
+
+| When task involves... | File |
+| ------------------------------------------------------------- | --------------------- |
+| Service container, providers, `Bind`/`Singleton`, runners | `bootstrap.md` |
+| Defining routes, middleware, rate limiting | `route.md` |
+| Request input, HTTP responses, cookies, streams | `request-response.md` |
+| Database queries, models, relations, transactions, ORM events | `orm.md` |
+| Database table creation, column types, indexes | `migration.md` |
+| Caching, atomic locks | `cache.md` |
+| JWT auth, login/logout, guards, gates, policies | `auth.md` |
+| Background jobs, dispatch, chaining | `queue.md` |
+| Events, listeners | `event.md` |
+| Sending email, Mailable structs | `mail.md` |
+| File upload, disk read/write, cloud storage | `storage.md` |
+| Log entries, channels, custom drivers | `log.md` |
+| Input validation, form requests, custom rules | `validation.md` |
+| Artisan commands, arguments, flags, prompts | `artisan.md` |
+| Scheduled tasks, cron | `schedule.md` |
+| HTTP requests to external APIs, test faking | `http-client.md` |
+| Password hashing, string encryption | `hash-crypt.md` |
+| Session read/write, flash data | `session.md` |
+| gRPC server/client, interceptors | `grpc.md` |
+| External shell commands, pipelines, pools | `process.md` |
+| Translations, pluralization, locale | `localization.md` |
+| View templates, CSRF | `view.md` |
+| HTTP tests, mock facades, Docker DB | `testing.md` |
+| `str.Of(...)` fluent string methods | `str.md` |
+| Date/time, `carbon.Parse`, arithmetic | `carbon.md` |
+| Path helpers, maps, slices, debug, color | `helpers.md` |
diff --git a/.ai/agents.json b/.ai/agents.json
new file mode 100644
index 000000000..6f4f0ff97
--- /dev/null
+++ b/.ai/agents.json
@@ -0,0 +1,412 @@
+{
+ "version": 1,
+ "base_url": "https://raw.githubusercontent.com/goravel/docs/master/.ai/knowledge",
+ "agents_md": "https://raw.githubusercontent.com/goravel/docs/master/.ai/AGENTS.md",
+ "local_dir": ".ai/knowledge",
+ "facades": {
+ "App": {
+ "file": "bootstrap.md",
+ "triggers": [
+ "foundation.Application",
+ "ServiceProvider",
+ "Bind",
+ "Singleton",
+ "Runner"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application_builder.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/service_provider.go"
+ ]
+ },
+ "Artisan": {
+ "file": "artisan.md",
+ "triggers": [
+ "console.Command",
+ "Artisan",
+ "make:command"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command/flags.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command/arguments.go"
+ ]
+ },
+ "Auth": {
+ "file": "auth.md",
+ "triggers": [
+ "auth.Auth",
+ "GuardDriver",
+ "Parse",
+ "Login",
+ "Gate"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/auth.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/access/gate.go"
+ ]
+ },
+ "Cache": {
+ "file": "cache.md",
+ "triggers": [
+ "cache.Cache",
+ "cache.Driver",
+ "Lock"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/cache/cache.go"
+ ]
+ },
+ "Config": {
+ "file": "bootstrap.md",
+ "triggers": [
+ "config.Config"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application_builder.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/service_provider.go"
+ ]
+ },
+ "Crypt": {
+ "file": "hash-crypt.md",
+ "triggers": [
+ "crypt.Crypt",
+ "EncryptString",
+ "DecryptString"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/hash/hash.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/crypt/crypt.go"
+ ]
+ },
+ "DB": {
+ "file": "orm.md",
+ "triggers": [
+ "database/db",
+ "DB.Table",
+ "DB.Raw"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/orm.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/events.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/observer.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/factory.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/factory/factory.go"
+ ]
+ },
+ "Event": {
+ "file": "event.md",
+ "triggers": [
+ "event.Event",
+ "event.Listener",
+ "Dispatch"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/event/events.go"
+ ]
+ },
+ "Gate": {
+ "file": "auth.md",
+ "triggers": [
+ "access.Gate",
+ "Define",
+ "Allows",
+ "Denies"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/auth.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/access/gate.go"
+ ]
+ },
+ "Grpc": {
+ "file": "grpc.md",
+ "triggers": [
+ "grpc.Grpc",
+ "Connect",
+ "grpc.Server"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/grpc/grpc.go"
+ ]
+ },
+ "Hash": {
+ "file": "hash-crypt.md",
+ "triggers": [
+ "hash.Hash",
+ "Make",
+ "Check",
+ "NeedsRehash"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/hash/hash.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/crypt/crypt.go"
+ ]
+ },
+ "Http": {
+ "file": "http-client.md",
+ "triggers": [
+ "http/client",
+ "client.Request",
+ "client.Response"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/request.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/response.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/factory.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/fake_response.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/fake_sequence.go"
+ ]
+ },
+ "Lang": {
+ "file": "localization.md",
+ "triggers": [
+ "translation.Translator",
+ "Choice",
+ "SetLocale"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/translation/translator.go"
+ ]
+ },
+ "Log": {
+ "file": "log.md",
+ "triggers": [
+ "log.Log",
+ "log.Writer",
+ "log.Logger",
+ "log.Handler"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/log/log.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/log/level.go"
+ ]
+ },
+ "Mail": {
+ "file": "mail.md",
+ "triggers": [
+ "mail.Mail",
+ "mail.Mailable",
+ "mail.Content",
+ "mail.Envelope"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/mail/mail.go"
+ ]
+ },
+ "Orm": {
+ "file": "orm.md",
+ "triggers": [
+ "orm.Orm",
+ "orm.Query",
+ "orm.Model",
+ "database/orm"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/orm.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/events.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/observer.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/factory.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/factory/factory.go"
+ ]
+ },
+ "Process": {
+ "file": "process.md",
+ "triggers": [
+ "process.Process",
+ "process.Result",
+ "process.Running"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/process.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/pipeline.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/pool.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/result.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running_pipe.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running_pool.go"
+ ]
+ },
+ "Queue": {
+ "file": "queue.md",
+ "triggers": [
+ "queue.Queue",
+ "queue.Job",
+ "queue.PendingJob",
+ "ChainJob"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/queue/job.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/queue/queue.go"
+ ]
+ },
+ "RateLimiter": {
+ "file": "route.md",
+ "triggers": [
+ "http.RateLimiter",
+ "Limit",
+ "PerMinute",
+ "PerHour"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/route/route.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/rate_limiter.go"
+ ]
+ },
+ "Route": {
+ "file": "route.md",
+ "triggers": [
+ "route.Route",
+ "route.Router",
+ "HandlerFunc"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/route/route.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/rate_limiter.go"
+ ]
+ },
+ "Schedule": {
+ "file": "schedule.md",
+ "triggers": [
+ "schedule.Schedule",
+ "schedule.Event",
+ "Cron"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/schedule/event.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/schedule/schedule.go"
+ ]
+ },
+ "Schema": {
+ "file": "migration.md",
+ "triggers": [
+ "schema.Schema",
+ "schema.Blueprint",
+ "Migration"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/schema.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/blueprint.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/index.go"
+ ]
+ },
+ "Seeder": {
+ "file": "orm.md",
+ "triggers": [
+ "seeder.Seeder",
+ "database/seeder"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/orm.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/events.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/observer.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/factory.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/factory/factory.go"
+ ]
+ },
+ "Session": {
+ "file": "session.md",
+ "triggers": [
+ "session.Session",
+ "session.Driver",
+ "session.Manager"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/session.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/manager.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/driver.go"
+ ]
+ },
+ "Storage": {
+ "file": "storage.md",
+ "triggers": [
+ "filesystem.Storage",
+ "filesystem.Driver",
+ "filesystem.File"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/filesystem/storage.go"
+ ]
+ },
+ "Validation": {
+ "file": "validation.md",
+ "triggers": [
+ "validation.Validation",
+ "validation.Validator",
+ "validation.Rule"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/validation/validation.go"
+ ]
+ },
+ "View": {
+ "file": "view.md",
+ "triggers": [
+ "view.View",
+ "ResponseView",
+ "Make"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/view/view.go"
+ ]
+ }
+ },
+ "extras": {
+ "str": {
+ "file": "str.md",
+ "triggers": [
+ "support/str",
+ "str.Of"
+ ],
+ "contract_urls": []
+ },
+ "carbon": {
+ "file": "carbon.md",
+ "triggers": [
+ "support/carbon",
+ "carbon.Now",
+ "carbon.DateTime"
+ ],
+ "contract_urls": []
+ },
+ "helpers": {
+ "file": "helpers.md",
+ "triggers": [
+ "support/path",
+ "support/maps",
+ "support/collect",
+ "support/convert"
+ ],
+ "contract_urls": []
+ },
+ "testing": {
+ "file": "testing.md",
+ "triggers": [
+ "testing.TestCase",
+ "testing/http",
+ "mock.Factory"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/testing.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/request.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/response.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/assertable_json.go"
+ ]
+ },
+ "request-response": {
+ "file": "request-response.md",
+ "triggers": [
+ "http.Context",
+ "ContextRequest",
+ "ContextResponse",
+ "HandlerFunc"
+ ],
+ "contract_urls": [
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/request.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/response.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/context.go",
+ "https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/cookie.go"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.ai/knowledge/artisan.md b/.ai/knowledge/artisan.md
new file mode 100644
index 000000000..ce46d83be
--- /dev/null
+++ b/.ai/knowledge/artisan.md
@@ -0,0 +1,135 @@
+# Artisan Console Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "time"
+ "github.com/goravel/framework/contracts/console"
+ "github.com/goravel/framework/contracts/console/command"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command/flags.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/console/command/arguments.go`
+
+## Available Methods
+
+**facades.Artisan():**
+
+- `Call(command string)` - run command string programmatically
+
+**console.Context - Arguments:**
+
+- `Argument(index int)` string - 0-based positional
+- `Arguments()` []string
+- `ArgumentString/Int/Int8/Int16/Int32/Int64(key)` T
+- `ArgumentUint/Uint8/Uint16/Uint32/Uint64(key)` T
+- `ArgumentFloat32/Float64(key)` T
+- `ArgumentStringSlice/IntSlice/Int8Slice/.../Float64Slice(key)` []T
+- `ArgumentTimestamp(key)` time.Time
+- `ArgumentTimestampSlice(key)` []time.Time
+
+**console.Context - Flags/Options:**
+
+- `Option(key)` string
+- `OptionBool(key)` bool
+- `OptionInt/OptionInt64/OptionFloat64(key)` T
+- `OptionSlice(key)` []string
+- `OptionIntSlice/OptionInt64Slice/OptionFloat64Slice(key)` []T
+
+**console.Context - Output:**
+
+- `Info/Warning/Error/Comment/Success/Line(message)`
+- `Green/Greenln/Red/Redln/Yellow/Yellowln/Black/Blackln(message)`
+- `NewLine(times ...int)`
+- `Divider(filler ...string)`
+- `TwoColumnDetail(first, second string, filler ...rune)`
+- `Table(headers []string, rows [][]string, option ...TableOption)`
+
+**console.Context - Interactive:**
+
+- `Ask(question, ...AskOption)` (string, error)
+- `Secret(question, ...SecretOption)` (string, error)
+- `Confirm(question, ...ConfirmOption)` bool
+- `Choice(question, []Choice, ...ChoiceOption)` (string, error)
+- `MultiSelect(question, []Choice, ...MultiSelectOption)` ([]string, error)
+
+**console.Context - Progress:**
+
+- `CreateProgressBar(total int)` Progress
+- `WithProgressBar(items []any, callback func(any) error)` ([]any, error)
+- `Spinner(message string, option SpinnerOption)` error
+
+## Implementation Example
+
+```go
+package commands
+
+import (
+ "fmt"
+ "github.com/goravel/framework/contracts/console"
+ "github.com/goravel/framework/contracts/console/command"
+)
+
+type SendEmails struct{}
+
+func (r *SendEmails) Signature() string { return "send:emails" }
+func (r *SendEmails) Description() string { return "Send bulk emails" }
+
+func (r *SendEmails) Extend() command.Extend {
+ return command.Extend{
+ Category: "mail",
+ Arguments: []command.Argument{
+ &command.ArgumentString{Name: "subject", Usage: "Email subject", Required: true},
+ },
+ Flags: []command.Flag{
+ &command.StringFlag{Name: "lang", Value: "en", Aliases: []string{"l"}},
+ &command.BoolFlag{Name: "dry-run"},
+ &command.IntSliceFlag{Name: "ids", Usage: "User IDs"},
+ },
+ }
+}
+
+func (r *SendEmails) Handle(ctx console.Context) error {
+ subject := ctx.ArgumentString("subject")
+ lang := ctx.Option("lang")
+ dryRun := ctx.OptionBool("dry-run")
+ ids := ctx.OptionIntSlice("ids")
+
+ ctx.Info(fmt.Sprintf("Sending to %d users (lang=%s dry=%v)", len(ids), lang, dryRun))
+
+ // Table output
+ ctx.Table([]string{"ID", "Status"}, [][]string{{"1", "queued"}})
+
+ // Auto progress bar
+ items := make([]any, len(ids))
+ for i, id := range ids { items[i] = id }
+ _, err := ctx.WithProgressBar(items, func(item any) error {
+ return nil
+ })
+
+ _ = subject
+ return err
+}
+```
+
+## Rules
+
+- Register via `WithCommands` in `bootstrap/app.go`; `make:command` auto-registers in `bootstrap/commands.go`.
+- `Signature()` is the CLI name and must be unique across all commands.
+- `Argument(index)` is 0-based positional; `ArgumentString(name)` is by declared name.
+- Slice flags (`IntSliceFlag`) use `OptionIntSlice`; `StringSliceFlag` uses `OptionSlice`.
+- `OptionFloat64Slice` reads `Float64SliceFlag` values.
+- `Ask/Confirm/Choice/MultiSelect` are interactive; do not use in non-TTY (CI) environments.
+- `CreateProgressBar` requires `.Start()` first, then `.Advance()`, then `.Finish()`.
+- `WithProgressBar` handles lifecycle automatically.
+- `Spinner.Action` must block; spinner stops when Action returns.
+- `facades.Artisan().Call("cmd args --flag val")` passes the full string including args and flags.
diff --git a/.ai/knowledge/auth.md b/.ai/knowledge/auth.md
new file mode 100644
index 000000000..7753d6748
--- /dev/null
+++ b/.ai/knowledge/auth.md
@@ -0,0 +1,129 @@
+# Auth Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "github.com/goravel/framework/contracts/http"
+ contractsauth "github.com/goravel/framework/contracts/auth"
+ contractsaccess "github.com/goravel/framework/contracts/auth/access"
+ "github.com/goravel/framework/auth/access" // access.NewAllowResponse(), access.NewDenyResponse()
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/auth.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/auth/access/gate.go`
+
+## Available Methods
+
+**facades.Auth(ctx):** - takes `http.Context`
+
+- `Check()` bool - logged in?
+- `Guest()` bool - not logged in?
+- `Login(&user)` (string, error) - generate token
+- `LoginUsingID(id)` (string, error)
+- `Parse(token)` (\*Payload, error) - **must call before User/ID**
+- `User(&dest)` error - populate struct (requires prior Parse)
+- `ID()` (string, error) - user ID as string (requires prior Parse)
+- `Refresh()` (string, error) - new token (requires prior Parse)
+- `Logout()` error
+- `Guard(name)` GuardDriver - switch guard
+- `Extend(name, GuardFunc)` - register custom guard driver
+- `Provider(name, UserProviderFunc)` - register custom user provider
+
+**facades.Gate():**
+
+- `Define(ability, func(ctx context.Context, args map[string]any) Response)`
+- `Allows/Denies(ability, args)` bool
+- `Inspect(ability, args)` Response
+- `Any/None([]string, args)` bool
+- `Before(func(ctx, ability, args) Response)` - runs before all checks
+- `After(func(ctx, ability, args, result) Response)` - runs after checks
+- `WithContext(ctx)` Gate
+
+**access helpers:**
+
+- `access.NewAllowResponse()` Response
+- `access.NewDenyResponse(message)` Response
+
+## Implementation Example
+
+```go
+// middleware/auth.go
+package middleware
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+
+func Auth() http.Middleware {
+ return func(ctx http.Context) {
+ token := ctx.Request().Header("Authorization")
+ if _, err := facades.Auth(ctx).Parse(token); err != nil {
+ ctx.Response().String(http.StatusUnauthorized, "unauthorized").Abort()
+ return
+ }
+ ctx.Request().Next()
+ }
+}
+
+// controllers/auth_controller.go
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+ "yourmodule/app/models"
+)
+
+type AuthController struct{}
+
+func (r *AuthController) Login(ctx http.Context) http.Response {
+ var user models.User
+ facades.Orm().Query().Where("email", ctx.Request().Input("email")).First(&user)
+
+ token, err := facades.Auth(ctx).Login(&user)
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, http.Json{"token": token})
+}
+
+func (r *AuthController) Me(ctx http.Context) http.Response {
+ // Auth middleware already called Parse; User() works here
+ var user models.User
+ if err := facades.Auth(ctx).User(&user); err != nil {
+ return ctx.Response().Json(http.StatusUnauthorized, http.Json{"error": "unauthorized"})
+ }
+ return ctx.Response().Json(http.StatusOK, user)
+}
+
+// Gate definition in bootstrap/app.go WithCallback:
+// facades.Gate().Define("update-post", func(ctx context.Context, args map[string]any) contractsaccess.Response {
+// // ctx here is context.Context, NOT http.Context
+// post := args["post"].(models.Post)
+// userID := args["user_id"].(uint)
+// if userID == post.UserID { return access.NewAllowResponse() }
+// return access.NewDenyResponse("forbidden")
+// })
+```
+
+## Rules
+
+- `facades.Auth(ctx)` takes `http.Context`, not `context.Context`.
+- `User()` and `ID()` require `Parse(token)` to have been called first on the same ctx.
+- `Check()` / `Guest()` also require prior `Parse`.
+- `Guard(name)` must be called before `Login`/`User`/etc. when using non-default guard.
+- `Extend` and `Provider` must be called inside `WithCallback` in `bootstrap/app.go`.
+- `UserProvider.RetriveByID` - spelled without the second 'e' ("Retrive") - this matches the contract.
+- Gate `Define` callback receives `context.Context`, not `http.Context` - extract user from ctx value.
+- Gate `Before` returning a non-nil Response short-circuits all other `Define` callbacks.
+- Gate `After` only fires if `Define` returned a result (not short-circuited by `Before`).
+- JWT config: `config/jwt.go` for global; per-guard overrides in `config/auth.go`.
diff --git a/.ai/knowledge/bootstrap.md b/.ai/knowledge/bootstrap.md
new file mode 100644
index 000000000..06e6479ef
--- /dev/null
+++ b/.ai/knowledge/bootstrap.md
@@ -0,0 +1,144 @@
+# Bootstrap, Service Container & Providers
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/foundation"
+ contractsfoundation "github.com/goravel/framework/contracts/foundation"
+ "github.com/goravel/framework/contracts/foundation/binding"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application_builder.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/application.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/foundation/service_provider.go`
+
+## Available Methods
+
+**Service container (inside providers):**
+
+- `app.Bind(key, func(Application) (any, error))` -- new instance per resolution
+- `app.Singleton(key, func(Application) (any, error))` -- single instance
+- `app.Instance(key, obj)` -- bind pre-created object
+- `app.BindWith(key, func(Application, map[string]any) (any, error))` -- with parameters
+- `app.Make(key)` (any, error) -- resolve
+- `app.MakeWith(key, params)` (any, error) -- resolve with parameters
+
+**Via App facade (outside providers):**
+
+- `facades.App().Make(key)` (any, error)
+- `facades.App().Bind(key, fn)`
+- `facades.App().Singleton(key, fn)`
+- `facades.App().SetLocale(ctx, locale)`
+- `facades.App().CurrentLocale(ctx)` string
+- `facades.App().IsLocale(ctx, locale)` bool
+- `facades.App().Restart()` error -- used in Docker testing
+
+**bootstrap/app.go builder:**
+
+- `WithProviders(func() []ServiceProvider)`
+- `WithConfig(func())`
+- `WithRouting(func())`
+- `WithMiddleware(func(Middleware))`
+- `WithCommands(func() []Command)`
+- `WithJobs(func() []Job)`
+- `WithMigrations(func() []Migration)`
+- `WithSeeders(func() []Seeder)`
+- `WithEvents(func() map[Event][]Listener)`
+- `WithSchedule(func() []schedule.Event)`
+- `WithRules(func() []Rule)`
+- `WithFilters(func() []Filter)`
+- `WithRunners(func() []Runner)`
+- `WithCallback(func())`
+- `WithPaths(func(Paths))`
+- `Create()` Application
+
+## Implementation Example
+
+```go
+// app/providers/payment_service_provider.go
+package providers
+
+import (
+ "github.com/goravel/framework/contracts/foundation"
+ "github.com/goravel/framework/contracts/foundation/binding"
+ "yourmodule/app/facades"
+)
+
+type PaymentServiceProvider struct{}
+
+func (r *PaymentServiceProvider) Register(app foundation.Application) {
+ // Only bind into container here. Never call facades.* here.
+ app.Singleton("payment", func(app foundation.Application) (any, error) {
+ return NewPaymentService(app.MakeConfig()), nil
+ })
+}
+
+func (r *PaymentServiceProvider) Boot(app foundation.Application) {
+ // All providers registered. Facades are safe here.
+ facades.Route().Get("/payment/health", paymentController.Health)
+}
+
+// Optional: declare dependency ordering
+func (r *PaymentServiceProvider) Relationship() binding.Relationship {
+ return binding.Relationship{
+ Bindings: []string{"payment"},
+ Dependencies: []string{binding.Config, binding.Route},
+ }
+}
+
+// app/facades/payment.go
+package facades
+
+import "yourmodule/app/contracts"
+
+func Payment() contracts.PaymentService {
+ instance, err := App().Make("payment")
+ if err != nil {
+ panic(err)
+ }
+ return instance.(contracts.PaymentService)
+}
+
+// Custom runner
+type QueueRunner struct{}
+
+func (r *QueueRunner) ShouldRun() bool { return true }
+func (r *QueueRunner) Run() error { /* start worker */ ; return nil }
+func (r *QueueRunner) Shutdown() error { /* graceful stop */ ; return nil }
+
+// bootstrap/app.go -- full example
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithProviders(Providers).
+ WithConfig(config.Boot).
+ WithRouting(func() { routes.Web() }).
+ WithCallback(func() {
+ facades.Gate().Define("update-post", policies.NewPostPolicy().Update)
+ facades.RateLimiter().For("api", func(ctx contractshttp.Context) contractshttp.Limit {
+ return httplimit.PerMinute(60).By(ctx.Request().Ip())
+ })
+ facades.Orm().Observe(models.User{}, &observers.UserObserver{})
+ }).
+ WithRunners(func() []contractsfoundation.Runner {
+ return []contractsfoundation.Runner{&QueueRunner{}}
+ }).
+ Create()
+}
+```
+
+## Rules
+
+- `Register` binds things into the container only. Never call `facades.*` inside `Register` -- bindings from other providers may not exist yet.
+- `Boot` runs after all `Register` calls complete. Facades are safe here.
+- `WithCallback` runs after all `Boot` calls. Use it for gates, observers, rate limiters, Schema extensions.
+- Providers without `Relationship` run after those that declare it; ordering between them is not guaranteed.
+- Auto-generated artifacts (`make:command`, `make:job`, `make:rule`, etc.) self-register in `bootstrap/` files.
+- `Runner.ShouldRun()` returning false skips that runner entirely.
+- `facades.App().Restart()` is used in Docker tests after reconfiguring the database connection.
diff --git a/.ai/knowledge/cache.md b/.ai/knowledge/cache.md
new file mode 100644
index 000000000..ebfc0f7c8
--- /dev/null
+++ b/.ai/knowledge/cache.md
@@ -0,0 +1,112 @@
+# Cache Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "time"
+ "github.com/goravel/framework/contracts/cache"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/cache/cache.go`
+
+## Available Methods
+
+**facades.Cache():**
+
+- `Store(name)` Driver - switch to named store
+- `WithContext(ctx)` Driver - inject context
+- `Get(key, default?)` any
+- `GetBool/GetInt/GetInt64/GetString(key, default?)` T
+- `Has(key)` bool
+- `Put(key, value, ttl)` error - `ttl=0` stores forever
+- `Add(key, value, ttl)` bool - stores only if absent; returns false if key exists
+- `Forever(key, value)` bool
+- `Remember(key, ttl, func() (any, error))` (any, error)
+- `RememberForever(key, func() (any, error))` (any, error)
+- `Pull(key, default?)` any - retrieve + delete atomically
+- `Increment(key, amount?)` (int64, error)
+- `Decrement(key, amount?)` (int64, error)
+- `Forget(key)` bool
+- `Flush()` bool
+- `Lock(key, ttl?)` Lock
+
+**Lock:**
+
+- `Get(callback?)` bool - acquire immediately; auto-releases after callback
+- `Block(duration, callback?)` bool - wait up to duration; auto-releases after callback
+- `BlockWithTicker(duration, ticker, callback?)` bool - poll every `ticker` interval
+- `Release()` bool - release (ownership-aware)
+- `ForceRelease()` bool - release regardless of owner
+
+## Implementation Example
+
+```go
+package services
+
+import (
+ "fmt"
+ "time"
+ "yourmodule/app/facades"
+ "yourmodule/app/models"
+)
+
+type UserService struct{}
+
+func (s *UserService) GetUser(id int) (*models.User, error) {
+ key := fmt.Sprintf("user:%d", id)
+
+ raw, err := facades.Cache().Remember(key, 5*time.Minute, func() (any, error) {
+ var user models.User
+ if err := facades.Orm().Query().FindOrFail(&user, id); err != nil {
+ return nil, err
+ }
+ return &user, nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return raw.(*models.User), nil
+}
+
+func (s *UserService) ProcessJob(jobID string) error {
+ lock := facades.Cache().Lock("job:"+jobID, 30*time.Second)
+
+ if !lock.Block(5*time.Second, func() {
+ facades.Cache().Put("job:"+jobID+":status", "processing", 30*time.Second)
+ }) {
+ return fmt.Errorf("could not acquire lock")
+ }
+ return nil
+}
+
+// Increment/Decrement return int64
+func (s *UserService) TrackVisit(userID int) {
+ key := fmt.Sprintf("visits:%d", userID)
+ count, err := facades.Cache().Increment(key)
+ if err == nil {
+ fmt.Printf("visits: %d\n", count) // count is int64
+ }
+}
+```
+
+## Rules
+
+- `Increment`/`Decrement` return `(int64, error)`, not `(int, error)`.
+- `Put(key, value, 0)` stores forever (equivalent to `Forever`).
+- `Add` returns `false` if key already exists; it does not overwrite.
+- `Remember` only executes the closure on cache miss; stores the result automatically.
+- `Pull` is atomic: retrieve + delete in one operation.
+- `Lock.Get(func(){})` auto-releases the lock after the closure completes.
+- `Lock.Block` returns `false` if lock cannot be acquired within the timeout.
+- `BlockWithTicker` polls every `ticker` interval instead of continuously retrying.
+- `ForceRelease` ignores ownership - use only for cleanup.
+- `OnOneServer` scheduling requires `memcached`, `dynamodb`, or `redis` as the default cache.
+- Configure stores in `config/cache.go` under `stores`; set `default` to the desired store name.
diff --git a/.ai/knowledge/carbon.md b/.ai/knowledge/carbon.md
new file mode 100644
index 000000000..2596f1302
--- /dev/null
+++ b/.ai/knowledge/carbon.md
@@ -0,0 +1,205 @@
+# Carbon (Date/Time)
+
+## Import
+
+```go
+import "github.com/goravel/framework/support/carbon"
+```
+
+## Contracts
+
+Support library wrapping `dromara/carbon`. No framework contract file.
+All types (`Carbon`, `DateTime`, `Date`, `Timestamp`, etc.) are defined in the `support/carbon` package directly.
+
+Wraps `dromara/carbon`. All methods are on the `Carbon` type returned by constructors.
+
+## Constructors
+
+```go
+carbon.Now()
+carbon.Now(timezone ...string)
+carbon.Yesterday(timezone ...string)
+carbon.Tomorrow(timezone ...string)
+
+// Parse
+carbon.Parse("2020-08-05 13:14:15")
+carbon.ParseByLayout("2020-08-05 13:14:15", carbon.DateTimeLayout)
+carbon.ParseByLayout("2020|08|05", []string{"2006|01|02", "2006|1|2"}) // multiple layouts
+carbon.ParseByFormat("2020-08-05 13:14:15", carbon.DateTimeFormat)
+carbon.ParseByFormat("2020|08|05", []string{"Y|m|d", "y|m|d"}) // multiple formats
+
+// From primitives
+carbon.FromTimestamp(int64)
+carbon.FromTimestampMilli(int64)
+carbon.FromTimestampMicro(int64)
+carbon.FromTimestampNano(int64)
+carbon.FromStdTime(t time.Time)
+
+// From components
+carbon.FromDateTime(year, month, day, hour, minute, second int)
+carbon.FromDateTimeMilli(year, month, day, hour, minute, second, millisecond int)
+carbon.FromDateTimeMicro(year, month, day, hour, minute, second, microsecond int)
+carbon.FromDateTimeNano(year, month, day, hour, minute, second, nanosecond int)
+carbon.FromDate(year, month, day int)
+carbon.FromDateMilli(year, month, day, millisecond int)
+carbon.FromDateMicro(year, month, day, microsecond int)
+carbon.FromDateNano(year, month, day, nanosecond int)
+carbon.FromTime(hour, minute, second int)
+carbon.FromTimeMilli(hour, minute, second, millisecond int)
+carbon.FromTimeMicro(hour, minute, second, microsecond int)
+carbon.FromTimeNano(hour, minute, second, nanosecond int)
+```
+
+## Built-in Layout / Format Constants
+
+```go
+carbon.DateTimeLayout // "2006-01-02 15:04:05"
+carbon.DateLayout // "2006-01-02"
+carbon.TimeLayout // "15:04:05"
+carbon.DateTimeFormat // "Y-m-d H:i:s"
+carbon.DateFormat // "Y-m-d"
+carbon.TimeFormat // "H:i:s"
+```
+
+## Global Config
+
+```go
+carbon.SetTimezone(carbon.UTC) // or "Asia/Shanghai" etc.
+carbon.SetLocale("en") // affects DiffForHumans, weekday names
+carbon.SetWeekStartsAt(carbon.Monday)
+```
+
+## Arithmetic
+
+```go
+c.AddYears(n int) Carbon
+c.AddYear() Carbon
+c.SubYears(n int) Carbon
+c.AddMonths(n int) Carbon
+c.AddWeeks(n int) Carbon
+c.AddDays(n int) Carbon
+c.AddDay() Carbon
+c.SubDays(n int) Carbon
+c.AddHours(n int) Carbon
+c.AddMinutes(n int) Carbon
+c.AddSeconds(n int) Carbon
+c.SubSeconds(n int) Carbon
+```
+
+## Start / End Of Period
+
+```go
+c.StartOfDay() Carbon
+c.EndOfDay() Carbon
+c.StartOfWeek() Carbon
+c.EndOfWeek() Carbon
+c.StartOfMonth() Carbon
+c.EndOfMonth() Carbon
+c.StartOfYear() Carbon
+c.EndOfYear() Carbon
+```
+
+## Comparison
+
+```go
+c.Gt(other Carbon) bool // greater than
+c.Lt(other Carbon) bool // less than
+c.Eq(other Carbon) bool // equal
+c.Gte(other Carbon) bool
+c.Lte(other Carbon) bool
+c.Between(min, max Carbon) bool
+c.IsPast() bool
+c.IsFuture() bool
+c.IsToday() bool
+c.IsYesterday() bool
+c.IsTomorrow() bool
+c.IsWeekday() bool
+c.IsWeekend() bool
+```
+
+## Output
+
+```go
+c.String() string // "2020-08-05 13:14:15"
+c.ToDateTimeString() string
+c.ToDateString() string
+c.ToTimeString() string
+c.ToTimestamp() int64
+c.ToTimestampMilli() int64
+c.ToTimestampMicro() int64
+c.ToTimestampNano() int64
+c.ToStdTime() time.Time
+c.DiffForHumans() string // "2 hours ago"
+c.Format(layout string) string // carbon format: "Y-m-d"
+c.Layout(layout string) string // Go layout: "2006-01-02"
+```
+
+## Getters
+
+```go
+c.Year() int
+c.Month() int
+c.Day() int
+c.Hour() int
+c.Minute() int
+c.Second() int
+c.DayOfWeek() int // 0=Sunday
+c.DayOfYear() int
+c.WeekOfYear() int
+c.DaysInMonth() int
+c.Timezone() string
+```
+
+## Validity
+
+```go
+c.IsZero() bool // true when carbon holds the zero value (invalid/unparseable input)
+c.IsValid() bool // true when carbon holds a non-zero value
+```
+
+## ORM Model Fields
+
+`orm.Model` embeds `CreatedAt` and `UpdatedAt` as `carbon.DateTime`, not `time.Time`.
+Use `carbon.DateTime` (or `carbon.Date`, `carbon.Timestamp` etc.) for model date fields.
+
+```go
+import (
+ "github.com/goravel/framework/database/orm"
+ "github.com/goravel/framework/support/carbon"
+)
+
+type User struct {
+ orm.Model // CreatedAt, UpdatedAt are carbon.DateTime
+ Name string
+ Birthday carbon.Date // date only, no time component
+ LastLogin carbon.DateTime // full datetime
+ DeletedAt carbon.DateTime `gorm:"column:deleted_at"`
+}
+
+// Reading a value
+user.CreatedAt.ToDateTimeString() // "2024-01-15 10:30:00"
+user.CreatedAt.ToStdTime() // time.Time
+
+// Writing a value
+user.Birthday = carbon.NewDate(1990, 6, 15)
+user.LastLogin = carbon.NewDateTime(2024, 1, 15, 10, 30, 0)
+```
+
+## Test Time Control
+
+```go
+carbon.SetTestNow(carbon.Now()) // freeze time for tests
+carbon.CleanTestNow() // unfreeze (call in test teardown)
+carbon.IsTestNow() bool
+```
+
+## Rules
+
+- `carbon.SetTimezone` and `carbon.SetLocale` are global and affect all subsequent calls.
+- In tests, always call `carbon.CleanTestNow()` in teardown to avoid polluting other tests.
+- `ParseByLayout` accepts either a single `string` layout or `[]string` (tries each in order).
+- `ParseByFormat` also accepts either a single `string` format or `[]string`.
+- `Format` uses carbon/PHP format strings (`Y`, `m`, `d`); `Layout` uses Go time format strings (`2006`, `01`, `02`).
+- Invalid parse returns a zero `Carbon` -- check `c.IsZero()` before use.
+- `orm.Model` fields `CreatedAt` and `UpdatedAt` are `carbon.DateTime`, NOT `time.Time`. Using `time.Time` causes scan errors.
+- For model date fields use the typed carbon wrappers: `carbon.Date`, `carbon.DateTime`, `carbon.Timestamp`, `carbon.TimestampMilli`, `carbon.TimestampMicro`, `carbon.TimestampNano`.
diff --git a/.ai/knowledge/event.md b/.ai/knowledge/event.md
new file mode 100644
index 000000000..8d44b6b2d
--- /dev/null
+++ b/.ai/knowledge/event.md
@@ -0,0 +1,124 @@
+# Event Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/event"
+
+ "yourmodule/app/facades"
+ "yourmodule/app/events"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/event/events.go`
+
+## Available Methods
+
+**facades.Event():**
+
+- `Job(event Event, args []Arg)` Task - create dispatchable event task
+- `Job(...).Dispatch()` error - fire event synchronously or via queue
+
+**Registration (bootstrap/app.go):**
+
+- `WithEvents(func() map[event.Event][]event.Listener)` - map events to listeners
+
+## Implementation Example
+
+```go
+// app/events/order_shipped.go
+package events
+
+import "github.com/goravel/framework/contracts/event"
+
+type OrderShipped struct{}
+
+func NewOrderShipped() *OrderShipped { return &OrderShipped{} }
+
+func (r *OrderShipped) Handle(args []event.Arg) ([]event.Arg, error) {
+ // Transform or enrich args before passing to listeners
+ return args, nil
+}
+
+// app/listeners/send_shipment_notification.go
+package listeners
+
+import "github.com/goravel/framework/contracts/event"
+
+type SendShipmentNotification struct{}
+
+func NewSendShipmentNotification() *SendShipmentNotification {
+ return &SendShipmentNotification{}
+}
+
+func (r *SendShipmentNotification) Signature() string {
+ return "send_shipment_notification"
+}
+
+func (r *SendShipmentNotification) Queue(args ...any) event.Queue {
+ return event.Queue{
+ Enable: true, // true = async via queue
+ Connection: "redis",
+ Queue: "notifications",
+ }
+}
+
+func (r *SendShipmentNotification) Handle(args ...any) error {
+ orderID := args[0].(int)
+ // send notification logic
+ _ = orderID
+ return nil
+}
+
+// bootstrap/app.go - registration
+// WithEvents(func() map[event.Event][]event.Listener {
+// return map[event.Event][]event.Listener{
+// events.NewOrderShipped(): {
+// listeners.NewSendShipmentNotification(),
+// },
+// }
+// })
+
+// controllers/order_controller.go - dispatching
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/event"
+
+ "yourmodule/app/facades"
+ "yourmodule/app/events"
+)
+
+type OrderController struct{}
+
+func (r *OrderController) Ship(ctx http.Context) http.Response {
+ orderID := ctx.Request().RouteInt("id")
+
+ err := facades.Event().Job(&events.OrderShipped{}, []event.Arg{
+ {Type: "int", Value: orderID},
+ {Type: "string", Value: "express"},
+ }).Dispatch()
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, http.Json{"shipped": true})
+}
+```
+
+## Rules
+
+- Events and listeners are registered via `WithEvents` in `bootstrap/app.go` - `make:event`/`make:listener` auto-register.
+- `Listener.Signature()` must be unique across all listeners.
+- `Event.Handle(args)` receives the dispatched args, can modify them, and returns the (potentially modified) args to listeners.
+- `Listener.Handle(args ...any)` receives the output of `Event.Handle` as variadic `any`.
+- To stop propagation to subsequent listeners, return an `error` from `Listener.Handle`.
+- `Listener.Queue(args).Enable = true` makes the listener asynchronous - requires queue to be configured.
+- Queued listeners dispatched within a DB transaction may run before the transaction commits - place the dispatch outside the transaction when the listener depends on newly committed data.
+- `event.Arg.Type` must be one of the supported primitive types (same as `queue.Arg`).
+- `make:event` creates in `app/events/`; `make:listener` creates in `app/listeners/`.
diff --git a/.ai/knowledge/grpc.md b/.ai/knowledge/grpc.md
new file mode 100644
index 000000000..facb1a585
--- /dev/null
+++ b/.ai/knowledge/grpc.md
@@ -0,0 +1,145 @@
+# gRPC Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "net/http"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/stats"
+ contractshttp "github.com/goravel/framework/contracts/http"
+
+ proto "github.com/goravel/example-proto" // your generated proto package
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/grpc/grpc.go`
+
+## Available Methods
+
+**facades.Grpc():**
+
+- `Server()` \*grpc.Server - get the gRPC server instance (use to register services in routes)
+- `Connect(name)` (\*grpc.ClientConn, error) - get client connection by server name from config
+
+**bootstrap/app.go registration:**
+
+- `WithGrpcServerInterceptors(func() []grpc.UnaryServerInterceptor)`
+- `WithGrpcClientInterceptors(func() map[string][]grpc.UnaryClientInterceptor)` - key = interceptor group name
+- `WithGrpcServerStatsHandlers(func() []stats.Handler)`
+- `WithGrpcClientStatsHandlers(func() map[string][]stats.Handler)` - key = server name from config
+
+## Implementation Example
+
+```go
+// config/grpc.go
+config.Add("grpc", map[string]any{
+ "host": config.Env("GRPC_HOST", ""),
+ "port": config.Env("GRPC_PORT", "9000"),
+ "servers": map[string]any{ // BREAKING v1.17: was "clients"
+ "user": map[string]any{
+ "host": config.Env("GRPC_USER_HOST", "127.0.0.1"),
+ "port": config.Env("GRPC_USER_PORT", "9001"),
+ "interceptors": []string{"default"}, // client interceptor group
+ "stats_handlers": []string{"user"}, // client stats handler group
+ },
+ },
+})
+
+// routes/grpc.go - register gRPC service
+package routes
+
+import (
+ proto "github.com/goravel/example-proto"
+ "yourmodule/app/facades"
+ grpccontrollers "yourmodule/app/grpc/controllers"
+)
+
+func Grpc() {
+ proto.RegisterUserServiceServer(
+ facades.Grpc().Server(),
+ grpccontrollers.NewUserController(),
+ )
+}
+
+// app/grpc/controllers/user_controller.go
+package controllers
+
+import (
+ "context"
+ "net/http"
+ proto "github.com/goravel/example-proto"
+)
+
+type UserController struct{}
+
+func NewUserController() *UserController { return &UserController{} }
+
+func (r *UserController) GetUser(ctx context.Context, req *proto.UserRequest) (*proto.UserResponse, error) {
+ return &proto.UserResponse{
+ Code: http.StatusOK,
+ Data: &proto.User{Id: 1, Name: "Goravel", Token: req.GetToken()},
+ }, nil
+}
+
+// HTTP controller calling gRPC
+// app/http/controllers/grpc_controller.go
+package controllers
+
+import (
+ "fmt"
+ proto "github.com/goravel/example-proto"
+ contractshttp "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+
+type GrpcController struct {
+ userService proto.UserServiceClient
+}
+
+func NewGrpcController() *GrpcController {
+ conn, err := facades.Grpc().Connect("user") // BREAKING: was Client()
+ if err != nil {
+ facades.Log().Errorf("grpc connect failed: %+v", err)
+ }
+ return &GrpcController{userService: proto.NewUserServiceClient(conn)}
+}
+
+func (r *GrpcController) GetUser(ctx contractshttp.Context) contractshttp.Response {
+ resp, err := r.userService.GetUser(ctx.Context(), &proto.UserRequest{
+ Token: ctx.Request().Input("token"),
+ })
+ if err != nil {
+ return ctx.Response().String(contractshttp.StatusInternalServerError, fmt.Sprintf("%+v", err))
+ }
+ return ctx.Response().Success().Json(resp.GetData())
+}
+
+// Interceptors - bootstrap/app.go
+// WithGrpcServerInterceptors(func() []grpc.UnaryServerInterceptor {
+// return []grpc.UnaryServerInterceptor{interceptors.AuthServer}
+// }).
+// WithGrpcClientInterceptors(func() map[string][]grpc.UnaryClientInterceptor {
+// return map[string][]grpc.UnaryClientInterceptor{
+// "default": {interceptors.LogClient}, // "default" matches config interceptors array
+// }
+// })
+```
+
+## Rules
+
+- `facades.Grpc().Client("name")` is **deprecated** - use `facades.Grpc().Connect("name")`.
+- Config key is `grpc.servers` - **not** `grpc.clients` (renamed in v1.17).
+- gRPC controllers use `context.Context` (stdlib), not `http.Context` (Goravel).
+- Register gRPC services in `routes/grpc.go` → called from `WithRouting` in `bootstrap/app.go`.
+- Client interceptor map key matches the `interceptors` array value in `config/grpc.go` servers config.
+- Stats handler map key for clients matches the `stats_handlers` array value in config.
+- `Connect` returns `*grpc.ClientConn` - wrap with `proto.NewXxxServiceClient(conn)` to get typed client.
+- Instantiate gRPC clients in the controller constructor (once), not per-request.
+- Server interceptors apply to all incoming gRPC calls; client interceptors apply per-connection group.
diff --git a/.ai/knowledge/hash-crypt.md b/.ai/knowledge/hash-crypt.md
new file mode 100644
index 000000000..b46e95828
--- /dev/null
+++ b/.ai/knowledge/hash-crypt.md
@@ -0,0 +1,118 @@
+# Hash & Crypt Facades
+
+## Core Imports
+
+```go
+import (
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/hash/hash.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/crypt/crypt.go`
+
+## Available Methods
+
+**facades.Hash():**
+
+- `Make(value string)` (string, error) - hash a plain-text password (Argon2id or Bcrypt)
+- `Check(value, hashedValue string)` bool - verify plain-text against hash
+- `NeedsRehash(hashedValue string)` bool - true if hash algorithm/cost has changed
+
+**facades.Crypt():**
+
+- `EncryptString(value string)` (string, error) - AES-256-GCM encrypt with GMAC signature
+- `DecryptString(value string)` (string, error) - decrypt; returns error if GMAC invalid
+
+## Implementation Example
+
+```go
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+ "yourmodule/app/models"
+)
+
+type AuthController struct{}
+
+// Register - hash password before storing
+func (r *AuthController) Register(ctx http.Context) http.Response {
+ plain := ctx.Request().Input("password")
+
+ hashed, err := facades.Hash().Make(plain)
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+
+ user := models.User{
+ Email: ctx.Request().Input("email"),
+ Password: hashed,
+ }
+ if err := facades.Orm().Query().Create(&user); err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusCreated, http.Json{"id": user.ID})
+}
+
+// Login - verify password
+func (r *AuthController) Login(ctx http.Context) http.Response {
+ var user models.User
+ facades.Orm().Query().Where("email", ctx.Request().Input("email")).First(&user)
+
+ if !facades.Hash().Check(ctx.Request().Input("password"), user.Password) {
+ return ctx.Response().Json(http.StatusUnauthorized, http.Json{"error": "invalid credentials"})
+ }
+
+ // Rehash if needed (e.g., after changing algorithm)
+ if facades.Hash().NeedsRehash(user.Password) {
+ newHash, _ := facades.Hash().Make(ctx.Request().Input("password"))
+ facades.Orm().Query().Model(&user).Update("password", newHash)
+ }
+
+ token, _ := facades.Auth(ctx).Login(&user)
+ return ctx.Response().Json(http.StatusOK, http.Json{"token": token})
+}
+
+// Encrypt/Decrypt sensitive data
+func (r *AuthController) StoreSecret(ctx http.Context) http.Response {
+ secret := ctx.Request().Input("secret")
+
+ encrypted, err := facades.Crypt().EncryptString(secret)
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+
+ // Store `encrypted` in DB...
+
+ // Later, decrypt:
+ plain, err := facades.Crypt().DecryptString(encrypted)
+ if err != nil {
+ return ctx.Response().Json(http.StatusBadRequest, http.Json{"error": "tampered or invalid data"})
+ }
+
+ return ctx.Response().Json(http.StatusOK, http.Json{"secret": plain})
+}
+```
+
+## Rules
+
+**Hash:**
+
+- Default hashing algorithm is Argon2id; Bcrypt also supported - configure in `config/hashing.go`.
+- `Check` is timing-safe - always use it instead of direct string comparison.
+- `NeedsRehash` should be checked on login and password re-hashed if `true`.
+- Never store plain-text passwords; always hash before persisting.
+
+**Crypt:**
+
+- Requires `APP_KEY` to be set in `.env`; generate with `./artisan key:generate`.
+- Encryption uses AES-256-GCM with GMAC authentication - data integrity is verified on decrypt.
+- `DecryptString` returns an error if data has been tampered with or the key is wrong.
+- Encrypted values are base64-encoded strings safe for storage in database columns.
+- Do **not** use Crypt for passwords - use Hash instead.
diff --git a/.ai/knowledge/helpers.md b/.ai/knowledge/helpers.md
new file mode 100644
index 000000000..179be2331
--- /dev/null
+++ b/.ai/knowledge/helpers.md
@@ -0,0 +1,121 @@
+# Helpers (path / maps / collect / convert / debug / color)
+
+For fluent strings see `str.md`. For date/time see `carbon.md`.
+
+## Contracts
+
+Support utilities. No framework contract files.
+All functions are in their respective `support/*` packages.
+
+## Imports
+
+```go
+import (
+ "github.com/goravel/framework/support/path"
+ "github.com/goravel/framework/support/maps"
+ "github.com/goravel/framework/support/collect"
+ "github.com/goravel/framework/support/convert"
+ "github.com/goravel/framework/support/debug"
+ "github.com/goravel/framework/support/color"
+ supphttp "github.com/goravel/framework/support/http"
+)
+```
+
+## path -- Absolute paths (respect WithPaths config)
+
+```go
+path.App(rel ...string) // abs path to app/
+path.Base(rel ...string) // project root
+path.Config(rel ...string) // config/
+path.Database(rel ...string) // database/
+path.Storage(rel ...string) // storage/
+path.Public(rel ...string) // public/
+path.Lang(rel ...string) // lang/
+path.Resource(rel ...string) // resources/
+```
+
+## maps -- map[string]any operations
+
+```go
+maps.Add(m, key, value) // add only if key absent
+maps.Exists(m, key) bool
+maps.Forget(m, keys ...string) // remove one or more keys
+maps.Get(m, key, defaultValue) any
+maps.Has(m, keys ...string) bool // true only if ALL keys present
+maps.HasAny(m, keys ...string) bool // true if ANY key present
+maps.Only(m, keys ...string) map[string]any // return subset
+maps.Pull(m, key, defaultValue ...any) any // get + remove
+maps.Set(m, key, value)
+maps.Where(m, func(k string, v any) bool) map[string]any
+```
+
+## collect -- generic slice/map helpers
+
+```go
+collect.Count(slice) int
+collect.CountBy(slice, func(v T) bool) int
+collect.Each(slice, func(v T, i int))
+collect.Filter(slice, func(v T) bool) []T
+collect.GroupBy(slice, func(v T) string) map[string][]T
+collect.Keys(m map[K]V) []K
+collect.Map(slice, func(v T, i int) U) []U
+collect.Max(slice) T
+collect.Min(slice) T
+collect.Merge(m1, m2 map[K]V) map[K]V // m2 wins on conflict
+collect.Reverse(slice) []T
+collect.Shuffle(slice) []T
+collect.Split(slice, size int) [][]T
+collect.Sum(slice) T
+collect.Unique(slice) []T // first occurrence kept
+collect.Values(m map[K]V) []V
+```
+
+## convert -- value utilities
+
+```go
+convert.Tap(val T, fn func(T)) T // pass to fn, return original val
+convert.Transform(val T, fn func(T) U) U // convert type
+convert.With(val T, fn func(T) U) U // run fn, return result
+convert.Default(val T, fallback T) T // first non-zero value
+convert.Pointer(val T) *T // wrap in pointer
+```
+
+## debug -- variable dump
+
+```go
+debug.Dump(vars ...any) // print to stdout
+debug.SDump(vars ...any) string // return as string
+debug.FDump(w io.Writer, vars ...any) // write to writer
+```
+
+## color -- terminal output
+
+```go
+// Constructors: Red, Green, Yellow, Blue, Magenta, Cyan, White, Black, Gray, Default
+color.Red().Println("msg")
+color.Green().Printf("ok: %s", val)
+color.Blue().Sprint("info") // returns colored string without newline
+color.New(color.FgRed).Println("custom")
+
+// Methods on each color: Print / Println / Printf / Sprint / Sprintln / Sprintf
+```
+
+## supphttp.NewBody -- request body builder
+
+Used for `facades.Http()` POST bodies and test HTTP requests.
+
+```go
+body := supphttp.NewBody().SetField("name", "Alice").SetField("role", "admin")
+built, err := body.Build()
+built.ContentType() string // set as Content-Type header
+built.Reader() io.Reader // pass as request body
+```
+
+## Rules
+
+- `path.*` returns absolute paths; always correct relative to `WithPaths` config -- never construct paths manually with `filepath.Join`.
+- `maps.*` mutates the map in-place for `Add`, `Set`, `Forget`, `Pull`.
+- `collect.Unique` keeps the first occurrence of duplicates, not the last.
+- `collect.Merge` is shallow -- nested maps are not deep-merged.
+- `convert.Default` returns the first argument if it is non-zero, otherwise the second.
+- `color` output is suppressed in non-TTY environments automatically.
diff --git a/.ai/knowledge/http-client.md b/.ai/knowledge/http-client.md
new file mode 100644
index 000000000..b56535150
--- /dev/null
+++ b/.ai/knowledge/http-client.md
@@ -0,0 +1,137 @@
+# HTTP Client Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "io"
+ "net/http"
+ contractsclient "github.com/goravel/framework/contracts/http/client"
+ supphttp "github.com/goravel/framework/support/http"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/request.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/response.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/factory.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/fake_response.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/client/fake_sequence.go`
+
+## Available Methods
+
+**facades.Http():**
+
+- `Get/Head/Options(uri)` (Response, error)
+- `Post/Put/Patch/Delete(uri, body io.Reader)` (Response, error)
+- `WithHeader/WithHeaders/ReplaceHeaders/WithoutHeader/FlushHeaders(...)`
+- `Accept(contentType)` / `AcceptJSON()` / `AsForm()`
+- `WithToken(token, type?)` - default "Bearer"
+- `WithBasicAuth(user, pass)`
+- `WithQueryParameter/WithQueryParameters/WithQueryString`
+- `WithUrlParameter/WithUrlParameters`
+- `WithCookie/*Cookies`
+- `WithContext(ctx)` Request
+- `Client(name?)` Request - named client from config
+- `Fake(map[string]any)` Factory
+- `Response()` FakeResponse
+- `Sequence()` FakeSequence
+- `Reset()` - clear all fakes
+- `AssertSent/AssertNotSent/AssertNothingSent/AssertSentCount`
+
+## Implementation Example
+
+```go
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+ supphttp "github.com/goravel/framework/support/http"
+ "yourmodule/app/facades"
+)
+
+type GithubService struct{}
+
+type GithubUser struct {
+ ID int `json:"id"`
+ Login string `json:"login"`
+}
+
+// GET request
+func (s *GithubService) GetUser(username string) (*GithubUser, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ resp, err := facades.Http().
+ WithContext(ctx).
+ AcceptJSON().
+ WithToken("ghp_xxx").
+ WithUrlParameter("username", username).
+ Get("https://api.github.com/users/{username}")
+ if err != nil {
+ return nil, err
+ }
+ if !resp.Successful() {
+ return nil, fmt.Errorf("request failed: %d", resp.Status())
+ }
+ var user GithubUser
+ return &user, resp.Bind(&user)
+}
+
+// POST with body
+func (s *GithubService) CreateIssue(repo string, title string) error {
+ body := supphttp.NewBody().
+ SetField("title", title).
+ SetField("body", "Created via API")
+ built, err := body.Build()
+ if err != nil {
+ return err
+ }
+ resp, err := facades.Http().
+ WithToken("ghp_xxx").
+ WithHeader("Content-Type", built.ContentType()).
+ Post("https://api.github.com/repos/"+repo+"/issues", built.Reader())
+ if err != nil {
+ return err
+ }
+ if !resp.Created() {
+ return fmt.Errorf("failed to create issue: %d", resp.Status())
+ }
+ return nil
+}
+
+// Testing with fakes
+// func TestGetUser(t *testing.T) {
+// defer facades.Http().Reset()
+// facades.Http().Fake(map[string]any{
+// "https://api.github.com/*": facades.Http().Response().Json(200, map[string]any{
+// "id": 1, "login": "goravel",
+// }),
+// })
+// user, err := (&GithubService{}).GetUser("goravel")
+// assert.Nil(t, err)
+// assert.Equal(t, "goravel", user.Login)
+// facades.Http().AssertSentCount(1)
+// }
+```
+
+## Rules
+
+- `Post/Put/Patch/Delete` all take `io.Reader` as body - use `supphttp.NewBody().Build()` or pass `nil`.
+- `Response.Bind(&dest)` on the response object - **not** `Request.Bind`.
+- `Response.Stream()` returns `(io.ReadCloser, error)` for streaming bodies.
+- `facades.Http().Reset()` must be deferred in every test using `Fake`; fakes are global state.
+- Never use `t.Parallel()` with `Fake` - it mutates global state, causing race conditions.
+- `"*"` as fake key matches all unmatched URLs.
+- `PreventStrayRequests()` panics if a request is made without a matching fake.
+- `Sequence` exhaustion returns error unless `WhenEmpty` is set.
+- `WithToken("tok")` defaults to `"Bearer"` scheme; pass second arg for custom scheme.
+- `WithUrlParameter("key", "val")` replaces `{key}` in the URL path.
+- HTTP client configs (timeouts, base URLs) are in `config/http.go` under `clients`.
diff --git a/.ai/knowledge/localization.md b/.ai/knowledge/localization.md
new file mode 100644
index 000000000..6735fa73f
--- /dev/null
+++ b/.ai/knowledge/localization.md
@@ -0,0 +1,120 @@
+# Localization Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/translation"
+ "github.com/goravel/framework/contracts/http"
+
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/translation/translator.go`
+
+## Available Methods
+
+**facades.Lang(ctx):** - takes `http.Context`
+
+- `Get(key string, opts ...translation.Option)` string - translate key
+- `Choice(key string, count int, opts ...translation.Option)` string - pluralized translation
+
+**facades.App():**
+
+- `SetLocale(ctx http.Context, locale string)` - change locale for request
+- `CurrentLocale(ctx http.Context)` string - get current locale
+- `IsLocale(ctx http.Context, locale string)` bool
+
+## Implementation Example
+
+```go
+// lang/en.json
+// {
+// "welcome": "Welcome, :name!",
+// "messages": {
+// "saved": "Record saved"
+// },
+// "apples": "There is one apple|There are many apples",
+// "items": "{0} None|[1,19] Some|[20,*] Many"
+// }
+
+// lang/en/user.json (categorized)
+// { "profile": "Your profile" }
+
+package controllers
+
+import (
+ "github.com/goravel/framework/translation"
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+
+type LangController struct{}
+
+func (r *LangController) Index(ctx http.Context) http.Response {
+ // Simple key
+ msg := facades.Lang(ctx).Get("messages.saved")
+
+ // With placeholder replacement (:name → "Alice")
+ welcome := facades.Lang(ctx).Get("welcome", translation.Option{
+ Replace: map[string]string{"name": "Alice"},
+ })
+
+ // Categorized file: "user/profile" = lang/en/user.json → "profile" key
+ profile := facades.Lang(ctx).Get("user/profile")
+
+ // Pluralization
+ one := facades.Lang(ctx).Choice("apples", 1) // "There is one apple"
+ many := facades.Lang(ctx).Choice("apples", 5) // "There are many apples"
+ none := facades.Lang(ctx).Choice("items", 0) // "None"
+
+ // Switch locale per-request
+ facades.App().SetLocale(ctx, "zh_CN")
+ translated := facades.Lang(ctx).Get("welcome", translation.Option{
+ Replace: map[string]string{"name": "Alice"},
+ })
+
+ return ctx.Response().Json(http.StatusOK, http.Json{
+ "msg": msg,
+ "welcome": welcome,
+ "profile": profile,
+ "one": one,
+ "many": many,
+ "none": none,
+ "translated": translated,
+ })
+}
+```
+
+### Embed lang files into binary (optional)
+
+```go
+// lang/fs.go
+package lang
+
+import "embed"
+
+//go:embed *
+var FS embed.FS
+
+// config/app.go
+"lang_path": "lang",
+"lang_fs": lang.FS,
+```
+
+## Rules
+
+- `facades.Lang(ctx)` takes `http.Context`, not `context.Context`.
+- Default and fallback locale set in `config/app.go` under `locale` and `fallback_locale`.
+- Lang files: flat JSON (`lang/en.json`) or categorized (`lang/en/user.json`).
+- Categorized key format: `"category/key"` or `"category/nested.key"`.
+- Placeholders use `:name` syntax in JSON values - replaced via `Option.Replace`.
+- `Choice` pluralization formats: `"one|many"` (count 1 = first, else second); `"{0} x|[1,5] y|[6,*] z"` (range-based).
+- When both file and embed exist for the same path, the file takes priority; embed is the fallback.
+- `SetLocale` changes locale for the current request context only - does not affect other requests.
+- Missing key returns the key string itself as fallback.
diff --git a/.ai/knowledge/log.md b/.ai/knowledge/log.md
new file mode 100644
index 000000000..275f9d078
--- /dev/null
+++ b/.ai/knowledge/log.md
@@ -0,0 +1,92 @@
+# Log Facade
+
+## Core Imports
+
+```go
+import (
+ contractslog "github.com/goravel/framework/contracts/log"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/log/log.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/log/level.go`
+
+## Available Methods
+
+- `WithContext(ctx)` Log
+- `Channel(name)` Log
+- `Stack([]string)` Log
+- `Debug/Info/Warning/Error/Fatal/Panic(args ...any)`
+- `Debugf/Infof/Warningf/Errorf/Fatalf/Panicf(format, args...)`
+- `Code(string)` Writer
+- `Hint(string)` Writer
+- `In(domain string)` Writer
+- `Owner(any)` Writer
+- `Request(http.ContextRequest)` Writer
+- `Response(http.ContextResponse)` Writer
+- `Tags(tags ...string)` Writer
+- `User(any)` Writer
+- `With(map[string]any)` Writer - pass a map, NOT key/value pair
+- `WithTrace()` Writer
+
+## Implementation Example
+
+```go
+package services
+
+import "yourmodule/app/facades"
+
+func Process(userID int, orderID int) error {
+ // Simple
+ facades.Log().Info("processing order")
+ facades.Log().Infof("order %d for user %d", orderID, userID)
+
+ // Rich context - With takes a MAP
+ facades.Log().
+ User(userID).
+ With(map[string]any{"order_id": orderID, "amount": 99.99}).
+ Tags("orders", "billing").
+ In("billing").
+ Code("ORD-001").
+ Hint("check payment gateway if this fails").
+ Info("payment initiated")
+
+ // With stack trace on error
+ if err := doWork(); err != nil {
+ facades.Log().
+ WithTrace().
+ User(userID).
+ With(map[string]any{"order_id": orderID}).
+ Error(err)
+ return err
+ }
+
+ // Specific channel
+ facades.Log().Channel("slack").Infof("order %d completed", orderID)
+
+ // Multiple channels simultaneously
+ facades.Log().Stack([]string{"daily", "slack"}).Error("critical failure")
+
+ return nil
+}
+
+func doWork() error { return nil }
+```
+
+## Rules
+
+- `With(data map[string]any)` takes a **map**, not `(key, value)` pair arguments.
+- `Request(req)` takes `http.ContextRequest` (from `ctx.Request()`), not `*http.Request`.
+- `Response(res)` takes `http.ContextResponse` (from `ctx.Response()`), not `*http.Response`.
+- `Fatal` calls `os.Exit(1)` after logging - do not use in goroutines where cleanup is needed.
+- `Panic` panics after logging - will trigger any registered `recover` middleware.
+- Chain methods are order-independent; must precede the terminal level call.
+- Custom driver: implement `Logger` interface (`Handle(channel) (Handler, error)`), not the deprecated `Hook`.
+- `Handler` has `Enabled(Level) bool` and `Handle(Entry) error`.
+- Use `log.HookToHandler(hook)` adapter only if adapting a legacy `Hook` implementation.
+- Default channel is `stack`; configure in `config/logging.go`.
diff --git a/.ai/knowledge/mail.md b/.ai/knowledge/mail.md
new file mode 100644
index 000000000..efe120304
--- /dev/null
+++ b/.ai/knowledge/mail.md
@@ -0,0 +1,127 @@
+# Mail Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/mail"
+ contractsmail "github.com/goravel/framework/contracts/mail"
+
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/mail/mail.go`
+
+## Available Methods
+
+**Fluent builder (facades.Mail()):**
+
+- `To([]string)` Mail - set recipients
+- `Cc([]string)` Mail - set CC recipients
+- `Bcc([]string)` Mail - set BCC recipients
+- `From(mail.Address(addr, name))` Mail - override sender
+- `Subject(string)` Mail - set subject
+- `Content(mail.Content{...})` Mail - set HTML body or template
+- `Attach([]string)` Mail - attach files by path
+- `Headers(map[string]string)` Mail - set custom headers
+- `Send()` error - send immediately
+- `Queue(queueConfig?)` error - push to queue (optional `mail.Queue()` config)
+
+**Mailable pattern:**
+
+- `facades.Mail().Send(mailable)` error - send from Mailable struct
+- `facades.Mail().Queue(mailable)` error - queue from Mailable struct
+
+**mail helpers:**
+
+- `mail.Html(htmlString)` Content - shorthand for raw HTML content
+- `mail.Address(address, name)` From - construct From struct
+- `mail.Queue()` Queue builder - `.Connection(name).Queue(name)` for custom queue
+
+## Implementation Example
+
+```go
+// Direct fluent send
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/mail"
+ "yourmodule/app/facades"
+)
+
+type OrderController struct{}
+
+func (r *OrderController) SendConfirmation(ctx http.Context) http.Response {
+ err := facades.Mail().
+ To([]string{"customer@example.com"}).
+ Cc([]string{"support@example.com"}).
+ Subject("Order Confirmed").
+ Content(mail.Html("
Your order #123 is confirmed!
")).
+ Attach([]string{"./storage/invoices/123.pdf"}).
+ Headers(map[string]string{"X-Order-ID": "123"}).
+ Send()
+
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, http.Json{"sent": true})
+}
+
+// Mailable struct pattern
+// app/mails/order_shipped.go
+package mails
+
+import "github.com/goravel/framework/contracts/mail"
+
+type OrderShipped struct {
+ OrderID int
+}
+
+func NewOrderShipped(id int) *OrderShipped { return &OrderShipped{OrderID: id} }
+
+func (m *OrderShipped) Attachments() []string { return []string{} }
+
+func (m *OrderShipped) Content() *mail.Content {
+ return &mail.Content{
+ View: "order_shipped.tmpl",
+ With: map[string]any{"OrderID": m.OrderID},
+ }
+}
+
+func (m *OrderShipped) Envelope() *mail.Envelope {
+ return &mail.Envelope{
+ To: []string{"customer@example.com"},
+ Subject: fmt.Sprintf("Order #%d Shipped", m.OrderID),
+ }
+}
+
+func (m *OrderShipped) Headers() map[string]string {
+ return map[string]string{"X-Mailer": "Goravel"}
+}
+
+func (m *OrderShipped) Queue() *mail.Queue {
+ return &mail.Queue{Connection: "redis", Queue: "mail"}
+}
+
+// Usage:
+// facades.Mail().Send(mails.NewOrderShipped(123))
+// facades.Mail().Queue(mails.NewOrderShipped(123))
+```
+
+## Rules
+
+- Configure SMTP in `config/mail.go`; global sender from `MAIL_FROM_ADDRESS` / `MAIL_FROM_NAME`.
+- Custom `From` address must be authorized by the configured SMTP server.
+- `Content(mail.Html(str))` sends raw HTML; `Content(mail.Content{View: "file.tmpl", With: data})` uses template engine.
+- Templates are loaded from the path configured in `config/mail.go` under `template.engines.html.path`.
+- `Queue()` with no argument uses the default queue; pass `mail.Queue().Connection("c").Queue("q")` for custom.
+- `Mailable.Queue()` returning `nil` causes `facades.Mail().Queue(mailable)` to send synchronously.
+- `make:mail OrderShipped` generates scaffold in `app/mails/` directory.
+- Attach paths are relative to the application working directory.
+- Template engine defaults to `html/template`; custom engines implement `contracts/mail.Template`.
diff --git a/.ai/knowledge/migration.md b/.ai/knowledge/migration.md
new file mode 100644
index 000000000..4c0d44c84
--- /dev/null
+++ b/.ai/knowledge/migration.md
@@ -0,0 +1,118 @@
+# Migration & Schema Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/database/schema"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/schema.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/blueprint.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/schema/index.go`
+
+## Available Methods
+
+**facades.Schema():**
+
+- `Create(table, func(Blueprint))` error - create new table
+- `Table(table, func(Blueprint))` error - modify existing table
+- `Drop(table)` error
+- `DropIfExists(table)` error
+- `Rename(from, to)` error
+- `HasTable(table)` bool
+- `HasColumn(table, column)` bool
+- `HasColumns(table, []string)` bool
+- `HasIndex(table, index)` bool
+- `Connection(name)` Schema - use specific DB connection
+- `Extend(Extension)` - register custom Go types for `make:model --table`
+
+**Migration struct methods:**
+
+- `Signature() string` - unique identifier (timestamp_name format)
+- `Up() error` - apply migration
+- `Down() error` - reverse migration
+- `Connection() string` - (optional) override DB connection
+
+## Implementation Example
+
+```go
+// database/migrations/20240101_create_users_table.go
+package migrations
+
+import (
+ "github.com/goravel/framework/contracts/database/schema"
+ "yourmodule/app/facades"
+)
+
+type M20240101000000CreateUsersTable struct{}
+
+func (r *M20240101000000CreateUsersTable) Signature() string {
+ return "20240101000000_create_users_table"
+}
+
+func (r *M20240101000000CreateUsersTable) Up() error {
+ if facades.Schema().HasTable("users") {
+ return nil
+ }
+ return facades.Schema().Create("users", func(table schema.Blueprint) {
+ table.ID()
+ table.String("name").Comment("display name")
+ table.String("email").Unique()
+ table.String("password")
+ table.Boolean("active").Default(true)
+ table.Json("settings").Nullable()
+ table.Timestamps()
+ table.SoftDeletes()
+ })
+}
+
+func (r *M20240101000000CreateUsersTable) Down() error {
+ return facades.Schema().DropIfExists("users")
+}
+
+// Adding columns to existing table
+type M20240102000000AddAvatarToUsersTable struct{}
+
+func (r *M20240102000000AddAvatarToUsersTable) Signature() string {
+ return "20240102000000_add_avatar_to_users_table"
+}
+
+func (r *M20240102000000AddAvatarToUsersTable) Up() error {
+ return facades.Schema().Table("users", func(table schema.Blueprint) {
+ table.String("avatar").Nullable().After("email")
+ table.UnsignedBigInteger("role_id").Nullable()
+ table.Foreign("role_id").References("id").On("roles")
+ table.Index("role_id")
+ })
+}
+
+func (r *M20240102000000AddAvatarToUsersTable) Down() error {
+ return facades.Schema().Table("users", func(table schema.Blueprint) {
+ table.DropForeign("role_id")
+ table.DropColumn("avatar", "role_id")
+ })
+}
+```
+
+## Rules
+
+- `Signature()` must be unique - conventional format: `YYYYMMDDHHMMSS_description`.
+- `make:migration` auto-registers in `bootstrap/migrations.go`; manual files must be added there.
+- Always guard `Up()` with `HasTable`/`HasColumn` checks to make migrations idempotent.
+- `Down()` should exactly reverse `Up()` - always implement it.
+- `migrate:rollback` (v1.17) rolls back the **entire last batch**, not one migration - use `--step=N` for finer control.
+- `Change()` modifier (column modification) is supported on MySQL, PostgreSQL, Sqlserver only.
+- `After()` (position) is **MySQL only**. `Always()`/`GeneratedAs()` are **PostgreSQL only**.
+- `Enum` on PostgreSQL/SQLite/Sqlserver is stored as string - not a native ENUM type.
+- `Morphs("taggable")` creates `taggable_id` (uint) + `taggable_type` (string) columns.
+- Foreign key: call `.References("id").On("table")` chained on `.Foreign("col")`.
+- `DropForeign("col")` drops by column name convention; `DropForeignByName("name")` by explicit constraint name.
+- `facades.Schema().Connection("postgres")` overrides the connection for that schema operation.
+- Register models for `make:model --table` via `facades.Schema().Extend()` in `WithCallback`.
diff --git a/.ai/knowledge/orm.md b/.ai/knowledge/orm.md
new file mode 100644
index 000000000..a9dd0bc3d
--- /dev/null
+++ b/.ai/knowledge/orm.md
@@ -0,0 +1,183 @@
+# ORM Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/database/orm"
+ contractsorm "github.com/goravel/framework/contracts/database/orm"
+ "github.com/goravel/framework/contracts/database/factory"
+ "github.com/goravel/framework/database/db" // for db.Raw()
+ "github.com/goravel/framework/errors" // for errors.OrmRecordNotFound
+ "github.com/goravel/framework/support/carbon" // for carbon.DateTime model fields
+
+ "yourmodule/app/facades"
+ "yourmodule/database/factories"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/orm.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/events.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/observer.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/orm/factory.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/database/factory/factory.go`
+
+## Available Methods
+
+**facades.Orm():**
+
+- `Query()` Query - get default DB query instance
+- `Connection(name)` Orm - switch database connection
+- `WithContext(ctx)` Orm - inject context
+- `DB()` (\*sql.DB, error) - raw sql.DB instance
+- `Transaction(func(Query) error)` error - atomic transaction
+- `Observe(model, observer)` - register model observer
+- `Factory()` Factory - get factory builder for the model
+
+**facades.Orm().Factory():**
+
+- `Make(&model, overrides ...map[string]any)` error - build without saving
+- `Create(&model, overrides ...map[string]any)` error - build and save to DB
+- `CreateQuietly(&model, overrides ...map[string]any)` error - create without model events
+- `Count(n int)` Factory - produce a collection of n models
+- `Times(n int)` Factory - alias for Count
+
+**Query (chained):**
+
+- `Find(&dest, id?)` error - nil error if not found (use FindOrFail for error)
+- `FindOrFail(&dest, id)` error - errors if not found
+- `First(&dest)` error - first record ordered by PK
+- `FirstOrFail(&dest)` error - errors if not found
+- `Get(&dest)` error - all matching records
+- `Create(&value)` error - INSERT; auto-fills CreatedAt/UpdatedAt
+- `Save(&value)` error - full UPDATE of existing model
+- `Update(col, val)` error - UPDATE specific column(s)
+- `Delete(&value)` (Result, error) - soft delete (or hard if no SoftDeletes)
+- `ForceDelete(&value)` (Result, error) - bypass soft delete
+- `WithTrashed()` Query - include soft-deleted records
+- `Restore(&value)` error - undelete soft-deleted record
+- `Count()` (int64, error)
+- `Exists()` (bool, error)
+- `Sum(col, &dest)` error - aggregate into pointer
+- `Paginate(page, perPage, &dest, &total)` error
+- `With("Relation")` Query - eager load
+- `Load(&model, "Relation")` error - lazy eager load
+- `Association("Field")` Association - manage has-many/m2m
+
+## Implementation Example
+
+```go
+// app/models/user.go
+package models
+
+import (
+ "github.com/goravel/framework/contracts/database/factory"
+ "github.com/goravel/framework/database/orm"
+ "github.com/goravel/framework/support/carbon"
+ "yourmodule/database/factories"
+)
+
+type User struct {
+ orm.Model // ID, CreatedAt carbon.DateTime, UpdatedAt carbon.DateTime
+ Name string
+ Email string
+ Birthday carbon.Date // date-only field; use carbon types, not time.Time
+ orm.SoftDeletes // DeletedAt carbon.DateTime
+}
+
+// Bind factory for test seeding
+func (u *User) Factory() factory.Factory {
+ return &factories.UserFactory{}
+}
+
+// Optional overrides
+func (u *User) TableName() string { return "goravel_user" }
+func (u *User) Connection() string { return "postgres" }
+
+// controllers/user_controller.go
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ contractsorm "github.com/goravel/framework/contracts/database/orm"
+ "github.com/goravel/framework/errors"
+
+ "yourmodule/app/facades"
+ "yourmodule/app/models"
+)
+
+type UserController struct{}
+
+func (r *UserController) Index(ctx http.Context) http.Response {
+ var users []models.User
+ if err := facades.Orm().WithContext(ctx.Context()).Query().
+ With("Posts").
+ Where("active", 1).
+ OrderBy("created_at", "desc").
+ Paginate(1, 15, &users, new(int64)); err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, users)
+}
+
+func (r *UserController) Show(ctx http.Context) http.Response {
+ var user models.User
+ err := facades.Orm().Query().FindOrFail(&user, ctx.Request().RouteInt("id"))
+ if errors.Is(err, errors.OrmRecordNotFound) {
+ return ctx.Response().Json(http.StatusNotFound, http.Json{"error": "not found"})
+ }
+ return ctx.Response().Json(http.StatusOK, user)
+}
+
+func (r *UserController) Store(ctx http.Context) http.Response {
+ user := models.User{Name: ctx.Request().Input("name")}
+ if err := facades.Orm().Query().Create(&user); err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusCreated, user)
+}
+
+func (r *UserController) Transaction(ctx http.Context) http.Response {
+ err := facades.Orm().Transaction(func(tx contractsorm.Query) error {
+ user := models.User{Name: "Alice"}
+ if err := tx.Create(&user); err != nil {
+ return err
+ }
+ return tx.Model(&models.Role{}).Where("id", 1).Update("user_id", user.ID)
+ })
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, http.Json{"ok": true})
+}
+
+// Factory usage (in tests or seeders)
+// var user models.User
+// facades.Orm().Factory().Create(&user)
+// facades.Orm().Factory().Count(5).Create(&users)
+// facades.Orm().Factory().Create(&user, map[string]any{"Name": "Alice"})
+// facades.Orm().Factory().CreateQuietly(&user) // no model events
+```
+
+## Rules
+
+- `Find(&model, id)` returns **nil error** when record not found - use `FindOrFail` to error on missing.
+- Struct `Update(struct{})` skips zero-value fields - use `map[string]any` to set zero values explicitly.
+- `GlobalScopes()` must return `map[string]func(contractsorm.Query) contractsorm.Query` - **not** a slice.
+- `Sum(column, &dest)` signature: `error` only (dest is a pointer to the result variable).
+- Model events only trigger when operating on a model instance. Batch operations (`Table("users").Delete()`) do **not** trigger events.
+- If both `DispatchesEvents` and `Observer` are set, only `DispatchesEvents` applies.
+- `WithoutEvents()` suppresses all model events for that query chain.
+- `SaveQuietly` saves without dispatching any events.
+- Relations use struct field names (PascalCase), not column names: `.With("Posts")` not `.With("posts")`.
+- `Association("Posts").Find(&posts)` uses the parent model instance already loaded.
+- `Cursor()` - do not use `With()` in the same query; use `Load()` inside the loop instead.
+- `ForceDelete` permanently removes records even if `orm.SoftDeletes` is embedded.
+- Default connection is set in `config/database.go`; override per-query with `Connection("name")`.
+- `orm.Model` embeds `CreatedAt` and `UpdatedAt` as `carbon.DateTime`, NOT `time.Time`. Using `time.Time` causes scan errors.
+- For custom date/time model fields use carbon types: `carbon.Date`, `carbon.DateTime`, `carbon.Timestamp`.
+- Factory `Definition()` map keys must be PascalCase struct field names, not column names.
diff --git a/.ai/knowledge/process.md b/.ai/knowledge/process.md
new file mode 100644
index 000000000..1a2f717ee
--- /dev/null
+++ b/.ai/knowledge/process.md
@@ -0,0 +1,139 @@
+# Process Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "io"
+ "os"
+ "time"
+ contractsprocess "github.com/goravel/framework/contracts/process"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/process.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/pipeline.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/pool.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/result.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running_pipe.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/process/running_pool.go`
+
+## Available Methods
+
+**facades.Process() - builder (chainable):**
+
+- `Path(dir)` - working directory
+- `Timeout(d)` - kill after duration
+- `Env(map[string]string)` - add env vars (inherits system)
+- `Input(io.Reader)` - pipe to stdin
+- `Quietly()` - capture but suppress terminal output
+- `DisableBuffering()` - do not buffer in memory (use with `OnOutput`)
+- `OnOutput(func(OutputType, []byte))` - stream output
+- `WithContext(ctx)` - bind to context
+- `TTY()` - interactive terminal; output not captured; `Input()` is ignored
+- `WithSpinner(message?)` - show spinner while running
+
+**Execution:**
+
+- `Run(name, args...)` Result - synchronous
+- `Start(name, args...)` (Running, error) - async; **must call `.Wait()`**
+- `Pipe(func(Pipe))` Pipeline - define pipe; set options **after** `Pipe()` call
+- `Pool(func(Pool))` PoolBuilder - define concurrent processes
+
+## Implementation Example
+
+```go
+package services
+
+import (
+ "fmt"
+ "os"
+ "time"
+ contractsprocess "github.com/goravel/framework/contracts/process"
+ "yourmodule/app/facades"
+)
+
+// Synchronous
+func RunBuild() error {
+ result := facades.Process().
+ Path("/var/www/app").
+ Timeout(5 * time.Minute).
+ Env(map[string]string{"NODE_ENV": "production"}).
+ Run("npm", "run", "build")
+ if result.Failed() {
+ return fmt.Errorf("exit %d: %s", result.ExitCode(), result.ErrorOutput())
+ }
+ return nil
+}
+
+// Async - must call Wait()
+func RunAsync() error {
+ running, err := facades.Process().Timeout(10 * time.Second).Start("sleep", "5")
+ if err != nil { return err }
+
+ select {
+ case <-running.Done():
+ case <-time.After(8 * time.Second):
+ running.Stop(2 * time.Second)
+ }
+ result := running.Wait() // always call Wait()
+ if result.Failed() { return result.Error() }
+ return nil
+}
+
+// Pipeline - options applied AFTER Pipe()
+func RunPipeline() error {
+ result := facades.Process().
+ Pipe(func(pipe contractsprocess.Pipe) {
+ pipe.Command("cat", "app.log").As("source")
+ pipe.Command("grep", "ERROR").As("filter")
+ }).
+ Timeout(30 * time.Second). // options AFTER Pipe()
+ OnOutput(func(typ contractsprocess.OutputType, line []byte, key string) {
+ fmt.Printf("[%s] %s", key, line) // key identifies which stage
+ }).
+ Run()
+ if result.Failed() { return result.Error() }
+ return nil
+}
+
+// Pool - concurrent processes
+func RunPool() error {
+ results, err := facades.Process().
+ Pool(func(pool contractsprocess.Pool) {
+ pool.Command("npm", "test").As("tests")
+ pool.Command("go", "vet", "./...").As("vet")
+ }).
+ Concurrency(2).
+ Timeout(10 * time.Minute).
+ Run()
+ if err != nil { return err }
+ for key, res := range results {
+ if res.Failed() {
+ return fmt.Errorf("%s failed: %s", key, res.ErrorOutput())
+ }
+ }
+ return nil
+}
+```
+
+## Rules
+
+- `Run("ls -la")` - shell string with spaces triggers `/bin/sh -c` wrapper automatically.
+- `Start()` is async - **always** call `.Wait()` to reap OS resources, even after `Done()`.
+- Pipeline options (`Timeout`, `Env`, `WithContext`) set **before** `Pipe()` are **ignored** - apply them after `Pipe()`.
+- `DisableBuffering` - `Result.Output()` and `Result.ErrorOutput()` return empty; use `OnOutput` to consume.
+- `TTY()` - output is not captured; `Input()` is ignored; your terminal becomes the subprocess stdin.
+- `Pool.OnOutput` callback is called from multiple goroutines - ensure thread safety.
+- `Pool.PoolCommand` supports per-process overrides: `Path`, `Timeout`, `Env`, `Input`, `Quietly`, `DisableBuffering`, `WithContext`.
+- `Pool.Run()` returns `map[string]Result` keyed by `.As(key)` names.
+- `Stop(timeout, sig?)` sends SIGTERM, then SIGKILL after timeout if process is still alive.
+- `WithSpinner` shows a terminal spinner while the process runs.
+- `Process` facade is registered via `&process.ServiceProvider{}` in `bootstrap/providers.go`.
diff --git a/.ai/knowledge/queue.md b/.ai/knowledge/queue.md
new file mode 100644
index 000000000..83985f337
--- /dev/null
+++ b/.ai/knowledge/queue.md
@@ -0,0 +1,116 @@
+# Queue Facade
+
+## Core Imports
+
+```go
+import (
+ "time"
+ "github.com/goravel/framework/contracts/queue"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/queue/job.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/queue/queue.go`
+
+## Available Methods
+
+**facades.Queue():**
+
+- `Job(job Job, args ...[]Arg)` PendingJob - create dispatchable task
+- `Chain([]ChainJob)` PendingJob - create chained sequence
+- `Worker(payloads ...Args)` Worker - create a queue worker
+- `Register(jobs []Job)` - register jobs manually
+
+**PendingJob:**
+
+- `.Dispatch()` error - push to queue
+- `.DispatchSync()` error - execute immediately in current process
+- `.Delay(time.Time)` PendingJob - delay until absolute time
+- `.OnQueue(name)` PendingJob - named queue
+- `.OnConnection(name)` PendingJob - named connection
+
+## Implementation Example
+
+```go
+// app/jobs/process_podcast.go
+package jobs
+
+import "time"
+
+type ProcessPodcast struct{}
+
+func (r *ProcessPodcast) Signature() string { return "process_podcast" }
+
+func (r *ProcessPodcast) Handle(args ...any) error {
+ podcastID := args[0].(int)
+ _ = podcastID
+ return nil
+}
+
+// Optional retry control
+func (r *ProcessPodcast) ShouldRetry(err error, attempt int) (bool, time.Duration) {
+ if attempt < 3 { return true, 10 * time.Second }
+ return false, 0
+}
+
+// app/jobs/send_notification.go
+type SendNotification struct{}
+func (r *SendNotification) Signature() string { return "send_notification" }
+func (r *SendNotification) Handle(args ...any) error { return nil }
+
+// controllers/podcast_controller.go
+package controllers
+
+import (
+ "time"
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/queue"
+ "yourmodule/app/facades"
+ "yourmodule/app/jobs"
+)
+
+type PodcastController struct{}
+
+func (r *PodcastController) Store(ctx http.Context) http.Response {
+ podcastID := 42
+
+ // Basic dispatch
+ _ = facades.Queue().Job(&jobs.ProcessPodcast{}, []queue.Arg{
+ {Type: "int", Value: podcastID},
+ }).Dispatch()
+
+ // Delayed to specific time
+ _ = facades.Queue().Job(&jobs.ProcessPodcast{}, []queue.Arg{
+ {Type: "int", Value: podcastID},
+ }).Delay(time.Now().Add(5 * time.Minute)).OnQueue("podcasts").Dispatch()
+
+ // Chained - stops on first failure
+ _ = facades.Queue().Chain([]queue.ChainJob{
+ {Job: &jobs.ProcessPodcast{}, Args: []queue.Arg{{Type: "int", Value: podcastID}}},
+ {Job: &jobs.SendNotification{}, Args: []queue.Arg{{Type: "string", Value: "done"}}},
+ }).Dispatch()
+
+ return ctx.Response().Json(http.StatusAccepted, http.Json{"queued": true})
+}
+```
+
+## Rules
+
+- Register jobs via `WithJobs(Jobs)` in `bootstrap/app.go`; `make:job` auto-registers in `bootstrap/jobs.go`.
+- `Signature()` must be unique across all jobs.
+- `Handle(args ...any)` receives args in the same order as `[]Arg` passed at dispatch.
+- `Arg.Type` must be a supported primitive string - see list below.
+- `Chain` stops at first job failure; subsequent jobs are not executed.
+- `Delay` takes `time.Time` (absolute), not `time.Duration` - use `time.Now().Add(d)`.
+- `DispatchSync` bypasses the queue entirely; executes in the current goroutine.
+- `Jobs` type alias for `ChainJob` is deprecated; use `ChainJob` directly.
+- Machinery driver is removed; available drivers: `sync`, `database`, `redis` (via `goravel/redis`).
+- Configure connections in `config/queue.go`; set `default` connection name.
+
+**Supported `Arg.Type` values:**
+`bool`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `string`, `[]bool`, `[]int`, `[]int8`, `[]int16`, `[]int32`, `[]int64`, `[]uint`, `[]uint8`, `[]uint16`, `[]uint32`, `[]uint64`, `[]float32`, `[]float64`, `[]string`
diff --git a/.ai/knowledge/request-response.md b/.ai/knowledge/request-response.md
new file mode 100644
index 000000000..cea93e671
--- /dev/null
+++ b/.ai/knowledge/request-response.md
@@ -0,0 +1,132 @@
+# Request & Response
+
+## Core Imports
+
+```go
+import (
+ "net/http"
+ "github.com/goravel/framework/contracts/http"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/request.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/response.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/context.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/cookie.go`
+
+## Available Methods - Request
+
+- `Path()` string - `/users/1`
+- `OriginPath()` string - `/users/{id}`
+- `Url()` / `FullUrl()` / `Host()` / `Method()` / `Name()` / `Ip()`
+- `Info()` Info - route handler/method/name/path
+- `Route(key)` string / `RouteInt` / `RouteInt64`
+- `Query(key, default?)` / `QueryInt` / `QueryInt64` / `QueryBool` / `QueryArray` / `QueryMap`
+- `Queries()` map[string]string - all query params
+- `Input(key, default?)` - JSON > form > query > route priority
+- `InputInt` / `InputInt64` / `InputBool` / `InputArray` / `InputMap` / `InputMapArray`
+- `All()` map[string]any - merged JSON + form + query
+- `Bind(&obj)` error - JSON or form -> struct (tags: `json:` or `form:`)
+- `BindQuery(&obj)` error - query string -> struct (tags: `form:`)
+- `Header(key, default?)` / `Headers()` http.Header
+- `Cookie(key, default?)`
+- `File(name)` (filesystem.File, error) / `Files(name)` ([]filesystem.File, error)
+- `Validate(rules)` (Validator, error)
+- `ValidateRequest(&formReq)` (Errors, error)
+- `Next()` - advance middleware chain
+- `Abort(code?)` - halt; optional status code, default 400
+- `Origin()` \*http.Request - underlying stdlib request
+
+## Available Methods - Response
+
+- `Header(key, value)` ContextResponse - chainable, before terminal call
+- `Cookie(Cookie)` ContextResponse / `WithoutCookie(name)` ContextResponse
+- `Json(code, obj)` AbortableResponse
+- `String(code, format, values...)` AbortableResponse
+- `Data(code, contentType, []byte)` AbortableResponse
+- `NoContent(code?)` AbortableResponse
+- `Redirect(code, url)` AbortableResponse
+- `File(path)` Response / `Download(path, name)` Response
+- `Stream(code, func(StreamWriter) error)` Response
+- `Success()` ResponseStatus - then `.Json(obj)` / `.String(fmt)` (no code needed)
+- `Status(code)` ResponseStatus - then `.Json(obj)` / `.String(fmt)`
+- `View()` ResponseView - then `.Make(name, data...)` / `.First(names, data...)`
+- `Origin()` ResponseOrigin - use in after-middleware to read body/status
+- `Flush()` - flush buffered data to client
+
+## Implementation Example
+
+```go
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/models"
+)
+
+type UserController struct{}
+
+func (r *UserController) Show(ctx http.Context) http.Response {
+ id := ctx.Request().RouteInt("id")
+
+ var input struct {
+ Name string `json:"name" form:"name"`
+ }
+ if err := ctx.Request().Bind(&input); err != nil {
+ return ctx.Response().Json(http.StatusBadRequest, http.Json{"error": err.Error()})
+ }
+
+ validator, err := ctx.Request().Validate(map[string]string{
+ "name": "required|max_len:255",
+ })
+ if err != nil || validator.Fails() {
+ return ctx.Response().Json(http.StatusUnprocessableEntity, validator.Errors().All())
+ }
+
+ return ctx.Response().
+ Header("X-ID", fmt.Sprint(id)).
+ Cookie(http.Cookie{Name: "visited", Value: "1", HttpOnly: true}).
+ Json(http.StatusOK, http.Json{"id": id, "name": input.Name})
+}
+
+func (r *UserController) Stream(ctx http.Context) http.Response {
+ return ctx.Response().Stream(http.StatusOK, func(w http.StreamWriter) error {
+ for _, chunk := range []string{"a", "b", "c"} {
+ if _, err := w.WriteString(chunk); err != nil { return err }
+ if err := w.Flush(); err != nil { return err }
+ }
+ return nil
+ })
+}
+
+// Form request pattern
+type StoreUserRequest struct {
+ Name string `form:"name" json:"name"`
+}
+func (r *StoreUserRequest) Authorize(ctx http.Context) error { return nil }
+func (r *StoreUserRequest) Rules(ctx http.Context) map[string]string {
+ return map[string]string{"name": "required|max_len:100"}
+}
+// Usage: errs, err := ctx.Request().ValidateRequest(&StoreUserRequest{})
+```
+
+## Rules
+
+- Every handler **must** return `http.Response` - missing return type is a compile error.
+- `Input` reads in priority order: JSON body > form > query > route params.
+- `All()` merges JSON + form + query; does not include route params.
+- `Bind` uses `json:` tags for JSON body, `form:` tags for form data.
+- `BindQuery` only reads query string; struct tags must be `form:`.
+- `Next()` must be called in middleware to advance the chain; omitting it halts execution.
+- `Abort(code?)` halts remaining middleware and handler; default status is 400.
+- `Header/Cookie/WithoutCookie` are chainable and must precede the terminal call.
+- `Success()` returns `ResponseStatus` - then call `.Json(obj)` without a status code.
+- `ResponseView.Make(view, data...)` takes variadic `any` (not a map argument).
+- `Origin()` on response is only useful in after-middleware to inspect the final response.
+- `FormRequest` optional interfaces (`WithFilters`, `WithMessages`, `WithAttributes`, `WithPrepareForValidation`) are detected by interface assertion - no explicit declaration needed.
+- `AbortableResponse.Abort()` aborts the request; call it after `Json`/`String` etc.
+- `ctx.WithValue(key, value)` / `ctx.Value(key)` pass data between middleware and handlers.
diff --git a/.ai/knowledge/route.md b/.ai/knowledge/route.md
new file mode 100644
index 000000000..4aff8157a
--- /dev/null
+++ b/.ai/knowledge/route.md
@@ -0,0 +1,125 @@
+# Route Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/route"
+ httplimit "github.com/goravel/framework/http/limit"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/route/route.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/http/rate_limiter.go`
+
+## Available Methods
+
+**facades.Route():**
+
+- `Get/Post/Put/Delete/Patch/Options/Any(path, handler)` Action
+- `Resource(path, controller)` Action
+- `Group(func(Router))`
+- `Prefix(path)` Router
+- `Middleware(middlewares...)` Router
+- `Static(path, root)` Action
+- `StaticFile(path, file)` Action
+- `StaticFS(path, fs)` Action
+- `Fallback(handler)` - unmatched routes
+- `GetRoutes()` []http.Info - all registered routes
+- `Info(name)` http.Info - route by name
+
+**facades.RateLimiter():**
+
+- `For(name, func(ctx) Limit)` - register single limiter
+- `ForWithLimits(name, func(ctx) []Limit)` - register multiple limits
+
+**httplimit constructors:**
+
+- `PerMinute(maxAttempts int)` Limit
+- `PerMinutes(decayMinutes, maxAttempts int)` Limit
+- `PerHour(maxAttempts int)` Limit
+- `PerHours(decayHours, maxAttempts int)` Limit
+- `PerDay(maxAttempts int)` Limit
+- `PerDays(decayDays, maxAttempts int)` Limit
+
+**Action:**
+
+- `.Name(name string)` Action - assign name to route
+
+## Implementation Example
+
+```go
+package routes
+
+import (
+ contractshttp "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/route"
+ httplimit "github.com/goravel/framework/http/limit"
+ httpmiddleware "github.com/goravel/framework/http/middleware"
+ "yourmodule/app/facades"
+ "yourmodule/app/http/controllers"
+ appmw "yourmodule/app/http/middleware"
+)
+
+func Web() {
+ // Rate limiter - register in WithCallback in bootstrap/app.go
+ facades.RateLimiter().For("api", func(ctx contractshttp.Context) contractshttp.Limit {
+ return httplimit.PerMinute(60).By(ctx.Request().Ip())
+ })
+ facades.RateLimiter().ForWithLimits("uploads", func(ctx contractshttp.Context) []contractshttp.Limit {
+ return []contractshttp.Limit{
+ httplimit.PerMinute(10).By(ctx.Request().Ip()),
+ httplimit.PerDay(100).By(ctx.Request().Ip()),
+ }
+ })
+
+ userCtrl := controllers.NewUserController()
+
+ // Named routes
+ facades.Route().Get("/", func(ctx contractshttp.Context) contractshttp.Response {
+ return ctx.Response().Json(contractshttp.StatusOK, contractshttp.Json{"ok": true})
+ }).Name("home")
+
+ // Prefix + middleware group
+ facades.Route().
+ Prefix("api/v1").
+ Middleware(appmw.Auth(), httpmiddleware.Throttle("api")).
+ Group(func(r route.Router) {
+ r.Get("users", userCtrl.Index).Name("users.index")
+ r.Post("users", userCtrl.Store)
+ r.Get("users/{id}", userCtrl.Show)
+ r.Put("users/{id}", userCtrl.Update)
+ r.Delete("users/{id}", userCtrl.Destroy)
+ })
+
+ // Resource routes (auto-generates Index/Show/Store/Update/Destroy)
+ facades.Route().Resource("photos", controllers.NewPhotoController())
+
+ // Static files
+ facades.Route().Static("storage", "./storage/app/public")
+
+ // 404 fallback
+ facades.Route().Fallback(func(ctx contractshttp.Context) contractshttp.Response {
+ return ctx.Response().Json(404, contractshttp.Json{"error": "not found"})
+ })
+}
+```
+
+## Rules
+
+- Every handler must have signature `func(ctx http.Context) http.Response`.
+- `facades.Route()` and `facades.RateLimiter()` are project facades from `yourmodule/app/facades`.
+- Rate limiters must be registered in `WithCallback` (all providers booted) before routes are used.
+- `Group` callback receives `route.Router`, not the facade.
+- `Resource` requires all 5 methods on the controller.
+- Route names are set via `.Name()` on the returned `Action`; retrieve with `facades.Route().Info(name)`.
+- `Throttle("name")` middleware name must match a registered `RateLimiter` name.
+- `PerMinutes(5, 100)` = 100 requests per 5 minutes; first arg is decay, second is max.
+- Static path is relative to the application working directory.
+- `GlobalMiddleware` on Route is deprecated; use `WithMiddleware` in `bootstrap/app.go`.
diff --git a/.ai/knowledge/schedule.md b/.ai/knowledge/schedule.md
new file mode 100644
index 000000000..8d00431e7
--- /dev/null
+++ b/.ai/knowledge/schedule.md
@@ -0,0 +1,110 @@
+# Schedule Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/schedule"
+
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/schedule/event.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/schedule/schedule.go`
+
+## Available Methods
+
+**facades.Schedule():**
+
+- `Call(func())` Event - schedule a closure
+- `Command(signature string)` Event - schedule an Artisan command by signature
+
+**Event frequency (chainable):**
+
+- `.Cron("* * * * *")` - 5-field cron (minutes); `.Cron("* * * * * *")` - 6-field (seconds)
+- `.EverySecond()` / `.EveryMinute()` / `.Hourly()` / `.Daily()` / `.Weekly()` / `.Monthly()` / `.Quarterly()` / `.Yearly()`
+- `.DailyAt("13:00")` - specific time daily
+- `.HourlyAt(17)` - at 17 minutes past each hour
+- `.Days(1, 3, 5)` - specific weekdays (1=Mon, 7=Sun)
+- `.Weekdays()` / `.Weekends()` / `.Mondays()` ... `.Sundays()`
+
+**Overlap control:**
+
+- `.SkipIfStillRunning()` - skip if previous execution is still running
+- `.DelayIfStillRunning()` - queue until previous execution finishes
+
+**Distributed:**
+
+- `.OnOneServer()` - run on only one server (requires non-memory cache driver)
+- `.Name("name")` - required for closure tasks with `.OnOneServer()`
+
+## Implementation Example
+
+```go
+// bootstrap/app.go
+package bootstrap
+
+import (
+ contractsfoundation "github.com/goravel/framework/contracts/foundation"
+ "github.com/goravel/framework/contracts/schedule"
+ "github.com/goravel/framework/foundation"
+
+ "yourmodule/app/facades"
+ "yourmodule/app/models"
+ "yourmodule/bootstrap/config"
+)
+
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithSchedule(func() []schedule.Event {
+ return []schedule.Event{
+ // Run closure every minute
+ facades.Schedule().Call(func() {
+ facades.Log().Info("heartbeat")
+ }).EveryMinute().Name("heartbeat"),
+
+ // Run closure daily at 2am, single server only
+ facades.Schedule().Call(func() {
+ facades.Orm().Query().
+ Where("1 = 1").
+ Delete(&models.TempFile{})
+ }).DailyAt("02:00").OnOneServer().Name("cleanup-temp"),
+
+ // Run Artisan command every 5 minutes
+ facades.Schedule().Command("send:emails --lang en").
+ EveryFiveMinutes().
+ SkipIfStillRunning(),
+
+ // Custom cron expression
+ facades.Schedule().Command("report:generate").
+ Cron("0 9 * * 1"). // every Monday at 9:00
+ OnOneServer(),
+
+ // Weekdays only
+ facades.Schedule().Call(func() {
+ // business hours sync
+ }).Weekdays().HourlyAt(0),
+ }
+ }).
+ Create()
+}
+```
+
+## Rules
+
+- Define schedules via `WithSchedule` in `bootstrap/app.go`; the scheduler runs automatically on `Start()`.
+- `Command("signature")` runs an Artisan command; include arguments/flags in the string.
+- `OnOneServer()` requires a distributed cache driver (`redis`, `memcached`, `dynamodb`) - **not** `memory`.
+- Closure tasks must have `.Name("unique-name")` when using `.OnOneServer()`.
+- `.SkipIfStillRunning()` silently skips; `.DelayIfStillRunning()` waits for the running task to complete.
+- `Cron("* * * * *")` is minute-resolution; `Cron("* * * * * *")` is second-resolution (6 fields).
+- When `app.debug = true`, all schedule logs are printed; otherwise only `error` level.
+- Run the scheduler manually: `./artisan schedule:run`.
+- View all scheduled tasks: `./artisan schedule:list`.
+- `Days(1, 3, 5)` - weekday integers: 0=Sunday, 1=Monday, ..., 6=Saturday.
diff --git a/.ai/knowledge/session.md b/.ai/knowledge/session.md
new file mode 100644
index 000000000..4ae06bc24
--- /dev/null
+++ b/.ai/knowledge/session.md
@@ -0,0 +1,137 @@
+# Session Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/http"
+ contractssession "github.com/goravel/framework/contracts/session"
+ httpmiddleware "github.com/goravel/framework/http/middleware"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/session.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/manager.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/session/driver.go`
+
+## Available Methods
+
+**ctx.Request():**
+
+- `HasSession()` bool
+- `Session()` Session
+- `SetSession(session)` ContextRequest
+
+**Session:**
+
+- `Get(key, default?)` any
+- `All()` map[string]any
+- `Only([]string)` map[string]any
+- `Has(key)` bool - present and not nil
+- `Exists(key)` bool - present even if nil
+- `Missing(key)` bool
+- `Token()` string - CSRF token
+- `Put(key, value)` Session
+- `Pull(key, default?)` any - retrieve + delete
+- `Flash(key, value)` Session - available next request only
+- `Now(key, value)` Session - available current request only
+- `Reflash()` Session - extend all flash by one more request
+- `Keep(keys...)` Session - extend specific flash keys
+- `Forget(keys...)` Session
+- `Flush()` Session - clear all
+- `Remove(key)` any - remove and return value
+- `Regenerate(destroy?)` error - new session ID
+- `Invalidate()` error - new ID + flush data
+- `GetID()` / `SetID(id)` / `GetName()` / `SetName(name)`
+- `Save()` error
+- `Start()` bool
+
+**facades.Session():**
+
+- `Driver(name?)` (Driver, error)
+- `BuildSession(driver, sessionID?)` (Session, error)
+- `ReleaseSession(session)`
+
+## Implementation Example
+
+```go
+// bootstrap/app.go - register session middleware globally
+// WithMiddleware(func(h configuration.Middleware) {
+// h.Append(httpmiddleware.StartSession())
+// })
+
+package controllers
+
+import (
+ "fmt"
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+
+type SessionController struct{}
+
+func (r *SessionController) Store(ctx http.Context) http.Response {
+ session := ctx.Request().Session()
+
+ session.Put("user_id", 42)
+ session.Flash("status", "Profile updated!")
+ session.Now("notice", "Visible this request only")
+
+ return ctx.Response().Json(http.StatusOK, http.Json{"stored": true})
+}
+
+func (r *SessionController) Read(ctx http.Context) http.Response {
+ session := ctx.Request().Session()
+
+ userID := session.Get("user_id", 0)
+ status := session.Pull("status") // read + delete
+
+ if session.Missing("preferences") {
+ session.Put("preferences", map[string]any{"theme": "light"})
+ }
+
+ return ctx.Response().Json(http.StatusOK, http.Json{
+ "user_id": userID,
+ "status": status,
+ })
+}
+
+func (r *SessionController) Regenerate(ctx http.Context) http.Response {
+ // Regenerate session ID after login (prevents fixation)
+ if err := ctx.Request().Session().Regenerate(); err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+
+ // Update session cookie with new ID
+ ctx.Response().Cookie(http.Cookie{
+ Name: ctx.Request().Session().GetName(),
+ Value: ctx.Request().Session().GetID(),
+ MaxAge: facades.Config().GetInt("session.lifetime") * 60,
+ Path: facades.Config().GetString("session.path"),
+ Domain: facades.Config().GetString("session.domain"),
+ Secure: facades.Config().GetBool("session.secure"),
+ HttpOnly: facades.Config().GetBool("session.http_only"),
+ SameSite: facades.Config().GetString("session.same_site"),
+ })
+
+ return ctx.Response().Json(http.StatusOK, http.Json{"ok": true})
+}
+```
+
+## Rules
+
+- Sessions are not started automatically; register `httpmiddleware.StartSession()` explicitly.
+- `ctx.Request().Session()` panics if no session middleware is registered; check `HasSession()` first if uncertain.
+- `Put/Flash/Now/Reflash/Keep/Forget/Flush` all return `Session` for chaining.
+- `Regenerate()` and `Invalidate()` return `error`, not `bool`.
+- `Remove(key)` returns the removed value as `any`.
+- `Flash` is available for exactly one subsequent request; `Reflash()` extends by one more.
+- `Now` is an immediate flash; visible in the **current** request, not the next.
+- After `Regenerate`/`Invalidate`, update the session cookie manually (see example above).
+- Default driver is `file`; configure in `config/session.go`.
+- `Token()` returns the CSRF token associated with the session.
diff --git a/.ai/knowledge/storage.md b/.ai/knowledge/storage.md
new file mode 100644
index 000000000..baf885876
--- /dev/null
+++ b/.ai/knowledge/storage.md
@@ -0,0 +1,146 @@
+# Storage Facade
+
+## Core Imports
+
+```go
+import (
+ "time"
+ "context"
+ "github.com/goravel/framework/filesystem"
+ contractsfilesystem "github.com/goravel/framework/contracts/filesystem"
+
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/filesystem/storage.go`
+
+## Available Methods
+
+**facades.Storage():**
+
+- `Disk(name)` Driver - switch to named disk
+- `WithContext(ctx)` Driver - inject context
+- `Get(path)` (string, error) - read file contents
+- `GetBytes(path)` ([]byte, error) - read as bytes
+- `Exists(path)` bool - check file exists
+- `Missing(path)` bool - check file is absent
+- `Put(path, content)` error - write string content
+- `PutFile(dir, file)` (string, error) - store with auto-generated name; returns path
+- `PutFileAs(dir, file, name)` (string, error) - store with explicit name
+- `Copy(src, dst)` error
+- `Move(src, dst)` error
+- `Delete(files...)` error
+- `Url(path)` string - public URL
+- `TemporaryUrl(path, time.Time)` (string, error) - expiring URL (non-local drivers)
+- `Size(path)` (int64, error)
+- `LastModified(path)` (time.Time, error)
+- `MimeType(path)` (string, error)
+- `Path(path)` string - absolute local path
+- `Files(dir)` ([]string, error) - files in directory
+- `AllFiles(dir)` ([]string, error) - files including subdirectories
+- `Directories(dir)` ([]string, error)
+- `AllDirectories(dir)` ([]string, error)
+- `MakeDirectory(dir)` error
+- `DeleteDirectory(dir)` error
+
+**filesystem.File (local file object):**
+
+- `filesystem.NewFile(path)` (\*File, error) - create from local path
+- `.Size()` (int64, error)
+- `.LastModified()` (time.Time, error)
+- `.MimeType()` (string, error)
+
+**Uploaded file (from ctx.Request().File):**
+
+- `.Store(dir)` (string, error) - save to default disk; returns stored path
+- `.StoreAs(dir, name)` (string, error) - save with specific name
+- `.Disk(name)` File - target a specific disk
+- `.GetClientOriginalName()` string - unsafe; use for display only
+- `.GetClientOriginalExtension()` string - unsafe
+- `.HashName()` string - safe random name
+- `.Extension()` (string, error) - safe MIME-based extension
+
+## Implementation Example
+
+```go
+package controllers
+
+import (
+ "time"
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/filesystem"
+ "yourmodule/app/facades"
+)
+
+type FileController struct{}
+
+// Upload file from request
+func (r *FileController) Upload(ctx http.Context) http.Response {
+ file, err := ctx.Request().File("avatar")
+ if err != nil {
+ return ctx.Response().Json(http.StatusBadRequest, http.Json{"error": "no file"})
+ }
+
+ // Store to default disk with auto-name
+ path, err := file.Store("avatars")
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ return ctx.Response().Json(http.StatusOK, http.Json{"path": path})
+}
+
+// Store to S3 with specific name
+func (r *FileController) UploadToS3(ctx http.Context) http.Response {
+ file, _ := ctx.Request().File("document")
+ path, err := file.Disk("s3").StoreAs("docs", file.HashName())
+ if err != nil {
+ return ctx.Response().Json(http.StatusInternalServerError, http.Json{"error": err.Error()})
+ }
+ url := facades.Storage().Disk("s3").Url(path)
+ return ctx.Response().Json(http.StatusOK, http.Json{"url": url})
+}
+
+// Read / write directly
+func (r *FileController) ProcessFile(ctx http.Context) http.Response {
+ // Write
+ _ = facades.Storage().Put("data/report.txt", "hello world")
+
+ // Read
+ content, _ := facades.Storage().Get("data/report.txt")
+
+ // Temporary URL (for S3/OSS etc.)
+ tmpUrl, _ := facades.Storage().Disk("s3").TemporaryUrl(
+ "private/file.pdf",
+ time.Now().Add(15*time.Minute),
+ )
+
+ // Copy local file to storage
+ localFile, _ := filesystem.NewFile("./resources/logo.png")
+ storedPath := facades.Storage().PutFile("images", localFile)
+
+ return ctx.Response().Json(http.StatusOK, http.Json{
+ "content": content,
+ "tmp_url": tmpUrl,
+ "logo_path": storedPath,
+ })
+}
+```
+
+## Rules
+
+- Default disk is `local`; configure in `config/filesystems.go` under the `default` key.
+- All paths are relative to the disk's configured `root` directory.
+- `Url()` on `local` driver prepends `/storage` - serve via `facades.Route().Static("storage", "./storage/app/public")`.
+- `TemporaryUrl()` only works on non-local drivers (S3, OSS, etc.).
+- `PutFile` auto-generates a unique filename; `PutFileAs` uses the name you provide.
+- If name in `StoreAs`/`PutFileAs` has no extension, the MIME-detected extension is appended automatically.
+- `GetClientOriginalName`/`GetClientOriginalExtension` are **unsafe** - can be tampered by clients.
+- Use `HashName()` and `Extension()` for security-safe filenames.
+- Custom driver: set `"driver": "custom"` and `"via": driverInstance` in `config/filesystems.go`.
+- Use `facades.Config().Env()` inside custom drivers - regular config is not yet loaded when the driver registers.
+- External drivers: S3 (`goravel/s3`), OSS (`goravel/oss`), COS (`goravel/cos`), Minio (`goravel/minio`).
diff --git a/.ai/knowledge/str.md b/.ai/knowledge/str.md
new file mode 100644
index 000000000..e4902310d
--- /dev/null
+++ b/.ai/knowledge/str.md
@@ -0,0 +1,167 @@
+# Fluent String (str)
+
+## Import
+
+```go
+import "github.com/goravel/framework/support/str"
+```
+
+## Contracts
+
+Support library. No framework contract file.
+The `*str.String` type and all methods are defined in `support/str`.
+
+## Usage Pattern
+
+`str.Of(s)` returns `*str.String`. Chain methods. Call `.String()` at the end to get a `string`.
+Methods that return a non-string value (`bool`, `int`, `[]string`) do NOT need `.String()`.
+
+## Transform Methods
+
+```go
+str.Of(s).Append(values ...string) *str.String
+str.Of(s).Prepend(values ...string) *str.String
+str.Of(s).Remove(values ...string) *str.String
+str.Of(s).Replace(search, replace string, caseSensitive ...bool) *str.String
+str.Of(s).ReplaceFirst(search, replace string) *str.String
+str.Of(s).ReplaceLast(search, replace string) *str.String
+str.Of(s).ReplaceStart(search, replace string) *str.String
+str.Of(s).ReplaceEnd(search, replace string) *str.String
+str.Of(s).ReplaceMatches(pattern, replace string) *str.String
+str.Of(s).Swap(pairs map[string]string) *str.String
+str.Of(s).Repeat(times int) *str.String
+str.Of(s).Limit(limit int, end ...string) *str.String // default end: "..."
+str.Of(s).Words(words int, end ...string) *str.String
+str.Of(s).Mask(char string, index int, length ...int) *str.String
+str.Of(s).PadLeft(length int, pad string) *str.String
+str.Of(s).PadRight(length int, pad string) *str.String
+str.Of(s).PadBoth(length int, pad string) *str.String
+str.Of(s).NewLine(count ...int) *str.String
+```
+
+## Case Methods
+
+```go
+str.Of(s).Lower() *str.String // "GORAVEL" -> "goravel"
+str.Of(s).Upper() *str.String // "goravel" -> "GORAVEL"
+str.Of(s).Title() *str.String // "goravel framework" -> "Goravel Framework"
+str.Of(s).UcFirst() *str.String // "goravel" -> "Goravel"
+str.Of(s).LcFirst() *str.String // "Goravel" -> "goravel"
+str.Of(s).Camel() *str.String // "hello_world" -> "helloWorld"
+str.Of(s).Studly() *str.String // "hello_world" -> "HelloWorld"
+str.Of(s).Snake() *str.String // "GoravelFramework" -> "goravel_framework"
+str.Of(s).Kebab() *str.String // "GoravelFramework" -> "goravel-framework"
+str.Of(s).Headline() *str.String // "bowen_han" -> "Bowen Han"
+str.Of(s).UcSplit() []string // "GoravelFramework" -> ["Goravel","Framework"]
+```
+
+## Trim Methods
+
+```go
+str.Of(s).Trim(chars ...string) *str.String // both sides; optional chars to trim
+str.Of(s).LTrim(chars ...string) *str.String // left only
+str.Of(s).RTrim(chars ...string) *str.String // right only
+str.Of(s).Squish() *str.String // collapse internal whitespace
+```
+
+## Extract Methods
+
+```go
+str.Of(s).After(search string) *str.String
+str.Of(s).AfterLast(search string) *str.String
+str.Of(s).Before(search string) *str.String
+str.Of(s).BeforeLast(search string) *str.String
+str.Of(s).Between(from, to string) *str.String
+str.Of(s).BetweenFirst(from, to string) *str.String
+str.Of(s).ChopStart(needle string) *str.String
+str.Of(s).ChopEnd(needle string) *str.String
+str.Of(s).Substr(start, length int) string // returns string directly
+str.Of(s).CharAt(index int) string // returns string directly
+str.Of(s).Basename(suffix ...string) *str.String
+str.Of(s).Dirname(levels ...int) *str.String
+str.Of(s).Except(excerpt string, opts ...str.ExcerptOption) *str.String
+str.Of(s).Explode(delimiter string) []string // returns []string directly
+str.Of(s).Split(delimiter string) []string // returns []string directly
+```
+
+## Check Methods (return bool -- no .String() needed)
+
+```go
+str.Of(s).Contains(needles ...string) bool // any needle matches
+str.Of(s).ContainsAll(needles ...string) bool // all needles match
+str.Of(s).StartsWith(needles ...string) bool
+str.Of(s).EndsWith(needles ...string) bool
+str.Of(s).Exactly(value string) bool
+str.Of(s).Is(patterns ...string) bool // glob patterns
+str.Of(s).IsEmpty() bool
+str.Of(s).IsNotEmpty() bool
+str.Of(s).IsAscii() bool
+str.Of(s).IsSlice() bool // valid JSON array
+str.Of(s).IsMap() bool // valid JSON object
+str.Of(s).IsUlid() bool
+str.Of(s).IsUuid() bool
+str.Of(s).IsMatch(pattern string) bool // regex
+str.Of(s).Test(pattern string) bool // alias for IsMatch
+str.Of(s).Length() int
+str.Of(s).WordCount() int
+```
+
+## Regex Methods
+
+```go
+str.Of(s).Match(pattern string) *str.String // first match
+str.Of(s).MatchAll(pattern string) []string // all matches; returns []string directly
+```
+
+## Pluralization
+
+```go
+str.Of(s).Plural(count ...int) *str.String // no arg or count>1 = plural; count=1 = singular
+str.Of(s).Singular() *str.String
+```
+
+## Path-like Methods
+
+```go
+str.Of(s).Finish(cap string) *str.String // ensure ends with cap
+str.Of(s).Start(prefix string) *str.String // ensure starts with prefix
+```
+
+## Conditional Chaining
+
+```go
+str.Of(s).When(cond bool, fn func(*str.String) *str.String, otherwise ...func(*str.String) *str.String) *str.String
+str.Of(s).Unless(cond func(*str.String) bool, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenEmpty(fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenNotEmpty(fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenContains(needle string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenContainsAll(needles []string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenStartsWith(needle string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenEndsWith(needle string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenExactly(value string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenNotExactly(value string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenIs(pattern string, fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenIsAscii(fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenIsUlid(fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenIsUuid(fn func(*str.String) *str.String) *str.String
+str.Of(s).WhenTest(pattern string, fn func(*str.String) *str.String) *str.String
+```
+
+## Utility
+
+```go
+str.Of(s).Tap(fn func(string)) *str.String // inspect without modifying
+str.Of(s).Pipe(fn func(string) string) *str.String
+str.Of(s).String() string // terminal: get final string value
+```
+
+## Rules
+
+- Always call `.String()` to extract a plain `string`; intermediate chain returns `*str.String`.
+- Methods returning `bool`, `int`, `[]string`, or `string` directly (e.g. `CharAt`, `Substr`, `Explode`) do NOT need `.String()`.
+- `Plural()` with no argument returns plural form. `Plural(1)` returns singular.
+- `Replace(search, replace, false)` performs case-insensitive replacement.
+- `Limit(7)` truncates to 7 characters and appends `"..."` by default.
+- `Mask("*", 3)` masks from index 3 to end. `Mask("*", -13, 3)` masks 3 chars from position -13 (from end).
+- `Except` extracts a contextual excerpt around the search term; `Radius` controls surrounding character count; `Omission` changes the ellipsis string (default `"..."`).
+- `UcFirst` (not `UpperFirst`) is the correct method name for capitalising the first character.
diff --git a/.ai/knowledge/testing.md b/.ai/knowledge/testing.md
new file mode 100644
index 000000000..90b8968b9
--- /dev/null
+++ b/.ai/knowledge/testing.md
@@ -0,0 +1,205 @@
+# Testing
+
+## Core Imports
+
+```go
+import (
+ "testing"
+ "github.com/stretchr/testify/suite"
+ testinghttp "github.com/goravel/framework/testing/http"
+ "github.com/goravel/framework/testing/mock"
+ contractstesting "github.com/goravel/framework/contracts/testing"
+ contractsorm "github.com/goravel/framework/contracts/database/orm"
+
+ "yourmodule/tests"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/testing.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/request.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/response.go`
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/testing/http/assertable_json.go`
+
+## Available Methods
+
+**Suite helpers (from `tests.TestCase`):**
+
+- `s.Http(t)` TestRequest - create HTTP test client
+- `s.Seed(seeders ...Seeder)` - run seeders (no arg = DatabaseSeeder)
+- `s.RefreshDatabase()` - migrate fresh before each test (call in `SetupTest`)
+
+**TestRequest (from `s.Http(t)`):**
+
+- `.WithHeader(key, value)` TestRequest
+- `.WithHeaders(map[string]string)` TestRequest
+- `.WithCookie(testinghttp.Cookie(name, value))` TestRequest
+- `.WithCookies(testinghttp.Cookies(map[string]string))` TestRequest
+- `.WithSession(map[string]any)` TestRequest
+- `.Get(path)` (TestResponse, error)
+- `.Post(path, body)` (TestResponse, error)
+- `.Put(path, body)` (TestResponse, error)
+- `.Patch(path, body)` (TestResponse, error)
+- `.Delete(path, body?)` (TestResponse, error)
+
+**TestResponse assertions:**
+
+- `.AssertStatus(code)`
+- `.AssertOk()` / `.AssertCreated()` / `.AssertAccepted()` / `.AssertNoContent()`
+- `.AssertPartialContent()` / `.AssertNotModified()` / `.AssertTemporaryRedirect()`
+- `.AssertMovedPermanently()` / `.AssertFound()`
+- `.AssertBadRequest()` / `.AssertUnauthorized()` / `.AssertPaymentRequired()`
+- `.AssertForbidden()` / `.AssertNotFound()` / `.AssertMethodNotAllowed()`
+- `.AssertRequestTimeout()` / `.AssertConflict()` / `.AssertGone()`
+- `.AssertUnprocessableEntity()` / `.AssertTooManyRequests()`
+- `.AssertInternalServerError()` / `.AssertServiceUnavailable()`
+- `.AssertSuccessful()` (2xx) / `.AssertServerError()` (5xx)
+- `.AssertHeader(key, value)` / `.AssertHeaderMissing(key)`
+- `.AssertCookie(name, value)` / `.AssertCookieMissing(name)`
+- `.AssertCookieExpired(name)` / `.AssertCookieNotExpired(name)`
+- `.AssertJson(map[string]any)` - subset match
+- `.AssertExactJson(map[string]any)` - exact match
+- `.AssertJsonMissing(map[string]any)`
+- `.AssertFluentJson(func(AssertableJSON))` - fluent chain
+- `.AssertSee([]string, escape bool)` / `.AssertDontSee([]string, escape bool)`
+- `.AssertSeeInOrder([]string, escape bool)`
+
+**TestResponse data:**
+
+- `.Content()` (string, error)
+- `.Json()` (map[string]any, error)
+- `.Cookies()` []\*http.Cookie
+- `.Headers()` http.Header
+- `.Session()` (map[string]any, error)
+
+**Mock factory (`mock.Factory()`):**
+
+- `.App()` / `.Artisan()` / `.Auth()` / `.Cache()` / `.Config()` / `.Crypt()`
+- `.Event()` + `.EventTask()`
+- `.Gate()` / `.Grpc()` / `.Hash()` / `.Lang()` / `.Log()` / `.Mail()`
+- `.Orm()` + `.OrmQuery()` + `.OrmTransaction()`
+- `.Queue()` + `.QueueTask()`
+- `.Storage()` + `.StorageDriver()`
+- `.Validation()` + `.ValidationValidator()` + `.ValidationErrors()`
+- `.View()`
+
+**Docker testing (`facades.Testing().Docker()`):**
+
+- `.Database(driver?)` (DatabaseImage, error) - default driver from config
+- `.Cache(driver?)` (CacheImage, error)
+- `.Image(contractstesting.Image)` (Image, error) - custom image
+- `.Build()` error - start container
+- `.Ready()` error - wait until healthy
+- `.Migrate()` error
+- `.Seed(seeders ...Seeder)` error
+- `.Config()` - get connection config (use after Build)
+- `.Fresh()` error - drop+migrate (not safe for parallel)
+- `.Shutdown()` error - stop container
+
+## Implementation Example
+
+```go
+// tests/feature/user_test.go
+package feature
+
+import (
+ "testing"
+ "github.com/stretchr/testify/suite"
+ contractstesting "github.com/goravel/framework/contracts/testing"
+ "github.com/goravel/framework/support/http"
+ testinghttp "github.com/goravel/framework/testing/http"
+ "github.com/goravel/framework/testing/mock"
+
+ "yourmodule/tests"
+ "yourmodule/app/facades"
+)
+
+type UserTestSuite struct {
+ suite.Suite
+ tests.TestCase
+}
+
+func TestUserSuite(t *testing.T) { suite.Run(t, new(UserTestSuite)) }
+
+func (s *UserTestSuite) SetupTest() {
+ s.RefreshDatabase() // fresh DB before each test
+}
+
+func (s *UserTestSuite) TestIndex() {
+ resp, err := s.Http(s.T()).
+ WithHeader("Accept", "application/json").
+ Get("/api/users")
+
+ s.Nil(err)
+ resp.AssertOk()
+ resp.AssertFluentJson(func(json contractstesting.AssertableJSON) {
+ json.Has("data").Count("data", 0)
+ })
+}
+
+func (s *UserTestSuite) TestCreate() {
+ body := http.NewBody().
+ SetField("name", "Alice").
+ SetField("email", "alice@example.com")
+ built, _ := body.Build()
+
+ resp, err := s.Http(s.T()).
+ WithHeader("Content-Type", built.ContentType()).
+ Post("/api/users", built)
+
+ s.Nil(err)
+ resp.AssertCreated()
+ resp.AssertFluentJson(func(json contractstesting.AssertableJSON) {
+ json.Where("name", "Alice").Has("id").Missing("password")
+ })
+}
+
+// Unit test with mock
+func TestUserServiceCache(t *testing.T) {
+ mockFactory := mock.Factory()
+ mockCache := mockFactory.Cache()
+ mockCache.On("Remember", "user:1", mock.Anything, mock.Anything).
+ Return(&User{ID: 1, Name: "Alice"}, nil).Once()
+
+ svc := &UserService{} // uses facades.Cache() internally
+ user, err := svc.GetUser(1)
+ assert.Nil(t, err)
+ assert.Equal(t, "Alice", user.Name)
+ mockCache.AssertExpectations(t)
+}
+
+// Docker test - tests/feature/main_test.go
+func TestMain(m *testing.M) {
+ db, err := facades.Testing().Docker().Database()
+ if err != nil { panic(err) }
+ if err := db.Build(); err != nil { panic(err) }
+ if err := db.Ready(); err != nil { panic(err) }
+ if err := db.Migrate(); err != nil { panic(err) }
+ if err := facades.App().Restart(); err != nil { panic(err) }
+
+ exit := m.Run()
+ db.Shutdown()
+ os.Exit(exit)
+}
+```
+
+## Rules
+
+- Embed `tests.TestCase` (not `suite.Suite` alone) to get `Http()`, `Seed()`, `RefreshDatabase()`.
+- `s.RefreshDatabase()` in `SetupTest()` drops and re-migrates before **each** test.
+- `s.Http(s.T())` creates a new test HTTP client per call; chain builder methods before the verb.
+- Post/Put/Patch body: use `http.NewBody().SetField(k,v).Build()` and set `Content-Type` from `built.ContentType()`.
+- `AssertJson` is a **subset** match - extra fields in response are allowed.
+- `AssertExactJson` requires **exact** match - no extra or missing fields.
+- JSON numeric values come back as `float64` in Go - use `float64(1)` not `1` in assertions.
+- Mock `Log` facade uses `fmt.Print` - it does not write real log files during tests.
+- `mock.Factory()` replaces the global facade binding for that test scope automatically.
+- Docker `Fresh()` is **not safe** for parallel tests - it drops all tables.
+- Never use `t.Parallel()` alongside `facades.Http().Fake(...)` - global state, race conditions.
+- Always `defer facades.Http().Reset()` in any test using `Fake`.
+- Docker images auto-shutdown after 1 hour if `Shutdown()` is not called.
+- `.env` in the test package directory overrides root `.env`; or pass `--env=.env.testing`.
diff --git a/.ai/knowledge/validation.md b/.ai/knowledge/validation.md
new file mode 100644
index 000000000..b4dc77f2e
--- /dev/null
+++ b/.ai/knowledge/validation.md
@@ -0,0 +1,149 @@
+# Validation Facade
+
+## Core Imports
+
+```go
+import (
+ "context"
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/validation"
+ validationpkg "github.com/goravel/framework/validation"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/validation/validation.go`
+
+## Available Methods
+
+**Inline on request:**
+
+- `ctx.Request().Validate(rules map[string]string, opts ...Option)` (Validator, error)
+- `ctx.Request().ValidateRequest(formRequest)` (Errors, error)
+
+**Manual:**
+
+- `facades.Validation().Make(ctx context.Context, data any, rules map[string]string, opts...Option)` (Validator, error)
+ - `ctx` is `context.Context` (use `ctx.Context()` from http.Context)
+ - `data` is `map[string]any` or struct
+
+**Validator:**
+
+- `Fails()` bool
+- `Errors().One(key?)` string - first error for optional key
+- `Errors().Get(key)` map[string]string - `map[ruleName]message`
+- `Errors().All()` map[string]map[string]string - `map[field]map[rule]message`
+- `Errors().Has(key)` bool
+- `Bind(&dest)` error - populate struct from validated data
+
+## Implementation Example
+
+```go
+// Inline validation
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ validationpkg "github.com/goravel/framework/validation"
+ "yourmodule/app/facades"
+)
+
+type PostController struct{}
+
+func (r *PostController) Store(ctx http.Context) http.Response {
+ validator, err := ctx.Request().Validate(map[string]string{
+ "title": "required|max_len:255",
+ "body": "required",
+ "author.name": "required", // nested
+ "tags.*": "required|string", // slice elements
+ })
+ if err != nil {
+ return ctx.Response().Json(http.StatusBadRequest, http.Json{"error": err.Error()})
+ }
+ if validator.Fails() {
+ // All() returns map[field]map[rule]message
+ return ctx.Response().Json(http.StatusUnprocessableEntity, validator.Errors().All())
+ }
+ return ctx.Response().Json(http.StatusCreated, http.Json{"ok": true})
+}
+
+// Form Request pattern
+// app/http/requests/store_post_request.go
+package requests
+
+import (
+ "context"
+ "github.com/goravel/framework/contracts/http"
+ "github.com/goravel/framework/contracts/validation"
+)
+
+type StorePostRequest struct {
+ Title string `form:"title" json:"title"`
+ Body string `form:"body" json:"body"`
+}
+
+func (r *StorePostRequest) Authorize(ctx http.Context) error { return nil }
+
+func (r *StorePostRequest) Rules(ctx http.Context) map[string]string {
+ return map[string]string{
+ "title": "required|max_len:255",
+ "body": "required",
+ }
+}
+// Optional - detected by interface assertion, no explicit declaration needed:
+func (r *StorePostRequest) Messages(ctx http.Context) map[string]string {
+ return map[string]string{"title.required": "Title is required"}
+}
+func (r *StorePostRequest) Attributes(ctx http.Context) map[string]string {
+ return map[string]string{"title": "post title"}
+}
+func (r *StorePostRequest) Filters(ctx http.Context) map[string]string {
+ return map[string]string{"title": "trim"}
+}
+func (r *StorePostRequest) PrepareForValidation(ctx http.Context, data validation.Data) error {
+ if title, ok := data.Get("title"); ok {
+ return data.Set("title", title.(string)+" [processed]")
+ }
+ return nil
+}
+
+// Usage:
+// var req requests.StorePostRequest
+// errs, err := ctx.Request().ValidateRequest(&req)
+// fmt.Println(req.Title) // auto-bound after validation
+
+// Manual with options:
+// validator, _ := facades.Validation().Make(
+// ctx.Context(), // context.Context, not http.Context
+// map[string]any{"name": "Goravel"},
+// map[string]string{"name": "required|max_len:50"},
+// validationpkg.Messages(map[string]string{"name.required": "Name is required"}),
+// )
+```
+
+## Rules
+
+- `facades.Validation().Make(ctx, ...)` - first arg is `context.Context`, use `ctx.Context()`.
+- `Errors.Get(key)` returns `map[string]string` (rule -> message), NOT `[]string`.
+- `Errors.All()` returns `map[string]map[string]string` (field -> rule -> message), NOT `map[string][]string`.
+- `Errors.One()` with no args returns first error of any field; `One("field")` returns first error of that field.
+- Custom `Rule.Passes(ctx context.Context, ...)` - `ctx` is `context.Context`, not `http.Context`.
+- Custom `Rule.Message(ctx context.Context)` - `ctx` is `context.Context`.
+- Custom `Filter.Handle(ctx context.Context) any` - must return a function (`func(val T) U`).
+- FormRequest optional interfaces (`WithFilters`, `WithMessages`, `WithAttributes`, `WithPrepareForValidation`) - detected by interface assertion, not embedded.
+- `ValidateRequest` auto-binds validated fields to struct; struct tags: `form:` or `json:`.
+- Nested fields use dot notation: `"author.name"`. Slice element rules use `*`: `"tags.*"`.
+- Register custom rules via `WithRules` and filters via `WithFilters` in `bootstrap/app.go`.
+
+**Built-in rules (pipe-separated):**
+`required`, `required_if:field,val`, `required_unless:field,val`, `required_with:fields`,
+`required_without:fields`, `int[:min[,max]]`, `uint`, `bool`, `string[:min[,max]]`, `float`,
+`in:a,b,c`, `not_in:a,b,c`, `between:min,max`, `min:val`, `max:val`, `eq:val`, `ne:val`,
+`lt:val`, `gt:val`, `len:val`, `min_len:val`, `max_len:val`, `email`, `array`, `map`,
+`file`, `image`, `date`, `gt_date:val`, `lt_date:val`, `alpha`, `alpha_num`, `alpha_dash`,
+`json`, `number`, `full_url`, `ip`, `ipv4`, `ipv6`, `regex:pattern`, `uuid`, `uuid3`,
+`uuid4`, `uuid5`, `starts_with:val`, `ends_with:val`
diff --git a/.ai/knowledge/view.md b/.ai/knowledge/view.md
new file mode 100644
index 000000000..f1a13ad3b
--- /dev/null
+++ b/.ai/knowledge/view.md
@@ -0,0 +1,115 @@
+# View Facade
+
+## Core Imports
+
+```go
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+```
+
+## Contracts
+
+Fetch these files for exact, always-current type definitions:
+
+- `https://raw.githubusercontent.com/goravel/framework/refs/heads/master/contracts/view/view.go`
+
+## Available Methods
+
+**ctx.Response().View():**
+
+- `Make(viewName, data?)` http.Response - render template; returns full http.Response
+- `First([]string, data?)` http.Response - render first existing view from list
+
+**facades.View():**
+
+- `Exist(name)` bool - check if template exists
+- `Share(key, value)` - share data with all views (call in `WithCallback`)
+- `GetShared()` map[string]any - get all shared data
+
+## Implementation Example
+
+```go
+// resources/views/welcome.tmpl
+// {{ define "welcome.tmpl" }}
+// Hello, {{ .name }}!
+// {{ end }}
+
+// resources/views/admin/dashboard.tmpl
+// {{ define "admin/dashboard.tmpl" }}
+// Admin: {{ .appName }}
+// {{ end }}
+
+// bootstrap/app.go - share global data
+// WithCallback(func() {
+// facades.View().Share("appName", "MyApp")
+// facades.View().Share("version", "1.0")
+// })
+
+package controllers
+
+import (
+ "github.com/goravel/framework/contracts/http"
+ "yourmodule/app/facades"
+)
+
+type PageController struct{}
+
+func (r *PageController) Welcome(ctx http.Context) http.Response {
+ return ctx.Response().View().Make("welcome.tmpl", map[string]any{
+ "name": ctx.Request().Query("name", "Guest"),
+ })
+}
+
+func (r *PageController) Dashboard(ctx http.Context) http.Response {
+ return ctx.Response().View().Make("admin/dashboard.tmpl", map[string]any{
+ "user": "Alice",
+ })
+}
+
+func (r *PageController) TryViews(ctx http.Context) http.Response {
+ // Try custom theme first, fall back to default
+ return ctx.Response().View().First(
+ []string{"themes/custom/welcome.tmpl", "welcome.tmpl"},
+ map[string]any{"name": "Guest"},
+ )
+}
+
+func (r *PageController) CheckView(ctx http.Context) http.Response {
+ if facades.View().Exist("maintenance.tmpl") {
+ return ctx.Response().View().Make("maintenance.tmpl")
+ }
+ return ctx.Response().View().Make("welcome.tmpl")
+}
+```
+
+### CSRF Protection
+
+```go
+// Register middleware globally or per-route
+import "github.com/goravel/framework/http/middleware"
+
+handler.Append(middleware.VerifyCsrfToken([]string{
+ "api/*", // exclude these paths from CSRF check
+ "webhook/*",
+}))
+
+// In template - include the injected csrf_token variable
+//
+// Or in AJAX headers: X-CSRF-TOKEN: {{ .csrf_token }}
+```
+
+## Rules
+
+- Template `define` name **must** match the string passed to `Make` exactly - including subdirectory path.
+ - `Make("admin/dashboard.tmpl")` requires `{{ define "admin/dashboard.tmpl" }}` in the file.
+- Default template engine: `html/template`; files use `.tmpl` extension.
+- Default views directory: `resources/views/` - configurable via `paths.Resources("dir")` in `WithPaths`.
+- `Share` data is global to all views; per-request data is passed via `Make`'s second argument - per-request data takes precedence on key collision.
+- Call `facades.View().Share(...)` in `WithCallback`, not in `Register` or `Boot` of a provider.
+- `View` facade is new in v1.17 - register `&view.ServiceProvider{}` in providers.
+- CSRF token is auto-injected as `csrf_token` into view data when `VerifyCsrfToken` middleware is active.
+- `VerifyCsrfToken` excludes paths by glob pattern; always exclude API and webhook endpoints.
+- Custom template engine (Gin): configure `"template"` key in `config/http.go` with `gin.NewTemplate(...)`.
+- `make:view welcome` generates `resources/views/welcome.tmpl` scaffold.
diff --git a/.ai/skills/.gitkeep b/.ai/skills/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/.github/workflows/generate-agents.yml b/.github/workflows/generate-agents.yml
new file mode 100644
index 000000000..e7b326ab7
--- /dev/null
+++ b/.github/workflows/generate-agents.yml
@@ -0,0 +1,266 @@
+name: Generate Agent Skill Files
+
+on:
+ push:
+ branches:
+ - "**"
+ paths:
+ - "en/**"
+ workflow_dispatch:
+ inputs:
+ force_files:
+ description: "Space-separated list of en/ files to treat as changed (leave empty to scan HEAD~1)"
+ required: false
+ default: ""
+
+permissions:
+ contents: write
+ pull-requests: write
+ issues: write
+
+jobs:
+ # ─────────────────────────────────────────────────────────────
+ # Job 1: Detect which .ai/ prompt files would be affected.
+ # Runs on every push. Zero API calls. Posts a summary comment
+ # on the commit so the team can decide whether to approve.
+ # ─────────────────────────────────────────────────────────────
+ detect:
+ name: Detect affected prompt files
+ runs-on: ubuntu-latest
+
+ outputs:
+ changed_sources: ${{ steps.changed.outputs.files }}
+ affected_outputs: ${{ steps.affected.outputs.files }}
+ has_changes: ${{ steps.affected.outputs.has_changes }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Detect changed en/ files
+ id: changed
+ run: |
+ if [ -n "${{ github.event.inputs.force_files }}" ]; then
+ CHANGED="${{ github.event.inputs.force_files }}"
+ else
+ CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'en/**' | tr '\n' ' ')
+ fi
+ echo "files=$CHANGED" >> "$GITHUB_OUTPUT"
+ echo "Changed en/ files: $CHANGED"
+
+ - name: Determine affected prompt files
+ id: affected
+ run: |
+ python3 - <<'EOF'
+ import json, os
+
+ changed_raw = os.environ.get("CHANGED_FILES", "")
+ changed = set(changed_raw.split())
+
+ with open("scripts/generate-agents/config.json") as f:
+ config = json.load(f)
+
+ outputs = set()
+ for topic in config["topics"]:
+ for src in topic["sources"]:
+ if src in changed:
+ outputs.add(topic["output"])
+ break
+
+ # AGENTS.md always regenerates when any source changes
+ if changed:
+ for topic in config["topics"]:
+ if topic["output"] == ".ai/AGENTS.md":
+ outputs.add(".ai/AGENTS.md")
+ break
+
+ result = " ".join(sorted(outputs))
+ has_changes = "true" if outputs else "false"
+ print(f"Affected outputs: {result}")
+ print(f"Has changes: {has_changes}")
+
+ with open(os.environ["GITHUB_OUTPUT"], "a") as f:
+ f.write(f"files={result}\n")
+ f.write(f"has_changes={has_changes}\n")
+ EOF
+ env:
+ CHANGED_FILES: ${{ steps.changed.outputs.files }}
+
+ - name: Post detection summary
+ if: steps.affected.outputs.has_changes == 'true'
+ uses: peter-evans/commit-comment@v3
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ body: |
+ ## Agent skill file regeneration needed
+
+ The following `en/` documentation files changed in this push:
+
+ ```
+ ${{ steps.changed.outputs.files }}
+ ```
+
+ The following `.ai/` prompt files would be regenerated:
+
+ ```
+ ${{ steps.affected.outputs.files }}
+ ```
+
+ **To trigger regeneration**, go to:
+ [Actions → Generate Agent Skill Files → Run workflow](../../actions/workflows/generate-agents.yml)
+ and click **Run workflow** on this branch.
+
+ > No API tokens are spent until you manually approve the regeneration run.
+
+ # ─────────────────────────────────────────────────────────────
+ # Job 2: Actually regenerate. Only runs on manual dispatch
+ # (workflow_dispatch). Requires the "ai-regeneration" GitHub
+ # environment to be configured with a required reviewer so a
+ # human must click Approve before DeepSeek is called.
+ #
+ # Setup: Repo Settings → Environments → New environment
+ # Name: ai-regeneration
+ # Required reviewers: add yourself (or the team)
+ # ─────────────────────────────────────────────────────────────
+ regenerate:
+ name: Regenerate prompt files (requires approval)
+ runs-on: ubuntu-latest
+ needs: detect
+ if: |
+ github.event_name == 'workflow_dispatch' &&
+ needs.detect.outputs.has_changes == 'true'
+ environment: ai-regeneration # ← approval gate lives here
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install dependencies
+ run: pip install requests
+
+ - name: Regenerate affected files
+ run: |
+ python3 - <<'EOF'
+ import json, os, sys, requests
+
+ api_key = os.environ["DEEPSEEK_API_KEY"]
+ affected = os.environ.get("AFFECTED_FILES", "").split()
+
+ if not affected:
+ print("No files to regenerate.")
+ sys.exit(0)
+
+ with open("scripts/generate-agents/config.json") as f:
+ config = json.load(f)
+
+ topics_by_output = {t["output"]: t for t in config["topics"]}
+
+ def read_sources(sources):
+ parts = []
+ for src in sources:
+ if os.path.exists(src):
+ with open(src) as f:
+ parts.append(f"### {src}\n\n{f.read()}")
+ else:
+ print(f"Warning: source file not found: {src}")
+ return "\n\n---\n\n".join(parts)
+
+ system_prompt = (
+ "You are generating AI agent skill files for the goravel/docs repository. "
+ "The output is used by AI coding agents (Cursor, Claude, Copilot) to write correct Goravel v1.17 code. "
+ "Rules:\n"
+ "- Output only what an AI needs to write code. No prose, no tutorial explanations.\n"
+ "- Show complete working code examples from the documentation.\n"
+ "- Include ALL configuration options, not just the defaults.\n"
+ "- List gotchas and wrong/correct patterns explicitly.\n"
+ "- Use exact import paths as shown in the docs.\n"
+ "- Mark v1.17 breaking changes with // BREAKING v1.17:\n"
+ "- Do not invent any behaviour not present in the source docs.\n"
+ "- Do not use em dashes. Write clearly and directly.\n"
+ "- If something is ambiguous, note it as a comment in code rather than inventing behaviour.\n"
+ )
+
+ for output_path in affected:
+ topic = topics_by_output.get(output_path)
+ if not topic:
+ print(f"No topic config found for: {output_path}")
+ continue
+
+ sources_content = read_sources(topic["sources"])
+
+ user_prompt = (
+ f"Regenerate the file `{output_path}` based on the following source documentation.\n\n"
+ f"{sources_content}\n\n"
+ f"Output only the raw Markdown content for `{output_path}`. "
+ f"No preamble, no commentary outside the file content."
+ )
+
+ print(f"Regenerating: {output_path}")
+
+ response = requests.post(
+ "https://api.deepseek.com/chat/completions",
+ headers={
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ },
+ json={
+ "model": "deepseek-chat",
+ "messages": [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": user_prompt},
+ ],
+ "temperature": 0.1,
+ "max_tokens": 8192,
+ },
+ timeout=120,
+ )
+
+ response.raise_for_status()
+ content = response.json()["choices"][0]["message"]["content"]
+
+ # Strip markdown code fences if the model wrapped the output
+ if content.startswith("```"):
+ lines = content.splitlines()
+ content = "\n".join(lines[1:-1]) if lines[-1].strip() == "```" else "\n".join(lines[1:])
+
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+ with open(output_path, "w") as f:
+ f.write(content)
+
+ print(f" Written: {output_path}")
+
+ print("Done.")
+ EOF
+ env:
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
+ AFFECTED_FILES: ${{ needs.detect.outputs.affected_outputs }}
+
+ - name: Open pull request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ branch: auto/regenerate-agent-files
+ commit-message: "chore: regenerate agent skill files"
+ title: "chore: regenerate .ai/ prompt files from updated docs"
+ body: |
+ This PR was opened automatically after a manually approved regeneration run.
+
+ **Changed source files:**
+ ${{ needs.detect.outputs.changed_sources }}
+
+ **Regenerated output files:**
+ ${{ needs.detect.outputs.affected_outputs }}
+
+ Please review the generated content before merging. Do not auto-merge.
+ delete-branch: true
+ draft: false
+ labels: "ai-generated,documentation"
diff --git a/scripts/generate-agents/config.json b/scripts/generate-agents/config.json
new file mode 100644
index 000000000..77b597f10
--- /dev/null
+++ b/scripts/generate-agents/config.json
@@ -0,0 +1,152 @@
+{
+ "topics": [
+ {
+ "output": ".ai/AGENTS.md",
+ "sources": [
+ "en/getting-started/installation.md",
+ "en/getting-started/configuration.md",
+ "en/getting-started/directory-structure.md",
+ "en/architecture-concepts/request-lifecycle.md",
+ "en/architecture-concepts/facades.md",
+ "en/architecture-concepts/service-container.md",
+ "en/architecture-concepts/service-providers.md",
+ "en/digging-deeper/artisan-console.md",
+ "en/database/migrations.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/bootstrap.md",
+ "sources": [
+ "en/architecture-concepts/request-lifecycle.md",
+ "en/architecture-concepts/service-container.md",
+ "en/architecture-concepts/service-providers.md",
+ "en/architecture-concepts/facades.md",
+ "en/getting-started/directory-structure.md",
+ "en/getting-started/configuration.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/route.md",
+ "sources": ["en/the-basics/routing.md", "en/the-basics/middleware.md"]
+ },
+ {
+ "output": ".ai/knowledge/request-response.md",
+ "sources": [
+ "en/the-basics/controllers.md",
+ "en/the-basics/request.md",
+ "en/the-basics/response.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/view.md",
+ "sources": ["en/the-basics/views.md"]
+ },
+ {
+ "output": ".ai/knowledge/session.md",
+ "sources": ["en/the-basics/session.md"]
+ },
+ {
+ "output": ".ai/knowledge/validation.md",
+ "sources": ["en/the-basics/validation.md"]
+ },
+ {
+ "output": ".ai/knowledge/log.md",
+ "sources": ["en/the-basics/logging.md"]
+ },
+ {
+ "output": ".ai/knowledge/grpc.md",
+ "sources": ["en/the-basics/grpc.md"]
+ },
+ {
+ "output": ".ai/knowledge/orm.md",
+ "sources": [
+ "en/orm/getting-started.md",
+ "en/orm/relationships.md",
+ "en/orm/factories.md",
+ "en/database/getting-started.md",
+ "en/database/queries.md",
+ "en/database/seeding.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/migration.md",
+ "sources": ["en/database/migrations.md"]
+ },
+ {
+ "output": ".ai/knowledge/auth.md",
+ "sources": [
+ "en/security/authentication.md",
+ "en/security/authorization.md",
+ "en/security/hashing.md",
+ "en/security/encryption.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/artisan.md",
+ "sources": ["en/digging-deeper/artisan-console.md"]
+ },
+ {
+ "output": ".ai/knowledge/schedule.md",
+ "sources": ["en/digging-deeper/task-scheduling.md"]
+ },
+ {
+ "output": ".ai/knowledge/cache.md",
+ "sources": ["en/digging-deeper/cache.md"]
+ },
+ {
+ "output": ".ai/knowledge/event.md",
+ "sources": ["en/digging-deeper/event.md"]
+ },
+ {
+ "output": ".ai/knowledge/queue.md",
+ "sources": ["en/digging-deeper/queues.md"]
+ },
+ {
+ "output": ".ai/knowledge/storage.md",
+ "sources": ["en/digging-deeper/filesystem.md"]
+ },
+ {
+ "output": ".ai/knowledge/mail.md",
+ "sources": ["en/digging-deeper/mail.md"]
+ },
+ {
+ "output": ".ai/knowledge/http-client.md",
+ "sources": ["en/digging-deeper/http-client.md"]
+ },
+ {
+ "output": ".ai/knowledge/process.md",
+ "sources": ["en/digging-deeper/processes.md"]
+ },
+ {
+ "output": ".ai/knowledge/localization.md",
+ "sources": ["en/digging-deeper/localization.md"]
+ },
+ {
+ "output": ".ai/knowledge/hash-crypt.md",
+ "sources": ["en/security/hashing.md", "en/security/encryption.md"]
+ },
+ {
+ "output": ".ai/knowledge/testing.md",
+ "sources": [
+ "en/testing/getting-started.md",
+ "en/testing/http-tests.md",
+ "en/testing/mock.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/helpers.md",
+ "sources": ["en/digging-deeper/helpers.md", "en/digging-deeper/color.md"]
+ },
+ {
+ "output": ".ai/knowledge/str.md",
+ "sources": [
+ "en/digging-deeper/strings.md",
+ "en/digging-deeper/pluralization.md"
+ ]
+ },
+ {
+ "output": ".ai/knowledge/carbon.md",
+ "sources": ["en/digging-deeper/helpers.md"]
+ }
+ ]
+}