@diviops/mcp-server 1.5.11 → 1.5.13
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 +20 -2
- package/dist/preset-cli/__tests__/cli.test.js +385 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
- package/dist/preset-cli/__tests__/registry.test.js +41 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +14 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +191 -0
- package/dist/preset-cli/__tests__/write-path.test.js +110 -1
- package/dist/preset-cli/cli.d.ts +6 -0
- package/dist/preset-cli/cli.js +198 -11
- package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
- package/dist/preset-cli/heading-font-emitter.js +166 -0
- package/dist/preset-cli/registry.d.ts +23 -9
- package/dist/preset-cli/registry.js +37 -13
- package/dist/preset-cli/text-body-font-emitter.d.ts +127 -0
- package/dist/preset-cli/text-body-font-emitter.js +169 -0
- package/dist/preset-cli/write-path.d.ts +31 -0
- package/dist/preset-cli/write-path.js +43 -0
- package/package.json +1 -1
package/dist/preset-cli/cli.js
CHANGED
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
* 4 write / network error
|
|
24
24
|
*/
|
|
25
25
|
import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
|
|
26
|
+
import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
|
|
27
|
+
import { emitTextBodyFontGroupPreset, buildTextBodyFontPresetCreateBody, } from "./text-body-font-emitter.js";
|
|
26
28
|
import { EvidenceGateError } from "./registry.js";
|
|
27
|
-
import { applyButtonPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
29
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
28
30
|
export const EXIT = {
|
|
29
31
|
OK: 0,
|
|
30
32
|
INVALID_INPUT: 1,
|
|
@@ -39,8 +41,12 @@ const realIO = {
|
|
|
39
41
|
const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
|
|
40
42
|
|
|
41
43
|
USAGE
|
|
42
|
-
diviops-preset button [options]
|
|
43
|
-
diviops-preset
|
|
44
|
+
diviops-preset button [options] Emit a divi/button group preset
|
|
45
|
+
diviops-preset heading-font [options] Emit a divi/font group preset for
|
|
46
|
+
divi/heading
|
|
47
|
+
diviops-preset text-body-font [options] Emit a divi/font-body group preset
|
|
48
|
+
for divi/text (Pattern A only)
|
|
49
|
+
diviops-preset --help Show this help
|
|
44
50
|
|
|
45
51
|
MODE
|
|
46
52
|
--dry-run Compose + print canonical JSON only (DEFAULT).
|
|
@@ -72,6 +78,51 @@ button OPTIONS (all styling fields optional; emit-on-specification only)
|
|
|
72
78
|
.value.padding.top="0px" (hover-padding-gate
|
|
73
79
|
workaround). Off by default.
|
|
74
80
|
|
|
81
|
+
heading-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
82
|
+
--name <string> Preset display name (required).
|
|
83
|
+
--pattern <google|local> Required. Selects the verified variant:
|
|
84
|
+
google — Pattern A: plain family + explicit
|
|
85
|
+
numeric weight (e.g. family "Inter",
|
|
86
|
+
weight "700"). Verified against
|
|
87
|
+
round-1a fixture.
|
|
88
|
+
local — Pattern B: weight-encoded family
|
|
89
|
+
string (e.g. family "Sora 700") with
|
|
90
|
+
NO --font-weight. Used for local-
|
|
91
|
+
hosted/EU-GDPR font flows. Verified
|
|
92
|
+
against round-1b fixture.
|
|
93
|
+
Pattern A and Pattern B are distinct registry
|
|
94
|
+
variants — there is NO default; omitting
|
|
95
|
+
--pattern is invalid input.
|
|
96
|
+
--font-family <string> Font family. Pattern A: plain name ("Inter").
|
|
97
|
+
Pattern B: weight-encoded ("Sora 700").
|
|
98
|
+
--font-weight <string> Font weight (e.g. "700"). Pattern A only —
|
|
99
|
+
passing it with --pattern local is refused.
|
|
100
|
+
--font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
|
|
101
|
+
token, or already-formed $variable(...)$ token.
|
|
102
|
+
--font-size <v> Font size (e.g. "48px").
|
|
103
|
+
--font-line-height <v> Font line-height (e.g. "1.1").
|
|
104
|
+
|
|
105
|
+
text-body-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
106
|
+
--name <string> Preset display name (required).
|
|
107
|
+
--pattern <google|local> Required. Example: --pattern google.
|
|
108
|
+
Pattern A is the only supported variant for now:
|
|
109
|
+
google — Pattern A: plain family + optional
|
|
110
|
+
numeric weight (e.g. family "Inter",
|
|
111
|
+
weight "400"). Verified against
|
|
112
|
+
round-2 body-text fixture.
|
|
113
|
+
local — Pattern B for divi/font-body is NOT
|
|
114
|
+
registered (no canonical-shape
|
|
115
|
+
capture exists yet). Selecting it
|
|
116
|
+
lands on a registry-absence refusal.
|
|
117
|
+
There is NO default; omitting --pattern is
|
|
118
|
+
invalid input.
|
|
119
|
+
--font-family <string> Font family (plain name, e.g. "Inter").
|
|
120
|
+
--font-weight <string> Font weight (e.g. "400").
|
|
121
|
+
--font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
|
|
122
|
+
token, or already-formed $variable(...)$ token.
|
|
123
|
+
--font-size <v> Font size (e.g. "16px").
|
|
124
|
+
--font-line-height <v> Font line-height (e.g. "1.5").
|
|
125
|
+
|
|
75
126
|
EXIT CODES
|
|
76
127
|
0 success 1 invalid input 2 evidence-gate refusal
|
|
77
128
|
3 capability missing 4 write error
|
|
@@ -82,6 +133,18 @@ EXAMPLES
|
|
|
82
133
|
--font-family Inter --font-weight 600 --font-color gcid-body-color
|
|
83
134
|
|
|
84
135
|
diviops-preset button --name "Primary" --bg-color "#2563eb" --apply
|
|
136
|
+
|
|
137
|
+
diviops-preset heading-font --name "Heading H1" --pattern google \\
|
|
138
|
+
--font-family Inter --font-weight 700 \\
|
|
139
|
+
--font-color gcid-heading-color --font-size 48px
|
|
140
|
+
|
|
141
|
+
diviops-preset heading-font --name "Heading H1 (local)" --pattern local \\
|
|
142
|
+
--font-family "Sora 700" \\
|
|
143
|
+
--font-color gcid-heading-color --font-size 48px
|
|
144
|
+
|
|
145
|
+
diviops-preset text-body-font --name "Body Text" --pattern google \\
|
|
146
|
+
--font-family Inter --font-weight 400 \\
|
|
147
|
+
--font-color gcid-body-color --font-size 16px
|
|
85
148
|
`;
|
|
86
149
|
const VALUE_FLAGS = new Set([
|
|
87
150
|
"--name",
|
|
@@ -100,6 +163,8 @@ const VALUE_FLAGS = new Set([
|
|
|
100
163
|
"--font-weight",
|
|
101
164
|
"--font-color",
|
|
102
165
|
"--font-size",
|
|
166
|
+
"--font-line-height",
|
|
167
|
+
"--pattern",
|
|
103
168
|
]);
|
|
104
169
|
/** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
|
|
105
170
|
export function parseArgs(argv) {
|
|
@@ -163,6 +228,8 @@ export class UsageError extends Error {
|
|
|
163
228
|
this.name = "UsageError";
|
|
164
229
|
}
|
|
165
230
|
}
|
|
231
|
+
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
232
|
+
const KNOWN_COMMANDS = new Set(["button", "heading-font", "text-body-font"]);
|
|
166
233
|
/** Map parsed `button` options into the emitter input shape. */
|
|
167
234
|
export function buildButtonInput(parsed) {
|
|
168
235
|
const opt = (k) => {
|
|
@@ -244,6 +311,89 @@ export function buildButtonInput(parsed) {
|
|
|
244
311
|
}
|
|
245
312
|
return input;
|
|
246
313
|
}
|
|
314
|
+
/** Map parsed `heading-font` options into the heading-font emitter input shape. */
|
|
315
|
+
export function buildHeadingFontInput(parsed) {
|
|
316
|
+
const opt = (k) => {
|
|
317
|
+
const v = parsed.options.get(k);
|
|
318
|
+
return typeof v === "string" ? v : undefined;
|
|
319
|
+
};
|
|
320
|
+
const name = opt("--name");
|
|
321
|
+
if (!name) {
|
|
322
|
+
throw new UsageError("heading-font command requires --name <string>.");
|
|
323
|
+
}
|
|
324
|
+
// --pattern is REQUIRED — Pattern A vs Pattern B are distinct registry/
|
|
325
|
+
// evidence variants and there is no safe default. An omitted --pattern
|
|
326
|
+
// is invalid input; the CLI fails BEFORE emission (no guessing).
|
|
327
|
+
const patternRaw = opt("--pattern");
|
|
328
|
+
if (patternRaw === undefined) {
|
|
329
|
+
throw new UsageError("heading-font command requires --pattern <google|local>. " +
|
|
330
|
+
"There is no default — Pattern A (google) and Pattern B (local) are distinct " +
|
|
331
|
+
"registry variants and must be selected intentionally.");
|
|
332
|
+
}
|
|
333
|
+
if (patternRaw !== "google" && patternRaw !== "local") {
|
|
334
|
+
throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
|
|
335
|
+
}
|
|
336
|
+
const pattern = patternRaw;
|
|
337
|
+
const input = { name, pattern };
|
|
338
|
+
const family = opt("--font-family");
|
|
339
|
+
if (family !== undefined)
|
|
340
|
+
input.family = family;
|
|
341
|
+
const weight = opt("--font-weight");
|
|
342
|
+
if (weight !== undefined)
|
|
343
|
+
input.weight = weight;
|
|
344
|
+
const color = opt("--font-color");
|
|
345
|
+
if (color !== undefined)
|
|
346
|
+
input.color = color;
|
|
347
|
+
const size = opt("--font-size");
|
|
348
|
+
if (size !== undefined)
|
|
349
|
+
input.size = size;
|
|
350
|
+
const lineHeight = opt("--font-line-height");
|
|
351
|
+
if (lineHeight !== undefined)
|
|
352
|
+
input.lineHeight = lineHeight;
|
|
353
|
+
return input;
|
|
354
|
+
}
|
|
355
|
+
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
356
|
+
export function buildTextBodyFontInput(parsed) {
|
|
357
|
+
const opt = (k) => {
|
|
358
|
+
const v = parsed.options.get(k);
|
|
359
|
+
return typeof v === "string" ? v : undefined;
|
|
360
|
+
};
|
|
361
|
+
const name = opt("--name");
|
|
362
|
+
if (!name) {
|
|
363
|
+
throw new UsageError("text-body-font command requires --name <string>.");
|
|
364
|
+
}
|
|
365
|
+
// --pattern is REQUIRED — there is no safe default. Pattern A only;
|
|
366
|
+
// passing "local" lands on the registry-absence refusal downstream
|
|
367
|
+
// (no Pattern B entry exists for `divi/font-body`).
|
|
368
|
+
const patternRaw = opt("--pattern");
|
|
369
|
+
if (patternRaw === undefined) {
|
|
370
|
+
throw new UsageError("text-body-font command requires --pattern <google|local>. " +
|
|
371
|
+
"Example: --pattern google. Pattern A (google) is the only " +
|
|
372
|
+
"supported variant for now; Pattern B (local) has no registry " +
|
|
373
|
+
"entry for `divi/font-body` and will be refused.");
|
|
374
|
+
}
|
|
375
|
+
if (patternRaw !== "google" && patternRaw !== "local") {
|
|
376
|
+
throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
|
|
377
|
+
}
|
|
378
|
+
const pattern = patternRaw;
|
|
379
|
+
const input = { name, pattern };
|
|
380
|
+
const family = opt("--font-family");
|
|
381
|
+
if (family !== undefined)
|
|
382
|
+
input.family = family;
|
|
383
|
+
const weight = opt("--font-weight");
|
|
384
|
+
if (weight !== undefined)
|
|
385
|
+
input.weight = weight;
|
|
386
|
+
const color = opt("--font-color");
|
|
387
|
+
if (color !== undefined)
|
|
388
|
+
input.color = color;
|
|
389
|
+
const size = opt("--font-size");
|
|
390
|
+
if (size !== undefined)
|
|
391
|
+
input.size = size;
|
|
392
|
+
const lineHeight = opt("--font-line-height");
|
|
393
|
+
if (lineHeight !== undefined)
|
|
394
|
+
input.lineHeight = lineHeight;
|
|
395
|
+
return input;
|
|
396
|
+
}
|
|
247
397
|
/**
|
|
248
398
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
249
399
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
@@ -264,17 +414,48 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
264
414
|
io.out(HELP);
|
|
265
415
|
return EXIT.OK;
|
|
266
416
|
}
|
|
267
|
-
if (parsed.command
|
|
417
|
+
if (parsed.command === null || !KNOWN_COMMANDS.has(parsed.command)) {
|
|
268
418
|
io.err(`Unknown command: ${parsed.command ?? "(none)"}. ` +
|
|
269
|
-
`
|
|
419
|
+
`Known commands: ${[...KNOWN_COMMANDS].sort().join(", ")}.`);
|
|
270
420
|
io.err("Run `diviops-preset --help` for usage.");
|
|
271
421
|
return EXIT.INVALID_INPUT;
|
|
272
422
|
}
|
|
273
423
|
// --- compose + gate -------------------------------------------------
|
|
274
|
-
|
|
424
|
+
// Per-command branch produces:
|
|
425
|
+
// - `dryRunBody`: the canonical JSON to print in --dry-run mode.
|
|
426
|
+
// - `applyFn`: a closure that issues the apply-mode write when the
|
|
427
|
+
// capability check passes. Both branches reuse the existing write-
|
|
428
|
+
// path plumbing (`assertStorageCapability` → `/preset/create`).
|
|
429
|
+
let dryRunBody;
|
|
430
|
+
let applyFn;
|
|
275
431
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
432
|
+
if (parsed.command === "button") {
|
|
433
|
+
const input = buildButtonInput(parsed);
|
|
434
|
+
const entry = emitButtonGroupPreset(input);
|
|
435
|
+
dryRunBody = buildPresetCreateBody(entry, { dry_run: true });
|
|
436
|
+
applyFn = (client, sv) => applyButtonPreset(client, entry, { serverVersion: sv });
|
|
437
|
+
}
|
|
438
|
+
else if (parsed.command === "heading-font") {
|
|
439
|
+
const input = buildHeadingFontInput(parsed);
|
|
440
|
+
const entry = emitHeadingFontGroupPreset(input);
|
|
441
|
+
dryRunBody = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
|
|
442
|
+
applyFn = (client, sv) => applyHeadingFontPreset(client, entry, { serverVersion: sv });
|
|
443
|
+
}
|
|
444
|
+
else if (parsed.command === "text-body-font") {
|
|
445
|
+
// text-body-font (Pattern A only; Pattern B lands on the
|
|
446
|
+
// registry-absence refusal in emitTextBodyFontGroupPreset).
|
|
447
|
+
const input = buildTextBodyFontInput(parsed);
|
|
448
|
+
const entry = emitTextBodyFontGroupPreset(input);
|
|
449
|
+
dryRunBody = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
|
|
450
|
+
applyFn = (client, sv) => applyTextBodyFontPreset(client, entry, { serverVersion: sv });
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Defensive: a new entry in KNOWN_COMMANDS without a dispatch
|
|
454
|
+
// branch here would silently break dry-run/apply. parseArgs
|
|
455
|
+
// already gates on KNOWN_COMMANDS upstream, so reaching this is a
|
|
456
|
+
// programmer error in the dispatch wiring.
|
|
457
|
+
throw new UsageError(`Unhandled command: ${parsed.command}`);
|
|
458
|
+
}
|
|
278
459
|
}
|
|
279
460
|
catch (err) {
|
|
280
461
|
if (err instanceof EvidenceGateError) {
|
|
@@ -286,13 +467,19 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
286
467
|
io.err("Run `diviops-preset --help` for usage.");
|
|
287
468
|
return EXIT.INVALID_INPUT;
|
|
288
469
|
}
|
|
470
|
+
if (err instanceof UnsupportedVariantCombinationError) {
|
|
471
|
+
// Distinct from EvidenceGateError: the registry IS complete for the
|
|
472
|
+
// verified variants, but the caller asked for a combination outside
|
|
473
|
+
// any verified variant. Surfaces as invalid input (exit 1).
|
|
474
|
+
io.err(err.message);
|
|
475
|
+
return EXIT.INVALID_INPUT;
|
|
476
|
+
}
|
|
289
477
|
io.err(err instanceof Error ? err.message : String(err));
|
|
290
478
|
return EXIT.INVALID_INPUT;
|
|
291
479
|
}
|
|
292
480
|
// --- dry-run --------------------------------------------------------
|
|
293
481
|
if (parsed.dryRun) {
|
|
294
|
-
|
|
295
|
-
io.out(JSON.stringify(body, null, 2));
|
|
482
|
+
io.out(JSON.stringify(dryRunBody, null, 2));
|
|
296
483
|
return EXIT.OK;
|
|
297
484
|
}
|
|
298
485
|
// --- apply ----------------------------------------------------------
|
|
@@ -307,7 +494,7 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
307
494
|
"This is an internal error — invoke via the `diviops-preset` bin.");
|
|
308
495
|
return EXIT.WRITE_ERROR;
|
|
309
496
|
}
|
|
310
|
-
const result = await
|
|
497
|
+
const result = await applyFn(client, serverVersion);
|
|
311
498
|
io.out(JSON.stringify(result, null, 2));
|
|
312
499
|
return EXIT.OK;
|
|
313
500
|
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,166 @@
|
|
|
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 { loadRegistry, gateWriteAttr, } from "./registry.js";
|
|
36
|
+
import { normalizeColorValue } from "./variable-token.js";
|
|
37
|
+
export const HEADING_FONT_MODULE = "divi/heading";
|
|
38
|
+
export const HEADING_FONT_GROUP_NAME = "divi/font";
|
|
39
|
+
export const HEADING_FONT_GROUP_ID = "designTitleText";
|
|
40
|
+
export const HEADING_FONT_PATTERN_FAMILY = "divi/font";
|
|
41
|
+
/** The two verified pattern variants for `divi/font` on `divi/heading`. */
|
|
42
|
+
export const HEADING_FONT_PATTERN_VARIANTS = {
|
|
43
|
+
google: "google_fonts_pattern_a",
|
|
44
|
+
local: "local_hosted_pattern_b",
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Raised when the caller passes a combination the registry does not
|
|
48
|
+
* authorize for the current Track — distinct from `EvidenceGateError`
|
|
49
|
+
* (registry-evidence shortfall) and `UsageError` (CLI arg parse).
|
|
50
|
+
*/
|
|
51
|
+
export class UnsupportedVariantCombinationError extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "UnsupportedVariantCombinationError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compose the canonical `attrs.title.decoration.font.font.desktop.value`
|
|
59
|
+
* bag from the input. Emit-on-specification: only specified sub-fields
|
|
60
|
+
* produce keys.
|
|
61
|
+
*/
|
|
62
|
+
export function composeHeadingFontAttrs(input) {
|
|
63
|
+
const value = {};
|
|
64
|
+
if (input.family !== undefined)
|
|
65
|
+
value.family = input.family;
|
|
66
|
+
if (input.weight !== undefined)
|
|
67
|
+
value.weight = input.weight;
|
|
68
|
+
if (input.color !== undefined)
|
|
69
|
+
value.color = normalizeColorValue(input.color);
|
|
70
|
+
if (input.size !== undefined)
|
|
71
|
+
value.size = input.size;
|
|
72
|
+
if (input.lineHeight !== undefined)
|
|
73
|
+
value.lineHeight = input.lineHeight;
|
|
74
|
+
return {
|
|
75
|
+
title: {
|
|
76
|
+
decoration: {
|
|
77
|
+
font: {
|
|
78
|
+
font: {
|
|
79
|
+
desktop: {
|
|
80
|
+
value,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Emit a canonical `divi/font` heading group preset.
|
|
90
|
+
*
|
|
91
|
+
* 1. Validate input shape (name, pattern, Pattern B + weight refusal).
|
|
92
|
+
* 2. Compose `attrs.title.decoration.font.font.desktop.value.*`
|
|
93
|
+
* (emit-on-specification).
|
|
94
|
+
* 3. Gate the chosen `(divi/font, pattern_variant)` cell on `divi/heading`
|
|
95
|
+
* against the verified-attrs registry — throws `EvidenceGateError`
|
|
96
|
+
* when effective evidence is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
97
|
+
*
|
|
98
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
99
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
100
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
101
|
+
* to match VB save semantics — see `trait-preset.php` `preset_create`.
|
|
102
|
+
* The Round 1a/1b fixtures capture the post-write storage shape.
|
|
103
|
+
*/
|
|
104
|
+
export function emitHeadingFontGroupPreset(input, registry = loadRegistry()) {
|
|
105
|
+
if (!input.name || typeof input.name !== "string") {
|
|
106
|
+
throw new Error("Heading-font emitter requires a non-empty `name`.");
|
|
107
|
+
}
|
|
108
|
+
if (input.pattern !== "google" && input.pattern !== "local") {
|
|
109
|
+
throw new Error(`Heading-font emitter requires \`pattern\` to be "google" or "local"; got ${JSON.stringify(input.pattern)}. ` +
|
|
110
|
+
`There is no default — Pattern A (google) and Pattern B (local) are distinct registry/evidence variants.`);
|
|
111
|
+
}
|
|
112
|
+
// Pattern B + explicit weight is out-of-scope for Track 5. The verified
|
|
113
|
+
// Pattern B capture proves `weight` is absent; no registry entry
|
|
114
|
+
// currently authorizes a local-hosted-with-explicit-weight shape.
|
|
115
|
+
if (input.pattern === "local" && input.weight !== undefined) {
|
|
116
|
+
throw new UnsupportedVariantCombinationError(`Pattern B (local-hosted) does not support an explicit \`weight\` — the verified ` +
|
|
117
|
+
`Pattern B shape has no \`weight\` key, and there is no registry variant ` +
|
|
118
|
+
`authorizing local-hosted-with-explicit-weight. Encode the weight into the ` +
|
|
119
|
+
`family string instead (e.g. \`family: "Sora 700"\`). A future registry entry ` +
|
|
120
|
+
`can authorize that shape if VB evidence supports it.`);
|
|
121
|
+
}
|
|
122
|
+
const attrs = composeHeadingFontAttrs(input);
|
|
123
|
+
// Sanity check: at least one styling field was specified. An empty
|
|
124
|
+
// value bag is a usage error — there is nothing to write.
|
|
125
|
+
const value = attrs.title.decoration.font.font.desktop.value;
|
|
126
|
+
if (!value || Object.keys(value).length === 0) {
|
|
127
|
+
throw new Error("Heading-font emitter produced an empty preset — pass at least one of " +
|
|
128
|
+
"family, weight, color, size, or lineHeight.");
|
|
129
|
+
}
|
|
130
|
+
// Registry gate: variant-aware. Pattern A evidence must NOT vouch for
|
|
131
|
+
// Pattern B and vice versa — gateWriteAttr resolves by both family AND
|
|
132
|
+
// variant, so a missing/under-verified variant entry throws here.
|
|
133
|
+
const patternVariant = HEADING_FONT_PATTERN_VARIANTS[input.pattern];
|
|
134
|
+
gateWriteAttr(registry, HEADING_FONT_MODULE, HEADING_FONT_PATTERN_FAMILY, patternVariant);
|
|
135
|
+
return {
|
|
136
|
+
type: "group",
|
|
137
|
+
module_name: HEADING_FONT_MODULE,
|
|
138
|
+
group_name: HEADING_FONT_GROUP_NAME,
|
|
139
|
+
group_id: HEADING_FONT_GROUP_ID,
|
|
140
|
+
pattern_variant: patternVariant,
|
|
141
|
+
name: input.name,
|
|
142
|
+
attrs,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build the `POST /diviops/v1/preset/create` request body from a heading
|
|
147
|
+
* font preset entry. Matches the body shape the `diviops_preset_create`
|
|
148
|
+
* MCP tool posts — the CLI reuses the existing route, it does not add one.
|
|
149
|
+
*
|
|
150
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
151
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
152
|
+
* client-side before the write.
|
|
153
|
+
*/
|
|
154
|
+
export function buildHeadingFontPresetCreateBody(entry, opts = {}) {
|
|
155
|
+
const body = {
|
|
156
|
+
module_name: entry.module_name,
|
|
157
|
+
name: entry.name,
|
|
158
|
+
attrs: entry.attrs,
|
|
159
|
+
type: entry.type,
|
|
160
|
+
group_name: entry.group_name,
|
|
161
|
+
group_id: entry.group_id,
|
|
162
|
+
};
|
|
163
|
+
if (opts.dry_run)
|
|
164
|
+
body.dry_run = true;
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
@@ -48,9 +48,11 @@ export interface VerifiedAttrsRegistry {
|
|
|
48
48
|
tier2?: Tier12Entry[];
|
|
49
49
|
tier3?: unknown[];
|
|
50
50
|
}
|
|
51
|
-
/** Resolution of a single `(module, pattern-family)` cell against the registry. */
|
|
51
|
+
/** Resolution of a single `(module, pattern-family[, pattern-variant])` cell against the registry. */
|
|
52
52
|
export interface EvidenceResolution {
|
|
53
53
|
patternFamily: string;
|
|
54
|
+
/** The matched entry's `pattern_variant`, if any. */
|
|
55
|
+
patternVariant?: string;
|
|
54
56
|
module: string;
|
|
55
57
|
/** Numeric pattern-level evidence (0–5). */
|
|
56
58
|
patternLevel: number;
|
|
@@ -78,17 +80,29 @@ export declare function loadRegistry(explicitPath?: string): VerifiedAttrsRegist
|
|
|
78
80
|
/** Reset the module-level cache. Used by tests. */
|
|
79
81
|
export declare function resetRegistryCache(): void;
|
|
80
82
|
/**
|
|
81
|
-
* Find a Tier 1 / Tier 2 entry by `pattern_family
|
|
82
|
-
*
|
|
83
|
+
* Find a Tier 1 / Tier 2 entry by `pattern_family` and (optionally)
|
|
84
|
+
* `pattern_variant`. Tier 2 is searched first, then Tier 1.
|
|
85
|
+
*
|
|
86
|
+
* Some pattern families (e.g. `divi/font`) carry multiple entries that
|
|
87
|
+
* differ ONLY by `pattern_variant` (`google_fonts_pattern_a` vs
|
|
88
|
+
* `local_hosted_pattern_b`). When `patternVariant` is supplied, ONLY an
|
|
89
|
+
* entry whose `pattern_variant` matches exactly is returned — a Pattern-A
|
|
90
|
+
* entry must not vouch for a Pattern-B caller, and vice versa.
|
|
91
|
+
*
|
|
92
|
+
* When `patternVariant` is omitted, the legacy behavior holds: the first
|
|
93
|
+
* matching `pattern_family` is returned regardless of variant — only safe
|
|
94
|
+
* for families with a single entry. New variant-aware callers should pass
|
|
95
|
+
* the variant explicitly.
|
|
83
96
|
*/
|
|
84
|
-
export declare function findPatternEntry(registry: VerifiedAttrsRegistry, patternFamily: string): Tier12Entry | undefined;
|
|
97
|
+
export declare function findPatternEntry(registry: VerifiedAttrsRegistry, patternFamily: string, patternVariant?: string): Tier12Entry | undefined;
|
|
85
98
|
/**
|
|
86
|
-
* Resolve effective evidence for a `(module, pattern-family)` cell.
|
|
99
|
+
* Resolve effective evidence for a `(module, pattern-family[, pattern-variant])` cell.
|
|
87
100
|
*
|
|
88
|
-
* Throws if the pattern family
|
|
89
|
-
*
|
|
101
|
+
* Throws if the pattern family (or, when supplied, the specific variant)
|
|
102
|
+
* is entirely absent from the registry — that is an unrecoverable gap
|
|
103
|
+
* (the CLI must fail, not guess).
|
|
90
104
|
*/
|
|
91
|
-
export declare function resolveEvidence(registry: VerifiedAttrsRegistry, module: string, patternFamily: string): EvidenceResolution;
|
|
105
|
+
export declare function resolveEvidence(registry: VerifiedAttrsRegistry, module: string, patternFamily: string, patternVariant?: string): EvidenceResolution;
|
|
92
106
|
/** Numeric ordering of the write-emitter threshold against a registry. */
|
|
93
107
|
export declare function writeThresholdNumber(registry: VerifiedAttrsRegistry): number;
|
|
94
108
|
/**
|
|
@@ -104,4 +118,4 @@ export declare class EvidenceGateError extends Error {
|
|
|
104
118
|
readonly thresholdName: string;
|
|
105
119
|
constructor(patternFamily: string, module: string, resolution: EvidenceResolution, thresholdNumber: number, thresholdName: string);
|
|
106
120
|
}
|
|
107
|
-
export declare function gateWriteAttr(registry: VerifiedAttrsRegistry, module: string, patternFamily: string): EvidenceResolution;
|
|
121
|
+
export declare function gateWriteAttr(registry: VerifiedAttrsRegistry, module: string, patternFamily: string, patternVariant?: string): EvidenceResolution;
|