create-stitchkit 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/README.md +3 -1
  3. package/UPGRADING.md +342 -0
  4. package/dist/cli.js +238 -42
  5. package/examples/repository/_env.example.append +21 -0
  6. package/examples/repository/packages/backend/src/domain/repository/github-cache.ts +2 -2
  7. package/examples/repository/packages/backend/src/surface.ts +1 -1
  8. package/examples/repository/packages/config/src/features.ts +17 -0
  9. package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +4 -4
  10. package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
  11. package/examples/repository/packages/frontend/src/app/api/[...path]/route.ts +66 -0
  12. package/examples/repository/packages/frontend/src/lib/api/client.ts +17 -12
  13. package/examples/repository/packages/frontend/src/lib/api/cross-origin.ts +87 -0
  14. package/examples/repository/packages/frontend/src/lib/api/place.ts +26 -0
  15. package/examples/repository/packages/frontend/src/lib/api/queries.ts +3 -0
  16. package/examples/repository/packages/frontend/src/lib/api/server-client.ts +11 -0
  17. package/examples/repository/packages/frontend/src/lib/realtime/repository.ts +44 -12
  18. package/examples/repository/packages/frontend/src/providers/client-providers.tsx +30 -0
  19. package/examples/repository/packages/frontend/src/providers/index.tsx +17 -12
  20. package/examples/repository/packages/frontend/src/providers/realtime.tsx +6 -4
  21. package/examples/repository/project.json +189 -0
  22. package/examples/repository/scripts/runtime-smoke.ts +38 -6
  23. package/package.json +12 -2
  24. package/template/AGENTS.md +23 -3
  25. package/template/README.md +83 -8
  26. package/template/_env.example +16 -4
  27. package/template/_gitignore +1 -0
  28. package/template/biome.json +6 -2
  29. package/template/bun.lock +115 -98
  30. package/template/e2e/starter.spec.ts +5 -7
  31. package/template/ecosystem.config.cjs +42 -19
  32. package/template/ecosystem.dev.config.cjs +41 -21
  33. package/template/package.json +12 -10
  34. package/template/packages/backend/package.json +2 -2
  35. package/template/packages/backend/src/cleanup.ts +121 -0
  36. package/template/packages/backend/src/cli.ts +6 -2
  37. package/template/packages/backend/src/index.ts +33 -8
  38. package/template/packages/backend/src/surface.ts +6 -1
  39. package/template/packages/backend/src/transport/errors.ts +4 -2
  40. package/template/packages/config/package.json +6 -2
  41. package/template/packages/config/src/app-identity.generated.ts +20 -0
  42. package/template/packages/config/src/declaration.ts +30 -0
  43. package/template/packages/config/src/server.ts +8 -17
  44. package/template/packages/config/src/shutdown.ts +20 -0
  45. package/template/packages/config/src/variables.ts +89 -0
  46. package/template/packages/db/package.json +2 -2
  47. package/template/packages/frontend/next.config.ts +3 -2
  48. package/template/packages/frontend/package.json +13 -13
  49. package/template/packages/frontend/scripts/serve.ts +70 -0
  50. package/template/packages/frontend/src/app/[locale]/layout.tsx +9 -8
  51. package/template/packages/frontend/src/app/[locale]/page.tsx +2 -2
  52. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
  53. package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +1 -1
  54. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +1 -1
  55. package/template/packages/frontend/src/app/robots.ts +4 -2
  56. package/template/packages/frontend/src/app/sitemap.ts +7 -19
  57. package/template/packages/frontend/src/components/ui/toaster.tsx +4 -1
  58. package/template/packages/frontend/src/env.ts +27 -8
  59. package/template/packages/frontend/src/lib/seo/cache-by-origin.test.ts +68 -0
  60. package/template/packages/frontend/src/lib/seo/cache-by-origin.ts +40 -0
  61. package/template/packages/frontend/src/lib/seo/metadata.ts +68 -11
  62. package/template/packages/frontend/src/lib/seo/pages.ts +1 -1
  63. package/template/packages/frontend/src/lib/seo/request-origin.ts +89 -0
  64. package/template/packages/frontend/src/theme/config.ts +1 -1
  65. package/template/packages/frontend/tsconfig.json +10 -3
  66. package/template/packages/shared/package.json +1 -1
  67. package/template/playwright.config.ts +1 -1
  68. package/template/project.json +169 -0
  69. package/template/scripts/acceptance-database.test.ts +73 -0
  70. package/template/scripts/acceptance-database.ts +92 -0
  71. package/template/scripts/acceptance-local.ts +144 -0
  72. package/template/scripts/build-inputs.test.ts +69 -0
  73. package/template/scripts/build-inputs.ts +58 -0
  74. package/template/scripts/build-stamp.test.ts +151 -0
  75. package/template/scripts/build-stamp.ts +169 -0
  76. package/template/scripts/check-authored.ts +18 -2
  77. package/template/scripts/client-boundary.test.ts +117 -0
  78. package/template/scripts/client-boundary.ts +148 -0
  79. package/template/scripts/declaration.test.ts +206 -0
  80. package/template/scripts/declaration.ts +271 -0
  81. package/template/scripts/deployment-preflight.ts +41 -0
  82. package/template/scripts/dev.ts +43 -20
  83. package/template/scripts/local-env.test.ts +2 -2
  84. package/template/scripts/local-env.ts +9 -3
  85. package/template/scripts/readiness.ts +92 -0
  86. package/template/scripts/release-steps.test.ts +87 -0
  87. package/template/scripts/release-steps.ts +112 -0
  88. package/template/scripts/release.ts +38 -0
  89. package/template/scripts/runtime-smoke.test.ts +178 -0
  90. package/template/scripts/runtime-smoke.ts +21 -5
  91. package/template/scripts/serve-mode.test.ts +36 -0
  92. package/template/scripts/shutdown-budget.fixture.ts +29 -0
  93. package/template/scripts/shutdown-budget.test.ts +164 -0
  94. package/template/scripts/supervision-signal.test.ts +94 -0
  95. package/template/scripts/surface-conformance.ts +8 -1
  96. package/template/scripts/tooling-env.ts +35 -3
  97. package/template/scripts/web-surface-smoke.ts +183 -2
  98. package/template/app.config.json +0 -9
  99. package/template/packages/config/src/identity.ts +0 -18
package/CHANGELOG.md CHANGED
@@ -4,8 +4,355 @@ All notable changes to **create-stitchkit** are documented here. The scaffolder
4
4
  has its own version and release line; the Stitchkit range tested by its template
5
5
  is declared in the template root catalog.
6
6
 
7
+ A release that changes the generated project in a way an existing project must
8
+ follow leads its entry with a **`### ⚠️ Breaking changes`** section. What else
9
+ has to happen — including the steps that touch a running machine — is in
10
+ [`UPGRADING.md`](./UPGRADING.md), because a changelog entry carrying an operator
11
+ step is overwritten by the next release.
12
+
7
13
  ## [Unreleased]
8
14
 
15
+ ## [0.4.1] — 2026-08-25
16
+
17
+ ### Fixed
18
+
19
+ - **The `Gates` list can be run top to bottom, and it never deploys.** It
20
+ presented five commands in a row, two of which check a *running* deployment —
21
+ with nothing in the list that starts one, so on a fresh project `bun run
22
+ runtime:smoke` failed with a connection reset from inside a check. The answer
23
+ is not a deploy command: `bun run pm2:prod` applies the declared migrations
24
+ and reloads the PM2 daemon the developer is running, so a gate list carrying
25
+ it quietly means "deploy". The runtime gates now run under `bun run
26
+ acceptance:local`, which creates a deployment of its own — separate
27
+ `PM2_HOME`, ephemeral ports, its own public-host allowlist, its own
28
+ database — and destroys it by naming the declared roles. Neither guide says
29
+ `pm2 delete all` any more: it empties whichever daemon it is pointed at,
30
+ including applications that have nothing to do with the project.
31
+ - **The acceptance gate writes only to a database of its own.** It inherited
32
+ `DATABASE_URL`, and the runtime gates write — the repository example's smoke
33
+ posts `/api/repository/refresh` twice, which upserts. So a command a developer
34
+ is told to run before handing work off wrote rows into whatever `.env` named.
35
+ It runs against `ACCEPTANCE_DATABASE_URL`, applies the declared migrations
36
+ there, and refuses to start when that variable is unset or reuses the
37
+ deployment's database **name** — with the line to paste in the refusal. The
38
+ name alone decides, because a hostname is not proof of a different server:
39
+ `localhost` and `127.0.0.1` are one, and so are two DNS names for the same
40
+ PostgreSQL. The deployment's URL is not in the harness's child environment at
41
+ all.
42
+ - **A release will not start an artifact that is not this source's.**
43
+ `assertBuildArtifacts()` checked that the declared paths exist, and existence
44
+ is not freshness: `pm2:prod` run without a build applied this source's
45
+ migrations and started the previous source's `dist` and `.next`, moving the
46
+ schema ahead of the code in the one direction a code rollback does not undo.
47
+ `bun run build` now leaves a digest of the source it read, and the release
48
+ refuses when the tree no longer hashes to it — a digest rather than a
49
+ timestamp, because a checkout rewrites every mtime and a formatter rewrites
50
+ some for no change at all. Everything is source unless something names it
51
+ otherwise, and the something is the **declaration**: the digest skips
52
+ `build.artifacts`, `node_modules`/`.git`, and runtime state. It no longer
53
+ skips by kind — every `.md`, every test file, every directory called
54
+ `generated` — because a project that imports MDX or keeps checked-in source in
55
+ a directory of that name would have changed its content, kept its digest, and
56
+ been told a stale artifact was current. `.env` stays out on purpose: a binding
57
+ is not an input to this build, and hashing it would refuse a correct artifact
58
+ whenever a deployment edited its own environment.
59
+ - **The shutdown budget is an upper bound again.** `terminationBudgetMs` adds a
60
+ fixed cleanup allowance to the drain floor and refuses a supervision policy
61
+ that allows less — but the closes that run after the drain (the MCP session,
62
+ the database pool) had no deadline of their own, so a hung close ran past the
63
+ very kill timeout the budget had approved and turned an orderly shutdown into
64
+ the SIGKILL that runs no cleanup at all. The role's cleanup now shares one
65
+ bounded budget with the generator, from one constant both read, and names
66
+ whatever it stopped waiting for — and then **ends the process**. Setting
67
+ `process.exitCode` only decides the code a process reports when it exits, and
68
+ a step that ran out of time is usually still holding the handle that stops it
69
+ from exiting; a close that *threw* is reported with its cause and is no longer
70
+ counted as a clean shutdown. The shared deadline is measured with
71
+ `performance.now()` and the clock can no longer be injected: a wall clock
72
+ stepped backwards widens the very upper bound the supervisor's kill timeout
73
+ was derived from, and a frozen injected clock hands every step a full budget.
74
+ - **The project declaration stays out of the browser bundle.**
75
+ `lib/seo/pages.ts` imported `appDeclaration` for one field, and a client
76
+ component imports that module — so role commands, working directories,
77
+ artifact and migration paths and every environment variable name travelled
78
+ into the client graph along with the Zod parser. It reads `appIdentity` now,
79
+ and a test walks every `'use client'` graph and fails if one reaches the
80
+ declaration — by resolving each specifier to a file rather than matching a
81
+ string, so a barrel re-export, a relative path into the config package and a
82
+ double-quoted import are all caught.
83
+ - **A duplicated host is one address.** The portability check counted the
84
+ entries of `PUBLIC_WEB_HOSTS` without deduplicating them, so the same host
85
+ written twice passed as two addresses and the proof compared the deployment
86
+ with itself.
87
+ - **Every dial in the runtime smoke is bounded.** An endpoint that accepts the
88
+ connection and never answers used to hang the gate with no output and no
89
+ deadline instead of failing it.
90
+ - **A build output cannot be published inside the template.** What the scaffolder
91
+ copies and what npm publishes were two lists that had to agree, and only one of
92
+ them was consulted when a name was excluded. The exclusions are data now, and a
93
+ test fails until the package manifest carries the same negation.
94
+ - **A role bound to an IPv6 address gets a readiness URL that parses.**
95
+ `BIND_HOST=::1` produced `http://::1:3211/health`, which is not an address
96
+ with a port and which `fetch` refuses — so the wait failed on the spelling
97
+ rather than on the role. The literal is bracketed.
98
+ - **`bun run dev` and `bun run pm2:prod` report the roles as running only once
99
+ they answer.** A supervisor returns at the spawn, seconds before a role
100
+ listens, so both printed their address at a moment when nothing was there and
101
+ every command after them raced the application they had just started. Both now
102
+ wait on each role's declared `readinessPath`.
103
+ - **`runtime:smoke` asks the deployment on the addresses it claims.** The
104
+ portability check carried two fixture hosts of its own, so it only ever passed
105
+ where somebody had put those exact names into `PUBLIC_WEB_HOSTS` — which only
106
+ the packed lane had. Everyone else got a bare 500 from a policy working as
107
+ designed. It now reads `PUBLIC_WEB_HOSTS`, and a deployment with too few
108
+ addresses to compare is told which line to add instead of being refused a
109
+ request.
110
+ - **A closed deployment is diagnosed, not reset.** `runtime:smoke` says what is
111
+ not listening and which command starts it, before the first check runs.
112
+ - **The theme value the toaster reads narrows again.** `@wrksz/themes` 1.2
113
+ changed `useThemeValue`'s type parameter to describe the map rather than the
114
+ value, and it now infers `const` — so naming the value union at the call site
115
+ widened the result to every member of `string` and the generated project
116
+ stopped type-checking. The call site lets inference do it.
117
+ - **A green `runtime:smoke` prints one line.** The MCP surface check asked the
118
+ server to list tools even when it advertised no tool capability, which made
119
+ the vendor client log a debug warning on every successful run. It reads the
120
+ advertised capability instead.
121
+
122
+ ### Changed
123
+
124
+ - **Every dependency of the generated project is on its latest release.**
125
+ Next 16.3.2, `next-intl` 4.13.7, `@tanstack/react-query` 5.102.3,
126
+ `@tanstack/react-table` 9.1.2, `framer-motion` 13.1.1, `@wrksz/themes` 1.2.0,
127
+ `shiki` 4.4.3, `sonner` 2.0.8, `ai` 7.0.78, `pg` 8.23.0, Playwright 1.62.1,
128
+ Biome 2.5.10 and the `@types/*` that go with them. No major crossed; the
129
+ Stitchkit range is unchanged and still declared once, in the catalog.
130
+ - **The generated project imports the declaration schema instead of mirroring
131
+ it.** `packages/config/src/declaration.ts` now reads
132
+ `parseProjectDeclaration` from `stitchkit/declaration`, and the 611-line
133
+ generated copy — `packages/config/src/project-declaration.generated.ts` — is
134
+ gone with the script that maintained it. The copy existed only because the
135
+ entrypoint was not on npm yet; the template's catalog targets `^0.60.0`,
136
+ which publishes it, so "one schema, three readers" is now literally true.
137
+ Adopting it is one import and one deletion — see
138
+ [`UPGRADING.md`](./UPGRADING.md).
139
+ - **`scripts/local-env.ts` reads the identity module, not the declaration.** It
140
+ needs one slug, and a project scaffolded with `--no-install` renders its
141
+ `.env` before anything is installed — a script that reaches for the
142
+ framework's schema to read a name cannot run in that window.
143
+
144
+ ## [0.4.0] — 2026-08-25
145
+
146
+ ### ⚠️ Breaking changes
147
+
148
+ - **The repository example's browser talks to its OWN origin by default.** The
149
+ example is what gets copied, and it was demonstrating the hard case: the
150
+ browser dialled the API role directly, so the address had to arrive from the
151
+ server at runtime, the API client could not exist until it did, and every call
152
+ site paid for that with a lazy accessor — `repositoryApi().read()` — plus a
153
+ runtime error when something rendered outside `<Providers>`. Somebody copying
154
+ it inherited that whether or not they were cross-origin at all. The body of
155
+ the example is now the default shape: a same-origin `/api/…` path forwarded by
156
+ the web role, and a client that is a module constant.
157
+ `// before: repositoryApi().read()` → `// after: repositoryApi.read()`
158
+ The cross-origin form is not lost — it moved to a named file,
159
+ `packages/frontend/src/lib/api/cross-origin.ts`, with what it costs written
160
+ next to it, and switching to it is one import in `queries.ts`.
161
+ - **`PUBLIC_REALTIME_ORIGIN` — the socket's address has a name of its own.**
162
+ `PUBLIC_API_ORIGIN` used to carry both questions and answer only one: it read
163
+ as a mode switch while in fact nothing but the realtime socket looked at it.
164
+ The two are genuinely different — HTTP can be forwarded by the web role, and a
165
+ WebSocket upgrade cannot survive a route handler — so a deployment can be
166
+ same-origin for HTTP and still have to name the socket's origin. Both are
167
+ optional; a deployment behind one routing layer sets neither.
168
+ `// before: PUBLIC_API_ORIGIN=https://api.example # …which only the socket read` →
169
+ `// after: PUBLIC_REALTIME_ORIGIN=https://api.example`
170
+ - **`app.config.json` is now `project.json` — the project *declaration*.** It no
171
+ longer describes only identity: it states what this repository is, the roles it
172
+ runs, what it builds, what it needs before it starts, the release steps that
173
+ must happen once, and the environment variables a deployment must supply.
174
+ Identity moved under an `identity` key and the file gained a `schemaVersion`,
175
+ so a reader that does not understand the format refuses the project instead of
176
+ interpreting it partially.
177
+ `// before: import { appIdentity } from '@app/config/identity'; appIdentity.name` →
178
+ `// after: import { appDeclaration } from '@app/config/declaration'; appDeclaration.identity.name`
179
+
180
+ A client component imports `@app/config/app-identity` instead — a generated
181
+ module carrying identity alone. Importing the whole declaration from the
182
+ browser would ship role commands, working directories, build artifact paths and
183
+ the migration lockfile in the bundle.
184
+ `// before: import { appIdentity } from '@app/config/identity' // in a 'use client' file` →
185
+ `// after: import { appIdentity } from '@app/config/app-identity'`
186
+
187
+ The rule the file exists to hold: **it must be complete with no machine in
188
+ existence.** Ports, hosts, addresses, machine paths, routing shape and
189
+ supervision policy are named there by variable and never by value — and the
190
+ schema refuses them by shape rather than by review.
191
+
192
+ - **The SEO helpers are async, and `siteOrigin` is gone.** They read the public
193
+ origin from the request instead of a build-time constant, so they cannot be
194
+ constants themselves. A page that calls them must await them — and TypeScript
195
+ will *not* catch it inside an inferred object literal, where a `Promise`
196
+ silently serialises as `{}`.
197
+ `// before: const url = absoluteSiteUrl('/en'); export const siteOrigin` →
198
+ `// after: const url = await absoluteSiteUrl('/en') // and the component becomes async`
199
+ `// before: createPageMetadata('home', locale)` →
200
+ `// after: await createPageMetadata('home', locale)`
201
+
202
+ - **A forwarded host must be claimed before it is believed.** The public origin
203
+ comes from the request, which makes one artifact serve many addresses — and
204
+ would let any caller choose the canonical URL, the sitemap and the OG metadata
205
+ if it were trusted blindly. Set `PUBLIC_WEB_ORIGIN` for a single address, or
206
+ `PUBLIC_WEB_HOSTS` for several; a host outside them is refused. `x-forwarded-proto`
207
+ is narrowed to `http` or `https`.
208
+ `// before: (nothing — any x-forwarded-host was honoured)` →
209
+ `// after: PUBLIC_WEB_HOSTS=app.example,www.app.example`
210
+
211
+ - **Environment variables are declared once, and the declaration lists them as
212
+ `env.variables`.** The server schema, the frontend schema and the tooling
213
+ schema were three overlapping copies that had already diverged.
214
+ `packages/config/src/variables.ts` is now the single declaration; `server.ts`
215
+ and `frontend/src/env.ts` are projections of it, and the declaration's list is
216
+ *derived* from it rather than restated. An overlay may now **tighten** a
217
+ variable, not only add one — the repository example requires `INTERNAL_API_URL`,
218
+ `PUBLIC_API_ORIGIN` and `CORS_ORIGIN` because its frontend dereferences them on
219
+ every render.
220
+ `// before: z.url() repeated in three files; env.required with required:false entries` →
221
+ `// after: applicationVariables.INTERNAL_API_URL, referenced; env.variables`
222
+
223
+ - **`NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_WEB_URL` are gone.** Anything prefixed
224
+ `NEXT_PUBLIC_` is substituted at BUILD time, so declaring one froze a value of
225
+ the place into the artifact: the built `robots.txt` and `sitemap.xml` carried
226
+ one origin inside their bytes, and the server chunk carried
227
+ `NEXT_PUBLIC_API_URL:"http://…"` as a literal. One build could not serve a
228
+ second address.
229
+ `// before: NEXT_PUBLIC_WEB_URL=https://app.example → baked at build` →
230
+ `// after: no variable; the origin is read from the request`
231
+ **Cost, stated plainly:** `/robots.txt`, `/sitemap.xml` and — because the root
232
+ layout's `generateMetadata` reads the request — the whole `[locale]` segment
233
+ are no longer prerendered as static content. Setting `PUBLIC_WEB_ORIGIN`
234
+ short-circuits the request read and restores static rendering for a deployment
235
+ that serves exactly one address. Answers are built once per address, not once
236
+ per request: `cacheByOrigin` memoises them behind a bounded LRU so a forged
237
+ `Host` cannot grow the cache.
238
+
239
+ - **`CORS_ORIGIN` is optional.** A frontend that reaches the API through its own
240
+ routing layer makes same-origin requests, and requiring an origin there was
241
+ requiring knowledge of the place. Set it only for a genuinely cross-origin
242
+ browser.
243
+ `// before: CORS_ORIGIN=https://app.example # required` →
244
+ `// after: unset unless the browser genuinely lives elsewhere`
245
+
246
+ - **The smoke and e2e addresses are `SMOKE_API_ORIGIN` and `SMOKE_WEB_ORIGIN`.**
247
+ They are legitimately bound to a place — they name the deployment a check dials —
248
+ but must not carry a prefix that makes the build substitute them.
249
+ `// before: NEXT_PUBLIC_API_URL=http://127.0.0.1:3211` →
250
+ `// after: SMOKE_API_ORIGIN=http://127.0.0.1:3211`
251
+
252
+ - **Each role is started by its own PROCESS, in its own directory, and PM2
253
+ process names follow the declared role names.** A role's command is `executable`
254
+ plus `args` in the declaration — argv, never a shell string — and the
255
+ supervision files are rendered from it.
256
+ `// before: <slug>-backend, <slug>-frontend` → `// after: <slug>-api, <slug>-web`
257
+ **Before your first `pm2:prod` on the new files**, remove the old processes, or
258
+ `startOrReload` will start the new pair beside them and both will fight for the
259
+ same ports under `autorestart`:
260
+ ```bash
261
+ pm2 delete <slug>-backend <slug>-frontend <slug>-backend-dev <slug>-frontend-dev
262
+ ```
263
+
264
+ - **The web role reads its bindings itself.** The supervisor no longer builds an
265
+ argv for it; injecting `WEB_PORT` is now sufficient, where before a deployment
266
+ that set it and stopped there got a web role on the wrong port, silently. There
267
+ is no default port in the repository any more — a missing variable fails by
268
+ name.
269
+ `// before: "start": "next start --port ${WEB_PORT:-3210}"` →
270
+ `// after: "start": "bun scripts/serve.ts production"`
271
+
272
+ ### Fixed
273
+
274
+ - **A supervised backend now actually drains.** The supervision files started the
275
+ role through a script runner, so the stop signal arrived twice — once from PM2,
276
+ once forwarded by the launcher — and a shutdown chain treats the second signal
277
+ as "force it now". Measured on a real PM2 stop: a declared 15 s grace period
278
+ ended after **1.3 ms** with `outcome: "forced"`, `reason: "signal"`, and the
279
+ only visible trace was a non-zero exit code. The supervisor now execs the role
280
+ itself, and the same stop reports `outcome: "clean"` with exit 0.
281
+ `// before: script: 'bun', args: ['run', 'start'] // an intermediate shape, never released` →
282
+ `// after: script: 'bun', args: ['dist/index.js']`
283
+ A generated project at the previous release ran `script: 'dist/index.js'` with
284
+ `interpreter: 'bun'`, which had the same property; the defect it *did* ship is
285
+ the timeout mismatch below.
286
+ - **Supervision no longer kills the backend mid-shutdown.** The application asked
287
+ for a 30 s drain while PM2 sent `SIGKILL` after `kill_timeout: 15000` in
288
+ production and 10000 in development — so a drain longer than the supervisor's
289
+ patience never finished, every time. The check now covers the **whole**
290
+ termination budget rather than the drain alone: drain floor, plus the force
291
+ window that follows it, plus cleanup. Comparing against the floor alone let
292
+ 15 s + 5 s meet a 20 s kill timeout exactly, with no margin at all.
293
+ - **The backend says how its shutdown ended.** `onComplete` logs the outcome, the
294
+ reason, the duration and how many requests completed or were aborted. Without
295
+ it an operator saw a process that vanished and an exit code, and could not tell
296
+ a clean drain from one that was cut short — which is exactly how the defect
297
+ above stayed invisible.
298
+ - **A requested stop of the web role reports success.** Next exits `130` on
299
+ `SIGINT`; the role passed that upward, so every ordinary supervised stop looked
300
+ like a failure. A stop the role was asked to perform now exits `0`, while a code
301
+ from any other cause is still passed on unchanged.
302
+ - **A deployment's environment is no longer overruled by a file.** The production
303
+ supervision file loaded `.env` with `override: true`, so a value injected into
304
+ the process lost to a value in the repository. Bindings come from the place; the
305
+ file fills gaps.
306
+ - **Release steps come from the declaration.** `pm2:prod` hand-carried
307
+ `db:deploy` and a build preflight as a shell string beside the declaration that
308
+ already stated them. It now runs `scripts/release.ts`, which checks every
309
+ artifact `build.artifacts` declares and applies migrations for the engine
310
+ `release.migrations` declares — refusing an engine it has no command for rather
311
+ than skipping the step, because silently not migrating is what leaves a machine
312
+ running against the wrong schema. Both declared migration paths are checked,
313
+ the lockfile included. Development runs the same step.
314
+ - **`bun run test` passes on a fresh scaffold.** A test read `.env` at module
315
+ load, before `env:ensure` had created it, so the second gate the README tells a
316
+ new user to run died with `ENOENT` before a single test executed. Both CI paths
317
+ write `.env` first, so nothing saw it.
318
+
319
+ ### Changed
320
+
321
+ - **The generated build declares whether it reads data, and CI proves it does
322
+ not.** Data read while building is a third kind of input — neither code nor a
323
+ binding — and a build that reads it undeclared is a function of whichever
324
+ machine had the database. The template answers it the default way: no route
325
+ can reach a data source at all (`check-authored` refuses the import and now
326
+ says why), so the build is a function of the source alone. The packed lane
327
+ builds against a database address that accepts nothing, which is the only
328
+ check that covers every transitive path at once. A project that genuinely
329
+ needs data at build time declares a frozen export in `build.inputs` and
330
+ `bun scripts/build-inputs.ts` refuses it the moment its digest drifts.
331
+ - **One artifact is now provably portable in CI, within a stated policy.**
332
+ `runtime:smoke` asks the running web role for `/sitemap.xml` and `/robots.txt`
333
+ under two different external addresses and requires two different answers —
334
+ and requires a *third*, unclaimed host to be refused. The first half catches a
335
+ build-time address creeping back in; the second catches the portability
336
+ mechanism turning into an open redirect for metadata.
337
+ - **The drain floor has one home.** The backend passed a literal grace period
338
+ while the declaration stated another — the same two-numbers-in-two-files shape
339
+ that let a 30 s floor meet a 15 s kill timeout. It now reads
340
+ `apiRole.drainFloorMs`, the number a supervisor reads too.
341
+ - **Three files are generated from the declaration** — both supervision files and
342
+ the client-safe identity module — plus the `env.variables` block of the
343
+ declaration itself. `bun run gen:declaration` renders them and the test suite
344
+ refuses a stale copy. The example's declaration is generated from the
345
+ template's in the same way.
346
+ - **The generated application targets Stitchkit `^0.59.0`.** The template's
347
+ catalog pointed at `^0.52.0`, so a project scaffolded today started seven
348
+ minors behind the framework — without neutral client-disconnect handling,
349
+ managed files, composed auth, async operation contracts or the CLI presentation
350
+ policy. Both the packed target lane and the packed HEAD lane pass on the new
351
+ target.
352
+ - **The scaffolder prints the addresses the generated project will actually
353
+ use**, read back from its declaration and example environment rather than
354
+ restated as constants.
355
+
9
356
  ## [0.3.3] — 2026-08-18
10
357
 
11
358
  ### Changed
package/README.md CHANGED
@@ -16,7 +16,9 @@ complete production UI system.
16
16
  It uses one conventional `packages/*` namespace: `backend`, `frontend`,
17
17
  `config`, `db` and `shared`. The destination name becomes the generated slug;
18
18
  `--display-name` sets the human title. Both are recorded once in
19
- `app.config.json` and drive package, process, transport, UI and SEO identity.
19
+ `project.json` — the generated project's **declaration**, the single
20
+ machine-readable statement it makes about itself — and drive package, process,
21
+ transport, UI and SEO identity.
20
22
 
21
23
  The default scaffold is domain-free. To add the runnable repository example:
22
24