{"section":"data","slug":"schema-sync","featureTitle":"Schema Sync, Not Migration Churn","summary":"Semitexa creates SQL only when the real schema changed, blocks destructive drops by default, and logs the exact DDL plan as SQL and JSON.","entryLine":"You do not hand-write busywork migrations all day. The ORM derives the plan, blocks dangerous drops by default, and records the exact SQL it ran.","highlights":["orm:sync","--dry-run","--allow-destructive","two-phase drop","AuditLogger"],"learnMoreLabel":"See the sync plan →","deepDiveLabel":"Why destructive changes are delayed →","documentBodyHtml":"<article class=\"sx-docs-fragment\" data-doc-id=\"data/schema-sync\" data-doc-locale=\"en\">\n<h1>Schema Sync, Not Migration Churn</h1>\n<p>Semitexa derives the schema change plan by comparing resource attribute definitions against the live database — no hand-written migration files required.</p>\n<h2>How it works</h2>\n<p>Running <code>bin/semitexa orm:sync</code> computes the diff between code and database. Safe operations execute immediately. Destructive operations such as <code>DROP COLUMN</code> are separated in the plan and require <code>--allow-destructive</code> to execute. A missing column triggers a two-phase flow: the first sync marks it deprecated, and a subsequent sync with the explicit flag performs the drop. Every executed sync writes an audit file as both <code>.json</code> and <code>.sql</code> to <code>var/migrations/history/</code>.</p>\n<h2>Why this matters</h2>\n<p>Teams waste time writing empty or obvious migrations that mirror what the code already says. Blocking destructive drops by default prevents accidental data loss, and the structured audit output gives ops a reviewable record of exactly what SQL ran and when.</p>\n</article>","sectionLabel":"Persistence","navSections":[{"key":"get-started","label":"Start Here","sidebarLabel":"Get Started","summary":"The shortest path from fresh scaffold to a trustworthy local Semitexa runtime: install, boot, understand the module map, bind a real host, and reach the first tenant boundary.","icon":"GO","eyebrow":"Onboarding","starter":true,"prerequisites":[],"featureCount":7,"href":"/demo/get-started","features":[{"section":"get-started","slug":"module-structure","label":"Start Here","title":"Module Structure","summary":"The minimal Semitexa module is a typed HTTP spine of payload, handler, resource, and template.","opensInNewTab":false,"href":"/demo/get-started/module-structure"},{"section":"get-started","slug":"installation","label":"Start Here","title":"Installation","summary":"Create the project, review the baseline env contract, and bring up the Semitexa runtime the supported way.","opensInNewTab":false,"href":"/demo/get-started/installation"},{"section":"get-started","slug":"local-domain","label":"Start Here","title":"Local Domain","summary":"Register .test domains through the built-in local-domain helper instead of relying on ad hoc host setup.","opensInNewTab":false,"href":"/demo/get-started/local-domain"},{"section":"get-started","slug":"base-tenant","label":"Start Here","title":"Base Tenant","summary":"Establish one default tenant context early so tenant-aware behavior is visible before the rest of the application grows.","opensInNewTab":false,"href":"/demo/get-started/base-tenant"},{"section":"get-started","slug":"locale-setup","label":"Start Here","title":"Locale Setup","summary":"Configure the minimal Locale contract so translations and locale-aware rendering become explicit early.","opensInNewTab":false,"href":"/demo/get-started/locale-setup"},{"section":"get-started","slug":"ai-console","label":"Start Here","title":"AI Console","summary":"Use the Semitexa AI console as a command translation surface over real operator commands.","opensInNewTab":false,"href":"/demo/get-started/ai-console"},{"section":"get-started","slug":"beyond-controllers","label":"Start Here","title":"Beyond Controllers","summary":"Understand why Semitexa keeps transport, use case, and rendering as separate explicit responsibilities.","opensInNewTab":false,"href":"/demo/get-started/beyond-controllers"}],"groups":[{"key":"structure","label":"Module Structure","featureCount":1,"features":[{"section":"get-started","slug":"module-structure","label":"Start Here","title":"Module Structure","summary":"The minimal Semitexa module is a typed HTTP spine of payload, handler, resource, and template.","opensInNewTab":false,"href":"/demo/get-started/module-structure"}]},{"key":"onboarding","label":"Onboarding","featureCount":6,"features":[{"section":"get-started","slug":"installation","label":"Start Here","title":"Installation","summary":"Create the project, review the baseline env contract, and bring up the Semitexa runtime the supported way.","opensInNewTab":false,"href":"/demo/get-started/installation"},{"section":"get-started","slug":"local-domain","label":"Start Here","title":"Local Domain","summary":"Register .test domains through the built-in local-domain helper instead of relying on ad hoc host setup.","opensInNewTab":false,"href":"/demo/get-started/local-domain"},{"section":"get-started","slug":"base-tenant","label":"Start Here","title":"Base Tenant","summary":"Establish one default tenant context early so tenant-aware behavior is visible before the rest of the application grows.","opensInNewTab":false,"href":"/demo/get-started/base-tenant"},{"section":"get-started","slug":"locale-setup","label":"Start Here","title":"Locale Setup","summary":"Configure the minimal Locale contract so translations and locale-aware rendering become explicit early.","opensInNewTab":false,"href":"/demo/get-started/locale-setup"},{"section":"get-started","slug":"ai-console","label":"Start Here","title":"AI Console","summary":"Use the Semitexa AI console as a command translation surface over real operator commands.","opensInNewTab":false,"href":"/demo/get-started/ai-console"},{"section":"get-started","slug":"beyond-controllers","label":"Start Here","title":"Beyond Controllers","summary":"Understand why Semitexa keeps transport, use case, and rendering as separate explicit responsibilities.","opensInNewTab":false,"href":"/demo/get-started/beyond-controllers"}]}]},{"key":"routing","label":"Routing & Handlers","summary":"Attribute-driven routes, typed handlers, and content negotiation in one coherent request pipeline.","icon":"RT","eyebrow":"HTTP Layer","starter":true,"prerequisites":[],"featureCount":7,"href":"/demo/routing","features":[{"section":"routing","slug":"basic","label":"Routing & Handlers","title":"Basic Route","summary":"Define a route with one access attribute on the payload — no XML, no YAML, no config files.","opensInNewTab":false,"href":"/demo/routing/basic"},{"section":"routing","slug":"parameterized","label":"Routing & Handlers","title":"Parameterized Route","summary":"Path parameters with regex constraints and typed injection.","opensInNewTab":false,"href":"/demo/routing/parameterized/headphones"},{"section":"routing","slug":"env-route-override","label":"Routing & Handlers","title":"Env Route Override","summary":"Keep the payload as the route source of truth while allowing operations to remap the public URL through .env.","opensInNewTab":false,"href":"/demo/routing/env-route-override"},{"section":"routing","slug":"payload-shield","label":"Routing & Handlers","title":"Payload As A Shield","summary":"Hydration happens before the handler, and each setter owns the normalization and guard logic for its own field.","opensInNewTab":false,"href":"/demo/routing/payload-shield"},{"section":"routing","slug":"payload-parts","label":"Routing & Handlers","title":"Payload Parts","summary":"One module owns the route, another module can extend the same payload contract without forking or reopening the base class.","opensInNewTab":false,"href":"/demo/routing/payload-parts"},{"section":"routing","slug":"content-negotiation","label":"Routing & Handlers","title":"Content Negotiation","summary":"One endpoint, multiple response formats — automatically.","opensInNewTab":false,"href":"/demo/routing/content-negotiation"},{"section":"routing","slug":"public-endpoint","label":"Routing & Handlers","title":"Public Payload","summary":"Anonymous endpoints opt in explicitly with the public access attribute. Every other payload requires authentication.","opensInNewTab":false,"href":"/demo/routing/public-endpoint"}],"groups":[{"key":"foundations","label":"Foundations","featureCount":3,"features":[{"section":"routing","slug":"basic","label":"Routing & Handlers","title":"Basic Route","summary":"Define a route with one access attribute on the payload — no XML, no YAML, no config files.","opensInNewTab":false,"href":"/demo/routing/basic"},{"section":"routing","slug":"parameterized","label":"Routing & Handlers","title":"Parameterized Route","summary":"Path parameters with regex constraints and typed injection.","opensInNewTab":false,"href":"/demo/routing/parameterized/headphones"},{"section":"routing","slug":"env-route-override","label":"Routing & Handlers","title":"Env Route Override","summary":"Keep the payload as the route source of truth while allowing operations to remap the public URL through .env.","opensInNewTab":false,"href":"/demo/routing/env-route-override"}]},{"key":"request-model","label":"Request Model","featureCount":2,"features":[{"section":"routing","slug":"payload-shield","label":"Routing & Handlers","title":"Payload As A Shield","summary":"Hydration happens before the handler, and each setter owns the normalization and guard logic for its own field.","opensInNewTab":false,"href":"/demo/routing/payload-shield"},{"section":"routing","slug":"payload-parts","label":"Routing & Handlers","title":"Payload Parts","summary":"One module owns the route, another module can extend the same payload contract without forking or reopening the base class.","opensInNewTab":false,"href":"/demo/routing/payload-parts"}]},{"key":"delivery","label":"Delivery","featureCount":2,"features":[{"section":"routing","slug":"content-negotiation","label":"Routing & Handlers","title":"Content Negotiation","summary":"One endpoint, multiple response formats — automatically.","opensInNewTab":false,"href":"/demo/routing/content-negotiation"},{"section":"routing","slug":"public-endpoint","label":"Routing & Handlers","title":"Public Payload","summary":"Anonymous endpoints opt in explicitly with the public access attribute. Every other payload requires authentication.","opensInNewTab":false,"href":"/demo/routing/public-endpoint"}]}]},{"key":"di","label":"Dependency Injection","summary":"Single-path DI with explicit lifecycles, deterministic contracts, and stable boot behavior for long-running workers.","icon":"DI","eyebrow":"Container","starter":true,"prerequisites":[],"featureCount":5,"href":"/demo/di","features":[{"section":"di","slug":"overview","label":"Dependency Injection","title":"DI Canon","summary":"One canonical DI path for container-managed classes: explicit properties, explicit lifecycles, deterministic boot.","opensInNewTab":false,"href":"/demo/di/overview"},{"section":"di","slug":"readonly","label":"Dependency Injection","title":"Readonly Injection","summary":"One explicit DI path, one shared worker instance — fast at runtime and stable under reload.","opensInNewTab":false,"href":"/demo/di/readonly"},{"section":"di","slug":"mutable","label":"Dependency Injection","title":"Mutable Injection","summary":"Execution-scoped services get a fresh clone every run — safe state without contaminating the worker.","opensInNewTab":false,"href":"/demo/di/mutable"},{"section":"di","slug":"factory","label":"Dependency Injection","title":"Factory Injection","summary":"On-demand creation stays explicit — lazy instances without falling back to service locator habits.","opensInNewTab":false,"href":"/demo/di/factory"},{"section":"di","slug":"contracts","label":"Dependency Injection","title":"Service Contracts","summary":"Depend on contracts, but keep ownership explicit — deterministic substitution instead of runtime magic.","opensInNewTab":false,"href":"/demo/di/contracts"}],"groups":[{"key":"container-basics","label":"Container Basics","featureCount":5,"features":[{"section":"di","slug":"overview","label":"Dependency Injection","title":"DI Canon","summary":"One canonical DI path for container-managed classes: explicit properties, explicit lifecycles, deterministic boot.","opensInNewTab":false,"href":"/demo/di/overview"},{"section":"di","slug":"readonly","label":"Dependency Injection","title":"Readonly Injection","summary":"One explicit DI path, one shared worker instance — fast at runtime and stable under reload.","opensInNewTab":false,"href":"/demo/di/readonly"},{"section":"di","slug":"mutable","label":"Dependency Injection","title":"Mutable Injection","summary":"Execution-scoped services get a fresh clone every run — safe state without contaminating the worker.","opensInNewTab":false,"href":"/demo/di/mutable"},{"section":"di","slug":"factory","label":"Dependency Injection","title":"Factory Injection","summary":"On-demand creation stays explicit — lazy instances without falling back to service locator habits.","opensInNewTab":false,"href":"/demo/di/factory"},{"section":"di","slug":"contracts","label":"Dependency Injection","title":"Service Contracts","summary":"Depend on contracts, but keep ownership explicit — deterministic substitution instead of runtime magic.","opensInNewTab":false,"href":"/demo/di/contracts"}]}]},{"key":"data","label":"Persistence","summary":"Attribute-mapped resources, repositories, filtering, pagination, and relations with real demo data.","icon":"DB","eyebrow":"Persistence","starter":true,"prerequisites":[],"featureCount":9,"href":"/demo/data","features":[{"section":"data","slug":"domain-models","label":"Persistence","title":"Domain-Level Models","summary":"Semitexa separates persistence resources from business models. Resources map tables; domain models carry behavior and invariants.","opensInNewTab":false,"href":"/demo/data/domain-models"},{"section":"data","slug":"repository-workflow","label":"Persistence","title":"Repository Workflow","summary":"The canonical Semitexa path: handlers depend on repository contracts, repositories return domain models, and persistence resources stay behind the boundary.","opensInNewTab":false,"href":"/demo/data/repository-workflow"},{"section":"data","slug":"schema-sync","label":"Persistence","title":"Schema Sync, Not Migration Churn","summary":"Semitexa creates SQL only when the real schema changed, blocks destructive drops by default, and logs the exact DDL plan as SQL and JSON.","opensInNewTab":false,"href":"/demo/data/schema-sync"},{"section":"data","slug":"query","label":"Persistence","title":"Query Builder","summary":"Compose type-safe queries with a fluent API — no raw SQL, no magic strings.","opensInNewTab":false,"href":"/demo/data/query"},{"section":"data","slug":"filtering","label":"Persistence","title":"Filtering","summary":"Mark a property #[Filterable] and the ORM handles the rest — no manual WHERE clauses.","opensInNewTab":false,"href":"/demo/data/filtering"},{"section":"data","slug":"pagination","label":"Persistence","title":"Pagination","summary":"Offset and cursor pagination out of the box — switch modes with a single query parameter.","opensInNewTab":false,"href":"/demo/data/pagination"},{"section":"data","slug":"relations","label":"Persistence","title":"Relations","summary":"Declare parent and child links on the resource itself, then read typed relations from the handler.","opensInNewTab":false,"href":"/demo/data/relations"},{"section":"data","slug":"table-extension","label":"Persistence","title":"Shared Table Extension","summary":"Two modules can extend one table independently, and the ORM merges the schema without forcing either side to edit the other.","opensInNewTab":false,"href":"/demo/data/table-extension"},{"section":"data","slug":"n-plus-one","label":"Persistence","title":"N+1 Without Magic","summary":"Semitexa avoids N+1 by using resource slices for the exact columns and relations each screen needs, instead of hiding database traffic behind implicit relation loading.","opensInNewTab":false,"href":"/demo/data/n-plus-one"}],"groups":[{"key":"modeling","label":"Modeling & Workflow","featureCount":3,"features":[{"section":"data","slug":"domain-models","label":"Persistence","title":"Domain-Level Models","summary":"Semitexa separates persistence resources from business models. Resources map tables; domain models carry behavior and invariants.","opensInNewTab":false,"href":"/demo/data/domain-models"},{"section":"data","slug":"repository-workflow","label":"Persistence","title":"Repository Workflow","summary":"The canonical Semitexa path: handlers depend on repository contracts, repositories return domain models, and persistence resources stay behind the boundary.","opensInNewTab":false,"href":"/demo/data/repository-workflow"},{"section":"data","slug":"schema-sync","label":"Persistence","title":"Schema Sync, Not Migration Churn","summary":"Semitexa creates SQL only when the real schema changed, blocks destructive drops by default, and logs the exact DDL plan as SQL and JSON.","opensInNewTab":false,"href":"/demo/data/schema-sync"}]},{"key":"querying","label":"Querying","featureCount":6,"features":[{"section":"data","slug":"query","label":"Persistence","title":"Query Builder","summary":"Compose type-safe queries with a fluent API — no raw SQL, no magic strings.","opensInNewTab":false,"href":"/demo/data/query"},{"section":"data","slug":"filtering","label":"Persistence","title":"Filtering","summary":"Mark a property #[Filterable] and the ORM handles the rest — no manual WHERE clauses.","opensInNewTab":false,"href":"/demo/data/filtering"},{"section":"data","slug":"pagination","label":"Persistence","title":"Pagination","summary":"Offset and cursor pagination out of the box — switch modes with a single query parameter.","opensInNewTab":false,"href":"/demo/data/pagination"},{"section":"data","slug":"relations","label":"Persistence","title":"Relations","summary":"Declare parent and child links on the resource itself, then read typed relations from the handler.","opensInNewTab":false,"href":"/demo/data/relations"},{"section":"data","slug":"table-extension","label":"Persistence","title":"Shared Table Extension","summary":"Two modules can extend one table independently, and the ORM merges the schema without forcing either side to edit the other.","opensInNewTab":false,"href":"/demo/data/table-extension"},{"section":"data","slug":"n-plus-one","label":"Persistence","title":"N+1 Without Magic","summary":"Semitexa avoids N+1 by using resource slices for the exact columns and relations each screen needs, instead of hiding database traffic behind implicit relation loading.","opensInNewTab":false,"href":"/demo/data/n-plus-one"}]}]},{"key":"auth","label":"Security","summary":"Typed session payloads, machine credentials, RBAC, and route protection without string-key auth chaos.","icon":"AU","eyebrow":"Security","starter":false,"prerequisites":[],"featureCount":7,"href":"/demo/auth","features":[{"section":"auth","slug":"session","label":"Security","title":"Session Auth","summary":"Google signs the user in, then the session stores the selected demo role and re-hydrates it on every request.","opensInNewTab":false,"href":"/demo/auth/session"},{"section":"auth","slug":"session-payloads","label":"Security","title":"Session Payloads","summary":"Semitexa forbids string-key session chaos: session state lives in typed Session Payloads or it does not exist.","opensInNewTab":false,"href":"/demo/auth/session-payloads"},{"section":"auth","slug":"google","label":"Security","title":"Google Authorization","summary":"Authorization is required for demo SSE blocks that keep a long-lived backend connection open.","opensInNewTab":false,"href":"/demo/auth/google"},{"section":"auth","slug":"machine","label":"Security","title":"Machine Auth","summary":"Service-to-service authentication via Bearer tokens — scoped, revocable, and audited.","opensInNewTab":false,"href":"/demo/auth/machine"},{"section":"auth","slug":"protected","label":"Security","title":"Protected Route","summary":"Add one access attribute and one optional permission attribute and the framework enforces access — 401 for unauthenticated requests, 403 for unauthorized ones.","opensInNewTab":false,"href":"/demo/auth/protected"},{"section":"auth","slug":"requires-permission","label":"Security","title":"Requires Permission","summary":"Declare one permission slug on the payload and let the framework enforce it before your handler runs.","opensInNewTab":false,"href":"/demo/auth/requires-permission"},{"section":"auth","slug":"rbac","label":"Security","title":"RBAC","summary":"Hybrid RBAC with coarse-grained capabilities, exact permission slugs, and module-owned permission catalogs.","opensInNewTab":false,"href":"/demo/auth/rbac"}],"groups":[{"key":"identity","label":"Identity","featureCount":4,"features":[{"section":"auth","slug":"session","label":"Security","title":"Session Auth","summary":"Google signs the user in, then the session stores the selected demo role and re-hydrates it on every request.","opensInNewTab":false,"href":"/demo/auth/session"},{"section":"auth","slug":"session-payloads","label":"Security","title":"Session Payloads","summary":"Semitexa forbids string-key session chaos: session state lives in typed Session Payloads or it does not exist.","opensInNewTab":false,"href":"/demo/auth/session-payloads"},{"section":"auth","slug":"google","label":"Security","title":"Google Authorization","summary":"Authorization is required for demo SSE blocks that keep a long-lived backend connection open.","opensInNewTab":false,"href":"/demo/auth/google"},{"section":"auth","slug":"machine","label":"Security","title":"Machine Auth","summary":"Service-to-service authentication via Bearer tokens — scoped, revocable, and audited.","opensInNewTab":false,"href":"/demo/auth/machine"}]},{"key":"access-control","label":"Access Control","featureCount":3,"features":[{"section":"auth","slug":"protected","label":"Security","title":"Protected Route","summary":"Add one access attribute and one optional permission attribute and the framework enforces access — 401 for unauthenticated requests, 403 for unauthorized ones.","opensInNewTab":false,"href":"/demo/auth/protected"},{"section":"auth","slug":"requires-permission","label":"Security","title":"Requires Permission","summary":"Declare one permission slug on the payload and let the framework enforce it before your handler runs.","opensInNewTab":false,"href":"/demo/auth/requires-permission"},{"section":"auth","slug":"rbac","label":"Security","title":"RBAC","summary":"Hybrid RBAC with coarse-grained capabilities, exact permission slugs, and module-owned permission catalogs.","opensInNewTab":false,"href":"/demo/auth/rbac"}]}]},{"key":"events","label":"Async","summary":"Synchronous and deferred event flows, queues, and SSE-style interactions.","icon":"EV","eyebrow":"Async","starter":false,"prerequisites":[],"featureCount":5,"href":"/demo/events","features":[{"section":"events","slug":"arena","label":"Async","title":"Execution Arena","summary":"Launch the same backend intent in sync, Swoole async, and queued modes, then watch the proof arrive over SSE.","opensInNewTab":false,"href":"/demo/events/arena"},{"section":"events","slug":"sync","label":"Async","title":"Sync Events","summary":"Dispatch an event and all sync listeners run before the response is sent.","opensInNewTab":false,"href":"/demo/events/sync"},{"section":"events","slug":"deferred","label":"Async","title":"Deferred Handler","summary":"Heavy work runs after the response is sent — the user gets instant feedback.","opensInNewTab":false,"href":"/demo/events/deferred"},{"section":"events","slug":"queued","label":"Async","title":"Queued Handler","summary":"Events survive restarts and scale across workers — backed by a durable message queue.","opensInNewTab":false,"href":"/demo/events/queued"},{"section":"events","slug":"sse","label":"Async","title":"SSE Stream","summary":"Real-time server push without WebSockets — connect once and receive real backend events over plain HTTP.","opensInNewTab":false,"href":"/demo/events/sse"}],"groups":[{"key":"event-flow","label":"Event Flow","featureCount":5,"features":[{"section":"events","slug":"arena","label":"Async","title":"Execution Arena","summary":"Launch the same backend intent in sync, Swoole async, and queued modes, then watch the proof arrive over SSE.","opensInNewTab":false,"href":"/demo/events/arena"},{"section":"events","slug":"sync","label":"Async","title":"Sync Events","summary":"Dispatch an event and all sync listeners run before the response is sent.","opensInNewTab":false,"href":"/demo/events/sync"},{"section":"events","slug":"deferred","label":"Async","title":"Deferred Handler","summary":"Heavy work runs after the response is sent — the user gets instant feedback.","opensInNewTab":false,"href":"/demo/events/deferred"},{"section":"events","slug":"queued","label":"Async","title":"Queued Handler","summary":"Events survive restarts and scale across workers — backed by a durable message queue.","opensInNewTab":false,"href":"/demo/events/queued"},{"section":"events","slug":"sse","label":"Async","title":"SSE Stream","summary":"Real-time server push without WebSockets — connect once and receive real backend events over plain HTTP.","opensInNewTab":false,"href":"/demo/events/sse"}]}]},{"key":"rendering","label":"UI Rendering & SSR","summary":"One rendering story from handler to HTML: page data, page regions, and live updates stay in the same server-driven model instead of splitting into frontend and backend template logic.","icon":"UI","eyebrow":"Frontend","starter":false,"prerequisites":[],"featureCount":15,"href":"/demo/rendering","features":[{"section":"rendering","slug":"philosophy","label":"UI Rendering & SSR","title":"SSR Philosophy","summary":"Semitexa SSR is one continuous rendering architecture: page, slots, deferred regions, live refresh, and interactive components stay inside one server-owned story.","opensInNewTab":false,"href":"/demo/rendering/philosophy"},{"section":"rendering","slug":"resource-dtos","label":"UI Rendering & SSR","title":"Resource DTOs","summary":"A Resource DTO is the one typed source of presentation data: handlers shape it once, templates consume it everywhere, and no view has to dissect random arrays.","opensInNewTab":false,"href":"/demo/rendering/resource-dtos"},{"section":"rendering","slug":"slots","label":"UI Rendering & SSR","title":"Slot Resources","summary":"Each page region is its own resource pipeline with the same template system as the main page — no scattered partial glue, no mystery wiring.","opensInNewTab":false,"href":"/demo/rendering/slots"},{"section":"rendering","slug":"components","label":"UI Rendering & SSR","title":"Components","summary":"Reusable, attribute-registered UI components — discovered automatically from the classmap.","opensInNewTab":false,"href":"/demo/rendering/components"},{"section":"rendering","slug":"seo","label":"UI Rendering & SSR","title":"SEO","summary":"Set title, description, and Open Graph tags from your handler — no template hacks needed.","opensInNewTab":false,"href":"/demo/rendering/seo"},{"section":"rendering","slug":"assets","label":"UI Rendering & SSR","title":"Asset Pipeline","summary":"Declare assets with glob patterns in assets.json — served, versioned, and injected automatically.","opensInNewTab":false,"href":"/demo/rendering/assets"},{"section":"rendering","slug":"component-scripts","label":"UI Rendering & SSR","title":"Component Script Assets","summary":"A Semitexa SSR component can own its optional enhancement asset, so behavior travels with the component instead of leaking into page-level glue.","opensInNewTab":false,"href":"/demo/rendering/component-scripts"},{"section":"rendering","slug":"deferred-scripts","label":"UI Rendering & SSR","title":"Script Injection","summary":"Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.","opensInNewTab":false,"href":"/demo/rendering/deferred-scripts"},{"section":"rendering","slug":"deferred","label":"UI Rendering & SSR","title":"Deferred Blocks","summary":"SSR renders the shell first, then expensive regions stream in as real HTML over SSE — no SPA handoff and no client-side page rebuild.","opensInNewTab":true,"href":"/demo/rendering/deferred"},{"section":"rendering","slug":"deferred-encapsulation","label":"UI Rendering & SSR","title":"Block Isolation","summary":"Two identical blocks on the same page run independently — scoped DOM, scoped JS, no conflicts.","opensInNewTab":false,"href":"/demo/rendering/deferred-encapsulation"},{"section":"rendering","slug":"deferred-live","label":"UI Rendering & SSR","title":"Live Widgets","summary":"A live slot can refresh itself on a timer while the page stays SSR-first — no SPA runtime and no handwritten polling layer.","opensInNewTab":false,"href":"/demo/rendering/deferred-live"},{"section":"rendering","slug":"reactive-report","label":"UI Rendering & SSR","title":"Reactive Report","summary":"Background work updates an SSR-first slot in place, so the UI feels live without falling back to SPA state orchestration.","opensInNewTab":false,"href":"/demo/rendering/reactive-report"},{"section":"rendering","slug":"reactive-import","label":"UI Rendering & SSR","title":"Reactive Import","summary":"Background batches keep moving, and the page reflects server progress as live HTML instead of a client-managed progress app.","opensInNewTab":false,"href":"/demo/rendering/reactive-import"},{"section":"rendering","slug":"reactive-analytics","label":"UI Rendering & SSR","title":"Reactive Analytics","summary":"Independent analytics jobs can light up one dashboard progressively, while the page stays server-rendered from the first byte.","opensInNewTab":false,"href":"/demo/rendering/reactive-analytics"},{"section":"rendering","slug":"reactive-ai","label":"UI Rendering & SSR","title":"Reactive AI Task","summary":"Submit a task and watch the AI pipeline stages reveal one by one as the cron job processes it.","opensInNewTab":false,"href":"/demo/rendering/reactive-ai"}],"groups":[{"key":"rendering-model","label":"SSR Foundation","featureCount":8,"features":[{"section":"rendering","slug":"philosophy","label":"UI Rendering & SSR","title":"SSR Philosophy","summary":"Semitexa SSR is one continuous rendering architecture: page, slots, deferred regions, live refresh, and interactive components stay inside one server-owned story.","opensInNewTab":false,"href":"/demo/rendering/philosophy"},{"section":"rendering","slug":"resource-dtos","label":"UI Rendering & SSR","title":"Resource DTOs","summary":"A Resource DTO is the one typed source of presentation data: handlers shape it once, templates consume it everywhere, and no view has to dissect random arrays.","opensInNewTab":false,"href":"/demo/rendering/resource-dtos"},{"section":"rendering","slug":"slots","label":"UI Rendering & SSR","title":"Slot Resources","summary":"Each page region is its own resource pipeline with the same template system as the main page — no scattered partial glue, no mystery wiring.","opensInNewTab":false,"href":"/demo/rendering/slots"},{"section":"rendering","slug":"components","label":"UI Rendering & SSR","title":"Components","summary":"Reusable, attribute-registered UI components — discovered automatically from the classmap.","opensInNewTab":false,"href":"/demo/rendering/components"},{"section":"rendering","slug":"seo","label":"UI Rendering & SSR","title":"SEO","summary":"Set title, description, and Open Graph tags from your handler — no template hacks needed.","opensInNewTab":false,"href":"/demo/rendering/seo"},{"section":"rendering","slug":"assets","label":"UI Rendering & SSR","title":"Asset Pipeline","summary":"Declare assets with glob patterns in assets.json — served, versioned, and injected automatically.","opensInNewTab":false,"href":"/demo/rendering/assets"},{"section":"rendering","slug":"component-scripts","label":"UI Rendering & SSR","title":"Component Script Assets","summary":"A Semitexa SSR component can own its optional enhancement asset, so behavior travels with the component instead of leaking into page-level glue.","opensInNewTab":false,"href":"/demo/rendering/component-scripts"},{"section":"rendering","slug":"deferred-scripts","label":"UI Rendering & SSR","title":"Script Injection","summary":"Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.","opensInNewTab":false,"href":"/demo/rendering/deferred-scripts"}]},{"key":"deferred","label":"Deferred Delivery","featureCount":2,"features":[{"section":"rendering","slug":"deferred","label":"UI Rendering & SSR","title":"Deferred Blocks","summary":"SSR renders the shell first, then expensive regions stream in as real HTML over SSE — no SPA handoff and no client-side page rebuild.","opensInNewTab":true,"href":"/demo/rendering/deferred"},{"section":"rendering","slug":"deferred-encapsulation","label":"UI Rendering & SSR","title":"Block Isolation","summary":"Two identical blocks on the same page run independently — scoped DOM, scoped JS, no conflicts.","opensInNewTab":false,"href":"/demo/rendering/deferred-encapsulation"}]},{"key":"live","label":"Reactive UI","featureCount":5,"features":[{"section":"rendering","slug":"deferred-live","label":"UI Rendering & SSR","title":"Live Widgets","summary":"A live slot can refresh itself on a timer while the page stays SSR-first — no SPA runtime and no handwritten polling layer.","opensInNewTab":false,"href":"/demo/rendering/deferred-live"},{"section":"rendering","slug":"reactive-report","label":"UI Rendering & SSR","title":"Reactive Report","summary":"Background work updates an SSR-first slot in place, so the UI feels live without falling back to SPA state orchestration.","opensInNewTab":false,"href":"/demo/rendering/reactive-report"},{"section":"rendering","slug":"reactive-import","label":"UI Rendering & SSR","title":"Reactive Import","summary":"Background batches keep moving, and the page reflects server progress as live HTML instead of a client-managed progress app.","opensInNewTab":false,"href":"/demo/rendering/reactive-import"},{"section":"rendering","slug":"reactive-analytics","label":"UI Rendering & SSR","title":"Reactive Analytics","summary":"Independent analytics jobs can light up one dashboard progressively, while the page stays server-rendered from the first byte.","opensInNewTab":false,"href":"/demo/rendering/reactive-analytics"},{"section":"rendering","slug":"reactive-ai","label":"UI Rendering & SSR","title":"Reactive AI Task","summary":"Submit a task and watch the AI pipeline stages reveal one by one as the cron job processes it.","opensInNewTab":false,"href":"/demo/rendering/reactive-ai"}]}]},{"key":"platform","label":"Tenancy","summary":"Multi-tenant resolution, tenant-aware configuration, and strict isolation of data and background work.","icon":"TN","eyebrow":"Multi-Tenant","starter":false,"prerequisites":["data","rendering"],"featureCount":5,"href":"/demo/platform","features":[{"section":"platform","slug":"tenancy-resolution","label":"Tenancy","title":"Tenant Context Resolution","summary":"See how Semitexa resolves the active tenant from subdomain, header, path, or query input before the rest of the platform runs.","opensInNewTab":false,"href":"/demo/platform/tenancy-resolution"},{"section":"platform","slug":"tenancy-config","label":"Tenancy","title":"Per-Tenant Configuration","summary":"Three demo tenants with distinct branding -- switch tenant, everything changes without if/else.","opensInNewTab":false,"href":"/demo/platform/tenancy-config"},{"section":"platform","slug":"tenancy-layers","label":"Tenancy","title":"Multi-Layer Tenancy","summary":"Organization, Locale, Theme, Environment -- four independent layers compose into one TenantContext.","opensInNewTab":false,"href":"/demo/platform/tenancy-layers"},{"section":"platform","slug":"tenancy-isolation","label":"Tenancy","title":"Data Isolation","summary":"Product listing scoped by tenant -- switch tenant, list changes. Zero manual WHERE clauses.","opensInNewTab":false,"href":"/demo/platform/tenancy-isolation"},{"section":"platform","slug":"tenancy-queue","label":"Tenancy","title":"Queue Tenant Propagation","summary":"Tenant context travels with queued jobs -- _tenant key injected automatically, restored by worker.","opensInNewTab":false,"href":"/demo/platform/tenancy-queue"}],"groups":[{"key":"resolution","label":"Tenant Resolution","featureCount":1,"features":[{"section":"platform","slug":"tenancy-resolution","label":"Tenancy","title":"Tenant Context Resolution","summary":"See how Semitexa resolves the active tenant from subdomain, header, path, or query input before the rest of the platform runs.","opensInNewTab":false,"href":"/demo/platform/tenancy-resolution"}]},{"key":"configuration","label":"Tenant Configuration","featureCount":2,"features":[{"section":"platform","slug":"tenancy-config","label":"Tenancy","title":"Per-Tenant Configuration","summary":"Three demo tenants with distinct branding -- switch tenant, everything changes without if/else.","opensInNewTab":false,"href":"/demo/platform/tenancy-config"},{"section":"platform","slug":"tenancy-layers","label":"Tenancy","title":"Multi-Layer Tenancy","summary":"Organization, Locale, Theme, Environment -- four independent layers compose into one TenantContext.","opensInNewTab":false,"href":"/demo/platform/tenancy-layers"}]},{"key":"isolation","label":"Isolation & Work","featureCount":2,"features":[{"section":"platform","slug":"tenancy-isolation","label":"Tenancy","title":"Data Isolation","summary":"Product listing scoped by tenant -- switch tenant, list changes. Zero manual WHERE clauses.","opensInNewTab":false,"href":"/demo/platform/tenancy-isolation"},{"section":"platform","slug":"tenancy-queue","label":"Tenancy","title":"Queue Tenant Propagation","summary":"Tenant context travels with queued jobs -- _tenant key injected automatically, restored by worker.","opensInNewTab":false,"href":"/demo/platform/tenancy-queue"}]}]},{"key":"api","label":"API","summary":"External API endpoints, machine auth, versioning, and consumer-facing schema behavior.","icon":"API","eyebrow":"Machine","starter":false,"prerequisites":["routing","auth"],"featureCount":7,"href":"/demo/api","features":[{"section":"api","slug":"rest-api","label":"API","title":"REST API","summary":"Classic Semitexa REST endpoints with typed payloads, versioning, and consumer-friendly response shaping.","opensInNewTab":false,"href":"/demo/api/rest-api"},{"section":"api","slug":"structured-errors","label":"API","title":"Structured Errors","summary":"Throw domain exceptions and let semitexa-api map them into stable machine-readable error envelopes.","opensInNewTab":false,"href":"/demo/api/structured-errors"},{"section":"api","slug":"active-version","label":"API","title":"Active Version","summary":"The current collection endpoint with a clean X-Api-Version header and no deprecation noise.","opensInNewTab":false,"href":"/demo/api/active-version"},{"section":"api","slug":"sunset-version","label":"API","title":"Sunset Version","summary":"A deprecated product endpoint that emits both Deprecation and Sunset headers.","opensInNewTab":false,"href":"/demo/api/sunset-version"},{"section":"api","slug":"schema-discovery","label":"API","title":"Schema Discovery","summary":"A mini Swagger-style explorer for the live product API contract, schema endpoint, and response shapes.","opensInNewTab":false,"href":"/demo/api/schema-discovery"},{"section":"api","slug":"graphql","label":"API","title":"GraphQL API","summary":"GraphQL-first Semitexa contracts built with typed payloads and typed output DTOs instead of resolver sprawl.","opensInNewTab":false,"href":"/demo/api/graphql"},{"section":"api","slug":"rest-graphql","label":"API","title":"REST + GraphQL","summary":"One Semitexa use case can serve both REST and GraphQL without duplicating handler logic into separate resolver classes.","opensInNewTab":false,"href":"/demo/api/rest-graphql"}],"groups":[{"key":"public-api","label":"REST Surface","featureCount":4,"features":[{"section":"api","slug":"rest-api","label":"API","title":"REST API","summary":"Classic Semitexa REST endpoints with typed payloads, versioning, and consumer-friendly response shaping.","opensInNewTab":false,"href":"/demo/api/rest-api"},{"section":"api","slug":"structured-errors","label":"API","title":"Structured Errors","summary":"Throw domain exceptions and let semitexa-api map them into stable machine-readable error envelopes.","opensInNewTab":false,"href":"/demo/api/structured-errors"},{"section":"api","slug":"active-version","label":"API","title":"Active Version","summary":"The current collection endpoint with a clean X-Api-Version header and no deprecation noise.","opensInNewTab":false,"href":"/demo/api/active-version"},{"section":"api","slug":"sunset-version","label":"API","title":"Sunset Version","summary":"A deprecated product endpoint that emits both Deprecation and Sunset headers.","opensInNewTab":false,"href":"/demo/api/sunset-version"}]},{"key":"schema","label":"Schema Discovery","featureCount":3,"features":[{"section":"api","slug":"schema-discovery","label":"API","title":"Schema Discovery","summary":"A mini Swagger-style explorer for the live product API contract, schema endpoint, and response shapes.","opensInNewTab":false,"href":"/demo/api/schema-discovery"},{"section":"api","slug":"graphql","label":"API","title":"GraphQL API","summary":"GraphQL-first Semitexa contracts built with typed payloads and typed output DTOs instead of resolver sprawl.","opensInNewTab":false,"href":"/demo/api/graphql"},{"section":"api","slug":"rest-graphql","label":"API","title":"REST + GraphQL","summary":"One Semitexa use case can serve both REST and GraphQL without duplicating handler logic into separate resolver classes.","opensInNewTab":false,"href":"/demo/api/rest-graphql"}]}]},{"key":"cli","label":"CLI","summary":"Operational, introspection, and AI-oriented command surfaces that explain and drive the framework from the terminal.","icon":"CLI","eyebrow":"Operations","starter":false,"prerequisites":["routing"],"featureCount":6,"href":"/demo/cli","features":[{"section":"cli","slug":"describe-commands","label":"CLI","title":"Project Graph Introspection","summary":"Routes, modules, contracts, and handlers can be introspected directly from the CLI instead of reverse-engineering the framework graph by hand.","opensInNewTab":false,"href":"/demo/cli/describe-commands"},{"section":"cli","slug":"runtime-maintenance","label":"CLI","title":"Runtime Maintenance","summary":"Reload workers, clear stale cache, sync registries, lint architecture rules, and probe handler wiring without reaching for ad-hoc shell scripts.","opensInNewTab":false,"href":"/demo/cli/runtime-maintenance"},{"section":"cli","slug":"scaffolding-generators","label":"CLI","title":"Scaffolding Generators","summary":"Scaffold modules, pages, payloads, services, and contracts through commands that already understand Semitexa structure and AI-friendly output modes.","opensInNewTab":false,"href":"/demo/cli/scaffolding-generators"},{"section":"cli","slug":"workers-scheduling","label":"CLI","title":"Workers & Scheduling","summary":"Run queues, scheduler pools, mail delivery, webhooks, and tenant-scoped commands from a coherent operator surface instead of bespoke daemons.","opensInNewTab":false,"href":"/demo/cli/workers-scheduling"},{"section":"cli","slug":"ai-tooling","label":"CLI","title":"AI Tooling Surface","summary":"Semitexa exposes AI-facing commands as explicit CLI contracts: capabilities, skills, log access, and a local assistant entrypoint.","opensInNewTab":false,"href":"/demo/cli/ai-tooling"},{"section":"cli","slug":"orm-console","label":"CLI","title":"ORM Console Toolkit","summary":"The ORM ships with a practical CLI surface: status, diff, sync, and seed commands with dry-run safety and SQL plan export.","opensInNewTab":false,"href":"/demo/cli/orm-console"}],"groups":[{"key":"inspection","label":"Describe & Inspect","featureCount":2,"features":[{"section":"cli","slug":"describe-commands","label":"CLI","title":"Project Graph Introspection","summary":"Routes, modules, contracts, and handlers can be introspected directly from the CLI instead of reverse-engineering the framework graph by hand.","opensInNewTab":false,"href":"/demo/cli/describe-commands"},{"section":"cli","slug":"runtime-maintenance","label":"CLI","title":"Runtime Maintenance","summary":"Reload workers, clear stale cache, sync registries, lint architecture rules, and probe handler wiring without reaching for ad-hoc shell scripts.","opensInNewTab":false,"href":"/demo/cli/runtime-maintenance"}]},{"key":"automation","label":"Automation","featureCount":4,"features":[{"section":"cli","slug":"scaffolding-generators","label":"CLI","title":"Scaffolding Generators","summary":"Scaffold modules, pages, payloads, services, and contracts through commands that already understand Semitexa structure and AI-friendly output modes.","opensInNewTab":false,"href":"/demo/cli/scaffolding-generators"},{"section":"cli","slug":"workers-scheduling","label":"CLI","title":"Workers & Scheduling","summary":"Run queues, scheduler pools, mail delivery, webhooks, and tenant-scoped commands from a coherent operator surface instead of bespoke daemons.","opensInNewTab":false,"href":"/demo/cli/workers-scheduling"},{"section":"cli","slug":"ai-tooling","label":"CLI","title":"AI Tooling Surface","summary":"Semitexa exposes AI-facing commands as explicit CLI contracts: capabilities, skills, log access, and a local assistant entrypoint.","opensInNewTab":false,"href":"/demo/cli/ai-tooling"},{"section":"cli","slug":"orm-console","label":"CLI","title":"ORM Console Toolkit","summary":"The ORM ships with a practical CLI surface: status, diff, sync, and seed commands with dry-run safety and SQL plan export.","opensInNewTab":false,"href":"/demo/cli/orm-console"}]}]},{"key":"llm","label":"LLM Module","sidebarLabel":"LLM","summary":"The dedicated `semitexa/llm` module: AI assistant entrypoint, skill discovery, planner, executor, provider backends, and skill authoring rules.","icon":"AI","eyebrow":"semitexa/llm","starter":false,"prerequisites":["cli"],"featureCount":4,"href":"/demo/llm","features":[{"section":"llm","slug":"overview","label":"LLM Module","title":"LLM Module Overview","summary":"What `semitexa/llm` adds to the framework and how your project can expose its own CLI skills to the assistant.","opensInNewTab":false,"href":"/demo/llm/overview"},{"section":"llm","slug":"providers","label":"LLM Module","title":"Providers & Backends","summary":"Provider contracts, backend resolution, local vs remote Ollama, and the environment knobs that shape LLM runtime behavior.","opensInNewTab":false,"href":"/demo/llm/providers"},{"section":"llm","slug":"skills","label":"LLM Module","title":"Adding Skills","summary":"How a console command becomes AI-executable through `#[AsAiSkill]`, metadata policy, and registry discovery.","opensInNewTab":false,"href":"/demo/llm/skills"},{"section":"llm","slug":"execution-flow","label":"LLM Module","title":"Execution Flow","summary":"How a user request becomes a planner decision, a reviewed skill proposal, and finally a real console execution.","opensInNewTab":false,"href":"/demo/llm/execution-flow"}],"groups":[{"key":"assistant-basics","label":"Assistant Surface","featureCount":2,"features":[{"section":"llm","slug":"overview","label":"LLM Module","title":"LLM Module Overview","summary":"What `semitexa/llm` adds to the framework and how your project can expose its own CLI skills to the assistant.","opensInNewTab":false,"href":"/demo/llm/overview"},{"section":"llm","slug":"providers","label":"LLM Module","title":"Providers & Backends","summary":"Provider contracts, backend resolution, local vs remote Ollama, and the environment knobs that shape LLM runtime behavior.","opensInNewTab":false,"href":"/demo/llm/providers"}]},{"key":"skill-system","label":"Skill System","featureCount":2,"features":[{"section":"llm","slug":"skills","label":"LLM Module","title":"Adding Skills","summary":"How a console command becomes AI-executable through `#[AsAiSkill]`, metadata policy, and registry discovery.","opensInNewTab":false,"href":"/demo/llm/skills"},{"section":"llm","slug":"execution-flow","label":"LLM Module","title":"Execution Flow","summary":"How a user request becomes a planner decision, a reviewed skill proposal, and finally a real console execution.","opensInNewTab":false,"href":"/demo/llm/execution-flow"}]}]},{"key":"project-graph","label":"Project Graph","summary":"The `semitexa-project-graph` package: stored structural graph, intelligence layer, impact analysis, and task-scoped context for serious repository work.","icon":"PG","eyebrow":"AI Accelerator","starter":true,"prerequisites":["cli"],"featureCount":3,"href":"/demo/project-graph","features":[{"section":"project-graph","slug":"overview","label":"Project Graph","title":"Project Graph Overview","summary":"Understand what `semitexa-project-graph` adds: a stored structural map, an intelligence layer, and task-scoped context for large-codebase work.","opensInNewTab":false,"href":"/demo/project-graph/overview"},{"section":"project-graph","slug":"inspection","label":"Project Graph","title":"Inspecting the Graph","summary":"Use Project Graph queries and intelligence views to inspect modules, dependencies, flows, events, and hotspots without reconstructing the repository manually.","opensInNewTab":false,"href":"/demo/project-graph/inspection"},{"section":"project-graph","slug":"impact","label":"Project Graph","title":"Impact, Context, and Watch Mode","summary":"Use impact analysis, context packing, and watch mode to scope risky changes and keep graph-backed answers current during long work sessions.","opensInNewTab":false,"href":"/demo/project-graph/impact"}],"groups":[{"key":"launch","label":"Start Here","featureCount":1,"features":[{"section":"project-graph","slug":"overview","label":"Project Graph","title":"Project Graph Overview","summary":"Understand what `semitexa-project-graph` adds: a stored structural map, an intelligence layer, and task-scoped context for large-codebase work.","opensInNewTab":false,"href":"/demo/project-graph/overview"}]},{"key":"exploration","label":"Explore & Inspect","featureCount":1,"features":[{"section":"project-graph","slug":"inspection","label":"Project Graph","title":"Inspecting the Graph","summary":"Use Project Graph queries and intelligence views to inspect modules, dependencies, flows, events, and hotspots without reconstructing the repository manually.","opensInNewTab":false,"href":"/demo/project-graph/inspection"}]},{"key":"change-safety","label":"Impact & Context","featureCount":1,"features":[{"section":"project-graph","slug":"impact","label":"Project Graph","title":"Impact, Context, and Watch Mode","summary":"Use impact analysis, context packing, and watch mode to scope risky changes and keep graph-backed answers current during long work sessions.","opensInNewTab":false,"href":"/demo/project-graph/impact"}]}]},{"key":"testing","label":"Testing","summary":"Contract-level verification patterns for payloads and other framework boundaries.","icon":"QA","eyebrow":"Verification","starter":false,"prerequisites":["routing"],"featureCount":1,"href":"/demo/testing","features":[{"section":"testing","slug":"payload-contracts","label":"Testing","title":"Payload Contract Testing","summary":"Run one project-level contract suite through the canonical test runner and let strategy profiles verify payload boundaries without hand-writing repetitive negative cases.","opensInNewTab":false,"href":"/demo/testing/payload-contracts"}],"groups":[{"key":"contracts","label":"Contracts","featureCount":1,"features":[{"section":"testing","slug":"payload-contracts","label":"Testing","title":"Payload Contract Testing","summary":"Run one project-level contract suite through the canonical test runner and let strategy profiles verify payload boundaries without hand-writing repetitive negative cases.","opensInNewTab":false,"href":"/demo/testing/payload-contracts"}]}]}],"featureTree":[{"key":"start-here","label":"Start Here","eyebrow":"Guided path","summary":"The smallest reliable sequence that makes Semitexa feel like a system instead of a brochure.","type":"feature-links","href":"/#start-here","features":[{"section":"get-started","slug":"installation","label":"Start Here","title":"Installation","summary":"Create the project, review the baseline env contract, and bring up the Semitexa runtime the supported way.","opensInNewTab":false,"href":"/demo/get-started/installation"},{"section":"get-started","slug":"local-domain","label":"Start Here","title":"Local Domain","summary":"Register .test domains through the built-in local-domain helper instead of relying on ad hoc host setup.","opensInNewTab":false,"href":"/demo/get-started/local-domain"},{"section":"get-started","slug":"module-structure","label":"Start Here","title":"Module Structure","summary":"The minimal Semitexa module is a typed HTTP spine of payload, handler, resource, and template.","opensInNewTab":false,"href":"/demo/get-started/module-structure"},{"section":"get-started","slug":"base-tenant","label":"Start Here","title":"Base Tenant","summary":"Establish one default tenant context early so tenant-aware behavior is visible before the rest of the application grows.","opensInNewTab":false,"href":"/demo/get-started/base-tenant"},{"section":"get-started","slug":"locale-setup","label":"Start Here","title":"Locale Setup","summary":"Configure the minimal Locale contract so translations and locale-aware rendering become explicit early.","opensInNewTab":false,"href":"/demo/get-started/locale-setup"},{"section":"get-started","slug":"ai-console","label":"Start Here","title":"AI Console","summary":"Use the Semitexa AI console as a command translation surface over real operator commands.","opensInNewTab":false,"href":"/demo/get-started/ai-console"},{"section":"project-graph","slug":"overview","label":"Project Graph","title":"Project Graph Overview","summary":"Understand what `semitexa-project-graph` adds: a stored structural map, an intelligence layer, and task-scoped context for large-codebase work.","opensInNewTab":false,"href":"/demo/project-graph/overview"},{"section":"routing","slug":"basic","label":"Routing & Handlers","title":"Basic Route","summary":"Define a route with one access attribute on the payload — no XML, no YAML, no config files.","opensInNewTab":false,"href":"/demo/routing/basic"},{"section":"di","slug":"overview","label":"Dependency Injection","title":"DI Canon","summary":"One canonical DI path for container-managed classes: explicit properties, explicit lifecycles, deterministic boot.","opensInNewTab":false,"href":"/demo/di/overview"}],"featureCount":9},{"key":"full-catalog","label":"Full Catalog","eyebrow":"Route-first map","summary":"The exhaustive live map, still route-first, still real, and still one click away from every feature route.","type":"section-groups","href":"/#full-catalog","sectionKeys":["get-started","routing","di","data","auth","events","rendering","platform","api","cli","project-graph","llm","testing"],"featureCount":81,"sections":[{"key":"get-started","label":"Start Here","sidebarLabel":"Get Started","summary":"The shortest path from fresh scaffold to a trustworthy local Semitexa runtime: install, boot, understand the module map, bind a real host, and reach the first tenant boundary.","icon":"GO","eyebrow":"Onboarding","starter":true,"prerequisites":[],"featureCount":7,"href":"/demo/get-started","features":[{"section":"get-started","slug":"module-structure","label":"Start Here","title":"Module Structure","summary":"The minimal Semitexa module is a typed HTTP spine of payload, handler, resource, and template.","opensInNewTab":false,"href":"/demo/get-started/module-structure"},{"section":"get-started","slug":"installation","label":"Start Here","title":"Installation","summary":"Create the project, review the baseline env contract, and bring up the Semitexa runtime the supported way.","opensInNewTab":false,"href":"/demo/get-started/installation"},{"section":"get-started","slug":"local-domain","label":"Start Here","title":"Local Domain","summary":"Register .test domains through the built-in local-domain helper instead of relying on ad hoc host setup.","opensInNewTab":false,"href":"/demo/get-started/local-domain"},{"section":"get-started","slug":"base-tenant","label":"Start Here","title":"Base Tenant","summary":"Establish one default tenant context early so tenant-aware behavior is visible before the rest of the application grows.","opensInNewTab":false,"href":"/demo/get-started/base-tenant"},{"section":"get-started","slug":"locale-setup","label":"Start Here","title":"Locale Setup","summary":"Configure the minimal Locale contract so translations and locale-aware rendering become explicit early.","opensInNewTab":false,"href":"/demo/get-started/locale-setup"},{"section":"get-started","slug":"ai-console","label":"Start Here","title":"AI Console","summary":"Use the Semitexa AI console as a command translation surface over real operator commands.","opensInNewTab":false,"href":"/demo/get-started/ai-console"},{"section":"get-started","slug":"beyond-controllers","label":"Start Here","title":"Beyond Controllers","summary":"Understand why Semitexa keeps transport, use case, and rendering as separate explicit responsibilities.","opensInNewTab":false,"href":"/demo/get-started/beyond-controllers"}],"groups":[{"key":"structure","label":"Module Structure","featureCount":1,"features":[{"section":"get-started","slug":"module-structure","label":"Start Here","title":"Module Structure","summary":"The minimal Semitexa module is a typed HTTP spine of payload, handler, resource, and template.","opensInNewTab":false,"href":"/demo/get-started/module-structure"}]},{"key":"onboarding","label":"Onboarding","featureCount":6,"features":[{"section":"get-started","slug":"installation","label":"Start Here","title":"Installation","summary":"Create the project, review the baseline env contract, and bring up the Semitexa runtime the supported way.","opensInNewTab":false,"href":"/demo/get-started/installation"},{"section":"get-started","slug":"local-domain","label":"Start Here","title":"Local Domain","summary":"Register .test domains through the built-in local-domain helper instead of relying on ad hoc host setup.","opensInNewTab":false,"href":"/demo/get-started/local-domain"},{"section":"get-started","slug":"base-tenant","label":"Start Here","title":"Base Tenant","summary":"Establish one default tenant context early so tenant-aware behavior is visible before the rest of the application grows.","opensInNewTab":false,"href":"/demo/get-started/base-tenant"},{"section":"get-started","slug":"locale-setup","label":"Start Here","title":"Locale Setup","summary":"Configure the minimal Locale contract so translations and locale-aware rendering become explicit early.","opensInNewTab":false,"href":"/demo/get-started/locale-setup"},{"section":"get-started","slug":"ai-console","label":"Start Here","title":"AI Console","summary":"Use the Semitexa AI console as a command translation surface over real operator commands.","opensInNewTab":false,"href":"/demo/get-started/ai-console"},{"section":"get-started","slug":"beyond-controllers","label":"Start Here","title":"Beyond Controllers","summary":"Understand why Semitexa keeps transport, use case, and rendering as separate explicit responsibilities.","opensInNewTab":false,"href":"/demo/get-started/beyond-controllers"}]}]},{"key":"routing","label":"Routing & Handlers","summary":"Attribute-driven routes, typed handlers, and content negotiation in one coherent request pipeline.","icon":"RT","eyebrow":"HTTP Layer","starter":true,"prerequisites":[],"featureCount":7,"href":"/demo/routing","features":[{"section":"routing","slug":"basic","label":"Routing & Handlers","title":"Basic Route","summary":"Define a route with one access attribute on the payload — no XML, no YAML, no config files.","opensInNewTab":false,"href":"/demo/routing/basic"},{"section":"routing","slug":"parameterized","label":"Routing & Handlers","title":"Parameterized Route","summary":"Path parameters with regex constraints and typed injection.","opensInNewTab":false,"href":"/demo/routing/parameterized/headphones"},{"section":"routing","slug":"env-route-override","label":"Routing & Handlers","title":"Env Route Override","summary":"Keep the payload as the route source of truth while allowing operations to remap the public URL through .env.","opensInNewTab":false,"href":"/demo/routing/env-route-override"},{"section":"routing","slug":"payload-shield","label":"Routing & Handlers","title":"Payload As A Shield","summary":"Hydration happens before the handler, and each setter owns the normalization and guard logic for its own field.","opensInNewTab":false,"href":"/demo/routing/payload-shield"},{"section":"routing","slug":"payload-parts","label":"Routing & Handlers","title":"Payload Parts","summary":"One module owns the route, another module can extend the same payload contract without forking or reopening the base class.","opensInNewTab":false,"href":"/demo/routing/payload-parts"},{"section":"routing","slug":"content-negotiation","label":"Routing & Handlers","title":"Content Negotiation","summary":"One endpoint, multiple response formats — automatically.","opensInNewTab":false,"href":"/demo/routing/content-negotiation"},{"section":"routing","slug":"public-endpoint","label":"Routing & Handlers","title":"Public Payload","summary":"Anonymous endpoints opt in explicitly with the public access attribute. Every other payload requires authentication.","opensInNewTab":false,"href":"/demo/routing/public-endpoint"}],"groups":[{"key":"foundations","label":"Foundations","featureCount":3,"features":[{"section":"routing","slug":"basic","label":"Routing & Handlers","title":"Basic Route","summary":"Define a route with one access attribute on the payload — no XML, no YAML, no config files.","opensInNewTab":false,"href":"/demo/routing/basic"},{"section":"routing","slug":"parameterized","label":"Routing & Handlers","title":"Parameterized Route","summary":"Path parameters with regex constraints and typed injection.","opensInNewTab":false,"href":"/demo/routing/parameterized/headphones"},{"section":"routing","slug":"env-route-override","label":"Routing & Handlers","title":"Env Route Override","summary":"Keep the payload as the route source of truth while allowing operations to remap the public URL through .env.","opensInNewTab":false,"href":"/demo/routing/env-route-override"}]},{"key":"request-model","label":"Request Model","featureCount":2,"features":[{"section":"routing","slug":"payload-shield","label":"Routing & Handlers","title":"Payload As A Shield","summary":"Hydration happens before the handler, and each setter owns the normalization and guard logic for its own field.","opensInNewTab":false,"href":"/demo/routing/payload-shield"},{"section":"routing","slug":"payload-parts","label":"Routing & Handlers","title":"Payload Parts","summary":"One module owns the route, another module can extend the same payload contract without forking or reopening the base class.","opensInNewTab":false,"href":"/demo/routing/payload-parts"}]},{"key":"delivery","label":"Delivery","featureCount":2,"features":[{"section":"routing","slug":"content-negotiation","label":"Routing & Handlers","title":"Content Negotiation","summary":"One endpoint, multiple response formats — automatically.","opensInNewTab":false,"href":"/demo/routing/content-negotiation"},{"section":"routing","slug":"public-endpoint","label":"Routing & Handlers","title":"Public Payload","summary":"Anonymous endpoints opt in explicitly with the public access attribute. Every other payload requires authentication.","opensInNewTab":false,"href":"/demo/routing/public-endpoint"}]}]},{"key":"di","label":"Dependency Injection","summary":"Single-path DI with explicit lifecycles, deterministic contracts, and stable boot behavior for long-running workers.","icon":"DI","eyebrow":"Container","starter":true,"prerequisites":[],"featureCount":5,"href":"/demo/di","features":[{"section":"di","slug":"overview","label":"Dependency Injection","title":"DI Canon","summary":"One canonical DI path for container-managed classes: explicit properties, explicit lifecycles, deterministic boot.","opensInNewTab":false,"href":"/demo/di/overview"},{"section":"di","slug":"readonly","label":"Dependency Injection","title":"Readonly Injection","summary":"One explicit DI path, one shared worker instance — fast at runtime and stable under reload.","opensInNewTab":false,"href":"/demo/di/readonly"},{"section":"di","slug":"mutable","label":"Dependency Injection","title":"Mutable Injection","summary":"Execution-scoped services get a fresh clone every run — safe state without contaminating the worker.","opensInNewTab":false,"href":"/demo/di/mutable"},{"section":"di","slug":"factory","label":"Dependency Injection","title":"Factory Injection","summary":"On-demand creation stays explicit — lazy instances without falling back to service locator habits.","opensInNewTab":false,"href":"/demo/di/factory"},{"section":"di","slug":"contracts","label":"Dependency Injection","title":"Service Contracts","summary":"Depend on contracts, but keep ownership explicit — deterministic substitution instead of runtime magic.","opensInNewTab":false,"href":"/demo/di/contracts"}],"groups":[{"key":"container-basics","label":"Container Basics","featureCount":5,"features":[{"section":"di","slug":"overview","label":"Dependency Injection","title":"DI Canon","summary":"One canonical DI path for container-managed classes: explicit properties, explicit lifecycles, deterministic boot.","opensInNewTab":false,"href":"/demo/di/overview"},{"section":"di","slug":"readonly","label":"Dependency Injection","title":"Readonly Injection","summary":"One explicit DI path, one shared worker instance — fast at runtime and stable under reload.","opensInNewTab":false,"href":"/demo/di/readonly"},{"section":"di","slug":"mutable","label":"Dependency Injection","title":"Mutable Injection","summary":"Execution-scoped services get a fresh clone every run — safe state without contaminating the worker.","opensInNewTab":false,"href":"/demo/di/mutable"},{"section":"di","slug":"factory","label":"Dependency Injection","title":"Factory Injection","summary":"On-demand creation stays explicit — lazy instances without falling back to service locator habits.","opensInNewTab":false,"href":"/demo/di/factory"},{"section":"di","slug":"contracts","label":"Dependency Injection","title":"Service Contracts","summary":"Depend on contracts, but keep ownership explicit — deterministic substitution instead of runtime magic.","opensInNewTab":false,"href":"/demo/di/contracts"}]}]},{"key":"data","label":"Persistence","summary":"Attribute-mapped resources, repositories, filtering, pagination, and relations with real demo data.","icon":"DB","eyebrow":"Persistence","starter":true,"prerequisites":[],"featureCount":9,"href":"/demo/data","features":[{"section":"data","slug":"domain-models","label":"Persistence","title":"Domain-Level Models","summary":"Semitexa separates persistence resources from business models. Resources map tables; domain models carry behavior and invariants.","opensInNewTab":false,"href":"/demo/data/domain-models"},{"section":"data","slug":"repository-workflow","label":"Persistence","title":"Repository Workflow","summary":"The canonical Semitexa path: handlers depend on repository contracts, repositories return domain models, and persistence resources stay behind the boundary.","opensInNewTab":false,"href":"/demo/data/repository-workflow"},{"section":"data","slug":"schema-sync","label":"Persistence","title":"Schema Sync, Not Migration Churn","summary":"Semitexa creates SQL only when the real schema changed, blocks destructive drops by default, and logs the exact DDL plan as SQL and JSON.","opensInNewTab":false,"href":"/demo/data/schema-sync"},{"section":"data","slug":"query","label":"Persistence","title":"Query Builder","summary":"Compose type-safe queries with a fluent API — no raw SQL, no magic strings.","opensInNewTab":false,"href":"/demo/data/query"},{"section":"data","slug":"filtering","label":"Persistence","title":"Filtering","summary":"Mark a property #[Filterable] and the ORM handles the rest — no manual WHERE clauses.","opensInNewTab":false,"href":"/demo/data/filtering"},{"section":"data","slug":"pagination","label":"Persistence","title":"Pagination","summary":"Offset and cursor pagination out of the box — switch modes with a single query parameter.","opensInNewTab":false,"href":"/demo/data/pagination"},{"section":"data","slug":"relations","label":"Persistence","title":"Relations","summary":"Declare parent and child links on the resource itself, then read typed relations from the handler.","opensInNewTab":false,"href":"/demo/data/relations"},{"section":"data","slug":"table-extension","label":"Persistence","title":"Shared Table Extension","summary":"Two modules can extend one table independently, and the ORM merges the schema without forcing either side to edit the other.","opensInNewTab":false,"href":"/demo/data/table-extension"},{"section":"data","slug":"n-plus-one","label":"Persistence","title":"N+1 Without Magic","summary":"Semitexa avoids N+1 by using resource slices for the exact columns and relations each screen needs, instead of hiding database traffic behind implicit relation loading.","opensInNewTab":false,"href":"/demo/data/n-plus-one"}],"groups":[{"key":"modeling","label":"Modeling & Workflow","featureCount":3,"features":[{"section":"data","slug":"domain-models","label":"Persistence","title":"Domain-Level Models","summary":"Semitexa separates persistence resources from business models. Resources map tables; domain models carry behavior and invariants.","opensInNewTab":false,"href":"/demo/data/domain-models"},{"section":"data","slug":"repository-workflow","label":"Persistence","title":"Repository Workflow","summary":"The canonical Semitexa path: handlers depend on repository contracts, repositories return domain models, and persistence resources stay behind the boundary.","opensInNewTab":false,"href":"/demo/data/repository-workflow"},{"section":"data","slug":"schema-sync","label":"Persistence","title":"Schema Sync, Not Migration Churn","summary":"Semitexa creates SQL only when the real schema changed, blocks destructive drops by default, and logs the exact DDL plan as SQL and JSON.","opensInNewTab":false,"href":"/demo/data/schema-sync"}]},{"key":"querying","label":"Querying","featureCount":6,"features":[{"section":"data","slug":"query","label":"Persistence","title":"Query Builder","summary":"Compose type-safe queries with a fluent API — no raw SQL, no magic strings.","opensInNewTab":false,"href":"/demo/data/query"},{"section":"data","slug":"filtering","label":"Persistence","title":"Filtering","summary":"Mark a property #[Filterable] and the ORM handles the rest — no manual WHERE clauses.","opensInNewTab":false,"href":"/demo/data/filtering"},{"section":"data","slug":"pagination","label":"Persistence","title":"Pagination","summary":"Offset and cursor pagination out of the box — switch modes with a single query parameter.","opensInNewTab":false,"href":"/demo/data/pagination"},{"section":"data","slug":"relations","label":"Persistence","title":"Relations","summary":"Declare parent and child links on the resource itself, then read typed relations from the handler.","opensInNewTab":false,"href":"/demo/data/relations"},{"section":"data","slug":"table-extension","label":"Persistence","title":"Shared Table Extension","summary":"Two modules can extend one table independently, and the ORM merges the schema without forcing either side to edit the other.","opensInNewTab":false,"href":"/demo/data/table-extension"},{"section":"data","slug":"n-plus-one","label":"Persistence","title":"N+1 Without Magic","summary":"Semitexa avoids N+1 by using resource slices for the exact columns and relations each screen needs, instead of hiding database traffic behind implicit relation loading.","opensInNewTab":false,"href":"/demo/data/n-plus-one"}]}]},{"key":"auth","label":"Security","summary":"Typed session payloads, machine credentials, RBAC, and route protection without string-key auth chaos.","icon":"AU","eyebrow":"Security","starter":false,"prerequisites":[],"featureCount":7,"href":"/demo/auth","features":[{"section":"auth","slug":"session","label":"Security","title":"Session Auth","summary":"Google signs the user in, then the session stores the selected demo role and re-hydrates it on every request.","opensInNewTab":false,"href":"/demo/auth/session"},{"section":"auth","slug":"session-payloads","label":"Security","title":"Session Payloads","summary":"Semitexa forbids string-key session chaos: session state lives in typed Session Payloads or it does not exist.","opensInNewTab":false,"href":"/demo/auth/session-payloads"},{"section":"auth","slug":"google","label":"Security","title":"Google Authorization","summary":"Authorization is required for demo SSE blocks that keep a long-lived backend connection open.","opensInNewTab":false,"href":"/demo/auth/google"},{"section":"auth","slug":"machine","label":"Security","title":"Machine Auth","summary":"Service-to-service authentication via Bearer tokens — scoped, revocable, and audited.","opensInNewTab":false,"href":"/demo/auth/machine"},{"section":"auth","slug":"protected","label":"Security","title":"Protected Route","summary":"Add one access attribute and one optional permission attribute and the framework enforces access — 401 for unauthenticated requests, 403 for unauthorized ones.","opensInNewTab":false,"href":"/demo/auth/protected"},{"section":"auth","slug":"requires-permission","label":"Security","title":"Requires Permission","summary":"Declare one permission slug on the payload and let the framework enforce it before your handler runs.","opensInNewTab":false,"href":"/demo/auth/requires-permission"},{"section":"auth","slug":"rbac","label":"Security","title":"RBAC","summary":"Hybrid RBAC with coarse-grained capabilities, exact permission slugs, and module-owned permission catalogs.","opensInNewTab":false,"href":"/demo/auth/rbac"}],"groups":[{"key":"identity","label":"Identity","featureCount":4,"features":[{"section":"auth","slug":"session","label":"Security","title":"Session Auth","summary":"Google signs the user in, then the session stores the selected demo role and re-hydrates it on every request.","opensInNewTab":false,"href":"/demo/auth/session"},{"section":"auth","slug":"session-payloads","label":"Security","title":"Session Payloads","summary":"Semitexa forbids string-key session chaos: session state lives in typed Session Payloads or it does not exist.","opensInNewTab":false,"href":"/demo/auth/session-payloads"},{"section":"auth","slug":"google","label":"Security","title":"Google Authorization","summary":"Authorization is required for demo SSE blocks that keep a long-lived backend connection open.","opensInNewTab":false,"href":"/demo/auth/google"},{"section":"auth","slug":"machine","label":"Security","title":"Machine Auth","summary":"Service-to-service authentication via Bearer tokens — scoped, revocable, and audited.","opensInNewTab":false,"href":"/demo/auth/machine"}]},{"key":"access-control","label":"Access Control","featureCount":3,"features":[{"section":"auth","slug":"protected","label":"Security","title":"Protected Route","summary":"Add one access attribute and one optional permission attribute and the framework enforces access — 401 for unauthenticated requests, 403 for unauthorized ones.","opensInNewTab":false,"href":"/demo/auth/protected"},{"section":"auth","slug":"requires-permission","label":"Security","title":"Requires Permission","summary":"Declare one permission slug on the payload and let the framework enforce it before your handler runs.","opensInNewTab":false,"href":"/demo/auth/requires-permission"},{"section":"auth","slug":"rbac","label":"Security","title":"RBAC","summary":"Hybrid RBAC with coarse-grained capabilities, exact permission slugs, and module-owned permission catalogs.","opensInNewTab":false,"href":"/demo/auth/rbac"}]}]},{"key":"events","label":"Async","summary":"Synchronous and deferred event flows, queues, and SSE-style interactions.","icon":"EV","eyebrow":"Async","starter":false,"prerequisites":[],"featureCount":5,"href":"/demo/events","features":[{"section":"events","slug":"arena","label":"Async","title":"Execution Arena","summary":"Launch the same backend intent in sync, Swoole async, and queued modes, then watch the proof arrive over SSE.","opensInNewTab":false,"href":"/demo/events/arena"},{"section":"events","slug":"sync","label":"Async","title":"Sync Events","summary":"Dispatch an event and all sync listeners run before the response is sent.","opensInNewTab":false,"href":"/demo/events/sync"},{"section":"events","slug":"deferred","label":"Async","title":"Deferred Handler","summary":"Heavy work runs after the response is sent — the user gets instant feedback.","opensInNewTab":false,"href":"/demo/events/deferred"},{"section":"events","slug":"queued","label":"Async","title":"Queued Handler","summary":"Events survive restarts and scale across workers — backed by a durable message queue.","opensInNewTab":false,"href":"/demo/events/queued"},{"section":"events","slug":"sse","label":"Async","title":"SSE Stream","summary":"Real-time server push without WebSockets — connect once and receive real backend events over plain HTTP.","opensInNewTab":false,"href":"/demo/events/sse"}],"groups":[{"key":"event-flow","label":"Event Flow","featureCount":5,"features":[{"section":"events","slug":"arena","label":"Async","title":"Execution Arena","summary":"Launch the same backend intent in sync, Swoole async, and queued modes, then watch the proof arrive over SSE.","opensInNewTab":false,"href":"/demo/events/arena"},{"section":"events","slug":"sync","label":"Async","title":"Sync Events","summary":"Dispatch an event and all sync listeners run before the response is sent.","opensInNewTab":false,"href":"/demo/events/sync"},{"section":"events","slug":"deferred","label":"Async","title":"Deferred Handler","summary":"Heavy work runs after the response is sent — the user gets instant feedback.","opensInNewTab":false,"href":"/demo/events/deferred"},{"section":"events","slug":"queued","label":"Async","title":"Queued Handler","summary":"Events survive restarts and scale across workers — backed by a durable message queue.","opensInNewTab":false,"href":"/demo/events/queued"},{"section":"events","slug":"sse","label":"Async","title":"SSE Stream","summary":"Real-time server push without WebSockets — connect once and receive real backend events over plain HTTP.","opensInNewTab":false,"href":"/demo/events/sse"}]}]},{"key":"rendering","label":"UI Rendering & SSR","summary":"One rendering story from handler to HTML: page data, page regions, and live updates stay in the same server-driven model instead of splitting into frontend and backend template logic.","icon":"UI","eyebrow":"Frontend","starter":false,"prerequisites":[],"featureCount":15,"href":"/demo/rendering","features":[{"section":"rendering","slug":"philosophy","label":"UI Rendering & SSR","title":"SSR Philosophy","summary":"Semitexa SSR is one continuous rendering architecture: page, slots, deferred regions, live refresh, and interactive components stay inside one server-owned story.","opensInNewTab":false,"href":"/demo/rendering/philosophy"},{"section":"rendering","slug":"resource-dtos","label":"UI Rendering & SSR","title":"Resource DTOs","summary":"A Resource DTO is the one typed source of presentation data: handlers shape it once, templates consume it everywhere, and no view has to dissect random arrays.","opensInNewTab":false,"href":"/demo/rendering/resource-dtos"},{"section":"rendering","slug":"slots","label":"UI Rendering & SSR","title":"Slot Resources","summary":"Each page region is its own resource pipeline with the same template system as the main page — no scattered partial glue, no mystery wiring.","opensInNewTab":false,"href":"/demo/rendering/slots"},{"section":"rendering","slug":"components","label":"UI Rendering & SSR","title":"Components","summary":"Reusable, attribute-registered UI components — discovered automatically from the classmap.","opensInNewTab":false,"href":"/demo/rendering/components"},{"section":"rendering","slug":"seo","label":"UI Rendering & SSR","title":"SEO","summary":"Set title, description, and Open Graph tags from your handler — no template hacks needed.","opensInNewTab":false,"href":"/demo/rendering/seo"},{"section":"rendering","slug":"assets","label":"UI Rendering & SSR","title":"Asset Pipeline","summary":"Declare assets with glob patterns in assets.json — served, versioned, and injected automatically.","opensInNewTab":false,"href":"/demo/rendering/assets"},{"section":"rendering","slug":"component-scripts","label":"UI Rendering & SSR","title":"Component Script Assets","summary":"A Semitexa SSR component can own its optional enhancement asset, so behavior travels with the component instead of leaking into page-level glue.","opensInNewTab":false,"href":"/demo/rendering/component-scripts"},{"section":"rendering","slug":"deferred-scripts","label":"UI Rendering & SSR","title":"Script Injection","summary":"Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.","opensInNewTab":false,"href":"/demo/rendering/deferred-scripts"},{"section":"rendering","slug":"deferred","label":"UI Rendering & SSR","title":"Deferred Blocks","summary":"SSR renders the shell first, then expensive regions stream in as real HTML over SSE — no SPA handoff and no client-side page rebuild.","opensInNewTab":true,"href":"/demo/rendering/deferred"},{"section":"rendering","slug":"deferred-encapsulation","label":"UI Rendering & SSR","title":"Block Isolation","summary":"Two identical blocks on the same page run independently — scoped DOM, scoped JS, no conflicts.","opensInNewTab":false,"href":"/demo/rendering/deferred-encapsulation"},{"section":"rendering","slug":"deferred-live","label":"UI Rendering & SSR","title":"Live Widgets","summary":"A live slot can refresh itself on a timer while the page stays SSR-first — no SPA runtime and no handwritten polling layer.","opensInNewTab":false,"href":"/demo/rendering/deferred-live"},{"section":"rendering","slug":"reactive-report","label":"UI Rendering & SSR","title":"Reactive Report","summary":"Background work updates an SSR-first slot in place, so the UI feels live without falling back to SPA state orchestration.","opensInNewTab":false,"href":"/demo/rendering/reactive-report"},{"section":"rendering","slug":"reactive-import","label":"UI Rendering & SSR","title":"Reactive Import","summary":"Background batches keep moving, and the page reflects server progress as live HTML instead of a client-managed progress app.","opensInNewTab":false,"href":"/demo/rendering/reactive-import"},{"section":"rendering","slug":"reactive-analytics","label":"UI Rendering & SSR","title":"Reactive Analytics","summary":"Independent analytics jobs can light up one dashboard progressively, while the page stays server-rendered from the first byte.","opensInNewTab":false,"href":"/demo/rendering/reactive-analytics"},{"section":"rendering","slug":"reactive-ai","label":"UI Rendering & SSR","title":"Reactive AI Task","summary":"Submit a task and watch the AI pipeline stages reveal one by one as the cron job processes it.","opensInNewTab":false,"href":"/demo/rendering/reactive-ai"}],"groups":[{"key":"rendering-model","label":"SSR Foundation","featureCount":8,"features":[{"section":"rendering","slug":"philosophy","label":"UI Rendering & SSR","title":"SSR Philosophy","summary":"Semitexa SSR is one continuous rendering architecture: page, slots, deferred regions, live refresh, and interactive components stay inside one server-owned story.","opensInNewTab":false,"href":"/demo/rendering/philosophy"},{"section":"rendering","slug":"resource-dtos","label":"UI Rendering & SSR","title":"Resource DTOs","summary":"A Resource DTO is the one typed source of presentation data: handlers shape it once, templates consume it everywhere, and no view has to dissect random arrays.","opensInNewTab":false,"href":"/demo/rendering/resource-dtos"},{"section":"rendering","slug":"slots","label":"UI Rendering & SSR","title":"Slot Resources","summary":"Each page region is its own resource pipeline with the same template system as the main page — no scattered partial glue, no mystery wiring.","opensInNewTab":false,"href":"/demo/rendering/slots"},{"section":"rendering","slug":"components","label":"UI Rendering & SSR","title":"Components","summary":"Reusable, attribute-registered UI components — discovered automatically from the classmap.","opensInNewTab":false,"href":"/demo/rendering/components"},{"section":"rendering","slug":"seo","label":"UI Rendering & SSR","title":"SEO","summary":"Set title, description, and Open Graph tags from your handler — no template hacks needed.","opensInNewTab":false,"href":"/demo/rendering/seo"},{"section":"rendering","slug":"assets","label":"UI Rendering & SSR","title":"Asset Pipeline","summary":"Declare assets with glob patterns in assets.json — served, versioned, and injected automatically.","opensInNewTab":false,"href":"/demo/rendering/assets"},{"section":"rendering","slug":"component-scripts","label":"UI Rendering & SSR","title":"Component Script Assets","summary":"A Semitexa SSR component can own its optional enhancement asset, so behavior travels with the component instead of leaking into page-level glue.","opensInNewTab":false,"href":"/demo/rendering/component-scripts"},{"section":"rendering","slug":"deferred-scripts","label":"UI Rendering & SSR","title":"Script Injection","summary":"Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.","opensInNewTab":false,"href":"/demo/rendering/deferred-scripts"}]},{"key":"deferred","label":"Deferred Delivery","featureCount":2,"features":[{"section":"rendering","slug":"deferred","label":"UI Rendering & SSR","title":"Deferred Blocks","summary":"SSR renders the shell first, then expensive regions stream in as real HTML over SSE — no SPA handoff and no client-side page rebuild.","opensInNewTab":true,"href":"/demo/rendering/deferred"},{"section":"rendering","slug":"deferred-encapsulation","label":"UI Rendering & SSR","title":"Block Isolation","summary":"Two identical blocks on the same page run independently — scoped DOM, scoped JS, no conflicts.","opensInNewTab":false,"href":"/demo/rendering/deferred-encapsulation"}]},{"key":"live","label":"Reactive UI","featureCount":5,"features":[{"section":"rendering","slug":"deferred-live","label":"UI Rendering & SSR","title":"Live Widgets","summary":"A live slot can refresh itself on a timer while the page stays SSR-first — no SPA runtime and no handwritten polling layer.","opensInNewTab":false,"href":"/demo/rendering/deferred-live"},{"section":"rendering","slug":"reactive-report","label":"UI Rendering & SSR","title":"Reactive Report","summary":"Background work updates an SSR-first slot in place, so the UI feels live without falling back to SPA state orchestration.","opensInNewTab":false,"href":"/demo/rendering/reactive-report"},{"section":"rendering","slug":"reactive-import","label":"UI Rendering & SSR","title":"Reactive Import","summary":"Background batches keep moving, and the page reflects server progress as live HTML instead of a client-managed progress app.","opensInNewTab":false,"href":"/demo/rendering/reactive-import"},{"section":"rendering","slug":"reactive-analytics","label":"UI Rendering & SSR","title":"Reactive Analytics","summary":"Independent analytics jobs can light up one dashboard progressively, while the page stays server-rendered from the first byte.","opensInNewTab":false,"href":"/demo/rendering/reactive-analytics"},{"section":"rendering","slug":"reactive-ai","label":"UI Rendering & SSR","title":"Reactive AI Task","summary":"Submit a task and watch the AI pipeline stages reveal one by one as the cron job processes it.","opensInNewTab":false,"href":"/demo/rendering/reactive-ai"}]}]},{"key":"platform","label":"Tenancy","summary":"Multi-tenant resolution, tenant-aware configuration, and strict isolation of data and background work.","icon":"TN","eyebrow":"Multi-Tenant","starter":false,"prerequisites":["data","rendering"],"featureCount":5,"href":"/demo/platform","features":[{"section":"platform","slug":"tenancy-resolution","label":"Tenancy","title":"Tenant Context Resolution","summary":"See how Semitexa resolves the active tenant from subdomain, header, path, or query input before the rest of the platform runs.","opensInNewTab":false,"href":"/demo/platform/tenancy-resolution"},{"section":"platform","slug":"tenancy-config","label":"Tenancy","title":"Per-Tenant Configuration","summary":"Three demo tenants with distinct branding -- switch tenant, everything changes without if/else.","opensInNewTab":false,"href":"/demo/platform/tenancy-config"},{"section":"platform","slug":"tenancy-layers","label":"Tenancy","title":"Multi-Layer Tenancy","summary":"Organization, Locale, Theme, Environment -- four independent layers compose into one TenantContext.","opensInNewTab":false,"href":"/demo/platform/tenancy-layers"},{"section":"platform","slug":"tenancy-isolation","label":"Tenancy","title":"Data Isolation","summary":"Product listing scoped by tenant -- switch tenant, list changes. Zero manual WHERE clauses.","opensInNewTab":false,"href":"/demo/platform/tenancy-isolation"},{"section":"platform","slug":"tenancy-queue","label":"Tenancy","title":"Queue Tenant Propagation","summary":"Tenant context travels with queued jobs -- _tenant key injected automatically, restored by worker.","opensInNewTab":false,"href":"/demo/platform/tenancy-queue"}],"groups":[{"key":"resolution","label":"Tenant Resolution","featureCount":1,"features":[{"section":"platform","slug":"tenancy-resolution","label":"Tenancy","title":"Tenant Context Resolution","summary":"See how Semitexa resolves the active tenant from subdomain, header, path, or query input before the rest of the platform runs.","opensInNewTab":false,"href":"/demo/platform/tenancy-resolution"}]},{"key":"configuration","label":"Tenant Configuration","featureCount":2,"features":[{"section":"platform","slug":"tenancy-config","label":"Tenancy","title":"Per-Tenant Configuration","summary":"Three demo tenants with distinct branding -- switch tenant, everything changes without if/else.","opensInNewTab":false,"href":"/demo/platform/tenancy-config"},{"section":"platform","slug":"tenancy-layers","label":"Tenancy","title":"Multi-Layer Tenancy","summary":"Organization, Locale, Theme, Environment -- four independent layers compose into one TenantContext.","opensInNewTab":false,"href":"/demo/platform/tenancy-layers"}]},{"key":"isolation","label":"Isolation & Work","featureCount":2,"features":[{"section":"platform","slug":"tenancy-isolation","label":"Tenancy","title":"Data Isolation","summary":"Product listing scoped by tenant -- switch tenant, list changes. Zero manual WHERE clauses.","opensInNewTab":false,"href":"/demo/platform/tenancy-isolation"},{"section":"platform","slug":"tenancy-queue","label":"Tenancy","title":"Queue Tenant Propagation","summary":"Tenant context travels with queued jobs -- _tenant key injected automatically, restored by worker.","opensInNewTab":false,"href":"/demo/platform/tenancy-queue"}]}]},{"key":"api","label":"API","summary":"External API endpoints, machine auth, versioning, and consumer-facing schema behavior.","icon":"API","eyebrow":"Machine","starter":false,"prerequisites":["routing","auth"],"featureCount":7,"href":"/demo/api","features":[{"section":"api","slug":"rest-api","label":"API","title":"REST API","summary":"Classic Semitexa REST endpoints with typed payloads, versioning, and consumer-friendly response shaping.","opensInNewTab":false,"href":"/demo/api/rest-api"},{"section":"api","slug":"structured-errors","label":"API","title":"Structured Errors","summary":"Throw domain exceptions and let semitexa-api map them into stable machine-readable error envelopes.","opensInNewTab":false,"href":"/demo/api/structured-errors"},{"section":"api","slug":"active-version","label":"API","title":"Active Version","summary":"The current collection endpoint with a clean X-Api-Version header and no deprecation noise.","opensInNewTab":false,"href":"/demo/api/active-version"},{"section":"api","slug":"sunset-version","label":"API","title":"Sunset Version","summary":"A deprecated product endpoint that emits both Deprecation and Sunset headers.","opensInNewTab":false,"href":"/demo/api/sunset-version"},{"section":"api","slug":"schema-discovery","label":"API","title":"Schema Discovery","summary":"A mini Swagger-style explorer for the live product API contract, schema endpoint, and response shapes.","opensInNewTab":false,"href":"/demo/api/schema-discovery"},{"section":"api","slug":"graphql","label":"API","title":"GraphQL API","summary":"GraphQL-first Semitexa contracts built with typed payloads and typed output DTOs instead of resolver sprawl.","opensInNewTab":false,"href":"/demo/api/graphql"},{"section":"api","slug":"rest-graphql","label":"API","title":"REST + GraphQL","summary":"One Semitexa use case can serve both REST and GraphQL without duplicating handler logic into separate resolver classes.","opensInNewTab":false,"href":"/demo/api/rest-graphql"}],"groups":[{"key":"public-api","label":"REST Surface","featureCount":4,"features":[{"section":"api","slug":"rest-api","label":"API","title":"REST API","summary":"Classic Semitexa REST endpoints with typed payloads, versioning, and consumer-friendly response shaping.","opensInNewTab":false,"href":"/demo/api/rest-api"},{"section":"api","slug":"structured-errors","label":"API","title":"Structured Errors","summary":"Throw domain exceptions and let semitexa-api map them into stable machine-readable error envelopes.","opensInNewTab":false,"href":"/demo/api/structured-errors"},{"section":"api","slug":"active-version","label":"API","title":"Active Version","summary":"The current collection endpoint with a clean X-Api-Version header and no deprecation noise.","opensInNewTab":false,"href":"/demo/api/active-version"},{"section":"api","slug":"sunset-version","label":"API","title":"Sunset Version","summary":"A deprecated product endpoint that emits both Deprecation and Sunset headers.","opensInNewTab":false,"href":"/demo/api/sunset-version"}]},{"key":"schema","label":"Schema Discovery","featureCount":3,"features":[{"section":"api","slug":"schema-discovery","label":"API","title":"Schema Discovery","summary":"A mini Swagger-style explorer for the live product API contract, schema endpoint, and response shapes.","opensInNewTab":false,"href":"/demo/api/schema-discovery"},{"section":"api","slug":"graphql","label":"API","title":"GraphQL API","summary":"GraphQL-first Semitexa contracts built with typed payloads and typed output DTOs instead of resolver sprawl.","opensInNewTab":false,"href":"/demo/api/graphql"},{"section":"api","slug":"rest-graphql","label":"API","title":"REST + GraphQL","summary":"One Semitexa use case can serve both REST and GraphQL without duplicating handler logic into separate resolver classes.","opensInNewTab":false,"href":"/demo/api/rest-graphql"}]}]},{"key":"cli","label":"CLI","summary":"Operational, introspection, and AI-oriented command surfaces that explain and drive the framework from the terminal.","icon":"CLI","eyebrow":"Operations","starter":false,"prerequisites":["routing"],"featureCount":6,"href":"/demo/cli","features":[{"section":"cli","slug":"describe-commands","label":"CLI","title":"Project Graph Introspection","summary":"Routes, modules, contracts, and handlers can be introspected directly from the CLI instead of reverse-engineering the framework graph by hand.","opensInNewTab":false,"href":"/demo/cli/describe-commands"},{"section":"cli","slug":"runtime-maintenance","label":"CLI","title":"Runtime Maintenance","summary":"Reload workers, clear stale cache, sync registries, lint architecture rules, and probe handler wiring without reaching for ad-hoc shell scripts.","opensInNewTab":false,"href":"/demo/cli/runtime-maintenance"},{"section":"cli","slug":"scaffolding-generators","label":"CLI","title":"Scaffolding Generators","summary":"Scaffold modules, pages, payloads, services, and contracts through commands that already understand Semitexa structure and AI-friendly output modes.","opensInNewTab":false,"href":"/demo/cli/scaffolding-generators"},{"section":"cli","slug":"workers-scheduling","label":"CLI","title":"Workers & Scheduling","summary":"Run queues, scheduler pools, mail delivery, webhooks, and tenant-scoped commands from a coherent operator surface instead of bespoke daemons.","opensInNewTab":false,"href":"/demo/cli/workers-scheduling"},{"section":"cli","slug":"ai-tooling","label":"CLI","title":"AI Tooling Surface","summary":"Semitexa exposes AI-facing commands as explicit CLI contracts: capabilities, skills, log access, and a local assistant entrypoint.","opensInNewTab":false,"href":"/demo/cli/ai-tooling"},{"section":"cli","slug":"orm-console","label":"CLI","title":"ORM Console Toolkit","summary":"The ORM ships with a practical CLI surface: status, diff, sync, and seed commands with dry-run safety and SQL plan export.","opensInNewTab":false,"href":"/demo/cli/orm-console"}],"groups":[{"key":"inspection","label":"Describe & Inspect","featureCount":2,"features":[{"section":"cli","slug":"describe-commands","label":"CLI","title":"Project Graph Introspection","summary":"Routes, modules, contracts, and handlers can be introspected directly from the CLI instead of reverse-engineering the framework graph by hand.","opensInNewTab":false,"href":"/demo/cli/describe-commands"},{"section":"cli","slug":"runtime-maintenance","label":"CLI","title":"Runtime Maintenance","summary":"Reload workers, clear stale cache, sync registries, lint architecture rules, and probe handler wiring without reaching for ad-hoc shell scripts.","opensInNewTab":false,"href":"/demo/cli/runtime-maintenance"}]},{"key":"automation","label":"Automation","featureCount":4,"features":[{"section":"cli","slug":"scaffolding-generators","label":"CLI","title":"Scaffolding Generators","summary":"Scaffold modules, pages, payloads, services, and contracts through commands that already understand Semitexa structure and AI-friendly output modes.","opensInNewTab":false,"href":"/demo/cli/scaffolding-generators"},{"section":"cli","slug":"workers-scheduling","label":"CLI","title":"Workers & Scheduling","summary":"Run queues, scheduler pools, mail delivery, webhooks, and tenant-scoped commands from a coherent operator surface instead of bespoke daemons.","opensInNewTab":false,"href":"/demo/cli/workers-scheduling"},{"section":"cli","slug":"ai-tooling","label":"CLI","title":"AI Tooling Surface","summary":"Semitexa exposes AI-facing commands as explicit CLI contracts: capabilities, skills, log access, and a local assistant entrypoint.","opensInNewTab":false,"href":"/demo/cli/ai-tooling"},{"section":"cli","slug":"orm-console","label":"CLI","title":"ORM Console Toolkit","summary":"The ORM ships with a practical CLI surface: status, diff, sync, and seed commands with dry-run safety and SQL plan export.","opensInNewTab":false,"href":"/demo/cli/orm-console"}]}]},{"key":"project-graph","label":"Project Graph","summary":"The `semitexa-project-graph` package: stored structural graph, intelligence layer, impact analysis, and task-scoped context for serious repository work.","icon":"PG","eyebrow":"AI Accelerator","starter":true,"prerequisites":["cli"],"featureCount":3,"href":"/demo/project-graph","features":[{"section":"project-graph","slug":"overview","label":"Project Graph","title":"Project Graph Overview","summary":"Understand what `semitexa-project-graph` adds: a stored structural map, an intelligence layer, and task-scoped context for large-codebase work.","opensInNewTab":false,"href":"/demo/project-graph/overview"},{"section":"project-graph","slug":"inspection","label":"Project Graph","title":"Inspecting the Graph","summary":"Use Project Graph queries and intelligence views to inspect modules, dependencies, flows, events, and hotspots without reconstructing the repository manually.","opensInNewTab":false,"href":"/demo/project-graph/inspection"},{"section":"project-graph","slug":"impact","label":"Project Graph","title":"Impact, Context, and Watch Mode","summary":"Use impact analysis, context packing, and watch mode to scope risky changes and keep graph-backed answers current during long work sessions.","opensInNewTab":false,"href":"/demo/project-graph/impact"}],"groups":[{"key":"launch","label":"Start Here","featureCount":1,"features":[{"section":"project-graph","slug":"overview","label":"Project Graph","title":"Project Graph Overview","summary":"Understand what `semitexa-project-graph` adds: a stored structural map, an intelligence layer, and task-scoped context for large-codebase work.","opensInNewTab":false,"href":"/demo/project-graph/overview"}]},{"key":"exploration","label":"Explore & Inspect","featureCount":1,"features":[{"section":"project-graph","slug":"inspection","label":"Project Graph","title":"Inspecting the Graph","summary":"Use Project Graph queries and intelligence views to inspect modules, dependencies, flows, events, and hotspots without reconstructing the repository manually.","opensInNewTab":false,"href":"/demo/project-graph/inspection"}]},{"key":"change-safety","label":"Impact & Context","featureCount":1,"features":[{"section":"project-graph","slug":"impact","label":"Project Graph","title":"Impact, Context, and Watch Mode","summary":"Use impact analysis, context packing, and watch mode to scope risky changes and keep graph-backed answers current during long work sessions.","opensInNewTab":false,"href":"/demo/project-graph/impact"}]}]},{"key":"llm","label":"LLM Module","sidebarLabel":"LLM","summary":"The dedicated `semitexa/llm` module: AI assistant entrypoint, skill discovery, planner, executor, provider backends, and skill authoring rules.","icon":"AI","eyebrow":"semitexa/llm","starter":false,"prerequisites":["cli"],"featureCount":4,"href":"/demo/llm","features":[{"section":"llm","slug":"overview","label":"LLM Module","title":"LLM Module Overview","summary":"What `semitexa/llm` adds to the framework and how your project can expose its own CLI skills to the assistant.","opensInNewTab":false,"href":"/demo/llm/overview"},{"section":"llm","slug":"providers","label":"LLM Module","title":"Providers & Backends","summary":"Provider contracts, backend resolution, local vs remote Ollama, and the environment knobs that shape LLM runtime behavior.","opensInNewTab":false,"href":"/demo/llm/providers"},{"section":"llm","slug":"skills","label":"LLM Module","title":"Adding Skills","summary":"How a console command becomes AI-executable through `#[AsAiSkill]`, metadata policy, and registry discovery.","opensInNewTab":false,"href":"/demo/llm/skills"},{"section":"llm","slug":"execution-flow","label":"LLM Module","title":"Execution Flow","summary":"How a user request becomes a planner decision, a reviewed skill proposal, and finally a real console execution.","opensInNewTab":false,"href":"/demo/llm/execution-flow"}],"groups":[{"key":"assistant-basics","label":"Assistant Surface","featureCount":2,"features":[{"section":"llm","slug":"overview","label":"LLM Module","title":"LLM Module Overview","summary":"What `semitexa/llm` adds to the framework and how your project can expose its own CLI skills to the assistant.","opensInNewTab":false,"href":"/demo/llm/overview"},{"section":"llm","slug":"providers","label":"LLM Module","title":"Providers & Backends","summary":"Provider contracts, backend resolution, local vs remote Ollama, and the environment knobs that shape LLM runtime behavior.","opensInNewTab":false,"href":"/demo/llm/providers"}]},{"key":"skill-system","label":"Skill System","featureCount":2,"features":[{"section":"llm","slug":"skills","label":"LLM Module","title":"Adding Skills","summary":"How a console command becomes AI-executable through `#[AsAiSkill]`, metadata policy, and registry discovery.","opensInNewTab":false,"href":"/demo/llm/skills"},{"section":"llm","slug":"execution-flow","label":"LLM Module","title":"Execution Flow","summary":"How a user request becomes a planner decision, a reviewed skill proposal, and finally a real console execution.","opensInNewTab":false,"href":"/demo/llm/execution-flow"}]}]},{"key":"testing","label":"Testing","summary":"Contract-level verification patterns for payloads and other framework boundaries.","icon":"QA","eyebrow":"Verification","starter":false,"prerequisites":["routing"],"featureCount":1,"href":"/demo/testing","features":[{"section":"testing","slug":"payload-contracts","label":"Testing","title":"Payload Contract Testing","summary":"Run one project-level contract suite through the canonical test runner and let strategy profiles verify payload boundaries without hand-writing repetitive negative cases.","opensInNewTab":false,"href":"/demo/testing/payload-contracts"}],"groups":[{"key":"contracts","label":"Contracts","featureCount":1,"features":[{"section":"testing","slug":"payload-contracts","label":"Testing","title":"Payload Contract Testing","summary":"Run one project-level contract suite through the canonical test runner and let strategy profiles verify payload boundaries without hand-writing repetitive negative cases.","opensInNewTab":false,"href":"/demo/testing/payload-contracts"}]}]}]}],"currentSection":"data","currentSlug":"schema-sync","infoWhat":"Semitexa minimizes migration churn by computing schema changes directly from code and the current database instead of requiring constant hand-written migrations.","infoHow":"orm:sync collects the code schema, compares it with the live database, builds an execution plan, and separates safe changes from destructive ones. Drops use a two-phase flow: first mark deprecated, later drop only with explicit approval.","infoWhy":"This reduces busywork, makes destructive intent visible, and still gives teams exact SQL artifacts when they need them for review or deployment.","infoKeywords":[{"term":"orm:sync","definition":"Command that computes and optionally executes the schema synchronization plan."},{"term":"Two-phase drop","definition":"Column or table removal is delayed: first deprecate, then drop on a later explicit destructive pass."},{"term":"AuditLogger","definition":"Writes executed sync operations as both JSON and SQL files under var/migrations/history."}],"navMode":"catalog","activeLayerKey":"full-catalog","authUi":{"isAuthenticated":false,"label":"Google user","shortLabel":"Google","email":null,"startUrl":"/demo/auth/google/start?return_to=%2Fdemo","authPageUrl":"/demo/auth/google?return_to=%2Fdemo","accountUrl":"/demo/auth/google?return_to=%2Fdemo","logoutUrl":"/demo/auth/google/logout?return_to=%2Fdemo","actionLabel":"Sign in with Google","signInTitle":"Sign in with Google to unlock advanced demos","signedInLabel":"Authorized as"},"explanation":{"what":"Semitexa minimizes migration churn by computing schema changes directly from code and the current database instead of requiring constant hand-written migrations.","how":"orm:sync collects the code schema, compares it with the live database, builds an execution plan, and separates safe changes from destructive ones. Drops use a two-phase flow: first mark deprecated, later drop only with explicit approval.","why":"This reduces busywork, makes destructive intent visible, and still gives teams exact SQL artifacts when they need them for review or deployment.","keywords":[{"term":"orm:sync","definition":"Command that computes and optionally executes the schema synchronization plan."},{"term":"Two-phase drop","definition":"Column or table removal is delayed: first deprecate, then drop on a later explicit destructive pass."},{"term":"AuditLogger","definition":"Writes executed sync operations as both JSON and SQL files under var/migrations/history."}]},"sourceCode":{"orm:sync Command":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Semitexa\\Orm\\Application\\Console\\Command;\n\nuse Semitexa\\Core\\Attribute\\AsCommand;\nuse Semitexa\\Core\\Console\\BaseCommand;\nuse Semitexa\\Orm\\Application\\Service\\Connection\\ConnectionRegistry;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\n#[AsCommand(name: 'orm:sync', description: 'Synchronize ORM schema with the database')]\nclass OrmSyncCommand extends BaseCommand\n{\n    public function __construct(\n        private readonly ConnectionRegistry $connections,\n    ) {\n        parent::__construct();\n    }\n\n    protected function configure(): void\n    {\n        $this->setName('orm:sync')\n            ->setDescription('Synchronize ORM schema with the database')\n            ->addOption('connection', 'c', InputOption::VALUE_REQUIRED, 'Connection name to sync', 'default')\n            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show SQL plan without executing')\n            ->addOption('allow-destructive', null, InputOption::VALUE_NONE, 'Allow destructive operations (DROP, type narrowing)')\n            ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Save SQL plan to file');\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $io = new SymfonyStyle($input, $output);\n        $connectionOption = $input->getOption('connection');\n        if (!is_string($connectionOption)) {\n            $io->error('Invalid --connection option value.');\n\n            return Command::FAILURE;\n        }\n\n        $connection = trim($connectionOption);\n        if ($connection === '') {\n            $io->error('The --connection option must not be empty.');\n\n            return Command::FAILURE;\n        }\n\n        $dryRun = (bool) $input->getOption('dry-run');\n        $allowDestructive = (bool) $input->getOption('allow-destructive');\n        $outputOption = $input->getOption('output');\n        $outputFile = is_string($outputOption) && $outputOption !== '' ? $outputOption : null;\n\n        try {\n            $orm = $this->connections->manager($connection);\n\n            // 1. Collect schema from code\n            $io->section(\"Collecting schema from code (connection: {$connection})...\");\n            $collector = $orm->getSchemaCollector();\n            $codeSchema = $collector->collect();\n\n            $errors = $collector->getErrors();\n            if ($errors !== []) {\n                $io->error('Schema validation errors:');\n                $io->listing($errors);\n                return Command::FAILURE;\n            }\n\n            $warnings = $collector->getWarnings();\n            if ($warnings !== []) {\n                $io->warning('Schema warnings:');\n                $io->listing($warnings);\n            }\n\n            $io->text(sprintf('Found %d table(s) in code.', count($codeSchema)));\n\n            // 2. Compare with DB\n            $io->section('Comparing with database...');\n            $comparator = $orm->getSchemaComparator();\n            $diff = $comparator->compare($codeSchema);\n\n            if ($diff->isEmpty()) {\n                $io->success('Database is up to date. No changes needed.');\n\n                return Command::SUCCESS;\n            }\n\n            // 3. Build execution plan\n            $syncEngine = $orm->getSyncEngine();\n            $plan = $syncEngine->buildPlan($diff);\n\n            $io->text($plan->getSummary());\n            $io->newLine();\n\n            // Show plan details\n            $safeOps = $plan->getSafeOperations();\n            $destructiveOps = $plan->getDestructiveOperations();\n\n            if ($safeOps !== []) {\n                $io->section('Safe operations:');\n                foreach ($safeOps as $op) {\n                    $io->text(\"  <info>[{$op->type->value}]</info> {$op->description}\");\n                    if ($output->isVerbose()) {\n                        $io->text(\"    SQL: {$op->sql}\");\n                    }\n                }\n            }\n\n            if ($destructiveOps !== []) {\n                $io->section('Destructive operations:');\n                foreach ($destructiveOps as $op) {\n                    $io->text(\"  <fg=red>[{$op->type->value}]</> {$op->description}\");\n                    if ($output->isVerbose()) {\n                        $io->text(\"    SQL: {$op->sql}\");\n                    }\n                }\n\n                if (!$allowDestructive) {\n                    $io->warning('Destructive operations will be skipped. Use --allow-destructive to include them.');\n                }\n            }\n\n            // Save to file if requested\n            if ($outputFile !== null) {\n                $statements = $plan->toSqlStatements($allowDestructive);\n                $content = implode(\";\\n\", $statements) . \";\\n\";\n                file_put_contents($outputFile, $content);\n                $io->text(\"SQL plan saved to: {$outputFile}\");\n            }\n\n            // Execute if not dry-run\n            if ($dryRun) {\n                $io->note('Dry run mode — no changes applied.');\n\n                return Command::SUCCESS;\n            }\n\n            $executed = $syncEngine->execute($plan, $allowDestructive);\n            $io->success(sprintf('Executed %d operation(s).', count($executed)));\n\n            $orm->shutdown();\n            return Command::SUCCESS;\n        } catch (\\Throwable $e) {\n            $io->error('Sync failed: ' . $e->getMessage());\n            if ($output->isVerbose()) {\n                $io->text($e->getTraceAsString());\n            }\n            return Command::FAILURE;\n        }\n    }\n}\n","SyncEngine":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Semitexa\\Orm\\Application\\Service\\Sync;\n\nuse Semitexa\\Orm\\Domain\\Enum\\DdlOperationType;\nuse Semitexa\\Orm\\Domain\\Model\\DdlOperation;\nuse Semitexa\\Orm\\Domain\\Model\\ExecutionPlan;\n\nuse Semitexa\\Orm\\Adapter\\DatabaseAdapterInterface;\nuse Semitexa\\Orm\\Adapter\\DatabaseType;\nuse Semitexa\\Orm\\Adapter\\MySqlType;\nuse Semitexa\\Orm\\Adapter\\SqliteType;\nuse Semitexa\\Orm\\Domain\\Model\\ColumnDefinition;\nuse Semitexa\\Orm\\Domain\\Model\\DbColumnState;\nuse Semitexa\\Orm\\Domain\\Model\\ForeignKeyDefinition;\nuse Semitexa\\Orm\\Domain\\Model\\ResourceMetadata;\nuse Semitexa\\Orm\\Domain\\Model\\SchemaDiff;\nuse Semitexa\\Orm\\Domain\\Model\\TableDefinition;\n\nclass SyncEngine\n{\n    private const DEPRECATED_COMMENT = 'SEMITEXA_DEPRECATED';\n\n    public function __construct(\n        private readonly DatabaseAdapterInterface $adapter,\n        private readonly ?AuditLogger $auditLogger = null,\n    ) {}\n\n    public function buildPlan(SchemaDiff $diff): ExecutionPlan\n    {\n        $plan = new ExecutionPlan();\n\n        // 1. CREATE TABLEs first (sorted by dependencies)\n        $sortedTables = $this->topologicalSort($diff->getCreateTables());\n        foreach ($sortedTables as $table) {\n            $plan->addOperation(new DdlOperation(\n                sql: $this->generateCreateTable($table),\n                type: DdlOperationType::CreateTable,\n                tableName: $table->name,\n                isDestructive: false,\n                description: \"Create table '{$table->name}'\",\n            ));\n\n            if ($this->isSqlite()) {\n                foreach ($table->getIndexes() as $index) {\n                    $name = $index->name ?? $this->generateIndexName($table->name, $index->columns, $index->unique);\n                    $plan->addOperation(new DdlOperation(\n                        sql: $this->generateAddIndex($table->name, $index, $name),\n                        type: DdlOperationType::AddIndex,\n                        tableName: $table->name,\n                        isDestructive: false,\n                        description: \"Add index '{$name}' on '{$table->name}'\",\n                    ));\n                }\n            }\n        }\n\n        // 2. ADD COLUMNs (safe)\n        foreach ($diff->getAddColumns() as $tableName => $columns) {\n            foreach ($columns as $column) {\n                $plan->addOperation(new DdlOperation(\n                    sql: $this->generateAddColumn($tableName, $column),\n                    type: DdlOperationType::AddColumn,\n                    tableName: $tableName,\n                    isDestructive: false,\n                    description: \"Add column '{$column->name}' to '{$tableName}'\",\n                ));\n            }\n        }\n\n        // 3. ALTER COLUMNs (may be safe or destructive)\n        foreach ($diff->getAlterColumns() as $tableName => $alterations) {\n            foreach ($alterations as $alteration) {\n                $column = $alteration['column'];\n                $changes = $alteration['changes'];\n                $isDestructive = $this->isAlterDestructive($changes);\n\n                $plan->addOperation(new DdlOperation(\n                    sql: $this->generateAlterColumn($tableName, $column),\n                    type: DdlOperationType::AlterColumn,\n                    tableName: $tableName,\n                    isDestructive: $isDestructive,\n                    description: \"Alter column '{$column->name}' in '{$tableName}': \" . implode(', ', $changes),\n                ));\n            }\n        }\n\n        // 4. ADD FOREIGN KEYs (safe — all tables already exist at this point)\n        foreach ($diff->getAddForeignKeys() as $fk) {\n            $plan->addOperation(new DdlOperation(\n                sql: $this->generateAddForeignKey($fk),\n                type: DdlOperationType::AddForeignKey,\n                tableName: $fk->table,\n                isDestructive: false,\n                description: \"Add FK constraint '{$fk->constraintName()}' on '{$fk->table}'.{$fk->column} → '{$fk->referencedTable}'.{$fk->referencedColumn}\",\n            ));\n        }\n\n        // 5–6. INDEX changes: DROP + ADD\n        //\n        // When an index is being recreated (same name appears in both drop and add\n        // lists for the same table), MySQL may refuse to drop it if a FK constraint\n        // depends on it (error 1553). Combining DROP INDEX + ADD INDEX into a single\n        // ALTER TABLE statement lets MySQL atomically swap the index definition\n        // without ever leaving the FK unsupported.\n        $dropIndexes = $diff->getDropIndexes();\n        $addIndexes  = $diff->getAddIndexes();\n\n        // Build a lookup of add-indexes keyed by table.name for pairing\n        $addByTableAndName = [];\n        foreach ($addIndexes as $tableName => $indexes) {\n            foreach ($indexes as $entry) {\n                $addByTableAndName[$tableName . '.' . $entry['name']] = $entry;\n            }\n        }\n\n        // Track which add-indexes have been paired with a drop (emitted as combined DDL)\n        $pairedAdds = [];\n\n        // 5. DROP INDEXes (destructive — must run before ADD to avoid duplicate key names)\n        foreach ($dropIndexes as $tableName => $indexNames) {\n            foreach ($indexNames as $indexName) {\n                $key = $tableName . '.' . $indexName;\n                $q = $this->quoteChar();\n\n                if (isset($addByTableAndName[$key])) {\n                    // Same index name is being dropped and re-added → combine into one statement\n                    $entry = $addByTableAndName[$key];\n                    $index = $entry['index'];\n\n                    if ($this->isSqlite()) {\n                        // SQLite: separate DROP and CREATE\n                        $plan->addOperation(new DdlOperation(\n                            sql: \"DROP INDEX {$q}{$indexName}{$q}\",\n                            type: DdlOperationType::DropIndex,\n                            tableName: $tableName,\n                            isDestructive: true,\n                            description: \"Drop index '{$indexName}' from '{$tableName}'\",\n                        ));\n                        $plan->addOperation(new DdlOperation(\n                            sql: $this->generateAddIndex($tableName, $index, $indexName),\n                            type: DdlOperationType::AddIndex,\n                            tableName: $tableName,\n                            isDestructive: false,\n                            description: \"Recreate index '{$indexName}' on '{$tableName}'\",\n                        ));\n                    } else {\n                        $cols = implode('`, `', $index->columns);\n                        $type = $index->unique ? 'UNIQUE INDEX' : 'INDEX';\n                        $sql = \"ALTER TABLE `{$tableName}` DROP INDEX `{$indexName}`, ADD {$type} `{$indexName}` (`{$cols}`)\";\n\n                        $plan->addOperation(new DdlOperation(\n                            sql: $sql,\n                            type: DdlOperationType::DropIndex,\n                            tableName: $tableName,\n                            isDestructive: true,\n                            description: \"Recreate index '{$indexName}' on '{$tableName}'\",\n                        ));\n                    }\n                    $pairedAdds[$key] = true;\n                } else {\n                    if ($this->isSqlite()) {\n                        $plan->addOperation(new DdlOperation(\n                            sql: \"DROP INDEX {$q}{$indexName}{$q}\",\n                            type: DdlOperationType::DropIndex,\n                            tableName: $tableName,\n                            isDestructive: true,\n                            description: \"Drop index '{$indexName}' from '{$tableName}'\",\n                        ));\n                    } else {\n                        $plan->addOperation(new DdlOperation(\n                            sql: \"ALTER TABLE `{$tableName}` DROP INDEX `{$indexName}`\",\n                            type: DdlOperationType::DropIndex,\n                            tableName: $tableName,\n                            isDestructive: true,\n                            description: \"Drop index '{$indexName}' from '{$tableName}'\",\n                        ));\n                    }\n                }\n            }\n        }\n\n        // 6. ADD INDEXes (safe) — skip any that were already emitted as part of a combined statement\n        foreach ($addIndexes as $tableName => $indexes) {\n            foreach ($indexes as $entry) {\n                $key = $tableName . '.' . $entry['name'];\n                if (isset($pairedAdds[$key])) {\n                    continue;\n                }\n\n                $index = $entry['index'];\n                $name = $entry['name'];\n                $plan->addOperation(new DdlOperation(\n                    sql: $this->generateAddIndex($tableName, $index, $name),\n                    type: DdlOperationType::AddIndex,\n                    tableName: $tableName,\n                    isDestructive: false,\n                    description: \"Add index '{$name}' on '{$tableName}'\",\n                ));\n            }\n        }\n\n        // 7. DROP COLUMNs — two-phase logic (destructive)\n        foreach ($diff->getDropColumns() as $tableName => $columns) {\n            foreach ($columns as $colInfo) {\n                $columnName = $colInfo['name'];\n                $comment    = $colInfo['comment'];\n                $dbState    = $colInfo['dbState'];\n\n                if ($comment !== self::DEPRECATED_COMMENT) {\n                    // Column was not previously marked as deprecated → block drop, add deprecation comment instead.\n                    // MODIFY COLUMN requires the full column definition — reconstruct it from DbColumnState.\n                    $plan->addOperation(new DdlOperation(\n                        sql: $this->generateDeprecationDdl($tableName, $dbState),\n                        type: DdlOperationType::AlterColumn,\n                        tableName: $tableName,\n                        isDestructive: false,\n                        description: \"Mark column '{$columnName}' in '{$tableName}' as deprecated (two-phase drop, phase 1)\",\n                    ));\n                } else {\n                    // Column was already deprecated → safe to drop\n                    $q = $this->quoteChar();\n                    $plan->addOperation(new DdlOperation(\n                        sql: \"ALTER TABLE {$q}{$tableName}{$q} DROP COLUMN {$q}{$columnName}{$q}\",\n                        type: DdlOperationType::DropColumn,\n                        tableName: $tableName,\n                        isDestructive: true,\n                        description: \"Drop deprecated column '{$columnName}' from '{$tableName}' (two-phase drop, phase 2)\",\n                    ));\n                }\n            }\n        }\n\n        // 8. DROP FOREIGN KEYs (destructive — must happen before DROP TABLE/COLUMN)\n        foreach ($diff->getDropForeignKeys() as $entry) {\n            if ($this->isSqlite()) {\n                // SQLite: FK constraints cannot be dropped separately.\n                // Table recreation would be needed — skip for now.\n                continue;\n            }\n            $plan->addOperation(new DdlOperation(\n                sql: \"ALTER TABLE `{$entry['table']}` DROP FOREIGN KEY `{$entry['constraintName']}`\",\n                type: DdlOperationType::DropForeignKey,\n                tableName: $entry['table'],\n                isDestructive: true,\n                description: \"Drop FK constraint '{$entry['constraintName']}' from '{$entry['table']}'\",\n            ));\n        }\n\n        // 9. DROP TABLEs — two-phase logic (destructive)\n        foreach ($diff->getDropTables() as $dbTable) {\n            $tableName = $dbTable->name;\n            $q = $this->quoteChar();\n            if ($dbTable->tableComment !== self::DEPRECATED_COMMENT) {\n                if ($this->isSqlite()) {\n                    // SQLite lacks table comments, so do not drop the table implicitly.\n                    // Force an explicit/manual follow-up instead of risking silent data loss.\n                    $plan->addOperation(new DdlOperation(\n                        sql: \"-- SQLITE_DROP_TABLE_REQUIRES_MANUAL_REVIEW:{$tableName}\",\n                        type: DdlOperationType::AlterColumn,\n                        tableName: $tableName,\n                        isDestructive: false,\n                        description: \"Manual review required before dropping SQLite table '{$tableName}'\",\n                    ));\n                } else {\n                    // Table was not previously marked as deprecated → block drop, add deprecation comment instead.\n                    $plan->addOperation(new DdlOperation(\n                        sql: \"ALTER TABLE `{$tableName}` COMMENT '\" . self::DEPRECATED_COMMENT . \"'\",\n                        type: DdlOperationType::AlterColumn,\n                        tableName: $tableName,\n                        isDestructive: false,\n                        description: \"Mark table '{$tableName}' as deprecated (two-phase drop, phase 1)\",\n                    ));\n                }\n            } else {\n                // Table was already deprecated → safe to drop\n                $plan->addOperation(new DdlOperation(\n                    sql: \"DROP TABLE `{$tableName}`\",\n                    type: DdlOperationType::DropTable,\n                    tableName: $tableName,\n                    isDestructive: true,\n                    description: \"Drop deprecated table '{$tableName}' (two-phase drop, phase 2)\",\n                ));\n            }\n        }\n\n        return $plan;\n    }\n\n    /**\n     * Execute a plan against the database.\n     *\n     * When the server supports atomic DDL (MySQL 8.0+), all operations are\n     * wrapped in a single transaction so a mid-plan failure rolls back cleanly.\n     * On older MySQL/MariaDB, operations are applied one by one (no rollback on failure).\n     *\n     * @return DdlOperation[] Executed operations\n     */\n    public function execute(ExecutionPlan $plan, bool $allowDestructive = false): array\n    {\n        $operations = array_filter(\n            $plan->getOperations(),\n            fn(DdlOperation $op) => $allowDestructive || !$op->isDestructive,\n        );\n\n        if ($operations === []) {\n            return [];\n        }\n\n        $useTransaction = $this->adapter->supports(\\Semitexa\\Orm\\Adapter\\ServerCapability::AtomicDdl);\n        $isSqlite = $this->isSqlite();\n\n        $executed = [];\n        try {\n            if ($useTransaction) {\n                $this->adapter->query($isSqlite ? 'BEGIN' : 'START TRANSACTION');\n            }\n\n            foreach ($operations as $operation) {\n                if ($isSqlite && $this->isSqlitePlaceholder($operation->sql)) {\n                    throw new \\RuntimeException(\n                        \"SQLite sync requires table recreation for unsupported operation: {$operation->description}\",\n                    );\n                }\n\n                $this->adapter->execute($operation->sql);\n                $executed[] = $operation;\n            }\n\n            if ($useTransaction) {\n                $this->adapter->query($isSqlite ? 'COMMIT' : 'COMMIT');\n            }\n        } catch (\\Throwable $e) {\n            if ($useTransaction) {\n                $this->adapter->query($isSqlite ? 'ROLLBACK' : 'ROLLBACK');\n            }\n            throw $e;\n        }\n\n        $this->auditLogger?->log($executed);\n\n        return $executed;\n    }\n\n    /**\n     * Check if SQL is a SQLite placeholder for unsupported operations.\n     */\n    private function isSqlitePlaceholder(string $sql): bool\n    {\n        return str_starts_with($sql, '-- SQLITE_');\n    }\n\n    private function generateCreateTable(TableDefinition $table): string\n    {\n        $isSqlite = $this->isSqlite();\n        $lines = [];\n        $pk = null;\n        $inlineFks = [];\n\n        foreach ($table->getColumns() as $col) {\n            $line = '  ' . $this->generateColumnDdl($col);\n            if ($col->isPrimaryKey) {\n                $pk = $col;\n            }\n            $lines[] = $line;\n        }\n\n        if ($pk !== null) {\n            if ($isSqlite && $pk->pkStrategy === 'auto' && $this->isSqliteAutoIncrementPrimaryKey($pk)) {\n                // SQLite: INTEGER PRIMARY KEY implies AUTOINCREMENT behavior\n                // Already handled in generateColumnDdl\n            } else {\n                $q = $isSqlite ? '\"' : '`';\n                $lines[] = \"  PRIMARY KEY ({$q}{$pk->name}{$q})\";\n            }\n        }\n\n        foreach ($table->getIndexes() as $index) {\n            $name = $index->name ?? $this->generateIndexName($table->name, $index->columns, $index->unique);\n            $q = $isSqlite ? '\"' : '`';\n            $cols = implode(\"{$q}, {$q}\", $index->columns);\n            if ($isSqlite) {\n                // SQLite: indexes are created separately, not inline in CREATE TABLE\n                // We'll handle them after table creation\n            } else {\n                $prefix = $index->unique ? 'UNIQUE KEY' : 'KEY';\n                $lines[] = \"  {$prefix} `{$name}` (`{$cols}`)\";\n            }\n        }\n\n        // Add inline FK constraints for SQLite (must be in CREATE TABLE)\n        if ($isSqlite) {\n            foreach ($table->getForeignKeys() as $fk) {\n                $lines[] = \"  FOREIGN KEY (\\\"{$fk->column}\\\") REFERENCES \\\"{$fk->referencedTable}\\\"(\\\"{$fk->referencedColumn}\\\") ON DELETE {$fk->onDelete->value} ON UPDATE {$fk->onUpdate->value}\";\n            }\n        }\n\n        $body = implode(\",\\n\", $lines);\n        $q = $isSqlite ? '\"' : '`';\n\n        if ($isSqlite) {\n            return \"CREATE TABLE {$q}{$table->name}{$q} (\\n{$body}\\n)\";\n        }\n\n        return \"CREATE TABLE `{$table->name}` (\\n{$body}\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\";\n    }\n\n    private function generateColumnDdl(ColumnDefinition $col): string\n    {\n        $isSqlite = $this->isSqlite();\n        $q = $this->quoteChar();\n        $type = $this->sqlType($col);\n        $null = $col->nullable ? 'NULL' : 'NOT NULL';\n\n        // Auto-increment handling\n        $auto = '';\n        if ($col->isPrimaryKey && $col->pkStrategy === 'auto') {\n            if ($isSqlite && $this->isSqliteAutoIncrementPrimaryKey($col)) {\n                // SQLite: INTEGER PRIMARY KEY auto-increments implicitly\n                // Type is already INTEGER from sqlType()\n                $auto = ' PRIMARY KEY AUTOINCREMENT';\n                // In SQLite, we skip the separate PRIMARY KEY clause\n                // and the NULL clause for autoincrement PKs\n                $deprecated = $col->isDeprecated ? \" -- \" . self::DEPRECATED_COMMENT : '';\n                return \"{$q}{$col->name}{$q} {$type}{$auto}{$deprecated}\";\n            } elseif ($col->type instanceof MySqlType && in_array($col->type, [MySqlType::Int, MySqlType::Bigint], true)) {\n                $auto = ' AUTO_INCREMENT';\n            }\n        }\n\n        $default = $this->defaultClause($col);\n        $deprecated = !$isSqlite && $col->isDeprecated ? \" COMMENT '\" . self::DEPRECATED_COMMENT . \"'\" : '';\n\n        return \"{$q}{$col->name}{$q} {$type} {$null}{$auto}{$default}{$deprecated}\";\n    }\n\n    private function sqlType(ColumnDefinition $col): string\n    {\n        // Delegate to the type's own toSql() method\n        return $col->type->toSql($col->length, $col->precision, $col->scale);\n    }\n\n    private function isSqliteAutoIncrementPrimaryKey(ColumnDefinition $col): bool\n    {\n        if ($col->type instanceof SqliteType) {\n            return in_array($col->type, [SqliteType::Int, SqliteType::Bigint], true);\n        }\n\n        if ($col->type instanceof MySqlType) {\n            return in_array($col->type, [MySqlType::Int, MySqlType::Bigint], true);\n        }\n\n        return false;\n    }\n\n    private function defaultClause(ColumnDefinition $col): string\n    {\n        if ($col->default === null && !$col->nullable) {\n            return '';\n        }\n\n        if ($col->default === null) {\n            return ' DEFAULT NULL';\n        }\n\n        if (is_bool($col->default)) {\n            return ' DEFAULT ' . ($col->default ? '1' : '0');\n        }\n\n        if (is_int($col->default) || is_float($col->default)) {\n            return ' DEFAULT ' . $col->default;\n        }\n\n        return \" DEFAULT '\" . str_replace(\"'\", \"''\", (string) $col->default) . \"'\";\n    }\n\n    private function generateAddColumn(string $tableName, ColumnDefinition $col): string\n    {\n        $q = $this->quoteChar();\n        $ddl = $this->generateColumnDdl($col);\n        return \"ALTER TABLE {$q}{$tableName}{$q} ADD COLUMN {$ddl}\";\n    }\n\n    private function generateAlterColumn(string $tableName, ColumnDefinition $col): string\n    {\n        if ($this->isSqlite()) {\n            // SQLite has very limited ALTER TABLE support.\n            // For column alterations, we need to recreate the table.\n            // This is handled specially in the execution phase.\n            return \"-- SQLITE_ALTER_COLUMN:{$tableName}:{$col->name}\";\n        }\n\n        $ddl = $this->generateColumnDdl($col);\n        return \"ALTER TABLE `{$tableName}` MODIFY COLUMN {$ddl}\";\n    }\n\n    /**\n     * Generate MODIFY COLUMN DDL that marks a live DB column as deprecated.\n     *\n     * MySQL MODIFY COLUMN requires the complete column definition — omitting\n     * the type causes MySQL to silently reset it to a default. We reconstruct\n     * the full definition from DbColumnState (the live DB snapshot read via\n     * INFORMATION_SCHEMA) and append the deprecation comment.\n     */\n    private function generateDeprecationDdl(string $tableName, DbColumnState $col): string\n    {\n        if ($this->isSqlite()) {\n            // SQLite doesn't support column comments.\n            // We skip deprecation marking for SQLite.\n            return \"-- SQLITE_DEPRECATION_NOT_SUPPORTED\";\n        }\n\n        // columnType from INFORMATION_SCHEMA is the authoritative full type string\n        // (e.g. \"varchar(255)\", \"decimal(10,2)\", \"tinyint(1)\") — use it verbatim.\n        $null    = $col->nullable ? 'NULL' : 'NOT NULL';\n        $auto    = $col->isAutoIncrement ? ' AUTO_INCREMENT' : '';\n        $default = '';\n\n        if ($col->defaultValue !== null) {\n            $default = \" DEFAULT '\" . str_replace(\"'\", \"''\", $col->defaultValue) . \"'\";\n        } elseif ($col->nullable) {\n            $default = ' DEFAULT NULL';\n        }\n\n        $comment = \" COMMENT '\" . self::DEPRECATED_COMMENT . \"'\";\n\n        $ddl = \"`{$col->name}` {$col->columnType} {$null}{$auto}{$default}{$comment}\";\n\n        return \"ALTER TABLE `{$tableName}` MODIFY COLUMN {$ddl}\";\n    }\n\n    /**\n     * @param \\Semitexa\\Orm\\Domain\\Model\\IndexDefinition $index\n     */\n    private function generateAddIndex(string $tableName, $index, string $name): string\n    {\n        $q = $this->quoteChar();\n        $cols = implode(\"{$q}, {$q}\", $index->columns);\n\n        if ($this->isSqlite()) {\n            $type = $index->unique ? 'UNIQUE INDEX' : 'INDEX';\n            return \"CREATE {$type} {$q}{$name}{$q} ON {$q}{$tableName}{$q} ({$q}{$cols}{$q})\";\n        }\n\n        $type = $index->unique ? 'UNIQUE INDEX' : 'INDEX';\n        return \"ALTER TABLE `{$tableName}` ADD {$type} `{$name}` (`{$cols}`)\";\n    }\n\n    private function generateAddForeignKey(ForeignKeyDefinition $fk): string\n    {\n        $q = $this->quoteChar();\n\n        if ($this->isSqlite()) {\n            // SQLite: FK constraints must be added during table creation.\n            // For existing tables, we need to recreate the table.\n            return \"-- SQLITE_ADD_FK:{$fk->table}:{$fk->column}:{$fk->referencedTable}:{$fk->referencedColumn}\";\n        }\n\n        $name = $fk->constraintName();\n        return sprintf(\n            'ALTER TABLE `%s` ADD CONSTRAINT `%s` FOREIGN KEY (`%s`) REFERENCES `%s`(`%s`) ON DELETE %s ON UPDATE %s',\n            $fk->table,\n            $name,\n            $fk->column,\n            $fk->referencedTable,\n            $fk->referencedColumn,\n            $fk->onDelete->value,\n            $fk->onUpdate->value,\n        );\n    }\n\n    /**\n     * Topological sort of tables by FK dependencies (BelongsTo relations).\n     * Tables without dependencies come first.\n     *\n     * @param TableDefinition[] $tables\n     * @return TableDefinition[]\n     */\n    private function topologicalSort(array $tables): array\n    {\n        $tableMap = [];\n        foreach ($tables as $table) {\n            $tableMap[$table->name] = $table;\n        }\n\n        // Build a reverse map: FQCN resource class → table name.\n        // Relations store target as a FQCN (e.g. App\\Resource\\UserResource), but\n        // $tableMap is keyed by table name (e.g. 'users'). Without this mapping\n        // the dependency lookup always misses, leaving tables in arbitrary order.\n        $classToTable = [];\n        foreach ($tables as $table) {\n            foreach ($table->getRelations() as $relation) {\n                $targetClass = $relation['target'];\n                if (isset($classToTable[$targetClass])) {\n                    continue;\n                }\n                try {\n                    $meta = \\Semitexa\\Orm\\Domain\\Model\\ResourceMetadata::for($targetClass);\n                    $classToTable[$targetClass] = $meta->getTableName();\n                } catch (\\Throwable) {\n                    // Target class not available in this context — skip gracefully\n                }\n            }\n        }\n\n        // Build dependency graph using table names throughout\n        $deps = [];\n        foreach ($tables as $table) {\n            $deps[$table->name] = [];\n            foreach ($table->getRelations() as $relation) {\n                if ($relation['type'] === 'belongs_to') {\n                    $targetTable = $classToTable[$relation['target']] ?? null;\n                    if ($targetTable !== null && isset($tableMap[$targetTable])) {\n                        $deps[$table->name][] = $targetTable;\n                    }\n                }\n            }\n        }\n\n        $sorted = [];\n        $visited = [];\n        $visiting = [];\n\n        $visit = function (string $name) use (&$visit, &$sorted, &$visited, &$visiting, $tableMap, $deps): void {\n            if (isset($visited[$name])) {\n                return;\n            }\n            if (isset($visiting[$name])) {\n                // Circular dependency — just add it; CREATE TABLE handles FK separately\n                return;\n            }\n\n            $visiting[$name] = true;\n\n            foreach ($deps[$name] ?? [] as $dep) {\n                if (isset($tableMap[$dep])) {\n                    $visit($dep);\n                }\n            }\n\n            unset($visiting[$name]);\n            $visited[$name] = true;\n\n            if (isset($tableMap[$name])) {\n                $sorted[] = $tableMap[$name];\n            }\n        };\n\n        foreach ($tableMap as $name => $table) {\n            $visit($name);\n        }\n\n        return $sorted;\n    }\n\n    /**\n     * Determine if column alteration is destructive.\n     *\n     * @param string[] $changes\n     */\n    private function isAlterDestructive(array $changes): bool\n    {\n        foreach ($changes as $change) {\n            if (str_starts_with($change, 'type:')) {\n                // Type change is potentially destructive — check if it's widening\n                if ($this->isTypeWidening($change)) {\n                    continue;\n                }\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private function isTypeWidening(string $change): bool\n    {\n        // Extract old → new from \"type: old → new\"\n        if (!preg_match('/type:\\s*(.+?)\\s*→\\s*(.+)/', $change, $matches)) {\n            return false;\n        }\n\n        $old = strtolower(trim($matches[1]));\n        $new = strtolower(trim($matches[2]));\n\n        // VARCHAR(N) → VARCHAR(M) where M >= N\n        if (preg_match('/^varchar\\((\\d+)\\)$/', $old, $oldM) && preg_match('/^varchar\\((\\d+)\\)$/', $new, $newM)) {\n            return (int) $newM[1] >= (int) $oldM[1];\n        }\n\n        // VARCHAR(any) → TEXT/MEDIUMTEXT/LONGTEXT — always wider\n        if (str_starts_with($old, 'varchar(') && in_array($new, ['text', 'mediumtext', 'longtext'], true)) {\n            return true;\n        }\n\n        // TEXT → MEDIUMTEXT → LONGTEXT\n        $textOrder = ['text' => 0, 'mediumtext' => 1, 'longtext' => 2];\n        if (isset($textOrder[$old], $textOrder[$new])) {\n            return $textOrder[$new] >= $textOrder[$old];\n        }\n\n        // Integer widening order: TINYINT → SMALLINT → INT → BIGINT\n        $intOrder = ['tinyint' => 0, 'smallint' => 1, 'int' => 2, 'bigint' => 3];\n        $oldBase = preg_replace('/\\(\\d+\\)/', '', $old); // strip (1) from tinyint(1)\n        if (isset($intOrder[$oldBase], $intOrder[$new])) {\n            return $intOrder[$new] >= $intOrder[$oldBase];\n        }\n\n        // Float widening: FLOAT → DOUBLE\n        if ($old === 'float' && $new === 'double') {\n            return true;\n        }\n\n        // CHAR(N) → CHAR(M) where M >= N\n        if (preg_match('/^char\\((\\d+)\\)$/', $old, $oldM) && preg_match('/^char\\((\\d+)\\)$/', $new, $newM)) {\n            return (int) $newM[1] >= (int) $oldM[1];\n        }\n\n        // CHAR(any) → VARCHAR(any) — always wider (fixed → variable)\n        if (str_starts_with($old, 'char(') && str_starts_with($new, 'varchar(')) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * @param string[] $columns\n     */\n    private function generateIndexName(string $tableName, array $columns, bool $unique): string\n    {\n        $prefix = $unique ? 'uniq' : 'idx';\n        return $prefix . '_' . $tableName . '_' . implode('_', $columns);\n    }\n\n    /**\n     * Check if the current adapter is SQLite.\n     */\n    private function isSqlite(): bool\n    {\n        return $this->adapter instanceof \\Semitexa\\Orm\\Adapter\\SqliteAdapter;\n    }\n\n    /**\n     * Get the quote character for the current database.\n     */\n    private function quoteChar(): string\n    {\n        return $this->isSqlite() ? '\"' : '`';\n    }\n}\n","AuditLogger":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Semitexa\\Orm\\Application\\Service\\Sync;\n\nclass AuditLogger\n{\n    public function __construct(\n        private readonly string $historyDir,\n    ) {}\n\n    /**\n     * @param DdlOperation[] $operations\n     */\n    public function log(array $operations): void\n    {\n        if ($operations === []) {\n            return;\n        }\n\n        if (!is_dir($this->historyDir)) {\n            mkdir($this->historyDir, 0755, true);\n        }\n\n        $now = \\DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true)));\n        $timestamp = $now !== false ? $now->format('Y-m-d_H-i-s.v') : date('Y-m-d_H-i-s') . '.' . substr((string) microtime(), 2, 3);\n        $filename = $this->historyDir . '/' . $timestamp . '_sync.json';\n\n        $entries = [];\n        foreach ($operations as $op) {\n            $entries[] = [\n                'type' => $op->type->value,\n                'table' => $op->tableName,\n                'destructive' => $op->isDestructive,\n                'description' => $op->description,\n                'sql' => $op->sql,\n            ];\n        }\n\n        $data = [\n            'timestamp' => date('c'),\n            'operations_count' => count($operations),\n            'operations' => $entries,\n        ];\n\n        file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));\n\n        // Also write a .sql file for DevOps convenience\n        $sqlFilename  = $this->historyDir . '/' . $timestamp . '_sync.sql';\n        $sqlLines = [\n            '-- Semitexa ORM Sync — ' . date('c'),\n            '-- Operations: ' . count($operations),\n            '',\n        ];\n\n        foreach ($operations as $op) {\n            $sqlLines[] = '-- ' . $op->description;\n            $sqlLines[] = $op->sql . ';';\n            $sqlLines[] = '';\n        }\n\n        file_put_contents($sqlFilename, implode(\"\\n\", $sqlLines));\n    }\n}\n","OrmManager":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Semitexa\\Orm;\n\nuse Semitexa\\Core\\Discovery\\ClassDiscovery;\nuse Semitexa\\Core\\Environment;\nuse Semitexa\\Core\\Support\\ProjectRoot;\nuse Semitexa\\Orm\\Adapter\\ConnectionPool;\nuse Semitexa\\Orm\\Adapter\\ConnectionPoolInterface;\nuse Semitexa\\Orm\\Domain\\Model\\ConnectionConfig;\nuse Semitexa\\Orm\\Adapter\\DatabaseAdapterInterface;\nuse Semitexa\\Orm\\Adapter\\MysqlAdapter;\nuse Semitexa\\Orm\\Adapter\\NullConnectionPool;\nuse Semitexa\\Orm\\Adapter\\SingleConnectionPool;\nuse Semitexa\\Orm\\Adapter\\SqliteAdapter;\nuse Semitexa\\Orm\\Application\\Service\\Schema\\SqliteSchemaComparator;\nuse Semitexa\\Orm\\Domain\\Model\\OrmBootstrapReport;\nuse Semitexa\\Orm\\Application\\Service\\OrmBootstrapValidator;\nuse Semitexa\\Orm\\Application\\Service\\Hydration\\ResourceModelHydrator;\nuse Semitexa\\Orm\\Application\\Service\\Hydration\\ResourceModelRelationLoader;\nuse Semitexa\\Orm\\Application\\Service\\Mapping\\MapperRegistry;\nuse Semitexa\\Orm\\Metadata\\ResourceModelMetadataRegistry;\nuse Semitexa\\Orm\\Application\\Service\\Persistence\\AggregateWriteEngine;\nuse Semitexa\\Orm\\Repository\\DomainRepository;\nuse Semitexa\\Orm\\Application\\Service\\Schema\\SchemaCollector;\nuse Semitexa\\Orm\\Application\\Service\\Schema\\SchemaComparator;\nuse Semitexa\\Orm\\Domain\\Contract\\SchemaComparatorInterface;\nuse Semitexa\\Orm\\Application\\Service\\Sync\\AuditLogger;\nuse Semitexa\\Orm\\Application\\Service\\Sync\\SeedRunner;\nuse Semitexa\\Orm\\Application\\Service\\Sync\\SyncEngine;\nuse Semitexa\\Orm\\Application\\Service\\Transaction\\TransactionManager;\n\nclass OrmManager\n{\n    private ClassDiscovery $classDiscovery;\n    private ?ConnectionPoolInterface $pool = null;\n    private ?DatabaseAdapterInterface $adapter = null;\n    private ?SchemaCollector $schemaCollector = null;\n    private ?SchemaComparatorInterface $schemaComparator = null;\n    private ?SyncEngine $syncEngine = null;\n    private ?TransactionManager $transactionManager = null;\n    private ?SeedRunner $seedRunner = null;\n    private ?MapperRegistry $mapperRegistry = null;\n    private ?ResourceModelMetadataRegistry $resourceModelMetadataRegistry = null;\n    private ?ResourceModelHydrator $resourceModelHydrator = null;\n    private ?ResourceModelRelationLoader $resourceModelRelationLoader = null;\n    private ?AggregateWriteEngine $aggregateWriteEngine = null;\n    private ?OrmBootstrapValidator $bootstrapValidator = null;\n\n    public function __construct(\n        ?ClassDiscovery $classDiscovery = null,\n        private readonly ?ConnectionConfig $config = null,\n        private readonly string $connectionName = 'default',\n    ) {\n        $this->classDiscovery = $classDiscovery ?? new ClassDiscovery();\n    }\n\n    public function getClassDiscovery(): ClassDiscovery\n    {\n        return $this->classDiscovery;\n    }\n\n    public function getAdapter(): DatabaseAdapterInterface\n    {\n        if ($this->adapter === null) {\n            $driver = $this->resolveDriver();\n\n            if ($driver === 'sqlite') {\n                $this->adapter = $this->createSqliteAdapter();\n            } else {\n                $this->adapter = new MysqlAdapter($this->getPool());\n            }\n        }\n\n        return $this->adapter;\n    }\n\n    public function getPool(): ConnectionPoolInterface\n    {\n        if ($this->pool === null) {\n            $driver = $this->resolveDriver();\n\n            // SQLite doesn't need connection pooling\n            if ($driver === 'sqlite') {\n                throw new \\LogicException('getPool() is not applicable for SQLite adapter. Use getAdapter() directly.');\n            }\n\n            $this->pool = $this->createPool();\n        }\n\n        return $this->pool;\n    }\n\n    public function getSchemaCollector(): SchemaCollector\n    {\n        if ($this->schemaCollector === null) {\n            $this->schemaCollector = new SchemaCollector(\n                $this->classDiscovery,\n                $this->resolveDriver(),\n                $this->connectionName,\n            );\n        }\n\n        return $this->schemaCollector;\n    }\n\n    public function getSchemaComparator(): SchemaComparatorInterface\n    {\n        if ($this->schemaComparator === null) {\n            if ($this->resolveDriver() === 'sqlite') {\n                $this->schemaComparator = new SqliteSchemaComparator(\n                    $this->getAdapter(),\n                    $this->resolveIgnoreTables(),\n                );\n            } else {\n                $this->schemaComparator = new SchemaComparator(\n                    $this->getAdapter(),\n                    $this->getDatabaseName(),\n                    $this->resolveIgnoreTables(),\n                );\n            }\n        }\n\n        return $this->schemaComparator;\n    }\n\n    public function getSyncEngine(): SyncEngine\n    {\n        if ($this->syncEngine === null) {\n            $historyDir = ProjectRoot::get() . '/var/migrations/history';\n            $this->syncEngine = new SyncEngine(\n                $this->getAdapter(),\n                new AuditLogger($historyDir),\n            );\n        }\n\n        return $this->syncEngine;\n    }\n\n    public function getTransactionManager(): TransactionManager\n    {\n        if ($this->transactionManager === null) {\n            $driver = $this->resolveDriver();\n\n            // SQLite: TransactionManager takes a dedicated SQLite code path\n            // and does not actually consult the pool — supply a named\n            // NullConnectionPool so any accidental pop() throws loudly.\n            $pool = $driver === 'sqlite'\n                ? new NullConnectionPool()\n                : $this->getPool();\n\n            $this->transactionManager = new TransactionManager(\n                $pool,\n                $this->getAdapter(),\n            );\n        }\n\n        return $this->transactionManager;\n    }\n\n    public function getSeedRunner(): SeedRunner\n    {\n        if ($this->seedRunner === null) {\n            $this->seedRunner = new SeedRunner($this->getAdapter(), $this->classDiscovery);\n        }\n\n        return $this->seedRunner;\n    }\n\n    public function getMapperRegistry(): MapperRegistry\n    {\n        if ($this->mapperRegistry === null) {\n            $this->mapperRegistry = new MapperRegistry($this->classDiscovery);\n            $this->mapperRegistry->build();\n        }\n\n        return $this->mapperRegistry;\n    }\n\n    public function getResourceModelMetadataRegistry(): ResourceModelMetadataRegistry\n    {\n        if ($this->resourceModelMetadataRegistry === null) {\n            $this->resourceModelMetadataRegistry = new ResourceModelMetadataRegistry();\n        }\n\n        return $this->resourceModelMetadataRegistry;\n    }\n\n    public function getResourceModelHydrator(): ResourceModelHydrator\n    {\n        if ($this->resourceModelHydrator === null) {\n            $this->resourceModelHydrator = new ResourceModelHydrator(\n                metadataRegistry: $this->getResourceModelMetadataRegistry(),\n            );\n        }\n\n        return $this->resourceModelHydrator;\n    }\n\n    public function getResourceModelRelationLoader(): ResourceModelRelationLoader\n    {\n        if ($this->resourceModelRelationLoader === null) {\n            $this->resourceModelRelationLoader = new ResourceModelRelationLoader(\n                $this->getAdapter(),\n                $this->getResourceModelHydrator(),\n                $this->getResourceModelMetadataRegistry(),\n            );\n        }\n\n        return $this->resourceModelRelationLoader;\n    }\n\n    public function getAggregateWriteEngine(): AggregateWriteEngine\n    {\n        if ($this->aggregateWriteEngine === null) {\n            $this->aggregateWriteEngine = new AggregateWriteEngine(\n                $this->getAdapter(),\n                $this->getResourceModelHydrator(),\n                $this->getResourceModelMetadataRegistry(),\n            );\n        }\n\n        return $this->aggregateWriteEngine;\n    }\n\n    public function getBootstrapValidator(): OrmBootstrapValidator\n    {\n        if ($this->bootstrapValidator === null) {\n            $this->bootstrapValidator = new OrmBootstrapValidator(\n                classDiscovery: $this->classDiscovery,\n                metadataRegistry: $this->getResourceModelMetadataRegistry(),\n                mapperRegistry: $this->getMapperRegistry(),\n            );\n        }\n\n        return $this->bootstrapValidator;\n    }\n\n    public function validateBootstrap(): OrmBootstrapReport\n    {\n        return $this->getBootstrapValidator()->validate();\n    }\n\n    /**\n     * @param class-string $resourceModelClass\n     * @param class-string $domainModelClass\n     */\n    public function repository(string $resourceModelClass, string $domainModelClass): DomainRepository\n    {\n        return new DomainRepository(\n            resourceModelClass: $resourceModelClass,\n            domainModelClass: $domainModelClass,\n            adapter: $this->getAdapter(),\n            mapperRegistry: $this->getMapperRegistry(),\n            hydrator: $this->getResourceModelHydrator(),\n            relationLoader: $this->getResourceModelRelationLoader(),\n            metadataRegistry: $this->getResourceModelMetadataRegistry(),\n            writeEngine: $this->getAggregateWriteEngine(),\n        );\n    }\n\n    public function getDatabaseName(): string\n    {\n        if ($this->config !== null) {\n            if ($this->config->driver === 'sqlite') {\n                return $this->config->sqliteMemory\n                    ? ':memory:'\n                    : ($this->config->sqlitePath ?? 'sqlite');\n            }\n\n            return $this->config->database;\n        }\n\n        if ($this->resolveDriver() === 'sqlite') {\n            $memory = Environment::getEnvValue('DB_SQLITE_MEMORY');\n            if (in_array(strtolower((string) $memory), ['1', 'true', 'yes'], true)) {\n                return ':memory:';\n            }\n\n            return Environment::getEnvValue('DB_SQLITE_PATH', ProjectRoot::get() . '/var/database/semitexa.sqlite')\n                ?? ProjectRoot::get() . '/var/database/semitexa.sqlite';\n        }\n\n        return Environment::getEnvValue('DB_DATABASE', 'semitexa') ?? 'semitexa';\n    }\n\n    public function shutdown(): void\n    {\n        $this->pool?->close();\n        $this->pool = null;\n        $this->adapter = null;\n    }\n\n    public function __destruct()\n    {\n        $this->shutdown();\n    }\n\n    /**\n     * Run a callback with a managed OrmManager instance.\n     * shutdown() is guaranteed via finally — even if the callback throws.\n     *\n     * @template T\n     * @param callable(OrmManager): T $callback\n     * @return T\n     */\n    public static function run(callable $callback): mixed\n    {\n        $orm = new self();\n        try {\n            return $callback($orm);\n        } finally {\n            $orm->shutdown();\n        }\n    }\n\n    /**\n     * @return string[]\n     */\n    private function resolveIgnoreTables(): array\n    {\n        $raw = Environment::getEnvValue('ORM_IGNORE_TABLES', '');\n        if ($raw === '') {\n            return [];\n        }\n\n        return array_values(array_filter(array_map('trim', explode(',', $raw))));\n    }\n\n    private function createPool(): ConnectionPoolInterface\n    {\n        // Ensure Swoole workers/coroutines resolve DB_* values from the project env files.\n        if (class_exists(\\Swoole\\Coroutine::class, false)) {\n            ProjectRoot::reset();\n            Environment::syncEnvFromFiles();\n        }\n\n        if ($this->config !== null) {\n            $host = $this->config->cliHost && !$this->isRunningInDocker()\n                ? $this->config->cliHost\n                : $this->config->host;\n            $port = $this->config->cliPort && !$this->isRunningInDocker()\n                ? $this->config->cliPort\n                : $this->config->port;\n            $database = $this->config->database;\n            $username = $this->config->username;\n            $password = $this->config->password;\n            $charset = $this->config->charset;\n            $poolSize = $this->config->poolSize;\n        } else {\n            $host = $this->resolveDbHost();\n            $port = $this->resolveDbPort();\n            $database = Environment::getEnvValue('DB_DATABASE', 'semitexa');\n            $username = Environment::getEnvValue('DB_USERNAME') ?? Environment::getEnvValue('DB_USER', 'root');\n            $password = Environment::getEnvValue('DB_PASSWORD', '');\n            $charset = Environment::getEnvValue('DB_CHARSET', 'utf8mb4');\n            $poolSize = (int) Environment::getEnvValue('DB_POOL_SIZE', '10');\n        }\n\n        $dsn = \"mysql:host={$host};port={$port};dbname={$database};charset={$charset}\";\n\n        $factory = static function () use ($dsn, $username, $password): \\PDO {\n            $pdo = new \\PDO($dsn, $username, $password, [\n                \\PDO::ATTR_ERRMODE => \\PDO::ERRMODE_EXCEPTION,\n                \\PDO::ATTR_DEFAULT_FETCH_MODE => \\PDO::FETCH_ASSOC,\n                \\PDO::ATTR_EMULATE_PREPARES => false,\n            ]);\n\n            return $pdo;\n        };\n\n        // Use ConnectionPool only inside a Swoole coroutine (e.g. request handler). CLI has no coroutine.\n        if (\n            class_exists(\\Swoole\\Coroutine\\Channel::class, false)\n            && class_exists(\\Swoole\\Coroutine::class, false)\n            && \\Swoole\\Coroutine::getCid() >= 0\n        ) {\n            return new ConnectionPool($poolSize, $factory);\n        }\n\n        return new SingleConnectionPool($factory);\n    }\n\n    /** When running on host (CLI), use DB_CLI_* so GUI/CLI connect to host port; inside Docker use DB_HOST/DB_PORT. */\n    private function resolveDbHost(): string\n    {\n        if (!$this->isRunningInDocker()) {\n            $cliHost = Environment::getEnvValue('DB_CLI_HOST');\n            if ($cliHost !== null && $cliHost !== '') {\n                return $cliHost;\n            }\n        }\n        return Environment::getEnvValue('DB_HOST', '127.0.0.1');\n    }\n\n    private function resolveDbPort(): string\n    {\n        if (!$this->isRunningInDocker()) {\n            $cliPort = Environment::getEnvValue('DB_CLI_PORT');\n            if ($cliPort !== null && $cliPort !== '') {\n                return $cliPort;\n            }\n        }\n        return Environment::getEnvValue('DB_PORT', '3306');\n    }\n\n    private function isRunningInDocker(): bool\n    {\n        return file_exists('/.dockerenv');\n    }\n\n    /**\n     * Resolve the database driver from environment configuration.\n     * Defaults to 'mysql' for backward compatibility.\n     */\n    private function resolveDriver(): string\n    {\n        $driverSource = $this->config !== null\n            ? $this->config->driver\n            : (Environment::getEnvValue('DB_DRIVER', 'mysql') ?? 'mysql');\n        $driver = strtolower($driverSource);\n\n        return match ($driver) {\n            'mysql', 'sqlite' => $driver,\n            default => throw new \\InvalidArgumentException(\n                \"Unsupported DB driver '{$driver}'. Expected 'mysql' or 'sqlite'.\",\n            ),\n        };\n    }\n\n    /**\n     * Create a SQLite adapter based on environment configuration.\n     *\n     * Supports:\n     * - DB_SQLITE_PATH: absolute or relative path to SQLite file\n     * - DB_SQLITE_MEMORY: if set to \"1\" or \"true\", use in-memory database\n     */\n    private function createSqliteAdapter(): SqliteAdapter\n    {\n        if ($this->config !== null) {\n            if ($this->config->sqliteMemory) {\n                return new SqliteAdapter('sqlite::memory:');\n            }\n\n            $path = $this->config->sqlitePath;\n            if ($path === null || $path === '') {\n                $path = ProjectRoot::get() . '/var/database/semitexa.sqlite';\n            }\n        } else {\n            $memory = Environment::getEnvValue('DB_SQLITE_MEMORY');\n            if (in_array(strtolower((string) $memory), ['1', 'true', 'yes'], true)) {\n                return new SqliteAdapter('sqlite::memory:');\n            }\n\n            $path = Environment::getEnvValue('DB_SQLITE_PATH');\n            if ($path === null || $path === '') {\n                $path = ProjectRoot::get() . '/var/database/semitexa.sqlite';\n            }\n        }\n\n        // Ensure directory exists\n        $dir = dirname($path);\n        if (!is_dir($dir)) {\n            mkdir($dir, 0755, true);\n        }\n\n        return new SqliteAdapter(\"sqlite:{$path}\");\n    }\n}\n"},"resultPreviewTemplate":"@project-layouts-semitexa-demo/components/previews/schema-sync-showcase.html.twig","resultPreviewData":{"painPoints":["Teams waste time writing empty or obvious migrations just to mirror what the code already says.","Column drops are dangerous when one careless migration can erase data immediately.","Ops often needs the actual SQL plan, not a hand-wavy promise that the framework will figure it out."],"stats":[{"value":"1","label":"command to compare code and DB"},{"value":"2","label":"phases before a destructive drop completes"},{"value":"2","label":"audit outputs written per sync run (.json + .sql)"}],"phases":[{"badge":"Phase 1","title":"Mark deprecated, do not drop yet","tone":"base","summary":"If a column disappears from code, the ORM first marks it deprecated instead of deleting it immediately.","chips":["safe operation","comment marker","review window"]},{"badge":"Phase 2","title":"Drop only when explicitly allowed","tone":"warning","summary":"A later sync can perform the actual DROP, and only when destructive operations are explicitly allowed.","chips":["DROP COLUMN","--allow-destructive","intentional action"]}],"snippets":[{"label":"Dry-run sync plan","code":"bin/semitexa orm:sync --dry-run\n\nSafe operations: 3\nDestructive operations: 1 (require --allow-destructive)"},{"label":"Export SQL plan","code":"bin/semitexa orm:sync --dry-run --output var/migrations/history/plan.sql"},{"label":"Audit files written after real sync","code":"var/migrations/history/2026-03-27_12-14-03.184_sync.json\nvar/migrations/history/2026-03-27_12-14-03.184_sync.sql"}]},"l2ContentTemplate":"@project-layouts-semitexa-demo/components/previews/schema-sync-rules.html.twig","l2ContentData":{"rules":["A missing column does not become an immediate DROP; the first pass only marks it deprecated.","Real destructive operations are separated in the execution plan and require explicit opt-in with --allow-destructive.","The executed plan is logged as both structured JSON and plain SQL for review, audit, and DevOps handoff.","If code and database already match, there is nothing to write and nothing to execute."]},"__page_document_html_iri":"/demo/data/schema-sync","__page_document_json_iri":"/demo/data/schema-sync?_format=json","__page_alternates":[{"type":"application/json","href":"/demo/data/schema-sync?_format=json"}]}