diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b54b4ce..e14a73d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ isolation. A change to `common/` or to an adapter reaches a project that already exists only when somebody runs `scaffold update` in it — see [ADR-0023](docs/decisions/0023-a-project-records-what-generated-it.md). A -change to a reusable workflow in `you/.github` reaches every project the next +change to a reusable workflow in *you/.github* reaches every project the next time it runs, once `v1` moves (ADR-0005). Knowing which of the two you are writing decides whether anything has to be done afterwards. diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index 01e7dc8..908e108 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -44,9 +44,9 @@ Excluded, and why: file-by-file origin and licence — which is what a reader who finds the directory will actually look at. `docs/public/logo.png` comes from the same repository and the same `NOTICE` covers it. -- **`docs/decisions/`, `docs/superpowers/specs/`** (this repository's own, at - the root — not `common/docs/decisions/`, which ships and is covered below). - These are this project's own record, not files copied from upstream. +- **`docs/decisions/`** (this repository's own, at the root — not + `common/docs/decisions/`, which ships and is covered below). This is this + project's own record, not files copied from upstream. - **`lib/*.sh`, `scaffold`, `scripts/*.sh`, `tests/*.bats`, `mise.toml`, `mise.lock`, `lefthook.yml`, `.github/workflows/*.yml`** (this repository's own CI and hooks, not `common/.github/workflows/` or `common/lefthook.yml`, diff --git a/docs/superpowers/plans/2026-09-13-flask-adapter.md b/docs/superpowers/plans/2026-09-13-flask-adapter.md deleted file mode 100644 index 8f1db50..0000000 --- a/docs/superpowers/plans/2026-09-13-flask-adapter.md +++ /dev/null @@ -1,668 +0,0 @@ -# Flask Adapter Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** `scaffold new demo --api flask` produces a Python project that lints, -type-checks, tests, builds an image, and answers both health probes against -each of the four services. - -**Architecture:** a new overlay adapter at `adapters/flask/` declaring -`ADAPTER_FAMILY="flask"`, generated by `uv init --bare` and completed by the -overlay's own files. Four service drivers teach the existing services how to -talk to it. Nothing outside `adapters/`, `services/`, `tests/` and two docs -files changes. - -**Tech Stack:** uv 0.12.13, Python 3.13, Flask 3, gunicorn, SQLAlchemy 2, -ruff, mypy `--strict`, pytest. - -**Spec:** `docs/superpowers/specs/2026-09-13-flask-adapter-design.md` - -## Global Constraints - -- **Comments are the exception, not the rule.** This repository just finished - three passes cutting comment density. Write a comment only when it records a - fact the code cannot: a landmine, a version pin's reason, a measured failure. - Never restate a line. The `comment-code` skill is the full rule. -- Every value below is exact and copied verbatim: `uv = "0.12.13"`, - `.python-version` = `3.13`, `--python 3.13`, image digests resolved at - implementation time. -- Task names come from ADR-0011 and are checked by `lib/lint.sh`: - `install`, `format`, `format-fix`, `lint`, `check`, `test`, `build`, - `ci-unit`, `checklist`. `format`, `lint` and `check` must not carry - `--write`, `--fix`, `-w`, `--in-place` or `--overwrite`. -- `local -r` for every local assigned once in new bash. Never file-level - `readonly` — it breaks re-sourcing into child processes. -- Every driver defines all four of `service_driver_apply`, - `service_driver_dockerfile`, `service_driver_compose_env`, - `service_driver_compose_migrate`. -- Every sed-spliced anchor is followed by a `grep` that fails loudly when the - anchor stops matching, as every existing driver does. -- Shell files carry the repository's header box (`Script:` / `Description:` / - `Author: ttncode`) and `# shellcheck shell=bash`. -- `mise run lint` and `scaffold lint` must pass at the end of every task. - ---- - -### Task 1: The flask adapter - -**Files:** -- Create: `adapters/flask/adapter.env` -- Create: `adapters/flask/mise.toml` -- Create: `adapters/flask/.python-version` -- Create: `adapters/flask/Dockerfile` -- Create: `adapters/flask/.dockerignore` -- Create: `adapters/flask/.env.example` -- Create: `adapters/flask/lefthook.fragment.yml` -- Create: `adapters/flask/README.md` -- Create: `adapters/flask/app/__init__.py` -- Create: `adapters/flask/app/health.py` -- Create: `adapters/flask/conftest.py` -- Create: `adapters/flask/tests/test_health.py` -- Modify: `tests/service.bats:142` - -**Interfaces:** -- Consumes: nothing. -- Produces: `ADAPTER_FAMILY="flask"`, which Task 2's drivers are keyed on. The - two anchors `# @DB_ENGINE@` and `# @DB_PROBE@` in `app/health.py`, and the - `raise RuntimeError("no database is configured for this project")` line - directly under `# @DB_PROBE@` — Task 2 replaces all three by exact text. - `DATABASE_URL` is the environment variable every driver writes. - -- [ ] **Step 1: Write `adapters/flask/adapter.env`** - -```sh -ADAPTER_NAME="flask" -ADAPTER_ROLE="api" -ADAPTER_TIER="A" -ADAPTER_LANGUAGE="python" -ADAPTER_FAMILY="flask" -# --bare writes pyproject.toml and nothing else; the default template adds a -# .git directory inside the app, a src/ package and a README the -# overlay would have to delete. uv comes through `mise x` because the generator -# runs in the project root, whose mise.toml pins node and pnpm only. -ADAPTER_GENERATOR='mise x uv@0.12.13 -- uv init --bare --vcs none --author-from none --no-workspace --python 3.13 "$APP_DIR"' -# The grep pair turns a resolution that reported success but wrote nothing into -# a build failure instead of an ImportError at container start. -ADAPTER_POST_GENERATE='uv add flask gunicorn && uv add --dev ruff mypy pytest && { grep -q "flask" pyproject.toml && grep -q "gunicorn" pyproject.toml; } || { echo "post-generate: flask or gunicorn missing from pyproject.toml after uv add" >&2; exit 1; }' -ADAPTER_LIVENESS_PATH="/health/live" -ADAPTER_READINESS_PATH="/health/ready" -``` - -- [ ] **Step 2: Write `adapters/flask/.python-version`** - -One line, no trailing content: - -``` -3.13 -``` - -- [ ] **Step 3: Write `adapters/flask/mise.toml`** - -```toml -# uv alone: uv resolves its own managed interpreter, so a `python` entry here -# would be installed and then ignored. .python-version is the pin uv reads. -[tools] -uv = "0.12.13" - -[tasks.install] -run = "uv sync --locked" - -[tasks.format] -run = "uv run ruff format --check ." - -[tasks."format-fix"] -run = "uv run ruff format ." - -[tasks.lint] -run = "uv run ruff check ." - -[tasks.check] -run = "uv run mypy --strict app tests" - -[tasks.test] -run = "uv run pytest -q" - -[tasks.build] -# The production sync drops ruff, which the pre-commit hook calls directly, so -# the second sync puts it back; `status=$?` keeps the production exit code. -run = "uv sync --locked --no-dev; status=$?; uv sync --locked --quiet; exit $status" - -[tasks.ci-unit] -run = [ - { task = ":install" }, - { task = ":format" }, - { task = ":lint" }, - { task = ":check" }, - { task = ":test" }, -] - -[tasks.checklist] -run = [{ task = ":ci-unit" }, { task = ":build" }] -``` - -- [ ] **Step 4: Write `adapters/flask/app/health.py`** - -Exactly this, byte for byte — Task 2 matches these lines by text: - -```python -from flask import Blueprint, Response, jsonify - -# @DB_ENGINE@ - -health = Blueprint("health", __name__) - -Reply = Response | tuple[Response, int] - - -@health.get("/health/live") -def live() -> Reply: - return jsonify(status="ok") - - -@health.get("/health/ready") -def ready() -> Reply: - try: - # @DB_PROBE@ - raise RuntimeError("no database is configured for this project") - except Exception as error: # noqa: BLE001 - return jsonify(status="unavailable", reason=str(error)), 503 -``` - -The `noqa` is required: ruff 0.16's default rule set rejects `except Exception` -(BLE001), and a readiness probe catches everything by design. - -- [ ] **Step 5: Write `adapters/flask/app/__init__.py`** - -```python -from flask import Flask - -from .health import health - - -def create_app() -> Flask: - app = Flask(__name__) - app.register_blueprint(health) - return app -``` - -- [ ] **Step 6: Write `adapters/flask/conftest.py`** - -The file is empty. It exists so pytest's default `prepend` import mode puts the -application root on `sys.path`; without it `from app import create_app` raises -`ModuleNotFoundError`. Create it with zero bytes, then record why in -`adapters/flask/README.md` — an empty file cannot carry its own comment. - -- [ ] **Step 7: Write `adapters/flask/tests/test_health.py`** - -Both tests must hold whether or not a database driver ran, so nothing asserts -`/health/ready`'s status code: - -```python -from app import create_app - - -def test_live_reports_ok() -> None: - response = create_app().test_client().get("/health/live") - assert response.status_code == 200 - assert response.get_json() == {"status": "ok"} - - -def test_ready_route_is_registered() -> None: - rules = create_app().url_map.iter_rules() - assert any(rule.rule == "/health/ready" for rule in rules) -``` - -- [ ] **Step 8: Write `adapters/flask/Dockerfile`** - -Resolve both image digests with `docker buildx imagetools inspect ` (or -`docker pull` then `docker inspect --format '{{index .RepoDigests 0}}'`) and -write them in. Do not invent a digest. - -```dockerfile -# syntax=docker/dockerfile:1 - -FROM ghcr.io/astral-sh/uv:0.12.13-python3.13-bookworm-slim@sha256: AS deps -WORKDIR /app -COPY pyproject.toml uv.lock ./ -RUN uv sync --locked --no-dev --no-install-project - -FROM python:3.13-slim@sha256: AS runtime -# @SERVICE_SETUP@ -WORKDIR /app -COPY --from=deps /app/.venv ./.venv -COPY . . -ENV PATH="/app/.venv/bin:${PATH}" -RUN useradd --create-home --uid 10001 app && chown -R app:app /app -USER app -EXPOSE 8080 -# python, not wget or curl: the slim image ships neither. -HEALTHCHECK --interval=30s --timeout=3s \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health/live')" || exit 1 -CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:create_app()"] -``` - -The `# @SERVICE_SETUP@` line must be present and exactly that — `tests/service.bats` -asserts every adapter Dockerfile carries it, and `apply_service_dockerfile` -removes it when no service was selected. - -- [ ] **Step 9: Write `adapters/flask/.dockerignore`** - -No comments; this file is self-evident. Model it on -`adapters/nestjs/.dockerignore` for style, with Python's artefacts: - -``` -.venv -__pycache__ -*.pyc -.pytest_cache -.mypy_cache -.ruff_cache -.env -.env.* -.git -``` - -- [ ] **Step 10: Write `adapters/flask/.env.example`** - -``` -# off by default: Flask's debug console executes arbitrary code from the browser -FLASK_DEBUG=0 -``` - -- [ ] **Step 11: Write `adapters/flask/lefthook.fragment.yml`** - -```yaml -pre-commit: - commands: - ruff: - glob: "*.py" - root: "@APP_ROOT@" - run: uv run ruff format {staged_files} - stage_fixed: true -``` - -- [ ] **Step 12: Write `adapters/flask/README.md`** - -Follow `adapters/nestjs/README.md` for length and tone. It must record the two -facts no file in the adapter can carry itself: why `conftest.py` is empty, and -why python is pinned in `.python-version` rather than in `mise.toml`. - -- [ ] **Step 13: Teach `tests/service.bats` the new family** - -Modify line 142 only: - -```bash - grep -Eq '^ADAPTER_FAMILY="(laravel|nest|next|flask)"$' "${adapter}adapter.env" \ -``` - -- [ ] **Step 14: Run the linters — expect one specific failure** - -Run: `mise run lint && ./scaffold lint` -Expected: `mise run lint` passes. `./scaffold lint` FAILS with four lines of -the form `: no driver for flask`, one per service. That failure is -Task 2's work and is the correct state at the end of Task 1. Every other check -must pass; if `scaffold lint` reports anything else, fix it here. - -- [ ] **Step 15: Verify the generated application really works** - -This is the gate for the whole task. Run, from a scratch directory: - -```bash -mise x uv@0.12.13 -- uv init --bare --vcs none --author-from none --no-workspace --python 3.13 demo -cd demo -cp -r /adapters/flask/app /adapters/flask/tests . -cp /adapters/flask/conftest.py /adapters/flask/.python-version . -mise x uv@0.12.13 -- bash -c 'uv add flask gunicorn && uv add --dev ruff mypy pytest' -mise x uv@0.12.13 -- bash -c 'uv run ruff format --check . && uv run ruff check . && uv run mypy --strict app tests && uv run pytest -q' -``` - -Expected: every command exits 0. This exact sequence was run while the spec was -written and passed; a failure means a file was transcribed wrong. - -- [ ] **Step 16: Commit** - -```bash -git add adapters/flask tests/service.bats -git commit -m "feat(adapters): add a flask adapter" -``` - ---- - -### Task 2: The four flask service drivers - -**Files:** -- Create: `services/shared/flask.sh` -- Create: `services/postgres/drivers/flask.sh` -- Create: `services/mysql/drivers/flask.sh` -- Create: `services/mongodb/drivers/flask.sh` -- Create: `services/redis/drivers/flask.sh` - -**Interfaces:** -- Consumes: Task 1's `adapters/flask/app/health.py` anchors — `# @DB_ENGINE@`, - `# @DB_PROBE@`, and the `raise RuntimeError(...)` line beneath the second. - `write_env_lines`, `die` and `SCAFFOLD_ROOT` come from the sourcing context, - as they do for every existing driver. -- Produces: nothing later tasks consume. - -Read `services/shared/laravel.sh`, `services/postgres/drivers/laravel.sh`, -`services/mongodb/drivers/laravel.sh` and `services/redis/drivers/laravel.sh` -first. This task is the same four shapes with Python in place of PHP. - -- [ ] **Step 1: Write `services/shared/flask.sh`** - -The parameterised SQLAlchemy body. Its contract, stated in the header: - -- `FLASK_DIALECT` — the SQLAlchemy URL scheme, e.g. `postgresql+psycopg` -- `FLASK_PACKAGES` — the DBAPI package to `uv add` alongside sqlalchemy -- `FLASK_PORT` — the host-side port written into `.env.example` -- `FLASK_COMPOSE_URL` — the same DSN against the compose network - -`service_driver_apply` does four things, each with `|| return 1`: - -```bash -service_driver_apply() { - uv add sqlalchemy "$FLASK_PACKAGES" || return 1 - - write_env_lines .env.example \ - "DATABASE_URL=${FLASK_DIALECT}://app:app@localhost:${FLASK_PORT}/app" \ - || return 1 - - splice_flask_probe \ - 'import os -from functools import cache - -from sqlalchemy import Engine, create_engine, text - - -@cache -def _engine() -> Engine: - return create_engine(os.environ["DATABASE_URL"], pool_pre_ping=True)' \ - ' with _engine().connect() as connection: - connection.execute(text("SELECT 1"))' -} -``` - -`@cache`, not an engine built inside the handler: a SQLAlchemy engine owns a -connection pool, and one per request exhausts the database's connection limit -under a polling probe — the failure `services/shared/nest.sh` records measuring -against Postgres. - -`splice_flask_probe ` is a helper in this same file, -used by all four drivers, so the anchor names and the verification live in one -place: - -```bash -splice_flask_probe() { - local -r engine="$1" probe="$2" - local -r file=app/health.py - - ENGINE="$engine" PROBE="$probe" awk ' - $0 == "# @DB_ENGINE@" { print ENVIRON["ENGINE"]; next } - $0 == " # @DB_PROBE@" { print ENVIRON["PROBE"]; next } - $0 == " raise RuntimeError(\"no database is configured for this project\")" { - print " return jsonify(status=\"ok\")"; next - } - { print } - ' "$file" > "${file}.tmp" || return 1 - mv "${file}.tmp" "$file" - - grep -q 'return jsonify(status="ok")' "$file" \ - && ! grep -q '@DB_ENGINE@' "$file" \ - && ! grep -q '@DB_PROBE@' "$file" \ - || die "could not splice the database probe into app/health.py — has the anchor moved?" -} -``` - -awk, not sed: both blocks are multi-line and carry `/`, `"` and backslashes that -sed's replacement syntax would eat. `ENVIRON`, not `-v`, for the same reason -`write_env_lines` uses it. - -The remaining three functions: - -```bash -service_driver_dockerfile() { - : -} - -service_driver_compose_env() { - printf 'DATABASE_URL: ${DATABASE_URL:-%s}\n' "$FLASK_COMPOSE_URL" -} - -service_driver_compose_migrate() { - : -} -``` - -`service_driver_migrate` printing nothing is deliberate and belongs in a -comment: this adapter ships no models and Flask has no migration tool of its -own, so there is no schema to apply. - -- [ ] **Step 2: Write `services/postgres/drivers/flask.sh`** - -```bash -# shellcheck disable=SC2034 # read by services/shared/flask.sh, sourced below -FLASK_DIALECT="postgresql+psycopg" -# the [binary] extra ships a wheel with libpq inside, so the image needs no -# system package and service_driver_dockerfile stays empty -FLASK_PACKAGES="psycopg[binary]" -FLASK_PORT="5432" -FLASK_COMPOSE_URL='postgresql+psycopg://${DB_USERNAME:-app}:${DB_PASSWORD}@database:5432/${DB_DATABASE:-app}' -# shellcheck source=/dev/null -. "${SCAFFOLD_ROOT}/services/shared/flask.sh" -``` - -Single-quote `FLASK_COMPOSE_URL`: `${DB_PASSWORD}` is compose's interpolation, -not this shell's. Check `services/postgres/drivers/nest.sh` for how -`PRISMA_COMPOSE_URL` spells the same thing and match it. - -- [ ] **Step 3: Write `services/mysql/drivers/flask.sh`** - -Same shape: - -```bash -FLASK_DIALECT="mysql+pymysql" -# pure python, so no build stage and no system package -FLASK_PACKAGES="PyMySQL" -FLASK_PORT="3306" -FLASK_COMPOSE_URL='mysql+pymysql://${DB_USERNAME:-app}:${DB_PASSWORD}@database:3306/${DB_DATABASE:-app}' -``` - -- [ ] **Step 4: Write `services/mongodb/drivers/flask.sh`** - -Self-contained, like `services/mongodb/drivers/laravel.sh`: a DSN, not -decomposed credentials. It sources `services/shared/flask.sh` only to reuse -`splice_flask_probe`, then defines its own `service_driver_apply` after the -source so the shared definition is overridden, and sets no `FLASK_*` variables. -If that ordering proves fragile, copy `splice_flask_probe`'s four lines instead -and say why in the header. - -```bash -service_driver_apply() { - uv add pymongo || return 1 - - write_env_lines .env.example \ - "DATABASE_URL=mongodb://app:app@localhost:27017/app?authSource=admin" \ - || return 1 - - splice_flask_probe \ - 'import os -from functools import cache -from typing import Any - -from pymongo import MongoClient - - -@cache -def _client() -> MongoClient[dict[str, Any]]: - return MongoClient(os.environ["DATABASE_URL"])' \ - ' _client().admin.command("ping")' -} -``` - -`MongoClient` is generic and mypy `--strict` rejects the bare name; the -parameter is the document type. If `--strict` still objects, fix the annotation -— do not add `# type: ignore`. - -`service_driver_compose_env` prints the compose-network DSN: - -```bash -service_driver_compose_env() { - printf 'DATABASE_URL: ${DATABASE_URL:-mongodb://${DB_USERNAME:-app}:${DB_PASSWORD}@database:27017/${DB_DATABASE:-app}?authSource=admin}\n' -} -``` - -- [ ] **Step 5: Write `services/redis/drivers/flask.sh`** - -Self-contained, mirroring `services/redis/drivers/laravel.sh`. A cache is not a -database: it touches no anchor in `app/health.py`, and -`service_driver_compose_migrate` prints nothing because there is no schema. - -```bash -service_driver_apply() { - uv add redis || return 1 - - write_env_lines .env.example \ - "REDIS_HOST=localhost" \ - "REDIS_PORT=6379" \ - "REDIS_PASSWORD=app" \ - || return 1 -} - -service_driver_dockerfile() { - : -} - -service_driver_compose_env() { - printf 'REDIS_HOST: cache\n' -} - -service_driver_compose_migrate() { - : -} -``` - -Check `services/redis/drivers/laravel.sh` for which variables compose already -supplies through `env_file` — only the host needs adding here, and the comment -there says so. - -- [ ] **Step 6: Run the linters — now expect them clean** - -Run: `mise run lint && ./scaffold lint` -Expected: both pass, with no `no driver for flask` lines. - -- [ ] **Step 7: Prove a real project generates and serves** - -Run each, and read the output rather than the exit code alone: - -```bash -./scripts/deploy-check.sh flask --db postgres -./scripts/deploy-check.sh flask --db mysql -./scripts/deploy-check.sh flask --db mongodb -``` - -Expected: each reports the stack healthy, including the readiness probe. A 503 -on `/health/ready` means the splice landed but the probe failed; an -`@DB_PROBE@` still in the generated `app/health.py` means the splice missed. - -- [ ] **Step 8: Commit** - -```bash -git add services -git commit -m "feat(services): teach every service to talk to flask" -``` - ---- - -### Task 3: The smoke suite - -**Files:** -- Create: `tests/new-flask.bats` - -**Interfaces:** -- Consumes: the adapter from Task 1 and the drivers from Task 2. -- Produces: nothing. - -Read `tests/new-laravel-api.bats` first — this suite is its counterpart and -should assert the same categories of thing, adjusted for Python. Do not copy -assertions that have no Python meaning. - -- [ ] **Step 1: Write `tests/new-flask.bats`** - -Use the same `setup`/`teardown` as `tests/new-laravel-api.bats`. The tests: - -1. `flask generates an app at apps/api` — `apps/api/pyproject.toml`, - `apps/api/uv.lock` and `apps/api/mise.toml` all exist. -2. `python is pinned in the app and never at the project root` — - `apps/api/.python-version` contains `3.13`, and the root `mise.toml` matches - neither `python` nor `uv`. Include a comment recording why the pin is a - `.python-version` and not a `mise.toml` tools entry; the laravel suite has - the equivalent note about php and ADR-0016. -3. `the flask lefthook fragment is merged with the common hooks` — - `.pre-commit.commands` has `ruff-apps-api` and still has `gitleaks`. -4. `the fragment resolves the app root` — `.pre-commit.commands.ruff-apps-api.root` - is `apps/api/`. -5. `a mixed-language project has no packages/types` — generate - `--api flask --web nextjs`; neither `packages/types` nor `packages-types` - exists. -6. `a mixed-language project keeps the supply-chain policy` — the same - generation still has `pnpm-workspace.yaml` with its `allowBuilds` keys. - -- [ ] **Step 2: Run it** - -Run: `bats tests/new-flask.bats` -Expected: all six pass. This suite generates real projects and takes minutes. - -- [ ] **Step 3: Record the tier** - -`ADAPTER_TIER="A"` was set on the spec's reasoning that `uv sync` compiles -nothing. Time `bats tests/new-flask.bats` and compare against -`bats tests/new-laravel-api.bats`. If flask is slower, change `ADAPTER_TIER` to -`B` in `adapters/flask/adapter.env` and say so in the commit message. If it is -faster, leave it and note the two timings in the commit message. - -- [ ] **Step 4: Commit** - -```bash -git add tests/new-flask.bats adapters/flask/adapter.env -git commit -m "test: smoke the flask adapter" -``` - ---- - -### Task 4: Documentation - -**Files:** -- Modify: `README.md` -- Modify: `docs/PROVENANCE.md` - -**Interfaces:** -- Consumes: the finished adapter. -- Produces: nothing. - -- [ ] **Step 1: Add the adapter to `README.md`** - -Find the adapter table and add a `flask` row in the existing format, with the -tier Task 3 settled on. Change nothing else — this is a one-row edit. - -- [ ] **Step 2: Add the provenance row** - -In `docs/PROVENANCE.md`'s table, one row: - -| File | Upstream path | Status | Notes | -| --- | --- | --- | --- | -| `adapters/flask/mise.toml` | `machine-learning/mise.toml` | adapted | the ADR-0011 task vocabulary and the uv shape (`uv sync --locked`, `uv run ruff`, `uv run mypy --strict`, `uv run pytest`) come from immich's only Python service; the tools differ — uv is pinned here and python is not, because uv resolves its own interpreter. | - -Then add a sentence to that row or a second row making the rest explicit: -every other file under `adapters/flask/` and all four -`services/*/drivers/flask.sh` are **original** — immich runs FastAPI, has no -Flask application, no application factory and no service-driver mechanism, so -there is no upstream file to adapt. Check each claim with `diff` against the -pinned upstream clone before writing it, as the document's own preamble -requires. - -- [ ] **Step 3: Commit** - -```bash -git add README.md docs/PROVENANCE.md -git commit -m "docs: record the flask adapter and its provenance" -``` diff --git a/docs/superpowers/plans/2026-09-16-clean-shell-and-comments.md b/docs/superpowers/plans/2026-09-16-clean-shell-and-comments.md deleted file mode 100644 index ba670a7..0000000 --- a/docs/superpowers/plans/2026-09-16-clean-shell-and-comments.md +++ /dev/null @@ -1,81 +0,0 @@ -# Clean Shell and Comments Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** every tracked file meets `comment-code`; every shell file meets the Clean Bash rules; lint keeps it that way. - -**Architecture:** five independent pull requests by area, each merged before the next branches. No behaviour change anywhere; the existing bats suites and real-project checklists are the regression net. - -**Tech Stack:** bash 5, shellcheck 0.11.0, shfmt 3.14.1, bats 1.13.0, mise. - -**Spec:** `docs/superpowers/specs/2026-09-16-clean-shell-and-comments-design.md` - -## Global Constraints - -- Rules: the `comment-code` skill (`~/.claude/skills/comment-code/SKILL.md`) and `~/.dotfiles/agentic/rules/bash.md`. Read both before editing. -- No behaviour change: error messages, stdout/stderr lines, exit codes and generated-file contents stay byte-identical. Never edit a test assertion to make a code change pass. -- No file-level `readonly`; `local -r` inside functions is fine. -- No `set -euo pipefail` added to sourced files (`lib/*.sh`, `services/**/*.sh`, `tests/helpers/*.bash`, fixture drivers). Executables (`scaffold`, `scripts/*.sh`, `common/install.sh`) must have it. -- Bats `@test` bodies keep `[ … ]`; helpers and non-test shell use `[[ ]]` / `(( ))`. -- `[ ]` → `[[ ]]`: quote the RHS of `=`, `==`, `!=` unless a glob is intended. Numeric tests → `(( ))`. -- `echo` → `printf '%s\n'` only where a variable/expansion is printed. -- Never `local x="$(cmd)"`: declare, then assign. -- Split a function over 20 lines only at a nameable seam. Linear sequences (big `case`, heredoc, render loop) stay; record each kept function and why in the task report. -- Function names that appear in `docs/` or `README.md` keep their name and contract: `add_app_service apply_adapter apply_service_dockerfile apply_service_drivers assemble_compose assert_known_tiers cmd_add cmd_new cmd_wizard config_roots generate_service_passwords init_project lint_adapters lint_services load_adapter load_toolchain_env project_name_is_usable register_config_root register_image_target role_path service_compose_key service_driver_dockerfile service_healthy sync_ci_roots tui_name_is_usable tui_prompt_name wizard_actions wizard_command wizard_new_args wizard_options wizard_prompt_for wizard_questions write_env_lines splice_flask_probe init_flask_alembic assert_nest_probe_spliced`. -- Keep: `#!` lines, `# shellcheck …` pragmas (each carrying a same-line reason), `# noqa`, `eslint-disable`, `@phpstan-`, `ponytail:` markers, and splice anchors `# @SERVICE_SETUP@`, `# @DB_ENGINE@`, `# @DB_PROBE@` (column and spelling are matched by awk/sed). -- Do not touch: `verbatim` rows of `docs/PROVENANCE.md` (`common/.editorconfig`), `common/docs/.vitepress/theme/vendor/**`, `docs/**`, `*.md`, `LICENSE`, `mise.lock`, `UPSTREAM`. -- Script headers: purpose + usage only. No `Description :`/`Author`/banner blocks. -- Commits: conventional (`chore:`, `refactor:`, `style:`), no co-author noise beyond repo convention. Each PR from a fresh branch off up-to-date `main`. -- Verification per PR: `mise run lint` clean; `mise run test-runner` exits 0. -- Real-project check (Tasks 2, 3, 5): in the scratchpad, `./scaffold new /p1 --api flask --web nextjs --db postgres --cache redis` and `./scaffold new /p2 --api nestjs --db mongodb`, then `mise run checklist` inside each. Delete both afterwards. Never run `scaffold publish`. - ---- - -### Task 1: Tooling - -**Files:** -- Modify: `mise.toml` (`[tools]`, `[tasks.lint]`) -- Modify: `services/**/*.sh`, `tests/helpers/setup.bash`, `tests/**/*.sh` (shellcheck findings only) -- Modify: every shell file (the `shfmt -w` commit) - -- [ ] **Step 1:** add `shfmt = "3.14.1"` under `[tools]`, run `mise install` so `mise.lock` records it. -- [ ] **Step 2:** change `[tasks.lint]` to lint every tracked shell file with both tools: - ```toml - [tasks.lint] - run = [ - "git ls-files -z -- scaffold '*.sh' '*.bash' '*.bats' | xargs -0 -r shellcheck", - "git ls-files -z -- scaffold '*.sh' '*.bash' '*.bats' | xargs -0 -r shfmt -i 2 -ci -d", - ] - ``` - Shrink the task's existing comment to what still holds (the `git ls-files` discovery rationale), or delete it. -- [ ] **Step 3:** run `mise run lint`; expect shellcheck findings in `services/`, `tests/` and `.bats` files. Fix each: real bugs get fixed; intentional ones (e.g. SC2016 single-quoted `$` written into generated code) get `# shellcheck disable=SCxxxx # ` on the narrowest scope. SC1091/SC1090 for sourced paths: prefer a `# shellcheck source=` directive over disable. Commit: `chore(lint): shellcheck every tracked shell file`. -- [ ] **Step 4:** `git ls-files -z -- scaffold '*.sh' '*.bash' '*.bats' | xargs -0 shfmt -i 2 -ci -w`. Verify `git diff -w --stat` is empty or whitespace-only in meaning (e.g. `die() { a; b; }` expanded to multiple lines is fine). Run `mise run test-runner`. Commit ONLY this: `style: shfmt -i 2 -ci every shell file`. -- [ ] **Step 5:** `mise run lint` exits 0; `mise run test-runner` exits 0. Push, open PR, wait for CI, merge with `gh pr merge --rebase --delete-branch` (not squash: squash would fold the pure shfmt commit into the lint fixes). - -### Task 2: Core — `scaffold`, `lib/*.sh`, `scripts/*.sh`, `common/install.sh` - -**Files:** those 17 files. `common/install.sh` ships to every generated project. - -- [ ] **Step 0:** find the shfmt commit on `main` (`git log --format='%H %s' | grep 'style: shfmt'`) and create `.git-blame-ignore-revs` with that hash under a one-line `#` comment naming its subject. A rebase merge rewrites hashes, which is why this waits until Task 1 is on `main`. -- [ ] **Step 1:** file by file, apply the Global Constraints: delete comments that fail the gate; shorten the ones that pass; convert tests/`echo`/`local`/`let`/`expr`; name positional args into locals; fix headers (`scripts/check-provenance.sh` has a banner-style `# Description :` header). -- [ ] **Step 2:** split functions over 20 lines at nameable seams. Candidates by size: `cmd_add` (68), `tui_select` (55), `record_release_age_exceptions` (55), `cmd_wizard` (51), `add_app_service` (49), `apply_adapter` (49), `cmd_update` (47), `cmd_publish` (42), `lint_adapter_env` (40), `_tui_render` (39). List all with `awk 'FNR==1{fn=""} /^[a-z_]+\(\) \{/{fn=$1;s=FNR} fn&&/^\}/{if(FNR-s-1>20)print FNR-s-1, FILENAME, fn;fn=""}' scaffold lib/*.sh scripts/*.sh common/install.sh`. -- [ ] **Step 3:** after each file or two, `mise run test-unit`; commit per file group (`refactor(lib): …`). -- [ ] **Step 4:** `mise run lint`, `mise run test-runner`, real-project check. Push, PR, CI green, `gh pr merge --squash --delete-branch`. - -### Task 3: Services — `services/**/*.sh` - -- [ ] **Step 1:** same treatment as Task 2 for the 15 driver and shared files. The splice guards (`assert_nest_probe_spliced`, the flask guard in `splice_flask_probe`) keep their logic exactly; their comments may shrink but the reason a guard checks the *absence* of the fallback must survive in one line. -- [ ] **Step 2:** `mise run lint`, `mise run test-runner`, real-project check. PR, CI, squash-merge. - -### Task 4: Tests — `tests/**` shell - -- [ ] **Step 1:** comments and helpers in `.bats`, `tests/helpers/setup.bash`, fixture `.sh`. Test names (`@test "…"`) are unchanged. Fixture drivers under `tests/fixtures/lint-services/` exist to be *wrong* in one specific way each — keep the defect each fixture is named for. -- [ ] **Step 2:** `mise run lint`, `mise run test-runner`. PR, CI, squash-merge. - -### Task 5: Non-shell comments - -**Files:** every tracked file outside Tasks 1–4 that carries comments, excluding the do-not-touch list. List with: -`git ls-files | grep -vE '\.(sh|bash|bats|md|lock)$|^docs/|^tests/|vendor/|^scaffold$|^LICENSE$|^UPSTREAM$|^common/\.editorconfig$' | xargs grep -lE '^\s*(#|//|/\*|\*|