diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs
index ba01d31d..37ef1833 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs
@@ -105,7 +105,13 @@ public sealed partial class VisualBriefingArtifactService
///
/// Encodes the stable JSON artifact header for embedding in an HTML comment.
///
- private static string EncodeHeader(VisualBriefingExportManifest exportManifest) => Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(exportManifest, JSON_OPTIONS)));
+ ///
+ /// The header is canonical JSON because verifying a stored briefing encodes it again and compares
+ /// the document hash. Plain serialization would tie every stored document to the order in which the
+ /// manifest properties happen to be declared, so moving one property would reject every briefing
+ /// ever exported.
+ ///
+ private static string EncodeHeader(VisualBriefingExportManifest exportManifest) => Convert.ToBase64String(Encoding.UTF8.GetBytes(VisualBriefingHashing.CanonicalJson(exportManifest)));
///
/// Defines RuntimeAIVersionRegex for the visual briefing feature.
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs
index c2b670ab..dbd5adf1 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs
@@ -150,12 +150,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
{
ArtifactId = Guid.NewGuid(),
CreatedAtUtc = DateTimeOffset.UtcNow,
- PayloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(previousPresentation.Layout, VisualBriefingJson.Canonical),
- previousPresentation.Profile.ToString(),
- compiled.TemplateHash,
- compiled.CssHash),
-
+ PayloadHash = VisualBriefingPayloadHash.ForPresentation(previousPresentation.Layout, previousPresentation.Profile, compiled.TemplateHash, compiled.CssHash),
Layout = previousPresentation.Layout,
Profile = previousPresentation.Profile,
TemplateHtml = compiled.TemplateHtml,
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs
index a7bfb80c..15215855 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs
@@ -67,18 +67,8 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
},
}, VisualBriefingJson.Canonical);
- // Keep in sync with the verification in VisualBriefingStore.ReadContentArtifactAsync:
- artifact.PayloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(artifact.Slots, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Charts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Controls, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Formulas, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.AccessibilityTexts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.SourceReferences, VisualBriefingJson.Canonical),
- artifact.ResetLabel,
- JsonSerializer.Serialize(artifact.SourceCoverage, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.AssetPlan, VisualBriefingJson.Canonical),
- artifact.StructuralSignature);
+ artifact.PayloadHash = VisualBriefingPayloadHash.ForContent(artifact.Slots, artifact.Charts, artifact.Controls, artifact.Formulas, artifact.AccessibilityTexts,
+ artifact.SourceReferences, artifact.ResetLabel, artifact.SourceCoverage, artifact.AssetPlan, artifact.StructuralSignature);
await store.WriteContentArtifactAsync(manifest.BriefingId, artifact, token);
build.ContentArtifactId = artifact.ArtifactId;
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs
index 4d59927c..46791db1 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs
@@ -46,12 +46,7 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
await FailAsync(store, build, stage, run, VisualBriefingValidationRule.REFERENCE_INVALID, token);
var response = run.Response!;
- var payloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(response.Facts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(response.Metrics, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(response.Tables, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(response.SourceCoverage, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(response.AssetPlan, VisualBriefingJson.Canonical));
+ var payloadHash = VisualBriefingPayloadHash.ForEvidence(response.Facts, response.Metrics, response.Tables, response.SourceCoverage, response.AssetPlan);
var artifact = new VisualBriefingEvidenceArtifact
{
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingHashing.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingHashing.cs
index 31c5f022..fbae19fd 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingHashing.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingHashing.cs
@@ -42,6 +42,20 @@ internal static class VisualBriefingHashing
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream, token));
}
+ ///
+ /// Returns canonical JSON for one value, with ordinally sorted object properties.
+ ///
+ ///
+ /// Hashed values go through here instead of being serialized directly. Plain serialization writes
+ /// properties in declaration order, which would tie every stored hash to the order in which the
+ /// members happen to appear in the C# file: reordering two properties would invalidate every
+ /// briefing already on disk, without any visible change to the data.
+ ///
+ /// The type of the value to canonicalize.
+ /// The value to canonicalize.
+ /// Compact canonical JSON.
+ internal static string CanonicalJson(T value) => CanonicalJson(JsonSerializer.SerializeToElement(value, VisualBriefingJson.Canonical));
+
///
/// Returns canonical JSON with ordinally sorted object properties.
///
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPayloadHash.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPayloadHash.cs
new file mode 100644
index 00000000..893e5772
--- /dev/null
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPayloadHash.cs
@@ -0,0 +1,102 @@
+namespace AIStudio.Assistants.VisualBriefing;
+
+///
+/// Computes the payload hashes that decide whether a stored intermediate artifact is still usable.
+///
+///
+/// Each formula lives here exactly once. The stage that writes an artifact and the store that reads it
+/// back have to agree on the sections down to their order, and they used to spell the formula out on
+/// both sides with a comment asking the next developer to keep them aligned. A single misplaced section
+/// makes the store discard every stored artifact of that kind, and it reports that as a missing
+/// artifact rather than as an error, so the mistake surfaces as a briefing that silently refuses to be
+/// reused. Sections are canonical JSON, which additionally makes the hashes independent of the order in
+/// which the artifact properties are declared.
+///
+internal static class VisualBriefingPayloadHash
+{
+ ///
+ /// Computes the payload hash of an evidence artifact.
+ ///
+ /// The extracted facts.
+ /// The extracted metrics.
+ /// The extracted tables.
+ /// The per-source coverage.
+ /// The planned visual assets.
+ /// The payload hash.
+ internal static string ForEvidence(
+ List facts,
+ List metrics,
+ List tables,
+ List sourceCoverage,
+ List assetPlan) =>
+ VisualBriefingHashing.ComputeSections(
+ VisualBriefingHashing.CanonicalJson(facts),
+ VisualBriefingHashing.CanonicalJson(metrics),
+ VisualBriefingHashing.CanonicalJson(tables),
+ VisualBriefingHashing.CanonicalJson(sourceCoverage),
+ VisualBriefingHashing.CanonicalJson(assetPlan));
+
+ ///
+ /// Computes the payload hash of a plan artifact.
+ ///
+ /// The planned sections.
+ /// The structural signature of the plan.
+ /// The payload hash.
+ internal static string ForPlan(
+ List sections,
+ string structuralSignature) => VisualBriefingHashing.ComputeSections(VisualBriefingHashing.CanonicalJson(sections), structuralSignature);
+
+ ///
+ /// Computes the payload hash of a content artifact.
+ ///
+ /// The filled content slots.
+ /// The chart specifications.
+ /// The interactive control specifications.
+ /// The formula specifications.
+ /// The accessibility texts per component.
+ /// The source references per component.
+ /// The localized reset label.
+ /// The per-source coverage.
+ /// The planned visual assets.
+ /// The structural signature of the business data.
+ /// The payload hash.
+ internal static string ForContent(
+ List slots,
+ List charts,
+ List controls,
+ List formulas,
+ Dictionary accessibilityTexts,
+ Dictionary> sourceReferences,
+ string resetLabel,
+ List sourceCoverage,
+ List assetPlan,
+ string structuralSignature) =>
+ VisualBriefingHashing.ComputeSections(
+ VisualBriefingHashing.CanonicalJson(slots),
+ VisualBriefingHashing.CanonicalJson(charts),
+ VisualBriefingHashing.CanonicalJson(controls),
+ VisualBriefingHashing.CanonicalJson(formulas),
+ VisualBriefingHashing.CanonicalJson(accessibilityTexts),
+ VisualBriefingHashing.CanonicalJson(sourceReferences),
+ resetLabel,
+ VisualBriefingHashing.CanonicalJson(sourceCoverage),
+ VisualBriefingHashing.CanonicalJson(assetPlan),
+ structuralSignature);
+
+ ///
+ /// Computes the payload hash of a presentation artifact.
+ ///
+ /// The compiled layout tree.
+ /// The design profile.
+ /// The hash of the compiled template.
+ /// The hash of the compiled CSS.
+ /// The payload hash.
+ internal static string ForPresentation(
+ VisualBriefingLayoutNode layout,
+ VisualBriefingDesignProfile profile,
+ string templateHash,
+ string cssHash) =>
+ VisualBriefingHashing.ComputeSections(
+ VisualBriefingHashing.CanonicalJson(layout),
+ profile.ToString(), templateHash, cssHash);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs
index 0b7d7fca..b3c6a029 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs
@@ -50,8 +50,6 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
await VisualBriefingEvidenceStage.FailAsync(store, build, stage, run, VisualBriefingValidationRule.REFERENCE_INVALID, token);
var sections = run.Response!.Sections;
- var payload = JsonSerializer.Serialize(sections, VisualBriefingJson.Canonical);
-
var structuralSignature = VisualBriefingHashing.Compute(string.Join('\u001f', sections.Select(section => $"{section.SectionId}:{section.Role}:{section.TitleSlotId}:{section.SummarySlotId}")
.Concat(sections.SelectMany(section => section.Components)
.Select(component =>
@@ -61,7 +59,7 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
{
ArtifactId = Guid.NewGuid(),
CreatedAtUtc = DateTimeOffset.UtcNow,
- PayloadHash = VisualBriefingHashing.ComputeSections(payload, structuralSignature),
+ PayloadHash = VisualBriefingPayloadHash.ForPlan(sections, structuralSignature),
Sections = sections,
StructuralSignature = structuralSignature,
Model = VisualBriefingModelNames.ExportLabel(provider),
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs
index a4846a08..9f66d464 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs
@@ -80,11 +80,7 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s
}
var compiled = VisualBriefingLayoutCompiler.Compile(plan, content, run.Response.Layout, run.Response.Profile);
- var payloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(run.Response.Layout, VisualBriefingJson.Canonical),
- run.Response.Profile.ToString(),
- compiled.TemplateHash,
- compiled.CssHash);
+ var payloadHash = VisualBriefingPayloadHash.ForPresentation(run.Response.Layout, run.Response.Profile, compiled.TemplateHash, compiled.CssHash);
var artifact = new VisualBriefingPresentationArtifact
{
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Builds.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Builds.cs
index e1076672..786fa80a 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Builds.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Builds.cs
@@ -163,13 +163,7 @@ public sealed partial class VisualBriefingStore
artifact.ArtifactId != artifactId)
return null;
- var hash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(artifact.Facts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Metrics, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Tables, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.SourceCoverage, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.AssetPlan, VisualBriefingJson.Canonical));
-
+ var hash = VisualBriefingPayloadHash.ForEvidence(artifact.Facts, artifact.Metrics, artifact.Tables, artifact.SourceCoverage, artifact.AssetPlan);
return string.Equals(hash, artifact.PayloadHash, StringComparison.Ordinal) ? artifact : null;
}
@@ -216,10 +210,8 @@ public sealed partial class VisualBriefingStore
artifact.ArtifactId != artifactId)
return null;
- var hash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(artifact.Sections, VisualBriefingJson.Canonical),
- artifact.StructuralSignature);
-
+ var hash = VisualBriefingPayloadHash.ForPlan(artifact.Sections, artifact.StructuralSignature);
+
return string.Equals(hash, artifact.PayloadHash, StringComparison.Ordinal) ? artifact : null;
}
@@ -272,18 +264,9 @@ public sealed partial class VisualBriefingStore
string.IsNullOrWhiteSpace(artifact.ResetLabel))
return null;
- var payloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(artifact.Slots, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Charts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Controls, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.Formulas, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.AccessibilityTexts, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.SourceReferences, VisualBriefingJson.Canonical),
- artifact.ResetLabel,
- JsonSerializer.Serialize(artifact.SourceCoverage, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(artifact.AssetPlan, VisualBriefingJson.Canonical),
- artifact.StructuralSignature);
-
+ var payloadHash = VisualBriefingPayloadHash.ForContent(artifact.Slots, artifact.Charts, artifact.Controls, artifact.Formulas, artifact.AccessibilityTexts,
+ artifact.SourceReferences, artifact.ResetLabel, artifact.SourceCoverage, artifact.AssetPlan, artifact.StructuralSignature);
+
return string.Equals(payloadHash, artifact.PayloadHash, StringComparison.Ordinal) ? artifact : null;
}
@@ -335,12 +318,7 @@ public sealed partial class VisualBriefingStore
artifact.ArtifactId != artifactId)
return null;
- var payloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(artifact.Layout, VisualBriefingJson.Canonical),
- artifact.Profile.ToString(),
- artifact.TemplateHash,
- artifact.CssHash);
-
+ var payloadHash = VisualBriefingPayloadHash.ForPresentation(artifact.Layout, artifact.Profile, artifact.TemplateHash, artifact.CssHash);
return string.Equals(payloadHash, artifact.PayloadHash, StringComparison.Ordinal) &&
string.Equals(
VisualBriefingHashing.Compute(artifact.TemplateHtml),
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Versions.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Versions.cs
index fa0dad9b..687cc6c9 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Versions.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Versions.cs
@@ -502,24 +502,10 @@ public sealed partial class VisualBriefingStore
new() { SlotId = "imported_data", Value = businessData },
};
- // Section order and count must match ReadContentArtifactAsync exactly:
- var contentHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(importedSlots, VisualBriefingJson.Canonical),
- "[]",
- "[]",
- "[]",
- "{}",
- "{}",
- "Reset",
- JsonSerializer.Serialize(coverage, VisualBriefingJson.Canonical),
- JsonSerializer.Serialize(assetPlan, VisualBriefingJson.Canonical),
- structuralSignature);
-
var content = new VisualBriefingContentArtifact
{
ArtifactId = Guid.NewGuid(),
CreatedAtUtc = DateTimeOffset.UtcNow,
- PayloadHash = contentHash,
Data = businessData,
Slots = importedSlots,
ResetLabel = "Reset",
@@ -528,6 +514,12 @@ public sealed partial class VisualBriefingStore
StructuralSignature = structuralSignature,
Model = "Imported artifact",
};
+
+ // An imported briefing carries no charts, controls, formulas, accessibility texts, or source
+ // references. Hashing the artifact itself keeps those empty sections in the right places
+ // without spelling them out as literals here.
+ content.PayloadHash = VisualBriefingPayloadHash.ForContent(content.Slots, content.Charts, content.Controls, content.Formulas, content.AccessibilityTexts,
+ content.SourceReferences, content.ResetLabel, content.SourceCoverage, content.AssetPlan, content.StructuralSignature);
var importedLayout = new VisualBriefingLayoutNode
{
@@ -550,11 +542,7 @@ public sealed partial class VisualBriefingStore
{
ArtifactId = Guid.NewGuid(),
CreatedAtUtc = DateTimeOffset.UtcNow,
- PayloadHash = VisualBriefingHashing.ComputeSections(
- JsonSerializer.Serialize(importedLayout, VisualBriefingJson.Canonical),
- VisualBriefingDesignProfile.EDITORIAL.ToString(),
- templateHash,
- cssHash),
+ PayloadHash = VisualBriefingPayloadHash.ForPresentation(importedLayout, VisualBriefingDesignProfile.EDITORIAL, templateHash, cssHash),
Layout = importedLayout,
Profile = VisualBriefingDesignProfile.EDITORIAL,
TemplateHtml = parts.TemplateHtml,