Releases: Wolfi-OwO/lattice
Release list
v1.4.0
Added
-
Maven and Python projects install themselves too. Only the npm stacks ever did.
A scaffoldedspring-bootorfastapiprinted its install commands and left the
user to run them, which meant the promise the whole tool is built on — the project
you land in boots — held for JavaScript and was a to-do list everywhere else.
./mvnw -DskipTests test-compileand a project-local.venvnow run at scaffold
time, so./mvnw spring-boot:runanduvicorn app.main:app --reloadwork
immediately. Verified end to end: a scaffolded FastAPI project's own suite passes
from its venv without a single manual step. -
A runtime check that reads the project rather than a constant. lattice installs
dependencies; it does not install runtimes, andsrc/toolchain.jsis what lets it
tell the difference. It detects the installed JDK or Python, compares it against the
floor the template itself declares —<java.version>in the pom,requires-python
in the pyproject — and if the runtime is missing or too old it says so, naming both
versions, and leaves the project complete. Downloading a JDK would change the
machine rather than the directory, and would need a system package manager lattice
would have to guess at. -
A styling choice for the React templates —
--styling plain|scss|bootstrap|tailwind.
The frontends shipped one hand-written stylesheet and no way to ask for anything
else, so wanting Tailwind meant installing it, wiring the build and rewriting the
components — most of the work scaffolding exists to save. Styling is now to a
frontend what storage is to a backend: the template carries every variant and the
scaffold keeps exactly one. What makes them interchangeable is a class-name
contract (.app__header,.app__nav,.table,.pager,.muted,.error) that
the components are written against and every variant implements, so the choice
changes the stylesheet and nothing else. Plain and SCSS compile to byte-identical
CSS; Bootstrap and Tailwind are themed to match rather than left at their defaults.
Only Tailwind touchesvite.config.js, and Sass and Tailwind land in
devDependencies— they are compile-time tools, not runtime ones. -
A documentation site, at wolfi-owo.github.io/lattice.
package.jsonhad pointedhomepageat that URL for some time and nothing was
served from it. Content lives indocumentation/, one folder per section, and the
navigation is generated from the folder tree rather than listed in a config — so
adding a page is adding a markdown file and adding a section is adding a directory.
Every page carries an edit link that opens it in GitHub's editor, which makes a
typo fix a two-minute pull request instead of a clone.The long-form documents stay at the repository root where they are actually read —
in a diff, in a pull request, in an editor — and the site borrows them at build
time (scripts/docs-mirror.js). Their links are rewritten to resolve on the site
and their edit links point back at the real file rather than at the generated copy.MkDocs is not an npm dependency and will not become one: ADR-0001 covers dev
dependencies too, and a docs toolchain inpackage.jsonwould be that rule broken
with a different label on it. It is pinned inmkdocs-requirements.txtand
installed only inside the CI job.tests/docs.test.jschecks the wiring — that the
workflow rebuilds for every file the site mirrors, that no navigation entry names a
page that does not exist, that a pull request builds but never deploys — none of
which needs MkDocs installed to run.
Changed
- Python installs into
.venv, and an existing one is reused. The old refusal
reasoned that Python has no one obvious package manager — pip, pipx, poetry, uv and
conda are all normal — and that is still right about the machine. It was never
right about.venv: a virtual environment inside the project is a directory in the
project, made with the standard library's ownvenv, deleted by deleting the
project. A.venvthat is already there is reused rather than rebuilt. - The next steps now describe the project that exists. A template can declare two
sequences andnextSteps()picks between them, so a project whose venv lattice just
built is not told to build one. Both the CLI andscripts/print-next-steps.jscall
that one function — CI runs exactly what the user is shown, and a second copy of the
rule could not have been kept honest. - Android is deliberately still not auto-installed. Its build needs the Android SDK
and accepted licences, and a./gradlewthat fails on a missing SDK is a worse first
impression than one that was never run.
v1.3.0
Added
- Every backend template now ships the products domain, not just Express. 1.2.0
claimed the repository seam was domain-agnostic and proved it across six storage
adapters — but only inside one template. Fastify, Spring Boot and FastAPI still
shipped a single domain each, so the claim held for one language. All four now
carry products: money as integer cents, stock moved by a signed delta rather than
set to an absolute, and SKU as a natural key so seeding is idempotent. The three
latent bugs below are what porting it found; none were reachable before, because
the code that exposes them did not exist. src/models/in the Fastify template, matching the Express layout: one flat
file per domain, describing the record in storage-neutral terms, with each
dialect's DDL derived from it rather than written out six times.requireRolein the Fastify template. It shippedrequireAuthalone, so a
route could ask whether a caller was authenticated but not whether they were
allowed — every admin-only operation was open to any signed-in user, and the
template had no way to express otherwise.- The Android template builds. It was the last template whose correctness rested
on review rather than execution, because Gradle's wrapper has no script-only form
and the no-binaries rule forbade committinggradle-wrapper.jar— so the template
shipped a build definition nobody could run. The wrapper now ships under a single
documented exception, and a CI job builds the debug APK on a runner with no Gradle
installed, which is the path a user cloning the generated project takes. The jar is
not trusted on faith: its SHA-256 is pinned in the unit suite, and CI runs
gradle/actions/wrapper-validation, which fails unless it is a byte-identical
published Gradle wrapper.
Fixed
- Fastify accepted writes it had been told to reject, and reported success.
Fastify defaults ajv toremoveAdditional: true, which quietly turns
additionalProperties: falsefrom "refuse these" into "delete these". A caller
PATCHing a read-only field was told the write succeeded while the value was
dropped on the floor — the request returned 200 where it should have returned 400,
and no log recorded that anything had been discarded. NowremoveAdditional: false. - Spring Boot answered every authorization denial with 500.
@PreAuthorize
throws inside the dispatcher, past the filter chain, soAccessDeniedException
reached@RestControllerAdviceas an unhandled exception rather than being turned
into a response byExceptionTranslationFilter. A forbidden request looked like a
server crash to any client, and to any log-based alert. - Spring Boot answered unauthenticated requests with 403 instead of 401. Without
an explicitauthenticationEntryPointthe container's default page shape won, so a
caller who had sent no credentials was told their credentials were insufficient —
the one response that tells a client not to bother retrying with a token. - The scaffolder copied build output into new projects.
copyTemplatewalked the
template tree indiscriminately, so anything a local build had left behind —
target/,build/,node_modules/,__pycache__/,.gradle/— was copied into
the generated project. A stale class file from a previous build was enough to make
spring-boot:repackagefail with anIllegalArgumentExceptionin a project the
user had just created and never built. Those directories are now skipped by name. - The Android template was missing a dependency its own source needed.
UsersScreenimportsandroidx.lifecycle.compose.collectAsStateWithLifecycle,
which lives inlifecycle-runtime-compose— a dependency the build never declared.
The screen could not compile, and every other error in the build cascaded from that
one unresolved import. It went unnoticed for exactly one reason: without a wrapper
the template had never once been built.
Changed
- The Express products domain has tests. It shipped in 1.2.0 with none — the
suite people saw pass covered health and users, so the domain added to prove the
storage seam was itself unverified. 26 tests now cover it, including the cases that
differ per adapter: SKU case-insensitivity, a refused stock movement leaving the row
untouched, PATCH refusing to set stock directly, and a float price being rejected. - Test counts across the templates, as a result: Express 5 → 32, Fastify 0 → 28,
Spring Boot 3 → 25, FastAPI 4 → 29.
Documentation
- STRUCTURE.md carried a "no binaries in the repository" rule with no exceptions and
a verification section written before any of the above existed — it still described
Android as unbuildable and understated every test count. Both are now accurate, and
the one committed binary is documented as an exception with its justification and
its digest rather than left to be discovered.
v1.2.0
Added
- A products domain, in every storage adapter. Until now every template shipped
exactly one domain, so the claim that the repository seam is domain-agnostic had
never been tested by anything. Products tests it across all six adapters. - A generated project whose layout would silently not work is now rejected. The
gate runs on the staged tree, as the last thing before the transaction commits, so
a violation rolls the whole generation back instead of leaving a rejected project
on disk. Every rule describes a placement that is silently wrong: a.java
outsidesrc/main/javais not on Maven's source path, so the class does not exist
at runtime and no compiler error points at it; a test undersrc/mainis packaged
into the shipped artifact along with its test-only dependencies. Anything that
merely offends taste stays inlattice doctor, which scores rather than refuses. - The Java templates ship a Maven wrapper. A scaffolded Spring Boot project had
no way to build: its README and the CLI both saymvn spring-boot:run, and the
template shipped no wrapper, so the project only ran for someone who already had a
compatible Maven installed. CI never saw it, becauseactions/setup-javaputs
Maven on PATH. Both Java templates now shipmvnw,mvnw.cmdand the wrapper
properties — Maven's script-only distribution has nomaven-wrapper.jar, so no
binary is committed.
Fixed
- A project is only installed if its storage driver actually works.
--database sqliteproduced a project that died on its first command with
Error: Could not locate the bindings file. better-sqlite3 compiles a native
binding during install, and when lattice spawned npm that install script was
silently skipped: npm exited 0, the package was present, lattice printed
"Installed dependencies", and nothing worked. Deterministic, three runs out of three.
Documentation
- Rule 5 said "adding a domain is adding a file". Shipping the products domain
proved that false — fifteen files, of which one was the seed data. The rest was a
repository in all six adapters, theDOMAINSregistration, and four API files.
The scope is now explicit and the real number is written down. - Where models and data-transfer objects live is now stated rather than implied.
v1.0.1
Changed
- Renumbered from 0.0.1. The code is identical; only the version differs.
0.0.1 is published and correct, but npm will not apply thelatesttag to a
version below one already published, and 1.0.0 cannot be removed — it is outside
npm's 72-hour unpublish window and above its 300-downloads-a-week exemption.
Moving the tag afterwards is not available either: trusted publishing mints a
credential scoped to publishing, and a dist-tag change is refused with E401. - 1.1.0, 2.0.0 and 2.1.0 were unpublished earlier and npm retires those numbers
permanently, so the first free version above 1.0.0 is 1.0.1. Numbering above the
old release is what letslatestapply on its own — no tag juggling, and nothing
left pointing at pre-restart code.
v0.0.1
Added
-
Generation is all-or-nothing. Every file a scaffold writes goes to a staging
directory first, and the project appears at its real path only once generation has
fully succeeded. A failure — unreadable template, full disk, Ctrl-C — leaves the
working directory exactly as it was. Previously a failure partway through left a
half-written project that then blocked its own retry, becauseisEmptyDir
correctly saw a non-empty directory and demanded--force.Installing dependencies and starting the database stay outside the transaction:
both are recoverable by design, and rolling back a valid project because a
registry blipped would destroy work that is entirely fine.
Fixed
- The Python templates told you to activate a virtualenv they never created.
lattice my-app --stack ml-projectprintedsource .venv/bin/activateinto a
directory with no.venv, because lattice deliberately does not auto-install for
Python. Both Python templates now print the two lines that actually create it —
the same ones their READMEs always had. better-sqlite3could not install on current Node. The pinned^11ships no
prebuilt binary for Node 24's ABI, so--database sqlitefell back to a node-gyp
source build and failed on any machine without a C++ toolchain. Now^12, which
supports through Node 26.node-clinever linted. Its ESLint config declaredecmaVersion: 2023, which
cannot parse the import attributes its own entrypoint uses; every run was a parse
error rather than a lint result.setTimeoutwas undefined to ESLint in the Express and Fastify templates, so
the graceful-shutdown path — the code that matters most at deploy time — was a
no-undeferror nobody saw.- The one line that keeps password hashes out of responses was itself a lint
error.const { passwordHash, ...safe } = useris CONVENTIONS.md rule 3; the
omitted key is unused on purpose, which is whatignoreRestSiblingsis for. baseUrlin the TypeScript template, deprecated in TypeScript 6 and removed
in 7, and unnecessary since TypeScript 5. The template now uses create-vite's
current shape — a solutiontsconfig.jsonreferencingtsconfig.app.jsonand
tsconfig.node.json— which also typechecksvite.config.tsfor the first time.- Neither React template shipped
public/, andindex.htmllinked no icon, so
every scaffolded app 404'd on its favicon. - 23 files across five templates were unformatted by their own Prettier config.
- GitHub Packages published releases that npm had refused. The job depended on
verifyrather thanpublish, so it inherited none of the checks guarding the
npm publish — not the tag/package.jsonagreement, not the human approval. It
now depends onpublish, and skips rather than fails when a version is already
present.
Changed
-
CI runs every check a template declares, not one chosen for it. Every
JavaScript template declared alintscript and none had ever been linted;
typecheckandprettier --checkhad never run at all. Checks are now derived
from each template's ownpackage.json, and a script that cannot be a pass/fail
check must be listed as exempt with a reason — a test fails on one that is
neither. Fifteen checks now run where five did. -
The Python workflow runs the steps the CLI actually prints, read from the
registry rather than retyped. It claimed to do this before and did not, which is
why it passed while the printed steps were broken. -
A
nativejob covers Node 22 and 24.better-sqlite3is the only dependency
with a compiled binary, and the storage matrix pins Node 20, so its ABI was only
ever exercised against one runtime. -
mongooseto^9, verified against a real MongoDB container. -
React 19, Vite 8, TypeScript 6, React Router 7. The frontend templates were a
full generation behindcreate-viteon every axis — not broken, which is why it
needed a deliberate change rather than waiting for a failure. No template source
had to change: the components, router, hooks and API client were already written
against APIs that survived all four majors.TypeScript 6 rather than 7, because
create-vitepins~6and the official
generator is what decides here. ESLint stays on 9:eslint-plugin-reactcaps its
peer range at^9.7, so 10 cannot install alongside it, and a template that
cannotnpm installis worse than one a major behind.
Added
.github/workflows/template-drift.yml— runs the official generators weekly
and reports what the built-in templates are missing or behind on. Both frontend
bugs above were exactly this drift, found by a person scaffolding a project
rather than by CI. It reports rather than merges: some of the difference is
deliberate, and a job that adopted upstream's output wholesale would delete the
router, API client and layout conventions that make these templates worth having.
Added
--generator <id>— scaffold with a framework's own official tool instead of
a built-in stack. 28 of them: everycreate-vitetemplate, Next, Nuxt, SvelteKit,
Astro, React Router, Vue, Expo, Angular, three .NET project types, Cargo, Go,
Laravel, Rails, Dart, Flutter and Swift. Opt-in on purpose — it needs the network,
it needs that toolchain installed, and its output is whatever upstream ships today
rather than something lattice verified. The built-in stacks remain the default and
the thing that is promised to boot.--listshows them; ADR-0003 explains the
tiers.--enterprise— overlay the scaffolding a repository needs once more than one
person works on it:docs/adr/,todo/,organizational/, community-health
files, a Trivy security scan, a release workflow, and CI. The project's own README
is kept and composed under a badge header rather than replaced, and a directory
table is generated from the tree — including a column for what must never go in
each directory, which is the half that stops a layout from rotting.- CI matched to the project's build tool.
--enterprisedetects what a project
is built with —package.json,go.mod,pom.xml,Cargo.toml,build.gradle,
pyproject.toml,composer.json,Gemfile,Package.swift,pubspec.yamlor a
.csproj— and writes the matchingci.ymlanddependabot.yml. Eleven
toolchains. A Go module getsgo test, a Maven projectmvn -B verify, a Rails
appbin/rails test. A project whose build tool is unrecognised gets everything
except the CI, which is better than a workflow that cannot pass. --owner <name>— the GitHub owner or organisation that fills the badge and
link slots in the enterprise overlay..github/workflows/generators.yml— scaffolds all 28 delegations, installs
and builds each, and asserts every one received the CI of its own build tool. The
argv for these are claims about other people's CLIs, and nothing but running
them can tell you when one goes stale. It earned this on its first run: Remix v2
had been upstreamed into React Router andcreate-remixno longer created
anything.
Changed
-
Releasing is now two steps, and nothing writes to
mainduring one. The
version bump and changelog cut happen in a reviewed pull request opened by the new
Prepare release workflow; publishing then only verifies and publishes, with
contents: read. Previously the version commit landed afternpm publish, so a
failure in that final step would leave a version on a registry that never forgets
and no record of it here.npm publishalso now waits for a human approval on the
productionenvironment. See Releasing in the README. -
lattice doctor [path]— score a project's structure and hygiene, offline.
It grades a tree 0–100 across structure, hygiene, testing, CI/CD, containerization,
observability and security, and lists what to fix most-load-bearing-first with the
why and the fix. Deterministic and local by design — no network, no model — so
it stays a scaffolder, not a service. The checks encode the bar these repositories
already hold themselves to.--strictexits non-zero below 60 so it can gate CI. -
A leveled logger (
src/logger.js) modeled on the Winston format used across
the author's services: level fromLATTICE_LOG_LEVEL/LOG_LEVEL, colourized
timestamp level: message, stacks on errors. Diagnostics go to stderr (so
--verbosenever contaminates stdout); the user-facing progress stays as it was. -
--verboseturns on debug diagnostics for a run. -
Project hygiene to match the rest of the author's repositories —
ARCHITECTURE.md,
CONTRIBUTING.md,SECURITY.md,.editorconfig,.github/CODEOWNERS, and
docs/adr/with the first two decision records (zero-dependency core; why lattice
is a library, not an enterprise-layered app).lattice doctorscores itself 100
now — it did not before, and it said so.
Added
- Fastify backend template. The same six storage adapters and the same
repository seam as Express, so--stack fastify --database postgresgets you a
schema-first API whose generated suite is green on all six. database/demo data in every backend. One file per domain, an idempotent
loader that goes through the repository rather than a driver, and a
database:seedscript that means the same thing in every language.CONVENTIONS.md— the rules every template obeys, and the reason the
templates read like one product instead of twenty personal styles.- The conventions are tests now, not prose.
tests/registry.test.jsasserts
the user shape, the demo-data layout, the frontend dependency rules and the
storage spelling, so a template that drifts fails the build instead of being
noticed a year later.
Changed
- The storage seam is spelled
databaseeverywhere — `src/databas...
v1.0.0
Added
- Fastify backend template. The same six storage adapters and the same
repository seam as Express, so--stack fastify --database postgresgets you a
schema-first API whose generated suite is green on all six. database/demo data in every backend. One file per domain, an idempotent
loader that goes through the repository rather than a driver, and a
database:seedscript that means the same thing in every language.CONVENTIONS.md— the rules every template obeys, and the reason the
templates read like one product instead of twenty personal styles.- The conventions are tests now, not prose.
tests/registry.test.jsasserts
the user shape, the demo-data layout, the frontend dependency rules and the
storage spelling, so a template that drifts fails the build instead of being
noticed a year later.
Changed
-
The storage seam is spelled
databaseeverywhere —src/database/, not
src/db/. One word for one concept, in every language and every template. -
The health probes are spelled out:
/api/health/livenessand
/api/health/readiness, replacing/liveand/ready. Rule 1 applies to URLs
too, and Spring Boot's Actuator already served the long names — the other three
backends were the ones out of step. -
Fastify drains through terminus, like Express, instead of a hand-rolled
process.once('SIGTERM')handler that flipped a flag a route read. Readiness
now lives on the http.Server underneath Fastify, which is the only place it
can answer 503 from the instant a signal lands while the app above goes on
serving the requests already in flight. -
Every backend now drains the same way, in every language. Terminus was only
ever the JavaScript spelling of it; the behaviour is the contract:Mechanism Express, Fastify @godaddy/terminus— readiness on the http.Server, below the frameworkSpring Boot ReadinessDrainLifecycle, aSmartLifecyclethat stops before the web server doesFastAPI app/core/lifecycle.py, which intercepts uvicorn's signal handlerserver.shutdown: gracefulalone was not enough for Spring, which is worth
writing down because the name suggests it is: it stops accepting new connections
immediately, so readiness does not go to 503 — it becomes unreachable, and a load
balancer that is still routing to this instance gets connections refused rather
than drained. The new lifecycle bean buys the window back.All three were drained under a real SIGTERM against a real booted server, not
asserted: readiness flips to 503 while liveness stays 200, in-flight requests
finish, then the process exits. -
CI scaffolds real projects and runs their suites, across six storages and
two backends. A scaffolder cannot be tested by testing the scaffolder.
Fixed
-
Two Postgres races that only a parallel test runner could find.
node:test
runs test files in a process each, so two processes booted the app against a
fresh database at once, and bothCREATE DATABASEandCREATE TABLE IF NOT EXISTSturned out to be check-then-act races — the second one despite its name.
The losers died onpg_database_datname_indexandpg_type_typname_nsp_index,
neither of which mentions the thing that actually collided. Creating the database
now tries and forgives, and the schema is created under an advisory lock, which
are the only forms of each that are atomic. It failed roughly one CI run in two;
it now survives a fresh database six times out of six. Fixed in Express as well
as Fastify: Mocha's sequential files hid it there, but the adapter is what has to
be right, not the runner that happens to hide it. -
The Spring Boot template could not start. Flyway 10 (which Spring Boot 3.3
manages) moved per-database support out offlyway-coreinto separate modules, so
with onlyflyway-coreon the classpath it connects, throwsUnsupported Database: PostgreSQL, and dies during context initialisation.mvn spring-boot:run— the
command the CLI prints as your next step — crashed, whilemvn testpassed,
because the test profile runs H2 withflyway.enabled=falseand never executes a
migration at all. A green suite on top of an application that does not boot is the
exact failure this project exists to prevent, so CI now boots the template against
a real PostgreSQL and probes it, rather than trusting the tests. -
SQLite could not be opened by two processes at once.
busy_timeoutwas missing,
and adding it was necessary but not sufficient:PRAGMA journal_mode = WALneeds an
exclusive lock and SQLite does not run the busy handler for it, so it fails
instantly regardless. It is set once now, by whoever gets there first. -
A failed
beforehook in the Fastify template reportedCannot read properties of undefined (reading 'close')from its teardown, burying the error that
actually caused it. -
The CLI told you to run
docker compose up -d db, but the service it generates
is nameddatabase— the command it printed errored out. It also pointed at
src/db/after that directory had been renamed. Both were user-facing, and
both shipped green, because the test guarding the spelling only looked at the
templates and never at the scaffolder itself. It looks at both now.