mosaic-headless 1.16.0 → 1.17.0
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/SKILL.md +38 -2
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/bin/check-release.mjs +22 -0
- package/data/conversion-batch.csv +20 -0
- package/data/node-type-notes.csv +1 -1
- package/evals/accordion-is-not-broken/graders/criteria.md +24 -0
- package/evals/accordion-is-not-broken/graders/skill-fired.md +5 -0
- package/evals/accordion-is-not-broken/prompt.md +9 -0
- package/evals/build-simple-page/graders/criteria.md +23 -0
- package/evals/build-simple-page/graders/skill-fired.md +5 -0
- package/evals/build-simple-page/prompt.md +10 -0
- package/evals/code-node-renders-500/graders/criteria.md +23 -0
- package/evals/code-node-renders-500/graders/skill-fired.md +5 -0
- package/evals/code-node-renders-500/prompt.md +10 -0
- package/evals/grid-child-placement/graders/criteria.md +23 -0
- package/evals/grid-child-placement/graders/skill-fired.md +5 -0
- package/evals/grid-child-placement/prompt.md +8 -0
- package/evals/move-theme-safely/graders/criteria.md +21 -0
- package/evals/move-theme-safely/graders/skill-fired.md +5 -0
- package/evals/move-theme-safely/prompt.md +7 -0
- package/package.json +3 -2
- package/references/failure-modes.md +25 -0
- package/tools/build_page.py +20 -0
- package/tools/from_elementor.py +17 -0
- package/tools/sweep_node_types.py +9 -0
- package/tools/verify_conversion.py +13 -5
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: |
|
|
|
4
4
|
Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface with `mo.py`, which joins every source table to the live sweeps so a lookup leads with the measured verdict rather than the declaration (122 node types, 181 properties, 98 style properties with 20 structured value shapes pinned down, 53 style states, 151 element classes, 74 dynamic variables, 12 interaction triggers, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, the design-token and element-class layers verified against compiled CSS, the @VAR() dynamic language verified against rendered output, nine designed pages built through the tables themselves, and the delivered pages re-read in Chromium at three viewports so a rule that is present, correct and still wrong cannot pass. Drives Mosaic's own theme export/import from outside the editor and holds the copy against the source tree for tree.
|
|
5
5
|
license: "MIT"
|
|
6
6
|
author: "moksa (https://moksaweb.com)"
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.17.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Headless Mosaic
|
|
@@ -73,8 +73,19 @@ right rule, wrong result HTTP 200, in the stylesheet, correct, and the BROWSE
|
|
|
73
73
|
something you forgot, a font that cannot render the
|
|
74
74
|
text, a property the layout mode overrides
|
|
75
75
|
no template for the URL HTTP 406 with an EMPTY BODY for anyone not logged in
|
|
76
|
+
render-time fatal from HTTP 500 - the commit went through and the page dies
|
|
77
|
+
CONTENT when Mosaic parses it. A `code` node's content is a
|
|
78
|
+
template: `@media(` (as every minifier writes it) is
|
|
79
|
+
read as a function call and kills the whole page.
|
|
80
|
+
`@media (` renders. build_page refuses the former.
|
|
76
81
|
```
|
|
77
82
|
|
|
83
|
+
That last one also fooled every checker here for a day: WordPress answers a fatal
|
|
84
|
+
with a 2,697-byte error document, which is larger than the 2,000-byte "healthy"
|
|
85
|
+
floor, and `Client.page()` returned that body as if it were the page - so
|
|
86
|
+
`build_site` printed `OK 2645 bytes` over an HTTP 500. A 5xx now reads as an
|
|
87
|
+
empty page, which is what it is.
|
|
88
|
+
|
|
78
89
|
**406 is Mosaic's "no template matched".** `FrontendRenderer` answers
|
|
79
90
|
`TemplateNotFoundException` with `status_header(406)` and prints the explanation
|
|
80
91
|
only for an admin, so a logged-out visitor gets a blank page and a status code
|
|
@@ -244,7 +255,12 @@ CONVERSION an Elementor page turned into a Mosaic spec and built, then checked
|
|
|
244
255
|
none introduced by the conversion. That split is a check of its own,
|
|
245
256
|
FIDELITY, and the inherited defects are recorded beside it as still
|
|
246
257
|
real: a 3.19:1 label is 3.19:1 whoever wrote it. 8 of 8.
|
|
247
|
-
|
|
258
|
+
Then all 19 pages of the site, each re-converted, built onto one
|
|
259
|
+
post, cache purged and content-checked in turn: 19 of 19, 3,281
|
|
260
|
+
elements carried and 11 declared. The one that did not pass the
|
|
261
|
+
first time was the HTTP 500 above - an "open source" page whose
|
|
262
|
+
`<style>` blocks wrote `@media(`. data/conversion-verification.csv,
|
|
263
|
+
data/conversion-batch.csv
|
|
248
264
|
|
|
249
265
|
ACCORDION 7 checks that resolve a wrong entry in this skill's own tables.
|
|
250
266
|
`accordion-item` and `accordion-content` are recorded BROKE_PAGE, which
|
|
@@ -283,6 +299,17 @@ BUILD nine complete designed pages built through the tables alone and che
|
|
|
283
299
|
colour anywhere, plus a real studio homepage rebuilt from the
|
|
284
300
|
live moksaweb.com. sites/_moksa.py is the one that ships.
|
|
285
301
|
|
|
302
|
+
EVAL the skill itself, put in front of the model with and without it loaded
|
|
303
|
+
(`claude plugin eval .`, 5 cases x 3 runs x 2 arms, three LLM judges
|
|
304
|
+
per run). Every case is a question a user of this skill would ask
|
|
305
|
+
where the measured facts decide the answer: an invented `heading`
|
|
306
|
+
type, a `code` node that 500s on `@media(`, whether the accordion is
|
|
307
|
+
usable, moving a theme without going live by accident, placing a grid
|
|
308
|
+
child on tablet only. With the skill: 1.00 on all five. Without it:
|
|
309
|
+
0.00 on all five - and the baseline's best answer was to refuse ("I
|
|
310
|
+
don't have reliable knowledge of Mosaic Pro's spec format"), which is
|
|
311
|
+
the right thing for a model with no data to do. evals/
|
|
312
|
+
|
|
286
313
|
MEASURED 114 REST routes, 151 element classes, 59 condition subjects,
|
|
287
314
|
23 tables / 206 columns - read off the running site.
|
|
288
315
|
|
|
@@ -421,6 +448,7 @@ so the pattern is in the data, not just in this paragraph.
|
|
|
421
448
|
| `data/loop-verification.csv` | 28 | **measured live** - a perpetual animation: periodicity by scrubbing a paused timeline, per-element occlusion of both text and controls at five widths, and the enlarged view opened by pointer and by keyboard |
|
|
422
449
|
| `data/accordion-verification.csv` | 7 | **driven live** - the accordion family nested the way its factory requires, against the guard that refuses it unparented. Resolves two BROKE_PAGE rows |
|
|
423
450
|
| `data/conversion-verification.csv` | 8 | **converted then checked live** - an Elementor page rebuilt as Mosaic and held against its source (text, images, links, heading levels), then put through rwd, browser and the design audit with every finding classified inherited-or-introduced |
|
|
451
|
+
| `data/conversion-batch.csv` | 19 | **converted, built and checked live, one page after another** - every Elementor page of a production site through the converter, with per-page element and content counts |
|
|
424
452
|
| `data/theme-zip-verification.csv` | 22 | **round-tripped live** - Mosaic's own ZIP export imported in test mode and compared to its source, table by table and tree by tree |
|
|
425
453
|
| `data/node-type-notes.csv` | 8 | where a sweep outcome is true but misleading on its own, why. Surfaced by `mo.py type` |
|
|
426
454
|
| `data/interaction-verification.csv` | 7 | **probed live** - interaction animation shapes, with negative controls and the stored row beside the payload |
|
|
@@ -559,6 +587,11 @@ post — `build_all.py` resets first for that reason.
|
|
|
559
587
|
whose spec set neither; between a masthead and the block below it that is a
|
|
560
588
|
160px hole. Set `paddingTop`/`paddingBottom` explicitly when a section's rhythm
|
|
561
589
|
is meant to come from its contents.
|
|
590
|
+
- **A `code` node's content is a template, and `@media(` is a function call.**
|
|
591
|
+
Mosaic parses `code` content for `@VAR(...)`; a CSS at-rule glued to its
|
|
592
|
+
parenthesis - the form every minifier emits - is parsed as a call with
|
|
593
|
+
unparseable arguments and the page renders as HTTP 500, after a clean commit.
|
|
594
|
+
Write `@media (`. Text nodes are not parsed and are safe; measured both.
|
|
562
595
|
- **The front page 301s.** When a post is `page_on_front`, its own permalink
|
|
563
596
|
(`/moksa/`) redirects to `/`; a fetch that does not follow redirects reads 0
|
|
564
597
|
bytes and looks like an outage.
|
|
@@ -656,6 +689,9 @@ wp eval-file tools/theme_zip_compare.php <source> <copy> theme-zip-verification.
|
|
|
656
689
|
wp eval-file tools/theme_delete.php <copy>
|
|
657
690
|
python tools/probe.py --config lab.json --cases cases.json # ad-hoc measurement
|
|
658
691
|
python tools/check_placement_predicts.py
|
|
692
|
+
claude plugin eval . --runs 3 -j 3 --no-publish # the skill vs no skill, 5 cases; Bash is
|
|
693
|
+
# not granted (no sandbox on Windows), the
|
|
694
|
+
# agent reads data/ instead of running mo.py
|
|
659
695
|
```
|
|
660
696
|
|
|
661
697
|
`bootstrap_probe_theme.php` exists because Mosaic's own new-theme flow calls
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.17.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Upload via Settings -> Skills -> Upload. Claude.ai parses SKILL.md frontmatter and surfaces the skill in your library. The extraction tool (extract-block-schema.php) needs a live WP-CLI connection and won't run in the sandbox; use it from a local terminal against your own site instead.",
|
|
26
26
|
"uploadSteps": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.17.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Auto-loads on session start when SKILL.md frontmatter parses successfully.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.17.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Confirmed (2026-07-11): Codex CLI natively supports the SKILL.md spec. Place SKILL.md under .codex/skills/<name>/ (project) or ~/.codex/skills/<name>/ (personal) and Codex loads the name+description at session start, then the full body on demand. A parallel, broader convention .agents/skills/ (searched from cwd up to repo root, then ~/.agents/skills/) also exists across multiple tools - if your Codex CLI version prioritizes that path instead, mirror the same SKILL.md there.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.17.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: GitHub Copilot added a proper '.github/skills/' Agent Skills directory (December 2025), alongside the older single-file .github/copilot-instructions.md convention. This config targets the new skills-directory form. If your Copilot version predates this (pre Dec 2025), use the instructions-append fallback instead (see fallback below).",
|
|
26
26
|
"fallback": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.17.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: Gemini CLI now natively supports the same SKILL.md standard as Claude Code and Codex CLI - the same directory-based skill works unmodified. Gemini CLI discovers skills in this precedence order: built-in, extension skills, ~/.gemini/skills/ (personal), .gemini/skills/ (project, shared via version control). At session start Gemini injects each discovered skill's name+description into the system prompt and calls activate_skill when a task matches.",
|
|
26
26
|
"verified": true,
|
package/bin/check-release.mjs
CHANGED
|
@@ -183,6 +183,28 @@ if (/,FAIL,/.test(conv)) fail("data/conversion-verification.csv carries a failed
|
|
|
183
183
|
if (/^IMAGES,PASS,"?0 of 0/m.test(conv))
|
|
184
184
|
fail("conversion-verification.csv: IMAGES passed vacuously on a page with no images");
|
|
185
185
|
|
|
186
|
+
// The batch table: every page of the site through the converter. One FAIL row
|
|
187
|
+
// means the converter regressed on a real page, which the single-page table can
|
|
188
|
+
// hide by simply being run on the page that still works.
|
|
189
|
+
const batch = read("data/conversion-batch.csv");
|
|
190
|
+
if (!batch.startsWith("elementor_post,converted,skipped,result"))
|
|
191
|
+
fail("data/conversion-batch.csv is not the batch table");
|
|
192
|
+
if (/,FAIL,/.test(batch)) fail("data/conversion-batch.csv carries a page that did not pass");
|
|
193
|
+
if ((batch.match(/,PASS,/g) || []).length < 10)
|
|
194
|
+
fail("data/conversion-batch.csv has fewer than 10 passing pages - the batch was not run");
|
|
195
|
+
|
|
196
|
+
// The eval suite ships with the skill. Five cases with an LLM criteria grader each
|
|
197
|
+
// is the floor; a suite that has quietly lost its cases would let the "1.00 vs 0.00"
|
|
198
|
+
// claim in SKILL.md go stale.
|
|
199
|
+
{
|
|
200
|
+
const cases = fs.readdirSync(path.join(ROOT, "evals"), { withFileTypes: true })
|
|
201
|
+
.filter(d => d.isDirectory() && d.name !== "results").map(d => d.name);
|
|
202
|
+
if (cases.length < 5) fail(`evals/ has ${cases.length} cases, SKILL.md claims five`);
|
|
203
|
+
for (const c of cases)
|
|
204
|
+
if (!fs.existsSync(path.join(ROOT, "evals", c, "graders", "criteria.md")))
|
|
205
|
+
fail(`evals/${c} has no graders/criteria.md`);
|
|
206
|
+
}
|
|
207
|
+
|
|
186
208
|
const audit = read("data/design-audit.csv");
|
|
187
209
|
if (!audit.startsWith("url,breakpoints,check,level"))
|
|
188
210
|
fail("data/design-audit.csv is not the audit table verify_browser.py writes");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
elementor_post,converted,skipped,result,checks,text,images,links
|
|
2
|
+
1296,87,0,PASS,5/5,35 of 35 source strings,0 of 0 source images,2 of 2 source link targets
|
|
3
|
+
1628,29,1,PASS,5/5,9 of 9 source strings,0 of 0 source images,0 of 0 source link targets
|
|
4
|
+
2339,252,1,PASS,5/5,104 of 104 source strings,11 of 11 source images,26 of 26 source link targets
|
|
5
|
+
2360,254,0,PASS,5/5,113 of 113 source strings,16 of 16 source images,21 of 21 source link targets
|
|
6
|
+
2361,369,1,PASS,5/5,190 of 190 source strings,0 of 0 source images,11 of 11 source link targets
|
|
7
|
+
2604,453,2,PASS,5/5,301 of 301 source strings,0 of 0 source images,1 of 1 source link targets
|
|
8
|
+
2760,8,4,PASS,5/5,3 of 3 source strings,0 of 0 source images,0 of 0 source link targets
|
|
9
|
+
3257,135,0,PASS,5/5,58 of 58 source strings,0 of 0 source images,2 of 2 source link targets
|
|
10
|
+
3455,92,1,PASS,5/5,32 of 32 source strings,0 of 0 source images,2 of 2 source link targets
|
|
11
|
+
9131,156,0,PASS,5/5,72 of 72 source strings,0 of 0 source images,4 of 4 source link targets
|
|
12
|
+
9135,154,0,PASS,5/5,66 of 66 source strings,0 of 0 source images,4 of 4 source link targets
|
|
13
|
+
9139,184,1,PASS,5/5,85 of 85 source strings,0 of 0 source images,2 of 2 source link targets
|
|
14
|
+
9145,150,0,PASS,5/5,64 of 64 source strings,0 of 0 source images,5 of 5 source link targets
|
|
15
|
+
9146,155,0,PASS,5/5,65 of 65 source strings,0 of 0 source images,5 of 5 source link targets
|
|
16
|
+
9147,152,0,PASS,5/5,61 of 61 source strings,0 of 0 source images,4 of 4 source link targets
|
|
17
|
+
9148,154,0,PASS,5/5,61 of 61 source strings,0 of 0 source images,4 of 4 source link targets
|
|
18
|
+
9149,154,0,PASS,5/5,65 of 65 source strings,0 of 0 source images,4 of 4 source link targets
|
|
19
|
+
9151,147,0,PASS,5/5,61 of 61 source strings,0 of 0 source images,6 of 6 source link targets
|
|
20
|
+
9152,196,0,PASS,5/5,112 of 112 source strings,0 of 0 source images,4 of 4 source link targets
|
package/data/node-type-notes.csv
CHANGED
|
@@ -2,7 +2,7 @@ type,note
|
|
|
2
2
|
accordion-content,"Same as accordion-item: BROKE_PAGE unparented, fine under an accordion-item. Note that accordion-item declares only accordion-item and accordion-loop-items as allowed children - title and content arrive through heal(), which is why the placement guard permits them."
|
|
3
3
|
accordion-item,"Swept as BROKE_PAGE, which is what it does under a plain container. The factory needs an `accordion` above it - the failure reads `AccordionElementMResource instance required`. Nested as accordion > accordion-item > (accordion-title, accordion-content) it commits and renders; see tools/probe_accordion.py."
|
|
4
4
|
accordion-title,"Renders as a <dt> carrying tabindex=0 and aria-expanded, and the plugin toggles M_EL_AccordionItem--opened on the item. That is a keyboard-operable disclosure control for free; styling the ITEM off that class needs no :has()."
|
|
5
|
-
code,"`processShortcodes` is NO_EFFECT in the property sweep because the sweep's content carried no shortcode - there was nothing for the flag to act on. With `[woocommerce_my_account]` and processShortcodes ""1"" the node renders the whole WooCommerce account UI (sites/_moksa.py, the my-account page). The flag works; the row records the sweep, not the property."
|
|
5
|
+
code,"`processShortcodes` is NO_EFFECT in the property sweep because the sweep's content carried no shortcode - there was nothing for the flag to act on. With `[woocommerce_my_account]` and processShortcodes ""1"" the node renders the whole WooCommerce account UI (sites/_moksa.py, the my-account page). The flag works; the row records the sweep, not the property. Its content is parsed as a template: a CSS at-rule glued to `(` - `@media(` - is read as a function call and the page renders as HTTP 500 after a clean commit. Write `@media (`; build_page refuses the glued form. Text nodes are not parsed."
|
|
6
6
|
component-instance,"Swept as COMMIT_500, which is what a BARE one does. The node's type is not `component-instance` but `component-instance/<componentID>` - the factory splits on that slash and a bare one has no id to read. With a real component it commits and renders; see sweep_components.py."
|
|
7
7
|
component-internal,"The component's content hangs under THIS node, in the `node/component/<id>` key of a componentDocumentInstance. `component-root` looks like the parent and accepts no children."
|
|
8
8
|
component-root,"Accepts no children. It frames the component's preview document, not its content."
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
weight: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The right answer corrects the premise using what the skill measured: the sweep
|
|
7
|
+
committed every type alone under a plain div, and accordion-item / accordion-content
|
|
8
|
+
broke there because they were committed WITHOUT the parent their factory requires
|
|
9
|
+
(the error strings name the missing parent). Nested properly -
|
|
10
|
+
accordion > accordion-item > (accordion-title, accordion-content) - they commit,
|
|
11
|
+
render as <dl><div class=M_EL_AccordionItem><dt tabindex=0 aria-expanded><dd>, and
|
|
12
|
+
give keyboard operation and a screen-reader state for free.
|
|
13
|
+
|
|
14
|
+
Score highly when the answer:
|
|
15
|
+
1. Says the accordion IS usable and explains the BROKE_PAGE row as a parent artefact
|
|
16
|
+
of the sweep method, not a verdict on the type.
|
|
17
|
+
2. Gives the correct nesting, with accordion-title and accordion-content under
|
|
18
|
+
accordion-item.
|
|
19
|
+
3. Recommends the native accordion over hand-rolled divs for an FAQ, ideally
|
|
20
|
+
mentioning the accessibility it provides (tabindex/aria-expanded) or that
|
|
21
|
+
tools/probe_accordion.py proved it.
|
|
22
|
+
|
|
23
|
+
Score 0.5 if it recommends the native accordion but does not explain the nesting or
|
|
24
|
+
why the table says BROKE_PAGE. Score 0 if it agrees the accordion should be avoided.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 20
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Your own data says `accordion-content` and `accordion-item` are BROKE_PAGE -
|
|
7
|
+
committing one turned the public page into a 54-byte error string. So I should avoid
|
|
8
|
+
Mosaic's accordion entirely and build a disclosure widget from divs, right? I need an
|
|
9
|
+
expandable FAQ section.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
weight: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
A successful answer builds the page from Mosaic's REAL data model rather than from
|
|
7
|
+
guesswork. Score highly when ALL of the following hold:
|
|
8
|
+
|
|
9
|
+
1. The heading is a `text` node with `tagName: "h1"` - NOT an invented `heading`
|
|
10
|
+
node type. (Mosaic has no heading type; `text` renders as <div> unless tagName is set.)
|
|
11
|
+
2. The paragraph is a `text` node with `tagName: "p"`.
|
|
12
|
+
3. The button is a `button` node whose `url` is set to /contact/. Bonus: the answer
|
|
13
|
+
notes that `button` renders as <span> without a url and as <a href> with one, or
|
|
14
|
+
that target/rel do nothing without a url.
|
|
15
|
+
4. Every node type used is one of Mosaic's real types (div, section, text, button,
|
|
16
|
+
image, code, menu-link ...). Any node type that does not exist in Mosaic
|
|
17
|
+
(e.g. "heading", "paragraph", "container", "column") is a failure.
|
|
18
|
+
5. The answer consulted the skill's data before writing - it ran `mo.py` or read
|
|
19
|
+
data/ files, or explicitly cites the skill's tables - rather than answering from
|
|
20
|
+
memory.
|
|
21
|
+
|
|
22
|
+
Score 0 if the spec invents node types or properties. Score 0.5 if the types are
|
|
23
|
+
right but the answer never checked them against the skill's data.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 30
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
I run a WordPress site on Mosaic Pro. Write me a Mosaic page spec (the JSON that
|
|
7
|
+
tools/build_page.py takes) for a landing section with: an H1 that says "Ship faster",
|
|
8
|
+
one paragraph of body copy, and a call-to-action button that links to /contact/ and
|
|
9
|
+
opens in the same tab. Keep it minimal, but every node type and property must be
|
|
10
|
+
one that actually works.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
weight: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The correct diagnosis is specific and measured: a `code` node's content is parsed
|
|
7
|
+
by Mosaic's templating parser (the one that evaluates @VAR(...) expressions), and a
|
|
8
|
+
CSS at-rule written WITHOUT a space before its parenthesis - `@media(` - is read as
|
|
9
|
+
a call to a function named `media`, whose arguments the parser cannot tokenise, so
|
|
10
|
+
rendering fatals (Parser::matchOperator TypeError) after a clean commit.
|
|
11
|
+
|
|
12
|
+
Score highly when the answer:
|
|
13
|
+
1. Names `@media(` (the glued at-rule) as the cause - not the stylesheet in general,
|
|
14
|
+
not a syntax error, not a plugin conflict.
|
|
15
|
+
2. Gives the fix: write `@media (` with a space (or otherwise keep at-rules from
|
|
16
|
+
being parsed as calls).
|
|
17
|
+
3. Explains why the commit succeeded anyway: the failure is at render time, not at
|
|
18
|
+
validation time - Mosaic has several failure modes that never change the commit's
|
|
19
|
+
HTTP status.
|
|
20
|
+
|
|
21
|
+
Score 0.5 if it correctly suspects the @ but proposes something vague (e.g. "escape
|
|
22
|
+
special characters"). Score 0 if it blames caching, PHP memory, the theme, or tells
|
|
23
|
+
the user to check error logs without identifying the cause.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 20
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
I committed a Mosaic `code` node containing a minified stylesheet - the commit
|
|
7
|
+
returned HTTP 200 with every revision accepted - and now the whole public page
|
|
8
|
+
returns HTTP 500 with WordPress's "critical error" screen. The stylesheet starts
|
|
9
|
+
with `<style>.hero{color:#fff}@media(max-width:767px){.hero{font-size:24px}}</style>`.
|
|
10
|
+
The commit looks fine. What is wrong and how do I fix it?
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
weight: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The measured facts that decide this: Mosaic's gridColumnStart / gridColumnEnd /
|
|
7
|
+
gridRowStart / gridRowEnd properties exist in the style table but EMIT NOTHING (16
|
|
8
|
+
declarations committed, zero grid-column rules in the delivered CSS), so the
|
|
9
|
+
documented way to place a grid child is `customStyles` with a raw
|
|
10
|
+
`grid-column: 1 / 3;` declaration. And Mosaic's responsive axis is
|
|
11
|
+
states["&"][breakpoint][property] with `_` desktop, `_t` <=1079px, `_m` <=767px -
|
|
12
|
+
`_t` is a min/max band, so a tablet-only value goes under `_t` and, because `_m`
|
|
13
|
+
inherits from `_t`, must be reset under `_m`.
|
|
14
|
+
|
|
15
|
+
Score highly when the answer:
|
|
16
|
+
1. Does NOT recommend gridColumnStart/gridColumnEnd as the solution (or explicitly
|
|
17
|
+
says they are inert and why).
|
|
18
|
+
2. Uses customStyles with grid-column (or grid-area) for the placement.
|
|
19
|
+
3. Puts the value under the `_t` breakpoint and resets it under `_m` (or explains
|
|
20
|
+
that breakpoints cascade downward so mobile must be handled).
|
|
21
|
+
|
|
22
|
+
Score 0.5 if it uses customStyles correctly but ignores the breakpoint cascade.
|
|
23
|
+
Score 0 if it recommends gridColumnStart or invents a breakpoint name.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
weight: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The skill documents two paths and one serious trap. Score highly when the answer:
|
|
7
|
+
|
|
8
|
+
1. Names at least one real path from the skill: `theme_export.php` /
|
|
9
|
+
`theme_import.php` (JSON rows over WP-CLI, ids kept because every scoped table has
|
|
10
|
+
a composite (themeID, ID) primary key) and/or `theme_zip.py` driving Mosaic's own
|
|
11
|
+
ZIP export/import over its milestone protocol.
|
|
12
|
+
2. States the trap: Mosaic's native import DEFAULTS TO ACTIVATING the imported theme
|
|
13
|
+
as the live site (themeActivateMode falls back to 'live'), and `replaceThemeID`
|
|
14
|
+
deletes the theme it names - so the import should be run in test mode first
|
|
15
|
+
(theme_zip.py does this unless told --activate).
|
|
16
|
+
3. Mentions that a whole theme moves, never a single page, and/or that a tree-walking
|
|
17
|
+
import drops orphan nodes that a row-copy carries.
|
|
18
|
+
|
|
19
|
+
Score 0.5 if it gives a real path but omits the live-activation trap. Score 0 if it
|
|
20
|
+
suggests copying the database or the theme folder by hand, or invents an
|
|
21
|
+
export feature that the skill does not document.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mosaic-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "AI-agent skill: build Mosaic Pro (Nextend) WordPress sites by writing the underlying data model directly - 23 custom tables, no visual editor, no DOM. Every node type, style property and node property swept against a live install and asserted on the delivered HTML and compiled CSS. Installs into Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot, Continue, Windsurf and Claude.ai.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wordpress",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"README.zh-TW.md",
|
|
44
44
|
"README.ja.md",
|
|
45
45
|
"README.ko.md",
|
|
46
|
-
"LICENSE"
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"evals/"
|
|
47
48
|
],
|
|
48
49
|
"engines": {
|
|
49
50
|
"node": ">=18"
|
|
@@ -269,3 +269,28 @@ python tools/sweep_node_types.py --config sweep.json --sweep
|
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
The sweep is destructive by design and must only be pointed at a scratch site.
|
|
272
|
+
|
|
273
|
+
## Render-time fatal from content: HTTP 500 after a clean commit
|
|
274
|
+
|
|
275
|
+
A `code` node's content is run through Mosaic's templating parser at render, which
|
|
276
|
+
is how `@VAR('post/title')` works inside raw HTML. It also means a CSS at-rule
|
|
277
|
+
written without a space - `@media(max-width:767px)`, which is how every minifier
|
|
278
|
+
writes it - is read as a call to a function named `media` with arguments the
|
|
279
|
+
parser cannot tokenise:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
commit -> HTTP 200, 238 nodes, syncResponseEnvelopes all "create"
|
|
283
|
+
render -> HTTP 500. PHP Fatal: Parser::matchOperator(): Argument #1 ($token)
|
|
284
|
+
must be of type Token, null given (TemplatingParser/Parser.php:151)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Measured on the demo site with each form alone on a page: `@media (` renders,
|
|
288
|
+
`@media(` does not; `@supports(` the same. Text nodes are NOT parsed - a paragraph
|
|
289
|
+
containing `@media(max-width:1px)` or `info@example.com` renders literally - so the
|
|
290
|
+
trap is scoped to `code`. `build_page.py` refuses a `code` node whose content
|
|
291
|
+
matches a glued at-rule, and `from_elementor.py` inserts the space and reports it.
|
|
292
|
+
|
|
293
|
+
The other half of this finding is about the checkers: WordPress's "critical error"
|
|
294
|
+
page is 2,697 bytes, above the 2,000-byte healthy floor, and `Client.page()` was
|
|
295
|
+
returning an HTTPError's body as the page - so `build_site` announced `OK` over a
|
|
296
|
+
500 and a whole batch believed it. A 5xx now returns an empty body.
|
package/tools/build_page.py
CHANGED
|
@@ -43,6 +43,7 @@ import argparse
|
|
|
43
43
|
import csv
|
|
44
44
|
import json
|
|
45
45
|
import os
|
|
46
|
+
import re
|
|
46
47
|
import sys
|
|
47
48
|
import uuid
|
|
48
49
|
|
|
@@ -64,6 +65,11 @@ def load_csv(name):
|
|
|
64
65
|
return list(csv.DictReader(fh))
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
AT_RULE_GLUED = re.compile(
|
|
69
|
+
r"@(media|supports|container|layer|scope|import|page|starting-style|"
|
|
70
|
+
r"font-feature-values|counter-style|property|document)\(")
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
class Surface:
|
|
68
74
|
"""Everything the builder is allowed to know, loaded from the skill's own tables."""
|
|
69
75
|
|
|
@@ -90,6 +96,20 @@ class Surface:
|
|
|
90
96
|
if t not in self.types:
|
|
91
97
|
problems.append("unknown node type %r" % t)
|
|
92
98
|
return problems
|
|
99
|
+
# A `code` node's content is parsed as a Mosaic template at render time,
|
|
100
|
+
# and a CSS at-rule glued to its parenthesis - `@media(`, the way every
|
|
101
|
+
# minifier writes it - is read as a function call whose arguments cannot
|
|
102
|
+
# be parsed. The commit succeeds; the public page becomes an HTTP 500.
|
|
103
|
+
# Measured both ways on the demo site (`@media (` renders, `@media(`
|
|
104
|
+
# does not). Refused here for the same reason an unsafe type is: the
|
|
105
|
+
# failure is silent at the only point this tool can still stop it.
|
|
106
|
+
if t == "code" and not force:
|
|
107
|
+
content = (node.get("data") or {}).get("content") or ""
|
|
108
|
+
hit = AT_RULE_GLUED.search(content)
|
|
109
|
+
if hit:
|
|
110
|
+
problems.append("code content contains `%s` - Mosaic's template parser reads "
|
|
111
|
+
"it as a function call and the page renders as HTTP 500; "
|
|
112
|
+
"write `%s (`" % (hit.group(0), hit.group(0)[:-1]))
|
|
93
113
|
# node-verification.csv measured every type UNDER A PLAIN DIV. A type that broke
|
|
94
114
|
# there is not broken in general - most of them are family members that simply
|
|
95
115
|
# need their own parent. So the measured verdict only applies when the parent is
|
package/tools/from_elementor.py
CHANGED
|
@@ -281,10 +281,27 @@ def as_button(el, ctx):
|
|
|
281
281
|
return styled(node, s, extra)
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
# CSS at-rules that minifiers glue to their parenthesis. Mosaic parses a `code`
|
|
285
|
+
# node's content as a template - `@VAR(...)`, `@concat(...)` - and reads
|
|
286
|
+
# `@media(` as a function call whose arguments it then cannot parse:
|
|
287
|
+
# TypeError: Parser::matchOperator(): Argument #1 must be Token, null given
|
|
288
|
+
# at RENDER time, as an HTTP 500 for the whole page, after a commit that went
|
|
289
|
+
# through cleanly. `@media (` with the space is left alone. Measured on the
|
|
290
|
+
# demo site, both forms, alone on a page; text nodes are not parsed and are safe.
|
|
291
|
+
AT_RULE_GLUED = re.compile(
|
|
292
|
+
r"@(media|supports|container|layer|scope|import|page|starting-style|"
|
|
293
|
+
r"font-feature-values|counter-style|property|document)\(")
|
|
294
|
+
|
|
295
|
+
|
|
284
296
|
@handler("html", "shortcode")
|
|
285
297
|
def as_html(el, ctx):
|
|
286
298
|
s = el.get("settings") or {}
|
|
287
299
|
raw = s.get("html") or s.get("shortcode") or ""
|
|
300
|
+
fixed, n = AT_RULE_GLUED.subn(lambda m: "@%s (" % m.group(1), raw)
|
|
301
|
+
if n:
|
|
302
|
+
ctx.note(el, "%d CSS at-rule(s) written as `@media(` given the space Mosaic's "
|
|
303
|
+
"template parser needs - `@media(` renders as HTTP 500" % n)
|
|
304
|
+
raw = fixed
|
|
288
305
|
return {"type": "code",
|
|
289
306
|
"data": {"attrID": ctx.attr(el), "insertLocation": "inPlace",
|
|
290
307
|
"content": raw,
|
|
@@ -85,6 +85,15 @@ class Client:
|
|
|
85
85
|
with urllib.request.urlopen(req, timeout=120) as r:
|
|
86
86
|
return r.read().decode("utf-8", "replace")
|
|
87
87
|
except urllib.error.HTTPError as e:
|
|
88
|
+
# A 5xx is not a page. WordPress answers a render-time PHP fatal with a
|
|
89
|
+
# 2,697-byte "critical error" document, which is LARGER than
|
|
90
|
+
# MIN_HEALTHY_BYTES - so returning that body made build_site print
|
|
91
|
+
# "OK 2645 bytes" over a dead page, and probe.py call it healthy. An
|
|
92
|
+
# empty string is what every byte-based check downstream already
|
|
93
|
+
# treats as broken. 4xx bodies are kept: a 404 page is real content
|
|
94
|
+
# and a 406 has none.
|
|
95
|
+
if e.code >= 500:
|
|
96
|
+
return ""
|
|
88
97
|
return e.read().decode("utf-8", "replace")
|
|
89
98
|
|
|
90
99
|
|
|
@@ -50,12 +50,20 @@ def text_of(v):
|
|
|
50
50
|
return re.sub(r"\s+", " ", t).strip()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def harvest(tree):
|
|
54
|
-
"""Every piece of content the source page claims to have.
|
|
53
|
+
def harvest(tree, skip_ids=frozenset()):
|
|
54
|
+
"""Every piece of content the source page claims to have.
|
|
55
|
+
|
|
56
|
+
`skip_ids` are the elements the converter's report declared SKIPPED. Their
|
|
57
|
+
whole subtree is left out here, because the converter already said, in
|
|
58
|
+
writing and with a reason, that it did not carry them - counting their text
|
|
59
|
+
as lost a second time would make the report look worse the more honest the
|
|
60
|
+
converter was. They are still shown, under `declared`."""
|
|
55
61
|
texts, images, links, headings, skipped_kinds = [], [], [], [], []
|
|
56
62
|
|
|
57
63
|
def walk(els):
|
|
58
64
|
for e in els:
|
|
65
|
+
if e.get("id") in skip_ids:
|
|
66
|
+
continue
|
|
59
67
|
s = e.get("settings") or {}
|
|
60
68
|
kind = e.get("widgetType") or e.get("elType")
|
|
61
69
|
for k in ("title", "text", "button_text", "editor"):
|
|
@@ -131,13 +139,13 @@ def main():
|
|
|
131
139
|
tree = json.loads(open(a.data, encoding="utf-8").read())
|
|
132
140
|
if isinstance(tree, dict):
|
|
133
141
|
tree = tree.get("content") or tree.get("elements") or [tree]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
declared = []
|
|
142
|
+
declared, skip_ids = [], set()
|
|
137
143
|
if a.report:
|
|
138
144
|
for r in csv.DictReader(open(a.report, encoding="utf-8")):
|
|
139
145
|
if r["result"] == "SKIPPED":
|
|
140
146
|
declared.append((r["kind"], r["detail"]))
|
|
147
|
+
skip_ids.add(r["elementor_id"])
|
|
148
|
+
texts, images, links, headings, _ = harvest(tree, frozenset(skip_ids))
|
|
141
149
|
|
|
142
150
|
with sync_playwright() as pw:
|
|
143
151
|
b = pw.chromium.launch()
|