slotlock 0.6.2 → 0.7.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/CONTRIBUTING.md +12 -5
  3. package/README.md +50 -1544
  4. package/SECURITY.md +5 -2
  5. package/SPEC.md +1 -1
  6. package/agent-server.d.ts +160 -8
  7. package/agent-server.js +560 -49
  8. package/agent-store-backend.d.ts +39 -7
  9. package/agent-store-backend.js +1439 -104
  10. package/api-keys.d.ts +69 -3
  11. package/api-keys.js +218 -29
  12. package/application-api.d.ts +59 -6
  13. package/application-api.js +842 -39
  14. package/approvals.d.ts +88 -0
  15. package/approvals.js +293 -0
  16. package/assistant.d.ts +58 -27
  17. package/assistant.js +151 -53
  18. package/billing.d.ts +114 -0
  19. package/billing.js +258 -0
  20. package/booking-rules.d.ts +30 -41
  21. package/booking-rules.js +17 -25
  22. package/change-watch.d.ts +1 -0
  23. package/change-watch.js +1 -1
  24. package/cli.js +3 -4
  25. package/config.d.ts +42 -6
  26. package/config.js +114 -7
  27. package/dashboard-icon.d.ts +1 -0
  28. package/dashboard-icon.js +5 -0
  29. package/dashboard.d.ts +203 -7
  30. package/dashboard.js +1558 -64
  31. package/ddl.d.ts +67 -35
  32. package/ddl.js +2732 -162
  33. package/engine.d.ts +6 -9
  34. package/engine.js +5 -9
  35. package/event-feed.d.ts +53 -35
  36. package/event-feed.js +201 -64
  37. package/event-schema.js +6 -9
  38. package/hooks.d.ts +2 -4
  39. package/hooks.js +2 -4
  40. package/ical-worker-thread.js +3 -4
  41. package/ical-worker.d.ts +8 -10
  42. package/ical-worker.js +7 -9
  43. package/index.d.ts +15 -4
  44. package/index.js +8 -2
  45. package/interop.js +1 -4
  46. package/maintenance.d.ts +24 -10
  47. package/maintenance.js +59 -28
  48. package/mcp-app.d.ts +7 -9
  49. package/mcp-app.js +7 -9
  50. package/mcp-modern.d.ts +14 -16
  51. package/mcp-modern.js +23 -27
  52. package/metrics.d.ts +41 -0
  53. package/metrics.js +22 -1
  54. package/node-server.d.ts +9 -9
  55. package/node-server.js +25 -34
  56. package/openapi.json +14675 -5957
  57. package/otel.d.ts +86 -0
  58. package/otel.js +273 -0
  59. package/overage.d.ts +33 -0
  60. package/overage.js +115 -0
  61. package/package-version.js +2 -4
  62. package/package.json +1 -1
  63. package/partner-billing.d.ts +81 -0
  64. package/partner-billing.js +333 -0
  65. package/partner-names.d.ts +8 -0
  66. package/partner-names.js +10 -0
  67. package/partners.d.ts +147 -0
  68. package/partners.js +375 -0
  69. package/plans.d.ts +92 -0
  70. package/plans.js +115 -0
  71. package/quotas.d.ts +108 -0
  72. package/quotas.js +258 -0
  73. package/rate-limit.d.ts +27 -2
  74. package/rate-limit.js +106 -23
  75. package/resource-profile.d.ts +135 -0
  76. package/resource-profile.js +263 -0
  77. package/rest-api.d.ts +15 -14
  78. package/rest-api.js +79 -15
  79. package/rules.d.ts +6 -7
  80. package/rules.js +30 -42
  81. package/sandbox.d.ts +70 -0
  82. package/sandbox.js +295 -0
  83. package/self-host.d.ts +46 -4
  84. package/self-host.js +990 -41
  85. package/slotlock.manifest.json +1 -1
  86. package/store.d.ts +482 -165
  87. package/store.js +1460 -484
  88. package/sync.d.ts +44 -59
  89. package/sync.js +325 -469
  90. package/teams.d.ts +25 -0
  91. package/teams.js +63 -0
  92. package/timezone.d.ts +25 -33
  93. package/timezone.js +50 -53
  94. package/types.d.ts +130 -57
  95. package/usage.d.ts +106 -0
  96. package/usage.js +238 -0
  97. package/webhooks.d.ts +22 -25
  98. package/webhooks.js +35 -40
  99. package/weekly-hours.d.ts +3 -0
  100. package/weekly-hours.js +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,498 @@
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
4
4
  versioning.
5
5
 
6
+ ## 0.7.0 - 2026-11-10
7
+
8
+ ### Added
9
+
10
+ - The sandbox (RFC 0004 A6), in a test environment only: seed a scenario (car rental, clinic,
11
+ desks) into an empty one, race agents for one slot and report who booked, who was refused and
12
+ how long each answer took, fast-forward (holds, waitlist offers and approvals that would lapse
13
+ within some minutes lapse now, then maintenance reaps them), and reset (everything but its keys,
14
+ webhooks, plan and usage). `slotlock sandbox …`, four admin operations (`/v1/sandbox/*`) and the
15
+ dashboard's Test overview. Each refuses a live tenant, and so does the database's
16
+ `slotlock.reset_test_tenant`, which acts only in the caller's own test tenant.
17
+ - Contention at scale (RFC 0004 A7): a CI test races 100 REST clients for one slot (one booking, 99
18
+ `overlap` refusals with alternatives, no 5xx, p99 under a second), and the benchmark runs three
19
+ write workloads against Slotlock's own schema as a new column. A refusal's alternatives now come
20
+ from one read through the backend's optional `findNextSlots`, as do
21
+ `slotlock_find_available_slots`' slots, and time zone names resolve once a process.
22
+ - Per-key limits (RFC 0004 A5): a key can be held below the server's limits, never above them:
23
+ its live holds and the hours they hold, its requests a minute (a bucket of its own, `key_limit`),
24
+ and `requires_approval`, which makes every booking it asks for wait for a person (policy
25
+ `api-key`). Set with `slotlock key create`/`key limits`, the dashboard's key form and Limits
26
+ action, or the partner API's `limits`; kept through rotation, erased with the tenant. The
27
+ principal carries them as `limits`. `slotlock.api_key_limits` is owner-only behind three new
28
+ functions: run `slotlock migrate` (or `grantApplicationRole`) so the serving role may call them;
29
+ until then keys authenticate without limits.
30
+ - Approval policies (RFC 0004 A4): an admin names which agent bookings a person approves first, or
31
+ which are refused, by resource, pool, location or group, narrowed by length, time outside the
32
+ resource's hours and kind (single, pool, bundle, move). A covered booking is tried as a dry run,
33
+ then its exact time is held for the approval and the agent is answered `409 approval_pending`;
34
+ a grant (the dashboard's Approvals page or `slotlock_decide_approval`) books it as the agent
35
+ asked, in the transaction that frees the hold, and the agent's retry with the same key answers
36
+ the booking. Declined and expired approvals free their time. Each policy sets its own wait,
37
+ bounded by the booking's start less `lead_minutes` and 30 days. Six application operations
38
+ (`/v1/approval-policies`, `/v1/approvals`), the feed's `approval.*` changes, and
39
+ `approvals_expired` in maintenance's answer.
40
+ - Alternatives with a refusal (RFC 0004 A1): an agent write refused with `overlap`, `pool_full` or
41
+ a booking rule (other than `booking_rule_uncertain`) carries `alternatives`, up to three free
42
+ times of the same length from the time asked for, within seven days, found by
43
+ `slotlock_find_available_slots`' own search: on the same resource, on a bundle's resources
44
+ together, or on any member of the pool. `slotlock_hold_slots` gives each candidate refused for its
45
+ time its own. On MCP, A2A and REST alike; never a hold.
46
+ - Dry runs (RFC 0004 A3): every agent and application write takes `dry_run: true`. The write
47
+ runs with every check, the database's constraints included, then rolls back: nothing is stored,
48
+ recorded or sent, and the answer is what it would have been (`dry_run: true`) or the refusal,
49
+ alternatives included. It is never put to a person to confirm and is charged as a read (on REST,
50
+ with `dry_run=true` in the query string). Erasure, maintenance and the partner API answer
51
+ `dry_run_not_supported`.
52
+ - Three named booking tools (RFC 0004 A2): `slotlock_confirm_slot` books a held time exactly
53
+ (`POST /v1/slot-holds/{hold_id}/confirm`), `slotlock_reschedule` moves an event
54
+ (`POST /v1/events/{event_id}/reschedule`) and `slotlock_cancel` cancels one
55
+ (`POST /v1/events/{event_id}/cancel`), each with flat arguments and an idempotency key it derives
56
+ when none is given. The tool descriptions say what each refuses and why, in plain words, and the
57
+ MCP instructions walk an agent through find, hold, confirm, reschedule and cancel.
58
+ - Hosted plans: `slotlock.tenant_plans` (owner-only, read and set through definer functions) caps a
59
+ tenant's resources, active API keys and webhook endpoints. Free allows 10, 10 and 2; Developer
60
+ 100, 100 and 10. A tenant with no plan, as on every self-hosted server, keeps the server's limits.
61
+ Erasure removes the plan. `readSlotlockTenantPlan` and `setSlotlockTenantPlan` in `plans.ts`.
62
+ Adding a resource or webhook endpoint now reads the plan through `slotlock.tenant_plan(text)`:
63
+ `slotlock migrate` grants it to the serving role; a library user runs `grantApplicationRole`
64
+ again, and a role without it is refused `application_role_not_granted`.
65
+ - Stripe billing (`billing.ts`) and the dashboard's Plan page, on the Worker with
66
+ `STRIPE_SECRET_KEY` and the Developer price ids: Upgrade opens a Stripe Checkout with Managed
67
+ Payments, Manage billing the customer portal, and a signed webhook at `/stripe/webhook` sets the
68
+ tenant's plan from the subscription Stripe holds.
69
+ - The dashboard serves the Slotlock mark as its favicon.
70
+ - A default plan in the database (`slotlock.plan_defaults`, owner-only): every tenant without a plan
71
+ of its own is on it from its date, through `slotlock.tenant_plan(text)`, so `slotlock serve` and
72
+ the dashboard Worker hold a tenant to the same limits whichever one a resource, key or webhook
73
+ endpoint is added through. Before the date, a new tenant (no API key, nothing in its feed, no
74
+ webhook) is put on it before it adds its first key, resource or endpoint
75
+ (`slotlock.enrol_tenant_plan(text)`); existing tenants keep their limits until the date. Set by
76
+ the schema owner with `slotlock plan default free --from 2026-12-09` (`none` removes it);
77
+ `slotlock plan status` prints it and `SLOTLOCK_TENANT`'s own plan. `readSlotlockPlanDefault`,
78
+ `setSlotlockPlanDefault` and `enrolSlotlockTenantPlan` in `plans.ts`. Run `slotlock migrate`.
79
+ - `countResources` on the tenant store: how many resources a tenant has, archived ones included, as
80
+ a resource limit counts them.
81
+ - Usage metering, off unless `SLOTLOCK_USAGE=on` (RFC 0003 part 3). `slotlock.usage_daily` counts
82
+ each tenant's use by UTC day, API key and surface: every operation that reaches the backend (MCP,
83
+ A2A, REST, the application API, the dashboard assistant), reported by the agent server's new
84
+ `onUsage` and the dispatch's, counted in memory by `createSlotlockUsageMeter` and recorded every
85
+ 15 seconds and on shutdown through `slotlock.record_usage`, once per batch id however often it is
86
+ sent (a batch it refuses for good, such as a day it no longer takes after a long outage, is
87
+ dropped and reported rather than held in front of the batches after it); and the bookings (`reservation.created`, `event.created`, `hold.confirmed`) and holds
88
+ (`hold.created`) committed on the tenant's feed, rolled up by maintenance
89
+ (`slotlock.roll_up_usage`, the store's `rollUpUsage`) before the feed is pruned, in the feed's
90
+ committed order, so nothing is counted twice and a rolled-back write never is; maintenance's own
91
+ changes are not counted, nor writes from before the release. Read with `slotlock.tenant_usage`
92
+ (`readSlotlockTenantUsage`). Kept 400 days, erased with the tenant; recording skips a tenant with
93
+ no key and no feed, so counts flushed after an erasure recreate nothing. The dashboard Worker
94
+ takes the same setting for the assistant's operations, recording each request's when it ends.
95
+ `serve` refuses to start with it on against a schema without the functions. The privacy notice
96
+ lists the counts. Run `slotlock migrate`.
97
+ - `GET /v1/usage` (`slotlock_get_usage`, admin scope): a tenant's usage from one UTC day to another,
98
+ by default the first of this month to today, at most 400 days: totals, then by day, by API key and
99
+ by surface, bounded whatever the volume; 400 `invalid_usage_range` for a range that is reversed,
100
+ too long or ends after today. In the OpenAPI document and the five clients. The store's
101
+ `readUsage` and usage.ts's `summarizeSlotlockUsage` back it.
102
+ - Usage also counts webhook deliveries an endpoint accepted (`slotlock serve`'s delivery pass), messages
103
+ sent to the dashboard assistant that it answered (the assistant's new `onMessage`), and each day's
104
+ resource count, archived ones included (maintenance's roll-up; a count, so `GET /v1/usage` totals
105
+ the last day's rather than summing). `GET /v1/usage` and the Plan page carry them, and the
106
+ privacy notice lists them; `slotlock.record_usage` takes the metrics a server counts and refuses
107
+ the ones the database does. The metric list's CHECK is replaced only when the list changes.
108
+ - Every answer of the agent server carries `Slotlock-Request-Id`, a UUID of its own; with
109
+ `onUsage` (`SLOTLOCK_USAGE=on`) an answer whose operations ran before it was sent also carries
110
+ `Slotlock-Usage: operations=<n>`, the operations counted for it, so a gateway can count on its side
111
+ and compare. The per-second `RateLimit` and `RateLimit-Policy` fields are as before.
112
+ - The dashboard's Plan page shows this month's operations, bookings and holds, and who used them by
113
+ API key, when the deployment meters (the dashboard's `usage` option, set from `SLOTLOCK_USAGE` by
114
+ `slotlock serve` and the Worker).
115
+ - A resource's profile (RFC 0003 G1, `resource-profile.ts`), each part optional: a `name` shown to
116
+ agents and people instead of the reference, a `description` (2,000 characters), a `kind`
117
+ (`person`, `room`, `desk`, `space`, `vehicle`, `machine`, `equipment`, `service`, `other`: a hint,
118
+ never a rule), a `capacity` (1-100,000, informational), up to 20 `labels` and `attributes` (up to
119
+ 50 keys of text, numbers and booleans, 4 KiB). New columns on `slotlock.resources` with constant
120
+ defaults, so no row is rewritten, bounded by a CHECK the migration validates after it commits, and
121
+ a GIN index on labels. `createResource` takes `profile` (an existing reference keeps the fields
122
+ not given); the store's new `updateResourceProfile` changes the fields given, `null` clearing a
123
+ name, description or capacity; `listResources` narrows by `kind` and `labels` (every one).
124
+ `POST /v1/resources` takes the profile and `PATCH /v1/resources/{resource_id}`
125
+ (`slotlock_update_resource`) changes it, 400 `invalid_resource_profile` naming the `field`; a
126
+ resource in the application API carries all six. `slotlock_list_resources` shows agents each
127
+ resource's name, kind, capacity, labels and the first 500 characters of its description (never
128
+ its attributes), and narrows by `kind` and `labels`; a confirmation names a resource by its name.
129
+ `resource.updated` on the feed lists the profile fields that changed in `profile_changed`, never
130
+ their values. The dashboard's resource page edits the profile and shows the name in place of the
131
+ reference. The privacy notice lists names, descriptions, labels and attributes as calendar
132
+ content. In the OpenAPI document and the five clients. Run `slotlock migrate`.
133
+ - Locations and groups (RFC 0003 G2, G3). A location is a place in a tree of the tenant's own
134
+ (`slotlock.locations`: at most 5 levels and 1,000 a tenant, held by a guard trigger under the
135
+ tenant's lock, which also refuses a location under itself), and a resource is at one at most
136
+ (`slotlock.resources.location_ref`, a foreign key added NOT VALID). A group is any set of the
137
+ tenant's resources (`slotlock.resource_groups` and `resource_group_members`: 500 a tenant, 20 a
138
+ resource), with no booking rule; a pool stays how resources are booked as a class. All three are
139
+ tenant tables under forced row-level security, erased with the tenant. A resource created at a
140
+ location without a time zone takes the nearest one its location, or one above it, has. The store
141
+ gains `listLocations`, `putLocation`, `updateLocation`, `deleteLocation` (`location_in_use` while
142
+ a resource is at it or a location is in it), `listGroups`, `putGroup`, `deleteGroup`,
143
+ `addGroupMember` and `removeGroupMember`; `createResource` and `updateResourceProfile` take a
144
+ `location`, and `listResources` narrows by `location` (with every location under it) and
145
+ `group`. The application API gains `GET`/`POST /v1/locations`, `PATCH`/`DELETE
146
+ /v1/locations/{location_ref}`, `GET`/`POST /v1/groups`, `DELETE /v1/groups/{group_ref}` and
147
+ `PUT`/`DELETE /v1/groups/{group_ref}/members/{resource_id}`; a resource carries its `location` and
148
+ `groups`, `POST /v1/resources` no longer defaults `timezone` in its schema (the store still gives
149
+ UTC), and `PATCH /v1/resources/{resource_id}` moves it. `slotlock_list_resources` shows agents each
150
+ resource's location (with its name) and groups, and narrows by `location` and `group`, with or
151
+ without a pool. `resource.created` and `resource.updated` carry the location. Run `slotlock
152
+ migrate`.
153
+ - Partner accounts (RFC 0003 P1-P4, T2). A partner reselling Slotlock runs a tenant for each of
154
+ its users, `partner:<partner>:<account>`. The schema owner makes partners with a contract (the
155
+ plans their accounts may be on, with limits and monthly quotas, the scopes their keys may carry,
156
+ and prices) and their keys: `slotlock partner create|list|suspend|unsuspend|key|revoke-key|
157
+ customer|webhook`. A partner key (`slk_partner_…`, a SHA-256 kept) carries the new `partner`
158
+ scope, which reaches only the eleven partner operations under `/v1/partner` (a new `Partner`
159
+ OpenAPI tag, `x-slotlock-scope: partner`, in the five clients): create an account or change its
160
+ plan, list and read accounts, make, list, rotate and revoke their keys, suspend and lift its own
161
+ suspension (never the deployment's), erase an account, and read its accounts' usage. An
162
+ application operation may now name its scope (`scope: 'partner'`); a principal carries
163
+ `partnerId`. New owner-only tables `slotlock.partners`, `partner_keys`, `partner_accounts` and
164
+ `partner_invoices`, and functions `partner_tenant_blocked`, `authenticate_partner_key`,
165
+ `partner_contract`, `put_partner_account`, `list_partner_accounts`, `get_partner_account`,
166
+ `set_partner_account_suspended` and `partner_usage`. `tenant_plans` gains `source`,
167
+ `partner_id` and the account's own quotas (`slotlock.tenant_plan_quotas`), and its plan check
168
+ takes a contract's plan names for a partner's account (added NOT VALID); quotas read a tenant's
169
+ own numbers before its plan's (`readSlotlockTenantQuotas`; `createSlotlockQuotas`'s `plan`
170
+ returns quota numbers). A suspended partner suspends every account (`tenant_suspended`) and
171
+ their keys stop authenticating (`authenticate_scoped_api_key`); erasing an account removes the
172
+ partner's record of it. Run `slotlock migrate`.
173
+ - Tracing and metrics to OpenTelemetry: a call's span is now a child of the caller's span (given to
174
+ the tracer alone through the agent server's new `onCallTrace`; the events and logs still never
175
+ carry it), and the metrics are exported over OTLP each minute (`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`,
176
+ or `<endpoint>/v1/metrics`; `createSlotlockOtlpMetricsExporter`, `SlotlockMetrics.collect`).
177
+ - The five clients take an account for a partner key to act for (`Slotlock-Account`): `account=`
178
+ (Python), `WithAccount` (Go), `Options.account` (Rust), `.account(…)` (Java),
179
+ `SlotlockClientOptions.Account` (.NET), checked before any request.
180
+ - Partner reconciliation and a daily billing job (RFC 0003 P5, P6): `slotlock_partner_reconcile_usage`
181
+ (`POST /v1/partner/usage/reconcile`) compares a partner's counts of an ended day with Slotlock's,
182
+ account by account, and records a day off by more than 0.5% in the new owner-only table
183
+ `slotlock.partner_reconciliations`. `slotlock billing daily`, for a daily cron job as the schema
184
+ owner, closes last month for every active partner once it has been over for 6 hours, reports
185
+ overage when it is on, and lists the week's mismatches, exiting 1 when anything needs attention.
186
+ Month close freezes the month (`slotlock.partner_month_closes`), bills erased accounts from
187
+ `slotlock.partner_erased_usage`, resends the webhook until it is taken, and adopts a Stripe item
188
+ an interrupted run made. The partner key calls take `environment: test`. Run `slotlock migrate`.
189
+ - Metered overage for Developer (RFC 0003 Later), off by default: with `SLOTLOCK_OVERAGE=on` (or a
190
+ stop percent, 111-1000) and `SLOTLOCK_QUOTAS=on`, a paying Developer tenant is refused only past
191
+ that share of its monthly operations (`readSlotlockTenantQuotas`'s `overageStopPercent`); the
192
+ Worker adds `SLOTLOCK_STRIPE_PRICE_OVERAGE`, a metered price, to a Developer checkout and shows
193
+ `SLOTLOCK_PLAN_PRICE_OVERAGE`; and `slotlock overage report [--month] [--event]`, as the schema
194
+ owner with `STRIPE_SECRET_KEY`, sends Stripe Billing Meter events for what each tenant used above
195
+ its quota and has not been billed for, recorded first in the new owner-only table
196
+ `slotlock.overage_reports` (erased with the tenant), so nothing is billed twice
197
+ (`reportSlotlockOverage`, `overage.ts`); an event Stripe refuses as already taken (its answer to a
198
+ resent identifier) counts as sent, so a lost answer neither sticks nor bills twice. Stripe's test
199
+ mode takes the metered price in a Managed Payments checkout; it stays off until a paid test
200
+ subscription shows the meter billed and the Terms name the price. Run `slotlock migrate`.
201
+ - OpenTelemetry trace export and published objectives (RFC 0002 P2-5, RFC 0003 Later). With
202
+ `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`), `serve` sends a SERVER
203
+ span for each MCP, A2A and REST call to an OpenTelemetry collector, OTLP over HTTP as JSON, in
204
+ batches from a bounded queue (a collector that is down drops spans, never a call): its protocol,
205
+ operation, outcome or HTTP status and duration, in the caller's trace when it sent a
206
+ `traceparent`, naming no tenant, key, argument or address. `OTEL_EXPORTER_OTLP_HEADERS` and
207
+ `OTEL_SERVICE_NAME` as the standard says. `createSlotlockOtlpExporter` and `slotlockCallSpan`
208
+ (`otel.ts`). The status page publishes the hosted service's objectives (99.5% of calls succeed,
209
+ 95% under a second, 99.5% of probe checks up), not an SLA, with Prometheus recording and
210
+ multiwindow burn-rate alerting rules in `docs/hosted/slo-rules.yml`.
211
+ - Composite resources (RFC 0003 G8, T2): a resource can be a part of another
212
+ (`slotlock_set_resource_parent`, `PUT /v1/resources/{resource_id}/parent`, `setResourceParent`,
213
+ `slotlock resource parent <ref> <parent-ref | none>`), one level deep, at most 50 parts. Each
214
+ booking or hold on a part or a whole claims its place in the new tenant table
215
+ `slotlock.composite_claims` (forced RLS), kept apart by `EXCLUDE (resource_id WITH =, during
216
+ WITH &&, span WITH &&)`: the whole conflicts with every part, two parts never. A trigger on
217
+ reservations keeps the claims; reaping an expired hold covers the composite's other resources;
218
+ free/busy and the next-free search read the composite's other bookings. `resources` gains
219
+ `parent_id` and `part_index` (the foreign key and check added NOT VALID); the application API's
220
+ resource gains `parent_id` and `part_index`, and `slotlock_list_resources` shows `part_of`. A
221
+ composite changes only while neither side has a booking or live hold to come
222
+ (`composite_busy`). A booking refused by the composite names the booking in its way; the feed's
223
+ `resource.updated` carries `parent` before and after; the dashboard's resource page has a
224
+ Composite panel. Run `slotlock migrate`.
225
+ - Sign-in with Google (OpenID Connect, RFC 0003 Later, T2), with `SLOTLOCK_GOOGLE_CLIENT_ID` and
226
+ `SLOTLOCK_GOOGLE_CLIENT_SECRET`: Continue with Google on the sign-in page (`/sign-in?with=google`,
227
+ back to the same callback), `openid email` with `state`, PKCE and a nonce; the ID token from
228
+ Google's token endpoint is checked for issuer, audience (and `azp`), expiry and nonce. Each
229
+ person is their own tenant, `google:<subject>`; `SLOTLOCK_DASHBOARD_USERS` takes
230
+ `google:<subject>` entries, and `serve` refuses a Google tenant's keys unless the list names it
231
+ (or is `*`). Not with a list of `SLOTLOCK_DASHBOARD_ORGS`. Erasing a Google person's tenant
232
+ removes their dashboard form records. The ID token's RS256 signature is checked against Google's
233
+ published keys (`google.jwksUrl`, kept for the answer's max-age), through WebCrypto. The
234
+ dashboard's `google` option.
235
+ - The Team plan, an organization's (Teams on Cloud): $40 a month or $400 a year per organization,
236
+ 500 resources, 100 keys and 25 webhook endpoints, with 2,000,000 operations and 100,000 bookings
237
+ a month, stopping at 110%. The dashboard's resource cap rises from 100 to 500 and a tenant's
238
+ webhook endpoints from 10 to 25 (`SLOTLOCK_DASHBOARD_MAX_RESOURCES`,
239
+ `SLOTLOCK_MAX_WEBHOOK_ENDPOINTS`); a plan's own limits stay lower. The Terms and the hosted page
240
+ name it. The Worker offers it to an organization's owner once `SLOTLOCK_STRIPE_PRICE_TEAM_MONTHLY`
241
+ and `SLOTLOCK_STRIPE_PRICE_TEAM_YEARLY` are set (the prices shown default to $40 and $400); a
242
+ subscription at a Team price gives Team (`slotlockPlanOfSubscription`'s `teamPrices`, the
243
+ Stripe client's `planOf`). `tenant_plans` takes `team` (its source check is replaced, NOT
244
+ VALID). `SlotlockPlan` gains `team`; the dashboard's billing gains `teamPrices` and checkout a
245
+ `plan`. Run `slotlock migrate`.
246
+ - Teams on Cloud (RFC 0003, T2): `SLOTLOCK_DASHBOARD_ORGS=enabled` gives every person their own
247
+ tenant and the GitHub organizations they belong to that an owner turned on, from the dashboard's
248
+ new Teams page (`POST /teams/enable`, `/teams/disable`, the latter confirmed with the
249
+ organization's name), by an owner who signed in within the last 15 minutes. The workspace menu switches between them; a session in an organization
250
+ turned off is back in its own tenant at its next request, and `serve` refuses the
251
+ organization's keys (checked in the database, kept 30 seconds: `createSlotlockOrganizationCheck`).
252
+ New owner-only table `slotlock.dashboard_organizations` with `enabled_organizations`,
253
+ `enable_organization` and `disable_organization`, granted by `slotlock migrate`; erasing an
254
+ organization's tenant removes its row. `createSlotlockDashboardTeams` (teams.ts), the dashboard's
255
+ `teams` option. Run `slotlock migrate`.
256
+ - Acting for a partner's account (RFC 0003 P3, T2): a partner key with a `Slotlock-Account` field
257
+ runs the request as that account, with the contract's key scopes, where the contract allows it
258
+ (`act_on_behalf`, off by default). The principal carries `onBehalfOf`, the history records the
259
+ partner key as the actor, and serve refuses the field with any other credential.
260
+ `SLOTLOCK_ACCOUNT_FIELD` in `partners.ts`.
261
+ - Test environments (RFC 0003 G9, T2). A tenant reference ending `~test` is the test environment of
262
+ the tenant before it: a tenant of its own, on its live tenant's plan and quotas, never billed
263
+ (`tenant_plan` returns no Stripe ids for it), suspended with its live tenant and erased with it
264
+ (`erase_tenant` reports its rows as `test.<table>`). Its keys start `slk_test_` and only its keys
265
+ do: `slotlock_api_keys_environment_valid` (added NOT VALID) and a widened prefix check.
266
+ `isSlotlockApiKey` takes both forms; `generateSlotlockApiKey(test)`, `isSlotlockTestTenant`,
267
+ `slotlockLiveTenant`, `SLOTLOCK_TEST_API_KEY_PREFIX`. The dashboard has a Live/Test switch
268
+ (`POST /environment`, kept in the session) with a banner in Test, where plan and billing actions
269
+ are refused. `slotlockServeAllowsTenant` judges a test environment as its live tenant, so a
270
+ person removed from `SLOTLOCK_DASHBOARD_USERS` loses their test keys too. Run `slotlock migrate`.
271
+ - Closing a partner's month (RFC 0003 P5, P6): `slotlock partner close <id> [--month YYYY-MM]`,
272
+ as the schema owner, closes an ended month: it prices each account's usage with the contract
273
+ (`priceSlotlockPartnerMonth`), records the lines once in `slotlock.partner_invoices`, sends the
274
+ signed `usage.period_closed` webhook (the totals by account and metric, the lines and a SHA-256,
275
+ `slotlockPartnerMonthChecksum`), and with `STRIPE_SECRET_KEY` adds each line Stripe has not
276
+ taken to the partner's next invoice; it is safe to run again (`closeSlotlockPartnerMonth` in
277
+ `partner-billing.ts`). `/metrics` gains `slotlock_requests_by_client_total{client}`, from the
278
+ `Slotlock-Client` request field matched against the new `SLOTLOCK_KNOWN_CLIENTS`.
279
+ - Keys limited to part of their tenant (RFC 0003 G6). `slotlock key create --locations <refs>
280
+ --groups <refs>`, the dashboard's "Limit to locations or groups" (Developer plan and up when the
281
+ dashboard has plans) or the key store's `resourceScope` makes a key that reaches only the
282
+ resources at those locations (each with every location in it) or in those groups, up to 20 of
283
+ each, read and write only, for good (kept through rotation). The database enforces it: withTenant's
284
+ new `resourceScope` sets `slotlock.resource_scope` to the key's resources for the transaction, and
285
+ a restrictive `resource_scope_v1` policy on every table keyed by resource or by event narrows each
286
+ read and write to them, so a resource outside is not found (`resource_not_found`, never
287
+ `forbidden`) and the feed carries only its resources' changes. New owner-only
288
+ `slotlock.api_key_resource_scopes` (erased with the key) and definer functions
289
+ `set_api_key_resource_scope`, `authenticate_scoped_api_key` and `list_api_key_resource_scopes`;
290
+ a principal carries `resourceScope`, which the store backends pass on; key lists show it. The
291
+ deployment's RLS check accepts the restrictive policy beside the tenant one, and nothing else. Run
292
+ `slotlock migrate`, then grant the application role again (`slotlock migrate` does).
293
+ - Group membership changes are on the feed. A resource joining or leaving a group, a group's
294
+ deletion included, appends `resource.updated` with the groups it was in `before` and is in
295
+ `after`, by reference, sorted: one change for each resource a statement touched (a statement
296
+ trigger over its transition table on `slotlock.resource_group_members`), none for adding it to a
297
+ group it is in, for the deletion of the resource itself (`resource.deleted` says so) or for
298
+ erasure. Webhooks deliver it, and a webhook filtered by a group matches the resource as it is when
299
+ the change is delivered, as every filter does. Run `slotlock migrate`.
300
+ - The dashboard edits locations and groups. Its new Locations and groups page, linked from
301
+ Resources, adds a location (with where it is and a time zone), moves, renames and deletes one, and
302
+ adds, renames and deletes a group, each refusal said in a person's words. A resource's page puts
303
+ it at a location and in its groups in one form, in one transaction, so a group it may not join
304
+ moves nothing. Resources adapters gain the optional `grouping`, which
305
+ `createSlotlockDashboardResources` provides; without it the page and the panel are not shown.
306
+ - Search by selection (RFC 0003 G4). `slotlock_get_free_busy`, `slotlock_find_next_available` and
307
+ `slotlock_find_available_slots` (and their REST routes) take `location` (with every location in
308
+ it), `group`, `labels` (every one), `kind` or `pool` in place of `resource_ids`, one or the other.
309
+ The agent server resolves the selection through the backend's new optional `selectResources` (the
310
+ shared, unarchived resources that match, by id, at most 100, or 20 for slots), runs the search on
311
+ them and names them in `selected_resource_ids`; more than the limit is refused whole with `400
312
+ selection_too_large`, none with `404 selection_empty`, and a backend without `selectResources`
313
+ answers `400 selection_not_supported`. The store's `listResources` also narrows by `pool`.
314
+ - Webhook filters (RFC 0003 G5). `POST /v1/webhooks` takes `resource_ids` (up to 100), `pools`,
315
+ `locations` and `groups` (up to 20 each): the endpoint then receives only changes about a resource
316
+ that matches one of them, as it is when the change is delivered, a location matching every
317
+ location in it, and no change about no resource. New columns on `slotlock.webhook_endpoints`
318
+ (bounded by a CHECK added NOT VALID), and `slotlock.location_path(text, text)`. The store's
319
+ `createWebhookEndpoint` takes a `filter` (`invalid_webhook_filter` past its bounds), an endpoint
320
+ carries its `filter`, `webhook.created` on the feed records it, and the dashboard's webhooks page
321
+ shows it. A change that says where its resource was also matches: `resource.deleted` (now with the
322
+ resource's `pool`, `location` and `groups`, recorded by a BEFORE DELETE trigger while its groups
323
+ are still there) and `resource.updated` by its `before`, so a filtered endpoint learns of a
324
+ resource deleted or moved out of what it follows. An endpoint that takes only some changes (by
325
+ type or filter) moves its cursor past those it does not take, up to the last change that has
326
+ ended, when none before it is for it: it no longer keeps the tenant's whole feed from being
327
+ pruned. This covers `event_types` filters too, which held it back the same way.
328
+ - Pool order (RFC 0003 G7). `PUT /v1/pools/{pool}/order` (`slotlock_set_pool_order`,
329
+ `setPoolOrder`) sets the order `slotlock_book_pool` tries a pool's members in: `stable` (by id, as
330
+ before, and the default), `least_booked` (fewest confirmed bookings made in the last 30 days
331
+ first) or `round_robin` (starting after the member it booked last). `GET /v1/pools` shows each
332
+ pool's `order`. New tenant table `slotlock.pool_settings`, under the tenant's forced row-level
333
+ security and erased with it, for at most 1,000 pools a tenant (`pool_order_limit_reached`, 409,
334
+ for one more); an unknown order is refused `invalid_pool_order`. Run `slotlock migrate`.
335
+ - Provider presets and a model list in the assistant's Settings (RFC 0003 A4). Anthropic, OpenAI,
336
+ OpenRouter, Google, Mistral, Groq, Together and Custom each fill the API and base URL, saving
337
+ nothing; **Load models** asks the provider for its models (`GET /v1/models` on the Anthropic API,
338
+ `GET /models` on an OpenAI-compatible one, listed under `data` or, as Together answers, as a bare
339
+ array) and offers them for the Model field. It checks the URL
340
+ as saving does, sends the saved key only to the provider and URL it was saved for, never follows a
341
+ redirect, and counts against the member's message rate. `SLOTLOCK_ASSISTANT_PRESETS` and
342
+ `listSlotlockAssistantModels` in `assistant.ts`.
343
+ - Monthly quotas, off unless `SLOTLOCK_QUOTAS=on` (which needs `SLOTLOCK_USAGE=on`; RFC 0003
344
+ part 3). A tenant on a plan may run so many operations and make so many bookings a UTC calendar
345
+ month (`SLOTLOCK_PLAN_QUOTAS`): Free 25,000 and 1,000, refused at 100%; Developer 500,000 and
346
+ 25,000, refused at 110%. Past the operations quota every operation is refused but
347
+ `slotlock_get_usage` and `slotlock_erase_tenant` (`SLOTLOCK_QUOTA_EXEMPT_OPERATIONS`), past the
348
+ bookings quota only those that can book (`SLOTLOCK_BOOKING_OPERATIONS`), so reads and
349
+ cancellations still run; the refusal is `429 quota_exceeded`, naming the metric, the quota, the stop, the count and
350
+ the reset, with `Retry-After` at the month's end. Every operation's answer lists the quotas in
351
+ its `RateLimit` and `RateLimit-Policy` fields (`"operations_month"`, `"bookings_month"`, counting
352
+ down to the stop), after the per-second bucket. `createSlotlockQuotas` reads the month so far
353
+ through `slotlock.tenant_quota_usage` (bookings not yet rolled up included) at most every 30
354
+ seconds, every 2 near a stop, and adds this server's unrecorded operations (the meter's new
355
+ `local`); a count it cannot read lets the operation run. Booking operations let run since a read
356
+ are counted as attempts, so before refusing one on that estimate a server reads the count again. At 80% and 100% of a quota the tenant's
357
+ feed records the new `usage.threshold_reached` change once, bookings as the database counted them, (`slotlock.note_usage_threshold`,
358
+ `slotlock.usage_notices`, erased with the tenant), which its webhooks deliver. The request limits
359
+ take `quotas` and a `quota` check for `authorize`; the dashboard assistant is checked when an
360
+ operation runs, never when one is proposed, on `slotlock serve` and the Worker. The dashboard's
361
+ `quotas` option shows the Plan page's use against them and warns from 80%. `/metrics` and the
362
+ refusal log count `quota_exceeded`. The Terms and the hosted service page name the quotas from 10
363
+ December 2026; the privacy notice lists the notices. Run `slotlock migrate`.
364
+ - `SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS`: up to three earlier values of `SLOTLOCK_ASSISTANT_SECRET`,
365
+ comma-separated, newest first, for `slotlock serve` and the dashboard Worker. A model key sealed
366
+ under one of them still opens, and is sealed again under the current secret the next time its
367
+ organization uses the assistant, by `resealAssistantKey` on the tenant store, which replaces the
368
+ stored seal only while it is still the one that was read, so a key an owner saved meanwhile is
369
+ never overwritten. Each must be as strong as a secret, and differ from the others, from the
370
+ current secret and from every other secret; the log redacts each one. Changing the assistant
371
+ secret no longer makes owners enter their keys again.
372
+ - `SLOTLOCK_DASHBOARD_TERMS_URL` and `SLOTLOCK_DASHBOARD_PRIVACY_URL`: with an https URL set, the
373
+ dashboard's sign-in page names the terms a person accepts by continuing, and links the privacy
374
+ notice. Without them it names no terms, as before.
375
+ - Docs: a Use cases page, on how to model car and van rental, equipment hire, clinics, courts,
376
+ garage service bays, salons, meeting rooms and field service, and where Slotlock does not fit.
377
+
378
+ ### Changed
379
+
380
+ - Rust client: every write's input struct has a `dry_run: Option<bool>` field (RFC 0004 A3), and
381
+ `PartnerCreateKeyInput` a `limits` field (A5). A struct literal that lists every field must now
382
+ end with `..Default::default()`, as the README's examples do; the other clients take them as
383
+ optional arguments.
384
+ - `resource_ids` is optional on `slotlock_get_free_busy`, `slotlock_find_next_available` and
385
+ `slotlock_find_available_slots`, since a selection can stand in for it. Calls written before
386
+ still compile in every client: Rust keeps the field a `Vec<String>` (left out when empty), Java
387
+ keeps the methods that take it first beside new ones that take it in `optional`, and .NET keeps
388
+ its 0.2.0 overloads (the generator now keeps a parameter that became optional, which
389
+ `RELAXED_ARGUMENTS` in `clients/generate.ts` lists). A Rust caller that names every field of an
390
+ input struct, rather than ending with `..Default::default()`, gains new fields to name.
391
+
392
+ - The dashboard Worker no longer reads `SLOTLOCK_FREE_PLAN_FROM` and answers 503 while it is set,
393
+ naming `slotlock plan default` instead: the date is the database's now.
394
+ - Erasing a tenant whose plan names a live Stripe subscription is refused with `billing_active`
395
+ (`slotlock tenant erase` exits 1; `/v1/tenant/erase` answers 409): cancel the subscription, whose
396
+ webhook moves the tenant to Free, then erase. The privacy notice already promised it.
397
+ - Upgrade on the Plan page opens no second Checkout for a customer that already has an active,
398
+ trialing or past-due subscription, and points it to Manage billing instead.
399
+ - A Stripe webhook about a subscription that ended no longer moves its tenant to Free while another
400
+ of the tenant's subscriptions still renews (two Checkouts paid in two tabs): the plan names the
401
+ live one. A second live subscription is logged as `slotlock billing duplicate subscription`, by
402
+ Stripe ids only, so it can be refunded.
403
+ - Stripe requests send `Stripe-Version: 2026-04-22.dahlia` (`SLOTLOCK_STRIPE_API_VERSION`) unless
404
+ `STRIPE_API_VERSION` is set, rather than the account's default: Managed Payments needs
405
+ 2025-03-31.basil or later.
406
+ - The Plan page counts resources as the plan does, archived ones included; the Resources page says
407
+ "*n* of *limit* on your plan" with the plan's limit, not the dashboard's 100, and a refused add,
408
+ key or webhook names the plan.
409
+ - Talk to the founder defaults to the founder's Pylota address, on the Plan page, the landing page
410
+ and the hosted service page.
411
+ - The hosted service page has an Assistant section: bring your own model (Anthropic or
412
+ OpenAI-compatible), what is sent to it, that writes wait for approval, its bounds, and that a model
413
+ must be reachable from the internet. A test holds it to the bounds in the code. The Worker's
414
+ wrangler.jsonc says to set `SLOTLOCK_ASSISTANT_SECRET` once and keep it.
415
+ - Site: the landing page leads with what agents can book and with Slotlock Cloud, in under 1,300
416
+ words instead of 2,607 and 12 bands instead of 15. It shows the dashboard in its first screen,
417
+ says Pylota checks every booking with Slotlock, lists the industries it serves, and has a pricing
418
+ table: Free, Developer, Team and Self-host, each hosted plan's limits and monthly allowances read
419
+ from `src/plans.ts` and its price and the Custom contact from the Plans table in
420
+ `docs/hosted/README.md`, so the page cannot offer a plan the code does not sell. Running it
421
+ yourself now lives in Pricing, a Source band and the docs; the hero's install card moved to the
422
+ developers band. Copy lint fails the build above 1,300 words.
423
+ - Privacy notice: email sent to support@, abuse@ or privacy@pylota.io is held in Google Workspace for
424
+ two years; Google is listed as a recipient, with its transfers. The notice has no owner TODO left.
425
+ - README: about 160 lines that say what Slotlock is, how to run it and where each topic is
426
+ documented, down from 1,659. Everything it explained is on
427
+ [slotlock.pylota.io/docs](https://slotlock.pylota.io/docs/), which gains pages for hours and
428
+ booking rules, limits, the dashboard on Cloudflare Workers, recurring events and external
429
+ calendars, and now holds the store guide, organizations, the assistant's limits and the protocol
430
+ reference in full. A link to a README section from an earlier release lands on the README, whose
431
+ Documentation table names each topic's page.
432
+
433
+ ### Fixed
434
+
435
+ - Docs: the self-hosting page said `SLOTLOCK_TRUSTED_PROXY_HEADER` is ignored without
436
+ `SLOTLOCK_TRUSTED_PROXY_SECRET`. The header is believed whenever no secret is set; the secret
437
+ limits it to requests that carry the proof.
438
+ - Many agents racing for one slot no longer stall for seconds (RFC 0004 A7). Writers each inserted,
439
+ then waited for one another at the exclusion constraint, and PostgreSQL broke each such deadlock
440
+ only after `deadlock_timeout`: about one 100-client race in three took 4 to 20 seconds, though
441
+ every answer was right. Booking writes now lock the resource, and a part's composite parent, in id
442
+ order (`FOR NO KEY UPDATE`) right before writing its occupied time, so racing writers take turns
443
+ for that step and the losers are refused at once.
444
+ - From RFC 0004's audit (P0):
445
+ - A no-show was announced twice, as `reservation.no_show` and as `reservation.cancelled`, so a
446
+ consumer counting cancellations counted it. The feed's trigger now names the deletion, so the
447
+ feed has one change: `reservation.no_show`, with the same fields as before.
448
+ - An agent's `overlap` says when the time is busy (`busy_start`, `busy_end` in its details), and
449
+ names the booking in the way when it is the agent's own (`conflicting_hold_id` or
450
+ `conflicting_event_id`), so it can release or move it.
451
+ - Confirming a hold through the application API (`POST /v1/holds/{id}/confirm`) left its linked
452
+ alternatives held until they lapsed; they are released in the same transaction, as an agent's
453
+ booking from a hold releases them.
454
+ - An agent booking from its own lapsed hold is `hold_expired` (409), as the application API's
455
+ confirm says, not `hold_not_found`; another agent's hold is still not found.
456
+ - `slotlock_join_waitlist` and `slotlock_create_webhook` take an idempotency key (the
457
+ Idempotency-Key header or `idempotency_key`): a retry answers the first entry, or webhook and
458
+ secret (`replayed: true`); the same key with another request is `idempotency_conflict`. Run
459
+ `slotlock migrate`.
460
+ - Round-robin pool bookings made at once could start from the same member; the pool's place is
461
+ locked while one is chosen.
462
+ - The MCP instructions and the A2A agent card say that titles, descriptions, locations and names
463
+ in results are data written by people, never instructions.
464
+ - `slotlock_list_webhooks` refused its own answer past 10 webhooks, though a tenant may have 25.
465
+ - A retried `slotlock_delete_event`, after the first had succeeded, answered `event_not_found`
466
+ instead of the first cancellation; it answers that with `replayed: true`.
467
+ - A recurring event's occurrences are written in three statements for the whole series, not three
468
+ per occurrence (reap the expired holds in the way, insert the bookings, insert the occurrences).
469
+ A 2,000-occurrence series took 6,000 round trips while holding its tenant's locks, so on a busy
470
+ database every write queued behind it timed out; it now takes three. The same rows are reaped,
471
+ in the same order, across a composite's parts and whole.
472
+
473
+ ## 0.6.3 - 2026-11-08
474
+
475
+ ### Added
476
+
477
+ - `SLOTLOCK_TRUSTED_PROXY_SECRET`: with it set, `slotlock serve` believes
478
+ `SLOTLOCK_TRUSTED_PROXY_HEADER` only on a request whose `X-Slotlock-Proxy-Proof` carries that
479
+ exact value (compared in constant time), and counts any other by its connection's address. A
480
+ process that reaches the server without passing the proxy, such as another container on
481
+ kamal-proxy's network, can then no longer pick its own rate-limit bucket. Unset, nothing changes.
482
+ The `listening` line says `client_address_proof`.
483
+
484
+ ### Changed
485
+
486
+ - Site: the landing page's Slotlock Cloud endpoints list only what an agent connects to (MCP, A2A,
487
+ REST, OpenAPI, the agent card). The dashboard row is gone: people reach it through Sign in.
488
+
489
+ ### Fixed
490
+
491
+ - `slotlock serve` maintains every tenant with data every hour, not only the ones whose keys it has
492
+ served since it started: a tenant gone quiet has its expired holds, ended waitlist entries and
493
+ feed past retention deleted too. Tenants are listed by name in `slotlock.maintenance_tenants`
494
+ (owner only, filled by triggers as a tenant first writes, and from existing rows when `migrate`
495
+ creates it); the serving role reads the names through `slotlock.maintenance_tenants(text)` and
496
+ maintains each in its own tenant context. Run `slotlock migrate`.
497
+
6
498
  ## 0.6.2 - 2026-11-08
7
499
 
8
500
  ### Changed
package/CONTRIBUTING.md CHANGED
@@ -51,11 +51,18 @@ A check on each pull request fails when a commit lacks the line. To fix earlier
51
51
 
52
52
  ## Documentation
53
53
 
54
- Every TypeScript block in README.md is quoted from a region of `examples/*.ts`
55
- (`// #region <name>` … `// #endregion <name>`) marked `<!-- example: examples/<file>#<name> -->`.
56
- Edit the example, not the README copy: `readme-examples.test.ts` fails when they differ, and
57
- `examples.integration.test.ts` runs the examples against PostgreSQL. The website and docs live in
58
- `site/`.
54
+ Every TypeScript example the docs show is a region of `examples/*.ts`
55
+ (`// #region <name>` … `// #endregion <name>`). README.md quotes one after
56
+ `<!-- example: examples/<file>#<name> -->`; a docs page in `site/docs-src/` names one with
57
+ `<!-- code examples/<file>#<name> -->`, and the site build puts the code in. Edit the example, not
58
+ a copy: `docs-examples.test.ts` fails when a README copy differs, a marker names no region, or a
59
+ region is shown nowhere, and `examples.integration.test.ts` runs the examples against PostgreSQL.
60
+
61
+ The website and docs live in `site/`. Document a topic on its page in `site/docs-src/`, the one
62
+ place it is written down. The README is the front door: it says what Slotlock is, how to run it,
63
+ and links to those pages; `readme.test.ts` keeps it short and its links working. Its intro, "Why
64
+ it is built for agents", "Project status and boundaries" and "Supported versions" also appear on
65
+ the site, which includes them.
59
66
 
60
67
  ## Pull requests
61
68