Skip to content

MarkdownToNotebook: WXF example-output cache mis-serves pre-WXF (Compress-era) entries as raw compressed strings instead of treating them as a miss #96

Description

@mbahram

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions