From 99937c8a3f05150deb2c7679fb8c24fffe54978a Mon Sep 17 00:00:00 2001 From: ExplodingUFO <1034917216@qq.com> Date: Wed, 13 May 2026 08:00:13 +0800 Subject: [PATCH] Add whiteboard persistence decision gate --- docs/en/phase-0-reactflow-parity-audit.md | 8 +- docs/zh-CN/phase-0-reactflow-parity-audit.md | 8 +- ...hWhiteboardPrimitivePersistenceDecision.cs | 57 ++++++++++++++ .../ReactFlowParityRoadmapDocsTests.cs | 38 ++++++++- ...mitivePersistenceDecisionContractsTests.cs | 78 +++++++++++++++++++ 5 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 src/AsterGraph.Core/Models/GraphWhiteboardPrimitivePersistenceDecision.cs create mode 100644 tests/AsterGraph.Editor.Tests/WhiteboardPrimitivePersistenceDecisionContractsTests.cs diff --git a/docs/en/phase-0-reactflow-parity-audit.md b/docs/en/phase-0-reactflow-parity-audit.md index 3cb1f297..61615c62 100644 --- a/docs/en/phase-0-reactflow-parity-audit.md +++ b/docs/en/phase-0-reactflow-parity-audit.md @@ -238,6 +238,10 @@ Phase 547 is GitHub #217 / `avalonia-node-map-rs0`, the first whiteboard primiti Phase 548 is GitHub #219 / `avalonia-node-map-10p`, the first whiteboard primitive renderer adapter skeleton. This internal Core adapter slice records `WHITEBOARD_PRIMITIVE_RENDERER_ADAPTER_SKELETON`: `GraphWhiteboardPrimitiveRendererAdapter` maps rectangle/freehand primitive projection into `GraphWhiteboardPrimitiveSceneSnapshot` scene items, and `GraphWhiteboardPrimitiveHitTestResult` records topmost primitive hit-testing evidence with z-order plus edit lifecycle metadata. The adapter remains renderer-neutral and separate from `GraphDocument`, `GraphEditorSceneSnapshot`, and `NodeCanvasConnectionSceneRenderer`. Phase 548 is stacked after PR #218 and must not merge before Phase 547. It authorizes no GraphDocument schema change, no public drawing API, no Avalonia renderer rewrite, no NodeCanvasConnectionSceneRenderer edits, no pointer coordinator changes, no toolbar/tool activation, no eraser behavior, no persistence/schema work, no schema version bump, no screenshot manifest expansion, no Cookbook visual proof implementation, no UI redesign, no retained API removal, and no full React Flow whiteboard parity. +## Phase 549 Update + +Phase 549 is GitHub #221 / `avalonia-node-map-3l6`, the whiteboard primitive persistence decision implementation gate. This internal Core policy slice records `WHITEBOARD_PRIMITIVE_PERSISTENCE_DECISION_GATE`: `GraphWhiteboardPrimitivePersistenceDecision` chooses a separate annotation surface and `ExcludedFromCurrentGraphDocumentSchema` for current whiteboard primitives, while keeping `GraphDocumentCompatibility`, `GraphDocumentSerializer`, and `CurrentSchemaVersion` graph-document scoped. The contract lists migration policy, compatibility tests, workspace persistence boundary tests, clipboard fragment boundary tests, and screenshot artifact boundary tests as required before any future GraphDocument schema change. Phase 549 is stacked after PR #220 and must not merge before Phase 548. It authorizes no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no public drawing API, no Avalonia pointer coordinator edits, no toolbar/tool activation, no eraser behavior, no screenshot manifest expansion, no Cookbook visual proof implementation, no UI redesign, no retained API removal, and no full React Flow whiteboard parity. + ## Phase 489 Update Phase 489 closed GitHub #101 / `avalonia-node-map-6sc` through PR #102 as a renderer virtualization design spike on branch `perf/renderer-virtualization-spike`. This slice was docs/tests only: it defined the proof contract required before any future ItemsRepeater/Skia-style renderer virtualization, background graph index, or graph-size claim expansion. It made no public API change and no runtime change. The current evidence remains viewport-budgeted scene projection/rendering, not a true renderer virtualization contract; `xlarge` stays telemetry-only. @@ -567,6 +571,8 @@ Phase 547 records the first whiteboard primitive model skeleton through GitHub # Phase 548 records the whiteboard primitive renderer adapter skeleton through GitHub #219 / `avalonia-node-map-10p`. It adds only internal renderer-neutral projection and topmost primitive hit-testing evidence for the Phase 547 primitive model; persistence/schema, Avalonia pointer coordinators, toolbar activation, eraser behavior, screenshot manifests, Cookbook implementation, and any full React Flow whiteboard parity claim remain later tracker-backed work. +Phase 549 records the whiteboard primitive persistence decision implementation gate through GitHub #221 / `avalonia-node-map-3l6`. It adds only the internal `GraphWhiteboardPrimitivePersistenceDecision` policy contract: current whiteboard primitives use a separate annotation surface, remain `ExcludedFromCurrentGraphDocumentSchema`, and do not change `GraphDocumentCompatibility`, `GraphDocumentSerializer`, `CurrentSchemaVersion`, workspace persistence, clipboard fragments, screenshot artifacts, or public drawing APIs. + | GitHub | Bead | Title | Priority | Likely write set | Parallelism | | --- | --- | --- | --- | --- | --- | | #193 | `avalonia-node-map-8l6` | Phase 535: refresh post-lasso visual feedback parity queue | P2 | parity roadmap docs and focused docs tests | Current docs/test queue refresh. Blocks the next implementation wave because it replaces the stale Phase 534 current row with tracker-backed follow-ups. | @@ -583,7 +589,7 @@ Phase 548 records the whiteboard primitive renderer adapter skeleton through Git | #215 | `avalonia-node-map-0l9` | Phase 546: post-Phase-545 whiteboard implementation queue refresh | P4 | parity roadmap docs and focused docs tests | Stacked after PR #214; do not merge before Phase 545. This queue refresh records the next candidates without runtime/API/model/schema/renderer/screenshot-manifest behavior. | | #217 | `avalonia-node-map-rs0` | Phase 547: whiteboard primitive model skeleton | P2 | Core/Editor primitive model tests, internal model types, and renderer-neutral contract docs | Stacked after PR #216; do not merge before Phase 546. This slice adds the internal model skeleton only and must not touch renderer adapters, persistence/schema, pointer coordinators, screenshot manifests, toolbar UX, or eraser behavior. | | #219 | `avalonia-node-map-10p` | Phase 548: whiteboard primitive renderer adapter skeleton | P2 | renderer adapter tests, scene snapshot projection proof, and topmost primitive hit-testing evidence | Stacked after PR #218; do not merge before Phase 547. Must keep persistence/schema, Cookbook screenshot rows, pointer coordinators, and toolbar activation out of scope. | -| TBD | TBD | Phase 549: whiteboard primitive persistence decision implementation gate | P3 | persistence decision tests, schema/surface choice, migration criteria, and compatibility coverage | Candidate after Phase 547 and the Phase 544 policy gate. Must decide storage ownership before any saved whiteboard primitive claim. | +| #221 | `avalonia-node-map-3l6` | Phase 549: whiteboard primitive persistence decision implementation gate | P3 | persistence decision tests, schema/surface choice, migration criteria, and compatibility coverage | Stacked after PR #220; do not merge before Phase 548. Chooses separate annotation surface and keeps GraphDocument schema/version/workspace persistence unchanged before any saved whiteboard primitive claim. | | TBD | TBD | Phase 550: whiteboard primitive Cookbook screenshot implementation gate | P3 | Cookbook screenshot implementation gate docs/tests, route metadata, shell state, and non-overlap visual proof | Candidate after model, renderer, and persistence decisions. Must add visual proof before any whiteboard visual parity claim. | ## Recommended Parallel Worktree Plan diff --git a/docs/zh-CN/phase-0-reactflow-parity-audit.md b/docs/zh-CN/phase-0-reactflow-parity-audit.md index 85993658..8015c694 100644 --- a/docs/zh-CN/phase-0-reactflow-parity-audit.md +++ b/docs/zh-CN/phase-0-reactflow-parity-audit.md @@ -238,6 +238,10 @@ Phase 547 是 GitHub #217 / `avalonia-node-map-rs0`,承接第一条 whiteboard Phase 548 是 GitHub #219 / `avalonia-node-map-10p`,承接第一条 whiteboard primitive renderer adapter skeleton。本 internal Core adapter slice 记录 `WHITEBOARD_PRIMITIVE_RENDERER_ADAPTER_SKELETON`:`GraphWhiteboardPrimitiveRendererAdapter` 将 rectangle/freehand primitive projection 映射为 `GraphWhiteboardPrimitiveSceneSnapshot` scene items,`GraphWhiteboardPrimitiveHitTestResult` 记录带 z-order 与 edit lifecycle metadata 的 topmost primitive hit-testing evidence。该 adapter 保持 renderer-neutral,并继续与 `GraphDocument`、`GraphEditorSceneSnapshot` 和 `NodeCanvasConnectionSceneRenderer` 分离。Phase 548 stacked after PR #218,不能早于 Phase 547 合并。它不授权 no GraphDocument schema change、no public drawing API、no Avalonia renderer rewrite、no NodeCanvasConnectionSceneRenderer edits、no pointer coordinator changes、no toolbar/tool activation、no eraser behavior、no persistence/schema work、no schema version bump、no screenshot manifest expansion、no Cookbook visual proof implementation、no UI redesign、no retained API removal 或 no full React Flow whiteboard parity。 +## Phase 549 更新 + +Phase 549 是 GitHub #221 / `avalonia-node-map-3l6`,记录 whiteboard primitive persistence decision implementation gate。本 internal Core policy slice 记录 `WHITEBOARD_PRIMITIVE_PERSISTENCE_DECISION_GATE`:`GraphWhiteboardPrimitivePersistenceDecision` 为当前 whiteboard primitives 选择 separate annotation surface,并标记 `ExcludedFromCurrentGraphDocumentSchema`,同时继续把 `GraphDocumentCompatibility`、`GraphDocumentSerializer` 和 `CurrentSchemaVersion` 限定为 graph-document scope。该 contract 记录任何后续 GraphDocument schema change 前必须补齐 migration policy、compatibility tests、workspace persistence boundary tests、clipboard fragment boundary tests 和 screenshot artifact boundary tests。Phase 549 stacked after PR #220,不能早于 Phase 548 合并。它不授权 no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no public drawing API、no Avalonia pointer coordinator edits、no toolbar/tool activation、no eraser behavior、no screenshot manifest expansion、no Cookbook visual proof implementation、no UI redesign、no retained API removal 或 no full React Flow whiteboard parity。 + ## Phase 489 更新 Phase 489 通过 PR #102 关闭 GitHub #101 / `avalonia-node-map-6sc`,完成 `perf/renderer-virtualization-spike` 分支上的 renderer virtualization design spike。本 slice 只做 docs/tests:先定义未来声明 ItemsRepeater/Skia-style renderer virtualization、background graph index 或扩大 graph-size claim 前必须满足的 proof contract。不做 public API change,也不做 runtime change。当前证据仍只支持 viewport-budgeted scene projection/rendering,不是真正的 renderer virtualization contract;`xlarge` 继续保持 telemetry-only。 @@ -567,6 +571,8 @@ Phase 547 通过 GitHub #217 / `avalonia-node-map-rs0` 记录第一条 whiteboar Phase 548 记录 whiteboard primitive renderer adapter skeleton,通过 GitHub #219 / `avalonia-node-map-10p`。它只为 Phase 547 primitive model 添加 internal renderer-neutral projection 与 topmost primitive hit-testing evidence;persistence/schema、Avalonia pointer coordinators、toolbar activation、eraser behavior、screenshot manifests、Cookbook implementation,以及任何 full React Flow whiteboard parity claim 都保留为后续 tracker-backed work。 +Phase 549 记录 whiteboard primitive persistence decision implementation gate,通过 GitHub #221 / `avalonia-node-map-3l6`。它只添加 internal `GraphWhiteboardPrimitivePersistenceDecision` policy contract:当前 whiteboard primitives 使用 separate annotation surface,保持 `ExcludedFromCurrentGraphDocumentSchema`,并且不修改 `GraphDocumentCompatibility`、`GraphDocumentSerializer`、`CurrentSchemaVersion`、workspace persistence、clipboard fragments、screenshot artifacts 或 public drawing APIs。 + | GitHub | Bead | 标题 | 优先级 | 可能 write set | 并行边界 | | --- | --- | --- | --- | --- | --- | | #193 | `avalonia-node-map-8l6` | Phase 535: refresh post-lasso visual feedback parity queue | P2 | parity roadmap docs 和 focused docs tests | Current docs/test queue refresh。它用 tracker-backed follow-ups 替换 stale Phase 534 current row,因此会阻塞下一批 implementation wave。 | @@ -583,7 +589,7 @@ Phase 548 记录 whiteboard primitive renderer adapter skeleton,通过 GitHub | #215 | `avalonia-node-map-0l9` | Phase 546: post-Phase-545 whiteboard implementation queue refresh | P4 | parity roadmap docs 和 focused docs tests | Stacked after PR #214;do not merge before Phase 545。本 queue refresh 只记录下一批 candidates,不做 runtime/API/model/schema/renderer/screenshot-manifest behavior。 | | #217 | `avalonia-node-map-rs0` | Phase 547: whiteboard primitive model skeleton | P2 | Core/Editor primitive model tests、internal model types 和 renderer-neutral contract docs | Stacked after PR #216;do not merge before Phase 546。本 slice 只添加 internal model skeleton,不得触碰 renderer adapters、persistence/schema、pointer coordinators、screenshot manifests、toolbar UX 或 eraser behavior。 | | #219 | `avalonia-node-map-10p` | Phase 548: whiteboard primitive renderer adapter skeleton | P2 | renderer adapter tests、scene snapshot projection proof 和 topmost primitive hit-testing evidence | Stacked after PR #218;do not merge before Phase 547。persistence/schema、Cookbook screenshot rows、pointer coordinators 和 toolbar activation 都保持 out of scope。 | -| TBD | TBD | Phase 549: whiteboard primitive persistence decision implementation gate | P3 | persistence decision tests、schema/surface choice、migration criteria 和 compatibility coverage | Phase 547 与 Phase 544 policy gate 后的 candidate。任何 saved whiteboard primitive claim 前必须先决定 storage ownership。 | +| #221 | `avalonia-node-map-3l6` | Phase 549: whiteboard primitive persistence decision implementation gate | P3 | persistence decision tests、schema/surface choice、migration criteria 和 compatibility coverage | Stacked after PR #220;do not merge before Phase 548。本 slice 选择 separate annotation surface,并在任何 saved whiteboard primitive claim 前保持 GraphDocument schema/version/workspace persistence unchanged。 | | TBD | TBD | Phase 550: whiteboard primitive Cookbook screenshot implementation gate | P3 | Cookbook screenshot implementation gate docs/tests、route metadata、shell state 和 non-overlap visual proof | model、renderer 与 persistence decisions 后的 candidate。任何 whiteboard visual parity claim 前必须先补 visual proof。 | ## 推荐并行 Worktree 计划 diff --git a/src/AsterGraph.Core/Models/GraphWhiteboardPrimitivePersistenceDecision.cs b/src/AsterGraph.Core/Models/GraphWhiteboardPrimitivePersistenceDecision.cs new file mode 100644 index 00000000..e395b03a --- /dev/null +++ b/src/AsterGraph.Core/Models/GraphWhiteboardPrimitivePersistenceDecision.cs @@ -0,0 +1,57 @@ +namespace AsterGraph.Core.Models; + +/// +/// Internal policy gate for future whiteboard primitive persistence. +/// +internal sealed record GraphWhiteboardPrimitivePersistenceDecision( + GraphWhiteboardPrimitivePersistenceOwner Owner, + GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy GraphDocumentSchemaPolicy, + GraphWhiteboardPrimitivePersistenceRequirement RequirementsBeforeGraphDocumentSchemaChange) +{ + /// + /// Current Phase 549 decision: whiteboard primitives are not part of the current graph document schema. + /// + public static GraphWhiteboardPrimitivePersistenceDecision Current { get; } = new( + GraphWhiteboardPrimitivePersistenceOwner.SeparateAnnotationSurface, + GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy.ExcludedFromCurrentGraphDocumentSchema, + GraphWhiteboardPrimitivePersistenceRequirement.MigrationPolicy + | GraphWhiteboardPrimitivePersistenceRequirement.GraphDocumentCompatibilityTests + | GraphWhiteboardPrimitivePersistenceRequirement.WorkspacePersistenceBoundaryTests + | GraphWhiteboardPrimitivePersistenceRequirement.ClipboardFragmentBoundaryTests + | GraphWhiteboardPrimitivePersistenceRequirement.ScreenshotArtifactBoundaryTests); + + /// + /// Whether the active decision stores whiteboard primitives inside GraphDocument payloads. + /// + public bool PersistsWhiteboardPrimitivesInGraphDocument + => Owner == GraphWhiteboardPrimitivePersistenceOwner.GraphDocumentSchema + && GraphDocumentSchemaPolicy == GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy.IncludedInGraphDocumentSchema; + + /// + /// Whether this implementation slice persists whiteboard primitives at all. + /// + public bool PersistsWhiteboardPrimitivesInCurrentSlice => false; +} + +internal enum GraphWhiteboardPrimitivePersistenceOwner +{ + GraphDocumentSchema, + SeparateAnnotationSurface, +} + +internal enum GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy +{ + ExcludedFromCurrentGraphDocumentSchema, + IncludedInGraphDocumentSchema, +} + +[Flags] +internal enum GraphWhiteboardPrimitivePersistenceRequirement +{ + None = 0, + MigrationPolicy = 1 << 0, + GraphDocumentCompatibilityTests = 1 << 1, + WorkspacePersistenceBoundaryTests = 1 << 2, + ClipboardFragmentBoundaryTests = 1 << 3, + ScreenshotArtifactBoundaryTests = 1 << 4, +} diff --git a/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs b/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs index b8c46379..0f51b988 100644 --- a/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs +++ b/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs @@ -1437,6 +1437,39 @@ public void ParityRoadmapDocs_RecordPhase548WhiteboardPrimitiveRendererAdapterSk Assert.Contains("Phase 548 记录 whiteboard primitive renderer adapter skeleton", chineseParity, StringComparison.Ordinal); } + [Fact] + public void ParityRoadmapDocs_RecordPhase549WhiteboardPrimitivePersistenceDecisionGateInBothLocales() + { + var englishParity = ReadRepoFile("docs/en/phase-0-reactflow-parity-audit.md"); + var chineseParity = ReadRepoFile("docs/zh-CN/phase-0-reactflow-parity-audit.md"); + + foreach (var contents in new[] { englishParity, chineseParity }) + { + Assert.Contains("Phase 549", contents, StringComparison.Ordinal); + Assert.Contains("GitHub #221", contents, StringComparison.Ordinal); + Assert.Contains("avalonia-node-map-3l6", contents, StringComparison.Ordinal); + Assert.Contains("whiteboard primitive persistence decision implementation gate", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("WHITEBOARD_PRIMITIVE_PERSISTENCE_DECISION_GATE", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardPrimitivePersistenceDecision", contents, StringComparison.Ordinal); + Assert.Contains("separate annotation surface", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("ExcludedFromCurrentGraphDocumentSchema", contents, StringComparison.Ordinal); + Assert.Contains("GraphDocumentCompatibility", contents, StringComparison.Ordinal); + Assert.Contains("CurrentSchemaVersion", contents, StringComparison.Ordinal); + Assert.Contains("no GraphDocument schema change", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no schema version bump", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no workspace persistence behavior change", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no public drawing API", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no full React Flow whiteboard parity", contents, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("| TBD | TBD | Phase 549: whiteboard primitive persistence decision implementation gate", contents, StringComparison.Ordinal); + Assert.DoesNotContain("whiteboard primitives are persisted in GraphDocument", contents, StringComparison.OrdinalIgnoreCase); + } + + AssertPostPhase545Queue(ExtractIssueWaveTable(englishParity)); + AssertPostPhase545Queue(ExtractIssueWaveTable(chineseParity)); + Assert.Contains("Phase 549 records the whiteboard primitive persistence decision implementation gate", englishParity, StringComparison.Ordinal); + Assert.Contains("Phase 549 记录 whiteboard primitive persistence decision implementation gate", chineseParity, StringComparison.Ordinal); + } + [Fact] public void ParityRoadmapDocs_RecordPhase501PostPhase500QueueRefreshInBothLocales() { @@ -1645,7 +1678,7 @@ private static void AssertPostPhase545Queue(string table) Assert.Contains("| #215 | `avalonia-node-map-0l9` | Phase 546: post-Phase-545 whiteboard implementation queue refresh", table, StringComparison.Ordinal); Assert.Contains("| #217 | `avalonia-node-map-rs0` | Phase 547: whiteboard primitive model skeleton", table, StringComparison.Ordinal); Assert.Contains("| #219 | `avalonia-node-map-10p` | Phase 548: whiteboard primitive renderer adapter skeleton", table, StringComparison.Ordinal); - Assert.Contains("Phase 549: whiteboard primitive persistence decision implementation gate", table, StringComparison.Ordinal); + Assert.Contains("| #221 | `avalonia-node-map-3l6` | Phase 549: whiteboard primitive persistence decision implementation gate", table, StringComparison.Ordinal); Assert.Contains("Phase 550: whiteboard primitive Cookbook screenshot implementation gate", table, StringComparison.Ordinal); Assert.Contains("TBD", table, StringComparison.Ordinal); Assert.Contains("Core/Editor primitive model tests", table, StringComparison.OrdinalIgnoreCase); @@ -1661,12 +1694,15 @@ private static void AssertPostPhase545Queue(string table) Assert.Contains("do not merge before Phase 546", table, StringComparison.OrdinalIgnoreCase); Assert.Contains("Stacked after PR #218", table, StringComparison.OrdinalIgnoreCase); Assert.Contains("do not merge before Phase 547", table, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Stacked after PR #220", table, StringComparison.OrdinalIgnoreCase); + Assert.Contains("do not merge before Phase 548", table, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("| #206 | `avalonia-node-map-b31` | Phase 542: whiteboard primitive core model contract gate | P2 | core/editor model tests, primitive identity/geometry/style contract docs, and API inventory | Future candidate", table, StringComparison.Ordinal); Assert.DoesNotContain("| #207 | `avalonia-node-map-aj8` | Phase 543: whiteboard renderer projection and hit-testing proof gate | P2 | renderer projection tests, hit-testing proof, and bounded Avalonia/editor evidence | Future candidate", table, StringComparison.Ordinal); Assert.DoesNotContain("| #208 | `avalonia-node-map-32n` | Phase 544: whiteboard primitive persistence schema policy gate | P3 | persistence/schema policy docs, migration criteria, and compatibility tests | Future candidate", table, StringComparison.Ordinal); Assert.DoesNotContain("| #209 | `avalonia-node-map-7ns` | Phase 545: whiteboard Cookbook and screenshot proof route gate | P3 | Cookbook route, screenshot proof route, visual gate docs, and focused tests | Future candidate", table, StringComparison.Ordinal); Assert.DoesNotContain("| TBD | TBD | Phase 547: whiteboard primitive model skeleton", table, StringComparison.Ordinal); Assert.DoesNotContain("| TBD | TBD | Phase 548: whiteboard primitive renderer adapter skeleton", table, StringComparison.Ordinal); + Assert.DoesNotContain("| TBD | TBD | Phase 549: whiteboard primitive persistence decision implementation gate", table, StringComparison.Ordinal); } private static void AssertBuiltInComponentMatrix(string table) diff --git a/tests/AsterGraph.Editor.Tests/WhiteboardPrimitivePersistenceDecisionContractsTests.cs b/tests/AsterGraph.Editor.Tests/WhiteboardPrimitivePersistenceDecisionContractsTests.cs new file mode 100644 index 00000000..70efb126 --- /dev/null +++ b/tests/AsterGraph.Editor.Tests/WhiteboardPrimitivePersistenceDecisionContractsTests.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using AsterGraph.Core.Models; +using AsterGraph.Core.Serialization; +using Xunit; + +namespace AsterGraph.Editor.Tests; + +public sealed class WhiteboardPrimitivePersistenceDecisionContractsTests +{ + [Fact] + public void WhiteboardPrimitivePersistenceDecision_RecordsSeparateAnnotationSurfaceOutsideGraphDocumentSchema() + { + var decision = GraphWhiteboardPrimitivePersistenceDecision.Current; + + Assert.False(typeof(GraphWhiteboardPrimitivePersistenceDecision).IsPublic); + Assert.Equal(typeof(GraphWhiteboardPrimitive).Assembly, typeof(GraphWhiteboardPrimitivePersistenceDecision).Assembly); + Assert.Equal(GraphWhiteboardPrimitivePersistenceOwner.SeparateAnnotationSurface, decision.Owner); + Assert.Equal( + GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy.ExcludedFromCurrentGraphDocumentSchema, + decision.GraphDocumentSchemaPolicy); + Assert.False(decision.PersistsWhiteboardPrimitivesInGraphDocument); + Assert.False(decision.PersistsWhiteboardPrimitivesInCurrentSlice); + AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement.MigrationPolicy); + AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement.GraphDocumentCompatibilityTests); + AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement.WorkspacePersistenceBoundaryTests); + AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement.ClipboardFragmentBoundaryTests); + AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement.ScreenshotArtifactBoundaryTests); + + void AssertRequiredBeforeSchemaChange(GraphWhiteboardPrimitivePersistenceRequirement requirement) + { + Assert.True( + decision.RequirementsBeforeGraphDocumentSchemaChange.HasFlag(requirement), + $"Expected Phase 549 persistence decision to require {requirement} before any GraphDocument schema change."); + } + } + + [Fact] + public void GraphDocumentSerialization_RemainsWhiteboardFreeUnderPersistenceDecision() + { + var decision = GraphWhiteboardPrimitivePersistenceDecision.Current; + var document = new GraphDocument( + "Persistence Boundary", + "Graph document remains graph-scoped.", + [], + [], + []); + + var json = GraphDocumentSerializer.Serialize(document); + + Assert.Equal(GraphWhiteboardPrimitiveGraphDocumentSchemaPolicy.ExcludedFromCurrentGraphDocumentSchema, decision.GraphDocumentSchemaPolicy); + Assert.False(decision.PersistsWhiteboardPrimitivesInGraphDocument); + Assert.Equal(6, GraphDocumentCompatibility.CurrentSchemaVersion); + Assert.DoesNotContain("Whiteboard", json, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("Primitive", json, StringComparison.OrdinalIgnoreCase); + AssertNoWhiteboardProperty(typeof(GraphDocument)); + AssertNoWhiteboardProperty(typeof(GraphScope)); + AssertNoWhiteboardProperty(typeof(GraphDocumentSerializer.GraphDocumentFilePayload)); + + using var parsed = JsonDocument.Parse(json); + var root = parsed.RootElement; + Assert.Equal(6, root.GetProperty(nameof(GraphDocumentSerializer.GraphDocumentFilePayload.SchemaVersion)).GetInt32()); + Assert.True(root.TryGetProperty(nameof(GraphDocumentSerializer.GraphDocumentFilePayload.GraphScopes), out _)); + Assert.DoesNotContain( + root.EnumerateObject(), + property => property.Name.Contains("Whiteboard", StringComparison.OrdinalIgnoreCase) + || property.Name.Contains("Primitive", StringComparison.OrdinalIgnoreCase)); + } + + private static void AssertNoWhiteboardProperty(Type type) + { + Assert.DoesNotContain( + type.GetProperties(), + property => property.Name.Contains("Whiteboard", StringComparison.OrdinalIgnoreCase) + || property.Name.Contains("Primitive", StringComparison.OrdinalIgnoreCase) + || property.PropertyType.Name.Contains("Whiteboard", StringComparison.OrdinalIgnoreCase) + || property.PropertyType.Name.Contains("Primitive", StringComparison.OrdinalIgnoreCase)); + } +}