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