From 7ace35869eabc5536e419b55c01bd15b47dce4ce Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:43:20 +0100 Subject: [PATCH 1/8] docs: scaffold-lint-clean design (#896) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- .../2026-11-10-scaffold-lint-clean-design.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/superpowers/specs/2026-11-10-scaffold-lint-clean-design.md diff --git a/docs/superpowers/specs/2026-11-10-scaffold-lint-clean-design.md b/docs/superpowers/specs/2026-11-10-scaffold-lint-clean-design.md new file mode 100644 index 00000000..626b682c --- /dev/null +++ b/docs/superpowers/specs/2026-11-10-scaffold-lint-clean-design.md @@ -0,0 +1,79 @@ +# A fresh app passes its own `make lint` (#896) + +## Goal and scope + +`smpy new bookshelf -y && make lint` must pass with no edits, and so must the app after +`smpy create-module ` and after `make migration`. Reproduced on `main` with the +local CLI: + +| Gate | Failure | Cause | +|---|---|---| +| `ruff format --check` | the initial-schema revision would be reformatted | Alembic writes single-quoted, unwrapped code | +| `ruff check` | 33 × E501, W291, I001 in that revision | same, plus `Revises: ` trailing space and the `${imports}` line in `script.py.mako` | +| `ruff check` after `create-module bookmarks` | I001 in `module.py` | the template lists `PERM_GROUP` before `PERM__*`, which is sorted only when the package name sorts after `group` (`hello` does, `bookmarks` does not) | +| `ty check` (any ty 0.0.90–0.0.100) | `unused-ignore-comment` on `model_config = ConfigDict(...) # ty: ignore[invalid-assignment]` | the app's ty config turns `invalid-assignment` off, so the ignore never applies; the framework repo turns it on, which is why the template has it | +| `ty check` with ty 0.0.100 | `renamed-rule`: `unresolved-attribute` is now `missing-attribute` | the scaffold pins `ty>=0.0.29`, so a new app gets the newest ty | + +Out of scope: #872 (`path_separator` in `alembic.ini`), the Vite warnings (#900), the +layout (#897), and bumping the framework repo's own ty. + +## Approach + +1. **Revisions: an Alembic `post_write_hooks` entry**, in the scaffold's `host/alembic.ini` + and in this repo's `host/alembic.ini`. It runs `python -m ruff check --select I --fix` + and then `python -m ruff format` on each new revision. Ruff picks up the project's own + config from the file's location. Only the isort rules get `--fix`, so the hook never + deletes the unused `sa`/`op` imports in an empty handwritten revision, which the + author is about to fill in. + - Rejected: excluding `host/migrations/versions/` from ruff. Every revision would pass, + but hand-edited revisions would no longer be linted, and this repo lints its own. +2. **`module.py` constants import: sorted when rendered.** A new `{{CONSTANTS_IMPORTS}}` + placeholder renders the names sorted the way ruff's isort sorts them (case-insensitive), + so `PERM_GROUP` lands in the right place for every package name. + - Rejected: renaming `PERM_GROUP`. Every module in this repo uses that name. +3. **ty:** + - The ignore comment moves behind a `{{MODEL_CONFIG_TY_IGNORE}}` placeholder: empty for + apps, ` # ty: ignore[invalid-assignment]` for `framework_repo=True`. + - The scaffold pins ty to the tested range `ty>=0.0.90,<0.0.100`, from one + `TY_REQUIREMENT` constant used by both the workspace root template and + `_app_project_manifests`. + - Rejected: renaming the rule to `missing-attribute`. Every ty before 0.0.100 would + then warn about an unknown rule, the framework repo included (it is on 0.0.90). + +## Decisions made + +- The upper bound is the version that renamed the rule. 0.0.90, 0.0.97 and 0.0.99 were + run against a fresh app and are clean once the ignore comment is gone. A test asserts + that the ty in this repo's `uv.lock` satisfies `TY_REQUIREMENT`. A Dependabot bump past + the range then fails that test, so the scaffold gets updated in the same PR. +- The hook uses Alembic's `module` runner (`sys.executable -m ruff`), so it runs the + project venv's ruff. Both scaffold layouts have ruff in the default dev group. Without + ruff, Alembic raises `Could not find module ruff` after the file is written; that is + acceptable for a dev-only command. +- Existing apps are left alone. The CHANGELOG entry gives the `[post_write_hooks]` + block to paste into their `host/alembic.ini`. +- `script.py.mako` itself is unchanged; the hook normalizes its output. + +## Testing + +- `test_cli_new_scaffold_layout.py`: the existing "passes its own ruff" test also runs + `create-module` for `bookmarks` (sorts before `group`) and `zebra` (after) before + running `ruff format --check` and `ruff check`. +- A plain `alembic revision` against a `--no-install` scaffold, run with the test + interpreter, which has ruff: the revision passes `ruff format --check` and + `ruff check --select I,W`. +- Unit tests: + - the placeholder renders with and without `framework_repo`; + - `TY_REQUIREMENT` admits the version in `uv.lock`; + - both manifests use it. +- Real check in /vf: `smpy new` with install from the worktree CLI, then + `create-module bookmarks`, `make migration`, and `make lint` (ruff and ty) pass. + `make new-module` in this repo still passes `make lint`, and this repo's + `make migration` output passes ruff. + +## Risks + +- `smpy new` bootstraps the first revision with `uv run --project host alembic`. If that + environment lacks ruff, the hook raises after the revision is written, and the + bootstrap fails. The real `smpy new` check covers this. The fallback is to bootstrap + without hooks and format explicitly. From fe130c2a0cd7d8fb84a50a56338d556b93332fdf Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:43:32 +0100 Subject: [PATCH 2/8] docs: scaffold-lint-clean plan (#896) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- .../plans/2026-11-10-scaffold-lint-clean.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/superpowers/plans/2026-11-10-scaffold-lint-clean.md diff --git a/docs/superpowers/plans/2026-11-10-scaffold-lint-clean.md b/docs/superpowers/plans/2026-11-10-scaffold-lint-clean.md new file mode 100644 index 00000000..e0182b6b --- /dev/null +++ b/docs/superpowers/plans/2026-11-10-scaffold-lint-clean.md @@ -0,0 +1,25 @@ +# Plan: a fresh app passes its own `make lint` (#896) + +Design: `docs/superpowers/specs/2026-11-10-scaffold-lint-clean-design.md`. + +1. **ty pin** (independent). Test first: `TY_REQUIREMENT` admits the ty version in + `uv.lock`, and both the workspace root template and `_APP_PY_DEV_DEPS` use it. Add + `TY_REQUIREMENT` to `scaffolding.py`, a `{{TY_REQUIREMENT}}` placeholder in + `templates/workspace/pyproject.toml.tpl`, and use it in `_app_project_manifests.py`. +2. **ty ignore comment** (independent). Test first: `create_module` renders no ignore + comment by default and `# ty: ignore[invalid-assignment]` with `framework_repo=True`. + Add the `{{MODEL_CONFIG_TY_IGNORE}}` placeholder in `contracts/schemas.py.tpl`. +3. **Sorted constants import** (independent). Test first: `create_module("bookmarks")` + and `create_module("zebra")` produce a `module.py` that passes `ruff check --select I`. + Add the `{{CONSTANTS_IMPORTS}}` placeholder in `module.py.tpl`, rendered by + `sorted(..., key=str.lower)`. +4. **Alembic write hook**. Test first: a plain `alembic revision` in a `--no-install` + scaffold passes `ruff format --check` and `ruff check --select I,W`. Add + `[post_write_hooks]` to `templates/host/alembic.ini` and `host/alembic.ini`. +5. **End-to-end and docs**. + - Extend the "passes its own ruff" test with `create-module bookmarks`/`zebra`. + - Update `docs/database/migrations.md` and add a CHANGELOG Fixed entry. + - Real `smpy new` + `create-module` + `make migration` + `make lint`. + - `make new-module` sanity check. + +Done check for every task: its targeted tests pass, `ruff`, and `ty` pass. From bf4f32660956632406cdc0df4371afcbd98fe3c4 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:46:42 +0100 Subject: [PATCH 3/8] fix(cli): a fresh smpy new app passes its own make lint (#896) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 1 + docs/database/migrations.md | 2 + .../_app_project_manifests.py | 3 +- .../cli/simple_module_cli/_lint_tooling.py | 37 ++++++++ .../cli/simple_module_cli/scaffolding.py | 9 ++ .../templates/host/alembic.ini | 12 +++ .../__PACKAGE__/contracts/schemas.py.tpl | 2 +- .../module/__PACKAGE__/module.py.tpl | 14 +-- .../templates/workspace/pyproject.toml.tpl | 2 +- .../cli/tests/test_scaffold_lint_tooling.py | 92 +++++++++++++++++++ host/alembic.ini | 12 +++ 11 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 framework/cli/simple_module_cli/_lint_tooling.py create mode 100644 framework/cli/tests/test_scaffold_lint_tooling.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 59414d47..d76d0d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed +- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too). `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). - `make doctor` (and `smpy doctor`) now runs the migration checks it documents. It reads `alembic_version` and the table list from `SM_DATABASE_URL` and the heads from the project's `alembic.ini`: `SM010` is an error when the database lacks any head and a warning when it was never migrated or could not be read, and `SM011` reports a module table that a database at every head does not have. A development boot reports `SM011` too once its database is at head (or once the setup wizard has migrated it), and the Doctor screen keeps it on re-run. Doctor now resolves a relative SQLite `SM_DATABASE_URL` against the project root as the app does, so run from `host/` it reads the app's database for `SM033` too. Before, neither code was ever reported. **Behaviour change:** `make doctor` now exits 1 against a dev database that is behind head; run `make migrate` (#895). - `background_tasks`: `SM_BG_TASKS_BROKER_URL` and `SM_BG_TASKS_RESULT_BACKEND` are reported as deprecated only when they hold a Redis URL (`redis`, `rediss`), the only values `SM_REDIS_URL` can replace. A production host that runs tasks in-process with `memory://` + `cache+memory://`, or uses an AMQP, SQS or `rpc://` transport, no longer logs a warning on every boot that it cannot act on, and the production localhost-broker error no longer calls those vars deprecated for non-Redis values (#868). - `SecurityHeadersMiddleware` no longer overwrites a `Content-Security-Policy` that a response sets. The app policy is sent beside it as a second header and browsers enforce both, so a page can tighten the policy (for example `frame-ancestors 'none'` on a public page) but never relax it. Before, only a policy containing `sandbox` survived and any other was dropped silently. **Behaviour change:** a response that set a looser policy, relying on it being replaced, now has both policies enforced: remove that header, or make it a policy the page can live with alongside the app policy (#870). diff --git a/docs/database/migrations.md b/docs/database/migrations.md index 2410b90a..a0f6032f 100644 --- a/docs/database/migrations.md +++ b/docs/database/migrations.md @@ -130,6 +130,8 @@ make migration msg="add orders tables" This runs `alembic -c host/alembic.ini revision --autogenerate` from the repo root. The resulting file lands in `host/migrations/versions/XXXX_add_orders_tables.py`. +A `post_write_hooks` entry in `host/alembic.ini` (this repo's and every scaffolded app's) runs the project's ruff on each new revision: `ruff check --select I --fix`, then `ruff format`. A fresh revision therefore passes `make lint`. Only the import rules are fixed, so an empty handwritten revision keeps its `sa` and `op` imports. Ruff must be installed in the venv Alembic runs from (it is in the default dev group); without it Alembic reports `Could not find module ruff` after writing the file. An app scaffolded before 0.0.45 can copy the `[post_write_hooks]` block from a new `smpy new` app's `host/alembic.ini` (#896). + **Always open and read the generated file** before committing. Autogenerate is good but not perfect: - It detects column additions, drops, type changes, index/constraint changes, new tables. diff --git a/framework/cli/simple_module_cli/_app_project_manifests.py b/framework/cli/simple_module_cli/_app_project_manifests.py index 7eb2e2a9..1fc13470 100644 --- a/framework/cli/simple_module_cli/_app_project_manifests.py +++ b/framework/cli/simple_module_cli/_app_project_manifests.py @@ -4,6 +4,7 @@ from collections.abc import Sequence +from simple_module_cli._lint_tooling import TY_REQUIREMENT from simple_module_cli.scaffolding import resolve_framework_version _FRAMEWORK_VERSION = resolve_framework_version() @@ -22,7 +23,7 @@ # Floor: unpinned, uv backtracks playwright to 1.18 (GH #445). "playwright>=1.45", "ruff>=0.8", - "ty>=0.0.29", + TY_REQUIREMENT, ] _APP_NPM_DEPS = { diff --git a/framework/cli/simple_module_cli/_lint_tooling.py b/framework/cli/simple_module_cli/_lint_tooling.py new file mode 100644 index 00000000..623ae021 --- /dev/null +++ b/framework/cli/simple_module_cli/_lint_tooling.py @@ -0,0 +1,37 @@ +"""Values that keep a scaffolded app passing its own ``make lint`` (GH #896).""" + +from __future__ import annotations + +# ty is pre-1.0 and renames rules between releases (0.0.100 renamed +# ``unresolved-attribute``, which the scaffold's ``[tool.ty.rules]`` names), so +# new apps get the range this repo is tested on, not the newest ty. +# ``test_scaffold_lint_tooling.py`` fails once ``uv.lock`` moves past it. +TY_REQUIREMENT = "ty>=0.0.90,<0.0.100" + +# Apps turn ty's ``invalid-assignment`` off, so this ignore would be reported as +# unused there; only a module inside the framework repo, which checks the rule, +# gets it. +MODEL_CONFIG_TY_IGNORE = " # ty: ignore[invalid-assignment]" + +_MODULE_CONSTANTS = ( + "API_PREFIX", + "LOCALE_NAMESPACE", + "MENU_ICON", + "MENU_LABEL", + "MENU_LABEL_KEY", + "MENU_ORDER", + "MENU_URL", + "PERM_GROUP", + "VIEW_PREFIX", +) +_MODULE_PERMISSIONS = ("CREATE", "DELETE", "EDIT", "VIEW") + + +def module_constants_import(package_upper: str) -> str: + """The ``module.py`` constants import list, in the order ruff's isort wants. + + Where ``PERM_GROUP`` falls among ``PERM__*`` depends on the package + name, so a fixed template order is unsorted for half of all names. + """ + names = [*_MODULE_CONSTANTS, *(f"PERM_{package_upper}_{p}" for p in _MODULE_PERMISSIONS)] + return "\n".join(f" {name}," for name in sorted(names, key=str.lower)) diff --git a/framework/cli/simple_module_cli/scaffolding.py b/framework/cli/simple_module_cli/scaffolding.py index bab3c425..2ea24158 100644 --- a/framework/cli/simple_module_cli/scaffolding.py +++ b/framework/cli/simple_module_cli/scaffolding.py @@ -21,6 +21,12 @@ from pathlib import Path from typing import TypeGuard +from simple_module_cli._lint_tooling import ( + MODEL_CONFIG_TY_IGNORE, + TY_REQUIREMENT, + module_constants_import, +) + # The template engine lives in _templating.py; the private names are imported # here because long-standing tests monkeypatch them on this module. from simple_module_cli._templating import ( @@ -137,6 +143,7 @@ def create_workspace( "{{HOST_NAME}}": validate_scaffold_name(name), "{{HOST_PYPI_NAME}}": to_kebab_case(name), "{{FRAMEWORK_VERSION}}": framework_version, + "{{TY_REQUIREMENT}}": TY_REQUIREMENT, }, preserve_existing=preserve_existing, ) @@ -239,6 +246,8 @@ def create_module( "{{SINGULAR}}": singular, "{{SINGULAR_UPPER}}": singular.upper(), "{{SINGULAR_CLASS}}": to_pascal_case(singular), + "{{CONSTANTS_IMPORTS}}": module_constants_import(package_name.upper()), + "{{MODEL_CONFIG_TY_IGNORE}}": MODEL_CONFIG_TY_IGNORE if framework_repo else "", # npm-side pin for @simple-module-py/* devDependencies; "*" when the # caller skips pinning (mirrors _should_pin_framework_version). "{{FRAMEWORK_VERSION}}": ( diff --git a/framework/cli/simple_module_cli/templates/host/alembic.ini b/framework/cli/simple_module_cli/templates/host/alembic.ini index 1850cf3a..9861650b 100644 --- a/framework/cli/simple_module_cli/templates/host/alembic.ini +++ b/framework/cli/simple_module_cli/templates/host/alembic.ini @@ -6,6 +6,18 @@ script_location = %(here)s/migrations sqlalchemy.url = +# Run the project's ruff on every new revision, so `make migration` output +# passes `make lint` (GH #896). Only the import rules are fixed: the unused +# `sa`/`op` imports of an empty revision stay for its author to use. +[post_write_hooks] +hooks = ruff_imports, ruff_format +ruff_imports.type = module +ruff_imports.module = ruff +ruff_imports.options = check --select I --fix --quiet REVISION_SCRIPT_FILENAME +ruff_format.type = module +ruff_format.module = ruff +ruff_format.options = format --quiet REVISION_SCRIPT_FILENAME + [loggers] keys = root,sqlalchemy,alembic diff --git a/framework/cli/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl index cba773ad..63a3bfe5 100644 --- a/framework/cli/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl +++ b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/contracts/schemas.py.tpl @@ -13,7 +13,7 @@ from {{PACKAGE_NAME}}.core.constants import DESCRIPTION_MAX_LENGTH, NAME_MAX_LEN class {{SINGULAR_CLASS}}Out(SQLModel): """{{SINGULAR_CLASS}} data returned by the API.""" - model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment] + model_config = ConfigDict(from_attributes=True){{MODEL_CONFIG_TY_IGNORE}} id: int name: str diff --git a/framework/cli/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl index dd070304..503edfa1 100644 --- a/framework/cli/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +++ b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl @@ -12,19 +12,7 @@ from simple_module_core.module_settings import register_module_settings from simple_module_core.permissions import PermissionRegistry from {{PACKAGE_NAME}}.core.constants import ( - API_PREFIX, - LOCALE_NAMESPACE, - MENU_ICON, - MENU_LABEL, - MENU_LABEL_KEY, - MENU_ORDER, - MENU_URL, - PERM_GROUP, - PERM_{{PACKAGE_NAME_UPPER}}_CREATE, - PERM_{{PACKAGE_NAME_UPPER}}_DELETE, - PERM_{{PACKAGE_NAME_UPPER}}_EDIT, - PERM_{{PACKAGE_NAME_UPPER}}_VIEW, - VIEW_PREFIX, +{{CONSTANTS_IMPORTS}} ) diff --git a/framework/cli/simple_module_cli/templates/workspace/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/workspace/pyproject.toml.tpl index 022d700d..868b631b 100644 --- a/framework/cli/simple_module_cli/templates/workspace/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/workspace/pyproject.toml.tpl @@ -28,7 +28,7 @@ dev = [ # pin, which pytest-playwright cannot load (GH #445). "playwright>=1.45", "ruff>=0.8", - "ty>=0.0.29", + "{{TY_REQUIREMENT}}", # Provides the build_test_app / fake_event_bus fixtures (pytest11 plugin). "simple_module_test=={{FRAMEWORK_VERSION}}", ] diff --git a/framework/cli/tests/test_scaffold_lint_tooling.py b/framework/cli/tests/test_scaffold_lint_tooling.py new file mode 100644 index 00000000..b9ed3663 --- /dev/null +++ b/framework/cli/tests/test_scaffold_lint_tooling.py @@ -0,0 +1,92 @@ +"""A scaffolded app and module pass the app's own lint gates (GH #896).""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +import tomllib +from pathlib import Path + +import pytest +from packaging.requirements import Requirement +from simple_module_cli._app_project_manifests import _APP_PY_DEV_DEPS +from simple_module_cli._lint_tooling import TY_REQUIREMENT +from simple_module_cli.app_project import create_app_project +from simple_module_cli.scaffolding import create_module, create_workspace + +_REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _ruff() -> str: + ruff = shutil.which("ruff") + if ruff is None: + pytest.skip("ruff not installed") + return ruff + + +def _locked_version(package: str) -> str: + lock = tomllib.loads((_REPO_ROOT / "uv.lock").read_text()) + return next(p["version"] for p in lock["package"] if p["name"] == package) + + +def test_ty_requirement_admits_the_locked_ty() -> None: + """The scaffold's ty range must contain the ty this repo is tested on; a + lock bump past it means re-checking the scaffold's ty rules first.""" + assert Requirement(TY_REQUIREMENT).specifier.contains(_locked_version("ty")) + + +def test_both_scaffold_layouts_pin_ty_to_the_tested_range(tmp_path: Path) -> None: + assert TY_REQUIREMENT in _APP_PY_DEV_DEPS + create_workspace(tmp_path / "ws", "demo") + dev = tomllib.loads((tmp_path / "ws" / "pyproject.toml").read_text())["dependency-groups"] + assert TY_REQUIREMENT in dev["dev"] + + +@pytest.mark.parametrize(("framework_repo", "expected"), [(False, False), (True, True)]) +def test_model_config_ignore_only_where_ty_checks_assignments( + tmp_path: Path, framework_repo: bool, expected: bool +) -> None: + """Apps turn ty's invalid-assignment off, so the ignore would be unused there.""" + dest = create_module(tmp_path / "orders", "orders", framework_repo=framework_repo) + schemas = (dest / "orders" / "contracts" / "schemas.py").read_text() + assert ("# ty: ignore[invalid-assignment]" in schemas) is expected + + +@pytest.mark.parametrize("name", ["bookmarks", "group", "g", "zebra"]) +def test_module_imports_are_sorted_for_any_package_name(tmp_path: Path, name: str) -> None: + """``PERM_GROUP`` sorts before or after ``PERM__*`` depending on the name.""" + app = tmp_path / "app" + create_app_project(app, name="app", db="sqlite", tenancy=False, selected=[]) + create_module(app / "modules" / name, name, standalone=False) + result = subprocess.run( + [_ruff(), "check", "--select", "I", f"modules/{name}"], + cwd=app, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + + +def test_new_revision_is_formatted_by_the_write_hook(tmp_path: Path) -> None: + """``alembic revision`` runs ruff on the file, so a new revision passes lint.""" + ruff = _ruff() + app = tmp_path / "app" + create_app_project(app, name="app", db="sqlite", tenancy=False, selected=[]) + subprocess.run( + [sys.executable, "-m", "alembic", "-c", "host/alembic.ini", "revision", "-m", "add x"], + cwd=app, + check=True, + capture_output=True, + ) + [revision] = (app / "host" / "migrations" / "versions").glob("*_add_x.py") + fmt = subprocess.run([ruff, "format", "--check", str(revision)], cwd=app, capture_output=True) + assert fmt.returncode == 0, fmt.stdout + # F401: an empty revision does not use sa/op yet; its author fills it in. + check = subprocess.run( + [ruff, "check", "--select", "I,W,E", str(revision)], + cwd=app, + capture_output=True, + text=True, + ) + assert check.returncode == 0, check.stdout diff --git a/host/alembic.ini b/host/alembic.ini index d26cf323..fa92fdcb 100644 --- a/host/alembic.ini +++ b/host/alembic.ini @@ -4,6 +4,18 @@ script_location = %(here)s/migrations sqlalchemy.url = +# Run the project's ruff on every new revision, so `make migration` output +# passes `make lint` (GH #896). Only the import rules are fixed: the unused +# `sa`/`op` imports of an empty revision stay for its author to use. +[post_write_hooks] +hooks = ruff_imports, ruff_format +ruff_imports.type = module +ruff_imports.module = ruff +ruff_imports.options = check --select I --fix --quiet REVISION_SCRIPT_FILENAME +ruff_format.type = module +ruff_format.module = ruff +ruff_format.options = format --quiet REVISION_SCRIPT_FILENAME + [loggers] keys = root,sqlalchemy,alembic From db2783d67763a88e5a7878c1c5476fa2b110bd22 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:51:15 +0100 Subject: [PATCH 4/8] fix: address code review findings (round 1, pass 1) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- docs/database/migrations.md | 2 +- .../templates/host/pyproject.toml.tpl | 5 +++++ .../cli/tests/test_scaffold_lint_tooling.py | 18 +++++++++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/database/migrations.md b/docs/database/migrations.md index a0f6032f..92ede05f 100644 --- a/docs/database/migrations.md +++ b/docs/database/migrations.md @@ -130,7 +130,7 @@ make migration msg="add orders tables" This runs `alembic -c host/alembic.ini revision --autogenerate` from the repo root. The resulting file lands in `host/migrations/versions/XXXX_add_orders_tables.py`. -A `post_write_hooks` entry in `host/alembic.ini` (this repo's and every scaffolded app's) runs the project's ruff on each new revision: `ruff check --select I --fix`, then `ruff format`. A fresh revision therefore passes `make lint`. Only the import rules are fixed, so an empty handwritten revision keeps its `sa` and `op` imports. Ruff must be installed in the venv Alembic runs from (it is in the default dev group); without it Alembic reports `Could not find module ruff` after writing the file. An app scaffolded before 0.0.45 can copy the `[post_write_hooks]` block from a new `smpy new` app's `host/alembic.ini` (#896). +A `post_write_hooks` entry in `host/alembic.ini` (this repo's and every scaffolded app's) runs the project's ruff on each new revision: `ruff check --select I --fix`, then `ruff format`. A fresh revision therefore passes `make lint`. Only the import rules are fixed, so an empty handwritten revision keeps its `sa` and `op` imports. Ruff must be installed in the venv Alembic runs from: every scaffold puts it in the default `dev` dependency group (the workspace root's, or the host's for `smpy create-host`). Without it Alembic writes the file and then fails with `Could not find module ruff`; run `uv add --dev ruff`. An app scaffolded before 0.0.45 can copy the `[post_write_hooks]` block from a new `smpy new` app's `host/alembic.ini` (#896). **Always open and read the generated file** before committing. Autogenerate is good but not perfect: diff --git a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl index ada8aca3..bc842380 100644 --- a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl @@ -13,6 +13,11 @@ dependencies = [ {{MODULE_DEPS}} ] +# `host/alembic.ini` runs ruff on every new revision. `smpy new` replaces this +# group with the app's full dev tooling; a standalone `smpy create-host` keeps it. +[dependency-groups] +dev = ["ruff>=0.8"] + # Host is an application, not a distributable package. [tool.uv] package = false diff --git a/framework/cli/tests/test_scaffold_lint_tooling.py b/framework/cli/tests/test_scaffold_lint_tooling.py index b9ed3663..de784fd9 100644 --- a/framework/cli/tests/test_scaffold_lint_tooling.py +++ b/framework/cli/tests/test_scaffold_lint_tooling.py @@ -2,6 +2,7 @@ from __future__ import annotations +import importlib.util import shutil import subprocess import sys @@ -13,7 +14,7 @@ from simple_module_cli._app_project_manifests import _APP_PY_DEV_DEPS from simple_module_cli._lint_tooling import TY_REQUIREMENT from simple_module_cli.app_project import create_app_project -from simple_module_cli.scaffolding import create_module, create_workspace +from simple_module_cli.scaffolding import create_host, create_module, create_workspace _REPO_ROOT = Path(__file__).resolve().parents[3] @@ -43,6 +44,14 @@ def test_both_scaffold_layouts_pin_ty_to_the_tested_range(tmp_path: Path) -> Non assert TY_REQUIREMENT in dev["dev"] +def test_standalone_host_installs_the_ruff_its_alembic_hook_runs(tmp_path: Path) -> None: + """``smpy create-host`` has no workspace root, so the host itself needs ruff.""" + create_host(tmp_path / "host", "demo", modules=[]) + dev = tomllib.loads((tmp_path / "host" / "pyproject.toml").read_text())["dependency-groups"] + assert any(Requirement(d).name == "ruff" for d in dev["dev"]) + assert "post_write_hooks" in (tmp_path / "host" / "alembic.ini").read_text() + + @pytest.mark.parametrize(("framework_repo", "expected"), [(False, False), (True, True)]) def test_model_config_ignore_only_where_ty_checks_assignments( tmp_path: Path, framework_repo: bool, expected: bool @@ -53,7 +62,7 @@ def test_model_config_ignore_only_where_ty_checks_assignments( assert ("# ty: ignore[invalid-assignment]" in schemas) is expected -@pytest.mark.parametrize("name", ["bookmarks", "group", "g", "zebra"]) +@pytest.mark.parametrize("name", ["bookmarks", "group", "g", "g_x", "a2", "zebra"]) def test_module_imports_are_sorted_for_any_package_name(tmp_path: Path, name: str) -> None: """``PERM_GROUP`` sorts before or after ``PERM__*`` depending on the name.""" app = tmp_path / "app" @@ -71,6 +80,8 @@ def test_module_imports_are_sorted_for_any_package_name(tmp_path: Path, name: st def test_new_revision_is_formatted_by_the_write_hook(tmp_path: Path) -> None: """``alembic revision`` runs ruff on the file, so a new revision passes lint.""" ruff = _ruff() + if importlib.util.find_spec("ruff") is None: + pytest.skip("the hook runs `python -m ruff`; ruff is not importable here") app = tmp_path / "app" create_app_project(app, name="app", db="sqlite", tenancy=False, selected=[]) subprocess.run( @@ -82,7 +93,8 @@ def test_new_revision_is_formatted_by_the_write_hook(tmp_path: Path) -> None: [revision] = (app / "host" / "migrations" / "versions").glob("*_add_x.py") fmt = subprocess.run([ruff, "format", "--check", str(revision)], cwd=app, capture_output=True) assert fmt.returncode == 0, fmt.stdout - # F401: an empty revision does not use sa/op yet; its author fills it in. + # F401 is not selected: an empty revision does not use sa/op yet, and the + # hook deliberately keeps those imports for its author. check = subprocess.run( [ruff, "check", "--select", "I,W,E", str(revision)], cwd=app, From adb60af7ef9a5a850b8c8f41df46774e0e2a179f Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:53:50 +0100 Subject: [PATCH 5/8] fix: address code review findings (round 1, pass 2) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 2 +- framework/cli/tests/test_scaffold_lint_tooling.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d76d0d53..e19c89ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed -- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too). `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). +- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). - `make doctor` (and `smpy doctor`) now runs the migration checks it documents. It reads `alembic_version` and the table list from `SM_DATABASE_URL` and the heads from the project's `alembic.ini`: `SM010` is an error when the database lacks any head and a warning when it was never migrated or could not be read, and `SM011` reports a module table that a database at every head does not have. A development boot reports `SM011` too once its database is at head (or once the setup wizard has migrated it), and the Doctor screen keeps it on re-run. Doctor now resolves a relative SQLite `SM_DATABASE_URL` against the project root as the app does, so run from `host/` it reads the app's database for `SM033` too. Before, neither code was ever reported. **Behaviour change:** `make doctor` now exits 1 against a dev database that is behind head; run `make migrate` (#895). - `background_tasks`: `SM_BG_TASKS_BROKER_URL` and `SM_BG_TASKS_RESULT_BACKEND` are reported as deprecated only when they hold a Redis URL (`redis`, `rediss`), the only values `SM_REDIS_URL` can replace. A production host that runs tasks in-process with `memory://` + `cache+memory://`, or uses an AMQP, SQS or `rpc://` transport, no longer logs a warning on every boot that it cannot act on, and the production localhost-broker error no longer calls those vars deprecated for non-Redis values (#868). - `SecurityHeadersMiddleware` no longer overwrites a `Content-Security-Policy` that a response sets. The app policy is sent beside it as a second header and browsers enforce both, so a page can tighten the policy (for example `frame-ancestors 'none'` on a public page) but never relax it. Before, only a policy containing `sandbox` survived and any other was dropped silently. **Behaviour change:** a response that set a looser policy, relying on it being replaced, now has both policies enforced: remove that header, or make it a policy the page can live with alongside the app policy (#870). diff --git a/framework/cli/tests/test_scaffold_lint_tooling.py b/framework/cli/tests/test_scaffold_lint_tooling.py index de784fd9..08303380 100644 --- a/framework/cli/tests/test_scaffold_lint_tooling.py +++ b/framework/cli/tests/test_scaffold_lint_tooling.py @@ -69,7 +69,7 @@ def test_module_imports_are_sorted_for_any_package_name(tmp_path: Path, name: st create_app_project(app, name="app", db="sqlite", tenancy=False, selected=[]) create_module(app / "modules" / name, name, standalone=False) result = subprocess.run( - [_ruff(), "check", "--select", "I", f"modules/{name}"], + [_ruff(), "check", "--select", "I,F401,F821", f"modules/{name}"], cwd=app, capture_output=True, text=True, From 4f90492b27ae23ea3c1e69acc8424ae7633c7659 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:55:55 +0100 Subject: [PATCH 6/8] fix: require alembic>=1.16.3 for the module post-write hook (Codex review) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 2 +- .../cli/simple_module_cli/templates/host/pyproject.toml.tpl | 3 ++- framework/db/pyproject.toml | 2 +- uv.lock | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19c89ee..87e2b6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed -- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). +- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. The hooks use Alembic's `module` runner, so `simple_module_db` and the host template now require `alembic>=1.16.3`. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). - `make doctor` (and `smpy doctor`) now runs the migration checks it documents. It reads `alembic_version` and the table list from `SM_DATABASE_URL` and the heads from the project's `alembic.ini`: `SM010` is an error when the database lacks any head and a warning when it was never migrated or could not be read, and `SM011` reports a module table that a database at every head does not have. A development boot reports `SM011` too once its database is at head (or once the setup wizard has migrated it), and the Doctor screen keeps it on re-run. Doctor now resolves a relative SQLite `SM_DATABASE_URL` against the project root as the app does, so run from `host/` it reads the app's database for `SM033` too. Before, neither code was ever reported. **Behaviour change:** `make doctor` now exits 1 against a dev database that is behind head; run `make migrate` (#895). - `background_tasks`: `SM_BG_TASKS_BROKER_URL` and `SM_BG_TASKS_RESULT_BACKEND` are reported as deprecated only when they hold a Redis URL (`redis`, `rediss`), the only values `SM_REDIS_URL` can replace. A production host that runs tasks in-process with `memory://` + `cache+memory://`, or uses an AMQP, SQS or `rpc://` transport, no longer logs a warning on every boot that it cannot act on, and the production localhost-broker error no longer calls those vars deprecated for non-Redis values (#868). - `SecurityHeadersMiddleware` no longer overwrites a `Content-Security-Policy` that a response sets. The app policy is sent beside it as a second header and browsers enforce both, so a page can tighten the policy (for example `frame-ancestors 'none'` on a public page) but never relax it. Before, only a policy containing `sandbox` survived and any other was dropped silently. **Behaviour change:** a response that set a looser policy, relying on it being replaced, now has both policies enforced: remove that header, or make it a policy the page can live with alongside the app policy (#870). diff --git a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl index bc842380..1809917f 100644 --- a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl @@ -8,7 +8,8 @@ dependencies = [ "simple_module_db>=1.0,<2.0", "simple_module_hosting>=1.0,<2.0", "simple_module_settings>=1.0,<2.0", - "alembic>=1.13", + # 1.16.3 added the `module` runner host/alembic.ini's post-write hooks use. + "alembic>=1.16.3", "uvicorn[standard]>=0.34", {{MODULE_DEPS}} ] diff --git a/framework/db/pyproject.toml b/framework/db/pyproject.toml index cb49f852..d661b6ab 100644 --- a/framework/db/pyproject.toml +++ b/framework/db/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ ] dependencies = [ "aiosqlite>=0.20,<0.23", - "alembic>=1.14,<2", + "alembic>=1.16.3,<2", "asyncpg>=0.30,<0.33", # psycopg2 is the *sync* Postgres driver Alembic uses for migrations. # The async app code goes through asyncpg; migration env.py rewrites the diff --git a/uv.lock b/uv.lock index 9c287186..fb3bea41 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", @@ -3267,7 +3267,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.20,<0.23" }, - { name = "alembic", specifier = ">=1.14,<2" }, + { name = "alembic", specifier = ">=1.16.3,<2" }, { name = "asyncpg", specifier = ">=0.30,<0.33" }, { name = "psycopg2-binary", specifier = ">=2.9,<3" }, { name = "simple-module-core", editable = "framework/core" }, From 6d9a9d6d12b4b6f62e66bc88aa7794c58b89c498 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:57:44 +0100 Subject: [PATCH 7/8] fix: keep uv.lock at lockfile revision 5 Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index fb3bea41..43187b1b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", From 7e90940854e7a4c44185067c075049316caf1829 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Tue, 10 Nov 2026 22:58:38 +0100 Subject: [PATCH 8/8] docs: existing apps need alembic>=1.16.3 for the copied hook (review) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 2 +- docs/database/migrations.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e2b6a8..2a919afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed -- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. The hooks use Alembic's `module` runner, so `simple_module_db` and the host template now require `alembic>=1.16.3`. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini`, drop the unused ignore comments and pin ty the same way (#896). +- A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. The hooks use Alembic's `module` runner, so `simple_module_db` and the host template now require `alembic>=1.16.3`. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini` (and raise its own `alembic` requirement to `>=1.16.3`), drop the unused ignore comments and pin ty the same way (#896). - `make doctor` (and `smpy doctor`) now runs the migration checks it documents. It reads `alembic_version` and the table list from `SM_DATABASE_URL` and the heads from the project's `alembic.ini`: `SM010` is an error when the database lacks any head and a warning when it was never migrated or could not be read, and `SM011` reports a module table that a database at every head does not have. A development boot reports `SM011` too once its database is at head (or once the setup wizard has migrated it), and the Doctor screen keeps it on re-run. Doctor now resolves a relative SQLite `SM_DATABASE_URL` against the project root as the app does, so run from `host/` it reads the app's database for `SM033` too. Before, neither code was ever reported. **Behaviour change:** `make doctor` now exits 1 against a dev database that is behind head; run `make migrate` (#895). - `background_tasks`: `SM_BG_TASKS_BROKER_URL` and `SM_BG_TASKS_RESULT_BACKEND` are reported as deprecated only when they hold a Redis URL (`redis`, `rediss`), the only values `SM_REDIS_URL` can replace. A production host that runs tasks in-process with `memory://` + `cache+memory://`, or uses an AMQP, SQS or `rpc://` transport, no longer logs a warning on every boot that it cannot act on, and the production localhost-broker error no longer calls those vars deprecated for non-Redis values (#868). - `SecurityHeadersMiddleware` no longer overwrites a `Content-Security-Policy` that a response sets. The app policy is sent beside it as a second header and browsers enforce both, so a page can tighten the policy (for example `frame-ancestors 'none'` on a public page) but never relax it. Before, only a policy containing `sandbox` survived and any other was dropped silently. **Behaviour change:** a response that set a looser policy, relying on it being replaced, now has both policies enforced: remove that header, or make it a policy the page can live with alongside the app policy (#870). diff --git a/docs/database/migrations.md b/docs/database/migrations.md index 92ede05f..e117d7e1 100644 --- a/docs/database/migrations.md +++ b/docs/database/migrations.md @@ -130,7 +130,7 @@ make migration msg="add orders tables" This runs `alembic -c host/alembic.ini revision --autogenerate` from the repo root. The resulting file lands in `host/migrations/versions/XXXX_add_orders_tables.py`. -A `post_write_hooks` entry in `host/alembic.ini` (this repo's and every scaffolded app's) runs the project's ruff on each new revision: `ruff check --select I --fix`, then `ruff format`. A fresh revision therefore passes `make lint`. Only the import rules are fixed, so an empty handwritten revision keeps its `sa` and `op` imports. Ruff must be installed in the venv Alembic runs from: every scaffold puts it in the default `dev` dependency group (the workspace root's, or the host's for `smpy create-host`). Without it Alembic writes the file and then fails with `Could not find module ruff`; run `uv add --dev ruff`. An app scaffolded before 0.0.45 can copy the `[post_write_hooks]` block from a new `smpy new` app's `host/alembic.ini` (#896). +A `post_write_hooks` entry in `host/alembic.ini` (this repo's and every scaffolded app's) runs the project's ruff on each new revision: `ruff check --select I --fix`, then `ruff format`. A fresh revision therefore passes `make lint`. Only the import rules are fixed, so an empty handwritten revision keeps its `sa` and `op` imports. Ruff must be installed in the venv Alembic runs from: every scaffold puts it in the default `dev` dependency group (the workspace root's, or the host's for `smpy create-host`). Without it Alembic writes the file and then fails with `Could not find module ruff`; run `uv add --dev ruff`. An app scaffolded before 0.0.45 can copy the `[post_write_hooks]` block from a new `smpy new` app's `host/alembic.ini`, provided its Alembic is 1.16.3 or newer, the first release with the `module` hook runner (#896). **Always open and read the generated file** before committing. Autogenerate is good but not perfect: