Skip to content

build: Turborepo fronts both toolchains - #9

Merged
jterrazz merged 10 commits into
mainfrom
build/turborepo
Sep 18, 2026
Merged

jterrazz merged 10 commits into
mainfrom
build/turborepo

Conversation

@jterrazz

Copy link
Copy Markdown
Owner

One task graph over go.work and the pnpm workspace, with caching, in place of the Makefile's sequential loops.

  • Turborepo 2.11 with the experimental Go workspace support; Go lint stays golangci-lint, not the go vet default.
  • The host app's module becomes spwn.sh/apps/gatehouse: turbo names a package by its module path's last component and refused the graph over apps/gate and packages/gate both deriving to gate. Nothing imported the module; directory, binary and JS package keep their names.
  • The tests/ workspace package becomes specs/, flattened one level, which is where the toolchain's rule I2 puts product specifications. The move surfaced three relative paths that resolved outside the repository.
  • @jterrazz/typescript 10.1.10, whose gate admits turbo's task-log directory.
  • Every CI job restores the turbo cache. Measured on one workstation: make lint 28.8s cold / 9.8s warm before, 16.7s / 1.7s after; make test 14.7s / 2.4s before, 7.9s / 1.4s after.

ADR-016 records the decision, the two experimental flags relied on, and what the graph cannot express (^build, since world and transpile require each other).

🤖 Generated with Claude Code

jterrazz and others added 10 commits September 18, 2026 21:52
spwn.sh/apps/gate and spwn.sh/packages/gate both reduce to their last
path component, so any tool that identifies a module by that component
sees one name for two modules. Turborepo is the first to care and it
refuses the whole graph over it.

The host app is the cheap side of the collision: nothing imports
spwn.sh/apps/gate, so this is the module line and nothing else. The
directory stays apps/gate, the binary stays spwn-gate, and the JS
package in the same directory keeps its own name.

Gatehouse is the word because the gate is the crossing and the gatehouse
is the host-side building that houses and guards it, which is exactly
what apps/gate is: the container that owns cookie custody, MCP routing
and the browser sidecar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both toolchains were walked by shell loops that started from nothing
every time: golangci-lint once per module, then pnpm -r lint, then the
same again for go test. Turborepo 2.11 reads go.work and
pnpm-workspace.yaml into a single graph, hashes each package's inputs
and replays what has not changed.

Two future flags carry it. experimentalGoWorkspaces is what makes a Go
module a package; experimentalTaskCommand is what keeps lint on
golangci-lint instead of the go vet Turborepo maps by default, which
would have dropped every rule .golangci.yml adds.

The cache goes to .artifacts/turbo so one rm -rf still empties the
repository. The per-task log Turborepo writes beside each package has
no such setting, and left untracked it hashes itself into the next run
— one task of twenty-one cached instead of twenty-one — so .gitignore
names it.

A root package.json is what Turborepo needs to find the package
manager. It also makes tests/ a root-level test directory to
@jterrazz/typescript's I2 rule, which is true and was never visible:
the 63 diagnostics enter the ratchet as debt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lint, test, build, generate, web-build, test-web-unit and test-gate-node
now call turbo run. Every target name CI invokes is unchanged, and so is
what each one covers.

GO_MODS goes with the loops it fed: go.work is still the single source of
truth for what gets linted and tested, but Turborepo reads it now, so the
Makefile has no list to derive. The comment says that rather than
disappearing with the variable.

Two filters encode facts about the graph. go-workspace is Turborepo's
synthetic scope over every module: lint carries a command override that
the scope would try to run at the repository root, so lint excludes it,
while test selects the Go half by asking for the scope and everything it
depends on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cache is only worth its wiring across runs, so each job restores
.artifacts/turbo before its Makefile target and saves it after, on a key
that tracks HEAD and a restore ladder that falls back to the branch, then
the lockfile, then anything. The key carries the job name because two
jobs saving one key race, and a job replays only the tasks it ran.

The Go-only jobs gain a pnpm setup they never needed before: a Makefile
target reaches turbo whatever toolchain it drives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… cost

The loop no longer describes go vet or pnpm -r lint, the toolchain table
names turbo, and the sentence that said GO_MODS reads go.work now says
Turborepo does.

ADR-016 carries the rest: the two experimental flags the wiring depends
on, the gate name collision that made turbo refuse the graph until a
module moved, the module cycle that rules out ^build, and the measured
before/after — warm lint 9.81s to 1.71s, warm test 2.36s to 1.40s, warm
build 0.63s to 1.41s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PATH MOVE — every reference to tests/ in this repository now reads
specs/, and the pnpm member tests is now specs. Peers holding work over
the old paths must rebase.

Turborepo made the repository root a package, which made tests/ a
root-level test directory in the eyes of @jterrazz/test's I2 rule: 63
diagnostics, baselined as debt by the commit that landed turbo. The rule
is not a false positive. It names the only two test roots the toolchain
knows — sibling module tests under src/, and product specifications in
specs/ — and this member holds the second kind. So the debt is paid by
taking the name, not by recording it.

The directory is renamed and flattened one level: tests/specs/cli/
becomes specs/cli/, tests/web/ becomes specs/web/, and the package root
IS the specs root. That is what makes c1-domain-structure read every
underscored folder of it as ground, which the tree already meant:
_contracts/, _simulators/, _fixtures/, _catalog/, _support/, and
manual/ which takes the underscore it was always owed. One file was not
ground and had to move: the raw-execSync upgrade smoke, which now sits
in the cli facet's smoke domain beside the scaffold smoke it runs with.

Three paths were depth-sensitive and would have resolved above the
repository root: the CLI runner's binary, the lint facet's grep root,
and the guard's own exclude-dir. The C1 fix and the two vitest configs'
F3 exemption are the only rules the new shape argued with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gitignore gate mapped .turbo to .artifacts/turbo/ and failed a
.gitignore that said otherwise, for a path Turborepo publishes no key to
move: only its cache obeys cacheDir, and the per-package
<package>/.turbo/turbo-<task>.log does not. This repository escaped the
rule with the spelling **/.turbo/, which the gate reads as anchored to
its own file and never judges — unenforced rather than held.

10.1.10 puts .turbo on the gate's closed exception list, so .turbo/ is
now a line the gate passes and a tracked file under .turbo/ still fails.
The escape goes; the comment states the invariant instead of the
workaround.

Nothing else in 10.1.7 through 10.1.10 touches this tree: the vitest
globs gained prose about *.e2e.ts and *.render.tsx without changing what
they match, and the suppressions pass learned to read a rule armed only
in an override. Both members report the counts they reported on 10.1.6 —
apps/web 1280 diagnostics and 4 suppressions, specs 366 and 0 — so no
baseline is rewritten here.

The lockfile carries the version and nothing else. A plain install also
flips which of the two installed typescript versions msw, shadcn and
vitest peer-resolve against, in either direction and with no package
changing version; the seven lines here are the bump alone, and
--frozen-lockfile accepts them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The record still described a gate gap and left a question with the
toolchain owner. It is answered: 10.1.10 admits the path, the plain
.gitignore line holds, and the exception is the toolchain's own decision
rather than this repository's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Turborepo fronts both toolchains of this repository, on the owner's word given at review of the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The root package.json turbo needs now carries packageManager, and pnpm/action-setup refuses two sources for the version: every job died at setup with 'Multiple versions of pnpm specified'. The workflow stops naming one; the action reads the field, so the version has exactly one owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jterrazz
jterrazz merged commit cdb9648 into main Sep 18, 2026
12 checks passed
@jterrazz
jterrazz deleted the build/turborepo branch September 18, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant