@mccaskie-labs/workbench-config 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - A new component, `availability` (workbench-availability: quotes, the bookings store and the tap-to-block calendar), with its client questions (what is booked, how it is booked in a day, event types, holidays, holds). The country manifest gains holidays (`holidaysFor`, `checkHolidays`) and the December probe `country.holidays`.
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,263 @@
1
+ {
2
+ "$schema": "./schema.json",
3
+ "format": 1,
4
+ "component": "availability",
5
+ "title": "Availability and pricing",
6
+ "summary": "workbench-availability answers \"is this date free, and what does it cost?\" for a bookable thing (a venue, a room, a photographer's day) from its calendar and its price rules, with the same answer every time and one plain line per rule applied. A store over the app's own database stops two bookings landing on one date, and a tap-to-block calendar lets the owner close dates from a phone. It runs in the app itself, with no Gateway call, no AI and no cost.",
7
+ "status": "built",
8
+ "delivery": {
9
+ "choice": "optional",
10
+ "intro": "Which dates are free and what each costs, worked out the same way every time, with a calendar the owner taps to block dates."
11
+ },
12
+ "surfaces": {
13
+ "package": [
14
+ "quote",
15
+ "createAvailability",
16
+ "availabilityMigration",
17
+ "calendarAction",
18
+ "checkSettings",
19
+ "stayClaims",
20
+ "staysClash",
21
+ "todayIn",
22
+ "AvailabilityError"
23
+ ]
24
+ },
25
+ "rules": [
26
+ {
27
+ "id": "availability.quote-is-fact",
28
+ "kind": "must",
29
+ "text": "Take every availability answer and price the app shows or sends from quote() (or createAvailability's quote), and pass its lines and explain on as facts. Never let AI work out a price or a free date, and never write a price into a page or prompt by hand.",
30
+ "enforced_by": "review",
31
+ "probe": "availability.quote"
32
+ },
33
+ {
34
+ "id": "availability.book-through-block",
35
+ "kind": "must",
36
+ "text": "Book, hold and block dates only through createAvailability's block(), which runs availability_block() in one transaction: never insert into availability_blocks directly, so the exclusion constraint and the hold expiry always apply.",
37
+ "enforced_by": "database",
38
+ "probe": "availability.store"
39
+ },
40
+ {
41
+ "id": "availability.calendar-days",
42
+ "kind": "must",
43
+ "text": "Treat dates as calendar days (YYYY-MM-DD) in the account's time zone: pass todayIn(timeZone) as today, never new Date() in UTC.",
44
+ "enforced_by": "types",
45
+ "probe": "availability.quote"
46
+ },
47
+ {
48
+ "id": "availability.no-names-in-notes",
49
+ "kind": "must_not",
50
+ "text": "Write a guest's name or phone number into a block's note: link the app's own booking in subject instead. The .ics feed carries neither.",
51
+ "enforced_by": "review",
52
+ "probe": "availability.store"
53
+ },
54
+ {
55
+ "id": "availability.public-gets-the-quote",
56
+ "kind": "must_not",
57
+ "text": "Let a public page or the anonymous key read availability_blocks: public pages get the finished quote from the server.",
58
+ "enforced_by": "database",
59
+ "probe": "availability.store"
60
+ },
61
+ {
62
+ "id": "availability.daily-cleanup",
63
+ "kind": "must",
64
+ "text": "Run createAvailability's cleanup() once a day from the app's cron with the service client, so lapsed holds show as expired and old blocks are deleted on time.",
65
+ "enforced_by": "review",
66
+ "probe": "availability.store"
67
+ },
68
+ {
69
+ "id": "availability.calendar-as-shipped",
70
+ "kind": "must",
71
+ "text": "Draw the owner's calendar with renderCalendar and CALENDAR_SCRIPT (under the page's nonce), and hand the posted body to calendarAction behind the owner's sign-in. Never hand-write the calendar's markup or script.",
72
+ "enforced_by": "review",
73
+ "probe": "availability.calendar"
74
+ }
75
+ ],
76
+ "examples": [
77
+ {
78
+ "id": "availability.example.quote",
79
+ "title": "Answer \"is 12 Dec free, and how much?\"",
80
+ "lang": "ts",
81
+ "probe": "availability.quote",
82
+ "code": "import { createAvailability } from '@mccaskie-labs/workbench-availability'\nimport settings from '../workbench/availability.json'\n\nconst av = createAvailability({ db: serviceClient, account: venue.id, settings, country: 'PH', timeZone: 'Asia/Manila' })\nconst q = await av.quote({ resource: 'villa-sol-pool', date: '2026-12-12', stay: 'day_use', guests: { min: 15, max: 15 }, eventType: 'birthday' })\n// q.available 'yes'; q.options[0].total { amount: '9500.00', currency: 'PHP' }; q.explain ['Weekend day use: ₱9,500 (rule r-weekend)', ...]"
83
+ },
84
+ {
85
+ "id": "availability.example.calendar",
86
+ "title": "The owner's tap-to-block calendar",
87
+ "lang": "ts",
88
+ "probe": "availability.calendar",
89
+ "code": "import { calendarAction } from '@mccaskie-labs/workbench-availability'\nimport { CALENDAR_SCRIPT, renderCalendar } from '@mccaskie-labs/workbench-availability/calendar'\n\nconst days = await av.month('villa-sol-pool', '2026-12')\nconst html = renderCalendar(days, t, { resource: 'villa-sol-pool', month: '2026-12', endpoint: '/owner/calendar', stays: [{ id: 'day_use', label: t('ui.stay.day_use')! }] })\n// The page adds <script nonce=...>${CALENDAR_SCRIPT}</script>; POST /owner/calendar (owner signed in):\nconst r = await calendarAction(av, await request.json())\nreturn Response.json(r.body, { status: r.status })"
90
+ }
91
+ ],
92
+ "probes": [
93
+ {
94
+ "id": "availability.quote",
95
+ "half": "runtime",
96
+ "expect": "pass",
97
+ "smoke": true,
98
+ "cost_hint_usd": 0,
99
+ "note": "In the Worker itself: quote() on the spec's HmPo example (Sat 12 Dec ₱9,500, Sat 19 Dec ₱10,925 with peak, 25 guests ₱10,250 over capacity, no guests a from price) and the alternatives when 19 Dec is booked; no database, no Gateway call"
100
+ },
101
+ {
102
+ "id": "availability.store",
103
+ "half": "runtime",
104
+ "expect": "pass",
105
+ "cost_hint_usd": 0,
106
+ "note": "Against the probe app's own database: two block(booked) calls for one night at once (one lands, one refused as clash), a lapsed hold quoted free and marked expired by cleanup(), another account's resource unreachable, then every probe block removed"
107
+ },
108
+ {
109
+ "id": "availability.calendar",
110
+ "half": "runtime",
111
+ "expect": "pass",
112
+ "cost_hint_usd": 0,
113
+ "note": "The probe page draws a month with renderCalendar and CALENDAR_SCRIPT: tap, Block day, Undo leaves no block; a change made offline shows \"1 change waiting\" and is sent on reconnect"
114
+ }
115
+ ],
116
+ "styles": {
117
+ "prefix": "wb-cal-"
118
+ },
119
+ "offer": {
120
+ "pattern": "settings",
121
+ "when": "The client takes bookings for something with dates: a venue, a room, a resort day pass, a photographer's or hire business's days, or wants prices that depend on the day, season, holiday, number of guests or kind of event, or wants a calendar to block dates.",
122
+ "say": "I'll set this up with Workbench availability, so every price and free date comes out the same way each time, explained line by line, and you get a calendar to block dates from your phone. Here's how a business like yours usually books; tell me what to change.",
123
+ "answers_file": "workbench/availability.json",
124
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"what\": \"<the client's words>\", \"stay_types\": [{ \"id\", \"start\", \"end\", \"clashes\" }], \"event_types\": [...], \"currency\": \"PHP\", \"alternatives\": { \"count\": 3, \"within_days\": 21 }, \"hold_days\": 3, \"holidays\": \"weekend\" | \"weekday\" | \"separate\" }`. Every stay type clashes with itself; list each pair that can't share a date on both sides. Take `currency` from workbench/country.json. checkSettings() reads this file at startup and names every problem. Each account's resources, packages and price rules are data in the app's database (availability_resources, availability_packages, availability_price_rules), entered on the owner's pages, never in this file. Copy availabilityMigration into supabase/migrations with its down file, and CALENDAR_WORDS into the strings catalogue.",
125
+ "needs": ["country"],
126
+ "questions": [
127
+ {
128
+ "id": "availability.what",
129
+ "sets": "availability.what",
130
+ "ask": "What do people book?",
131
+ "style": "table",
132
+ "must_ask": true,
133
+ "other": {
134
+ "kind": "text",
135
+ "min": 3,
136
+ "max": 200,
137
+ "description": "In the client's words, such as \"a resort's pool and function hall\" or \"me, as a photographer, by the half or full day\"."
138
+ },
139
+ "note": "Only the client knows. Their answer names the resources (one per thing booked separately) and suggests the stay types to propose next."
140
+ },
141
+ {
142
+ "id": "availability.stays",
143
+ "sets": "availability.stay_types",
144
+ "ask": "Here are the usual ways it's booked in a day, with their hours and which can't share a date. Accept them, change some, or add or remove?",
145
+ "style": "table",
146
+ "must_ask": true,
147
+ "other": {
148
+ "kind": "list",
149
+ "min": 1,
150
+ "max": 10,
151
+ "description": "Each way it's booked, with its start and end time and the others it can't share a date with."
152
+ },
153
+ "note": "Propose from the client's answer to availability.what. A resort or venue: day use 08:00 to 17:00, night use 19:00 to 07:00, overnight 14:00 to 12:00 the next day (overnight clashes with both; day and night use don't clash with each other). A photographer or hire business: half day and full day, clashing with each other. One room or slot a day: a single stay type."
154
+ },
155
+ {
156
+ "id": "availability.events",
157
+ "sets": "availability.event_types",
158
+ "ask": "Should prices depend on the kind of event? Here is the usual list.",
159
+ "style": "cards",
160
+ "answers": [
161
+ {
162
+ "label": "Yes, use this list",
163
+ "value": "list",
164
+ "means": "Birthday, debut, wedding, baptism, reunion, team building, other: a rule can add a fee for some of them."
165
+ },
166
+ {
167
+ "label": "No, same price for all",
168
+ "value": "none",
169
+ "means": "No event types; the quote never asks for one."
170
+ },
171
+ {
172
+ "label": "Yes, with my own list",
173
+ "value": "own",
174
+ "means": "Type the kinds of event the business prices differently."
175
+ }
176
+ ],
177
+ "other": {
178
+ "kind": "list",
179
+ "min": 1,
180
+ "max": 20,
181
+ "description": "The kinds of event, each a short name."
182
+ },
183
+ "note": "Recommend the usual list for venues and resorts; \"none\" for most other businesses. Write ids in lower case with underscores (team_building)."
184
+ },
185
+ {
186
+ "id": "availability.holidays",
187
+ "sets": "availability.holidays",
188
+ "ask": "Should holidays be priced like weekends?",
189
+ "style": "cards",
190
+ "answers": [
191
+ {
192
+ "label": "Yes, like a weekend",
193
+ "value": "weekend",
194
+ "means": "A regular holiday or special non-working day on a weekday costs the weekend price."
195
+ },
196
+ {
197
+ "label": "No, by the day of the week",
198
+ "value": "weekday",
199
+ "means": "Holidays cost what their day of the week costs."
200
+ },
201
+ {
202
+ "label": "Set holiday prices separately",
203
+ "value": "separate",
204
+ "means": "Holidays get their own price rules, entered on the owner's pages."
205
+ }
206
+ ],
207
+ "other": {
208
+ "kind": "text",
209
+ "min": 3,
210
+ "max": 200,
211
+ "description": "How holidays should be priced, in the client's words."
212
+ },
213
+ "note": "Holidays come from workbench-country's yearly list (the Philippines' proclamation each year); a local fiesta goes in the availability_holidays table. Special working days (the EDSA anniversary) are never priced as holidays."
214
+ },
215
+ {
216
+ "id": "availability.holds",
217
+ "sets": "availability.hold_days",
218
+ "ask": "How long is a date held while waiting for a deposit?",
219
+ "style": "cards",
220
+ "answers": [
221
+ {
222
+ "label": "3 days",
223
+ "value": 3,
224
+ "means": "The date shows as held (H) for 3 days, then is free again unless marked booked."
225
+ },
226
+ {
227
+ "label": "1 day",
228
+ "value": 1
229
+ },
230
+ {
231
+ "label": "7 days",
232
+ "value": 7
233
+ },
234
+ {
235
+ "label": "No holds",
236
+ "value": 0,
237
+ "means": "A date is free until it is booked; nothing is held."
238
+ }
239
+ ],
240
+ "other": {
241
+ "kind": "whole",
242
+ "min": 0,
243
+ "max": 60,
244
+ "description": "Days, 0 to 60."
245
+ }
246
+ }
247
+ ],
248
+ "not_asked": [
249
+ {
250
+ "sets": "availability.prices",
251
+ "why": "Each account's prices, packages and rules change often, so they are data the owner enters on their own pages (or that onboarding reads from their rate card), not project settings."
252
+ },
253
+ {
254
+ "sets": "availability.alternatives",
255
+ "why": "Up to 3 nearest free dates within 21 days, same weekday first, suits every business so far; change it in the answers file if a client asks."
256
+ },
257
+ {
258
+ "sets": "availability.currency",
259
+ "why": "It is the country's own, from workbench/country.json."
260
+ }
261
+ ]
262
+ }
263
+ }
@@ -25,7 +25,11 @@
25
25
  "currencySymbol",
26
26
  "currencySymbolFrom",
27
27
  "WORLDWIDE",
28
- "isWorldwide"
28
+ "isWorldwide",
29
+ "HOLIDAYS",
30
+ "holidaysFor",
31
+ "holidayOn",
32
+ "checkHolidays"
29
33
  ]
30
34
  },
31
35
  "rules": [
@@ -50,6 +54,13 @@
50
54
  "enforced_by": "types",
51
55
  "probe": "country.packs"
52
56
  },
57
+ {
58
+ "id": "country.holidays-from-proclamation",
59
+ "kind": "must",
60
+ "text": "Take public holidays from holidaysFor or holidayOn, never worked out or guessed: a year with no list returns undefined, and checkHolidays fails from 1 December until next year's list is in.",
61
+ "enforced_by": "review",
62
+ "probe": "country.holidays"
63
+ },
53
64
  {
54
65
  "id": "country.spelling-from-pack",
55
66
  "kind": "must",
@@ -82,6 +93,14 @@
82
93
  "expect": "pass",
83
94
  "cost_hint_usd": 0,
84
95
  "note": "Every pack's spelling is one the Gateway accepts (en-PH, en-AU, en-GB, en-US), checked against the ai manifest's spellings; no Gateway call"
96
+ },
97
+ {
98
+ "id": "country.holidays",
99
+ "half": "runtime",
100
+ "expect": "pass",
101
+ "smoke": true,
102
+ "cost_hint_usd": 0,
103
+ "note": "The December probe: checkHolidays for every country with a built-in list, on today in that country's time zone. Fails when this year's list is missing, and from 1 December when next year's is, so the list is added from the proclamation before January's prices need it"
85
104
  }
86
105
  ],
87
106
  "offer": {
@@ -35,7 +35,8 @@
35
35
  "strings",
36
36
  "ui",
37
37
  "forms",
38
- "sign-in"
38
+ "sign-in",
39
+ "availability"
39
40
  ]
40
41
  },
41
42
  "title": {
@@ -430,7 +431,8 @@
430
431
  "strings",
431
432
  "ui",
432
433
  "forms",
433
- "sign-in"
434
+ "sign-in",
435
+ "availability"
434
436
  ]
435
437
  }
436
438
  },
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "de365af27674";
1
+ export declare const MANIFEST_VERSION: "9aae488ad117";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -454,6 +454,221 @@ export declare const capabilities: {
454
454
  readonly intro: "Counting page views and the things visitors do on the site.";
455
455
  };
456
456
  };
457
+ readonly availability: {
458
+ readonly $schema: "./schema.json";
459
+ readonly format: 1;
460
+ readonly component: "availability";
461
+ readonly title: "Availability and pricing";
462
+ readonly summary: "workbench-availability answers \"is this date free, and what does it cost?\" for a bookable thing (a venue, a room, a photographer's day) from its calendar and its price rules, with the same answer every time and one plain line per rule applied. A store over the app's own database stops two bookings landing on one date, and a tap-to-block calendar lets the owner close dates from a phone. It runs in the app itself, with no Gateway call, no AI and no cost.";
463
+ readonly status: "built";
464
+ readonly surfaces: {
465
+ readonly package: readonly ["quote", "createAvailability", "availabilityMigration", "calendarAction", "checkSettings", "stayClaims", "staysClash", "todayIn", "AvailabilityError"];
466
+ };
467
+ readonly rules: readonly [{
468
+ readonly id: "availability.quote-is-fact";
469
+ readonly kind: "must";
470
+ readonly text: "Take every availability answer and price the app shows or sends from quote() (or createAvailability's quote), and pass its lines and explain on as facts. Never let AI work out a price or a free date, and never write a price into a page or prompt by hand.";
471
+ readonly enforced_by: "review";
472
+ readonly probe: "availability.quote";
473
+ }, {
474
+ readonly id: "availability.book-through-block";
475
+ readonly kind: "must";
476
+ readonly text: "Book, hold and block dates only through createAvailability's block(), which runs availability_block() in one transaction: never insert into availability_blocks directly, so the exclusion constraint and the hold expiry always apply.";
477
+ readonly enforced_by: "database";
478
+ readonly probe: "availability.store";
479
+ }, {
480
+ readonly id: "availability.calendar-days";
481
+ readonly kind: "must";
482
+ readonly text: "Treat dates as calendar days (YYYY-MM-DD) in the account's time zone: pass todayIn(timeZone) as today, never new Date() in UTC.";
483
+ readonly enforced_by: "types";
484
+ readonly probe: "availability.quote";
485
+ }, {
486
+ readonly id: "availability.no-names-in-notes";
487
+ readonly kind: "must_not";
488
+ readonly text: "Write a guest's name or phone number into a block's note: link the app's own booking in subject instead. The .ics feed carries neither.";
489
+ readonly enforced_by: "review";
490
+ readonly probe: "availability.store";
491
+ }, {
492
+ readonly id: "availability.public-gets-the-quote";
493
+ readonly kind: "must_not";
494
+ readonly text: "Let a public page or the anonymous key read availability_blocks: public pages get the finished quote from the server.";
495
+ readonly enforced_by: "database";
496
+ readonly probe: "availability.store";
497
+ }, {
498
+ readonly id: "availability.daily-cleanup";
499
+ readonly kind: "must";
500
+ readonly text: "Run createAvailability's cleanup() once a day from the app's cron with the service client, so lapsed holds show as expired and old blocks are deleted on time.";
501
+ readonly enforced_by: "review";
502
+ readonly probe: "availability.store";
503
+ }, {
504
+ readonly id: "availability.calendar-as-shipped";
505
+ readonly kind: "must";
506
+ readonly text: "Draw the owner's calendar with renderCalendar and CALENDAR_SCRIPT (under the page's nonce), and hand the posted body to calendarAction behind the owner's sign-in. Never hand-write the calendar's markup or script.";
507
+ readonly enforced_by: "review";
508
+ readonly probe: "availability.calendar";
509
+ }];
510
+ readonly examples: readonly [{
511
+ readonly id: "availability.example.quote";
512
+ readonly title: "Answer \"is 12 Dec free, and how much?\"";
513
+ readonly lang: "ts";
514
+ readonly code: "import { createAvailability } from '@mccaskie-labs/workbench-availability'\nimport settings from '../workbench/availability.json'\n\nconst av = createAvailability({ db: serviceClient, account: venue.id, settings, country: 'PH', timeZone: 'Asia/Manila' })\nconst q = await av.quote({ resource: 'villa-sol-pool', date: '2026-12-12', stay: 'day_use', guests: { min: 15, max: 15 }, eventType: 'birthday' })\n// q.available 'yes'; q.options[0].total { amount: '9500.00', currency: 'PHP' }; q.explain ['Weekend day use: ₱9,500 (rule r-weekend)', ...]";
515
+ readonly probe: "availability.quote";
516
+ }, {
517
+ readonly id: "availability.example.calendar";
518
+ readonly title: "The owner's tap-to-block calendar";
519
+ readonly lang: "ts";
520
+ readonly code: "import { calendarAction } from '@mccaskie-labs/workbench-availability'\nimport { CALENDAR_SCRIPT, renderCalendar } from '@mccaskie-labs/workbench-availability/calendar'\n\nconst days = await av.month('villa-sol-pool', '2026-12')\nconst html = renderCalendar(days, t, { resource: 'villa-sol-pool', month: '2026-12', endpoint: '/owner/calendar', stays: [{ id: 'day_use', label: t('ui.stay.day_use')! }] })\n// The page adds <script nonce=...>${CALENDAR_SCRIPT}</script>; POST /owner/calendar (owner signed in):\nconst r = await calendarAction(av, await request.json())\nreturn Response.json(r.body, { status: r.status })";
521
+ readonly probe: "availability.calendar";
522
+ }];
523
+ readonly probes: readonly [{
524
+ readonly id: "availability.quote";
525
+ readonly half: "runtime";
526
+ readonly expect: "pass";
527
+ readonly smoke: true;
528
+ readonly cost_hint_usd: 0;
529
+ readonly note: "In the Worker itself: quote() on the spec's HmPo example (Sat 12 Dec ₱9,500, Sat 19 Dec ₱10,925 with peak, 25 guests ₱10,250 over capacity, no guests a from price) and the alternatives when 19 Dec is booked; no database, no Gateway call";
530
+ }, {
531
+ readonly id: "availability.store";
532
+ readonly half: "runtime";
533
+ readonly expect: "pass";
534
+ readonly cost_hint_usd: 0;
535
+ readonly note: "Against the probe app's own database: two block(booked) calls for one night at once (one lands, one refused as clash), a lapsed hold quoted free and marked expired by cleanup(), another account's resource unreachable, then every probe block removed";
536
+ }, {
537
+ readonly id: "availability.calendar";
538
+ readonly half: "runtime";
539
+ readonly expect: "pass";
540
+ readonly cost_hint_usd: 0;
541
+ readonly note: "The probe page draws a month with renderCalendar and CALENDAR_SCRIPT: tap, Block day, Undo leaves no block; a change made offline shows \"1 change waiting\" and is sent on reconnect";
542
+ }];
543
+ readonly offer: {
544
+ readonly pattern: "settings";
545
+ readonly when: "The client takes bookings for something with dates: a venue, a room, a resort day pass, a photographer's or hire business's days, or wants prices that depend on the day, season, holiday, number of guests or kind of event, or wants a calendar to block dates.";
546
+ readonly say: "I'll set this up with Workbench availability, so every price and free date comes out the same way each time, explained line by line, and you get a calendar to block dates from your phone. Here's how a business like yours usually books; tell me what to change.";
547
+ readonly answers_file: "workbench/availability.json";
548
+ readonly answers_format: "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"what\": \"<the client's words>\", \"stay_types\": [{ \"id\", \"start\", \"end\", \"clashes\" }], \"event_types\": [...], \"currency\": \"PHP\", \"alternatives\": { \"count\": 3, \"within_days\": 21 }, \"hold_days\": 3, \"holidays\": \"weekend\" | \"weekday\" | \"separate\" }`. Every stay type clashes with itself; list each pair that can't share a date on both sides. Take `currency` from workbench/country.json. checkSettings() reads this file at startup and names every problem. Each account's resources, packages and price rules are data in the app's database (availability_resources, availability_packages, availability_price_rules), entered on the owner's pages, never in this file. Copy availabilityMigration into supabase/migrations with its down file, and CALENDAR_WORDS into the strings catalogue.";
549
+ readonly questions: readonly [{
550
+ readonly id: "availability.what";
551
+ readonly sets: "availability.what";
552
+ readonly ask: "What do people book?";
553
+ readonly style: "table";
554
+ readonly other: {
555
+ readonly kind: "text";
556
+ readonly min: 3;
557
+ readonly max: 200;
558
+ readonly description: "In the client's words, such as \"a resort's pool and function hall\" or \"me, as a photographer, by the half or full day\".";
559
+ };
560
+ readonly must_ask: true;
561
+ readonly note: "Only the client knows. Their answer names the resources (one per thing booked separately) and suggests the stay types to propose next.";
562
+ }, {
563
+ readonly id: "availability.stays";
564
+ readonly sets: "availability.stay_types";
565
+ readonly ask: "Here are the usual ways it's booked in a day, with their hours and which can't share a date. Accept them, change some, or add or remove?";
566
+ readonly style: "table";
567
+ readonly other: {
568
+ readonly kind: "list";
569
+ readonly min: 1;
570
+ readonly max: 10;
571
+ readonly description: "Each way it's booked, with its start and end time and the others it can't share a date with.";
572
+ };
573
+ readonly must_ask: true;
574
+ readonly note: "Propose from the client's answer to availability.what. A resort or venue: day use 08:00 to 17:00, night use 19:00 to 07:00, overnight 14:00 to 12:00 the next day (overnight clashes with both; day and night use don't clash with each other). A photographer or hire business: half day and full day, clashing with each other. One room or slot a day: a single stay type.";
575
+ }, {
576
+ readonly id: "availability.events";
577
+ readonly sets: "availability.event_types";
578
+ readonly ask: "Should prices depend on the kind of event? Here is the usual list.";
579
+ readonly style: "cards";
580
+ readonly answers: readonly [{
581
+ readonly label: "Yes, use this list";
582
+ readonly value: "list";
583
+ readonly means: "Birthday, debut, wedding, baptism, reunion, team building, other: a rule can add a fee for some of them.";
584
+ }, {
585
+ readonly label: "No, same price for all";
586
+ readonly value: "none";
587
+ readonly means: "No event types; the quote never asks for one.";
588
+ }, {
589
+ readonly label: "Yes, with my own list";
590
+ readonly value: "own";
591
+ readonly means: "Type the kinds of event the business prices differently.";
592
+ }];
593
+ readonly other: {
594
+ readonly kind: "list";
595
+ readonly min: 1;
596
+ readonly max: 20;
597
+ readonly description: "The kinds of event, each a short name.";
598
+ };
599
+ readonly note: "Recommend the usual list for venues and resorts; \"none\" for most other businesses. Write ids in lower case with underscores (team_building).";
600
+ }, {
601
+ readonly id: "availability.holidays";
602
+ readonly sets: "availability.holidays";
603
+ readonly ask: "Should holidays be priced like weekends?";
604
+ readonly style: "cards";
605
+ readonly answers: readonly [{
606
+ readonly label: "Yes, like a weekend";
607
+ readonly value: "weekend";
608
+ readonly means: "A regular holiday or special non-working day on a weekday costs the weekend price.";
609
+ }, {
610
+ readonly label: "No, by the day of the week";
611
+ readonly value: "weekday";
612
+ readonly means: "Holidays cost what their day of the week costs.";
613
+ }, {
614
+ readonly label: "Set holiday prices separately";
615
+ readonly value: "separate";
616
+ readonly means: "Holidays get their own price rules, entered on the owner's pages.";
617
+ }];
618
+ readonly other: {
619
+ readonly kind: "text";
620
+ readonly min: 3;
621
+ readonly max: 200;
622
+ readonly description: "How holidays should be priced, in the client's words.";
623
+ };
624
+ readonly note: "Holidays come from workbench-country's yearly list (the Philippines' proclamation each year); a local fiesta goes in the availability_holidays table. Special working days (the EDSA anniversary) are never priced as holidays.";
625
+ }, {
626
+ readonly id: "availability.holds";
627
+ readonly sets: "availability.hold_days";
628
+ readonly ask: "How long is a date held while waiting for a deposit?";
629
+ readonly style: "cards";
630
+ readonly answers: readonly [{
631
+ readonly label: "3 days";
632
+ readonly value: 3;
633
+ readonly means: "The date shows as held (H) for 3 days, then is free again unless marked booked.";
634
+ }, {
635
+ readonly label: "1 day";
636
+ readonly value: 1;
637
+ }, {
638
+ readonly label: "7 days";
639
+ readonly value: 7;
640
+ }, {
641
+ readonly label: "No holds";
642
+ readonly value: 0;
643
+ readonly means: "A date is free until it is booked; nothing is held.";
644
+ }];
645
+ readonly other: {
646
+ readonly kind: "whole";
647
+ readonly min: 0;
648
+ readonly max: 60;
649
+ readonly description: "Days, 0 to 60.";
650
+ };
651
+ }];
652
+ readonly not_asked: readonly [{
653
+ readonly sets: "availability.prices";
654
+ readonly why: "Each account's prices, packages and rules change often, so they are data the owner enters on their own pages (or that onboarding reads from their rate card), not project settings.";
655
+ }, {
656
+ readonly sets: "availability.alternatives";
657
+ readonly why: "Up to 3 nearest free dates within 21 days, same weekday first, suits every business so far; change it in the answers file if a client asks.";
658
+ }, {
659
+ readonly sets: "availability.currency";
660
+ readonly why: "It is the country's own, from workbench/country.json.";
661
+ }];
662
+ readonly needs: readonly ["country"];
663
+ };
664
+ readonly styles: {
665
+ readonly prefix: "wb-cal-";
666
+ };
667
+ readonly delivery: {
668
+ readonly choice: "optional";
669
+ readonly intro: "Which dates are free and what each costs, worked out the same way every time, with a calendar the owner taps to block dates.";
670
+ };
671
+ };
457
672
  readonly config: {
458
673
  readonly $schema: "./schema.json";
459
674
  readonly format: 1;
@@ -552,7 +767,7 @@ export declare const capabilities: {
552
767
  readonly summary: "workbench-country writes dates, times, numbers and money the way readers in the Philippines, Australia, the UK and the US expect, from one pack per country, and gives each pack's spelling for the Gateway. It runs in the app itself, with no Gateway call and no cost.";
553
768
  readonly status: "built";
554
769
  readonly surfaces: {
555
- readonly package: readonly ["COUNTRY_PACKS", "countryPackFor", "audiencePack", "checkPack", "CountryPackError", "formatDate", "formatDateTime", "calendarDayIn", "formatNumber", "formatMoney", "formattingLocale", "currencySymbol", "currencySymbolFrom", "WORLDWIDE", "isWorldwide"];
770
+ readonly package: readonly ["COUNTRY_PACKS", "countryPackFor", "audiencePack", "checkPack", "CountryPackError", "formatDate", "formatDateTime", "calendarDayIn", "formatNumber", "formatMoney", "formattingLocale", "currencySymbol", "currencySymbolFrom", "WORLDWIDE", "isWorldwide", "HOLIDAYS", "holidaysFor", "holidayOn", "checkHolidays"];
556
771
  };
557
772
  readonly rules: readonly [{
558
773
  readonly id: "country.one-formatter";
@@ -572,6 +787,12 @@ export declare const capabilities: {
572
787
  readonly text: "Write an amount with a bare \"$\": Australian dollars are A$ and US dollars US$.";
573
788
  readonly enforced_by: "types";
574
789
  readonly probe: "country.packs";
790
+ }, {
791
+ readonly id: "country.holidays-from-proclamation";
792
+ readonly kind: "must";
793
+ readonly text: "Take public holidays from holidaysFor or holidayOn, never worked out or guessed: a year with no list returns undefined, and checkHolidays fails from 1 December until next year's list is in.";
794
+ readonly enforced_by: "review";
795
+ readonly probe: "country.holidays";
575
796
  }, {
576
797
  readonly id: "country.spelling-from-pack";
577
798
  readonly kind: "must";
@@ -599,6 +820,13 @@ export declare const capabilities: {
599
820
  readonly expect: "pass";
600
821
  readonly cost_hint_usd: 0;
601
822
  readonly note: "Every pack's spelling is one the Gateway accepts (en-PH, en-AU, en-GB, en-US), checked against the ai manifest's spellings; no Gateway call";
823
+ }, {
824
+ readonly id: "country.holidays";
825
+ readonly half: "runtime";
826
+ readonly expect: "pass";
827
+ readonly smoke: true;
828
+ readonly cost_hint_usd: 0;
829
+ readonly note: "The December probe: checkHolidays for every country with a built-in list, on today in that country's time zone. Fails when this year's list is missing, and from 1 December when next year's is, so the list is added from the proclamation before January's prices need it";
602
830
  }];
603
831
  readonly offer: {
604
832
  readonly pattern: "project";
@@ -1,6 +1,6 @@
1
1
  // Generated by `npm run capabilities:build` from capabilities/*.json. Do not edit.
2
2
  // check:capabilities fails in Checks when this file is stale.
3
- export const MANIFEST_VERSION = "de365af27674";
3
+ export const MANIFEST_VERSION = "9aae488ad117";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -627,6 +627,271 @@ export const capabilities = {
627
627
  "intro": "Counting page views and the things visitors do on the site."
628
628
  }
629
629
  },
630
+ "availability": {
631
+ "$schema": "./schema.json",
632
+ "format": 1,
633
+ "component": "availability",
634
+ "title": "Availability and pricing",
635
+ "summary": "workbench-availability answers \"is this date free, and what does it cost?\" for a bookable thing (a venue, a room, a photographer's day) from its calendar and its price rules, with the same answer every time and one plain line per rule applied. A store over the app's own database stops two bookings landing on one date, and a tap-to-block calendar lets the owner close dates from a phone. It runs in the app itself, with no Gateway call, no AI and no cost.",
636
+ "status": "built",
637
+ "surfaces": {
638
+ "package": [
639
+ "quote",
640
+ "createAvailability",
641
+ "availabilityMigration",
642
+ "calendarAction",
643
+ "checkSettings",
644
+ "stayClaims",
645
+ "staysClash",
646
+ "todayIn",
647
+ "AvailabilityError"
648
+ ]
649
+ },
650
+ "rules": [
651
+ {
652
+ "id": "availability.quote-is-fact",
653
+ "kind": "must",
654
+ "text": "Take every availability answer and price the app shows or sends from quote() (or createAvailability's quote), and pass its lines and explain on as facts. Never let AI work out a price or a free date, and never write a price into a page or prompt by hand.",
655
+ "enforced_by": "review",
656
+ "probe": "availability.quote"
657
+ },
658
+ {
659
+ "id": "availability.book-through-block",
660
+ "kind": "must",
661
+ "text": "Book, hold and block dates only through createAvailability's block(), which runs availability_block() in one transaction: never insert into availability_blocks directly, so the exclusion constraint and the hold expiry always apply.",
662
+ "enforced_by": "database",
663
+ "probe": "availability.store"
664
+ },
665
+ {
666
+ "id": "availability.calendar-days",
667
+ "kind": "must",
668
+ "text": "Treat dates as calendar days (YYYY-MM-DD) in the account's time zone: pass todayIn(timeZone) as today, never new Date() in UTC.",
669
+ "enforced_by": "types",
670
+ "probe": "availability.quote"
671
+ },
672
+ {
673
+ "id": "availability.no-names-in-notes",
674
+ "kind": "must_not",
675
+ "text": "Write a guest's name or phone number into a block's note: link the app's own booking in subject instead. The .ics feed carries neither.",
676
+ "enforced_by": "review",
677
+ "probe": "availability.store"
678
+ },
679
+ {
680
+ "id": "availability.public-gets-the-quote",
681
+ "kind": "must_not",
682
+ "text": "Let a public page or the anonymous key read availability_blocks: public pages get the finished quote from the server.",
683
+ "enforced_by": "database",
684
+ "probe": "availability.store"
685
+ },
686
+ {
687
+ "id": "availability.daily-cleanup",
688
+ "kind": "must",
689
+ "text": "Run createAvailability's cleanup() once a day from the app's cron with the service client, so lapsed holds show as expired and old blocks are deleted on time.",
690
+ "enforced_by": "review",
691
+ "probe": "availability.store"
692
+ },
693
+ {
694
+ "id": "availability.calendar-as-shipped",
695
+ "kind": "must",
696
+ "text": "Draw the owner's calendar with renderCalendar and CALENDAR_SCRIPT (under the page's nonce), and hand the posted body to calendarAction behind the owner's sign-in. Never hand-write the calendar's markup or script.",
697
+ "enforced_by": "review",
698
+ "probe": "availability.calendar"
699
+ }
700
+ ],
701
+ "examples": [
702
+ {
703
+ "id": "availability.example.quote",
704
+ "title": "Answer \"is 12 Dec free, and how much?\"",
705
+ "lang": "ts",
706
+ "code": "import { createAvailability } from '@mccaskie-labs/workbench-availability'\nimport settings from '../workbench/availability.json'\n\nconst av = createAvailability({ db: serviceClient, account: venue.id, settings, country: 'PH', timeZone: 'Asia/Manila' })\nconst q = await av.quote({ resource: 'villa-sol-pool', date: '2026-12-12', stay: 'day_use', guests: { min: 15, max: 15 }, eventType: 'birthday' })\n// q.available 'yes'; q.options[0].total { amount: '9500.00', currency: 'PHP' }; q.explain ['Weekend day use: ₱9,500 (rule r-weekend)', ...]",
707
+ "probe": "availability.quote"
708
+ },
709
+ {
710
+ "id": "availability.example.calendar",
711
+ "title": "The owner's tap-to-block calendar",
712
+ "lang": "ts",
713
+ "code": "import { calendarAction } from '@mccaskie-labs/workbench-availability'\nimport { CALENDAR_SCRIPT, renderCalendar } from '@mccaskie-labs/workbench-availability/calendar'\n\nconst days = await av.month('villa-sol-pool', '2026-12')\nconst html = renderCalendar(days, t, { resource: 'villa-sol-pool', month: '2026-12', endpoint: '/owner/calendar', stays: [{ id: 'day_use', label: t('ui.stay.day_use')! }] })\n// The page adds <script nonce=...>${CALENDAR_SCRIPT}</script>; POST /owner/calendar (owner signed in):\nconst r = await calendarAction(av, await request.json())\nreturn Response.json(r.body, { status: r.status })",
714
+ "probe": "availability.calendar"
715
+ }
716
+ ],
717
+ "probes": [
718
+ {
719
+ "id": "availability.quote",
720
+ "half": "runtime",
721
+ "expect": "pass",
722
+ "smoke": true,
723
+ "cost_hint_usd": 0,
724
+ "note": "In the Worker itself: quote() on the spec's HmPo example (Sat 12 Dec ₱9,500, Sat 19 Dec ₱10,925 with peak, 25 guests ₱10,250 over capacity, no guests a from price) and the alternatives when 19 Dec is booked; no database, no Gateway call"
725
+ },
726
+ {
727
+ "id": "availability.store",
728
+ "half": "runtime",
729
+ "expect": "pass",
730
+ "cost_hint_usd": 0,
731
+ "note": "Against the probe app's own database: two block(booked) calls for one night at once (one lands, one refused as clash), a lapsed hold quoted free and marked expired by cleanup(), another account's resource unreachable, then every probe block removed"
732
+ },
733
+ {
734
+ "id": "availability.calendar",
735
+ "half": "runtime",
736
+ "expect": "pass",
737
+ "cost_hint_usd": 0,
738
+ "note": "The probe page draws a month with renderCalendar and CALENDAR_SCRIPT: tap, Block day, Undo leaves no block; a change made offline shows \"1 change waiting\" and is sent on reconnect"
739
+ }
740
+ ],
741
+ "offer": {
742
+ "pattern": "settings",
743
+ "when": "The client takes bookings for something with dates: a venue, a room, a resort day pass, a photographer's or hire business's days, or wants prices that depend on the day, season, holiday, number of guests or kind of event, or wants a calendar to block dates.",
744
+ "say": "I'll set this up with Workbench availability, so every price and free date comes out the same way each time, explained line by line, and you get a calendar to block dates from your phone. Here's how a business like yours usually books; tell me what to change.",
745
+ "answers_file": "workbench/availability.json",
746
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"what\": \"<the client's words>\", \"stay_types\": [{ \"id\", \"start\", \"end\", \"clashes\" }], \"event_types\": [...], \"currency\": \"PHP\", \"alternatives\": { \"count\": 3, \"within_days\": 21 }, \"hold_days\": 3, \"holidays\": \"weekend\" | \"weekday\" | \"separate\" }`. Every stay type clashes with itself; list each pair that can't share a date on both sides. Take `currency` from workbench/country.json. checkSettings() reads this file at startup and names every problem. Each account's resources, packages and price rules are data in the app's database (availability_resources, availability_packages, availability_price_rules), entered on the owner's pages, never in this file. Copy availabilityMigration into supabase/migrations with its down file, and CALENDAR_WORDS into the strings catalogue.",
747
+ "questions": [
748
+ {
749
+ "id": "availability.what",
750
+ "sets": "availability.what",
751
+ "ask": "What do people book?",
752
+ "style": "table",
753
+ "other": {
754
+ "kind": "text",
755
+ "min": 3,
756
+ "max": 200,
757
+ "description": "In the client's words, such as \"a resort's pool and function hall\" or \"me, as a photographer, by the half or full day\"."
758
+ },
759
+ "must_ask": true,
760
+ "note": "Only the client knows. Their answer names the resources (one per thing booked separately) and suggests the stay types to propose next."
761
+ },
762
+ {
763
+ "id": "availability.stays",
764
+ "sets": "availability.stay_types",
765
+ "ask": "Here are the usual ways it's booked in a day, with their hours and which can't share a date. Accept them, change some, or add or remove?",
766
+ "style": "table",
767
+ "other": {
768
+ "kind": "list",
769
+ "min": 1,
770
+ "max": 10,
771
+ "description": "Each way it's booked, with its start and end time and the others it can't share a date with."
772
+ },
773
+ "must_ask": true,
774
+ "note": "Propose from the client's answer to availability.what. A resort or venue: day use 08:00 to 17:00, night use 19:00 to 07:00, overnight 14:00 to 12:00 the next day (overnight clashes with both; day and night use don't clash with each other). A photographer or hire business: half day and full day, clashing with each other. One room or slot a day: a single stay type."
775
+ },
776
+ {
777
+ "id": "availability.events",
778
+ "sets": "availability.event_types",
779
+ "ask": "Should prices depend on the kind of event? Here is the usual list.",
780
+ "style": "cards",
781
+ "answers": [
782
+ {
783
+ "label": "Yes, use this list",
784
+ "value": "list",
785
+ "means": "Birthday, debut, wedding, baptism, reunion, team building, other: a rule can add a fee for some of them."
786
+ },
787
+ {
788
+ "label": "No, same price for all",
789
+ "value": "none",
790
+ "means": "No event types; the quote never asks for one."
791
+ },
792
+ {
793
+ "label": "Yes, with my own list",
794
+ "value": "own",
795
+ "means": "Type the kinds of event the business prices differently."
796
+ }
797
+ ],
798
+ "other": {
799
+ "kind": "list",
800
+ "min": 1,
801
+ "max": 20,
802
+ "description": "The kinds of event, each a short name."
803
+ },
804
+ "note": "Recommend the usual list for venues and resorts; \"none\" for most other businesses. Write ids in lower case with underscores (team_building)."
805
+ },
806
+ {
807
+ "id": "availability.holidays",
808
+ "sets": "availability.holidays",
809
+ "ask": "Should holidays be priced like weekends?",
810
+ "style": "cards",
811
+ "answers": [
812
+ {
813
+ "label": "Yes, like a weekend",
814
+ "value": "weekend",
815
+ "means": "A regular holiday or special non-working day on a weekday costs the weekend price."
816
+ },
817
+ {
818
+ "label": "No, by the day of the week",
819
+ "value": "weekday",
820
+ "means": "Holidays cost what their day of the week costs."
821
+ },
822
+ {
823
+ "label": "Set holiday prices separately",
824
+ "value": "separate",
825
+ "means": "Holidays get their own price rules, entered on the owner's pages."
826
+ }
827
+ ],
828
+ "other": {
829
+ "kind": "text",
830
+ "min": 3,
831
+ "max": 200,
832
+ "description": "How holidays should be priced, in the client's words."
833
+ },
834
+ "note": "Holidays come from workbench-country's yearly list (the Philippines' proclamation each year); a local fiesta goes in the availability_holidays table. Special working days (the EDSA anniversary) are never priced as holidays."
835
+ },
836
+ {
837
+ "id": "availability.holds",
838
+ "sets": "availability.hold_days",
839
+ "ask": "How long is a date held while waiting for a deposit?",
840
+ "style": "cards",
841
+ "answers": [
842
+ {
843
+ "label": "3 days",
844
+ "value": 3,
845
+ "means": "The date shows as held (H) for 3 days, then is free again unless marked booked."
846
+ },
847
+ {
848
+ "label": "1 day",
849
+ "value": 1
850
+ },
851
+ {
852
+ "label": "7 days",
853
+ "value": 7
854
+ },
855
+ {
856
+ "label": "No holds",
857
+ "value": 0,
858
+ "means": "A date is free until it is booked; nothing is held."
859
+ }
860
+ ],
861
+ "other": {
862
+ "kind": "whole",
863
+ "min": 0,
864
+ "max": 60,
865
+ "description": "Days, 0 to 60."
866
+ }
867
+ }
868
+ ],
869
+ "not_asked": [
870
+ {
871
+ "sets": "availability.prices",
872
+ "why": "Each account's prices, packages and rules change often, so they are data the owner enters on their own pages (or that onboarding reads from their rate card), not project settings."
873
+ },
874
+ {
875
+ "sets": "availability.alternatives",
876
+ "why": "Up to 3 nearest free dates within 21 days, same weekday first, suits every business so far; change it in the answers file if a client asks."
877
+ },
878
+ {
879
+ "sets": "availability.currency",
880
+ "why": "It is the country's own, from workbench/country.json."
881
+ }
882
+ ],
883
+ "needs": [
884
+ "country"
885
+ ]
886
+ },
887
+ "styles": {
888
+ "prefix": "wb-cal-"
889
+ },
890
+ "delivery": {
891
+ "choice": "optional",
892
+ "intro": "Which dates are free and what each costs, worked out the same way every time, with a calendar the owner taps to block dates."
893
+ }
894
+ },
630
895
  "config": {
631
896
  "$schema": "./schema.json",
632
897
  "format": 1,
@@ -781,7 +1046,11 @@ export const capabilities = {
781
1046
  "currencySymbol",
782
1047
  "currencySymbolFrom",
783
1048
  "WORLDWIDE",
784
- "isWorldwide"
1049
+ "isWorldwide",
1050
+ "HOLIDAYS",
1051
+ "holidaysFor",
1052
+ "holidayOn",
1053
+ "checkHolidays"
785
1054
  ]
786
1055
  },
787
1056
  "rules": [
@@ -806,6 +1075,13 @@ export const capabilities = {
806
1075
  "enforced_by": "types",
807
1076
  "probe": "country.packs"
808
1077
  },
1078
+ {
1079
+ "id": "country.holidays-from-proclamation",
1080
+ "kind": "must",
1081
+ "text": "Take public holidays from holidaysFor or holidayOn, never worked out or guessed: a year with no list returns undefined, and checkHolidays fails from 1 December until next year's list is in.",
1082
+ "enforced_by": "review",
1083
+ "probe": "country.holidays"
1084
+ },
809
1085
  {
810
1086
  "id": "country.spelling-from-pack",
811
1087
  "kind": "must",
@@ -838,6 +1114,14 @@ export const capabilities = {
838
1114
  "expect": "pass",
839
1115
  "cost_hint_usd": 0,
840
1116
  "note": "Every pack's spelling is one the Gateway accepts (en-PH, en-AU, en-GB, en-US), checked against the ai manifest's spellings; no Gateway call"
1117
+ },
1118
+ {
1119
+ "id": "country.holidays",
1120
+ "half": "runtime",
1121
+ "expect": "pass",
1122
+ "smoke": true,
1123
+ "cost_hint_usd": 0,
1124
+ "note": "The December probe: checkHolidays for every country with a built-in list, on today in that country's time zone. Fails when this year's list is missing, and from 1 December when next year's is, so the list is added from the proclamation before January's prices need it"
841
1125
  }
842
1126
  ],
843
1127
  "offer": {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms", "sign-in"];
2
+ export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms", "sign-in", "availability"];
3
3
  export type Component = (typeof COMPONENTS)[number];
4
4
  export declare const allowedSchemas: {
5
5
  gateway: z.ZodObject<{
@@ -163,6 +163,7 @@ export declare const capabilitySchema: z.ZodObject<{
163
163
  access: "access";
164
164
  ai: "ai";
165
165
  analytics: "analytics";
166
+ availability: "availability";
166
167
  config: "config";
167
168
  country: "country";
168
169
  database: "database";
@@ -294,6 +295,7 @@ export declare const capabilitySchema: z.ZodObject<{
294
295
  access: "access";
295
296
  ai: "ai";
296
297
  analytics: "analytics";
298
+ availability: "availability";
297
299
  config: "config";
298
300
  country: "country";
299
301
  database: "database";
@@ -26,6 +26,7 @@ export const COMPONENTS = [
26
26
  "ui",
27
27
  "forms",
28
28
  "sign-in",
29
+ "availability",
29
30
  ];
30
31
  const id = z.string().regex(/^[a-z0-9][a-z0-9.-]*$/, "lower case, digits, dots and hyphens");
31
32
  const code = z.string().regex(/^[a-z][a-z0-9_]*$/, "a snake_case error code");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",