rustclamp 1.0.0

A minimal synchronous entrypoint for Clamp applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# Changelog: rustclamp

## Unreleased

## 1.0.0 - 2026-10-19

### Added

- `examples/blog` (`cargo run --example blog --features auth,mail`): the
  phase 11 driver on the facade. An admin area behind login and roles, with
  tests for guests, 403 as 404, an app role (`editor`), blocking mid-session
  and role changes; a contact form whose tests pass the mail throttle, the
  outbox and the daily cap.
- Postgres behind `Db` (ADR 0034, step 2c of 3; #121). With the `postgres`
  feature, `DB_CONNECTION=pgsql` opens a pool of Postgres connections instead
  of a SQLite file, and `Db`, `Query`, `Schema`, `Model`, `Db::migrate` and
  transactions run on it unchanged.
  - Config: `DATABASE_URL`, or `DB_HOST`, `DB_PORT` (5432), `DB_DATABASE`,
    `DB_USERNAME`, `DB_PASSWORD`, `DB_SSLMODE` (`disable`, `require`,
    `verify-full`; the default is `verify-full` off localhost),
    `DB_SSLROOTCERT` and `DB_POOL` (10). `db::Settings` gains `url` and `pool`;
    `Settings::from_config` reads them (its `Debug` hides the URL, which
    carries the password), and the web template's
    `app/config/database.rs` now calls it. Without the feature, `pgsql` stops
    the app at startup with an error naming it.
  - Pool: idle clients in a `Vec`, opened on demand up to `DB_POOL`, a 5 s
    wait for a free one and then an error, a broken client dropped instead
    of returned. `Db::read` uses it too; a transaction holds one client.
  - `Db::transaction` is `BEGIN`/`COMMIT`/`ROLLBACK`; `transaction_immediate`
    also takes `pg_advisory_xact_lock`. `Db::migrate` holds
    `pg_advisory_lock` for the run and creates `migrations` with an identity
    `id`. `migrate_user_version` is SQLite only and returns an error.
  - `db::Error::sqlstate()` is set from the server's error, so
    `is_unique_violation()` works on both engines.
  - Tests for `Db`, `Query`, `Schema` and `Model` run on both engines through
    one helper; CI starts `postgres:17-alpine` and sets
    `RUSTCLAMP_TEST_DATABASE_URL`.
  - Not yet (step 3): the framework's own tables (`jobs`, `failed_jobs`,
    `mail_outbox`, `sessions`, `state_history`, auth) are still created with
    SQLite DDL, the queue's `SKIP LOCKED` claim and the mail daily-cap claim.
- Dialect-aware SQL for Postgres (ADR 0034, step 2a of 3; #121). Step 2a
  only generated the SQL; step 2c runs it.
  - `db::Dialect`, `Db::dialect()` and `Connection::dialect()`.
  - `Schema::create`/`::table` render for both dialects (ids as identity
    columns, `BIGINT`, `DOUBLE PRECISION`, `BOOLEAN`, `to_char(now() ..)`
    timestamps); `Table::blob`, `Column::default_bool` and
    `Column::default_now` render per dialect. SQLite output is unchanged.
  - `Query` writes `$N` marks, `RETURNING id` and `OFFSET` for Postgres; new
    `Query::insert_only` for tables without an `id`.
  - `db::Value::Bool`: `bool` binds as `0`/`1` on SQLite and reads from an
    integer or a bool.
  - `db::Error::sqlstate()` and `is_unique_violation()` (SQLite
    `SQLITE_CONSTRAINT_UNIQUE`/`_PRIMARYKEY`, Postgres `23505`).
  - Empty `where_in` writes `1 = 0` instead of `0`, which Postgres rejects.
- `postgres` feature: a std-only Postgres wire client in `db` (protocol 3.0,
  SCRAM-SHA-256, TLS with `require` or `verify-full`, extended queries in text
  format), ADR 0034 step 2b of #121; it adds no crates beyond those `crypto` and
  `mail` already use.

### Changed (breaking)

- `db::Settings` has two more fields, `url` and `pool` (step 2c). Build it
  with `Settings::from_config(config)` instead of a struct literal.
- `Migration::up` and `down` return `db::Sql` instead of `String` (ADR 0034,
  step 2a). `Schema::create`, `::table` and `::drop` return it, so a migration
  built from them only changes its signature: `fn up(&self) -> Sql`
  (`use rustclamp::db::Sql`). Literal SQL becomes `"...".into()`. (The ADR
  names `impl Into<Sql>`, which cannot be called through `&dyn Migration`.)
  A `Schema` result that was inspected as a `String` needs `.to_string()`
  (the SQLite rendering).

- `rustclamp::db` names no engine in its API (ADR 0034, step 1 of 3; #121).
  The `db::sqlite` (rusqlite) re-export is gone; use the facade types:
  - `Db::with`, `Db::read`, `Db::blocking` and `Tx` lend a `db::Connection`
    with `execute`, `execute_batch`, `query_row` and `query` (all rows as a
    `Vec`; `.pop()` replaces `.optional()`). `prepare`/`query_map` become
    `query`, `last_insert_rowid` becomes `INSERT ... RETURNING id`.
  - Parameters are a slice: `[]` becomes `&[]`, `[id]` becomes `&[&id]`, and
    `sqlite::params![..]` becomes `db::params![..]`. `&dyn ToSql` becomes
    `&dyn db::Param` (`where_eq`, `where_in`, `update`, `update_values`,
    `States::transition_in`); `Query::insert` takes `&[&dyn Param]`.
  - `sqlite::Row` becomes `db::Row`, and `row.get::<_, T>(..)` becomes
    `row.get::<T>(..)`. `Model::from_row(row: &db::Row) -> db::Result<Self>`.
  - `sqlite::Result`/`sqlite::Error` become `db::Result`/`db::Error`
    (`DbError::Open`, `Transition::Database` and `queue::Error::Db` carry it).
  - Your own types bind with `impl db::Param` and read with
    `impl db::FromColumn` instead of `ToSql`/`FromSql`.

- `web::App` has a `commands` field (#122). Add `commands: &[]` to an
  `App { .. }` literal that predates it.

- Views use templates instead of `<!--key-->` markers (ADR 0012). Old callers
  still compile but render wrong: markers are left as invisible comments, and
  values passed through `escape` are escaped twice. To migrate:
  - Replace `<!--key-->` with `{{ key }}`, or `{!! key !!}` for HTML you made.
  - Drop `escape(...)` from values passed to `render`, `Request::render` and
    `package_view`.
  - `security_headers` now blocks inline `<script>` and `on*=` handlers: move
    them into `resources/js`, or into `public/` with `vite-ignore`.
  - `<!--status-->`/`<!--reason-->` become `{{ status }}`/`{{ reason }}`;
    `<!--csrf-->` becomes `{!! csrf !!}`, and `<!--old:email-->` becomes
    `{{ old.email }}`. `<!--flash-->`/`<!--errors-->` become
    `@if(flash)…{{ flash }}…@endif` and `@foreach(errors as error)`.

- `Db::transaction` hands `work` a `Tx` instead of `&Connection` (#36). `Tx`
  derefs to `Connection`, so `|sql| sql.execute(..)` and `|c| helper(c)`
  keep compiling; a closure annotated `|c: &Connection|` must drop the
  annotation or take `&Tx`.

- `db::States` records history only after `.with_history()`; without it no
  `state_history` table is created and `history()` returns nothing (#40).
  Add `.with_history()` to a `States` whose history you read (nejctest's
  blog and ladder level 04 do).

### Changed

- Ready for crates.io: `publish = false` is gone and the manifest carries
  homepage, documentation, keywords and categories. Nothing is uploaded until
  rustclamp's `tools/publish.py --execute` runs.
- Without `LOG_CHANNEL` (and no `app/config/logging.rs`), `Log` writes to
  stderr unless the working directory has a `storage/` folder, so a CLI or
  service started elsewhere no longer creates a stray `storage/logs/app.log`
  (#47). Set `LOG_CHANNEL=single` to keep the file anywhere.

- `Uuid::v7` comes from `rustclamp_core::Reference` (ADR 0021), so v7 IDs and
  request references share one generator. The `uuid` feature now pulls in
  `rustclamp-core` (no dependencies of its own); Pico is unchanged.
- Faster view rendering, same output: a render reads and parses each view once,
  so an `@include` inside `@foreach` no longer rereads its file on every
  iteration. `{{ }}` escapes in one pass without copying the value.
- `web::serve` keeps connections open between requests (HTTP/1.1
  keep-alive): up to 1000 requests or 5 s idle per connection. A thread
  never waits on an idle connection while others are queued, and busy
  connections take turns. HTTP/1.0 and `Connection: close` still close.
  `HEAD` responses no longer carry a body.
- A full in-memory session store drops its least recently used tenth at
  once instead of one session per new visitor, so a flood of cookieless
  requests no longer scans the whole store on every request.

### Added

- A `regex:NAME` validation rule (#125, ADR 0033) behind the new optional
  `regex` feature, which adds the `regex` crate (no `perf` features). Patterns
  are registered with `web::Patterns::new(&[(name, pattern)])` in
  `Router::state` and compiled once, so `|` in a pattern is safe; a pattern
  that doesn't compile panics at startup. Matching is linear-time.
- App console commands (#122, ADR 0024): `web::Command` (name, description,
  `fn(&[String], &Config, &Db) -> i32`) listed in `App::commands`, run with
  `cargo run -- NAME ARGS`; `cargo run -- list` shows built-in and app
  commands. The web template keeps them in `app/console/`, and
  `clamp make:command NAME` writes one.
- The web template has an HTTP kernel (#122): `app/http/kernel.rs` holds all
  middleware (`global`, and the `web` and `api` groups that `routes/` use),
  and `GET /` goes to a real controller, `http/controllers/home.rs`.
  `clamp make:controller`, `make:middleware`, `make:request` and `make:model`
  write stubs and add them to the module map in `app/lib.rs`.
- Queue (ADR 0019, #119), the optional `queue` feature (`web`, `db`): `App`
  gains `jobs`, handlers by name (`fn(&Job) -> Result<(), String>`, a panic
  is a failure); `request.queue().dispatch(name, payload)` and
  `dispatch_later` refuse unknown names. At-least-once with
  `QUEUE_RETRY_AFTER` (90 s), `QUEUE_TRIES` (3) with 10/60/300 s backoff, then
  the `failed_jobs` table and an error log. `QUEUE_CONNECTION=database` (a
  `jobs` table, one `UPDATE ... RETURNING` claim) or `redis` (with `redis`;
  one Lua script, `REDIS_PREFIX` required in production). `App::run` starts
  `QUEUE_WORKERS` threads (1, or 0 in production); the argument `queue:work`
  runs only workers. Tests call `app.queue(..).work_once(now)`.
- Scheduled tasks (ADR 0035, #120), the optional `schedule` feature (`web`,
  `db`): `App` gains `schedule: &'static [Task]`, each a name, a UTC `At`
  (`EveryMinutes(n)`, `Hourly`, `Daily`, `Weekly`) and a
  `fn(&Config, &Db) -> Result<(), String>` (a panic is a failure). The
  built-in `schedule:run` runs the tasks due this minute, for cron or a
  systemd timer; `App::run` also runs them on a thread when `SCHEDULE_THREAD`
  is true (default outside production). A `schedule_runs` table claims each
  minute with one `UPDATE`, so a task runs once per minute across processes
  and never overlaps itself (`SCHEDULE_STALE_AFTER`, 3600 s); missed minutes
  are skipped. Tests call `app.schedule(..).run_due(now)`.
  `rustclamp::time` gains `hour_minute` and `weekday`.
- `queue:retry ID|all` and `queue:forget ID|all` (#136): built-in commands
  with the `queue` feature, and `Queue::retry` / `Queue::forget`. A retried
  job goes back to the `jobs` table or the Redis list with its attempts reset.
  An unknown ID exits 1.
- Pagination and route binding (#118): `Query::offset`, `Query::paginate(page,
  per_page, map)` returning a `Page` (`items`, `total`, `last_page`,
  `previous`, `next`; a view value with `web`), `Model::find_public` and
  `Request::model::<M>(param)` to load a row by its `public_id` route
  parameter. The `Model` docs show `has_many`, `belongs_to` and eager loading
  with the existing builder.
- Validation rules (#117): `numeric`, `date` (`YYYY-MM-DD`), `in:a,b`,
  `confirmed`, and with `db` `unique:table[,column[,except_id]]` against the
  `Db` in `Router::state`. `Request::validate_with(rules, messages)` takes
  custom messages keyed `field.rule`.
- `clamp init NAME --profile cli,service,worker` (ADR 0032, #114): flat,
  combinable profiles in one binary. Each writes its own `src/` file and
  facade features; `main.rs` runs the first, or `serve` / `work`.
- Metrics and tracing (ADR 0030, #108): `Router::metrics(path, registry)`
  (`web` + `metrics`) serves a `metrics::Registry` and counts requests, `5xx`
  and time into it; `examples/09-observability` shows rustclamp-http's
  `with_metrics` and one correlation id from the API's request to the
  Worker's handler.
- `auth` bearer API tokens (ADR 0023, proposed, #33): `Auth::issue_token` /
  `revoke_token`, the `bearer` middleware and `token_role` guard, and
  `Request::principal()`. Tokens are stored as SHA-256; the app owns the
  `api_tokens` table. Username-only accounts are designed in the ADR, not
  built.
- `rustclamp::error`: the `ExitError` trait (`exit_code()`, `problem()`) with
  a `Problem` in the shape of `HttpError`, implemented for `ConfigError`,
  `DbError` and `ServeError`, and `error::run` / `run_json`, which print the
  error and exit with its code (#44). Kernel errors are not covered yet.

- JSON APIs on the std server (#28, #29, #30): the router answers `405` with
  `Allow` for a path that exists under other methods (a `HEAD` on a `GET`
  route is now `405`, not `404`); `Router::json_errors` renders `404`, `405`,
  `413` and `500` as `application/problem+json` (`web::problem`, the shape of
  `HttpError`); `json_status` and a public `reason`;
  `Router::body_limit` (router or group) decides `413` from the headers, so
  middleware runs first and the body is never read (`Request::body_too_large`);
  `Request::with_extension`/`extension` hand typed values from middleware to
  handlers; `{id:u64}` route segments and `Request::param_as`.

- `rustclamp::cache` (optional `cache` feature): a bounded in-process cache with
  LRU eviction, optional TTL, `get_or_insert_with` and invalidation.
- `rustclamp::metrics` (optional `metrics` feature): counters and gauges with a
  Prometheus text `render()`; the app mounts its own `/metrics` route.
- `Router::reveal_forbidden` answers `403` as `403`, for APIs whose clients
  already know the resource; by default a `403` is still the `404` page
  (#27).

- `web::try_serve` returns a `ServeError` (bad `PORT`/`WEB_THREADS`, or no
  port to bind) instead of panicking or exiting, and `web::serve_on` serves
  a listener the app bound itself; `serve` behaves as before (#31).

- `web::serve_until` with a `web::Shutdown` handle drains on `stop()`: no new
  connections, requests already sent are answered with `Connection: close`,
  idle kept-alive connections close, and it waits up to a grace period for
  the rest (#31).

- `Db::try_connect_with(settings, Journal::Keep)` opens a file database
  without switching it to WAL, so a rollback-journal file's header is never
  rewritten; `try_connect` still uses WAL (#35).

- `clamp init NAME --vue` / `--react`: the `--web` app with Vue or React
  components mounted on server-rendered pages (`data-component` +
  `data-props`, loaded per page); CI now installs and builds all three web
  kits.

- `rustclamp::time` (feature `time`, included by `log` and `db`):
  `format_rfc3339`/`parse_rfc3339` (offsets, fractions), `format_date`/
  `parse_date` and `add_days` on `SystemTime`, no time crate (#42).

- `States::transition_in` moves a row inside the caller's transaction, at a
  time the app passes (its clock), setting extra columns such as
  `completed_at` too; `db::timestamp::format` writes a `SystemTime` the way
  `CURRENT_TIMESTAMP` does (#40).

- `Db::try_connect` returns a `DbError` instead of panicking and does not
  create the database's folder, so CLIs and services exit with their own
  code and leave a bad file untouched; `connect` behaves as before (#35).

- `crypto::random_bytes`, `crypto::constant_time_eq`, `crypto::sha1` (for
  protocols such as the WebSocket handshake, not for security) and
  `crypto::base64_encode`/`base64_decode` (#43).

- `Throttle::capacity` sets how many clients are tracked at once (10,000 by
  default). At the cap a live window is dropped only when none has expired;
  size it above your peak clients per window (#32).

- `Db::transaction_immediate`: `BEGIN IMMEDIATE`, so a read-then-write
  cannot fail halfway with "database is locked" (#36).
- `Tx::table`: the query builder inside a transaction. Calling `db.table(..)`
  there waited forever on the lock the transaction holds (#36).

- Every web request has a `Reference` (UUIDv7, ADR 0021): `Request::reference()`,
  sent back in `X-Request-Id`, appended as `ref=<reference>` to each log line
  written while the request is handled, passed to error views as
  `{{ reference }}` and shown on the built-in 5xx page. A client's own
  `X-Request-Id` is never used.

- Account flows (ADR 0016), with `auth` + `mail`: `Auth::register` (no
  enumeration, no auto-login), email verification (24 h signed link,
  `auth::verified` guard, `resend_verification`), `forgot`/`open_reset`/
  `reset` (60 min link, single use, token moved into the session), and
  `confirm` with the `password_confirmed` guard (3 h). Links come from
  `APP_URL` and are signed with `APP_KEY`; both are required with `auth` +
  `mail`. A password change ends every session (fingerprint check in
  `authenticate`). Auth mail: 3/hour per client, 1 per 10 minutes per
  address. `User::verified`, `Auth::mark_verified`; `user:create` accounts
  are verified. Apps need `email_verified_at` on `users`.
- Mail (ADR 0015), the optional `mail` feature: SMTP with STARTTLS over
  rustls (ring provider; never a plaintext fallback, AUTH only over TLS),
  UTF-8 bodies and RFC 2047 headers, a `log` mailer. `Mailer::send` queues
  in a `mail_outbox` table; a thread from `web::App::run` delivers it under
  an atomic per-UTC-day cap (`MAIL_DAILY_CAP`, default 100) with retries.
  `Recipient` is built only from config or an `auth::User`. `App` takes
  `mail`; `Request::mailer`.
- Login and roles (ADR 0013), the optional `auth` feature: `web::auth::Auth`
  (`attempt`, `create_user`, `sync_role`, `at_least`), `authenticate`,
  `required` and `role("admin")` middleware, `allows` with the before rule
  (`blocked` refused, `super-admin` allowed), `Request::user`/`auth`,
  `Session::invalidate` for logout. One `role` column per user. Logins are
  throttled per email and address; unknown email and wrong password are
  indistinguishable. `App` takes `roles` and runs `user:create`. The router
  answers every `403` as `404`, logged with `http_status_code=403`, with the
  same body and headers as a real 404. The login throttle
  (`LOGIN_PER_MINUTE`, default 5) honours `TRUST_PROXY`.
  `testing::Client::with_header`.
  `testing::Client::cookie`.
- Security: `security_headers` also sends a strict Content-Security-Policy
  (`web::CONTENT_SECURITY_POLICY`; scripts only from the app's files, a
  route's own policy wins), HSTS and a Permissions-Policy.
  `Session::regenerate()` moves a session to a new ID and CSRF token (call it
  on login). A `url` validation rule accepts only `http`/`https` links.
- Less app code: handlers may return `web::Result` and use `?` (the error is
  logged, the visitor gets `500`); `Sessions::from_config` reads
  `SESSION_MINUTES`/`SESSION_SECURE` and refuses an insecure cookie in
  production (`Config::is_production`); `web::markdown::to_html` behind the
  new `markdown` feature (pulldown-cmark) escapes raw HTML and unsafe link
  schemes; `web::App` runs a web app's startup; `Router::up` health check;
  `Request::flash`; `db::timestamp::{date, iso8601}`. The web template uses
  them and drops its health controller.
- `web::App` owns an app's wiring: it holds the routes too, opens and
  migrates the database, shares it and the disks with handlers and adds
  `security_headers`. `main.rs` is `my_site::app().run()`; tests use
  `app().test(env)` (in-memory, migrated, seeded). `web::testing::Client`
  browses a router like a visitor: cookie jar, CSRF token from the last page,
  `see`/`location`/`body_text` on `Response`. `Throttle::from_config(config,
  key, default)` reads the limit and `TRUST_PROXY`; `markdown::front_matter`
  splits `key: value` headers from Markdown.
- `web::serve` answers on a fixed pool of `WEB_THREADS` threads (default 32)
  instead of one request at a time; accepted connections queue four per
  thread, so a flood cannot grow threads or memory without bound.
- Web apps keep config in one file per area, like Laravel's `config/`:
  `app/config/app.rs`, `database.rs` and `logging.rs` name every key and its
  default. The framework takes them as `db::Settings` (`Db::connect`) and
  `log::Settings` (`Logger::new`); `Log::init` installs the app's logger at
  startup. `Db::open` and `Logger::from_config` still read `.env` directly.
- `rustclamp::storage` (`storage` feature, in `web`), like Laravel's
  `Storage`: named disks with `put`, `get`, `exists`, `delete` and `url`,
  refusing paths that leave the disk. Web apps configure `local`
  (`storage/app/private`) and `public` (`storage/app/public`, served at
  `/storage`) in `app/config/filesystems.rs`, reach them with
  `request.storage()`, and link `public/storage` at startup.
- File uploads: `multipart/form-data` forms, up to `web::MAX_UPLOAD` (10 MiB;
  other bodies keep the 1 MiB `MAX_BODY`). `Request::file`/`files` return
  `UploadedFile`s; `Request::form` and validation read the form's text fields,
  CSRF token included. `UploadedFile::store(disk, folder, allowed)` saves under
  a new UUIDv7 name and keeps only the listed extensions, so an upload cannot
  put `.html` or `.svg` on a public disk. Validation rules for uploads, as in
  Laravel: `file`, `image` (JPEG, PNG, GIF or WebP, checked by content; no
  SVG), `mimes:pdf,txt`, and `min`/`max` in kilobytes on file fields.
- Less boilerplate in apps: `build::database("app/database")` replaces the
  three `build::migrations`/`seeders`/`states` calls (still available) and
  writes one `database.rs`; migrations it lists are named after their file,
  so `fn name() { migration_name(file!()) }` can go (a hand-listed migration
  without `name()` panics rather than guessing). `log::request_log` is the
  request-logging middleware the web template used to carry in
  `app/http/middleware/`.
- `#[derive(Model)]` (ADR 0014) with `#[model(table = "posts")]` writes
  `Model::from_row`, reading each field from the column of the same name.
  It comes from the new `rustclamp-macros` proc-macro crate, pulled in by the
  `db` feature and re-exported as `rustclamp::db::Model`.
- Views are templates (ADR 0012): a std-only Blade subset rendered at request
  time from the Vite-built HTML. `{{ name }}` escapes, `{!! name !!}` does
  not; `@if`/`@else`, `@foreach`, `@extends`/`@section`/`@yield`,
  `@include`, with named values for components (`post: featured`). Data is
  `web::Value`, built from anything `web::ToValue`
  (text, numbers, bools, lists, options, and your models). `render`,
  `Request::render` and `package_view` take `&[(&str, &dyn ToValue)]`
  instead of `<!--key-->` slots; `Request::render` passes `csrf`, `flash`,
  `errors` and `old`; error views get `status` and `reason`. An unknown name
  or broken view is logged and answers `500`. `public/build/views/` is no
  longer served raw.
- The CLI is 0.5.0 (release tag `clamp-v0.5.0`): the framework with account
  flows (register, verify email, reset and confirm password; ADR 0016) as
  well as mail (ADR 0015).
- The CLI is 0.4.0 (release tag `clamp-v0.4.0`): `clamp init --web` apps
  get `app/config/{app,database,filesystems,logging}.rs`, storage disks and
  uploads, `web::App` wiring, `build::database` and `#[derive(Model)]`.
- The CLI is 0.3.0 (release tag `clamp-v0.3.0`): database, crypto and `.env`
  commands, and `clamp init --web` apps with `db` and `crypto`.

- Web packages: `web::Package`, `Router::package` (package middleware stays on
  package routes), `web::package_view` with app overrides under
  `views/vendor/{package}/`, and `clamp init NAME --package` (ADR 0008).
- `rustclamp::db` (optional `db` feature): SQLite via rusqlite with bundled
  SQLite, opened from `DB_CONNECTION`/`DB_DATABASE`. Laravel-style
  `Migration` structs with `up`/`down`, batches and `Db::rollback`;
  `Seeder` structs and `Db::seed`; a `Schema` builder (`create`, `table`,
  `drop`); a `db.table(...)` query builder; `db::command` for `migrate`,
  `migrate:rollback` and `db:seed`.
- Web: `Router::state` and `Request::state` share app values with handlers
  (`request.db()` with `db`); `Request::validate` with `required`, `min`,
  `max`, `email` and `integer` rules, and `Invalid::back` redirecting with
  errors and old input; `Session::flash`; `Request::render` passes the
  CSRF field, flash, errors and old input to the view.
- `Sessions::database(db)`: sessions in a SQLite `sessions` table, so they
  survive restarts; expired rows are pruned.
- `rustclamp::build::states`: every file in `app/database/states/` becomes
  `database::states::*`; the web template's `build.rs` calls it.
- `db::States`: allowed transitions for a status column, each move recorded
  in `state_history` with who made it; `history`, `can`, `allowed_from`.
- `clamp make:migration NAME` (dated stub; `create_x` and `add_y_to_x` are
  filled in) and `clamp make:seeder NAME`; `clamp migrate`,
  `migrate:rollback`, `migrate:status` and `db:seed` run the app's
  `db::command`, which gains `migrate:status`.
- Log channels as in Laravel: `single` (`file` still works), `daily`
  (`app-YYYY-MM-DD.log`, newest `LOG_DAILY_DAYS` kept), `stderr` and `stack`
  (`LOG_STACK=daily,stderr`); `Logger::daily`.
- `rustclamp::uuid` (optional std-only `uuid` feature, included by `db`):
  `Uuid::v7` (time-ordered, strictly increasing per process) and `Uuid::v4`,
  `parse`, SQLite mapping as text, and `Schema`'s `table.public_id()`.
- `rustclamp::crypto` (optional `crypto` feature, RustCrypto): Argon2id
  `Hash::make`/`check`, `Key` (`APP_KEY`, `base64:`) and `Crypt`
  (XChaCha20-Poly1305), `sha256`, `hmac_sha256` and constant-time
  `hmac_verify` (ADR 0010).
- `clamp key:generate`, `clamp env:encrypt` and `clamp env:decrypt`
  (`--key=`, `--env=`, `--force`, `CLAMP_ENV_KEY`); `clamp init --web` turns on
  `crypto` and lists `APP_KEY` in `.env.example`.
- `Db::transaction`: kept on `Ok`, undone on `Err`, and nested as a
  savepoint inside another transaction; migrations, rollbacks and state
  transitions use it, so they work inside a seeder. `Seeder::run` returns
  `SeedError` (any error), so `?` works on transitions and I/O too.
- `db::Model`: a table as a struct (`TABLE`, `from_row`) with `query`,
  `all` and `find`.
- `rustclamp::build` (optional `build` feature): `build.rs` discovery of
  `app/database/migrations/` and `app/database/seeders/`. `clamp init NAME
  --web` ships the `build.rs`, so a new file there is all it takes (ADR 0009).
- `clamp --version`; the CLI is 0.2.0 (release tag `clamp-v0.2.0`).
- Phase 0 package scaffold and development checks.
- Synchronous `Clamp::run` entrypoint and a prelude exporting `Clamp`.
- Pico example, plain Rust comparison, isolated dependency regression checks,
  ownership/error/panic tests, and reproducible cost and allocation measurements.
- Expanded the project README, architecture diagrams, verification guidance,
  measured comparisons, evidence, and release documentation.
- Updated isolated checks to include declared internal dependency closures and
  distinguish package-list validation from registry-dependent archive verification.
- Added colored test/benchmark terminal verdicts, per-benchmark intent text, and
  Phase 2 qualifier, cardinality, and composition-edit measurements.
- Added facade-free Clock and module consumer examples, request-state and
  non-Send borrowing tests, and example checks to the coordinated runner.
- Recorded Phase 2 example build reports and synthetic 1/5/20-module measurements.
- Added a target-owned CLI contribution example with qualified command sets,
  structured conflicts, orphan validation, and assembly/runtime measurements.
- Started process projection with stable application/process/execution
  identities, root-based reachability, and a CLI/Worker example sharing Clock.
- Completed the process-composition prototype with provider defaults and
  replacement provenance, projection-scoped configuration, freeze/inspection,
  process isolation assertions, build-target comparison, and scale evidence.
- Started Phase 5 with a synchronous fake-resource lifecycle proof covering
  dependency order, readiness, failure cleanup, deadlines, health, and shutdown.
- Added Phase 5 lifecycle evidence with fake-clock metrics and qualified
  build-footprint comparisons against the Phase 4 process example.
- Documented Core's opt-in synchronous lifecycle participation contracts in
  the lifecycle example and Phase 5 evidence.
- Added an external fake Database owner path that preserves caller ownership
  through process shutdown, with managed/external behavior and fake-clock
  comparisons in the lifecycle docs.
- Added a coexisting Worker/Reporter projection proof: application-owned fake
  Database state is shared and survives process shutdown, while process state
  remains isolated.
- Added an execution-scoped fake resource handle that can be constructed only
  for roots in the frozen process projection and stops independently.
- Added lifecycle outcome inspection for dependency edges, actual resource
  owners, participant phases, and cleanup state, with example output.
- Added optional Axum/Tower HTTP route integration, bounded streaming and
  graceful listener shutdown, plus qualified SQLx PostgreSQL pools and explicit
  migrations.
- Added the shared Users domain, console and HTTP adapters, optional
  transaction-scoped PostgreSQL adapter, isolated dependency checks, and Phase 6
  architecture/evidence documentation.
- Added release workload comparisons for direct Axum and SQLx paths, including
  measurement-only allocation instrumentation and dependency/build/binary reports.
- Added transport-neutral message envelopes, bounded in-memory and JetStream
  messaging, process-separated email workers, classified retries, deadlines,
  dead-letter handling, and tracing correlation.
- Added the PostgreSQL transactional outbox/inbox CreateOrder proof, payment
  uncertainty and compensation, process/ownership inspection, and live crash-
  window evidence against PostgreSQL and JetStream.
- Added the dependency-light Scheduler target with controlled-clock, overlap,
  misfire, admission, and drain checks, plus Phase 7 throughput and dependency
  measurements.
- Completed Phase 8 deterministic loop/device proofs, a Kernel-free no_std +
  alloc consumer, portability findings, cross-domain dependency-absence checks,
  and 1/5/20/50/100/500-module plus loop/footprint measurements.
- Added a Phase 9 RustClamp HTTP site-hosting example and compared its responses
  byte-for-byte with both Python preview sites.
- `web::error(status)` renders the app's `errors/{status}` or `errors/{N}xx`
  view, else a built-in page; router 404s, missing files and the unbuilt-frontend
  503 use it, and responses carry proper reason phrases.
- `web` parses full requests (headers, query, `Content-Length` body, peer) with
  size limits and a read timeout, adds response headers, `redirect`, and
  `Router::middleware` / `Router::group`; a panicking handler answers 500.
- `web::Throttle` / `throttle` fixed-window rate limiting (429 with
  `Retry-After`, opt-in `X-Forwarded-For`), `web::Sessions` server-side sessions
  and `web::csrf` (419); tokens come from `/dev/urandom`, keeping the facade
  dependency-free.
- `rustclamp::config` (`config` feature, included by `web`): `.env` plus
  environment, typed getters, values hidden from `Debug`.
- `web` routes accept `{name}` segments (`/blog/{slug}`), read with
  `Request::param`.
- `rustclamp::log` (`log` feature, included by `web`): `Log::debug` through
  `Log::emergency` (PSR-3 levels) to `storage/logs/app.log` or stderr, set by
  `LOG_LEVEL` (or `silent`), `LOG_CHANNEL` and `APP_ENV`; the web server logs
  handler panics.
- `web::render` (fills `<!--key-->` markers in a built view), `web::escape` and
  the `web::security_headers` middleware; `Throttle::per_minute`, and
  `Throttle::trust_forwarded` now takes a `bool`.
- `clamp init --web` uses the my-site layout: `app/routes/{web,api}.rs`,
  `app/resources/`, `.env.example`, with `/storage` and `.env` git-ignored.
- `web::error` fills `<!--status-->` and `<!--reason-->` in the app's error
  views. `clamp init --web` adds `errors/4xx.html` and `errors/5xx.html`, a
  Tailwind-only welcome page, `config/app.rs`, a base
  `http/controllers/controller.rs`, a health controller, a `request_log`
  middleware, and `http/requests/` and `models/` folders.

### Fixed

- `Db::connect` no longer fails with "database is locked" when several
  processes open a new database at once (#34): `busy_timeout` is set before
  the switch to WAL, and the switch retries until the same 5 s deadline.
- A panic inside `Db::transaction` rolls it back. Before, the savepoint stayed
  open and every later transaction nested inside it and never committed.