@zerotal/arch 1.9.0 → 1.11.0

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.
@@ -171,6 +171,21 @@ await testApp.withSession({ locale: "fr", flash: "saved" }).get("/profile");
171
171
  `session.secret` and `session.cookie` from your config. `withSession()` preserves
172
172
  any `user_id` already set by `actingAs()`.
173
173
 
174
+ > **No users table?** `withSession()` is the whole answer, and it is the one to reach
175
+ > for when identity is not a row — an app whose login _is_ an IMAP login has no user
176
+ > to hand `actingAs()`. Seed whatever your app reads from the session and the request
177
+ > is authenticated:
178
+ >
179
+ > ```typescript fragment
180
+ > // in a test
181
+ > await testApp.withSession({ mail_wallet: { primary: "a@example.test" } }).get("/mail");
182
+ > ```
183
+ >
184
+ > Reaching past this to the session driver is the wrong layer and does not work —
185
+ > `driver.write()` is not a method, and `saveSession()` wants an id and a `Response`
186
+ > you do not have yet. Both of these encode through the app's _own_ driver, so the
187
+ > cookie always matches the format the app will read.
188
+
174
189
  ### Headers and redirects
175
190
 
176
191
  ```typescript fragment
@@ -396,42 +411,43 @@ expect(ctx.response?.status).toBe(200);
396
411
 
397
412
  ### TestResponse
398
413
 
399
- | Member | Signature | Description |
400
- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------- |
401
- | `assertStatus` | `assertStatus(expected: number): this` | Assert the status code. |
402
- | `assertOk` / `assertCreated` / `assertNoContent` | `(): this` | Assert `200` / `201` / `204`. |
403
- | `assertSuccessful` | `(): this` | Assert any `2xx`. |
404
- | `assertMovedPermanently` | `(): this` | Assert `301`. |
405
- | `assertUnauthorized` / `assertForbidden` / `assertNotFound` / `assertUnprocessable` | `(): this` | Assert `401` / `403` / `404` / `422`. |
406
- | `assertServerError` | `(): this` | Assert `500`. |
407
- | `assertRedirect` | `assertRedirect(url: string): this` | Assert a `3xx` whose `Location` contains `url`. |
408
- | `assertHeader` | `assertHeader(name, value?): this` | Assert a header is present (and contains `value`). |
409
- | `assertHeaderMissing` | `assertHeaderMissing(name): this` | Assert a header is absent. |
410
- | `assertJson` | `assertJson(expected): this` | Assert each key in `expected` matches the JSON body. |
411
- | `assertJsonPath` | `assertJsonPath(path, expected): this` | Assert a dot-notation path in the JSON body. |
412
- | `assertJsonCount` | `assertJsonCount(count, key?): this` | Assert an array length at the body or `key`. |
413
- | `assertSee` / `assertBodyContains` | `(needle): this` | Assert the body contains `needle`. |
414
- | `assertDontSee` | `assertDontSee(needle): this` | Assert the body does not contain `needle`. |
415
- | `assertSeeText` / `assertDontSeeText` | `(needle): this` | The same, against the body with its tags stripped. |
416
- | `assertInvalid` | `assertInvalid(fields?): this` | Assert validation failed, optionally on `fields`. |
417
- | `assertValid` | `assertValid(fields?): this` | Assert validation did not fail. |
418
- | `validationErrors` | `(): Record<string, string[]> \| null` | The errors, from the body or the session. |
419
- | `assertAuthenticated` | `(): this` | Assert the session holds a `user_id`. |
420
- | `assertAuthenticatedAs` | `assertAuthenticatedAs(user \| id): this` | Assert that specific user is signed in. |
421
- | `assertGuest` | `(): this` | Assert nobody is signed in. |
422
- | `assertCookie` | `assertCookie(name, value?): this` | Assert a `Set-Cookie` (and optional value). |
423
- | `assertCookieMissing` | `assertCookieMissing(name): this` | Assert no such cookie is set. |
424
- | `assertSessionHas` | `assertSessionHas(key, value?): this` | Assert the session contains `key`. |
425
- | `assertSessionMissing` | `assertSessionMissing(key): this` | Assert the session lacks `key`. |
426
- | `assertSessionHasErrors` / `assertSessionHasNoErrors` | `(fields?): this` | Assert flashed validation errors. |
427
- | `session` | `(): Record<string, unknown> \| null` | The decoded session. |
428
- | `assertInertia` | `assertInertia(component?, props?): this` | Assert the Inertia page and a partial prop match. |
429
- | `assertInertiaProp` | `assertInertiaProp(key, value?): this` | Assert a single Inertia prop. |
430
- | `inertia` | `(): InertiaPage \| null` | The Inertia page object, from either wire shape. |
431
- | `exception` | `(): unknown` | The exception the request raised, if any. |
432
- | `json` | `json<T>(): T` | Parse and return the full JSON body. |
433
- | `text` | `text(): string` | Return the body as text. |
434
- | `status` / `ok` / `headers` | getters | The underlying `Response` status, `ok`, and headers. |
414
+ | Member | Signature | Description |
415
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------ |
416
+ | `assertStatus` | `assertStatus(expected: number): this` | Assert the status code. |
417
+ | `assertOk` / `assertCreated` / `assertNoContent` | `(): this` | Assert `200` / `201` / `204`. |
418
+ | `assertSuccessful` | `(): this` | Assert any `2xx`. |
419
+ | `assertMovedPermanently` | `(): this` | Assert `301`. |
420
+ | `assertUnauthorized` / `assertForbidden` / `assertNotFound` / `assertUnprocessable` | `(): this` | Assert `401` / `403` / `404` / `422`. |
421
+ | `assertServerError` | `(): this` | Assert `500`. |
422
+ | `assertRedirect` | `assertRedirect(url: string): this` | Assert a `3xx` whose `Location` path equals `url`. |
423
+ | `assertRedirectContains` | `assertRedirectContains(fragment: string): this` | Assert a `3xx` whose `Location` merely contains `fragment` — for a signed URL. |
424
+ | `assertHeader` | `assertHeader(name, value?): this` | Assert a header is present (and contains `value`). |
425
+ | `assertHeaderMissing` | `assertHeaderMissing(name): this` | Assert a header is absent. |
426
+ | `assertJson` | `assertJson(expected): this` | Assert each key in `expected` matches the JSON body. |
427
+ | `assertJsonPath` | `assertJsonPath(path, expected): this` | Assert a dot-notation path in the JSON body. |
428
+ | `assertJsonCount` | `assertJsonCount(count, key?): this` | Assert an array length at the body or `key`. |
429
+ | `assertSee` / `assertBodyContains` | `(needle): this` | Assert the body contains `needle`. |
430
+ | `assertDontSee` | `assertDontSee(needle): this` | Assert the body does not contain `needle`. |
431
+ | `assertSeeText` / `assertDontSeeText` | `(needle): this` | The same, against the body with its tags stripped. |
432
+ | `assertInvalid` | `assertInvalid(fields?): this` | Assert validation failed, optionally on `fields`. |
433
+ | `assertValid` | `assertValid(fields?): this` | Assert validation did not fail. |
434
+ | `validationErrors` | `(): Record<string, string[]> \| null` | The errors, from the body or the session. |
435
+ | `assertAuthenticated` | `(): this` | Assert the session holds a `user_id`. |
436
+ | `assertAuthenticatedAs` | `assertAuthenticatedAs(user \| id): this` | Assert that specific user is signed in. |
437
+ | `assertGuest` | `(): this` | Assert nobody is signed in. |
438
+ | `assertCookie` | `assertCookie(name, value?): this` | Assert a `Set-Cookie` (and optional value). |
439
+ | `assertCookieMissing` | `assertCookieMissing(name): this` | Assert no such cookie is set. |
440
+ | `assertSessionHas` | `assertSessionHas(key, value?): this` | Assert the session contains `key`. |
441
+ | `assertSessionMissing` | `assertSessionMissing(key): this` | Assert the session lacks `key`. |
442
+ | `assertSessionHasErrors` / `assertSessionHasNoErrors` | `(fields?): this` | Assert flashed validation errors. |
443
+ | `session` | `(): Record<string, unknown> \| null` | The decoded session. |
444
+ | `assertInertia` | `assertInertia(component?, props?): this` | Assert the Inertia page and a partial prop match. |
445
+ | `assertInertiaProp` | `assertInertiaProp(key, value?): this` | Assert a single Inertia prop. |
446
+ | `inertia` | `(): InertiaPage \| null` | The Inertia page object, from either wire shape. |
447
+ | `exception` | `(): unknown` | The exception the request raised, if any. |
448
+ | `json` | `json<T>(): T` | Parse and return the full JSON body. |
449
+ | `text` | `text(): string` | Return the body as text. |
450
+ | `status` / `ok` / `headers` | getters | The underlying `Response` status, `ok`, and headers. |
435
451
 
436
452
  ## Next steps
437
453
 
@@ -209,6 +209,146 @@ observers, global scopes, and state-machine callbacks, plus framework event
209
209
  subscriptions. `createTestApp()` and `testApp.close()` call it for you, so suites
210
210
  using those helpers don't need the explicit `afterEach`.
211
211
 
212
+ ## Pages render
213
+
214
+ A test that asserts a status code or an Inertia payload proves the _server_ did its
215
+ job. It proves nothing about the component, and a page can throw on its first paint
216
+ while every such test passes — the route answers `200`, the payload is correct, and
217
+ the failure happens in a browser the suite never opened.
218
+
219
+ An app shipped a blank page to production with **614 passing tests** exactly that way:
220
+ a [layout callback](/docs/inertia/rendering#persistent-layouts) read `page.props`,
221
+ which the callback is not given.
222
+
223
+ `renderPage()` builds the component tree and lets whatever it throws escape:
224
+
225
+ ```typescript fragment
226
+ // tests/pages.test.ts
227
+ import { renderPage } from "@zerotal/inertia/testing";
228
+ import Profile from "../resources/js/pages/profile";
229
+
230
+ test("profile builds", async () => {
231
+ await renderPage(Profile, { title: "Profile" }, { shared: SHARED });
232
+ });
233
+ ```
234
+
235
+ It renders through Inertia's own `<App>`, so `usePage()`, `<Head>` and a persistent
236
+ layout all behave as they do in the browser — the layout is resolved and rendered
237
+ too, which is the case worth catching.
238
+
239
+ Two things to know:
240
+
241
+ - **Seed the shared props.** A component that destructures `auth` or `flash` throws
242
+ without them. That is a real failure and rarely the one you are testing for, so
243
+ pass the shape your `Inertia.share()` actually sends.
244
+ - **It is not a DOM.** `useEffect` does not run and nothing clicks; this is
245
+ `renderToString`. For behaviour after paint, use
246
+ [the browser harness](/docs/testing/browser).
247
+
248
+ The React scaffold ships one of these covering every page it generates. Add a line
249
+ when you add a page — the cost is one line and the bug it catches is a white screen
250
+ your users find first.
251
+
252
+ ## `bun test` vs `bun zt test`
253
+
254
+ Both run the same files. `bun zt test` is a wrapper that sets up three things Bun's
255
+ runner does not, and each of them has cost somebody a day:
256
+
257
+ | | `bun test` | `bun zt test` |
258
+ | ---------------- | --------------------- | ---------------------------------------------------------- |
259
+ | Per-test timeout | Bun's default, 5000ms | 30000ms (`--timeout`, override with `--timeout=`) |
260
+ | Runtime check | none | refuses a Bun below the project's `engines.bun` |
261
+ | DB wiring | none | preloads `@zerotal/testing/preload` and passes `ZT_DB_URL` |
262
+
263
+ ### The timeout
264
+
265
+ Bun's default per-test timeout is 5000ms, and a suite that boots an app per file
266
+ exceeds it on a loaded machine — CI, or a laptop that has just run `tsc`. The
267
+ failures look like flakes, which is the expensive part: a flake gets re-run, and a
268
+ re-run passes.
269
+
270
+ `bun zt test` sets `--timeout=30000`. If you run `bun test` directly, pass it
271
+ yourself, because **the two documented-looking alternatives do not work**:
272
+
273
+ - `[test] timeout` in `bunfig.toml` — ignored.
274
+ - `setDefaultTimeout()` in a preload — applies to the first test file only. Bun
275
+ re-imports the preload per file, but the setting does not survive.
276
+
277
+ The command-line flag is the only mechanism that covers hooks as well as tests,
278
+ which matters because it is usually a `beforeAll` that boots the app.
279
+
280
+ ### The runtime
281
+
282
+ `engines.bun` in your `package.json` is a floor, and until you enforce it, it is a
283
+ comment. The shell's `bun` and the project's can differ, and the difference between
284
+ two Bun releases is real and narrow: `Intl` formatting, the SQLite bindings and
285
+ `node:` compatibility all move. So a handful of currency or date assertions go red
286
+ and the rest pass, and you go looking for a bug in the code they touch, because
287
+ nothing in the failure says "wrong binary".
288
+
289
+ `bun zt test` refuses to run below the declared floor. Direct `bun test` runs get the
290
+ same check as a warning if you load the preload:
291
+
292
+ ```toml
293
+ # bunfig.toml
294
+ [test]
295
+ preload = ["@zerotal/testing/preload"]
296
+ timeout = 30000 # note: currently ignored by Bun — pass --timeout on the command line
297
+ ```
298
+
299
+ Set `ZT_ALLOW_RUNTIME_MISMATCH=1` to downgrade the refusal to a warning while you
300
+ are mid-upgrade.
301
+
302
+ ### `@zerotal/core/runtime`
303
+
304
+ The checks behind the two paragraphs above, exported so a script or a test of your
305
+ own can make the same assertion. `zt` runs both at the top of every command; the test
306
+ preload runs the floor check as a warning.
307
+
308
+ | Export | Signature | What it answers |
309
+ | -------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
310
+ | `declaredBunFloor` | `declaredBunFloor(cwd): { range, manifest } \| null` | The nearest `engines.bun` up the tree from `cwd`. |
311
+ | `runtimeBelowFloor` | `runtimeBelowFloor(cwd?): RuntimeFloor \| null` | Is this process below that floor? `null` when it is met or none is declared. |
312
+ | `runtimeBelowFloorMessage` | `runtimeBelowFloorMessage(floor): string` | The explanation to print — both versions, the manifest, and the way out. |
313
+ | `installedBunVersion` | `installedBunVersion(cwd): { version, manifest } \| null` | The Bun in `node_modules`, if the project installs one as a package. |
314
+ | `declaresBunDependency` | `declaresBunDependency(cwd): boolean` | Whether the project _asked_ for that package, or acquired it as a transitive peer. |
315
+ | `runtimeMismatch` | `runtimeMismatch(cwd?): RuntimeMismatch \| null` | Does the running Bun differ from the installed one? Compared exactly — a patch is a binary. |
316
+ | `runtimeMismatchMessage` | `runtimeMismatchMessage(mismatch): string` | The explanation for that one. |
317
+ | `runtimeMismatchAllowed` | `runtimeMismatchAllowed(): boolean` | Whether `ZT_ALLOW_RUNTIME_MISMATCH` is set. |
318
+ | `bunBinary` | `bunBinary(): string` | The binary to spawn a child with — `process.execPath`, never the name PATH resolves. |
319
+ | `RUNTIME_MISMATCH_ESCAPE` | `"ZT_ALLOW_RUNTIME_MISMATCH"` | The env var name, so a script can set it without hardcoding the string. |
320
+
321
+ `RuntimeFloor` is `{ running, required, manifest }`; `RuntimeMismatch` is
322
+ `{ running, installed, manifest }`. Both name the file the second version came from,
323
+ because "which one is wrong" is the question you actually have.
324
+
325
+ ## Configuration is per-process, and `bun test` is one process
326
+
327
+ Zerotal resolves configuration once, at boot. `bun test` runs every file in the same
328
+ process, so **whichever file boots the app first fixes the configuration for all of
329
+ them.**
330
+
331
+ A test that sets an environment variable in its own `beforeAll` and then asserts on
332
+ the resulting behaviour passes alone and fails in the suite — or worse, passes in the
333
+ suite for a reason unrelated to what it claims to test:
334
+
335
+ ```typescript fragment
336
+ // Passes alone. In a suite, the app may already be booted with CSRF on, and the
337
+ // three "rejects without a token" assertions below pass on a 419 they would have
338
+ // got anyway — never reaching the guard they name.
339
+ beforeAll(() => {
340
+ Bun.env.CSRF_DISABLED = "1";
341
+ });
342
+ ```
343
+
344
+ Assert on the _relationship_ rather than on a literal — the published origin equals
345
+ the configured one, whatever it is — or boot a dedicated app for the case:
346
+
347
+ ```typescript fragment
348
+ const app = await createTestApp({ config: { app: { url: "https://example.test" } } });
349
+ expect(page.canonical).toBe(config("app.url"));
350
+ ```
351
+
212
352
  ## Running the suite from a script
213
353
 
214
354
  A script that gates on the tests has to read the tests' exit status, and a pipe hides it:
package/docs/upgrade.md CHANGED
@@ -10,15 +10,28 @@ what changed in each version, see the [Release Notes](/docs/changelog).
10
10
 
11
11
  ## Versioning
12
12
 
13
- Zerotal follows semantic versioning across its `@zerotal/*` packages, which share a
14
- version line:
15
-
16
- - **Patch** (`x.y.Z`) — bug fixes, safe to take anytime.
17
- - **Minor** (`x.Y.z`) — new features, backward compatible.
18
- - **Major** (`X.y.z`) breaking changes; read the version's section in the
19
- [Release Notes](/docs/changelog) before upgrading.
20
-
21
- > **Warning** while the 1.x line is young, a breaking change may also land in a minor or a patch. It is always labelled **BREAKING** in the [Release Notes](/docs/changelog) with migration steps. Read the notes for every version you cross, not only the majors. See [Releases and versioning](/docs/support-policy#releases-and-versioning) for which ones have shipped and when this carve-out ends.
13
+ Zerotal's `@zerotal/*` packages share one version line, and what each number means
14
+ is set by how much the framework still moves in a year rather than by the letter of
15
+ semver:
16
+
17
+ - **Patch** (`x.y.Z`) — anything that does not break. Fixes, and features too.
18
+ Safe to take at any time.
19
+ - **Minor** (`x.Y.z`) a breaking change. Always labelled **BREAKING** in the
20
+ [Release Notes](/docs/changelog), with the reason and the migration steps, and
21
+ given its own section on this page.
22
+ - **Major** (`X.y.z`) — an annual consolidation, cut each July. The next is 2.0, in
23
+ July 2027.
24
+
25
+ Why not strict semver: a framework this young corrects itself often, and under
26
+ strict semver every correction is a major. A version line that reaches 9.0 in its
27
+ first year tells a reader nothing about how much has changed — only that the
28
+ project is willing to break things, which the release notes already say far more
29
+ precisely. Keeping the major for a yearly line in the sand leaves it meaning
30
+ something, and puts the work where it is useful: reading the notes for each minor.
31
+
32
+ > **Warning** — **a caret range crosses a minor.** `"zerotal": "^1.10.0"` will
33
+ > install 1.11.0, and its breaking change, without asking. Read the notes for every
34
+ > minor you cross, or pin with a tilde (`~1.10.0`) and cross them deliberately.
22
35
 
23
36
  > **Warning** — always upgrade the `@zerotal/*` packages together. Mixing versions across core, ORM, and feature packages leads to type and runtime mismatches.
24
37
 
@@ -169,6 +182,113 @@ these are the changes that need action. Full detail is in the
169
182
  interface so pages that read them do not look unpassed; see
170
183
  [Typed props](/docs/inertia/props#typed-props).
171
184
 
185
+ ## 1.9 to 1.10
186
+
187
+ Three settings changed meaning. Each is quiet if it does not apply to you, and each is
188
+ worth thirty seconds of checking if it does.
189
+
190
+ 1. **`scheduler.timezone` is honoured.** It was documented as informational and read by
191
+ nothing, so whatever you put there had no effect and your schedules ran in the
192
+ server's zone. It is now the zone every schedule is evaluated in unless the task sets
193
+ its own.
194
+
195
+ Its default moved from the literal `"UTC"` to **the system zone**, so an app that never
196
+ set the key keeps doing exactly what it did. The case to check is an app that _did_:
197
+
198
+ ```ts fragment
199
+ // config/scheduler.ts
200
+ export default SchedulerConfig({ timezone: env("APP_TIMEZONE", "UTC") });
201
+ ```
202
+
203
+ On a server that is not on UTC, that line used to do nothing and now moves every
204
+ schedule. Either set it to the zone you actually want your crons read in — which is
205
+ the point of the setting — or delete the key to keep the server's zone.
206
+
207
+ `bun zt schedule:list` prints each task's next run in its own zone, which is the
208
+ quickest way to see whether anything moved.
209
+
210
+ 2. **Named rate limiters need `.trustedProxies(n)` behind a proxy.** `RateLimiter`'s
211
+ `.byIp()`, `.byUser()` and `.byApiKey()` ignored the proxy count entirely and read
212
+ `X-Forwarded-For` unconditionally. They now follow the same rule `ThrottleMiddleware`
213
+ already did — the header is consulted only when you say how many proxies sit in front:
214
+
215
+ ```ts fragment
216
+ RateLimiter.for("login").limit(5).every(60).byIp().trustedProxies(1).register();
217
+ ```
218
+
219
+ Without it the address used is the socket's, which behind a proxy is the _proxy's_, and
220
+ every visitor shares one bucket. `bun zt doctor` reports any limiter that needs this —
221
+ it could not before, because its check exempted custom key resolvers and all three of
222
+ these are one.
223
+
224
+ 3. **React apps using SSR need `@inertiajs/react` installed.** The same adapter your
225
+ browser entry point already uses. Server-side rendering now goes through its `<App>`,
226
+ which is what makes `<Head>` produce a title and an og: card in the HTML your server
227
+ actually sends. If it is missing you get a named error at render time, not a silent
228
+ omission.
229
+
230
+ Nothing to change if you already have it as a dependency, which every React Inertia app
231
+ does.
232
+
233
+ ## 1.10 to 1.11
234
+
235
+ Two changes to how the database is treated. Both are **BREAKING** in the narrow sense
236
+ that a working app can stop working on upgrade, and both refuse loudly rather than
237
+ doing something quiet.
238
+
239
+ 1. **SQLite enforces foreign keys.** `database.sqlite.foreignKeys` defaults to `true`,
240
+ so `PRAGMA foreign_keys = ON` is set on every connection. Until now SQLite ignored
241
+ them, which meant `constrained()` and `cascadeOnDelete()` in your migrations
242
+ described behaviour the database would not perform — deleting a parent left its
243
+ children, silently.
244
+
245
+ The risk is data you already have. A child row whose parent is missing was legal
246
+ without enforcement and is a constraint violation with it, so a write touching one
247
+ now fails. Find them before deploying:
248
+
249
+ ```bash fragment
250
+ bun zt db:check-foreign-keys
251
+ ```
252
+
253
+ It lists every offending row by table and rowid and exits non-zero, so a release
254
+ script can gate on it. `zt doctor` reports the same thing. Delete them or repoint
255
+ them at a parent that exists.
256
+
257
+ To take the release without dealing with it yet:
258
+
259
+ ```ts fragment
260
+ // config/database.ts
261
+ export default DatabaseConfig({ sqlite: { foreignKeys: false } });
262
+ ```
263
+
264
+ Take that override back off afterwards. With it in place `cascadeOnDelete()` is a
265
+ comment.
266
+
267
+ 2. **A renumbered migration is refused rather than re-run.** A migration is recorded
268
+ under its filename, so renaming one makes an applied migration look pending — the
269
+ runner tries it again and fails on `table already exists`. Renumbering `001_` to
270
+ `0001_` to match the scaffold's convention is exactly the kind of tidying that
271
+ causes it, and it takes every migration with it.
272
+
273
+ `migrate` now recognises that shape and stops:
274
+
275
+ ```
276
+ "0001_create_users" looks like "001_create_users", which has already run — the
277
+ same migration renumbered rather than a new one.
278
+ ```
279
+
280
+ If you meant to rename, pin the identity to what the database already holds and the
281
+ filename is then free:
282
+
283
+ ```ts fragment
284
+ export default class CreateUsers extends Migration {
285
+ static override id = "001_create_users";
286
+ }
287
+ ```
288
+
289
+ If it really is a new migration, give it a name that does not collide once the
290
+ leading digits are removed.
291
+
172
292
  ## The managed zt.ts
173
293
 
174
294
  `zt.ts` is framework-managed — the header says _do not modify_. If a release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/arch",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -35,11 +35,11 @@
35
35
  "typecheck": "tsc --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@zerotal/core": "1.9.0"
38
+ "@zerotal/core": "1.11.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.8.0",
42
- "@zerotal/orm": "1.9.0"
42
+ "@zerotal/orm": "1.11.0"
43
43
  },
44
44
  "description": "The Zerotal agent surface — an MCP server that hands coding agents the framework's machine-readable truth: exact API signatures, live routes and schema, version-matched docs, and `zt doctor`.",
45
45
  "keywords": [
@@ -150,7 +150,7 @@ export interface GuidelineOptions {
150
150
  * How this project is configured, from {@link detectShape}. Omitted, the block
151
151
  * is what it always was — a function of the package list.
152
152
  */
153
- shape?: ProjectShape;
153
+ shape?: ProjectShape | undefined;
154
154
  }
155
155
 
156
156
  /**
package/src/mcp/stdio.ts CHANGED
@@ -27,11 +27,11 @@ import type { JsonRpcResponse } from "./types.ts";
27
27
  export interface StdioOptions {
28
28
  server: McpServer;
29
29
  /** Byte source. Defaults to this process's stdin. */
30
- input?: ReadableStream<Uint8Array>;
30
+ input?: ReadableStream<Uint8Array> | undefined;
31
31
  /** Frame sink. Defaults to this process's stdout. Injected in tests. */
32
- write?: (frame: string) => void;
32
+ write?: ((frame: string) => void) | undefined;
33
33
  /** Diagnostics sink. Defaults to stderr — never stdout. */
34
- log?: (message: string) => void;
34
+ log?: ((message: string) => void) | undefined;
35
35
  }
36
36
 
37
37
  /**
@@ -35,8 +35,8 @@ export interface ProbeRunner {
35
35
 
36
36
  export interface SpawnProbeOptions {
37
37
  /** Where to start looking for the app. Defaults to the server's working directory. */
38
- cwd?: string;
39
- timeoutMs?: number;
38
+ cwd?: string | undefined;
39
+ timeoutMs?: number | undefined;
40
40
  }
41
41
 
42
42
  /**