@diviops/mcp-server 1.5.10 → 1.5.12

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 (28) hide show
  1. package/README.md +34 -0
  2. package/dist/preset-cli/__tests__/button-emitter.test.d.ts +8 -0
  3. package/dist/preset-cli/__tests__/button-emitter.test.js +188 -0
  4. package/dist/preset-cli/__tests__/cli.test.d.ts +9 -0
  5. package/dist/preset-cli/__tests__/cli.test.js +330 -0
  6. package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
  7. package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
  8. package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +13 -0
  9. package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +64 -0
  10. package/dist/preset-cli/__tests__/registry.test.d.ts +5 -0
  11. package/dist/preset-cli/__tests__/registry.test.js +149 -0
  12. package/dist/preset-cli/__tests__/write-path.test.d.ts +8 -0
  13. package/dist/preset-cli/__tests__/write-path.test.js +174 -0
  14. package/dist/preset-cli/bin.d.ts +8 -0
  15. package/dist/preset-cli/bin.js +32 -0
  16. package/dist/preset-cli/button-emitter.d.ts +117 -0
  17. package/dist/preset-cli/button-emitter.js +218 -0
  18. package/dist/preset-cli/cli.d.ts +62 -0
  19. package/dist/preset-cli/cli.js +429 -0
  20. package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
  21. package/dist/preset-cli/heading-font-emitter.js +166 -0
  22. package/dist/preset-cli/registry.d.ts +121 -0
  23. package/dist/preset-cli/registry.js +192 -0
  24. package/dist/preset-cli/variable-token.d.ts +42 -0
  25. package/dist/preset-cli/variable-token.js +70 -0
  26. package/dist/preset-cli/write-path.d.ts +74 -0
  27. package/dist/preset-cli/write-path.js +110 -0
  28. package/package.json +4 -2
@@ -0,0 +1,429 @@
1
+ /**
2
+ * `diviops-preset` — standalone preset-emitter CLI (Track 4 scaffold).
3
+ *
4
+ * Emits byte-canonical Divi 5.5.x preset JSON, gated by the verified-attrs
5
+ * registry and routed through the existing storage-path contract. Track 4
6
+ * ships one emitter: `divi/button` group presets.
7
+ *
8
+ * Usage:
9
+ * diviops-preset button [options] Emit a divi/button group preset
10
+ * diviops-preset --help Show help
11
+ *
12
+ * Modes:
13
+ * --dry-run (default) Compose and print canonical JSON. No credentials,
14
+ * no handshake, no network.
15
+ * --apply Capability-gate + POST to /preset/create. Reuses
16
+ * WP_URL / WP_USER / WP_APP_PASSWORD env vars.
17
+ *
18
+ * Exit codes:
19
+ * 0 success
20
+ * 1 invalid input / usage error
21
+ * 2 evidence-gate refusal (attr below VB_PRESET_STORAGE_VERIFIED)
22
+ * 3 capability-missing (plugin lacks storage_multipath_probe_v1)
23
+ * 4 write / network error
24
+ */
25
+ import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
26
+ import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
27
+ import { EvidenceGateError } from "./registry.js";
28
+ import { applyButtonPreset, applyHeadingFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
29
+ export const EXIT = {
30
+ OK: 0,
31
+ INVALID_INPUT: 1,
32
+ EVIDENCE_GATE: 2,
33
+ CAPABILITY_MISSING: 3,
34
+ WRITE_ERROR: 4,
35
+ };
36
+ const realIO = {
37
+ out: (t) => process.stdout.write(t + "\n"),
38
+ err: (t) => process.stderr.write(t + "\n"),
39
+ };
40
+ const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
41
+
42
+ USAGE
43
+ diviops-preset button [options] Emit a divi/button group preset
44
+ diviops-preset heading-font [options] Emit a divi/font group preset for
45
+ divi/heading
46
+ diviops-preset --help Show this help
47
+
48
+ MODE
49
+ --dry-run Compose + print canonical JSON only (DEFAULT).
50
+ No credentials, no handshake, no network.
51
+ --apply Capability-gate, then POST to /preset/create.
52
+ Requires WP_URL / WP_USER / WP_APP_PASSWORD.
53
+
54
+ button OPTIONS (all styling fields optional; emit-on-specification only)
55
+ --name <string> Preset display name (required).
56
+ --bg-color <value> Desktop background color. Hex literal, or a
57
+ bare gcid-*/gvid-* token, or a $variable(...)$ token.
58
+ --bg-color-hover <value> Hover background color (same value forms).
59
+ --radius-top-left <v> Border radius corner. Any subset of the four
60
+ --radius-top-right <v> corners; the radius widget emits a sync flag
61
+ --radius-bottom-left <v> alongside any corner ("on" only when all four
62
+ --radius-bottom-right <v> corners are given and equal, else "off";
63
+ override with --radius-sync).
64
+ --radius-sync <on|off> Explicit radius sync flag.
65
+ --radius <v> Shorthand: sets all four corners to <v>.
66
+ --border-width <v> Outline-button border width.
67
+ --border-style <v> Outline-button border style (solid|dashed|...).
68
+ --border-color <value> Outline-button border color (same value forms).
69
+ --font-family <string> Button font family (literal string).
70
+ --font-weight <string> Button font weight (e.g. "600").
71
+ --font-color <value> Button font color (same value forms).
72
+ --font-size <v> Button font size (e.g. "16px").
73
+ --bypass-hover-padding-gate
74
+ Opt-in: emit button.decoration.button.desktop
75
+ .value.padding.top="0px" (hover-padding-gate
76
+ workaround). Off by default.
77
+
78
+ heading-font OPTIONS (all styling fields optional; emit-on-specification only)
79
+ --name <string> Preset display name (required).
80
+ --pattern <google|local> Required. Selects the verified variant:
81
+ google — Pattern A: plain family + explicit
82
+ numeric weight (e.g. family "Inter",
83
+ weight "700"). Verified against
84
+ round-1a fixture.
85
+ local — Pattern B: weight-encoded family
86
+ string (e.g. family "Sora 700") with
87
+ NO --font-weight. Used for local-
88
+ hosted/EU-GDPR font flows. Verified
89
+ against round-1b fixture.
90
+ Pattern A and Pattern B are distinct registry
91
+ variants — there is NO default; omitting
92
+ --pattern is invalid input.
93
+ --font-family <string> Font family. Pattern A: plain name ("Inter").
94
+ Pattern B: weight-encoded ("Sora 700").
95
+ --font-weight <string> Font weight (e.g. "700"). Pattern A only —
96
+ passing it with --pattern local is refused.
97
+ --font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
98
+ token, or already-formed $variable(...)$ token.
99
+ --font-size <v> Font size (e.g. "48px").
100
+ --font-line-height <v> Font line-height (e.g. "1.1").
101
+
102
+ EXIT CODES
103
+ 0 success 1 invalid input 2 evidence-gate refusal
104
+ 3 capability missing 4 write error
105
+
106
+ EXAMPLES
107
+ diviops-preset button --name "Primary" --bg-color gcid-primary-color \\
108
+ --bg-color-hover gcid-secondary-color --radius 8px \\
109
+ --font-family Inter --font-weight 600 --font-color gcid-body-color
110
+
111
+ diviops-preset button --name "Primary" --bg-color "#2563eb" --apply
112
+
113
+ diviops-preset heading-font --name "Heading H1" --pattern google \\
114
+ --font-family Inter --font-weight 700 \\
115
+ --font-color gcid-heading-color --font-size 48px
116
+
117
+ diviops-preset heading-font --name "Heading H1 (local)" --pattern local \\
118
+ --font-family "Sora 700" \\
119
+ --font-color gcid-heading-color --font-size 48px
120
+ `;
121
+ const VALUE_FLAGS = new Set([
122
+ "--name",
123
+ "--bg-color",
124
+ "--bg-color-hover",
125
+ "--radius",
126
+ "--radius-top-left",
127
+ "--radius-top-right",
128
+ "--radius-bottom-left",
129
+ "--radius-bottom-right",
130
+ "--radius-sync",
131
+ "--border-width",
132
+ "--border-style",
133
+ "--border-color",
134
+ "--font-family",
135
+ "--font-weight",
136
+ "--font-color",
137
+ "--font-size",
138
+ "--font-line-height",
139
+ "--pattern",
140
+ ]);
141
+ /** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
142
+ export function parseArgs(argv) {
143
+ const parsed = {
144
+ command: null,
145
+ help: false,
146
+ apply: false,
147
+ dryRun: false,
148
+ options: new Map(),
149
+ };
150
+ let i = 0;
151
+ // First non-flag token is the command.
152
+ if (argv.length > 0 && !argv[0].startsWith("-")) {
153
+ parsed.command = argv[0];
154
+ i = 1;
155
+ }
156
+ for (; i < argv.length; i++) {
157
+ const tok = argv[i];
158
+ if (tok === "--help" || tok === "-h") {
159
+ parsed.help = true;
160
+ continue;
161
+ }
162
+ if (tok === "--apply") {
163
+ parsed.apply = true;
164
+ continue;
165
+ }
166
+ if (tok === "--dry-run") {
167
+ parsed.dryRun = true;
168
+ continue;
169
+ }
170
+ if (tok === "--bypass-hover-padding-gate") {
171
+ parsed.options.set(tok, true);
172
+ continue;
173
+ }
174
+ if (VALUE_FLAGS.has(tok)) {
175
+ const val = argv[i + 1];
176
+ if (val === undefined || val.startsWith("--")) {
177
+ throw new UsageError(`Flag ${tok} requires a value.`);
178
+ }
179
+ parsed.options.set(tok, val);
180
+ i++;
181
+ continue;
182
+ }
183
+ if (!tok.startsWith("-") && parsed.command === null) {
184
+ parsed.command = tok;
185
+ continue;
186
+ }
187
+ throw new UsageError(`Unknown flag or argument: ${tok}`);
188
+ }
189
+ if (parsed.apply && parsed.dryRun) {
190
+ throw new UsageError("--apply and --dry-run are mutually exclusive.");
191
+ }
192
+ // dry-run is the default-safe mode when neither is given.
193
+ if (!parsed.apply)
194
+ parsed.dryRun = true;
195
+ return parsed;
196
+ }
197
+ export class UsageError extends Error {
198
+ constructor(message) {
199
+ super(message);
200
+ this.name = "UsageError";
201
+ }
202
+ }
203
+ /** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
204
+ const KNOWN_COMMANDS = new Set(["button", "heading-font"]);
205
+ /** Map parsed `button` options into the emitter input shape. */
206
+ export function buildButtonInput(parsed) {
207
+ const opt = (k) => {
208
+ const v = parsed.options.get(k);
209
+ return typeof v === "string" ? v : undefined;
210
+ };
211
+ const name = opt("--name");
212
+ if (!name) {
213
+ throw new UsageError("button command requires --name <string>.");
214
+ }
215
+ const input = { name };
216
+ const bg = opt("--bg-color");
217
+ if (bg !== undefined)
218
+ input.bg_color = bg;
219
+ const bgh = opt("--bg-color-hover");
220
+ if (bgh !== undefined)
221
+ input.bg_color_hover = bgh;
222
+ // Radius — shorthand --radius sets all four corners.
223
+ const radius = {};
224
+ const radiusAll = opt("--radius");
225
+ if (radiusAll !== undefined) {
226
+ radius.topLeft = radiusAll;
227
+ radius.topRight = radiusAll;
228
+ radius.bottomLeft = radiusAll;
229
+ radius.bottomRight = radiusAll;
230
+ }
231
+ const rtl = opt("--radius-top-left");
232
+ if (rtl !== undefined)
233
+ radius.topLeft = rtl;
234
+ const rtr = opt("--radius-top-right");
235
+ if (rtr !== undefined)
236
+ radius.topRight = rtr;
237
+ const rbl = opt("--radius-bottom-left");
238
+ if (rbl !== undefined)
239
+ radius.bottomLeft = rbl;
240
+ const rbr = opt("--radius-bottom-right");
241
+ if (rbr !== undefined)
242
+ radius.bottomRight = rbr;
243
+ const rsync = opt("--radius-sync");
244
+ if (rsync !== undefined) {
245
+ if (rsync !== "on" && rsync !== "off") {
246
+ throw new UsageError('--radius-sync must be "on" or "off".');
247
+ }
248
+ radius.sync = rsync;
249
+ }
250
+ if (Object.keys(radius).length > 0)
251
+ input.radius = radius;
252
+ // Outline border styles.
253
+ const border = {};
254
+ const bw = opt("--border-width");
255
+ if (bw !== undefined)
256
+ border.width = bw;
257
+ const bs = opt("--border-style");
258
+ if (bs !== undefined)
259
+ border.style = bs;
260
+ const bc = opt("--border-color");
261
+ if (bc !== undefined)
262
+ border.color = bc;
263
+ if (Object.keys(border).length > 0)
264
+ input.border = border;
265
+ // Font.
266
+ const font = {};
267
+ const ff = opt("--font-family");
268
+ if (ff !== undefined)
269
+ font.family = ff;
270
+ const fw = opt("--font-weight");
271
+ if (fw !== undefined)
272
+ font.weight = fw;
273
+ const fc = opt("--font-color");
274
+ if (fc !== undefined)
275
+ font.color = fc;
276
+ const fs = opt("--font-size");
277
+ if (fs !== undefined)
278
+ font.size = fs;
279
+ if (Object.keys(font).length > 0)
280
+ input.font = font;
281
+ if (parsed.options.get("--bypass-hover-padding-gate") === true) {
282
+ input.bypass_hover_padding_gate = true;
283
+ }
284
+ return input;
285
+ }
286
+ /** Map parsed `heading-font` options into the heading-font emitter input shape. */
287
+ export function buildHeadingFontInput(parsed) {
288
+ const opt = (k) => {
289
+ const v = parsed.options.get(k);
290
+ return typeof v === "string" ? v : undefined;
291
+ };
292
+ const name = opt("--name");
293
+ if (!name) {
294
+ throw new UsageError("heading-font command requires --name <string>.");
295
+ }
296
+ // --pattern is REQUIRED — Pattern A vs Pattern B are distinct registry/
297
+ // evidence variants and there is no safe default. An omitted --pattern
298
+ // is invalid input; the CLI fails BEFORE emission (no guessing).
299
+ const patternRaw = opt("--pattern");
300
+ if (patternRaw === undefined) {
301
+ throw new UsageError("heading-font command requires --pattern <google|local>. " +
302
+ "There is no default — Pattern A (google) and Pattern B (local) are distinct " +
303
+ "registry variants and must be selected intentionally.");
304
+ }
305
+ if (patternRaw !== "google" && patternRaw !== "local") {
306
+ throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
307
+ }
308
+ const pattern = patternRaw;
309
+ const input = { name, pattern };
310
+ const family = opt("--font-family");
311
+ if (family !== undefined)
312
+ input.family = family;
313
+ const weight = opt("--font-weight");
314
+ if (weight !== undefined)
315
+ input.weight = weight;
316
+ const color = opt("--font-color");
317
+ if (color !== undefined)
318
+ input.color = color;
319
+ const size = opt("--font-size");
320
+ if (size !== undefined)
321
+ input.size = size;
322
+ const lineHeight = opt("--font-line-height");
323
+ if (lineHeight !== undefined)
324
+ input.lineHeight = lineHeight;
325
+ return input;
326
+ }
327
+ /**
328
+ * Run the CLI. Returns the structured exit code (does NOT call
329
+ * `process.exit` — the thin bin wrapper does). `io` is injectable so
330
+ * tests capture output without touching real stdio.
331
+ */
332
+ export async function run(argv, io = realIO, serverVersion) {
333
+ let parsed;
334
+ try {
335
+ parsed = parseArgs(argv);
336
+ }
337
+ catch (err) {
338
+ io.err(err instanceof Error ? err.message : String(err));
339
+ io.err("");
340
+ io.err("Run `diviops-preset --help` for usage.");
341
+ return EXIT.INVALID_INPUT;
342
+ }
343
+ if (parsed.help || (parsed.command === null && parsed.options.size === 0)) {
344
+ io.out(HELP);
345
+ return EXIT.OK;
346
+ }
347
+ if (parsed.command === null || !KNOWN_COMMANDS.has(parsed.command)) {
348
+ io.err(`Unknown command: ${parsed.command ?? "(none)"}. ` +
349
+ `Known commands: ${[...KNOWN_COMMANDS].sort().join(", ")}.`);
350
+ io.err("Run `diviops-preset --help` for usage.");
351
+ return EXIT.INVALID_INPUT;
352
+ }
353
+ // --- compose + gate -------------------------------------------------
354
+ // Per-command branch produces:
355
+ // - `dryRunBody`: the canonical JSON to print in --dry-run mode.
356
+ // - `applyFn`: a closure that issues the apply-mode write when the
357
+ // capability check passes. Both branches reuse the existing write-
358
+ // path plumbing (`assertStorageCapability` → `/preset/create`).
359
+ let dryRunBody;
360
+ let applyFn;
361
+ try {
362
+ if (parsed.command === "button") {
363
+ const input = buildButtonInput(parsed);
364
+ const entry = emitButtonGroupPreset(input);
365
+ dryRunBody = buildPresetCreateBody(entry, { dry_run: true });
366
+ applyFn = (client, sv) => applyButtonPreset(client, entry, { serverVersion: sv });
367
+ }
368
+ else {
369
+ // heading-font
370
+ const input = buildHeadingFontInput(parsed);
371
+ const entry = emitHeadingFontGroupPreset(input);
372
+ dryRunBody = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
373
+ applyFn = (client, sv) => applyHeadingFontPreset(client, entry, { serverVersion: sv });
374
+ }
375
+ }
376
+ catch (err) {
377
+ if (err instanceof EvidenceGateError) {
378
+ io.err(err.message);
379
+ return EXIT.EVIDENCE_GATE;
380
+ }
381
+ if (err instanceof UsageError) {
382
+ io.err(err.message);
383
+ io.err("Run `diviops-preset --help` for usage.");
384
+ return EXIT.INVALID_INPUT;
385
+ }
386
+ if (err instanceof UnsupportedVariantCombinationError) {
387
+ // Distinct from EvidenceGateError: the registry IS complete for the
388
+ // verified variants, but the caller asked for a combination outside
389
+ // any verified variant. Surfaces as invalid input (exit 1).
390
+ io.err(err.message);
391
+ return EXIT.INVALID_INPUT;
392
+ }
393
+ io.err(err instanceof Error ? err.message : String(err));
394
+ return EXIT.INVALID_INPUT;
395
+ }
396
+ // --- dry-run --------------------------------------------------------
397
+ if (parsed.dryRun) {
398
+ io.out(JSON.stringify(dryRunBody, null, 2));
399
+ return EXIT.OK;
400
+ }
401
+ // --- apply ----------------------------------------------------------
402
+ try {
403
+ const client = buildClientFromEnv();
404
+ // Apply mode requires the server version for the plugin handshake; the
405
+ // /handshake route gates on `mcp_server_version`. The bin entrypoint
406
+ // always supplies it — guard here so apply mode can never reach the
407
+ // handshake with an undefined/empty version.
408
+ if (!serverVersion) {
409
+ io.err("Apply mode requires the server version for the plugin handshake. " +
410
+ "This is an internal error — invoke via the `diviops-preset` bin.");
411
+ return EXIT.WRITE_ERROR;
412
+ }
413
+ const result = await applyFn(client, serverVersion);
414
+ io.out(JSON.stringify(result, null, 2));
415
+ return EXIT.OK;
416
+ }
417
+ catch (err) {
418
+ if (err instanceof CapabilityMissingError) {
419
+ io.err(err.message);
420
+ return EXIT.CAPABILITY_MISSING;
421
+ }
422
+ if (err instanceof CredentialsMissingError) {
423
+ io.err(err.message);
424
+ return EXIT.INVALID_INPUT;
425
+ }
426
+ io.err(`Write failed: ${err instanceof Error ? err.message : String(err)}`);
427
+ return EXIT.WRITE_ERROR;
428
+ }
429
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `divi/font` heading group-preset emitter — Track 5 vertical slice.
3
+ *
4
+ * Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font` preset
5
+ * targeting `divi/heading` at `attrs.title.decoration.font.font.desktop.value.*`,
6
+ * gated by the verified-attrs registry. Canonical shape:
7
+ * `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
8
+ * (`divi/font` section), cross-checked against
9
+ * `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-1a-heading-h1-pattern-a-google.json`
10
+ * and `…/round-1b-heading-h1-pattern-b-local.json`.
11
+ *
12
+ * Two verified Pattern variants live in the registry — they are NOT
13
+ * interchangeable evidence. The caller MUST select one explicitly:
14
+ *
15
+ * - Pattern A (Google Fonts CDN): plain family + explicit numeric weight.
16
+ * Registry entry: `divi/font` with `pattern_variant: "google_fonts_pattern_a"`.
17
+ * Verified evidence: round-1a fixture.
18
+ * - Pattern B (local-hosted, EU-GDPR): weight encoded into the family
19
+ * string ("Sora 700") and NO `weight` key at all in the preset.
20
+ * Registry entry: `divi/font` with `pattern_variant: "local_hosted_pattern_b"`.
21
+ * Verified evidence: round-1b fixture.
22
+ *
23
+ * Shape rules enforced here:
24
+ * - Emit-on-specification only — omitted params produce NO keys.
25
+ * - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
26
+ * `/preset/create` route mirrors `attrs` into all three buckets to match
27
+ * VB save semantics); the CLI request body only carries `attrs`.
28
+ * - Double-font nesting: `title.decoration.font.font.desktop.value.*` —
29
+ * outer `font` is the decoration category, inner `font` is the attr
30
+ * name within that category.
31
+ * - Pattern B with an explicit `weight` is REFUSED — the verified Pattern
32
+ * B capture has no `weight` key and there is no registry entry
33
+ * authorizing local-hosted-with-explicit-weight.
34
+ */
35
+ import { type VerifiedAttrsRegistry } from "./registry.js";
36
+ export declare const HEADING_FONT_MODULE = "divi/heading";
37
+ export declare const HEADING_FONT_GROUP_NAME = "divi/font";
38
+ export declare const HEADING_FONT_GROUP_ID = "designTitleText";
39
+ export declare const HEADING_FONT_PATTERN_FAMILY = "divi/font";
40
+ /** The two verified pattern variants for `divi/font` on `divi/heading`. */
41
+ export declare const HEADING_FONT_PATTERN_VARIANTS: {
42
+ readonly google: "google_fonts_pattern_a";
43
+ readonly local: "local_hosted_pattern_b";
44
+ };
45
+ export type HeadingFontPattern = "google" | "local";
46
+ export interface HeadingFontEmitterInput {
47
+ /** Required display name for the preset. */
48
+ name: string;
49
+ /**
50
+ * Required pattern selector. There is no default — Pattern A and
51
+ * Pattern B are distinct registry/evidence variants and the caller
52
+ * must select intentionally.
53
+ */
54
+ pattern: HeadingFontPattern;
55
+ /**
56
+ * Font family.
57
+ * - Pattern A (google): plain family name, e.g. `"Inter"`.
58
+ * - Pattern B (local): weight-encoded family name, e.g. `"Sora 700"`.
59
+ */
60
+ family?: string;
61
+ /**
62
+ * Font weight (numeric-string, e.g. `"700"`).
63
+ * - Pattern A: optional; emitted when specified.
64
+ * - Pattern B: PROHIBITED — passing a weight in `local` mode is a
65
+ * usage error (the verified Pattern B shape has no `weight` key,
66
+ * and there is no registry entry vouching for that combination).
67
+ */
68
+ weight?: string;
69
+ /** Font color — literal hex or bare/formed variable token. */
70
+ color?: string;
71
+ /** Font size — literal (e.g. `"48px"`) or already-formed `$variable(...)$`. */
72
+ size?: string;
73
+ /** Line height — optional, emit-on-specification only. */
74
+ lineHeight?: string;
75
+ }
76
+ /** The composed canonical preset entry shape sent to `/preset/create`. */
77
+ export interface HeadingFontPresetEntry {
78
+ type: "group";
79
+ module_name: string;
80
+ group_name: string;
81
+ group_id: string;
82
+ pattern_variant: string;
83
+ name: string;
84
+ attrs: Record<string, unknown>;
85
+ }
86
+ /**
87
+ * Raised when the caller passes a combination the registry does not
88
+ * authorize for the current Track — distinct from `EvidenceGateError`
89
+ * (registry-evidence shortfall) and `UsageError` (CLI arg parse).
90
+ */
91
+ export declare class UnsupportedVariantCombinationError extends Error {
92
+ constructor(message: string);
93
+ }
94
+ /**
95
+ * Compose the canonical `attrs.title.decoration.font.font.desktop.value`
96
+ * bag from the input. Emit-on-specification: only specified sub-fields
97
+ * produce keys.
98
+ */
99
+ export declare function composeHeadingFontAttrs(input: HeadingFontEmitterInput): Record<string, unknown>;
100
+ /**
101
+ * Emit a canonical `divi/font` heading group preset.
102
+ *
103
+ * 1. Validate input shape (name, pattern, Pattern B + weight refusal).
104
+ * 2. Compose `attrs.title.decoration.font.font.desktop.value.*`
105
+ * (emit-on-specification).
106
+ * 3. Gate the chosen `(divi/font, pattern_variant)` cell on `divi/heading`
107
+ * against the verified-attrs registry — throws `EvidenceGateError`
108
+ * when effective evidence is below `VB_PRESET_STORAGE_VERIFIED`.
109
+ *
110
+ * `styleAttrs` and `renderAttrs` are intentionally NOT part of this
111
+ * entry: the plugin's `/preset/create` route mirrors the single `attrs`
112
+ * bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
113
+ * to match VB save semantics — see `trait-preset.php` `preset_create`.
114
+ * The Round 1a/1b fixtures capture the post-write storage shape.
115
+ */
116
+ export declare function emitHeadingFontGroupPreset(input: HeadingFontEmitterInput, registry?: VerifiedAttrsRegistry): HeadingFontPresetEntry;
117
+ /**
118
+ * Build the `POST /diviops/v1/preset/create` request body from a heading
119
+ * font preset entry. Matches the body shape the `diviops_preset_create`
120
+ * MCP tool posts — the CLI reuses the existing route, it does not add one.
121
+ *
122
+ * `pattern_variant` is informational metadata on the in-memory entry and
123
+ * is NOT sent over the wire — the registry-gate decision happens
124
+ * client-side before the write.
125
+ */
126
+ export declare function buildHeadingFontPresetCreateBody(entry: HeadingFontPresetEntry, opts?: {
127
+ dry_run?: boolean;
128
+ }): Record<string, unknown>;