No description
  • Python 93.6%
  • HTML 6.2%
  • Dockerfile 0.2%
Find a file
Dustin Gohlke 42f9a4bac9 the container was the way in, and it should have been the way out
Two things, and the first is the one that cost a colleague a day.

**The dev loop.** ADR-0055 made the container the default and `forge dev
--native` an "escape hatch, opened by a person". The guidance every assistant
reads then said the container path had "one path and no alternative" - so no
assistant ever offered the other one. On Windows the repository is
bind-mounted and the file events a dev server watches for do not reliably
cross the WSL2 boundary, so **a saved edit changes nothing on screen**. Hot
reload is most of what building an application is, and it was off, on the
platform's most common machine.

The decision's own reason still stands where it was aimed: somebody with an
empty machine must not be refused, and the container is what makes that true.
It was written as though uniformity were the goal everywhere.

So the order is now stated, in the rules and in the ADR: native where node is
present; **ask** to install node where it is not, saying what it is for; the
container when the answer is no or the machine cannot have it. "Never step 3
first" is in the guidance in those words, because the previous wording was
obeyed exactly as written.

blueprints/fullstack-ts 0.9.7 -> 0.9.8, python-web 0.2.1 -> 0.2.2.

**And the bar has a way back.** The chrome the platform draws into every Spark
carried the person's name, the theme and the language, and no route to the
platform - so a colleague on any page of any Spark, including one that 404s
where nothing else is a link, could reach the catalogue only by typing the
address. It is the first thing in the bar now.

The address is derived from the base domain and scheme the shim already has
for the preference cookie, rather than a second variable that can disagree.
Absent without a domain, which is `forge dev` on a laptop - one Spark, no
portal, and a bar with one fewer control beats a link to nowhere. That absence
is half the test.

Styled inline like the rest of the bar, with a fallback on every custom
property: it is drawn into somebody else's page and an unstyled anchor in a
header reads as a broken layout.

[skip ci] - Actions budget.
2026-08-31 19:36:01 +02:00
.cursor/rules the container was the way in, and it should have been the way out 2026-08-31 19:36:01 +02:00
src notify: a constant address, because doctor was right to refuse a variable 2026-08-23 20:13:53 +02:00
tests blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00
.gitignore blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00
.python-version blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00
CLAUDE.md the container was the way in, and it should have been the way out 2026-08-31 19:36:01 +02:00
Dockerfile blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00
pyproject.toml blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00
README.md everywhere: no em dashes, and a check so they stay gone 2026-08-16 15:42:44 +02:00
spark.yaml the container was the way in, and it should have been the way out 2026-08-31 19:36:01 +02:00
uv.lock blueprint: python-web, the four platform concerns in Python 2026-08-11 23:56:01 +02:00

python-web

The blueprint for a Spark that keeps its language. An application that arrived written in Python stays written in Python; the platform's four concerns are solved here, in Python's own idiom. → ADR-0046

src/app/routes.py        the routes, jobs among them
src/app/templates/       Jinja2 - where the original's HTML lands
src/data/                the only place that touches the database
src/db/                  connection and tables, one file per table
src/lib/identity.py      require_actor() from the platform's headers
src/lib/problem.py       expected failures are values, not exceptions
src/lib/source.py        read / read_all, cannot_work vs failed

The example

One example, and it is the shape most imported applications actually are: a scheduled job fetches from outside, replaces a table whole, and a page shows it. refresh-rates → exchange_rate → /rates. Delete all three when you no longer need them - src/db/exchange_rate.py, src/data/exchange_rate.py, the route, and src/app/templates/rates.html.

Running it

uv sync
uv run pytest        # the suite
uv run pyright       # strict, and not optional - see pyproject.toml
forge dev            # run it here, with a working login