@diviops/mcp-server 1.5.11 → 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.
- package/README.md +15 -2
- package/dist/preset-cli/__tests__/cli.test.js +181 -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 +15 -0
- package/dist/preset-cli/__tests__/write-path.test.js +55 -1
- package/dist/preset-cli/cli.d.ts +3 -0
- package/dist/preset-cli/cli.js +114 -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/write-path.d.ts +15 -0
- package/dist/preset-cli/write-path.js +21 -0
- package/package.json +1 -1
package/dist/preset-cli/cli.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
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";
|
|
26
27
|
import { EvidenceGateError } from "./registry.js";
|
|
27
|
-
import { applyButtonPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
28
|
+
import { applyButtonPreset, applyHeadingFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
28
29
|
export const EXIT = {
|
|
29
30
|
OK: 0,
|
|
30
31
|
INVALID_INPUT: 1,
|
|
@@ -39,8 +40,10 @@ const realIO = {
|
|
|
39
40
|
const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
|
|
40
41
|
|
|
41
42
|
USAGE
|
|
42
|
-
diviops-preset button [options]
|
|
43
|
-
diviops-preset
|
|
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
|
|
44
47
|
|
|
45
48
|
MODE
|
|
46
49
|
--dry-run Compose + print canonical JSON only (DEFAULT).
|
|
@@ -72,6 +75,30 @@ button OPTIONS (all styling fields optional; emit-on-specification only)
|
|
|
72
75
|
.value.padding.top="0px" (hover-padding-gate
|
|
73
76
|
workaround). Off by default.
|
|
74
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
|
+
|
|
75
102
|
EXIT CODES
|
|
76
103
|
0 success 1 invalid input 2 evidence-gate refusal
|
|
77
104
|
3 capability missing 4 write error
|
|
@@ -82,6 +109,14 @@ EXAMPLES
|
|
|
82
109
|
--font-family Inter --font-weight 600 --font-color gcid-body-color
|
|
83
110
|
|
|
84
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
|
|
85
120
|
`;
|
|
86
121
|
const VALUE_FLAGS = new Set([
|
|
87
122
|
"--name",
|
|
@@ -100,6 +135,8 @@ const VALUE_FLAGS = new Set([
|
|
|
100
135
|
"--font-weight",
|
|
101
136
|
"--font-color",
|
|
102
137
|
"--font-size",
|
|
138
|
+
"--font-line-height",
|
|
139
|
+
"--pattern",
|
|
103
140
|
]);
|
|
104
141
|
/** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
|
|
105
142
|
export function parseArgs(argv) {
|
|
@@ -163,6 +200,8 @@ export class UsageError extends Error {
|
|
|
163
200
|
this.name = "UsageError";
|
|
164
201
|
}
|
|
165
202
|
}
|
|
203
|
+
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
204
|
+
const KNOWN_COMMANDS = new Set(["button", "heading-font"]);
|
|
166
205
|
/** Map parsed `button` options into the emitter input shape. */
|
|
167
206
|
export function buildButtonInput(parsed) {
|
|
168
207
|
const opt = (k) => {
|
|
@@ -244,6 +283,47 @@ export function buildButtonInput(parsed) {
|
|
|
244
283
|
}
|
|
245
284
|
return input;
|
|
246
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
|
+
}
|
|
247
327
|
/**
|
|
248
328
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
249
329
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
@@ -264,17 +344,34 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
264
344
|
io.out(HELP);
|
|
265
345
|
return EXIT.OK;
|
|
266
346
|
}
|
|
267
|
-
if (parsed.command
|
|
347
|
+
if (parsed.command === null || !KNOWN_COMMANDS.has(parsed.command)) {
|
|
268
348
|
io.err(`Unknown command: ${parsed.command ?? "(none)"}. ` +
|
|
269
|
-
`
|
|
349
|
+
`Known commands: ${[...KNOWN_COMMANDS].sort().join(", ")}.`);
|
|
270
350
|
io.err("Run `diviops-preset --help` for usage.");
|
|
271
351
|
return EXIT.INVALID_INPUT;
|
|
272
352
|
}
|
|
273
353
|
// --- compose + gate -------------------------------------------------
|
|
274
|
-
|
|
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;
|
|
275
361
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
375
|
}
|
|
279
376
|
catch (err) {
|
|
280
377
|
if (err instanceof EvidenceGateError) {
|
|
@@ -286,13 +383,19 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
286
383
|
io.err("Run `diviops-preset --help` for usage.");
|
|
287
384
|
return EXIT.INVALID_INPUT;
|
|
288
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
|
+
}
|
|
289
393
|
io.err(err instanceof Error ? err.message : String(err));
|
|
290
394
|
return EXIT.INVALID_INPUT;
|
|
291
395
|
}
|
|
292
396
|
// --- dry-run --------------------------------------------------------
|
|
293
397
|
if (parsed.dryRun) {
|
|
294
|
-
|
|
295
|
-
io.out(JSON.stringify(body, null, 2));
|
|
398
|
+
io.out(JSON.stringify(dryRunBody, null, 2));
|
|
296
399
|
return EXIT.OK;
|
|
297
400
|
}
|
|
298
401
|
// --- apply ----------------------------------------------------------
|
|
@@ -307,7 +410,7 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
307
410
|
"This is an internal error — invoke via the `diviops-preset` bin.");
|
|
308
411
|
return EXIT.WRITE_ERROR;
|
|
309
412
|
}
|
|
310
|
-
const result = await
|
|
413
|
+
const result = await applyFn(client, serverVersion);
|
|
311
414
|
io.out(JSON.stringify(result, null, 2));
|
|
312
415
|
return EXIT.OK;
|
|
313
416
|
}
|
|
@@ -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;
|
|
@@ -77,28 +77,48 @@ function levelName(registry, num) {
|
|
|
77
77
|
return "UNVERIFIED";
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* Find a Tier 1 / Tier 2 entry by `pattern_family
|
|
81
|
-
*
|
|
80
|
+
* Find a Tier 1 / Tier 2 entry by `pattern_family` and (optionally)
|
|
81
|
+
* `pattern_variant`. Tier 2 is searched first, then Tier 1.
|
|
82
|
+
*
|
|
83
|
+
* Some pattern families (e.g. `divi/font`) carry multiple entries that
|
|
84
|
+
* differ ONLY by `pattern_variant` (`google_fonts_pattern_a` vs
|
|
85
|
+
* `local_hosted_pattern_b`). When `patternVariant` is supplied, ONLY an
|
|
86
|
+
* entry whose `pattern_variant` matches exactly is returned — a Pattern-A
|
|
87
|
+
* entry must not vouch for a Pattern-B caller, and vice versa.
|
|
88
|
+
*
|
|
89
|
+
* When `patternVariant` is omitted, the legacy behavior holds: the first
|
|
90
|
+
* matching `pattern_family` is returned regardless of variant — only safe
|
|
91
|
+
* for families with a single entry. New variant-aware callers should pass
|
|
92
|
+
* the variant explicitly.
|
|
82
93
|
*/
|
|
83
|
-
export function findPatternEntry(registry, patternFamily) {
|
|
94
|
+
export function findPatternEntry(registry, patternFamily, patternVariant) {
|
|
84
95
|
const tiers = [registry.tier2 ?? [], registry.tier1 ?? []];
|
|
85
96
|
for (const tier of tiers) {
|
|
86
|
-
const hit = tier.find((e) =>
|
|
97
|
+
const hit = tier.find((e) => {
|
|
98
|
+
if (e.pattern_family !== patternFamily)
|
|
99
|
+
return false;
|
|
100
|
+
if (patternVariant !== undefined) {
|
|
101
|
+
return e.pattern_variant === patternVariant;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
87
105
|
if (hit)
|
|
88
106
|
return hit;
|
|
89
107
|
}
|
|
90
108
|
return undefined;
|
|
91
109
|
}
|
|
92
110
|
/**
|
|
93
|
-
* Resolve effective evidence for a `(module, pattern-family)` cell.
|
|
111
|
+
* Resolve effective evidence for a `(module, pattern-family[, pattern-variant])` cell.
|
|
94
112
|
*
|
|
95
|
-
* Throws if the pattern family
|
|
96
|
-
*
|
|
113
|
+
* Throws if the pattern family (or, when supplied, the specific variant)
|
|
114
|
+
* is entirely absent from the registry — that is an unrecoverable gap
|
|
115
|
+
* (the CLI must fail, not guess).
|
|
97
116
|
*/
|
|
98
|
-
export function resolveEvidence(registry, module, patternFamily) {
|
|
99
|
-
const entry = findPatternEntry(registry, patternFamily);
|
|
117
|
+
export function resolveEvidence(registry, module, patternFamily, patternVariant) {
|
|
118
|
+
const entry = findPatternEntry(registry, patternFamily, patternVariant);
|
|
100
119
|
if (!entry) {
|
|
101
|
-
|
|
120
|
+
const variantSuffix = patternVariant !== undefined ? ` (variant "${patternVariant}")` : "";
|
|
121
|
+
throw new Error(`Pattern family "${patternFamily}"${variantSuffix} is absent from verified-attrs.json. ` +
|
|
102
122
|
`The CLI cannot emit an unregistered attr. File this as a registry-gap backlog candidate.`);
|
|
103
123
|
}
|
|
104
124
|
const patternLevel = levelNumber(registry, entry.pattern_evidence_level);
|
|
@@ -113,6 +133,7 @@ export function resolveEvidence(registry, module, patternFamily) {
|
|
|
113
133
|
"verified-attrs.json (no source field)";
|
|
114
134
|
return {
|
|
115
135
|
patternFamily,
|
|
136
|
+
patternVariant: entry.pattern_variant,
|
|
116
137
|
module,
|
|
117
138
|
patternLevel,
|
|
118
139
|
patternLevelName: entry.pattern_evidence_level,
|
|
@@ -141,7 +162,10 @@ export class EvidenceGateError extends Error {
|
|
|
141
162
|
thresholdNumber;
|
|
142
163
|
thresholdName;
|
|
143
164
|
constructor(patternFamily, module, resolution, thresholdNumber, thresholdName) {
|
|
144
|
-
|
|
165
|
+
const variantTag = resolution.patternVariant !== undefined
|
|
166
|
+
? ` variant "${resolution.patternVariant}"`
|
|
167
|
+
: "";
|
|
168
|
+
super(`Evidence-gate refusal: attr family "${patternFamily}"${variantTag} on module "${module}" ` +
|
|
145
169
|
`has EFFECTIVE evidence ${resolution.effectiveLevelName} (${resolution.effectiveLevel}), ` +
|
|
146
170
|
`below the write threshold ${thresholdName} (${thresholdNumber}). ` +
|
|
147
171
|
(resolution.applicabilityMissing
|
|
@@ -158,8 +182,8 @@ export class EvidenceGateError extends Error {
|
|
|
158
182
|
this.name = "EvidenceGateError";
|
|
159
183
|
}
|
|
160
184
|
}
|
|
161
|
-
export function gateWriteAttr(registry, module, patternFamily) {
|
|
162
|
-
const resolution = resolveEvidence(registry, module, patternFamily);
|
|
185
|
+
export function gateWriteAttr(registry, module, patternFamily, patternVariant) {
|
|
186
|
+
const resolution = resolveEvidence(registry, module, patternFamily, patternVariant);
|
|
163
187
|
const thresholdNumber = writeThresholdNumber(registry);
|
|
164
188
|
if (resolution.effectiveLevel < thresholdNumber) {
|
|
165
189
|
throw new EvidenceGateError(patternFamily, module, resolution, thresholdNumber, WRITE_EMITTER_THRESHOLD_LEVEL);
|