Summary
The example-output cache was changed to WXF (MarkdownToNotebook.wl, lines 6181 and 6185):
exampleCacheGet[name_String] := Replace[
PersistentSymbol[name, $cacheLocation],
b_ByteArray :> BinaryDeserialize[b]
]
exampleCacheSet[name_String, v_] := (PersistentSymbol[name, $cacheLocation] = BinarySerialize[v];)
The prior committed code (HEAD 486a861, lines 6036-6037) stored Compress[v] (a String) and read it back with c_String :> Uncompress[c]. The new reader has no String branch, so a legacy Compress-era entry (a String) is returned unchanged: the raw compressed blob, neither deserialized nor Missing.
Effect
MissingQ of that String is False, so:
allHit (line 6320) counts the legacy entry as a cache HIT,
outputs = cached (line 6329) passes the compressed String to annotateOutputs, whose non-Association branch (line 1240) wraps it as OutputBoxes -> "1:eJx...", so the example renders its output as a literal compressed-string blob, and
- because
allHit is True, the re-store / heal branch (line 6337, gated on Not[allHit]) is skipped, so the wrong output does not self-heal on later builds.
The cache is content-addressed with no format version (exampleCacheName line 6163, $cacheLocation = "Local" line 6137), so a warm rebuild of a document whose example cells are unchanged since a Compress-era build hits this. The immediately preceding committed format is Compress, so such entries exist for anyone who built with committed code.
Repro (kernel only)
Get[".../MarkdownToNotebook.wl"];
legacy = Compress[<|"outs" -> {"\"hello\""}, "msgs" -> {}, "prints" -> {}, "cells" -> {}|>];
Replace[legacy, b_ByteArray :> BinaryDeserialize[b]] === legacy (* True: returned unchanged, not deserialized *)
MissingQ[Replace[legacy, b_ByteArray :> BinaryDeserialize[b]]] (* False: counted as a cache hit *)
Which change introduced it
The uncommitted Compress -> WXF change, which dropped the c_String :> Uncompress[c] handling. The accompanying comment claims "a pre-WXF entry that still parses returns as-is (non-ByteArray)"; that is true only for the oldest raw-Association format. A Compress String also "returns as-is", but as the compressed blob, not the value.
Fix options
- Keep old caches working: add a legacy branch, e.g.
s_String :> Uncompress[s] alongside the b_ByteArray branch (raw-Association entries still pass through as-is).
- Or force a heal: treat any value that is neither a ByteArray nor an Association as
Missing, so it re-evaluates and is rewritten as WXF.
Severity
Medium: wrong example outputs (compressed-string blobs) on the first and every subsequent warm build over a Compress-era cache, with no self-heal. Mitigations are "UseCache" -> False or clearing the persistent cache, but the default warm path is broken for legacy entries.
Summary
The example-output cache was changed to WXF (MarkdownToNotebook.wl, lines 6181 and 6185):
The prior committed code (HEAD 486a861, lines 6036-6037) stored
Compress[v](a String) and read it back withc_String :> Uncompress[c]. The new reader has no String branch, so a legacy Compress-era entry (a String) is returned unchanged: the raw compressed blob, neither deserialized norMissing.Effect
MissingQof that String isFalse, so:allHit(line 6320) counts the legacy entry as a cache HIT,outputs = cached(line 6329) passes the compressed String toannotateOutputs, whose non-Association branch (line 1240) wraps it asOutputBoxes -> "1:eJx...", so the example renders its output as a literal compressed-string blob, andallHitisTrue, the re-store / heal branch (line 6337, gated onNot[allHit]) is skipped, so the wrong output does not self-heal on later builds.The cache is content-addressed with no format version (
exampleCacheNameline 6163,$cacheLocation = "Local"line 6137), so a warm rebuild of a document whose example cells are unchanged since a Compress-era build hits this. The immediately preceding committed format is Compress, so such entries exist for anyone who built with committed code.Repro (kernel only)
Which change introduced it
The uncommitted Compress -> WXF change, which dropped the
c_String :> Uncompress[c]handling. The accompanying comment claims "a pre-WXF entry that still parses returns as-is (non-ByteArray)"; that is true only for the oldest raw-Association format. A Compress String also "returns as-is", but as the compressed blob, not the value.Fix options
s_String :> Uncompress[s]alongside theb_ByteArraybranch (raw-Association entries still pass through as-is).Missing, so it re-evaluates and is rewritten as WXF.Severity
Medium: wrong example outputs (compressed-string blobs) on the first and every subsequent warm build over a Compress-era cache, with no self-heal. Mitigations are
"UseCache" -> Falseor clearing the persistent cache, but the default warm path is broken for legacy entries.