Sync Collapses Multiline YAML Script Bodies
DL#208 — sync collapses multi-line script bodies (YAML corruption)
Written for AI agents. See Log Methodology Note below for details.
Status: BUG — root-caused, fix proposed, not implemented.
Decisions for the Implementer (TL;DR)
- Symptom:
jay-stack syncrewrites a page and collapses the multi-line body of<script type="application/jay-headless">(and other YAML script bodies) onto a single line (newlines → spaces), corrupting whitespace-significant YAML. Multi-key blocks break; single-key one-liners survive, so it's easy to miss. - Root cause (not in sync):
prettifyHtmlflattens the whole document to one line in its line-join loop (compiler-shared/lib/prettify.ts:54-63). It protects onlyapplication/jay-databodies (JAY_DATA_SCRIPTregex,prettify.ts:32-33), swapping them to placeholders before the join and restoring them verbatim after (:42-45, 70-73). ✅ verified - Why
synctriggers it:syncPageContentwritesprettifyHtml(merged)to disk (run-sync.ts:72, written:288).materialiseandmergeScopeCssround-trip through node-html-parserroot.toString(), which preserves script raw text — so newlines are intact right up untilprettifyHtmlruns.prettifyHtmlis the single collapse point. ✅ verified - Why the normal build is unaffected: the compile path only reads YAML bodies into memory
(
headlessProps,validationOverrides) and never writes the.jay-htmlsource back, so it never runs the collapse on source. The bug is specific to thesyncrewrite serializer. ✅ verified (parser is read-only) / ⚠️ assumed (no other jay-html source writeback exists). - Fix: extend the verbatim-placeholder protection in
prettify.tsfromapplication/jay-dataonly to the whole significant-whitespace family:application/jay-headless,application/jay-params,application/jay-validations, andapplication/yaml. One regex + the existing swap/restore mechanism already present atprettify.ts:32-45, 70-73. - Two failure modes, one cause: headless/params bodies fail loudly (
validate: "bad indentation of a mapping entry (1:35)");jay-validationsfails silently —parseValidationOverridesswallows the YAML error in acatch { return undefined }(jay-html-parser.ts:1065-1077), so a nested suppression (e.g.no-lcp-image: false) just vanishes and the warning quietly reappears. ✅ verified
Observed failing cases
Multi-key headless props (
type="application/jay-headless" key="templates"):<!-- before: valid --> <!-- after sync: broken, one line --> <script ... key="templates"> <script ... key="templates">contentDir: content/templates file: data.yaml</script> contentDir: content/templates file: data.yaml </script>→
validatefails:bad indentation of a mapping entry (1:35). Single-key bodies (contentDir: content/docs/developer) stay valid as one-liners — bug only bites multi-key blocks.Multi-key
application/jay-params— same collapse, same loud YAML-indentation failure on the next parse. ⚠️ assumed (same code path as case 1; not separately reproduced).Nested
application/jay-validationssuppression — e.g.some-rule: no-lcp-image: falsecollapses to
some-rule: no-lcp-image: false;parseValidationOverrideseither reparses to a flattened structure (losing the nested key) or throws and is silently swallowed (jay-html-parser.ts:1065-1077). The suppression disappears and the suppressed warning silently returns. ✅ verified (collapse + silent-catch).
Root cause (verified trace)
packages/compiler/compiler-shared/lib/prettify.ts:54-63 // the collapse
let joined = '';
for (const line of withPlaceholders.split('\n').map((l) => l.trim())) {
if (line === '') continue;
if (joined === '') { joined = line; continue; }
const gap = joined[joined.length - 1] !== '>' && line[0] !== '<' ? ' ' : '';
joined += gap + line;
}
For a two-key headless body the loop joins …key="templates"> + contentDir: content/templates
(prev char > ⇒ no gap) + file: data.yaml (prev char s, next f ⇒ injected space) + </script>
(next char < ⇒ no gap), producing the one-line corruption. ✅ verified
application/jay-data escapes because JAY_DATA_SCRIPT (prettify.ts:32-33) swaps those blocks to
placeholders before the join (:42-45) and restores them verbatim after (:70-73); the comment at
:26-31 explicitly notes YAML indentation is significant — but the protection was never widened
beyond jay-data. js-beautify afterward (:65-68) cannot restore newlines already destroyed. ✅
verified
Write path: syncPageContent → prettifyHtml(merged) (run-sync.ts:72) → writeFile
(run-sync.ts:288). materialise serializes via root.toString() without prettify unless
opts.prettify is passed, and syncPageContent passes only { preserveOverrides: true }
(run-sync.ts:66) — so the collapse is solely in prettifyHtml. ✅ verified
Fix
Primary (one-line-ish): generalize the protected-script regex in prettify.ts:32-33 from
application/jay-data to the significant-whitespace family and reuse the existing
swap-to-placeholder / restore-verbatim code (prettify.ts:42-45, 70-73):
application/jay-data(already)application/jay-headlessapplication/jay-paramsapplication/jay-validationsapplication/yaml
All of these have whitespace-significant YAML bodies that must round-trip verbatim through any HTML prettifier.
Defense-in-depth (separate, smaller follow-up — decide in review): make
parseValidationOverrides (jay-html-parser.ts:1065-1077) not swallow YAML errors silently —
surface a validation error instead of return undefined. This wouldn't fix the corruption but would
convert the silent suppression-loss (case 3) into a visible failure, so a future regression can't
hide. Prevention-first: a loud parse error is better than a vanished suppression.
Scope / what this is NOT
- Not a
syncmerge/materialise bug —syncoutput is correct untilprettifyHtml. Fixingprettify.tsfixes every current and future caller, not justsync. - Not limited to
syncconceptually: any caller that prettifies jay-html containing these script bodies would corrupt them.syncis just the caller that writes back to disk today.
Verification criteria
- Fixture: a page with (a) a multi-key
jay-headlessbody, (b) a multi-keyjay-paramsbody, and (c) a nestedjay-validationssuppression → runprettifyHtml(and an end-to-endsync) → each body is byte-identical in its YAML content (newlines + indentation preserved), andvalidatestays clean with the suppression intact. - Regression: single-key bodies still fine; non-YAML script handling unchanged; normal (non-sync) build output unchanged.
- Test via parsed-result / file-content equality (per repo standards) — not
toContainon code.
Pointers (for the upstream issue)
- Fix site:
packages/compiler/compiler-shared/lib/prettify.ts:32-33, 42-45, 54-63, 70-73 - Trigger:
packages/jay-stack/stack-cli/lib/run-sync.ts:66, 72, 288 - Silent-drop site:
packages/compiler/compiler-jay-html/lib/jay-target/jay-html-parser.ts:1065-1077 - Related docs:
agent-kit/designer/jay-html-components.md,agent-kit/designer/data-files-usage.md, and thesynccommand. - Related DLs: DL#196 (sync), DL#176 (suppressible validations — the suppression this bug drops).
Implementation Results
Status: PRIMARY fix implemented and tested. Defense-in-depth parser change (silent-catch in
parseValidationOverrides) intentionally not done — it is an explicit separate follow-up.
What changed
packages/compiler/compiler-shared/lib/prettify.ts — widened the protected-script matching from
application/jay-data only to the whitespace-significant YAML family, reusing the existing
swap-to-placeholder / restore-verbatim mechanism unchanged:
- Renamed
JAY_DATA_SCRIPT→PROTECTED_SCRIPTand widened itstypealternation tojay-data | jay-headless | jay-params | jay-validations | yaml(regex:application\/(?:jay-data|jay-headless|jay-params|jay-validations|yaml)). - Renamed
JAY_DATA_PLACEHOLDER→PROTECTED_PLACEHOLDERandJAY_DATA_PLACEHOLDER_RE→PROTECTED_PLACEHOLDER_RE(placeholder markupdata-jay-data-raw="<i>"left unchanged, so no behavior change for existing callers). Updated the comment block to describe the family and cite DL#208. - No change to the swap loop (
html.replace(PROTECTED_SCRIPT, …)pushes each block topreserved[]) or the restore loop (formatted.replace(PROTECTED_PLACEHOLDER_RE, (_m, i) => preserved[Number(i)])).
Multiple-block restore
Already correct by construction and unchanged: each matched block is pushed to preserved[] in
document order and the placeholder carries its index i; restore looks up preserved[Number(i)] per
placeholder. So N distinct protected blocks (any mix of the five types) in one document each
round-trip verbatim, not just the first. Verified by a dedicated test with four distinct blocks
(headless + params + validations + yaml) in one document.
Tests
Added to packages/compiler/compiler-shared/test/prettify.test.ts (inline-string style matching the
existing file; full-string toEqual, no toContain), via a small extractScriptBody helper that
pulls a block's inner text out of the prettified output and compares it byte-for-byte:
- multi-key
application/jay-headlessbody preserved byte-for-byte; - multi-key
application/jay-paramsbody preserved byte-for-byte; - nested
application/jay-validationssuppression (some-rule:→no-lcp-image: false) preserved; - multiple distinct protected blocks (headless + params + validations + yaml) in one document all preserved — proves the restore handles more than one;
- full-document case asserting the HTML around the blocks is prettified normally while the bodies stay
intact (full-string
toEqual); - regression: single-key YAML body still intact. The pre-existing word-spacing/ordinary-HTML tests remain green.
Results
yarn vitest run test/prettify.test.ts: 10/10 passing.yarn vitest run(whole package): 130/130 passing (8 files).yarn build: clean (JS bundle + d.ts).
Deviation
None from the primary design. Only renamed the constants to drop the now-misleading JAY_DATA_
prefix; the placeholder attribute name was left as-is to avoid churn.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.