From 34b4e97b01a9b8a24b8e42070ac524f9c63076c2 Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Wed, 16 Sep 2026 23:31:35 +0700 Subject: [PATCH 1/5] refactor(common): drop internal task-number provenance from check-paths.mjs Two comments cited this toolbox's own "task 6/9/11" development history, which a client developer reading their generated project has no context for. Keep the rationale, drop the provenance. --- common/docs/scripts/check-paths.mjs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/common/docs/scripts/check-paths.mjs b/common/docs/scripts/check-paths.mjs index bf00e52..9b5038d 100644 --- a/common/docs/scripts/check-paths.mjs +++ b/common/docs/scripts/check-paths.mjs @@ -5,11 +5,10 @@ import { extname, join, resolve } from "node:path"; const PROJECT_ROOT = resolve(import.meta.dirname, "..", ".."); // backticked strings that look like repository paths const PATH_PATTERN = /`((?:[\w.-]+\/)+[\w.-]+)`/g; -// a comment citing an adr by path, not just by prose in a backtick — the -// shape task 6 and task 9 both got wrong, in files that ship (compose.yaml, -// install.sh, an adapter's own mise.toml), while what they meant was the -// scaffold toolbox's own history, which never ships past 0000 and the -// template (see common/docs/decisions/). +// a comment citing an adr by path, not just by prose in a backtick — in files +// that ship (compose.yaml, install.sh, an adapter's own mise.toml) that path +// must resolve against this project's own docs/decisions/, not the scaffold +// toolbox's, which never ships past 0000 and the template. const ADR_REFERENCE_PATTERN = /\bdocs\/decisions\/(\d{4})[\w.-]*/g; const ADR_SCAN_EXTENSIONS = new Set([".sh", ".toml", ".mjs", ".yaml", ".yml"]); @@ -22,8 +21,7 @@ const SKIP_DIRS = new Set(["node_modules", ".git", ".vitepress"]); // root, so the root-relative path scan reports them as dead when they are not // (e.g. create-next-app's AGENTS.md and README.md). This applies to the // markdown path scan only — the ADR-citation scan must still walk apps/, or -// a dead ADR reference in an adapter's own mise.toml goes unseen again -// (the hole task 11 closed). +// a dead ADR reference in an adapter's own mise.toml goes unseen. const MARKDOWN_SKIP_DIRS = new Set([...SKIP_DIRS, "apps"]); async function filesMatching(dir, matches, skipDirs = SKIP_DIRS) { From 225d4c9156f0fff2b732fba23529980e46ff720b Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Thu, 17 Sep 2026 00:26:34 +0700 Subject: [PATCH 2/5] refactor(adapters): cut adapter and service-fragment comments to the one-line why Drop history, anecdotes and restatement from the adapter overlays and the service compose fragments; keep the pins, platform workarounds and security reasons a client developer would otherwise undo. Comment text only. --- adapters/flask/.env.example | 1 - adapters/flask/Dockerfile | 4 +-- adapters/flask/adapter.env | 9 ++---- adapters/flask/app/health.py | 5 ++- adapters/flask/mise.toml | 11 +++---- adapters/laravel-api/.dockerignore | 5 ++- adapters/laravel-api/.env.example | 3 +- adapters/laravel-api/Dockerfile | 24 +++++--------- adapters/laravel-api/adapter.env | 17 ++++------ adapters/laravel-api/mise.toml | 23 ++++--------- adapters/laravel-api/routes/health.php | 22 ++++--------- adapters/laravel-inertia/.dockerignore | 9 ++---- adapters/laravel-inertia/.env.example | 3 +- adapters/laravel-inertia/Dockerfile | 32 ++++++------------- adapters/laravel-inertia/adapter.env | 16 ++++------ adapters/laravel-inertia/mise.toml | 29 +++++------------ adapters/laravel-inertia/routes/health.php | 22 ++++--------- adapters/nestjs/.env.example | 1 - adapters/nestjs/.prettierignore | 2 -- adapters/nestjs/Dockerfile | 17 +++------- adapters/nestjs/Dockerfile.workspace | 28 ++++++---------- adapters/nestjs/adapter.env | 14 +++----- adapters/nestjs/mise.toml | 16 +++------- .../nestjs/src/health/health.controller.ts | 27 ++++------------ adapters/nextjs/Dockerfile | 17 +++------- adapters/nextjs/Dockerfile.workspace | 22 ++++--------- adapters/nextjs/adapter.env | 10 ++---- adapters/nextjs/lefthook.fragment.yml | 2 +- adapters/nextjs/mise.toml | 10 ++---- adapters/nextjs/next.config.ts | 4 +-- services/mongodb/compose.fragment.yaml | 4 +-- services/redis/compose.fragment.yaml | 6 ++-- 32 files changed, 124 insertions(+), 291 deletions(-) diff --git a/adapters/flask/.env.example b/adapters/flask/.env.example index 9a32ea8..0812081 100644 --- a/adapters/flask/.env.example +++ b/adapters/flask/.env.example @@ -1,3 +1,2 @@ # off by default: Flask's debug console executes arbitrary code from the browser FLASK_DEBUG=0 -# the database variables are written by the selected service's driver diff --git a/adapters/flask/Dockerfile b/adapters/flask/Dockerfile index 9297939..c949d87 100644 --- a/adapters/flask/Dockerfile +++ b/adapters/flask/Dockerfile @@ -1,8 +1,6 @@ # syntax=docker/dockerfile:1 -# python3.13-bookworm-slim is not published for this uv release — only -# python3.13-trixie-slim is; python:3.13-slim below already resolves to -# slim-trixie, so this keeps both stages on the same Debian release. +# trixie, matching python:3.13-slim below; uv publishes no bookworm-slim tag. FROM ghcr.io/astral-sh/uv:0.12.13-python3.13-trixie-slim@sha256:c0ba49559fc5622531fd05a5747b52afb49ffa883574bbf8eb719ebd103efb84 AS deps WORKDIR /app COPY pyproject.toml uv.lock .python-version ./ diff --git a/adapters/flask/adapter.env b/adapters/flask/adapter.env index 977afe5..a3dd8d8 100644 --- a/adapters/flask/adapter.env +++ b/adapters/flask/adapter.env @@ -3,13 +3,10 @@ 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. +# --bare: the default template adds .git, a src/ package and a README. `mise x`: +# the project root's mise.toml pins only node and pnpm. 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. +# The grep pair fails a `uv add` that reports success but writes nothing. 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" diff --git a/adapters/flask/app/health.py b/adapters/flask/app/health.py index ec9a52d..b5db359 100644 --- a/adapters/flask/app/health.py +++ b/adapters/flask/app/health.py @@ -18,8 +18,7 @@ def ready() -> Reply: # @DB_PROBE@ raise RuntimeError("no database is configured for this project") except Exception as error: # noqa: BLE001 - # Logged, not returned: a driver's connection error names the host, - # port, user and database, and /health/ready is unauthenticated. An - # orchestrator reads the status code and nothing else. + # Logged, not returned: the error names host and user, and this route is + # unauthenticated. current_app.logger.warning("readiness probe failed: %s", error) return jsonify(status="unavailable"), 503 diff --git a/adapters/flask/mise.toml b/adapters/flask/mise.toml index 286fdf4..a42c2d3 100644 --- a/adapters/flask/mise.toml +++ b/adapters/flask/mise.toml @@ -1,5 +1,4 @@ -# 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. +# No python: uv installs its own from .python-version and would ignore mise's. [tools] uv = "0.12.13" @@ -22,14 +21,12 @@ run = "uv run mypy --strict app tests" 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. +# The second sync restores ruff for the pre-commit hook; `status=$?` keeps the +# production sync's exit code. run = "uv sync --locked --no-dev; status=$?; uv sync --locked --quiet; exit $status" [tasks.migrate] -# alembic.ini exists only for postgres/mysql, app/migrate.py only for mongodb; -# --db none or a cache-only project ships neither, so this must not assume one -# of the two is always there. +# alembic.ini: postgres/mysql; app/migrate.py: mongodb; neither: --db none. run = "if [ -f alembic.ini ]; then uv run alembic upgrade head; elif [ -f app/migrate.py ]; then uv run python -m app.migrate; else echo 'nothing to migrate'; fi" [tasks.ci-unit] diff --git a/adapters/laravel-api/.dockerignore b/adapters/laravel-api/.dockerignore index acffaad..ccf162c 100644 --- a/adapters/laravel-api/.dockerignore +++ b/adapters/laravel-api/.dockerignore @@ -9,7 +9,6 @@ node_modules storage/logs storage/framework/cache -# both the directory and its manifests must survive: composer writes them -# locally, naming dev providers a --no-dev image lacks, but artisan also -# refuses to boot without the directory itself present +# the manifests, not the directory: local ones name dev providers a --no-dev +# image lacks, and artisan will not boot without the directory bootstrap/cache/*.php diff --git a/adapters/laravel-api/.env.example b/adapters/laravel-api/.env.example index f521bd1..a1ed573 100644 --- a/adapters/laravel-api/.env.example +++ b/adapters/laravel-api/.env.example @@ -3,6 +3,5 @@ APP_KEY= # off by default: Laravel's debug page leaks APP_KEY and DB_PASSWORD on a 500 APP_DEBUG=false APP_URL=http://localhost:8000 -# the database variables are written by the selected service's driver -# the readiness probe logs its failure reason; storage/logs is unreachable in a container +# storage/logs is unreadable in a container, and the readiness probe logs there LOG_CHANNEL=stderr diff --git a/adapters/laravel-api/Dockerfile b/adapters/laravel-api/Dockerfile index b7573d5..bc4b192 100644 --- a/adapters/laravel-api/Dockerfile +++ b/adapters/laravel-api/Dockerfile @@ -6,11 +6,8 @@ COPY composer.json composer.lock ./ RUN composer install --no-dev --no-scripts --no-interaction \ --prefer-dist --optimize-autoloader -# FrankenPHP, because php-fpm speaks FastCGI and this stack has no web server -# in front of it: nothing served HTTP at all, and compose published a dead -# port. Documented as a first-class server in laravel.com/docs/13.x/deployment, -# part of the PHP Foundation since May 2025, and what Laravel Cloud runs. -# Alpine specifically: services/mongodb/drivers/laravel.sh emits `apk add`. +# FrankenPHP: php-fpm speaks FastCGI and nothing fronts it. +# Alpine: the mongodb driver emits `apk add`. FROM dunglas/frankenphp:1.12.7-php8.3-alpine@sha256:049b8d8356efceb93c91ed42866de890534310bcef4ad4dde902029e4a0d20c3 AS runtime RUN docker-php-ext-install opcache # @SERVICE_SETUP@ @@ -18,15 +15,12 @@ WORKDIR /var/www COPY --from=vendor /app/vendor ./vendor COPY . . COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini -# :8080 is how frankenphp listens on an unprivileged port and declines to -# provision TLS, which belongs to whatever proxy this lands behind. +# A bare port: unprivileged, and no TLS, which belongs to the proxy in front. ENV SERVER_NAME=:8080 -# laravel's default `stack` channel writes to storage/logs, which nothing -# reads in a container; the readiness probe's failure reason is logged. +# Nothing reads storage/logs in a container; the readiness probe logs failures. ENV LOG_CHANNEL=stderr -# Caddy writes here and will not start if it may not. The app tree needs the -# same: COPY runs as root, the server runs as www-data, and the first request -# that compiles a blade view or writes a log fails without this. +# Caddy will not start without writable config and data dirs, and www-data +# must write storage/ and bootstrap/cache/ that COPY left owned by root. ENV XDG_CONFIG_HOME=/config XDG_DATA_HOME=/data RUN mkdir -p /config /data \ && chown -R www-data:www-data /config /data \ @@ -35,8 +29,6 @@ USER www-data EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -qO- http://localhost:8080/up || exit 1 -# CMD replaces the base image's default args entirely rather than extending -# them, and those args are what point frankenphp at the Caddyfile that -# defines the :8080 site block; without them it starts only the admin API on -# 127.0.0.1:2019 and nothing ever listens on 8080. +# The base image's args, restated: CMD replaces them, and without the Caddyfile +# only the admin API starts and nothing listens on 8080. CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] diff --git a/adapters/laravel-api/adapter.env b/adapters/laravel-api/adapter.env index 3df312c..5479615 100644 --- a/adapters/laravel-api/adapter.env +++ b/adapters/laravel-api/adapter.env @@ -2,19 +2,14 @@ ADAPTER_NAME="laravel-api" ADAPTER_ROLE="api" ADAPTER_TIER="A" ADAPTER_LANGUAGE="php" -# The two laravel adapters need identical service wiring, so drivers are keyed -# on this rather than on the adapter name: one file per family, not two files -# holding the same thing and drifting apart. +# Drivers are keyed on the family, so both laravel adapters share one set. ADAPTER_FAMILY="laravel" -# Unpinned, composer silently resolves whichever major the local php allows -# (13.x needs ^8.3, 12.x needs ^8.2). Pinned, with mise.toml's install guard -# holding the same floor. See docs/decisions/0016. +# Unpinned, composer picks whatever major the local php allows; mise.toml's +# install guard holds the same php floor (docs/decisions/0016). ADAPTER_GENERATOR='composer create-project laravel/laravel:^13.0 "$APP_DIR" --no-interaction --prefer-dist' -# the skeleton ships phpunit and pint, but nothing for the check task. The sed -# wires routes/health.php (copied in separately by apply_adapter) into -# bootstrap/app.php, since laravel auto-loads neither; the grep pair after it -# turns a skeleton reformat that breaks the sed into a build failure instead -# of a silent 404. +# larastan backs the check task, which the skeleton leaves empty. +# The sed wires routes/health.php into bootstrap/app.php; the grep pair fails +# the build if a skeleton change breaks it. ADAPTER_POST_GENERATE='composer require --dev larastan/larastan phpstan/phpstan --no-interaction && sed -i "s|health: '"'"'/up'"'"',|health: '"'"'/up'"'"',\n then: function (): void {\n require __DIR__.'"'"'/../routes/health.php'"'"';\n },|" bootstrap/app.php && { grep -q "then: function (): void {" bootstrap/app.php && grep -q "require __DIR__.'"'"'/../routes/health.php'"'"';" bootstrap/app.php; } || { echo "post-generate: health route wiring missing from bootstrap/app.php — the laravel skeleton likely changed its withRouting shape; update the sed pattern in ADAPTER_POST_GENERATE to match" >&2; exit 1; }' ADAPTER_LIVENESS_PATH="/up" ADAPTER_READINESS_PATH="/health/ready" diff --git a/adapters/laravel-api/mise.toml b/adapters/laravel-api/mise.toml index d7610cf..ef76236 100644 --- a/adapters/laravel-api/mise.toml +++ b/adapters/laravel-api/mise.toml @@ -1,6 +1,5 @@ -# composer only: mise's php backends compile from source and fail here, so php -# comes from the system (ADR-0016). It stays declared in this file alone — the -# root mise.toml never learns this app is php. +# composer only: mise's php backends compile from source and fail, so php comes +# from the system (ADR-0016). [tools] "ubi:composer/composer" = "2.10.2" @@ -17,8 +16,7 @@ run = "./vendor/bin/pint --test" [tasks."format-fix"] run = "./vendor/bin/pint" -# pint is both formatter and linter; an honest duplicate beats inventing a -# second linter to fill the slot +# pint is formatter and linter both [tasks.lint] run = "./vendor/bin/pint --test" @@ -26,27 +24,18 @@ run = "./vendor/bin/pint --test" run = "./vendor/bin/phpstan analyse --no-progress --memory-limit=512M" [tasks.test] -# APP_KEY lives in the uncommitted .env, so a fresh checkout has none and every -# test that boots the app dies on MissingAppKeyException. A throwaway key beats -# committing a real one into every project this toolbox makes. +# APP_KEY lives in the uncommitted .env; a throwaway key lets tests boot the app. run = "APP_KEY=base64:$(head -c 32 /dev/urandom | base64) ./vendor/bin/phpunit" [tasks.build] -# --no-dev strips pint, phpstan and phpunit from vendor/, and the pre-commit -# hook calls ./vendor/bin/pint directly — so the next commit fails on a missing -# binary. Put them back; `status=$?` keeps the production install's exit code. -# The real artifact is the Dockerfile's own --no-dev install. +# The second install restores pint for the pre-commit hook; `status=$?` keeps +# the --no-dev install's exit code. The image does its own --no-dev install. run = "composer.phar install --no-dev --optimize-autoloader --classmap-authoritative; status=$?; composer.phar install --no-interaction --prefer-dist --quiet; exit $status" [tasks.migrate] run = "php artisan migrate --force" [tasks.ci-unit] -# :lint runs the same pint invocation as :format here, so this repeats about -# half a second of work. Listed anyway: every other adapter runs all five, and -# an omission that reads as deliberate is indistinguishable from one that is -# not — the day pint stops being both halves, the missing call would go -# unnoticed in CI. run = [ { task = ":install" }, { task = ":format" }, diff --git a/adapters/laravel-api/routes/health.php b/adapters/laravel-api/routes/health.php index 8d76da3..73feb2f 100644 --- a/adapters/laravel-api/routes/health.php +++ b/adapters/laravel-api/routes/health.php @@ -2,27 +2,17 @@ use Illuminate\Support\Facades\Route; -// Registered from bootstrap/app.php's withRouting(then: ...) — laravel loads -// only routes/web.php and routes/api.php on its own, so a file dropped here -// with nothing pointing at it would 404 forever. -// -// The probe is written by the selected service's driver: laravel has no -// provider-agnostic read, so a SQL connection gets `select 1` and mongodb -// gets a ping command. A project generated with --db none keeps the -// anchor's fallback and reports 503, because there is nothing here that -// could honestly report ready. +// Loaded by bootstrap/app.php's withRouting(then: ...); laravel loads only +// web.php and api.php itself. The database driver splices in the probe; with +// --db none the throw stands and readiness reports 503. Route::get('/health/ready', function () { try { // @DB_PROBE@ throw new RuntimeException('no database is configured for this project'); } catch (Throwable $e) { - // Logged, not returned: a driver's connection error names the host, - // port, user and database, and /health/ready is unauthenticated. An - // orchestrator reads the status code and nothing else. logger(), not - // the Log facade: an import here would sit between the two `use` - // lines services/mongodb/drivers/laravel.sh splices, and pint's - // ordered_imports would then fail the generated project's own format - // task. + // Logged, not returned: the error names host and user, and this route is + // unauthenticated. logger(), not Log: another `use` breaks pint's import + // order once the driver splices in DB. logger()->warning('readiness probe failed: '.$e->getMessage()); return response()->json(['status' => 'unavailable'], 503); diff --git a/adapters/laravel-inertia/.dockerignore b/adapters/laravel-inertia/.dockerignore index 2a52b6a..44fba9e 100644 --- a/adapters/laravel-inertia/.dockerignore +++ b/adapters/laravel-inertia/.dockerignore @@ -9,12 +9,9 @@ node_modules storage/logs storage/framework/cache -# both the directory and its manifests must survive: composer writes them -# locally, naming dev providers a --no-dev image lacks, but artisan also -# refuses to boot without the directory itself present — for this adapter the -# failure surfaces in vite's wayfinder plugin, several stages from the cause +# the manifests, not the directory: local ones name dev providers a --no-dev +# image lacks, and artisan will not boot without the directory bootstrap/cache/*.php -# a build context is not a git tree: without this, a host `npm run build` -# ships over what the assets stage just built +# a host `npm run build` would overwrite the assets stage's output public/build diff --git a/adapters/laravel-inertia/.env.example b/adapters/laravel-inertia/.env.example index ea0ecbd..bb9ea55 100644 --- a/adapters/laravel-inertia/.env.example +++ b/adapters/laravel-inertia/.env.example @@ -3,7 +3,6 @@ APP_KEY= # off by default: Laravel's debug page leaks APP_KEY and DB_PASSWORD on a 500 APP_DEBUG=false APP_URL=http://localhost:8000 -# the database variables are written by the selected service's driver VITE_APP_NAME="${APP_NAME}" -# the readiness probe logs its failure reason; storage/logs is unreachable in a container +# storage/logs is unreadable in a container, and the readiness probe logs there LOG_CHANNEL=stderr diff --git a/adapters/laravel-inertia/Dockerfile b/adapters/laravel-inertia/Dockerfile index fedd27e..5a7ca31 100644 --- a/adapters/laravel-inertia/Dockerfile +++ b/adapters/laravel-inertia/Dockerfile @@ -6,12 +6,8 @@ COPY composer.json composer.lock ./ RUN composer install --no-dev --no-scripts --no-interaction \ --prefer-dist --optimize-autoloader -# @laravel/vite-plugin-wayfinder shells out to `php artisan` during the vite -# build itself, so this stage needs both php and node, and only one base can -# be pinned by digest. Pin php: it decides whether artisan runs at all (the -# app declares php: ^8.3), while node here only compiles assets and never -# reaches the runtime image, which inherits nothing from this stage but -# public/build. +# wayfinder runs `php artisan` inside the vite build, so this stage needs php and +# node. Only one base can be digest-pinned: php, which decides whether artisan runs. FROM php:8.3.21-fpm-alpine@sha256:d2170b0f8da574062b289566a05f25ab57173315a356c9f7519b5e444ae96dac AS assets RUN apk add --no-cache nodejs npm WORKDIR /app @@ -21,11 +17,8 @@ COPY --from=vendor /app/vendor ./vendor COPY . . RUN npm run build -# FrankenPHP, because php-fpm speaks FastCGI and this stack has no web server -# in front of it: nothing served HTTP at all, and compose published a dead -# port. Documented as a first-class server in laravel.com/docs/13.x/deployment, -# part of the PHP Foundation since May 2025, and what Laravel Cloud runs. -# Alpine specifically: services/mongodb/drivers/laravel.sh emits `apk add`. +# FrankenPHP: php-fpm speaks FastCGI and nothing fronts it. +# Alpine: the mongodb driver emits `apk add`. FROM dunglas/frankenphp:1.12.7-php8.3-alpine@sha256:049b8d8356efceb93c91ed42866de890534310bcef4ad4dde902029e4a0d20c3 AS runtime RUN docker-php-ext-install opcache # @SERVICE_SETUP@ @@ -34,15 +27,12 @@ COPY --from=vendor /app/vendor ./vendor COPY . . COPY --from=assets /app/public/build ./public/build COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini -# :8080 is how frankenphp listens on an unprivileged port and declines to -# provision TLS, which belongs to whatever proxy this lands behind. +# A bare port: unprivileged, and no TLS, which belongs to the proxy in front. ENV SERVER_NAME=:8080 -# laravel's default `stack` channel writes to storage/logs, which nothing -# reads in a container; the readiness probe's failure reason is logged. +# Nothing reads storage/logs in a container; the readiness probe logs failures. ENV LOG_CHANNEL=stderr -# Caddy writes here and will not start if it may not. The app tree needs the -# same: COPY runs as root, the server runs as www-data, and the first request -# that compiles a blade view or writes a log fails without this. +# Caddy will not start without writable config and data dirs, and www-data +# must write storage/ and bootstrap/cache/ that COPY left owned by root. ENV XDG_CONFIG_HOME=/config XDG_DATA_HOME=/data RUN mkdir -p /config /data \ && chown -R www-data:www-data /config /data \ @@ -51,8 +41,6 @@ USER www-data EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -qO- http://localhost:8080/up || exit 1 -# CMD replaces the base image's default args entirely rather than extending -# them, and those args are what point frankenphp at the Caddyfile that -# defines the :8080 site block; without them it starts only the admin API on -# 127.0.0.1:2019 and nothing ever listens on 8080. +# The base image's args, restated: CMD replaces them, and without the Caddyfile +# only the admin API starts and nothing listens on 8080. CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] diff --git a/adapters/laravel-inertia/adapter.env b/adapters/laravel-inertia/adapter.env index 92801fd..0aacc64 100644 --- a/adapters/laravel-inertia/adapter.env +++ b/adapters/laravel-inertia/adapter.env @@ -3,17 +3,13 @@ ADAPTER_ROLE="app" ADAPTER_TIER="B" ADAPTER_LANGUAGE="php" ADAPTER_FAMILY="laravel" -# pinned to a commit so create-project resolves the same tree every run; move -# to a tag once laravel/vue-starter-kit targets laravel 13. SHELL_VERBOSITY=-1 -# satisfies the starter kit's install:features prompt, which --no-interaction -# does not reach; COMPOSER_PROCESS_TIMEOUT=900 because a cold cache exceeds -# the 300s default — widened, not disabled, so a real hang still ends the run. +# A commit pin until laravel/vue-starter-kit tags a laravel 13 release. +# SHELL_VERBOSITY=-1 answers a prompt --no-interaction misses; a cold cache +# outlasts composer's 300s timeout, and a real hang must still end the run. ADAPTER_GENERATOR='SHELL_VERBOSITY=-1 COMPOSER_PROCESS_TIMEOUT=900 composer create-project "laravel/vue-starter-kit:dev-main#11a7368240d7d9146415aa6bfc858a90b6ce8490" "$APP_DIR" --no-interaction --prefer-dist' -# rm -rf .github: the starter kit's own inert dependabot.yml still fails -# zizmor's audit with exit 13. The sed wires routes/health.php (copied in -# separately by apply_adapter) into bootstrap/app.php, since laravel -# auto-loads neither; the grep pair after it turns a skeleton reformat that -# breaks the sed into a build failure instead of a silent 404. +# .github goes: the starter kit's dependabot.yml fails zizmor. +# The sed wires routes/health.php into bootstrap/app.php; the grep pair fails +# the build if a skeleton change breaks it. ADAPTER_POST_GENERATE='rm -rf .github && sed -i "s|health: '"'"'/up'"'"',|health: '"'"'/up'"'"',\n then: function (): void {\n require __DIR__.'"'"'/../routes/health.php'"'"';\n },|" bootstrap/app.php && { grep -q "then: function (): void {" bootstrap/app.php && grep -q "require __DIR__.'"'"'/../routes/health.php'"'"';" bootstrap/app.php; } || { echo "post-generate: health route wiring missing from bootstrap/app.php — the laravel skeleton likely changed its withRouting shape; update the sed pattern in ADAPTER_POST_GENERATE to match" >&2; exit 1; }' ADAPTER_LIVENESS_PATH="/up" ADAPTER_READINESS_PATH="/health/ready" diff --git a/adapters/laravel-inertia/mise.toml b/adapters/laravel-inertia/mise.toml index fa0850e..8671dc7 100644 --- a/adapters/laravel-inertia/mise.toml +++ b/adapters/laravel-inertia/mise.toml @@ -1,18 +1,12 @@ -# php itself is not pinned (ADR-0016). node is here because vite builds the -# assets: picking laravel to get one app instead of two does not avoid node, -# and that is better learned here than in CI. +# php comes from the system (ADR-0016); node builds the vite assets. [tools] "ubi:composer/composer" = "2.10.2" node = "24.15.0" [tasks.install] # mise installs composer as composer.phar, not composer -# `npm run build` belongs here, not in check or test. It produces two -# gitignored artifacts nothing else generates: resources/js/{actions,routes}, -# which types:check compiles against, and public/build/manifest.json, without -# which every test rendering an inertia page 500s. Both survive locally from -# the last run, so the app passes every local run and never passes a clean CI -# checkout. Same order the starter kit's own CI uses. +# `npm run build` here: types:check needs resources/js/{actions,routes} and page +# tests need public/build/manifest.json, both gitignored. run = [ "php -r 'exit(version_compare(PHP_VERSION, \"8.3.0\", \">=\") ? 0 : 1);' || { echo \"laravel-inertia requires system php >= 8.3.0 (found $(php -r 'echo PHP_VERSION;' 2>/dev/null || echo 'no php on PATH')); install php 8.3 or newer via your OS package manager (e.g. apt install php8.3-cli) or https://php.net, then re-run mise install\" >&2; exit 1; }", "composer.phar install --no-interaction --prefer-dist", @@ -20,18 +14,14 @@ run = [ "npm run build", ] -# vite-plus's `vp check` covers formatting and linting in one command; there is -# no separate format:check to call. Scoped to resources/ because an unscoped run -# also reformats this file. +# `vp check` formats and lints; unscoped, it also rewrites this file. [tasks.format] run = ["./vendor/bin/pint --test", "npm run check -- resources"] [tasks."format-fix"] run = ["./vendor/bin/pint", "npm run check:fix -- resources"] -# both halves of lint are already formatters that lint: pint on the php side, -# vp on the frontend. Same honest duplicate as laravel-api rather than a second -# linter invented to fill the slot. +# pint and vp are formatter and linter both [tasks.lint] run = ["./vendor/bin/pint --test", "npm run check -- resources"] @@ -39,15 +29,12 @@ run = ["./vendor/bin/pint --test", "npm run check -- resources"] run = ["./vendor/bin/phpstan analyse --no-progress --memory-limit=512M", "npm run types:check"] [tasks.test] -# Same uncommitted-.env problem as laravel-api: no APP_KEY means every test -# that boots the app dies on MissingAppKeyException. +# APP_KEY lives in the uncommitted .env; a throwaway key lets tests boot the app. run = "APP_KEY=base64:$(head -c 32 /dev/urandom | base64) ./vendor/bin/phpunit" [tasks.build] -# --no-dev strips pint, phpstan and phpunit from vendor/, and the pre-commit -# hook calls ./vendor/bin/pint directly — so the next commit fails on a missing -# binary. Put them back; `status=$?` keeps the production install's exit code. -# The real artifact is the Dockerfile's own --no-dev install. +# The second install restores pint for the pre-commit hook; `status=$?` keeps +# the --no-dev install's exit code. The image does its own --no-dev install. run = [ "composer.phar install --no-dev --optimize-autoloader --classmap-authoritative; status=$?; composer.phar install --no-interaction --prefer-dist --quiet; exit $status", "npm run build", diff --git a/adapters/laravel-inertia/routes/health.php b/adapters/laravel-inertia/routes/health.php index 8d76da3..73feb2f 100644 --- a/adapters/laravel-inertia/routes/health.php +++ b/adapters/laravel-inertia/routes/health.php @@ -2,27 +2,17 @@ use Illuminate\Support\Facades\Route; -// Registered from bootstrap/app.php's withRouting(then: ...) — laravel loads -// only routes/web.php and routes/api.php on its own, so a file dropped here -// with nothing pointing at it would 404 forever. -// -// The probe is written by the selected service's driver: laravel has no -// provider-agnostic read, so a SQL connection gets `select 1` and mongodb -// gets a ping command. A project generated with --db none keeps the -// anchor's fallback and reports 503, because there is nothing here that -// could honestly report ready. +// Loaded by bootstrap/app.php's withRouting(then: ...); laravel loads only +// web.php and api.php itself. The database driver splices in the probe; with +// --db none the throw stands and readiness reports 503. Route::get('/health/ready', function () { try { // @DB_PROBE@ throw new RuntimeException('no database is configured for this project'); } catch (Throwable $e) { - // Logged, not returned: a driver's connection error names the host, - // port, user and database, and /health/ready is unauthenticated. An - // orchestrator reads the status code and nothing else. logger(), not - // the Log facade: an import here would sit between the two `use` - // lines services/mongodb/drivers/laravel.sh splices, and pint's - // ordered_imports would then fail the generated project's own format - // task. + // Logged, not returned: the error names host and user, and this route is + // unauthenticated. logger(), not Log: another `use` breaks pint's import + // order once the driver splices in DB. logger()->warning('readiness probe failed: '.$e->getMessage()); return response()->json(['status' => 'unavailable'], 503); diff --git a/adapters/nestjs/.env.example b/adapters/nestjs/.env.example index b722a81..df09d75 100644 --- a/adapters/nestjs/.env.example +++ b/adapters/nestjs/.env.example @@ -1,2 +1 @@ PORT=3001 -# the database variables are written by the selected service's driver diff --git a/adapters/nestjs/.prettierignore b/adapters/nestjs/.prettierignore index 0245695..0730306 100644 --- a/adapters/nestjs/.prettierignore +++ b/adapters/nestjs/.prettierignore @@ -1,4 +1,2 @@ pnpm-lock.yaml -# nest build writes here; unignored, format fails on the previous run's own -# output once build has run once dist diff --git a/adapters/nestjs/Dockerfile b/adapters/nestjs/Dockerfile index dedbc70..407cc5a 100644 --- a/adapters/nestjs/Dockerfile +++ b/adapters/nestjs/Dockerfile @@ -3,9 +3,8 @@ FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS build WORKDIR /app RUN corepack enable -# pnpm-workspace.yaml carries the allowBuilds decisions ADR-0017 records — -# without it in the build context, pnpm has nothing to check unrs-resolver's -# postinstall build against and refuses non-interactively. +# pnpm-workspace.yaml holds the allowBuilds policy (ADR-0017); without it pnpm +# refuses unrs-resolver's postinstall build non-interactively. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile COPY . . @@ -17,17 +16,11 @@ WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist -# `pris[m]a`, not `prisma`: a bracket glob that matches nothing copies -# nothing instead of failing the build, which a bare `prisma` would do on a -# --db none project that never ran the driver that creates this directory. -# Needed for the migrate service (see services/shared/nest.sh) to find its -# schema — schema.prisma is not itself an artifact `nest build` produces, so -# nothing else in this image carries it forward from the build context. +# The migrate service reads the schema from here. `pris[m]a` copies nothing on a +# --db none project, where a bare `prisma` would fail the build. COPY --from=build /app/pris[m]a ./prisma USER node -# 8080 because that is the container port common/compose.yaml publishes, and -# nothing rewrites it — an adapter listening anywhere else publishes a dead -# port. Nest reads PORT in main.ts's `app.listen(process.env.PORT ?? 3000)`. +# The container port compose.yaml publishes; main.ts reads PORT. ENV PORT=8080 EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ diff --git a/adapters/nestjs/Dockerfile.workspace b/adapters/nestjs/Dockerfile.workspace index 734101b..3081948 100644 --- a/adapters/nestjs/Dockerfile.workspace +++ b/adapters/nestjs/Dockerfile.workspace @@ -1,14 +1,12 @@ # syntax=docker/dockerfile:1 -# used instead of Dockerfile when every app in the project is typescript: -# apps/api is then a pnpm workspace member with no manifests of its own — -# see docs/PROVENANCE.md and immich's server/Dockerfile, which this follows. +# Used instead of Dockerfile when every app is typescript: apps/api is then a +# pnpm workspace member with no manifests of its own. FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS build WORKDIR /app RUN corepack enable -# pnpm-workspace.yaml carries the allowBuilds decisions ADR-0017 records — -# without it in the build context, pnpm has nothing to check unrs-resolver's -# postinstall build against and refuses non-interactively. +# pnpm-workspace.yaml holds the allowBuilds policy (ADR-0017); without it pnpm +# refuses unrs-resolver's postinstall build non-interactively. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/@APP_FILTER@/package.json apps/@APP_FILTER@/package.json RUN pnpm --filter @APP_FILTER@ install --frozen-lockfile @@ -17,10 +15,8 @@ WORKDIR /app/apps/@APP_FILTER@ # @SERVICE_SETUP@ RUN pnpm exec nest build WORKDIR /app -# pruning at the workspace root, not from apps/api: most of its dependencies -# are hoisted there, so a prune scoped to the app alone would leave dev -# dependencies behind. Same TTY-abort as a relink (ADR-0017) — pruning a -# shared node_modules needs the same non-interactive answer. +# At the root, where dependencies are hoisted; a purge prompt would abort +# without a TTY (ADR-0017). RUN pnpm prune --prod --config.confirm-modules-purge=false FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS runtime @@ -29,17 +25,11 @@ ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/apps/@APP_FILTER@/node_modules ./apps/@APP_FILTER@/node_modules COPY --from=build /app/apps/@APP_FILTER@/dist ./apps/@APP_FILTER@/dist -# `pris[m]a`, not `prisma`: a bracket glob that matches nothing copies -# nothing instead of failing the build, which a bare `prisma` would do on a -# --db none project that never ran the driver that creates this directory. -# Needed for the migrate service (see services/shared/nest.sh) to find its -# schema — schema.prisma is not itself an artifact `nest build` produces, so -# nothing else in this image carries it forward from the build context. +# The migrate service reads the schema from here. `pris[m]a` copies nothing on a +# --db none project, where a bare `prisma` would fail the build. COPY --from=build /app/apps/@APP_FILTER@/pris[m]a ./apps/@APP_FILTER@/prisma USER node -# 8080 because that is the container port common/compose.yaml publishes, and -# nothing rewrites it — an adapter listening anywhere else publishes a dead -# port. Nest reads PORT in main.ts's `app.listen(process.env.PORT ?? 3000)`. +# The container port compose.yaml publishes; main.ts reads PORT. ENV PORT=8080 EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s \ diff --git a/adapters/nestjs/adapter.env b/adapters/nestjs/adapter.env index 5d55c19..f97f413 100644 --- a/adapters/nestjs/adapter.env +++ b/adapters/nestjs/adapter.env @@ -3,17 +3,11 @@ ADAPTER_ROLE="api" ADAPTER_TIER="A" ADAPTER_LANGUAGE="typescript" ADAPTER_FAMILY="nest" -# v11, not @latest: v12's template swaps jest for vitest with no -# decorator-metadata transform, so Nest's DI never injects AppService and the -# generated test fails on every fresh project. Before moving off this pin, -# generate with the new major and confirm `//apps/api:ci-unit` passes clean. +# v11: v12's vitest template lacks decorator metadata, so DI fails the generated +# test. Confirm `//apps/api:ci-unit` passes before moving the pin. ADAPTER_GENERATOR='pnpm dlx @nestjs/cli@11 new "$APP_DIR" --package-manager pnpm --skip-git' -# main.ts's un-awaited bootstrap() trips --max-warnings 0. HealthModule is -# wired here rather than shipped in app.module.ts because that file is the -# generator's own output; the grep pair after the sed calls turns a generator -# reformat that breaks the sed into a build failure instead of a silent 404 -# on /health/live. +# `void bootstrap()` satisfies --max-warnings 0. app.module.ts is generator +# output, so HealthModule is wired by sed; the grep pair fails a sed that missed. ADAPTER_POST_GENERATE='sed -i "s/^bootstrap();$/void bootstrap();/" src/main.ts && sed -i "1i import { HealthModule } from '"'"'./health/health.module'"'"';" src/app.module.ts && sed -i "s/imports: \[\]/imports: [HealthModule]/" src/app.module.ts && { grep -q "import { HealthModule } from '"'"'./health/health.module'"'"';" src/app.module.ts && grep -q "imports: \[HealthModule\]" src/app.module.ts || { echo "post-generate: HealthModule wiring missing from src/app.module.ts; the nest generator likely changed its output format — update the sed patterns in ADAPTER_POST_GENERATE to match" >&2; exit 1; }; } && pnpm exec prettier --write .' -# the generator produces only `/`; both health routes are this adapter's own ADAPTER_LIVENESS_PATH="/health/live" ADAPTER_READINESS_PATH="/health/ready" diff --git a/adapters/nestjs/mise.toml b/adapters/nestjs/mise.toml index e6ef8aa..bcd5f44 100644 --- a/adapters/nestjs/mise.toml +++ b/adapters/nestjs/mise.toml @@ -1,8 +1,6 @@ [tasks.install] -# packages/types joins the workspace only after this app's own generator -# has already installed once, so this is always the first frozen install -# to see both together; pnpm needs to relink node_modules for that and -# must not stop to ask — every contract task runs non-interactively. +# The first frozen install after packages/types joins the workspace relinks +# node_modules, and pnpm must not stop to ask. env = { npm_config_confirm_modules_purge = "false" } run = "pnpm install --frozen-lockfile" @@ -16,17 +14,11 @@ run = "pnpm exec prettier --write ." run = "pnpm exec eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0" [tasks.prisma] -# prisma writes the client that tsc type-checks against, so it has to run -# before check and before build — the same shape as the nextjs adapter's -# `next typegen`. A project generated with --db none has no schema and no -# prisma, and this does nothing; a real prisma failure still fails. +# tsc checks against the generated client; a --db none project has no schema. run = "if [ -f prisma/schema.prisma ]; then pnpm exec prisma generate; fi" [tasks.migrate] -# prisma's mongodb provider rejects `migrate deploy` outright — measured: -# `The "mongodb" provider is not supported with this command.` — and takes -# `db push` instead. Branching on the schema rather than on a recorded value -# keeps this true for a project whose provider changes. +# prisma's mongodb provider rejects `migrate deploy` and takes `db push`. run = """ if ! [ -f prisma/schema.prisma ]; then exit 0; fi if grep -q 'provider *= *"mongodb"' prisma/schema.prisma; then diff --git a/adapters/nestjs/src/health/health.controller.ts b/adapters/nestjs/src/health/health.controller.ts index 1512aac..4ffcd53 100644 --- a/adapters/nestjs/src/health/health.controller.ts +++ b/adapters/nestjs/src/health/health.controller.ts @@ -10,11 +10,8 @@ import { export class HealthController { private readonly logger = new Logger(HealthController.name); - // The client field and the probe below are both written by the selected - // service's driver: prisma has no provider-agnostic read, so a SQL - // provider gets $queryRawUnsafe and mongodb gets $runCommandRaw. A project - // generated with --db none leaves this anchor as a comment and the probe - // falls through to its throw, reporting 503 honestly. + // The database driver splices in the client and the probe; with --db none the + // probe throws and readiness reports 503. // @DB_CLIENT@ @Get('live') @@ -22,28 +19,16 @@ export class HealthController { return { status: 'ok' }; } - // Readiness is polled by every orchestrator, often every few seconds, so - // the probe below reuses the client field declared above instead of - // constructing a new PrismaClient per request: HealthController is a - // Nest singleton (the default provider/controller scope), so one instance - // — and one connection pool — lives for the process, the same lifetime a - // NestJS Prisma integration normally gives it via a connect-once service. - // A fresh client per call would need its own $disconnect() to avoid - // leaking a connection per poll, but tearing a real pool down and back up - // every few seconds is the wasteful version of the same fix. - // Not `async` here: with --db none there is nothing to await, and - // @typescript-eslint/require-await fails a generated project on its own - // lint. services/shared/nest.sh adds the keyword when it splices in a - // probe, which is the only case that awaits anything. + // Reuses the singleton's client: readiness is polled every few seconds. + // Not `async`: with --db none, require-await fails lint; the driver adds it. @Get('ready') ready(): Promise<{ status: string }> { try { // @DB_PROBE@ throw new Error('no database is configured for this project'); } catch (error) { - // Logged, not returned: a driver's connection error names the host, - // port, user and database, and /health/ready is unauthenticated. An - // orchestrator reads the status code and nothing else. + // Logged, not returned: the error names host and user, and this route is + // unauthenticated. this.logger.warn(`readiness probe failed: ${(error as Error).message}`); throw new HttpException( { status: 'unavailable' }, diff --git a/adapters/nextjs/Dockerfile b/adapters/nextjs/Dockerfile index 39f2c75..0eb026d 100644 --- a/adapters/nextjs/Dockerfile +++ b/adapters/nextjs/Dockerfile @@ -3,9 +3,8 @@ FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS deps WORKDIR /app RUN corepack enable -# pnpm-workspace.yaml carries the allowBuilds decisions ADR-0017 records — -# without it in the build context, pnpm has nothing to check unrs-resolver's -# postinstall build against and refuses non-interactively. +# pnpm-workspace.yaml holds the allowBuilds policy (ADR-0017); without it pnpm +# refuses unrs-resolver's postinstall build non-interactively. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile @@ -20,20 +19,14 @@ RUN pnpm exec next build FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS runtime WORKDIR /app ENV NODE_ENV=production -# next standalone output carries only the server files it actually needs COPY --from=build /app/.next/standalone ./ COPY --from=build /app/.next/static ./.next/static COPY --from=build /app/public ./public USER node -# 8080 because that is the container port common/compose.yaml publishes, and -# nothing rewrites it — an adapter listening anywhere else publishes a dead -# port. next's standalone server.js reads PORT itself. +# The container port compose.yaml publishes; server.js reads PORT. ENV PORT=8080 -# Docker sets HOSTNAME to the container's own id, and the standalone -# server.js binds to `process.env.HOSTNAME || '0.0.0.0'` — so without this it -# listens on that id-derived address. Traffic from outside still reaches it, -# but the HEALTHCHECK below dials localhost from inside the container and -# fails forever while the app answers everyone else. +# Docker sets HOSTNAME to the container id, which server.js would bind to, and +# the HEALTHCHECK's loopback dial would never connect. ENV HOSTNAME="0.0.0.0" EXPOSE 8080 # 127.0.0.1, not localhost: "0.0.0.0" above is an IPv4-only bind, but this diff --git a/adapters/nextjs/Dockerfile.workspace b/adapters/nextjs/Dockerfile.workspace index a33f69c..dbe30b1 100644 --- a/adapters/nextjs/Dockerfile.workspace +++ b/adapters/nextjs/Dockerfile.workspace @@ -1,14 +1,12 @@ # syntax=docker/dockerfile:1 -# used instead of Dockerfile when every app in the project is typescript: -# apps/web is then a pnpm workspace member with no manifests of its own — -# see docs/PROVENANCE.md and immich's server/Dockerfile, which this follows. +# Used instead of Dockerfile when every app is typescript: apps/web is then a +# pnpm workspace member with no manifests of its own. FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS deps WORKDIR /app RUN corepack enable -# pnpm-workspace.yaml carries the allowBuilds decisions ADR-0017 records — -# without it in the build context, pnpm has nothing to check unrs-resolver's -# postinstall build against and refuses non-interactively. +# pnpm-workspace.yaml holds the allowBuilds policy (ADR-0017); without it pnpm +# refuses unrs-resolver's postinstall build non-interactively. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/@APP_FILTER@/package.json apps/@APP_FILTER@/package.json RUN pnpm --filter @APP_FILTER@ install --frozen-lockfile @@ -26,20 +24,14 @@ RUN pnpm exec next build FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS runtime WORKDIR /app ENV NODE_ENV=production -# next standalone output carries only the server files it actually needs COPY --from=build /app/apps/@APP_FILTER@/.next/standalone ./ COPY --from=build /app/apps/@APP_FILTER@/.next/static ./apps/@APP_FILTER@/.next/static COPY --from=build /app/apps/@APP_FILTER@/public ./apps/@APP_FILTER@/public USER node -# 8080 because that is the container port common/compose.yaml publishes, and -# nothing rewrites it — an adapter listening anywhere else publishes a dead -# port. next's standalone server.js reads PORT itself. +# The container port compose.yaml publishes; server.js reads PORT. ENV PORT=8080 -# Docker sets HOSTNAME to the container's own id, and the standalone -# server.js binds to `process.env.HOSTNAME || '0.0.0.0'` — so without this it -# listens on that id-derived address. Traffic from outside still reaches it, -# but the HEALTHCHECK below dials localhost from inside the container and -# fails forever while the app answers everyone else. +# Docker sets HOSTNAME to the container id, which server.js would bind to, and +# the HEALTHCHECK's loopback dial would never connect. ENV HOSTNAME="0.0.0.0" EXPOSE 8080 # 127.0.0.1, not localhost: "0.0.0.0" above is an IPv4-only bind, but this diff --git a/adapters/nextjs/adapter.env b/adapters/nextjs/adapter.env index 8458582..a872b41 100644 --- a/adapters/nextjs/adapter.env +++ b/adapters/nextjs/adapter.env @@ -3,13 +3,9 @@ ADAPTER_ROLE="web" ADAPTER_TIER="A" ADAPTER_LANGUAGE="typescript" ADAPTER_FAMILY="next" -# node and pnpm come from the project root, so this adapter declares no tools. -# Pinned to the major: an unpinned generator is what let the nestjs adapter -# cross a major silently. See docs/decisions/0008. +# Pinned to the major (docs/decisions/0008). ADAPTER_GENERATOR='pnpm create next-app@16 "$APP_DIR" --ts --app --eslint --tailwind --src-dir --import-alias "@/*" --use-pnpm --yes' -# create-next-app ships neither prettier nor vitest, and its own output is not -# formatted — the contract's format and test tasks need both. +# create-next-app ships neither prettier nor vitest, and leaves its output unformatted. ADAPTER_POST_GENERATE='pnpm add -D prettier vitest && pnpm exec prettier --write .' -# a route handler answers without rendering the home page, so the probe does -# not depend on whatever the client later puts on `/` +# a route handler, so the probe never depends on what the client puts on `/` ADAPTER_LIVENESS_PATH="/api/health/live" diff --git a/adapters/nextjs/lefthook.fragment.yml b/adapters/nextjs/lefthook.fragment.yml index 176ad19..b0cb7df 100644 --- a/adapters/nextjs/lefthook.fragment.yml +++ b/adapters/nextjs/lefthook.fragment.yml @@ -1,2 +1,2 @@ -# nextjs needs no extra hook; prettier in the common layer already covers it. +# prettier in the common layer already covers typescript sources. {} diff --git a/adapters/nextjs/mise.toml b/adapters/nextjs/mise.toml index b08ffef..ae8cfaf 100644 --- a/adapters/nextjs/mise.toml +++ b/adapters/nextjs/mise.toml @@ -1,8 +1,6 @@ [tasks.install] -# packages/types joins the workspace only after this app's own generator -# has already installed once, so this is always the first frozen install -# to see both together; pnpm needs to relink node_modules for that and -# must not stop to ask — every contract task runs non-interactively. +# The first frozen install after packages/types joins the workspace relinks +# node_modules, and pnpm must not stop to ask. env = { npm_config_confirm_modules_purge = "false" } run = "pnpm install --frozen-lockfile" @@ -16,9 +14,7 @@ run = "pnpm exec prettier --write ." run = "pnpm exec eslint . --max-warnings 0" [tasks.check] -# next generates the route and layout types the app's own pages are typed -# against, into .next/types, which is not committed. Without this, tsc passes -# on a machine that happens to have built once and fails everywhere else. +# tsc checks pages against .next/types, which is generated and uncommitted. run = [ "pnpm exec next typegen", "pnpm exec tsc --noEmit", diff --git a/adapters/nextjs/next.config.ts b/adapters/nextjs/next.config.ts index de2e24d..4f07a8f 100644 --- a/adapters/nextjs/next.config.ts +++ b/adapters/nextjs/next.config.ts @@ -1,9 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - // the Dockerfile copies .next/standalone, and next only emits that tree when - // the build is told to. Without this the image build fails on a missing COPY - // source, long after `next build` reported success. + // the Dockerfile copies .next/standalone, which next emits only when asked output: 'standalone', }; diff --git a/services/mongodb/compose.fragment.yaml b/services/mongodb/compose.fragment.yaml index ca95892..580bbff 100644 --- a/services/mongodb/compose.fragment.yaml +++ b/services/mongodb/compose.fragment.yaml @@ -5,9 +5,7 @@ services: MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME:-app} MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-app} healthcheck: - # mongosh, not a tcp probe: the port is open well before the server will - # accept a query, and a check that passes early is worse than none — - # depends_on releases the application into a database that is not ready. + # mongosh, not a tcp probe: the port opens before the server accepts queries test: ['CMD', 'mongosh', '--quiet', '--eval', "db.adminCommand('ping')"] interval: 10s timeout: 5s diff --git a/services/redis/compose.fragment.yaml b/services/redis/compose.fragment.yaml index f4f36fc..d06ead6 100644 --- a/services/redis/compose.fragment.yaml +++ b/services/redis/compose.fragment.yaml @@ -1,9 +1,7 @@ services: cache: - # Block style, and it has to be block *here*: yq's merge keeps the style of - # its first input, so the prod fragment below inherits it. In flow style the - # assembled prod command runs past prettier's print width, and the project's - # own pre-commit hook rewrites the compose.yaml scaffold just handed it. + # Block style here: yq's merge keeps its first input's style, and a flow-style + # prod command overruns prettier's print width. command: - redis-server - --requirepass From ab9e6d4e0ecd7afe5959179680b3b10255d8f1e6 Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Thu, 17 Sep 2026 00:26:48 +0700 Subject: [PATCH 3/5] refactor(common): cut the shipped common-layer comments to the one-line why Drop toolbox-internal provenance and prose from files copied into every generated project. Comment text only; the docs mise.toml sentence the tour cites is kept byte-identical. --- common/.dockerignore | 5 ++--- common/.github/workflows/build.yml | 9 +++------ common/.github/workflows/ci.yml | 6 ++---- common/.github/workflows/docs.yml | 6 ++---- common/.github/workflows/release.yml | 15 +++++---------- common/.github/workflows/security.yml | 6 ++---- common/.gitignore | 4 +--- common/.prettierignore | 3 +-- common/compose.dev.yaml | 5 ++--- common/compose.test.yaml | 4 +--- common/compose.yaml | 10 ++-------- common/docs/.prettierignore | 3 +-- common/docs/.vitepress/config.ts | 1 - common/docs/.vitepress/theme/custom.css | 6 +----- common/docs/.vitepress/theme/index.js | 4 +--- common/docs/mise.toml | 4 +--- common/docs/pnpm-workspace.yaml | 5 ++--- common/docs/scripts/check-paths.mjs | 18 +++--------------- common/example.env | 3 +-- common/lefthook.yml | 6 ++---- common/mise.root.toml | 13 ++++--------- common/packages-types/mise.toml | 6 ++---- common/pnpm-workspace.yaml | 6 ++---- 23 files changed, 43 insertions(+), 105 deletions(-) diff --git a/common/.dockerignore b/common/.dockerignore index 688adc7..92f7d3c 100644 --- a/common/.dockerignore +++ b/common/.dockerignore @@ -1,6 +1,5 @@ -# only read when the build context is the project root — an all-typescript -# project's apps/*/Dockerfile.workspace; node_modules is copied from a pinned -# build stage, so a host copy would overwrite it +# Read only for a root build context (Dockerfile.workspace). node_modules comes +# from a build stage; a host copy would overwrite it. **/node_modules **/.next **/dist diff --git a/common/.github/workflows/build.yml b/common/.github/workflows/build.yml index 89a6902..164aee0 100644 --- a/common/.github/workflows/build.yml +++ b/common/.github/workflows/build.yml @@ -8,15 +8,12 @@ concurrency: permissions: {} jobs: build: - # no more than app-build's own jobs ask for; the workflow-level - # `permissions: {}` above grants nothing to inherit. + # what app-build's jobs need; `permissions: {}` above leaves nothing to inherit. permissions: contents: read packages: write - # `@v1` moves on purpose: one fix reaches every project, and that same - # channel outlives the engagement (ADR-0005). + # `@v1` floats on purpose: one fix reaches every project (ADR-0005). uses: you/.github/.github/workflows/app-build.yml@v1 # zizmor: ignore[unpinned-uses,ref-confusion] with: - # {image, context, dockerfile} per application, written by scaffold as - # each is generated (ADR-0022). Empty means nothing to publish. + # {image, context, dockerfile} per application, written by scaffold (ADR-0022). images: "[]" diff --git a/common/.github/workflows/ci.yml b/common/.github/workflows/ci.yml index 36bfac1..38334af 100644 --- a/common/.github/workflows/ci.yml +++ b/common/.github/workflows/ci.yml @@ -9,13 +9,11 @@ concurrency: permissions: {} jobs: ci: - # no more than app-ci's own jobs ask for; the workflow-level - # `permissions: {}` above grants nothing to inherit. + # what app-ci's jobs need; `permissions: {}` above leaves nothing to inherit. permissions: contents: read pull-requests: read - # `@v1` moves on purpose: one fix reaches every project, and that same - # channel outlives the engagement (ADR-0005). + # `@v1` floats on purpose: one fix reaches every project (ADR-0005). uses: you/.github/.github/workflows/app-ci.yml@v1 # zizmor: ignore[unpinned-uses,ref-confusion] with: roots: '["docs"]' diff --git a/common/.github/workflows/docs.yml b/common/.github/workflows/docs.yml index 0463f69..8bb3b11 100644 --- a/common/.github/workflows/docs.yml +++ b/common/.github/workflows/docs.yml @@ -6,10 +6,8 @@ on: permissions: {} jobs: docs: - # no more than app-docs's own jobs ask for; the workflow-level - # `permissions: {}` above grants nothing to inherit. + # what app-docs's jobs need; `permissions: {}` above leaves nothing to inherit. permissions: contents: read - # `@v1` moves on purpose: one fix reaches every project, and that same - # channel outlives the engagement (ADR-0005). + # `@v1` floats on purpose: one fix reaches every project (ADR-0005). uses: you/.github/.github/workflows/app-docs.yml@v1 # zizmor: ignore[unpinned-uses,ref-confusion] diff --git a/common/.github/workflows/release.yml b/common/.github/workflows/release.yml index 567e8af..288886c 100644 --- a/common/.github/workflows/release.yml +++ b/common/.github/workflows/release.yml @@ -8,24 +8,19 @@ concurrency: permissions: {} jobs: release: - # no more than app-release's own jobs ask for; the workflow-level - # `permissions: {}` above grants nothing to inherit. + # what app-release's jobs need; `permissions: {}` above leaves nothing to inherit. permissions: contents: write issues: write packages: write pull-requests: write - # `@v1` moves on purpose: one fix reaches every project, and that same - # channel outlives the engagement (ADR-0005). + # `@v1` floats on purpose: one fix reaches every project (ADR-0005). uses: you/.github/.github/workflows/app-release.yml@v1 # zizmor: ignore[unpinned-uses,ref-confusion] - # With these, release please opens its pull request as a GitHub app and the - # checks on it run; without them it falls back to GITHUB_TOKEN, whose checks - # sit at "Action required" and expire red. Named rather than `inherit`, so - # the called workflow reads only the two it declared. + # Without an app token, checks on the release PR sit at "Action required" + # and expire red. Named, not `inherit`: the callee reads only these two. secrets: RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} with: - # {image, context, dockerfile} per application, written by scaffold as - # each is generated (ADR-0022). Empty means nothing to publish. + # {image, context, dockerfile} per application, written by scaffold (ADR-0022). images: "[]" diff --git a/common/.github/workflows/security.yml b/common/.github/workflows/security.yml index 8f55a55..981282b 100644 --- a/common/.github/workflows/security.yml +++ b/common/.github/workflows/security.yml @@ -8,13 +8,11 @@ on: permissions: {} jobs: security: - # no more than app-security's own jobs ask for; the workflow-level - # `permissions: {}` above grants nothing to inherit. + # what app-security's jobs need; `permissions: {}` above leaves nothing to inherit. permissions: contents: read security-events: write pull-requests: read actions: read - # `@v1` moves on purpose: one fix reaches every project, and that same - # channel outlives the engagement (ADR-0005). + # `@v1` floats on purpose: one fix reaches every project (ADR-0005). uses: you/.github/.github/workflows/app-security.yml@v1 # zizmor: ignore[unpinned-uses,ref-confusion] diff --git a/common/.gitignore b/common/.gitignore index 840622f..1741847 100644 --- a/common/.gitignore +++ b/common/.gitignore @@ -3,9 +3,7 @@ dist/ build/ coverage/ -# .env* rather than .env: install.sh writes ./.env.XXXXXX holding the -# generated database password, and a signal between creating and moving it -# leaves that file behind. +# .env*: an interrupted install.sh leaves .env.XXXXXX holding the database password. .env* !.env.example *.local diff --git a/common/.prettierignore b/common/.prettierignore index 8f4781a..55d1b14 100644 --- a/common/.prettierignore +++ b/common/.prettierignore @@ -1,5 +1,4 @@ -# prettier rewrites pnpm-lock.yaml and fights Release Please for CHANGELOG.md; -# neither is written by hand +# tool-written: prettier would fight pnpm and Release Please over these pnpm-lock.yaml **/pnpm-lock.yaml package-lock.json diff --git a/common/compose.dev.yaml b/common/compose.dev.yaml index 491fbd5..1ad7c5e 100644 --- a/common/compose.dev.yaml +++ b/common/compose.dev.yaml @@ -1,6 +1,5 @@ -# Local development only: throwaway services an app started outside docker can -# point at on localhost. Never shipped as a release asset — compose.yaml is the -# stack a client runs. scaffold merges the selected services in. +# Local development only: throwaway services on localhost for an app run outside +# docker. Not a release asset; compose.yaml is. name: app-dev services: {} diff --git a/common/compose.test.yaml b/common/compose.test.yaml index ba4ff4b..25dfdc4 100644 --- a/common/compose.test.yaml +++ b/common/compose.test.yaml @@ -1,6 +1,4 @@ -# ci and local test runs: same services as compose.dev.yaml, but tmpfs storage -# so every run starts from an empty database and nothing persists between -# runs. +# ci and local tests: compose.dev.yaml's services on tmpfs, so every run starts empty. name: app-test services: {} diff --git a/common/compose.yaml b/common/compose.yaml index 42ad3d7..8fb3f81 100644 --- a/common/compose.yaml +++ b/common/compose.yaml @@ -1,11 +1,5 @@ -# The production-like stack clients run. Attached to every release so the -# compose file and the image always match (ADR-0014). Every variable below has -# a default so this file validates in a freshly generated project, before a -# .env exists — the real values live in .env, written by install.sh. -# -# Empty on purpose: scaffold merges in one service per application (ADR-0022) -# and whichever database and cache were selected (ADR-0019), so a project ships -# exactly what it asked for rather than a service nothing connects to. +# The stack clients run, attached to every release (ADR-0014). Every variable has +# a default so the file validates before install.sh writes .env. name: app services: {} diff --git a/common/docs/.prettierignore b/common/docs/.prettierignore index 2904772..c3feee1 100644 --- a/common/docs/.prettierignore +++ b/common/docs/.prettierignore @@ -2,6 +2,5 @@ pnpm-lock.yaml .vitepress/cache .vitepress/dist -# copied verbatim from escrcpy under Apache-2.0; reformatting it would make it -# a modified file, which section 4(b) then asks us to mark as changed +# escrcpy under Apache-2.0: reformatting makes it a modified file (section 4(b)) .vitepress/theme/vendor diff --git a/common/docs/.vitepress/config.ts b/common/docs/.vitepress/config.ts index b435165..d083ced 100644 --- a/common/docs/.vitepress/config.ts +++ b/common/docs/.vitepress/config.ts @@ -4,7 +4,6 @@ export default defineConfig({ title: '@PROJECT_TITLE@', description: 'Project documentation', head: [['link', { rel: 'icon', href: '/logo.png' }]], - // a dead link is a failed build, not a warning ignoreDeadLinks: false, themeConfig: { logo: '/logo.png', diff --git a/common/docs/.vitepress/theme/custom.css b/common/docs/.vitepress/theme/custom.css index a0db43a..b03af5d 100644 --- a/common/docs/.vitepress/theme/custom.css +++ b/common/docs/.vitepress/theme/custom.css @@ -1,8 +1,4 @@ -/* The home page is short enough not to scroll and every guide page is not, so - navigating between them adds and removes the scrollbar — and the centred - content jumps sideways by its width each time. Reserving the gutter costs - nothing on a page that scrolls anyway, and nothing at all on the overlay - scrollbars macOS and mobile use. */ +/* Stops content jumping sideways between pages that scroll and pages that don't. */ html { scrollbar-gutter: stable; } diff --git a/common/docs/.vitepress/theme/index.js b/common/docs/.vitepress/theme/index.js index 183651b..e7d3936 100644 --- a/common/docs/.vitepress/theme/index.js +++ b/common/docs/.vitepress/theme/index.js @@ -1,8 +1,6 @@ import DefaultTheme from 'vitepress/theme'; -// rainbow.css must load first: vars.css consumes the brand variables it -// defines (--vp-c-brand-next, --vp-c-brand-light, --vp-c-brand-darker), -// and CSS resolves them in load order. +// rainbow.css first: vars.css consumes the brand variables it defines. import './vendor/escrcpy/rainbow.css'; import './vendor/escrcpy/vars.css'; import './custom.css'; diff --git a/common/docs/mise.toml b/common/docs/mise.toml index e3bb123..80571c7 100644 --- a/common/docs/mise.toml +++ b/common/docs/mise.toml @@ -1,7 +1,5 @@ [tasks.install] -# docs joins the same workspace apps/packages-types do; see the scaffold -# toolbox's ADR-0017 (not shipped here) for why this env is needed on a -# frozen install. +# A frozen install that relinks node_modules must not stop to ask (ADR-0017). env = { npm_config_confirm_modules_purge = "false" } run = "pnpm install --frozen-lockfile" diff --git a/common/docs/pnpm-workspace.yaml b/common/docs/pnpm-workspace.yaml index 228a42a..1a5d31e 100644 --- a/common/docs/pnpm-workspace.yaml +++ b/common/docs/pnpm-workspace.yaml @@ -1,5 +1,4 @@ -# docs ships whatever adapters a project picks, so it needs its own policy for -# a project with no root workspace. Deleted by enable_typescript_workspace once -# one exists. esbuild has no pure-js fallback, so it is allowed. +# docs' own build policy until a root workspace replaces this file. esbuild has +# no pure-js fallback, so it is allowed. allowBuilds: esbuild: true diff --git a/common/docs/scripts/check-paths.mjs b/common/docs/scripts/check-paths.mjs index 9b5038d..1bfb1cb 100644 --- a/common/docs/scripts/check-paths.mjs +++ b/common/docs/scripts/check-paths.mjs @@ -5,23 +5,13 @@ import { extname, join, resolve } from "node:path"; const PROJECT_ROOT = resolve(import.meta.dirname, "..", ".."); // backticked strings that look like repository paths const PATH_PATTERN = /`((?:[\w.-]+\/)+[\w.-]+)`/g; -// a comment citing an adr by path, not just by prose in a backtick — in files -// that ship (compose.yaml, install.sh, an adapter's own mise.toml) that path -// must resolve against this project's own docs/decisions/, not the scaffold -// toolbox's, which never ships past 0000 and the template. +// an adr cited by path in a shipped file must exist in this project's docs/decisions/ const ADR_REFERENCE_PATTERN = /\bdocs\/decisions\/(\d{4})[\w.-]*/g; const ADR_SCAN_EXTENSIONS = new Set([".sh", ".toml", ".mjs", ".yaml", ".yml"]); -// node_modules and .git ship files nothing here authored — vendor markdown -// full of paths relative to whatever package it belongs to, or plumbing -// with no bearing on this project's own docs or adr citations. const SKIP_DIRS = new Set(["node_modules", ".git", ".vitepress"]); -// apps/ ships generator-owned markdown (AGENTS.md, README.md) whose backticked -// paths are written relative to the app's own directory, not the project -// root, so the root-relative path scan reports them as dead when they are not -// (e.g. create-next-app's AGENTS.md and README.md). This applies to the -// markdown path scan only — the ADR-citation scan must still walk apps/, or -// a dead ADR reference in an adapter's own mise.toml goes unseen. +// generator-owned markdown under apps/ uses app-relative paths; the adr scan +// still walks apps/ const MARKDOWN_SKIP_DIRS = new Set([...SKIP_DIRS, "apps"]); async function filesMatching(dir, matches, skipDirs = SKIP_DIRS) { @@ -62,8 +52,6 @@ function missingAdrReferences(content, shippedNumbers) { const failures = []; -// the whole project, not just docs/ — a backticked dead path in -// deploy-adapters/README.md is the same defect as one in docs/index.md. for (const file of await filesMatching( PROJECT_ROOT, (name) => name.endsWith(".md"), diff --git a/common/example.env b/common/example.env index 7ea91e5..219589f 100644 --- a/common/example.env +++ b/common/example.env @@ -1,3 +1,2 @@ -# copy to .env; install.sh does this and replaces every `changeme`. Scaffold -# appends the per-app port, database and cache variables at generation time. +# copy to .env; install.sh does this and replaces every `changeme`. IMAGE_TAG=latest diff --git a/common/lefthook.yml b/common/lefthook.yml index 21c3e36..7f01d35 100644 --- a/common/lefthook.yml +++ b/common/lefthook.yml @@ -1,7 +1,5 @@ -# Through `mise exec --` so only mise has to be on PATH. These tools exist -# only in this project's mise.toml, so calling them bare fails a plain -# `git commit` with `gitleaks: not found`. lefthook's `rc:` key would do this -# too, but it is baked into .git/hooks at install time and drifts silently. +# `mise exec --`: these tools exist only in mise.toml, and lefthook's `rc:` is +# baked into .git/hooks at install time. pre-commit: parallel: true commands: diff --git a/common/mise.root.toml b/common/mise.root.toml index 150a63e..f302810 100644 --- a/common/mise.root.toml +++ b/common/mise.root.toml @@ -1,18 +1,13 @@ monorepo_root = true -# The services this project was generated against. `scaffold add` reads these -# so an application added later is wired to the same database as the first -# one. Changing them here does not migrate anything — see the toolbox's -# ADR-0019. +# Read by `scaffold add` to wire a new application to the same services. +# Editing them migrates nothing (ADR-0019). [vars] database = "@DATABASE@" cache = "@CACHE@" -# The registry path every application in this project publishes under, with -# its own directory name appended: apps/web becomes -web (ADR-0022). -# Read by `scaffold add`, so an application added in month six lands under the -# same path as the first one, and by compose.yaml and the build workflows, -# which have to name one string or a release publishes an image nothing pulls. +# apps/web publishes as -web (ADR-0022). `scaffold add`, compose.yaml +# and the build workflows must all name this one string. image = "ghcr.io/you/@PROJECT_NAME@" [monorepo] diff --git a/common/packages-types/mise.toml b/common/packages-types/mise.toml index dc25aa7..e30cf10 100644 --- a/common/packages-types/mise.toml +++ b/common/packages-types/mise.toml @@ -1,8 +1,6 @@ [tasks.install] -# this package joins the workspace only after the app it ships alongside -# has already installed once, so this is always the first frozen install -# to see both together; pnpm needs to relink node_modules for that and -# must not stop to ask — every contract task runs non-interactively. +# The first frozen install after this package joins the workspace relinks +# node_modules, and pnpm must not stop to ask. env = { npm_config_confirm_modules_purge = "false" } run = "pnpm install --frozen-lockfile" diff --git a/common/pnpm-workspace.yaml b/common/pnpm-workspace.yaml index 5ad1ce8..0208957 100644 --- a/common/pnpm-workspace.yaml +++ b/common/pnpm-workspace.yaml @@ -2,10 +2,8 @@ packages: - apps/* - packages/* - docs -# pnpm blocks an unreviewed postinstall build rather than warning, which would -# abort ci-unit non-interactively — so each has to be decided here. esbuild is -# allowed because its postinstall fetches the binary vite actually runs; the -# other two have pure-js fallbacks and are denied. See ADR-0017 in the toolbox. +# pnpm blocks an unreviewed postinstall build, aborting ci-unit (ADR-0017). +# esbuild fetches the binary vite runs; the others have pure-js fallbacks. allowBuilds: unrs-resolver: false esbuild: true From ed05068404fa0551ae86908672e413110ea9296e Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Thu, 17 Sep 2026 00:27:04 +0700 Subject: [PATCH 4/5] refactor: cut toolbox config and workflow comments to the one-line why Drop incident tallies, timings and journal prose from mise.toml, lefthook.yml, .gitignore and the workflows. Comment text only. --- .editorconfig | 1 - .github/workflows/adapters.yml | 26 ++++++-------------------- .github/workflows/ci.yml | 25 ++++--------------------- .github/workflows/provenance.yml | 18 +++--------------- .github/workflows/pull-request.yml | 17 +++-------------- .gitignore | 7 +------ lefthook.yml | 26 +++++--------------------- mise.toml | 20 ++++---------------- 8 files changed, 26 insertions(+), 114 deletions(-) diff --git a/.editorconfig b/.editorconfig index 43e1c06..6bdfaf8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,3 @@ -# Editor configuration, see https://editorconfig.org root = true [*] diff --git a/.github/workflows/adapters.yml b/.github/workflows/adapters.yml index 262edbc..5a3425d 100644 --- a/.github/workflows/adapters.yml +++ b/.github/workflows/adapters.yml @@ -4,8 +4,7 @@ on: schedule: # nightly: tier a only (docs/decisions/0012) - cron: "17 3 * * *" - # weekly: tier a, every tier b adapter, plus the compose/install check — - # this exact string is also scripts/adapter-matrix.sh's WEEKLY_CRON + # weekly: every tier plus compose; must equal scripts/adapter-matrix.sh's WEEKLY_CRON - cron: "23 2 * * 1" workflow_dispatch: @@ -49,10 +48,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # one full generation per test, 5-6 tests per adapter timeout-minutes: 15 strategy: - # a broken adapter must not hide the state of the others fail-fast: false matrix: adapter: ${{ fromJson(needs.discover.outputs.tier-a) }} @@ -88,8 +85,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # measured at ~5 minutes per test (docs/decisions/0012); five tests - # per adapter plus margin, well under an unbounded hang. + # ~5 minutes per test, five tests per adapter (docs/decisions/0012) timeout-minutes: 40 strategy: fail-fast: false @@ -127,7 +123,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # a generation plus an image build plus a container start, per adapter. timeout-minutes: 30 strategy: fail-fast: false @@ -165,7 +160,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # same per-adapter cost as deploy: generation, image build, container start timeout-minutes: 30 strategy: fail-fast: false @@ -198,9 +192,7 @@ jobs: run: ./scripts/deploy-check.sh "$ADAPTER" deploy-multi-app: - # deploy runs one adapter at a time, so nothing else proves a project with - # two applications comes up on two images (ADR-0022). Weekly, like compose - # below: two generations plus two image builds. + # The only proof two applications come up on two images (ADR-0022). if: ${{ github.event.schedule == '23 2 * * 1' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest permissions: @@ -219,8 +211,6 @@ jobs: run: ./scripts/deploy-check.sh nextjs nestjs --db postgres compose: - # expensive relative to the other checks here, so gated on the weekly - # schedule (not the nightly tier-a-only one) or a manual run if: ${{ github.event.schedule == '23 2 * * 1' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest permissions: @@ -241,8 +231,7 @@ jobs: run: bats tests/compose.bats services: - # Eight combinations per adapter at minutes each, so the full grid is - # nightly; a pull request gets the default cell through tests/new-*.bats. + # The full grid is nightly; a pull request gets the default cell via tests/new-*.bats. if: ${{ github.event.schedule == '17 3 * * *' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest permissions: @@ -280,9 +269,7 @@ jobs: ADAPTER: ${{ matrix.adapter }} DB: ${{ matrix.db }} CACHE: ${{ matrix.cache }} - # resolve_github_owner falls back to `gh auth login` or git's - # github.user before giving up, and a runner has neither. Every other - # caller sets its own; this job calls `scaffold new` directly. + # A runner has neither `gh auth` nor git's github.user to fall back on. SCAFFOLD_GITHUB_OWNER: ${{ github.repository_owner }} run: | work="$(mktemp -d)" @@ -293,8 +280,7 @@ jobs: mise run //apps/api:checklist notify-on-schedule-failure: - # a weekly-only lane nobody watches is theatre; surface a red run as an - # issue instead of leaving it in a schedule tab no one opens. + # Nobody watches the schedule tab; a red scheduled run becomes an issue. if: >- ${{ always() && github.event_name == 'schedule' && (needs.smoke.result == 'failure' || needs.smoke-tier-b.result == 'failure' || diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20a0bea..8dfeb32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # ci-unit is lint plus test-unit, and test-unit is genuinely offline — no - # adapter generator anywhere in its setup(), asserted by - # tests/contract.bats rather than promised by this comment — so no - # pnpm/php provisioning is needed here. The whole job measured 79 seconds - # on a runner, which is what the timeout below leaves room around. + # No pnpm/php: tests/contract.bats asserts test-unit generates no adapter. timeout-minutes: 5 steps: - name: Checkout @@ -35,20 +31,12 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # add-app.bats, compose.bats, docs.bats and workflows.bats each - # generate a real nestjs/laravel-api project as a fixture — same cost - # class as tier a in adapters.yml (bounded, not tier b's ~25 minutes), - # moved here because ADR-0012's tiers are about per-adapter smoke - # tests, not these cross-cutting mechanics suites. timeout-minutes: 25 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # workflows.bats walks this repository's own history to find the - # commit that introduced an adapter, then diffs against its parent. - # A default checkout has one commit and no parent, so the test fails - # on `unknown revision` rather than on anything it is testing. + # workflows.bats diffs the commit that introduced an adapter against its parent. fetch-depth: 0 persist-credentials: false - name: Setup mise @@ -74,12 +62,7 @@ jobs: persist-credentials: false - name: Setup mise uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - # Only this repository's own workflows. common/ holds templates whose - # `uses: you/.github/...` names no real repository, so ref-confusion - # cannot resolve it and the audit errors out rather than reporting a - # finding — a failure about the placeholder, not about the pipeline. - # What a generated project's call sites get instead is structural: - # tests/workflows.bats asserts sha-pinned actions, closed permission - # sets and shared-repository-only `uses:`. No audit runs on them. + # Not common/: its placeholder `uses: you/.github/...` makes ref-confusion + # error out. tests/workflows.bats checks those call sites structurally. - name: Run zizmor run: mise exec -- zizmor .github/workflows/ diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index 2d104e3..607a466 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -14,10 +14,7 @@ permissions: {} jobs: self-test: - # "does the checker work" belongs on every change; "has upstream moved" - # (the check job below) belongs on a schedule — different questions. - # workflow_dispatch is included here, rather than duplicating this job's - # bats step inside check, so a manual run exercises both jobs. + # Tests the checker on every change; `check` asks whether upstream moved. if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest permissions: @@ -30,9 +27,6 @@ jobs: persist-credentials: false - name: Setup mise uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - # tests/provenance.bats needs a local upstream clone the same as - # check does below — its own copy here, the way adapters.yml's - # tier-b matrix legs each do their own generation independently. - name: Clone upstream immich run: git clone --filter=blob:none --no-checkout https://github.com/immich-app/immich.git "${RUNNER_TEMP}/immich" - name: Run the provenance self-test @@ -52,9 +46,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # check-provenance.sh has no network fallback of its own (a denied - # fetch must fail loudly, not read as "everything drifted"); a - # partial clone gets the full commit graph without every blob. + # check-provenance.sh never fetches; blob:none still gives the full commit graph. - name: Clone upstream immich run: git clone --filter=blob:none --no-checkout https://github.com/immich-app/immich.git "${RUNNER_TEMP}/immich" - name: Check for upstream drift @@ -62,11 +54,7 @@ jobs: env: SCAFFOLD_UPSTREAM_CLONE: ${{ runner.temp }}/immich run: ./scripts/check-provenance.sh | tee "${RUNNER_TEMP}/report.txt" - # a bare custom if: is implicitly ANDed with success() — on a real - # check failure the job is already failing, success() is false, and - # this step would silently never run. failure() first makes the - # step eligible; steps.check.outcome narrows it to the real check - # specifically, not some other step failing. + # A bare if: is ANDed with success(), so it would never run after a failure. - name: Open an issue for upstream drift if: failure() && steps.check.outcome == 'failure' env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8bb7c19..40f54ad 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,8 +1,5 @@ name: Pull request -# Its own workflow, not a job in ci.yml, because of `edited`: this check is -# fixed by editing the body, which ci.yml does not run on — so it stayed red for -# a fix already made. Adding `edited` to ci.yml instead would re-run the -# twenty-minute integration lane on every description tweak. +# Not in ci.yml: `edited` there would re-run the integration lane on every body edit. on: pull_request: types: [opened, edited, synchronize, reopened] @@ -14,16 +11,8 @@ concurrency: permissions: {} jobs: - # The checklist is only worth having if something reads it. This parses the - # headings out of the template itself rather than holding a second copy of - # them, so editing the template changes what is enforced — the convention and - # its enforcement cannot drift apart. Borrowed from immich's auto-close.yml. - # - # The job's name is load-bearing: `main`'s branch protection requires a check - # called `pull-request-body`, and a required check that never reports blocks - # every merge forever. Rename it here and the repository setting has to change - # first — nothing in this repository records that setting, which is ADR-0004's - # point about the guardrail that is not a file. + # Headings come from the template itself, so the two cannot drift. The job name + # is load-bearing: main's branch protection requires `pull-request-body`. pull-request-body: runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index f451aa7..2af182b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,9 @@ .DS_Store *.swp -# A node_modules anywhere here is a stray pnpm run, and tmp/ is where a probe -# or a hand-run script lands. Both have been committed by a wide `git add -A` -# before — tmp/ carried a whole generated Nest project for a week. node_modules/ tmp/ -# Rooted, not bare: four files under common/ carry these names and ship into -# every generated project. Unanchored, the rule matched those too, so a new -# template beside them would be dropped by `git add -A` without a word. +# Rooted: files under common/ with these names ship into generated projects. /package.json /pnpm-lock.yaml diff --git a/lefthook.yml b/lefthook.yml index 5cd80d0..5ad42b3 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,18 +1,10 @@ -# The same two hooks the toolbox ships into every generated project -# (common/lefthook.yml), run here on itself. -# -# Through `mise exec --` so only mise has to be on PATH: both tools exist only -# in this repository's mise.toml, so calling them bare fails a plain -# `git commit` with `gitleaks: not found`. lefthook's `rc:` key would do this -# too, but it is baked into .git/hooks at install time and drifts silently. +# Mirrors common/lefthook.yml. `mise exec --`: gitleaks exists only in +# mise.toml, and lefthook's `rc:` is baked into .git/hooks at install time. pre-commit: parallel: true commands: lint: - # `mise run lint`, not a staged-files glob: a glob that has to name an - # extensionless file (`scaffold`) is a second copy of the list mise.toml's - # lint task already discovers with `git ls-files`, for a third of a second - # over the whole repository. + # Not a staged-files glob: the lint task already finds extensionless `scaffold`. run: mise run lint gitleaks: run: mise exec -- gitleaks protect --staged --redact --no-banner @@ -20,19 +12,11 @@ pre-commit: commit-msg: commands: conventional: - # A grep, not commitlint: commitlint is a node package, and this - # repository has no node in it — adding a package.json and a lockfile to - # a bash toolbox to check one line of text is the wrong trade. The types - # are the ones this repository's history actually uses, `merge:` - # included. + # A grep, not commitlint: this repository has no node. run: | head -1 {1} | grep -qE '^(feat|fix|docs|chore|refactor|test|perf|build|ci|style|revert|merge)(\([a-z0-9./-]+\))?!?: .+' || { echo "commit message must be a Conventional Commit, e.g. 'fix(tui): flush the command line left'" >&2 exit 1 } -# No pre-push gate. A generated project pushes its whole checklist because its -# CI is somebody else's repository and a red run costs a client's afternoon; -# here the same suites run in this repository's own CI on every push, and -# test-unit takes over two minutes. Paying that on every push to catch what CI -# catches anyway is a tax, not a guardrail. +# No pre-push gate: CI runs the same suites on every push. diff --git a/mise.toml b/mise.toml index 630624e..7512142 100644 --- a/mise.toml +++ b/mise.toml @@ -6,9 +6,7 @@ yq = "4.48.1" jq = "1.8.1" zizmor = "1.29.0" "ubi:shenwei356/rush" = "0.9.0" -# lefthook.yml's own two tools, at the versions this toolbox pins into every -# generated project (common/mise.root.toml) — the hooks here and the hooks -# there should not be testing different releases of the same scanner. +# The same versions common/mise.root.toml pins into generated projects. lefthook = "2.1.11" gitleaks = "8.30.0" @@ -17,25 +15,18 @@ pin = true lockfile = true [tasks.lint] -# discovers tracked shell files rather than listing them, so a new one is -# caught automatically; xargs -r skips the run when nothing matches yet. 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", ] [tasks.test] -# --jobs changes no isolation: bats already gives every test its own process -# and BATS_TEST_TMPDIR, and no suite shares a fixture. A broken lib/adapter.sh -# surfaced 85 seconds in, against 30 minutes sequentially. run = "bats --jobs $(nproc) --parallel-binary-name rush tests/" [tasks."test-unit"] description = "the suites with no adapter generator anywhere in setup — genuinely offline" -# Listed explicitly: a `tests/new-*.bats` glob was wrong in both directions — -# new-project.bats generates no adapter, while add-app, docs and compose all -# do. provenance.bats is offline too but needs an upstream clone, so it runs -# in .github/workflows/provenance.yml instead. +# Listed, not globbed: new-project.bats generates no adapter; provenance.bats +# needs an upstream clone (runs in provenance.yml). run = "bats --jobs $(nproc) --parallel-binary-name rush tests/add-app-errors.bats tests/cli.bats tests/contract.bats tests/documentation.bats tests/install.bats tests/new-project.bats tests/publish.bats tests/service.bats tests/update.bats tests/wizard.bats" [tasks."test-integration"] @@ -44,10 +35,7 @@ run = "bats --jobs $(nproc) --parallel-binary-name rush tests/add-app.bats tests [tasks."test-runner"] description = "both test lanes under the environment a github runner actually has" -# What jdx/mise-action exports on every runner. pnpm turns on -# --frozen-lockfile whenever CI is set and mise auto-trusts every config, so a -# green run without these says nothing about the runner — three CI failures in -# a row came from exactly that gap. +# The env jdx/mise-action sets on runners: CI makes pnpm use --frozen-lockfile. env = { CI = "true", GITHUB_ACTIONS = "true", MISE_YES = "1" } run = [{ task = "test-unit" }, { task = "test-integration" }] From 707c973c570bc008dfbe7d13f68817ff92281c91 Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Thu, 17 Sep 2026 01:27:05 +0700 Subject: [PATCH 5/5] refactor: restore four load-bearing comments cut in the comment pass Restore the driver-written database variables note in each .env.example, the deliberate :lint duplicate in both laravel mise.toml files and the .gitignore rooting hazard; drop a readiness comment that was false for --db none projects. Comment text only. --- .gitignore | 2 +- adapters/flask/.env.example | 1 + adapters/laravel-api/.env.example | 1 + adapters/laravel-api/mise.toml | 1 + adapters/laravel-inertia/.env.example | 1 + adapters/laravel-inertia/mise.toml | 1 + adapters/nestjs/.env.example | 1 + adapters/nestjs/src/health/health.controller.ts | 1 - 8 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2af182b..b42848c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,6 @@ node_modules/ tmp/ -# Rooted: files under common/ with these names ship into generated projects. +# Rooted: unanchored, `git add -A` would silently drop common/'s same-named templates. /package.json /pnpm-lock.yaml diff --git a/adapters/flask/.env.example b/adapters/flask/.env.example index 0812081..9a32ea8 100644 --- a/adapters/flask/.env.example +++ b/adapters/flask/.env.example @@ -1,2 +1,3 @@ # off by default: Flask's debug console executes arbitrary code from the browser FLASK_DEBUG=0 +# the database variables are written by the selected service's driver diff --git a/adapters/laravel-api/.env.example b/adapters/laravel-api/.env.example index a1ed573..2b4c9ef 100644 --- a/adapters/laravel-api/.env.example +++ b/adapters/laravel-api/.env.example @@ -3,5 +3,6 @@ APP_KEY= # off by default: Laravel's debug page leaks APP_KEY and DB_PASSWORD on a 500 APP_DEBUG=false APP_URL=http://localhost:8000 +# the database variables are written by the selected service's driver # storage/logs is unreadable in a container, and the readiness probe logs there LOG_CHANNEL=stderr diff --git a/adapters/laravel-api/mise.toml b/adapters/laravel-api/mise.toml index ef76236..6705985 100644 --- a/adapters/laravel-api/mise.toml +++ b/adapters/laravel-api/mise.toml @@ -36,6 +36,7 @@ run = "composer.phar install --no-dev --optimize-autoloader --classmap-authorita run = "php artisan migrate --force" [tasks.ci-unit] +# :lint repeats :format's pint call on purpose: every adapter runs all five. run = [ { task = ":install" }, { task = ":format" }, diff --git a/adapters/laravel-inertia/.env.example b/adapters/laravel-inertia/.env.example index bb9ea55..c95f3d7 100644 --- a/adapters/laravel-inertia/.env.example +++ b/adapters/laravel-inertia/.env.example @@ -3,6 +3,7 @@ APP_KEY= # off by default: Laravel's debug page leaks APP_KEY and DB_PASSWORD on a 500 APP_DEBUG=false APP_URL=http://localhost:8000 +# the database variables are written by the selected service's driver VITE_APP_NAME="${APP_NAME}" # storage/logs is unreadable in a container, and the readiness probe logs there LOG_CHANNEL=stderr diff --git a/adapters/laravel-inertia/mise.toml b/adapters/laravel-inertia/mise.toml index 8671dc7..5faf34a 100644 --- a/adapters/laravel-inertia/mise.toml +++ b/adapters/laravel-inertia/mise.toml @@ -44,6 +44,7 @@ run = [ run = "php artisan migrate --force" [tasks.ci-unit] +# :lint repeats :format's pint call on purpose: every adapter runs all five. run = [ { task = ":install" }, { task = ":format" }, diff --git a/adapters/nestjs/.env.example b/adapters/nestjs/.env.example index df09d75..b722a81 100644 --- a/adapters/nestjs/.env.example +++ b/adapters/nestjs/.env.example @@ -1 +1,2 @@ PORT=3001 +# the database variables are written by the selected service's driver diff --git a/adapters/nestjs/src/health/health.controller.ts b/adapters/nestjs/src/health/health.controller.ts index 4ffcd53..7be4e64 100644 --- a/adapters/nestjs/src/health/health.controller.ts +++ b/adapters/nestjs/src/health/health.controller.ts @@ -19,7 +19,6 @@ export class HealthController { return { status: 'ok' }; } - // Reuses the singleton's client: readiness is polled every few seconds. // Not `async`: with --db none, require-await fails lint; the driver adds it. @Get('ready') ready(): Promise<{ status: string }> {