Skip to content

feat!: recover malformed elements and preserve repair workflows - #2159

Merged
yuto-trd merged 44 commits into
mainfrom
fix/malformed-element-recovery
Sep 8, 2026
Merged

feat!: recover malformed elements and preserve repair workflows#2159
yuto-trd merged 44 commits into
mainfrom
fix/malformed-element-recovery

Conversation

@yuto-trd

@yuto-trd yuto-trd commented Jul 31, 2026

Copy link
Copy Markdown
Member

Description

Opening a scene with a malformed or partially unavailable .belm sidecar now recovers the affected element and reports structured warnings while healthy elements remain usable. Recovered sidecars retain their original bytes through save, autosave, removal, and Save As. Once the last recovery blocker is repaired, normal persistence resumes; undo restores the protected source bytes.

Recovery assigns stable, collision-free IDs and migrates references without replacing a surviving owner. It includes scene-owned plugin property graphs, preserves every logical retained-sidecar path, and avoids overwriting unrelated files during rehoming. Fatal plugin constructor failures propagate instead of being converted to content fallbacks. Declarative edits reject newly introduced fallbacks, including values hidden inside plugin wrappers.

Plugins can complete repairs through ElementRecoveryService.TryCompleteRepair(element, history) before committing their transaction. The shared service is used by built-in editors and preserves Undo/Redo behavior. Scene-owned identity capture includes serialized wrappers; reference migration preserves read-only and immutable collection comparison policies and sequence order. Composite expressions can opt in through IReferenceRewritable.

Affected areas

  • Beutl.Engine (rendering / scene / track)
  • Beutl.ProjectSystem (project / document persistence)
  • UI (Beutl.Editor, Beutl.Editor.Components, Beutl.Controls)
  • Beutl.Extensibility (plugin abstractions)
  • Beutl.NodeGraph (node editor)
  • Beutl.FFmpegIpc / Beutl.FFmpegWorker (media IPC boundary)
  • Beutl.Api (server API client)
  • Build / CI / docs only

Also changes Beutl.Core serialization/property replacement and Beutl.AgentToolkit validation/persistence.

Breaking changes

Plugins implementing the following Beutl.Engine contracts must update their implementations and rebuild against this version:

  • IProperty.ReplaceCurrentValue(object?) and IProperty<T>.ReplaceCurrentValue(T): implement reference replacement that accepts a distinct reference even when value equality reports equality. Preserve the implementation's validation and change-notification behavior. Value types retain value-equality semantics.
  • IKeyFrame.ReplaceValue(object?): implement the same explicit replacement semantics for keyframe values.
  • IReferenceExpression.Rebind(Guid): return an equivalent expression for the new target ID while preserving all implementation-specific state and its property path. Implementations that cannot preserve their state must explicitly return null; recovery then retains the original expression. This member intentionally has no compatibility default.

These are required authoring contracts, not binary-compatible additions. Static CoreProperty accessors using SetAndRaise now receive scoped forced-reference replacement intent through their existing setter.

The public Beutl.AgentToolkit.Common.PathComparison and Beutl.AgentToolkit.Common.PathBoundary helpers have been removed. Replace PathComparison.ForCurrentPlatform with Beutl.FilePathComparison.Comparison from Beutl.Core. There is no public replacement for PathBoundary.ResolveExistingPath(string) or PathBoundary.ResolveDeepestExistingTarget(string): consumers must implement their own symlink-aware canonicalization and boundary policy. The shared Beutl.PathBoundary implementation is internal and cannot be used by external consumers.

Custom editor subclasses should replace calls to ResumeElementPersistenceAfterFallbackReplacement(previous) and ResumeElementPersistenceAfterKnownRecoveryBlockerReplacement() with CompleteElementRepair(). Repair completion now checks the remaining graph without requiring the removed value. See element recovery for the public plugin API.

Test plan

  • 191 scoped recovery, immutable-reference migration, path-boundary, keyframe, and property tests passed; 1 existing platform-specific symlink test skipped.
  • 213 public API contract tests passed, including repair -> save -> Undo -> rejected Save As -> source/current-location save with retained nested bytes restored.
  • 196 reconciliation, session, and path-boundary tests passed, including existing fallbacks with nondeterministic plugin error messages.
  • Regression cases cover all eight immutable collection shapes, comparison policies and order, composite-expression serialization, wrapped fatal collection constructors, and delete -> save -> reopen -> re-add lifecycles under the default recursive include pattern.
  • Full dotnet format Beutl.slnx and git diff checks completed. Independent extensibility design review found no medium-or-higher issues. The previous head also passed dotnet-format CI.

Fixed issues / References

Extracted from the feature-004 review hardening so it can land independently of speckit/004-gpu-pass-fusion.

BREAKING CHANGE: Beutl.Engine plugin implementations must implement IProperty.ReplaceCurrentValue(object?), IProperty.ReplaceCurrentValue(T), IKeyFrame.ReplaceValue(object?), and IReferenceExpression.Rebind(Guid), then rebuild. Replacement must accept distinct references despite value equality; Rebind must preserve custom state or return null. Beutl.Editor.Components custom editor subclasses must migrate the removed ResumeElementPersistenceAfterFallbackReplacement and ResumeElementPersistenceAfterKnownRecoveryBlockerReplacement helpers to CompleteElementRepair(). Beutl.AgentToolkit consumers must replace PathComparison.ForCurrentPlatform with Beutl.Core's FilePathComparison.Comparison and provide their own policy for the removed public PathBoundary helpers.

…writing them

Opening a scene whose .belm sidecar no longer parses previously failed
the whole project open. Such elements now load as disabled fallback
elements that retain the original raw text: open_project reports a
warning naming the file and parser error while healthy elements keep
loading and rendering.

Recovered elements are unpersistable at the CoreSerializer level, so
every save path — Scene.Serialize, the editor's Ctrl+S child loop, the
auto-save service, and toolkit saves — leaves the un-parseable file
byte-identical on disk, and the auto-save delete branch exempts them so
removing a recovered element cannot destroy the recoverable sidecar.
Their element Id comes from a quote-aware top-level scan of the raw text
when present, or a deterministic UUIDv5 of the filename, so repeated
opens agree, and the fallback's declarative projection carries a valid
$type and Id so edits to unrelated elements reconcile normally.
Copilot AI review requested due to automatic review settings July 31, 2026 03:27
@coderabbitai

ghost commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The project system recovers malformed element sidecars with stable IDs and suppressed writes. Project opening reports deserialization warnings and structured incidents. Save, rehome, autosave, rendering, editing, and deletion preserve recovered source bytes.

Changes

Malformed element recovery

Layer / File(s) Summary
Serialization recovery contracts
src/Beutl.Core/..., src/Beutl.Engine/Animation/KeyFrame.cs
Deserialization validates discriminator types, records fallback incidents, handles malformed type names, tracks lossy easing, and defines suppressed storage sources.
Element recovery and stable IDs
src/Beutl.ProjectSystem/ProjectSystem/Scene.cs, src/Beutl.Engine/Engine/Expressions/*
Scene restoration handles malformed JSON, fallback objects, disabled placeholders, recovered metadata, duplicate IDs, deterministic IDs, reference migration, and persisted remaps.
Project warning reporting and reconciliation
src/Beutl.AgentToolkit/Tools/SessionTools.cs, src/Beutl.AgentToolkit/Reconciliation/Reconciler.cs, docs/specs/001-agent-editing-toolkit/contracts/mcp-tools.md
Project opening traverses nested values and returns warnings with structured recovery incidents. Reconciliation uses cycle-safe fallback traversal.
Recovered storage and editor persistence
src/Beutl.Core/Serialization/CoreSerializer.cs, src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs, src/Beutl.Editor/*, src/Beutl/ViewModels/Editors/*
Recovered objects retain raw bytes, suppress protected writes and deletions, preserve paths during rehome, and resume persistence after fallback replacement.
Recovery behavior validation
tests/Beutl.UnitTests/*, tests/Beutl.AgentToolkit.Tests/*, tests/Beutl.HeadlessUITests/*
Tests cover recovery, warnings, rendering, edits, deletion, stable IDs, rehome behavior, fallback preservation, direct saves, autosaves, easing recovery, and undo.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OpenProject
  participant Scene
  participant ElementSidecar
  participant CoreSerializer
  OpenProject->>Scene: Open project and restore elements
  Scene->>ElementSidecar: Read serialized element
  Scene->>CoreSerializer: Deserialize element
  CoreSerializer-->>Scene: Return fallback or restored object
  CoreSerializer-->>Scene: Record fallback incident
  Scene->>Scene: Preserve recovery metadata and raw bytes
  Scene-->>OpenProject: Return recovered elements
  OpenProject->>OpenProject: Collect warnings and incidents
  OpenProject-->>OpenProject: Return project summary
Loading

Possibly related PRs

  • b-editor/beutl#1925: Both PRs modify CoreSerializer.RestoreFromUri legacy discriminator handling.
  • b-editor/beutl#2017: This PR extends headless UI recovery and persistence coverage introduced by that PR.
  • b-editor/beutl#2164: Both PRs modify project persistence and malformed element recovery behavior.

Suggested labels: pending-merge

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: recovery of malformed elements and preservation of repair workflows.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/malformed-element-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@drift-check

ghost commented Jul 31, 2026

Copy link
Copy Markdown

Code Review Bot

No reviewable code changes were analyzed. ⚠️ The documentation drift check could not be evaluated. Reviewed 0 file(s); skipped 76.

@greptile-apps

ghost commented Jul 31, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Implements malformed-sidecar recovery, deterministic identity deduplication, protected persistence, and graph-wide reference migration; the previously reported identity issues are addressed by the current code and regression coverage.
src/Beutl.Core/Serialization/CoreSerializer.cs Adds retained-source serialization behavior and deserialization incident handling needed to preserve malformed sidecar bytes.
src/Beutl.Core/ReferenceRewriting.cs Adds recursive reference rewriting support for wrapped values and collection types while preserving collection policies.
src/Beutl.Editor/Services/ElementRecoveryService.cs Provides the shared history-aware API that resumes normal element persistence after all recovery blockers are repaired.
src/Beutl.AgentToolkit/Reconciliation/Reconciler.cs Integrates recovery completion into declarative edits and rejects edits that introduce unresolved fallback values.
src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs Rehomes scene and retained element sidecars during Save As while preserving logical relative paths.
tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs Covers malformed restoration, stable unique identities, reference migration, protected persistence, and repair lifecycles.
tests/Beutl.PublicApiContractTests/ElementRecoveryContractTests.cs Exercises public repair APIs and save, Save As, undo, and redo persistence contracts.

Reviews (44): Last reviewed commit: "feat!: complete recovered reference migr..." | Re-trigger Greptile

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs (1)

35-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the corrupt payload into one field.

The same truncated-JSON literal appears on Lines 35, 79, 93, and 111. A single private static readonly byte[] s_corruptBytes keeps the four tests in sync when the payload shape changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs` around
lines 35 - 36, Extract the repeated truncated-JSON byte payload into a single
private static readonly field named s_corruptBytes in
MalformedElementRecoveryTests, then replace the inline literals at all four test
locations with that shared field while preserving the existing test behavior.
tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs (2)

674-723: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse this fixture in the two warning tests.

CreateProjectWithMalformedElement repeats the project, healthy element, and malformed element setup that Open_project_warns_about_malformed_element_json_and_keeps_healthy_elements builds inline on Lines 98-135. The two blocks differ only in the malformed payload. Add a payload parameter to the fixture builder and call it from that test. Also extract the repeated RenderTools construction (Lines 66-78 and 144-156) into a helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs` around lines 674 -
723, Refactor the malformed-element warning tests to reuse
CreateProjectWithMalformedElement: add a malformed-payload parameter, use it
when writing the malformed element JSON, and update
Open_project_warns_about_malformed_element_json_and_keeps_healthy_elements to
obtain its setup from the fixture. Extract the duplicated RenderTools
construction into a helper and use that helper in both warning tests.

165-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two warning assertions depend on error text that Beutl does not own. The shared root cause is that both tests match substrings produced by the JSON layer rather than the message CollectDeserializationWarnings formats. A .NET or converter update can reword that text and break both tests without any behavior regression. Assert on the element filename plus the Beutl-owned phrase could not be deserialized, and treat the parser detail as an optional extra assertion.

  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs#L165-L169: replace the "JsonReaderException" and "invalid start" substring matches with the Beutl-owned warning phrase.
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs#L87-L89: replace the "could not be converted" substring match with the Beutl-owned warning phrase.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs` around lines 165 -
169, The warning assertions in SessionToolsTests should rely on Beutl-owned
wording rather than parser-specific error text. At
tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs:165-169, keep the
malformed element filename assertion and replace the JsonReaderException and
invalid start checks with “could not be deserialized”; at
tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs:87-89, replace the
“could not be converted” check with the same phrase. Parser details may remain
only as optional assertions.
src/Beutl.ProjectSystem/ProjectSystem/Scene.cs (1)

64-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify recovered-element tracking.

_recoveredElements only stores entries made when MarkRecoveredElement sets element.IsStorageWriteSuppressed = true, and CoreSerializer.StoreToUri already returns for that state. RecoveredElementSource.RawText is not read anywhere in the codebase, and entries are never removed when children are detached. If no future code needs the retained raw text, remove the dictionary and rely on Element.IsStorageWriteSuppressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs` around lines 64 - 65, Remove
the unused _recoveredElements tracking and any associated
RecoveredElementSource/RawText bookkeeping in Scene and recovery-related
methods. Preserve MarkRecoveredElement’s behavior by setting
Element.IsStorageWriteSuppressed directly, and update callers to rely on that
flag instead of dictionary lookups or retained raw text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Beutl.AgentToolkit/Tools/SessionTools.cs`:
- Around line 80-83: Update the session response construction so
CollectDeserializationWarnings(result.Project) executes through
result.Session.ReadOnSession, matching the dispatch used by CreateSummary. Keep
the warning collection within the session-dispatch callback to ensure traversal
of the live project graph occurs on the owning editor thread.

In `@src/Beutl.Core/Serialization/CoreSerializer.cs`:
- Around line 230-234: Update the CoreSerializer flow around StoreToUri so
IsStorageWriteSuppressed does not make skipped writes indistinguishable from
successful writes: when the target URI changes, allow the write and Uri update,
and for new locations copy the original recovered sidecar bytes. Preserve
suppression only when no URI relocation or sidecar copy is required.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs`:
- Around line 765-784: Remove the matches[0] fallback branch from
ResolveRecoveredElementId; only accept an ID returned by FindTopLevelIdMatch,
and otherwise fall through to CreateVersion5Guid using the relative path.
- Around line 694-733: The catch in RestoreElementOrFallback currently handles
only JsonException; broaden it to include InvalidOperationException and the
serializer’s unsupported-deserialization exception type so valid sidecars with
unresolvable types enter the existing fallback construction path. Preserve the
current fallback metadata, projection, recovery marking, and return behavior.

In `@tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs`:
- Around line 141-142: Add a success assertion immediately after each
OpenProject call in tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs at
lines 141-142, 183-183, and 219-219, using opened.IsSuccess and
opened.Error?.Message before accessing opened.Value, opened.Value!.Session, or
serializing the result.

In `@tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs`:
- Around line 51-54: Update the recovery test around the first and second
restored IDs to assert that the recovered ID is not Guid.Empty, while retaining
the existing equality assertion between both restores.

---

Nitpick comments:
In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs`:
- Around line 64-65: Remove the unused _recoveredElements tracking and any
associated RecoveredElementSource/RawText bookkeeping in Scene and
recovery-related methods. Preserve MarkRecoveredElement’s behavior by setting
Element.IsStorageWriteSuppressed directly, and update callers to rely on that
flag instead of dictionary lookups or retained raw text.

In `@tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs`:
- Around line 674-723: Refactor the malformed-element warning tests to reuse
CreateProjectWithMalformedElement: add a malformed-payload parameter, use it
when writing the malformed element JSON, and update
Open_project_warns_about_malformed_element_json_and_keeps_healthy_elements to
obtain its setup from the fixture. Extract the duplicated RenderTools
construction into a helper and use that helper in both warning tests.
- Around line 165-169: The warning assertions in SessionToolsTests should rely
on Beutl-owned wording rather than parser-specific error text. At
tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs:165-169, keep the
malformed element filename assertion and replace the JsonReaderException and
invalid start checks with “could not be deserialized”; at
tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs:87-89, replace the
“could not be converted” check with the same phrase. Parser details may remain
only as optional assertions.

In `@tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs`:
- Around line 35-36: Extract the repeated truncated-JSON byte payload into a
single private static readonly field named s_corruptBytes in
MalformedElementRecoveryTests, then replace the inline literals at all four test
locations with that shared field while preserving the existing test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ef5de1e-6292-495a-bc04-9373741552d7

📥 Commits

Reviewing files that changed from the base of the PR and between edb53a9 and b10445d.

📒 Files selected for processing (7)
  • src/Beutl.AgentToolkit/Tools/SessionTools.cs
  • src/Beutl.Core/CoreObject.cs
  • src/Beutl.Core/Serialization/CoreSerializer.cs
  • src/Beutl.Editor/AutoSaveService.cs
  • src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
  • tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs

Comment thread src/Beutl.AgentToolkit/Tools/SessionTools.cs
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
Comment thread tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs Outdated

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b10445d024

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Save-as now copies a recovered element's retained raw bytes to the new
location instead of silently skipping the write, so a saved-as project
keeps the element while the original file stays untouched. Recovery
catches the full deserialization-domain failure set (a resolvable but
non-Element $type surfaced as InvalidCastException and aborted the whole
open), reads sidecar text only on the recovery path instead of doubling
every healthy element's I/O, and never adopts a nested or quoted Id when
no top-level Id exists. open_project collects deserialization warnings on
the session thread, and the tests assert open success before use, a
non-empty recovered Id, save-as rehoming, and non-Element-discriminator
recovery.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1688213481

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
…discriminators before deserializing

- SuppressedStorageSource now retains raw bytes, so rehoming keeps a BOM,
  foreign encodings, and undecodable bytes verbatim.
- The rehome path no longer mutates the suppression record; the source
  location stays skip-protected even if a failed multi-file save rolls
  Uri back.
- TypeFormat.ToType returns null for unparsable names instead of leaking
  parser exceptions, and the legacy discriminator fill-in keys on string
  presence so garbage $type recovers instead of loading as the default.
- RestoreFromUri rejects a discriminator type incompatible with the
  expected type before instantiating it, preventing wrong-type load side
  effects (e.g. a Scene declared in a .belm globbing element files).
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8acc3245f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
- The legacy discriminator fill-in keys on the $type/@type property key
  alone, so a non-string or blank discriminator recovers instead of
  loading as the legacy default.
- The recovery filter inverts to catch every non-filesystem failure
  (value converters throw freely, e.g. FormatException from
  Color.Parse); IOException/UnauthorizedAccessException still propagate.
- Fallback creation records a thread-local incident so elements whose
  only fallback lives outside the hierarchy (e.g. a plain property or
  keyframe value) are still byte-frozen.
- A recovered element that surfaces an Id another element owns yields it
  and falls back to its deterministic path-derived identity.
- The rehome branch is create-only: it never overwrites an existing
  file, preserving manual repairs at the destination.
- The toolkit's Save As keeps element sidecar file names so path-derived
  recovery identities stay stable across rehoming.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Beutl.Core/Serialization/CoreSerializer.cs`:
- Around line 243-271: Replace the File.Exists check and subsequent
File.WriteAllBytes call in the suppressedObj rehoming branch with an atomic
FileMode.CreateNew write, preserving the existing-file behavior by catching the
already-exists condition, updating suppressedObj.Uri, and returning without
overwriting the file.

In `@src/Beutl.Core/TypeFormat.cs`:
- Around line 13-25: Update ParseNestedType to guard the resolved type before
calling MakeGenericType, returning null when parent?.GetNestedType or
_assembly?.GetType yields null. Also catch ArgumentException from
Type.MakeGenericType, while preserving the existing null result behavior for
malformed type names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8db0d964-a15f-463c-8910-0be1fe9be983

📥 Commits

Reviewing files that changed from the base of the PR and between b10445d and e663648.

📒 Files selected for processing (15)
  • src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs
  • src/Beutl.AgentToolkit/Tools/SessionTools.cs
  • src/Beutl.Core/CoreObject.cs
  • src/Beutl.Core/Serialization/CoreSerializer.cs
  • src/Beutl.Core/Serialization/DeserializationIncidents.cs
  • src/Beutl.Core/Serialization/FallbackDeserializationHelper.cs
  • src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs
  • src/Beutl.Core/Serialization/SuppressedStorageSource.cs
  • src/Beutl.Core/TypeFormat.cs
  • src/Beutl.Editor/AutoSaveService.cs
  • src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
  • tests/Beutl.AgentToolkit.Tests/Sessions/FileEditingSessionTests.cs
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
  • tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs
  • tests/Beutl.UnitTests/Serialization/DeserializationIncidentsTests.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Beutl.Editor/AutoSaveService.cs
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
  • src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
Comment thread src/Beutl.Core/TypeFormat.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e663648f57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
Comment thread src/Beutl.AgentToolkit/Tools/SessionTools.cs Outdated
Comment thread src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs Outdated
Comment thread src/Beutl.Editor/AutoSaveService.cs
…riminator parsing

- The create-only rehome now opens the destination with FileMode.CreateNew,
  closing the exists-check/write race; an already-existing file is treated
  as the protected skip path and other IO failures still propagate.
- TypeNameParser.ParseNestedType returns null when the assembly or nested
  type cannot be resolved instead of calling MakeGenericType on null, and
  ToType's filter also absorbs ArgumentException so malformed generic
  discriminators read as unknown types.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13d9a6212a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
…command, and nested rehome paths

- Nested deserialization (TryDeserializeCoreSerializable and
  DeserializeFromJsonObject) rejects a discriminator type that is not
  assignable to the expected base before instantiating it, so a Scene
  declared inside Objects becomes a fallback instead of recursively
  reopening its own sidecar.
- open_project warning collection traverses each property's keyframe
  animation values, surfacing fallbacks that live only in keyframes.
- Scene's DeleteCommand skips File.Delete for elements carrying a
  suppressed storage source, so deleting a recovered element keeps the
  retained sidecar bytes.
- Save As preserves each sidecar's scene-relative subpath (falling back
  to the basename for rooted/escaping paths), keeping path-derived
  recovery identities stable for nested layouts.
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29f89260e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/Beutl.ProjectSystem/ProjectSystem/Scene.cs (3)

712-715: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject generated IDs that are already claimed.

The claimedIds.Add(child.Id) result is ignored after assigning the path-derived ID. If that ID matches a healthy element or an earlier recovered element, duplicate Element.Id values remain. Check each generated candidate and derive another deterministic candidate when the candidate is already claimed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs` around lines 712 - 715,
Update the recovered-element ID assignment in the surrounding recovery method to
check the result of claimedIds.Add for the path-derived candidate. When the
candidate is already claimed, deterministically derive another candidate and
retry until it can be added, then assign that unique ID to child.Id.

741-745: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Limit recovery to content-related exceptions.

This catch block converts every non-I/O Exception into a disabled fallback element, including conversion failures as intended, but also programming errors and fatal runtime conditions. Project loading can then succeed while hiding the failure and preserving only raw bytes. Catch the known deserialization and conversion exceptions, and rethrow cancellation plus fatal runtime exceptions such as OutOfMemoryException.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs` around lines 741 - 745,
Update the recovery catch filter in Scene loading to catch only the known
deserialization and value-conversion exceptions needed for fallback handling.
Explicitly exclude cancellation and fatal runtime exceptions such as
OutOfMemoryException, while preserving propagation of filesystem failures and
unexpected programming errors.

799-813: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require a root object and a non-empty ID before using the recovered ID.

Guid.TryParse("00000000-0000-0000-0000-000000000000", out var id) returns true while reassigning Guid.Empty; FindTopLevelIdMatch also accepts an inner object of a root array. Require the root JSON object and topLevelId != Guid.Empty before calling return topLevelId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs` around lines 799 - 813,
Update ResolveRecoveredElementId to require the recovered JSON root to be an
object, ensure FindTopLevelIdMatch does not accept an inner object from a root
array, and only return topLevelId when the parsed value is non-empty (topLevelId
!= Guid.Empty). Otherwise preserve the existing deterministic filename GUID
fallback.
src/Beutl.Core/Serialization/CoreSerializer.cs (1)

266-280: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate write failures after a successful CreateNew.

The try block also covers stream.Write; dispose-related failures can follow the same path. If creation succeeds but writing or disposal fails, the destination still exists, so the catch treats the partial file as an existing destination and returns successfully. Catch the existing-file error only around FileStream construction. Let write and disposal failures propagate, and remove any partial destination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Beutl.Core/Serialization/CoreSerializer.cs` around lines 266 - 280,
Restrict the existing-file IOException handling in the recovery flow to the
FileStream construction in CoreSerializer, so stream.Write and disposal failures
propagate instead of being treated as successful recovery. If writing or
disposal fails after creation, remove the partial rehomedPath destination before
rethrowing, while preserving the existing-file behavior for CreateNew
collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Beutl.Core/Serialization/CoreSerializer.cs`:
- Around line 70-75: Update both CoreSerializer entry points and
TryDeserializeCoreSerializable to resolve an existing $type/@type discriminator
before checking baseType.IsSealed. Validate that the resolved actualType is
assignable to baseType before instantiation; use baseType only when neither
discriminator key is present and fallback is required.

---

Outside diff comments:
In `@src/Beutl.Core/Serialization/CoreSerializer.cs`:
- Around line 266-280: Restrict the existing-file IOException handling in the
recovery flow to the FileStream construction in CoreSerializer, so stream.Write
and disposal failures propagate instead of being treated as successful recovery.
If writing or disposal fails after creation, remove the partial rehomedPath
destination before rethrowing, while preserving the existing-file behavior for
CreateNew collisions.

In `@src/Beutl.ProjectSystem/ProjectSystem/Scene.cs`:
- Around line 712-715: Update the recovered-element ID assignment in the
surrounding recovery method to check the result of claimedIds.Add for the
path-derived candidate. When the candidate is already claimed, deterministically
derive another candidate and retry until it can be added, then assign that
unique ID to child.Id.
- Around line 741-745: Update the recovery catch filter in Scene loading to
catch only the known deserialization and value-conversion exceptions needed for
fallback handling. Explicitly exclude cancellation and fatal runtime exceptions
such as OutOfMemoryException, while preserving propagation of filesystem
failures and unexpected programming errors.
- Around line 799-813: Update ResolveRecoveredElementId to require the recovered
JSON root to be an object, ensure FindTopLevelIdMatch does not accept an inner
object from a root array, and only return topLevelId when the parsed value is
non-empty (topLevelId != Guid.Empty). Otherwise preserve the existing
deterministic filename GUID fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcaef780-8c81-434f-9965-efa26f1768ba

📥 Commits

Reviewing files that changed from the base of the PR and between 13d9a62 and 29f8926.

📒 Files selected for processing (8)
  • src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs
  • src/Beutl.AgentToolkit/Tools/SessionTools.cs
  • src/Beutl.Core/Serialization/CoreSerializer.cs
  • src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs
  • src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
  • tests/Beutl.AgentToolkit.Tests/Sessions/FileEditingSessionTests.cs
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
  • tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs
  • src/Beutl.AgentToolkit/Sessions/FileEditingSession.cs
  • tests/Beutl.AgentToolkit.Tests/Sessions/FileEditingSessionTests.cs
  • src/Beutl.AgentToolkit/Tools/SessionTools.cs
  • tests/Beutl.AgentToolkit.Tests/Tools/SessionToolsTests.cs
  • tests/Beutl.UnitTests/ProjectSystem/MalformedElementRecoveryTests.cs

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
…m, and recovery scanning

- A rehome write failure after FileMode.CreateNew succeeded deletes the
  partial file and rethrows instead of being misread as a pre-existing
  repair; only a create-phase failure with the file present skips.
- Recovered-id dedupe processes children in stable sidecar-path order
  (the parallel load is unordered) and derives further deterministic
  UUIDv5 candidates when the path-derived replacement is itself taken.
- Fallback projections keep the original discriminator; the runtime
  fallback type is written only when the projection has none.
- Save As containment resolves the full path instead of a '..' prefix
  test, so directories like '..assets' keep their subpath.
- The top-level Id scan tracks array nesting, so a root-array sidecar's
  inner Id is never adopted.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be2f05d0a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.AgentToolkit/Tools/SessionTools.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
…k edits with non-hierarchical fallbacks

- Recovered-id UUIDv5 inputs and the dedupe ordering key normalize the
  scene-relative path to forward slashes, so identities match across
  Windows and Unix.
- Collision remaps persist in the scene file (RecoveredElementIds,
  path -> Guid): a remap applies on load regardless of the current
  claimant set, entries are pruned when their sidecar heals or leaves,
  and a persisted id a healthy element now owns is dropped in favor of
  a fresh deterministic derivation.
- Recovery warnings name the scene-relative sidecar path, so same-named
  files in different subdirectories are distinguishable.
- Reconciler baseline collection uses the same full serialized-graph
  traversal as its sandbox (property values and keyframe animation
  values), so a pre-existing non-hierarchical fallback no longer rejects
  every apply_edit.
- The sealed-baseType discriminator short-circuit is kept and documented:
  sealed wrappers such as Optional<T> carry the wrapped payload's $type
  on their own node and interpret it themselves.
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59bd61dd4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
@yuto-trd

ghost commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@codex review

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

catch (Exception ex) when (FallbackDeserializationHelper.TryCreateFallback(
baseType, actualType, obj, ex) is { } fallback)

P2 Badge Propagate fatal failures before creating nested fallbacks

When a nested plugin type with a declared fallback, such as a Transform subclass, throws AccessViolationException or OutOfMemoryException from its constructor or deserializer, this filter converts the wrapped failure into the fallback and the Scene-level fatal-failure guard never sees it. Fresh evidence beyond the resolved Scene-level thread is that this nested catch, along with the equivalent CoreSerializer filters, still calls TryCreateFallback without checking ExceptionHelpers.ContainsFatalFailure; reject fatal exception chains before fallback conversion.

AGENTS.md reference: AGENTS.md:L60-L60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/SerializedGraphTraversal.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.AgentToolkit/Sessions/ProjectOperations.cs
@chatgpt-codex-connector

ghost commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T15:10:54.076789Z 580bb7e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62b60d2583

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Engine/Engine/Expressions/IReferenceExpression.cs
Comment thread src/Beutl.Engine/Animation/KeyFrame.cs Outdated
Comment thread src/Beutl/ViewModels/Editors/ListEditorViewModel.cs Outdated
Comment thread src/Beutl/ViewModels/Editors/CoreObjectEditorViewModel.cs Outdated
Comment thread src/Beutl.Core/CoreObject.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c798cb6815

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Engine/Engine/IProperty.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
BREAKING CHANGE: Beutl.Engine plugin implementations must implement IProperty.ReplaceCurrentValue, IProperty<T>.ReplaceCurrentValue, IKeyFrame.ReplaceValue, and IReferenceExpression.Rebind, then rebuild. Replacement hooks must preserve validation and notifications while replacing equality-equivalent references. Rebind must preserve expression state for the new ID or explicitly return null. Beutl.Core static property setters now propagate scoped reference-replacement intent through SetAndRaise.
@yuto-trd yuto-trd changed the title feat(project-system): recover malformed element files without ever rewriting them feat!: recover malformed elements and preserve repair workflows Sep 8, 2026

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50d892ff17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.AgentToolkit/Documents/DeclarativeDocumentApplier.cs Outdated
Comment thread src/Beutl.Core/Serialization/FilePathBoundary.cs
Capture referenced storage reads independently of plugin graph traversal,
retain URIs on content-failure fallbacks, and preserve original easing JSON
until an explicit replacement. Cover Save As, repair, and undo lifecycles.

BREAKING CHANGE: Beutl.AgentToolkit.Common.PathComparison and PathBoundary
have been removed. Replace PathComparison.ForCurrentPlatform with
Beutl.FilePathComparison.Comparison from Beutl.Core. There is no public
replacement for PathBoundary.ResolveExistingPath(string) or
ResolveDeepestExistingTarget(string); consumers must provide their own
symlink-aware canonicalization and boundary policy. Beutl.PathBoundary
is internal.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cbec02855e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e9f950d01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Engine/Engine/IProperty.cs Outdated
Comment thread src/Beutl.Engine/Animation/KeyFrame{T}.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
Comment thread src/Beutl.Core/CoreObject.cs
Preserve nullable values, dictionary/set comparison policies, wrapped scene
identities, and retained sidecars after undoing a rehomed repair. Unify
built-in editors on the public repair completion service and verify the
contract from a non-friend test assembly.

BREAKING CHANGE: Beutl editor subclasses must replace
ResumeElementPersistenceAfterFallbackReplacement(previous) and
ResumeElementPersistenceAfterKnownRecoveryBlockerReplacement() with
CompleteElementRepair(). External editors can use
Beutl.Editor.Services.ElementRecoveryService.TryCompleteRepair(element,
history) before committing their repair transaction.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 621e80ba58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs
Comment thread src/Beutl.Core/Serialization/FallbackDeserializationHelper.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7c1df2577

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Engine/Engine/IProperty.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/RecoveredCollectionFactory.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.AgentToolkit/Reconciliation/Reconciler.cs Outdated
Comment thread src/Beutl.Core/Serialization/FilePathBoundary.cs Outdated
@github-actions

ghost commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

No TODO comments were found.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd0075439b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs
Comment thread src/Beutl.ProjectSystem/ProjectSystem/Scene.cs Outdated
Comment thread src/Beutl.Core/Serialization/CoreSerializer.cs Outdated
@codecov

ghost commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.44068% with 376 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.87%. Comparing base (4d86285) to head (580bb7e).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
src/Beutl.ProjectSystem/ProjectSystem/Scene.cs 89.57% 85 Missing and 61 partials ⚠️
src/Beutl.Core/Serialization/CoreSerializer.cs 82.53% 35 Missing and 9 partials ⚠️
src/Beutl.Core/Serialization/FilePathBoundary.cs 67.64% 24 Missing and 9 partials ⚠️
...System/ProjectSystem/RecoveredCollectionFactory.cs 70.45% 5 Missing and 21 partials ⚠️
src/Beutl.Utilities/ExceptionHelpers.cs 69.76% 11 Missing and 2 partials ⚠️
...rc/Beutl.AgentToolkit/Reconciliation/Reconciler.cs 89.18% 5 Missing and 7 partials ⚠️
src/Beutl.Engine/Animation/KeyFrame.cs 89.32% 9 Missing and 2 partials ⚠️
...tl/ViewModels/Editors/CoreObjectEditorViewModel.cs 40.00% 8 Missing and 1 partial ⚠️
...eutl/ViewModels/Editors/GeometryEditorViewModel.cs 25.00% 8 Missing and 1 partial ⚠️
...ctSystem/ProjectSystem/SerializedGraphTraversal.cs 92.85% 4 Missing and 4 partials ⚠️
... and 19 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2159      +/-   ##
==========================================
+ Coverage   55.62%   58.87%   +3.25%     
==========================================
  Files        2164     2208      +44     
  Lines      207432   233940   +26508     
  Branches    23294    26012    +2718     
==========================================
+ Hits       115377   137738   +22361     
- Misses      86302    89440    +3138     
- Partials     5753     6762    +1009     
Files with missing lines Coverage Δ
...entToolkit/Documents/DeclarativeDocumentApplier.cs 83.69% <100.00%> (-0.66%) ⬇️
src/Beutl.Core/CoreObject.cs 72.14% <100.00%> (+3.75%) ⬆️
src/Beutl.Core/OptionalJsonConverter.cs 87.17% <100.00%> (+14.20%) ⬆️
...ore/Serialization/FallbackDeserializationHelper.cs 78.94% <100.00%> (+49.53%) ⬆️
...ialization/JsonSerializationContext.Deserialize.cs 84.45% <100.00%> (+11.03%) ⬆️
...erialization/JsonSerializationContext.Serialize.cs 92.25% <100.00%> (+5.01%) ⬆️
...eutl.Core/Serialization/SuppressedStorageSource.cs 100.00% <100.00%> (ø)
src/Beutl.Core/TypeFormat.cs 88.44% <100.00%> (+7.25%) ⬆️
src/Beutl.Core/ValueReplacement.cs 100.00% <100.00%> (ø)
src/Beutl.Editor/Services/ElementObjectService.cs 86.56% <100.00%> (+0.41%) ⬆️
... and 36 more

... and 98 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e24f083...580bb7e. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Rebuild immutable collections with their comparers and order, support opted-in
composite expressions, propagate fatal migration failures, and keep transient
plugin diagnostics out of incident identity. Preserve Undo restoration after a
failed nested copy and reuse the shared platform path comparison policy.

BREAKING CHANGE: Beutl.Engine plugin implementations must implement IProperty.ReplaceCurrentValue(object?), IProperty<T>.ReplaceCurrentValue(T), IKeyFrame.ReplaceValue(object?), and IReferenceExpression.Rebind(Guid), then rebuild. Replacement must preserve distinct reference identity; Rebind must preserve custom state or return null. Beutl.Editor.Components custom editors must replace the removed ResumeElementPersistenceAfterFallbackReplacement and ResumeElementPersistenceAfterKnownRecoveryBlockerReplacement helpers with CompleteElementRepair(). Beutl.AgentToolkit consumers must use Beutl.Core FilePathComparison.Comparison instead of PathComparison.ForCurrentPlatform and supply their own policy for removed public PathBoundary helpers.
@yuto-trd
yuto-trd enabled auto-merge (squash) September 8, 2026 15:09
@yuto-trd
yuto-trd disabled auto-merge September 8, 2026 15:09
@yuto-trd
yuto-trd merged commit a38d645 into main Sep 8, 2026
@yuto-trd
yuto-trd deleted the fix/malformed-element-recovery branch September 8, 2026 15:10

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 580bb7e034

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


protected ImmutableArray<CoreObject?> GetStorables() => [_element];

protected void CompleteElementRepair()

ghost Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Complete recovery after displacement-map paste

When an unavailable DisplacementMapTransform is loaded as a null slot with a non-fallback incident, DisplacementMapTransformEditorViewModel.TryPasteJson still assigns the pasted transform directly at lines 160–168 and commits without reaching this new repair-completion hook. Suppression therefore remains active, so saving preserves the malformed .belm and the pasted repair disappears after reopening. Fresh evidence beyond the resolved ChangeType/null-slot thread is this separate paste path; route it through SetValue or call CompleteElementRepair before committing, and add a lifecycle regression.

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

foreach (Element item in Children)
{
CoreSerializer.StoreToUri(item, item.Uri!);
CoreSerializer.StoreToUri(item, item.Uri!, sidecarRoot);

ghost Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve retained sidecar destinations before saving children

When a recovered element retains a URI-backed source whose path is also the sidecar URI of a later live element—a valid possibility for plugin-owned serialized graphs—this loop has no global reservation for retained destinations. During Save As, the recovered child can copy its retained bytes first and the later StoreToUri then overwrites that same path normally, leaving the recovered .belm pointing at bytes different from those captured for recovery; reversing child order can instead reject the save on a byte mismatch. Preflight live and retained destinations and reject or safely rehome such collisions before writing any child.

AGENTS.md reference: AGENTS.md:L60-L60

Useful? React with 👍 / 👎.


public static bool ContainsNonRecoverableFileSystemFailure(Exception exception)
=> Contains(exception, static current => current is UnauthorizedAccessException
or IOException and not FileNotFoundException);

ghost Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recover missing-directory failures like missing files

When a plugin element constructor or deserializer accesses a resource beneath a directory that has been moved or deleted, .NET throws DirectoryNotFoundException, but this predicate classifies every IOException except FileNotFoundException as non-recoverable. Scene.RestoreElementOrFallback therefore lets that exception abort the entire scene load, while the equivalent missing-file case becomes a disabled recovered element and preserves the other healthy elements. Exclude DirectoryNotFoundException from this predicate as well and add the corresponding recovery regression.

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants