@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
package/README.md
CHANGED
|
@@ -91,6 +91,8 @@ run it directly. Current commands:
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `diviops-preset button [options]` | `divi/button` group preset |
|
|
93
93
|
| `diviops-preset heading-font [options]` | `divi/font` group preset for `divi/heading` (Pattern A — Google Fonts — or Pattern B — local-hosted) |
|
|
94
|
+
| `diviops-preset text-body-font [options]` | `divi/font-body` group preset for `divi/text` — **Pattern A (Google Fonts) only**; Pattern B for body-text has no registered canonical shape and is refused |
|
|
95
|
+
| `diviops-preset spacing [options]` | `divi/spacing` group preset (currently `divi/section` only; padding + margin, desktop state). Other module cells are `SCHEMA_OBSERVED` and refused at the gate |
|
|
94
96
|
|
|
95
97
|
```bash
|
|
96
98
|
diviops-preset button --name "Primary" --bg-color gcid-primary-color \
|
|
@@ -100,6 +102,13 @@ diviops-preset button --name "Primary" --bg-color gcid-primary-color \
|
|
|
100
102
|
diviops-preset heading-font --name "Heading H1" --pattern google \
|
|
101
103
|
--font-family Inter --font-weight 700 \
|
|
102
104
|
--font-color gcid-heading-color --font-size 48px
|
|
105
|
+
|
|
106
|
+
diviops-preset text-body-font --name "Body Text" --pattern google \
|
|
107
|
+
--font-family Inter --font-weight 400 \
|
|
108
|
+
--font-color gcid-body-color --font-size 16px
|
|
109
|
+
|
|
110
|
+
diviops-preset spacing --name "Section Rhythm" --module divi/section \
|
|
111
|
+
--padding-top 80px --padding-bottom 80px --margin-bottom 40px
|
|
103
112
|
```
|
|
104
113
|
|
|
105
114
|
`--dry-run` (the default) composes and prints the canonical JSON with no
|
|
@@ -85,14 +85,14 @@
|
|
|
85
85
|
"priority": 2,
|
|
86
86
|
"pattern_family": "divi/spacing",
|
|
87
87
|
"current_effective_evidence": "SCHEMA_OBSERVED",
|
|
88
|
-
"blocker": "
|
|
88
|
+
"blocker": "Track 7a (2026-05-23, addresses #747) captured divi/spacing group-preset bound to divi/section on Divi 5.6.0, with both padding and margin axes confirmed sparse-emit + always-paired-sync-flags shape. Pattern level + divi/section cell promoted to VB_PRESET_STORAGE_VERIFIED. Heading/text/button cells still need their own roundtrips on divi5-clean.local (canonical reference substrate) — each module's per-wrapper spacing cell may have its own shape quirks per feedback_preset_map_per_module, AND the group_id value (currently schema-inferred as 'title.decoration.spacing' / 'content.decoration.spacing' / 'module.decoration.spacing') is now KNOWN-MAYBE-WRONG since the section cell uses groupId='designSpacing' (Composable Settings panel id, not dotted attr path); per-cell capture must verify whether each cell uses the same value or a wrapper-prefixed variant.",
|
|
89
89
|
"capture_targets": [
|
|
90
|
-
"divi/spacing group preset bound to
|
|
91
|
-
"divi/spacing group preset bound to
|
|
92
|
-
"divi/spacing group preset bound to
|
|
90
|
+
"divi/spacing group preset bound to title wrapper on divi/heading (proper capture on divi5-clean.local; do NOT use divi5-ai sandbox preset ecs423stpr or yjh59i25br as evidence — they are unverified residue from prior exploration)",
|
|
91
|
+
"divi/spacing group preset bound to content wrapper on divi/text (proper capture on divi5-clean.local)",
|
|
92
|
+
"divi/spacing group preset bound to module wrapper on divi/button (Button exception path — Button uses module.decoration.spacing per Button exception, not button.decoration.spacing which is VB-hidden)"
|
|
93
93
|
],
|
|
94
94
|
"value_to_authoring": "very high (Composable Settings spacing presets are the entire preset-driven authoring story for spacing)",
|
|
95
|
-
"notes": "
|
|
95
|
+
"notes": "Pattern-level already promoted via Track 7a divi/section capture. Remaining captures lift only their per-module cells (no further pattern-level promotion needed). Each capture must independently verify groupId — do NOT assume 'designSpacing' propagates from the section cell. Note: divi5-ai sandbox has three pre-existing divi/spacing presets (g4zpdl2v6g, ecs423stpr, yjh59i25br) that visually carry sparse-emit shape including title-wrapper variants, but these are unverified residue; treat as 'question is approachable' only, not as evidence."
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
"priority": 2,
|
package/data/verified-attrs.json
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"pattern_evidence_source": "docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-4-section.json",
|
|
78
78
|
"pattern_divi_version": "5.5.2",
|
|
79
79
|
"pattern_verified_at": "2026-05-18",
|
|
80
|
-
"canonical_shape_note": "module.decoration.spacing.desktop.value.padding
|
|
80
|
+
"canonical_shape_note": "module.decoration.spacing.desktop.value.padding (and .margin) is sparse-emit: only user-touched corners (top, right, bottom, left) appear in attrs, plus syncVertical + syncHorizontal which BOTH always emit on any spacing touch (atomic sibling pair — both engage together even when only one direction was touched). Sync flag values are 'off' by default, 'on' when user explicitly enables sync. Round-4 section main capture (top+bottom touched) stored {top, bottom, syncVertical, syncHorizontal} — no left/right. Round-4 discriminator (top+bottom only, sync explicitly NOT touched) confirms sync flags engage atomically while corners do not. Phase 2 emitter rule: emit only the specified corners + always emit both sync flags. Atomicity claim revised 2026-05-23 per Track 7a (#747) group-bucket capture which corroborates the same sparse-emit + always-paired-sync shape on the group-preset path (evidence: docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json).",
|
|
81
81
|
"applicability": {
|
|
82
82
|
"divi/section": {
|
|
83
83
|
"wrapper": "module",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"source": "docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-4-section.json",
|
|
89
89
|
"preset_map_key": "module.decoration.spacing__padding",
|
|
90
90
|
"state": "desktop.value",
|
|
91
|
-
"caveats": ["Padding
|
|
91
|
+
"caveats": ["Padding (and margin) is sparse-emit: corners appear ONLY when user touched them; syncVertical + syncHorizontal always emit on any spacing touch (atomic sibling pair). Phase 2 emitter rule: emit only the specified corners + always emit both sync flags. Default sync values are 'off'; 'on' when user explicitly enables sync. Atomicity claim revised 2026-05-23 per Track 7a evidence."]
|
|
92
92
|
},
|
|
93
93
|
"divi/heading": {
|
|
94
94
|
"wrapper": "module",
|
|
@@ -495,11 +495,11 @@
|
|
|
495
495
|
},
|
|
496
496
|
{
|
|
497
497
|
"pattern_family": "divi/spacing",
|
|
498
|
-
"pattern_evidence_level": "
|
|
499
|
-
"pattern_evidence_source": "
|
|
500
|
-
"pattern_divi_version": "5.
|
|
501
|
-
"pattern_verified_at": "2026-05-
|
|
502
|
-
"canonical_shape_note": "Composable Settings spacing bucket
|
|
498
|
+
"pattern_evidence_level": "VB_PRESET_STORAGE_VERIFIED",
|
|
499
|
+
"pattern_evidence_source": "docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json",
|
|
500
|
+
"pattern_divi_version": "5.6.0",
|
|
501
|
+
"pattern_verified_at": "2026-05-23",
|
|
502
|
+
"canonical_shape_note": "Composable Settings spacing bucket (group preset). VB-verified on Divi 5.6.0 via Track 7a (divi/section cell only): groupId is the Composable Settings panel id 'designSpacing' (NOT a dotted attribute path — prior 'module.decoration.spacing' note was a misread). primaryAttrName is 'module' for section spacing; registry schema does not carry primaryAttrName today, recorded in the divi/section cell caveats instead. Attrs root: attrs.<primaryAttrName>.decoration.spacing.desktop.value.{padding|margin}, where both padding and margin follow the IDENTICAL sparse-emit rule: only user-touched corners (top, right, bottom, left) appear, with syncVertical + syncHorizontal always emitted together on any spacing touch (atomic sibling pair; 'off' by default, 'on' when user explicitly enables sync). Margin capture (kxc3elho8v on divi5-ai sandbox) corroborates the same shape rule with single-corner touch on the margin axis. Round-trip render-side verification (divi/section block referencing an MCP-created preset on divi5-ai page 1520): clean — empty corners stay empty in VB, no phantom defaults. Inner-element variants (title, content wrappers) and non-section module cells (heading, text, button) are NOT verified — they retain schema-inferred group_id values with caveats; do NOT extrapolate this section evidence to those cells per feedback_preset_map_per_module.",
|
|
503
503
|
"applicability": {
|
|
504
504
|
"divi/heading": {
|
|
505
505
|
"wrapper": "title",
|
|
@@ -510,7 +510,10 @@
|
|
|
510
510
|
"cell_divi_version": "5.5.2",
|
|
511
511
|
"verified_at": "2026-05-19",
|
|
512
512
|
"source": ".claude/skills/divi-5-builder/references/presets.md#known-group-bucket-inventory-vb-verified-2026-05-04",
|
|
513
|
-
"caveats": [
|
|
513
|
+
"caveats": [
|
|
514
|
+
"divi/spacing group bucket VB-confirmed (5.4.0); per-module title-spacing cell needs its own roundtrip",
|
|
515
|
+
"group_id is NOT VB-verified for this cell — value 'title.decoration.spacing' is schema-inferred. Track 7a (5.6.0) proved the section cell uses groupId='designSpacing' (Composable Settings panel id, not dotted attr path); whether heading uses the same value or a wrapper-prefixed variant is unverified. Do NOT use this group_id for preset writes until a heading-spacing capture on divi5-clean.local lands. Note: sandbox presets on divi5-ai.local (ids ecs423stpr, yjh59i25br) are bound to title wrapper with the same sparse-emit shape, but these are unverified residue and explicitly NOT used as evidence; proper capture on canonical reference substrate is required."
|
|
516
|
+
]
|
|
514
517
|
},
|
|
515
518
|
"divi/text": {
|
|
516
519
|
"wrapper": "content",
|
|
@@ -521,18 +524,26 @@
|
|
|
521
524
|
"cell_divi_version": "5.5.2",
|
|
522
525
|
"verified_at": "2026-05-19",
|
|
523
526
|
"source": ".claude/skills/divi-5-builder/references/presets.md#known-group-bucket-inventory-vb-verified-2026-05-04",
|
|
524
|
-
"caveats": [
|
|
527
|
+
"caveats": [
|
|
528
|
+
"group_id is NOT VB-verified for this cell — value 'content.decoration.spacing' is schema-inferred. Track 7a (5.6.0) proved the section cell uses groupId='designSpacing' (Composable Settings panel id, not dotted attr path); whether text uses the same value or a wrapper-prefixed variant is unverified. Do NOT use this group_id for preset writes until a text-spacing capture on divi5-clean.local lands."
|
|
529
|
+
]
|
|
525
530
|
},
|
|
526
531
|
"divi/section": {
|
|
527
532
|
"wrapper": "module",
|
|
528
533
|
"preset_type": "group",
|
|
529
534
|
"group_name": "divi/spacing",
|
|
530
|
-
"group_id": "
|
|
531
|
-
"cell_evidence_level": "
|
|
532
|
-
"cell_divi_version": "5.
|
|
533
|
-
"verified_at": "2026-05-
|
|
534
|
-
"source": "
|
|
535
|
-
"
|
|
535
|
+
"group_id": "designSpacing",
|
|
536
|
+
"cell_evidence_level": "VB_PRESET_STORAGE_VERIFIED",
|
|
537
|
+
"cell_divi_version": "5.6.0",
|
|
538
|
+
"verified_at": "2026-05-23",
|
|
539
|
+
"source": "docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json",
|
|
540
|
+
"preset_map_key": "module.decoration.spacing__padding",
|
|
541
|
+
"state": "desktop.value",
|
|
542
|
+
"caveats": [
|
|
543
|
+
"Sparse-emit padding AND margin: only user-touched corners appear in attrs; syncVertical + syncHorizontal always emit on any spacing touch (atomic sibling pair, both as 'off' by default — 'on' when user explicitly enables sync).",
|
|
544
|
+
"primaryAttrName='module' for this cell. Registry schema does not carry primaryAttrName as a first-class field today; recorded here in caveats. Phase 2 emitter must include primaryAttrName='module' when writing this preset shape.",
|
|
545
|
+
"VB authoring footgun: padding/margin field values MUST be committed via Enter or blur before saving the preset in the Composable Settings panel, or the preset saves as an empty registry shell with no 'attrs' key. CLI-driven emitters don't have this problem (values are passed via flags), but worth knowing for any UI-driven preset authoring."
|
|
546
|
+
]
|
|
536
547
|
},
|
|
537
548
|
"divi/button": {
|
|
538
549
|
"wrapper": "module",
|
|
@@ -543,7 +554,10 @@
|
|
|
543
554
|
"cell_divi_version": "5.5.2",
|
|
544
555
|
"verified_at": "2026-05-19",
|
|
545
556
|
"source": ".claude/skills/divi-5-builder/references/presets.md#known-group-bucket-inventory-vb-verified-2026-05-04",
|
|
546
|
-
"caveats": [
|
|
557
|
+
"caveats": [
|
|
558
|
+
"Button padding is canonically authored on module.decoration.spacing per Button exception (NOT button.decoration.spacing which is VB-hidden)",
|
|
559
|
+
"group_id is NOT VB-verified for this cell — value 'module.decoration.spacing' is schema-inferred. Track 7a (5.6.0) proved the section cell uses groupId='designSpacing' (Composable Settings panel id, not dotted attr path); whether button uses the same value or differs is unverified. Do NOT use this group_id for preset writes until a button-spacing capture on divi5-clean.local lands."
|
|
560
|
+
]
|
|
547
561
|
}
|
|
548
562
|
}
|
|
549
563
|
}
|
|
@@ -319,6 +319,210 @@ test("heading-font --apply without credentials exits 1 with a credentials hint",
|
|
|
319
319
|
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
|
+
// ------------------------------------------------------------------
|
|
323
|
+
// text-body-font command — CLI integration (parse → emit → dry-run JSON)
|
|
324
|
+
// Track 6: Pattern A only; Pattern B refused via registry-absence.
|
|
325
|
+
// ------------------------------------------------------------------
|
|
326
|
+
test("--help advertises the text-body-font command (Pattern A only)", async () => {
|
|
327
|
+
const io = capture();
|
|
328
|
+
const code = await run(["--help"], io);
|
|
329
|
+
assert.equal(code, EXIT.OK);
|
|
330
|
+
const help = io.stdout.join("\n");
|
|
331
|
+
assert.match(help, /text-body-font/);
|
|
332
|
+
assert.match(help, /Pattern A/);
|
|
333
|
+
});
|
|
334
|
+
test("text-body-font dry-run (Pattern A) emits canonical JSON, exit 0", async () => {
|
|
335
|
+
const io = capture();
|
|
336
|
+
const code = await run([
|
|
337
|
+
"text-body-font",
|
|
338
|
+
"--name",
|
|
339
|
+
"Body",
|
|
340
|
+
"--pattern",
|
|
341
|
+
"google",
|
|
342
|
+
"--font-family",
|
|
343
|
+
"Inter",
|
|
344
|
+
"--font-weight",
|
|
345
|
+
"400",
|
|
346
|
+
"--font-color",
|
|
347
|
+
"gcid-body-color",
|
|
348
|
+
"--font-size",
|
|
349
|
+
"16px",
|
|
350
|
+
], io);
|
|
351
|
+
assert.equal(code, EXIT.OK);
|
|
352
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
353
|
+
assert.equal(parsed.type, "group");
|
|
354
|
+
assert.equal(parsed.dry_run, true);
|
|
355
|
+
assert.equal(parsed.module_name, "divi/text");
|
|
356
|
+
assert.equal(parsed.group_name, "divi/font-body");
|
|
357
|
+
assert.equal(parsed.group_id, "designText");
|
|
358
|
+
const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
|
|
359
|
+
assert.equal(value.family, "Inter");
|
|
360
|
+
assert.equal(value.weight, "400");
|
|
361
|
+
assert.equal(value.size, "16px");
|
|
362
|
+
assert.equal(value.color, '$variable({"type":"color","value":{"name":"gcid-body-color","settings":{}}})$');
|
|
363
|
+
});
|
|
364
|
+
test("text-body-font dry-run omits weight/size/lineHeight keys when not specified", async () => {
|
|
365
|
+
const io = capture();
|
|
366
|
+
const code = await run([
|
|
367
|
+
"text-body-font",
|
|
368
|
+
"--name",
|
|
369
|
+
"Body",
|
|
370
|
+
"--pattern",
|
|
371
|
+
"google",
|
|
372
|
+
"--font-family",
|
|
373
|
+
"Inter",
|
|
374
|
+
], io);
|
|
375
|
+
assert.equal(code, EXIT.OK);
|
|
376
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
377
|
+
const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
|
|
378
|
+
assert.deepEqual(Object.keys(value), ["family"]);
|
|
379
|
+
});
|
|
380
|
+
test("text-body-font without --pattern exits 1 (invalid input)", async () => {
|
|
381
|
+
const io = capture();
|
|
382
|
+
const code = await run([
|
|
383
|
+
"text-body-font",
|
|
384
|
+
"--name",
|
|
385
|
+
"Body",
|
|
386
|
+
"--font-family",
|
|
387
|
+
"Inter",
|
|
388
|
+
"--font-weight",
|
|
389
|
+
"400",
|
|
390
|
+
], io);
|
|
391
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
392
|
+
assert.match(io.stderr.join("\n"), /--pattern/);
|
|
393
|
+
assert.match(io.stderr.join("\n"), /google\|local/);
|
|
394
|
+
});
|
|
395
|
+
test("text-body-font without --name exits 1", async () => {
|
|
396
|
+
const io = capture();
|
|
397
|
+
const code = await run(["text-body-font", "--pattern", "google", "--font-family", "Inter"], io);
|
|
398
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
399
|
+
assert.match(io.stderr.join("\n"), /requires --name/);
|
|
400
|
+
});
|
|
401
|
+
test("text-body-font --pattern local is refused (registry-absence), exit 2", async () => {
|
|
402
|
+
// Track 6 contract: Pattern B has NO registry entry for `divi/font-body`.
|
|
403
|
+
// The CLI must exit non-zero. The registry-absence throw is NOT an
|
|
404
|
+
// EvidenceGateError (no resolution constructed) — it surfaces as a
|
|
405
|
+
// plain Error and the CLI's fallback branch returns INVALID_INPUT (1).
|
|
406
|
+
// What matters here is: (a) non-zero exit; (b) the error message names
|
|
407
|
+
// both family and variant so the operator can file the gap.
|
|
408
|
+
const io = capture();
|
|
409
|
+
const code = await run([
|
|
410
|
+
"text-body-font",
|
|
411
|
+
"--name",
|
|
412
|
+
"Body",
|
|
413
|
+
"--pattern",
|
|
414
|
+
"local",
|
|
415
|
+
"--font-family",
|
|
416
|
+
"Inter",
|
|
417
|
+
"--font-color",
|
|
418
|
+
"#666666",
|
|
419
|
+
], io);
|
|
420
|
+
assert.notEqual(code, EXIT.OK, "refusal MUST exit non-zero");
|
|
421
|
+
const err = io.stderr.join("\n");
|
|
422
|
+
assert.match(err, /absent from verified-attrs\.json/);
|
|
423
|
+
assert.match(err, /divi\/font-body/);
|
|
424
|
+
assert.match(err, /local_hosted_pattern_b/);
|
|
425
|
+
});
|
|
426
|
+
test("text-body-font --pattern local --font-weight is ALSO refused (compound input)", async () => {
|
|
427
|
+
// Compound-input parity: adding --font-weight (or any other field) does
|
|
428
|
+
// NOT change the refusal — the gate fires on the missing variant entry
|
|
429
|
+
// regardless of which other fields are set.
|
|
430
|
+
const io = capture();
|
|
431
|
+
const code = await run([
|
|
432
|
+
"text-body-font",
|
|
433
|
+
"--name",
|
|
434
|
+
"Body",
|
|
435
|
+
"--pattern",
|
|
436
|
+
"local",
|
|
437
|
+
"--font-family",
|
|
438
|
+
"Inter",
|
|
439
|
+
"--font-weight",
|
|
440
|
+
"700",
|
|
441
|
+
], io);
|
|
442
|
+
assert.notEqual(code, EXIT.OK);
|
|
443
|
+
const err = io.stderr.join("\n");
|
|
444
|
+
assert.match(err, /absent from verified-attrs\.json/);
|
|
445
|
+
assert.match(err, /local_hosted_pattern_b/);
|
|
446
|
+
});
|
|
447
|
+
test("text-body-font --pattern with an invalid value exits 1", async () => {
|
|
448
|
+
const io = capture();
|
|
449
|
+
const code = await run([
|
|
450
|
+
"text-body-font",
|
|
451
|
+
"--name",
|
|
452
|
+
"Body",
|
|
453
|
+
"--pattern",
|
|
454
|
+
"auto",
|
|
455
|
+
"--font-family",
|
|
456
|
+
"Inter",
|
|
457
|
+
], io);
|
|
458
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
459
|
+
assert.match(io.stderr.join("\n"), /--pattern must be/);
|
|
460
|
+
});
|
|
461
|
+
test("text-body-font dry-run requires no credentials and no network", async () => {
|
|
462
|
+
const saved = {
|
|
463
|
+
WP_URL: process.env.WP_URL,
|
|
464
|
+
WP_USER: process.env.WP_USER,
|
|
465
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
466
|
+
};
|
|
467
|
+
delete process.env.WP_URL;
|
|
468
|
+
delete process.env.WP_USER;
|
|
469
|
+
delete process.env.WP_APP_PASSWORD;
|
|
470
|
+
try {
|
|
471
|
+
const io = capture();
|
|
472
|
+
const code = await run([
|
|
473
|
+
"text-body-font",
|
|
474
|
+
"--name",
|
|
475
|
+
"Body",
|
|
476
|
+
"--pattern",
|
|
477
|
+
"google",
|
|
478
|
+
"--font-family",
|
|
479
|
+
"Inter",
|
|
480
|
+
], io);
|
|
481
|
+
assert.equal(code, EXIT.OK);
|
|
482
|
+
assert.equal(io.stderr.length, 0, "no error output on credential-free dry-run");
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
if (saved.WP_URL !== undefined)
|
|
486
|
+
process.env.WP_URL = saved.WP_URL;
|
|
487
|
+
if (saved.WP_USER !== undefined)
|
|
488
|
+
process.env.WP_USER = saved.WP_USER;
|
|
489
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
490
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
test("text-body-font --apply without credentials exits 1 with a credentials hint", async () => {
|
|
494
|
+
const saved = {
|
|
495
|
+
WP_URL: process.env.WP_URL,
|
|
496
|
+
WP_USER: process.env.WP_USER,
|
|
497
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
498
|
+
};
|
|
499
|
+
delete process.env.WP_URL;
|
|
500
|
+
delete process.env.WP_USER;
|
|
501
|
+
delete process.env.WP_APP_PASSWORD;
|
|
502
|
+
try {
|
|
503
|
+
const io = capture();
|
|
504
|
+
const code = await run([
|
|
505
|
+
"text-body-font",
|
|
506
|
+
"--name",
|
|
507
|
+
"Body",
|
|
508
|
+
"--pattern",
|
|
509
|
+
"google",
|
|
510
|
+
"--font-family",
|
|
511
|
+
"Inter",
|
|
512
|
+
"--apply",
|
|
513
|
+
], io);
|
|
514
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
515
|
+
assert.match(io.stderr.join("\n"), /requires WordPress credentials/);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
if (saved.WP_URL !== undefined)
|
|
519
|
+
process.env.WP_URL = saved.WP_URL;
|
|
520
|
+
if (saved.WP_USER !== undefined)
|
|
521
|
+
process.env.WP_USER = saved.WP_USER;
|
|
522
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
523
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
322
526
|
test("dry-run output includes the bypass corner when requested", async () => {
|
|
323
527
|
const io = capture();
|
|
324
528
|
const code = await run(["button", "--name", "P", "--bg-color", "#111", "--bypass-hover-padding-gate"], io);
|
|
@@ -328,3 +532,260 @@ test("dry-run output includes the bypass corner when requested", async () => {
|
|
|
328
532
|
desktop: { value: { padding: { top: "0px" } } },
|
|
329
533
|
});
|
|
330
534
|
});
|
|
535
|
+
// ------------------------------------------------------------------
|
|
536
|
+
// spacing command — CLI integration (parse → emit → dry-run JSON)
|
|
537
|
+
// Track 7b: divi/section only; other modules refused by registry gate.
|
|
538
|
+
// ------------------------------------------------------------------
|
|
539
|
+
test("--help advertises the spacing command (divi/section only caveat)", async () => {
|
|
540
|
+
const io = capture();
|
|
541
|
+
const code = await run(["--help"], io);
|
|
542
|
+
assert.equal(code, EXIT.OK);
|
|
543
|
+
const help = io.stdout.join("\n");
|
|
544
|
+
assert.match(help, /spacing/);
|
|
545
|
+
assert.match(help, /divi\/section/);
|
|
546
|
+
});
|
|
547
|
+
test("spacing dry-run emits canonical sparse-emit JSON, exit 0", async () => {
|
|
548
|
+
const io = capture();
|
|
549
|
+
const code = await run([
|
|
550
|
+
"spacing",
|
|
551
|
+
"--name",
|
|
552
|
+
"Section Rhythm",
|
|
553
|
+
"--module",
|
|
554
|
+
"divi/section",
|
|
555
|
+
"--padding-top",
|
|
556
|
+
"40px",
|
|
557
|
+
], io);
|
|
558
|
+
assert.equal(code, EXIT.OK);
|
|
559
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
560
|
+
assert.equal(parsed.type, "group");
|
|
561
|
+
assert.equal(parsed.dry_run, true);
|
|
562
|
+
assert.equal(parsed.module_name, "divi/section");
|
|
563
|
+
assert.equal(parsed.group_name, "divi/spacing");
|
|
564
|
+
assert.equal(parsed.group_id, "designSpacing");
|
|
565
|
+
assert.equal(parsed.primary_attr_name, "module");
|
|
566
|
+
const value = parsed.attrs.module.decoration.spacing.desktop.value;
|
|
567
|
+
assert.deepEqual(value.padding, {
|
|
568
|
+
top: "40px",
|
|
569
|
+
syncVertical: "off",
|
|
570
|
+
syncHorizontal: "off",
|
|
571
|
+
});
|
|
572
|
+
assert.equal("margin" in value, false, "no margin bag when only padding flag passed");
|
|
573
|
+
});
|
|
574
|
+
test("spacing dry-run emits both bags when padding + margin flags passed", async () => {
|
|
575
|
+
const io = capture();
|
|
576
|
+
const code = await run([
|
|
577
|
+
"spacing",
|
|
578
|
+
"--name",
|
|
579
|
+
"Both",
|
|
580
|
+
"--module",
|
|
581
|
+
"divi/section",
|
|
582
|
+
"--padding-top",
|
|
583
|
+
"40px",
|
|
584
|
+
"--margin-bottom",
|
|
585
|
+
"80px",
|
|
586
|
+
], io);
|
|
587
|
+
assert.equal(code, EXIT.OK);
|
|
588
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
589
|
+
const value = parsed.attrs.module.decoration.spacing.desktop.value;
|
|
590
|
+
assert.deepEqual(value.padding, {
|
|
591
|
+
top: "40px",
|
|
592
|
+
syncVertical: "off",
|
|
593
|
+
syncHorizontal: "off",
|
|
594
|
+
});
|
|
595
|
+
assert.deepEqual(value.margin, {
|
|
596
|
+
bottom: "80px",
|
|
597
|
+
syncVertical: "off",
|
|
598
|
+
syncHorizontal: "off",
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
test("spacing honors --padding-sync-vertical on", async () => {
|
|
602
|
+
const io = capture();
|
|
603
|
+
const code = await run([
|
|
604
|
+
"spacing",
|
|
605
|
+
"--name",
|
|
606
|
+
"Sync",
|
|
607
|
+
"--module",
|
|
608
|
+
"divi/section",
|
|
609
|
+
"--padding-top",
|
|
610
|
+
"40px",
|
|
611
|
+
"--padding-bottom",
|
|
612
|
+
"40px",
|
|
613
|
+
"--padding-sync-vertical",
|
|
614
|
+
"on",
|
|
615
|
+
], io);
|
|
616
|
+
assert.equal(code, EXIT.OK);
|
|
617
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
618
|
+
const padding = parsed.attrs.module.decoration.spacing.desktop.value.padding;
|
|
619
|
+
assert.equal(padding.syncVertical, "on");
|
|
620
|
+
assert.equal(padding.syncHorizontal, "off");
|
|
621
|
+
});
|
|
622
|
+
test("spacing without --name exits 1", async () => {
|
|
623
|
+
const io = capture();
|
|
624
|
+
const code = await run(["spacing", "--module", "divi/section", "--padding-top", "40px"], io);
|
|
625
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
626
|
+
assert.match(io.stderr.join("\n"), /requires --name/);
|
|
627
|
+
});
|
|
628
|
+
test("spacing without --module exits 1", async () => {
|
|
629
|
+
const io = capture();
|
|
630
|
+
const code = await run(["spacing", "--name", "X", "--padding-top", "40px"], io);
|
|
631
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
632
|
+
assert.match(io.stderr.join("\n"), /requires --module/);
|
|
633
|
+
});
|
|
634
|
+
test("spacing with no corners exits non-zero (empty preset)", async () => {
|
|
635
|
+
const io = capture();
|
|
636
|
+
const code = await run(["spacing", "--name", "X", "--module", "divi/section"], io);
|
|
637
|
+
assert.notEqual(code, EXIT.OK);
|
|
638
|
+
assert.match(io.stderr.join("\n"), /empty preset/);
|
|
639
|
+
});
|
|
640
|
+
test("spacing --module divi/heading is refused by evidence gate, exit 2", async () => {
|
|
641
|
+
const io = capture();
|
|
642
|
+
const code = await run([
|
|
643
|
+
"spacing",
|
|
644
|
+
"--name",
|
|
645
|
+
"H",
|
|
646
|
+
"--module",
|
|
647
|
+
"divi/heading",
|
|
648
|
+
"--padding-top",
|
|
649
|
+
"40px",
|
|
650
|
+
], io);
|
|
651
|
+
assert.equal(code, EXIT.EVIDENCE_GATE);
|
|
652
|
+
const err = io.stderr.join("\n");
|
|
653
|
+
assert.match(err, /Evidence-gate refusal/);
|
|
654
|
+
assert.match(err, /divi\/heading/);
|
|
655
|
+
assert.match(err, /divi\/spacing/);
|
|
656
|
+
});
|
|
657
|
+
test("spacing --module divi/text is refused (exit 2)", async () => {
|
|
658
|
+
const io = capture();
|
|
659
|
+
const code = await run([
|
|
660
|
+
"spacing",
|
|
661
|
+
"--name",
|
|
662
|
+
"T",
|
|
663
|
+
"--module",
|
|
664
|
+
"divi/text",
|
|
665
|
+
"--padding-top",
|
|
666
|
+
"40px",
|
|
667
|
+
], io);
|
|
668
|
+
assert.equal(code, EXIT.EVIDENCE_GATE);
|
|
669
|
+
});
|
|
670
|
+
test("spacing --module divi/button is refused (exit 2)", async () => {
|
|
671
|
+
const io = capture();
|
|
672
|
+
const code = await run([
|
|
673
|
+
"spacing",
|
|
674
|
+
"--name",
|
|
675
|
+
"B",
|
|
676
|
+
"--module",
|
|
677
|
+
"divi/button",
|
|
678
|
+
"--padding-top",
|
|
679
|
+
"40px",
|
|
680
|
+
], io);
|
|
681
|
+
assert.equal(code, EXIT.EVIDENCE_GATE);
|
|
682
|
+
});
|
|
683
|
+
test("spacing rejects bare gvid-* token in --padding-top", async () => {
|
|
684
|
+
const io = capture();
|
|
685
|
+
const code = await run([
|
|
686
|
+
"spacing",
|
|
687
|
+
"--name",
|
|
688
|
+
"V",
|
|
689
|
+
"--module",
|
|
690
|
+
"divi/section",
|
|
691
|
+
"--padding-top",
|
|
692
|
+
"gvid-space-1",
|
|
693
|
+
], io);
|
|
694
|
+
assert.notEqual(code, EXIT.OK);
|
|
695
|
+
assert.match(io.stderr.join("\n"), /Variable-token support deferred/);
|
|
696
|
+
});
|
|
697
|
+
test("spacing rejects $variable(...) form in --margin-bottom", async () => {
|
|
698
|
+
const io = capture();
|
|
699
|
+
const code = await run([
|
|
700
|
+
"spacing",
|
|
701
|
+
"--name",
|
|
702
|
+
"V",
|
|
703
|
+
"--module",
|
|
704
|
+
"divi/section",
|
|
705
|
+
"--margin-bottom",
|
|
706
|
+
'$variable({"type":"length","value":{"name":"gvid-space-1","settings":{}}})$',
|
|
707
|
+
], io);
|
|
708
|
+
assert.notEqual(code, EXIT.OK);
|
|
709
|
+
assert.match(io.stderr.join("\n"), /Variable-token support deferred/);
|
|
710
|
+
});
|
|
711
|
+
test("spacing --padding-sync-vertical with invalid value exits 1", async () => {
|
|
712
|
+
const io = capture();
|
|
713
|
+
const code = await run([
|
|
714
|
+
"spacing",
|
|
715
|
+
"--name",
|
|
716
|
+
"X",
|
|
717
|
+
"--module",
|
|
718
|
+
"divi/section",
|
|
719
|
+
"--padding-top",
|
|
720
|
+
"40px",
|
|
721
|
+
"--padding-sync-vertical",
|
|
722
|
+
"maybe",
|
|
723
|
+
], io);
|
|
724
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
725
|
+
assert.match(io.stderr.join("\n"), /must be "on" or "off"/);
|
|
726
|
+
});
|
|
727
|
+
test("spacing dry-run requires no credentials and no network", async () => {
|
|
728
|
+
const saved = {
|
|
729
|
+
WP_URL: process.env.WP_URL,
|
|
730
|
+
WP_USER: process.env.WP_USER,
|
|
731
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
732
|
+
};
|
|
733
|
+
delete process.env.WP_URL;
|
|
734
|
+
delete process.env.WP_USER;
|
|
735
|
+
delete process.env.WP_APP_PASSWORD;
|
|
736
|
+
try {
|
|
737
|
+
const io = capture();
|
|
738
|
+
const code = await run([
|
|
739
|
+
"spacing",
|
|
740
|
+
"--name",
|
|
741
|
+
"Sec",
|
|
742
|
+
"--module",
|
|
743
|
+
"divi/section",
|
|
744
|
+
"--padding-top",
|
|
745
|
+
"40px",
|
|
746
|
+
], io);
|
|
747
|
+
assert.equal(code, EXIT.OK);
|
|
748
|
+
assert.equal(io.stderr.length, 0);
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
if (saved.WP_URL !== undefined)
|
|
752
|
+
process.env.WP_URL = saved.WP_URL;
|
|
753
|
+
if (saved.WP_USER !== undefined)
|
|
754
|
+
process.env.WP_USER = saved.WP_USER;
|
|
755
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
756
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
test("spacing --apply without credentials exits 1 with a credentials hint", async () => {
|
|
760
|
+
const saved = {
|
|
761
|
+
WP_URL: process.env.WP_URL,
|
|
762
|
+
WP_USER: process.env.WP_USER,
|
|
763
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
764
|
+
};
|
|
765
|
+
delete process.env.WP_URL;
|
|
766
|
+
delete process.env.WP_USER;
|
|
767
|
+
delete process.env.WP_APP_PASSWORD;
|
|
768
|
+
try {
|
|
769
|
+
const io = capture();
|
|
770
|
+
const code = await run([
|
|
771
|
+
"spacing",
|
|
772
|
+
"--name",
|
|
773
|
+
"Sec",
|
|
774
|
+
"--module",
|
|
775
|
+
"divi/section",
|
|
776
|
+
"--padding-top",
|
|
777
|
+
"40px",
|
|
778
|
+
"--apply",
|
|
779
|
+
], io);
|
|
780
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
781
|
+
assert.match(io.stderr.join("\n"), /requires WordPress credentials/);
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
if (saved.WP_URL !== undefined)
|
|
785
|
+
process.env.WP_URL = saved.WP_URL;
|
|
786
|
+
if (saved.WP_USER !== undefined)
|
|
787
|
+
process.env.WP_USER = saved.WP_USER;
|
|
788
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
789
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
790
|
+
}
|
|
791
|
+
});
|
|
@@ -147,3 +147,29 @@ test("variant-aware lookup selects the correct entry by pattern_variant", () =>
|
|
|
147
147
|
// A bogus variant under a known family throws — no silent fallback.
|
|
148
148
|
assert.throws(() => resolveEvidence(reg, "divi/heading", "divi/font", "no_such_variant"), /absent from verified-attrs\.json/);
|
|
149
149
|
});
|
|
150
|
+
test("variant-aware lookup: divi/font-body Pattern A on divi/text clears the write threshold", () => {
|
|
151
|
+
// Track 6 contract: the shipped registry MUST carry one cleared
|
|
152
|
+
// `divi/font-body` + `google_fonts_pattern_a` entry on `divi/text`
|
|
153
|
+
// (effective level = VB_PRESET_STORAGE_VERIFIED). If a registry edit
|
|
154
|
+
// ever drops it, this canary fires.
|
|
155
|
+
const reg = loadRegistry();
|
|
156
|
+
const a = resolveEvidence(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
|
|
157
|
+
assert.equal(a.patternVariant, "google_fonts_pattern_a");
|
|
158
|
+
assert.equal(a.effectiveLevel, 4, "divi/font-body Pattern A on divi/text must clear VB_PRESET_STORAGE_VERIFIED");
|
|
159
|
+
// gateWriteAttr must not throw at this cell.
|
|
160
|
+
gateWriteAttr(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
|
|
161
|
+
});
|
|
162
|
+
test("variant-aware lookup: divi/font-body Pattern B is absent from the registry (refused)", () => {
|
|
163
|
+
// Track 6 contract: the shipped registry MUST NOT carry a
|
|
164
|
+
// `divi/font-body` + `local_hosted_pattern_b` entry. Pattern B for
|
|
165
|
+
// body-text was deliberately deferred per round-2 _meta — there is no
|
|
166
|
+
// captured evidence. resolveEvidence MUST throw the standard
|
|
167
|
+
// registry-absence error.
|
|
168
|
+
const reg = loadRegistry();
|
|
169
|
+
assert.throws(() => resolveEvidence(reg, "divi/text", "divi/font-body", "local_hosted_pattern_b"), (err) => {
|
|
170
|
+
assert.match(err.message, /absent from verified-attrs\.json/);
|
|
171
|
+
assert.match(err.message, /divi\/font-body/);
|
|
172
|
+
assert.match(err.message, /local_hosted_pattern_b/);
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
});
|