Search by

cluion / moduark

ningyungame

Laravel-native modular architecture toolkit.

Package info

github.com/cluion/moduark

pkg:composer/cluion/moduark

Statistics

Installs: 294

Dependents: 1

Suggesters: 1

Stars: 1

Open Issues: 0

v1.3.0 2026-09-11 13:01 UTC

README

Moduark is a Laravel-native modular architecture toolkit. It keeps Modules in a normal Laravel application while making their dependencies, lifecycle order, resources, and architecture boundaries executable and inspectable.

Stable status: 1.3.0 is the current stable release. Levels 0 through 2 are Stable, Level 3 remains Preview, and the zero-configuration default remains Level 1. This minor release adds Preview activation, package extraction, Composer package Modules, package-set export, and an opt-in native Laravel Maker bridge without promoting those surfaces to Stable.

Requirements

  • PHP 8.2 or later
  • Laravel 12 or 13
  • Composer 2.1 or later

Installation

Install the stable line from Packagist:

composer require cluion/moduark:^1.3

Review the upgrade guide and retain a rollback path before changing a production constraint.

Laravel package discovery registers Cluion\Moduark\ModuarkServiceProvider. Configuration publishing is optional because package defaults are merged even when config/moduark.php does not exist in the application.

When nwidart/laravel-modules is installed, Moduark keeps nwidart's module:* commands and config/modules.php untouched. A null moduark.path follows nwidart's configured Module root when that package is present and uses app/Modules otherwise; set a non-empty path to override auto-detection. Moduark discovers entry classes at either <Module>/<Module>Module.php or <Module>/app/<Module>Module.php. When Moduark and nwidart resolve the same Module root, nwidart's active Module set is authoritative, including after Laravel config caching: disabling a Module removes it from Moduark's registry, analysis, graphs, cache, providers, Capability bindings, and native resources; re-enabling it restores those surfaces. nwidart continues to own its conventional routes, views, translations, migrations, and direct commands, while resources explicitly declared by Module::resources() remain Moduark-owned. An explicit non-empty moduark.path remains independent when it does not resolve to nwidart's Module root. Publish Moduark settings independently with:

php artisan vendor:publish --tag=moduark-config

The 1.3.0 Preview activation commands persist standalone state in the configurable moduark.activation.path (moduark-modules.json by default), or update nwidart's configured file-activator status file when both packages share the same Module root. Custom nwidart activators remain dry-run-only unless they can provide the same atomic contract.

Aggregate diagnostics and every runtime surface consume only the committed active Module set. A targeted moduark:doctor <module> or moduark:resources <module> request can still report a known disabled Module as state: disabled, with no class, dependencies, or resources loaded; moduark:inspect <module> requires the Module to be active. Cold discovery and Module-cached boot retain the same boundary.

nwidart-generated Module classes must already be Composer-autoloadable. Follow nwidart's installation guidance by loading Modules/*/composer.json through its Composer merge plugin, or provide equivalent explicit per-Module PSR-4 mappings, then run composer dump-autoload.

For nwidart's default external Modules/ root, use nwidart's module:make and module:make-* commands to create Modules and their Laravel classes, and place the Moduark entry at Modules/<Name>/app/<Name>Module.php. Moduark's Maker commands target Modules inside Laravel's application source root; in particular, moduark:make intentionally rejects an external Module path. See Adopting Moduark for the complete setup.

See ADR-0047, ADR-0048, ADR-0059, ADR-0060, ADR-0071, and the upgrade guide for version-specific migration guidance.

The optional cluion/moduark-phpstan v0.2.0 companion supports the Moduark ^1.0 line, defaults to config/moduark.php, and understands both classic and nwidart Modules/*/app source roots. Install it as a development dependency:

composer require --dev cluion/moduark-phpstan:^0.2

See PHPStan and Larastan Integration. The companion extension covers only internal_api_access; moduark:check remains authoritative for the complete rule set.

Laravel Boost Agent Skill

The 1.0.0 release includes a Laravel Boost-compatible moduark-development Skill in this Composer package. Applications using Boost can run its installation flow after adding or updating Moduark:

php artisan boost:install

Boost discovers the packaged Skill and installs it for the coding agents chosen by the application. No separate Moduark Codex plugin is required. The installed Moduark CLI and package documentation remain authoritative; the Skill guides an agent through inventory, staged Level adoption, diagnostics, reviewed debt, and upgrade verification. See ADR-0044.

Stability and Versioning

The 1.x release line is Stable for Levels 0 through 2. Level 3 remains an opt-in Preview, and lifecycle internals such as capability resolver and cache objects are not application extension points. RC.2 validated the revised command and configuration namespaces in a real nwidart application before the same boundary was promoted to stable.

See Stability and Versioning for the PHP, configuration, CLI, diagnostic, and machine-schema contracts, and ADR-0045 for the boundary decision. The Level 3 promotion review remains a documented no-go for 1.0.0; see ADR-0046. Before changing versions, follow Upgrading Moduark so caches and application-owned architecture debt are reviewed rather than rewritten.

Quick Start

Create the smallest valid Module:

php artisan moduark:make-module User

This creates one file, app/Modules/User/UserModule.php:

<?php

declare(strict_types=1);

namespace App\Modules\User;

use Cluion\Moduark\Module;

final class UserModule extends Module
{
}

Choose an additive scaffold preset when the Module needs more than its entry class:

php artisan moduark:make-module Blog --preset=minimal
php artisan moduark:make-module Blog --preset=web
php artisan moduark:make-module Blog --preset=api
php artisan moduark:make-module Blog --preset=domain
php artisan moduark:make-module Blog --preset=full
php artisan moduark:make-module Blog --preset=full --dry-run
php artisan moduark:make-module Blog --preset=full --dry-run --format=json

web adds Module-owned routes, an invokable controller, view, English translations, and feature test. api adds routes, an invokable controller, request, resource, and feature test. domain adds tracked Domain/, Application/, and Infrastructure/ roots. full is their deterministic union. The command preflights every target before writing, never overwrites an existing target, and rolls back the complete scaffold when a write fails. --dry-run displays the same ordered plan without filesystem mutation. Presets do not run a package manager or install frontend dependencies.

Generate classes inside an existing Module through Moduark's single Maker entry point:

php artisan moduark:make User model Profile
php artisan moduark:make User controller ProfileController
php artisan moduark:make User controller ProfileController --invokable
php artisan moduark:make User controller ProfileController --resource --api
php artisan moduark:make User model Profile --dry-run
php artisan moduark:make User model Profile --dry-run --format=json
php artisan moduark:make User model Profile --factory --migration
php artisan moduark:make User class Support/InvokableTask --invokable
php artisan moduark:make User cast Money/AmountCast --inbound
php artisan moduark:make User channel Billing/InvoiceChannel
php artisan moduark:make User command SyncOrders --command=orders:sync
php artisan moduark:make User config billing/services
php artisan moduark:make User enum Workflow/Status --string
php artisan moduark:make User event Billing/InvoicePaid
php artisan moduark:make User exception Billing/PaymentFailed --render --report
php artisan moduark:make User factory Billing/InvoiceFactory --model=Profile
php artisan moduark:make User interface Lookup/UserLookup
php artisan moduark:make User job Billing/ProcessInvoice
php artisan moduark:make User job Billing/SyncInvoice --sync
php artisan moduark:make User job Billing/ReconcileInvoices --batched
php artisan moduark:make User job-middleware Billing/WithoutOverlappingInvoices
php artisan moduark:make User listener Billing/SendInvoiceReceipt --event=Billing/InvoicePaid --queued
php artisan moduark:make User mail Billing/InvoiceReceipt
php artisan moduark:make User middleware Admin/EnsureProfileIsComplete
php artisan moduark:make User notification Billing/InvoicePaid
php artisan moduark:make User migration CreateAuditLogsTable --create=audit_logs
php artisan moduark:make User migration AddStatusToProfilesTable --table=profiles
php artisan moduark:make User observer Audit/ProfileObserver
php artisan moduark:make User observer Profile/ProfileObserver --model=Profile
php artisan moduark:make User policy Admin/ManageProfiles
php artisan moduark:make User policy Profile/ProfilePolicy --model=Profile --guard=web
php artisan moduark:make User provider Billing/BillingServiceProvider
php artisan moduark:make User request Profile/StoreProfileRequest
php artisan moduark:make User resource Profile/ProfileResource
php artisan moduark:make User resource Profile/ProfileCollection --collection
php artisan moduark:make User resource Profile/ProfileJsonApiResource --json-api
php artisan moduark:make User rule Profile/ValidDisplayName
php artisan moduark:make User rule Profile/RequiredProfile --implicit
php artisan moduark:make User scope Visibility/PublishedScope
php artisan moduark:make User seeder Billing/ProfileSeeder
php artisan moduark:make User test Billing/InvoiceFeatureTest
php artisan moduark:make User test Billing/InvoiceUnitTest --unit
php artisan moduark:make User test Billing/InvoicePestTest --pest
php artisan moduark:make User job Billing/RebuildInvoiceIndex --test
php artisan moduark:make User trait Serialization/SerializesAttributes

Models are generated below Models/; controllers are generated below Http/Controllers/. Both types support --force. Controllers additionally support --invokable, --resource, and --api; --invokable cannot be combined with the resource or API modes. --dry-run resolves and validates the complete generation plan, including collisions, then displays each Module-relative target without writing files. With --force, an existing target is shown as OVERWRITE; otherwise it remains a collision.

Models additionally support --factory and --migration. These options plan a factory below Database/Factories/ and a create-table migration below Database/Migrations/, wire Model::factory() to the Module-owned factory, and commit the complete plan through one rollback-capable executor. Any preflight collision prevents every write. If a later write fails, newly created targets are removed and overwritten targets are restored; an incomplete rollback is reported as a tool error rather than claimed as atomic success.

The PHP type Makers delegate to Laravel's native stubs while fixing their Module ownership before execution. Generic classes use the name-relative Module path; casts use Casts/, enums use Enums/, exceptions use Exceptions/, interfaces use the Level 1 Public API convention Contracts/, scopes use Models/Scopes/, and traits use Concerns/. Classes support --invokable; casts support --inbound; enums support --int and --string; exceptions support --render and --report. All seven PHP types support nested names, --force, and --dry-run through the same plan and collision preflight.

Application/framework Makers complete the 31-name Laravel 12 / 13 inventory. Commands are direct classes below Console/Commands/, use Laravel's native stub, and accept --command=, --force, and Module-owned matching-test options. The Maker retains its 1.1 direct-class contract; hand-written nested commands can opt into recursive runtime discovery through resources(). Config files are template-backed targets below the Module's lowercase config/ tree; generating one does not write to the application's config/ directory, and runtime merge or publication remains an explicit resource declaration. Providers are template-backed below Providers/; generation never invokes Laravel's native provider Maker because that command mutates bootstrap/providers.php. Add generated providers explicitly to the Module's providers() metadata. See ADR-0055.

HTTP request and resource Makers also retain Laravel's native stubs while fixing ownership first. Requests use Http/Requests/; resources use Http/Resources/. Resources support standard JSON resources, --collection, and --json-api modes. The two specialized resource modes are mutually exclusive so one requested stub cannot silently override the other. Both types support nested names, --force, and --dry-run.

Middleware uses the Module-owned Http/Middleware/ path and Laravel's native stub. It supports nested names and --dry-run. Laravel's Middleware Maker does not expose --force, so Moduark rejects that option instead of emulating an overwrite. --test, --pest, and --phpunit add a Module-owned matching test to the same preflighted, rollback-safe plan.

Policies use the Module-owned Policies/ path and Laravel's native plain or model-aware stubs. A relative --model=Profile is intentionally resolved as the selected Module's Models\Profile; external fully qualified model names are rejected. --guard selects Laravel's application auth user provider and does not create another user or model. Policy generation remains a single-file plan and supports --force and --dry-run.

Validation rules use the Module-owned Rules/ path and Laravel's native ValidationRule stub. --implicit selects Laravel's implicit-rule variant; both modes remain single-file plans and support nested names, --force, and --dry-run without creating requests, policies, or other related artifacts.

Standalone factories and seeders stay below the selected Module's Database/Factories/ and Database/Seeders/ directories. They use Moduark-owned templates because Laravel's native commands hard-code the application-level database/ path. Factory names receive the conventional Factory suffix and infer a same-name Module model unless --model supplies a different Module-relative model. Neither Maker supports --force, matching its Laravel 12 / 13 native option contract, and neither changes a model or root DatabaseSeeder.

Observers use the Module-owned Observers/ path and Laravel's native plain or model-aware stub. A relative --model=Profile resolves only inside the selected Module's Models\Profile; external fully qualified model names are rejected. Observers support nested names, --force, and --dry-run, but do not create a model or register themselves with a provider or event listener.

Standalone migrations use the Module-owned Database/Migrations/ path and Moduark-owned copies of Laravel's plain, create-table, and update-table stubs. The StudlyCase input name is normalized to Laravel's snake_case timestamped filename. --create=table and --table=table select the corresponding stub; without either option, Laravel-compatible name patterns infer the mode or fall back to the plain stub. The two options are mutually exclusive. Standalone migrations reject --force, duplicate logical names, nested names, and invalid table identifiers, and never write to the application-level database/ tree.

Events use the Module-owned Events/ path and Laravel's native event stub. They support nested names, --force, and --dry-run through the shared single-target plan, and never create listeners or provider registrations.

Listeners use the Module-owned Listeners/ path and Laravel's native plain, typed, queued, or typed-queued stub. A relative --event=Billing/InvoicePaid is validated and qualified below the selected Module's Events/ namespace; external event classes are rejected. Listener generation supports --force and --dry-run, but never creates the referenced event or provider registration.

Jobs use the Module-owned Jobs/ path and Laravel's native queued, synchronous, or batched queued stub. The default is queued; --sync selects the synchronous stub and --batched selects the batch-aware queued stub. The two modes are mutually exclusive. Job generation supports --force and --dry-run, but does not create matching tests, queue infrastructure, or batch migrations.

Job middleware uses the Module-owned Jobs/Middleware/ path and Laravel's native middleware stub. It supports --force and --dry-run, but does not create jobs, matching tests, queue infrastructure, or registration.

Notifications use the Module-owned Notifications/ path and Laravel's native plain notification stub. They support --force and --dry-run. Laravel's --markdown mode also writes an application-global view below resources/views/, so Moduark explicitly rejects it and never creates a related view.

Mailables use the Module-owned Mail/ path and Laravel's native plain mail stub. They support --force and --dry-run. Laravel's --markdown and --view modes also write application-global views below resources/views/, so Moduark rejects both before generation.

Broadcast channels use the Module-owned Broadcasting/ path and Laravel's native channel stub. The generated join method references the application's configured authentication-provider model, but generation does not create that model, matching tests, routes, providers, or channel registration.

Blade components support class, inline, anonymous-view, and custom-view-path modes. Class components live below View/Components/; related Blade files live below the same Module's resources/views/ tree and use the lowercase Module view namespace. Default class-and-view generation is planned, preflighted, and rolled back as one atomic operation. --inline creates no view, while a value-less --view creates only an anonymous Blade view. --path= accepts only Module-relative lowercase kebab-case directory segments.

Standalone Blade views accept nested dot, slash, or backslash names and write a single deterministic target below the selected Module's resources/views/. Names normalize to lowercase kebab-case paths; --extension= defaults to blade.php and accepts only lowercase alphanumeric dot segments.

Verification targets live below each Module's fixed Tests/Feature/ or Tests/Unit/ root and use the Module namespace. test defaults to a feature test, --unit selects the unit root, and --pest / --phpunit select the runner syntax. When neither runner flag is explicit, an installed Pest application is detected using Laravel's native convention; explicit --phpunit takes precedence. Laravel Makers with matching-test support accept --test, --pest, or --phpunit and add the matching Module-owned feature test to the same preflighted, rollback-safe plan. No mode writes to the application-global tests/ tree.

The target Module must already exist and its configured path must be inside the Laravel application source root. Other composite Laravel Maker options that create controllers, requests, policies, or seeds remain deliberately unexposed until every generated file can retain Module ownership. Delegated Laravel Makers run non-interactively so framework prompts cannot create undeclared related artifacts. Moduark does not inject --module into Laravel or third-party make:* commands. See ADR-0032. The reviewed Laravel 12/13 Maker inventory and executable 1.1 registry boundary are recorded in ADR-0049. Composite ownership and rollback semantics are recorded in ADR-0050; they do not add a new top-level Maker type. Human-readable and JSON plan output share the same immutable plan. JSON schema version 1 reports planned, collisions_found, or incomplete, the compatible exit code, ordered Module-relative targets, generator IDs, create/overwrite operations, overwrite intent, and collision state. JSON is available only with --dry-run, so normal Laravel delegate output cannot corrupt the machine document. See ADR-0052.

Third-party packages may add a template-backed Maker by implementing GeneratorDescriptor and registering its class from a Laravel service provider:

use Cluion\Moduark\Generation\GeneratorRegistration;

public function register(): void
{
    GeneratorRegistration::register($this->app, ValueObjectGenerator::class);
}

The descriptor declares its canonical ID, target namespace, supported moduark:make options, and complete immutable plan. All targets must use GenerationFileTemplate and remain below the selected Module; the common planner owns JSON/text output, collision preflight, --force, execution, and rollback. Third-party Artisan delegation and direct filesystem writes are not part of this extension contract. See ADR-0053 and the permanent package fixture.

Inspect the discovered architecture:

php artisan moduark:list
php artisan moduark:check
php artisan moduark:check --format=json
php artisan moduark:check --format=github
php artisan moduark:graph
php artisan moduark:graph --format=mermaid
php artisan moduark:graph --view=capability
php artisan moduark:graph --view=capability --format=mermaid
php artisan moduark:graph --view=combined
php artisan moduark:inspect Order
php artisan moduark:cache

The default configuration uses Level 1, so a successful check evaluates six rules: Module structure, identity, missing and undeclared dependencies, cycles, and internal API access.

Module Metadata

Dependencies, service providers, owned tables, and explicit exports are typed PHP metadata on the Module entry class. Dependencies are registered before their consumers.

<?php

declare(strict_types=1);

namespace App\Modules\Order;

use App\Modules\Order\Contracts\OrderQuery;
use App\Modules\Order\Data\OrderData;
use App\Modules\Order\Events\OrderPlaced;
use App\Modules\Order\Providers\OrderServiceProvider;
use App\Modules\User\UserModule;
use Cluion\Moduark\Module;

final class OrderModule extends Module
{
    public function dependencies(): array
    {
        return [UserModule::class];
    }

    public function providers(): array
    {
        return [OrderServiceProvider::class];
    }

    public function tables(): array
    {
        return ['orders', 'order_items'];
    }

    public function exports(): array
    {
        return [OrderQuery::class, OrderData::class, OrderPlaced::class];
    }
}

Class metadata must contain concrete class strings. exports() accepts existing classes, interfaces, traits, and enums; ownership is verified by the Level 3 rule. tables() accepts unique, unquoted dot-separated names such as orders or audit.events; one canonical table can have only one Module owner, compared case-insensitively. Duplicate references, missing Modules, circular dependencies, and conflicting ownership fail deterministically.

Level 3 Database Ownership

At Level 3, database_ownership compares Laravel query evidence with the explicit Table Ownership Index. It reports:

  • MOD-TABLE-001 when a Module directly queries another Module's table;
  • MOD-TABLE-002 when a literal table has no declared owner;
  • warning MOD-TABLE-003 when the table expression cannot be resolved safely.

The AST collector recognizes imported or fully qualified DB::table(), Schema::table(), their connection()->table() forms, and table-bearing from() / join*() methods on fluent builders rooted in DB::table() or DB::query(). Common literal aliases such as users as u are matched to users; an explicit DB::table() inside a subquery is collected independently.

Raw SQL, Eloquent table inference, builders stored in variables, callback query parameters, unimported runtime Facade aliases, connection/schema mapping, and table prefixes are not guessed. Dynamic or unsupported expressions remain visible warnings rather than false ownership conclusions. See ADR-0037.

Level 3 Migration Ownership

migration_ownership requires recognized Laravel schema mutations to live below the declaring Module's Database/Migrations/ directory and to reference tables owned by that Module. It reports:

  • MOD-MIGRATION-001 for another Module's table;
  • MOD-MIGRATION-002 for an unowned literal table;
  • MOD-MIGRATION-003 for schema mutation code outside the migration directory;
  • warning MOD-MIGRATION-004 for a dynamic or unsupported table expression.

The analyzer recognizes imported or fully qualified Schema::create(), table(), rename(), drop(), and dropIfExists(), including their Schema::connection() forms. Both rename() operands must have explicit ownership. Keep historical renamed or dropped table names in tables() while shipped migrations still reference them, or record a narrow reviewed suppression for a deliberate orchestration migration.

Schema macros, custom wrappers, raw SQL schema statements, application-level migrations outside discovered Modules, connection/schema mapping, and table prefixes are not inferred. See ADR-0038.

Level 3 Cross-Module Foreign Keys

cross_module_foreign_keys audits extraction coupling between tables owned by different Modules. Its Level 3 default is a warning: a relational monolith may intentionally keep database integrity while accepting the migration coupling. It reports:

  • MOD-FK-001 for a resolved foreign key whose tables have different owners;
  • warning MOD-FK-002 when either table cannot be resolved safely;
  • MOD-FK-003 when either resolved table has no declared owner.

The analyzer recognizes foreign(...)->references(...)->on(...) and foreignId(), foreignUuid(), or foreignUlid() followed by constrained(...) on the first Blueprint callback parameter of recognized Schema::create() and Schema::table() calls, including connection variants. Laravel's conventional target-table inference is retained. Model-based foreignIdFor() targets, plus Laravel 13's foreignUuidFor() and foreignUlidFor() targets, stay unresolved unless the table is supplied explicitly because the model table is a runtime decision.

Custom Blueprint macros or wrappers, raw SQL, global migrations, callback data-flow, runtime model tables, connection/schema mapping, and prefixes are not inferred. Disable the rule when cross-Module FKs are the project-wide policy, or use a narrow reviewed suppression for an intentional exception. See ADR-0039.

Level 3 Cross-Module Transactions

cross_module_transactions audits direct Query Builder writes inside inline DB::transaction() and DB::connection()->transaction() callbacks. Its Level 3 default is a warning because an atomic cross-owner workflow can be deliberate in a modular monolith. It reports:

  • MOD-TRANSACTION-001 when one transaction directly mutates tables owned by multiple Modules;
  • warning MOD-TRANSACTION-002 when a direct write target cannot be resolved;
  • MOD-TRANSACTION-003 when a resolved write table has no declared owner.

The analyzer recognizes direct mutation chains rooted in DB::table() or DB::query()->from(), including connection variants, and Laravel Query Builder insert*, update*, upsert, increment/decrement, delete, and truncate methods. Raw DB::insert(), update(), delete(), and affectingStatement() remain unresolved warnings because Moduark does not parse SQL strings.

Repository or Port calls, Eloquent writes, builder variables, nested arbitrary callbacks, raw SQL target parsing, and manual beginTransaction() / commit() / rollBack() scopes are not inferred. Keep intentional atomic orchestration with a narrow reviewed suppression, or move cross-owner writes behind Module Ports. See ADR-0040.

Level 3 Explicit Public Exports

explicit_public_exports requires every cross-Module class-like reference other than the Module entry identity to appear in the provider's exports() metadata. It reports:

  • MOD-EXPORT-001 when a consumer references a symbol the provider does not explicitly export;
  • MOD-EXPORT-002 when an export is not found in indexed Module source;
  • MOD-EXPORT-003 when a Module attempts to export another Module's symbol.

Level 3 composes this rule with Level 1's Contracts/, Data/, Events/, and Module-entry convention. Explicit metadata narrows that convention: listing a Services/ class in exports() does not make it public while internal_api_access remains enabled. The Module entry class is always an implicit public identity and does not need to list itself.

The rule uses the existing AST symbol/reference index, so PHPDoc and dynamic class strings are not inferred. It validates visibility and ownership, not API backward compatibility. See ADR-0041.

Level 1 Public API

A declared dependency permits a relationship; it does not make every provider symbol public. The provider-owned public surface is convention-based:

  • the Module entry class;
  • named class-like symbols below Contracts/;
  • named class-like symbols below Data/;
  • named class-like symbols below Events/.

Directory names are exact and case-sensitive. Symbols below Actions/, Models/, Ports/, Services/, Support/, and every other directory are internal by default.

For example, Order may reference User\Contracts\UserFinder after declaring UserModule::class in dependencies(). A direct reference to User\Services\UserService produces MOD-BOUNDARY-001 even when the Module dependency is declared.

The analyzer resolves named PHP class-like references from attributes, types, inheritance, interfaces, traits, catch clauses, construction, static access, class constants, and instanceof. An unused use statement, PHPDoc, and dynamic string references are not treated as observed dependencies in the current release.

Laravel Resource Conventions

Existing paths are loaded through Laravel's native mechanisms; absent paths are ignored. Additive 1.2 resource behavior is opt-in through pure-data Module::resources() metadata.

Module-relative path Behavior
routes/web.php Loaded as a Laravel route file
routes/api.php Loaded as a Laravel route file
resources/views/ View namespace is the lowercase Module name
resources/lang/ Translation namespace is the lowercase Module name
Database/Migrations/ Loaded as application migrations
Console/Commands/*.php Concrete commands are registered in console runs

Module-specific service providers belong in providers() metadata. Moduark does not generate per-Module Composer packages or manifests.

An opt-in Module may declare runtime resources like this:

public function resources(): array
{
    return [
        'routes' => [
            ['path' => 'routes/admin.php', 'group' => ['prefix' => 'admin']],
        ],
        'config' => [
            ['path' => 'config/order.php', 'key' => 'order', 'publish' => true],
        ],
        'commands' => ['recursive' => true],
        'factories' => true,
        'seeders' => [OrderDatabaseSeeder::class],
        'policies' => [Order::class => OrderPolicy::class],
        'listeners' => [OrderPlaced::class => [SendReceipt::class]],
        'components' => true,
        'assets' => [
            'resources/js/order.js',
            ['path' => 'resources/public/icon.svg', 'type' => 'public', 'publish_to' => 'vendor/order/icon.svg'],
        ],
        'tests' => true,
        'extensions' => ['frontend' => ['driver' => 'vite']],
    ];
}

Metadata must contain only scalar, null, and nested array values. New resource types are never activated merely because a matching directory exists. Events are represented separately from listeners in the manifest; providers retain the dependency-ordered providers() lifecycle. Generic Vite inputs are available from ModuleAssetManifest::inputs(), and public assets can be published with php artisan vendor:publish --tag=moduark-assets.

Third-party packages can add a discover/handle pair by registering a ResourcePlugin through ResourcePluginRegistration::register() from their service provider. See ADR-0056.

Configuration

Publish the defaults only when the application needs to change them:

php artisan vendor:publish --tag=moduark-config
<?php

return [
    'path' => app_path('Modules'),

    'generation' => [
        // 1.3.0 Preview; false leaves every make:* command untouched.
        'native_bridge' => false,
    ],

    'architecture' => [
        'level' => 1,
        'baseline' => base_path('moduark-baseline.json'),
        'suppressions' => base_path('moduark-suppressions.json'),
        'rules' => [
            // 'internal_api_access' => false,
        ],
    ],
];

Rule overrides are booleans. Unlisted rules inherit the selected Level preset. Use overrides as documented exceptions: disabling a rule reduces that Level's guarantee, while enabling an unavailable rule makes the analysis incomplete.

--level changes one command run without changing configuration:

php artisan moduark:check --level=0
php artisan moduark:check --level=1

See Architecture Levels for the complete preset matrix and Adopting Moduark for a staged migration workflow.

Commands

Command Current contract
moduark:make-module {name} [--preset=minimal|web|api|domain|full] [--dry-run] [--format=text|json] Plan or create a deterministic, non-overwriting, rollback-safe Module scaffold; JSON is dry-run only and omitted preset remains minimal
moduark:make {module} {type} {name} [--dry-run] [--format=text|json] Plan or generate supported Module-owned artifacts and tests, with descriptor-specific options, atomic related targets, and dry-run JSON output
moduark:baseline [--level=0..3] [--force] [--prune] Adopt current violations explicitly or safely remove stale baseline debt
moduark:cache Cache deterministic Module discovery and typed metadata
moduark:clear Remove cached Module metadata and incremental source analysis
moduark:enable {module} [--dry-run] [--format=text|json] Validate and enable a Module, or preview the exact plan with --dry-run
moduark:disable {module} [--dry-run] [--format=text|json] Validate and disable a Module, or preview the exact plan with --dry-run
moduark:native-bridge [--format=text|json] Inspect the Preview native make:* --module bridge, its active decorators, and collision diagnostics
moduark:list List discovered Modules in deterministic order
moduark:check [--level=0..3] [--format=text|json|github] [--show-suppressions] Run the effective architecture rules, audit suppressions, and optionally emit JSON or GitHub Actions annotations
moduark:graph [module] [--view=module|capability|combined] [--format=text|mermaid] Render direct, Capability, or combined relationships and optionally select one neighborhood
moduark:inspect {module} Inspect one Module's identity, dependencies, providers, Capabilities, owned tables, and Public API convention
moduark:resources [module] [--format=text|json] Inspect the canonical enabled resource manifest and deterministic collisions
moduark:doctor [module] [--extractable] [--format=text|json] Diagnose runtime health or whether one active Module can enter export dry-run planning
moduark:migrate {module} [--format=text|json] Run only the selected active Module's forward migrations
moduark:seed {module} [--format=text|json] Run only seeders declared by the selected active Module
moduark:test {module} [arguments...] [--runner=auto|phpunit|pest] [--list] [--format=text|json] Run or list the selected active Module's declared test paths

The activation commands are a 1.3.0 Preview surface. --dry-run reports the authoritative standalone or nwidart driver and complete proposed plan without mutation. Without --dry-run, an executable non-no-op plan clears Module metadata, source-analysis, route, and event caches before atomically committing the authoritative file state. The running process is never hot-switched; start a new application process to consume the committed set.

The 1.3.0 Preview native bridge inspects the 31 Module-owned Laravel Maker candidates, their concrete command owner, required name argument, and any existing --module option. The default moduark.generation.native_bridge=false leaves every make:* definition untouched. When enabled, all 31 reviewed commands must be safe before any are decorated. A native call without --module delegates to the original Laravel command; a call with --module reuses moduark:make and its Generator Registry, Generation Plan, collision preflight, and rollback. Explicit native options outside the reviewed Moduark mapping fail with exit 2 before generation. JSON schema version 2 reports disabled, planned, active, or blocked, the active count, mutation state, and stable ownership, signature, option, registration, and decoration-drift diagnostics.

moduark:check exit codes are part of the Stable 1.x contract:

Exit Meaning
0 No blocking violation; warnings may exist
1 One or more blocking architecture violations
2 Command input, analyzer, or unavailable-rule tool error; result is incomplete

Use JSON when another tool needs the complete result without parsing terminal formatting. This option is included in v0.3.0-beta.1:

php artisan moduark:check --format=json
php artisan moduark:check --level=2 --format=json

Schema version 1 includes status, complete, exit_code, effective architecture and rule configuration, summary counts, unavailable rules, per-rule violations, additive suppression and baseline audit metadata, and an error object for failures that occur before a report is produced. Status is passed, violations_found, or incomplete; the exit codes remain exactly the same as text output. See ADR-0028.

Use GitHub output in an Actions workflow to attach each violation to its source file and line while preserving the same exit-code contract:

- name: Check Module architecture
  run: php artisan moduark:check --format=github

Errors and warnings become workflow annotations; a clean run emits one notice. Incomplete analysis and command failures remain errors with exit code 2. See ADR-0029.

Architecture Suppressions

Use a suppression only for one reviewed exception that cannot be fixed yet. The default moduark-suppressions.json is repository-visible and requires a stable rule, diagnostic code, narrow scope, and non-empty reason:

{
    "schema_version": 1,
    "suppressions": [
        {
            "rule": "internal_api_access",
            "code": "MOD-BOUNDARY-001",
            "file": "app/Modules/Order/Actions/CreateOrder.php",
            "line": 17,
            "reason": "Legacy integration tracked by ADR-012."
        }
    ]
}

The scope may select a repository-relative file and optional line, a symbol, or a consumer plus target Module pair. Selectors can be combined for a narrower match. Global ignores, absolute paths, missing reasons, unknown fields, duplicate selectors, and overlapping matches are tool errors.

Normal text output summarizes suppression debt. Audit every entry and its reason with:

php artisan moduark:check --show-suppressions

An entry is matched, stale when its evaluated rule no longer produces a match, or inactive when that rule was not evaluated at the selected Level. JSON always includes the structured audit, and GitHub output emits a summary notice. Suppressions are applied before the architecture baseline, so baseline creation and pruning never duplicate an explicitly suppressed violation. See ADR-0034.

Architecture Baseline

For a brownfield application, first review the unsuppressed violations, then create one repository-visible baseline:

php artisan moduark:check --level=1
php artisan moduark:baseline --level=1
git add moduark-baseline.json

Normal moduark:check runs automatically apply the configured baseline. The identity excludes diagnostic wording and line number, but retains rule, code, severity, file, Module endpoints, and symbol. If the number of matching current violations grows beyond the recorded count, the whole group is reported so a new occurrence cannot be guessed away.

Routine cleanup is one-way and cannot adopt new debt:

php artisan moduark:baseline --prune

The command refuses to overwrite an existing baseline by default. Use --force only after reviewing the complete raw result because replacement can adopt regressions. Text, JSON, and GitHub output report matched, stale, and exceeded counts. See ADR-0031.

Starting with the 0.5.x hardening, undeclared dependencies are reported once per ordered consumer / provider Module pair. Review and prune stale per-symbol MOD-DEPENDENCY-002 baseline entries after upgrading; new entries use stable pair identity. Migrate file- or symbol-only suppressions to explicit consumer and target selectors, and do not carry old amplified counts forward. See ADR-0043.

The graph command defaults to direct Module dependencies. The Capability view renders typed requires and provides edges:

php artisan moduark:graph --view=capability
php artisan moduark:graph Order --view=capability
php artisan moduark:graph --view=capability --format=mermaid
php artisan moduark:graph --view=combined
php artisan moduark:graph Order --view=combined --format=mermaid

Selecting a Module in the Capability view retains its connected Capabilities, providers, and other consumers so the relationship remains complete. The combined view overlays labeled depends, requires, and provides edges and uses the union of direct and Capability neighborhoods. JSON graph output remains later work. These views are included in v0.2.0-beta.2. moduark:check JSON and GitHub Actions annotations are included in v0.3.0-beta.1; inline suppressions are intentionally replaced by the reviewable external suppression manifest. Per-Module check filtering remains later work.

Use moduark:inspect Order when one Module needs more detail than the graph. It shows the effective architecture level, discovered or missing direct dependencies, Module ServiceProviders, each required Capability's resolved provider, consumer Port and Adapter, provided Capabilities, explicit owned tables, explicit exports, and symbols exposed by the current Contracts/, Data/, Events/, and Module-entry convention. The two Public API views remain separate so Level 3 narrowing is directly reviewable.

Preview extractability diagnostics are read-only:

php artisan moduark:doctor Order --extractable
php artisan moduark:doctor Order --extractable --format=json

The report checks the supported standalone or nwidart source layout, entry-class autoload identity, provider and file-resource ownership, and declared metadata classes outside every active Module or Composer vendor tree. Exit 0 and ready_for_export_dry_run mean only that the Module can enter the future export planning phase; they do not prove Composer dependencies, package Testbench installation, or independent test execution. Blockers use exit 1; invalid or inactive Modules use exit 2. See ADR-0061.

For nwidart layouts, provider classes remain owned by the app/ source root, while routes, migrations, views, and other file-backed resources may live in the full Module root beside app/.

The same report also runs raw Level 3 evidence for undeclared dependencies, Capability completeness, table ownership, cross-Module foreign keys and transactions, and explicit public exports. Baselines and suppressions do not hide extraction blockers; warnings block planning too. Evidence involving the selected Module as either consumer or target is included. A disabled or unavailable required rule is reported as not evaluated and blocks the result. See ADR-0062.

Five additional runtime-portability checks reuse the active resource manifest to verify built-in plugin contracts, Module-scoped config / view / translation / component namespaces, manifest collisions, asset inputs, and safe config or public-asset publish targets. Declared providers are parsed without execution; application-global classes, unscoped string keys, dynamic or contextual container bindings block export planning. Unknown resource plugins also block until their portable package contract is explicit. This does not execute publishing, inspect destination files, or infer Composer / frontend dependencies. See ADR-0063.

When all extractability checks pass, build the Preview package plan explicitly:

php artisan moduark:export User --dry-run \
    --target=packages/user-module \
    --package=acme/user-module \
    --namespace='Acme\UserModule'
php artisan moduark:export User --dry-run \
    --target=packages/user-module \
    --package=acme/user-module \
    --namespace='Acme\UserModule' \
    --format=json

The command maps standalone or nwidart files to package roots, plans generated Composer and package-provider targets, records namespace rewrites and runtime or manual dependencies, and checks destination collisions. Package identity is never guessed. Module dependencies without an explicit Composer mapping block readiness. Resolve a known dependency with a repeatable, reviewed mapping that includes its Composer constraint and exported namespace:

php artisan moduark:export Order --dry-run \
    --target=packages/order-module \
    --package=acme/order-module \
    --namespace='Acme\OrderModule' \
    --dependency='User=acme/user-module:^1.0=>Acme\UserModule'

The mapping must name a declared active Module. Moduark writes the requirement to generated Composer metadata and rewrites references from the application Module namespace to the dependency package namespace; it never infers a package, constraint, or namespace from PHP imports. Export plan schema version 2 adds the nullable dependency namespace field.

Plan a complete set without writing package files:

php artisan moduark:export-set \
    --package='User=acme/user-module:^1.0=>Acme\UserModule' \
    --package='Order=acme/order-module:^1.0=>Acme\OrderModule' \
    --target='User=packages/user-module' \
    --target='Order=packages/order-module' \
    --format=json

Without --materialize, moduark:export-set is read-only. It requires every declared Module dependency to be selected, emits a dependency-first canonical order, embeds each schema version 2 package plan in package-set schema version 1, and blocks overlapping targets. Package, target, or CLI input order does not affect output.

After reviewing a ready plan, explicitly materialize the complete set:

php artisan moduark:export-set \
    --package='User=acme/user-module:^1.0=>Acme\UserModule' \
    --package='Order=acme/order-module:^1.0=>Acme\OrderModule' \
    --target='User=packages/user-module' \
    --target='Order=packages/order-module' \
    --materialize \
    --format=json

Moduark prepares every package before publishing any target, rechecks all target collisions, then publishes in dependency order. A later publish failure rolls back targets created by that attempt; JSON reports any target or cleanup path that remains. This is failure-atomic recovery, not a multi-directory filesystem transaction: concurrent readers may briefly observe a published prefix, and an interrupted process has no durable recovery journal. The command does not install or publish Composer packages. See ADR-0069 and ADR-0070.

Omit --dry-run from the single-package command to materialize the same validated plan:

php artisan moduark:export User \
    --target=packages/user-module \
    --package=acme/user-module \
    --namespace='Acme\UserModule'

The target must not exist. Moduark writes every target into a same-filesystem staging directory, parses generated PHP, and publishes the complete package with one rename; it never merges or overwrites a package. The generated Composer metadata uses the conservative proprietary license placeholder so strict validation passes without assigning the application's code an unrequested open-source license; replace it with the real package license before publication. It auto-discovers a portable package ServiceProvider that registers the exported Module providers, Capabilities, config, routes, views, translations, migrations, commands, policies, listeners, components, and public assets without requiring an application Module directory. When the package contains a Moduark descriptor, its Module joins the host application's canonical active registry. See ADR-0064, ADR-0065, and ADR-0067. Cross-package dependency mappings are specified by ADR-0068, and dependency-closed package-set planning is specified by ADR-0069; opt-in set materialization and rollback evidence are specified by ADR-0070.

Exported Composer metadata also contains schema-versioned extra.moduark descriptors with the package-relative Module class source. Moduark reads those descriptors directly from Composer's installed-package manifest, validates the autoloaded class against the installed source, sorts a deterministic catalog, and rejects duplicate package Module names or classes. The catalog and its stable fingerprint are the LC1-F-A discovery contract. The host merges its application or nwidart active Modules with the installed package catalog before registry, analysis, graph, cache, lifecycle, Capability, and resource work. Package Modules are immutable-active while installed: moduark:enable and moduark:disable reject them, and Composer install or remove changes the set. Cache schema version 6 includes the package fingerprint, and descriptor-aware portable providers delegate to this canonical runtime to avoid duplicate provider or resource registration. See ADR-0066 and ADR-0067.

Application bootstrap happens before Artisan invokes a command. A configuration, discovery, metadata, or runtime Capability-resolution exception raised during bootstrap may therefore be rendered by Laravel itself rather than by moduark:check's exit-code renderer.

Module Cache

For deployment, cache Module discovery and typed metadata directly or through Laravel's optimization command:

php artisan moduark:cache
# or
php artisan optimize

The versioned scalar PHP manifest is stored at bootstrap/cache/moduark.php. It contains the configured Module root, sorted discovery records, dependency-ordered descriptors, and schema-versioned runtime resource manifest. Runtime lifecycle, Capability validation, graphs, inspection, checks, operations, and resource handlers reuse the same enabled Module set. Cached boot consumes the serialized descriptors without repeating filesystem resource discovery.

Rebuild the cache after adding, removing, or moving a Module, or after changing dependencies(), providers(), requires(), provides(), tables(), exports(), or resources(). Clear it to return to fresh discovery:

php artisan moduark:clear
# or
php artisan optimize:clear

An unknown cache schema, a manifest for another configured Module root, or a manifest for another nwidart active Module set is ignored safely. A malformed current-schema manifest fails with its exact cache path instead of silently booting from ambiguous metadata. See ADR-0030. This integration is included in v0.3.0-beta.2.

Incremental Source Analysis

When an enabled rule needs the PHP source index, moduark:check stores an internal per-file analysis manifest at bootstrap/cache/moduark-analysis.php. Every run still reads each current Module PHP file and computes its SHA-256 content hash. Only entries with the same content hash, Module owner, and cache schema reuse their symbol, unresolved class-reference, query table-access, schema mutation, foreign-key, and inline transaction summaries; global symbol and table ownership are resolved again on every check.

Changed files are parsed again, removed files are pruned, and a moved file is a new cache entry. An unknown, malformed, or semantically invalid cache falls back to a complete cold analysis. A failed analysis never replaces the previous manifest, and an unwritable cache cannot turn a complete fresh result into a tool error. moduark:clear and optimize:clear remove both the Module metadata cache and this source-analysis cache. moduark:cache intentionally does not pre-parse application source; the first source-enabled check creates the incremental manifest. See ADR-0033.

Development

composer verify
composer test:dependencies
composer test:lowest
composer test:distribution
composer test:installation
composer test:installation -- --boost
composer benchmark
composer benchmark:generation
composer test:performance

composer verify runs PHPUnit and PHPStan level max. The generated performance baseline exercises cold and content-hash-cached checks over 50 Modules / 5,000 PHP files and 100 Modules / 10,000 PHP files without checking generated fixtures into Git. See ADR-0012 for the method and initial evidence, and ADR-0033 for the incremental comparison.

composer benchmark:generation builds 100 disposable full scaffold Modules and measures 1,400 real production-template targets through planning, collision preflight, and execution. It reports evidence without enforcing a portable SLA. composer test:performance runs the same fixture with a 5,000 ms median-total budget in the fixed PHP 8.5 Ubuntu CI job. The deliberately generous threshold blocks major regressions while tolerating shared-runner and filesystem variance; it is not a cross-machine performance promise. See ADR-0054.

The Level 2 acceptance fixture models eight business Modules, five shared Capabilities, and twelve consumer-owned Port/Adapter bindings. It proves all eight Level 2 rules, runtime container composition, combined graph output, and moduark:inspect against one connected architecture. See ADR-0026.

composer test:distribution builds the repository's Git archive and verifies that runtime source, configuration, stubs, license, and public documentation are present while tests/, benchmarks/, workbench/, repository automation, and development-only analysis files are absent. See ADR-0027.

composer test:dependencies resolves the Laravel 12/13 lowest/highest matrix in disposable Composer projects. It simulates the supported PHP floors for dependency solving, leaves Composer's security blocking enabled, and reports the exact framework, Testbench, and PHPUnit versions selected. It does not replace executing the test suite on those PHP runtimes.

composer test:lowest copies the current checkout into an isolated disposable project, resolves the Laravel 12 dependency floor, installs that graph, and runs the Architecture, Unit, and Feature PHPUnit suites. Composer simulates PHP 8.2 for dependency selection; the summary reports the actual local PHP runtime, so this gate complements rather than replaces the blocking PHP 8.2 CI job. The process-based GenerationBenchmarkTest is excluded because newer host PHP deprecations from the floor graph can contaminate its JSON subprocess; the separate composer test:performance gate remains authoritative for that path.

composer test:installation is the slower, networked acceptance matrix. It creates disposable Laravel 12 and 13 applications, installs this checkout through a Composer path repository, and exercises package discovery and the core commands without publishing configuration. It is intentionally separate from the default offline-friendly verification command. See ADR-0013 for the matrix contract and initial resolved versions.

Add --boost to install Laravel Boost in the same disposable applications, select Codex and cluion/moduark through a deterministic boost.json, and run the skills-only installation twice. This gate verifies that the complete moduark-development Skill is copied to .agents/skills/ without drift and that repeated installation is idempotent.

After a version is published, pass an exact version to repeat the same Laravel acceptance against the Packagist dist instead of the local path repository. This mode also verifies the installed archive layout:

composer test:installation -- --package=1.3.0

The GitHub Actions compatibility workflow runs PHPUnit on all four Laravel/PHP/dependency combinations and runs the matching clean installation on both highest-dependency jobs. A separate PHP 8.2 job runs PHPStan against the highest resolvable tooling dependencies. See ADR-0014 for the release-gate contract.

Documentation

Current Scope

The 1.3.0 stable release adds Preview dependency- and Capability-aware Module activation, extractability diagnostics, standalone package and dependency-closed package-set export, Composer-installed package Modules, and an opt-in native make:* --module bridge. These surfaces share the canonical active Module set, Generation Plan, resource manifest, graph, cache, provider, and Capability contracts. They remain Preview in 1.3.0 and do not broaden the Stable Level 0 through 2 contract.

The 1.2.0 minor release completes Runtime Completeness with a serializable Resource Plugin manifest shared by cold discovery, cached boot, diagnostics, runtime registration, generic assets, and Module-scoped test/migrate/seed operations. Laravel resources remain metadata opt-in, database operations are forward-only, and nwidart's enabled Module set remains authoritative whenever both packages share its Module root.

The 1.1.0 minor release retains the Stable command, configuration, diagnostic, and architecture boundaries while completing the Generation Foundation. Thirty-one Module-owned Maker types, additive scaffold presets, immutable Generation Plans, shared collision preflight, atomic rollback, text/JSON dry runs, Laravel 12 / 13 parity fixtures, and the Stable template-backed third-party registration API now form one verified generation surface. Config and provider Makers create Module-owned artifacts without silently activating runtime resources or mutating application bootstrap state. Levels 0 through 2 remain Stable and the complete Level 3 preset remains Preview. Level 2 includes typed Capability metadata, descriptor-only provider resolution, lifecycle preflight, consumer-owned Port wiring, Capability contract validation, source-enforced Adapter boundaries, deterministic Capability and combined graphs, moduark:inspect, and the large Level 2 acceptance fixture. Developer Experience output includes versioned JSON reports, GitHub Actions annotations, and deterministic Module metadata caching with Laravel optimize integration. Brownfield adoption includes a reviewable architecture baseline with conservative count matching and safe pruning. Reviewed architecture exceptions use an auditable external suppression manifest with narrow selectors, mandatory reasons, and stale/inactive reporting. Content-hash caching reuses unchanged per-file source analysis without persisting cross-file ownership decisions. All six Level 3 rules audit direct cross-Module Eloquent Model, table, migration, foreign-key, inline transaction, and explicit export access. Explicit tables() metadata feeds a deterministic single-owner index; Laravel-aware AST evidence covers literal Facade queries, Schema mutations, Blueprint constraints, and direct Query Builder writes inside transaction callbacks, while unresolved expressions remain reviewable warnings. Explicit exports() metadata narrows the convention-based Public API. The complete fourteen-rule Level 3 preset can now produce a complete pass. The optional cluion/moduark-phpstan v0.2.0 stable companion integrates internal_api_access with PHPStan and Larastan across the Moduark 1.x line and nwidart-compatible source layouts; suppression expiry and extension coverage for the remaining rules remain later work. See ADR-0035, ADR-0036, ADR-0037, ADR-0038, ADR-0039, ADR-0040, and ADR-0041. The optional integration boundary is defined by ADR-0042. The final v0.5.0-beta.1 release adopts two complete existing Laravel applications as static corpora: 1,511 PHP files produced 807 table accesses, 413 Schema mutations, and 156 foreign-key references. Independent token-based oracles reached zero resolved-line misses, zero table-evidence anchoring collisions, and complete recall across 1,077 literal Facade and fluent table operations after hardening. Command discovery now permits co-located interfaces, traits, enums, and abstract classes, query evidence points at the literal table argument, and undeclared dependencies report once per ordered Module pair. See ADR-0043.

Moduark is open-source software licensed under the MIT License.