diff --git a/.cursor/commands/audit.md b/.cursor/commands/audit.md index c516b4b7..ce03bb1c 100644 --- a/.cursor/commands/audit.md +++ b/.cursor/commands/audit.md @@ -33,7 +33,7 @@ Any divergence between the two-way sync path and the CLI syncback path is a bug ## Sync System Layout -The auditor MUST read the relevant files from each pipeline when tracing round-trip identity. This section maps every file involved in both sync systems. Read these files -- do not skip any that are in the scope of the feature being audited. +The auditor MUST read the relevant files from each pipeline when tracing round-trip identity. This section maps every file involved in all three sync systems. Read these files -- do not skip any that are in the scope of the feature being audited. ### CLI Syncback Pipeline (`atlas syncback`) @@ -54,10 +54,11 @@ Syncback Core │ middleware context through the pipeline. Methods: │ with_joined_path(), get_path_filtered_properties(). ├── src/syncback/file_names.rs Filename generation. slugify_name() replaces forbidden -│ chars with '_'. deduplicate_name() appends ~1, ~2 on -│ collision. name_for_inst() is the main entry point. -│ adjacent_meta_path() computes .meta.json5 paths for -│ scripts (strips .server/.client suffixes from stem). +│ chars (incl. ~) with '_'. deduplicate_name_with_ext() +│ appends ~2, ~3 on collision. name_for_inst() is the +│ main entry point. adjacent_meta_path() computes +│ .meta.json5 paths for scripts (strips .server/.client +│ suffixes from stem). ├── src/syncback/property_filter.rs Property filtering. filter_properties() removes Name, │ Parent, Source (scripts), Value (StringValue), Contents │ (LocalizationTable), Ref/UniqueId, unscriptable props. @@ -148,13 +149,28 @@ Change Batching (Plugin) │ Encodes a single property value using RbxDom.EncodedValue. │ encode(). Maps Roblox values to wire format. ├── plugin/src/ChangeBatcher/encodeInstance.lua -│ Full instance encoding for additions. All children are -│ encoded including those with duplicate names -- the server -│ handles dedup. Skips Ref properties (target has no server -│ ID during addition). Attributes/Tags support. +│ Full instance encoding for additions. Uses encodeHelpers +│ for property iteration. All children encoded including +│ duplicates -- server handles dedup. Duplicate detection +│ helpers: findDuplicateNames(), hasDuplicateSiblings(), +│ isPathUnique(). Skips Ref properties during addition. +├── plugin/src/ChangeBatcher/encodeHelpers.lua +│ Shared encoding helpers used by encodeInstance and +│ encodeService. Exports: encodeAttributes(), +│ encodeTags(), forEachEncodableProperty(). Defines +│ SKIP_PROPERTIES (Parent, Name, Archivable). Filters +│ by scriptability, serialization, and data type. +├── plugin/src/ChangeBatcher/encodeService.lua +│ Service-level property encoding for live syncback. +│ Encodes properties via RbxDom.findClassDescriptor(). +│ Skips: Parent, Name, Archivable, Attributes, Tags. +│ Ref properties: creates temporary ObjectValue carriers +│ (Name=propertyName, Value=target). Returns (chunk, +│ refTargets). Class-specific excludes (Lighting.ClockTime). └── plugin/src/ChangeBatcher/propertyFilter.lua - Filters which properties are synced. Controls which data - types and property names are included/excluded. + Filters which properties are synced. Only UniqueId is + blocked. Ref properties are explicitly allowed (handled + by encodePatchUpdate via InstanceMap resolution). Session & Transport (Plugin) ├── plugin/src/ServeSession.lua Orchestrates the session. onInstanceChanged → guards @@ -194,7 +210,8 @@ Server Write Processing (receives plugin writes) │ updates on rename. Constants: REF_PATH_ATTRIBUTE_PREFIX, │ REF_POINTER_ATTRIBUTE_PREFIX, REF_ID_ATTRIBUTE_NAME. ├── src/variant_eq.rs Property value comparison. variant_eq() compares Variant -│ values with fuzzy float matching (approx_eq! with epsilon). +│ values with fuzzy float matching (custom fuzzy_eq_f32/ +│ fuzzy_eq_f64 with EPSILON 0.0001, absolute+relative). │ Used by matching algorithms for property diff scoring. └── src/serve_session.rs Server-side session. Owns RojoTree, ChangeProcessor, MessageQueue, RefPathIndex, git_repo_root. Coordinates @@ -287,7 +304,93 @@ Patch Application (Plugin) └── plugin/src/Reconciler/Error.lua Error types for reconciliation failures. ``` -### Shared Infrastructure (used by both pipelines) +### Live Syncback Pipeline (`POST /api/syncback`) + +One-click full sync from Studio to filesystem. Does NOT require an active WebSocket connection -- the plugin POSTs directly from the NotConnected page. The server restarts after completion. + +``` +PLUGIN SIDE (encoding and transport) +==================================== + +UI & Orchestration +├── plugin/src/App/init.lua SyncbackConfirm floating dialog (StudioPluginGui with +│ isEphemeral=true). Rendered when showingSyncbackConfirm +│ is true. SYNCBACK_SERVICES constant (14 services): +│ Lighting, MaterialService, ReplicatedFirst, +│ ReplicatedStorage, ServerScriptService, ServerStorage, +│ SoundService, StarterGui, StarterPack, StarterPlayer, +│ Teams, TextChatService, VoiceChatService, Workspace. +│ performSyncback() iterates services, encodes each via +│ encodeService(), serializes all children via +│ SerializationService:SerializeInstancesAsync() into a +│ single buffer, destroys temporary carriers, POSTs to +│ /api/syncback. Triggered from NotConnected page. +└── plugin/src/App/StatusPages/NotConnected.lua + "Syncback" button (Danger style) opens SyncbackConfirm. + +Service Encoding +├── plugin/src/ChangeBatcher/encodeService.lua +│ Encodes service-level properties for a single service. +│ Returns (chunk, refTargets). chunk = { className, +│ childCount, refTargetCount, properties?, refs? }. +│ Ref properties use temporary ObjectValue carriers +│ (carrier.Name = propertyName, carrier.Value = target). +│ refs map stores 1-based carrier indices. +│ Class-specific excludes: Lighting.ClockTime. +│ Skips: Parent, Name, Archivable, Attributes, Tags. +└── plugin/src/ChangeBatcher/encodeHelpers.lua + Shared helpers used by encodeService. encodeAttributes(), + encodeTags(), forEachEncodableProperty(). + +Transport: HTTP POST /api/syncback (MessagePack body) + Request: { protocolVersion, serverVersion, placeId, data, services } + +SERVER SIDE (validation, DOM reconstruction, syncback) +====================================================== + +API Handler +├── src/web/api.rs handle_api_syncback() validates: +│ (1) protocol version == PROTOCOL_VERSION (6) +│ (2) server version MAJOR.MINOR matches +│ (3) place ID passes servePlaceIds/blockedPlaceIds +│ Fires SyncbackSignal with payload. +│ Returns 400 (protocol/version mismatch), 403 (place ID), +│ or 409 (syncback already in progress). + +Signal & Server Lifecycle +├── src/web/mod.rs SyncbackSignal: Mutex> + Notify. +│ fire() deposits payload and notifies. +│ take_payload() retrieves and clears. +│ LiveServer::start() uses tokio::select! to wait for +│ new connections OR syncback signal. Exits accept loop +│ when signal fires. + +Wire Types +├── src/web/interface.rs SyncbackRequest { protocol_version: f64, +│ server_version: String, place_id: Option, +│ data: Vec, services: Vec } +│ ServiceChunk { class_name, child_count, ref_target_count, +│ properties: HashMap, +│ refs: HashMap } +│ SyncbackPayload { data: Vec, +│ services: Vec } +│ ServerExitReason::SyncbackRequested(SyncbackPayload) + +DOM Reconstruction & Syncback +└── src/cli/serve.rs run_live_syncback() is the main handler. + build_dom_from_chunks() reconstructs WeakDom: + (1) parses rbxm blob with rbx_binary::from_reader() + (2) creates services from ServiceChunk metadata + (3) distributes children using child_count + + ref_target_count ranges per service + (4) resolves Ref properties via ObjectValue carriers + (5) ensures all VISIBLE_SERVICES exist + Runs syncback_loop(incremental=false) -- always clean + mode. write_to_vfs_parallel() writes files. + refresh_git_index() updates git. Serve loop restarts. +``` + +### Shared Infrastructure (used by all three pipelines) ``` Snapshot System @@ -353,6 +456,26 @@ Plugin Shared Modules ├── plugin/src/ChangeMetadata.lua Change metadata tracking for sync operations. ├── plugin/src/strict.lua Module export wrapper (strict mode enforcement). └── plugin/src/DiffUtil.lua Diff utilities for comparing instance data. + +Integration Tests (tests/tests/) +├── tests/tests/build.rs Build tests (gen_build_tests! macro). +├── tests/tests/serve.rs Forward-sync serve tests. +├── tests/tests/connected_mode.rs Connected mode tests. +├── tests/tests/two_way_sync.rs Two-way sync via /api/write tests. +├── tests/tests/syncback.rs CLI syncback tests. +├── tests/tests/syncback_roundtrip.rs Full round-trip (syncback → build → compare). +├── tests/tests/syncback_format_transitions.rs File-to-folder and folder-to-file. +├── tests/tests/clean_mode.rs Clean mode syncback tests. +├── tests/tests/clean_mode_stress.rs Clean mode stress tests. +├── tests/tests/live_syncback.rs Live syncback parity tests (live output matches CLI), +│ validation tests (protocol/version/placeId), lifecycle +│ tests (server restart, sequential syncbacks), edge cases +│ (attributes, tags, cross-service refs, rapid sequential). +├── tests/tests/git_sync_defaults.rs Git-based sync default selection tests. +│ Hash computation parity, default selection logic, +│ auto-staging flow, no-git-repo fallback. +└── tests/tests/matching_fixtures.rs Matching algorithm fixture tests across all three + implementations (Rust syncback, Rust forward, Lua plugin). ``` ### Audit Reading Order @@ -368,6 +491,7 @@ When auditing a feature, read files in this order to build understanding before 7. **Ref properties:** `src/rojo_ref.rs` (constants, `RefPathIndex`, `ref_target_path_from_tree`), `src/syncback/ref_properties.rs` (syncback ref linking), `src/snapshot/patch_apply.rs` (deferred ref resolution) -- understand cross-instance references 8. **Matching algorithms:** `src/snapshot/matching.rs` (forward sync), `src/syncback/matching.rs` (CLI syncback), `plugin/src/Reconciler/matching.lua` (plugin) -- three parallel implementations that must produce identical pairings. Also `src/variant_eq.rs` (Rust) and `plugin/src/Reconciler/trueEquals.lua` (Lua) for property comparison 9. **Git integration:** `src/git.rs` (server-side git metadata, blob hashing, auto-staging), `plugin/src/PatchTree.lua` (default selection logic using gitMetadata), `plugin/src/SHA1.luau` (plugin-side hash computation) +10. **Live syncback:** `plugin/src/ChangeBatcher/encodeService.lua` (service encoding), `plugin/src/App/init.lua` (`performSyncback`, `SyncbackConfirm`, `SYNCBACK_SERVICES`), `src/web/api.rs` (`handle_api_syncback`), `src/web/mod.rs` (`SyncbackSignal`, `LiveServer`), `src/cli/serve.rs` (`build_dom_from_chunks`, `run_live_syncback`), `src/web/interface.rs` (`SyncbackRequest`, `ServiceChunk`) --- @@ -667,6 +791,49 @@ The confirmation dialog uses git metadata for smart default selections (`PatchTr - **Staging split**: `api.rs` stages additions/removals/push files directly. `change_processor` stages Source writes after they complete (via `stage_ids` on PatchSet). Verify both paths handle the `stage_ids` list correctly. - **No git repo**: When `git_repo_root` is `None`, all defaults should be `nil` and no staging should be attempted. +#### 11n. Live Syncback Parity + +Live syncback (`POST /api/syncback`) is a third sync path that must produce filesystem output identical to CLI syncback. It runs in clean mode (`incremental=false`) and restarts the server after completion. + +**Verify plugin-side encoding:** +- Does `encodeService.lua` encode all service-level properties that CLI syncback captures? Compare against `filter_properties()` in `property_filter.rs`. +- Are `Attributes` and `Tags` handled correctly? (`encodeService` skips them from the property loop but `encodeHelpers.encodeAttributes/encodeTags` handle them separately.) +- Are class-specific excludes (`Lighting.ClockTime`) consistent with CLI syncback behavior? +- Is the `SKIP_PROPERTIES` list in `encodeHelpers.lua` (Parent, Name, Archivable) consistent with server-side filtering? + +**Verify ObjectValue carrier lifecycle:** +- Are carriers created correctly? (`carrier.Name = propertyName`, `carrier.Value = targetInstance`) +- Are carriers included in the serialization buffer? (appended to service's children range) +- Are carriers destroyed after serialization? (cleanup in `performSyncback()`) +- Does `build_dom_from_chunks()` correctly parse the `ref_target_count` range and resolve carriers? +- Does the server destroy carriers after resolving Ref properties? (they must not appear in the synced tree) + +**Verify rbxm blob reconstruction:** +- Does `build_dom_from_chunks()` distribute children correctly using `child_count` + `ref_target_count` ranges? +- Are services with zero children handled? (empty services still get created) +- Are `VISIBLE_SERVICES` in Rust consistent with `SYNCBACK_SERVICES` in Lua? (must list the same 14 services) +- Does the server handle out-of-range or zero-index carrier references gracefully? + +**Verify clean mode semantics:** +- Live syncback always uses `incremental=false`. Does this match CLI syncback behavior for the same input? +- Are old files properly removed before new files are written? +- Is `refresh_git_index()` called after writes? + +**Verify server restart behavior:** +- Does the server exit the accept loop cleanly when `SyncbackSignal` fires? +- Is the serve loop restarted with a new session after syncback completes? +- Can a new plugin connection be established after restart? +- Does `SyncbackSignal.fire()` return false if syncback is already in progress? (409 Conflict) + +**Verify validation:** +- Protocol version mismatch → 400 +- Server version MAJOR.MINOR mismatch → 400 +- Place ID not in `servePlaceIds` whitelist → 403 +- Place ID in `blockedPlaceIds` → 403 +- No restrictions configured → any place ID allowed + +**Verify round-trip:** Build → live syncback → rebuild → compare. The rebuilt instance tree must be bit-identical to the original (same as CLI syncback round-trip test). + ### 12. Run Static Analysis Run `cargo clippy` on the modified code to catch Rust issues the audit might miss: diff --git a/.cursor/plans/update_audit_and_rules_0625423a.plan.md b/.cursor/plans/update_audit_and_rules_0625423a.plan.md new file mode 100644 index 00000000..7549ab2f --- /dev/null +++ b/.cursor/plans/update_audit_and_rules_0625423a.plan.md @@ -0,0 +1,110 @@ +--- +name: Update audit and rules +overview: Update `.cursor/commands/audit.md` to cover the live syncback pipeline, new files (encodeHelpers, encodeService, new test suites), and refactored encoding architecture. Fix discrepancies in rule MDC files where they don't reflect recent code changes. +todos: + - id: audit-live-syncback + content: Add Live Syncback Pipeline section to audit.md with full file tree covering plugin encoding, transport, server handling, and serve loop restart + status: completed + - id: audit-encode-helpers + content: Add encodeHelpers.lua and encodeService.lua entries to the Plugin->Server ChangeBatcher section in audit.md + status: completed + - id: audit-new-tests + content: Add live_syncback.rs, git_sync_defaults.rs, matching_fixtures.rs to the Shared Infrastructure test section in audit.md + status: completed + - id: audit-11n-live-syncback + content: Add section 11n (Live Syncback Parity) audit checklist to audit.md + status: completed + - id: fix-atlas-api-mdc + content: Add encodeHelpers.lua to atlas-api.mdc ChangeBatcher table and update encodeInstance.lua description + status: completed + - id: fix-atlas-plugin-mdc + content: Add encodeHelpers.lua to atlas-plugin.mdc ChangeBatcher listing and fix SyncbackConfirm placement + status: completed + - id: fix-atlas-dedup-mdc + content: Add encodeHelpers.lua to atlas-dedup.mdc Key Files Map table + status: completed +isProject: false +--- + +# Update Audit Command and Rule Files + +## Summary of Discrepancies Found + +After comparing all 8 rule MDC files and the audit command against the actual source code and recent git history (40+ commits including live syncback, matching session caching, encodeHelpers refactor, float precision, git sync defaults), here are the issues: + +### audit.md - Major Gaps + +1. **No live syncback pipeline** -- The audit command describes only two pipelines (CLI syncback, two-way sync). The live syncback pipeline (`POST /api/syncback`) is a third distinct flow involving `encodeService.lua`, `SyncbackConfirm` UI, `handle_api_syncback`, `build_dom_from_chunks`, and `run_live_syncback`. Not covered at all. +2. **Missing `encodeHelpers.lua`** -- New shared helper module (`plugin/src/ChangeBatcher/encodeHelpers.lua`) that consolidates `encodeAttributes`, `encodeTags`, and `forEachEncodableProperty`. Both `encodeInstance.lua` and `encodeService.lua` import from it. Not mentioned anywhere in audit.md. +3. **Missing `encodeService.lua`** -- New file (`plugin/src/ChangeBatcher/encodeService.lua`) for service-level property encoding during live syncback. Not mentioned. +4. **Missing 3 new test suites** -- `tests/tests/live_syncback.rs`, `tests/tests/git_sync_defaults.rs`, `tests/tests/matching_fixtures.rs` are not referenced in the audit's test infrastructure section. +5. **Missing `SyncbackConfirm` UI** -- The floating confirmation dialog for live syncback (rendered in `App/init.lua`) is not mentioned in the plugin UI or data flow sections. +6. **Missing live syncback audit checklist** -- No equivalent of sections 11a-11m for live syncback-specific concerns (service property encoding, ObjectValue carrier lifecycle, rbxm blob reconstruction, clean-mode-only semantics, server restart behavior). + +### atlas-api.mdc - Issues + +1. **Missing `encodeHelpers.lua`** in the ChangeBatcher file table (section "ChangeBatcher"). The table lists `init.lua`, `createPatchSet.lua`, `encodePatchUpdate.lua`, `encodeInstance.lua`, `encodeService.lua`, `encodeProperty.lua`, `propertyFilter.lua` -- but `encodeHelpers.lua` is missing. +2. `**encodeInstance.lua` description** says "includes duplicate detection helpers" but doesn't note that property iteration was refactored to use `encodeHelpers.forEachEncodableProperty()`. + +### atlas-plugin.mdc - Issues + +1. **Missing `encodeHelpers.lua`** from the file listing under "4. ChangeBatcher" section. The `encodeHelpers.lua` file exports `encodeAttributes`, `encodeTags`, `forEachEncodableProperty`. +2. `**SyncbackConfirm**` is listed under StatusPages (`SyncbackConfirm` - "Floating dialog for live syncback confirmation (rendered by `App/init.lua`)") which is slightly misleading. It's NOT a StatusPage file -- it's rendered inline in `App/init.lua` as a `StudioPluginGui` with `isEphemeral = true`. The parenthetical note is correct but the placement under StatusPages implies a separate file. + +### atlas-dedup.mdc - Issues + +1. **Missing `encodeHelpers.lua`** from the Key Files Map table. This file is relevant because it contains the shared property filtering logic (`forEachEncodableProperty`) used during instance encoding for both two-way sync additions and live syncback. + +## Changes + +### Changes to `audit.md` + +**Section "Sync System Layout" -- add Live Syncback Pipeline** after the Two-Way Sync section: + +Add a third pipeline section covering: + +- Plugin-side: `SYNCBACK_SERVICES` in `App/init.lua`, `encodeService.lua`, `performSyncback()`, `SyncbackConfirm` dialog +- Transport: `POST /api/syncback` (MessagePack) +- Server-side: `handle_api_syncback` in `api.rs`, `SyncbackSignal`/`LiveServer` in `mod.rs`, `build_dom_from_chunks`/`run_live_syncback` in `serve.rs` +- Wire types: `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload` in `interface.rs` + +**Section "Plugin --> Server" -- add `encodeHelpers.lua`** to the Change Batching subsection: + +- Add entry for `plugin/src/ChangeBatcher/encodeHelpers.lua` describing `encodeAttributes`, `encodeTags`, `forEachEncodableProperty` +- Add entry for `plugin/src/ChangeBatcher/encodeService.lua` describing service-level encoding for live syncback + +**Section "Shared Infrastructure" -- add new test files:** + +- `tests/tests/live_syncback.rs` -- Live syncback parity, validation, lifecycle, edge cases +- `tests/tests/git_sync_defaults.rs` -- Git-based sync default selection tests +- `tests/tests/matching_fixtures.rs` -- Matching algorithm fixture tests + +**Add new audit section 11n: Live Syncback Parity** covering: + +- Service property encoding completeness (`encodeService.lua` vs `filter_properties`) +- ObjectValue carrier lifecycle (created, serialized, destroyed) +- rbxm blob reconstruction (`build_dom_from_chunks` child distribution) +- Clean mode semantics (always `incremental=false`) +- Server restart behavior (session invalidation, reconnection) +- `SYNCBACK_SERVICES` list consistency with `VISIBLE_SERVICES` in Rust +- Protocol/version/placeId validation +- Round-trip: live syncback output must match CLI syncback output + +**Update Audit Reading Order** (step 9 area) to include live syncback flow. + +### Changes to Rule MDC Files + +`**atlas-api.mdc`:** + +- Add `encodeHelpers.lua` row to ChangeBatcher file table with description: "Shared encoding helpers: `encodeAttributes`, `encodeTags`, `forEachEncodableProperty`. Used by `encodeInstance.lua` and `encodeService.lua`." +- Update `encodeInstance.lua` row to note it uses `encodeHelpers.forEachEncodableProperty()` for property iteration + +`**atlas-plugin.mdc`:** + +- Add `encodeHelpers.lua` to the "4. ChangeBatcher" file listing with description: "Shared property encoding helpers (`encodeAttributes`, `encodeTags`, `forEachEncodableProperty`)" +- Move `SyncbackConfirm` from the StatusPages list to its own note under the App section, clarifying it's rendered inline in `App/init.lua` as a floating `StudioPluginGui`, not a separate StatusPage file + +`**atlas-dedup.mdc`:** + +- Add `plugin/src/ChangeBatcher/encodeHelpers.lua` to the Key Files Map table with subsystem "Encoding" and purpose "Shared property filtering/encoding helpers used by `encodeInstance.lua` and `encodeService.lua`" + diff --git a/.cursor/rules/atlas-api.mdc b/.cursor/rules/atlas-api.mdc index f406aabf..ab1484f2 100644 --- a/.cursor/rules/atlas-api.mdc +++ b/.cursor/rules/atlas-api.mdc @@ -408,7 +408,9 @@ end | `init.lua` | Main batcher: accumulator, RenderStepped loop, pause/resume, deferred refs | | `createPatchSet.lua` | Converts pending changes → PatchSet (passes instanceMap for Ref resolution) | | `encodePatchUpdate.lua` | Encodes property updates including Ref properties via InstanceMap | -| `encodeInstance.lua` | Full instance encoding (for additions/syncback) | +| `encodeInstance.lua` | Full instance encoding (for additions/syncback); uses `encodeHelpers` for property iteration; includes duplicate detection helpers (`findDuplicateNames`, `hasDuplicateSiblings`, `isPathUnique`) | +| `encodeHelpers.lua` | Shared encoding helpers: `encodeAttributes`, `encodeTags`, `forEachEncodableProperty`. Used by `encodeInstance.lua` and `encodeService.lua`. Defines `SKIP_PROPERTIES` (Parent, Name, Archivable). | +| `encodeService.lua` | Encodes service-level properties/attributes/refs for live syncback; uses ObjectValue carriers for Ref properties | | `encodeProperty.lua` | Single property value encoding via RbxDom | | `propertyFilter.lua` | Property type filtering (only `UniqueId` blocked; `Ref` is allowed) | @@ -548,14 +550,14 @@ end ### encodeInstance.lua: Full Instance Encoding (Syncback/Additions) -Used when pulling new instances from Studio to filesystem. Duplicate-named instances are encoded and sent to the server; the server handles filesystem collisions via the dedup suffix system (`~N`). +Used when pulling new instances from Studio to filesystem. Uses `encodeHelpers.forEachEncodableProperty()` for property iteration (shared with `encodeService.lua`). Duplicate-named instances are encoded and sent to the server; the server handles filesystem collisions via the dedup suffix system (`~N`). Includes duplicate detection helpers (`findDuplicateNames`, `hasDuplicateSiblings`, `isPathUnique`). ```lua function encodeInstance(instance, parentId, _skipPathCheck) local properties = {} - encodeAttributes(instance, properties) - encodeTags(instance, properties) + Helpers.encodeAttributes(instance, properties) + Helpers.encodeTags(instance, properties) if SCRIPT_CLASS_NAMES[instance.ClassName] then local success, encoded = encodeProperty(instance, "Source", descriptor) @@ -563,24 +565,13 @@ function encodeInstance(instance, parentId, _skipPathCheck) properties.Source = encoded end - local classDescriptor = RbxDom.findClassDescriptor(instance.ClassName) - for propertyName, meta in classDescriptor.properties do - if SKIP_PROPERTIES[propertyName] then continue end - if SKIP_PROPERTIES_BY_CLASS[instance.ClassName]?[propertyName] then continue end - - local isReadable = meta.scriptability == "ReadWrite" or meta.scriptability == "Read" - local doesSerialize = meta.serialization ~= "DoesNotSerialize" - - if isReadable and doesSerialize then - local success, encoded = encodeProperty(instance, propertyName, descriptor) - if success and encoded ~= nil then - properties[propertyName] = encoded - end + Helpers.forEachEncodableProperty(instance.ClassName, SKIP_PROPERTIES_BY_CLASS, function(propertyName, descriptor) + local success, encoded = encodeProperty(instance, propertyName, descriptor) + if success and encoded ~= nil then + properties[propertyName] = encoded end - end + end) - -- All children encoded (including those with duplicate names; - -- the server handles duplicates via dedup suffix system) local children = {} for _, child in instance:GetChildren() do local encoded = encodeInstance(child, nil, true) diff --git a/.cursor/rules/atlas-dedup.mdc b/.cursor/rules/atlas-dedup.mdc index 8c9dcbd9..fc6cb7ef 100644 --- a/.cursor/rules/atlas-dedup.mdc +++ b/.cursor/rules/atlas-dedup.mdc @@ -56,6 +56,7 @@ All three now have **`MatchingSession` caching** to avoid redundant recursive sc | `plugin/src/Reconciler/trueEquals.lua` | Matching | `trueEquals` property comparison (Lua) | | `src/change_processor.rs` | Integration | Dedup cleanup on deletion, VFS suppression, ref path updates | | `src/web/api.rs` | Integration | `added_paths`, `syncback_updated_properties`, ref-to-attribute conversion | +| `plugin/src/ChangeBatcher/encodeHelpers.lua` | Encoding | Shared property filtering/encoding helpers (`encodeAttributes`, `encodeTags`, `forEachEncodableProperty`). Used by `encodeInstance.lua` and `encodeService.lua`. | --- diff --git a/.cursor/rules/atlas-plugin.mdc b/.cursor/rules/atlas-plugin.mdc index fc2b6145..ca131f9d 100644 --- a/.cursor/rules/atlas-plugin.mdc +++ b/.cursor/rules/atlas-plugin.mdc @@ -14,7 +14,7 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica ### Key Components **1. UI Layer** (`App/`) -- **init.lua** - Root component, manages connection state, auto-reconnect, sync reminders, playtest server auto-connect +- **init.lua** - Root component, manages connection state, auto-reconnect, sync reminders, playtest server auto-connect. Also renders `SyncbackConfirm` floating dialog (StudioPluginGui with `isEphemeral=true`, not a StatusPage file). Contains `performSyncback()` and `SYNCBACK_SERVICES` constant. - **Theme.lua** - Studio-matched color palette (auto light/dark) - **Page.lua** - Page wrapper component - **Components/** - Reusable UI components @@ -28,7 +28,6 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica - `NotConnected.lua` - Connection form (host/port input), "Syncback" button (Danger style) - `Connecting.lua` - Loading screen - `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections - - `SyncbackConfirm` - Floating dialog for live syncback confirmation (rendered by `App/init.lua`) - `Connected.lua` - Status display with patch history - `Error.lua` - Error display - `Settings/` - Settings UI (`init.lua`, `Setting.lua`) @@ -57,7 +56,8 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica - **init.lua** - Main batcher: accumulates changes every 200ms, pause/resume, deferred Ref resolution - **createPatchSet.lua** - Converts pending changes to PatchSet - **encodePatchUpdate.lua** - Encodes property updates including Ref properties via InstanceMap -- **encodeInstance.lua** - Full instance encoding (for additions/syncback); includes duplicate detection helpers (`findDuplicateNames`, `hasDuplicateSiblings`, `isPathUnique`) +- **encodeInstance.lua** - Full instance encoding (for additions/syncback); uses `encodeHelpers` for property iteration; includes duplicate detection helpers (`findDuplicateNames`, `hasDuplicateSiblings`, `isPathUnique`) +- **encodeHelpers.lua** - Shared property encoding helpers (`encodeAttributes`, `encodeTags`, `forEachEncodableProperty`). Used by `encodeInstance.lua` and `encodeService.lua`. Defines `SKIP_PROPERTIES` (Parent, Name, Archivable). - **encodeService.lua** - Encodes service-level properties/attributes/refs for live syncback; uses ObjectValue carriers for Ref properties - **encodeProperty.lua** - Single property value encoding via RbxDom - **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed) diff --git a/plugin/src/Reconciler/trueEquals.spec.lua b/plugin/src/Reconciler/trueEquals.spec.lua index fbe3aacf..111d2ee7 100644 --- a/plugin/src/Reconciler/trueEquals.spec.lua +++ b/plugin/src/Reconciler/trueEquals.spec.lua @@ -521,64 +521,56 @@ return function() describe("ColorSequence", function() it("identical two-keypoint", function() - expect( - trueEquals( - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }), - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }) - ) - ).to.equal(true) + expect(trueEquals( + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }), + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }) + )).to.equal(true) end) it("color within epsilon", function() - expect( - trueEquals( - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }), - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }) - ) - ).to.equal(true) + expect(trueEquals( + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }), + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }) + )).to.equal(true) end) it("color beyond epsilon", function() - expect( - trueEquals( - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }), - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }) - ) - ).to.equal(false) + expect(trueEquals( + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }), + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }) + )).to.equal(false) end) it("different keypoint count", function() - expect( - trueEquals( - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }), - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }) - ) - ).to.equal(false) + expect(trueEquals( + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }), + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }) + )).to.equal(false) end) it("simple constructor identical", function() @@ -588,18 +580,16 @@ return function() end) it("time within epsilon", function() - expect( - trueEquals( - ColorSequence.new({ - ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }), - ColorSequence.new({ - ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)), - ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), - }) - ) - ).to.equal(true) + expect(trueEquals( + ColorSequence.new({ + ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }), + ColorSequence.new({ + ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)), + ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)), + }) + )).to.equal(true) end) end)