You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(markdown): a MarkdownContent payload with no body binds as an empty document - #7097
Refs #5736, Refs #6639. Both issues come from the same place: MarkdownContent.Content was required.
Root cause
A payload that names MarkdownContent and has no content member threw JsonException on every bind. That included payloads carrying only members the record itself declares, such as {"$type":"MarkdownContent"} or {"$type":"MarkdownContent","tags":[…]}, which are just documents with no body yet. Three things followed:
Read:ObjectPolymorphicConverter degraded the node to raw JSON ("could not be deserialized; preserving raw JSON"). ReadMarkdownContent then reported an ordinary empty page as Unreadable (because of tags).
Update:NodeUpdatePipeline logged As<MarkdownContent> for <path> could not recover value: JsonException at Error and passed the validators an untyped snapshot.
ReadMarkdownContent counts only undeclared members as unreadable.
Docs: ContentSchemaOnWrite.md now carries a table of which payloads bind and which are refused. UpdateValidatorsSeeTypedContent.md is aligned with it.
Proof
New arms, run with the fix removed (negative control): 7 failed in MeshWeaver.Hosting.Test and 2 failed in MeshWeaver.Graph.Test. With the fix, all pass: Hosting NodeUpdateContentTypeChangeTest + MarkdownContentWithoutBodyTest 37/37, Graph MarkdownUnreadableContentTest + ContentSchemaValidationTest 24/24.
Update pipeline (WithExistingContentTyped, real hub options, JsonElement and DOM): a body-less payload is typed as an empty MarkdownContent, keeps its tags and abstract, and logs nothing. Counterparty: $type plus body/markdown/text stays raw and stays at Error, and the log names the path but not the text.
Read seams (MeshNodeStreamCache GetStream and GetQuery): the body-less payload arrives typed with no warning. Through the hub serializer it reads as Absent and writes back with content, so the row heals on its next save. A body-keyed payload stays a JsonElement and reads as Unreadable.
Release -warnaserror is clean for MeshWeaver.Markdown, .Graph, .Documentation, Hosting.Test and Graph.Test.
Why Refs and not Fixes
The production rows measured on #5736/#6639 (ReinsuranceDemo/Cedents and ReinsuranceDemo/Claims, plus the repaired Posts/*) carry their body under an undeclared key. Those rows are refused by design, and they close through the installer fix (MeshWeaver.Reinsurance#262) and the data repair already described on the issues. This PR removes the other kind, the body-less one, which was failing for no reason. Not established: whether any live row has this body-less shape. No query isolated one.
Pairs-with: none — required removed from one member (a relaxation: existing callers that set it keep compiling and binding); no public type or member leaves.
…ty document
MarkdownContent.Content was `required`, so a payload naming MarkdownContent
with no `content` member threw on every bind, even when it carried nothing
but members the record declares ({"$type":"MarkdownContent"},
{"tags":[...]}). The read seam degraded such a node to raw JSON, the
overview called an ordinary empty page Unreadable, and every update logged
"As<MarkdownContent> for <path> could not recover value" at Error.
Content is no longer required. IJsonOnDeserialized binds an absent body as
"" UNLESS the payload carries a member the record does not declare
(body/markdown/text): the body may be under that key, so it is still
refused (#4600) and keeps its Unreadable read and its Error on update.
ReadMarkdownContent counts only undeclared members as unreadable.
Refs #5736, #6639
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This PR allows body-less MarkdownContent payloads to bind as empty documents while preserving rejection of payloads whose body may be stored under an undeclared key.
Changes:
Replaces the required body property with conditional deserialization validation.
Updates empty-versus-unreadable classification and adds read/update regression tests.
…ared member stays unreadable
Review on #7097.
- MarkdownContent refuses a payload with no `content` but a stored
`prerenderedHtml`: its source is missing, so MarkdownBody.Render must fall
back to the stored HTML (GetPreRenderedHtml kept returning the document
before this PR). An explicit `"content": ""` still binds and renders empty.
- ReadMarkdownContent asks MarkdownContent's own contract whether a body-less
raw payload binds, instead of filtering by member NAME, so a declared member
holding a wrong-shaped value (`"tags": "# text"`, an object under
`abstract`) stays Unreadable and is named.
Refs #5736, #6639
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Preserve null source so stored HTML fallback remains available
src/MeshWeaver.Markdown/MarkdownContent.cs:45
Normalizing JSON null here bypasses the new stored-HTML guard. For {"$type":"MarkdownContent","content":null,"prerenderedHtml":"<p>Existing document</p>"}, MarkdownBody.Render previously found no string source and returned the stored HTML. This setter now stores "", so OnDeserialized returns early and rendering produces an empty document instead. Keep null in the backing field until the callback checks for stored HTML; the public getter can remain non-null. Add this null-source regression alongside the missing-source and explicit-empty-string cases.
Preserve node-level cached HTML when source is missing
src/MeshWeaver.Markdown/MarkdownContent.cs:162
The new guard protects HTML inside the content payload, but not the separate MeshNode.PreRenderedHtml cache. A node with a nonempty node-level cache and {"$type":"MarkdownContent","tags":["draft"]} now binds with an empty body because this guard sees no HTML. Previously the payload stayed raw, so MarkdownBody.Render reached its node-level fallback. Now the non-null empty getter makes Render return an empty document before checking that cache (MarkdownBody.cs:86–90). Preserve whether the source was supplied across binding and rendering so both cache locations remain usable when source is missing, without reviving stale HTML after an explicit empty-string clear. Add a serialization-to-render regression with node-level cached HTML.
Results for commit 5d52308. ± Comparison against base commit 23a1840.
This pull request removes 1645 and adds 37 tests. Note that renamed tests count towards both.
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---)
--- End of inner exception stack trace ---, expected: True)
--- End of inner exception stack trace ---, isDenial: True)
---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
---> (Inner Exception #1) System.InvalidOperationException: boom<---
---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "c6c7af33-5f73-4993-8dae-44a8f5cf3f72")
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.AggregateException: One or more errors occurred. (Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.)
---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
--- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.InvalidOperationException: landing faulted
---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
--- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
---> System.UnauthorizedAccessException: Access denied
--- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
--- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
---> System.TimeoutException: The operation has timed out.
--- End of inner exception stack trace ---
---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
---> System.ArgumentException: Value does not fall within the expected range.
--- End of inner exception stack trace ---
---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
--- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
--- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
---> System.TimeoutException: The operation has timed out.
--- End of inner exception stack trace ---
---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
MeshWeaver.Graph.Test.MarkdownUnreadableContentTest ‑ BodylessContentWithNodeLevelHtml_RendersTheStoredHtml_WhileAnExplicitClearStaysEmpty
…
♻️ This comment has been updated with latest results.
Review on #7097. ReadMarkdownContent's contract check binds
case-insensitively, so raw {"Content":"# doc"} - which the exact-case
`content` lookups miss - binds WITH the document and was then reported
Absent, putting the invitation to edit over it. Read the bound record's
body instead: non-empty is Present, empty is Absent.
Refs #5736, #6639
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bind, render and write-back
Review on #7097. A body-less MarkdownContent was normalised to Content == "",
so on a node carrying its rendering on MeshNode.PreRenderedHtml,
MarkdownBody.Render rendered the empty source and never reached the
node-level fallback, hiding the stored document.
MarkdownContent now keeps "no source" as its own state (HasSource false):
- the wire member `content` is omitted while there is no source, so a
write-back never turns it into a deliberate clear;
- MarkdownBody.Render falls back to the cached HTML for it;
- ReadMarkdownContent shows it Unreadable over a cached rendering (typed
and raw), never the invitation to edit.
An explicit "content": "" is still a clear: it renders empty.
Refs #5736, #6639
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #5736, Refs #6639. Both issues come from the same place:
MarkdownContent.Contentwasrequired.Root cause
A payload that names
MarkdownContentand has nocontentmember threwJsonExceptionon every bind. That included payloads carrying only members the record itself declares, such as{"$type":"MarkdownContent"}or{"$type":"MarkdownContent","tags":[…]}, which are just documents with no body yet. Three things followed:ObjectPolymorphicConverterdegraded the node to raw JSON ("could not be deserialized; preserving raw JSON").ReadMarkdownContentthen reported an ordinary empty page as Unreadable (because oftags).NodeUpdatePipelineloggedAs<MarkdownContent> for <path> could not recover value: JsonExceptionat Error and passed the validators an untyped snapshot.Fix
MarkdownContent.Contentis no longerrequired, and its getter never returns null.IJsonOnDeserializedbinds an absent body as""unless the payload carries a member the record does not declare (body,markdown,text;$-metadata is ignored). The body may be stored under that key, so that payload is still refused. This keeps the Markdown node with unreadable content renders as "No content yet" — GetMarkdownContent collapses "unreadable" into "absent" #4600 guarantee: no "No content yet" placeholder over a full document. That is the objection under which relaxingrequiredwas refused earlier on MarkdownContent rows without 'content' degrade to raw JSON; the log names the type but not the node path #6639, and it still holds.ReadMarkdownContentcounts only undeclared members as unreadable.ContentSchemaOnWrite.mdnow carries a table of which payloads bind and which are refused.UpdateValidatorsSeeTypedContent.mdis aligned with it.Proof
New arms, run with the fix removed (negative control): 7 failed in
MeshWeaver.Hosting.Testand 2 failed inMeshWeaver.Graph.Test. With the fix, all pass: HostingNodeUpdateContentTypeChangeTest+MarkdownContentWithoutBodyTest37/37, GraphMarkdownUnreadableContentTest+ContentSchemaValidationTest24/24.WithExistingContentTyped, real hub options, JsonElement and DOM): a body-less payload is typed as an emptyMarkdownContent, keeps its tags and abstract, and logs nothing. Counterparty:$typeplusbody/markdown/textstays raw and stays at Error, and the log names the path but not the text.MeshNodeStreamCacheGetStream and GetQuery): the body-less payload arrives typed with no warning. Through the hub serializer it reads as Absent and writes back withcontent, so the row heals on its next save. Abody-keyed payload stays a JsonElement and reads as Unreadable.Release
-warnaserroris clean for MeshWeaver.Markdown, .Graph, .Documentation, Hosting.Test and Graph.Test.Why Refs and not Fixes
The production rows measured on #5736/#6639 (
ReinsuranceDemo/CedentsandReinsuranceDemo/Claims, plus the repairedPosts/*) carry their body under an undeclared key. Those rows are refused by design, and they close through the installer fix (MeshWeaver.Reinsurance#262) and the data repair already described on the issues. This PR removes the other kind, the body-less one, which was failing for no reason. Not established: whether any live row has this body-less shape. No query isolated one.Pairs-with: none —
requiredremoved from one member (a relaxation: existing callers that set it keep compiling and binding); no public type or member leaves.🤖 Generated with Claude Code