@diviops/mcp-server 1.5.10 → 1.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +34 -0
  2. package/dist/preset-cli/__tests__/button-emitter.test.d.ts +8 -0
  3. package/dist/preset-cli/__tests__/button-emitter.test.js +188 -0
  4. package/dist/preset-cli/__tests__/cli.test.d.ts +9 -0
  5. package/dist/preset-cli/__tests__/cli.test.js +330 -0
  6. package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
  7. package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
  8. package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +13 -0
  9. package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +64 -0
  10. package/dist/preset-cli/__tests__/registry.test.d.ts +5 -0
  11. package/dist/preset-cli/__tests__/registry.test.js +149 -0
  12. package/dist/preset-cli/__tests__/write-path.test.d.ts +8 -0
  13. package/dist/preset-cli/__tests__/write-path.test.js +174 -0
  14. package/dist/preset-cli/bin.d.ts +8 -0
  15. package/dist/preset-cli/bin.js +32 -0
  16. package/dist/preset-cli/button-emitter.d.ts +117 -0
  17. package/dist/preset-cli/button-emitter.js +218 -0
  18. package/dist/preset-cli/cli.d.ts +62 -0
  19. package/dist/preset-cli/cli.js +429 -0
  20. package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
  21. package/dist/preset-cli/heading-font-emitter.js +166 -0
  22. package/dist/preset-cli/registry.d.ts +121 -0
  23. package/dist/preset-cli/registry.js +192 -0
  24. package/dist/preset-cli/variable-token.d.ts +42 -0
  25. package/dist/preset-cli/variable-token.js +70 -0
  26. package/dist/preset-cli/write-path.d.ts +74 -0
  27. package/dist/preset-cli/write-path.js +110 -0
  28. package/package.json +4 -2
@@ -0,0 +1,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
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Verified-attrs registry consumption for the preset-emitter CLI.
3
+ *
4
+ * Loads `diviops-server/data/verified-attrs.json` and resolves the
5
+ * EFFECTIVE evidence level per `(module, pattern-family)` cell using the
6
+ * `min()` rule documented in `docs/verification/README.md`:
7
+ *
8
+ * effective_evidence = min(pattern_evidence_level,
9
+ * applicability[<module>].cell_evidence_level)
10
+ *
11
+ * A missing `applicability[<module>]` resolves to `UNVERIFIED` (0) — never
12
+ * to the pattern level. No invisible inheritance crosses the write
13
+ * threshold.
14
+ *
15
+ * This module is the consumer side of the verified-attrs registry
16
+ * contract. It does NOT mutate the registry; if a needed cell is absent
17
+ * or under-verified, the caller fails and the gap is filed as a backlog
18
+ * candidate.
19
+ */
20
+ /**
21
+ * Effective evidence required for a write emitter to emit an attr.
22
+ * `VB_PRESET_STORAGE_VERIFIED` has numeric ordering 4.
23
+ */
24
+ export declare const WRITE_EMITTER_THRESHOLD_LEVEL = "VB_PRESET_STORAGE_VERIFIED";
25
+ export interface ApplicabilityCell {
26
+ wrapper?: string;
27
+ preset_type?: string;
28
+ group_name?: string;
29
+ group_id?: string;
30
+ cell_evidence_level?: string;
31
+ cell_divi_version?: string;
32
+ verified_at?: string;
33
+ source?: string;
34
+ caveats?: string[];
35
+ }
36
+ export interface Tier12Entry {
37
+ pattern_family: string;
38
+ pattern_variant?: string;
39
+ pattern_evidence_level: string;
40
+ pattern_evidence_source?: string;
41
+ applicability?: Record<string, ApplicabilityCell>;
42
+ }
43
+ export interface VerifiedAttrsRegistry {
44
+ schema_version?: string;
45
+ registry_version?: string;
46
+ evidence_level_ordering: Record<string, number>;
47
+ tier1?: Tier12Entry[];
48
+ tier2?: Tier12Entry[];
49
+ tier3?: unknown[];
50
+ }
51
+ /** Resolution of a single `(module, pattern-family[, pattern-variant])` cell against the registry. */
52
+ export interface EvidenceResolution {
53
+ patternFamily: string;
54
+ /** The matched entry's `pattern_variant`, if any. */
55
+ patternVariant?: string;
56
+ module: string;
57
+ /** Numeric pattern-level evidence (0–5). */
58
+ patternLevel: number;
59
+ patternLevelName: string;
60
+ /** Numeric cell-level evidence (0–5); 0 when applicability[<module>] is absent. */
61
+ cellLevel: number;
62
+ cellLevelName: string;
63
+ /** min(patternLevel, cellLevel). */
64
+ effectiveLevel: number;
65
+ effectiveLevelName: string;
66
+ /** True when applicability[<module>] is missing — cellLevel is the 0-fallback. */
67
+ applicabilityMissing: boolean;
68
+ /** Best-available durable source string for error messages. */
69
+ source: string;
70
+ /** The applicability cell, if present (carries group_name / group_id / wrapper). */
71
+ cell?: ApplicabilityCell;
72
+ }
73
+ /**
74
+ * Load and cache `data/verified-attrs.json`. The path is resolved relative
75
+ * to the compiled module location (`dist/preset-cli/registry.js` →
76
+ * `dist/../data/verified-attrs.json`), and also tolerates the `src/` layout
77
+ * for direct ts-node-style execution / tests.
78
+ */
79
+ export declare function loadRegistry(explicitPath?: string): VerifiedAttrsRegistry;
80
+ /** Reset the module-level cache. Used by tests. */
81
+ export declare function resetRegistryCache(): void;
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.
96
+ */
97
+ export declare function findPatternEntry(registry: VerifiedAttrsRegistry, patternFamily: string, patternVariant?: string): Tier12Entry | undefined;
98
+ /**
99
+ * Resolve effective evidence for a `(module, pattern-family[, pattern-variant])` cell.
100
+ *
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).
104
+ */
105
+ export declare function resolveEvidence(registry: VerifiedAttrsRegistry, module: string, patternFamily: string, patternVariant?: string): EvidenceResolution;
106
+ /** Numeric ordering of the write-emitter threshold against a registry. */
107
+ export declare function writeThresholdNumber(registry: VerifiedAttrsRegistry): number;
108
+ /**
109
+ * Gate a write-targeted attr. Returns the resolution when it clears the
110
+ * `VB_PRESET_STORAGE_VERIFIED` threshold; throws an `EvidenceGateError`
111
+ * naming the attr, its effective level, and the registry source otherwise.
112
+ */
113
+ export declare class EvidenceGateError extends Error {
114
+ readonly patternFamily: string;
115
+ readonly module: string;
116
+ readonly resolution: EvidenceResolution;
117
+ readonly thresholdNumber: number;
118
+ readonly thresholdName: string;
119
+ constructor(patternFamily: string, module: string, resolution: EvidenceResolution, thresholdNumber: number, thresholdName: string);
120
+ }
121
+ export declare function gateWriteAttr(registry: VerifiedAttrsRegistry, module: string, patternFamily: string, patternVariant?: string): EvidenceResolution;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Verified-attrs registry consumption for the preset-emitter CLI.
3
+ *
4
+ * Loads `diviops-server/data/verified-attrs.json` and resolves the
5
+ * EFFECTIVE evidence level per `(module, pattern-family)` cell using the
6
+ * `min()` rule documented in `docs/verification/README.md`:
7
+ *
8
+ * effective_evidence = min(pattern_evidence_level,
9
+ * applicability[<module>].cell_evidence_level)
10
+ *
11
+ * A missing `applicability[<module>]` resolves to `UNVERIFIED` (0) — never
12
+ * to the pattern level. No invisible inheritance crosses the write
13
+ * threshold.
14
+ *
15
+ * This module is the consumer side of the verified-attrs registry
16
+ * contract. It does NOT mutate the registry; if a needed cell is absent
17
+ * or under-verified, the caller fails and the gap is filed as a backlog
18
+ * candidate.
19
+ */
20
+ import { readFileSync } from "node:fs";
21
+ import { fileURLToPath } from "node:url";
22
+ import { dirname, join } from "node:path";
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ /**
25
+ * Effective evidence required for a write emitter to emit an attr.
26
+ * `VB_PRESET_STORAGE_VERIFIED` has numeric ordering 4.
27
+ */
28
+ export const WRITE_EMITTER_THRESHOLD_LEVEL = "VB_PRESET_STORAGE_VERIFIED";
29
+ let cachedRegistry = null;
30
+ /**
31
+ * Load and cache `data/verified-attrs.json`. The path is resolved relative
32
+ * to the compiled module location (`dist/preset-cli/registry.js` →
33
+ * `dist/../data/verified-attrs.json`), and also tolerates the `src/` layout
34
+ * for direct ts-node-style execution / tests.
35
+ */
36
+ export function loadRegistry(explicitPath) {
37
+ if (!explicitPath && cachedRegistry)
38
+ return cachedRegistry;
39
+ const candidates = explicitPath
40
+ ? [explicitPath]
41
+ : [
42
+ // dist/preset-cli/registry.js -> dist/../data
43
+ join(__dirname, "..", "..", "data", "verified-attrs.json"),
44
+ // src/preset-cli/registry.ts -> src/../data
45
+ join(__dirname, "..", "data", "verified-attrs.json"),
46
+ ];
47
+ let lastErr = null;
48
+ for (const p of candidates) {
49
+ try {
50
+ const raw = readFileSync(p, "utf8");
51
+ const parsed = JSON.parse(raw);
52
+ if (!explicitPath)
53
+ cachedRegistry = parsed;
54
+ return parsed;
55
+ }
56
+ catch (err) {
57
+ lastErr = err;
58
+ }
59
+ }
60
+ throw new Error(`Could not load verified-attrs.json (tried: ${candidates.join(", ")}). ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
61
+ }
62
+ /** Reset the module-level cache. Used by tests. */
63
+ export function resetRegistryCache() {
64
+ cachedRegistry = null;
65
+ }
66
+ function levelNumber(registry, name) {
67
+ if (!name)
68
+ return 0;
69
+ const n = registry.evidence_level_ordering[name];
70
+ return typeof n === "number" ? n : 0;
71
+ }
72
+ function levelName(registry, num) {
73
+ for (const [name, n] of Object.entries(registry.evidence_level_ordering)) {
74
+ if (n === num)
75
+ return name;
76
+ }
77
+ return "UNVERIFIED";
78
+ }
79
+ /**
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.
93
+ */
94
+ export function findPatternEntry(registry, patternFamily, patternVariant) {
95
+ const tiers = [registry.tier2 ?? [], registry.tier1 ?? []];
96
+ for (const tier of tiers) {
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
+ });
105
+ if (hit)
106
+ return hit;
107
+ }
108
+ return undefined;
109
+ }
110
+ /**
111
+ * Resolve effective evidence for a `(module, pattern-family[, pattern-variant])` cell.
112
+ *
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).
116
+ */
117
+ export function resolveEvidence(registry, module, patternFamily, patternVariant) {
118
+ const entry = findPatternEntry(registry, patternFamily, patternVariant);
119
+ if (!entry) {
120
+ const variantSuffix = patternVariant !== undefined ? ` (variant "${patternVariant}")` : "";
121
+ throw new Error(`Pattern family "${patternFamily}"${variantSuffix} is absent from verified-attrs.json. ` +
122
+ `The CLI cannot emit an unregistered attr. File this as a registry-gap backlog candidate.`);
123
+ }
124
+ const patternLevel = levelNumber(registry, entry.pattern_evidence_level);
125
+ // Missing applicability[<module>] resolves to UNVERIFIED (0) — no
126
+ // invisible inheritance from the pattern level.
127
+ const cell = entry.applicability?.[module];
128
+ const applicabilityMissing = !cell;
129
+ const cellLevel = cell ? levelNumber(registry, cell.cell_evidence_level) : 0;
130
+ const effectiveLevel = Math.min(patternLevel, cellLevel);
131
+ const source = cell?.source ??
132
+ entry.pattern_evidence_source ??
133
+ "verified-attrs.json (no source field)";
134
+ return {
135
+ patternFamily,
136
+ patternVariant: entry.pattern_variant,
137
+ module,
138
+ patternLevel,
139
+ patternLevelName: entry.pattern_evidence_level,
140
+ cellLevel,
141
+ cellLevelName: cell?.cell_evidence_level ?? "UNVERIFIED",
142
+ effectiveLevel,
143
+ effectiveLevelName: levelName(registry, effectiveLevel),
144
+ applicabilityMissing,
145
+ source,
146
+ cell,
147
+ };
148
+ }
149
+ /** Numeric ordering of the write-emitter threshold against a registry. */
150
+ export function writeThresholdNumber(registry) {
151
+ return levelNumber(registry, WRITE_EMITTER_THRESHOLD_LEVEL);
152
+ }
153
+ /**
154
+ * Gate a write-targeted attr. Returns the resolution when it clears the
155
+ * `VB_PRESET_STORAGE_VERIFIED` threshold; throws an `EvidenceGateError`
156
+ * naming the attr, its effective level, and the registry source otherwise.
157
+ */
158
+ export class EvidenceGateError extends Error {
159
+ patternFamily;
160
+ module;
161
+ resolution;
162
+ thresholdNumber;
163
+ thresholdName;
164
+ constructor(patternFamily, module, resolution, thresholdNumber, thresholdName) {
165
+ const variantTag = resolution.patternVariant !== undefined
166
+ ? ` variant "${resolution.patternVariant}"`
167
+ : "";
168
+ super(`Evidence-gate refusal: attr family "${patternFamily}"${variantTag} on module "${module}" ` +
169
+ `has EFFECTIVE evidence ${resolution.effectiveLevelName} (${resolution.effectiveLevel}), ` +
170
+ `below the write threshold ${thresholdName} (${thresholdNumber}). ` +
171
+ (resolution.applicabilityMissing
172
+ ? `applicability["${module}"] is absent from the registry entry — resolved to UNVERIFIED (0). `
173
+ : `pattern=${resolution.patternLevelName}(${resolution.patternLevel}), ` +
174
+ `cell=${resolution.cellLevelName}(${resolution.cellLevel}). `) +
175
+ `Registry source: ${resolution.source}. ` +
176
+ `The CLI will not emit an under-verified attr — file a registry-gap backlog candidate.`);
177
+ this.patternFamily = patternFamily;
178
+ this.module = module;
179
+ this.resolution = resolution;
180
+ this.thresholdNumber = thresholdNumber;
181
+ this.thresholdName = thresholdName;
182
+ this.name = "EvidenceGateError";
183
+ }
184
+ }
185
+ export function gateWriteAttr(registry, module, patternFamily, patternVariant) {
186
+ const resolution = resolveEvidence(registry, module, patternFamily, patternVariant);
187
+ const thresholdNumber = writeThresholdNumber(registry);
188
+ if (resolution.effectiveLevel < thresholdNumber) {
189
+ throw new EvidenceGateError(patternFamily, module, resolution, thresholdNumber, WRITE_EMITTER_THRESHOLD_LEVEL);
190
+ }
191
+ return resolution;
192
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `$variable()` color-token helpers for the preset-emitter CLI.
3
+ *
4
+ * Divi 5.5.x color bindings use the token grammar:
5
+ *
6
+ * $variable({"type":"color","value":{"name":"gcid-heading-color","settings":{}}})$
7
+ *
8
+ * Two load-bearing rules (both from VB-verified memory):
9
+ * - The token MUST end with `)$` — a missing trailing `$` causes silent
10
+ * render failure (`feedback_variable_trailing_dollar`).
11
+ * - The `value` payload MUST be an object `{name, settings}`, not a flat
12
+ * `gcid-*` string — a flat string crashes PHP 8 in Divi's DynamicData
13
+ * resolver (`feedback_variable_color_value_object_shape`).
14
+ *
15
+ * The CLI accepts a color param that is EITHER a literal (a hex string,
16
+ * or an already-formed `$variable(...)$` token) OR a bare `gcid-*` /
17
+ * `gvid-*` token name, which this module wraps into the canonical shape.
18
+ */
19
+ /** An already-formed `$variable(...)$` token. */
20
+ export declare function isVariableToken(value: string): boolean;
21
+ /** A bare token name like `gcid-primary-color`. */
22
+ export declare function isBareTokenName(value: string): boolean;
23
+ /**
24
+ * Build a canonical `$variable()` color token from a bare token name.
25
+ *
26
+ * The inner JSON uses single-escape `\"` when the whole preset is later
27
+ * `JSON.stringify`-d — that is handled by JSON.stringify itself; here we
28
+ * return the raw string value (un-stringified), which is what belongs in
29
+ * the in-memory preset object.
30
+ */
31
+ export declare function buildColorVariableToken(tokenName: string): string;
32
+ /**
33
+ * Normalize a CLI color param to the value that belongs in the preset.
34
+ *
35
+ * - An already-formed `$variable(...)$` token → returned verbatim (the
36
+ * caller is responsible for its correctness; the trailing `)$` is
37
+ * asserted).
38
+ * - A bare `gcid-*`/`gvid-*` name → wrapped into the canonical token.
39
+ * - Anything else (a hex string, `rgba(...)`, etc.) → returned verbatim
40
+ * as a literal color value.
41
+ */
42
+ export declare function normalizeColorValue(value: string): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `$variable()` color-token helpers for the preset-emitter CLI.
3
+ *
4
+ * Divi 5.5.x color bindings use the token grammar:
5
+ *
6
+ * $variable({"type":"color","value":{"name":"gcid-heading-color","settings":{}}})$
7
+ *
8
+ * Two load-bearing rules (both from VB-verified memory):
9
+ * - The token MUST end with `)$` — a missing trailing `$` causes silent
10
+ * render failure (`feedback_variable_trailing_dollar`).
11
+ * - The `value` payload MUST be an object `{name, settings}`, not a flat
12
+ * `gcid-*` string — a flat string crashes PHP 8 in Divi's DynamicData
13
+ * resolver (`feedback_variable_color_value_object_shape`).
14
+ *
15
+ * The CLI accepts a color param that is EITHER a literal (a hex string,
16
+ * or an already-formed `$variable(...)$` token) OR a bare `gcid-*` /
17
+ * `gvid-*` token name, which this module wraps into the canonical shape.
18
+ */
19
+ /** A bare Divi variable token name (gcid-* or gvid-*). */
20
+ const BARE_TOKEN_RE = /^(gcid|gvid)-[A-Za-z0-9_-]+$/;
21
+ /** An already-formed `$variable(...)$` token. */
22
+ export function isVariableToken(value) {
23
+ return value.startsWith("$variable(") && value.endsWith(")$");
24
+ }
25
+ /** A bare token name like `gcid-primary-color`. */
26
+ export function isBareTokenName(value) {
27
+ return BARE_TOKEN_RE.test(value);
28
+ }
29
+ /**
30
+ * Build a canonical `$variable()` color token from a bare token name.
31
+ *
32
+ * The inner JSON uses single-escape `\"` when the whole preset is later
33
+ * `JSON.stringify`-d — that is handled by JSON.stringify itself; here we
34
+ * return the raw string value (un-stringified), which is what belongs in
35
+ * the in-memory preset object.
36
+ */
37
+ export function buildColorVariableToken(tokenName) {
38
+ if (!isBareTokenName(tokenName)) {
39
+ throw new Error(`Invalid variable token name "${tokenName}". Expected a bare gcid-*/gvid-* name ` +
40
+ `(e.g. "gcid-primary-color"), a hex literal, or an already-formed $variable(...)$ token.`);
41
+ }
42
+ const payload = JSON.stringify({
43
+ type: "color",
44
+ value: { name: tokenName, settings: {} },
45
+ });
46
+ return `$variable(${payload})$`;
47
+ }
48
+ /**
49
+ * Normalize a CLI color param to the value that belongs in the preset.
50
+ *
51
+ * - An already-formed `$variable(...)$` token → returned verbatim (the
52
+ * caller is responsible for its correctness; the trailing `)$` is
53
+ * asserted).
54
+ * - A bare `gcid-*`/`gvid-*` name → wrapped into the canonical token.
55
+ * - Anything else (a hex string, `rgba(...)`, etc.) → returned verbatim
56
+ * as a literal color value.
57
+ */
58
+ export function normalizeColorValue(value) {
59
+ if (isVariableToken(value)) {
60
+ return value;
61
+ }
62
+ if (value.startsWith("$variable(") && !value.endsWith(")$")) {
63
+ throw new Error(`Malformed $variable() token "${value}" — must end with ")$" ` +
64
+ `(missing trailing "$" causes silent render failure).`);
65
+ }
66
+ if (isBareTokenName(value)) {
67
+ return buildColorVariableToken(value);
68
+ }
69
+ return value;
70
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Apply-mode write path for the preset-emitter CLI.
3
+ *
4
+ * Reuses the existing server WP-client conventions:
5
+ * - `WP_URL` / `WP_USER` / `WP_APP_PASSWORD` env vars.
6
+ * - `WPClient` from `wp-client.ts` for the HTTP + Basic-Auth machinery.
7
+ * - `WPClient.handshake()` for the plugin capability map.
8
+ *
9
+ * Before any write the CLI verifies the plugin handshake reports the
10
+ * `storage_multipath_probe_v1` capability (the storage-path capability
11
+ * contract, shipped in plugin 1.4.9). Absent → fail fast, before composing
12
+ * a write.
13
+ *
14
+ * `--dry-run` never reaches this module: it requires no credentials, no
15
+ * handshake, and no network.
16
+ */
17
+ import { WPClient } from "../wp-client.js";
18
+ import type { HandshakeResult } from "../compatibility.js";
19
+ import type { DiviopsResponse } from "../envelope.js";
20
+ import type { ButtonPresetEntry } from "./button-emitter.js";
21
+ import type { HeadingFontPresetEntry } from "./heading-font-emitter.js";
22
+ /** The plugin capability the storage-path capability contract ships. */
23
+ export declare const STORAGE_CAPABILITY = "storage_multipath_probe_v1";
24
+ /** The REST route the CLI posts to — same route `diviops_preset_create` uses. */
25
+ export declare const PRESET_CREATE_ROUTE = "/preset/create";
26
+ /** Minimal client surface the write path needs — eased for mocking in tests. */
27
+ export interface PresetWriteClient {
28
+ handshake(serverVersion: string): Promise<HandshakeResult>;
29
+ requestEnveloped<T = unknown>(endpoint: string, options?: {
30
+ method?: string;
31
+ body?: Record<string, unknown>;
32
+ params?: Record<string, string>;
33
+ }): Promise<DiviopsResponse<T>>;
34
+ }
35
+ export declare class CredentialsMissingError extends Error {
36
+ constructor(missing: string[]);
37
+ }
38
+ export declare class CapabilityMissingError extends Error {
39
+ readonly capability: string;
40
+ readonly pluginVersion: string;
41
+ constructor(capability: string, pluginVersion: string);
42
+ }
43
+ /** Build a `WPClient` from the standard env vars, or throw if any are absent. */
44
+ export declare function buildClientFromEnv(env?: NodeJS.ProcessEnv): WPClient;
45
+ /**
46
+ * Verify the plugin handshake reports `storage_multipath_probe_v1`.
47
+ * Throws `CapabilityMissingError` if absent. Returns the handshake result
48
+ * so callers can surface the plugin version.
49
+ */
50
+ export declare function assertStorageCapability(client: PresetWriteClient, serverVersion: string): Promise<HandshakeResult>;
51
+ /**
52
+ * Apply a button preset: capability-gate, then POST to `/preset/create`.
53
+ *
54
+ * The capability check runs BEFORE the write. The write goes through the
55
+ * existing storage-routed route — no plugin route is added here.
56
+ */
57
+ export declare function applyButtonPreset(client: PresetWriteClient, entry: ButtonPresetEntry, opts: {
58
+ serverVersion: string;
59
+ dry_run?: boolean;
60
+ }): Promise<DiviopsResponse<unknown>>;
61
+ /**
62
+ * Apply a `divi/font` heading group preset: capability-gate, then POST
63
+ * to `/preset/create`. Mirrors `applyButtonPreset`'s sequence — the
64
+ * capability check runs BEFORE the write, and the write reuses the
65
+ * existing storage-routed route (no plugin route is added).
66
+ *
67
+ * The `pattern_variant` metadata is intentionally NOT in the wire body —
68
+ * variant selection is a client-side registry-gate decision and the
69
+ * server route accepts only the standard preset-create fields.
70
+ */
71
+ export declare function applyHeadingFontPreset(client: PresetWriteClient, entry: HeadingFontPresetEntry, opts: {
72
+ serverVersion: string;
73
+ dry_run?: boolean;
74
+ }): Promise<DiviopsResponse<unknown>>;