@echovisionlab/geul-common 0.1.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 (47) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +37 -0
  3. package/package.json +89 -0
  4. package/src/collaboration/artist.ts +63 -0
  5. package/src/collaboration/block-room-codec/ai-document-applicator.ts +319 -0
  6. package/src/collaboration/block-room-codec/ai-document-field-mutations.ts +557 -0
  7. package/src/collaboration/block-room-codec/ai-document-page-structure-mutations.ts +449 -0
  8. package/src/collaboration/block-room-codec/ai-document-values.ts +368 -0
  9. package/src/collaboration/block-room-codec/hydration.ts +257 -0
  10. package/src/collaboration/block-room-codec/internal.ts +432 -0
  11. package/src/collaboration/block-room-codec/locale-change-validation.ts +237 -0
  12. package/src/collaboration/block-room-codec/locale-presence.ts +827 -0
  13. package/src/collaboration/block-room-codec/materialization.ts +450 -0
  14. package/src/collaboration/block-room-codec/observation.ts +518 -0
  15. package/src/collaboration/block-room-codec/payload-mutations.ts +347 -0
  16. package/src/collaboration/block-room-codec/room-access.ts +154 -0
  17. package/src/collaboration/block-room-codec/structure-mutations.ts +456 -0
  18. package/src/collaboration/block-room-codec.ts +86 -0
  19. package/src/collaboration/campaign.ts +37 -0
  20. package/src/collaboration/document-layout.ts +75 -0
  21. package/src/collaboration/document.ts +185 -0
  22. package/src/collaboration/email-layout.ts +150 -0
  23. package/src/collaboration/form.ts +513 -0
  24. package/src/collaboration/label.ts +49 -0
  25. package/src/collaboration/map-theme.ts +157 -0
  26. package/src/collaboration/member-id.ts +9 -0
  27. package/src/collaboration/menu.ts +258 -0
  28. package/src/collaboration/metadata-ai.ts +99 -0
  29. package/src/collaboration/page.ts +483 -0
  30. package/src/collaboration/post-series.ts +96 -0
  31. package/src/collaboration/post.ts +59 -0
  32. package/src/collaboration/release.ts +221 -0
  33. package/src/collaboration/runtime-events.ts +547 -0
  34. package/src/collaboration/work.ts +107 -0
  35. package/src/editor/link-normalization.ts +212 -0
  36. package/src/editor/materialized-blocks.ts +58 -0
  37. package/src/index.ts +22 -0
  38. package/src/media/block-schemas.ts +78 -0
  39. package/src/media/hydration.ts +226 -0
  40. package/src/page/block-fixtures.ts +586 -0
  41. package/src/page/index.ts +3 -0
  42. package/src/page/types.ts +57 -0
  43. package/src/post/index.ts +1 -0
  44. package/src/post/types.ts +13 -0
  45. package/src/test/random-id.ts +9 -0
  46. package/src/translation/release.ts +88 -0
  47. package/src/types.ts +14 -0
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ const CANONICAL_UUID_PATTERN =
4
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
5
+
6
+ /** Domain Member IDs cross service and durable-state boundaries in canonical form only. */
7
+ export const memberIdSchema = z
8
+ .string()
9
+ .regex(CANONICAL_UUID_PATTERN, "Expected a canonical Member UUID");
@@ -0,0 +1,258 @@
1
+ import * as Y from "yjs";
2
+
3
+ export const MENU_CONTEXT_MAP_NAME = "menu-context";
4
+ export const MENU_ROOT_MAP_NAME = "menu-root";
5
+ export const MENU_ITEMS_MAP_NAME = "menu-items";
6
+ export const MENU_PARENTS_MAP_NAME = "menu-parents";
7
+ export const MENU_ORDERS_MAP_NAME = "menu-orders";
8
+ export const MENU_SOURCE_LABELS_MAP_NAME = "menu-source-labels";
9
+ export const MENU_LOCALE_LABELS_MAP_NAME = "menu-locale-labels";
10
+ export const MENU_ROOT_PARENT = "root";
11
+
12
+ export interface MenuCollaborationItem {
13
+ id: string;
14
+ label?: string;
15
+ linkType: string;
16
+ url?: string;
17
+ targetId?: string;
18
+ targetSlug?: string;
19
+ openInNewTab?: boolean;
20
+ visibilityMode?: string;
21
+ visibilityRoles?: string[];
22
+ localizationMode?: string;
23
+ fixedLocale?: string;
24
+ children?: MenuCollaborationItem[];
25
+ }
26
+
27
+ type StoredMenuItem = Omit<MenuCollaborationItem, "label" | "children">;
28
+
29
+ export interface MenuCanonicalRoomInput {
30
+ sourceLocale: string;
31
+ locale: string;
32
+ localeExists: boolean;
33
+ name: string;
34
+ items: readonly MenuCollaborationItem[];
35
+ sourceLabels: Readonly<Record<string, string>>;
36
+ requestedLabels: Readonly<Record<string, string>>;
37
+ }
38
+
39
+ export interface MenuCanonicalSnapshot {
40
+ name: string;
41
+ items: MenuCollaborationItem[];
42
+ requestedLabels: Record<string, string>;
43
+ }
44
+
45
+ export function hydrateMenuCanonicalRoom(input: MenuCanonicalRoomInput): Y.Doc {
46
+ const document = new Y.Doc();
47
+ document.transact(() => {
48
+ const context = document.getMap<string | boolean>(MENU_CONTEXT_MAP_NAME);
49
+ context.set("sourceLocale", input.sourceLocale);
50
+ context.set("locale", input.locale);
51
+ context.set("localeExists", input.localeExists);
52
+ document.getMap<string>(MENU_ROOT_MAP_NAME).set("name", input.name);
53
+ replaceMenuStructure(document, input.items);
54
+ const sourceLabels = document.getMap<string>(MENU_SOURCE_LABELS_MAP_NAME);
55
+ const requestedLabels = menuLocaleLabelsMap(document);
56
+ for (const [id, label] of Object.entries(input.sourceLabels))
57
+ sourceLabels.set(id, label);
58
+ for (const [id, label] of Object.entries(input.requestedLabels))
59
+ requestedLabels.set(id, label);
60
+ });
61
+ return document;
62
+ }
63
+
64
+ export function menuLocaleLabelsMap(document: Y.Doc): Y.Map<string> {
65
+ return document.getMap<string>(MENU_LOCALE_LABELS_MAP_NAME);
66
+ }
67
+
68
+ export function materializeMenuCanonicalItems(
69
+ document: Y.Doc,
70
+ ): MenuCollaborationItem[] {
71
+ const items = document.getMap<string>(MENU_ITEMS_MAP_NAME);
72
+ const parents = document.getMap<string>(MENU_PARENTS_MAP_NAME);
73
+ const orders = document.getMap<number>(MENU_ORDERS_MAP_NAME);
74
+ const sourceLabels = document.getMap<string>(MENU_SOURCE_LABELS_MAP_NAME);
75
+ const requestedLabels = menuLocaleLabelsMap(document);
76
+ const locale = document
77
+ .getMap<string | boolean>(MENU_CONTEXT_MAP_NAME)
78
+ .get("locale");
79
+ if (typeof locale !== "string") {
80
+ throw new Error("Menu collaboration locale is required");
81
+ }
82
+ const children = new Map<string, string[]>();
83
+ for (const id of items.keys()) {
84
+ const parent = parents.get(id) ?? MENU_ROOT_PARENT;
85
+ const ids = children.get(parent) ?? [];
86
+ ids.push(id);
87
+ children.set(parent, ids);
88
+ }
89
+ for (const ids of children.values()) {
90
+ ids.sort(
91
+ (left, right) =>
92
+ (orders.get(left) ?? 0) - (orders.get(right) ?? 0) ||
93
+ left.localeCompare(right),
94
+ );
95
+ }
96
+ let materializedCount = 0;
97
+ const build = (parent: string, depth: number): MenuCollaborationItem[] => {
98
+ if (depth > 32) throw new Error("Invalid Menu collaboration tree depth");
99
+ return (children.get(parent) ?? []).map((id) => {
100
+ materializedCount += 1;
101
+ const raw = items.get(id)!;
102
+ const stored = JSON.parse(raw) as StoredMenuItem;
103
+ const nested = build(id, depth + 1);
104
+ const ownsLabel = menuItemOwnsLocaleLabel(stored, locale);
105
+ return {
106
+ ...stored,
107
+ id,
108
+ label: ownsLabel
109
+ ? requestedLabels.has(id)
110
+ ? (requestedLabels.get(id) ?? "")
111
+ : (sourceLabels.get(id) ?? "")
112
+ : "",
113
+ ...(nested.length > 0 ? { children: nested } : {}),
114
+ };
115
+ });
116
+ };
117
+ const result = build(MENU_ROOT_PARENT, 0);
118
+ if (materializedCount !== items.size) {
119
+ throw new Error("Invalid Menu collaboration tree parent");
120
+ }
121
+ return result;
122
+ }
123
+
124
+ export function replaceMenuCanonicalSource(
125
+ document: Y.Doc,
126
+ name: string,
127
+ items: readonly MenuCollaborationItem[],
128
+ ): void {
129
+ document.getMap<string>(MENU_ROOT_MAP_NAME).set("name", name);
130
+ replaceMenuStructure(document, items);
131
+ const labels = menuLocaleLabelsMap(document);
132
+ const locale = document
133
+ .getMap<string | boolean>(MENU_CONTEXT_MAP_NAME)
134
+ .get("locale");
135
+ if (typeof locale !== "string") {
136
+ throw new Error("Menu collaboration locale is required");
137
+ }
138
+ const ids = new Set(flattenMenuItems(items).map(({ item }) => item.id));
139
+ for (const id of [...labels.keys()]) if (!ids.has(id)) labels.delete(id);
140
+ for (const { item } of flattenMenuItems(items)) {
141
+ if (!menuItemOwnsLocaleLabel(item, locale)) {
142
+ labels.delete(item.id);
143
+ } else if (item.label !== undefined) {
144
+ labels.set(item.id, item.label);
145
+ }
146
+ }
147
+ }
148
+
149
+ export function setMenuLocaleLabel(
150
+ document: Y.Doc,
151
+ itemId: string,
152
+ label: string,
153
+ ): void {
154
+ if (!document.getMap<string>(MENU_ITEMS_MAP_NAME).has(itemId)) {
155
+ throw new Error(`Unknown Menu item: ${itemId}`);
156
+ }
157
+ menuLocaleLabelsMap(document).set(itemId, label);
158
+ }
159
+
160
+ export function unsetMenuLocaleLabel(document: Y.Doc, itemId: string): void {
161
+ menuLocaleLabelsMap(document).delete(itemId);
162
+ }
163
+
164
+ export function extractMenuCanonicalSnapshot(
165
+ document: Y.Doc,
166
+ ): MenuCanonicalSnapshot {
167
+ const requestedLabels: Record<string, string> = {};
168
+ const items = document.getMap<string>(MENU_ITEMS_MAP_NAME);
169
+ for (const [id, label] of menuLocaleLabelsMap(document)) {
170
+ if (!items.has(id))
171
+ throw new Error(`Unknown Menu locale label item: ${id}`);
172
+ requestedLabels[id] = label;
173
+ }
174
+ return {
175
+ name: document.getMap<string>(MENU_ROOT_MAP_NAME).get("name") ?? "",
176
+ items: materializeMenuCanonicalItems(document).map(stripMenuLabels),
177
+ requestedLabels,
178
+ };
179
+ }
180
+
181
+ function replaceMenuStructure(
182
+ document: Y.Doc,
183
+ nextItems: readonly MenuCollaborationItem[],
184
+ ): void {
185
+ const flattened = flattenMenuItems(nextItems);
186
+ const nextIDs = new Set(flattened.map(({ item }) => item.id));
187
+ if (nextIDs.size !== flattened.length || nextIDs.has(MENU_ROOT_PARENT)) {
188
+ throw new Error("Invalid Menu collaboration item identity");
189
+ }
190
+ const items = document.getMap<string>(MENU_ITEMS_MAP_NAME);
191
+ const parents = document.getMap<string>(MENU_PARENTS_MAP_NAME);
192
+ const orders = document.getMap<number>(MENU_ORDERS_MAP_NAME);
193
+ for (const id of [...items.keys()]) {
194
+ if (!nextIDs.has(id)) {
195
+ items.delete(id);
196
+ parents.delete(id);
197
+ orders.delete(id);
198
+ }
199
+ }
200
+ for (const { item, parent, order } of flattened) {
201
+ const stored = menuItemStructure(item);
202
+ items.set(item.id, JSON.stringify(stored));
203
+ parents.set(item.id, parent);
204
+ orders.set(item.id, order);
205
+ }
206
+ }
207
+
208
+ function flattenMenuItems(items: readonly MenuCollaborationItem[]): Array<{
209
+ item: MenuCollaborationItem;
210
+ parent: string;
211
+ order: number;
212
+ }> {
213
+ const output: Array<{
214
+ item: MenuCollaborationItem;
215
+ parent: string;
216
+ order: number;
217
+ }> = [];
218
+ const walk = (
219
+ current: readonly MenuCollaborationItem[],
220
+ parent: string,
221
+ depth: number,
222
+ ) => {
223
+ if (depth > 32) throw new Error("Invalid Menu collaboration tree depth");
224
+ current.forEach((item, order) => {
225
+ if (!item.id) throw new Error("Menu collaboration item ID is required");
226
+ output.push({ item, parent, order });
227
+ walk(item.children ?? [], item.id, depth + 1);
228
+ });
229
+ };
230
+ walk(items, MENU_ROOT_PARENT, 0);
231
+ return output;
232
+ }
233
+
234
+ function stripMenuLabels(item: MenuCollaborationItem): MenuCollaborationItem {
235
+ const shared = menuItemStructure(item);
236
+ const children = item.children;
237
+ return {
238
+ ...shared,
239
+ ...(children?.length ? { children: children.map(stripMenuLabels) } : {}),
240
+ };
241
+ }
242
+
243
+ function menuItemStructure(item: MenuCollaborationItem): StoredMenuItem {
244
+ const stored = { ...item };
245
+ delete stored.label;
246
+ delete stored.children;
247
+ return stored;
248
+ }
249
+
250
+ function menuItemOwnsLocaleLabel(
251
+ item: MenuCollaborationItem,
252
+ locale: string,
253
+ ): boolean {
254
+ const fixed =
255
+ item.localizationMode === "fixed_locale" ||
256
+ (item.localizationMode === undefined && item.fixedLocale !== undefined);
257
+ return !fixed || item.fixedLocale === locale;
258
+ }
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import { memberIdSchema } from "./member-id.ts";
3
+
4
+ export const metadataAiFieldSchema = z.enum(["summary"]);
5
+
6
+ export const metadataAiStatusSchema = z.enum([
7
+ "idle",
8
+ "generating",
9
+ "ready",
10
+ "applying",
11
+ ]);
12
+
13
+ export const metadataAiSharedStateSchema = z
14
+ .object({
15
+ status: metadataAiStatusSchema,
16
+ generationId: z.string().nullable(),
17
+ jobId: z.string().nullable(),
18
+ requesterMemberId: memberIdSchema.nullable(),
19
+ /** Non-authoritative Member nickname snapshot; never a Member lookup or mapping key. */
20
+ requesterNickname: z.string().min(1).max(100).nullable(),
21
+ requestedFields: z.array(metadataAiFieldSchema),
22
+ allMetadata: z.boolean(),
23
+ startedAt: z.number().int().nullable(),
24
+ updatedAt: z.number().int().nullable(),
25
+ orphanedAt: z.number().int().nullable(),
26
+ autoClearAt: z.number().int().nullable(),
27
+ })
28
+ .strict()
29
+ .refine(
30
+ (state) =>
31
+ state.requesterNickname === null || state.requesterMemberId !== null,
32
+ {
33
+ message: "requesterNickname requires requesterMemberId",
34
+ path: ["requesterNickname"],
35
+ },
36
+ );
37
+
38
+ export const METADATA_AI_MAP_NAME = "metadata-ai";
39
+ export const METADATA_AI_GRACE_PERIOD_MS = 10_000;
40
+
41
+ export type MetadataAiField = z.infer<typeof metadataAiFieldSchema>;
42
+ export type MetadataAiStatus = z.infer<typeof metadataAiStatusSchema>;
43
+ export type MetadataAiSharedState = z.infer<typeof metadataAiSharedStateSchema>;
44
+ export type MetadataAiFieldValue = string | number | boolean | string[] | null;
45
+
46
+ export const DEFAULT_METADATA_AI_SHARED_STATE: MetadataAiSharedState = {
47
+ status: "idle",
48
+ generationId: null,
49
+ jobId: null,
50
+ requesterMemberId: null,
51
+ requesterNickname: null,
52
+ requestedFields: [],
53
+ allMetadata: false,
54
+ startedAt: null,
55
+ updatedAt: null,
56
+ orphanedAt: null,
57
+ autoClearAt: null,
58
+ };
59
+
60
+ export const METADATA_AI_JSON_KEYS: ReadonlySet<keyof MetadataAiSharedState> =
61
+ new Set(["requestedFields"]);
62
+
63
+ export function extractMetadataAiSharedState(fieldsMap: {
64
+ get(key: string): MetadataAiFieldValue | undefined;
65
+ }): MetadataAiSharedState {
66
+ if (
67
+ fieldsMap.get("requesterUserId") !== undefined ||
68
+ fieldsMap.get("requesterName") !== undefined ||
69
+ fieldsMap.get("requesterDisplayName") !== undefined
70
+ ) {
71
+ throw new Error("Legacy metadata AI requester fields are not supported");
72
+ }
73
+
74
+ const raw: Record<string, unknown> = {};
75
+
76
+ for (const key of Object.keys(metadataAiSharedStateSchema.shape)) {
77
+ let value = fieldsMap.get(key);
78
+
79
+ if (
80
+ METADATA_AI_JSON_KEYS.has(key as keyof MetadataAiSharedState) &&
81
+ typeof value === "string"
82
+ ) {
83
+ try {
84
+ value = JSON.parse(value) as MetadataAiFieldValue;
85
+ } catch {
86
+ throw new Error(`Failed to parse JSON for metadata AI field "${key}"`);
87
+ }
88
+ }
89
+
90
+ if (value !== undefined) {
91
+ raw[key] = value;
92
+ }
93
+ }
94
+
95
+ return metadataAiSharedStateSchema.parse({
96
+ ...DEFAULT_METADATA_AI_SHARED_STATE,
97
+ ...raw,
98
+ });
99
+ }