@diviops/mcp-server 1.5.12 → 1.5.14
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 +9 -0
- package/data/verified-attrs-backlog.json +5 -5
- package/data/verified-attrs.json +30 -16
- package/dist/preset-cli/__tests__/cli.test.js +461 -0
- package/dist/preset-cli/__tests__/registry.test.js +26 -0
- package/dist/preset-cli/__tests__/spacing-emitter.test.d.ts +20 -0
- package/dist/preset-cli/__tests__/spacing-emitter.test.js +409 -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 +114 -1
- package/dist/preset-cli/cli.d.ts +6 -0
- package/dist/preset-cli/cli.js +217 -8
- package/dist/preset-cli/spacing-emitter.d.ts +132 -0
- package/dist/preset-cli/spacing-emitter.js +276 -0
- 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 +32 -0
- package/dist/preset-cli/write-path.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/spacing` section group-preset emitter — Track 7b vertical slice.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.6.0 `type: "group"` `divi/spacing` preset
|
|
5
|
+
* targeting `divi/section` at
|
|
6
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json`.
|
|
9
|
+
*
|
|
10
|
+
* Scope: `divi/section` ONLY. Heading / text / button spacing cells remain
|
|
11
|
+
* `SCHEMA_OBSERVED` and resolve to `EvidenceGateError` here per
|
|
12
|
+
* `feedback_preset_map_per_module` (no cross-module shape carry-over).
|
|
13
|
+
*
|
|
14
|
+
* Shape rules enforced here (brief §4):
|
|
15
|
+
* - Sparse-emit per axis — only user-touched corners appear under
|
|
16
|
+
* `padding`/`margin`. Untouched corners are absent (not present-as-empty,
|
|
17
|
+
* not present-as-null).
|
|
18
|
+
* - Paired sync flags per axis — any spacing touch on an axis emits BOTH
|
|
19
|
+
* `syncVertical` AND `syncHorizontal` as siblings. Default `"off"` on
|
|
20
|
+
* both unless the caller explicitly toggled them.
|
|
21
|
+
* - Padding and margin are INDEPENDENT bags — passing only padding flags
|
|
22
|
+
* omits the margin bag entirely, and vice versa.
|
|
23
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
24
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
25
|
+
* VB save semantics); the CLI request body only carries `attrs`. The
|
|
26
|
+
* Track 7a fixture captures the post-write storage shape — do NOT add
|
|
27
|
+
* `styleAttrs` / `renderAttrs` keys to the emitter output.
|
|
28
|
+
* - `groupId: "designSpacing"` — the Composable Settings panel id, NOT a
|
|
29
|
+
* dotted attr path (the prior schema-inferred `module.decoration.spacing`
|
|
30
|
+
* note was a misread, corrected in PR #751).
|
|
31
|
+
* - `primaryAttrName: "module"` for section spacing.
|
|
32
|
+
* - Variable tokens (`$variable(...)` / bare `gvid-*`) in length flags are
|
|
33
|
+
* REFUSED — Track 7a capture exercised literal CSS lengths only;
|
|
34
|
+
* variable-token shape needs its own capture before this emitter writes
|
|
35
|
+
* it.
|
|
36
|
+
*/
|
|
37
|
+
import { loadRegistry, gateWriteAttr, } from "./registry.js";
|
|
38
|
+
export const SPACING_GROUP_NAME = "divi/spacing";
|
|
39
|
+
export const SPACING_GROUP_ID = "designSpacing";
|
|
40
|
+
export const SPACING_PRIMARY_ATTR_NAME = "module";
|
|
41
|
+
export const SPACING_PATTERN_FAMILY = "divi/spacing";
|
|
42
|
+
/**
|
|
43
|
+
* The single currently-supported module cell. The `--module` flag accepts
|
|
44
|
+
* any string and routes through the registry gate (so the surface stays
|
|
45
|
+
* stable when heading/text/button cells eventually promote), but only
|
|
46
|
+
* `divi/section` is wired with fixtures + tests + canonical shape today.
|
|
47
|
+
*
|
|
48
|
+
* Promoting another module is NOT a free dispatch-clear via the registry
|
|
49
|
+
* gate — each new cell needs a Track-7a-style canonical capture PR landing
|
|
50
|
+
* first AND a follow-up implementation/docs PR.
|
|
51
|
+
*/
|
|
52
|
+
export const SPACING_SUPPORTED_MODULES = ["divi/section"];
|
|
53
|
+
/** Detect bare `gvid-*` / `gcid-*` variable token names. */
|
|
54
|
+
const BARE_VARIABLE_TOKEN_RE = /^(gvid|gcid)-/;
|
|
55
|
+
/**
|
|
56
|
+
* Positive validator for CSS length values. Accepts only the v1 unit set
|
|
57
|
+
* from the brief: integer or decimal numbers (including a leading minus
|
|
58
|
+
* sign and bare `0`) followed by `px`, `rem`, `em`, `%`, `vw`, or `vh`.
|
|
59
|
+
* Track 7a capture exercised literal values only — variable tokens,
|
|
60
|
+
* `var(...)`, `calc(...)`, and free-form strings like `banana` are all
|
|
61
|
+
* refused before emission. Adding any new unit requires a brief update
|
|
62
|
+
* (canonical-capture re-test if the unit changes server-side rendering).
|
|
63
|
+
*/
|
|
64
|
+
const LITERAL_CSS_LENGTH_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|%|vw|vh)$/;
|
|
65
|
+
/**
|
|
66
|
+
* Validate that `value` is a literal CSS length and not a deferred-shape
|
|
67
|
+
* token. The check runs on EVERY length flag before the emission path.
|
|
68
|
+
*
|
|
69
|
+
* Three refusal paths:
|
|
70
|
+
* 1. `$variable(...)` token — deferred shape, needs its own capture.
|
|
71
|
+
* 2. Bare `gvid-*` / `gcid-*` token name — same deferred shape.
|
|
72
|
+
* 3. Anything else that does NOT match the v1 literal CSS length grammar
|
|
73
|
+
* (`px`, `rem`, `em`, `%`, `vw`, `vh`). Catches `banana`, `var(--x)`,
|
|
74
|
+
* `calc(8px + 2vw)`, etc. — none of which Track 7a verified.
|
|
75
|
+
*
|
|
76
|
+
* The variable-token branches fire BEFORE the generic literal check so
|
|
77
|
+
* the operator gets the precise "variable-token support deferred" hint
|
|
78
|
+
* rather than the generic "not a literal CSS length" message.
|
|
79
|
+
*/
|
|
80
|
+
function assertLiteralLength(value, flagLabel) {
|
|
81
|
+
// Trim before pattern-matching so leading / trailing whitespace can't
|
|
82
|
+
// bypass any prefix check (e.g. " gvid-space-1"). The raw value
|
|
83
|
+
// appears in the error message so the operator sees exactly what was
|
|
84
|
+
// passed.
|
|
85
|
+
const trimmed = value.trim();
|
|
86
|
+
if (trimmed.startsWith("$variable(")) {
|
|
87
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is a $variable() token. ` +
|
|
88
|
+
`Variable-token support deferred — pending canonical capture. ` +
|
|
89
|
+
`Track 7a exercised literal CSS length values only (px / rem / em / % / vw / vh).`);
|
|
90
|
+
}
|
|
91
|
+
if (BARE_VARIABLE_TOKEN_RE.test(trimmed)) {
|
|
92
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is a bare ${trimmed.split("-")[0]}-* variable token. ` +
|
|
93
|
+
`Variable-token support deferred — pending canonical capture. ` +
|
|
94
|
+
`Track 7a exercised literal CSS length values only (px / rem / em / % / vw / vh).`);
|
|
95
|
+
}
|
|
96
|
+
if (!LITERAL_CSS_LENGTH_RE.test(trimmed)) {
|
|
97
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is not a literal CSS length. ` +
|
|
98
|
+
`v1 accepts only \`<number><unit>\` where unit is one of px / rem / em / % / vw / vh ` +
|
|
99
|
+
`(e.g. "40px", "1.5rem", "100%"). Track 7a canonical capture exercised literal ` +
|
|
100
|
+
`values only; broader value grammars (var(...), calc(...), etc.) need their own capture.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compose a single axis bag (padding or margin) from a corner input.
|
|
105
|
+
*
|
|
106
|
+
* Returns `undefined` when the axis input is entirely absent (the axis is
|
|
107
|
+
* absent from the emitted attrs — sparse-emit at axis level). When ANY
|
|
108
|
+
* corner is passed, both sync flags are emitted as paired siblings
|
|
109
|
+
* (default `"off"` on each side unless the caller explicitly toggled
|
|
110
|
+
* them).
|
|
111
|
+
*
|
|
112
|
+
* Sync-flag-only input on an axis with no touched corner is REFUSED with
|
|
113
|
+
* an explicit error — silently no-op'ing a passed sync flag would be a
|
|
114
|
+
* surprising footgun. If the caller wants to toggle sync, they must also
|
|
115
|
+
* pass at least one corner on the same axis to anchor it.
|
|
116
|
+
*/
|
|
117
|
+
function composeAxis(input, flagPrefix) {
|
|
118
|
+
if (!input)
|
|
119
|
+
return undefined;
|
|
120
|
+
const corners = ["top", "right", "bottom", "left"].filter((c) => input[c] !== undefined);
|
|
121
|
+
if (corners.length === 0) {
|
|
122
|
+
// Sync flag passed without any corner on the same axis → explicit
|
|
123
|
+
// refusal. The paired sync flags only have meaning anchored to at
|
|
124
|
+
// least one corner; emitting them alone would silently drop them.
|
|
125
|
+
if (input.syncVertical !== undefined ||
|
|
126
|
+
input.syncHorizontal !== undefined) {
|
|
127
|
+
throw new Error(`--${flagPrefix}-sync-vertical / --${flagPrefix}-sync-horizontal require at ` +
|
|
128
|
+
`least one --${flagPrefix}-{top,right,bottom,left} corner on the same axis. ` +
|
|
129
|
+
`Sync flags are paired-sibling metadata anchored to corners; emitting them ` +
|
|
130
|
+
`alone would silently no-op.`);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const bag = {};
|
|
135
|
+
for (const c of corners) {
|
|
136
|
+
const v = input[c];
|
|
137
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
138
|
+
throw new Error(`--${flagPrefix}-${c} requires a non-empty CSS length value.`);
|
|
139
|
+
}
|
|
140
|
+
assertLiteralLength(v, `--${flagPrefix}-${c}`);
|
|
141
|
+
bag[c] = v;
|
|
142
|
+
}
|
|
143
|
+
bag.syncVertical = input.syncVertical ?? "off";
|
|
144
|
+
bag.syncHorizontal = input.syncHorizontal ?? "off";
|
|
145
|
+
return bag;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Compose the canonical
|
|
149
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}` bag.
|
|
150
|
+
* Sparse-emit per axis; padding and margin are independent.
|
|
151
|
+
*
|
|
152
|
+
* Returns BOTH the full `attrs` tree (what the emitter ships) AND the
|
|
153
|
+
* inner `value` bag — the caller does empty-preset validation against
|
|
154
|
+
* `value` without re-walking the nested attrs tree (no `as any` deep
|
|
155
|
+
* casts).
|
|
156
|
+
*/
|
|
157
|
+
export function composeSpacingAttrs(input) {
|
|
158
|
+
const padding = composeAxis(input.padding, "padding");
|
|
159
|
+
const margin = composeAxis(input.margin, "margin");
|
|
160
|
+
const value = {};
|
|
161
|
+
if (padding)
|
|
162
|
+
value.padding = padding;
|
|
163
|
+
if (margin)
|
|
164
|
+
value.margin = margin;
|
|
165
|
+
const attrs = {
|
|
166
|
+
[SPACING_PRIMARY_ATTR_NAME]: {
|
|
167
|
+
decoration: {
|
|
168
|
+
spacing: {
|
|
169
|
+
desktop: {
|
|
170
|
+
value,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return { attrs, value };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Emit a canonical `divi/spacing` section group preset.
|
|
180
|
+
*
|
|
181
|
+
* 1. Validate input shape (name, module, at least one corner across either
|
|
182
|
+
* axis).
|
|
183
|
+
* 2. Reject variable-token values in any length flag (deferred until a
|
|
184
|
+
* canonical capture lands).
|
|
185
|
+
* 3. Compose sparse-emit `attrs.module.decoration.spacing.desktop.value.*`
|
|
186
|
+
* with paired sync flags.
|
|
187
|
+
* 4. Gate `(divi/spacing, <module>)` against the verified-attrs registry —
|
|
188
|
+
* throws `EvidenceGateError` when effective evidence is below
|
|
189
|
+
* `VB_PRESET_STORAGE_VERIFIED`.
|
|
190
|
+
*
|
|
191
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this entry:
|
|
192
|
+
* the plugin's `/preset/create` route mirrors the single `attrs` bag into
|
|
193
|
+
* all three buckets at write time. The Track 7a fixture captures the
|
|
194
|
+
* post-write storage shape (which is why both `attrs` and `styleAttrs`
|
|
195
|
+
* appear there byte-identical); the CLI emits the request shape only.
|
|
196
|
+
*/
|
|
197
|
+
export function emitSpacingGroupPreset(input, registry = loadRegistry()) {
|
|
198
|
+
if (!input.name || typeof input.name !== "string") {
|
|
199
|
+
throw new Error("spacing emitter requires a non-empty `name`.");
|
|
200
|
+
}
|
|
201
|
+
if (!input.module || typeof input.module !== "string") {
|
|
202
|
+
throw new Error("spacing emitter requires `module` — currently only `divi/section` is " +
|
|
203
|
+
"wired (other module cells are SCHEMA_OBSERVED in the registry; " +
|
|
204
|
+
"promoting them requires a Track-7a-style canonical capture PR + a " +
|
|
205
|
+
"follow-up implementation/docs PR).");
|
|
206
|
+
}
|
|
207
|
+
const { attrs, value } = composeSpacingAttrs(input);
|
|
208
|
+
// Empty-input rejection: at least one corner across either axis MUST be
|
|
209
|
+
// specified. An empty value bag is a usage error — there is nothing to
|
|
210
|
+
// write, and the resulting preset would be the empty-shell VB authoring
|
|
211
|
+
// footgun documented in the Track 7a capture.
|
|
212
|
+
if (Object.keys(value).length === 0) {
|
|
213
|
+
throw new Error("spacing emitter produced an empty preset — pass at least one " +
|
|
214
|
+
"padding or margin corner (--padding-top, --margin-bottom, etc.).");
|
|
215
|
+
}
|
|
216
|
+
// Registry gate: dispatches off `(divi/spacing, <module>)`. Cells below
|
|
217
|
+
// VB_PRESET_STORAGE_VERIFIED (currently divi/heading, divi/text,
|
|
218
|
+
// divi/button — all SCHEMA_OBSERVED) throw EvidenceGateError natively
|
|
219
|
+
// here. Unknown modules with no applicability cell resolve to
|
|
220
|
+
// UNVERIFIED (0) and also throw.
|
|
221
|
+
gateWriteAttr(registry, input.module, SPACING_PATTERN_FAMILY);
|
|
222
|
+
// Implementation-supported-modules guard: even if a future registry-only
|
|
223
|
+
// PR promotes another cell to VB_PRESET_STORAGE_VERIFIED, this emitter
|
|
224
|
+
// must NOT silently start emitting it — heading/text/button cells need
|
|
225
|
+
// their own per-module wrapper (the section cell uses `module.*`, but a
|
|
226
|
+
// heading-spacing capture might surface `title.*` or a different
|
|
227
|
+
// primaryAttrName). Per `feedback_preset_map_per_module`, never assume
|
|
228
|
+
// cross-module shape carry-over. Promoting another module requires
|
|
229
|
+
// adding it to `SPACING_SUPPORTED_MODULES` alongside the fixture +
|
|
230
|
+
// tests + README updates that prove the per-module shape.
|
|
231
|
+
if (!SPACING_SUPPORTED_MODULES.includes(input.module)) {
|
|
232
|
+
throw new Error(`spacing emitter does not yet implement module "${input.module}". ` +
|
|
233
|
+
`Registry evidence cleared the cell, but the per-module wrapper + ` +
|
|
234
|
+
`attr shape have not been verified against a canonical capture ` +
|
|
235
|
+
`(this emitter hard-codes the divi/section wrapper "module" and ` +
|
|
236
|
+
`primaryAttrName "module"). Supported modules: ` +
|
|
237
|
+
`${SPACING_SUPPORTED_MODULES.join(", ")}. Adding a new module ` +
|
|
238
|
+
`requires a Track-7a-style canonical capture PR landing first AND ` +
|
|
239
|
+
`a follow-up implementation PR extending this constant alongside ` +
|
|
240
|
+
`fixtures + tests + README updates.`);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
type: "group",
|
|
244
|
+
module_name: input.module,
|
|
245
|
+
group_name: SPACING_GROUP_NAME,
|
|
246
|
+
group_id: SPACING_GROUP_ID,
|
|
247
|
+
primary_attr_name: SPACING_PRIMARY_ATTR_NAME,
|
|
248
|
+
name: input.name,
|
|
249
|
+
attrs,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Build the `POST /diviops/v1/preset/create` request body from a spacing
|
|
254
|
+
* preset entry. Matches the body shape the `diviops_preset_create` MCP
|
|
255
|
+
* tool posts — the CLI reuses the existing route, it does not add one.
|
|
256
|
+
*
|
|
257
|
+
* `primary_attr_name` IS sent on the wire (the plugin's `/preset/create`
|
|
258
|
+
* route accepts it as an optional snake_case param and stores it as
|
|
259
|
+
* `primaryAttrName` in the preset). Tracks 4/5/6 emitters omit it because
|
|
260
|
+
* their preset types do not carry it; the divi/spacing cell does, per the
|
|
261
|
+
* Track 7a capture's load-bearing finding #2.
|
|
262
|
+
*/
|
|
263
|
+
export function buildSpacingPresetCreateBody(entry, opts = {}) {
|
|
264
|
+
const body = {
|
|
265
|
+
module_name: entry.module_name,
|
|
266
|
+
name: entry.name,
|
|
267
|
+
attrs: entry.attrs,
|
|
268
|
+
type: entry.type,
|
|
269
|
+
group_name: entry.group_name,
|
|
270
|
+
group_id: entry.group_id,
|
|
271
|
+
primary_attr_name: entry.primary_attr_name,
|
|
272
|
+
};
|
|
273
|
+
if (opts.dry_run)
|
|
274
|
+
body.dry_run = true;
|
|
275
|
+
return body;
|
|
276
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` body-text group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font-body` preset
|
|
5
|
+
* targeting `divi/text` at
|
|
6
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
|
|
9
|
+
* (`divi/font-body` section), cross-checked against
|
|
10
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-2-body-text-pattern-a.json`.
|
|
11
|
+
*
|
|
12
|
+
* Scope: Pattern A (Google Fonts) ONLY. Pattern B (local-hosted) has NO
|
|
13
|
+
* registry entry for `divi/font-body` — the heading-vs-body shape
|
|
14
|
+
* divergence question deferred per round-2 _meta. Selecting
|
|
15
|
+
* `pattern: "local"` therefore resolves to a missing-variant registry-
|
|
16
|
+
* absence error from `resolveEvidence` (the standard
|
|
17
|
+
* `findPatternEntry → undefined → throw` shape). No special-cased
|
|
18
|
+
* Pattern-B branch lives in this file — the registry IS the gate.
|
|
19
|
+
*
|
|
20
|
+
* Shape rules enforced here:
|
|
21
|
+
* - Emit-on-specification only — omitted params produce NO keys.
|
|
22
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
23
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
24
|
+
* VB save semantics); the CLI request body only carries `attrs`.
|
|
25
|
+
* - Wrapper chain: `content.decoration.bodyFont.body.font.desktop.value.*` —
|
|
26
|
+
* note the per-bucket divergence vs `divi/font` (which uses
|
|
27
|
+
* `title.decoration.font.font.desktop.value.*`). Sibling buckets do NOT
|
|
28
|
+
* share canonical keys (per `feedback_preset_map_per_module`).
|
|
29
|
+
* - Link color (`bodyFont.link.font.desktop.value.color`) is OUT OF SCOPE
|
|
30
|
+
* for this emitter — no `--link-color` option, no link emission. Needs
|
|
31
|
+
* a dedicated capture-backed follow-up before the CLI writes it.
|
|
32
|
+
* - Body-text emission on modules OTHER than `divi/text` is OUT OF SCOPE
|
|
33
|
+
* — Testimonial / Accordion-item / Slide / Blurb cells are
|
|
34
|
+
* `SCHEMA_OBSERVED` in the registry today (under the write threshold)
|
|
35
|
+
* and the gate will refuse them.
|
|
36
|
+
*/
|
|
37
|
+
import { type VerifiedAttrsRegistry } from "./registry.js";
|
|
38
|
+
export declare const TEXT_BODY_FONT_MODULE = "divi/text";
|
|
39
|
+
export declare const TEXT_BODY_FONT_GROUP_NAME = "divi/font-body";
|
|
40
|
+
export declare const TEXT_BODY_FONT_GROUP_ID = "designText";
|
|
41
|
+
export declare const TEXT_BODY_FONT_PATTERN_FAMILY = "divi/font-body";
|
|
42
|
+
/**
|
|
43
|
+
* The verified pattern variants the CLI honors for `divi/font-body`.
|
|
44
|
+
*
|
|
45
|
+
* Only Pattern A is supported. Pattern B is included in the type so the
|
|
46
|
+
* CLI surface mirrors `heading-font` (and so `--pattern local` parses
|
|
47
|
+
* cleanly enough to reach the registry-gate refusal), but its registry
|
|
48
|
+
* entry is intentionally absent — `resolveEvidence` will throw a
|
|
49
|
+
* registry-absence error when the local variant is selected. There is NO
|
|
50
|
+
* shape-level Pattern B encoding here (no weight-in-family heuristic, no
|
|
51
|
+
* `weight`-refusal branch beyond what the missing-variant gate produces).
|
|
52
|
+
*/
|
|
53
|
+
export declare const TEXT_BODY_FONT_PATTERN_VARIANTS: {
|
|
54
|
+
readonly google: "google_fonts_pattern_a";
|
|
55
|
+
readonly local: "local_hosted_pattern_b";
|
|
56
|
+
};
|
|
57
|
+
export type TextBodyFontPattern = "google" | "local";
|
|
58
|
+
export interface TextBodyFontEmitterInput {
|
|
59
|
+
/** Required display name for the preset. */
|
|
60
|
+
name: string;
|
|
61
|
+
/**
|
|
62
|
+
* Required pattern selector. There is no default. Only Pattern A is
|
|
63
|
+
* supported; passing `"local"` resolves to a registry-absence refusal
|
|
64
|
+
* (no Pattern B entry exists for `divi/font-body` today).
|
|
65
|
+
*/
|
|
66
|
+
pattern: TextBodyFontPattern;
|
|
67
|
+
/** Font family (Pattern A: plain family name, e.g. `"Inter"`). */
|
|
68
|
+
family?: string;
|
|
69
|
+
/** Font weight (numeric-string, e.g. `"400"`); emit-on-specification. */
|
|
70
|
+
weight?: string;
|
|
71
|
+
/** Font color — literal hex or bare/formed variable token. */
|
|
72
|
+
color?: string;
|
|
73
|
+
/** Font size — literal (e.g. `"16px"`) or already-formed `$variable(...)$`. */
|
|
74
|
+
size?: string;
|
|
75
|
+
/** Line height — optional, emit-on-specification only. */
|
|
76
|
+
lineHeight?: string;
|
|
77
|
+
}
|
|
78
|
+
/** The composed canonical preset entry shape sent to `/preset/create`. */
|
|
79
|
+
export interface TextBodyFontPresetEntry {
|
|
80
|
+
type: "group";
|
|
81
|
+
module_name: string;
|
|
82
|
+
group_name: string;
|
|
83
|
+
group_id: string;
|
|
84
|
+
pattern_variant: string;
|
|
85
|
+
name: string;
|
|
86
|
+
attrs: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compose the canonical
|
|
90
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value` bag from the
|
|
91
|
+
* input. Emit-on-specification: only specified sub-fields produce keys.
|
|
92
|
+
*/
|
|
93
|
+
export declare function composeTextBodyFontAttrs(input: TextBodyFontEmitterInput): Record<string, unknown>;
|
|
94
|
+
/**
|
|
95
|
+
* Emit a canonical `divi/font-body` body-text group preset for `divi/text`.
|
|
96
|
+
*
|
|
97
|
+
* 1. Validate input shape (name, pattern).
|
|
98
|
+
* 2. Compose `attrs.content.decoration.bodyFont.body.font.desktop.value.*`
|
|
99
|
+
* (emit-on-specification).
|
|
100
|
+
* 3. Gate the chosen `(divi/font-body, pattern_variant)` cell on
|
|
101
|
+
* `divi/text` against the verified-attrs registry — throws an Error
|
|
102
|
+
* (registry-absence) when the variant is missing (Pattern B is
|
|
103
|
+
* missing by design), or `EvidenceGateError` when effective evidence
|
|
104
|
+
* is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
105
|
+
*
|
|
106
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
107
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
108
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
109
|
+
* to match VB save semantics. The Round 2 fixture captures the post-write
|
|
110
|
+
* storage shape (note: round-2 _meta records `renderAttrs_present: false`
|
|
111
|
+
* for font-styling group presets, which is consistent with rounds 1a/1b —
|
|
112
|
+
* the mirror happens at the route layer regardless).
|
|
113
|
+
*/
|
|
114
|
+
export declare function emitTextBodyFontGroupPreset(input: TextBodyFontEmitterInput, registry?: VerifiedAttrsRegistry): TextBodyFontPresetEntry;
|
|
115
|
+
/**
|
|
116
|
+
* Build the `POST /diviops/v1/preset/create` request body from a
|
|
117
|
+
* text-body-font preset entry. Matches the body shape the
|
|
118
|
+
* `diviops_preset_create` MCP tool posts — the CLI reuses the existing
|
|
119
|
+
* route, it does not add one.
|
|
120
|
+
*
|
|
121
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
122
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
123
|
+
* client-side before the write.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildTextBodyFontPresetCreateBody(entry: TextBodyFontPresetEntry, opts?: {
|
|
126
|
+
dry_run?: boolean;
|
|
127
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` body-text group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font-body` preset
|
|
5
|
+
* targeting `divi/text` at
|
|
6
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
|
|
9
|
+
* (`divi/font-body` section), cross-checked against
|
|
10
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-2-body-text-pattern-a.json`.
|
|
11
|
+
*
|
|
12
|
+
* Scope: Pattern A (Google Fonts) ONLY. Pattern B (local-hosted) has NO
|
|
13
|
+
* registry entry for `divi/font-body` — the heading-vs-body shape
|
|
14
|
+
* divergence question deferred per round-2 _meta. Selecting
|
|
15
|
+
* `pattern: "local"` therefore resolves to a missing-variant registry-
|
|
16
|
+
* absence error from `resolveEvidence` (the standard
|
|
17
|
+
* `findPatternEntry → undefined → throw` shape). No special-cased
|
|
18
|
+
* Pattern-B branch lives in this file — the registry IS the gate.
|
|
19
|
+
*
|
|
20
|
+
* Shape rules enforced here:
|
|
21
|
+
* - Emit-on-specification only — omitted params produce NO keys.
|
|
22
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
23
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
24
|
+
* VB save semantics); the CLI request body only carries `attrs`.
|
|
25
|
+
* - Wrapper chain: `content.decoration.bodyFont.body.font.desktop.value.*` —
|
|
26
|
+
* note the per-bucket divergence vs `divi/font` (which uses
|
|
27
|
+
* `title.decoration.font.font.desktop.value.*`). Sibling buckets do NOT
|
|
28
|
+
* share canonical keys (per `feedback_preset_map_per_module`).
|
|
29
|
+
* - Link color (`bodyFont.link.font.desktop.value.color`) is OUT OF SCOPE
|
|
30
|
+
* for this emitter — no `--link-color` option, no link emission. Needs
|
|
31
|
+
* a dedicated capture-backed follow-up before the CLI writes it.
|
|
32
|
+
* - Body-text emission on modules OTHER than `divi/text` is OUT OF SCOPE
|
|
33
|
+
* — Testimonial / Accordion-item / Slide / Blurb cells are
|
|
34
|
+
* `SCHEMA_OBSERVED` in the registry today (under the write threshold)
|
|
35
|
+
* and the gate will refuse them.
|
|
36
|
+
*/
|
|
37
|
+
import { loadRegistry, gateWriteAttr, } from "./registry.js";
|
|
38
|
+
import { normalizeColorValue } from "./variable-token.js";
|
|
39
|
+
export const TEXT_BODY_FONT_MODULE = "divi/text";
|
|
40
|
+
export const TEXT_BODY_FONT_GROUP_NAME = "divi/font-body";
|
|
41
|
+
export const TEXT_BODY_FONT_GROUP_ID = "designText";
|
|
42
|
+
export const TEXT_BODY_FONT_PATTERN_FAMILY = "divi/font-body";
|
|
43
|
+
/**
|
|
44
|
+
* The verified pattern variants the CLI honors for `divi/font-body`.
|
|
45
|
+
*
|
|
46
|
+
* Only Pattern A is supported. Pattern B is included in the type so the
|
|
47
|
+
* CLI surface mirrors `heading-font` (and so `--pattern local` parses
|
|
48
|
+
* cleanly enough to reach the registry-gate refusal), but its registry
|
|
49
|
+
* entry is intentionally absent — `resolveEvidence` will throw a
|
|
50
|
+
* registry-absence error when the local variant is selected. There is NO
|
|
51
|
+
* shape-level Pattern B encoding here (no weight-in-family heuristic, no
|
|
52
|
+
* `weight`-refusal branch beyond what the missing-variant gate produces).
|
|
53
|
+
*/
|
|
54
|
+
export const TEXT_BODY_FONT_PATTERN_VARIANTS = {
|
|
55
|
+
google: "google_fonts_pattern_a",
|
|
56
|
+
local: "local_hosted_pattern_b",
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Compose the canonical
|
|
60
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value` bag from the
|
|
61
|
+
* input. Emit-on-specification: only specified sub-fields produce keys.
|
|
62
|
+
*/
|
|
63
|
+
export function composeTextBodyFontAttrs(input) {
|
|
64
|
+
const value = {};
|
|
65
|
+
if (input.family !== undefined)
|
|
66
|
+
value.family = input.family;
|
|
67
|
+
if (input.weight !== undefined)
|
|
68
|
+
value.weight = input.weight;
|
|
69
|
+
if (input.color !== undefined)
|
|
70
|
+
value.color = normalizeColorValue(input.color);
|
|
71
|
+
if (input.size !== undefined)
|
|
72
|
+
value.size = input.size;
|
|
73
|
+
if (input.lineHeight !== undefined)
|
|
74
|
+
value.lineHeight = input.lineHeight;
|
|
75
|
+
return {
|
|
76
|
+
content: {
|
|
77
|
+
decoration: {
|
|
78
|
+
bodyFont: {
|
|
79
|
+
body: {
|
|
80
|
+
font: {
|
|
81
|
+
desktop: {
|
|
82
|
+
value,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Emit a canonical `divi/font-body` body-text group preset for `divi/text`.
|
|
93
|
+
*
|
|
94
|
+
* 1. Validate input shape (name, pattern).
|
|
95
|
+
* 2. Compose `attrs.content.decoration.bodyFont.body.font.desktop.value.*`
|
|
96
|
+
* (emit-on-specification).
|
|
97
|
+
* 3. Gate the chosen `(divi/font-body, pattern_variant)` cell on
|
|
98
|
+
* `divi/text` against the verified-attrs registry — throws an Error
|
|
99
|
+
* (registry-absence) when the variant is missing (Pattern B is
|
|
100
|
+
* missing by design), or `EvidenceGateError` when effective evidence
|
|
101
|
+
* is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
102
|
+
*
|
|
103
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
104
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
105
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
106
|
+
* to match VB save semantics. The Round 2 fixture captures the post-write
|
|
107
|
+
* storage shape (note: round-2 _meta records `renderAttrs_present: false`
|
|
108
|
+
* for font-styling group presets, which is consistent with rounds 1a/1b —
|
|
109
|
+
* the mirror happens at the route layer regardless).
|
|
110
|
+
*/
|
|
111
|
+
export function emitTextBodyFontGroupPreset(input, registry = loadRegistry()) {
|
|
112
|
+
if (!input.name || typeof input.name !== "string") {
|
|
113
|
+
throw new Error("text-body-font emitter requires a non-empty `name`.");
|
|
114
|
+
}
|
|
115
|
+
if (input.pattern !== "google" && input.pattern !== "local") {
|
|
116
|
+
throw new Error(`text-body-font emitter requires \`pattern\` to be "google" or "local"; got ${JSON.stringify(input.pattern)}. ` +
|
|
117
|
+
`There is no default — Pattern A (google) is the only registry-verified variant for ` +
|
|
118
|
+
`\`divi/font-body\` today; Pattern B (local) resolves to a registry-absence refusal.`);
|
|
119
|
+
}
|
|
120
|
+
const attrs = composeTextBodyFontAttrs(input);
|
|
121
|
+
// Sanity check: at least one styling field was specified. An empty
|
|
122
|
+
// value bag is a usage error — there is nothing to write.
|
|
123
|
+
const value = attrs.content.decoration.bodyFont.body.font.desktop
|
|
124
|
+
.value;
|
|
125
|
+
if (!value || Object.keys(value).length === 0) {
|
|
126
|
+
throw new Error("text-body-font emitter produced an empty preset — pass at least one of " +
|
|
127
|
+
"family, weight, color, size, or lineHeight.");
|
|
128
|
+
}
|
|
129
|
+
// Registry gate: variant-aware. Pattern A evidence must NOT vouch for
|
|
130
|
+
// Pattern B and vice versa — gateWriteAttr resolves by both family AND
|
|
131
|
+
// variant, so a missing/under-verified variant entry throws here.
|
|
132
|
+
// Pattern B has NO registry entry for `divi/font-body`, so
|
|
133
|
+
// `--pattern local` lands on `resolveEvidence`'s "absent from
|
|
134
|
+
// verified-attrs.json" throw natively — no special-cased branch needed.
|
|
135
|
+
const patternVariant = TEXT_BODY_FONT_PATTERN_VARIANTS[input.pattern];
|
|
136
|
+
gateWriteAttr(registry, TEXT_BODY_FONT_MODULE, TEXT_BODY_FONT_PATTERN_FAMILY, patternVariant);
|
|
137
|
+
return {
|
|
138
|
+
type: "group",
|
|
139
|
+
module_name: TEXT_BODY_FONT_MODULE,
|
|
140
|
+
group_name: TEXT_BODY_FONT_GROUP_NAME,
|
|
141
|
+
group_id: TEXT_BODY_FONT_GROUP_ID,
|
|
142
|
+
pattern_variant: patternVariant,
|
|
143
|
+
name: input.name,
|
|
144
|
+
attrs,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build the `POST /diviops/v1/preset/create` request body from a
|
|
149
|
+
* text-body-font preset entry. Matches the body shape the
|
|
150
|
+
* `diviops_preset_create` MCP tool posts — the CLI reuses the existing
|
|
151
|
+
* route, it does not add one.
|
|
152
|
+
*
|
|
153
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
154
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
155
|
+
* client-side before the write.
|
|
156
|
+
*/
|
|
157
|
+
export function buildTextBodyFontPresetCreateBody(entry, opts = {}) {
|
|
158
|
+
const body = {
|
|
159
|
+
module_name: entry.module_name,
|
|
160
|
+
name: entry.name,
|
|
161
|
+
attrs: entry.attrs,
|
|
162
|
+
type: entry.type,
|
|
163
|
+
group_name: entry.group_name,
|
|
164
|
+
group_id: entry.group_id,
|
|
165
|
+
};
|
|
166
|
+
if (opts.dry_run)
|
|
167
|
+
body.dry_run = true;
|
|
168
|
+
return body;
|
|
169
|
+
}
|
|
@@ -19,6 +19,8 @@ import type { HandshakeResult } from "../compatibility.js";
|
|
|
19
19
|
import type { DiviopsResponse } from "../envelope.js";
|
|
20
20
|
import type { ButtonPresetEntry } from "./button-emitter.js";
|
|
21
21
|
import type { HeadingFontPresetEntry } from "./heading-font-emitter.js";
|
|
22
|
+
import type { TextBodyFontPresetEntry } from "./text-body-font-emitter.js";
|
|
23
|
+
import type { SpacingPresetEntry } from "./spacing-emitter.js";
|
|
22
24
|
/** The plugin capability the storage-path capability contract ships. */
|
|
23
25
|
export declare const STORAGE_CAPABILITY = "storage_multipath_probe_v1";
|
|
24
26
|
/** The REST route the CLI posts to — same route `diviops_preset_create` uses. */
|
|
@@ -72,3 +74,33 @@ export declare function applyHeadingFontPreset(client: PresetWriteClient, entry:
|
|
|
72
74
|
serverVersion: string;
|
|
73
75
|
dry_run?: boolean;
|
|
74
76
|
}): Promise<DiviopsResponse<unknown>>;
|
|
77
|
+
/**
|
|
78
|
+
* Apply a `divi/font-body` body-text group preset for `divi/text`:
|
|
79
|
+
* capability-gate, then POST to `/preset/create`. Mirrors
|
|
80
|
+
* `applyHeadingFontPreset`'s sequence — the capability check runs BEFORE
|
|
81
|
+
* the write, and the write reuses the existing storage-routed route (no
|
|
82
|
+
* plugin route is added).
|
|
83
|
+
*
|
|
84
|
+
* The `pattern_variant` metadata is intentionally NOT in the wire body —
|
|
85
|
+
* variant selection is a client-side registry-gate decision and the
|
|
86
|
+
* server route accepts only the standard preset-create fields.
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyTextBodyFontPreset(client: PresetWriteClient, entry: TextBodyFontPresetEntry, opts: {
|
|
89
|
+
serverVersion: string;
|
|
90
|
+
dry_run?: boolean;
|
|
91
|
+
}): Promise<DiviopsResponse<unknown>>;
|
|
92
|
+
/**
|
|
93
|
+
* Apply a `divi/spacing` section group preset: capability-gate, then POST
|
|
94
|
+
* to `/preset/create`. Mirrors `applyTextBodyFontPreset`'s sequence — the
|
|
95
|
+
* capability check runs BEFORE the write, and the write reuses the
|
|
96
|
+
* existing storage-routed route (no plugin route is added).
|
|
97
|
+
*
|
|
98
|
+
* Unlike the font emitters, the spacing entry carries `primary_attr_name`
|
|
99
|
+
* (`"module"` for the section cell per Track 7a), which IS sent on the
|
|
100
|
+
* wire — the `/preset/create` route accepts it as an optional snake_case
|
|
101
|
+
* param and stores it as `primaryAttrName` in the preset.
|
|
102
|
+
*/
|
|
103
|
+
export declare function applySpacingPreset(client: PresetWriteClient, entry: SpacingPresetEntry, opts: {
|
|
104
|
+
serverVersion: string;
|
|
105
|
+
dry_run?: boolean;
|
|
106
|
+
}): Promise<DiviopsResponse<unknown>>;
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import { WPClient } from "../wp-client.js";
|
|
18
18
|
import { buildPresetCreateBody } from "./button-emitter.js";
|
|
19
19
|
import { buildHeadingFontPresetCreateBody } from "./heading-font-emitter.js";
|
|
20
|
+
import { buildTextBodyFontPresetCreateBody } from "./text-body-font-emitter.js";
|
|
21
|
+
import { buildSpacingPresetCreateBody } from "./spacing-emitter.js";
|
|
20
22
|
/** The plugin capability the storage-path capability contract ships. */
|
|
21
23
|
export const STORAGE_CAPABILITY = "storage_multipath_probe_v1";
|
|
22
24
|
/** The REST route the CLI posts to — same route `diviops_preset_create` uses. */
|
|
@@ -108,3 +110,45 @@ export async function applyHeadingFontPreset(client, entry, opts) {
|
|
|
108
110
|
body,
|
|
109
111
|
});
|
|
110
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Apply a `divi/font-body` body-text group preset for `divi/text`:
|
|
115
|
+
* capability-gate, then POST to `/preset/create`. Mirrors
|
|
116
|
+
* `applyHeadingFontPreset`'s sequence — the capability check runs BEFORE
|
|
117
|
+
* the write, and the write reuses the existing storage-routed route (no
|
|
118
|
+
* plugin route is added).
|
|
119
|
+
*
|
|
120
|
+
* The `pattern_variant` metadata is intentionally NOT in the wire body —
|
|
121
|
+
* variant selection is a client-side registry-gate decision and the
|
|
122
|
+
* server route accepts only the standard preset-create fields.
|
|
123
|
+
*/
|
|
124
|
+
export async function applyTextBodyFontPreset(client, entry, opts) {
|
|
125
|
+
await assertStorageCapability(client, opts.serverVersion);
|
|
126
|
+
const body = buildTextBodyFontPresetCreateBody(entry, {
|
|
127
|
+
dry_run: opts.dry_run,
|
|
128
|
+
});
|
|
129
|
+
return client.requestEnveloped(PRESET_CREATE_ROUTE, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Apply a `divi/spacing` section group preset: capability-gate, then POST
|
|
136
|
+
* to `/preset/create`. Mirrors `applyTextBodyFontPreset`'s sequence — the
|
|
137
|
+
* capability check runs BEFORE the write, and the write reuses the
|
|
138
|
+
* existing storage-routed route (no plugin route is added).
|
|
139
|
+
*
|
|
140
|
+
* Unlike the font emitters, the spacing entry carries `primary_attr_name`
|
|
141
|
+
* (`"module"` for the section cell per Track 7a), which IS sent on the
|
|
142
|
+
* wire — the `/preset/create` route accepts it as an optional snake_case
|
|
143
|
+
* param and stores it as `primaryAttrName` in the preset.
|
|
144
|
+
*/
|
|
145
|
+
export async function applySpacingPreset(client, entry, opts) {
|
|
146
|
+
await assertStorageCapability(client, opts.serverVersion);
|
|
147
|
+
const body = buildSpacingPresetCreateBody(entry, {
|
|
148
|
+
dry_run: opts.dry_run,
|
|
149
|
+
});
|
|
150
|
+
return client.requestEnveloped(PRESET_CREATE_ROUTE, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body,
|
|
153
|
+
});
|
|
154
|
+
}
|