Repository navigation
Preserve partially understood markup and define durable wire format compatibility #61
Description
Problem
MarkupText is durable game data. Reading a payload that is structurally unfamiliar or only partially understood must not turn it into a successfully writable empty/plain value that silently loses information on the next save.
The current handwritten serializer already decouples storage from the C# object model and preserves unknown markup kinds. The gap is lossless preservation and explicit compatibility at the full document and known-codec boundaries.
Verified source findings
Inspected MarkupString v2.18.0 / 09b995c3d158320810d766ac21046cfe0882490f, currently consumed by SharpMUSH main b8211f16ed15ff50ed2d8ef5f77c02b9ae6b46b9.
- No envelope version dispatch. MarkupTextSerializer.Read reads only
t/p/r. Missing or non-stringtbecomes empty; missing or non-arrayp/rbecomes plain text. A hypothetical future object such as{"v":2,"text":"important"}is therefore inferred from source to read as empty and serialize as{}. This is an illustrative unsupported payload, not a claim that a v2 format exists today. - Unknown properties on known kinds are not retained. AnsiMarkupCodec creates a typed style and writes its known properties. For example, the additional
futurefield in{"t":"x","p":[null,[{"k":"ansi","f":1,"future":{"x":7}}]],"r":[1,1]}has no preservation path. - Unknown layout block kinds lose their subtree. BlockReader.Object/Unread substitutes an empty stack and marks the layout incomplete. LayoutJson.Read makes it ineligible for relayout, preserving visible fallback text, but serialization subsequently writes the substituted tree rather than the raw unknown block.
- UnknownMarkup is narrower than whole-document preservation. ReadMarkup stores the raw JSON object only when no codec claims its kind; WriteMarkup writes that object back. It does not retain unknown envelope properties, unread layout subtrees, or unknown properties in recognized kinds.
- Malformed and unsupported are not differentiated. Some malformed covers are intentionally clipped or dropped, while invalid JSON/trailing content, a non-object root, or a throwing custom codec can fail. Durable callers need diagnostics and a write-safety signal, not merely a recovered MarkupText.
These are source-confirmed code paths. Runtime reproduction and cross-release compatibility tests are still pending; no actual database corruption or whole-database startup failure has been demonstrated.
SharpMUSH impact
Attribute writes persist serialized markup directly in attr.val; reads call Deserialize without a contextual compatibility result. Channels, mail, feeds, scene edit history and object snapshots also retain serialized markup.
A bad payload can fail its read or an enumeration, or silently lose information if a degraded value is later saved. Object metadata and attribute metadata are separate, so this does not establish that one incompatible markup value makes LMDB unopenable.
Consumer migration must account for feed byte accounting and snapshot digests. A blanket JSON rewrite is unsafe.
Proposed direction to investigate
- Freeze and document the existing unversioned format as a supported legacy wire contract. Keep historical readers independent of package/API versions.
- Add explicit envelope-version dispatch before interpreting fields. Unknown/newer versions remain opaque and non-writable rather than becoming empty/plain successes.
- Add a loss-aware read result carrying raw input, parsed value where safe, diagnostics and whether a rewrite is lossless. Keep display fallback separate from persistence and softcode execution.
- Preserve unknown known-kind fields and unknown layout content, or reject rewrites that cannot preserve them. Do not automatically overwrite the original with plain fallback.
- Use a new kind for incompatible layer semantics; do not assume adding a per-kind version field protects old readers that ignore it.
- Deploy compatible readers first, keeping the existing writer target. Enable a new writer only after server, renderer, cached browser clients and durable consumers are compatible.
- Keep conversion opt-in, backup-gated, resumable and idempotent. Prefer dual-reading over bulk rewriting.
Investigation and acceptance checklist
- Add historical golden payload fixtures sourced from released writers, including pre-kind and legacy ANSI forms.
- Reproduce changed-envelope, unknown-envelope-field, known-kind-extension, unknown-layout-block, malformed-root, trailing-content and throwing-codec cases against the pinned release.
- Assert full text, run coverage, layer order and preserved opaque JSON; MarkupText.Equals compares plain text and cannot prove persistence fidelity.
- Define separate outcomes for known valid, lossless unknown layers, unsupported format and damaged data.
- Test new-reader/old-writer, old-reader/new-writer and downgrade limits using separate pinned package runners.
- Verify read-only access never mutates stored bytes; unsupported/damaged values cannot be silently rewritten.
- Add SharpMUSH integration coverage for isolated bad records, enumeration behavior, startup attributes, feed accounting, scene history and snapshot restore.
- Document an explicit compatibility and rollback policy and gate it in CI.
Tracking and investigation only at this stage. No production migration or automatic data rewrite is authorized by this issue.
Activity
HarryCordewener commentedon Oct 11, 2026
Investigation update: preservation, test coverage, and rollout
Evidence status: the findings below come from source review at MarkupString
09b995c3(v2.18.0) and SharpMUSHb8211f16, which pins that package. New runtime reproductions are still pending. Existing CI passing does not verify these cases. No code fixes, migrations, or production-data changes have been made.Further source findings
tto empty, and can fall back to plain text whenp/ris absent or malformed. A hypothetical future structural rename can therefore be accepted and then saved with content removed. An extravfield alone cannot protect against this existing reader.IColorMarkup, so an opaque unknown-color layer can disappear at that projection.MarkupRegistry.Defaultfor custom block codecs.A test trap worth addressing first
LayoutMarkup.Equals compares identity and layout metadata, but not the
Roottree. MarkupSet interning can consequently reuse an earlier equivalent layer in the same process and hide a lossy reconstructed tree.Use literal fixtures with fresh identities and isolated reader/writer processes, and assert nested fields and retained JSON. Neither MarkupText's plain-text equality nor render-equivalence establishes storage fidelity.
Small source-derived cases to execute:
{"v":99,"t":"x","future":true}: current read/write is expected to remove the unrecognized envelope fields.{"v":2,"text":"important"}: hypothetical renamed text field is expected to become empty; this is an illustration, not an actual v2 format.SharpMUSH impact is record/path dependent
Attribute hydration calls the decoder without a contextual recovery boundary; metadata and values are stored separately and values can be lazy. A bad value can fail its read or a scan that reaches it, but this does not establish that the entire LMDB database becomes unopenable.
Channels, mail, feeds, scene edit history, and object snapshots also contain markup. Scene fallback treats a parse failure as bare text, which should not by itself authorize saving that projection. Future conversion must account for feed byte totals and snapshot digests.
Established precedents and the smallest fit
Keeping JSON is a reasonable first choice:
Proposed implementation sequence
t/p/ras a legacy format. Commit historical golden payloads with producing package/commit, including existing legacy readers, nested layouts, plugins, Unicode, and malformed cases. Run isolated historical/current package runners.The smallest safe first release is preservation plus read-only handling for partially understood values. A broad format replacement or an automatic rewrite of the world is not required to get that protection.
Never fail a read on a newer wire version or a throwing codec
Keep a layout read from an edited copy from being interned as the ori…
VersionOf and NeedsUpgrade, and the rules an upgrade step keeps
Count a value written back as another kind as lost
Say whether a stored text can be saved back without loss; cut 2.19.0 (#…
HarryCordewener commentedon Oct 13, 2026
The library part of this issue is done in #63 (2.19.0):
"k"legacy formsInspectwith four outcomes (Complete,Opaque,Unsupported,Damaged) andCanRewriteVersionOf/NeedsUpgrade, and the upgrade-step rules indocs/serialization.mdUnknown properties on a known kind, and unknown layout blocks, are reported through
CanRewrite(false) rather than preserved. That is the "or reject rewrites" branch above.Separate pinned package runners are not added. The writer's bytes are unchanged, and the golden fixtures pin every released writer's output, so they cover new-reader/old-writer. Old-reader/new-writer does not arise until a new wire version exists.
The SharpMUSH side (rewrite checks on edit, copy, snapshot and pose paths, integration tests, and the rollback policy) moves to SharpMUSH/SharpMUSH#1818.
Generated by Claude Code