Skip to content

site-core: a collection category's lead survives the round trip (tile-lab#3) - #3

Merged
cverorg merged 1 commit into
mainfrom
fix/site-core-collection-lead
Aug 28, 2026
Merged

site-core: a collection category's lead survives the round trip (tile-lab#3)#3
cverorg merged 1 commit into
mainfrom
fix/site-core-collection-lead

Conversation

@cverorg

@cverorg cverorg commented Aug 28, 2026

Copy link
Copy Markdown
Member

What

parseSite collected a collection category's lead paragraph into group.lead, the model
builder dropped it, and serializeSite had nowhere to write it back — so
serializeSite(parseSite(page)) !== page for every collection whose categories carry an intro,
and every save path silently deleted those paragraphs. people (the other two-level coral) has
kept its group lead since it was written; collection walked the same shape and only ever built
half the model. Fixed by adding the one missing model field and the one missing emit, in the exact
position the parser reads it from: between the ### heading and the first #### item.

A second instance of the same class, worse because it is visible: splitCellHeading parses a
leading Lucide shortcode (### :book-open: The Comic) into c.icon, the renderer draws it, but
serializeSite never emitted it — so a grid icon rendered until the first save and then vanished.
Same one-term fix.

site-core-roundtrip.test.mjs is new: one canonical page per coral asserted byte-identical, plus
a content-preservation sweep over every shipped .md fixture comparing authored bytes to output
bytes as a multiset (so a lost duplicate can't hide behind set membership). Both rulers are proven
to fire against a real, injected deletion.

Test output

Repo gate (bash scripts/test.sh), exit 0, all green (astro/browser smokes skip for missing
optional deps, as they do on a stock checkout):

$ node packages/sitetile/site-core.test.js
sitetile: 76 passed, all green

$ node packages/sitetile/site-core-roundtrip.test.mjs
sitetile round-trip: 46 passed, all green

$ bash scripts/test.sh
...
✅ ALL GREEN

Not fixed here (measured, needs a decision, not a symmetry fix)

  • A form field's non-- option body prose is dropped by the model builder by design; giving
    helper text a home needs a render decision.
  • A collection category's lead is not rendered by either renderer today, though the people
    equivalent is. This change stops the paragraph being deleted; making it appear would change how
    existing live pages look, which is a site-owner decision, not this fix's.

`parseSite` collected a `collection` category's lead paragraph into
`group.lead`, the model builder dropped it, and `serializeSite` had
nowhere to write it back — so `serializeSite(parseSite(page)) !== page`
for every collection whose categories carry an intro, and every save
through every door (MCP `save_page`, the console editor, batch
`save_pages`) silently deleted those paragraphs. Found by reef W88
against a real 制作実績 page. `people` — the other two-level coral — has
kept its group lead since the day it was written; collection walked the
same shape and only ever built half the model. Fixed by adding the one
missing model field and the one missing emit, in the exact position the
parser reads it from: between the `###` heading and the first `####`
item. Emitted after the items it would re-parse as the LAST item's body
and change the paragraph's owner on every save instead of deleting it.

🔴 The audit for the same class found a SECOND one, and it is worse
because it is visible. `splitCellHeading` parses a leading Lucide
shortcode (`### :book-open: The Comic`) into `c.icon`; astro's
GridCell.astro RENDERS it; `serializeSite` never emitted it. So a grid
icon rendered correctly until the first save and then vanished off the
built page. Same one-term fix, icon before emoji — the order
splitCellHeading reads them.

🩸 And the reason neither was caught: `serialize∘parse` was already a
STABLE FIXPOINT on the broken code and on all eight fixtures this
package ships. Of course it was — dropping the same text twice drops
nothing the second time. An idempotence check agrees with itself while
both halves are wrong. site-core-roundtrip.test.mjs therefore carries
two rulers, neither of which asks the parser whether it agrees with
itself: one canonical page per coral asserted byte-identical, and a
content-preservation sweep over every .md in the package that claims to
be a sitetile page, comparing the author's bytes to the output's bytes
as a multiset (set membership cannot see a lost duplicate). Both rulers
are proven to fire, by control tests that hand them a real deletion —
and the fixture sweep prints its sample count and asserts a floor,
because a ruler that measures nothing reports zero and passes forever.

Worth stating plainly: the shipped fixtures are all green and always
were. Not one of them uses collection, people, faq or timeline, which
is exactly why the bug lived here. The corals table is the part of this
file that carries those structures.

Found but NOT fixed, both measured, both needing a decision rather than
a symmetry fix:
  · a `form` field's non-`- option` body prose is dropped by the model
    builder, which by design carries only structure (labels/kinds/
    options) — giving helper text a home needs a render decision.
  · a collection category's lead is not rendered by EITHER renderer
    (site-core's own, or astro's Collection.astro), though People.astro
    renders the people equivalent. This commit stops the paragraph being
    deleted; making it appear changes how existing live pages look, and
    that is the site owner's call, not this fix's.

The whole repo suite is green (scripts/test.sh, exit 0). The astro smoke
and browser smokes skipped for missing deps and say so by name; neither
covers this change, which is confined to serializeSite — the astro build
parses and never serializes, so its output is unchanged by construction.
@cverorg
cverorg merged commit 1a8a254 into main Aug 28, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant