@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.
- package/README.md +34 -0
- package/dist/preset-cli/__tests__/button-emitter.test.d.ts +8 -0
- package/dist/preset-cli/__tests__/button-emitter.test.js +188 -0
- package/dist/preset-cli/__tests__/cli.test.d.ts +9 -0
- package/dist/preset-cli/__tests__/cli.test.js +330 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +13 -0
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +64 -0
- package/dist/preset-cli/__tests__/registry.test.d.ts +5 -0
- package/dist/preset-cli/__tests__/registry.test.js +149 -0
- package/dist/preset-cli/__tests__/write-path.test.d.ts +8 -0
- package/dist/preset-cli/__tests__/write-path.test.js +174 -0
- package/dist/preset-cli/bin.d.ts +8 -0
- package/dist/preset-cli/bin.js +32 -0
- package/dist/preset-cli/button-emitter.d.ts +117 -0
- package/dist/preset-cli/button-emitter.js +218 -0
- package/dist/preset-cli/cli.d.ts +62 -0
- package/dist/preset-cli/cli.js +429 -0
- package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
- package/dist/preset-cli/heading-font-emitter.js +166 -0
- package/dist/preset-cli/registry.d.ts +121 -0
- package/dist/preset-cli/registry.js +192 -0
- package/dist/preset-cli/variable-token.d.ts +42 -0
- package/dist/preset-cli/variable-token.js +70 -0
- package/dist/preset-cli/write-path.d.ts +74 -0
- package/dist/preset-cli/write-path.js +110 -0
- 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>>;
|