[Bug]: site_apply_css reports cssRulesCreated for rules that are never published when the selector ends in a bare class
Summary
site_apply_css reports cssRulesCreated for rules that are then never published.
Same call, same response shape, but whether a rule reaches the stylesheet depends on the
form of its selector, and the return value does not distinguish the two.
Concretely: I supplied 18 rules, got {"cssRulesCreated": 18}, and 8 of them appeared in
the published CSS. The other 10 were silently discarded. Nothing in the response says so,
and the only way to find out is to fetch the published stylesheet and diff it.
Steps to reproduce
On a page imported from static HTML whose elements carry class="field-stage" etc. as
imported HTML attributes:
site_apply_csswithoperation: "merge"and 18 rules, all prefixed the same way:
.field-stage { position: relative; perspective: 1100px; ... }
.field-image { position: relative; padding: var(--space-3); ... }
.field-image img { width: 100%; height: auto; }
.field-motion:hover { background: var(--action-bg); ... }
.field-footer > span { color: var(--muted); }
.field-heading span:last-child { text-align: right; }
...- Response:
{"cssRulesCreated": 18, "cssRulesUpdated": 0} site_publish- Fetch
/_instatic/css/style-<hash>.cssand grep for the selectors.
Expected behavior
Either all 18 ship, or the response reports which ones did not and why. A rule that cannot be emitted should not be counted as created.
Actual behavior
8 of 18 shipped. The split is exact and reproducible: a rule ships only if its selector ends in something other than a bare class.
Shipped (trailing tag, pseudo-class, pseudo-element, or child combinator):
.field-heading span:last-child
.field-image img
.field-image::before, .field-image::after
.field-image::before
.field-image::after
.field-footer > span
.field-motion:hover
.field-motion:activeDiscarded (selector ends in a plain class):
.field-heading .field-stage .field-tilt .field-float .field-plane
.field-plane--grid .field-image .field-scan .field-footer .field-motionI then retried the same 10 with two different ancestors, .hero-field .field-stage and
figure.hero-field .field-stage. Identical outcome both times: reported created, never
published. So it is the trailing simple selector that decides, not the ancestor.
This is presumably the kind: 'class' vs ambient split described in #404. A selector
ending in a bare class is registered as a class rule, and a class rule only ships when
that class is assigned to a node. These elements carry the class in the published HTML
(it is in the emitted markup, which is how the page selects them) but it arrived as an
imported HTML attribute rather than an Instatic class binding, so the rule has nothing to
attach to. The important part is not the internal policy, it is that the tool reports
success either way.
Worth noting [data-od-id="hero"] .field-stage does ship on the same page, from the
original import. So the same selector shape is publishable; the difference is invisible
from the API.
Two related failures in the same session
Silent no-op on an equal-specificity selector. Supplying 10 rules under a new ancestor that ties on specificity with existing ones:
site_apply_css merge '[data-od-id="hero-photograph"] .field-stage { ... }' (x10)
-> {"cssRulesCreated": 0, "cssRulesUpdated": 0}No error, nothing written. Raising specificity was the workaround. This is the same behaviour I noted as item 3 of #490, now reproduced a second time on unrelated selectors, so it is not specific to that stylesheet.
Partial application, unreported. Supplying 10 rules under
figure.hero-field[data-od-id="hero-photograph"] returned
{"cssRulesCreated": 0, "cssRulesUpdated": 5}. Ten went in, five were accounted for, five
vanished. The counts do not sum to the input and there is no per-rule result, so a caller
cannot tell which five landed.
Why this matters
The failure mode is the dangerous one: the tool says it worked. An agent or script driving
site_apply_css has no signal to retry or fall back, and the page silently keeps rendering
unstyled. In our case a component rendered as a raw full bleed image on a production page,
and four separate apply-and-publish cycles reported success before I fetched the stylesheet
and diffed it.
A per-rule result, or simply not counting discarded rules as created, would make this self-evident.
Workaround
Write the CSS as a stylesheet code asset instead of into the rule store:
site_write_code_asset path: src/styles/field-plate.css type: style
runtime: { enabled: true, scope: { type: "all-pages" } }That bypasses the class/ambient classification entirely, ships verbatim, and loads after
style.css. Every one of the 18 rules published on the first attempt.
Version or commit
Hosted Railway deployment behind a custom domain, tested 2026-09-06. Driven over the HTTP MCP connector.
Deployment mode
Hosted (Railway)
Related
#404 (commitStyleRules only reconciles kind:'class') describes the same class/ambient
split from the import side. #249 (cross-page class-name collisions silently drop styles)
is the same silent-drop failure mode on the import path. #490 item 3 is the
equal-specificity no-op.
Source: CoreBunch/Instatic