mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Update audit documentation and rules for live syncback enhancements
- Added a new section in `audit.md` for the Live Syncback Pipeline, detailing the plugin encoding, transport, and server handling processes. - Included entries for newly introduced files `encodeHelpers.lua` and `encodeService.lua` in the audit documentation. - Updated the audit checklist to cover live syncback parity and related concerns. - Revised rule MDC files (`atlas-api.mdc`, `atlas-plugin.mdc`, `atlas-dedup.mdc`) to reflect recent changes, including the addition of `encodeHelpers.lua` and clarifications on `SyncbackConfirm`. - Ensured comprehensive coverage of new test suites in the audit documentation. These changes aim to enhance the clarity and accuracy of the audit documentation and rules, aligning them with the latest codebase updates.
This commit is contained in:
parent
49b4d8581f
commit
532425df9e
6 changed files with 356 additions and 97 deletions
|
|
@ -33,7 +33,7 @@ Any divergence between the two-way sync path and the CLI syncback path is a bug
|
||||||
|
|
||||||
## Sync System Layout
|
## 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`)
|
### CLI Syncback Pipeline (`atlas syncback`)
|
||||||
|
|
||||||
|
|
@ -54,10 +54,11 @@ Syncback Core
|
||||||
│ middleware context through the pipeline. Methods:
|
│ middleware context through the pipeline. Methods:
|
||||||
│ with_joined_path(), get_path_filtered_properties().
|
│ with_joined_path(), get_path_filtered_properties().
|
||||||
├── src/syncback/file_names.rs Filename generation. slugify_name() replaces forbidden
|
├── src/syncback/file_names.rs Filename generation. slugify_name() replaces forbidden
|
||||||
│ chars with '_'. deduplicate_name() appends ~1, ~2 on
|
│ chars (incl. ~) with '_'. deduplicate_name_with_ext()
|
||||||
│ collision. name_for_inst() is the main entry point.
|
│ appends ~2, ~3 on collision. name_for_inst() is the
|
||||||
│ adjacent_meta_path() computes .meta.json5 paths for
|
│ main entry point. adjacent_meta_path() computes
|
||||||
│ scripts (strips .server/.client suffixes from stem).
|
│ .meta.json5 paths for scripts (strips .server/.client
|
||||||
|
│ suffixes from stem).
|
||||||
├── src/syncback/property_filter.rs Property filtering. filter_properties() removes Name,
|
├── src/syncback/property_filter.rs Property filtering. filter_properties() removes Name,
|
||||||
│ Parent, Source (scripts), Value (StringValue), Contents
|
│ Parent, Source (scripts), Value (StringValue), Contents
|
||||||
│ (LocalizationTable), Ref/UniqueId, unscriptable props.
|
│ (LocalizationTable), Ref/UniqueId, unscriptable props.
|
||||||
|
|
@ -148,13 +149,28 @@ Change Batching (Plugin)
|
||||||
│ Encodes a single property value using RbxDom.EncodedValue.
|
│ Encodes a single property value using RbxDom.EncodedValue.
|
||||||
│ encode(). Maps Roblox values to wire format.
|
│ encode(). Maps Roblox values to wire format.
|
||||||
├── plugin/src/ChangeBatcher/encodeInstance.lua
|
├── plugin/src/ChangeBatcher/encodeInstance.lua
|
||||||
│ Full instance encoding for additions. All children are
|
│ Full instance encoding for additions. Uses encodeHelpers
|
||||||
│ encoded including those with duplicate names -- the server
|
│ for property iteration. All children encoded including
|
||||||
│ handles dedup. Skips Ref properties (target has no server
|
│ duplicates -- server handles dedup. Duplicate detection
|
||||||
│ ID during addition). Attributes/Tags support.
|
│ 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
|
└── plugin/src/ChangeBatcher/propertyFilter.lua
|
||||||
Filters which properties are synced. Controls which data
|
Filters which properties are synced. Only UniqueId is
|
||||||
types and property names are included/excluded.
|
blocked. Ref properties are explicitly allowed (handled
|
||||||
|
by encodePatchUpdate via InstanceMap resolution).
|
||||||
|
|
||||||
Session & Transport (Plugin)
|
Session & Transport (Plugin)
|
||||||
├── plugin/src/ServeSession.lua Orchestrates the session. onInstanceChanged → guards
|
├── 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,
|
│ updates on rename. Constants: REF_PATH_ATTRIBUTE_PREFIX,
|
||||||
│ REF_POINTER_ATTRIBUTE_PREFIX, REF_ID_ATTRIBUTE_NAME.
|
│ REF_POINTER_ATTRIBUTE_PREFIX, REF_ID_ATTRIBUTE_NAME.
|
||||||
├── src/variant_eq.rs Property value comparison. variant_eq() compares Variant
|
├── 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.
|
│ Used by matching algorithms for property diff scoring.
|
||||||
└── src/serve_session.rs Server-side session. Owns RojoTree, ChangeProcessor,
|
└── src/serve_session.rs Server-side session. Owns RojoTree, ChangeProcessor,
|
||||||
MessageQueue, RefPathIndex, git_repo_root. Coordinates
|
MessageQueue, RefPathIndex, git_repo_root. Coordinates
|
||||||
|
|
@ -287,7 +304,93 @@ Patch Application (Plugin)
|
||||||
└── plugin/src/Reconciler/Error.lua Error types for reconciliation failures.
|
└── 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<Option<SyncbackPayload>> + 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<f64>,
|
||||||
|
│ data: Vec<u8>, services: Vec<ServiceChunk> }
|
||||||
|
│ ServiceChunk { class_name, child_count, ref_target_count,
|
||||||
|
│ properties: HashMap<String, Variant>,
|
||||||
|
│ refs: HashMap<String, u32> }
|
||||||
|
│ SyncbackPayload { data: Vec<u8>,
|
||||||
|
│ services: Vec<ServiceChunk> }
|
||||||
|
│ 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
|
Snapshot System
|
||||||
|
|
@ -353,6 +456,26 @@ Plugin Shared Modules
|
||||||
├── plugin/src/ChangeMetadata.lua Change metadata tracking for sync operations.
|
├── plugin/src/ChangeMetadata.lua Change metadata tracking for sync operations.
|
||||||
├── plugin/src/strict.lua Module export wrapper (strict mode enforcement).
|
├── plugin/src/strict.lua Module export wrapper (strict mode enforcement).
|
||||||
└── plugin/src/DiffUtil.lua Diff utilities for comparing instance data.
|
└── 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
|
### 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
|
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
|
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)
|
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.
|
- **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.
|
- **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
|
### 12. Run Static Analysis
|
||||||
|
|
||||||
Run `cargo clippy` on the modified code to catch Rust issues the audit might miss:
|
Run `cargo clippy` on the modified code to catch Rust issues the audit might miss:
|
||||||
|
|
|
||||||
110
.cursor/plans/update_audit_and_rules_0625423a.plan.md
Normal file
110
.cursor/plans/update_audit_and_rules_0625423a.plan.md
Normal file
|
|
@ -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`"
|
||||||
|
|
||||||
|
|
@ -408,7 +408,9 @@ end
|
||||||
| `init.lua` | Main batcher: accumulator, RenderStepped loop, pause/resume, deferred refs |
|
| `init.lua` | Main batcher: accumulator, RenderStepped loop, pause/resume, deferred refs |
|
||||||
| `createPatchSet.lua` | Converts pending changes → PatchSet (passes instanceMap for Ref resolution) |
|
| `createPatchSet.lua` | Converts pending changes → PatchSet (passes instanceMap for Ref resolution) |
|
||||||
| `encodePatchUpdate.lua` | Encodes property updates including Ref properties via InstanceMap |
|
| `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 |
|
| `encodeProperty.lua` | Single property value encoding via RbxDom |
|
||||||
| `propertyFilter.lua` | Property type filtering (only `UniqueId` blocked; `Ref` is allowed) |
|
| `propertyFilter.lua` | Property type filtering (only `UniqueId` blocked; `Ref` is allowed) |
|
||||||
|
|
||||||
|
|
@ -548,14 +550,14 @@ end
|
||||||
|
|
||||||
### encodeInstance.lua: Full Instance Encoding (Syncback/Additions)
|
### 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
|
```lua
|
||||||
function encodeInstance(instance, parentId, _skipPathCheck)
|
function encodeInstance(instance, parentId, _skipPathCheck)
|
||||||
local properties = {}
|
local properties = {}
|
||||||
|
|
||||||
encodeAttributes(instance, properties)
|
Helpers.encodeAttributes(instance, properties)
|
||||||
encodeTags(instance, properties)
|
Helpers.encodeTags(instance, properties)
|
||||||
|
|
||||||
if SCRIPT_CLASS_NAMES[instance.ClassName] then
|
if SCRIPT_CLASS_NAMES[instance.ClassName] then
|
||||||
local success, encoded = encodeProperty(instance, "Source", descriptor)
|
local success, encoded = encodeProperty(instance, "Source", descriptor)
|
||||||
|
|
@ -563,24 +565,13 @@ function encodeInstance(instance, parentId, _skipPathCheck)
|
||||||
properties.Source = encoded
|
properties.Source = encoded
|
||||||
end
|
end
|
||||||
|
|
||||||
local classDescriptor = RbxDom.findClassDescriptor(instance.ClassName)
|
Helpers.forEachEncodableProperty(instance.ClassName, SKIP_PROPERTIES_BY_CLASS, function(propertyName, descriptor)
|
||||||
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)
|
local success, encoded = encodeProperty(instance, propertyName, descriptor)
|
||||||
if success and encoded ~= nil then
|
if success and encoded ~= nil then
|
||||||
properties[propertyName] = encoded
|
properties[propertyName] = encoded
|
||||||
end
|
end
|
||||||
end
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
-- All children encoded (including those with duplicate names;
|
|
||||||
-- the server handles duplicates via dedup suffix system)
|
|
||||||
local children = {}
|
local children = {}
|
||||||
for _, child in instance:GetChildren() do
|
for _, child in instance:GetChildren() do
|
||||||
local encoded = encodeInstance(child, nil, true)
|
local encoded = encodeInstance(child, nil, true)
|
||||||
|
|
|
||||||
|
|
@ -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) |
|
| `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/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 |
|
| `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`. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
**1. UI Layer** (`App/`)
|
**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)
|
- **Theme.lua** - Studio-matched color palette (auto light/dark)
|
||||||
- **Page.lua** - Page wrapper component
|
- **Page.lua** - Page wrapper component
|
||||||
- **Components/** - Reusable UI components
|
- **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)
|
- `NotConnected.lua` - Connection form (host/port input), "Syncback" button (Danger style)
|
||||||
- `Connecting.lua` - Loading screen
|
- `Connecting.lua` - Loading screen
|
||||||
- `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections
|
- `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
|
- `Connected.lua` - Status display with patch history
|
||||||
- `Error.lua` - Error display
|
- `Error.lua` - Error display
|
||||||
- `Settings/` - Settings UI (`init.lua`, `Setting.lua`)
|
- `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
|
- **init.lua** - Main batcher: accumulates changes every 200ms, pause/resume, deferred Ref resolution
|
||||||
- **createPatchSet.lua** - Converts pending changes to PatchSet
|
- **createPatchSet.lua** - Converts pending changes to PatchSet
|
||||||
- **encodePatchUpdate.lua** - Encodes property updates including Ref properties via InstanceMap
|
- **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
|
- **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
|
- **encodeProperty.lua** - Single property value encoding via RbxDom
|
||||||
- **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed)
|
- **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed)
|
||||||
|
|
|
||||||
|
|
@ -521,8 +521,7 @@ return function()
|
||||||
|
|
||||||
describe("ColorSequence", function()
|
describe("ColorSequence", function()
|
||||||
it("identical two-keypoint", function()
|
it("identical two-keypoint", function()
|
||||||
expect(
|
expect(trueEquals(
|
||||||
trueEquals(
|
|
||||||
ColorSequence.new({
|
ColorSequence.new({
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
|
@ -531,13 +530,11 @@ return function()
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
})
|
})
|
||||||
)
|
)).to.equal(true)
|
||||||
).to.equal(true)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("color within epsilon", function()
|
it("color within epsilon", function()
|
||||||
expect(
|
expect(trueEquals(
|
||||||
trueEquals(
|
|
||||||
ColorSequence.new({
|
ColorSequence.new({
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
|
@ -546,13 +543,11 @@ return function()
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
})
|
})
|
||||||
)
|
)).to.equal(true)
|
||||||
).to.equal(true)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("color beyond epsilon", function()
|
it("color beyond epsilon", function()
|
||||||
expect(
|
expect(trueEquals(
|
||||||
trueEquals(
|
|
||||||
ColorSequence.new({
|
ColorSequence.new({
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
|
@ -561,13 +556,11 @@ return function()
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
})
|
})
|
||||||
)
|
)).to.equal(false)
|
||||||
).to.equal(false)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("different keypoint count", function()
|
it("different keypoint count", function()
|
||||||
expect(
|
expect(trueEquals(
|
||||||
trueEquals(
|
|
||||||
ColorSequence.new({
|
ColorSequence.new({
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
|
@ -577,8 +570,7 @@ return function()
|
||||||
ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)),
|
ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
})
|
})
|
||||||
)
|
)).to.equal(false)
|
||||||
).to.equal(false)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("simple constructor identical", function()
|
it("simple constructor identical", function()
|
||||||
|
|
@ -588,8 +580,7 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("time within epsilon", function()
|
it("time within epsilon", function()
|
||||||
expect(
|
expect(trueEquals(
|
||||||
trueEquals(
|
|
||||||
ColorSequence.new({
|
ColorSequence.new({
|
||||||
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
|
@ -598,8 +589,7 @@ return function()
|
||||||
ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)),
|
ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)),
|
||||||
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
})
|
})
|
||||||
)
|
)).to.equal(true)
|
||||||
).to.equal(true)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue