Repository navigation
fix(#3625): the untyped-content gate can fire, and keys on the event not the sentence #3628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
meshweaver-cloud
merged 7 commits into
main
from
fix/3625-the-untyped-content-gate-can-fire
Sep 7, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
44625af
fix(#3625): the untyped-content gate can fire, and keys on the event …
rbuergi 8d4eb81
review: the fixture is what changes, never the gate — no allow-list, …
rbuergi a3bfb67
Merge remote-tracking branch 'origin/main' into fix/3625-the-untyped-…
rbuergi f4b0420
review: the one fixture this gate would have red, changed to the shap…
rbuergi 344b743
fix(#3625): the gate's first real catch — every instance's module inv…
rbuergi 46706d2
docs(#3625): classify what the repaired gate found, and the assertion…
rbuergi 3f232aa
fix(#3625): a test whose SUBJECT is a degradation ASSERTS the record …
rbuergi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
74 changes: 74 additions & 0 deletions
74
src/MeshWeaver.Data.Contract/MeshNodeContentDegradedException.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| namespace MeshWeaver.Mesh; | ||
|
|
||
| /// <summary> | ||
| /// 🚨 <b>The IDENTITY of a content degradation — an exception object attached to the diagnostic, | ||
| /// deliberately never thrown.</b> | ||
| /// | ||
| /// <para><b>What it marks.</b> A <c>MeshNode</c>'s <c>Content</c> arrived as JSON, the | ||
| /// polymorphic reader could not resolve its <c>$type</c> against the reading hub's TypeRegistry, | ||
| /// and the mesh-wide <c>IMeshContentTypeRegistry</c> could not recover it either. The value crosses | ||
| /// the read boundary STILL untyped, so every downstream <c>Content is X</c> / <c>as X</c> silently | ||
| /// answers absent: a view renders empty, a reactive wait never completes. Nothing throws — a READ | ||
| /// must stay bad-data tolerant, and that is exactly why this class of defect is invisible.</para> | ||
| /// | ||
| /// <para>🚨 <b>Why an exception for something that is not thrown.</b> Because carrying one is what | ||
| /// makes the diagnostic DURABLE, and durability here is a property of the CALL, not of the wording | ||
| /// or the level. A CI shard's authoritative sink — <c>_meshweaver-test-trace.log</c>, written by | ||
| /// <c>XUnitFileLogger.Log</c> → <c>TestTraceLog.AppendFault</c> — takes a record <b>if and only if</b> | ||
| /// <c>exception is not null && logLevel >= Warning</c>. The degradation warnings passed no | ||
| /// exception, so they could reach that file by construction never; and the job log carries only the | ||
| /// output of tests that FAILED, while a degradation is overwhelmingly logged under a test that | ||
| /// PASSES. The consequence was measured in MeshWeaver#3625: the shard gate | ||
| /// <c>check-untyped-content.sh</c>, which greps that very directory, had <b>no way to fire</b> — a | ||
| /// permanently green check, indistinguishable from a passing one.</para> | ||
| /// | ||
| /// <para>This is the third recorded instance of the same trap (#890's <c>PROCESS CANNOT EMIT</c>, | ||
| /// #3413's <c>never rendered</c>, and this one) — see | ||
| /// <c>Doc/Architecture/ReadingCiSignals</c> → "for a fault the trace log is the sink".</para> | ||
| /// | ||
| /// <para>🚨 <b>The gate keys on this TYPE NAME, not on the message.</b> A prose phrase is a | ||
| /// description of the event; the type is the event's identity, bound by the compiler at every | ||
| /// construction site. That gives the coupling two independent bindings instead of one: renaming the | ||
| /// type is a compile-wide change, and <c>UntypedContentDegradationGate</c> pins the script's key to | ||
| /// <c>nameof(MeshNodeContentDegradedException)</c> so a rename that forgets the script reds.</para> | ||
| /// | ||
| /// <para>Constructing an exception without throwing it costs one allocation and captures no stack, | ||
| /// so <see cref="System.Exception.ToString"/> yields exactly <c>type: message</c> — which is the | ||
| /// whole record a reader needs here, since the fault is in the DATA and not in a call path.</para> | ||
| /// </summary> | ||
| public sealed class MeshNodeContentDegradedException : System.Exception | ||
| { | ||
| /// <summary>The read seam that observed the degradation (e.g. <c>MeshNodeStreamCache.GetStream</c>).</summary> | ||
| public string Seam { get; } | ||
|
|
||
| /// <summary>The node whose content stayed untyped.</summary> | ||
| public string? NodePath { get; } | ||
|
|
||
| /// <summary>The node's NodeType, which is the key the content-type registry recovery is attempted under.</summary> | ||
| public string? NodeType { get; } | ||
|
|
||
| /// <summary>Truncated raw JSON of the content that could not be materialised.</summary> | ||
| public string? RawJson { get; } | ||
|
|
||
| /// <summary>Creates the degradation marker.</summary> | ||
| /// <param name="seam">The read seam that observed it.</param> | ||
| /// <param name="nodePath">The node path.</param> | ||
| /// <param name="nodeType">The node's NodeType.</param> | ||
| /// <param name="rawJson">Truncated raw JSON of the unreadable content.</param> | ||
| /// <param name="inner">The deserialization exception, when there was one.</param> | ||
| public MeshNodeContentDegradedException( | ||
| string seam, string? nodePath, string? nodeType, string? rawJson, System.Exception? inner = null) | ||
| : base( | ||
| $"{seam}: content for '{nodePath ?? "<unknown>"}' (nodeType '{nodeType ?? "<none>"}') " | ||
| + "stayed an untyped JsonElement — the $type discriminator resolved to no registered " | ||
| + "CLR type on the reading hub and the mesh-wide content-type registry did not recover " | ||
| + "it. Downstream 'Content is X' / 'as X' consumers read it as ABSENT: views render " | ||
| + $"empty and reactive waits never complete. Raw: {rawJson ?? "<none>"}", | ||
| inner) | ||
| { | ||
| Seam = seam; | ||
| NodePath = nodePath; | ||
| NodeType = nodeType; | ||
| RawJson = rawJson; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adopted, with a different instrument — and applied to both greps in one shared
EXCLUDESarray so they cannot drift.The concern is real:
dotnet-test.ymlcopies/tmp/coredumps/*.dmpintocollected-logs/, plus asymbols-<project>/directory of.dll+.pdbwhenever a host dies on a signal. A core dump is a snapshot of process memory, so it can contain the marker simply because the process once formatted it — a match there is not a log record.I did not use
-I, and the reason is the same rule this whole script exists to enforce.-Idecides by CONTENT: a text log carrying one stray NUL byte becomes invisible to the scan, silently, and the gate then reports "No content-type degradation" — a pass-by-scanning-nothing, which is the one outcome this file must make impossible. A false red is loud and investigable; a false green is not. Excluding by NAME (--exclude='*.dmp' --exclude='*.dll' --exclude='*.pdb') can only ever skip an extension listed in the open, right there in the script, and the reasoning is written beside it.