@murumets-ee/yhikas-sync 0.92.0 → 0.94.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 (57) hide show
  1. package/README.md +108 -0
  2. package/dist/client-J_w85GUr.mjs +2 -0
  3. package/dist/client-J_w85GUr.mjs.map +1 -0
  4. package/dist/config-DM3zAAhm.d.mts +244 -0
  5. package/dist/config-DM3zAAhm.d.mts.map +1 -0
  6. package/dist/constants-Bmm3P17G.mjs +2 -0
  7. package/dist/constants-Bmm3P17G.mjs.map +1 -0
  8. package/dist/en-BOWwWf-X.mjs +2 -0
  9. package/dist/en-BOWwWf-X.mjs.map +1 -0
  10. package/dist/et-CGDUOj83.mjs +2 -0
  11. package/dist/et-CGDUOj83.mjs.map +1 -0
  12. package/dist/i18n.d.mts +18 -0
  13. package/dist/i18n.d.mts.map +1 -0
  14. package/dist/i18n.mjs +2 -0
  15. package/dist/i18n.mjs.map +1 -0
  16. package/dist/index.d.mts +360 -900
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +2 -1
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/jobs-Di4Z8go9.mjs +2 -0
  21. package/dist/jobs-Di4Z8go9.mjs.map +1 -0
  22. package/dist/plugin.d.mts +6 -4
  23. package/dist/plugin.d.mts.map +1 -1
  24. package/dist/plugin.mjs +1 -1
  25. package/dist/plugin.mjs.map +1 -1
  26. package/dist/rolldown-runtime-DK3Fl9T5.mjs +1 -0
  27. package/dist/room-page-registry-BFbyGTC7.mjs +2 -0
  28. package/dist/room-page-registry-BFbyGTC7.mjs.map +1 -0
  29. package/dist/room-page-registry-CnqXH_Ls.d.mts +974 -0
  30. package/dist/room-page-registry-CnqXH_Ls.d.mts.map +1 -0
  31. package/dist/room-pages-adapters-DxFWkFFF.mjs +2 -0
  32. package/dist/room-pages-adapters-DxFWkFFF.mjs.map +1 -0
  33. package/dist/room-pages-table-Vd9BqTSl.mjs +2 -0
  34. package/dist/room-pages-table-Vd9BqTSl.mjs.map +1 -0
  35. package/dist/room-type-DtMaQbza.mjs +2 -0
  36. package/dist/room-type-DtMaQbza.mjs.map +1 -0
  37. package/dist/ru-uzvamDhj.mjs +2 -0
  38. package/dist/ru-uzvamDhj.mjs.map +1 -0
  39. package/dist/sync-state-table-CZjRypK0.mjs +2 -0
  40. package/dist/sync-state-table-CZjRypK0.mjs.map +1 -0
  41. package/dist/widgets-client.d.mts +27 -0
  42. package/dist/widgets-client.d.mts.map +1 -0
  43. package/dist/widgets-client.mjs +3 -0
  44. package/dist/widgets-client.mjs.map +1 -0
  45. package/dist/widgets.d.mts +73 -0
  46. package/dist/widgets.d.mts.map +1 -0
  47. package/dist/widgets.mjs +2 -0
  48. package/dist/widgets.mjs.map +1 -0
  49. package/package.json +41 -7
  50. package/dist/config-CHVLiXZf.d.mts +0 -77
  51. package/dist/config-CHVLiXZf.d.mts.map +0 -1
  52. package/dist/jobs-QeVJDpW9.mjs +0 -2
  53. package/dist/jobs-QeVJDpW9.mjs.map +0 -1
  54. package/dist/sync-state-table-BrcoCu-Z.mjs +0 -2
  55. package/dist/sync-state-table-BrcoCu-Z.mjs.map +0 -1
  56. package/dist/watchdog-CcOXpzE3.mjs +0 -2
  57. package/dist/watchdog-CcOXpzE3.mjs.map +0 -1
package/dist/index.d.mts CHANGED
@@ -1,768 +1,9 @@
1
- import { i as resolveYhikasSyncConfig, n as YhikasSyncConfig, r as YhikasSyncConfigError, t as ResolvedYhikasSyncConfig } from "./config-CHVLiXZf.mjs";
1
+ import { $ as EN_LOCALE, A as secondaryLocaleOf, B as roomTypesResponseSchema, C as ProjectedRow, D as projectLegalDocuments, E as SkippedRow, F as SiteNoticeRow, G as PlanDiffInput, H as siteNoticeRowSchema, I as legalDocumentRowSchema, J as stableHash, K as SanityFloor, L as legalDocumentsResponseSchema, M as MultilingualText, N as RoomTypeRow, O as projectRoomTypes, P as SiteInfoResponse, Q as DECLARED_VAT_TREATMENT, R as multilingualTextSchema, S as HtmlSanitizer, T as ProjectionResult, U as DiffPlan, V as siteInfoResponseSchema, W as LocalRow, X as BASE_URL_ENV_VAR, Y as API_KEY_ENV_VAR, Z as DECLARED_CURRENCY, _ as SyncEntityClient, a as YhikasRoomPageRecordError, at as SITE_INFO_RESOURCE, b as applyPlan, c as ROOM_PAGE_ORIGINS, ct as SyncResource, d as RoomPageRefusalKind, dt as YHIKAS_SYNC_PLUGIN_NAME, et as ET_LOCALE, f as yhikasRoomPagesTable, g as ApplyPlanInput, h as ApplyCounts, i as RoomPageRegistry, it as SITE_INFO_KEY, j as LegalDocumentRow, k as projectSiteInfo, l as ROOM_PAGE_REFUSAL_KINDS, lt as WATCHDOG_JOB_NAME, m as toSyncEntityClient, n as RoomPageRecord, nt as MAX_SITE_NOTICES, o as createRoomPageRegistry, ot as SYNC_JOB_NAME, p as AdminClientLike, q as planDiff, r as RoomPageRefusal, rt as ROOM_TYPES_RESOURCE, s as ROOM_PAGE_CLAIM_LEASE_MS, st as SYNC_RESOURCES, t as ROOM_PAGE_READ_CHUNK, tt as LEGAL_DOCUMENTS_RESOURCE, u as RoomPageOrigin, ut as YHIKAS_SYNC_ACTOR_ID, v as SyncLogger, w as ProjectionOptions, x as readLocalRows, y as YhikasApplyPartialError, z as roomTypeRowSchema } from "./room-page-registry-CnqXH_Ls.mjs";
2
+ import { a as RoomPageDraft, c as YhikasSyncConfigError, d as isPublishedRoomType, f as roomTypeForPageFrom, g as isRoomOffer, h as RoomOfferPredicate, i as RoomPageBlock, l as resolveYhikasSyncConfig, m as RoomOfferCandidate, n as ResolvedRoomPagesConfig, o as YhikasRoomPagesConfig, p as AREA_PRICED_UNIT_NAMES, r as ResolvedYhikasSyncConfig, s as YhikasSyncConfig, t as PAGE_SLUG_PATTERN, u as RoomTypeForPage } from "./config-DM3zAAhm.mjs";
2
3
  import { Behavior } from "@murumets-ee/entity";
3
- import { z } from "zod";
4
- import { ToolkitApp } from "@murumets-ee/core";
4
+ import { PgColumn } from "drizzle-orm/pg-core";
5
+ import { LiveFactSpec, LiveFreshness, LiveItem, LiveSourceSpec } from "@murumets-ee/blocks/schema";
5
6
 
6
- //#region src/constants.d.ts
7
- /**
8
- * Names, identifiers and stated numbers for the yhikas-admin sync.
9
- *
10
- * Pure module — no imports, no side effects. Both the jiti-loaded plugin entry
11
- * and the worker-side job modules read from here, so nothing in this file may
12
- * reach a `server-only` module.
13
- */
14
- /** Plugin name, as it appears in `app.plugins` and `Plugin.requires`. */
15
- declare const YHIKAS_SYNC_PLUGIN_NAME = "@murumets-ee/yhikas-sync";
16
- /**
17
- * The synthetic actor every sync write is attributed to.
18
- *
19
- * Not `'cli'`. `auditable()` records this in `created_by`/`updated_by`, so a
20
- * human reading the audit log can tell a sync write from an operator running a
21
- * `lumi` command — and the machine-written guard (see `entities/guard.ts`) can
22
- * refuse every other writer by name. Fits `varchar(255)`; user ids in this
23
- * codebase are varchar, never uuid.
24
- */
25
- declare const YHIKAS_SYNC_ACTOR_ID = "yhikas-sync";
26
- /**
27
- * Job names. `JOB_NAME_RE` in `@murumets-ee/queue` is
28
- * `/^[a-zA-Z0-9:_-]{1,128}$/` — colons are the `plugin:action` separator and
29
- * dots are rejected outright.
30
- */
31
- declare const SYNC_JOB_NAME = "yhikas-sync:pull";
32
- declare const WATCHDOG_JOB_NAME = "yhikas-sync:staleness-watchdog";
33
- /**
34
- * The three resources in scope.
35
- *
36
- * `site_info` was deliberately absent until 2026-08-11 (D027). PR 05 left it
37
- * out on a MEASUREMENT — `site_info`/`site_notice` appear only in
38
- * yhikas-admin's `src/db/schema.ts` and its public route, with no admin UI, no
39
- * server action, no seed and no internal consumer — from which the plan
40
- * concluded that no rows existed and a sync would pull nothing.
41
- *
42
- * Measured against production on 2026-08-11 (R028), that premise had expired:
43
- * the route answers 200 with populated reception hours and two live notices.
44
- * The rows were written outside the app. Hardcoding them in the rebuilt site
45
- * would therefore have frozen live data, not stood in for an empty upstream.
46
- */
47
- declare const ROOM_TYPES_RESOURCE = "room_types";
48
- declare const LEGAL_DOCUMENTS_RESOURCE = "legal_documents";
49
- declare const SITE_INFO_RESOURCE = "site_info";
50
- declare const SYNC_RESOURCES: readonly ["room_types", "legal_documents", "site_info"];
51
- type SyncResource = (typeof SYNC_RESOURCES)[number];
52
- /**
53
- * The business key of the one `yhikas_site_info` row.
54
- *
55
- * Upstream `site_info` is a singleton and its notices carry no id or code of
56
- * their own — only `{text, isActive, order}` — so there is no natural key to
57
- * diff N rows on. Modelling it as ONE row with a constant key keeps the whole
58
- * package on a single identity story (`code`, `type`, and now `key`), and
59
- * keeps the notices ordered without inventing an identity that upstream would
60
- * not preserve across an edit.
61
- */
62
- declare const SITE_INFO_KEY = "default";
63
- /**
64
- * Locale codes. Upstream `MultilingualText` is `{ en, et }` and the lumi codes
65
- * are identical, so there is no mapping to configure — inventing one would be
66
- * a seam for a second customer this package does not have (N1).
67
- */
68
- declare const ET_LOCALE = "et";
69
- declare const EN_LOCALE = "en";
70
- /**
71
- * Currency and VAT treatment, DECLARED here because they are not data upstream
72
- * (R009 §6 / R013): `room_type` has no currency column and no tax column, and
73
- * VAT is computed at invoice time against Merit article codes, never stored.
74
- * Stating them makes a mis-entered source value render as a visibly wrong
75
- * number against a declared unit rather than as a plausible one.
76
- *
77
- * The third semantic — the period — stays encoded in the field NAME
78
- * (`monthlyRent` / `dailyRent`), because one row carries two different periods
79
- * and a single `period` column could only be wrong about one of them.
80
- */
81
- declare const DECLARED_CURRENCY = "EUR";
82
- /** `net` = the amount excludes VAT. */
83
- declare const DECLARED_VAT_TREATMENT = "net";
84
- /**
85
- * Hard ceiling on ticker items. A ticker is a marquee: past a couple of dozen
86
- * entries nobody reads the tail, and an unbounded array from upstream would
87
- * ride into a JSONB column and out onto every page of the site.
88
- */
89
- declare const MAX_SITE_NOTICES = 25;
90
- /**
91
- * Env var carrying the sync's OWN bearer credential — deliberately not the
92
- * site's `PUBLIC_SITE_API_KEY`. The upstream limiter buckets on
93
- * `sha256(key).slice(0,16)` rather than on the caller IP (there is no
94
- * `x-forwarded-for` over the Docker-internal path), so sharing a key means
95
- * sharing one 60/min bucket, and revoking one credential would blind both.
96
- */
97
- declare const API_KEY_ENV_VAR = "YHIKAS_SYNC_API_KEY";
98
- declare const BASE_URL_ENV_VAR = "YHIKAS_ADMIN_BASE_URL";
99
- //#endregion
100
- //#region src/diff.d.ts
101
- /** One local row, reduced to what the diff needs. */
102
- interface LocalRow {
103
- readonly id: string;
104
- /** The business key this row was synced under. */
105
- readonly key: string;
106
- readonly status: string;
107
- /**
108
- * `null` on a row whose last sync did not complete — the hash is written
109
- * LAST, after every other write for the row succeeded, so a null (or stale)
110
- * hash is exactly the signal that the row needs redoing.
111
- */
112
- readonly sourceHash: string | null;
113
- /** Preserved across updates so it keeps meaning "first published". */
114
- readonly publishedAt: Date | null;
115
- }
116
- interface DiffPlan<T> {
117
- /** Upstream rows with no local counterpart. */
118
- readonly create: readonly T[];
119
- /** Local rows whose hash differs, OR whose status drifted from `published`. */
120
- readonly update: readonly {
121
- readonly local: LocalRow;
122
- readonly row: T;
123
- }[];
124
- /** Local rows already identical and already published — no write at all. */
125
- readonly unchanged: readonly LocalRow[];
126
- /** Published locally, absent upstream. Unpublished, never deleted (D016). */
127
- readonly retire: readonly LocalRow[];
128
- }
129
- interface SanityFloor {
130
- /** Refuse a snapshot larger than this rather than truncating it. */
131
- readonly maxRows: number;
132
- /** Refuse a run retiring more than this fraction of published rows. */
133
- readonly maxRetireFraction: number;
134
- /** The fraction rule applies only once at least this many rows are published. */
135
- readonly minRowsForFraction: number;
136
- }
137
- interface PlanDiffInput<T> {
138
- readonly resource: SyncResource;
139
- readonly upstream: readonly T[];
140
- readonly local: readonly LocalRow[];
141
- readonly keyOf: (row: T) => string;
142
- readonly hashOf: (row: T) => string;
143
- readonly floor: SanityFloor;
144
- }
145
- /**
146
- * Stable content hash of an upstream row.
147
- *
148
- * Keys are sorted so the hash does not depend on JSON property order, which no
149
- * part of the HTTP stack guarantees. `undefined` and `null` are distinguished
150
- * because a null price is meaningful data here, not an absence.
151
- */
152
- declare function stableHash(value: unknown): string;
153
- /**
154
- * Compare an authoritative upstream snapshot against local state.
155
- *
156
- * **The caller must not invoke this with a snapshot that did not fully
157
- * succeed.** That guard lives one level up, in the client: a non-2xx, a
158
- * timeout or a body failing the wire schema throws before the differ is ever
159
- * reached, so a partial response can never present as an absence here. This
160
- * function's own guards are for a snapshot that IS authoritative but whose
161
- * shape makes acting on it reckless.
162
- *
163
- * @throws {YhikasSyncRefusedError} for any of the D016 refusals. Every one of
164
- * them aborts before a single write, so a refused run leaves local content
165
- * exactly as it was — which is the first of PR 05's three obligatory
166
- * negative tests.
167
- */
168
- declare function planDiff<T>(input: PlanDiffInput<T>): DiffPlan<T>;
169
- //#endregion
170
- //#region src/upstream/wire.d.ts
171
- /**
172
- * `MultilingualText` — a Postgres `json` column, so it arrives as a nested
173
- * object and is never stringified.
174
- *
175
- * NOT `.strict()`: unknown keys are stripped rather than rejected, so adding a
176
- * third language upstream degrades to "the sync ignores it" instead of "every
177
- * run fails".
178
- *
179
- * 🔴 **Each locale key is nullable AND optional, and that is the load-bearing
180
- * clause** (F058, cross-referenced to upstream F031). The column is declared
181
- * `json(...).notNull()` — and **notNull covers the OBJECT, not its keys**. There
182
- * is no CHECK constraint, and two of the four public columns
183
- * (`room_type.name`, `legal_document.title`) have no upstream validation at all:
184
- * the write is a raw `formData.get(…) as string`, which is `null` the moment a
185
- * field is absent. So `{"et":"Tuba","en":null}` — and `{"et":"Tuba"}` — are rows
186
- * yhikas-admin accepts today, through its ordinary admin UI.
187
- *
188
- * Requiring both keys would fail the ENTIRE response for one such row, so 32
189
- * room types would stop syncing because one lacks an English name — every six
190
- * hours, until somebody edited it upstream. That is exactly the blast-radius
191
- * mistake `legalDocumentRowSchema.slug` documents below and deliberately
192
- * avoided; a missing translation belongs at one row, and `projection.ts` is
193
- * where it is decided (the locale is suppressed, or the row is skipped with a
194
- * reason).
195
- *
196
- * What is NOT relaxed, on purpose:
197
- *
198
- * - **The object itself stays required.** A missing translation is DATA; a
199
- * missing FIELD is a renamed column or a changed envelope. The consequence of
200
- * tolerating one is caught either way — `assertSomethingSurvived` refuses a
201
- * snapshot in which no row survived — but refusing here NAMES it
202
- * (`title: Required`) instead of reporting 32 identical "no 'et' title
203
- * upstream" skips and leaving the reader to infer the cause.
204
- * - **A non-string value is still a shape violation.** `{"en":42}` would
205
- * otherwise be written as a name.
206
- *
207
- * Note the asymmetry this leaves, deliberately: a RENAMED key (`et_EE`) is
208
- * indistinguishable from two absent ones, because unknown keys are stripped by
209
- * design so a third language degrades gracefully. That case falls through to
210
- * `assertSomethingSurvived`, which is exactly what it is for.
211
- */
212
- declare const multilingualTextSchema: z.ZodObject<{
213
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
214
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
215
- }, "strip", z.ZodTypeAny, {
216
- et?: string | null | undefined;
217
- en?: string | null | undefined;
218
- }, {
219
- et?: string | null | undefined;
220
- en?: string | null | undefined;
221
- }>;
222
- type MultilingualText = z.infer<typeof multilingualTextSchema>;
223
- /**
224
- * One `room_type` row.
225
- *
226
- * `depositAmount` / `discountedDepositAmount` are absent by design. Upstream
227
- * ships them as hardcoded `null` with no backing column; validating them as
228
- * `z.null()` would turn the day someone adds the column into a hard sync
229
- * failure. Deposits are unbuilt admin-side work, not something the sync can
230
- * surface.
231
- */
232
- declare const roomTypeRowSchema: z.ZodObject<{
233
- code: z.ZodString;
234
- name: z.ZodObject<{
235
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
236
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
237
- }, "strip", z.ZodTypeAny, {
238
- et?: string | null | undefined;
239
- en?: string | null | undefined;
240
- }, {
241
- et?: string | null | undefined;
242
- en?: string | null | undefined;
243
- }>;
244
- totalArea: z.ZodNullable<z.ZodString>;
245
- livingArea: z.ZodNullable<z.ZodString>;
246
- commonArea: z.ZodNullable<z.ZodString>;
247
- capacity: z.ZodNullable<z.ZodNumber>;
248
- monthlyRent: z.ZodNullable<z.ZodString>;
249
- discountedRent: z.ZodNullable<z.ZodString>;
250
- dailyRent: z.ZodNullable<z.ZodString>;
251
- placesOccupied: z.ZodNullable<z.ZodNumber>;
252
- }, "strip", z.ZodTypeAny, {
253
- code: string;
254
- name: {
255
- et?: string | null | undefined;
256
- en?: string | null | undefined;
257
- };
258
- totalArea: string | null;
259
- livingArea: string | null;
260
- commonArea: string | null;
261
- capacity: number | null;
262
- placesOccupied: number | null;
263
- monthlyRent: string | null;
264
- dailyRent: string | null;
265
- discountedRent: string | null;
266
- }, {
267
- code: string;
268
- name: {
269
- et?: string | null | undefined;
270
- en?: string | null | undefined;
271
- };
272
- totalArea: string | null;
273
- livingArea: string | null;
274
- commonArea: string | null;
275
- capacity: number | null;
276
- placesOccupied: number | null;
277
- monthlyRent: string | null;
278
- dailyRent: string | null;
279
- discountedRent: string | null;
280
- }>;
281
- type RoomTypeRow = z.infer<typeof roomTypeRowSchema>;
282
- /**
283
- * One active `legal_document` row.
284
- *
285
- * `type` is `text().notNull().unique()` upstream — NOT a pgEnum, and there is
286
- * no TS union anywhere. The five values seeded today are closed by convention
287
- * only and the admin UI can mint a sixth, so this validates the SHAPE of the
288
- * business key and never its membership in a list. A whitelist here would turn
289
- * a new upstream document into a hard sync failure.
290
- */
291
- declare const legalDocumentRowSchema: z.ZodObject<{
292
- type: z.ZodString;
293
- title: z.ZodObject<{
294
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
295
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
296
- }, "strip", z.ZodTypeAny, {
297
- et?: string | null | undefined;
298
- en?: string | null | undefined;
299
- }, {
300
- et?: string | null | undefined;
301
- en?: string | null | undefined;
302
- }>;
303
- /**
304
- * Accepted as free text HERE, and screened per-row in the projection.
305
- *
306
- * Upstream derives it from an unvalidated free-text form field
307
- * (`type.toLowerCase().replace(/_/g, '-')`), so an Estonian title yields an
308
- * Estonian slug — `üldtingimused` — and a title with a space yields a slug
309
- * with a space. A character-class regex on the RESPONSE schema would fail
310
- * `legalDocumentsResponseSchema` for the whole payload, so one newly
311
- * authored document would take the entire resource offline every six hours
312
- * until someone edited it upstream. The blast radius belongs at one row.
313
- */
314
- slug: z.ZodString;
315
- htmlContentEt: z.ZodString;
316
- htmlContentEn: z.ZodString;
317
- order: z.ZodNumber;
318
- }, "strip", z.ZodTypeAny, {
319
- type: string;
320
- slug: string;
321
- title: {
322
- et?: string | null | undefined;
323
- en?: string | null | undefined;
324
- };
325
- order: number;
326
- htmlContentEt: string;
327
- htmlContentEn: string;
328
- }, {
329
- type: string;
330
- slug: string;
331
- title: {
332
- et?: string | null | undefined;
333
- en?: string | null | undefined;
334
- };
335
- order: number;
336
- htmlContentEt: string;
337
- htmlContentEn: string;
338
- }>;
339
- type LegalDocumentRow = z.infer<typeof legalDocumentRowSchema>;
340
- /**
341
- * `success: z.literal(true)` is the load-bearing clause, not decoration.
342
- *
343
- * Every upstream failure path sets `success: false` — 401 (missing header,
344
- * wrong scheme, wrong key, AND an unset server-side key: all four
345
- * indistinguishable, deny-by-default through one branch), 429, and 500. No
346
- * route returns 200 with a degraded body. So the discriminator is `success`,
347
- * never array length, and a snapshot that fails this schema can never be
348
- * mistaken for an authoritative empty one.
349
- */
350
- declare const roomTypesResponseSchema: z.ZodObject<{
351
- success: z.ZodLiteral<true>;
352
- roomTypes: z.ZodArray<z.ZodObject<{
353
- code: z.ZodString;
354
- name: z.ZodObject<{
355
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
356
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
357
- }, "strip", z.ZodTypeAny, {
358
- et?: string | null | undefined;
359
- en?: string | null | undefined;
360
- }, {
361
- et?: string | null | undefined;
362
- en?: string | null | undefined;
363
- }>;
364
- totalArea: z.ZodNullable<z.ZodString>;
365
- livingArea: z.ZodNullable<z.ZodString>;
366
- commonArea: z.ZodNullable<z.ZodString>;
367
- capacity: z.ZodNullable<z.ZodNumber>;
368
- monthlyRent: z.ZodNullable<z.ZodString>;
369
- discountedRent: z.ZodNullable<z.ZodString>;
370
- dailyRent: z.ZodNullable<z.ZodString>;
371
- placesOccupied: z.ZodNullable<z.ZodNumber>;
372
- }, "strip", z.ZodTypeAny, {
373
- code: string;
374
- name: {
375
- et?: string | null | undefined;
376
- en?: string | null | undefined;
377
- };
378
- totalArea: string | null;
379
- livingArea: string | null;
380
- commonArea: string | null;
381
- capacity: number | null;
382
- placesOccupied: number | null;
383
- monthlyRent: string | null;
384
- dailyRent: string | null;
385
- discountedRent: string | null;
386
- }, {
387
- code: string;
388
- name: {
389
- et?: string | null | undefined;
390
- en?: string | null | undefined;
391
- };
392
- totalArea: string | null;
393
- livingArea: string | null;
394
- commonArea: string | null;
395
- capacity: number | null;
396
- placesOccupied: number | null;
397
- monthlyRent: string | null;
398
- dailyRent: string | null;
399
- discountedRent: string | null;
400
- }>, "many">;
401
- }, "strip", z.ZodTypeAny, {
402
- success: true;
403
- roomTypes: {
404
- code: string;
405
- name: {
406
- et?: string | null | undefined;
407
- en?: string | null | undefined;
408
- };
409
- totalArea: string | null;
410
- livingArea: string | null;
411
- commonArea: string | null;
412
- capacity: number | null;
413
- placesOccupied: number | null;
414
- monthlyRent: string | null;
415
- dailyRent: string | null;
416
- discountedRent: string | null;
417
- }[];
418
- }, {
419
- success: true;
420
- roomTypes: {
421
- code: string;
422
- name: {
423
- et?: string | null | undefined;
424
- en?: string | null | undefined;
425
- };
426
- totalArea: string | null;
427
- livingArea: string | null;
428
- commonArea: string | null;
429
- capacity: number | null;
430
- placesOccupied: number | null;
431
- monthlyRent: string | null;
432
- dailyRent: string | null;
433
- discountedRent: string | null;
434
- }[];
435
- }>;
436
- declare const legalDocumentsResponseSchema: z.ZodObject<{
437
- success: z.ZodLiteral<true>;
438
- documents: z.ZodArray<z.ZodObject<{
439
- type: z.ZodString;
440
- title: z.ZodObject<{
441
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
442
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
443
- }, "strip", z.ZodTypeAny, {
444
- et?: string | null | undefined;
445
- en?: string | null | undefined;
446
- }, {
447
- et?: string | null | undefined;
448
- en?: string | null | undefined;
449
- }>;
450
- /**
451
- * Accepted as free text HERE, and screened per-row in the projection.
452
- *
453
- * Upstream derives it from an unvalidated free-text form field
454
- * (`type.toLowerCase().replace(/_/g, '-')`), so an Estonian title yields an
455
- * Estonian slug — `üldtingimused` — and a title with a space yields a slug
456
- * with a space. A character-class regex on the RESPONSE schema would fail
457
- * `legalDocumentsResponseSchema` for the whole payload, so one newly
458
- * authored document would take the entire resource offline every six hours
459
- * until someone edited it upstream. The blast radius belongs at one row.
460
- */
461
- slug: z.ZodString;
462
- htmlContentEt: z.ZodString;
463
- htmlContentEn: z.ZodString;
464
- order: z.ZodNumber;
465
- }, "strip", z.ZodTypeAny, {
466
- type: string;
467
- slug: string;
468
- title: {
469
- et?: string | null | undefined;
470
- en?: string | null | undefined;
471
- };
472
- order: number;
473
- htmlContentEt: string;
474
- htmlContentEn: string;
475
- }, {
476
- type: string;
477
- slug: string;
478
- title: {
479
- et?: string | null | undefined;
480
- en?: string | null | undefined;
481
- };
482
- order: number;
483
- htmlContentEt: string;
484
- htmlContentEn: string;
485
- }>, "many">;
486
- }, "strip", z.ZodTypeAny, {
487
- success: true;
488
- documents: {
489
- type: string;
490
- slug: string;
491
- title: {
492
- et?: string | null | undefined;
493
- en?: string | null | undefined;
494
- };
495
- order: number;
496
- htmlContentEt: string;
497
- htmlContentEn: string;
498
- }[];
499
- }, {
500
- success: true;
501
- documents: {
502
- type: string;
503
- slug: string;
504
- title: {
505
- et?: string | null | undefined;
506
- en?: string | null | undefined;
507
- };
508
- order: number;
509
- htmlContentEt: string;
510
- htmlContentEn: string;
511
- }[];
512
- }>;
513
- /**
514
- * One `site_notice` — a ticker item.
515
- *
516
- * No id, no code, no timestamp: `{text, isActive, order}` is the whole row as
517
- * the route serves it. That absence is why `site_info` is modelled as one
518
- * local row carrying an ordered list rather than as N diffable rows — there is
519
- * no key an edit upstream would preserve.
520
- *
521
- * `isActive` is honoured HERE rather than assumed: the route returns inactive
522
- * notices too, and a ticker that shows a retired notice is worse than one that
523
- * shows nothing.
524
- */
525
- declare const siteNoticeRowSchema: z.ZodObject<{
526
- text: z.ZodObject<{
527
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
528
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
529
- }, "strip", z.ZodTypeAny, {
530
- et?: string | null | undefined;
531
- en?: string | null | undefined;
532
- }, {
533
- et?: string | null | undefined;
534
- en?: string | null | undefined;
535
- }>;
536
- isActive: z.ZodBoolean;
537
- order: z.ZodNumber;
538
- }, "strip", z.ZodTypeAny, {
539
- text: {
540
- et?: string | null | undefined;
541
- en?: string | null | undefined;
542
- };
543
- order: number;
544
- isActive: boolean;
545
- }, {
546
- text: {
547
- et?: string | null | undefined;
548
- en?: string | null | undefined;
549
- };
550
- order: number;
551
- isActive: boolean;
552
- }>;
553
- type SiteNoticeRow = z.infer<typeof siteNoticeRowSchema>;
554
- /**
555
- * `/api/public/site-info` — reception hours plus the notice ticker.
556
- *
557
- * 🔴 **This envelope cannot express "not configured yet".** The route answers
558
- * `200 { success: true, receptionHours: {et:'',en:''}, notices: [] }` when no
559
- * row exists, which is byte-identical to an operator having deliberately
560
- * cleared everything (F015). The other two resources have no such hole —
561
- * theirs discriminate on `success` and on array length against a local
562
- * snapshot.
563
- *
564
- * The schema is therefore NOT where that is solved, and deliberately so: the
565
- * shape is valid either way. `projectSiteInfo` refuses an all-empty payload as
566
- * unusable (D027), which is the only place that has the standing to decide
567
- * that a well-formed response is not authoritative. Upstream's later
568
- * `updatedAt` / `noticeCount` are not declared here, so zod strips them; why
569
- * they are not needed is on `projectSiteInfo`.
570
- */
571
- declare const siteInfoResponseSchema: z.ZodObject<{
572
- success: z.ZodLiteral<true>;
573
- receptionHours: z.ZodObject<{
574
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
575
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
576
- }, "strip", z.ZodTypeAny, {
577
- et?: string | null | undefined;
578
- en?: string | null | undefined;
579
- }, {
580
- et?: string | null | undefined;
581
- en?: string | null | undefined;
582
- }>;
583
- notices: z.ZodArray<z.ZodObject<{
584
- text: z.ZodObject<{
585
- et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
586
- en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
587
- }, "strip", z.ZodTypeAny, {
588
- et?: string | null | undefined;
589
- en?: string | null | undefined;
590
- }, {
591
- et?: string | null | undefined;
592
- en?: string | null | undefined;
593
- }>;
594
- isActive: z.ZodBoolean;
595
- order: z.ZodNumber;
596
- }, "strip", z.ZodTypeAny, {
597
- text: {
598
- et?: string | null | undefined;
599
- en?: string | null | undefined;
600
- };
601
- order: number;
602
- isActive: boolean;
603
- }, {
604
- text: {
605
- et?: string | null | undefined;
606
- en?: string | null | undefined;
607
- };
608
- order: number;
609
- isActive: boolean;
610
- }>, "many">;
611
- }, "strip", z.ZodTypeAny, {
612
- receptionHours: {
613
- et?: string | null | undefined;
614
- en?: string | null | undefined;
615
- };
616
- notices: {
617
- text: {
618
- et?: string | null | undefined;
619
- en?: string | null | undefined;
620
- };
621
- order: number;
622
- isActive: boolean;
623
- }[];
624
- success: true;
625
- }, {
626
- receptionHours: {
627
- et?: string | null | undefined;
628
- en?: string | null | undefined;
629
- };
630
- notices: {
631
- text: {
632
- et?: string | null | undefined;
633
- en?: string | null | undefined;
634
- };
635
- order: number;
636
- isActive: boolean;
637
- }[];
638
- success: true;
639
- }>;
640
- type SiteInfoResponse = z.infer<typeof siteInfoResponseSchema>;
641
- //#endregion
642
- //#region src/projection.d.ts
643
- /** Injected at the seam; see the module docblock for why it is not imported. */
644
- type HtmlSanitizer = (html: string) => string;
645
- interface ProjectionOptions {
646
- /** The locale whose values live on the base entity row. `et` or `en`. */
647
- readonly defaultLocale: string;
648
- }
649
- interface ProjectedRow {
650
- /** The business key — `room_type.code` or `legal_document.type`. */
651
- readonly key: string;
652
- /** Base-row fields, including the default locale's values for translatable fields. */
653
- readonly base: Record<string, unknown>;
654
- /**
655
- * Translatable values for the non-default locale, or `null` when that
656
- * locale's text is empty upstream. `null` means "unpublish that locale",
657
- * never "write an empty string".
658
- */
659
- readonly secondary: Record<string, unknown> | null;
660
- readonly hash: string;
661
- }
662
- interface SkippedRow {
663
- readonly key: string;
664
- readonly reason: string;
665
- }
666
- interface ProjectionResult {
667
- readonly projected: readonly ProjectedRow[];
668
- readonly skipped: readonly SkippedRow[];
669
- }
670
- /** The locale that is NOT the base row's. */
671
- declare function secondaryLocaleOf(defaultLocale: string): string;
672
- declare function projectRoomTypes(rows: readonly RoomTypeRow[], options: ProjectionOptions): ProjectionResult;
673
- declare function projectLegalDocuments(rows: readonly LegalDocumentRow[], options: ProjectionOptions, sanitize: HtmlSanitizer): ProjectionResult;
674
- /**
675
- * Reception hours + the notice ticker → the one `yhikas_site_info` row.
676
- *
677
- * ## 🔴 The refusal is the point of this function
678
- *
679
- * `/api/public/site-info` answers `200 { success: true, receptionHours:
680
- * {et:'',en:''}, notices: [] }` when no upstream row exists. That is
681
- * byte-identical to an operator having deliberately cleared both, and it is the
682
- * state the endpoint is in whenever nobody has filled it in (F015). The other
683
- * two resources have no equivalent hole — theirs discriminate on `success` and
684
- * on array length against a local snapshot.
685
- *
686
- * The shipped `empty-snapshot` floor cannot cover it, and the reason is
687
- * structural rather than an oversight: that rule is a row-COUNT test, and this
688
- * resource always projects exactly one row. The count is 1 whether the row says
689
- * anything or not, so the floor never fires and an all-empty payload would be
690
- * applied as an authoritative blanking — emptying the ticker and the hours on
691
- * every page of the site, silently, six hours after upstream hiccupped.
692
- *
693
- * So emptiness is restated here as a CONTENT test: if the default locale has
694
- * neither hours nor a single active notice, the payload is refused as
695
- * unusable. Local content is left exactly as it was and the failure arms the
696
- * staleness watchdog, which is the same posture as an unreachable endpoint —
697
- * because epistemically it is the same situation.
698
- *
699
- * Note what is NOT refused: hours with no notices, or notices with no hours.
700
- * Both are ordinary states of a real dormitory, and refusing them would make
701
- * the guard fire on exactly the operator action it exists to protect.
702
- *
703
- * ## Why upstream's `updatedAt: null` is not read (F015, decided 2026-09-29)
704
- *
705
- * yhikas-admin now answers `updatedAt: null` when both of its tables are
706
- * empty, which looks like the missing "never configured" signal. It adds
707
- * nothing this refusal does not already know. Upstream enforces a database
708
- * CHECK that every `site_info.receptionHours` from a row has both locales
709
- * non-blank; the empty `{et:'',en:''}` is a value the route FABRICATES when no
710
- * row exists (its `PUBLIC_API.md`, "The one exception"). So once the
711
- * constraints are deployed, "deliberately cleared" is not a state an operator
712
- * can produce: empty hours can only mean no row, and every all-empty response
713
- * is already "never configured". Reading the null would split one case into
714
- * one case. It would also make the guard depend on a field an older
715
- * deployment omits — an absent key, not an error. So the content test stays
716
- * the single rule, and it holds whether or not the field is present.
717
- */
718
- declare function projectSiteInfo(response: SiteInfoResponse, options: ProjectionOptions): ProjectionResult;
719
- //#endregion
720
- //#region src/apply.d.ts
721
- /**
722
- * The `AdminClient` surface the sync uses, structurally.
723
- *
724
- * Declared rather than imported so the apply logic is unit-testable with a
725
- * fake — CI runs unit tests only, with no database, so a design that could
726
- * only be exercised by an integration test would in practice be exercised by
727
- * nothing.
728
- */
729
- interface SyncEntityClient {
730
- findMany(options: {
731
- limit: number;
732
- }): Promise<Record<string, unknown>[]>;
733
- create(data: Record<string, unknown>): Promise<Record<string, unknown>>;
734
- update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
735
- updateForLocale(id: string, data: Record<string, unknown>, locale: string): Promise<Record<string, unknown>>;
736
- deleteTranslation(id: string, locale: string): Promise<void>;
737
- }
738
- interface SyncLogger {
739
- info(obj: Record<string, unknown>, msg: string): void;
740
- warn(obj: Record<string, unknown>, msg: string): void;
741
- error(obj: Record<string, unknown>, msg: string): void;
742
- }
743
- interface ApplyCounts {
744
- created: number;
745
- updated: number;
746
- retired: number;
747
- unchanged: number;
748
- failed: number;
749
- }
750
- /** Thrown when at least one row failed; carries the counts that were achieved. */
751
- declare class YhikasApplyPartialError extends Error {
752
- readonly counts: ApplyCounts;
753
- constructor(resource: SyncResource, counts: ApplyCounts);
754
- }
755
- declare function readLocalRows(client: SyncEntityClient, keyField: string, limit: number): Promise<LocalRow[]>;
756
- interface ApplyPlanInput {
757
- readonly resource: SyncResource;
758
- readonly plan: DiffPlan<ProjectedRow>;
759
- readonly client: SyncEntityClient;
760
- /** The locale that is NOT on the base row — the one whose publish state is toggled. */
761
- readonly secondaryLocale: string;
762
- readonly logger: SyncLogger;
763
- }
764
- declare function applyPlan(input: ApplyPlanInput): Promise<ApplyCounts>;
765
- //#endregion
766
7
  //#region src/entities/guard.d.ts
767
8
  /** Thrown when a writer other than the sync tries to mutate synced content. */
768
9
  declare class YhikasSyncedContentReadOnlyError extends Error {
@@ -776,132 +17,6 @@ declare class YhikasSyncedContentReadOnlyError extends Error {
776
17
  */
777
18
  declare function machineWritten<N extends string>(entityName: N): Behavior<Record<never, never>, `yhikas-sync:machine-written:${N}`>;
778
19
  //#endregion
779
- //#region src/entities/index.d.ts
780
- /**
781
- * The entities this plugin syncs — ONE list, consumed by the plugin's
782
- * declaration and by the contract tests.
783
- *
784
- * Adding `site_info` turned twelve tests red across two files, every one of
785
- * them because a resource or entity had been re-listed by hand somewhere. This
786
- * export exists so the next one cannot: `plugin.ts` spreads it into
787
- * `server.entities`, and `contract.test.ts` iterates it, so a fourth entity is
788
- * covered by both the moment it is added here.
789
- */
790
- declare const SYNCED_ENTITIES: readonly [import("@murumets-ee/entity").Entity<{
791
- id: import("@murumets-ee/entity").IdField;
792
- } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
793
- code: import("@murumets-ee/entity").TextField & {
794
- readonly required: true;
795
- readonly unique: true;
796
- readonly indexed: true;
797
- readonly maxLength: 190;
798
- };
799
- name: import("@murumets-ee/entity").TextField & {
800
- readonly required: true;
801
- readonly translatable: true;
802
- };
803
- totalArea: import("@murumets-ee/entity").TextField & {
804
- readonly maxLength: 32;
805
- };
806
- livingArea: import("@murumets-ee/entity").TextField & {
807
- readonly maxLength: 32;
808
- };
809
- commonArea: import("@murumets-ee/entity").TextField & {
810
- readonly maxLength: 32;
811
- };
812
- capacity: import("@murumets-ee/entity").NumberField & {
813
- readonly integer: true;
814
- };
815
- placesOccupied: import("@murumets-ee/entity").NumberField & {
816
- readonly integer: true;
817
- };
818
- monthlyRent: import("@murumets-ee/entity").TextField & {
819
- readonly maxLength: 32;
820
- };
821
- discountedMonthlyRent: import("@murumets-ee/entity").TextField & {
822
- readonly maxLength: 32;
823
- };
824
- dailyRent: import("@murumets-ee/entity").TextField & {
825
- readonly maxLength: 32;
826
- };
827
- currency: import("@murumets-ee/entity").TextField & {
828
- readonly required: true;
829
- readonly maxLength: 3;
830
- readonly default: "EUR";
831
- };
832
- vatTreatment: import("@murumets-ee/entity").SelectField & {
833
- options: readonly ["net", "gross"];
834
- } & {
835
- readonly options: readonly ["net", "gross"];
836
- readonly required: true;
837
- readonly default: "net";
838
- };
839
- hasEnglish: import("@murumets-ee/entity").BooleanField & {
840
- readonly required: true;
841
- readonly default: false;
842
- };
843
- sourceHash: import("@murumets-ee/entity").TextField & {
844
- readonly maxLength: 64;
845
- };
846
- }, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>, import("@murumets-ee/entity").Entity<{
847
- id: import("@murumets-ee/entity").IdField;
848
- } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
849
- type: import("@murumets-ee/entity").TextField & {
850
- readonly required: true;
851
- readonly unique: true;
852
- readonly indexed: true;
853
- readonly maxLength: 190;
854
- };
855
- title: import("@murumets-ee/entity").TextField & {
856
- readonly required: true;
857
- readonly translatable: true;
858
- };
859
- sourceSlug: import("@murumets-ee/entity").TextField & {
860
- readonly required: true;
861
- readonly indexed: true;
862
- readonly maxLength: 190;
863
- };
864
- body: import("@murumets-ee/entity").RichTextField & {
865
- readonly required: true;
866
- readonly translatable: true;
867
- };
868
- order: import("@murumets-ee/entity").NumberField & {
869
- readonly integer: true;
870
- readonly required: true;
871
- readonly default: 0;
872
- };
873
- hasEnglish: import("@murumets-ee/entity").BooleanField & {
874
- readonly required: true;
875
- readonly default: false;
876
- };
877
- sourceHash: import("@murumets-ee/entity").TextField & {
878
- readonly maxLength: 64;
879
- };
880
- }, "yhikas_legal_document", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_legal_document">]>, import("@murumets-ee/entity").Entity<{
881
- id: import("@murumets-ee/entity").IdField;
882
- } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
883
- key: import("@murumets-ee/entity").TextField & {
884
- readonly required: true;
885
- readonly unique: true;
886
- readonly indexed: true;
887
- readonly maxLength: 32;
888
- };
889
- receptionHours: import("@murumets-ee/entity").TextField & {
890
- readonly translatable: true;
891
- readonly maxLength: 500;
892
- };
893
- notices: import("@murumets-ee/entity").JsonField & {
894
- readonly translatable: true;
895
- };
896
- hasEnglish: import("@murumets-ee/entity").BooleanField & {
897
- readonly default: false;
898
- readonly required: true;
899
- };
900
- sourceHash: import("@murumets-ee/entity").TextField & {
901
- readonly maxLength: 64;
902
- };
903
- }, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>];
904
- //#endregion
905
20
  //#region src/entities/legal-document.d.ts
906
21
  /**
907
22
  * `yhikas_legal_document` — the local projection of yhikas-admin's
@@ -1118,6 +233,122 @@ declare const YhikasRoomType: import("@murumets-ee/entity").Entity<{
1118
233
  };
1119
234
  }, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>;
1120
235
  //#endregion
236
+ //#region src/entities/index.d.ts
237
+ declare const SYNCED_ENTITIES: readonly [import("@murumets-ee/entity").Entity<{
238
+ id: import("@murumets-ee/entity").IdField;
239
+ } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
240
+ code: import("@murumets-ee/entity").TextField & {
241
+ readonly required: true;
242
+ readonly unique: true;
243
+ readonly indexed: true;
244
+ readonly maxLength: 190;
245
+ };
246
+ name: import("@murumets-ee/entity").TextField & {
247
+ readonly required: true;
248
+ readonly translatable: true;
249
+ };
250
+ totalArea: import("@murumets-ee/entity").TextField & {
251
+ readonly maxLength: 32;
252
+ };
253
+ livingArea: import("@murumets-ee/entity").TextField & {
254
+ readonly maxLength: 32;
255
+ };
256
+ commonArea: import("@murumets-ee/entity").TextField & {
257
+ readonly maxLength: 32;
258
+ };
259
+ capacity: import("@murumets-ee/entity").NumberField & {
260
+ readonly integer: true;
261
+ };
262
+ placesOccupied: import("@murumets-ee/entity").NumberField & {
263
+ readonly integer: true;
264
+ };
265
+ monthlyRent: import("@murumets-ee/entity").TextField & {
266
+ readonly maxLength: 32;
267
+ };
268
+ discountedMonthlyRent: import("@murumets-ee/entity").TextField & {
269
+ readonly maxLength: 32;
270
+ };
271
+ dailyRent: import("@murumets-ee/entity").TextField & {
272
+ readonly maxLength: 32;
273
+ };
274
+ currency: import("@murumets-ee/entity").TextField & {
275
+ readonly required: true;
276
+ readonly maxLength: 3;
277
+ readonly default: "EUR";
278
+ };
279
+ vatTreatment: import("@murumets-ee/entity").SelectField & {
280
+ options: readonly ["net", "gross"];
281
+ } & {
282
+ readonly options: readonly ["net", "gross"];
283
+ readonly required: true;
284
+ readonly default: "net";
285
+ };
286
+ hasEnglish: import("@murumets-ee/entity").BooleanField & {
287
+ readonly required: true;
288
+ readonly default: false;
289
+ };
290
+ sourceHash: import("@murumets-ee/entity").TextField & {
291
+ readonly maxLength: 64;
292
+ };
293
+ }, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>, import("@murumets-ee/entity").Entity<{
294
+ id: import("@murumets-ee/entity").IdField;
295
+ } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
296
+ type: import("@murumets-ee/entity").TextField & {
297
+ readonly required: true;
298
+ readonly unique: true;
299
+ readonly indexed: true;
300
+ readonly maxLength: 190;
301
+ };
302
+ title: import("@murumets-ee/entity").TextField & {
303
+ readonly required: true;
304
+ readonly translatable: true;
305
+ };
306
+ sourceSlug: import("@murumets-ee/entity").TextField & {
307
+ readonly required: true;
308
+ readonly indexed: true;
309
+ readonly maxLength: 190;
310
+ };
311
+ body: import("@murumets-ee/entity").RichTextField & {
312
+ readonly required: true;
313
+ readonly translatable: true;
314
+ };
315
+ order: import("@murumets-ee/entity").NumberField & {
316
+ readonly integer: true;
317
+ readonly required: true;
318
+ readonly default: 0;
319
+ };
320
+ hasEnglish: import("@murumets-ee/entity").BooleanField & {
321
+ readonly required: true;
322
+ readonly default: false;
323
+ };
324
+ sourceHash: import("@murumets-ee/entity").TextField & {
325
+ readonly maxLength: 64;
326
+ };
327
+ }, "yhikas_legal_document", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_legal_document">]>, import("@murumets-ee/entity").Entity<{
328
+ id: import("@murumets-ee/entity").IdField;
329
+ } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
330
+ key: import("@murumets-ee/entity").TextField & {
331
+ readonly required: true;
332
+ readonly unique: true;
333
+ readonly indexed: true;
334
+ readonly maxLength: 32;
335
+ };
336
+ receptionHours: import("@murumets-ee/entity").TextField & {
337
+ readonly translatable: true;
338
+ readonly maxLength: 500;
339
+ };
340
+ notices: import("@murumets-ee/entity").JsonField & {
341
+ readonly translatable: true;
342
+ };
343
+ hasEnglish: import("@murumets-ee/entity").BooleanField & {
344
+ readonly default: false;
345
+ readonly required: true;
346
+ };
347
+ sourceHash: import("@murumets-ee/entity").TextField & {
348
+ readonly maxLength: 64;
349
+ };
350
+ }, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>];
351
+ //#endregion
1121
352
  //#region src/entities/site-info.d.ts
1122
353
  /**
1123
354
  * `yhikas_site_info` — reception hours and the notice ticker, as ONE row.
@@ -1208,18 +439,105 @@ declare const YhikasSiteInfo: import("@murumets-ee/entity").Entity<{
1208
439
  };
1209
440
  }, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>;
1210
441
  //#endregion
1211
- //#region src/entity-client.d.ts
1212
- /** The `AdminClient` methods the adapter forwards, structurally. */
1213
- type AdminClientLike = ReturnType<ToolkitApp['getClient']>;
442
+ //#region src/room-pages.d.ts
443
+ /** The most pages one run creates. The rest are created on later runs. */
444
+ declare const MAX_ROOM_PAGES_PER_RUN = 20;
445
+ /** The page entity, as the drafts read and write it. */
446
+ interface RoomPageWriter {
447
+ /** Pages whose default-locale slug is `slug`, at most `limit`. */
448
+ findBySlug(slug: string, limit: number): Promise<readonly {
449
+ readonly id: string;
450
+ readonly parent: string | null;
451
+ }[]>;
452
+ /** The children of `parentId` whose default-locale slug is one of `slugs`: slug → page id. */
453
+ findChildren(parentId: string, slugs: readonly string[]): Promise<ReadonlyMap<string, string>>;
454
+ /** Create a DRAFT page in the default locale; answers its id. */
455
+ createDraft(page: {
456
+ readonly slug: string;
457
+ readonly parentId: string;
458
+ readonly title: string;
459
+ readonly layout: RoomPageDraft['layout'];
460
+ }): Promise<string>;
461
+ /** Write one non-default locale's title, tree and slug. Never touches status. */
462
+ writeLocale(id: string, locale: string, page: {
463
+ readonly slug: string;
464
+ readonly title: string;
465
+ readonly layout: RoomPageDraft['layout'];
466
+ }): Promise<void>;
467
+ }
468
+ /** The synced room types, as the drafts read them. */
469
+ interface RoomTypeReader {
470
+ /**
471
+ * PUBLISHED room types in `locale` (name in that locale), at most `limit`;
472
+ * only those with a code in `codes` when it is given.
473
+ */
474
+ published(locale: string, options: {
475
+ readonly limit: number;
476
+ readonly codes?: readonly string[];
477
+ }): Promise<readonly RoomTypeForPage[]>;
478
+ /** Which of these row ids are room types, published or not. Asked only about a path conflict. */
479
+ existingIds(ids: readonly string[]): Promise<ReadonlySet<string>>;
480
+ }
481
+ interface RoomPageDraftDeps {
482
+ readonly config: ResolvedRoomPagesConfig;
483
+ readonly rooms: RoomTypeReader;
484
+ readonly pages: RoomPageWriter;
485
+ readonly registry: RoomPageRegistry;
486
+ readonly logger: SyncLogger;
487
+ /** The app's real default locale — the base row's language. */
488
+ readonly defaultLocale: string;
489
+ /** Every enabled content locale, the default included. */
490
+ readonly locales: readonly string[];
491
+ /** The most room types read in one query — the sync's snapshot ceiling (`maxRowsPerResource`). */
492
+ readonly maxRows: number;
493
+ readonly now?: () => Date;
494
+ /** This run's claim token. Default: a random UUID. */
495
+ readonly token?: string;
496
+ /** Override {@link MAX_ROOM_PAGES_PER_RUN} — tests only. */
497
+ readonly maxPerRun?: number;
498
+ }
499
+ interface RoomPageSkip {
500
+ readonly code: string;
501
+ readonly reason: string;
502
+ }
503
+ interface RoomPageDraftSummary {
504
+ /** Codes a draft page was created for this run. */
505
+ readonly created: readonly string[];
506
+ /** Codes whose page already existed under the parent, now recorded. */
507
+ readonly found: readonly string[];
508
+ /**
509
+ * Codes whose page a CRASHED run created before recording it — found by
510
+ * slug with that run's claim still on the row, and recorded as `created`
511
+ * so the dashboard still lists them as waiting.
512
+ */
513
+ readonly recovered: readonly string[];
514
+ /** Refused codes left out of this run because their last refusal is too recent. */
515
+ readonly backedOff: number;
516
+ /** Codes not acted on this run, with why. Retried next run. */
517
+ readonly skipped: readonly RoomPageSkip[];
518
+ /** Codes over the per-run cap, left for the next run. */
519
+ readonly deferred: number;
520
+ /** Non-default locale writes that failed (`code:locale`) — the page exists and stays recorded. */
521
+ readonly localeFailures: readonly string[];
522
+ /** Why nothing was done at all, when nothing was. */
523
+ readonly halted?: 'nothing-pending' | 'no-parent' | 'ambiguous-parent';
524
+ }
525
+ /** A code as a page slug: lowercased; `undefined` when it cannot be one. */
526
+ declare function roomPageSlug(code: string): string | undefined;
527
+ /**
528
+ * Why the page entity refused a room's page, for the dashboard. A path held by
529
+ * a `yhikas_room_type` row is named as such, because its fix is specific: the
530
+ * room type's OLD URL row, left in the path index from before `roomPages`,
531
+ * still holds the address the page needs.
532
+ */
533
+ declare function classifyRefusal(err: unknown, rooms: RoomTypeReader): Promise<RoomPageRefusal>;
1214
534
  /**
1215
- * @param defaultLocale The app's REAL default locale — resolved from
1216
- * `@murumets-ee/content`, never configured. It is passed explicitly on every
1217
- * `updateForLocale` call and is not optional, because
1218
- * `elevateRequestContext` deliberately strips `locale`/`defaultLocale` from
1219
- * the context it builds, and `updateForLocale` THROWS when it can resolve
1220
- * the default locale from neither the options nor the context.
535
+ * Create the missing draft pages. Throws only for a failure of its own reads
536
+ * (rooms, registry, the parent lookup) — the caller logs it; per-room failures
537
+ * are caught, logged, kept on the registry row for the dashboard and reported
538
+ * in the summary.
1221
539
  */
1222
- declare function toSyncEntityClient(client: AdminClientLike, defaultLocale: string): SyncEntityClient;
540
+ declare function createRoomPageDrafts(deps: RoomPageDraftDeps): Promise<RoomPageDraftSummary>;
1223
541
  //#endregion
1224
542
  //#region src/sync-state-table.d.ts
1225
543
  /**
@@ -1366,6 +684,141 @@ type SyncStateClient = ReturnType<typeof yhikasSyncStateTable.makeClient>;
1366
684
  declare function truncateError(message: string): string;
1367
685
  declare function createSyncStateStore(client: SyncStateClient): SyncStateStore;
1368
686
  //#endregion
687
+ //#region src/room-facts.d.ts
688
+ /** The answer `resolve` gives — a record of facts per room type code. */
689
+ type LiveAnswer = Awaited<ReturnType<LiveSourceSpec['resolve']>>;
690
+ /** The fact names this source answers. */
691
+ declare const ROOM_FACT_NAMES: readonly ["monthly", "discounted", "daily", "places", "area"];
692
+ type RoomFactName = (typeof ROOM_FACT_NAMES)[number];
693
+ /** What a room answers — declared, so the panel offers them as checkboxes. */
694
+ declare const ROOM_FACTS: readonly LiveFactSpec[];
695
+ /** The library field a facts source is granted by default — `liveFacts.values`. */
696
+ declare const DEFAULT_ROOM_FACT_FIELDS: readonly string[];
697
+ /**
698
+ * A stored decimal string as a number, or `undefined` — never `NaN`, never a
699
+ * negative, never a fabricated 0 for a missing value.
700
+ */
701
+ declare function decimalOf(raw: string | null): number | undefined;
702
+ /** One room type's facts — only the values it actually has. */
703
+ declare function roomFactsOf(room: RoomTypeForPage): Partial<Record<RoomFactName, number>>;
704
+ /** The pure half of `resolve`: the answer for `keys`, given the published rows. Unknown keys are absent. */
705
+ declare function roomFactsFrom(rooms: readonly RoomTypeForPage[], keys: readonly string[]): LiveAnswer;
706
+ /** The picker's items: offered rooms, by code, at most {@link MAX_ITEMS}. */
707
+ declare function roomFactItemsFrom(rooms: readonly RoomTypeForPage[], offered: RoomOfferPredicate): readonly LiveItem[];
708
+ /**
709
+ * A queue schedule as English words — `'@every 6h'` → `'every 6 hours'` —
710
+ * plus a translation key when the catalogue has one for it. A cron expression
711
+ * is named as such: its meaning is not worth a parser here.
712
+ */
713
+ declare function cadenceOf(schedule: string): {
714
+ readonly text: string;
715
+ readonly key?: string;
716
+ };
717
+ interface YhikasRoomFactsOptions {
718
+ /** The source's English name in the panel. Default `'Room facts'`. */
719
+ readonly label?: string;
720
+ /**
721
+ * yhikas-admin's PUBLIC `https:` base URL — the panel links to
722
+ * `${adminUrl}/admin/toad`, where room prices are changed (yhikas-admin has
723
+ * no page per room). Omitted: no link.
724
+ */
725
+ readonly adminUrl?: string;
726
+ /**
727
+ * The sync's schedule, for "synced every 6 hours". Default: the registered
728
+ * sync plugin's own `schedule` — read when the panel asks, so the provenance
729
+ * cannot drift from the job that actually runs — else `@every 6h`.
730
+ */
731
+ readonly schedule?: string;
732
+ /**
733
+ * The staleness window. Default: the registered sync plugin's own
734
+ * `staleAfterMs`, else 24 h — so the facts go stale exactly when the
735
+ * watchdog alerts.
736
+ */
737
+ readonly staleAfterMs?: number;
738
+ /** Which rooms the picker lists. Default: the sync's `roomPages.offered`, else {@link isRoomOffer}. */
739
+ readonly offered?: RoomOfferPredicate;
740
+ /**
741
+ * Library fields this source may also be bound to — see
742
+ * `LiveSourceSpec.fields`. Default {@link DEFAULT_ROOM_FACT_FIELDS}: the
743
+ * library Facts block's `values`, so a site binds a room page's facts with
744
+ * no further wiring. Pass `[]` to grant none.
745
+ */
746
+ readonly fields?: readonly string[];
747
+ }
748
+ /** The part of the registered sync plugin's config the facts read. */
749
+ interface RegisteredSyncConfig {
750
+ readonly schedule: string;
751
+ readonly staleAfterMs: number;
752
+ readonly roomPages?: {
753
+ readonly offered: RoomOfferPredicate;
754
+ } | undefined;
755
+ }
756
+ /** What the factory reads at RUN time — injectable, so each part is testable without an app. */
757
+ interface RoomFactsRuntime {
758
+ /** Published room types through the caller's own client. */
759
+ readonly rooms: () => Promise<RoomTypeReader>;
760
+ /** The sync state, for freshness. */
761
+ readonly syncState: () => Promise<readonly SyncStateRecord[]>;
762
+ /**
763
+ * The registered sync plugin's resolved config, when it is registered.
764
+ * Synchronous: `provenance` is a plain property the canvas reads without
765
+ * awaiting.
766
+ */
767
+ readonly syncConfig: () => RegisteredSyncConfig | undefined;
768
+ readonly now: () => Date;
769
+ }
770
+ /** Room types' freshness, from the sync state — stale past the window, or never synced. */
771
+ declare function roomTypesFreshness(records: readonly SyncStateRecord[], now: Date, staleAfterMs: number): LiveFreshness;
772
+ /**
773
+ * The synced room types as a live facts source. Keys are room type `code`s.
774
+ *
775
+ * Throws only for a structurally invalid `adminUrl` — at config load, where
776
+ * the message can name the knob.
777
+ */
778
+ declare function yhikasRoomFacts(options?: YhikasRoomFactsOptions, runtime?: RoomFactsRuntime): LiveSourceSpec;
779
+ //#endregion
780
+ //#region src/room-page-record.d.ts
781
+ /**
782
+ * `recordRoomPage(code, pageId)` — for a site's own migration or seeder that
783
+ * wrote a room's page itself: records that the room HAS its page, so the sync
784
+ * never creates a draft for it (`yhikas_room_pages`, origin `registered`).
785
+ *
786
+ * Never overwrites: a code already recorded keeps its first page. Server-side
787
+ * only — call it from a seeder or a `lumi` command, never from a route that
788
+ * forwards request input.
789
+ */
790
+ /**
791
+ * @returns `true` when recorded; `false` when the code already has a page, or
792
+ * a sync run is creating one right now (its claim is fresh).
793
+ */
794
+ declare function recordRoomPage(code: string, pageId: string): Promise<boolean>;
795
+ //#endregion
796
+ //#region src/room-pages-adapters.d.ts
797
+ /**
798
+ * One column of an entity table, by field name — or a loud error naming it.
799
+ *
800
+ * `getTable()` is typed `PgTableWithColumns<any>`: an entity's columns are
801
+ * generated at runtime, so their names are not in the type. Narrowed here with
802
+ * drizzle's own `is()` (which survives duplicated module instances, unlike
803
+ * `instanceof`) instead of trusting an `any`. For the page entity, `slug`
804
+ * comes from `sluggable()` and `parent` from `pathParent()`; a page entity
805
+ * without either cannot hold room pages, and says so.
806
+ */
807
+ declare function columnOf(table: Readonly<Record<string, unknown>>, entity: string, field: string): PgColumn;
808
+ /**
809
+ * Published room types, read through the CALLER's client — the job's (as the
810
+ * sync actor) or a render's (as the visitor). An unpublished row — the sync's
811
+ * way of retiring a room — is never answered, here or by the status filter in
812
+ * SQL: both, because the reader is the one place a draft price could leak.
813
+ */
814
+ declare function toRoomTypeReader(client: AdminClientLike, defaultLocale?: string): RoomTypeReader;
815
+ /**
816
+ * The page entity, for the drafts. Writes go through `AdminClient` — hooks,
817
+ * validation, the path guard, audit — as the job's own identity; nothing here
818
+ * elevates.
819
+ */
820
+ declare function toRoomPageWriter(client: AdminClientLike, defaultLocale: string, logger?: SyncLogger): RoomPageWriter;
821
+ //#endregion
1369
822
  //#region src/upstream/client.d.ts
1370
823
  interface YhikasUpstreamClientOptions {
1371
824
  /** Origin of the yhikas-admin deployment. Must be http or https. */
@@ -1408,6 +861,13 @@ interface SyncRunDeps {
1408
861
  readonly floor: SanityFloor;
1409
862
  /** Injectable so tests are not order-dependent on the wall clock. */
1410
863
  readonly now?: () => Date;
864
+ /**
865
+ * Runs once, after every resource has been attempted, when ROOM TYPES
866
+ * synced successfully — the room-page drafts (`roomPages`). Whatever it
867
+ * throws is logged and swallowed: the prices are already current, and a page
868
+ * that cannot be written must never fail, retry or alert the sync.
869
+ */
870
+ readonly afterRoomTypesSynced?: () => Promise<void>;
1411
871
  }
1412
872
  interface ResourceOutcome {
1413
873
  readonly resource: SyncResource;
@@ -1499,5 +959,5 @@ declare function assessStaleness(records: readonly SyncStateRecord[], now: Date,
1499
959
  */
1500
960
  declare function assertNotStale(records: readonly SyncStateRecord[], now: Date, staleAfterMs: number): ResourceStaleness[];
1501
961
  //#endregion
1502
- export { API_KEY_ENV_VAR, type AdminClientLike, type ApplyCounts, type ApplyPlanInput, BASE_URL_ENV_VAR, DECLARED_CURRENCY, DECLARED_VAT_TREATMENT, type DiffPlan, EN_LOCALE, ET_LOCALE, type HtmlSanitizer, LEGAL_DOCUMENTS_RESOURCE, type LegalDocumentRow, type LocalRow, MAX_SITE_NOTICES, type MultilingualText, type PlanDiffInput, type ProjectedRow, type ProjectionOptions, type ProjectionResult, ROOM_TYPES_RESOURCE, type ResolvedYhikasSyncConfig, type ResourceOutcome, type ResourceStaleness, type RoomTypeRow, SITE_INFO_KEY, SITE_INFO_RESOURCE, SYNCED_ENTITIES, SYNC_JOB_NAME, SYNC_RESOURCES, type SanityFloor, type SiteInfoResponse, type SiteNoticeRow, type SkippedRow, type SyncEntityClient, type SyncLogger, type SyncResource, type SyncRunDeps, type SyncRunSummary, type SyncStateRecord, type SyncStateStore, type UpstreamFailureKind, VAT_TREATMENTS, WATCHDOG_JOB_NAME, YHIKAS_LEGAL_DOCUMENT_ENTITY, YHIKAS_ROOM_TYPE_ENTITY, YHIKAS_SITE_INFO_ENTITY, YHIKAS_SYNC_ACTOR_ID, YHIKAS_SYNC_PLUGIN_NAME, YhikasApplyPartialError, YhikasLegalDocument, YhikasRoomType, YhikasSiteInfo, type YhikasSyncConfig, YhikasSyncConfigError, YhikasSyncRefusedError, YhikasSyncRunError, YhikasSyncStaleError, YhikasSyncedContentReadOnlyError, YhikasUpstreamClient, type YhikasUpstreamClientOptions, YhikasUpstreamError, applyPlan, assertNotStale, assessStaleness, createSyncStateStore, legalDocumentRowSchema, legalDocumentsResponseSchema, machineWritten, multilingualTextSchema, planDiff, projectLegalDocuments, projectRoomTypes, projectSiteInfo, readLocalRows, resolveYhikasSyncConfig, roomTypeRowSchema, roomTypesResponseSchema, runYhikasSync, secondaryLocaleOf, siteInfoResponseSchema, siteNoticeRowSchema, stableHash, toSyncEntityClient, truncateError, yhikasSyncStateTable };
962
+ export { API_KEY_ENV_VAR, AREA_PRICED_UNIT_NAMES, type AdminClientLike, type ApplyCounts, type ApplyPlanInput, BASE_URL_ENV_VAR, DECLARED_CURRENCY, DECLARED_VAT_TREATMENT, DEFAULT_ROOM_FACT_FIELDS, type DiffPlan, EN_LOCALE, ET_LOCALE, type HtmlSanitizer, LEGAL_DOCUMENTS_RESOURCE, type LegalDocumentRow, type LocalRow, MAX_ROOM_PAGES_PER_RUN, MAX_SITE_NOTICES, type MultilingualText, PAGE_SLUG_PATTERN, type PlanDiffInput, type ProjectedRow, type ProjectionOptions, type ProjectionResult, ROOM_FACTS, ROOM_FACT_NAMES, ROOM_PAGE_CLAIM_LEASE_MS, ROOM_PAGE_ORIGINS, ROOM_PAGE_READ_CHUNK, ROOM_PAGE_REFUSAL_KINDS, ROOM_TYPES_RESOURCE, type RegisteredSyncConfig, type ResolvedRoomPagesConfig, type ResolvedYhikasSyncConfig, type ResourceOutcome, type ResourceStaleness, type RoomFactName, type RoomFactsRuntime, type RoomOfferCandidate, type RoomOfferPredicate, type RoomPageBlock, type RoomPageDraft, type RoomPageDraftDeps, type RoomPageDraftSummary, type RoomPageOrigin, type RoomPageRecord, type RoomPageRefusal, type RoomPageRefusalKind, type RoomPageRegistry, type RoomPageSkip, type RoomPageWriter, type RoomTypeForPage, type RoomTypeReader, type RoomTypeRow, SITE_INFO_KEY, SITE_INFO_RESOURCE, SYNCED_ENTITIES, SYNC_JOB_NAME, SYNC_RESOURCES, type SanityFloor, type SiteInfoResponse, type SiteNoticeRow, type SkippedRow, type SyncEntityClient, type SyncLogger, type SyncResource, type SyncRunDeps, type SyncRunSummary, type SyncStateRecord, type SyncStateStore, type UpstreamFailureKind, VAT_TREATMENTS, WATCHDOG_JOB_NAME, YHIKAS_LEGAL_DOCUMENT_ENTITY, YHIKAS_ROOM_TYPE_ENTITY, YHIKAS_SITE_INFO_ENTITY, YHIKAS_SYNC_ACTOR_ID, YHIKAS_SYNC_PLUGIN_NAME, YhikasApplyPartialError, YhikasLegalDocument, type YhikasRoomFactsOptions, YhikasRoomPageRecordError, type YhikasRoomPagesConfig, YhikasRoomType, YhikasSiteInfo, type YhikasSyncConfig, YhikasSyncConfigError, YhikasSyncRefusedError, YhikasSyncRunError, YhikasSyncStaleError, YhikasSyncedContentReadOnlyError, YhikasUpstreamClient, type YhikasUpstreamClientOptions, YhikasUpstreamError, applyPlan, assertNotStale, assessStaleness, cadenceOf, classifyRefusal, columnOf, createRoomPageDrafts, createRoomPageRegistry, createSyncStateStore, decimalOf, isPublishedRoomType, isRoomOffer, legalDocumentRowSchema, legalDocumentsResponseSchema, machineWritten, multilingualTextSchema, planDiff, projectLegalDocuments, projectRoomTypes, projectSiteInfo, readLocalRows, recordRoomPage, resolveYhikasSyncConfig, roomFactItemsFrom, roomFactsFrom, roomFactsOf, roomPageSlug, roomTypeForPageFrom, roomTypeRowSchema, roomTypesFreshness, roomTypesResponseSchema, runYhikasSync, secondaryLocaleOf, siteInfoResponseSchema, siteNoticeRowSchema, stableHash, toRoomPageWriter, toRoomTypeReader, toSyncEntityClient, truncateError, yhikasRoomFacts, yhikasRoomPagesTable, yhikasSyncStateTable };
1503
963
  //# sourceMappingURL=index.d.mts.map