diff --git a/docs/en/phase-0-reactflow-parity-audit.md b/docs/en/phase-0-reactflow-parity-audit.md index db48c575..0dceb013 100644 --- a/docs/en/phase-0-reactflow-parity-audit.md +++ b/docs/en/phase-0-reactflow-parity-audit.md @@ -617,6 +617,8 @@ Phase 556 records the whiteboard primitive persistence deferred decision through Phase 557 records the whiteboard annotation store contract gate through GitHub #237 / `avalonia-node-map-zfe`. It defines `WHITEBOARD_ANNOTATION_STORE_CONTRACT_GATE` as the next separate annotation-store contract before saved whiteboard primitives may exist: future work must define store ownership, store lifetime, annotation identity, primitive reference rules, style/geometry serialization, edit lifecycle serialization, migration metadata, read/write API boundary candidates, and compatibility proof across `GraphDocumentCompatibility`, `WorkspacePersistence`, `ClipboardFragment`, `SceneExportArtifact`, and `ScreenshotArtifact`. This gate keeps no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no saved whiteboard primitive state, and no full React Flow whiteboard parity. +Phase 558 records the whiteboard annotation store contract skeleton through GitHub #239 / `avalonia-node-map-84u`. It adds `WHITEBOARD_ANNOTATION_STORE_CONTRACT_SKELETON` as internal Core contract coverage only: `GraphWhiteboardAnnotationStoreContract`, `GraphWhiteboardAnnotationStoreMetadata`, `GraphWhiteboardAnnotationIdentity`, `GraphWhiteboardPrimitiveReference`, `GraphWhiteboardAnnotationPrimitivePayload`, `GraphWhiteboardAnnotationRecord`, `GraphWhiteboardAnnotationStoreSnapshot`, `GraphWhiteboardAnnotationMigrationMetadata`, and `IGraphWhiteboardAnnotationStoreBoundary` define store ownership, workspace-scoped lifetime, annotation identity, primitive reference, style/geometry/edit lifecycle payload shape, migration/schema metadata, and a persistence-neutral read/write boundary. This skeleton keeps no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no renderer or pointer behavior change, no public API exposure, no saved whiteboard primitive state, and no full React Flow whiteboard parity. + | 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. | @@ -642,6 +644,7 @@ Phase 557 records the whiteboard annotation store contract gate through GitHub # | #229 | `avalonia-node-map-71c` | Phase 555: whiteboard primitive eraser behavior route | P3 | eraser-on-primitive behavior tests, primitive hit-testing, and graph-deletion separation | Depends on Phase 554. Owns eraser-on-primitive behavior only; graph-selection delete remains separate and no broad eraser cursor redesign, GraphDocument schema changes, renderer rewrite, retained API removal, or full whiteboard parity claim is authorized. | | #230 | `avalonia-node-map-bck` | Phase 556: whiteboard primitive persistence implementation decision | P4 | persistence implementation decision, schema/version evidence, workspace and clipboard boundaries | Depends on Phase 555; stacked after PR #235 and do not merge before Phase 555. Records `DeferredUntilSeparateAnnotationStoreContract` with boundary coverage only; no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no saved whiteboard primitive state, or full whiteboard parity claim. | | #237 | `avalonia-node-map-zfe` | Phase 557: whiteboard annotation store contract gate | P4 | annotation-store contract docs, persistence boundary requirements, and focused docs tests | Depends on Phase 556; stacked after PR #236 and do not merge before Phase 556. Defines the separate annotation-store contract gate only; no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no saved whiteboard primitive state, or full whiteboard parity claim. | +| #239 | `avalonia-node-map-84u` | Phase 558: whiteboard annotation store contract skeleton | P4 | internal Core annotation-store contract types, boundary tests, and parity docs | Depends on Phase 557; stacked after PR #238 and do not merge before Phase 557. Adds the internal contract skeleton only; no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no renderer or pointer behavior change, no public API exposure, no saved whiteboard primitive state, or full whiteboard parity claim. | ## Recommended Parallel Worktree Plan @@ -700,6 +703,7 @@ Phase 557 records the whiteboard annotation store contract gate through GitHub # - `feature/phase-555-whiteboard-eraser-behavior`: owns #229 / `avalonia-node-map-71c`; current stacked implementation worktree for eraser-on-primitive behavior after Phase 554, with graph `selection.delete`, persistence/schema, renderer rewrite, and full whiteboard parity out of scope. - `feature/phase-556-whiteboard-persistence-decision`: owns #230 / `avalonia-node-map-bck`; current stacked implementation/deferred-decision worktree after Phase 555, with no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no saved whiteboard primitive state, and no full React Flow whiteboard parity. - `docs/phase-557-whiteboard-annotation-store-contract`: owns #237 / `avalonia-node-map-zfe`; current stacked docs/test contract gate after Phase 556, with no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, and no saved whiteboard primitive state. +- `feature/phase-558-whiteboard-annotation-store-contract-skeleton`: owns #239 / `avalonia-node-map-84u`; current stacked internal Core contract skeleton after Phase 557, with no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no renderer or pointer behavior change, no public API exposure, and no saved whiteboard primitive state. ## UI Verification Policy @@ -773,4 +777,5 @@ Current coverage includes scene-level route captures plus ten manifest-driven fu - Phase 555 is GitHub #229 / `avalonia-node-map-71c`; it records the whiteboard primitive eraser behavior route through `NodeCanvasWhiteboardDrawingMode.Eraser`, `whiteboard-drawing.eraser`, `PART_WhiteboardEraserButton`, `TryEraseWhiteboardPrimitive_RemovesTopmostHitPrimitiveWithoutClearingOthers`, and `HandlePressed_WithWhiteboardEraserMode_RemovesHitPrimitiveWithoutDeletingGraphSelection`. Graph-selection delete remains separate through graph `selection.delete`; broad eraser cursor redesign, persisted whiteboard primitive state, GraphDocument schema changes, renderer rewrites, retained API removal, and full React Flow whiteboard parity remain out of scope. - Phase 556 is GitHub #230 / `avalonia-node-map-bck`; it records the whiteboard primitive persistence implementation decision through `GraphWhiteboardPrimitivePersistenceOutcome.DeferredUntilSeparateAnnotationStoreContract` and `GraphWhiteboardPrimitivePersistenceBoundary` coverage for `GraphDocumentCompatibility`, `WorkspacePersistence`, `ClipboardFragment`, `ScreenshotArtifact`, and `SceneExportArtifact`. It keeps no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no saved whiteboard primitive state, and no full React Flow whiteboard parity. - Phase 557 is GitHub #237 / `avalonia-node-map-zfe`; it records the whiteboard annotation store contract gate and `WHITEBOARD_ANNOTATION_STORE_CONTRACT_GATE` with store ownership, store lifetime, annotation identity, primitive reference, style/geometry serialization, edit lifecycle serialization, migration metadata, read/write API boundary candidates, and compatibility requirements across GraphDocument, workspace, clipboard/export, scene export, and screenshot artifacts. It keeps no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no saved whiteboard primitive state, and no full React Flow whiteboard parity. +- Phase 558 is GitHub #239 / `avalonia-node-map-84u`; it records `WHITEBOARD_ANNOTATION_STORE_CONTRACT_SKELETON` with internal Core annotation-store contract types, annotation identity/reference records, primitive payload shape, migration metadata, and a persistence-neutral `IGraphWhiteboardAnnotationStoreBoundary`. It keeps no production annotation store, no GraphDocument schema change, no schema version bump, no workspace persistence behavior change, no clipboard/export serialization change, no screenshot manifest expansion, no renderer or pointer behavior change, no public API exposure, no saved whiteboard primitive state, and no full React Flow whiteboard parity. - Product code remains out of scope for Phase 478, Phase 484, Phase 490, Phase 491, Phase 492, Phase 493, Phase 494, Phase 495, Phase 497, Phase 498, Phase 499, Phase 500, Phase 501, Phase 502, Phase 503, Phase 504, Phase 505, Phase 506, Phase 507, Phase 508, Phase 509, Phase 510, Phase 511, Phase 512, Phase 513, Phase 520, Phase 521, Phase 522, Phase 523, Phase 524, Phase 525, Phase 526, Phase 527, Phase 528, Phase 529, Phase 535, Phase 539, Phase 540, Phase 541, Phase 546, Phase 550, Phase 551, and Phase 557 unless a focused test proves a specific missing contract. diff --git a/docs/zh-CN/phase-0-reactflow-parity-audit.md b/docs/zh-CN/phase-0-reactflow-parity-audit.md index 8203edf9..3272a476 100644 --- a/docs/zh-CN/phase-0-reactflow-parity-audit.md +++ b/docs/zh-CN/phase-0-reactflow-parity-audit.md @@ -617,6 +617,8 @@ Phase 556 记录 whiteboard primitive persistence deferred decision,通过 Git Phase 557 记录 whiteboard annotation store contract gate,通过 GitHub #237 / `avalonia-node-map-zfe`。它把 `WHITEBOARD_ANNOTATION_STORE_CONTRACT_GATE` 定义为 saved whiteboard primitives 之前必须先具备的 separate annotation-store contract:后续实现必须明确 store ownership、store lifetime、annotation identity、primitive reference 规则、style/geometry serialization、edit lifecycle serialization、migration metadata、read/write API boundary candidates,以及覆盖 `GraphDocumentCompatibility`、`WorkspacePersistence`、`ClipboardFragment`、`SceneExportArtifact` 和 `ScreenshotArtifact` 的 compatibility proof。本 gate 保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 +Phase 558 记录 whiteboard annotation store contract skeleton,通过 GitHub #239 / `avalonia-node-map-84u`。它只添加 internal Core contract coverage:`GraphWhiteboardAnnotationStoreContract`、`GraphWhiteboardAnnotationStoreMetadata`、`GraphWhiteboardAnnotationIdentity`、`GraphWhiteboardPrimitiveReference`、`GraphWhiteboardAnnotationPrimitivePayload`、`GraphWhiteboardAnnotationRecord`、`GraphWhiteboardAnnotationStoreSnapshot`、`GraphWhiteboardAnnotationMigrationMetadata` 和 `IGraphWhiteboardAnnotationStoreBoundary` 定义 store ownership、workspace-scoped lifetime、annotation identity、primitive reference、style/geometry/edit lifecycle payload shape、migration/schema metadata,以及 persistence-neutral read/write boundary。本 skeleton 保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no renderer or pointer behavior change、no public API exposure、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 + | 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。 | @@ -642,6 +644,7 @@ Phase 557 记录 whiteboard annotation store contract gate,通过 GitHub #237 | #229 | `avalonia-node-map-71c` | Phase 555: whiteboard primitive eraser behavior route | P3 | eraser-on-primitive behavior tests、primitive hit-testing 和 graph-deletion separation | Depends on Phase 554。只负责 eraser-on-primitive behavior;graph-selection delete 保持分离,不授权 broad eraser cursor redesign、GraphDocument schema changes、renderer rewrite、retained API removal 或 full whiteboard parity claim。 | | #230 | `avalonia-node-map-bck` | Phase 556: whiteboard primitive persistence implementation decision | P4 | persistence implementation decision、schema/version evidence、workspace and clipboard boundaries | Depends on Phase 555;stacked after PR #235,并且 do not merge before Phase 555。只记录 `DeferredUntilSeparateAnnotationStoreContract` 和 boundary coverage;不做 no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no saved whiteboard primitive state 或 full whiteboard parity claim。 | | #237 | `avalonia-node-map-zfe` | Phase 557: whiteboard annotation store contract gate | P4 | annotation-store contract docs、persistence boundary requirements 和 focused docs tests | Depends on Phase 556;stacked after PR #236,并且 do not merge before Phase 556。只定义 separate annotation-store contract gate;不做 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no saved whiteboard primitive state 或 full whiteboard parity claim。 | +| #239 | `avalonia-node-map-84u` | Phase 558: whiteboard annotation store contract skeleton | P4 | internal Core annotation-store contract types、boundary tests 和 parity docs | Depends on Phase 557;stacked after PR #238,并且 do not merge before Phase 557。只添加 internal contract skeleton only;不做 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no renderer or pointer behavior change、no public API exposure、no saved whiteboard primitive state 或 full whiteboard parity claim。 | ## 推荐并行 Worktree 计划 @@ -700,6 +703,7 @@ Phase 557 记录 whiteboard annotation store contract gate,通过 GitHub #237 - `feature/phase-555-whiteboard-eraser-behavior`:负责 #229 / `avalonia-node-map-71c`;Phase 554 之后的 current stacked implementation worktree,用于 eraser-on-primitive behavior,graph `selection.delete`、persistence/schema、renderer rewrite 和 full whiteboard parity 都保持 out of scope。 - `feature/phase-556-whiteboard-persistence-decision`:负责 #230 / `avalonia-node-map-bck`;Phase 555 之后的 current stacked implementation/deferred-decision worktree,保持 no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 - `docs/phase-557-whiteboard-annotation-store-contract`:负责 #237 / `avalonia-node-map-zfe`;Phase 556 之后的 current stacked docs/test contract gate,保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion 和 no saved whiteboard primitive state。 +- `feature/phase-558-whiteboard-annotation-store-contract-skeleton`:负责 #239 / `avalonia-node-map-84u`;Phase 557 之后的 current stacked internal Core contract skeleton,保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no renderer or pointer behavior change、no public API exposure 和 no saved whiteboard primitive state。 ## UI 验证策略 @@ -773,4 +777,5 @@ Phase 557 记录 whiteboard annotation store contract gate,通过 GitHub #237 - Phase 555 是 GitHub #229 / `avalonia-node-map-71c`;它通过 `NodeCanvasWhiteboardDrawingMode.Eraser`、`whiteboard-drawing.eraser`、`PART_WhiteboardEraserButton`、`TryEraseWhiteboardPrimitive_RemovesTopmostHitPrimitiveWithoutClearingOthers` 和 `HandlePressed_WithWhiteboardEraserMode_RemovesHitPrimitiveWithoutDeletingGraphSelection` 记录 whiteboard primitive eraser behavior route。Graph-selection delete 仍通过 graph `selection.delete` 分离;broad eraser cursor redesign、persisted whiteboard primitive state、GraphDocument schema changes、renderer rewrites、retained API removal 和 full React Flow whiteboard parity 都保持 out of scope。 - Phase 556 是 GitHub #230 / `avalonia-node-map-bck`;它通过 `GraphWhiteboardPrimitivePersistenceOutcome.DeferredUntilSeparateAnnotationStoreContract` 和 `GraphWhiteboardPrimitivePersistenceBoundary` 覆盖 `GraphDocumentCompatibility`、`WorkspacePersistence`、`ClipboardFragment`、`ScreenshotArtifact` 与 `SceneExportArtifact`,记录 whiteboard primitive persistence implementation decision。它保持 no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 - Phase 557 是 GitHub #237 / `avalonia-node-map-zfe`;它记录 whiteboard annotation store contract gate 和 `WHITEBOARD_ANNOTATION_STORE_CONTRACT_GATE`,要求明确 store ownership、store lifetime、annotation identity、primitive reference、style/geometry serialization、edit lifecycle serialization、migration metadata、read/write API boundary candidates,以及 GraphDocument、workspace、clipboard/export、scene export 和 screenshot artifacts compatibility requirements。它保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 +- Phase 558 是 GitHub #239 / `avalonia-node-map-84u`;它用 internal Core annotation-store contract types、annotation identity/reference records、primitive payload shape、migration metadata 和 persistence-neutral `IGraphWhiteboardAnnotationStoreBoundary` 记录 `WHITEBOARD_ANNOTATION_STORE_CONTRACT_SKELETON`。它保持 no production annotation store、no GraphDocument schema change、no schema version bump、no workspace persistence behavior change、no clipboard/export serialization change、no screenshot manifest expansion、no renderer or pointer behavior change、no public API exposure、no saved whiteboard primitive state 和 no full React Flow whiteboard parity。 - Phase 478、Phase 484、Phase 490、Phase 491、Phase 492、Phase 493、Phase 494、Phase 495、Phase 497、Phase 498、Phase 499、Phase 500、Phase 501、Phase 502、Phase 503、Phase 504、Phase 505、Phase 506、Phase 507、Phase 508、Phase 509、Phase 510、Phase 511、Phase 512、Phase 513、Phase 520、Phase 521、Phase 522、Phase 523、Phase 524、Phase 525、Phase 526、Phase 527、Phase 528、Phase 529、Phase 535、Phase 539、Phase 540、Phase 541、Phase 546、Phase 550、Phase 551 和 Phase 557 都不修改产品代码;除非 focused test 证明存在具体 missing contract。 diff --git a/src/AsterGraph.Core/Models/GraphWhiteboardAnnotationStoreContract.cs b/src/AsterGraph.Core/Models/GraphWhiteboardAnnotationStoreContract.cs new file mode 100644 index 00000000..348e9005 --- /dev/null +++ b/src/AsterGraph.Core/Models/GraphWhiteboardAnnotationStoreContract.cs @@ -0,0 +1,259 @@ +namespace AsterGraph.Core.Models; + +/// +/// Internal contract gate for a future separate whiteboard annotation store. +/// +internal sealed record GraphWhiteboardAnnotationStoreContract( + GraphWhiteboardAnnotationStoreOwner Owner, + GraphWhiteboardAnnotationStoreLifetime Lifetime, + int CurrentStoreSchemaVersion, + GraphWhiteboardAnnotationStoreCompatibilityBoundary CompatibilityBoundaries) +{ + /// + /// Current Phase 558 contract: define the internal store shape without persisting annotation state. + /// + public static GraphWhiteboardAnnotationStoreContract Current { get; } = new( + GraphWhiteboardAnnotationStoreOwner.SeparateAnnotationSurface, + GraphWhiteboardAnnotationStoreLifetime.WorkspaceScoped, + GraphWhiteboardAnnotationMigrationMetadata.Current.StoreSchemaVersion, + GraphWhiteboardAnnotationStoreCompatibilityBoundary.GraphDocumentCompatibility + | GraphWhiteboardAnnotationStoreCompatibilityBoundary.WorkspacePersistence + | GraphWhiteboardAnnotationStoreCompatibilityBoundary.ClipboardFragment + | GraphWhiteboardAnnotationStoreCompatibilityBoundary.SceneExportArtifact + | GraphWhiteboardAnnotationStoreCompatibilityBoundary.ScreenshotArtifact); + + /// + /// Whether the contract places annotation data inside GraphDocument payloads. + /// + public bool PersistsInGraphDocument => false; + + /// + /// Whether this implementation slice persists annotation data anywhere. + /// + public bool PersistsInCurrentSlice => false; +} + +internal sealed record GraphWhiteboardAnnotationStoreMetadata +{ + private string _storeId = string.Empty; + + public GraphWhiteboardAnnotationStoreMetadata( + string StoreId, + GraphWhiteboardAnnotationStoreOwner Owner, + GraphWhiteboardAnnotationStoreLifetime Lifetime, + GraphWhiteboardAnnotationMigrationMetadata Migration) + { + this.StoreId = StoreId; + this.Owner = Owner; + this.Lifetime = Lifetime; + this.Migration = Migration ?? throw new ArgumentNullException(nameof(Migration)); + } + + public string StoreId + { + get => _storeId; + init + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _storeId = value; + } + } + + public GraphWhiteboardAnnotationStoreOwner Owner { get; init; } + + public GraphWhiteboardAnnotationStoreLifetime Lifetime { get; init; } + + public GraphWhiteboardAnnotationMigrationMetadata Migration { get; init; } +} + +internal sealed record GraphWhiteboardAnnotationMigrationMetadata +{ + private string _migrationPolicyKey = string.Empty; + private int _storeSchemaVersion; + + public GraphWhiteboardAnnotationMigrationMetadata( + int StoreSchemaVersion, + string MigrationPolicyKey) + { + this.StoreSchemaVersion = StoreSchemaVersion; + this.MigrationPolicyKey = MigrationPolicyKey; + } + + public static GraphWhiteboardAnnotationMigrationMetadata Current { get; } = new( + StoreSchemaVersion: 1, + MigrationPolicyKey: "phase-558-no-persisted-store"); + + public int StoreSchemaVersion + { + get => _storeSchemaVersion; + init + { + if (value < 1) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Store schema version must be positive."); + } + + _storeSchemaVersion = value; + } + } + + public string MigrationPolicyKey + { + get => _migrationPolicyKey; + init + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _migrationPolicyKey = value; + } + } +} + +internal sealed record GraphWhiteboardAnnotationIdentity +{ + private string _annotationId = string.Empty; + + public GraphWhiteboardAnnotationIdentity(string AnnotationId) + { + this.AnnotationId = AnnotationId; + } + + public string AnnotationId + { + get => _annotationId; + init + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _annotationId = value; + } + } +} + +internal sealed record GraphWhiteboardPrimitiveReference +{ + private string _primitiveId = string.Empty; + + public GraphWhiteboardPrimitiveReference( + string PrimitiveId, + GraphWhiteboardPrimitiveKind Kind) + { + this.PrimitiveId = PrimitiveId; + this.Kind = Kind; + } + + public string PrimitiveId + { + get => _primitiveId; + init + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _primitiveId = value; + } + } + + public GraphWhiteboardPrimitiveKind Kind { get; init; } +} + +internal sealed record GraphWhiteboardAnnotationPrimitivePayload +{ + public GraphWhiteboardAnnotationPrimitivePayload( + GraphWhiteboardPrimitiveKind Kind, + GraphWhiteboardPrimitiveGeometry Geometry, + GraphWhiteboardPrimitiveStyle Style, + int ZIndex, + GraphWhiteboardPrimitiveEditLifecycle EditLifecycle) + { + this.Kind = Kind; + this.Geometry = Geometry ?? throw new ArgumentNullException(nameof(Geometry)); + this.Style = Style ?? throw new ArgumentNullException(nameof(Style)); + this.ZIndex = ZIndex; + this.EditLifecycle = EditLifecycle ?? throw new ArgumentNullException(nameof(EditLifecycle)); + } + + public GraphWhiteboardPrimitiveKind Kind { get; init; } + + public GraphWhiteboardPrimitiveGeometry Geometry { get; init; } + + public GraphWhiteboardPrimitiveStyle Style { get; init; } + + public int ZIndex { get; init; } + + public GraphWhiteboardPrimitiveEditLifecycle EditLifecycle { get; init; } +} + +internal sealed record GraphWhiteboardAnnotationRecord +{ + public GraphWhiteboardAnnotationRecord( + GraphWhiteboardAnnotationIdentity Identity, + GraphWhiteboardPrimitiveReference PrimitiveReference, + GraphWhiteboardAnnotationPrimitivePayload Payload) + { + this.Identity = Identity ?? throw new ArgumentNullException(nameof(Identity)); + this.PrimitiveReference = PrimitiveReference ?? throw new ArgumentNullException(nameof(PrimitiveReference)); + this.Payload = Payload ?? throw new ArgumentNullException(nameof(Payload)); + if (this.PrimitiveReference.Kind != this.Payload.Kind) + { + throw new ArgumentException( + "Primitive reference kind must match payload kind.", + nameof(PrimitiveReference)); + } + } + + public GraphWhiteboardAnnotationIdentity Identity { get; init; } + + public GraphWhiteboardPrimitiveReference PrimitiveReference { get; init; } + + public GraphWhiteboardAnnotationPrimitivePayload Payload { get; init; } +} + +internal sealed record GraphWhiteboardAnnotationStoreSnapshot +{ + private IReadOnlyList _records = []; + + public GraphWhiteboardAnnotationStoreSnapshot( + GraphWhiteboardAnnotationStoreMetadata Metadata, + IReadOnlyList? Records = null) + { + this.Metadata = Metadata ?? throw new ArgumentNullException(nameof(Metadata)); + this.Records = Records ?? []; + } + + public GraphWhiteboardAnnotationStoreMetadata Metadata { get; init; } + + public IReadOnlyList Records + { + get => _records; + init + { + ArgumentNullException.ThrowIfNull(value); + _records = value.ToList(); + } + } +} + +internal interface IGraphWhiteboardAnnotationStoreBoundary +{ + GraphWhiteboardAnnotationStoreSnapshot ReadSnapshot(); + + void WriteSnapshot(GraphWhiteboardAnnotationStoreSnapshot snapshot); +} + +internal enum GraphWhiteboardAnnotationStoreOwner +{ + SeparateAnnotationSurface, +} + +internal enum GraphWhiteboardAnnotationStoreLifetime +{ + WorkspaceScoped, +} + +[Flags] +internal enum GraphWhiteboardAnnotationStoreCompatibilityBoundary +{ + None = 0, + GraphDocumentCompatibility = 1 << 0, + WorkspacePersistence = 1 << 1, + ClipboardFragment = 1 << 2, + SceneExportArtifact = 1 << 3, + ScreenshotArtifact = 1 << 4, +} diff --git a/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs b/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs index f2f5964f..8372a9cc 100644 --- a/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs +++ b/tests/AsterGraph.Demo.Tests/ReactFlowParityRoadmapDocsTests.cs @@ -1848,6 +1848,58 @@ public void ParityRoadmapDocs_RecordPhase557WhiteboardAnnotationStoreContractGat Assert.Contains("Phase 557 记录 whiteboard annotation store contract gate", chineseParity, StringComparison.Ordinal); } + [Fact] + public void ParityRoadmapDocs_RecordPhase558WhiteboardAnnotationStoreContractSkeletonInBothLocales() + { + 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 558", contents, StringComparison.Ordinal); + Assert.Contains("GitHub #239", contents, StringComparison.Ordinal); + Assert.Contains("avalonia-node-map-84u", contents, StringComparison.Ordinal); + Assert.Contains("whiteboard annotation store contract skeleton", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("WHITEBOARD_ANNOTATION_STORE_CONTRACT_SKELETON", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationStoreContract", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationStoreMetadata", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationIdentity", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardPrimitiveReference", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationPrimitivePayload", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationRecord", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationStoreSnapshot", contents, StringComparison.Ordinal); + Assert.Contains("GraphWhiteboardAnnotationMigrationMetadata", contents, StringComparison.Ordinal); + Assert.Contains("IGraphWhiteboardAnnotationStoreBoundary", contents, StringComparison.Ordinal); + Assert.Contains("workspace-scoped lifetime", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("persistence-neutral", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no production annotation store", contents, StringComparison.OrdinalIgnoreCase); + 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 clipboard/export serialization change", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no screenshot manifest expansion", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no renderer or pointer behavior change", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no public API exposure", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no saved whiteboard primitive state", contents, StringComparison.OrdinalIgnoreCase); + Assert.Contains("no full React Flow whiteboard parity", contents, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("whiteboard annotations are persisted", contents, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("saved whiteboard primitive state is implemented", contents, StringComparison.OrdinalIgnoreCase); + } + + var englishIssueWave = ExtractIssueWaveTable(englishParity); + var chineseIssueWave = ExtractIssueWaveTable(chineseParity); + foreach (var table in new[] { englishIssueWave, chineseIssueWave }) + { + Assert.Contains("| #239 | `avalonia-node-map-84u` | Phase 558: whiteboard annotation store contract skeleton", table, StringComparison.Ordinal); + Assert.Contains("Depends on Phase 557", table, StringComparison.OrdinalIgnoreCase); + Assert.Contains("internal contract skeleton only", table, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("| TBD | TBD | Phase 558", table, StringComparison.Ordinal); + } + + Assert.Contains("Phase 558 records the whiteboard annotation store contract skeleton", englishParity, StringComparison.Ordinal); + Assert.Contains("Phase 558 记录 whiteboard annotation store contract skeleton", chineseParity, StringComparison.Ordinal); + } + [Fact] public void ParityRoadmapDocs_RecordPhase501PostPhase500QueueRefreshInBothLocales() { diff --git a/tests/AsterGraph.Editor.Tests/WhiteboardAnnotationStoreContractTests.cs b/tests/AsterGraph.Editor.Tests/WhiteboardAnnotationStoreContractTests.cs new file mode 100644 index 00000000..f5e844cf --- /dev/null +++ b/tests/AsterGraph.Editor.Tests/WhiteboardAnnotationStoreContractTests.cs @@ -0,0 +1,214 @@ +using AsterGraph.Core.Models; +using AsterGraph.Core.Serialization; +using AsterGraph.Editor.Diagnostics; +using AsterGraph.Editor.Runtime; +using AsterGraph.Editor.Services; +using Xunit; + +namespace AsterGraph.Editor.Tests; + +public sealed class WhiteboardAnnotationStoreContractTests +{ + [Fact] + public void AnnotationStoreContract_CapturesInternalStoreMetadataPrimitivePayloadAndNeutralBoundary() + { + var contract = GraphWhiteboardAnnotationStoreContract.Current; + var metadata = new GraphWhiteboardAnnotationStoreMetadata( + "whiteboard-annotation-store-001", + contract.Owner, + contract.Lifetime, + GraphWhiteboardAnnotationMigrationMetadata.Current); + var geometry = new GraphWhiteboardPrimitiveGeometry( + new GraphPoint(12d, 18d), + new GraphSize(120d, 64d), + [ + new GraphPoint(12d, 18d), + new GraphPoint(24d, 32d), + ]); + var payload = new GraphWhiteboardAnnotationPrimitivePayload( + GraphWhiteboardPrimitiveKind.Freehand, + geometry, + new GraphWhiteboardPrimitiveStyle( + FillHex: "#6AD5C4", + StrokeHex: "#1A1F2E", + StrokeThickness: 2.5d, + Opacity: 0.72d), + ZIndex: 12, + new GraphWhiteboardPrimitiveEditLifecycle( + GraphWhiteboardPrimitiveEditState.Committed, + ActiveHandleKey: "freehand-finalized")); + var record = new GraphWhiteboardAnnotationRecord( + new GraphWhiteboardAnnotationIdentity("annotation-001"), + new GraphWhiteboardPrimitiveReference("whiteboard-primitive-001", GraphWhiteboardPrimitiveKind.Freehand), + payload); + var snapshot = new GraphWhiteboardAnnotationStoreSnapshot(metadata, [record]); + IGraphWhiteboardAnnotationStoreBoundary boundary = new RecordingAnnotationStoreBoundary(); + + boundary.WriteSnapshot(snapshot); + var read = boundary.ReadSnapshot(); + + Assert.Equal(GraphWhiteboardAnnotationStoreOwner.SeparateAnnotationSurface, contract.Owner); + Assert.Equal(GraphWhiteboardAnnotationStoreLifetime.WorkspaceScoped, contract.Lifetime); + Assert.False(contract.PersistsInGraphDocument); + Assert.False(contract.PersistsInCurrentSlice); + Assert.Equal(1, contract.CurrentStoreSchemaVersion); + AssertBoundary(contract.CompatibilityBoundaries, GraphWhiteboardAnnotationStoreCompatibilityBoundary.GraphDocumentCompatibility); + AssertBoundary(contract.CompatibilityBoundaries, GraphWhiteboardAnnotationStoreCompatibilityBoundary.WorkspacePersistence); + AssertBoundary(contract.CompatibilityBoundaries, GraphWhiteboardAnnotationStoreCompatibilityBoundary.ClipboardFragment); + AssertBoundary(contract.CompatibilityBoundaries, GraphWhiteboardAnnotationStoreCompatibilityBoundary.SceneExportArtifact); + AssertBoundary(contract.CompatibilityBoundaries, GraphWhiteboardAnnotationStoreCompatibilityBoundary.ScreenshotArtifact); + Assert.Equal("whiteboard-annotation-store-001", read.Metadata.StoreId); + Assert.Equal(GraphWhiteboardAnnotationMigrationMetadata.Current, read.Metadata.Migration); + var readRecord = Assert.Single(read.Records); + Assert.Equal("annotation-001", readRecord.Identity.AnnotationId); + Assert.Equal("whiteboard-primitive-001", readRecord.PrimitiveReference.PrimitiveId); + Assert.Equal(GraphWhiteboardPrimitiveKind.Freehand, readRecord.Payload.Kind); + Assert.Equal(new GraphPoint(12d, 18d), readRecord.Payload.Geometry.Origin); + Assert.Equal(new GraphSize(120d, 64d), readRecord.Payload.Geometry.Size); + Assert.Equal([new GraphPoint(12d, 18d), new GraphPoint(24d, 32d)], readRecord.Payload.Geometry.Points); + Assert.Equal("#6AD5C4", readRecord.Payload.Style.FillHex); + Assert.Equal("#1A1F2E", readRecord.Payload.Style.StrokeHex); + Assert.Equal(2.5d, readRecord.Payload.Style.StrokeThickness); + Assert.Equal(0.72d, readRecord.Payload.Style.Opacity); + Assert.Equal(12, readRecord.Payload.ZIndex); + Assert.Equal(GraphWhiteboardPrimitiveEditState.Committed, readRecord.Payload.EditLifecycle.State); + Assert.Equal("freehand-finalized", readRecord.Payload.EditLifecycle.ActiveHandleKey); + + static void AssertBoundary( + GraphWhiteboardAnnotationStoreCompatibilityBoundary boundaries, + GraphWhiteboardAnnotationStoreCompatibilityBoundary expected) + { + Assert.True(boundaries.HasFlag(expected), $"Expected Phase 558 annotation store contract to cover {expected}."); + } + } + + [Fact] + public void AnnotationRecord_RejectsPrimitiveReferencePayloadKindMismatch() + { + var payload = new GraphWhiteboardAnnotationPrimitivePayload( + GraphWhiteboardPrimitiveKind.Freehand, + new GraphWhiteboardPrimitiveGeometry( + new GraphPoint(12d, 18d), + new GraphSize(120d, 64d), + [ + new GraphPoint(12d, 18d), + new GraphPoint(24d, 32d), + ]), + new GraphWhiteboardPrimitiveStyle( + FillHex: "#6AD5C4", + StrokeHex: "#1A1F2E", + StrokeThickness: 2.5d, + Opacity: 0.72d), + ZIndex: 12, + new GraphWhiteboardPrimitiveEditLifecycle( + GraphWhiteboardPrimitiveEditState.Committed, + ActiveHandleKey: "freehand-finalized")); + + var exception = Assert.Throws(() => + new GraphWhiteboardAnnotationRecord( + new GraphWhiteboardAnnotationIdentity("annotation-001"), + new GraphWhiteboardPrimitiveReference("whiteboard-primitive-001", GraphWhiteboardPrimitiveKind.Rectangle), + payload)); + + Assert.Equal("PrimitiveReference", exception.ParamName); + Assert.Contains("must match payload kind", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void AnnotationStoreContract_StaysInternalAndDoesNotChangeGraphPersistenceRendererPointerOrArtifacts() + { + Assert.False(typeof(GraphWhiteboardAnnotationStoreContract).IsPublic); + Assert.False(typeof(GraphWhiteboardAnnotationStoreMetadata).IsPublic); + Assert.False(typeof(GraphWhiteboardAnnotationIdentity).IsPublic); + Assert.False(typeof(GraphWhiteboardPrimitiveReference).IsPublic); + Assert.False(typeof(GraphWhiteboardAnnotationPrimitivePayload).IsPublic); + Assert.False(typeof(GraphWhiteboardAnnotationRecord).IsPublic); + Assert.False(typeof(GraphWhiteboardAnnotationStoreSnapshot).IsPublic); + Assert.False(typeof(IGraphWhiteboardAnnotationStoreBoundary).IsPublic); + Assert.Equal(typeof(GraphWhiteboardPrimitive).Assembly, typeof(GraphWhiteboardAnnotationStoreContract).Assembly); + Assert.DoesNotContain( + typeof(GraphWhiteboardAnnotationStoreContract).Assembly.GetReferencedAssemblies(), + assemblyName => assemblyName.Name is "AsterGraph.Editor" or "AsterGraph.Avalonia"); + Assert.DoesNotContain( + typeof(GraphWhiteboardAnnotationStoreContract).Assembly.GetExportedTypes(), + type => type.Name.Contains("WhiteboardAnnotation", StringComparison.Ordinal) + || type.Name.Contains("AnnotationStore", StringComparison.Ordinal)); + + var document = new GraphDocument( + "Phase 558 Boundary", + "Graph document remains graph-scoped while annotation store contracts stay internal.", + [CreateNode("source-node")], + [], + []); + var json = GraphDocumentSerializer.Serialize(document); + var scene = new GraphEditorSceneSnapshot( + document, + new GraphEditorSelectionSnapshot(["source-node"], "source-node"), + new GraphEditorViewportSnapshot(1d, 0d, 0d, 960d, 640d), + [], + [], + [], + GraphEditorPendingConnectionSnapshot.Create(false, null, null)); + var svg = GraphSceneSvgDocumentBuilder.Build(scene); + var pointerCoordinator = Type.GetType( + "AsterGraph.Avalonia.Controls.Internal.NodeCanvasPointerInteractionCoordinator, AsterGraph.Avalonia", + throwOnError: true)!; + + Assert.Equal(6, GraphDocumentCompatibility.CurrentSchemaVersion); + AssertBoundaryPayload("graph-document-json", json); + AssertBoundaryPayload("scene-svg", svg); + AssertNoAnnotationStoreMember(typeof(GraphDocument)); + AssertNoAnnotationStoreMember(typeof(GraphDocumentSerializer.GraphDocumentFilePayload)); + AssertNoAnnotationStoreMember(typeof(GraphWhiteboardPrimitiveRendererAdapter)); + AssertNoAnnotationStoreMember(typeof(GraphEditorSceneSnapshot)); + AssertNoAnnotationStoreMember(typeof(GraphSceneSvgDocumentBuilder)); + AssertNoAnnotationStoreMember(typeof(GraphSceneImageExportService)); + AssertNoAnnotationStoreMember(pointerCoordinator); + } + + private static GraphNode CreateNode(string id) + => new( + id, + "Annotation Store Source", + "Tests", + "Produces annotation-store boundary evidence", + "Used by Phase 558 annotation-store contract tests.", + new GraphPoint(120, 160), + new GraphSize(240, 160), + [], + [ + new GraphPort( + "result", + "Result", + PortDirection.Output, + "float", + "#6AD5C4"), + ], + "#6AD5C4"); + + private static void AssertBoundaryPayload(string name, string payload) + { + Assert.DoesNotContain("AnnotationStore", payload, StringComparison.Ordinal); + Assert.DoesNotContain("WhiteboardAnnotation", payload, StringComparison.Ordinal); + Assert.Contains(name == "scene-svg" ? " member.Name.Contains("AnnotationStore", StringComparison.Ordinal) + || member.Name.Contains("WhiteboardAnnotation", StringComparison.Ordinal)); + } + + private sealed class RecordingAnnotationStoreBoundary : IGraphWhiteboardAnnotationStoreBoundary + { + private GraphWhiteboardAnnotationStoreSnapshot? _snapshot; + + public GraphWhiteboardAnnotationStoreSnapshot ReadSnapshot() + => _snapshot ?? throw new InvalidOperationException("No annotation store snapshot has been written."); + + public void WriteSnapshot(GraphWhiteboardAnnotationStoreSnapshot snapshot) + => _snapshot = snapshot; + } +}