Skip to content
Troubleshooting

Fix boundary violations

Resolve a rejected Core or cross-App import by publishing the smallest neutral SDK contract instead of adding an exception.

Package boundary violations

This reference is for Nexia maintainers who already have authorized access to the private host environment. Core is not distributed to external developers. These host commands are not prerequisites for the public CLI and cloud sandbox; start with the quickstart.

The coupling ratchet rejects a reference from an App Package into the platform or another App. A clean App has no baseline file; if a pre-existing exception remains, it permits only that App's recorded file-and-symbol pair in .nexia/coupling-baseline.json.

First find an existing capability in SDK contracts. Replace the rejected import with that public contract. If the behavior belongs to the App, keep it there; only a missing platform responsibility warrants a new SDK surface.

Symptom index

SymptomSection
Ratchet rejects a new file-and-symbol pairA PHP import into App*
Module not found: @/…, @shell/…, @shared/uiA frontend platform alias import
An import of another App's classAn App-to-App import
A local SDK change has no runtime effectThe local SDK is not selected
Target [Nexia\…] is not instantiable.The contract exists but is unbound
The capability I need has no SDK contractNo contract exists yet
A baseline file seems to prove the pattern is fineBaseline is not precedent

A PHP import into App*

New PHP app-to-core coupling detected.

Cause. An App Package imported a platform implementation class. PHP App code may import host-facing contracts only from Nexia\*.

Fix. Find the published contract for the capability:

You reached forImport instead
A platform authorization serviceNexia\Laravel\Access\Contracts\ResourceAuthorization
Actor or Party lookupNexia\Identity\Contracts\ActorDirectory, Nexia\Laravel\Identity\Contracts\PartyDirectory
Legal Entity or Operating Unit lookupNexia\Organization\Contracts\OrganizationDirectory
Tenant context restorationNexia\Tenancy\Contracts\TenantRunner
Event publicationNexia\Events\Contracts\Outbox
Command Palette commandsNexia\Palette\Contracts\PaletteCommandContribution
Searchable Resource recordsPublish the Resource model and mark fields searchable: true in resourceListFields(); Core owns the governed Search allowlist and authorization path
A host identity or organization referenceActorDirectory, PartyDirectory, or OrganizationDirectory

Resolve from the container so the platform supplies the implementation:

Code example
PHP
use Nexia\Laravel\Access\Contracts\ResourceAuthorization;

// $query is the generated Note model query inside the authenticated tenant request.
$query = app(ResourceAuthorization::class)->scopeVisible($query, 'workshop.note');

Confirm. The ratchet passes:

Code example
Shell
php scripts/coupling-ratchet.php check quality

The doctor does not check coupling — it covers discovery, registration, permissions, frontend entry, and translations.

Prevent. Check the namespace map in SDK contracts before writing an import.

A frontend platform alias import

Module not found: Can't resolve '@shell-primitives/resources/list'

Cause. App frontend code resolved an import into the Core source folder. Forbidden prefixes: @/, @core/, @hooks/, @lib/, @shared/, @shell/, @shell-primitives/.

Fix. Import the same symbols from the SDK:

Code example
TypeScript
import {
    ResourceTable,
    ResourcePrimaryCell,
    NxButton,
    api,
    can,
    useResourceListParams,
} from "@nexia/sdk/host";

The SDK exports the layout primitives, form controls, table components, data hooks, inspector surfaces, and the Approval composer registry. The commonly used ones: React components and hooks.

Confirm. The ratchet is what rejects a platform alias — the dependency validator skips local and alias specifiers and never sees them:

Code example
Shell
php scripts/coupling-ratchet.php check quality

Prevent. Follow Build an App screen for supported imports. A historical baseline entry permits only its recorded file-and-symbol pair; it is not evidence that current generators should emit Core aliases.

An App-to-App import

An App Package imports a class or frontend module from another App Package.

Cause. An App imported another App's class, or declared it as a package dependency. App-to-App imports and package dependencies are forbidden, with no exception.

Fix. Choose by what you actually need:

NeedMechanism
Read another App's record nowNexia\ResourceReference — store the Resource Key and public_id, let Core resolve it
React to another App's changeNexia\Events — the platform invokes the consumer inside the tenant; only delegated listeners restore the user and recheck authority
Share a capabilityA new app-neutral SDK contract, bound by the platform

A ResourceRef names a record without importing its model, so the owning App keeps control of exposure and the actor's authority is applied on resolution.

Confirm. The ratchet passes and no packages/* requirement names another App.

Prevent. Do not create a competing source of truth. Use authorized Resource References for live values; keep historical snapshots or event projections only when the owner contract explicitly supports that use.

The local SDK is not selected

A local SDK edit has no effect after the host reloads.

Cause. A checkout may exist without being in the persisted local package selection. The host then uses the release package declared by its committed lock.

Fix. From the Core root, add the SDK source through the supported helper:

Code example
Shell
task apps:add PACKAGES=app-sdk
task apps:status -- --offline

The add task prepares the Composer overlay, frontend local lock, SDK package metadata, and frontend dependency validation. App manifests and imports use the published amuzcorp/nexia-app-sdk-laravel and @nexia/sdk identities.

Confirm. task apps:status -- --offline lists app-sdk, and git -C packages/app-sdk rev-parse --show-toplevel returns the independent SDK repository.

Prevent. Change a persisted selection only with task apps:add or task apps:remove. Runtime tooling resolves installed package paths through Composer\InstalledVersions::getInstallPath() and the generated App map.

The contract exists but is unbound

Target [Nexia\Laravel\Access\Contracts\ResourceAuthorization] is not instantiable.

Cause. The SDK publishes the interface, but the platform has no binding for it in this context. The SDK carries no host-owned implementation by design — an unbound interface means the platform binding is missing, not that the SDK is incomplete.

Fix. This is a host problem, not an App problem. The platform binding is missing or not registered for the current context. Do not work around it by instantiating a platform class.

Confirm. The contract resolves from the container in a normal request.

Prevent. Follow the contract order: publish the SDK surface, make its commit available, bind it in Core, then consume it. Skipping the binding step produces exactly this error.

No contract exists yet

The App needs host behavior, but no app-neutral Nexia\* contract exposes it.

Cause. Some capabilities are genuinely host-only today:

CapabilityHost-only class
Site ConfigurationApp\Settings\SiteConfigRegistry
Installation initializersApp\AppRuntime\TenantAppInitializerContribution

Global record search is not a per-App Palette contribution. Publish a Resource model with searchable fields in resourceListFields(); Core's governed Search runtime decides whether the Resource is eligible, obtains candidate identifiers, and reloads every result through tenant-database visibility and Policy checks. Do not invent a Palette-specific query contract.

Fix. Either model the need inside your App, or add the contract:

SituationApproach
App configurationModel it as an App Resource behind a settings destination
Required rows at installShip them in a guarded tenant migration
Genuinely platform-widePublish the smallest app-neutral SDK contract and bind it in the platform

When adding a contract, keep it minimal. Prefer a one-method interface over exposing a platform Eloquent model — a model leaks schema and turns every internal change into a breaking one. Never copy a platform implementation into the SDK.

Confirm. Your App imports only Nexia\* and the ratchet passes.

Prevent. Check availability before designing around a capability. The Extension Model lists what an App Package may and may not implement. Governed Search Runtime explains the separate record-search path.

Baseline is not precedent

The coupling ratchet rejects a new file-and-symbol pair beside an existing exception.

Cause. Reading .nexia/coupling-baseline.json as a list of approved patterns. It is an explicit record of an exception that still needs removal, not precedent.

Fix. Find an existing public contract first. If none fits, decide whether the behavior belongs in the App or expresses a durable platform responsibility. A baseline entry alone does not justify adding an SDK API. See SDK contracts.

Confirm. Your new code imports only Nexia\* and @nexia/sdk, and the baseline did not grow.

Prevent. Run all three before committing. Only the first covers the import boundary; the other two cover compatibility declarations and the dependency graph:

Code example
Shell
php scripts/coupling-ratchet.php check quality
task artisan -- nexia-apps:validate-app-compat --app=quality --require-declarations
task artisan -- nexia-apps:validate-app-frontend-dependencies --app=quality --require-manifests
Source of truth: docs/developers/content/en/troubleshooting/package-boundary-violations.md