00 - Bootstrap
On this page 5
Getting the application to exist, run, and talk to a database. Complete.
Several items here were framework or tooling bugs rather than application work. They are recorded because the fixes live in other repositories, and because anyone reproducing this setup on a clean machine benefits from knowing what was wrong.
Application
- Scaffold with
buddy newinto the existing repository - Package-based layout: no
storage/framework/core, every@stacksjs/*resolved from npm - Generate
APP_KEYand drop the template's undecryptable encrypted env files - Name the application:
package.json,config/app.ts, README, MIT license - Point
lint,typecheck, andtestscripts at the buddy commands - Disable the commerce, cms, marketing, and monitoring feature bundles
- Write
AGENTS.md: domain vocabulary, git storage layout, framework sync-back rule ./buddy setup:ai claudefor the skill set and launch config
Database
- Switch
DB_CONNECTIONto postgres in.envand.env.example DB_USERNAME=postgres, the only role pantry's cluster has- Regenerate the migration corpus for Postgres (
./buddy migrate:regenerate postgres) - Remove the stale SQLite model snapshot so the dialect guard passes
./buddy migrateapplies cleanly: 82 tables./buddy seedruns without errors- Database is created automatically from
.envby pantry, not by hand
Environment
deps.yamlgenerated fromconfig/deps.tsplus.env, installing Postgres 17 and Bun 1.3.14- PostgreSQL starts as a pantry service before anything tries to connect
- Link the local Stacks checkout with
./buddy link:core --all
Upstream fixes this required
Each one is committed and pushed in the repository named.
-
stacks -
buddy newrefused any existing directory, so cloning a repository first and scaffolding into it was impossible. It now accepts an empty directory, or one holding only.git, and skipsgit initwhen a repository is already there. -
stacks -
buddy newnow resolves the framework from npm by default rather than vendoring 2,000 files into the first commit.--with-coreopts back in. -
stacks -
buddy setupinstalled every database engine rather than the oneDB_CONNECTIONnames, and emitted no services section, so PostgreSQL was installed but never started and its own database creation failed with a connection refused. -
stacks - The query log columns hold whole SQL statements and stack traces but were
varchar(255). SQLite never minded; Postgres rejected every insert. -
stacks - Query logging recorded the literal string
[object Promise]for every statement, which also made the N+1 detector report[OBJECT PROMISE]as the repeating query shape. -
stacks - A 13 MB packed tarball was committed at the repository root and shipped into every scaffolded project.
-
pantry - Two of the four
initdbcall sites omitted--username=postgres, so whichever one created the cluster decided its superuser. Database creation then failed withrole "postgres" does not existagainst pantry's own cluster. -
pantry - Service units are per-project but the PostgreSQL data directory was global, so two projects on different majors destroyed each other's cluster in a loop, each backing up and re-initializing what the other had just built.
-
pickier - A function whose return type is written as an inline union (
): { ok: true, ... } | { ok: false, ... } {) makesno-unused-varsreport every parameter as unused, because the parser does not find the body.transitionDraftinapp/Actions/Pull/state.tsis the case that found it, andresolveExpiryinapp/TokenScopes.tsis the case that proved it recurs.Fixed upstream and verified against the published build: the rule scanned past the return type for a
{and stopped at the first one following a completed brace pair, so the second member of the union was read as the body. The body then read as empty and--fixrenamed every parameter to_namewhile the body kept referring toname- code that no longer compiles, which is what makes it worse than noise.pickier@0.1.49, withtest/rules/no-unused-vars-return-types.test.tspinning it. -
pickier -
no-unused-varsalso missed a module-levelconstreferenced before it is declared, which is ordinary and valid: the eight content constants inSeedDemo.tswere each reported as unused while being used. Also fixed in0.1.49. -
pickier - the
markdown/heading-start-leftauto-fix corrupts a document when the heading it moves is nested inside a list item. It dedents the heading to column 0 and leaves the prose under it at its original indent, which closes the list, promotes the heading out of the item, and turns that prose into an indented code block.Reproduced on a six-line fixture and confirmed by rendering both versions: the prose that read as a paragraph inside the item comes back as
<pre><code>.--fixreports success while doing it, so a sweep across a repository full of roadmap prose is a silent content change.Already fixed upstream, and not filed for that reason. It reproduces on the
0.1.67this repository resolves, and0.1.76neither flags nor rewrites the same fixture, while still flagging and correctly dedenting a heading that is genuinely indented at the top level with no list around it. So the fix is the targeted one rather than the rule being dropped, and the only thing standing between us and it is a stale lock entry: the declared range already admits0.1.76, and dependency updates here belong to buddy-bot.Checking this before writing an issue is the whole point. Four upstream claims earlier in this project did not survive the same check, one of them because it had been fixed a release after the version in the lockfile.
The three warnings it fired on here were in
docs/todo/14-diff-engine.md, and the rule is right that those headings did not belong: they were###and##inside a- [x]item. They are bold lead-ins now, which is the house style everywhere else in these files and what the surrounding prose in that same entry already used. -
pickier was installed three times, and the oldest copy was the one doing the linting.
package.jsondeclaredpickierdirectly whilebetter-dxalready ships it, which AGENTS.md forbids precisely because two ranges for one tool drift. They had: the direct^0.1.67pinned the hoisted copy at0.1.67, so@stacksjs/lint(which wants^0.1.70) got a nested0.1.72and@stacksjs/logsmitha nested0.1.45. Three copies, andbuddy lintran the oldest of them.Dropping the declaration alone changed nothing, because the lockfile still satisfied every range with what it had.
bun update pickiercollapsed it: one copy at0.1.76, the two nested ones and a transitiveclarityunder them gone, and no other package touched. The direct declaration was independenciesrather thandevDependencies, so a linter was also a runtime dependency of the application; the only reference to it anywhere isimport type { PickierOptions }inconfig/code-style.ts, which needs nothing at runtime and still typechecks.The stale pin was hiding a whole class of files from the linter.
0.1.67scanned 3,466 files and0.1.76scans 3,513, and every one of the 13 files that had never been flagged lives under a bracketed route directory -[owner]/index.stx,[owner]/[repository]/pull/[number].stx,.../run/[number].stx,.../settings.stx,view/[...target].stx. Those are the application's primary views, including the pull request page, and they had never been linted at all. 68 class ordering warnings came out of them, verified as a permutation the same way as the first sweep: identical class tokens on every line, byte-identical outside eachclassattribute.So the markdown bug above was a symptom. The reason a fixed rule was still corrupting documents here is that the tool doing the work was nine patch releases behind, held there by a declaration that should not have existed.
-
Swept the rest of what
better-dxships, and@stacksjs/bunpresshad the same shape, four copies deep. Of the 14 packages better-dx bundles, exactly one other was declared directly here:@stacksjs/bunpressat^0.2.14, which pinned the hoisted copy to0.2.14while@stacksjs/actions,@stacksjs/docsand@stacksjs/typesall want^0.2.15. They each got their own nested0.2.16. One declaration, four installs.Removing it resolved to a single
0.2.22, and stx and crosswind stayed at one copy each throughout, so the duplication had been confined to bunpress itself. The reference isimport type { BunPressOptions }inconfig/docs.ts, a type-only import again, and it still typechecks with nothing declared.This one broke the docs build, which is how it earned its keep.
0.2.22validates markdown links and0.2.14has no such check at all - verified by unpacking0.2.14and finding the error string absent rather than by inferring it. Three links in12-api-and-agents.mdpointed at./09-ci-cd.md, a file that has never existed; phase 9 is09-checks-ci.md. Every one of them was a dead link on the published site, and the old version walked past all three. Fixed, and the docs build passes.The pattern across both: a direct declaration of something
better-dxalready ships does not just risk drift, it silently holds the whole tree at the oldest range in play, and what you lose is whatever the newer versions had learned to catch.It came back on 2026-10-24, which is the useful part of the story.
de2df3fere-declared@stacksjs/bunpress, this time at^0.2.25, and the lockfile went to five copies: the root at 0.2.25 and four nested 0.2.22s under@stacksjs/actions,@stacksjs/docs,@stacksjs/typesandbetter-dx. Nothing required the split - every one of those asks for0.2.15or0.2.8, all satisfied by the root - so the nested entries were stale resolutions the lock had kept from when the root sat at 0.2.14. A re-resolve collapsed them to one copy at 0.2.28, touching nothing butbun.lock; the declared range was left exactly as it was found, since the duplication was the thing to fix and the floor somebody else chose was not.Worth saying plainly: the second occurrence was milder than the first. The direct range was ahead of every other, so no package was held on an old copy, which is the harm that made the first one matter. The declaration is still the thing that reopens the split whenever it falls behind, and AGENTS.md forbids it for that reason, but removing it a second time is a call for whoever added it.
Two upstream issues came out of the link check that this upgrade turned on. Both were reproduced on a three-file fixture against
0.2.28before filing, and both are about the diagnostics rather than the check, which bunpress#86 was right to add:- stacksjs/bunpress#92 - the failure names the
dead link but not the file containing it, under roughly 1,700 lines of minified bundle output.
The throw site already has the answer:
resolveLink(G, z)readsG.sourceFileon the line above, to resolve the destination it then rejects. - stacksjs/bunpress#93 - the build stops at the first one, so N dead links cost N builds. Ours had three and each needed its own run.
A third candidate was dropped rather than filed. The roadmap says in phase 7 that bunpress "does not rewrite"
.mdcross-links; on0.2.28it does, and[the guide](./guide.md)emitshref="/guide". That note predates bunpress#86 and is now wrong rather than a gap.Both workarounds stay. Naming the union and moving the constants into
demo-content.tswere better code independently of the linter, and reverting structure to prove a tool is fixed is how you end up doing it twice. - stacksjs/bunpress#92 - the failure names the
dead link but not the file containing it, under roughly 1,700 lines of minified bundle output.
The throw site already has the answer:
-
bun-query-builder - Enum type names are table-qualified, but only newly added columns were stamped with the qualified name, so altering an existing enum column referenced a type nothing creates. Migrating to Postgres died on the last file with
type "channel_type" does not exist. -
bun-query-builder - The seeder handed factories
@stacksjs/ts-fakerdirectly, and every factory in this ecosystem is written in the faker-js dialect:helpers.arrayElementthrew,string.alphanumeric(12)silently returned one character,datatypeandlocationdid not exist. The compat layer now translates both ways and is exported as a type, so a factory written the normal way is neither a runtime error nor a type error.0.2.29. -
ts-validation - The declaration emitter widened
EnumValidator.nametounknown, soschema.enum([...])was not assignable to theEnumValidatorTypethis library exports and a framework's env config rejected values it validates happily at runtime. A.d.tsbug wearing a type error's clothes.0.5.4, with a type test. -
stacks -
@stacksjs/fakerbuilds an enhanced faker -datatype,location,helpers.arrayElement,catchPhrase- and exported the type of the library underneath it. Every model factory in every Stacks application was therefore a type error against a type describing a different object: 116 of them here, 73 in the framework's own default models. Fixed in 0.70.371 along with the env config's enum type. -
stacks - bunpress was pinned at
^0.1.18, where/search-index.jsondoes not exist, so every documentation site built on this framework had a search box that took a query and answered nothing.0.70.370. -
stacks -
faker.datatype.boolean(0.2)- the bare-probability form faker-js accepts, and the form the framework's ownProductUnitmodel uses - was a type error, and the one error./buddy typecheckreported here. It was fixed and tagged as 0.70.372, and that release could not publish: theReleaserjob failed buildingstorage/framework/core/mobile, which importscraft-native/mobile- a subpath the publishedcraft-native@0.0.55did not export.**Closed by the 0.72 line, which carries the fix and publishes.** The probability form typechecks against the installed framework, and `app/Models/ReviewThread.ts` uses it rather than a bare `boolean()`: a factory that resolves a fifth of its threads is closer to a real repository than one that resolves half, and a use in the tree is what makes a regression here fail `./buddy typecheck` instead of going unnoticed until somebody writes the form again. -
bun-router - Static files were served gzipped and nothing else was. Every response a route produced went out whole: 253 KB of HTML on this project's landing page,
Accept-Encodingignored, on every request from every visitor. Compression now happens once, where every response passes through, with the two rules that make it safe rather than merely smaller - aVaryon both branches, so a cache cannot hand a gzipped body to a client that did not ask, and the body piped rather than buffered, so a streamed response keeps streaming: this product's diff manifest still arrives a file at a time, now compressed.The first attempt used `Content-Length` to tell a buffered response from a stream, and Bun does not set one on a `Response` built from a string - so the guard skipped every server-rendered page, which is the entire case. Piping needs no length and covers both. Two follow-ups, both found by watching what the application served rather than by reading the code: with no length there was nothing to compare against the threshold, so a 356-byte `/api/health` was being gzipped into something larger - the body is now peeked up to the threshold and no further - and `text/event-stream` matched `text/*` and was eligible, which would have turned a live channel into one that arrives in clumps. `0.0.26`. Measured on the landing page: **252,661 bytes to 61,184 on the wire**, with `/api/health` untouched and the diff manifest still arriving a file at a time. -
The framework upgrade, 0.72.50 to 0.75.65, with stx, bun-query-builder, ts-cloud, bun-router, ts-images, pickier, ts-pantry and better-dx moved with it. Not optional and not separable: stacks 0.75 requires
bun-query-builder 0.3.5,@stacksjs/ts-cloud 0.16.25and@stacksjs/bun-router ^0.1.21, and all three were pinned to older majors inoverrides, so a partial bump resolves to a set that cannot work. Thebuddy-bot/update-stacksbranch is exactly that partial bump and should not be merged as it stands.**The 408 type errors a fresh checkout reported were stale generated declarations, not the version gap.** `node_modules` held 0.70.352 while the lockfile said 0.72.50, and `storage/framework/types` was generated in the 0.70 era. An install plus `buddy generate:types` took it to 2, and both of those were real: `config/pages.ts` and `config/publicdiff.ts` each compared a `string | undefined` env var against a boolean, so one clause of each was dead. Behaviour was already correct, which is why nothing reported it. What the upgrade itself then broke was 24 errors in four groups, and three of the four were the framework telling the truth about this code. The event map lost its index signature, so every event this instance fires has to be declared on `AppEvents`: that is `app/domain-events.d.ts`, and writing it caught three payloads whose declared shape was wrong (`written` is a per-kind record and not a count, a push's `owner` can be null, and its `closedIssues` is the issue numbers rather than how many). `config/errors.ts` had the string-length message keyed `min` where the validator calls it `minLength`, and no numeric `min` at all. `config/services.ts` still configured an FCM `serverKey`, which Google deprecated and the framework has dropped. Two things regenerating surfaced that had been latent for longer. `app/Commands/demo-content.ts` exported constants from the directory where every file is expected to default-export a command, which aborted part of `generate:types`; it is `app/Cli/demo-content.ts` now, beside the other command helpers. And `parseRestrictions` was re-exported from both `resources/functions/repo.ts` and `resources/functions/review.ts`, which is a duplicate export and therefore a barrel that does not compile at all. The 0.70-era barrel never listed either one, so the collision test passed by never seeing them. `repo.ts` owns the name now, because branch protection is a repository concern, and the one view that read it from `review.ts` reads it from `repo.ts`. `ts-images` 0.2.25 also stopped writing `"purpose": "any maskable"` into `site.webmanifest` and writes `"any"`. Worth a look rather than a shrug: `maskable` promises Android a safe zone this mark does not have, so dropping it is probably the correct claim, but it does change what an installed icon looks like. Left standing: 13 type errors and 7 lint errors, every one of them upstream and listed below. Unit tests are 4,980 passing and 3 failing, and none of the three is this upgrade: two want a `repair_settings` table a local database is simply behind on, and the third is the Bun backpressure defect [phase 16](./16-hardening-scale.md) pins on purpose. -
stx -
0.2.372could not be installed by anybody: it depended on@stacksjs/desktopat exactly0.2.372, and that package was never published past0.2.371, so resolution failed outright. Worked around by pinning the whole stx family exactly, since^0.2.371floats straight back into it. Resolved upstream:desktopis published through0.2.378and the family is a caret range again. -
Not the framework: a stale
env.d.ts, and eleven variables never declared. Filed as nothing, because reading the contract showed the fault was here. SevenStacksEnvtype errors looked like the generator ignoringconfig/env.ts, andStacksEnvreally did lose its[key: string]catch-all. Butstorage/framework/types/env.d.tsis derived, not generated, as of 0.75: the shipped version is thirteen lines that read the schema,interface StacksEnv extends InferEnv<typeof import('../../../config/env')['default']>, and the.env-scraping generator that wrote the old one is the thing the framework moved away from. This repository was still holding the scraped 0.70-era file, which is whyPAGES_CUSTOM_DOMAINStyped asstringwhere the schema saysschema.boolean(), and whyPAGES_MAX_AGEwas absent while variables that happened to be in the local.envwere present.Taking the derived form made the schema's types flow immediately, and turned seven errors into a longer and more honest list: eleven variables this instance reads through `env` and never declared anywhere. `AUTH_IDLE_TIMEOUT`, `CACHE_DRIVER`, `REDIS_USERNAME`, `REDIS_TLS`, `GITHUB_TOKEN`, `PUBLIC_DIFF_ENABLED`, `PUBLIC_DIFF_RATE` and the four `TYPESENSE_*` are in `config/env.ts` now with validators and defaults, which is the only thing that types them. Three reads changed with them: a variable declared `schema.boolean()` arrives as a boolean, so comparing it against `'true'` or `'false'` was the old scraped string type showing through. The lesson is the same one the stale declarations taught twice before it. A generated or derived file left behind by an upgrade does not report itself; it reports a bug somewhere else. -
Not the framework either: two actions on the wrong side of our own convention. Four errors in
routes/api.ts, where'Mcp/McpAction'and'Api/GitHubCompatAction'were not assignable toStacksHandler, whiletests/e2e/mcp.test.tsandtests/e2e/github-compat.test.tsboth passed against them. A type describing less than the runtime, which looked like a framework gap:resolveStringHandlerUncachedfalls through toappPath(${modulePath}.ts)for any path withoutActionsin it, soapp/Mcp/McpAction.tsresolves, whileActionPathis the registry, and the registry is the actions barrel, and the barrel scansapp/Actions/**.But the convention this project already follows is the one in [AGENTS.md](../../AGENTS.md): an action lives in `app/Actions/<Domain>/`, and `app/<Domain>/` holds the support modules around it. `app/Actions/Api/` already had six actions beside the eleven support modules in `app/Api/`, and `routes/wellknown.ts` already named its sibling `'Actions/Api/JwksAction'`, with the prefix. These two were simply in the wrong directory. Moved to `app/Actions/Api/GitHubCompatAction.ts` and `app/Actions/Mcp/McpAction.ts`, named with the prefix, and both e2e suites still pass. `docs/api.md` is regenerated, since the action name is rendered into it. -
storage/framework/defaultswas a badly stale vendored copy, now synced. Two type errors and three lint errors came from it while the published package had none of them, so it was never an upstream defect. It was not a two-file fix either:app/alone differed in 858 files against@stacksjs/defaults@0.75.81.Synced with the framework's own `syncPackageProjectFiles` rather than by hand, so the source-to-target mapping is the one `buddy upgrade` uses: `+497 ~1113 -116` across 1,726 paths. `buddy upgrade` itself is still not what ran, because it also rewrites `workspace:*` to a pinned range. Five paths it wanted outside the defaults tree were reverted to HEAD and verified byte-identical afterwards: `buddy`, `bootstrap`, `storage/framework/tsconfig.app.json`, `storage/framework/server/tsconfig.docker.json`, and `pantry.lock`, **which the sync wanted to delete**. Typecheck is 0 for the first time, from 2. Lint is 4, from 7, and all four are now in generated or framework-owned files rather than anything this repository writes. **What the sync changed about behaviour, which is the part worth reading.** The framework grew about thirty-seven default models and three default route files between 0.70 and 0.75, and `route.importRoutes()` mounts the default route files alongside this application's own. So **269 routes are newly registered**, and the generated document goes from 783 paths to 944. Among them: `/api/pledges`, `/api/auctions`, `/api/auction-items`, `/api/couriers`, `/api/delivery-stops`, `/api/menus`, `/api/forms`, `/api/sites`, `/api/redirects`, 103 more under `/api/dashboard`, plus `/oauth/authorize`, `/auth/magic-link` and `/webhooks/email/*`. This is a widening of an arrangement the application already had rather than a new one: the committed document already carried 783 paths and 358 mentions of `/api/dashboard` before any of this. Worth deciding about deliberately all the same, because a forge serving `/api/auctions` is surface nobody asked for. **And none of those tables exist.** 209 models are typed in `database/types.d.ts` against 134 tables in the database: `pledges`, `auctions`, `couriers`, `delivery_stops`, `menus`, `forms`, `sites` and `redirects` are all absent, so those endpoints cannot answer. That predates the sync too, which only made more of it, and `buddy migrate --diff` is right to report no pending changes, because the default models are typed without being migrated. It is still an API surface that is mounted and cannot work, and it wants its own decision: either the default route files and model APIs should be opt-in for an application that wants none of them, or the tables behind them belong in the corpus. -
Two consequences of the sync, fixed with it.
routes/buddy.tsrouted/commandsatActions/Buddy/CommandsAction, which upstream deleted; nothing on this instance called it and there is no override, so the route is gone. Andopenapi-coverage.test.tsnow excludes/installand/test-error, which the framework'sroutes/dashboard.tsregisters insideif (IS_LOCAL_ENV): they do not exist on a deployed instance, so documenting them would describe a surface nobody can call. -
stacks -
buddy upgraderewritesworkspace:*to a pinned range in the workspace packages understorage/framework. Those are workspace members, and a version range stops them resolving locally, so the one command that delivers the scaffolded declarations also does that. The declarations this app was missing (model-events,registries,gates,models,request-context,authenticated-user) were copied from@stacksjs/defaults/project/storage/framework/typesinstead, which is the same source the upgrade's own types step uses.**Filed as stacksjs/stacks#2880**, still reproducing on 0.75.89. It is one line in `resolveManifestSpec`: `if (spec.startsWith('workspace:')) return \`^${target}\``, applied by `reconcileVendoredManifests` to every manifest under `storage/framework` - 33 declarations across 7 of them here. Demonstrated by calling the published function rather than reading `dist` and inferring, which is the mistake that made the #2868 report wrong: ``` workspace:* -> "^0.75.89" file:../router -> null 0.75.81 -> "0.75.89" next -> null ``` `workspace:` is the only non-numeric spec that gets rewritten, and the comment directly below that line says a git url, a `file:` link and a tag are left alone because "it was set deliberately and guessing at it would be worse". `workspace:*` is in that category and was set by `buddy new`. -
stacks -
buddy typecheckfinishes its work and then never exits. Filed as stacksjs/stacks#2868. It typechecks in about two seconds, logs "Finished running typecheck", and the process stays up: still running at 2m16s, and killing the invoking shell leaves it reparented to PID 1 and alive at 17m. Anything reading it through a pipe therefore never sees EOF, so./buddy typecheck | tailprints nothing at all and runs until killed.**The diagnosis in this entry was wrong, and the fix is `230e0078c8`.** It is not `runCommands`, which returns with zero active handles against the published packages. It is `process.on('beforeExit')` in `core/logging`: the listener calls `log.flush()`, and a `beforeExit` listener that schedules real event-loop work gets `beforeExit` emitted again when the loop next empties, so the pair never settles. `process.once` fixes it. All 144 buddy commands that fall off the end were exposed, not one; every command ending in an explicit `process.exit` was immune by accident, because `process.exit` does not emit `beforeExit`. The claim below that `typecheck.js` is the only action without an exit is also wrong, and wrong for an instructive reason: the grep ran over `node_modules/.../dist/`, where a bundled entry inlines an imported module's `process.exit` and so appears to have one. In source it is six files. Kept here rather than deleted, because a `dist` grep reading as source is a mistake worth being able to recognise again. `@stacksjs/actions/dist/typecheck.js` is three imports and one `await runCommands(...)` with no exit after it. Every other action in that package that calls `runCommands` ends with an explicit `process.exit`, and this is the only one that does not - which reads less like a convention than like the same workaround applied one file at a time, with this file missed. `.github/workflows/ci.yml` has been calling `tsc` directly since 2026-08-19 for exactly this, and its note guessed the wrapper was surviving locally because of a warm cache. It was not; it hangs here identically. The `typecheck` and `test:types` scripts in `package.json` now run the same direct command, which is also what the framework's own `buddy upgrade` writes. Both finish in about two seconds and, unlike before, end. Worth knowing when reading an earlier claim of mine here: the first measurement of this was `./buddy typecheck | tail -15`, reported as "no output in 2m44s". The no-output half was the pipe, not the command. It was the right conclusion from the wrong evidence, and the real behaviour is worse than the reported one, since the work does finish and only the process does not. -
stacks -
buddy migrate --diffsays the models match a database thatbuddy migratesays is missing tables. Filed as stacksjs/stacks#2860. Found on this machine, where the ledger had fallen behind after the corpus was rebuilt upstream: 82 rows against 304 migration files, 82 tables, and 49 of this application's 111 models had no table at all, every one of them with acreate-<table>-tablemigration sitting on disk.--diffanswered "No pending schema changes - your models match the database."The one-step reproduction is smaller than that. Drop `repair_settings`, which `app/Models/RepairSetting.ts` declares, and `--diff` still reports a match, while plain `buddy migrate` prints "The live schema does not match the models. Missing tables: repair_settings" and then exits `SUCCESS` with "already up to date" without creating it. So the dry run contradicts the real run, the real run names the drift and does not repair it, and both exit zero. An operator who reads the last line is told the database is fine, and CI only reads the exit code. **Fixed upstream the same day and verified here on 0.75.81.** `buddy migrate:status` is new and names the condition exactly: it reports `0000000297-create-repair_settings-table.sql` as REVERTED, "recorded as applied, but the effects are gone from the schema", and exits with "Drift detected." `--diff` now reports the missing table and says plainly that migrations will not fix it on their own, rather than claiming a match. `--strict` is there for CI. And there is a repair short of dropping the database, which was the other half of the ask: `migrate:status --reconcile --requeue-reverted` un-records only the migrations whose tables are actually gone, and the next `buddy migrate` rebuilds them. Verified end to end here: ledger 304 to 303 to 304, table back, nothing else touched. `migrate:fresh` was the only repair before that, and it worked: 304 ledger rows, 134 tables, no model without a table. The 49 were not peripheral, they were `deployments`, `environments`, `merge_queue_entries`, `runner_pools`, `git_refs`, `git_wal_entries`, `pages_sites`, `atproto_identities`, `managed_tests`, `check_annotations` and `commit_statuses` among others, which is phases 9, 10, 13, 15, 16 and 18. None of those features can ever have run here. -
stacks - the seeder never resolves a
belongsTotoUser,TeamorCustomer, and writesnullinstead. Filed as stacksjs/stacks#2861. The contrast inside a single row is the whole bug:INSERT INTO "stars"("repository_id","user_id")fails withnull value in column "user_id", having resolvedrepository_idin the same statement.Where the user key is `NOT NULL` the insert fails and the table stays empty. Fourteen of them do: `stars`, `watches`, `repo_collaborators`, `org_members`, `team_members`, `issue_assignees`, `reactions`, `ssh_keys`, `gpg_keys`, `access_tokens`, `notification_subscriptions`, `notification_mutes`, `notification_schedules` and `review_checkpoints`, plus `access_token_repositories` and `release_assets` downstream of those. **Where it is nullable the insert succeeds, which is the half that does real damage.** The seeded corpus has 30 issues, 20 pull requests, 60 issue comments, 50 review comments and 25 reviews, and `author_id` or `reviewer_id` is null on every single one of them, against zero nulls in `repository_id` or `commentable_id` on the same rows. So a demo database for a product whose entire thesis is the review contains no review with a reviewer and no comment with an author, and nothing reports it. **It is a hardcoded name list, not a trait.** `relationColumns` in `@stacksjs/database` skips any parent for which `isAccountModel(parent)` holds unless `options.allowProtected` is set, and `ACCOUNT_MODELS` is `['User', 'Team', 'Customer']`. Nothing to do with `useAuth`, which was the first guess here and was wrong: `team_members` populates under the flag too, and `Team` carries no auth trait at all. The function only ever *reads* `existingRows`, so the list is not protecting `users` from being written to. The flag is the other half of the problem. `--allow-protected` is documented as "Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)", and skipping the *seeding of* those models is worth keeping. Resolving a foreign key against existing rows writes nothing and invalidates nothing, so one flag gates two unrelated things and the harmless half is only reachable by opting into the harmful one. **Fixed upstream the same day and verified here on 0.75.81, and the flag is no longer needed.** Plain `buddy seed --fresh` now reaches 97 of 99 models, where the `--allow-protected` workaround only reached 90 of 98: account relations attach to the rows the same run seeded, and composite unique indexes are honoured, so `watches`, `repo_collaborators`, `repo_topics`, `review_drafts` and `stars` all seed instead of dying on their own declared index. Authorship is populated everywhere it was null, with no flag. The composite-index half of #2861 is fixed with it. What remains is the polymorphic half: a `subject_type` plus `subject_id` pair has no `belongsTo` to resolve from, and a factory is handed `faker` rather than a database, so `reactions` and `timeline_entries` are the 2 of 99 still failing on a null `subject_id`. Left alone rather than papered over by setting their seeder counts to zero, which would trade a loud failure for a quiet absence. -
Two seeder bugs of our own, which only the upstream fix could expose. Both were hidden behind the account-relation defect: nothing downstream of a user could seed, so nothing downstream of these could fail either.
`ReleaseAsset` asks for fifteen rows and `belongsTo: ['Release']`, and `Release` had no `useSeeder` at all, so its seeder could never succeed and the releases screen had nothing to render. `Release` is seeded now, twelve of them, and the assets attach. `RepositoryMirror.repository_id` carried `factory: faker => faker.number.int({ min: 1, max: 8 })`, inventing a repository id rather than letting the seeder resolve one. That held only while a fresh database happened to number repositories 1 to 8; they are 39 to 46 here after a few re-seeds, so every mirror row failed its foreign key. It is `factory: () => null` now, like every other relation column in this codebase. -
A warning about the
overridesblock, which cost an hour here. Raising stacks to 0.75.81 brokebuddy migrate --diffwithexport 'parseBearerToken' not found in '@stacksjs/bun-router', and it was not an upstream bug.@stacksjs/router@0.75.81requiresbun-router 0.1.24; the override inpackage.jsonforced0.1.22, which does not export it.@stacksjs/ts-cloudwas behind the same way,0.16.35against a required0.16.40.This is the same trap the `buddy-bot/update-stacks` branch sets, from the other direction: an override that pins a transitive dependency below what stacks asks for fails at *runtime*, with a missing export, and installs without complaint. **Audit every entry in `overrides` against what the new stacks requires whenever the framework moves**, and consider whether an override that merely restates the required range needs to exist at all. -
The three dependency bumps that outlived their branches. buddy-bot had proposed each one and all six of its branches were deleted upstream before any of them landed, so these were taken directly:
@anthropic-ai/sdk0.120.0to0.131.0,mermaid^11.16.1to^12.1.0, and the Dockerfile's base image.`mermaid` is the only one with teeth. It is a major, and the package range is not what the browser loads: `public/js/mermaid.js` is a committed 3.5MB bundle, built by `scripts/vendor-mermaid.ts` and vendored rather than taken from a CDN so that a forge on a closed network still draws diagrams and a reader of an issue is not announced to a third party. Bumping the range alone would have changed nothing a reader sees, which is the stale artifact pattern this phase has now hit four times. Re-bundled, and the asset goes from **3.49MB to 5.05MB** - paid only by pages that contain a diagram, since the loader in `resources/views/layouts/app.stx` imports it on demand, but worth knowing. Verified by importing the built bundle and checking the two functions that loader calls, `initialize` and `run`, plus the six markdown e2e tests. The base image went to `oven/bun:1.4` rather than the `1.4.2` the bot proposed, because the Dockerfile pins a floating minor and always has. Taking a patch pin would have been a second decision smuggled in beside a version bump. Worth noting the image was two minors behind what this machine runs, which is Bun 1.4.3. Left alone: `config/services.ts` still defaults `anthropic.model` to the pinned `claude-sonnet-4-20250514`. Nothing reads it - it is framework scaffold, and the live AI path is `config/ci-repair.ts`, already on a bare `claude-opus-5` for the reason that file explains. Changing an unconsumed default would be churn in a config this project does not own. -
The framework's default route bundles are opt-in now, and this instance takes none of them.
STACKS_DEFAULT_ROUTES=nonein.env.example, which the configuration docs pick up automatically from the comment above it. 1,408 registered routes to 714, and the generated document from 944 paths to 447.The lever already existed, which is the lesson rather than the change. `route-loader` has carried `STACKS_DEFAULT_ROUTES` since stacksjs/stacks#2229, with a long note on why bundles are named explicitly instead of gated on `feature()`: `config/auth.ts` ships enabled in every scaffolded app, so gating on it would silently widen the public surface of every app on upgrade, and "an opt-in that is on by default is not an opt-in". The six bundles are `auth`, `dashboard`, `delivery`, `email`, `forms` and `payments`; `social` was already opt-in. **The reason this is not tidiness: the `auth` bundle was a second, complete, unused authentication surface.** It mounts `/login`, `/register`, `/logout`, `/logout-all`, `/me`, `/me/data-export`, `/tokens`, `/refresh`, `/enable-two-factor`, `/oauth/clients`, `/referrals` and twenty-eight more at bare paths. This forge has its own under `/api/auth/*` and `/api/user/*`, which is what `login.stx`, `register.stx` and `forgot-password.stx` post to, and its own passkeys, sessions, two-factor and token management beside them. The default set was reachable, parallel, and maintained by nobody here. Verified rather than assumed, because turning off auth routes is the kind of change that is discovered in production. Every auth, passkey, session and token e2e suite passes: 97 tests across seven files. Nothing in `resources/`, `app/` or `tests/` references `/webhooks/email`, `/api/forms`, `/webhooks/payments`, `/delivery/`, `/api/contact`, `/auth/magic-link`, `/referrals` or `/oauth/clients`, and the only `/api/dashboard` mentions are two vocabulary tests that exclude it and one that mocks `fetch`. Turning a bundle off stops its routes registering and not its actions resolving, so anything here can still point a route at a framework action it never copied. -
Model APIs are opt-in now, and this instance takes none of the framework's. The other half of the question, and it needs the framework.
@stacksjs/orm/routesiterates every model in the auto-imports barrel and registers REST routes for any with auseApitrait, and the only things it reads areconfig/qb.ts,config/security.tsforapi.rowScoping, andSTACKS_API_ROW_SCOPING. There is no allowlist and no per-model gate.rowScopingis alreadydenyhere, which only withholdsstore/update/destroyfrom models with no row-level scoping, and the commerce defaults haveownershipconfig so their writes register anyway.**72 default models carry `useApi`, accounting for 178 of the 714 routes that remain**: `auctions`, `bids`, `boards`, `campaigns`, `carts`, `coupons`, `couriers`, `customers`, `menus`, `orders`, `payments`, `pledges`, `products`, `redirects`, `sites`, `subscribers`, `transactions` and more. The barrel is not configurable either: `defaults/app/Models` is hardcoded in five framework packages. Four of them are worse than noise because they collide with this product's own vocabulary. `/api/reviews` is the framework's *product review* model on a forge built around code review, and `/api/comments`, `/api/labels` and `/api/tags` read as this instance's own while being something else entirely. See the domain-vocabulary table in [AGENTS.md](../../AGENTS.md) for why that matters here more than it would elsewhere. Filed as stacksjs/stacks#2866 and **fixed upstream in `9977c5233`**, shipped in 0.75.89. `security.api.models` in `config/security.ts` takes `'all'` (the default, so nothing changes on upgrade), `'own'`, `'none'` or a list of names, with `STACKS_MODEL_APIS` overriding it - the config-then-env pairing `api.rowScoping` already had. It narrows and never adds: a named model still needs its own `useApi` trait. This instance takes `'own'`, and the surface it leaves is smaller than the 178 asked about: | | before | after | |---|---|---| | mounted routes | 714 | **259** | | documented paths | 447 | **233** | | component schemas | 78 | **3** | 455 rather than 178, because `'own'` drops every framework-model API rather than only the ones over tables that do not exist. Checked before accepting it: everything removed is commerce, CMS or marketing, and nothing in `app/`, `routes/`, `resources/` or `tests/` addresses any of it. The one apparent hit, `/api/labels`, is a mock of *GitHub's* API inside `import-github.test.ts`. The three schemas left are the three `app/Models` that carry `useApi` - `Notification`, `NotificationDelivery` and `Release` - which is the generator applying the same selection to the document rather than a document that lost something. -
The end-to-end suite is green: 1,584 tests, 0 failures. It had 21, which had been there since August and were found only because this was the first time the whole suite was run rather than
tests/unit.**The first version of this entry said CI does not run `tests/e2e`. That was wrong**, and the truth is worse. `buddy test` calls `runTestSuites([''])`, which globs `tests/` recursively, so CI runs 6,569 tests across 494 files and always has. It had simply been **red on every run since 2026-08-22**, including Chris's own last commits, so a red CI was the normal state and carried no signal at all. That is the finding behind the other four, and it is fixed in the entry below. **Thirteen were a stale search index, not code.** Every query answered 503, from `Could not find a field named 'pushed_at' in the schema for sorting`: the Typesense collections predated that field, and `repositories` held 897 documents against the 8 the database has. `./buddy search:reindex <Model>` for each searchable model rebuilt the schemas. Worth knowing as an operational step rather than a fix - a reseed does not rebuild the index, and the symptom is a search page that answers nothing. **Three were a real product bug, and the worst thing found this week.** Branch protection and the merge-strategy settings could not be saved from the interface at all. The markup pairs a checkbox with a hidden field, which is the only way HTML can send a false, so a ticked box arrives as `x=false&x=true` and the router correctly surfaces `['false', 'true']`. Six copies of `readFlag` across six actions did `String(value)` on that, got `'false,true'`, matched none of their spellings, and returned undefined - which the handlers read as "not sent" and answered `422 Nothing to change` to somebody who had just ticked a box. One copy now, in `app/Actions/inputs.ts`, taking the last value. The fix had to be made in six places to be made at all, which is the argument for it living in one. **Three were a test that stopped being true.** `auto-merge` posts JSON with `number: '2'`, a string, to an action that declares `schema.number()`. The framework coerces a query or form value, because those are always text, and takes a JSON value as sent, because it has a type of its own - so the browser form works and the JSON string does not. The test was written on 2026-08-07 against an action with no `validations` block at all; `e3374927` added one the next day. Two sibling tests went on passing because they expect 422 anyway, for the wrong reason. **One was eight repositories owned by nobody.** `Repository.owner_id` had `factory: faker.number.int({ min: 1, max: 4 })`, the same invented-id bug as `RepositoryMirror`, and organizations are numbered 21 to 24 here. The owner is polymorphic, so a `belongsTo` is not the fix: it would generate a foreign key on a column that also names organizations. `database/seeders/RepositoryOwners.ts` repairs orphans after the model pass instead, which is the one place that can see both tables. `app/Ops/admin.ts` falls back to the raw id when a handle is missing, deliberately, so the only visible symptom was `4` where every other row showed a handle. **One was an assertion that predated syntax highlighting.** `browse-tree` looked for `export const deeper` in the HTML of a highlighted blob, where it arrives as `<span class="t-keyword">export</span>...` and never appears contiguously. The view was right the whole time. It asserts the text now, with the tags stripped, which is what the test was about. One thing checked and found **not** to be a bug, recorded because the first read said otherwise: 123 fields across the actions declare `schema.number()` or `schema.boolean()` for a value their handler coerces, which looks like the mirror image of the 40 fields `app/Actions/inputs.ts` was written for. It is not. Those arrive from a query string or a form, both of which the framework coerces to the declared type, and `tests/e2e/fine-grained-token-api.test.ts` proves it by passing `number=1` as text to an action declaring `schema.number()` and asserting 200. The `coerced` rule is still the more honest declaration for a field a handler coerces, but nothing is broken and a 123-field sweep would be churn. -
CI can go green again, which is the only thing that makes it mean anything. It had been red on every run since 2026-08-22. Three causes, and none of them was a missing test run.
**The lint job failed on four unused imports**, three of them in `storage/framework/types/auto-imports.d.ts`, which `buddy generate` writes. `config/code-style.ts` already ignored `storage/framework/auto-imports/**` as "never user-editable source"; the generated *declarations* were the same category in a different path and were simply not matched. They are now. The fourth was a genuinely unused `fs` in `storage/framework/server/build.ts` and is gone. Worth noting these had been reported here all week as "4 errors, all framework-owned, unchanged" without anybody checking whether they gated anything. They gated the whole job. **The test job failed on `openapi-coverage`, and that one was ours from yesterday.** CI has no `.env`, so `STACKS_DEFAULT_ROUTES` was unset and defaulted to `all`, mounting the 269 default routes this instance deliberately does not take, while the committed document describes the surface without them. `STACKS_DEFAULT_ROUTES: none` is in the job's env block now, beside the other values that are there "because CI has no `.env` at all". Regenerating the document instead would have been the wrong fix: a document describing routes this instance does not serve is worse than a failing test. **And the third failure was the backpressure canary, which nobody read.** See [phase 16](./16-hardening-scale.md): it asserts the defect rather than the fix, so it fails when Bun repairs it, which Bun did in 1.4.3. It was reported as "the deliberate Bun canary, expected" for an entire session. A test expected to fail is indistinguishable from a test that is failing, which is precisely how a red CI stops being information. -
Review threads anchored to nothing, and the fix was a near-miss rather than a rewrite. 61 threads carried a path, a line and a side, and
original_commit_shawas null or a repeated digit - so the product's primary object pointed at a commit that did not exist, on a file that did not either. The thread factory already namesREADME.md,greet.tsline 3, andsrc/<noun>.tsat lines as deep as 398; the first version ofRepositoryGitwrotesrc/index.ts, a file of my own invention that nothing referenced.So the seeder writes the files its own fixtures name: `greet.ts` whose third line is the one eleven threads point at, and a generated `src/<noun>.ts` per referenced path, long enough to hold the deepest line. Every thread is then repointed at the commit it was written against. **61 of 61 anchored, from 0.** It also makes the repositories worth browsing, since a tree with one file in it demonstrates nothing about a code browser. -
A stack, so the feature the product leads with can be opened.
stackhas its own row in AGENTS.md's domain vocabulary, the landing page argues for it, andresources/functions/stack.tscarries the ordering, orphan detection and landable-prefix rules. None of it was reachable: every seeded change branched off the default branch, so the instance had 185 pull requests and not one stack.`database/seeders/StackedChange.ts` builds its own repository with three dependent changes - chained branches, chained bases, and the `stack_parent_id` links `buildStack` walks. Its own repository rather than retrofitting three existing changes, which would have rewritten rows that were just made consistent to produce a stack nobody could find. Verified in the running app: each change reports exactly "1 changed file", which is the claim that each shows only its own work, and every member's page lists the others with their numbers and states. -
Seeded handles contained spaces, so four organizations could own nothing. Found while the stack seeder silently did nothing: it took the first user, whose handle was
helenjames 1, andrepositoryPathrefused it asinvalid-characters.Two defects, and only one of them was ours. **Upstream**, filed as stacksjs/stacks#2893: `claimUniqueValues` disambiguates a duplicate value in a unique column with `` `${value} ${suffix}` ``. For a number that is fine; for a string it writes U+0020 into a value whose whole job may be to survive a URL, a path, a DNS label or an email. Ten of 348 users and **all four** organizations were in that state, so no organization could own a repository and twelve repositories could not be built. Nothing errored: the seed reported success and the damage surfaced only when something turned a handle into a path. **Ours**: a factory that collides at all. `User.handle` and `Organization.handle` slugified a faker value with no uniqueness, so across hundreds of rows duplicates were certain. Both carry their own short suffix now, in the handle's own alphabet, so the framework's path is never reached. `database/seeders/OwnerHandles.ts` repairs instances that already have the damage, deriving the new value from the old - `helenjames 1` becomes `helenjames-1` rather than a new random string, so a handle somebody linked to stays recognisable. 14 repaired, 0 unusable afterwards, and the next run built the 12 repositories whose owners had been unusable. One more bug of my own on the way: `StackedChange` keyed its "already built" check on `(owner, name)`, so repairing the handles changed which owner it picked and it created a *second* stack. It keys on the repository now, which is the thing that should be unique. -
Seeded repositories had no git in them, so the product's own surface could not be opened.
buddy seedwrote 75 repository rows and not one commit - no seeder touchedstorage/reposat all. Every repository page loaded, and everytree/andcommits/under it answered 404, so on a fresh checkout the code browser, the diff and the review screen could not be reached without creating a repository by hand. For a forge built around review, that is demo data which proves the schema and nothing else.`database/seeders/RepositoryGit.ts` builds them through `app/Actions/Git/` rather than spawning plumbing itself, which AGENTS.md requires and which also inherits the concurrency slots, the ref guards and the safe-path checks: `initBare` for the repository, `createCommit` for each commit, the same path `app/Actions/Profile/seed.ts` already uses to write a profile README into a bare repository with no clone and no worktree. Two commits on the default branch, then a branch per pull request with its own commit, and each pull request's row repointed at the shas that now exist - the same repair `RepositoryOwners` does for polymorphic owners, since the factory invented `head_sha` as a repeated digit naming no object. 171 of 171 pull requests resolve a diff now; before, none did. **Three things it got wrong first, each found by measuring rather than by reading it back.** Pull requests in one repository shared a `head_branch`, so the second `createCommit` hit the ref guard and that row silently kept its invented sha while the first looked fine; the branch carries the number now. The skip was per repository, so a repository built on an earlier run never had its remaining pull requests wired - building history and wiring changes are separate questions and only the first is answered by the repository existing. And the cap on how many to build counted *this run's* builds, so a second run built 24 more rather than converging, while the repositories it skipped kept rendering empty reviews. It builds all of them now, in about 35 seconds, and a second run builds nothing. Verified through the running app rather than the database: both pull requests on a seeded repository report "1 changed file", the commits page lists the history, and `tree/main/src/index.ts` serves 200. -
A suite that stands down now fails CI, and the switch for it had been there all along.
tests/setup.tscollects every[name] skipping:warning, repeats it after the summary, and turns it into a failure underTESTS_REQUIRE_ALL=1. Its comment says that is "what a machine with a database - CI, or this one - should run". It was set in no workflow, no script and no env file.So **1,500 guarded assertions across 169 test files** could report green having asserted nothing. Not hypothetically: the same comment records the defect it already caught once, where the `checks` token scope was missing from the model, its suite could not create a token, and it reported green until somebody read the warning. It had never been switched on because it was all-or-nothing, and one suite legitimately cannot run in the `test` job: `microvm-egress` needs KVM, a hypervisor and a built guest. So `TESTS_ALLOW_SKIP` names the exceptions, and naming is the point - an allowlist entry is a sentence in a reviewed file that claims *this job cannot run that one*, where a missing `TESTS_REQUIRE_ALL` claimed nothing at all. The `microvm` job sets the switch with no allowlist, because running that suite is the only reason it exists. Verified in all three states, exit codes included: unset passes with the warning, required and unlisted fails with the reason, required and listed passes and says "(allowed by TESTS_ALLOW_SKIP)". Then the whole suite as CI runs it - 6,585 tests, 0 fail, one suite standing down and it the allowlisted one. **And it let three copies of a workaround be deleted.** The search suites each carried a rethrow keyed on `CI`, added the day before this, to stop exactly this failure in exactly those three files. One mechanism covers every self-gating suite instead, and covers a developer who asks for it locally rather than only CI, so the bespoke gates are gone and the callers are back to warning and standing down. -
Twenty-three search tests were not skipping in CI, they were passing. The entry here said thirteen tests skipped. Both numbers were wrong and the verb was the important one. Each test in
search-page,search-actionandsearch-push-reindexopens withif (!available) return, and a body that returns immediately is a pass - so with no node to talk to, all twenty-three reported green having asserted nothing, three of them being the check on whether a private repository leaks into a stranger's search results. "Skipped" at least sounds like absent coverage. This looked like coverage.`search` is a service in the `test` job now, `typesense/typesense:30.2`, pinned to the version `deps.yaml` installs. Two things were checked rather than assumed, against the pantry build of the same version: - **It takes all its configuration from the environment.** A service container cannot override an image's command the way `compose.yaml` does, so the `--data-dir` and `--api-key` it passes are unavailable. `typesense-server` with no arguments reads `TYPESENSE_DATA_DIR` and `TYPESENSE_API_KEY` and serves `/health`. - **It refuses to start when its data directory does not exist.** "Data directory ... does not exist", and it exits before listening. Nothing creates one inside a service container, so the obvious `TYPESENSE_DATA_DIR: /tmp/typesense` would have failed the job before the first step. It is `/tmp`, which is always there. Readiness is a step on the runner rather than a `--health-cmd` on the service, because a health check runs inside the service image and what that image ships is not checkable from here - while curl on `ubuntu-latest` is a given. It dumps the container's log before failing. And the skip is loud now: when the search node is the thing missing and `CI` is set, the three suites rethrow instead of warning. Verified in all three directions - no node and `CI` set fails the file, no node without `CI` still stands down with a warning, a node present passes 23. The previous note called a health check unguessable "with no Docker to try it against"; there were four working service containers in the same file and the thing that actually needed trying was the server's own behaviour, which pantry had installed all along. -
Two MySQL failures and two suites that stood down, all three from a timestamp. The run on
025514cfwas green on lint, typecheck and the Postgres leg, and failed on MySQL withp95_msreading 600001 against an expected 600000.Not a dialect difference. `tests/e2e/insight.test.ts` called its `ago()` helper - which is `Date.now()` minus an offset - three separate times for what the fixture means as one instant, so whenever the clock ticked between two of them the measured wait was off by exactly the tick. Postgres had been winning that race, not avoiding it. The instant is read once now and the other two values derived from it. The same run also lost `tests/e2e/reviewer-load.test.ts` and `pull-list-and-stack.test.ts` entirely, each to one warning in the log: both wrote `created_at` as an ISO-8601 string, MySQL refuses that for a `datetime` column, and the throw landed in the `beforeAll` catch that exists to stand down when there is no database. `dbTimestamp` has existed for this since August and says so in its own doc comment; these two predate it. Note that `pull_request_reviewers.responded_at` next to one of them is a `varchar(255)` holding an ISO string and is correct as it is: the schema decides which is which, not the column name.
Known gaps, deferred deliberately
-
Stacks -
notifications.user_idandnotification_deliveries.user_idforeign keys were missing from the live schema on every installation, not just this one.The last note here had the reproduction right and the mechanism half right, and guessed at the ordering. It is not a guarantee running during boot:
runDatabaseMigrationcallsmigrateNotificationTables()before the model batch, deliberately and with a comment saying why - a generated model migration may normalize or rebuild these tables and needs them to exist first. That is also exactly why the keys never landed. The guarantee creates the table without them, and the model's ownCREATE TABLE IF NOT EXISTS … REFERENCES "users"("id") ON DELETE CASCADEis then a no-op against a table that already exists. The migration runs, the corpus declares the key, and the key is not there.Putting
REFERENCESinline in the guarantee does not work either: on a brand-new database nothing has createdusersyet, so the CREATE would fail and take the boot with it.Fixed in Stacks 0.70.318 by adding the keys after the batch, when
usersis certain to exist- the same defensive-ALTER-and-swallow pattern
ensureUsersAuthColumnsalready uses inauth-tables.ts, and for the same reason: an installation that has deliberately dropped the relation must not fail its migration over a constraint it does not want. Verified against this database, which now carries both with the cascade.
- the same defensive-ALTER-and-swallow pattern
-
The
jobstable was dropped bymigrate:freshand not recreated by the corpus, so seeding skipped it.Resolved by the thing that fixed the queue itself:
app/Models/Job.tsoverrides the framework default, because two of its columns describe something other than what@stacksjs/queuestores. A model in the corpus means a generated migration in the corpus, so0000000012-create-jobs-table.sqlis replayed bymigrate:freshlike anything else - the table had been missing precisely because nothing described it here. -
Pantry could not auto-activate environments under the
denshell: it recognized zsh, bash, fish and nushell, and den has nochpwdor pre-prompt hook to attach to.Fixed upstream and verified against the installed binary -
PANTRY_SHELL=den pantry dev:shellcodeemits it. The hook is written out and sourced rather than eval'd, because den'sevalparses its argument as a command chain and a chain cannot carry a function definition, so an eval'd hook defines nothing. It also looks nothing like the bash template on purpose: in den a[test costs around 5ms where bash measures it in microseconds, so the shell-side parent walk that other shells use would spend most of a second percddeciding a directory is not a project. It forkspantry shell:lookuponce instead and lets native code walk. -
Adopt the published Stacks 0.75.205 and query-builder 0.3.13 cohort through normal package installation, with exact enum validation and model-driven mirrored-review identities. CI uses the fixed native Buddy typecheck path.
Generated database types, the nullable review identity migration and its Postgres snapshot, auto-imports and OpenAPI clients come from Buddy. STX pages compute dynamic lookups on the server and interpolate individual attribute values. The normal suite passes against private Pantry-managed Postgres 17.10 and Typesense services with pinned Bun 1.4.2; a local feature-off setting no longer disables the public-diff surface the test suite deliberately exercises.
-
Adopt published Stacks 0.75.212 for temporary workspace journals, public defaults package imports and flushed deployment diagnostics. Native generation preserves registry names and produces identical bytes across independent runs; the consumer resolves the published exports directly.
-
Fix Pantry MySQL project isolation at the source: each project uses its own data directory and startup script, so inspecting or starting a second project cannot rewrite the first project's port. Keep
config/deps.tsengine-neutral and generate the selected database service fromDB_CONNECTION.