Skip to content

Releases: Wolfi-OwO/lattice

v1.4.0

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 26 Jul 10:34
6c6c886

Added

  • Maven and Python projects install themselves too. Only the npm stacks ever did.
    A scaffolded spring-boot or fastapi printed 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-compile and a project-local .venv now run at scaffold
    time, so ./mvnw spring-boot:run and uvicorn app.main:app --reload work
    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, and src/toolchain.js is 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 touches vite.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.json had pointed homepage at that URL for some time and nothing was
    served from it. Content lives in documentation/, 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 in package.json would be that rule broken
    with a different label on it. It is pinned in mkdocs-requirements.txt and
    installed only inside the CI job. tests/docs.test.js checks 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 own venv, deleted by deleting the
    project. A .venv that is already there is reused rather than rebuilt.
  • The next steps now describe the project that exists. A template can declare two
    sequences and nextSteps() picks between them, so a project whose venv lattice just
    built is not told to build one. Both the CLI and scripts/print-next-steps.js call
    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 ./gradlew that fails on a missing SDK is a worse first
    impression than one that was never run.

v1.3.0

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 24 Jul 17:39
93182b8

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.
  • requireRole in the Fastify template. It shipped requireAuth alone, 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 committing gradle-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 to removeAdditional: true, which quietly turns
    additionalProperties: false from "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. Now removeAdditional: false.
  • Spring Boot answered every authorization denial with 500. @PreAuthorize
    throws inside the dispatcher, past the filter chain, so AccessDeniedException
    reached @RestControllerAdvice as an unhandled exception rather than being turned
    into a response by ExceptionTranslationFilter. 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 explicit authenticationEntryPoint the 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. copyTemplate walked 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:repackage fail with an IllegalArgumentException in 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.
    UsersScreen imports androidx.lifecycle.compose.collectAsStateWithLifecycle,
    which lives in lifecycle-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

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 22 Jul 18:55
e365b68

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
    outside src/main/java is not on Maven's source path, so the class does not exist
    at runtime and no compiler error points at it; a test under src/main is packaged
    into the shipped artifact along with its test-only dependencies. Anything that
    merely offends taste stays in lattice 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 say mvn 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, because actions/setup-java puts
    Maven on PATH. Both Java templates now ship mvnw, mvnw.cmd and the wrapper
    properties — Maven's script-only distribution has no maven-wrapper.jar, so no
    binary is committed.

Fixed

  • A project is only installed if its storage driver actually works.
    --database sqlite produced 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, the DOMAINS registration, 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

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 19 Jul 16:26
fa56be8

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 the latest tag 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 lets latest apply on its own — no tag juggling, and nothing
    left pointing at pre-restart code.

v0.0.1

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 19 Jul 16:15
fe675c3

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, because isEmptyDir
    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-project printed source .venv/bin/activate into 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-sqlite3 could not install on current Node. The pinned ^11 ships no
    prebuilt binary for Node 24's ABI, so --database sqlite fell back to a node-gyp
    source build and failed on any machine without a C++ toolchain. Now ^12, which
    supports through Node 26.
  • node-cli never linted. Its ESLint config declared ecmaVersion: 2023, which
    cannot parse the import attributes its own entrypoint uses; every run was a parse
    error rather than a lint result.
  • setTimeout was 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-undef error nobody saw.
  • The one line that keeps password hashes out of responses was itself a lint
    error.
    const { passwordHash, ...safe } = user is CONVENTIONS.md rule 3; the
    omitted key is unused on purpose, which is what ignoreRestSiblings is for.
  • baseUrl in 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 solution tsconfig.json referencing tsconfig.app.json and
    tsconfig.node.json — which also typechecks vite.config.ts for the first time.
  • Neither React template shipped public/, and index.html linked 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
    verify rather than publish, so it inherited none of the checks guarding the
    npm publish — not the tag/package.json agreement, not the human approval. It
    now depends on publish, 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 a lint script and none had ever been linted;
    typecheck and prettier --check had never run at all. Checks are now derived
    from each template's own package.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 native job covers Node 22 and 24. better-sqlite3 is the only dependency
    with a compiled binary, and the storage matrix pins Node 20, so its ABI was only
    ever exercised against one runtime.

  • mongoose to ^9, verified against a real MongoDB container.

  • React 19, Vite 8, TypeScript 6, React Router 7. The frontend templates were a
    full generation behind create-vite on 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-vite pins ~6 and the official
    generator is what decides here. ESLint stays on 9: eslint-plugin-react caps its
    peer range at ^9.7, so 10 cannot install alongside it, and a template that
    cannot npm install is 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: every create-vite template, 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. --list shows 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. --enterprise detects what a project
    is built with — package.json, go.mod, pom.xml, Cargo.toml, build.gradle,
    pyproject.toml, composer.json, Gemfile, Package.swift, pubspec.yaml or a
    .csproj — and writes the matching ci.yml and dependabot.yml. Eleven
    toolchains. A Go module gets go test, a Maven project mvn -B verify, a Rails
    app bin/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 and create-remix no longer created
    anything.

Changed

  • Releasing is now two steps, and nothing writes to main during 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 after npm 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 publish also now waits for a human approval on the
    production environment. 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. --strict exits 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 from LATTICE_LOG_LEVEL / LOG_LEVEL, colourized
    timestamp level: message, stacks on errors. Diagnostics go to stderr (so
    --verbose never contaminates stdout); the user-facing progress stays as it was.

  • --verbose turns on debug diagnostics for a run.

  • Project hygiene to match the rest of the author's repositoriesARCHITECTURE.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 doctor scores 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 postgres gets 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:seed script 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.js asserts
    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 database everywhere — `src/databas...
Read more

v1.0.0

Choose a tag to compare

@Wolfi-OwO Wolfi-OwO released this 15 Jul 18:48

Added

  • Fastify backend template. The same six storage adapters and the same
    repository seam as Express, so --stack fastify --database postgres gets 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:seed script 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.js asserts
    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 database everywheresrc/database/, not
    src/db/. One word for one concept, in every language and every template.

  • The health probes are spelled out: /api/health/liveness and
    /api/health/readiness, replacing /live and /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 framework
    Spring Boot ReadinessDrainLifecycle, a SmartLifecycle that stops before the web server does
    FastAPI app/core/lifecycle.py, which intercepts uvicorn's signal handler

    server.shutdown: graceful alone 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 both CREATE DATABASE and CREATE TABLE IF NOT EXISTS turned out to be check-then-act races — the second one despite its name.
    The losers died on pg_database_datname_index and pg_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 of flyway-core into separate modules, so
    with only flyway-core on the classpath it connects, throws Unsupported Database: PostgreSQL, and dies during context initialisation. mvn spring-boot:run — the
    command the CLI prints as your next step — crashed, while mvn test passed,
    because the test profile runs H2 with flyway.enabled=false and 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_timeout was missing,
    and adding it was necessary but not sufficient: PRAGMA journal_mode = WAL needs 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 before hook in the Fastify template reported Cannot 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 named database — 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.