mosaic-headless 1.17.0 → 1.17.2

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 CHANGED
@@ -3,19 +3,91 @@
3
3
  [![npm downloads](https://img.shields.io/npm/dt/mosaic-headless?label=npm%20downloads&color=cb3837)](https://www.npmjs.com/package/mosaic-headless)
4
4
 
5
5
  Build and modify [Mosaic Pro](https://mosaicbuilder.com) (Nextend) sites by writing
6
- the data model directly — no visual editor, no DOM.
6
+ the data model directly — no visual editor, no DOM. Convert Elementor pages into it.
7
+ Move whole themes between installs. Every claim measured on a live site.
7
8
 
8
9
  *Read this in [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md)*
9
10
 
10
11
  ---
11
12
 
13
+ ## Install
14
+
15
+ ```bash
16
+ npx mosaic-headless # interactive: pick a platform
17
+ npx mosaic-headless claude-code --global # Claude Code, into ~/.claude/skills/
18
+ npx mosaic-headless cursor --to ./my-project
19
+ npx mosaic-headless --list # all eight platforms
20
+ ```
21
+
22
+ | platform | what is installed | where |
23
+ |---|---|---|
24
+ | Claude Code | full skill: SKILL.md + references/ + tools/ + data/ + sites/ | `~/.claude/skills/` or `./.claude/skills/` |
25
+ | Codex CLI | full skill | `~/.codex/` |
26
+ | Gemini CLI | full skill | `~/.gemini/` |
27
+ | GitHub Copilot | full skill, plus a section appended to `copilot-instructions.md` | `./.github/` |
28
+ | Cursor | one `.mdc` rule with the references embedded | `~/.cursor/rules/` |
29
+ | Windsurf | one rule file with the references embedded | `./.devin/` |
30
+ | Continue | one rule file with the references embedded | `~/.continue/` |
31
+ | Claude.ai | a zip to upload as a project skill | wherever you save it |
32
+
33
+ Every platform install is verified by the release gate against its template. The
34
+ tools need Python 3 and Playwright to run; the rule-file platforms get the knowledge
35
+ without the tools.
36
+
37
+ **Updating does not happen on its own.** A new version on npm changes nothing in
38
+ the folder your agent loads; re-run the installer with `--force` (without it, it
39
+ refuses to overwrite a SKILL.md you may have edited):
40
+
41
+ ```bash
42
+ npx mosaic-headless@latest claude-code --global --force
43
+ ```
44
+
45
+
46
+ ## What this is
47
+
12
48
  Mosaic keeps a page in **23 custom database tables**, not in `post_content` and not
13
49
  in `postmeta`. One row per element, the tree carried by a `parentID` column, sibling
14
50
  order by a fractional-index string. The editor is one client of that model. It is not
15
51
  the format, and you do not need it.
16
52
 
17
53
  This skill is the map of that model — measured against a live install rather than
18
- read off the source.
54
+ read off the source — plus the tools to write through it, check what came out, and
55
+ bring pages in from Elementor.
56
+
57
+ ## How the pieces fit
58
+
59
+ ```mermaid
60
+ flowchart LR
61
+ subgraph measure["measured once, against a live install"]
62
+ SRC[plugin source] -->|extract_*.py| D[(data/*.csv)]
63
+ SW[sweep_*.py / probe_*.py] -->|commit, render, assert| D
64
+ end
65
+
66
+ subgraph write["every page you build"]
67
+ Q[mo.py] -->|one answer, verdict first| SPEC[page spec]
68
+ EL[Elementor _elementor_data] -->|from_elementor.py| SPEC
69
+ SPEC -->|build_page.py refuses what breaks| REST[Mosaic REST: checkout, check, commit]
70
+ REST --> DB[(23 tables)]
71
+ DB --> PAGE[served page]
72
+ end
73
+
74
+ subgraph verify["never trust the commit"]
75
+ PAGE --> V1[verify_rwd.py]
76
+ PAGE --> V2[verify_browser.py + design audit]
77
+ PAGE --> V3[verify_intro.py / verify_loop.py]
78
+ PAGE --> V4[verify_conversion.py]
79
+ V1 & V2 & V3 & V4 --> CSV[(verification CSVs)]
80
+ CSV --> GATE[check-release.mjs]
81
+ end
82
+
83
+ D --> Q
84
+ D --> SPEC
85
+ ```
86
+
87
+ Left to right: the tables are measured once and shipped; every page is written
88
+ through them and refused when they say no; and nothing is believed until the
89
+ delivered page has been read back and the result recorded in a table that the
90
+ release gate checks.
19
91
 
20
92
  ## The one rule
21
93
 
@@ -33,13 +105,14 @@ keyboard-operable disclosure. `mo.py type` shows all three at once.
33
105
  ```bash
34
106
  python tools/mo.py type accordion-content # one type, joined to every live sweep
35
107
  python tools/mo.py check div text button # exits 1 on an unsafe or unknown type
108
+ python tools/mo.py params text # everything settable on one type
36
109
  python tools/mo.py style --grouped # the 20 that are inert set on their own
37
110
  python tools/mo.py states --verified # the states measured to compile
38
- python tools/mo.py params text # everything settable on one type
111
+ python tools/mo.py css grid-column # which Mosaic key drives this CSS
39
112
  ```
40
113
 
41
- Then check the page. Mosaic has four failure modes and **only one of them changes the
42
- HTTP status code**:
114
+ Then check the page. Mosaic has **seven** failure modes and only two of them change
115
+ the HTTP status code:
43
116
 
44
117
  ```
45
118
  clean validator rejection HTTP 200 + an `exceptions` array in the body
@@ -50,10 +123,17 @@ wrong value SHAPE HTTP 200, stored, and the CSS rule is simply absent
50
123
  right rule, wrong result HTTP 200, in the stylesheet, correct, and the BROWSER
51
124
  computes something else
52
125
  no template for the URL HTTP 406 with an EMPTY BODY for anyone not logged in
126
+ render-time fatal from HTTP 500 - the commit went through, the page dies when
127
+ CONTENT Mosaic parses it. A `code` node's content is a template:
128
+ `@media(` as every minifier writes it is read as a
129
+ function call. `@media (` renders. build_page refuses
130
+ the former.
53
131
  ```
54
132
 
55
133
  A successful commit is not evidence of a working page, and neither is a correct
56
- stylesheet.
134
+ stylesheet. Every tool here fetches the page afterwards — and treats a 5xx as an
135
+ empty page, because WordPress's "critical error" screen is 2,697 bytes and larger
136
+ than any naive healthy-page floor.
57
137
 
58
138
  ## What was verified, and how
59
139
 
@@ -63,25 +143,47 @@ factories, so Pro types register and render regardless.
63
143
 
64
144
  | pass | result |
65
145
  |---|---|
66
- | **node types** | 122 / 122 swept one per document, committed → rendered → asserted → deleted: 70 RENDERED, 30 COMMITTED, 15 COMMIT_5xx, 7 BROKE_PAGE |
146
+ | **node types** | 122 / 122 swept one per document, committed → rendered → asserted → deleted: 70 RENDERED, 30 COMMITTED, 15 COMMIT_5xx, 7 BROKE_PAGE. Three of the non-rendering rows are artefacts of committing without the required parent, and say so beside the row |
67
147
  | **style properties** | 98 / 98 written to a live page and checked against the compiled CSS: 58 COMPILED, 18 ABSENT, 21 SKIPPED |
68
148
  | **node properties** | 181 / 181 re-probed with a value shaped by each property's own validator chain: 35 APPLIED, 42 NO_EFFECT, 55 NO_HOST, 47 SKIPPED |
69
149
  | **responsive** | 731 `_t`/`_m` declarations across two sites asserted against the stylesheet the site actually served — all verified |
70
- | **components** | the component system driven end to end, **8 of 8**: created under a category, document healed, tree filled through the writable instance, the read-only one refused the same write as a negative control, and two instances on a page rendering one definition twice |
71
- | **style states** | 52 of the 53 states written to a live page and matched against the selector the table promises: **36 compiled exactly**, 12 NO_HOST, 3 SKIPPED, 1 BROKE_PAGE. All seven globally usable states verified |
72
- | **interactions** | the JS animation path probed with negative controls and the row read back: `propertyMetas` **is** accepted and stored; the property values still do not bind, and the boundary is now exact |
73
- | **entrance animation** | the page-load sequence sampled at fifteen timestamps on a monotonic clock and asserted on eight counts — it plays, its animated `@property` counter reaches 100, the veil leaves hit-testing, nothing in the viewport is stranded at opacity 0, a real click reaches the document, under `prefers-reduced-motion` the veil never exists, and something is still moving once everything has settled. Costs one late frame over a page with no animation at all, because it waits for the document's first layout |
74
- | **perpetual animation** | a corner plate that keeps printing itself and opens to full size when tapped, **28 checks**: periodicity proved by scrubbing a paused timeline, occlusion of text *and* controls at five widths and twenty-five scroll stops with "never readable" as the failing condition, opened by pointer and by Enter, still under reduced motion. Built on Mosaic's own accordion |
75
- | **accordion** | `accordion-item` and `accordion-content` sit in the sweep table as BROKE_PAGE; nested as their factory requires they commit and render, **7 of 7**. The note now lives beside the row |
76
150
  | **browser** | 3,988 computed-style readings on two delivered pages in Chromium at three viewports: 2,929 compared and agreed, 912 not-comparable and labelled, **0 overridden** |
77
151
  | **design audit** | contrast, font fallback, CJK tracking, overflow, clipped text, line measure — run in the browser, **26 findings, every one ruled on in writing** — an acknowledgement without a reason is refused by the release gate |
78
- | **theme export/import** | two paths, both round-tripped. `theme_export.php` moves rows as JSON over WP-CLI, ids intact, and the copy served byte-identical pages. `theme_zip.py` drives Mosaic's **own** ZIP export/import over its milestone protocol — import lands in test mode unless told `--activate`, because the default is to switch the live site — and **22 checks** hold the copy against the source tree for tree: every table equal, 68,337 node ids kept, override nodes re-keyed, the one missing row an orphan no tree-walk should carry |
152
+ | **components** | the component system driven end to end, **8 of 8**: created under a category, document healed, tree filled through the writable instance, the read-only one refused the same write as a negative control, two instances on a page rendering one definition twice |
153
+ | **style states** | 52 of the 53 states written to a live page and matched against the selector the table promises: **36 compiled exactly**, 12 NO_HOST, 3 SKIPPED, 1 BROKE_PAGE. Pseudo-classes are emitted UPPERCASE (`.M_EL9:HOVER`) |
154
+ | **interactions** | the JS animation path probed with negative controls and the row read back: `propertyMetas` **is** accepted and stored; the property values still do not bind, and the boundary is now exact |
155
+ | **accordion** | `accordion-item` and `accordion-content` sit in the sweep table as BROKE_PAGE; nested as their factory requires they commit and render, **7 of 7** |
156
+ | **entrance animation** | the page-load sequence sampled at fifteen timestamps on a monotonic clock and asserted on eight counts. Costs one late frame over a page with no animation at all, because it waits for the document's first layout |
157
+ | **perpetual animation** | a corner plate that keeps printing itself and opens to full size when tapped, **28 checks**: periodicity by scrubbing a paused timeline, occlusion of text *and* controls at five widths and twenty-five scroll stops with "never readable" as the failing condition, opened by pointer and by Enter, still under reduced motion |
158
+ | **Elementor conversion** | every Elementor page of a production site — 19 pages, 3,292 elements — converted, built and checked against its source: **19 of 19**, 3,281 elements carried, 11 declared. Then the converted page through rwd, browser and the audit, with every finding classified inherited-or-introduced: **0 introduced** |
159
+ | **theme export/import** | two paths, both round-tripped. `theme_export.php` moves rows as JSON over WP-CLI, ids intact. `theme_zip.py` drives Mosaic's **own** ZIP export/import — import lands in test mode unless told `--activate`, because the default is to switch the live site — and **22 checks** hold the copy against the source tree for tree |
160
+ | **the skill itself** | `claude plugin eval .` — five cases a user would ask, three runs each, with and without the skill loaded, three LLM judges a run. **With: 1.00 on all five. Without: 0.00 on all five.** The baseline's best answer was to refuse |
79
161
  | **measured live** | 114 REST routes, 151 element classes, 59 condition subjects, 23 tables / 206 columns |
80
162
 
81
163
  `SKIPPED`, `NO_HOST` and `INCONCLUSIVE` are never folded into a pass rate. A sweep
82
164
  that scores its own blind spots as successes is the thing this skill argues against.
83
165
 
84
- ### Two results worth knowing before you write anything
166
+ ## What it costs to consult
167
+
168
+ Three ways an agent can learn what a Mosaic node type or style key actually takes,
169
+ priced on the same six tasks with tiktoken (`tools/benchmark_tokens.py`; run it
170
+ yourself):
171
+
172
+ | task | read the source | load every table | `mo.py` query |
173
+ |---|---:|---:|---:|
174
+ | place a heading, a paragraph and a linked button | 10,005 | 259,539 | **961** |
175
+ | set padding, a border and a radius, responsively | 3,490 | 259,539 | **396** |
176
+ | decide whether the accordion is usable, and how to nest it | 15,615 | 259,539 | **397** |
177
+ | find which hover/focus states actually compile | 3,619 | 259,539 | **1,054** |
178
+ | find which Mosaic key drives one CSS property | 1,862 | 259,539 | **51** |
179
+ | know what is unsafe before committing anything | 63,172 | 259,539 | **288** |
180
+
181
+ **71–99.5% fewer tokens than reading the source, 99.6%+ fewer than loading the
182
+ tables** — and the source could not have answered four of the six at all, because
183
+ "declared" and "compiles" are different questions and only the sweeps asked the
184
+ second one. The tables total 259,539 tokens; never load them. `mo.py` is the query.
185
+
186
+ ### Results worth knowing before you write anything
85
187
 
86
188
  **A property that belongs to a `group` is inert when set on its own.** Exact in both
87
189
  directions: 78 ungrouped properties gave 58 COMPILED and 0 ABSENT; all 20 grouped
@@ -90,24 +192,66 @@ three instances of one rule, not three oddities. Use the grouped shape — `bord
90
192
  takes `{width, style, color}` — or `customStyles`.
91
193
 
92
194
  **A breakpoint override can CHANGE a property but never REMOVE one.** Narrow-screen
93
- `customStyles` that merely omits a border leaves the wide-screen border standing,
94
- drawing rules down the middle of a collapsed layout. Say `border-left:0` out loud.
195
+ `customStyles` that merely omits a border leaves the wide-screen border standing.
196
+ Say `border-left:0` out loud.
95
197
 
96
- ## Tools
198
+ **Only four types take a `url`**: `button`, `menu-link`, `wysiwyg-link`,
199
+ `dropdown-toggle`. On a `text` or an `image` it is accepted, stored, and emits no
200
+ anchor. Wrap the thing in a `menu-link` instead — it takes any children and becomes
201
+ a real `<a href>`.
202
+
203
+ **An image's attachment-protocol path is relative to the uploads directory.**
204
+ `wp-attachment://image/<id>/full/2026/09/pic.png` resolves and carries the
205
+ attachment's width and height. Give it the full `wp-content/uploads/...` path — the
206
+ obvious guess — and Mosaic prefixes the uploads base a second time, silently.
207
+
208
+ ## Elementor → Mosaic
97
209
 
98
210
  ```bash
99
- wp eval-file tools/bootstrap_probe_theme.php # licence-free scratch theme
100
- python tools/build_site.py --config c.json --site sites/moksa.json
101
- python tools/verify_rwd.py --config c.json --site sites/moksa.json --csv rwd.csv
102
- python tools/copy_styles.py --config c.json --from a --to-prefix b- --only "&._m"
103
- wp eval-file tools/theme_export.php active > theme.json
104
- wp eval-file tools/theme_import.php theme.json "Name" rebind activate
211
+ wp post meta get 2360 _elementor_data > page.json
212
+ python tools/from_elementor.py --data page.json --out spec.json --report conv.csv \
213
+ --uploads-base https://site/wp-content/uploads --slug works --post 208
214
+ python tools/build_site.py --config c.json --site spec.json
215
+ python tools/verify_conversion.py --data page.json --url https://site/works/ --report conv.csv
105
216
  ```
106
217
 
107
- `sites/_moksa.py` is the worked example: a real studio homepage — masthead, spec
108
- block, services, a nine-row work table, process, stack, products, testimonials,
109
- contact — 618 nodes committed entirely through the tables, with a scroll-tracking
110
- clause index built on named view timelines and no JavaScript.
218
+ Scope was decided by counting, not by taste: across a real site's 19 pages,
219
+ container / heading / text-editor / button / html / icon-list / divider / image are
220
+ 99.6% of every element present. The long tail — loop grids, forms, countdowns,
221
+ third-party addons — is dynamic and has no node to become; each is reported by
222
+ name and reason, never dropped, and `--strict` refuses to write a lossy spec.
223
+
224
+ Layout, typography, colour, borders, links and images cross over, at all three
225
+ breakpoints (`_tablet`/`_mobile` → `_t`/`_m`). What does not: entrance animations
226
+ (Mosaic's interaction binding is unsolved), shape dividers, gradient overlays. The
227
+ verifier then holds the built page against the source — every string, image, link
228
+ and heading level — and it earned its place at once: it caught the converter losing
229
+ 19 of 21 links by writing `url` onto nodes that ignore it.
230
+
231
+ ## Tools
232
+
233
+ | tool | does |
234
+ |---|---|
235
+ | `mo.py` | query the measured surface — **the front door** |
236
+ | `build_page.py` / `build_site.py` | commit a spec through the guarded write path; refuses what is measured to break |
237
+ | `from_elementor.py` / `verify_conversion.py` | Elementor → Mosaic, and proof the content arrived |
238
+ | `verify_browser.py` | does the browser compute what the stylesheet promised, and does it pass a design audit |
239
+ | `verify_rwd.py` | does every `_t`/`_m` declaration reach the served stylesheet |
240
+ | `verify_intro.py` / `verify_loop.py` | a page-load sequence that ENDS; a perpetual one that loops, hides nothing, and opens |
241
+ | `theme_export.php` / `theme_import.php` | a whole theme as JSON rows over WP-CLI, ids intact |
242
+ | `theme_zip.py` / `theme_zip_compare.php` / `theme_delete.php` | Mosaic's own ZIP export/import from outside the editor, the copy held against the source, and a clean delete that refuses the live theme |
243
+ | `sweep_*.py` / `probe_*.py` | the instruments the tables were made with |
244
+ | `bootstrap_probe_theme.php` / `mint_session.php` | a licence-free scratch theme and a REST session from WP-CLI |
245
+
246
+ ## The worked example
247
+
248
+ `sites/_moksa.py` builds a real studio site through the tables alone and ships as
249
+ the reference: a homepage of 1,286 nodes with a scroll-tracking clause index on named
250
+ view timelines, an entrance sequence that prints an ukiyo-e sheet one carved block
251
+ at a time, a corner plate that keeps printing forever and opens when tapped, and a
252
+ WooCommerce My Account page whose UI arrives through one `code` node running a
253
+ shortcode. No JavaScript of its own anywhere. Every verification table in `data/`
254
+ was produced against it.
111
255
 
112
256
  ## Where to start
113
257
 
@@ -120,51 +264,21 @@ clause index built on named view timelines and no JavaScript.
120
264
 
121
265
  ## Releasing
122
266
 
123
- One command. The version lives in three places — `package.json`, the SKILL.md
124
- frontmatter an agent reads, and the frontmatter each platform template writes on
125
- install — and nothing keeps them together on its own.
126
-
127
267
  ```bash
128
- npm version patch # or minor / major
268
+ npm version minor # bumps package.json, SKILL.md and eight platform templates,
269
+ # commits, tags, pushes; the tag triggers release.yml
129
270
  ```
130
271
 
131
- That runs, in order:
132
-
133
- 1. `preversion` → `bin/check-release.mjs`
134
- 2. npm bumps `package.json`
135
- 3. `version` → `bin/sync-version.mjs` writes the new number into SKILL.md and all
136
- eight platform templates, and stages them
137
- 4. npm commits and tags `vX.Y.Z`
138
- 5. `postversion` → pushes the commit and the tag
139
-
140
- The tag push triggers `.github/workflows/release.yml`, which refuses to publish
141
- unless the tag matches `package.json`, re-runs the release checks, proves the
142
- installer runs, prints the tarball, then publishes with provenance and opens a
143
- GitHub release.
144
-
145
- `bin/check-release.mjs` is the gate, and it checks the things that are easy to get
146
- wrong rather than the things that are easy to check:
147
-
148
- - the three version numbers agree
149
- - every glob in `files` matches something
150
- - the row counts in the verification CSVs still equal the numbers SKILL.md quotes
151
- - `SKIPPED` labels survive into the shipped data, because a sweep that hides its
152
- blind spots is the failure this skill argues against
153
- - **the tarball itself is inspected**, not the intent. npm's `files` allowlist
154
- *overrides* `.gitignore`: naming a directory ships everything inside it, ignored
155
- or not. Listing `sites/` once put a real client's generator and content into the
156
- tarball — gitignored, and about to be published anyway.
157
-
158
- Publishing runs on npm trusted publishing (OIDC): npm trusts this repository's
159
- `release.yml` directly, so there is no token in the repository's secrets and
160
- nothing to rotate. Provenance is attached automatically.
161
-
162
- One-time setup, on npmjs.com under the package's Settings → Trusted Publisher:
163
- publisher `GitHub Actions`, organisation `Moksa1123`, repository
164
- `mosaic-headless`, workflow filename `release.yml`, environment name left
165
- **empty** — the workflow declares no environment, and a value here that the run
166
- does not match is refused. The connection cannot be edited afterwards, only
167
- deleted and recreated.
272
+ `bin/check-release.mjs` gates every release on the things that are easy to get
273
+ wrong: the version numbers agree, every `files` glob matches, every verification
274
+ CSV still has the row count SKILL.md and the four READMEs quote, no design-audit
275
+ finding is unreviewed, the eval suite is present, and **the tarball itself is
276
+ inspected** — npm's `files` allowlist overrides `.gitignore`, and once put a real
277
+ client's site into a package that was about to publish.
278
+
279
+ Publishing runs on npm trusted publishing (OIDC): no token anywhere. Setup on
280
+ npmjs.com under the package's Trusted Publisher: GitHub Actions, `Moksa1123` /
281
+ `mosaic-headless`, workflow `release.yml`, environment **empty**.
168
282
 
169
283
  ## Licence
170
284