Fix contribution discovery
Diagnose a contributor that exists in source but never reaches the Shell, the permission catalog, or the runtime registry.
Fix contribution discovery
Your class is written and the file is saved, but the navigation entry, the permission, the descriptor, or the surface is not there. Something in the discovery chain dropped it.
Start with the package doctor:
Commands below use quality as the example App key; substitute your App. Set APP_TENANT_ID to the tenant ID you are diagnosing. --tenants limits installation and repair to that tenant.
task artisan -- nexia-apps:doctor-package-app qualityThe doctor prints one OK, WARN, or FAIL row per check. Find your symptom
below by its row, and fix the owning package declaration.
Ordinary requests read the generated contribution manifest. They do not walk package source directories, so a correct source edit can remain invisible until that manifest is rebuilt.
Never fix discovery by adding a host-side fallback. A contribution that only works because the platform special-cases it is not discovered — it is hard-coded, and it will break the next App.
Symptom index
| Doctor row | Section |
|---|---|
runtime app registration — AppRegistry cannot see {app_key} | App is invisible to the runtime |
runtime app metadata — registered manifest metadata differs from composer.json | Manifest and composer.json disagree |
contribution locations — no package contribution location is registered | Contribution directory is not declared |
permission contributors / navigation contributors — 0 classes | Class is not recognized as a contributor |
permissions discovered — 0 permission definitions | Class is not recognized as a contributor |
| Source changed, but the old contribution remains | Generated manifest is stale |
| Fresh CLI output is correct, but the browser still shows the old contribution | Octane worker still serves the old contribution |
frontend component registration — index.ts does not call … | Frontend surface is never registered |
shell component pairing — missing index.ts registrations: … | Frontend surface is never registered |
translation catalogs — missing or invalid | Labels render as raw keys |
All rows OK, menu entry still absent | Everything is discovered but nothing is visible |
App is invisible to the runtime
WARN runtime app registration AppRegistry cannot see quality. Run activation,
then restart the process if Composer autoload changed.
Cause. AppRegistry is built from the installed-App map, not from the
filesystem. The package is not in the host's Composer requirements, or the
running PHP process is holding a stale autoloader from before the package was
added.
Fix.
task artisan -- nexia-apps:activate-package-app quality
task artisan -- nexia-runtime:generate-app-map
task artisan -- nexia-runtime:refresh-runtime-cachesThen restart long-running processes — queue workers, Octane, Horizon, and the Vite dev server. A new class in a new namespace is invisible to a process whose autoload map predates it.
Confirm. The row becomes OK and names the manifest class:
OK runtime app registration Amuzcorp\Nexia\Quality\QualityAppManifest
Prevent. Treat a package add or namespace change as a process restart, not just a file change.
Manifest and composer.json disagree
FAIL runtime app metadata registered manifest metadata differs from composer.json.
Cause. The doctor compares AppDefinition::toArray() from the registered
manifest against extra.nexia.app in packages/{app_key}/composer.json. They
diverged — usually because composer.json was edited while a cached or stale
definition is still loaded, or a field was hand-edited in only one place.
Fix. Make extra.nexia.app the single edit point, then re-activate:
task artisan -- nexia-access:sync-manifest
task artisan -- nexia-apps:activate-package-app qualityapp_key and app_table_prefix are contractually stable. If one of them is
actually wrong, that is a rename operation across tables, permissions,
descriptors, and translation keys — not a metadata edit.
Confirm. OK runtime app metadata registered manifest matches composer.json.
Prevent. Never mirror definition fields into a second file.
Contribution directory is not declared
WARN contribution locations no package contribution location is registered in
the current process.
Cause. Manifest generation scans only the namespace/directory pairs your App
Manifest returns from contributionLocations(). A class outside those roots is
not indexed, no matter which interfaces it implements. Ordinary requests trust
the generated index and do not scan either declared or undeclared directories.
Fix. Declare the directory in the manifest:
public function contributionLocations(): array
{
return [
['namespace' => 'Amuzcorp\Nexia\Quality\Contribution', 'directory' => __DIR__.'/Contribution'],
['namespace' => 'Amuzcorp\Nexia\Quality\Descriptors', 'directory' => __DIR__.'/Descriptors'],
];
}The namespace must match the PSR-4 namespace of classes in directory
exactly. Subdirectories are covered — Contribution/Resources/NoteModule.php
is found by the Contribution entry — so a mismatch is almost always a typo or
a class placed outside every declared root.
Rebuild the contribution and route caches after changing the roots:
task artisan -- nexia-runtime:refresh-runtime-cachesConfirm. OK contribution locations 2 registered, followed by non-zero
contributor counts.
Prevent. Add new contributors under an already-declared root rather than creating a sibling directory.
Class is not recognized as a contributor
OK contribution locations 2 registered
WARN permission contributors 0 classes
WARN permissions discovered 0 permission definitions
Cause. When the manifest was generated, your class did not satisfy the contract used to build that surface list. Indexing matches on the interface, and each interface requires its declared methods (some contribution families use instance methods). The most common cause is using a trait without declaring the interface it implements — the trait supplies the method, but the class is not selected because it does not declare the type.
| Interface | Required method | Trait that can supply it |
|---|---|---|
ResourceCatalogContribution | resourceKey(), resourceModelClass() | — |
ResourceAuthorizationContribution | resourceAuthorization() | — |
PermissionContribution | catalogPermissionDefinitions() | ContributesResourcePermissions (from permissionResources()) |
NavigationContribution | navigationItems() | ContributesNavigationDestination (from $navigation) |
Fix. Declare every interface the class actually provides:
Add a menu destination provides a complete NavigationContribution with imports and discovery commands. Apply that interface pattern to your own class; each additional contribution must implement its own required methods.
Also check that the class is not abstract, and that the file name matches the
class name so PSR-4 can autoload it.
Then rebuild the generated index:
task artisan -- nexia-runtime:refresh-runtime-cachesConfirm. The contributor count increases and the expected permission keys appear. Counts depend on the current App; use Test an App to assert the specific contribution.
Prevent. Add the interface and the trait in the same edit.
Generated manifest is stale
The contribution class changed in source, but the previous navigation,
permission, or descriptor result is still served.
Cause. The schema-v1 contribution manifest is a complete interface-to-class index. When the installed-location hash still matches, a normal request trusts the stored list — even a missing interface key is a cached empty result. Source mtime checks are deliberately not part of the warm request path.
Fix. Rebuild contribution metadata and the route cache:
task artisan -- nexia-runtime:refresh-runtime-cachesConfirm. nexia-apps:doctor-package-app reports the expected non-zero contributor
counts and the changed surface appears in a fresh process.
Prevent. Keep nexia-runtime:refresh-runtime-caches in source-change and deployment
workflows. scripts/run-vite-dev.sh runs nexia-runtime:cache-contributions for local
Vite startup, and Laravel optimize invokes the full refresh through the host
service provider.
Octane worker still serves the old contribution
A fresh CLI process resolves the new navigation or resource definition, but the
browser still renders the previous contribution.
Cause. The fresh CLI process reads the edited source, while the running Octane worker can still hold classes or a previously built contribution registry. An already-open Shell can also retain its earlier navigation result. Refreshing the generated manifest does not replace either process state.
Fix. Restart only the web application container and wait for it to become healthy:
task app:reloadThen reload the browser. If the edit added or removed a contributor class, interface, or route, rebuild the generated metadata first:
task artisan -- nexia-runtime:refresh-runtime-caches
task app:reloadConfirm. task status reports app as healthy and a reloaded Shell shows
the current contribution.
Prevent. Use task app:reload as the deterministic local fallback when
Octane watch does not reflect a backend edit. It does not reset data or restart
other services.
Frontend surface is never registered
FAIL frontend component registration index.ts does not call
appComponentRegistry.register or autoRegisterPackageSurfaces.
Cause. The backend publishes component names through pageElements(), and
the frontend must bind each name to a component. Two doctor rows cover two
different failures, and reading the wrong one sends you to the wrong fix:
| Row | Fails when |
|---|---|
frontend component registration | index.ts calls neither appComponentRegistry.register(...) nor autoRegisterPackageSurfaces — no registration at all |
shell component pairing | Registration exists, but one name published by pageElements() has no binding |
The message above is the first row: nothing is registered. For a single missing
name, read shell component pairing, which lists the unmatched names.
The doctor accepts an explicit appComponentRegistry.register(...) call, an
overrides entry on autoRegisterPackageSurfaces, or an auto-derivable surface
file at resources/js/resources/{resource}/surface/. With none of those, the
route resolves and renders nothing.
Fix. Keep the generated registration in index.ts, and keep generated
surfaces on their derivable path — resources/js/resources/notes/surface/NoteListSurface.tsx.
When you move or rename a surface, add an explicit overrides entry.
For a built-asset installation, rebuild the package after changing registration:
task artisan -- nexia-apps:activate-package-app quality --build-assetsA related row worth reading:
WARN frontend entry shape entry exists, but app-specific route/component
tokens were not obvious.
That means index.ts exists but mentions neither /{app_key} nor the studly
app key — usually a stub that was never filled in.
Confirm. OK frontend component registration and OK shell component pairing, and the surface renders at its App route.
Prevent. Run both checks before committing a frontend change — the validator covers the dependency graph, the ratchet covers the import boundary:
task artisan -- nexia-apps:validate-app-frontend-dependencies --app={app_key} --require-manifests
php scripts/coupling-ratchet.php check {app_key}Labels render as raw keys
quality.defect_code.status.ACTIVE
You see quality.defect_code.status.ACTIVE in the UI instead of a label.
WARN translation catalogs missing ko.json. Add packages/<app>/resources/lang/{locale}.json.
WARN translation catalogs invalid ko.json (catalog must be a flat JSON object).
Cause. Package catalogs are flat JSON objects at
packages/{app_key}/resources/lang/{locale}.json, one per supported locale.
The doctor rejects a catalog that is a JSON list, or that contains an entry
whose key has no ., contains :, or whose value is not a string.
Fix. Use flat dotted keys and string values:
{
"quality.defect_code.status.ACTIVE": "Active",
"quality.defect_code.status.RETIRED": "Retired"
}Then validate and refresh the cache:
task artisan -- nexia-runtime:validate-translations
task artisan -- nexia-runtime:clear-translation-catalog-cacheConfirm. OK translation catalogs, and the label renders in every
supported locale.
Prevent. Add all locales in the same commit as the key. The catalog is merged, not replaced, so generated entries and hand-written entries coexist.
Everything is discovered but nothing is visible
Doctor is clean, routes respond, and the App Menu entry is still absent.
Cause. One of three, in this order of likelihood.
Fix. Check the three gates below in order and correct the first one that does not match the current tenant and actor.
The entry is deliberately hidden. --without-navigation writes
visible => false on the contributor. Routes, permissions, and catalog
metadata all remain; only the menu entry is suppressed:
protected static array $navigation = [
'group' => 'master-data',
'visible' => false,
];Set it to true to publish the entry.
No one has the permission. Activation registers permission definitions
and grants nothing. A navigation entry gated on quality.defect_code.read is
correctly hidden from an actor without it — including you. An access
administrator must create or select an App Role, attach the permissions, and
assign it.
The App is not installed for this tenant. Host activation and tenant
installation are separate steps. Without installation, app.installed:{app_key}
fails closed and the App contributes nothing to this tenant:
task artisan -- tenants:run "nexia-apps:install-tenant-app quality --dry-run" --tenants="$APP_TENANT_ID"
task artisan -- tenants:run "nexia-apps:install-tenant-app quality" --tenants="$APP_TENANT_ID"For an unpublished local App (APP_ENV=local), use task artisan -- nexia-apps:install-dev-app quality --tenant="$APP_TENANT_ID" --dry-run, then rerun with --with-prerequisites instead of --dry-run. This leaves publication/readiness unchanged and also provides the local recovery path. The normal install and repair commands retain production distribution checks.
Confirm. The entry appears for a user holding the gating permission in a tenant where the App is installed and operational.
Prevent. When something is missing, check the permission grant and the tenant installation before you suspect discovery.
Related
- Create a Resource — the generated contributor and where each declaration lands
- Resource contracts — required interfaces, methods, and generator error messages
- Fix installation errors — failures during activation and tenant installation