@femtomc/mu-core 26.2.105 → 26.2.107

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/README.md CHANGED
@@ -49,6 +49,26 @@ console.log(await jsonl.read());
49
49
 
50
50
  These are runtime-agnostic primitives intended for shared HUD capture/render pipelines.
51
51
 
52
+ ## UI contract helpers
53
+
54
+ Interactive UI documents are modeled as first-class, versioned contracts that describe renderable components, actions, and revisions so any rendering surface can stay in sync with agents. `@femtomc/mu-core` exposes the following helpers:
55
+
56
+ - `UI_CONTRACT_VERSION`, `UiDocSchema`, `UiRevisionSchema`, `UiComponentSchema`, and `UiActionSchema` for validating document payloads.
57
+ - `parseUiDoc(...)`, `normalizeUiDocs(...)`, and `uiDocRevisionConflict(...)` for deterministic selection, conflict detection, and safe merging of candidate documents.
58
+ - `UiEventSchema` + `parseUiEvent(...)` for structured event payloads emitted by frontends (every event includes `ui_id`, `action_id`, the originating `revision`, optional `callback_token`, `payload`, and `created_at_ms`).
59
+
60
+ ### Field limits
61
+
62
+ The UI contract enforces strict bounds to keep cross-frontends renderable:
63
+
64
+ - titles are capped at 256 characters and summaries at 1024 characters.
65
+ - documents can contain at most 64 components, where text segments are limited to 2048 characters, lists can hold at most 32 items, and key/value rows are capped at 32 entries.
66
+ - actions are limited to 32 entries; labels are 128 characters, descriptions 512, and callback tokens 128.
67
+
68
+ ### Deterministic ordering
69
+
70
+ `normalizeUiDocs(...)` deduplicates by `ui_id`, chooses the highest `revision.version`, breaks ties with `updated_at_ms`, and falls back to a stable JSON comparison (`stableSerializeJson`) so every renderer observes the same ordering. Use `uiDocRevisionConflict(...)` to detect when two docs claim the same revision but differ, enabling agents to guard against race conditions.
71
+
52
72
  ## Tests / Typecheck
53
73
 
54
74
  From the `mu/` repo root:
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export * from "./hud_runtime.js";
4
4
  export * from "./ids.js";
5
5
  export * from "./persistence.js";
6
6
  export * from "./spec.js";
7
+ export * from "./ui.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export * from "./hud_runtime.js";
4
4
  export * from "./ids.js";
5
5
  export * from "./persistence.js";
6
6
  export * from "./spec.js";
7
+ export * from "./ui.js";
package/dist/ui.d.ts ADDED
@@ -0,0 +1,281 @@
1
+ import { z } from "zod";
2
+ export declare const UI_CONTRACT_VERSION: 1;
3
+ export declare const UiListItemSchema: z.ZodObject<{
4
+ id: z.ZodString;
5
+ label: z.ZodString;
6
+ detail: z.ZodOptional<z.ZodString>;
7
+ tone: z.ZodOptional<z.ZodEnum<{
8
+ error: "error";
9
+ info: "info";
10
+ success: "success";
11
+ warning: "warning";
12
+ muted: "muted";
13
+ accent: "accent";
14
+ dim: "dim";
15
+ }>>;
16
+ }, z.core.$strict>;
17
+ export type UiListItem = z.infer<typeof UiListItemSchema>;
18
+ export declare const UiKeyValueRowSchema: z.ZodObject<{
19
+ key: z.ZodString;
20
+ value: z.ZodString;
21
+ tone: z.ZodOptional<z.ZodEnum<{
22
+ error: "error";
23
+ info: "info";
24
+ success: "success";
25
+ warning: "warning";
26
+ muted: "muted";
27
+ accent: "accent";
28
+ dim: "dim";
29
+ }>>;
30
+ }, z.core.$strict>;
31
+ export type UiKeyValueRow = z.infer<typeof UiKeyValueRowSchema>;
32
+ export declare const UiComponentTextSchema: z.ZodObject<{
33
+ kind: z.ZodLiteral<"text">;
34
+ id: z.ZodString;
35
+ text: z.ZodString;
36
+ tone: z.ZodOptional<z.ZodEnum<{
37
+ error: "error";
38
+ info: "info";
39
+ success: "success";
40
+ warning: "warning";
41
+ muted: "muted";
42
+ accent: "accent";
43
+ dim: "dim";
44
+ }>>;
45
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
46
+ }, z.core.$strict>;
47
+ export type UiComponentText = z.infer<typeof UiComponentTextSchema>;
48
+ export declare const UiComponentListSchema: z.ZodObject<{
49
+ kind: z.ZodLiteral<"list">;
50
+ id: z.ZodString;
51
+ title: z.ZodOptional<z.ZodString>;
52
+ items: z.ZodArray<z.ZodObject<{
53
+ id: z.ZodString;
54
+ label: z.ZodString;
55
+ detail: z.ZodOptional<z.ZodString>;
56
+ tone: z.ZodOptional<z.ZodEnum<{
57
+ error: "error";
58
+ info: "info";
59
+ success: "success";
60
+ warning: "warning";
61
+ muted: "muted";
62
+ accent: "accent";
63
+ dim: "dim";
64
+ }>>;
65
+ }, z.core.$strict>>;
66
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
67
+ }, z.core.$strict>;
68
+ export declare const UiComponentKeyValueSchema: z.ZodObject<{
69
+ kind: z.ZodLiteral<"key_value">;
70
+ id: z.ZodString;
71
+ title: z.ZodOptional<z.ZodString>;
72
+ rows: z.ZodArray<z.ZodObject<{
73
+ key: z.ZodString;
74
+ value: z.ZodString;
75
+ tone: z.ZodOptional<z.ZodEnum<{
76
+ error: "error";
77
+ info: "info";
78
+ success: "success";
79
+ warning: "warning";
80
+ muted: "muted";
81
+ accent: "accent";
82
+ dim: "dim";
83
+ }>>;
84
+ }, z.core.$strict>>;
85
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
86
+ }, z.core.$strict>;
87
+ export declare const UiComponentDividerSchema: z.ZodObject<{
88
+ kind: z.ZodLiteral<"divider">;
89
+ id: z.ZodString;
90
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
+ }, z.core.$strict>;
92
+ export declare const UiComponentSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
93
+ kind: z.ZodLiteral<"text">;
94
+ id: z.ZodString;
95
+ text: z.ZodString;
96
+ tone: z.ZodOptional<z.ZodEnum<{
97
+ error: "error";
98
+ info: "info";
99
+ success: "success";
100
+ warning: "warning";
101
+ muted: "muted";
102
+ accent: "accent";
103
+ dim: "dim";
104
+ }>>;
105
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
106
+ }, z.core.$strict>, z.ZodObject<{
107
+ kind: z.ZodLiteral<"list">;
108
+ id: z.ZodString;
109
+ title: z.ZodOptional<z.ZodString>;
110
+ items: z.ZodArray<z.ZodObject<{
111
+ id: z.ZodString;
112
+ label: z.ZodString;
113
+ detail: z.ZodOptional<z.ZodString>;
114
+ tone: z.ZodOptional<z.ZodEnum<{
115
+ error: "error";
116
+ info: "info";
117
+ success: "success";
118
+ warning: "warning";
119
+ muted: "muted";
120
+ accent: "accent";
121
+ dim: "dim";
122
+ }>>;
123
+ }, z.core.$strict>>;
124
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
125
+ }, z.core.$strict>, z.ZodObject<{
126
+ kind: z.ZodLiteral<"key_value">;
127
+ id: z.ZodString;
128
+ title: z.ZodOptional<z.ZodString>;
129
+ rows: z.ZodArray<z.ZodObject<{
130
+ key: z.ZodString;
131
+ value: z.ZodString;
132
+ tone: z.ZodOptional<z.ZodEnum<{
133
+ error: "error";
134
+ info: "info";
135
+ success: "success";
136
+ warning: "warning";
137
+ muted: "muted";
138
+ accent: "accent";
139
+ dim: "dim";
140
+ }>>;
141
+ }, z.core.$strict>>;
142
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
143
+ }, z.core.$strict>, z.ZodObject<{
144
+ kind: z.ZodLiteral<"divider">;
145
+ id: z.ZodString;
146
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
147
+ }, z.core.$strict>], "kind">;
148
+ export type UiComponent = z.infer<typeof UiComponentSchema>;
149
+ export declare const UiActionKindSchema: z.ZodEnum<{
150
+ primary: "primary";
151
+ secondary: "secondary";
152
+ danger: "danger";
153
+ link: "link";
154
+ }>;
155
+ export type UiActionKind = z.infer<typeof UiActionKindSchema>;
156
+ export declare const UiActionSchema: z.ZodObject<{
157
+ id: z.ZodString;
158
+ label: z.ZodString;
159
+ kind: z.ZodOptional<z.ZodEnum<{
160
+ primary: "primary";
161
+ secondary: "secondary";
162
+ danger: "danger";
163
+ link: "link";
164
+ }>>;
165
+ description: z.ZodOptional<z.ZodString>;
166
+ component_id: z.ZodOptional<z.ZodString>;
167
+ callback_token: z.ZodOptional<z.ZodString>;
168
+ payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
169
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
170
+ }, z.core.$strict>;
171
+ export type UiAction = z.infer<typeof UiActionSchema>;
172
+ export declare const UiRevisionSchema: z.ZodObject<{
173
+ id: z.ZodString;
174
+ version: z.ZodNumber;
175
+ }, z.core.$strict>;
176
+ export type UiRevision = z.infer<typeof UiRevisionSchema>;
177
+ export declare const UiDocSchema: z.ZodObject<{
178
+ v: z.ZodDefault<z.ZodLiteral<1>>;
179
+ ui_id: z.ZodString;
180
+ title: z.ZodString;
181
+ summary: z.ZodOptional<z.ZodString>;
182
+ components: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
183
+ kind: z.ZodLiteral<"text">;
184
+ id: z.ZodString;
185
+ text: z.ZodString;
186
+ tone: z.ZodOptional<z.ZodEnum<{
187
+ error: "error";
188
+ info: "info";
189
+ success: "success";
190
+ warning: "warning";
191
+ muted: "muted";
192
+ accent: "accent";
193
+ dim: "dim";
194
+ }>>;
195
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
196
+ }, z.core.$strict>, z.ZodObject<{
197
+ kind: z.ZodLiteral<"list">;
198
+ id: z.ZodString;
199
+ title: z.ZodOptional<z.ZodString>;
200
+ items: z.ZodArray<z.ZodObject<{
201
+ id: z.ZodString;
202
+ label: z.ZodString;
203
+ detail: z.ZodOptional<z.ZodString>;
204
+ tone: z.ZodOptional<z.ZodEnum<{
205
+ error: "error";
206
+ info: "info";
207
+ success: "success";
208
+ warning: "warning";
209
+ muted: "muted";
210
+ accent: "accent";
211
+ dim: "dim";
212
+ }>>;
213
+ }, z.core.$strict>>;
214
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
215
+ }, z.core.$strict>, z.ZodObject<{
216
+ kind: z.ZodLiteral<"key_value">;
217
+ id: z.ZodString;
218
+ title: z.ZodOptional<z.ZodString>;
219
+ rows: z.ZodArray<z.ZodObject<{
220
+ key: z.ZodString;
221
+ value: z.ZodString;
222
+ tone: z.ZodOptional<z.ZodEnum<{
223
+ error: "error";
224
+ info: "info";
225
+ success: "success";
226
+ warning: "warning";
227
+ muted: "muted";
228
+ accent: "accent";
229
+ dim: "dim";
230
+ }>>;
231
+ }, z.core.$strict>>;
232
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
233
+ }, z.core.$strict>, z.ZodObject<{
234
+ kind: z.ZodLiteral<"divider">;
235
+ id: z.ZodString;
236
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
237
+ }, z.core.$strict>], "kind">>;
238
+ actions: z.ZodDefault<z.ZodArray<z.ZodObject<{
239
+ id: z.ZodString;
240
+ label: z.ZodString;
241
+ kind: z.ZodOptional<z.ZodEnum<{
242
+ primary: "primary";
243
+ secondary: "secondary";
244
+ danger: "danger";
245
+ link: "link";
246
+ }>>;
247
+ description: z.ZodOptional<z.ZodString>;
248
+ component_id: z.ZodOptional<z.ZodString>;
249
+ callback_token: z.ZodOptional<z.ZodString>;
250
+ payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
251
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
252
+ }, z.core.$strict>>>;
253
+ revision: z.ZodObject<{
254
+ id: z.ZodString;
255
+ version: z.ZodNumber;
256
+ }, z.core.$strict>;
257
+ updated_at_ms: z.ZodNumber;
258
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
259
+ }, z.core.$strict>;
260
+ export type UiDoc = z.infer<typeof UiDocSchema>;
261
+ export declare const UiEventSchema: z.ZodObject<{
262
+ ui_id: z.ZodString;
263
+ action_id: z.ZodString;
264
+ component_id: z.ZodNullable<z.ZodOptional<z.ZodString>>;
265
+ revision: z.ZodObject<{
266
+ id: z.ZodString;
267
+ version: z.ZodNumber;
268
+ }, z.core.$strict>;
269
+ callback_token: z.ZodOptional<z.ZodString>;
270
+ payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
271
+ created_at_ms: z.ZodNumber;
272
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
273
+ }, z.core.$strict>;
274
+ export type UiEvent = z.infer<typeof UiEventSchema>;
275
+ export declare function parseUiDoc(input: unknown): UiDoc | null;
276
+ export declare function normalizeUiDocs(input: unknown, opts?: {
277
+ maxDocs?: number;
278
+ }): UiDoc[];
279
+ export declare function uiDocRevisionConflict(left: UiDoc, right: UiDoc): boolean;
280
+ export declare function parseUiEvent(input: unknown): UiEvent | null;
281
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,mBAAmB,EAAG,CAAU,CAAC;AAsB9C,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;kBAOnB,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;kBAMtB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;kBAQxB,CAAC;AACX,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;kBAQxB,CAAC;AAEX,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;kBAQ5B,CAAC;AAEX,eAAO,MAAM,wBAAwB;;;;kBAM3B,CAAC;AAEX,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAK5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,kBAAkB;;;;;EAAqD,CAAC;AACrF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;kBAWjB,CAAC;AACX,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,eAAO,MAAM,gBAAgB;;;kBAKnB,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAYd,CAAC;AACX,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,eAAO,MAAM,aAAa;;;;;;;;;;;;kBAWhB,CAAC;AACX,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAkDpD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAEvD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,KAAK,EAAE,CAoBxF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAQxE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAM3D"}
package/dist/ui.js ADDED
@@ -0,0 +1,203 @@
1
+ import { z } from "zod";
2
+ import { HudToneSchema, stableSerializeJson } from "./hud.js";
3
+ export const UI_CONTRACT_VERSION = 1;
4
+ const UI_DOC_TITLE_MAX_LENGTH = 256;
5
+ const UI_DOC_SUBTITLE_MAX_LENGTH = 512;
6
+ const UI_DOC_SUMMARY_MAX_LENGTH = 1024;
7
+ const UI_COMPONENT_LIMIT = 64;
8
+ const UI_TEXT_COMPONENT_MAX_LENGTH = 2048;
9
+ const UI_LIST_ITEM_LIMIT = 32;
10
+ const UI_LIST_ITEM_DETAIL_MAX_LENGTH = 1024;
11
+ const UI_KEY_VALUE_ROW_LIMIT = 32;
12
+ const UI_KEY_MAX_LENGTH = 64;
13
+ const UI_VALUE_MAX_LENGTH = 256;
14
+ const UI_ACTION_LIMIT = 32;
15
+ const UI_ACTION_LABEL_MAX_LENGTH = 128;
16
+ const UI_ACTION_DESCRIPTION_MAX_LENGTH = 512;
17
+ const UI_CALLBACK_TOKEN_MAX_LENGTH = 128;
18
+ const UI_REVISION_ID_MAX_LENGTH = 64;
19
+ const NonEmptyText = (max) => z.string().trim().min(1).max(max);
20
+ const NonEmptyId = z.string().trim().min(1).max(64);
21
+ const CallbackTokenSchema = z.string().trim().min(1).max(UI_CALLBACK_TOKEN_MAX_LENGTH);
22
+ export const UiListItemSchema = z
23
+ .object({
24
+ id: NonEmptyId,
25
+ label: NonEmptyText(UI_ACTION_LABEL_MAX_LENGTH),
26
+ detail: NonEmptyText(UI_LIST_ITEM_DETAIL_MAX_LENGTH).optional(),
27
+ tone: HudToneSchema.optional(),
28
+ })
29
+ .strict();
30
+ export const UiKeyValueRowSchema = z
31
+ .object({
32
+ key: NonEmptyText(UI_KEY_MAX_LENGTH),
33
+ value: NonEmptyText(UI_VALUE_MAX_LENGTH),
34
+ tone: HudToneSchema.optional(),
35
+ })
36
+ .strict();
37
+ export const UiComponentTextSchema = z
38
+ .object({
39
+ kind: z.literal("text"),
40
+ id: NonEmptyId,
41
+ text: NonEmptyText(UI_TEXT_COMPONENT_MAX_LENGTH),
42
+ tone: HudToneSchema.optional(),
43
+ metadata: z.record(z.string(), z.unknown()).default({}),
44
+ })
45
+ .strict();
46
+ export const UiComponentListSchema = z
47
+ .object({
48
+ kind: z.literal("list"),
49
+ id: NonEmptyId,
50
+ title: NonEmptyText(UI_DOC_SUBTITLE_MAX_LENGTH).optional(),
51
+ items: z.array(UiListItemSchema).min(1).max(UI_LIST_ITEM_LIMIT),
52
+ metadata: z.record(z.string(), z.unknown()).default({}),
53
+ })
54
+ .strict();
55
+ export const UiComponentKeyValueSchema = z
56
+ .object({
57
+ kind: z.literal("key_value"),
58
+ id: NonEmptyId,
59
+ title: NonEmptyText(UI_DOC_SUBTITLE_MAX_LENGTH).optional(),
60
+ rows: z.array(UiKeyValueRowSchema).min(1).max(UI_KEY_VALUE_ROW_LIMIT),
61
+ metadata: z.record(z.string(), z.unknown()).default({}),
62
+ })
63
+ .strict();
64
+ export const UiComponentDividerSchema = z
65
+ .object({
66
+ kind: z.literal("divider"),
67
+ id: NonEmptyId,
68
+ metadata: z.record(z.string(), z.unknown()).default({}),
69
+ })
70
+ .strict();
71
+ export const UiComponentSchema = z.discriminatedUnion("kind", [
72
+ UiComponentTextSchema,
73
+ UiComponentListSchema,
74
+ UiComponentKeyValueSchema,
75
+ UiComponentDividerSchema,
76
+ ]);
77
+ export const UiActionKindSchema = z.enum(["primary", "secondary", "danger", "link"]);
78
+ export const UiActionSchema = z
79
+ .object({
80
+ id: NonEmptyId,
81
+ label: NonEmptyText(UI_ACTION_LABEL_MAX_LENGTH),
82
+ kind: UiActionKindSchema.optional(),
83
+ description: NonEmptyText(UI_ACTION_DESCRIPTION_MAX_LENGTH).optional(),
84
+ component_id: NonEmptyId.optional(),
85
+ callback_token: CallbackTokenSchema.optional(),
86
+ payload: z.record(z.string(), z.unknown()).default({}),
87
+ metadata: z.record(z.string(), z.unknown()).default({}),
88
+ })
89
+ .strict();
90
+ export const UiRevisionSchema = z
91
+ .object({
92
+ id: NonEmptyText(UI_REVISION_ID_MAX_LENGTH),
93
+ version: z.number().int().nonnegative(),
94
+ })
95
+ .strict();
96
+ export const UiDocSchema = z
97
+ .object({
98
+ v: z.literal(UI_CONTRACT_VERSION).default(UI_CONTRACT_VERSION),
99
+ ui_id: NonEmptyId,
100
+ title: NonEmptyText(UI_DOC_TITLE_MAX_LENGTH),
101
+ summary: NonEmptyText(UI_DOC_SUMMARY_MAX_LENGTH).optional(),
102
+ components: z.array(UiComponentSchema).min(1).max(UI_COMPONENT_LIMIT),
103
+ actions: z.array(UiActionSchema).max(UI_ACTION_LIMIT).default([]),
104
+ revision: UiRevisionSchema,
105
+ updated_at_ms: z.number().int().nonnegative(),
106
+ metadata: z.record(z.string(), z.unknown()).default({}),
107
+ })
108
+ .strict();
109
+ export const UiEventSchema = z
110
+ .object({
111
+ ui_id: NonEmptyId,
112
+ action_id: NonEmptyId,
113
+ component_id: NonEmptyId.optional().nullable(),
114
+ revision: UiRevisionSchema,
115
+ callback_token: CallbackTokenSchema.optional(),
116
+ payload: z.record(z.string(), z.unknown()).default({}),
117
+ created_at_ms: z.number().int().nonnegative(),
118
+ metadata: z.record(z.string(), z.unknown()).default({}),
119
+ })
120
+ .strict();
121
+ function isPlainObject(value) {
122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
123
+ }
124
+ function parseUiDocCandidate(value) {
125
+ const parsed = UiDocSchema.safeParse(value);
126
+ if (!parsed.success) {
127
+ return null;
128
+ }
129
+ return parsed.data;
130
+ }
131
+ function uiDocCandidates(input) {
132
+ if (Array.isArray(input)) {
133
+ return input;
134
+ }
135
+ if (isPlainObject(input)) {
136
+ return [input];
137
+ }
138
+ return [];
139
+ }
140
+ function normalizedUiDocLimit(limit) {
141
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
142
+ return 12;
143
+ }
144
+ const parsed = Math.trunc(limit);
145
+ if (parsed < 1) {
146
+ return 1;
147
+ }
148
+ if (parsed > 64) {
149
+ return 64;
150
+ }
151
+ return parsed;
152
+ }
153
+ function deterministicUiDocChoice(left, right) {
154
+ if (left.revision.version !== right.revision.version) {
155
+ return left.revision.version > right.revision.version ? left : right;
156
+ }
157
+ if (left.updated_at_ms !== right.updated_at_ms) {
158
+ return left.updated_at_ms > right.updated_at_ms ? left : right;
159
+ }
160
+ const leftText = stableSerializeJson(left);
161
+ const rightText = stableSerializeJson(right);
162
+ return leftText <= rightText ? left : right;
163
+ }
164
+ export function parseUiDoc(input) {
165
+ return parseUiDocCandidate(input);
166
+ }
167
+ export function normalizeUiDocs(input, opts = {}) {
168
+ const maxDocs = normalizedUiDocLimit(opts.maxDocs);
169
+ const byId = new Map();
170
+ for (const candidate of uiDocCandidates(input)) {
171
+ const parsed = parseUiDocCandidate(candidate);
172
+ if (!parsed) {
173
+ continue;
174
+ }
175
+ const current = byId.get(parsed.ui_id);
176
+ if (!current) {
177
+ byId.set(parsed.ui_id, parsed);
178
+ continue;
179
+ }
180
+ byId.set(parsed.ui_id, deterministicUiDocChoice(current, parsed));
181
+ }
182
+ const docs = [...byId.values()].sort((a, b) => a.ui_id.localeCompare(b.ui_id));
183
+ if (docs.length <= maxDocs) {
184
+ return docs;
185
+ }
186
+ return docs.slice(0, maxDocs);
187
+ }
188
+ export function uiDocRevisionConflict(left, right) {
189
+ if (left.ui_id !== right.ui_id) {
190
+ return false;
191
+ }
192
+ if (left.revision.version !== right.revision.version) {
193
+ return false;
194
+ }
195
+ return stableSerializeJson(left) !== stableSerializeJson(right);
196
+ }
197
+ export function parseUiEvent(input) {
198
+ const parsed = UiEventSchema.safeParse(input);
199
+ if (!parsed.success) {
200
+ return null;
201
+ }
202
+ return parsed.data;
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-core",
3
- "version": "26.2.105",
3
+ "version": "26.2.107",
4
4
  "description": "Core primitives for mu: IDs, events, schemas, and persistence interfaces.",
5
5
  "keywords": [
6
6
  "mu",