Diagnostic codes
The framework runs a set of static checks over installed modules at app boot. The full structural/page/i18n suite runs in development (SM_ENVIRONMENT=development), prints to stderr, and aborts boot on any ERROR. In non-development environments (anything other than development) module discovery is strict — entry-point failures, missing/invalid meta (the SM001 class of problems) and missing depends_on targets (SM030) raise at boot — and the migration check (SM010) fails boot in every environment.
Levels
- ERROR — fails boot in production. Always fix.
- WARNING — printed; doesn't fail boot. Fix before shipping.
- INFO — informational; safe to ignore but useful signal.
The codes
| Code | Level | Trigger | Fix |
|---|---|---|---|
SM001 | ERROR | Module subclass has no meta attribute, or meta is not a ModuleMeta. | Declare meta = ModuleMeta(name=..., version=..., ...) on the class. |
SM003 | WARNING | pages/<name>.tsx exists but no inertia.render() call in any module references the corresponding page key. | Remove the orphan file, or add the matching inertia.render("<Module>/<name>", ...) in views.py. |
SM004 | WARNING | An inertia.render("<Module>/<name>") call exists but no matching .tsx file is shipped. | Create pages/<name>.tsx, or correct the render key. |
SM007 | INFO | Module overrides zero register_* hooks and has no on_startup/on_shutdown. | Delete the module if it's vestigial; otherwise ignore. |
SM008 | ERROR | Two modules declare the same ModuleMeta.name. Table-prefix collision. | Rename one. Remember: the name is the table-name prefix (all modules share one DB schema) and the Inertia namespace. |
SM009 | ERROR | A file under framework/* imports from any package under modules/*, including dynamic imports with literal names or fixed f-string package prefixes. | Invert the dependency: have the module register a callback into the framework (see principal_serializer pattern). |
SM010 | ERROR or WARNING | make doctor reads alembic_version from SM_DATABASE_URL and the heads from the project's alembic.ini (else host/alembic.ini). ERROR when the database lacks any head; WARNING when it was never migrated (a production boot sends that to /setup) or could not be read, in which case SM011 is skipped too. A SQLite file that does not exist yet counts as never migrated and is not created. | Run make migrate. The lifespan migration check also refuses a behind-head database at boot in every environment. |
SM011 | WARNING | A table in a module's models is missing from a database at every head, so no migration creates it. Reported by make doctor and, once the database is open, a development boot (also on the Doctor screen). Not checked while the database is behind head. | Generate a revision on the module's branch (make migration LABEL=<branch_label> msg="..." in this repo, make migration msg="..." in a scaffolded app), review it, make migrate. |
SM012 | WARNING | Module overrides register_settings but does not assign to app.state.<module_lower>. Dev-only. | Either remove the override or move your state assignment into it. |
SM013 | WARNING | Supported locale has no corresponding file in some module's locales/. | Add modules/<name>/<name>/locales/<locale>.json, or drop the locale from SM_I18N_SUPPORTED_LOCALES. |
SM014 | WARNING | Non-default locale is missing keys present in the default (untranslated). | Translate the missing keys. |
SM015 | WARNING | Non-default locale has keys not in the default (stale / orphan translation). | Remove the stale keys, or add them to the default file if they really belong. |
SM016 | ERROR | Locale JSON is invalid or contains non-string leaves. | Fix the JSON; keys must flatten to strings only. |
SM017 | WARNING | Module ships .tsx pages but has no package.json / tsconfig.json. Vite can't resolve type imports. | Run smpy create-module on a dummy name and copy the generated config, or scaffold by hand. |
SM018 | WARNING | An Inertia router.post/patch/put/delete() call in a page targets a JSON /api/* endpoint, which would return raw JSON and be rejected by Inertia. | Point the call at a view endpoint that returns inertia.render(...) or a redirect; or use plain fetch() if you really want a JSON response. |
SM019 | WARNING | Module declares a non-empty view_prefix and overrides register_routes but registers neither menu items nor permissions — admins can't reach the pages from the sidebar or grant access from the role editor. | Add register_menu_items for a sidebar entry, or register_permissions to surface the module in the role editor (sub-pages of another module typically just register permissions). |
SM020 | ERROR | More than one auth-provider module is installed (e.g. both users and keycloak). | Install exactly one auth provider. |
SM021 | WARNING | No auth-provider module is installed. | Install an auth provider (e.g. simple-module-users or simple-module-keycloak). |
SM022 | WARNING | A module's styles.css contains a top-level @theme, @custom-variant or @utility block. That file is imported into layer(components), where those at-rules are inert. | Move the block to the module's theme.css, which is imported unlayered so its tokens actually register. |
SM023 | WARNING | A module's theme.css contains an unlayered plain rule (anything but an at-rule or a :root-style selector). Unlayered CSS outranks every Tailwind utility. | Move the rule to the module's styles.css, which is imported into layer(components) so utilities still win. |
SM024 | WARNING | A unique column, constraint or index on a MultiTenantMixin table does not include tenant_id, so the first tenant to claim a value locks every other tenant out of it. | Make the key per tenant: add tenant_id to it (Index(..., "tenant_id", "slug", unique=True)). |
SM025 | WARNING | multi_tenant is on but no module registered app.state.tenant_resolver, so only the principal's tenant_id claim can bind a tenant — with most auth providers every tenant-scoped query then fails closed. Checked at boot (it needs the built app), in every environment, not by make doctor. | Install the tenants module, or register your own async (Request) -> str | None resolver on app.state.tenant_resolver. |
SM026 | INFO | The configured database is SQLite and a module model declares an expression index (e.g. lower(email)). SQLite cannot reflect those, so autogenerate and alembic check cannot verify them and a clean diff says nothing about them. | Add the index to a revision by hand and verify with make migrations-roundtrip-pg (PostgreSQL). See Migrations. |
SM027 | WARNING | A key in host/locales/overrides/<locale>.json matches no catalog key, so the override was skipped. Reported by make doctor and at dev boot. | Fix the key: overrides replace an existing key by its full dotted path (e.g. users.login.aside_heading). See i18n. |
SM028 | WARNING | A module's tasks.py imports torch, tensorflow, ultralytics, osgeo, rasterio, cv2 or sklearn at module level, so every process that loads the module's tasks (web, each worker, beat) pays for it and a queue-restricted worker cannot avoid it. | Import the package inside the task function. See background_tasks. |
SM029 | INFO | A module overrides db_extensions() (Postgres extensions such as ltree) and the configured database is SQLite, which has none. ensure_db_extensions raises UnsupportedDatabaseError when migrations run. | Point SM_DATABASE_URL at PostgreSQL. See Postgres extensions and column types. |
SM030 | WARNING at a dev boot; ERROR in make doctor | A module's depends_on names a module that is not loaded (not installed, left out of SM_MODULES_ENABLED, or dropped because another auth provider was selected — keycloak drops Users). Outside development the boot raises MissingDependencyError listing every pair. | Install and enable the dependency, or leave the dependent module out. A relationship that is only about order belongs in optional_depends_on. See Discovery. |
SM031 | WARNING | An /api route has no permission, TokenAllowed or Authenticated gate anywhere in its dependency tree (include-level dependencies count) and no public-route match, so the schema cannot say who may call it. Needs the built app: reported at a dev boot (and in the Doctor screen), not by make doctor. | Add Depends(RequiresPermission(...)), or Depends(Authenticated()) when any signed-in user may call it, or register it in register_public_routes. |
SM032 | ERROR (fails boot in every environment) | A ModuleBase.spa_mounts() prefix overlaps a module's route_prefix/view_prefix/admin_view_prefix, a static_mounts() prefix, a framework path (/api, /admin, /setup, /i18n, /health, /static, /modules) or another SPA mount. The mount would shadow, or be shadowed by, that router. | Pick a prefix nothing else uses. See Legacy SPA mounts. |
SM033 | ERROR or WARNING | A module data compatibility check found a missing semantic upgrade (a WARNING when the database could not be checked; a SQLite file that does not exist yet is skipped). Token schema/legacy credentials are errors; unmoved mail system keys warn. Errors refuse boot outside development. Never-migrated first runs defer checks. | Run the reported smpy migrations prepare <upgrade-id> --head <host-head-or-label@head>, review the host revision and apply upgrade heads; see Upgrading a host. |
SM034 | WARNING | A subscribe_durable(...) call in a module's package source has no name=. The default name is the handler's import path, so moving or renaming the handler strands its undelivered outbox rows (they retry as an unknown handler, then go dead). Found by AST scan; tests/ is skipped. | Pass name= with a constant from constants.py whose value is the current default name. See Durable delivery. |
SM022/SM023 are the two halves of the same invariant: a module's optional theme.css is imported unlayered and styles.css into layer(components), and CSS put in the wrong one silently does nothing (or silently wins everything).
When diagnostics fire
| Context | What runs |
|---|---|
| App boot in development | Full structural/page/i18n suite, results logged to stderr. ERRORS abort boot. After the routes are wired, SM031 (unguarded /api routes) is added; once the database is open and at head, SM011. |
| App boot in non-development | Strict module discovery (raises on SM001-class failures) + the migration check (SM010) + SM025 (logged as a warning). The page/locale static suite is dev-only. |
Sample dev-mode output:
running diagnostics …
WARNING SM003 modules/orders/orders/pages/Unused.tsx: no inertia.render("Orders/Unused") call found
WARNING SM013 locale "es" missing file for module "orders" (expected modules/orders/orders/locales/es.json)
ERROR SM010 Database is behind: missing heads '9f8e7d6c5b4a' — run `make migrate`
1 error, 2 warningsTreat a clean dev boot as the "ready to ship" gate. If you want to run the diagnostics manually without booting the app, the framework repo's make doctor target does exactly that — it's a contributor convenience and not part of the user-facing flow.
Adding a new diagnostic
If you think a rule deserves a code, open a design doc in docs/plans/ first — existing codes are stable contracts (downstream tooling can grep for them). Use the next free number in the SM0XX range and:
- Add the check to
simple_module_core.diagnostics. - Wire it into
run_diagnostics()(or the relevant*Diagnostics.run()method) so it runs at app boot. - Update this page with the row.
- Add a test case under
framework/core/tests/.