@ggui-ai/protocol 0.16.0 → 0.18.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 (36) hide show
  1. package/dist/gadgets/stdlib-gadgets.d.ts +1 -1
  2. package/dist/gadgets/stdlib-gadgets.js +1 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -2
  6. package/dist/integrations/mcp-apps.d.ts +91 -8
  7. package/dist/integrations/mcp-apps.d.ts.map +1 -1
  8. package/dist/integrations/mcp-apps.js +62 -12
  9. package/dist/integrations/overlay-hash.d.ts +1 -1
  10. package/dist/integrations/overlay-hash.d.ts.map +1 -1
  11. package/dist/schemas/app-generation-profile.d.ts +200 -0
  12. package/dist/schemas/app-generation-profile.d.ts.map +1 -0
  13. package/dist/schemas/app-generation-profile.js +168 -0
  14. package/dist/schemas/app-theme.d.ts +274 -0
  15. package/dist/schemas/app-theme.d.ts.map +1 -1
  16. package/dist/schemas/app-theme.js +219 -0
  17. package/dist/schemas/data-contract.d.ts +46 -0
  18. package/dist/schemas/data-contract.d.ts.map +1 -1
  19. package/dist/schemas/data-contract.js +162 -0
  20. package/dist/schemas/mcp.d.ts +14 -5
  21. package/dist/schemas/mcp.d.ts.map +1 -1
  22. package/dist/schemas/mcp.js +15 -6
  23. package/dist/schemas/ops-blueprint.d.ts +7 -0
  24. package/dist/schemas/ops-blueprint.d.ts.map +1 -1
  25. package/dist/schemas/ops-blueprint.js +12 -0
  26. package/dist/schemas/rendering-context.d.ts +48 -0
  27. package/dist/schemas/rendering-context.d.ts.map +1 -0
  28. package/dist/schemas/rendering-context.js +39 -0
  29. package/dist/types/data-contract.d.ts +15 -1
  30. package/dist/types/data-contract.d.ts.map +1 -1
  31. package/dist/types/render.d.ts +3 -2
  32. package/dist/types/render.d.ts.map +1 -1
  33. package/dist/version.d.ts +101 -1
  34. package/dist/version.d.ts.map +1 -1
  35. package/dist/version.js +101 -1
  36. package/package.json +1 -1
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The generator PROFILE slot on an app's `generation` section (ggui#991;
4
+ * the D7 mechanism of ggui#987 — "chosen at generation time, like layout").
5
+ *
6
+ * Four members of free text the app's operator writes and the generator
7
+ * reads when it composes a component: `styling` is the brief (voice,
8
+ * mood, references), `density` and `layout` are one line each, and
9
+ * `direction` (2026-09-11, ggui#1027 follower) is the COMPOSITION
10
+ * direction of one variant — what leads (a hero panel, rows with arrows,
11
+ * a compact row of chips, a split panel), what is left out (icons,
12
+ * helper text, status lines), motion or none — one short paragraph, so
13
+ * a family of variants is one draft under one judge with the direction
14
+ * as the only arm. The slot
15
+ * is a GENERATION-TIME input: it is not a theme-document field, not a
16
+ * consumed token, and MUST never be projected into the `--ggui-*`
17
+ * vocabulary the card reads. An absent or empty profile MUST leave the
18
+ * generator's prompts byte-identical to today's.
19
+ *
20
+ * Parties and obligations:
21
+ * - WRITERS (an operator console, `ggui deploy`) send the object below
22
+ * through the app-config write door; the door trims, bounds each
23
+ * member at {@link APP_GENERATION_PROFILE_BOUNDS} and refuses control
24
+ * characters other than tab, newline and carriage return, answering
25
+ * `invalid_app_config` with {@link appGenerationProfileRefusalBodySchema}
26
+ * in the transport's refusal slot (REST 422 body, AppSync `errorInfo`,
27
+ * MCP `structuredContent`).
28
+ * - READERS (`@ggui-ai/ui-gen` behind the server's render path) cap
29
+ * defensively at the SAME numbers — a value over the bound is a door
30
+ * defect, never a second rule — and treat absent and empty alike.
31
+ *
32
+ * Observable violation: a profile that reaches the generator unbounded, or
33
+ * a profile name surfacing as a `--ggui-*` token, is non-conformant.
34
+ */
35
+ export const APP_GENERATION_PROFILE_BOUNDS = {
36
+ styling: 2000,
37
+ density: 200,
38
+ layout: 200,
39
+ direction: 600,
40
+ };
41
+ /**
42
+ * True when the text carries a C0 control character other than tab (0x09),
43
+ * newline (0x0A) or carriage return (0x0D), or DEL (0x7F). Written as a
44
+ * code-point walk on purpose: the intent is explicit, and no control
45
+ * character has to appear in source.
46
+ */
47
+ function hasForbiddenControlChars(text) {
48
+ for (const ch of text) {
49
+ const c = ch.codePointAt(0) ?? 0;
50
+ if (c === 0x7f)
51
+ return true;
52
+ if (c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d)
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+ function profileText(member) {
58
+ const max = APP_GENERATION_PROFILE_BOUNDS[member];
59
+ return z
60
+ .string()
61
+ .trim()
62
+ .max(max, `generation.profile.${member} exceeds ${max} characters`)
63
+ .refine((s) => !hasForbiddenControlChars(s), `generation.profile.${member} contains control characters`);
64
+ }
65
+ /**
66
+ * The effort level (ggui#1058, founder's A2): ONE name over the reader's
67
+ * dials (model tier × turn cap × eval rounds × visual bar × judge model).
68
+ * The wire carries only the name; the name → dials table is the reader's
69
+ * (versioned by it), so a price line can move without a wire change.
70
+ * Absent ⇒ the deployment's default level, which MUST equal today's fixed
71
+ * options. A level the deployment has not enabled is REFUSED at the write
72
+ * door (`{ profile: { effort: 'unavailable' } }`) — never downgraded.
73
+ */
74
+ export const APP_GENERATION_PROFILE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'ultra'];
75
+ /** Catalogue entry ids are slugs: trace-line and path safe, 2–64 chars. */
76
+ export const APP_GENERATION_AESTHETIC_ID_RE = /^[a-z0-9][a-z0-9-]{1,63}$/;
77
+ /** A preset content version: no whitespace, ≤ 32 chars. */
78
+ export const APP_GENERATION_AESTHETIC_VERSION_RE = /^[A-Za-z0-9._-]{1,32}$/;
79
+ /**
80
+ * A reference INTO the aesthetic/variance catalogue (data, kept by the
81
+ * deployment, never inside the generator): `id` is the catalogue entry — the
82
+ * same slug a draft carries as its variance `aesthetic` when both are
83
+ * present — and `version` pins the preset's content so a preset can be
84
+ * re-authored without re-keying served takes. The door validates GRAMMAR
85
+ * only; resolution happens at read, and an unresolvable reference is
86
+ * NON-FATAL: the generator proceeds without the aesthetic section and
87
+ * reports `profile_aesthetic_unresolved`.
88
+ */
89
+ export const appGenerationAestheticRefSchema = z
90
+ .object({
91
+ id: z.string().regex(APP_GENERATION_AESTHETIC_ID_RE, 'aesthetic id must be a slug (2–64 chars, lowercase, digits, dashes)'),
92
+ version: z.string().regex(APP_GENERATION_AESTHETIC_VERSION_RE, 'aesthetic version: 1–32 chars, no whitespace').optional(),
93
+ })
94
+ .strict();
95
+ export const appGenerationProfileSchema = z
96
+ .object({
97
+ styling: profileText('styling').optional(),
98
+ density: profileText('density').optional(),
99
+ layout: profileText('layout').optional(),
100
+ direction: profileText('direction').optional(),
101
+ effort: z.enum(APP_GENERATION_PROFILE_EFFORTS).optional(),
102
+ aesthetic: appGenerationAestheticRefSchema.optional(),
103
+ })
104
+ .strict();
105
+ /**
106
+ * The READ-door variant of {@link appGenerationProfileSchema} (ggui#1105).
107
+ *
108
+ * The write door is strict and MUST stay so — it is what bounds each member
109
+ * and refuses control characters before a profile is stored. A READ door has
110
+ * the opposite job: a stored profile written by a LATER release carries a
111
+ * member this one cannot name, and refusing it there does not protect
112
+ * anything — it destroys a mint. Today the same stored profile is read two
113
+ * ways: the mint parses it strictly and THROWS (the mint dies), while the
114
+ * judge hand-rolls a `.strip()`. One document, two postures, diverging on
115
+ * exactly the member a later release adds.
116
+ *
117
+ * Third instance of one posture — see `schemas/app-theme.ts`
118
+ * (`parseAppThemeAtReadDoor`) and `schemas/data-contract.ts`
119
+ * (`parseDataContractAtReadDoor`). The rule they share:
120
+ *
121
+ * **a read whose purpose is to INTERPRET state strips unknown members and
122
+ * names what it stripped; a read whose purpose is to REPRODUCE state must
123
+ * not strip at all.**
124
+ *
125
+ * This is an INTERPRET door. Only members this release does not NAME are
126
+ * stripped — never members it finds INVALID: a bound, a control character or
127
+ * a level outside the vocabulary is refused exactly as the write door refuses
128
+ * it.
129
+ */
130
+ export const appGenerationProfileReadSchema = z.object(appGenerationProfileSchema.shape);
131
+ /**
132
+ * Parse a stored generation profile at a READ door: `ok` with the profile and
133
+ * the top-level members that were stripped (payload order, `[]` when none), or
134
+ * the issues in `path: message` form when the profile is refused for a reason
135
+ * the write door would also refuse.
136
+ *
137
+ * Every reader of a STORED profile should use this — the mint and the judge
138
+ * reading one document two ways is the defect ggui#1105 exists for, and one
139
+ * exported door is how that stops being possible rather than being noticed.
140
+ */
141
+ export function parseAppGenerationProfileAtReadDoor(input) {
142
+ const parsed = appGenerationProfileReadSchema.safeParse(input);
143
+ if (!parsed.success) {
144
+ return { ok: false, issues: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`) };
145
+ }
146
+ const known = new Set(Object.keys(appGenerationProfileSchema.shape));
147
+ const stripped = typeof input === 'object' && input !== null && !Array.isArray(input)
148
+ ? Object.keys(input).filter((k) => !known.has(k))
149
+ : [];
150
+ return { ok: true, profile: parsed.data, stripped };
151
+ }
152
+ /** Why the door refused one member — one reason per member, at least one member named. */
153
+ export const appGenerationProfileRefusalReasonSchema = z.enum(['too-long', 'not-text', 'control-chars', 'unavailable']);
154
+ export const appGenerationProfileRefusalBodySchema = z
155
+ .object({
156
+ profile: z
157
+ .object({
158
+ styling: appGenerationProfileRefusalReasonSchema.optional(),
159
+ density: appGenerationProfileRefusalReasonSchema.optional(),
160
+ layout: appGenerationProfileRefusalReasonSchema.optional(),
161
+ direction: appGenerationProfileRefusalReasonSchema.optional(),
162
+ effort: appGenerationProfileRefusalReasonSchema.optional(),
163
+ aesthetic: appGenerationProfileRefusalReasonSchema.optional(),
164
+ })
165
+ .strict()
166
+ .refine((p) => Object.keys(p).length > 0, 'a profile refusal names at least one member'),
167
+ })
168
+ .strict();
@@ -40,6 +40,76 @@ export declare const CSS_VALUE_SAFE_RE: RegExp;
40
40
  */
41
41
  export declare const KEYFRAMES_NAME_RE: RegExp;
42
42
  export declare function isKeyframesText(text: string): boolean;
43
+ /**
44
+ * One declared font face (ggui#987 §5; lifted here for ggui#1093 / #990 so the
45
+ * theme DOCUMENT's `typography.faces` and the AppTheme WIRE's `fonts` share
46
+ * ONE grammar — `@ggui-ai/project-config` imports this schema for the
47
+ * document door). `src` MUST be `https:` with a well-formed host; the face is
48
+ * DECLARED, never fetched by any door: the render host unions the origin into
49
+ * its `font-src`, the shell renders the `@font-face` rule, and a face that
50
+ * fails to load in the browser falls back down the family's stack.
51
+ */
52
+ export declare const fontFaceDeclarationSchema: z.ZodObject<{
53
+ family: z.ZodString;
54
+ src: z.ZodString;
55
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
56
+ style: z.ZodOptional<z.ZodString>;
57
+ display: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strict>;
59
+ export type FontFaceDeclaration = z.infer<typeof fontFaceDeclarationSchema>;
60
+ /** The most faces one theme may declare on the wire — a CSP and a `<style>` block stay bounded. */
61
+ export declare const APP_THEME_FONTS_MAX = 16;
62
+ /** The imagery slots a theme may fill (ggui#1093): a brand mark, a hero image, a repeating pattern. */
63
+ export declare const APP_THEME_IMAGERY_KINDS: readonly ["mark", "hero", "pattern"];
64
+ export type AppThemeImageryKind = (typeof APP_THEME_IMAGERY_KINDS)[number];
65
+ /**
66
+ * One image asset: https-only `src` (never fetched by a door), an optional
67
+ * `alt` (≤ 200) and an optional `tone` — the image's own tonality (`light` /
68
+ * `dark`), so a composer can place ink over it without sampling pixels.
69
+ */
70
+ export declare const appThemeImageAssetSchema: z.ZodObject<{
71
+ src: z.ZodString;
72
+ alt: z.ZodOptional<z.ZodString>;
73
+ tone: z.ZodOptional<z.ZodEnum<{
74
+ light: "light";
75
+ dark: "dark";
76
+ }>>;
77
+ }, z.core.$strict>;
78
+ export type AppThemeImageAsset = z.infer<typeof appThemeImageAssetSchema>;
79
+ /**
80
+ * The host's imagery, by slot (ggui#1093, #1075 Track C (a)). Parties: a
81
+ * WRITER (an operator's design tooling, or a harvest of the host page) declares; the WRITE door
82
+ * validates this grammar and nothing more; the render host unions every
83
+ * `src` origin into its `img-src`; the composer reads the slots through
84
+ * the design primitives. Absent ⇒ no imagery, today's card.
85
+ */
86
+ export declare const appThemeImagerySchema: z.ZodObject<{
87
+ mark: z.ZodOptional<z.ZodObject<{
88
+ src: z.ZodString;
89
+ alt: z.ZodOptional<z.ZodString>;
90
+ tone: z.ZodOptional<z.ZodEnum<{
91
+ light: "light";
92
+ dark: "dark";
93
+ }>>;
94
+ }, z.core.$strict>>;
95
+ hero: z.ZodOptional<z.ZodObject<{
96
+ src: z.ZodString;
97
+ alt: z.ZodOptional<z.ZodString>;
98
+ tone: z.ZodOptional<z.ZodEnum<{
99
+ light: "light";
100
+ dark: "dark";
101
+ }>>;
102
+ }, z.core.$strict>>;
103
+ pattern: z.ZodOptional<z.ZodObject<{
104
+ src: z.ZodString;
105
+ alt: z.ZodOptional<z.ZodString>;
106
+ tone: z.ZodOptional<z.ZodEnum<{
107
+ light: "light";
108
+ dark: "dark";
109
+ }>>;
110
+ }, z.core.$strict>>;
111
+ }, z.core.$strict>;
112
+ export type AppThemeImagery = z.infer<typeof appThemeImagerySchema>;
43
113
  export declare const appThemeSchema: z.ZodObject<{
44
114
  mode: z.ZodOptional<z.ZodEnum<{
45
115
  light: "light";
@@ -57,14 +127,216 @@ export declare const appThemeSchema: z.ZodObject<{
57
127
  dark: z.ZodOptional<z.ZodString>;
58
128
  }, z.core.$strict>>;
59
129
  frameless: z.ZodOptional<z.ZodBoolean>;
130
+ fonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
131
+ family: z.ZodString;
132
+ src: z.ZodString;
133
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
134
+ style: z.ZodOptional<z.ZodString>;
135
+ display: z.ZodOptional<z.ZodString>;
136
+ }, z.core.$strict>>>;
137
+ imagery: z.ZodOptional<z.ZodObject<{
138
+ mark: z.ZodOptional<z.ZodObject<{
139
+ src: z.ZodString;
140
+ alt: z.ZodOptional<z.ZodString>;
141
+ tone: z.ZodOptional<z.ZodEnum<{
142
+ light: "light";
143
+ dark: "dark";
144
+ }>>;
145
+ }, z.core.$strict>>;
146
+ hero: z.ZodOptional<z.ZodObject<{
147
+ src: z.ZodString;
148
+ alt: z.ZodOptional<z.ZodString>;
149
+ tone: z.ZodOptional<z.ZodEnum<{
150
+ light: "light";
151
+ dark: "dark";
152
+ }>>;
153
+ }, z.core.$strict>>;
154
+ pattern: z.ZodOptional<z.ZodObject<{
155
+ src: z.ZodString;
156
+ alt: z.ZodOptional<z.ZodString>;
157
+ tone: z.ZodOptional<z.ZodEnum<{
158
+ light: "light";
159
+ dark: "dark";
160
+ }>>;
161
+ }, z.core.$strict>>;
162
+ }, z.core.$strict>>;
60
163
  }, z.core.$strict>;
61
164
  export type AppTheme = z.infer<typeof appThemeSchema>;
165
+ /**
166
+ * The READ-door posture (ggui#1093 belt, 2026-09-15; VERSION-POLICY §3.6).
167
+ *
168
+ * `appThemeSchema` is the WRITE door: strict, an unknown top-level member is
169
+ * refused (`invalid_app_config`). A READ door — a stored row turned into
170
+ * paint, a carried `_meta["ai.ggui/render"].theme` slice — parses with THIS
171
+ * schema instead: an unknown TOP-LEVEL member is stripped, the overlays are
172
+ * kept, and the caller reports what it stripped (one line per read, keys
173
+ * only, never theme content). Everything below the top level is unchanged —
174
+ * nested objects stay strict, token grammar and value safety still refuse,
175
+ * the v1 shape still refuses — and the attestation is untouched because
176
+ * `overlayHash` covers `{ overlays, cssVariables, keyframes }` only.
177
+ *
178
+ * Why: a reader on release N−1 meeting a member release N added would
179
+ * otherwise drop the WHOLE theme — colours and all — until it rolled
180
+ * (guuey#1266's class). Parties: every read door of an `AppTheme` MUST use
181
+ * this variant (or {@link parseAppThemeAtReadDoor}); every write door MUST
182
+ * stay on `appThemeSchema`. Observable violation: a read door dropping a
183
+ * theme whose overlays are valid because of an unknown top-level member —
184
+ * the kit's `n1-compat` forward case grades it.
185
+ */
186
+ export declare const appThemeReadSchema: z.ZodObject<{
187
+ mode: z.ZodOptional<z.ZodEnum<{
188
+ light: "light";
189
+ dark: "dark";
190
+ }>>;
191
+ name: z.ZodOptional<z.ZodString>;
192
+ overlayHash: z.ZodString;
193
+ overlays: z.ZodObject<{
194
+ light: z.ZodRecord<z.ZodString, z.ZodString>;
195
+ dark: z.ZodRecord<z.ZodString, z.ZodString>;
196
+ }, z.core.$strict>;
197
+ cssVariables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
198
+ keyframes: z.ZodOptional<z.ZodObject<{
199
+ light: z.ZodOptional<z.ZodString>;
200
+ dark: z.ZodOptional<z.ZodString>;
201
+ }, z.core.$strict>>;
202
+ frameless: z.ZodOptional<z.ZodBoolean>;
203
+ fonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
204
+ family: z.ZodString;
205
+ src: z.ZodString;
206
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
207
+ style: z.ZodOptional<z.ZodString>;
208
+ display: z.ZodOptional<z.ZodString>;
209
+ }, z.core.$strict>>>;
210
+ imagery: z.ZodOptional<z.ZodObject<{
211
+ mark: z.ZodOptional<z.ZodObject<{
212
+ src: z.ZodString;
213
+ alt: z.ZodOptional<z.ZodString>;
214
+ tone: z.ZodOptional<z.ZodEnum<{
215
+ light: "light";
216
+ dark: "dark";
217
+ }>>;
218
+ }, z.core.$strict>>;
219
+ hero: z.ZodOptional<z.ZodObject<{
220
+ src: z.ZodString;
221
+ alt: z.ZodOptional<z.ZodString>;
222
+ tone: z.ZodOptional<z.ZodEnum<{
223
+ light: "light";
224
+ dark: "dark";
225
+ }>>;
226
+ }, z.core.$strict>>;
227
+ pattern: z.ZodOptional<z.ZodObject<{
228
+ src: z.ZodString;
229
+ alt: z.ZodOptional<z.ZodString>;
230
+ tone: z.ZodOptional<z.ZodEnum<{
231
+ light: "light";
232
+ dark: "dark";
233
+ }>>;
234
+ }, z.core.$strict>>;
235
+ }, z.core.$strict>>;
236
+ }, z.core.$strip>;
237
+ export type AppThemeReadDoorResult = {
238
+ readonly ok: true;
239
+ readonly theme: AppTheme;
240
+ readonly stripped: readonly string[];
241
+ } | {
242
+ readonly ok: false;
243
+ readonly issues: readonly string[];
244
+ };
245
+ /**
246
+ * Parse a stored or carried theme at a READ door: `ok` with the theme and
247
+ * the top-level members that were stripped (payload order, `[]` when none),
248
+ * or the issues in `path: message` form when the theme is refused for a
249
+ * reason the write door would also refuse.
250
+ *
251
+ * TWO READ PATHS, OPPOSITE OBLIGATIONS — they sit next to each other and
252
+ * they are NOT the same door (ggui#1124; do not "tidy" them into one):
253
+ *
254
+ * **a read whose purpose is to INTERPRET state strips unknown members
255
+ * and names what it stripped; a read whose purpose is to REPRODUCE
256
+ * state must not strip at all.**
257
+ *
258
+ * This function is the INTERPRET side — a server painting a theme, an
259
+ * operator surface rendering one — and a reader must not be handed data it
260
+ * cannot validate.
261
+ * The REPRODUCE side is a writer's CARRY path, which receives the stored
262
+ * document VERBATIM, including members this release does not name: its job
263
+ * is fidelity, not interpretation. Applying THIS strip to a carry path is
264
+ * the ggui#1124 defect exactly — the writer carries a stripped document,
265
+ * writes it back, and destroys the members the belt exists to protect.
266
+ */
267
+ export declare function parseAppThemeAtReadDoor(input: unknown): AppThemeReadDoorResult;
62
268
  /**
63
269
  * The ONE refusal body every write door returns for an overlay it will not
64
270
  * store (ggui#987 §3.4): REST 422 `invalid_app_config`, the AppSync
65
271
  * `errorInfo`, the MCP ops door's `{ ok: false }` structured content. A
66
272
  * discriminated union by key — exactly one of the four.
67
273
  */
274
+ /**
275
+ * The top-level members the STORED theme holds that an incoming write does
276
+ * not carry — i.e. exactly what the write would destroy (ggui#1124).
277
+ *
278
+ * `putAppTheme` REPLACES the whole `theme` attribute (the shared writer's
279
+ * `SET theme = :theme`), so a writer that composes fewer members than the
280
+ * stored document silently deletes the rest. On 2026-09-10, 26 rows lost
281
+ * their theme outright and ~20 more lost members nobody can now name,
282
+ * because a writer believed the door merged. **The rule lives here because
283
+ * what makes a write destructive is a fact about the wire shape, not about
284
+ * a storage engine** — the shared writer enforces it at the choke point, so
285
+ * safety does not depend on each door remembering.
286
+ *
287
+ * Deliberately total and deliberately dumb: it compares TOP-LEVEL keys and
288
+ * nothing else. A changed VALUE is not a drop. An explicit `undefined` IS a
289
+ * drop, because absent and undefined are the same wire fact. A member this
290
+ * release does not name still counts — that is the whole point, since a
291
+ * strict schema cannot see the member a later release wrote, and a
292
+ * whole-column write is exactly what destroys it. Given anything that is not
293
+ * a plain object on either side it reports nothing rather than inventing a
294
+ * drop: a guard that guesses is worse than one that abstains.
295
+ *
296
+ * Parties: the SHARED WRITER calls this before writing and refuses a
297
+ * non-empty result with {@link appThemeRefusalBodySchema}'s `wouldDrop` arm,
298
+ * naming the members; the WRITER retries carrying them (read the stored
299
+ * document, carry what it does not own by REST OMISSION — never by
300
+ * enumerating what it knows — and hash after the carry). Observable
301
+ * violation: a stored member absent after a write that did not name it.
302
+ */
303
+ export declare function appThemeDroppedMembers(stored: unknown, incoming: unknown): readonly string[];
304
+ /**
305
+ * The ONE wording for a `wouldDrop` refusal (ggui#1124, founder's ruling
306
+ * 2026-09-16: *"the refusal names the clear-then-set path"*). Exported so
307
+ * every door that refuses a destructive theme write carries the SAME words
308
+ * rather than paraphrasing them — a writer meeting two different
309
+ * explanations of one rule learns neither.
310
+ *
311
+ * Why the wording is part of the contract and not decoration: **a refusal
312
+ * with no compliance path converts "we protected your data" into "we broke
313
+ * your write", and the writer cannot tell which happened to them.** Naming
314
+ * the path collapses that ambiguity — it turns the refusal from a VERDICT
315
+ * into an INSTRUCTION. So the text MUST do three things, and the tests pin
316
+ * each: name the members that would have been dropped (so a writer can see
317
+ * whether they meant it), give the carry path for a writer who did not, and
318
+ * give the clear-then-set path for a writer who did — stating plainly that
319
+ * the app has no theme between those two writes, because that is the kind
320
+ * of fact a caller must not discover in production.
321
+ *
322
+ * The CLEAR half names an OUTCOME, not a mechanism, and that is deliberate
323
+ * (ggui#1124, from cloud's per-door measurement): `theme: null` clears on
324
+ * exactly ONE of the four doors that write a theme — the provisioning theme
325
+ * door. On the other three a writer following a universal `theme: null`
326
+ * instruction gets a SECOND refusal, which is the verdict-without-a-path
327
+ * this ruling exists to prevent, reintroduced by the text meant to prevent
328
+ * it. The CARRY half needs no such hedge: it is implementable on every
329
+ * door, which is why it is stated first and unconditionally.
330
+ *
331
+ * The better end state is uniform — `theme: null` clearing at the shared
332
+ * writer so all four doors honour one mechanism — but that is a change to
333
+ * three doors' semantics and belongs to its own decision with cloud, not to
334
+ * a wording fix.
335
+ *
336
+ * Throws on an empty list: a refusal that names nothing is not an
337
+ * instruction.
338
+ */
339
+ export declare function appThemeWouldDropRefusalText(wouldDrop: readonly string[]): string;
68
340
  export declare const appThemeRefusalBodySchema: z.ZodUnion<readonly [z.ZodObject<{
69
341
  uncovered: z.ZodObject<{
70
342
  light: z.ZodArray<z.ZodString>;
@@ -79,6 +351,8 @@ export declare const appThemeRefusalBodySchema: z.ZodUnion<readonly [z.ZodObject
79
351
  overlayHash: z.ZodLiteral<"mismatch">;
80
352
  }, z.core.$strict>, z.ZodObject<{
81
353
  refused: z.ZodLiteral<"v1 shape">;
354
+ }, z.core.$strict>, z.ZodObject<{
355
+ wouldDrop: z.ZodArray<z.ZodString>;
82
356
  }, z.core.$strict>]>;
83
357
  export type AppThemeRefusalBody = z.infer<typeof appThemeRefusalBodySchema>;
84
358
  //# sourceMappingURL=app-theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-theme.d.ts","sourceRoot":"","sources":["../../src/schemas/app-theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,QAAiB,CAAC;AAahD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,QAA+B,CAAC;AAE9D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAoCrD;AAQD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;kBAuChB,CAAC;AAEZ,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;oBAKpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"app-theme.d.ts","sourceRoot":"","sources":["../../src/schemas/app-theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,QAAiB,CAAC;AAahD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,QAA+B,CAAC;AAE9D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAoCrD;AAwBD;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB;;;;;;kBAMpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,mGAAmG;AACnG,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,sCAAuC,CAAC;AAC5E,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;;kBAInC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;kBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoDhB,CAAC;AAEZ,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiC,CAAC;AAEjE,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAW9E;AAED;;;;;GAKG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,OAAO,GAChB,SAAS,MAAM,EAAE,CAGnB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAcjF;AAED,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;oBAapC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}